Compare commits
178 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bb7a5ef93 | |||
| b70725da36 | |||
| f2978ca389 | |||
| 880474065d | |||
| d0506f9e8b | |||
| 6b2293ad1a | |||
| 0cadbba30c | |||
| fc4bba23b3 | |||
| d72c1ac365 | |||
| 036b7259ad | |||
| 77507e2311 | |||
| 019f5f33be | |||
| 66ac7e8537 | |||
| 93f879583f | |||
| b13139ad19 | |||
| d35aec1828 | |||
| 53a2f0b5d2 | |||
| 9cb44a2988 | |||
| c37102eff4 | |||
| d0e3657014 | |||
| cb397b6be9 | |||
| a1fae5f6f3 | |||
| 717f0d07cd | |||
| 9f0ce13abc | |||
| 3ddbcc1d92 | |||
| 3642a848f3 | |||
| ad7f279428 | |||
| c0b8ede86b | |||
| ecae3686a7 | |||
| 9aadf855bf | |||
| d7263e09cf | |||
| d988b31619 | |||
| 0d0e0d0440 | |||
| 761d1d2b15 | |||
| 0411be6ff0 | |||
| 95282572d3 | |||
| 3c3dd4c165 | |||
| eb02f34df9 | |||
| f04bef530d | |||
| a1a95096ba | |||
| 4c562758cd | |||
| 7ce2debb65 | |||
| 8a48b308a8 | |||
| 668c64276f | |||
| bbef38e1a8 | |||
| 2de9a9da20 | |||
| 3a4f27132a | |||
| fab44bb0fd | |||
| f69f08792a | |||
| a23e601b6a | |||
| c0dc0573e4 | |||
| 9032359fdc | |||
| 0c65434696 | |||
| d4cddc576b | |||
| 14edbc24de | |||
| e6ae48d4d7 | |||
| 41e3898ff8 | |||
| 64556dc8e7 | |||
| 7b626f8653 | |||
| 7a71904b24 | |||
| 49cf7ab2c2 | |||
| 021d4bf093 | |||
| e1ff615233 | |||
| 6116cff52b | |||
| 935d1d527c | |||
| c68a8cb47c | |||
| 540c889fa1 | |||
| db467925ca | |||
| 9a50ab3b2e | |||
| 898292ff2b | |||
| 8598cfa781 | |||
| 330f92b7ff | |||
| db67ccfa28 | |||
| 5d0e14370d | |||
| 56d999f6f1 | |||
| c26cb969e8 | |||
| 290970097e | |||
| 42a6b11ac6 | |||
| d82a74eefc | |||
| a7a08ac958 | |||
| 5b7db2ccd3 | |||
| b67d17b2a5 | |||
| 6ba38e6f7a | |||
| 1cb42e0b02 | |||
| 9927724bf2 | |||
| dbffca3d32 | |||
| 6106f223d2 | |||
| 3e20a1df6e | |||
| e63da1d730 | |||
| 0d6cdbe6cd | |||
| fa683c2655 | |||
| 37142195b9 | |||
| 148cfd63c7 | |||
| fb5db71b53 | |||
| ff90824247 | |||
| 3143a03af6 | |||
| 71fd4c23d6 | |||
| c06046b604 | |||
| f839f1e8d8 | |||
| b9d06d5f77 | |||
| 539daa04bc | |||
| b05fe37e2e | |||
| 8f46c63839 | |||
| 59b2ff8e63 | |||
| a5e299cf2c | |||
| 6e53f6f941 | |||
| 6e5513435e | |||
| f50126a210 | |||
| f9a11fc3a2 | |||
| 8181216eb6 | |||
| b79e2b8215 | |||
| 223c50f92f | |||
| ded90736fc | |||
| 43eab5f8cd | |||
| bfe4681d97 | |||
| f0889d9b70 | |||
| 16f3863e0c | |||
| 37a284160b | |||
| 30d9e9296d | |||
| 36d5f2928f | |||
| a78a420d76 | |||
| 05b56060ab | |||
| 6c209835b7 | |||
| b6b92602ed | |||
| b3720dbeb6 | |||
| 2a4da3388f | |||
| 239cf61795 | |||
| c2e05e1305 | |||
| 8d4d17e7a8 | |||
| 5f3982174e | |||
| 9aff1ea098 | |||
| 2c2efd83fd | |||
| e2cd0b6057 | |||
| bbc93c66a3 | |||
| 8b7023810b | |||
| e8e865557f | |||
| c7314b2357 | |||
| 0712e5b08c | |||
| 175abcd8a4 | |||
| 6cadf7f496 | |||
| f9d1723f6e | |||
| 35f524ff34 | |||
| fc7ad5bb69 | |||
| a70019263d | |||
| ac0adfea15 | |||
| 9b5c47f60f | |||
| e48b71a5d1 | |||
| d2389b4d39 | |||
| 5c312bb7da | |||
| 41ba9f8bc9 | |||
| cbb189aade | |||
| 5635cb5e57 | |||
| b6feab8e94 | |||
| 333f7abfc0 | |||
| eb20edf890 | |||
| ff7a7d3b6b | |||
| fb5f1be032 | |||
| 83a66814b0 | |||
| ae7beb0df7 | |||
| d4d0ddf849 | |||
| 3566eed9e1 | |||
| 3473e80876 | |||
| a48fb88e4c | |||
| bfdb399772 | |||
| c577fb2ff5 | |||
| b6b3f3877f | |||
| 9ed79d53a6 | |||
| 8615e6b46d | |||
| e694a189f8 | |||
| 2aeae07d0b | |||
| fcfa3b9938 | |||
| 6cf30350ea | |||
| c1c9f19870 | |||
| 77b05a84f2 | |||
| 2e19d85b18 | |||
| b9ce72d39a | |||
| 6defb54300 | |||
| 43eaa96bc9 |
@@ -0,0 +1,16 @@
|
||||
.git
|
||||
.github
|
||||
build
|
||||
build-*
|
||||
cmake-build-*
|
||||
*.dat
|
||||
*.log
|
||||
*.pid
|
||||
*.conf
|
||||
*.key
|
||||
*.pem
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
.triangles
|
||||
wallet.dat
|
||||
wallet.dat.*
|
||||
+369
-70
@@ -8,12 +8,20 @@ on:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-linux-unit:
|
||||
# This is the canonical CI gate for unit tests. Failures here MUST block
|
||||
# the PR — see PR #26 incident (2026-07-11): the previous
|
||||
# `continue-on-error: true` + `|| true` soft-gate allowed a PR with broken
|
||||
# master-side code to merge because the link failure wasn't blocking.
|
||||
# Sanitizer regression = blocking PR (test-linux-sanitizers below).
|
||||
# Unit regression = blocking PR (this job).
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -80,19 +88,21 @@ jobs:
|
||||
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
|
||||
echo "::error::test_chaindb_equivalence was not built"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run unit tests
|
||||
run: cd build && ctest --output-on-failure || true
|
||||
# ctest exit code is the gate. NO `|| true` — failures must block
|
||||
# the PR (see comment at top of this job). --output-on-failure gives
|
||||
# the failing assertion + suite name inline rather than requiring a
|
||||
# log download.
|
||||
run: cd build && ctest --output-on-failure
|
||||
|
||||
test-linux-sanitizers:
|
||||
# ASan + UBSan build of the daemon + unit tests. Allowed to fail until
|
||||
# findings are triaged — see .github/workflows/lint.yml comment block.
|
||||
# Once the test suite is clean under sanitizers, drop continue-on-error.
|
||||
# ASan + UBSan build of the daemon + unit tests. This is a blocking
|
||||
# signal: sanitizer regressions should fail the PR.
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: true
|
||||
env:
|
||||
# ASan: leak detection off by default (BDB and OpenSSL produce noise on shutdown).
|
||||
# Re-enable once we've quieted the legitimate suspects.
|
||||
@@ -103,7 +113,7 @@ jobs:
|
||||
# and BDB until they're fixed file-by-file.
|
||||
SAN_FLAGS: "-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined -fno-sanitize=alignment,signed-integer-overflow,vptr"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -148,17 +158,236 @@ jobs:
|
||||
- name: Run unit tests under sanitizers
|
||||
run: cd build-san && ctest --output-on-failure
|
||||
|
||||
test-fuzz-smoke:
|
||||
# libFuzzer smoke test for src/script.cpp (fuzz_script harness).
|
||||
# Builds with ASan+UBSan+libFuzzer and runs for 5 minutes. Any crash
|
||||
# is uploaded as an artifact and the job fails — fuzz regressions
|
||||
# must block the PR.
|
||||
# See src/test/fuzz/README.md for harness details.
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1:abort_on_error=1:print_stacktrace=1"
|
||||
UBSAN_OPTIONS: "halt_on_error=1:abort_on_error=1:print_stacktrace=1"
|
||||
SAN_FLAGS: "-fsanitize=address,undefined,fuzzer-no-link -fno-omit-frame-pointer -fno-sanitize-recover=undefined -fno-sanitize=alignment,signed-integer-overflow,vptr"
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install clang + dependencies
|
||||
# libFuzzer ships with clang since v6; clang-15 is on the runner.
|
||||
# libgflags-dev: fuzz link line references -lgflags (RocksDB builds
|
||||
# expect gflags as a transitive dep). Without it the link step fails
|
||||
# with "cannot find -lgflags". CI's ubuntu-22.04 runner does NOT ship
|
||||
# it by default; DNS2 has it as an automatic dep of build-essential,
|
||||
# which is why local dry-runs didn't catch this.
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-15 cmake ninja-build \
|
||||
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
|
||||
libevent-dev libminiupnpc-dev zlib1g-dev \
|
||||
libsnappy-dev liblz4-dev libzstd-dev \
|
||||
libgflags-dev
|
||||
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-15 100
|
||||
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-15 100
|
||||
|
||||
- name: Build RocksDB from source
|
||||
run: sudo bash scripts/ci/build-rocksdb.sh
|
||||
|
||||
- name: Configure with fuzzing + sanitizers
|
||||
# NB: do NOT pass -fsanitize=fuzzer in CMAKE_EXE_LINKER_FLAGS — that
|
||||
# pulls libFuzzer's main() into CMake's compiler-probe linker test
|
||||
# and trips "multiple definition of `main`". The fuzz_script target's
|
||||
# custom clang++ link step adds -fsanitize=fuzzer in src/CMakeLists.txt
|
||||
# (see BUILD_FUZZ block).
|
||||
# SECP256K1_ASM=OFF: clang-15+ register allocator is sometimes stricter
|
||||
# than clang-14 about the x86_64 inline asm in scalar_4x64_impl.h and
|
||||
# fails with "inline assembly requires more registers than available"
|
||||
# on some runner images. The fuzz target only exercises script.cpp —
|
||||
# ECC ops use the C fallback (slower, still correct).
|
||||
run: |
|
||||
cmake -B build-fuzz -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_C_FLAGS="$SAN_FLAGS" \
|
||||
-DCMAKE_CXX_FLAGS="$SAN_FLAGS" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="$SAN_FLAGS" \
|
||||
-DBUILD_QT=OFF \
|
||||
-DBUILD_DAEMON=ON \
|
||||
-DBUILD_TESTS=ON \
|
||||
-DBUILD_FUZZ=ON \
|
||||
-DUSE_UPNP=OFF \
|
||||
-DSECP256K1_ASM=OFF
|
||||
|
||||
- name: Build libtor (embedded Tor static lib)
|
||||
# BUILD_FUZZ pulls in triangles_common + trianglesd_objects (OBJECT lib)
|
||||
# via the fuzz target's CMake deps. The link line references libtor.a,
|
||||
# which the Tor submodule script produces — CMake doesn't build it.
|
||||
# Mirror the unit/sanitizer jobs here before invoking the fuzz target.
|
||||
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 fuzz_script
|
||||
# CMake target is named `fuzz_script` (matches FUZZ_BIN_DIR/fuzz_script
|
||||
# in src/CMakeLists.txt — see add_custom_target(fuzz_script ...)).
|
||||
run: cmake --build build-fuzz --target fuzz_script -j$(nproc)
|
||||
|
||||
- name: Generate seed corpus from JSON fixtures
|
||||
# Uses src/test/data/script_{valid,invalid}.json so the fuzzer
|
||||
# starts from real Bitcoin-style scripts instead of empty input.
|
||||
run: |
|
||||
mkdir -p build-fuzz/fuzz_corpus
|
||||
python3 src/test/fuzz/seed_corpus.py \
|
||||
src/test/data/script_valid.json \
|
||||
build-fuzz/fuzz_corpus valid
|
||||
python3 src/test/fuzz/seed_corpus.py \
|
||||
src/test/data/script_invalid.json \
|
||||
build-fuzz/fuzz_corpus invalid
|
||||
|
||||
- name: Run fuzzer for 5 minutes
|
||||
# -max_total_time=300 hard-caps runtime. Crashes go to artifact
|
||||
# prefix; we upload any artifacts and fail the job if any exist.
|
||||
run: |
|
||||
mkdir -p build-fuzz/fuzz_artifacts
|
||||
set +e
|
||||
./build-fuzz/bin/fuzz_script \
|
||||
-max_total_time=300 \
|
||||
-max_len=4096 \
|
||||
-artifact_prefix=build-fuzz/fuzz_artifacts/ \
|
||||
build-fuzz/fuzz_corpus/ \
|
||||
2>&1 | tee build-fuzz/fuzz_log.txt
|
||||
FUZZ_EXIT=${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [ -n "$(ls -A build-fuzz/fuzz_artifacts/ 2>/dev/null | grep -v '\.tmp$')" ]; then
|
||||
echo "::error::Fuzzer produced crash/leak artifacts"
|
||||
exit 1
|
||||
fi
|
||||
exit "$FUZZ_EXIT"
|
||||
|
||||
- name: Upload fuzzer artifacts on success
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: fuzz-artifacts
|
||||
path: build-fuzz/fuzz_artifacts/
|
||||
|
||||
test-fuzz-smoke-tx:
|
||||
# libFuzzer smoke test for src/test/fuzz/transaction_deserialize_fuzz.cpp.
|
||||
# Mirrors test-fuzz-smoke but exercises CTransaction deserialization
|
||||
# instead of the script interpreter. Any crash is uploaded as an artifact
|
||||
# and the job fails — fuzz regressions must block the PR.
|
||||
# See src/test/fuzz/transaction_deserialize_fuzz.cpp for harness details.
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1:abort_on_error=1:print_stacktrace=1"
|
||||
UBSAN_OPTIONS: "halt_on_error=1:abort_on_error=1:print_stacktrace=1"
|
||||
SAN_FLAGS: "-fsanitize=address,undefined,fuzzer-no-link -fno-omit-frame-pointer -fno-sanitize-recover=undefined -fno-sanitize=alignment,signed-integer-overflow,vptr"
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install clang + dependencies
|
||||
# libFuzzer ships with clang since v6; clang-15 is on the runner.
|
||||
# libgflags-dev: fuzz link line references -lgflags (RocksDB builds
|
||||
# expect gflags as a transitive dep). Without it the link step fails
|
||||
# with "cannot find -lgflags". CI's ubuntu-22.04 runner does NOT ship
|
||||
# it by default.
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-15 cmake ninja-build \
|
||||
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
|
||||
libevent-dev libminiupnpc-dev zlib1g-dev \
|
||||
libsnappy-dev liblz4-dev libzstd-dev \
|
||||
libgflags-dev
|
||||
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-15 100
|
||||
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-15 100
|
||||
|
||||
- name: Build RocksDB from source
|
||||
run: sudo bash scripts/ci/build-rocksdb.sh
|
||||
|
||||
- name: Configure with fuzzing + sanitizers
|
||||
# NB: do NOT pass -fsanitize=fuzzer in CMAKE_EXE_LINKER_FLAGS — that
|
||||
# pulls libFuzzer's main() into CMake's compiler-probe linker test
|
||||
# and trips "multiple definition of `main`". The transaction_deserialize_fuzz
|
||||
# target's custom clang++ link step adds -fsanitize=fuzzer in src/CMakeLists.txt
|
||||
# (see BUILD_FUZZ block).
|
||||
# SECP256K1_ASM=OFF: clang-15+ register allocator is sometimes stricter
|
||||
# than clang-14 about the x86_64 inline asm in scalar_4x64_impl.h.
|
||||
run: |
|
||||
cmake -B build-fuzz -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_C_FLAGS="$SAN_FLAGS" \
|
||||
-DCMAKE_CXX_FLAGS="$SAN_FLAGS" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="$SAN_FLAGS" \
|
||||
-DBUILD_QT=OFF \
|
||||
-DBUILD_DAEMON=ON \
|
||||
-DBUILD_TESTS=ON \
|
||||
-DBUILD_FUZZ=ON \
|
||||
-DUSE_UPNP=OFF \
|
||||
-DSECP256K1_ASM=OFF
|
||||
|
||||
- name: Build libtor (embedded Tor static lib)
|
||||
# BUILD_FUZZ pulls in triangles_common + trianglesd_objects (OBJECT lib)
|
||||
# via the fuzz target's CMake deps. The link line references libtor.a,
|
||||
# which the Tor submodule script produces — CMake doesn't build it.
|
||||
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 transaction_deserialize_fuzz
|
||||
# CMake target is named `transaction_deserialize_fuzz` (matches
|
||||
# add_custom_target(transaction_deserialize_fuzz ...) in src/CMakeLists.txt).
|
||||
run: cmake --build build-fuzz --target transaction_deserialize_fuzz -j$(nproc)
|
||||
|
||||
- name: Run fuzzer for 5 minutes
|
||||
# -max_total_time=300 hard-caps runtime. Crashes go to artifact
|
||||
# prefix; we upload any artifacts and fail the job if any exist.
|
||||
# The transaction_deserialize_fuzz target does not need a seed
|
||||
# corpus — it accepts arbitrary bytes as a transaction payload.
|
||||
run: |
|
||||
mkdir -p build-fuzz/fuzz_artifacts_tx build-fuzz/fuzz_corpus_tx
|
||||
set +e
|
||||
./build-fuzz/bin/transaction_deserialize_fuzz \
|
||||
-max_total_time=300 \
|
||||
-max_len=200000 \
|
||||
-artifact_prefix=build-fuzz/fuzz_artifacts_tx/ \
|
||||
build-fuzz/fuzz_corpus_tx/ \
|
||||
2>&1 | tee build-fuzz/fuzz_log.txt
|
||||
FUZZ_EXIT=${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [ -n "$(ls -A build-fuzz/fuzz_artifacts_tx/ 2>/dev/null | grep -v '\.tmp$')" ]; then
|
||||
echo "::error::Fuzzer produced crash/leak artifacts"
|
||||
exit 1
|
||||
fi
|
||||
exit "$FUZZ_EXIT"
|
||||
|
||||
- name: Upload fuzzer artifacts on success
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: fuzz-artifacts-tx
|
||||
path: build-fuzz/fuzz_artifacts_tx/
|
||||
|
||||
build-windows-qt:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: msys2/setup-msys2@v2
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
with:
|
||||
msystem: MINGW64
|
||||
update: true
|
||||
@@ -175,6 +404,7 @@ 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
|
||||
@@ -182,9 +412,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
|
||||
|
||||
@@ -195,7 +425,7 @@ jobs:
|
||||
-DBUILD_QT=ON \
|
||||
-DBUILD_DAEMON=OFF \
|
||||
-DBUILD_TESTS=OFF \
|
||||
-DUSE_UPNP=ON \
|
||||
-DUSE_UPNP=OFF \
|
||||
-DUSE_QRCODE=OFF \
|
||||
-DUSE_I2P_EMBEDDED=ON
|
||||
|
||||
@@ -279,17 +509,50 @@ jobs:
|
||||
Get-Item "Cryptographic-Triangles-${env:VERSION}-win-x64.zip"
|
||||
|
||||
- name: Upload artifact (portable zip)
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: windows-qt-zip
|
||||
path: Cryptographic-Triangles-*-win-x64.zip
|
||||
|
||||
- name: Download Tor
|
||||
# Resilient download: archive.torproject.org occasionally times out
|
||||
# from CI egress (observed 2026-07-03: macOS job exit code 6 after
|
||||
# exactly 30s of curl hang). Retries cover transient connection drops;
|
||||
# size check rejects 0-byte "200 OK" responses from broken mirrors.
|
||||
# NOTE: Invoke-WebRequest on PowerShell 5.1 (default on Windows-latest
|
||||
# runners) does NOT accept -ConnectionTimeout/-OperationTimeout — those
|
||||
# are PowerShell 7+. We rely on the retry loop + size check only.
|
||||
shell: powershell
|
||||
run: |
|
||||
$TOR_VERSION = "15.0.9"
|
||||
$TOR_SHA256 = "adebc1b7c65dc1b5e471064ed17585464af6f6198c3fe5c8c9108138b59ccf65"
|
||||
$TOR_URL = "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-windows-x86_64-${TOR_VERSION}.tar.gz"
|
||||
Invoke-WebRequest -Uri $TOR_URL -OutFile tor-bundle.tar.gz
|
||||
$torPath = "tor-bundle.tar.gz"
|
||||
$attempts = 0
|
||||
$maxAttempts = 3
|
||||
$downloaded = $false
|
||||
while ($attempts -lt $maxAttempts -and -not $downloaded) {
|
||||
$attempts++
|
||||
try {
|
||||
if (Test-Path $torPath) { Remove-Item $torPath -ErrorAction SilentlyContinue }
|
||||
Invoke-WebRequest -Uri $TOR_URL -OutFile $torPath -UseBasicParsing
|
||||
$size = (Get-Item $torPath).Length
|
||||
if ($size -gt 1MB) {
|
||||
Write-Host "Downloaded $size bytes on attempt $attempts"
|
||||
$downloaded = $true
|
||||
} else {
|
||||
Write-Host "Download too small ($size bytes), retrying..."
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Download attempt $attempts failed: $_"
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
}
|
||||
if (-not $downloaded) { throw "Tor bundle download failed after $maxAttempts attempts" }
|
||||
$actualSha256 = (Get-FileHash -Algorithm SHA256 $torPath).Hash.ToLowerInvariant()
|
||||
if ($actualSha256 -ne $TOR_SHA256) {
|
||||
throw "Tor bundle SHA256 mismatch: expected $TOR_SHA256, got $actualSha256"
|
||||
}
|
||||
New-Item -ItemType Directory -Path tor-extract -Force
|
||||
tar -xzf tor-bundle.tar.gz -C tor-extract
|
||||
New-Item -ItemType Directory -Path tor-files -Force
|
||||
@@ -306,23 +569,11 @@ jobs:
|
||||
- name: Install NSIS via MSYS2
|
||||
run: pacman -S --noconfirm mingw-w64-x86_64-nsis
|
||||
|
||||
- name: Install NSIS inetc plugin
|
||||
run: |
|
||||
pacman -S --noconfirm unzip
|
||||
NSIS_DIR="/mingw64/share/nsis"
|
||||
cd /tmp
|
||||
curl -L -o Inetc.zip "https://nsis.sourceforge.io/mediawiki/images/c/c9/Inetc.zip"
|
||||
unzip -o Inetc.zip -d inetc_extract
|
||||
# MSYS2 mingw64 NSIS is 64-bit, needs amd64-unicode plugin in Plugins/unicode/
|
||||
mkdir -p "$NSIS_DIR/Plugins/unicode"
|
||||
cp inetc_extract/Plugins/amd64-unicode/INetC.dll "$NSIS_DIR/Plugins/unicode/"
|
||||
echo "Installed 64-bit INetC.dll to $NSIS_DIR/Plugins/unicode/"
|
||||
|
||||
- name: Build NSIS installer
|
||||
run: makensis //DVERSION=$VERSION contrib/nsis/setup.nsi
|
||||
|
||||
- name: Upload installer
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: windows-qt-setup
|
||||
path: contrib/nsis/Cryptographic-Triangles-*-setup.exe
|
||||
@@ -333,11 +584,11 @@ jobs:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: msys2/setup-msys2@v2
|
||||
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
|
||||
with:
|
||||
msystem: MINGW64
|
||||
update: true
|
||||
@@ -352,6 +603,7 @@ 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
|
||||
@@ -362,7 +614,7 @@ jobs:
|
||||
-DBUILD_DAEMON=ON \
|
||||
-DBUILD_CLI=ON \
|
||||
-DBUILD_TESTS=OFF \
|
||||
-DUSE_UPNP=ON \
|
||||
-DUSE_UPNP=OFF \
|
||||
-DUSE_I2P_EMBEDDED=ON
|
||||
|
||||
- name: Build libtor (embedded Tor static lib)
|
||||
@@ -384,10 +636,44 @@ jobs:
|
||||
run: bash scripts/ci/package-windows-daemon.sh daemon-dist trianglesd triangles-cli
|
||||
|
||||
- name: Bundle Tor for daemon
|
||||
# Resilient download: archive.torproject.org occasionally times out
|
||||
# from CI egress (observed 2026-07-03: macOS job exit code 6 after
|
||||
# exactly 30s of curl hang). Retries cover transient connection drops;
|
||||
# size check rejects 0-byte "200 OK" responses from broken mirrors.
|
||||
# NOTE: Invoke-WebRequest on PowerShell 5.1 (default on Windows-latest
|
||||
# runners) does NOT accept -ConnectionTimeout/-OperationTimeout — those
|
||||
# are PowerShell 7+. We rely on the retry loop + size check only.
|
||||
shell: powershell
|
||||
run: |
|
||||
$TOR_VERSION = "15.0.9"
|
||||
Invoke-WebRequest -Uri "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-windows-x86_64-${TOR_VERSION}.tar.gz" -OutFile tor-bundle.tar.gz
|
||||
$TOR_SHA256 = "adebc1b7c65dc1b5e471064ed17585464af6f6198c3fe5c8c9108138b59ccf65"
|
||||
$TOR_URL = "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-windows-x86_64-${TOR_VERSION}.tar.gz"
|
||||
$torPath = "tor-bundle.tar.gz"
|
||||
$attempts = 0
|
||||
$maxAttempts = 3
|
||||
$downloaded = $false
|
||||
while ($attempts -lt $maxAttempts -and -not $downloaded) {
|
||||
$attempts++
|
||||
try {
|
||||
if (Test-Path $torPath) { Remove-Item $torPath -ErrorAction SilentlyContinue }
|
||||
Invoke-WebRequest -Uri $TOR_URL -OutFile $torPath -UseBasicParsing
|
||||
$size = (Get-Item $torPath).Length
|
||||
if ($size -gt 1MB) {
|
||||
Write-Host "Downloaded $size bytes on attempt $attempts"
|
||||
$downloaded = $true
|
||||
} else {
|
||||
Write-Host "Download too small ($size bytes), retrying..."
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Download attempt $attempts failed: $_"
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
}
|
||||
if (-not $downloaded) { throw "Tor bundle download failed after $maxAttempts attempts" }
|
||||
$actualSha256 = (Get-FileHash -Algorithm SHA256 $torPath).Hash.ToLowerInvariant()
|
||||
if ($actualSha256 -ne $TOR_SHA256) {
|
||||
throw "Tor bundle SHA256 mismatch: expected $TOR_SHA256, got $actualSha256"
|
||||
}
|
||||
New-Item -ItemType Directory -Path tor-extract -Force
|
||||
tar -xzf tor-bundle.tar.gz -C tor-extract
|
||||
Copy-Item -Recurse tor-extract/tor/* daemon-dist/tor/
|
||||
@@ -396,7 +682,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: windows-daemon
|
||||
path: daemon-dist/
|
||||
@@ -404,7 +690,7 @@ jobs:
|
||||
build-linux-qt:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -413,9 +699,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
|
||||
|
||||
@@ -438,7 +724,7 @@ jobs:
|
||||
-DBUILD_QT=ON \
|
||||
-DBUILD_DAEMON=OFF \
|
||||
-DBUILD_TESTS=OFF \
|
||||
-DUSE_UPNP=ON \
|
||||
-DUSE_UPNP=OFF \
|
||||
-DUSE_I2P_EMBEDDED=ON
|
||||
|
||||
- name: Build libtor (embedded Tor static lib)
|
||||
@@ -461,8 +747,18 @@ jobs:
|
||||
|
||||
- name: Build .deb package (fully self-contained)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TOR_VERSION="15.0.9"
|
||||
curl -sL "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-linux-x86_64-${TOR_VERSION}.tar.gz" -o tor-bundle.tar.gz
|
||||
TOR_SHA256="7ea13e14cddafb36c6347a9c4f4e639f6010364c16acfd519157c29e226277f2"
|
||||
# Resilient download: archive.torproject.org occasionally times out
|
||||
# from CI egress (observed 2026-07-03: macOS job exit code 6 after
|
||||
# exactly 30s of curl hang). Retries + --fail-with-body surface the
|
||||
# next failure loudly instead of silently producing a 0-byte file.
|
||||
curl -fSL --connect-timeout 15 --max-time 120 \
|
||||
--retry 3 --retry-delay 5 --retry-connrefused --retry-all-errors \
|
||||
"https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-linux-x86_64-${TOR_VERSION}.tar.gz" \
|
||||
-o tor-bundle.tar.gz
|
||||
printf '%s %s\n' "$TOR_SHA256" tor-bundle.tar.gz | sha256sum --check --strict -
|
||||
mkdir -p tor-extract && tar -xzf tor-bundle.tar.gz -C tor-extract
|
||||
|
||||
PKG="cryptographic-triangles_${VERSION}_amd64"
|
||||
@@ -532,7 +828,7 @@ jobs:
|
||||
dpkg-deb --build ${PKG}
|
||||
|
||||
- name: Upload .deb
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: linux-qt-deb
|
||||
path: cryptographic-triangles_*_amd64.deb
|
||||
@@ -540,7 +836,7 @@ jobs:
|
||||
build-linux-daemon:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -549,9 +845,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
|
||||
|
||||
@@ -574,7 +870,7 @@ jobs:
|
||||
-DBUILD_DAEMON=ON \
|
||||
-DBUILD_CLI=ON \
|
||||
-DBUILD_TESTS=OFF \
|
||||
-DUSE_UPNP=ON \
|
||||
-DUSE_UPNP=OFF \
|
||||
-DUSE_I2P_EMBEDDED=ON
|
||||
|
||||
- name: Build libtor (embedded Tor static lib)
|
||||
@@ -603,7 +899,7 @@ jobs:
|
||||
run: bash scripts/ci/package-linux-daemon.sh "${VERSION}"
|
||||
|
||||
- name: Upload .deb
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: linux-daemon-deb
|
||||
path: cryptographic-triangles-daemon_*_amd64.deb
|
||||
@@ -611,7 +907,7 @@ jobs:
|
||||
build-macos:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -620,9 +916,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
|
||||
|
||||
@@ -643,7 +939,7 @@ jobs:
|
||||
-DBUILD_QT=ON \
|
||||
-DBUILD_DAEMON=OFF \
|
||||
-DBUILD_TESTS=OFF \
|
||||
-DUSE_UPNP=ON \
|
||||
-DUSE_UPNP=OFF \
|
||||
-DUSE_I2P_EMBEDDED=ON \
|
||||
-DBOOST_ROOT=/opt/homebrew/opt/boost \
|
||||
-DBDB_INCLUDE_PATH=/opt/homebrew/opt/berkeley-db@5/include \
|
||||
@@ -670,18 +966,6 @@ jobs:
|
||||
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
|
||||
@@ -730,9 +1014,20 @@ jobs:
|
||||
otool -L "$BINARY" | head -30
|
||||
|
||||
- name: Bundle Tor into app
|
||||
# Resilient download: archive.torproject.org occasionally times out
|
||||
# from Azure westus egress (observed 2026-07-03: macOS job exit code 6
|
||||
# after exactly 30s of curl hang). --retry 3 with --retry-connrefused
|
||||
# handles transient connection refusals and timeouts; --fail-with-body
|
||||
# surfaces HTTP error bodies so the next failure isn't silent.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TOR_VERSION="15.0.9"
|
||||
curl -sL "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-macos-aarch64-${TOR_VERSION}.tar.gz" -o tor-bundle.tar.gz
|
||||
TOR_SHA256="8ab84587b09b0053e85a137969b501744fa14640aa126af6e36997189950d254"
|
||||
curl -fSL --connect-timeout 15 --max-time 120 \
|
||||
--retry 3 --retry-delay 5 --retry-connrefused --retry-all-errors \
|
||||
"https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-macos-aarch64-${TOR_VERSION}.tar.gz" \
|
||||
-o tor-bundle.tar.gz
|
||||
printf '%s %s\n' "$TOR_SHA256" tor-bundle.tar.gz | shasum -a 256 --check -
|
||||
mkdir -p tor-extract && tar -xzf tor-bundle.tar.gz -C tor-extract
|
||||
APP=$(find build/bin -name "*.app" -maxdepth 1 | head -1)
|
||||
mkdir -p "$APP/Contents/MacOS/tor"
|
||||
@@ -752,7 +1047,7 @@ jobs:
|
||||
"Cryptographic-Triangles-v${VERSION}-macos-arm64.dmg"
|
||||
|
||||
- name: Upload DMG
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: macos-arm64-dmg
|
||||
path: "*.dmg"
|
||||
@@ -768,7 +1063,7 @@ jobs:
|
||||
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
@@ -790,13 +1085,17 @@ jobs:
|
||||
ls -la release/
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
|
||||
with:
|
||||
files: release/*
|
||||
generate_release_notes: true
|
||||
|
||||
trigger-tripi:
|
||||
name: Trigger TRI-PI ARM64 Build
|
||||
# Only fire on tag-push events. To trigger a TRI-PI rebuild after a
|
||||
# release is created via gh API (without re-pushing the tag), use:
|
||||
# curl -X POST .../repos/SamiAhmed7777/tri-pi/dispatches \
|
||||
# -d '{"event_type":"new-release","client_payload":{"version":"vX.Y.Z","source_repo":"SamiAhmed7777/triangles_v5"}}'
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -71,16 +71,16 @@ jobs:
|
||||
# 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
|
||||
for i in {1..90}; 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)"
|
||||
echo " waiting for release v${VERSION} daemon .deb... ($i/90)"
|
||||
sleep 20
|
||||
done
|
||||
echo "::error::Release v${VERSION} daemon .deb never became available after 10 minutes"
|
||||
echo "::error::Release v${VERSION} daemon .deb never became available after 30 minutes"
|
||||
exit 1
|
||||
|
||||
- name: Build and push
|
||||
@@ -137,16 +137,16 @@ jobs:
|
||||
- name: Wait for release artifacts
|
||||
if: env.AUR_SSH_KEY != ''
|
||||
run: |
|
||||
for i in {1..30}; do
|
||||
for i in {1..90}; do
|
||||
URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/cryptographic-triangles_${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}... ($i/30)"
|
||||
echo " waiting for release v${VERSION}... ($i/90)"
|
||||
sleep 20
|
||||
done
|
||||
echo "::error::Release v${VERSION} .deb never became available after 10 minutes"
|
||||
echo "::error::Release v${VERSION} .deb never became available after 30 minutes"
|
||||
exit 1
|
||||
|
||||
- name: Download source .debs
|
||||
@@ -276,16 +276,16 @@ jobs:
|
||||
- name: Wait for release artifacts
|
||||
if: env.HOMEBREW_GITHUB_TOKEN != ''
|
||||
run: |
|
||||
for i in {1..30}; do
|
||||
for i in {1..90}; do
|
||||
URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/Cryptographic-Triangles-v${VERSION}-macos-arm64.dmg"
|
||||
if curl -fsSL --head "$URL" >/dev/null 2>&1; then
|
||||
echo "✓ Release .dmg available: $URL"
|
||||
exit 0
|
||||
fi
|
||||
echo " waiting for release v${VERSION}... ($i/30)"
|
||||
echo " waiting for release v${VERSION}... ($i/90)"
|
||||
sleep 20
|
||||
done
|
||||
echo "::error::Release v${VERSION} macOS .dmg never became available"
|
||||
echo "::error::Release v${VERSION} macOS .dmg never became available after 30 minutes"
|
||||
exit 1
|
||||
|
||||
- name: Compute macOS .dmg SHA256
|
||||
@@ -379,16 +379,16 @@ jobs:
|
||||
if: env.CHOCO_API_KEY != '' && env.CHOCO_SKIP_WACATAC != ''
|
||||
shell: bash
|
||||
run: |
|
||||
for i in {1..30}; do
|
||||
for i in {1..90}; do
|
||||
URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/Cryptographic-Triangles-${VERSION}-win-x64-setup.exe"
|
||||
if curl -fsSL --head "$URL" >/dev/null 2>&1; then
|
||||
echo "✓ Release .exe available: $URL"
|
||||
exit 0
|
||||
fi
|
||||
echo " waiting for release v${VERSION}... ($i/30)"
|
||||
echo " waiting for release v${VERSION}... ($i/90)"
|
||||
sleep 20
|
||||
done
|
||||
echo "::error::Release v${VERSION} Windows installer never became available"
|
||||
echo "::error::Release v${VERSION} Windows installer never became available after 30 minutes"
|
||||
exit 1
|
||||
|
||||
- name: Compute installer SHA256
|
||||
@@ -486,16 +486,16 @@ jobs:
|
||||
- name: Wait for release artifacts
|
||||
if: env.WINGET_TOKEN != ''
|
||||
run: |
|
||||
for i in {1..30}; do
|
||||
for i in {1..90}; do
|
||||
URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/Cryptographic-Triangles-${VERSION}-win-x64-setup.exe"
|
||||
if curl -fsSL --head "$URL" >/dev/null 2>&1; then
|
||||
echo "✓ Release .exe available: $URL"
|
||||
exit 0
|
||||
fi
|
||||
echo " waiting for release v${VERSION}... ($i/30)"
|
||||
echo " waiting for release v${VERSION}... ($i/90)"
|
||||
sleep 20
|
||||
done
|
||||
echo "::error::Release v${VERSION} Windows installer never became available"
|
||||
echo "::error::Release v${VERSION} Windows installer never became available after 30 minutes"
|
||||
exit 1
|
||||
|
||||
- name: Compute installer SHA256
|
||||
|
||||
+46
-16
@@ -26,12 +26,20 @@ jobs:
|
||||
|
||||
- name: Check format on changed lines
|
||||
run: |
|
||||
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
|
||||
echo "Comparing against merge-base: $BASE_SHA"
|
||||
# Diff-only on PRs (have a base_ref). On workflow_dispatch, base_ref is
|
||||
# empty — in that case run clang-format on the whole tree so a manual
|
||||
# trigger still produces a useful signal instead of erroring out.
|
||||
if [ -n "${{ github.base_ref }}" ]; then
|
||||
BASE_SHA=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
|
||||
echo "Comparing against merge-base: $BASE_SHA"
|
||||
|
||||
# git-clang-format prints a diff if any changed line violates style.
|
||||
# --diff exits non-zero when reformatting would change something.
|
||||
OUTPUT=$(git clang-format --diff "$BASE_SHA" -- '*.cpp' '*.h' '*.hpp' '*.cc' || true)
|
||||
# git-clang-format prints a diff if any changed line violates style.
|
||||
# --diff exits non-zero when reformatting would change something.
|
||||
OUTPUT=$(git clang-format --diff "$BASE_SHA" -- '*.cpp' '*.h' '*.hpp' '*.cc' || true)
|
||||
else
|
||||
echo "No base_ref (workflow_dispatch) — running clang-format on whole tree"
|
||||
OUTPUT=$(git clang-format --diff $(git rev-list --max-parents=0 HEAD | head -1) -- '*.cpp' '*.h' '*.hpp' '*.cc' || true)
|
||||
fi
|
||||
|
||||
if [ -z "$OUTPUT" ] || [ "$OUTPUT" = "no modified files to format" ] || [ "$OUTPUT" = "clang-format did not modify any files" ]; then
|
||||
echo "clang-format: clean"
|
||||
@@ -83,9 +91,6 @@ jobs:
|
||||
|
||||
- name: Run clang-tidy on changed lines
|
||||
run: |
|
||||
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
|
||||
echo "Comparing against merge-base: $BASE_SHA"
|
||||
|
||||
# clang-tidy-diff.py ships with clang-tidy; runs tidy only on changed lines.
|
||||
DIFF_SCRIPT=$(dpkg -L clang-tidy-15 | grep clang-tidy-diff.py | head -1)
|
||||
if [ -z "$DIFF_SCRIPT" ]; then
|
||||
@@ -93,17 +98,42 @@ jobs:
|
||||
fi
|
||||
echo "Using: $DIFF_SCRIPT"
|
||||
|
||||
if [ -n "${{ github.base_ref }}" ]; then
|
||||
BASE_SHA=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
|
||||
echo "Comparing against merge-base: $BASE_SHA"
|
||||
git diff -U0 "$BASE_SHA" -- 'src/*.cpp' 'src/*.h' \
|
||||
':(exclude)src/json/nlohmann_json.hpp' \
|
||||
':(exclude)src/leveldb/*' \
|
||||
':(exclude)src/lz4/*' \
|
||||
':(exclude)src/tor/tor-src/*' > /tmp/changes.diff
|
||||
else
|
||||
echo "No base_ref (workflow_dispatch) — running clang-tidy on whole tree"
|
||||
git diff -U0 -- $(git rev-list --max-parents=0 HEAD | head -1)..HEAD -- 'src/*.cpp' 'src/*.h' \
|
||||
':(exclude)src/json/nlohmann_json.hpp' \
|
||||
':(exclude)src/leveldb/*' \
|
||||
':(exclude)src/lz4/*' \
|
||||
':(exclude)src/tor/tor-src/*' > /tmp/changes.diff || true
|
||||
# If the initial commit was so old that the diff is empty, fall back to HEAD vs HEAD~100
|
||||
if [ ! -s /tmp/changes.diff ]; then
|
||||
git diff -U0 HEAD~100..HEAD -- 'src/*.cpp' 'src/*.h' \
|
||||
':(exclude)src/json/nlohmann_json.hpp' \
|
||||
':(exclude)src/leveldb/*' \
|
||||
':(exclude)src/lz4/*' \
|
||||
':(exclude)src/tor/tor-src/*' > /tmp/changes.diff || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -s /tmp/changes.diff ]; then
|
||||
echo "No changes to lint in dispatch context — skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# -p1 strips the leading "a/"/"b/" from git diff paths.
|
||||
# -path=build points clang-tidy at compile_commands.json.
|
||||
# -iregex restricts to project sources (not vendored).
|
||||
git diff -U0 "$BASE_SHA" -- 'src/*.cpp' 'src/*.h' \
|
||||
':(exclude)src/json/nlohmann_json.hpp' \
|
||||
':(exclude)src/leveldb/*' \
|
||||
':(exclude)src/lz4/*' \
|
||||
':(exclude)src/tor/tor-src/*' \
|
||||
| python3 "$DIFF_SCRIPT" -p1 -path build \
|
||||
-iregex '.*\.(cpp|cc|h|hpp)$' \
|
||||
-j$(nproc) || EXIT=$?
|
||||
cat /tmp/changes.diff | python3 "$DIFF_SCRIPT" -p1 -path build \
|
||||
-iregex '.*\.(cpp|cc|h|hpp)$' \
|
||||
-j$(nproc) || EXIT=$?
|
||||
|
||||
# Warn-only initially. Flip this to `exit ${EXIT:-0}` once we're clean.
|
||||
exit 0
|
||||
|
||||
@@ -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.
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Triangles (TRI) are documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [6.2.5] - 2026-08-03
|
||||
|
||||
### Fixed
|
||||
- **Stake-age soft cap reverted** in `src/kernel.cpp::GetWeight`. The V5-fork
|
||||
7-day soft cap (activated 2026-04-12) was the regression that capped
|
||||
long-dormant coins at 7 days of weight, killing the diamond-hands
|
||||
incentive. Restored to original Peercoin `min(nAge, nStakeMaxAge)`.
|
||||
Chain was frozen at block 2,224,763 since 2026-07-18 with no blocks
|
||||
ever produced under the soft cap, so reverting changes zero historical
|
||||
block validation results.
|
||||
- **`ReadUtxo` lazy fallback** in `src/txdb-base.cpp`. The fallback to
|
||||
`txindex.vSpent[]` exists in `HaveUtxo` but was missing in `ReadUtxo`,
|
||||
so nodes with incomplete UTXO snapshots could not find pre-snapshot
|
||||
unspent outputs (chain stalled at 2,224,763 since 2026-07-18).
|
||||
Added: when UTXO DB misses an entry but `txindex.vSpent[n].IsNull()`,
|
||||
read the transaction from disk and reconstruct the full CUtxoEntry
|
||||
including exact block height via `mapBlockIndex` lookup.
|
||||
- **`DisconnectBlock` height reconstruction** in `src/main.cpp`. Reorg
|
||||
path now recovers exact block height via `mapBlockIndex` instead of
|
||||
leaving `nHeight = 0` on restored UTXOs.
|
||||
|
||||
## [6.2.4] - 2026-08-02
|
||||
|
||||
### Changed
|
||||
- **RocksDB bumped 8.9.1 → 10.10.1** in CI (`scripts/ci/build-rocksdb.sh`).
|
||||
Required to read the Hetzner Dropbox bootstrap snapshot's chain DB,
|
||||
whose SST files are at format_version=7. RocksDB 10.10.1 still uses
|
||||
`format_version=6` as its own default; the daemon does NOT pin a
|
||||
different value, so newly written SSTs continue to land at v6. This
|
||||
is deliberate: mixed v6/v7 SST files in the same DB are supported by
|
||||
RocksDB, and v7 writes from this build would close the door on
|
||||
downgrade to 6.2.3 (or any RocksDB < 10.4.0) without fixing anything.
|
||||
|
||||
### Fixed
|
||||
- **`scripts/ci/build-rocksdb.sh`** now strips `-std=c++XX` (regex covers
|
||||
`-std=c++17` / `-std=c++20` / `-std=c++2b` / future values) from
|
||||
`rocksdb.pc` Cflags instead of only the `-std=c++17` value. RocksDB
|
||||
10.x writes `-std=c++20`, which `pkg-config` injects into every
|
||||
Triangles translation unit. C++ translation units ignore the
|
||||
redundant flag, but C units (e.g. `src/lz4/lz4.c`) hit a fatal
|
||||
`error: invalid argument '-std=c++XX' not allowed with 'C'` from
|
||||
clang. Previously, the daemon build tolerated this as a warning;
|
||||
the fuzz build (`clang-15` + sanitizers) treated it as a hard
|
||||
error and the `test-fuzz-smoke` / `test-fuzz-smoke-tx` jobs failed
|
||||
in the 6.2.4 CI run #30744702062 at the `Build fuzz_script` /
|
||||
`Build transaction_deserialize_fuzz` step.
|
||||
|
||||
### Notes for operators upgrading from 6.2.3
|
||||
- The daemon's runtime dependency is `librocksdb.so.10.10.1`
|
||||
(replacing the previous `librocksdb.so.8.9.1`). Install or build
|
||||
rocksdb from source before rolling 6.2.4 onto a node; the .deb
|
||||
from CI bundles the right SONAME and should just work on
|
||||
Ubuntu 22.04 / 24.04.
|
||||
- If you imported the Hetzner Dropbox bootstrap snapshot's chain DB
|
||||
into this node, that DB still contains v7 SSTs. Any daemon down to
|
||||
RocksDB 10.4.0 will read it; RocksDB ≤ 10.3.x will reject the v7
|
||||
SSTs with `Corrupt or unsupported format_version: 7`. After the
|
||||
daemon compacts the imported chain DB, the v7 SSTs may be re-written
|
||||
at v6 and the DB becomes readable by older rocksdb again — that
|
||||
happens naturally as part of normal compaction, no extra action
|
||||
required.
|
||||
- Package checksums in `packaging/flatpak`, `packaging/scoop`, and
|
||||
`packaging/winget` are regenerated during the CI release workflow
|
||||
after artifacts are produced; do not ship those package manifests
|
||||
until their SHA-256 sums match the v6.2.4 release artifacts.
|
||||
|
||||
## [6.2.3] - 2026-08-01
|
||||
|
||||
### Changed
|
||||
- **Local snapshot loading no longer requires a compiled-in SHA match.**
|
||||
Previously, loading `utxo-snapshot.bin` from the data dir rejected the
|
||||
file unless its SHA256 was present in `Checkpoints::mapSnapshotHashes`
|
||||
(which only knows about one or two canonical tips at compile time).
|
||||
Local file loads are operator-trusted — the operator already has
|
||||
filesystem access — so the SHA gate was friction without a security
|
||||
benefit. The gate still exists for P2P-delivered snapshots via
|
||||
`SnapshotNet` (requireCheckpoint=true there).
|
||||
|
||||
### Added
|
||||
- `-acceptanylocalsnapshot` CLI flag: forces acceptance of a local
|
||||
`utxo-snapshot.bin` whose SHA is not in the compiled map, with an
|
||||
explicit warning log line. Use only with operator-signed snapshots.
|
||||
|
||||
## [6.2.2] - 2026-08-01
|
||||
|
||||
|
||||
### Fixed
|
||||
- **Snapshot regeneration: full chain index, not just the last 2000.**
|
||||
`UTXO_SNAPSHOT_DEFAULT_HEADERS` was 2000, which silently trimmed the
|
||||
snapshot to the last 2000 blocks even though the v2+ format is designed
|
||||
to carry the full chain index. The too-small snapshot caused
|
||||
`GetKernelStakeModifier() : block not indexed` errors after a fresh
|
||||
node loaded it — the kernel-stake-modifier walk in `CreateCoinStake`
|
||||
needs blocks older than the last 2000 because `nStakeModifierSelectionInterval`
|
||||
is multi-day. The block index was effectively unusable for the
|
||||
StakeMiner on the recovered node. Default is now 0 (all headers); the
|
||||
trim is bypassed when `nHeaders=0`. Callers may still pass an explicit
|
||||
positive value for a small diagnostic snapshot.
|
||||
|
||||
|
||||
### Fixed
|
||||
- **Build portability: v6.1.9 binary crashed with SIGILL on every
|
||||
production node.** v6.1.9 was built on GitHub Actions' EPYC 7763
|
||||
runner (AVX-512 capable). GCC 11.4 + libstdc++ inlining emitted 741
|
||||
`vpbroadcastq` EVEX instructions into the daemon binary even though
|
||||
the cmake `AddCompilerFlags.cmake` was setting `-march=x86-64-v2
|
||||
-mtune=generic`. The resulting binary crashed on every production
|
||||
CPU that lacks AVX-512: KVM-virtualized EPYC (DNS2), Ryzen 5 3600
|
||||
(SAMI-PC), and any non-x86_64 node. v6.2.0 adds an explicit
|
||||
`-mno-avx512f -mno-avx512*` block to the global compile options so
|
||||
the build cannot leak AVX-512 regardless of what the build host
|
||||
supports. Carries forward the v6.1.9 staking-selfheal fix unchanged.
|
||||
See `references/avx-512-sigill-build-fix.md` for the full diagnosis.
|
||||
|
||||
### Changed
|
||||
- Bump version 6.1.9 → 6.2.0 to reflect the build-system change.
|
||||
|
||||
## [6.1.9] - 2026-07-31
|
||||
|
||||
### Fixed
|
||||
- **Staking deadlock on idle networks.** `IsStakingSafe()` refused to
|
||||
stake whenever `IsInitialBlockDownload()` was true, and `IBD` flipped
|
||||
true whenever the chain tip was older than 24h. After 24h of no blocks,
|
||||
every node simultaneously refused to stake and the chain deadlocked.
|
||||
The `staking: true` flag in `getstakinginfo` was misleading — it only
|
||||
reflected a single search in the brief window after a restart. Narrowed
|
||||
the gate to "refuse only when IBD is true AND local height is behind
|
||||
the peer/checkpoint estimate" (`f69f087`). A node at the peer median
|
||||
now clears the gate and keeps staking through idle periods, so the
|
||||
chain self-heals. Genuinely-behind nodes still hold off. Block
|
||||
validation, reorg rules, and checkpoint rules are unchanged. The
|
||||
`-forcestaking` bootstrap escape hatch still works on nodes caught
|
||||
up to the checkpoint.
|
||||
|
||||
### Changed
|
||||
- CLI: `-conf=` (empty value) now falls back to the default config
|
||||
path instead of erroring out (`41e3898`).
|
||||
- CLI: `-conf` / `-datadir` / `-rpcuser` / `-rpcpassword` are honored
|
||||
in the documented order, with clearer error messages on bad input
|
||||
(`64556dc`).
|
||||
- Build: reproducible build + signed release pipeline (PR #26 chain).
|
||||
|
||||
## [6.1.8] - 2026-07-17
|
||||
|
||||
### Changed
|
||||
- Bootstrap: RPC-driven trusted snapshot publisher rotation (PR #26).
|
||||
Operators can rotate the snapshot publisher via RPC instead of
|
||||
hard-coding it in the binary.
|
||||
- Consensus: removed local-finality, fixed `getheaders` fork recovery
|
||||
(`935d1d5`).
|
||||
- Consensus: fail-closed reorg guard when the startup checkpoint
|
||||
pointer is null (`6116cff`).
|
||||
- IBD: allow `getblocks`/`getheaders` on OneShot peers during IBD
|
||||
(`c68a8cb`).
|
||||
- Build: bump revision 7 → 8.
|
||||
|
||||
### ⚠️ Known issue
|
||||
- v6.1.8 introduced a staking deadlock on idle networks via the
|
||||
`IsStakingSafe()` gate. Operators on v6.1.8 should set
|
||||
`staking=1` and `forcestaking=1` in `triangles.conf` and restart
|
||||
to unstick the chain. v6.1.9 fixes the root cause.
|
||||
|
||||
## [6.1.7] - 2026-07-08
|
||||
|
||||
### Changed
|
||||
- Overview page UI: the Total balance label is now rendered with
|
||||
`font-weight: 900` (full bold) instead of Qt's default bold (75,
|
||||
medium-bold). On builds where the font has a true heavy variant,
|
||||
the Total now visually pops as the headline number against the
|
||||
Spendable / Stake / Unconfirmed rows.
|
||||
- Transactions amount column **Confirming tier color** is now
|
||||
`#4A8C5E` (mid green) instead of `#C5EBC9` (pale mint). The pale
|
||||
mint was too close to the bright `#7CDB8A` Confirmed green on
|
||||
the dark background and read as the same color. Mid green sits
|
||||
clearly between grey (Unconfirmed) and bright green (Confirmed)
|
||||
so the three tiers are visually distinct.
|
||||
- Transactions amount column **now reads confirmation depth
|
||||
directly** (new `DepthRole` on `TransactionTableModel`) instead
|
||||
of going through the `TransactionStatus` enum. The rule fires on
|
||||
every block increment, not just on enum state transitions.
|
||||
Affects both `transactiontablemodel.cpp` (Transactions tab) and
|
||||
`overviewpage.cpp` (Overview recent-5 list).
|
||||
|
||||
## [6.1.6] - 2026-07-08
|
||||
|
||||
### Changed
|
||||
- Overview page UI: conditional color on the **Total** balance label.
|
||||
Renders money-green (`#7CDB8A`) when the total is greater than zero
|
||||
and brand-red (`#e32105`) when the wallet is empty. Previously a
|
||||
static green stylesheet rule failed to cascade on some Qt builds,
|
||||
leaving Total always red.
|
||||
- Transactions list (and Overview recent-5 list) **amount column** now
|
||||
uses a 3-tier color rule keyed off the existing `TransactionStatus`
|
||||
state machine, so the amount color agrees with the status icon:
|
||||
- 0 confirms (`Unconfirmed`) → grey (`#61280E`)
|
||||
- 1–3 confirms (`Confirming`) → pale mint (`#C5EBC9`)
|
||||
- 4+ confirms (`Confirmed`) → money-green (`#7CDB8A`)
|
||||
- Conflicted → grey
|
||||
- Negative amounts (spent) stay red across all tiers.
|
||||
- Internal: added `COLOR_CONFIRMING` constant in `guiconstants.h`;
|
||||
rewired both amount paint sites
|
||||
(`overviewpage.cpp::TxViewDelegate::paint` and
|
||||
`transactiontablemodel.cpp::ForegroundRole`) to share the rule.
|
||||
|
||||
### Fixed
|
||||
- `overviewpage.cpp` now includes `transactionrecord.h` so the
|
||||
`TransactionStatus::Confirming` enum value is in scope (was
|
||||
previously only forward-declared via `transactiontablemodel.h`).
|
||||
|
||||
## [6.1.5] - 2026-07-08
|
||||
|
||||
### Added
|
||||
- New `tweet@sami-ahmed.net` uid on the maintainer signing key, with
|
||||
`hello@sami-ahmed.net` verified on the GitHub account — release tags now
|
||||
show as "Verified" on github.com.
|
||||
- `CHANGELOG.md` at the repo root (this file).
|
||||
|
||||
### Changed
|
||||
- Overview page UI: pending (`labelUnconfirmed`) and immature (`labelImmature`)
|
||||
balance labels now render in **olive green** (`#A8B847`) instead of the
|
||||
same light green as confirmed balances. The distinction reads as
|
||||
"incoming but not yet confirmed" instead of "incoming and final".
|
||||
- `doc/release-process.md`: corrected signing-key identity to match the
|
||||
actual key in use (RSA-4096 `Krystie Triangles Release <krystie-triangles-release@dns2.sami.tailnet>`,
|
||||
not the Ed25519 `sami@cryptographic-triangles.org` the doc previously claimed).
|
||||
|
||||
### Fixed
|
||||
- Wallet close-hang on Windows: detached `std::thread` instances backing the
|
||||
embedded Tor and I2P controllers now join cleanly on shutdown, removing
|
||||
the ~30s exit delay. (`#20`)
|
||||
- Consensus: live proof-of-stake checks run during stale-tip IBD instead of
|
||||
being suppressed, fixing a divergence path where a node could accept a
|
||||
stale chain tip while local PoS validity checks were off. (`#18`)
|
||||
- CI: `simd.c:265` UBSan build-id drift resolved; reproducible-build
|
||||
warnings now ignore untracked files. (`#17`)
|
||||
|
||||
### Security
|
||||
- Audit follow-ups merged: kernel coverage, keystore coverage, sigcache
|
||||
fixes, wallet-DB test fixes. (`#14`, `#15`)
|
||||
|
||||
## [6.1.4] - 2026-07-04
|
||||
|
||||
### Fixed
|
||||
- CI: Tor bundle download resilience.
|
||||
- `NeedsBootstrap` flag now correctly persists across `rocksdb/` restarts.
|
||||
|
||||
## [6.1.3] - 2026-07-01
|
||||
|
||||
### Changed
|
||||
- Chain-DB migration hardening.
|
||||
- BIP39 passphrase support.
|
||||
- HD-wallet indicator in the UI.
|
||||
- Test isolation improvements.
|
||||
|
||||
## [6.1.2] - 2026-06-30 [YANKED]
|
||||
|
||||
Hotfix for v3 snapshot seek-offset corruption. Superseded by 6.1.3.
|
||||
Do not use.
|
||||
|
||||
## [6.1.1] - 2026-06-22
|
||||
|
||||
### Fixed
|
||||
- Minor wallet bugs.
|
||||
|
||||
## [6.1.0] - 2026-06-15
|
||||
|
||||
### Added
|
||||
- Initial 6.x release line. C++20 modernization, embedded Tor/I2P support.
|
||||
|
||||
[6.1.7]: https://github.com/SamiAhmed7777/triangles_v5/compare/v6.1.6...v6.1.7
|
||||
[6.1.6]: https://github.com/SamiAhmed7777/triangles_v5/compare/v6.1.5...v6.1.6
|
||||
[6.1.5]: https://github.com/SamiAhmed7777/triangles_v5/compare/v6.1.4...v6.1.5
|
||||
[6.1.4]: https://github.com/SamiAhmed7777/triangles_v5/compare/v6.1.3...v6.1.4
|
||||
[6.1.3]: https://github.com/SamiAhmed7777/triangles_v5/compare/v6.1.2...v6.1.3
|
||||
[6.1.2]: https://github.com/SamiAhmed7777/triangles_v5/compare/v6.1.1...v6.1.2
|
||||
[6.1.1]: https://github.com/SamiAhmed7777/triangles_v5/compare/v6.1.0...v6.1.1
|
||||
[6.1.0]: https://github.com/SamiAhmed7777/triangles_v5/releases/tag/v6.1.0
|
||||
+54
-5
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
|
||||
endif()
|
||||
|
||||
project(Triangles
|
||||
VERSION 6.0.0
|
||||
VERSION 6.2.5
|
||||
DESCRIPTION "Cryptographic Triangles Wallet"
|
||||
LANGUAGES C CXX
|
||||
)
|
||||
@@ -37,6 +37,41 @@ if(ENABLE_UNITY_BUILD)
|
||||
set(CMAKE_UNITY_BUILD_BATCH_SIZE 8)
|
||||
endif()
|
||||
|
||||
# ── Reproducible-build support ─────────────────────────────────────────────
|
||||
# REPRODUCIBLE_BUILD=ON strips absolute source paths from the final binary
|
||||
# via -ffile-prefix-map. Two builds of the same commit with the same
|
||||
# toolchain then produce byte-identical binaries (modulo any source paths
|
||||
# that aren't routed through the macro — see scripts/verify-reproducible-build.sh
|
||||
# for the full verification protocol).
|
||||
#
|
||||
# Default ON: this is a security property we want by default. Disable if
|
||||
# you need stack traces with absolute paths (e.g. debugging a post-mortem).
|
||||
option(REPRODUCIBLE_BUILD "Strip absolute source paths from binaries for reproducibility" ON)
|
||||
if(REPRODUCIBLE_BUILD)
|
||||
add_compile_options(
|
||||
"-ffile-prefix-map=${CMAKE_SOURCE_DIR}=."
|
||||
"-ffile-prefix-map=${CMAKE_BINARY_DIR}=."
|
||||
)
|
||||
# SOURCE_DATE_EPOCH is the canonical reproducible-build env var
|
||||
# (https://reproducible-builds.org/docs/source-date-epoch/). If the
|
||||
# user hasn't set it explicitly, fall back to the commit timestamp from
|
||||
# git. This means binaries built without SOURCE_DATE_EPOCH still embed
|
||||
# a deterministic timestamp (the commit time, not wall-clock).
|
||||
if(NOT DEFINED ENV{SOURCE_DATE_EPOCH})
|
||||
execute_process(
|
||||
COMMAND git log -n 1 --format=%ct
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
OUTPUT_VARIABLE SOURCE_DATE_EPOCH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
)
|
||||
if(NOT SOURCE_DATE_EPOCH)
|
||||
set(SOURCE_DATE_EPOCH "1700000000") # 2023-11-14 fallback
|
||||
endif()
|
||||
endif()
|
||||
message(STATUS "Reproducible build: ON (SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH})")
|
||||
endif()
|
||||
|
||||
# ── Output directories ──
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||
@@ -49,10 +84,10 @@ option(BUILD_QT "Build triangles-qt (Qt5 GUI wallet)" ON)
|
||||
option(BUILD_DAEMON "Build trianglesd (headless daemon)" ON)
|
||||
option(BUILD_CLI "Build triangles-cli (JSON-RPC client)" ON)
|
||||
option(BUILD_TESTS "Build test_triangles (Boost.Test unit tests)" ON)
|
||||
option(USE_UPNP "Enable UPnP support via miniupnpc" ON)
|
||||
option(USE_UPNP "Enable UPnP support via miniupnpc" OFF)
|
||||
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_DBUS "Enable D-Bus notifications (Linux only)" OFF)
|
||||
option(USE_ZMQ "Enable ZMQ publisher support" 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
|
||||
@@ -69,7 +104,7 @@ if(DEFINED USE_TOR_EMBEDDED AND NOT USE_TOR_EMBEDDED)
|
||||
"instead.")
|
||||
endif()
|
||||
option(USE_O3 "Use -O3 optimization instead of -O2" OFF)
|
||||
option(ENABLE_PIE "Build position-independent executables" OFF)
|
||||
option(ENABLE_PIE "Build position-independent executables" ON)
|
||||
option(ENABLE_STATIC "Prefer static linking (Linux release builds)" OFF)
|
||||
|
||||
# Embedded I2P (i2pd) — runs an I2P router in-process alongside Tor.
|
||||
@@ -154,6 +189,9 @@ 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+ —
|
||||
@@ -248,7 +286,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)
|
||||
@@ -267,6 +305,17 @@ include(BuildLevelDB)
|
||||
# ── Generate build.h from git describe ──
|
||||
include(GenerateBuildInfo)
|
||||
|
||||
# ── Enable CTest at the TOP level ──
|
||||
# add_test() is called in src/CMakeLists.txt, but without enable_testing()
|
||||
# here the top-level build/CTestTestfile.cmake is never generated, so
|
||||
# `ctest` run from the build root discovers ZERO tests. CI does exactly
|
||||
# `cd build && ctest`, which means the unit suites were silently not run.
|
||||
# Calling enable_testing() at the root generates the top-level test file
|
||||
# that recurses into src/ and registers all four test executables.
|
||||
if(BUILD_TESTS)
|
||||
enable_testing()
|
||||
endif()
|
||||
|
||||
# ── Descend into source tree ──
|
||||
add_subdirectory(src)
|
||||
|
||||
|
||||
+73
-28
@@ -1,38 +1,83 @@
|
||||
FROM ubuntu:22.04
|
||||
FROM ubuntu:24.04 AS builder
|
||||
|
||||
LABEL maintainer="Cryptographic Triangles Team"
|
||||
LABEL description="Cryptographic Triangles (TRI) headless daemon"
|
||||
LABEL version="5.9.24"
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG SOURCE_DATE_EPOCH=1700000000
|
||||
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
autoconf \
|
||||
automake \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
libssl3 \
|
||||
libdb5.3++ \
|
||||
libboost-system1.74.0 \
|
||||
libboost-filesystem1.74.0 \
|
||||
libboost-program-options1.74.0 \
|
||||
libboost-thread1.74.0 \
|
||||
libboost-chrono1.74.0 \
|
||||
libevent-2.1-7 \
|
||||
libminiupnpc17 \
|
||||
tor \
|
||||
cmake \
|
||||
libboost-all-dev \
|
||||
libdb++-dev \
|
||||
libevent-dev \
|
||||
libleveldb-dev \
|
||||
liblz4-dev \
|
||||
liblzma-dev \
|
||||
libminiupnpc-dev \
|
||||
librocksdb-dev \
|
||||
libsnappy-dev \
|
||||
libsqlite3-dev \
|
||||
libssl-dev \
|
||||
libtool \
|
||||
libzstd-dev \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
zlib1g-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG VERSION=5.7.6
|
||||
RUN curl -L -o /usr/local/bin/trianglesd \
|
||||
https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/Cryptographic-Triangles-v${VERSION}-linux-x64-daemon \
|
||||
&& chmod +x /usr/local/bin/trianglesd
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
RUN test -s src/secp256k1/CMakeLists.txt \
|
||||
&& test -s src/tor/tor-src/configure.ac
|
||||
|
||||
RUN LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \
|
||||
bash src/tor/build-libtor.sh
|
||||
|
||||
RUN cmake -S . -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_QT=OFF \
|
||||
-DBUILD_DAEMON=ON \
|
||||
-DBUILD_CLI=ON \
|
||||
-DBUILD_TESTS=OFF \
|
||||
-DUSE_UPNP=OFF \
|
||||
-DUSE_I2P_EMBEDDED=OFF \
|
||||
&& cmake --build build --parallel 2
|
||||
|
||||
RUN install -D -m 0755 build/bin/trianglesd /opt/triangles/bin/trianglesd \
|
||||
&& install -D -m 0755 build/bin/triangles-cli /opt/triangles/bin/triangles-cli \
|
||||
&& mkdir -p /opt/triangles/rootfs \
|
||||
&& { ldd /opt/triangles/bin/trianglesd; ldd /opt/triangles/bin/triangles-cli; } \
|
||||
| awk '/=> \// {print $3} /^\// {print $1}' \
|
||||
| sort -u \
|
||||
| while IFS= read -r library; do \
|
||||
cp --parents -L "${library}" /opt/triangles/rootfs; \
|
||||
done
|
||||
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd --gid 10001 triangles \
|
||||
&& useradd --uid 10001 --gid triangles --home-dir /var/lib/triangles \
|
||||
--no-create-home --shell /usr/sbin/nologin triangles \
|
||||
&& install -d -m 0700 -o triangles -g triangles /var/lib/triangles
|
||||
|
||||
COPY --from=builder /opt/triangles/rootfs/ /
|
||||
COPY --from=builder /opt/triangles/bin/ /usr/local/bin/
|
||||
RUN ldconfig
|
||||
|
||||
RUN useradd -m -s /bin/bash triangles
|
||||
USER triangles
|
||||
WORKDIR /home/triangles
|
||||
WORKDIR /var/lib/triangles
|
||||
|
||||
RUN mkdir -p .triangles
|
||||
EXPOSE 24112
|
||||
VOLUME ["/var/lib/triangles"]
|
||||
STOPSIGNAL SIGTERM
|
||||
|
||||
EXPOSE 24112 19112
|
||||
|
||||
VOLUME ["/home/triangles/.triangles"]
|
||||
|
||||
ENTRYPOINT ["trianglesd"]
|
||||
CMD ["-printtoconsole", "-txindex=1"]
|
||||
ENTRYPOINT ["/usr/local/bin/trianglesd"]
|
||||
CMD ["-datadir=/var/lib/triangles", "-printtoconsole", "-upnp=0", "-rest=0", "-rpcbind=127.0.0.1"]
|
||||
|
||||
@@ -1,250 +1,314 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
### Trusted Snapshot Publisher (UTXO Snapshots)
|
||||
|
||||
The daemon verifies that any UTXO snapshot it loads was signed by a
|
||||
**trusted publisher**. Starting with v6.1.8, the trusted publisher can
|
||||
be rotated at runtime via RPC — no rebuild required. The compiled-in
|
||||
fallback (`TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX`, Sami's legacy key)
|
||||
remains in effect if no runtime override is set.
|
||||
|
||||
```bash
|
||||
# Rotate to a new publisher
|
||||
trianglesd settrustedv2snapshotpublisher TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH
|
||||
|
||||
# Check current publisher
|
||||
trianglesd gettrustedv2snapshotpublisher
|
||||
|
||||
# Revert to the compiled-in fallback
|
||||
trianglesd unsettrustedv2snapshotpublisher
|
||||
```
|
||||
|
||||
The model is single-slot: calling `settrustedv2snapshotpublisher`
|
||||
atomically drops the previous publisher. See `docs/snapshot-publisher.md`
|
||||
for the full operator guide.
|
||||
|
||||
### 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
|
||||
|
||||
The JSON-RPC CLI is `triangles-cli`. For full flag reference, custom
|
||||
data-dir setups, and the full list of operations, see
|
||||
**[doc/triangles-cli.md](doc/triangles-cli.md)**. Quick start:
|
||||
|
||||
```bash
|
||||
# Default datadir (Linux: ~/.cryptographic-triangles)
|
||||
triangles-cli getinfo
|
||||
|
||||
# Custom datadir — most production nodes need this
|
||||
triangles-cli -datadir=/var/lib/triangles getinfo
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
### Trusted Snapshot Publisher (v6.1.8+)
|
||||
- `settrustedv2snapshotpublisher <address>` - Atomically replace the trusted snapshot publisher (previous one dropped immediately). Persists to `<datadir>/snapshot-publisher.json`.
|
||||
- `gettrustedv2snapshotpublisher` - Returns the currently active runtime publisher and whether a runtime override is in effect.
|
||||
- `unsettrustedv2snapshotpublisher` - Clear the runtime override and revert to the compiled-in fallback list.
|
||||
|
||||
See `docs/snapshot-publisher.md` for the full operator guide.
|
||||
|
||||
## 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.
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
# Security Policy
|
||||
|
||||
Triangles is wallet software and should be treated as security-sensitive. Do
|
||||
not use an experimental build to custody funds that you cannot afford to lose.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please report suspected vulnerabilities through a private GitHub security
|
||||
advisory for this repository. Do not include secrets, wallet files, seed
|
||||
phrases, private keys, or live RPC credentials in an issue, pull request, log,
|
||||
or test fixture.
|
||||
|
||||
Include the affected commit, platform, reproduction steps, impact, and a
|
||||
minimal proof of concept when possible. Public disclosure should wait until a
|
||||
fix is available and users have had a reasonable upgrade window.
|
||||
|
||||
## Deployment boundary
|
||||
|
||||
The JSON-RPC protocol uses HTTP Basic authentication and does not provide TLS.
|
||||
Keep it on loopback or a private Unix host boundary. Never expose the RPC port
|
||||
directly to the internet.
|
||||
|
||||
For application integrations:
|
||||
|
||||
- Run `trianglesd` as a dedicated, unprivileged operating-system user.
|
||||
- Bind RPC explicitly to loopback with `rpcbind=127.0.0.1`.
|
||||
- Use a unique random RPC username and password stored in a mode `0600` file.
|
||||
- Set `rpcallowip=127.0.0.1` and an exact `rpcallowmethod` list.
|
||||
- Keep `rest=0`, `upnp=0`, and wallet RPC methods disabled unless required.
|
||||
- Do not pass RPC passwords on a process command line.
|
||||
- Separate the node wallet and files from the integrating application's user.
|
||||
- Start new integrations with an empty wallet and no production funds.
|
||||
|
||||
The container image runs as UID/GID `10001` and intentionally does not create
|
||||
or print RPC credentials. Mount a private `/var/lib/triangles` volume containing
|
||||
an owner-only `triangles.conf`; startup without valid RPC credentials fails with
|
||||
a nonzero exit status. Do not provide wallet or RPC secrets through Docker
|
||||
command arguments or environment variables.
|
||||
|
||||
Set `listen=0` when inbound P2P is unnecessary. When inbound peers are needed,
|
||||
use `bind=<address>` and publish only the P2P port. The RPC port must remain
|
||||
unpublished and loopback-bound.
|
||||
|
||||
Remote snapshot bootstrap is opt-in. A snapshot is accepted only when its file
|
||||
hash and checkpoint are compiled into the client. Treat changes to snapshot
|
||||
hashes, checkpoints, seed hosts, release keys, submodule revisions, and CI
|
||||
workflows as security-critical review items.
|
||||
|
||||
## Wallet handling
|
||||
|
||||
- Encrypt wallets before funding them.
|
||||
- Record the HD mnemonic offline and test recovery on an isolated machine.
|
||||
- Keep multiple offline backups; filesystem permissions are not a backup.
|
||||
- Encrypting the live wallet does not retroactively encrypt old copies,
|
||||
migration backups, snapshots, or filesystem remnants. Inventory and protect
|
||||
every pre-encryption copy as if it contains plaintext private keys.
|
||||
- Never share a seed phrase with support personnel or paste it into an RPC call.
|
||||
- Stop the node and investigate any wallet database integrity error rather than
|
||||
attempting to continue with a partially loaded wallet.
|
||||
|
||||
## Build trust
|
||||
|
||||
Build from a reviewed commit, initialize submodules at the recorded revisions,
|
||||
and verify release signatures against a key fingerprint obtained through an
|
||||
independent trusted channel. A valid signature proves key possession, not the
|
||||
identity of the key owner.
|
||||
@@ -31,6 +31,9 @@ Triangles is a Tor-only PoS cryptocurrency. PoW ended at block 9000; from block
|
||||
| `getrawmempool` | | Returns all transaction IDs currently in the mempool. |
|
||||
| `getcheckpoint` | | Returns info about the current synchronized checkpoint. |
|
||||
| `getchaintips` | | Returns info about all known chain tips (forks). |
|
||||
| `settrustedv2snapshotpublisher` | `<address>` | Atomically replaces the trusted snapshot publisher. The previous publisher is dropped immediately (no grace period). The new publisher is persisted to `<datadir>/snapshot-publisher.json`. Returns `{ previous, current }`. See `docs/snapshot-publisher.md`. |
|
||||
| `gettrustedv2snapshotpublisher` | | Returns the currently active trusted snapshot publisher and whether a runtime override is in effect. Returns `{ active, has_runtime_override }`. |
|
||||
| `unsettrustedv2snapshotpublisher` | | Clears the runtime trusted snapshot publisher override. Reverts to the built-in fallback list (compiled in). Removes `<datadir>/snapshot-publisher.json`. |
|
||||
| `invalidateblock` | `<hash>` | Permanently marks a block as invalid and rewinds the chain past it. |
|
||||
| `reconsiderblock` | `<hash>` | Removes the invalid mark from a previously invalidated block. |
|
||||
| `recalculatesupply` | | Recalculates money supply by summing all UTXOs. Updates the stored value at the chain tip and persists to disk. Returns old/new supply and difference. |
|
||||
|
||||
@@ -107,6 +107,13 @@
|
||||
|
||||
## P2 — Polish & Optimization
|
||||
|
||||
### T024: PoS reward exact-proportionality rework — REJECTED
|
||||
- **Status**: REJECTED
|
||||
- **Depends**: none
|
||||
- **Description**: Audit review of 2a4da33 (PoS reward rework, reverted by 05b5606) and 239cf61 (sigcache fix, reverted by 36d5f29) on 2026-07-07 concluded the PoS reward rework must stay reverted. Reasons: (1) consensus split risk — round-half-up pays 1 unit more than truncation for ~half of all inputs, so a block claiming that unit is valid to upgraded nodes and rejected by un-upgraded nodes; (2) motivation gone — the only driver was a unit-test assertion of exact proportionality (a78a420 already relaxed it to ±1 truncation), which is aesthetic, not correctness; (3) the new formula is worse than advertised — pre-truncating coin-age to whole-COIN units *before* multiplying drops fractional coin-age that the old formula credited, and `nWholeCoinAge * RATE * 2` is int64_t and can overflow. If exact proportionality is ever truly wanted, it must ship as a height-gated hard fork (both formulas in code, switch at activation height, coordinated node upgrade). Not worth it for cosmetic rounding. The sigcache fix from the same review (239cf61) was approved and re-landed in PR #21 / branch `fix/sigcache-false-positives` as a 6.1.6 candidate.
|
||||
- **Files**: `src/main.cpp` (GetProofOfStakeReward)
|
||||
- **Acceptance**: none — task is to leave the code as-is and not reopen
|
||||
|
||||
### T020: Remove unused Gemini/Google references from codebase
|
||||
- **Status**: TODO
|
||||
- **Depends**: none
|
||||
|
||||
@@ -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.
|
||||
@@ -7,6 +7,15 @@ add_compile_options(
|
||||
-Wformat -Wformat-security -Wno-unused-parameter
|
||||
)
|
||||
|
||||
# Bitcoin-derived source uses C99-style adjacent string-literal concatenation
|
||||
# for printf format macros: `"%"PRId64`. gcc tolerates this without a space;
|
||||
# clang promotes `-Wreserved-user-defined-literal` to an error in C++20 mode
|
||||
# and trips on hundreds of sites in util.cpp, kernel.cpp, etc. Suppress only
|
||||
# under clang so gcc builds keep the original diagnostic behavior.
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
add_compile_options(-Wno-reserved-user-defined-literal)
|
||||
endif()
|
||||
|
||||
# ── Common defines ──
|
||||
add_compile_definitions(
|
||||
BOOST_SPIRIT_THREADSAFE
|
||||
@@ -47,6 +56,54 @@ 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)
|
||||
# Belt-and-suspenders: explicitly disable AVX-512 / AVX10 / SVE
|
||||
# family ISAs that GCC 11+ can otherwise autovectorize into via
|
||||
# inlined libstdc++ std::string / std::copy / memcpy paths even when
|
||||
# -march=x86-64-v2 is set. Discovered 2026-08-01: v6.1.9 binary built
|
||||
# on EPYC 7763 (AVX-512) contained 741 vpbroadcastq EVEX instructions
|
||||
# which crash with SIGILL on every production node (KVM EPYC,
|
||||
# Ryzen 3600, ARM64) that lacks AVX-512. -mno-avx512f alone is
|
||||
# enough to suppress the SIGILL; the -mno-*avx10/sve* siblings
|
||||
# future-proof against the next GCC version autovectorizing
|
||||
# beyond AVX-512. See references/avx-512-sigill-build-fix.md
|
||||
# for the full diagnosis recipe.
|
||||
# NB: -mno-avx512*4fmaps / -mno-avx512*4vnniw use NO dash between
|
||||
# 'avx512' and the sub-feature (correct: -mno-avx5124fmaps). The
|
||||
# -mno-avx512-4fmaps form (with a dash) is rejected by GCC and
|
||||
# makes the whole build fail with "unrecognized command-line option".
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||
add_compile_options(
|
||||
-mno-avx512f -mno-avx512pf -mno-avx512er -mno-avx512cd
|
||||
-mno-avx512vl -mno-avx512bw -mno-avx512dq -mno-avx512ifma
|
||||
-mno-avx512vbmi -mno-avx512vbmi2 -mno-avx512vnni
|
||||
-mno-avx512bitalg -mno-avx512vpopcntdq
|
||||
-mno-avx5124fmaps -mno-avx5124vnniw -mno-avx512vp2intersect
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ── Platform: Windows (MSYS2 MinGW64) ──
|
||||
if(WIN32)
|
||||
add_compile_options(-Wa,-mbig-obj)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# CMake toolchain for cross-compiling to aarch64 (Pi 3/4/5)
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR aarch64)
|
||||
|
||||
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
|
||||
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
|
||||
|
||||
# Also search the multiarch lib path
|
||||
set(CMAKE_LIBRARY_PATH /usr/lib/aarch64-linux-gnu)
|
||||
set(CMAKE_INCLUDE_PATH /usr/include)
|
||||
@@ -0,0 +1,15 @@
|
||||
# CMake toolchain for cross-compiling to armhf (Pi Zero/1/2/3 in 32-bit mode)
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR arm)
|
||||
|
||||
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
|
||||
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
|
||||
|
||||
set(CMAKE_LIBRARY_PATH /usr/lib/arm-linux-gnueabihf)
|
||||
set(CMAKE_INCLUDE_PATH /usr/include)
|
||||
@@ -31,10 +31,6 @@ RequestExecutionLevel user
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
|
||||
; Bootstrap page
|
||||
Page custom BootstrapPage
|
||||
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
@@ -43,34 +39,6 @@ Page custom BootstrapPage
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
; Bootstrap selection variable
|
||||
Var BootstrapChoice
|
||||
|
||||
; Bootstrap page function
|
||||
Function BootstrapPage
|
||||
!insertmacro MUI_HEADER_TEXT "Blockchain Sync" "Choose how to synchronize the blockchain"
|
||||
|
||||
nsDialogs::Create 1018
|
||||
Pop $0
|
||||
|
||||
${NSD_CreateLabel} 0 10u 100% 20u "The Triangles blockchain requires ~1GB of data. Choose sync method:"
|
||||
Pop $0
|
||||
|
||||
${NSD_CreateRadioButton} 10u 40u 100% 12u "Download bootstrap (~1.3GB) — Recommended (fast)"
|
||||
Pop $1
|
||||
${NSD_Check} $1
|
||||
|
||||
${NSD_CreateRadioButton} 10u 60u 100% 12u "Sync from network — Slow (may take days)"
|
||||
Pop $2
|
||||
|
||||
${NSD_CreateLabel} 10u 80u 100% 30u "Bootstrap will download a recent blockchain snapshot, saving hours or days of sync time. Network bandwidth required: ~1.3GB."
|
||||
Pop $0
|
||||
|
||||
nsDialogs::Show
|
||||
|
||||
${NSD_GetState} $1 $BootstrapChoice
|
||||
FunctionEnd
|
||||
|
||||
Section "Install"
|
||||
SetOutPath "$INSTDIR"
|
||||
|
||||
@@ -84,25 +52,6 @@ Section "Install"
|
||||
; Create data directory
|
||||
CreateDirectory "$APPDATA\Triangles"
|
||||
|
||||
; Download blockchain bootstrap if selected
|
||||
${If} $BootstrapChoice == ${BST_CHECKED}
|
||||
DetailPrint "Downloading blockchain bootstrap..."
|
||||
inetc::get /CAPTION "Downloading Blockchain" /CANCELTEXT "Skip" \
|
||||
"http://bootstrap.cryptographic-triangles.org/tri-blockchain.tar.gz" \
|
||||
"$TEMP\tri-blockchain.tar.gz" /END
|
||||
Pop $0
|
||||
${If} $0 == "OK"
|
||||
DetailPrint "Extracting blockchain..."
|
||||
nsExec::ExecToLog '"$INSTDIR\7z.exe" x "$TEMP\tri-blockchain.tar.gz" -o"$TEMP" -y'
|
||||
nsExec::ExecToLog '"$INSTDIR\7z.exe" x "$TEMP\tri-blockchain.tar" -o"$APPDATA\Triangles" -y'
|
||||
Delete "$TEMP\tri-blockchain.tar.gz"
|
||||
Delete "$TEMP\tri-blockchain.tar"
|
||||
DetailPrint "Blockchain bootstrap installed!"
|
||||
${Else}
|
||||
DetailPrint "Bootstrap download failed or skipped — will sync from network"
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
; Uninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Triangles Documentation
|
||||
|
||||
Cryptographic Triangles (TRI) is a privacy-focused proof-of-stake
|
||||
cryptocurrency derived from Bitcoin, with Tor v3 hidden services
|
||||
mandatory and a 120-second block time. This directory holds
|
||||
operator- and developer-facing documentation.
|
||||
|
||||
## Operator docs
|
||||
|
||||
- **[triangles-cli.md](triangles-cli.md)** — operating the JSON-RPC
|
||||
CLI against one or more daemon instances, including custom
|
||||
data-dir setups, common operations, and the full flag reference.
|
||||
- **[release-process.md](release-process.md)** — how a release is
|
||||
cut, signed, and published.
|
||||
|
||||
## Developer docs
|
||||
|
||||
- **[build-unix.txt](build-unix.txt)** — building on Linux.
|
||||
- **[build-osx.txt](build-osx.txt)** — building on macOS.
|
||||
- **[build-msw.txt](build-msw.txt)** — building on Windows.
|
||||
- **[coding.txt](coding.txt)** — coding style and conventions.
|
||||
- **[translation_process.md](translation_process.md)** — how
|
||||
translations are managed.
|
||||
- **[embedded-tor-rebase.md](embedded-tor-rebase.md)** — bumping
|
||||
the embedded Tor submodule.
|
||||
- **[i2p.md](i2p.md)** — I2P integration notes.
|
||||
|
||||
## Misc
|
||||
|
||||
- **[README_windows.txt](README_windows.txt)** — Windows README
|
||||
(legacy, predates the markdown docs).
|
||||
- **[assets-attribution.txt](assets-attribution.txt)** — third-party
|
||||
asset attributions.
|
||||
- **[Doxyfile](Doxyfile)** — Doxygen configuration for source
|
||||
documentation.
|
||||
+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.
|
||||
@@ -0,0 +1,244 @@
|
||||
# Triangles Release Process
|
||||
|
||||
> Canonical release pipeline for `SamiAhmed7777/triangles_v5`. This document
|
||||
> is the source of truth for *how* a release is cut. The implementation lives
|
||||
> in `scripts/verify-reproducible-build.sh` and `scripts/sign-release.sh`.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Reproducible** — any two builders with the same source tree, same
|
||||
toolchain, and same flags produce byte-identical binaries.
|
||||
2. **Signed** — every release artifact has a detached PGP signature that
|
||||
verifiers can check against a known public key.
|
||||
3. **Verifiable end-to-end** — a third party can confirm a release is
|
||||
legitimate using only `gpg` and `sha256sum`, both installed by default
|
||||
on every Linux distribution.
|
||||
|
||||
## Pipeline overview
|
||||
|
||||
```
|
||||
source tag (e.g. v6.1.4)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ CI builds all 4 │ .github/workflows/build-all.yml
|
||||
│ targets on each │ (ubuntu / windows / macos)
|
||||
│ platform │
|
||||
└──────────┬───────────┘
|
||||
│ produces: daemon.tar.gz, qt.tar.gz, .deb, .dmg, .exe, ...
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Local maintainer │ scripts/sign-release.sh <release-dir>
|
||||
│ signs artifacts │ (uses release signing key in local keyring)
|
||||
└──────────┬───────────┘
|
||||
│ produces: SHA256SUMS, *.asc detached signatures
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Push to GitHub │ .github/workflows/distribute.yml
|
||||
│ release + Docker │ (uploads artifacts, builds Docker image,
|
||||
│ + Homebrew tap + │ updates Homebrew formula, submits
|
||||
│ WinGet + Snap │ WinGet + Snap PRs)
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Verifier │ scripts/sign-release.sh --verify <dir>
|
||||
│ independently │ + gpg --import <release-pubkey>
|
||||
│ confirms │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
## Reproducibility — how it works today
|
||||
|
||||
The Triangles build is already reproducible for Release builds with the
|
||||
following properties:
|
||||
|
||||
| Property | Implementation |
|
||||
|---|---|
|
||||
| `BUILD_DESC` | Git describe output, written to `build.h` at build time |
|
||||
| `BUILD_DATE` | **Commit timestamp** (NOT wall-clock), from `git log -n 1 --format=%ci` |
|
||||
| `__DATE__`/`__TIME__` fallback | Dead code in practice — `build.h` always defines `BUILD_DATE` |
|
||||
| Build paths in binaries | Mapped with `-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.` so absolute source paths do not leak into debug info |
|
||||
|
||||
### Verifying reproducibility
|
||||
|
||||
Run on a clean checkout:
|
||||
|
||||
```bash
|
||||
scripts/verify-reproducible-build.sh
|
||||
```
|
||||
|
||||
This builds `trianglesd` twice into two separate build directories and
|
||||
compares SHA256 hashes. Exits 0 on success.
|
||||
|
||||
Options:
|
||||
- `BUILD_TYPE=Debug scripts/verify-reproducible-build.sh`
|
||||
- `TARGET=triangles-qt scripts/verify-reproducible-build.sh`
|
||||
- `BUILD_DIR_A=/tmp/A BUILD_DIR_B=/tmp/B scripts/verify-reproducible-build.sh`
|
||||
|
||||
## Signing — how it works
|
||||
|
||||
### Generate (or import) a release signing key
|
||||
|
||||
**One-time setup** (the maintainer's machine):
|
||||
|
||||
```bash
|
||||
# The release-signing key currently in use is:
|
||||
#
|
||||
# uid: Krystie Triangles Release <krystie-triangles-release@dns2.sami.tailnet>
|
||||
# fp: 523A 8183 3EB7 2015 73E1 EFE1 DCF2 5799 6810 7984
|
||||
# sub: 6913 E136 10F6 9818 3429 CE20 C2DC 6061 8C85 A159
|
||||
# algo: RSA-4096, created 2026-04-29, expires 2028-04-28
|
||||
#
|
||||
# This is an unattended signing key used by the release CI to sign
|
||||
# release artifacts (daemon.tar.gz, qt.tar.gz, .deb, .dmg, .exe, .AppImage)
|
||||
# without a human in the loop. It is stored as a GitHub Actions secret.
|
||||
#
|
||||
# Git tags are signed by the maintainer's personal key
|
||||
# (uid `Sami <hello@sami-ahmed.net>`, fp `53AA 858E F0DD D528 EC2C 2ABD
|
||||
# 0BF7 F887 2FE0 E859`) so the tag and the artifacts can be verified
|
||||
# independently.
|
||||
|
||||
# To print the public key for the current release-signing key:
|
||||
gpg --armor --export 0xDCF2579968107984 > release-pubkey.asc
|
||||
|
||||
# To export the maintainer's tag-signing secret key (for backup):
|
||||
gpg --export-secret-keys 0x0BF7F8872FE0E859 > release-seckey-BACKUP.asc
|
||||
chmod 600 release-seckey-BACKUP.asc
|
||||
```
|
||||
|
||||
**Import an existing key** (e.g. on a new maintainer machine):
|
||||
|
||||
```bash
|
||||
gpg --import release-seckey-BACKUP.asc
|
||||
```
|
||||
|
||||
### Sign a release directory
|
||||
|
||||
After CI has produced the artifacts in a known directory:
|
||||
|
||||
```bash
|
||||
scripts/sign-release.sh /path/to/release-dir
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Generate `SHA256SUMS` for every release artifact (.tar.gz, .deb, .dmg,
|
||||
.exe, .zip, .AppImage)
|
||||
2. Write a detached PGP signature (`<artifact>.asc`) for each artifact
|
||||
3. Write a detached PGP signature over `SHA256SUMS` itself
|
||||
4. Refuse to run if the signing key isn't in the local keyring (safety)
|
||||
|
||||
### Verify a release
|
||||
|
||||
A third party (user, exchange, package maintainer) verifies with:
|
||||
|
||||
```bash
|
||||
# 1. Import the public key (one-time).
|
||||
gpg --import release-pubkey.asc
|
||||
|
||||
# 2. Verify everything in the release directory.
|
||||
scripts/sign-release.sh --verify /path/to/release-dir
|
||||
```
|
||||
|
||||
This checks:
|
||||
- `SHA256SUMS.asc` against `SHA256SUMS` (the master signature)
|
||||
- Each `<artifact>.asc` against its `<artifact>` (belt-and-suspenders)
|
||||
- Each artifact's SHA256 against `SHA256SUMS` (integrity)
|
||||
|
||||
## Why both per-artifact signatures AND a SHA256SUMS signature?
|
||||
|
||||
- **SHA256SUMS + signature**: small, fast to verify, single point of trust.
|
||||
If the SHA256SUMS.asc checks out and a file's SHA256 matches an entry,
|
||||
you're done — you trust that entry.
|
||||
- **Per-artifact signatures**: defense against a hypothetical attack where
|
||||
someone modifies `SHA256SUMS` but not the artifacts (or vice versa).
|
||||
Two independent signature chains.
|
||||
|
||||
For most verifiers, checking `SHA256SUMS.asc` + `sha256sum -c SHA256SUMS`
|
||||
is sufficient. The per-artifact .asc files are insurance.
|
||||
|
||||
## CI integration
|
||||
|
||||
`.github/workflows/build-all.yml` already produces the artifacts. The
|
||||
remaining work (separate PR) is to add a "sign" job that runs
|
||||
`scripts/sign-release.sh` against the assembled release directory using a
|
||||
key stored as a GitHub Actions secret.
|
||||
|
||||
**Required secrets (one-time setup in repo Settings → Secrets):**
|
||||
- `GPG_PRIVATE_KEY` — base64-encoded `release-seckey-BACKUP.asc`
|
||||
(see [GitHub docs on encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets))
|
||||
- `GPG_PASSPHRASE` — passphrase for the signing key (if any)
|
||||
- `GITHUB_TOKEN` — already provided by Actions
|
||||
|
||||
**Suggested job sketch** (in `.github/workflows/build-all.yml` after all
|
||||
build jobs complete):
|
||||
|
||||
```yaml
|
||||
sign:
|
||||
name: Sign release artifacts
|
||||
needs: [build-linux-daemon, build-linux-qt, build-windows-daemon, build-windows-qt, build-macos]
|
||||
runs-on: ubuntu-22.04
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Import signing key
|
||||
run: |
|
||||
echo "${{ secrets.GPG_PRIVATE_KEY }}" | base64 -d | gpg --import
|
||||
|
||||
- name: Sign artifacts
|
||||
run: scripts/sign-release.sh release-artifacts/
|
||||
env:
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
```
|
||||
|
||||
## Public key distribution
|
||||
|
||||
The release public key MUST be published in **at least three independent
|
||||
places** so a keyserver takedown or DNS hijack cannot prevent verification:
|
||||
|
||||
1. **This repository** — `release-pubkey.asc` at the repo root, committed
|
||||
on every release tag.
|
||||
2. **The website** — `https://cryptographic-triangles.org/release-pubkey.asc`
|
||||
3. **Public keyservers** — submit to `keys.openpgp.org`, `keyserver.ubuntu.com`,
|
||||
`pgp.mit.edu`. Each is independently operated.
|
||||
|
||||
Distribution list refreshed with every key rotation (rare; treat as
|
||||
multi-year commitment).
|
||||
|
||||
## Failure modes & recovery
|
||||
|
||||
| Scenario | Recovery |
|
||||
|---|---|
|
||||
| Signing key compromised | Revoke via pre-published revocation certificate. Re-cut release. Document incident. |
|
||||
| Signing key lost (no backup) | Cannot sign new releases. Existing artifacts still verify against the published public key. Treat as catastrophic; re-mint a new key and treat the chain as fork-vulnerable until community updates. |
|
||||
| Public key not yet distributed | User gets `gpg: Can't check signature: No public key`. Provide clear "first verify the key fingerprint out-of-band" instructions on the website. |
|
||||
| CI secret leaked | Rotate the signing key immediately; treat all artifacts signed with the old key as suspect. |
|
||||
| `SHA256SUMS` signed but artifacts don't match | `sha256sum -c` fails. Either an artifact was corrupted in transit, or someone tampered. Re-download from GitHub and re-verify. |
|
||||
|
||||
## Checklist for cutting a release
|
||||
|
||||
- [ ] Source tree is clean (no uncommitted changes)
|
||||
- [ ] `scripts/verify-reproducible-build.sh` passes (builds are reproducible)
|
||||
- [ ] All CI jobs on the release tag are green
|
||||
- [ ] Release artifacts are in a single directory (`release-artifacts/`)
|
||||
- [ ] `scripts/sign-release.sh release-artifacts/` runs without error
|
||||
- [ ] `scripts/sign-release.sh --verify release-artifacts/` passes
|
||||
- [ ] `release-pubkey.asc` is current and committed to the repo
|
||||
- [ ] GitHub release created with all artifacts + SHA256SUMS + SHA256SUMS.asc
|
||||
- [ ] `distribute.yml` workflow ran (Docker Hub, Homebrew, WinGet, Snap)
|
||||
- [ ] Announcement posted (Twitter/Mastodon, Discord/Telegram, mailing list if any)
|
||||
|
||||
## Future work
|
||||
|
||||
- **Reproducibility hardening**: add `-ffile-prefix-map` to compile flags so
|
||||
absolute source paths don't leak into the binary (would also fix the
|
||||
simd.c:265 UBSan build-id drift).
|
||||
- **Gitian-style deterministic builds**: containerized build environment
|
||||
pinned to a specific GCC/binutils version, so multiple independent
|
||||
verifiers can rebuild from source and get identical hashes.
|
||||
- **Transparency log**: publish each release artifact hash to a Sigstore /
|
||||
sigsum / Certificate Transparency-style log so any tampering is publicly
|
||||
auditable.
|
||||
- **Key rotation policy**: document how/when the signing key gets rotated
|
||||
(probably never, but state the policy).
|
||||
@@ -0,0 +1,301 @@
|
||||
# Triangles CLI Operations
|
||||
|
||||
> Operator-facing guide for `triangles-cli`, the JSON-RPC client that ships
|
||||
> with the Triangles daemon. Companion to `contrib/triangles.conf.example`
|
||||
> (daemon config) and `scripts/tri/README.md` (friendly wrapper).
|
||||
|
||||
## What `triangles-cli` is
|
||||
|
||||
`triangles-cli` is a small standalone binary that talks JSON-RPC over TCP
|
||||
to a running `trianglesd` daemon. It is the canonical way to read chain
|
||||
state, manage the wallet, and trigger node actions from the shell.
|
||||
|
||||
It does **not** start, stop, or manage the daemon. It just talks to one
|
||||
that is already running.
|
||||
|
||||
The binary lives in the same directory as `trianglesd` after build:
|
||||
|
||||
| Platform | Default install path |
|
||||
|---|---|
|
||||
| Linux (Debian package) | `/usr/lib/cryptographic-triangles/triangles-cli` |
|
||||
| Linux (manual) | wherever you put it; this doc assumes `/usr/local/bin` |
|
||||
| macOS (Homebrew) | `/usr/local/bin/triangles-cli` |
|
||||
| Windows | `<install-dir>\triangles-cli.exe` |
|
||||
|
||||
## Connection parameters
|
||||
|
||||
`triangles-cli` needs four pieces of information to reach the daemon:
|
||||
|
||||
| Param | Default | Override flag |
|
||||
|---|---|---|
|
||||
| RPC host | `127.0.0.1` | `-rpcconnect=<ip>` |
|
||||
| RPC port | `19111` (mainnet) / `19112` (testnet) | `-rpcport=<port>` |
|
||||
| RPC user | *(none — required)* | `-rpcuser=<user>` |
|
||||
| RPC pass | *(none — required)* | `-rpcpassword=<pw>` |
|
||||
|
||||
**RPC user and password have no default.** The daemon refuses to start
|
||||
RPC unless `rpcuser` and `rpcpassword` are set in its `triangles.conf`.
|
||||
You must either set them in the conf, or pass them on the command line.
|
||||
|
||||
The conf is found in this order (highest precedence first):
|
||||
|
||||
1. **`-conf=<absolute-path>`** flag on the command line
|
||||
2. **`<datadir>/triangles.conf`** — datadir resolved from `-datadir`
|
||||
if given, otherwise from the default per-platform path (see below)
|
||||
3. **Hard-coded fallback** — `triangles.conf` in the current working
|
||||
directory (rarely useful; only fires if neither `-conf` nor `-datadir`
|
||||
is set and the cwd happens to contain the file)
|
||||
|
||||
## Default data directories
|
||||
|
||||
When `-datadir` is not passed, `triangles-cli` looks in:
|
||||
|
||||
| Platform | Path |
|
||||
|---|---|
|
||||
| Linux | `$HOME/.cryptographic-triangles` |
|
||||
| macOS | `$HOME/Library/Application Support/CryptographicTriangles` |
|
||||
| Windows | `%APPDATA%\CryptographicTriangles` |
|
||||
|
||||
The conf lookup in step 2 above resolves to
|
||||
`<default-datadir>/triangles.conf`. **If you keep your conf anywhere
|
||||
else — common for ops setups with custom data dirs — you must either
|
||||
pass `-conf` explicitly, or pass `-datadir` so the conf is found
|
||||
alongside it.**
|
||||
|
||||
## Operating a node with a non-default data directory
|
||||
|
||||
Most production nodes do **not** use the default datadir. The most
|
||||
common ops shapes are:
|
||||
|
||||
### Shape 1: Custom datadir, conf in the same directory
|
||||
|
||||
```bash
|
||||
# Daemon runs with:
|
||||
trianglesd -datadir=/var/lib/triangles -conf=/var/lib/triangles/triangles.conf
|
||||
|
||||
# CLI uses the same -datadir, and the conf is found automatically:
|
||||
triangles-cli -datadir=/var/lib/triangles getinfo
|
||||
```
|
||||
|
||||
`-conf` is omitted because `triangles-cli` infers
|
||||
`<datadir>/triangles.conf` when `-conf` is not given.
|
||||
|
||||
### Shape 2: Custom datadir, conf at an unrelated path
|
||||
|
||||
```bash
|
||||
# Conf lives somewhere else entirely (e.g. under /etc):
|
||||
triangles-cli -conf=/etc/triangles/triangles.conf -datadir=/var/lib/triangles getinfo
|
||||
```
|
||||
|
||||
When `-conf` is an **absolute path**, the `-datadir` flag is only used
|
||||
for resolving other relative paths (logs, pid file, etc.) — the conf
|
||||
itself is read from the absolute `-conf` path.
|
||||
|
||||
### Shape 3: Default datadir, override a single flag
|
||||
|
||||
```bash
|
||||
# Use the default datadir but connect to a daemon on a different port
|
||||
# (e.g. testnet daemon, or remote node via SSH tunnel):
|
||||
triangles-cli -rpcport=19112 -rpcuser=tripi -rpcpassword=secret getinfo
|
||||
```
|
||||
|
||||
### Shape 4: Multiple nodes on the same box (no flag conflicts)
|
||||
|
||||
```bash
|
||||
# Mainnet node, datadir /var/lib/triangles-mainnet
|
||||
triangles-cli -datadir=/var/lib/triangles-mainnet -rpcport=19111 getinfo
|
||||
|
||||
# Testnet node, datadir /var/lib/triangles-testnet
|
||||
triangles-cli -datadir=/var/lib/triangles-testnet -rpcport=19112 -testnet getinfo
|
||||
```
|
||||
|
||||
## Common operations
|
||||
|
||||
All examples assume `-datadir=/var/lib/triangles` for the production
|
||||
node. Drop the flag if your conf lives at the default path.
|
||||
|
||||
```bash
|
||||
# ── Chain state ──────────────────────────────────────────────
|
||||
triangles-cli -datadir=/var/lib/triangles getblockchaininfo
|
||||
triangles-cli -datadir=/var/lib/triangles getbestblockhash
|
||||
triangles-cli -datadir=/var/lib/triangles getblockcount
|
||||
triangles-cli -datadir=/var/lib/triangles getdifficulty
|
||||
triangles-cli -datadir=/var/lib/triangles getnetworkinfo
|
||||
triangles-cli -datadir=/var/lib/triangles getconnectioncount
|
||||
|
||||
# ── Wallet ───────────────────────────────────────────────────
|
||||
# List unspent outputs
|
||||
triangles-cli -datadir=/var/lib/triangles listunspent
|
||||
|
||||
# Balance
|
||||
triangles-cli -datadir=/var/lib/triangles getbalance
|
||||
triangles-cli -datadir=/var/lib/triangles getbalance "*" 6 # 6-confirmations
|
||||
|
||||
# Send
|
||||
triangles-cli -datadir=/var/lib/triangles sendtoaddress <addr> <amount> ["comment"]
|
||||
|
||||
# Backup wallet — ALWAYS back up before any operation that
|
||||
# mutates the wallet (sendtoaddress, importprivkey, keypoolrefill...)
|
||||
triangles-cli -datadir=/var/lib/triangles backupwallet /root/tri-wallet-$(date +%F).dat
|
||||
|
||||
# ── Staking ──────────────────────────────────────────────────
|
||||
triangles-cli -datadir=/var/lib/triangles getstakinginfo
|
||||
triangles-cli -datadir=/var/lib/triangles setstaking true|false
|
||||
|
||||
# ── Snapshots (if your node is a snapshot publisher) ─────────
|
||||
triangles-cli -datadir=/var/lib/triangles getsnapshotinfo
|
||||
```
|
||||
|
||||
For the full list of available RPC commands, run:
|
||||
|
||||
```bash
|
||||
triangles-cli -datadir=/var/lib/triangles help
|
||||
triangles-cli -datadir=/var/lib/triangles help <command> # help for one
|
||||
```
|
||||
|
||||
## Output formats
|
||||
|
||||
The default output is **pretty-printed JSON**. For piping into `jq`
|
||||
or other tools, add `-raw`:
|
||||
|
||||
```bash
|
||||
triangles-cli -datadir=/var/lib/triangles -raw getblockcount
|
||||
# 2418017
|
||||
|
||||
triangles-cli -datadir=/var/lib/triangles -raw getbestblockhash | head -c 64
|
||||
```
|
||||
|
||||
For a synthesized summary (version, balance, blocks, connections,
|
||||
stake weight) without having to chain multiple calls:
|
||||
|
||||
```bash
|
||||
triangles-cli -datadir=/var/lib/triangles -getinfo
|
||||
```
|
||||
|
||||
## The `tri` wrapper (recommended for humans)
|
||||
|
||||
`scripts/tri/` ships a friendly bash wrapper that takes care of
|
||||
`-datadir` / `-rpcuser` / `-rpcpassword` from a single config file.
|
||||
See `scripts/tri/README.md` for install + config. Once installed:
|
||||
|
||||
```bash
|
||||
tri getinfo
|
||||
tri getblockchaininfo
|
||||
tri sendtoaddress <addr> <amount>
|
||||
```
|
||||
|
||||
…with no need to remember flags. The wrapper reads
|
||||
`/etc/tri/nodes.conf` (or whatever you set `TRI_NODES_CONF` to).
|
||||
|
||||
## Reading JSON-RPC responses into shell variables
|
||||
|
||||
`triangles-cli` is one-shot — each invocation connects, sends one
|
||||
request, prints the result, exits. To grab a field:
|
||||
|
||||
```bash
|
||||
# Single field, no jq
|
||||
HEIGHT=$(triangles-cli -datadir=/var/lib/triangles -raw getblockcount)
|
||||
echo "Chain height: $HEIGHT"
|
||||
|
||||
# With jq for nested fields
|
||||
NETWORK=$(triangles-cli -datadir=/var/lib/triangles -raw getnetworkinfo \
|
||||
| jq -r .networkid)
|
||||
```
|
||||
|
||||
## Cross-host operation (SSH tunnel)
|
||||
|
||||
To run a CLI command against a node on a different host without
|
||||
exposing RPC publicly, tunnel the port over SSH first:
|
||||
|
||||
```bash
|
||||
# Local:19111 -> remote:19111 over SSH
|
||||
ssh -f -N -L 19111:127.0.0.1:19111 user@node.example.com
|
||||
|
||||
# Now talk to the remote daemon as if it were local:
|
||||
triangles-cli -rpcconnect=127.0.0.1 -rpcport=19111 \
|
||||
-rpcuser=<user> -rpcpassword=<pw> getinfo
|
||||
```
|
||||
|
||||
Or use the `tri` wrapper, which has a built-in SSH host setting —
|
||||
see `scripts/tri/README.md`.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
### "missing RPC credentials" with no useful error
|
||||
|
||||
The CLI prints:
|
||||
```
|
||||
triangles-cli: missing RPC credentials. Set rpcuser/rpcpassword in triangles.conf
|
||||
or pass -rpcuser=<user> -rpcpassword=<pw> on the command line.
|
||||
(RPC config file: /root/.cryptographic-triangles/triangles.conf)
|
||||
```
|
||||
|
||||
This message is **misleading in one case**: the conf path it prints is
|
||||
the *fallback* path the CLI would have used. The actual conf it
|
||||
*tried* to read is the one resolved from your `-conf` or `-datadir`
|
||||
flag. If you passed `-conf` and still see this, your conf is missing
|
||||
`rpcuser=` or `rpcpassword=`, or has them commented out.
|
||||
|
||||
If you **did not** pass `-datadir` or `-conf`, the message is literal:
|
||||
the CLI looked at `<default-datadir>/triangles.conf` and did not find
|
||||
`rpcuser`/`rpcpassword` there.
|
||||
|
||||
**Fix:** either edit the conf and add credentials, or pass them on the
|
||||
command line:
|
||||
```bash
|
||||
triangles-cli -rpcuser=trianglesrpc -rpcpassword=secret -datadir=/var/lib/triangles getinfo
|
||||
```
|
||||
|
||||
### Daemon not running
|
||||
|
||||
If the daemon isn't running, `triangles-cli` will fail to connect
|
||||
after a few seconds. Verify the daemon is up first:
|
||||
|
||||
```bash
|
||||
systemctl status trianglesd # systemd-managed install
|
||||
pgrep -af trianglesd # manual install
|
||||
tail -50 /var/log/trianglesd.log # recent log lines
|
||||
```
|
||||
|
||||
### Testnet vs mainnet port mismatch
|
||||
|
||||
Mainnet default is `19111`; testnet is `19112`. If you run a testnet
|
||||
daemon but invoke the CLI without `-testnet`, the CLI connects to
|
||||
`19111` (empty mainnet port) and fails. Use either:
|
||||
|
||||
```bash
|
||||
triangles-cli -testnet -datadir=/var/lib/triangles-testnet getinfo
|
||||
# OR (equivalent):
|
||||
triangles-cli -rpcport=19112 -datadir=/var/lib/triangles-testnet getinfo
|
||||
```
|
||||
|
||||
### Multiple nodes on one host
|
||||
|
||||
If you run two daemons on the same box (e.g. mainnet + testnet), you
|
||||
need to set **different** `rpcport=` for each in their respective
|
||||
confs, and pass the matching `-rpcport` to the CLI. Default
|
||||
`127.0.0.1:<port>` will not route correctly otherwise.
|
||||
|
||||
## Reference: all flags
|
||||
|
||||
| Flag | Purpose |
|
||||
|---|---|
|
||||
| `-conf=<path>` | Path to triangles.conf (absolute path recommended) |
|
||||
| `-datadir=<path>` | Data directory; conf resolved to `<datadir>/triangles.conf` if `-conf` is not absolute |
|
||||
| `-testnet` | Use testnet RPC port (19112 instead of 19111) |
|
||||
| `-rpcconnect=<ip>` | RPC host (default `127.0.0.1`) |
|
||||
| `-rpcport=<port>` | RPC port (default `19111` mainnet, `19112` testnet) |
|
||||
| `-rpcuser=<user>` | RPC username (overrides conf) |
|
||||
| `-rpcpassword=<pw>` | RPC password (overrides conf) |
|
||||
| `-stdin` | Read extra command params from stdin, one per line |
|
||||
| `-raw` | Print raw JSON, no pretty-printing |
|
||||
| `-getinfo` | Synthesized summary from multiple RPCs |
|
||||
| `-version` | Print version and exit |
|
||||
| `-?` / `-h` | Print help and exit |
|
||||
|
||||
## See also
|
||||
|
||||
- `contrib/triangles.conf.example` — daemon configuration reference
|
||||
- `scripts/tri/README.md` — `tri` wrapper (operator-friendly alias)
|
||||
- `doc/release-process.md` — release pipeline
|
||||
- `doc/build-unix.txt` — building the CLI from source
|
||||
@@ -0,0 +1,240 @@
|
||||
# Trusted Snapshot Publisher — Operator Guide
|
||||
|
||||
This document explains how the trusted snapshot publisher mechanism works
|
||||
in Triangles and how to rotate the publisher without rebuilding the
|
||||
daemon. It is written for the person who operates the Triangles network
|
||||
after Sami — whoever that turns out to be.
|
||||
|
||||
## Background
|
||||
|
||||
The Triangles daemon verifies that any UTXO snapshot it loads was
|
||||
**signed by a trusted publisher**. This prevents a malicious snapshot
|
||||
file from tricking a node into accepting a fake chain state.
|
||||
|
||||
In versions before v6.1.8, the trusted publisher list was hardcoded
|
||||
in the binary. To rotate keys, the daemon had to be rebuilt and
|
||||
re-released. That was bad for handover.
|
||||
|
||||
Starting with v6.1.8, the daemon supports a **runtime-configurable
|
||||
single-slot trusted publisher** via RPC. The compiled-in fallback list
|
||||
is still consulted if no runtime publisher is set, so a fresh daemon
|
||||
never fails to verify an old snapshot.
|
||||
|
||||
## The model — Design A (single-slot, auto-replace)
|
||||
|
||||
- **At most ONE runtime publisher exists at any time.**
|
||||
- Calling `settrustedv2snapshotpublisher <addr>` **atomically
|
||||
replaces** the current publisher. The previous one is dropped
|
||||
immediately. There is no grace period, no retirement list, no
|
||||
rollback path. Pure single-slot.
|
||||
- The active publisher is persisted to
|
||||
`<datadir>/snapshot-publisher.json`, so it survives daemon
|
||||
restarts.
|
||||
- The built-in fallback list (read-only, compiled into the binary) is
|
||||
consulted only if no runtime publisher is set. That list contains:
|
||||
- `TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX` — Sami's legacy snapshot
|
||||
publisher key (the original, used from v6.1.5 through v6.1.7).
|
||||
|
||||
## The three RPCs
|
||||
|
||||
### `settrustedv2snapshotpublisher <address>`
|
||||
|
||||
Atomically replaces the active trusted publisher. The previous
|
||||
publisher is dropped immediately. The new publisher is persisted to
|
||||
`<datadir>/snapshot-publisher.json` so the choice survives restarts.
|
||||
|
||||
```
|
||||
triangles-cli settrustedv2snapshotpublisher TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH
|
||||
```
|
||||
|
||||
Result:
|
||||
```json
|
||||
{
|
||||
"previous": "TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX",
|
||||
"current": "TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH"
|
||||
}
|
||||
```
|
||||
|
||||
The `previous` field is empty if no runtime publisher was set before.
|
||||
|
||||
### `gettrustedv2snapshotpublisher`
|
||||
|
||||
Returns the currently active runtime publisher.
|
||||
|
||||
```
|
||||
triangles-cli gettrustedv2snapshotpublisher
|
||||
```
|
||||
|
||||
Result:
|
||||
```json
|
||||
{
|
||||
"active": "TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH",
|
||||
"has_runtime_override": true
|
||||
}
|
||||
```
|
||||
|
||||
If `has_runtime_override` is `false`, only the built-in fallback list
|
||||
is consulted. The fallback currently contains `TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX`.
|
||||
|
||||
### `unsettrustedv2snapshotpublisher`
|
||||
|
||||
Clears the runtime override. Reverts to the built-in fallback list.
|
||||
Also removes `<datadir>/snapshot-publisher.json`.
|
||||
|
||||
```
|
||||
triangles-cli unsettrustedv2snapshotpublisher
|
||||
```
|
||||
|
||||
Use this if you want to "go back to the legacy trusted signer"
|
||||
without a rebuild.
|
||||
|
||||
## Common rotation scenarios
|
||||
|
||||
### Rotate to a new key (forward rotation)
|
||||
|
||||
1. Generate a new key in the wallet:
|
||||
```
|
||||
triangles-cli getnewaddress
|
||||
# returns: TNewAddressHere...
|
||||
```
|
||||
2. (Optional but recommended) Label it so you remember its role:
|
||||
```
|
||||
triangles-cli setaccount TNewAddressHere... "snapshot publisher"
|
||||
```
|
||||
3. Set it as the trusted publisher:
|
||||
```
|
||||
triangles-cli settrustedv2snapshotpublisher TNewAddressHere...
|
||||
```
|
||||
4. Verify:
|
||||
```
|
||||
triangles-cli gettrustedv2snapshotpublisher
|
||||
```
|
||||
Should show `active: TNewAddressHere...`.
|
||||
|
||||
Old publisher is dropped immediately. New one is in effect for this
|
||||
daemon and any daemon that syncs from `<datadir>/snapshot-publisher.json`.
|
||||
|
||||
### Roll back to the legacy publisher
|
||||
|
||||
If the new key is lost / compromised / you just want to revert:
|
||||
|
||||
```
|
||||
triangles-cli unsettrustedv2snapshotpublisher
|
||||
```
|
||||
|
||||
This reverts to the built-in fallback (`TG8f76ykt...`). No rebuild
|
||||
required. The legacy address will continue to verify any snapshot
|
||||
that was signed before your rotation.
|
||||
|
||||
### Rotate during a handover (publisher A hands off to publisher B)
|
||||
|
||||
1. Publisher B installs v6.1.8+ daemon.
|
||||
2. Publisher B sets themselves as the trusted publisher:
|
||||
```
|
||||
triangles-cli settrustedv2snapshotpublisher TBsAddress...
|
||||
```
|
||||
3. Publisher B signs a new snapshot with their key (see
|
||||
`publishcheckpoint` in `TRIANGLES-RPC-COMMANDS.md`).
|
||||
4. Publisher A can leave the network; their key is no longer trusted
|
||||
on any node that has called `settrustedv2snapshotpublisher`.
|
||||
|
||||
Note: because Design A auto-drops the previous publisher, **only one
|
||||
operator can publish at a time.** If you need overlap (both A and B
|
||||
publishing during a transition), that requires Design B (multi-slot
|
||||
with grace period) — not supported in v6.1.8. Contact Sami for the
|
||||
upgrade path.
|
||||
|
||||
## Files
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `<datadir>/snapshot-publisher.json` | Runtime publisher override. Plain JSON. Inspectable with `cat`. |
|
||||
| `<datadir>/wallet.dat` | Must contain the privkey for the active publisher, otherwise `publishcheckpoint` will fail at signing time. (Trust is governed by the override; signing is governed by the wallet.) |
|
||||
|
||||
### `<datadir>/snapshot-publisher.json` format
|
||||
|
||||
```json
|
||||
{
|
||||
"address": "TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH",
|
||||
"set_at": 1752168000,
|
||||
"note": "Set via triangles-cli settrustedv2snapshotpublisher. Replace atomically; previous publisher is dropped."
|
||||
}
|
||||
```
|
||||
|
||||
`set_at` is the Unix timestamp when the RPC was last called. `note` is
|
||||
informational only.
|
||||
|
||||
## Recovery if RPC fails
|
||||
|
||||
If for some reason the runtime override can't be persisted (e.g. JSON
|
||||
write fails), the RPC returns a warning but the in-memory change is
|
||||
already live for the current session. To check:
|
||||
|
||||
```
|
||||
triangles-cli gettrustedv2snapshotpublisher
|
||||
```
|
||||
|
||||
If `active` is set, you're good for the current session. The next
|
||||
daemon restart will lose it unless `snapshot-publisher.json` exists.
|
||||
Inspect it manually:
|
||||
|
||||
```
|
||||
cat ~/.triangles/snapshot-publisher.json
|
||||
```
|
||||
|
||||
If the file doesn't exist but you need the override to survive restart,
|
||||
hand-write it:
|
||||
```json
|
||||
{
|
||||
"address": "TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH",
|
||||
"set_at": 1752168000,
|
||||
"note": "Hand-set; rotate via triangles-cli settrustedv2snapshotpublisher."
|
||||
}
|
||||
```
|
||||
|
||||
The daemon reads this file at startup. Address must be 34 chars and
|
||||
start with `T`. Anything else is logged and ignored.
|
||||
|
||||
## When you DO need a rebuild
|
||||
|
||||
- **Adding a new entry to the built-in fallback list** (the
|
||||
read-only list compiled into the binary). Edit
|
||||
`BUILTIN_TRUSTED_SNAPSHOT_SIGNERS[]` in `src/bootstrap.cpp`, rebuild,
|
||||
release. This is only needed if you want a publisher to be trusted
|
||||
*without* any operator running the RPC.
|
||||
- **Changing the RPC names or argument shapes.** Edit source, rebuild.
|
||||
|
||||
For everyday "I want to add or rotate a trusted publisher," the RPC
|
||||
is enough. Don't rebuild.
|
||||
|
||||
## Why "single-slot, no grace period"
|
||||
|
||||
Sami asked for it explicitly when designing the operator-experience
|
||||
for this feature. The trade-off: if the active key is lost or
|
||||
compromised, there's no automatic fallback. The operator must either
|
||||
re-add the previous key (which requires they kept the JSON file or
|
||||
remember the address) or rebuild with the new key in
|
||||
`BUILTIN_TRUSTED_SNAPSHOT_SIGNERS[]`.
|
||||
|
||||
If this trade-off becomes painful — for example if multiple
|
||||
operators need to publish during a handover — the alternative is
|
||||
Design B (multi-slot with grace period). That's a one-day patch on
|
||||
top of this one. Ask Sami for the upgrade.
|
||||
|
||||
## Versioning
|
||||
|
||||
This feature is introduced in **v6.1.8**. Daemons older than v6.1.8
|
||||
still use the hardcoded `TG8f76ykt...` only — they cannot use the new
|
||||
key until they upgrade.
|
||||
|
||||
## Related RPCs
|
||||
|
||||
For the publishing side (signing snapshots, not verifying them),
|
||||
see:
|
||||
|
||||
- `publishcheckpoint <interval> <signing_address> <output_path>` —
|
||||
builds and signs a checkpoint document.
|
||||
- `gencheckpoints` — generates raw checkpoint data without signing.
|
||||
- `getcheckpoint` — returns the current synchronized checkpoint.
|
||||
|
||||
See `TRIANGLES-RPC-COMMANDS.md` for full details on those.
|
||||
@@ -3,7 +3,7 @@
|
||||
# Run on a Linux x64 system with appimagetool installed
|
||||
set -e
|
||||
|
||||
VERSION="5.9.24"
|
||||
VERSION="6.2.4"
|
||||
APPDIR="Triangles-x86_64.AppDir"
|
||||
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
|
||||
|
||||
|
||||
@@ -41,6 +41,31 @@
|
||||
</provides>
|
||||
|
||||
<releases>
|
||||
<release version="6.1.5" date="2026-07-08">
|
||||
<description>
|
||||
<p>UI: olive-green for unconfirmed/immature stake balances. Wallet: close-hang on Windows from detached Tor/I2P threads fixed. Consensus: live PoS checks during stale-tip IBD. Plus release infrastructure (reproducible builds, signed release pipeline) and audit follow-ups.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="6.1.4" date="2026-07-04">
|
||||
<description>
|
||||
<p>CI: Tor bundle download resilience. NeedsBootstrap flag now correctly persists across rocksdb/ restarts. CI reliability only; no protocol/wallet/chain format changes.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="6.1.3" date="2026-07-02">
|
||||
<description>
|
||||
<p>Chain-DB migration hardening, BIP39 passphrase support, HD-wallet indicator, test isolation improvements. Supersedes the broken v6.1.2 hotfix.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="6.1.1" date="2026-07-01">
|
||||
<description>
|
||||
<p>v3 snapshot support, portable x86-64-v2 baseline, anti-spam fix, continuous finality checkpoints.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="6.1.0" date="2026-06-30">
|
||||
<description>
|
||||
<p>SQLite wallet backend, RocksDB default, Boost removal, I2P startup fix. Initial 6.x line with C++20 modernization and embedded Tor/I2P support.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="5.3.7" date="2026-03-24">
|
||||
<description>
|
||||
<p>Version 5.3.7 release.</p>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Run from the packaging/debian directory
|
||||
set -e
|
||||
|
||||
VERSION="5.9.24"
|
||||
VERSION="6.2.4"
|
||||
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.24
|
||||
ARG VERSION=6.2.4
|
||||
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.24
|
||||
ARG VERSION=6.2.4
|
||||
|
||||
LABEL maintainer="Cryptographic Triangles Team"
|
||||
LABEL description="Cryptographic Triangles (TRI) headless daemon"
|
||||
LABEL version="5.9.24"
|
||||
LABEL version="6.2.4"
|
||||
|
||||
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.9.24
|
||||
image: cryptographic-triangles/trianglesd:6.2.4
|
||||
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.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-qt
|
||||
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.2.4/Cryptographic-Triangles-v6.2.4-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.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-daemon
|
||||
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.2.4/Cryptographic-Triangles-v6.2.4-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.9.24"
|
||||
VERSION="6.2.4"
|
||||
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.9.24
|
||||
Version: 6.2.4
|
||||
Release: 1%{?dist}
|
||||
Summary: Cryptographic Triangles (TRI) cryptocurrency wallet
|
||||
License: MIT
|
||||
@@ -42,3 +42,49 @@ install -Dm644 %{SOURCE2} %{buildroot}%{_datadir}/applications/triangles-qt.desk
|
||||
%{_bindir}/triangles-qt
|
||||
%{_bindir}/trianglesd
|
||||
%{_datadir}/applications/triangles-qt.desktop
|
||||
|
||||
%changelog
|
||||
* Wed Jul 08 2026 Sami Ahmed <sami@cryptographic-triangles.org> - 6.1.7-1
|
||||
- 6.1.7 release. UI: Overview Total label font-weight bumped from 75
|
||||
to 900 so the Total actually reads as bold against Spendable/Stake.
|
||||
Transactions amount column Confirming-tier color changed from pale
|
||||
mint (#C5EBC9) to mid green (#4A8C5E) so it reads as visibly
|
||||
different from the bright Confirmed green. Both paint sites now
|
||||
read confirmation depth via a new DepthRole on the table model
|
||||
instead of the status enum, so the color fires on every block
|
||||
increment.
|
||||
|
||||
* Wed Jul 08 2026 Sami Ahmed <sami@cryptographic-triangles.org> - 6.1.6-1
|
||||
- 6.1.6 release. UI: Overview Total label now conditional (green when
|
||||
total > 0, red when empty), Transactions amount column now 3-tier
|
||||
(grey / pale mint / money-green) by confirmation depth. Plus
|
||||
sigcache entry-size fix and Polish CI/build fixes.
|
||||
|
||||
* Wed Jul 08 2026 Sami Ahmed <sami@cryptographic-triangles.org> - 6.1.5-1
|
||||
- 6.1.5 release. UI: olive-green for unconfirmed/immature stake balances.
|
||||
Wallet: close-hang on Windows from detached Tor/I2P threads fixed.
|
||||
Consensus: live PoS checks during stale-tip IBD. Plus release
|
||||
infrastructure (reproducible builds, signed release pipeline) and
|
||||
audit follow-ups.
|
||||
|
||||
* Sat Jul 04 2026 Sami Ahmed <sami@cryptographic-triangles.org> - 6.1.4-1
|
||||
- 6.1.4 release. CI: Tor bundle download resilience. NeedsBootstrap
|
||||
flag now correctly persists across rocksdb/ restarts. CI reliability
|
||||
only; no protocol/wallet/chain format changes.
|
||||
|
||||
* Thu Jul 02 2026 Sami Ahmed <sami@cryptographic-triangles.org> - 6.1.3-1
|
||||
- 6.1.3 release. Chain-DB migration hardening, BIP39 passphrase
|
||||
support, HD-wallet indicator, test isolation improvements.
|
||||
Supersedes the broken v6.1.2 hotfix.
|
||||
|
||||
* Wed Jul 01 2026 Sami Ahmed <sami@cryptographic-triangles.org> - 6.1.1-1
|
||||
- 6.1.1 release. v3 snapshot support, portable x86-64-v2 baseline,
|
||||
anti-spam fix, continuous finality checkpoints.
|
||||
|
||||
* Tue Jun 30 2026 Sami Ahmed <sami@cryptographic-triangles.org> - 6.1.0-1
|
||||
- 6.1.0 release. SQLite wallet backend, RocksDB default, Boost
|
||||
removal, I2P startup fix. Initial 6.x line with C++20 modernization
|
||||
and embedded Tor/I2P support.
|
||||
|
||||
* Tue Mar 24 2026 Sami Ahmed <sami@cryptographic-triangles.org> - 5.3.7-1
|
||||
- 5.3.7 release.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"version": "5.9.24",
|
||||
"version": "6.2.4",
|
||||
"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.9.24/Cryptographic-Triangles-5.9.24-win-x64.zip",
|
||||
"url": "https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.2.4/Cryptographic-Triangles-6.2.4-win-x64.zip",
|
||||
"hash": "6f002a669a7e92aaf3d8dd7b1ae80f06a086c99a15ca05cf107665009ffc06b7"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PackageIdentifier: CryptographicTriangles.TrianglesQt
|
||||
PackageVersion: 5.9.24
|
||||
PackageVersion: 6.2.4
|
||||
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.9.24/Cryptographic-Triangles-5.9.24-win-x64.zip
|
||||
InstallerUrl: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.2.4/Cryptographic-Triangles-6.2.4-win-x64.zip
|
||||
InstallerSha256: 6F002A669A7E92AAF3D8DD7B1AE80F06A086C99A15CA05CF107665009FFC06B7
|
||||
ManifestType: singleton
|
||||
ManifestVersion: 1.6.0
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# AVX-512 SIGILL build fix — `-mno-avx512f` belt-and-suspenders
|
||||
|
||||
**TL;DR:** GCC 11+ on an AVX-512-capable CI runner will emit AVX-512
|
||||
instructions in libstdc++-inlined `std::string` / `std::copy` / `memcpy` code
|
||||
paths even when `-march=x86-64-v2 -mtune=generic` is set globally. The
|
||||
resulting binary crashes with `SIGILL (Illegal instruction)` on every
|
||||
production node that lacks AVX-512 (KVM EPYC, Ryzen 3600, ARM64, anything
|
||||
pre-Skylake-X). The fix is to add `-mno-avx512f -mno-avx512*` to the
|
||||
global compile options. **Don't trust `-march=x86-64-v2` alone** — it sets
|
||||
the baseline ISA but does not prevent auto-vectorization from emitting
|
||||
higher-ISA instructions.
|
||||
|
||||
## Symptom (v6.1.9, 2026-07-31)
|
||||
|
||||
DNS2 attempted to install the v6.1.9 `.deb`. Daemon started and died
|
||||
immediately with `status=4/ILL` (illegal instruction), before reaching
|
||||
`main()`. The systemd journal showed:
|
||||
|
||||
```
|
||||
Aug 01 04:38:28 vmi3080415 trianglesd[367821]: status=4/ILL
|
||||
```
|
||||
|
||||
The daemon was previously working on v6.1.4.0. The only thing that
|
||||
changed was the binary.
|
||||
|
||||
## Diagnosis recipe (15 minutes)
|
||||
|
||||
```bash
|
||||
# 1. Reproduce the crash under gdb so you can see the failing instruction
|
||||
systemctl stop trianglesd
|
||||
sleep 3
|
||||
gdb --batch \
|
||||
-ex "set startup-with-shell off" \
|
||||
-ex "run -datadir=/root/.triangles -conf=/root/.triangles/triangles.conf" \
|
||||
-ex "info symbol \$pc" \
|
||||
-ex "x/3i \$pc" \
|
||||
-ex "x/8bx \$pc-4" \
|
||||
/usr/lib/cryptographic-triangles/trianglesd 2>&1 | tail -15
|
||||
```
|
||||
|
||||
You will see something like:
|
||||
|
||||
```
|
||||
Program received signal SIGILL, Illegal instruction.
|
||||
0x00005555556bbe49 in ?? ()
|
||||
No symbol matches $pc.
|
||||
=> 0x5555556bbe49: vpbroadcastq %rax,%xmm0
|
||||
0x5555556bbe4f: sub %r14,%rdx
|
||||
0x5555556bbe52: test %rdx,%rdx
|
||||
0x5555556bbe45: 0x08 0x49 0x89 0xc4 0x62 0xf2 0xfd 0x08
|
||||
```
|
||||
|
||||
The bytes `0x62 0xf2 0xfd 0x08` are the **EVEX prefix** — an AVX-512
|
||||
encoding. The disassembled instruction `vpbroadcastq %rax, %xmm0` is
|
||||
the broadcast form, which uses EVEX even when the destination is XMM.
|
||||
|
||||
## Why this happens
|
||||
|
||||
The Triangles cmake file `cmake/AddCompilerFlags.cmake` already sets
|
||||
`-march=x86-64-v2 -mtune=generic` for `x86_64 && NOT WIN32 && NOT APPLE`:
|
||||
|
||||
```cmake
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$" AND NOT WIN32 AND NOT APPLE)
|
||||
option(CMAKE_X86_64_BASELINE "..." ON)
|
||||
if(CMAKE_X86_64_BASELINE)
|
||||
add_compile_options(-march=x86-64-v2)
|
||||
add_compile_options(-mtune=generic)
|
||||
endif()
|
||||
endif()
|
||||
```
|
||||
|
||||
`-march=x86-64-v2` sets the **baseline ISA** to ~Nehalem (SSE4.2 + POPCNT +
|
||||
CMPXCHG16B). GCC should not emit anything higher. In practice GCC 11.4 +
|
||||
`-O3` + libstdc++ inlining of `std::string::operator=`, `std::copy`, and
|
||||
`memcpy` patterns from libstdc++ headers that contain `#pragma GCC
|
||||
push_options` blocks for AVX-512 detection — together they emit
|
||||
`vpbroadcastq` EVEX instructions into user code via header inlining.
|
||||
|
||||
The instruction comes from **libstdc++ inlining**, not from any
|
||||
Triangles-specific source. The disassembly shows the inlined function
|
||||
is in a region marked as `std::string::operator=(std::string&&) + 0x2610`
|
||||
because the symbol table merges the entire `.text` into the closest
|
||||
named symbol — but the AVX-512 instruction itself is in a Triangles
|
||||
translation unit (the call chain eventually reaches it from
|
||||
`main.cpp`/`net.cpp` via `std::string` operations on the onion/I2P
|
||||
addrman paths).
|
||||
|
||||
## The fix
|
||||
|
||||
Add an explicit `-mno-avx512*` family block to
|
||||
`cmake/AddCompilerFlags.cmake` inside the existing
|
||||
`CMAKE_X86_64_BASELINE` block:
|
||||
|
||||
```cmake
|
||||
if(CMAKE_X86_64_BASELINE)
|
||||
add_compile_options(-march=x86-64-v2)
|
||||
add_compile_options(-mtune=generic)
|
||||
# Belt-and-suspenders: GCC 11+ can autovectorize libstdc++
|
||||
# std::string / std::copy / memcpy paths into AVX-512 EVEX
|
||||
# instructions even when -march=x86-64-v2 is set. Force-disable
|
||||
# the whole AVX-512 family so a CI runner's EPYC 7763 (or any
|
||||
# AVX-512-capable build host) cannot leak AVX-512 into a binary
|
||||
# that needs to run on KVM EPYC, Ryzen 3000, or ARM64.
|
||||
# NB: -mno-avx512*4fmaps / -mno-avx512*4vnniw use NO dash between
|
||||
# 'avx512' and the sub-feature (correct: -mno-avx5124fmaps). The
|
||||
# -mno-avx512-4fmaps form (with a dash) is rejected by GCC and
|
||||
# makes the whole build fail with "unrecognized command-line option".
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||
add_compile_options(
|
||||
-mno-avx512f -mno-avx512pf -mno-avx512er -mno-avx512cd
|
||||
-mno-avx512vl -mno-avx512bw -mno-avx512dq -mno-avx512ifma
|
||||
-mno-avx512vbmi -mno-avx512vbmi2 -mno-avx512vnni
|
||||
-mno-avx512bitalg -mno-avx512vpopcntdq
|
||||
-mno-avx5124fmaps -mno-avx5124vnniw -mno-avx512vp2intersect
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
```
|
||||
|
||||
`-mno-avx512f` is the critical one (it's the foundation of the family).
|
||||
The others cover AVX-512 sub-features GCC may emit. The clang-equivalent
|
||||
of this is `-mno-avx512f -mno-avx512fp16 -mno-avx512pf -mno-avx512er
|
||||
-mno-avx512cd -mno-avx512vl -mno-avx512bw -mno-avx512dq -mno-avx512ifma`
|
||||
but this Triangles fix is GCC-only because the existing code already
|
||||
guards on `CMAKE_CXX_COMPILER_ID STREQUAL "GNU"`.
|
||||
|
||||
## Verify the fix landed in the new binary
|
||||
|
||||
```bash
|
||||
# Build, install, then check for EVEX-encoded instructions
|
||||
objdump -d /usr/lib/cryptographic-triangles/trianglesd 2>/dev/null \
|
||||
| grep -c "vpbroadcastq"
|
||||
# Expected: 0 (was 741 before the fix)
|
||||
|
||||
# Also check for any other EVEX-encoded instructions
|
||||
objdump -d /usr/lib/cryptographic-triangles/trianglesd 2>/dev/null \
|
||||
| grep -E "vpcompress|vpdpwssd|vpdpbusd|gfni|vaes|vpclmulqdq" | head
|
||||
# Expected: empty
|
||||
```
|
||||
|
||||
The smoke test that should have caught this: **add a job to the
|
||||
`Build All Platforms` workflow that runs the resulting trianglesd
|
||||
binary on a non-AVX-512 runner before publishing artifacts.** Catches
|
||||
this class of bug forever.
|
||||
|
||||
## Why this wasn't caught before
|
||||
|
||||
GitHub Actions' hosted `ubuntu-22.04` runner is an AMD EPYC 7763 (Zen 3,
|
||||
AVX-512 capable). Every CI build worked because the runner has the
|
||||
required ISA. No unit test actually runs the produced binary, so the
|
||||
build-vs-run gap is invisible until the binary ships to a CPU without
|
||||
AVX-512 (which is most production hardware, including KVM-virtualized
|
||||
EPYC, Ryzen 3000/5000 series, and ARM64 nodes). The fix is both the
|
||||
cmake `-mno-avx512f` belt and a CI smoke-test step that executes the
|
||||
binary on a non-AVX-512 runner.
|
||||
|
||||
## Files changed for v6.2.0
|
||||
|
||||
- `cmake/AddCompilerFlags.cmake` — added the `-mno-avx512*` block
|
||||
- `src/clientversion.h` — bumped to 6.2.0.0
|
||||
- All version-bearing files updated by `./scripts/bump-version.sh 6.2.0`
|
||||
|
||||
## Pitfall — don't do these things
|
||||
|
||||
- **Don't just add `-march=x86-64-v2`** without also adding
|
||||
`-mno-avx512*`. The march alone is not enough on GCC 11+ with libstdc++
|
||||
inlining. The behavior was verified locally: `-march=x86-64-v2` alone
|
||||
still produced 741 AVX-512 instructions in the test build.
|
||||
- **Don't add `-fno-tree-vectorize`** to "fix" the symptom. That would
|
||||
regress performance across the whole daemon. `-mno-avx512f` is the
|
||||
surgical fix.
|
||||
- **Don't use `set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mno-avx512f")`**.
|
||||
`add_compile_options` is the correct API — it propagates to subdirectory
|
||||
targets (libsecp256k1, libtor, etc.) that were the actual sources of
|
||||
the AVX-512 in earlier sessions.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- The Triangles release v6.1.9 was the first release with the staking-
|
||||
selfheal fix (`f69f087 [grade=B] fix(staking): carve out caught-up
|
||||
nodes from IBD gate so chain can self-heal`). v6.1.9 was the binary
|
||||
that exhibited this bug; v6.2.0 carries both the staking fix AND this
|
||||
build-portability fix.
|
||||
- The git history for this fix is the v6.2.0 release.
|
||||
@@ -0,0 +1,65 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGnxdoUBEACaICSRk5Clg4kI5IubMXnXLbsSWzi0TKIpqh4Tqgl2k1bgSxda
|
||||
tuBabHcsaw6Kpo96CJl9aZ63VIrEhCSdirGm/wWlbnTvm6cK4EDucGgS4BdEfm9B
|
||||
Lw2c+iTjuJqJt2HLbRkZmF8qHy0Mo1DjsjbWUiwIP62RkuxCNuW2Wl9euak504UW
|
||||
ZTFB9f3Bu1C6rknsWQ0VR5HJwWN4UrVMukZhvlzLRjKgW7W2XchSXUIAe7b0/5jo
|
||||
pFB30pwxbaBIoeJu8AHYnzBYRThp0WbDTC/LK5FSnSgG751jOtkbheRNGjO65a2L
|
||||
gkaclxo1NUIIu+WqdBtTbpUQM7UEd50FOXxUgq/xJhGujNMJyOMMPEzfJ+kP9pD4
|
||||
p+gkNCLLgvT+gu1PnF0iTIAb4qggHGzZGRgc5lTxC28XEud0DAx+Pdcdf/nlQTsu
|
||||
AOjZZgiiLIjwJZo/RYwId1Wh+LmtYZqVZ6j4vqqaXXPADpN40LGyUo376+oVSn77
|
||||
1w2j1CWSmTEPaq4KmvTvnTvFfbeXkKckmUziBYwqZI0uA2xE6ShNUaAS4kdIaZhO
|
||||
Bb3t9xrwu2QAR1rRlNTCChOyNbauvo32GLRnXg5BXYTBsmMU/QHe6EBJsycq/IHl
|
||||
2yNPQUtynxzkDZ9OYrwbZaTZOCJK0pHwm4HUmV3rPiEPXUKJXXDojWQYpwARAQAB
|
||||
tHlLcnlzdGllIFRyaWFuZ2xlcyBSZWxlYXNlIChBdXRvbm9tb3VzIHJlbGVhc2Ug
|
||||
c2lnbmluZyBrZXkgZm9yIHRyaWFuZ2xlc192NSkgPGtyeXN0aWUtdHJpYW5nbGVz
|
||||
LXJlbGVhc2VAZG5zMi5zYW1pLnRhaWxuZXQ+iQJYBBMBCgBCFiEEUjqBgz63IBVz
|
||||
4e/h3PJXmWgQeYQFAmnxdoUDGy8EBQkDwmcABQsJCAcCAiICBhUKCQgLAgQWAgMB
|
||||
Ah4HAheAAAoJENzyV5loEHmEPm0P/3y2Y5Y1rhgSj6yN/1PuXhpp1sNqXBOJZxTW
|
||||
uUx/4LUqLgqbtFC0fR4BwpTYEkGGaofi0/95sPwKu0jmVR6hJ+8Omk/4TMRmXUYq
|
||||
JUTA0/xzj9sOndaqiwRY3Y/YO/ytahL89y8xl5cYSaOOwLI/f9xo8pq1t20Iiuiw
|
||||
kcaUBRQgpTVMI49VcXwrEUMnjV9cldGqql8v7CSKds5rRxQgT8ifaC6euTWxK0Tn
|
||||
5Yu/wnBd+akU5/bcI8PEp5VyUyAJMZJPZ6mUqriWXlnhiUj0NawEKtfG9qlkMixL
|
||||
5ujz9lu/9MvFUYC4QSvcd1O3k9MJ6T4Yk/uEygEca8Y/3DcccWRMHjW2Ah+ewhHE
|
||||
yHy0tctzCe7pco+jfB7zicKv0bjXarvwBZ43e5F/zG5PMpo0XAS9EkEUV+/9BJ38
|
||||
jBHvzqwXsYTnxS0hgOSONJk9Cc6i0NN1ex3rPOrYvBvHWZ+9n3AU2taUljuypDGO
|
||||
RweCHsFMYGx/oOI94bD7wTeVey0tAZ+3Urz6T5qY5SmNKiwZ5NtbYo0Mp8r5DdPJ
|
||||
N9KtXtaDMPI/rORjl1Ad9xhDbGMCr7EH9SjTU+z51me31/ZU58jICGlvm3/JDcb5
|
||||
CAWyDppvW0ul9yqo1fecSi3w7m2sI+4F+tj8oLFmO+5rQw85F4LPqjVVMbUUkoAH
|
||||
udtoU3Y8uQINBGnxdoUBEACtFpgwuwEZqxbsfmL+uBxHnxSSRm2vlQc7HRtQG6Nu
|
||||
Tg1x4s9xFO6kNkcslPgZx9XSvFkPt1RUCNViTYE34UoOfkBs+aNkw4ztwuKGt/AS
|
||||
CZFRX99yBx7P0kiV4Nt/Cj3oQBtEXQixMmGK4+N0WBskV/QxRFA7hl+ZQBeEFsYP
|
||||
15UyjX2h6HFRYTSPKufEmtE/OkO9dg3fyxTvZ3+1o3eWWjT4VReX4jvmzXn3RNP1
|
||||
BwuAy+iwmnqUBcuEZ0qQiT/+oRLCHOFLCAjVoSsPY9WJfF67XpDb2noV/0RqltMD
|
||||
jUc/MT8Bxn/y8qHKvQuyPms/YO5jMI7q+/D1eayO4R48qhsMVp6Rjb31xalMWT2W
|
||||
rwQg1XaFG80vUisbfX6CU0sH34tWQkqAL7AiwradPtwB0Sn60Em5UgHdWQ7rkd+h
|
||||
mFOUjYi3Q1hOuPQNuzDK51n5sv8qOIrfghR0F2AtRkpbhBYM9435U+JkcZTjJ6wp
|
||||
WYLBTAys4qo9MnL18Z4byaw4e122eBgI3/UOvG+7C7wIAwmiDvnYzqErz7iOmuTe
|
||||
+cgdWYmLFvkfx8P6Ka+6likSV4ZY/ASP4Uo/gTspatwqHApAmphfVEGwm0/wKMl2
|
||||
Br+zuZZ8RJ1GxahwJ1oo3uuGjIQjGNplh2wHVvbsfg4mlFKDbShdJ5adtx/E6BrT
|
||||
NQARAQABiQRyBBgBCgAmFiEEUjqBgz63IBVz4e/h3PJXmWgQeYQFAmnxdoUCGy4F
|
||||
CQPCZwACQAkQ3PJXmWgQeYTBdCAEGQEKAB0WIQRpE+E2EPaYGDQpziDC3GBhjIWh
|
||||
WQUCafF2hQAKCRDC3GBhjIWhWQYID/0Ru2U9rLatIAjoSWI6TMFaOaxHf1NAsTcz
|
||||
fPRbFNxx0d4ByjfjLlrfnDpQXsFpMa6/BpQ1Ps1ApW+wQsuHXxj/jdZVSi5f/sOT
|
||||
XKZq/MRZu8enA1foj0b6sJ13ZWY0iIWmIeK8NWuNBFWz2QTjRie2hqoOTR+Hy43r
|
||||
gRMlzPaXNoeD2UuvhoDphH2g2OWcppxd2b1yk7W9kh0CgvXXg4cPee71LmXLZMoL
|
||||
GJcmtSkU24fiwa95TSk2J5qQ3voP5Knk8e/VgGmOSUoUzr+O5N6tEO2KPVr3bsFt
|
||||
8zKHEyuddDYUju4U2Fl+xq4yJCYX3h6AKyh/c3bOAGp4f3zs62XPjn9RIXlTH9Lw
|
||||
Vp97pJRzAEYzXRGXfGJRz54hQzft1L+BkhqWpVwzxI1fnflpVghahHOIoa0bnpyH
|
||||
ycxxvkGY6o5TS5Ymqf4yry/4G+C64kX2GlBgmN2I2+UJ3z/cyEqY4XVMGk4S7uLq
|
||||
d0eKrA2ZaSHUce0F/gGpMynxGFP+BNlfNBcSwzgBbnvcyFhOtls4LvTAcLmyBpjM
|
||||
gEugtkskDSxJd/HcnTcFF5P9UcVPdD7vg7tlUXQ37AvbeppFC4pFbxYK01SOYk+W
|
||||
nXH/Mq1XkFFcArVtsL1octAWuaqn8M/5kXnKvhw/TCBNPfQ7Kljx1V65kErMXNl2
|
||||
F/cJXWQKCXPtD/92EXa9uvIxCINwxyZidwEvqx1xpBTIDDdYvDt8ZXHr957xpiaz
|
||||
ls3aHy0mMUGigzVEL0AcPToBEudEzy+z1pB0y23znveycDZRTRsGnDwLrdb9eqTu
|
||||
JDViRtB6WBASGsU3XHMYFietvEukmqJj55KCDl5YapZDKUb1iraERJ72PH9xk3C7
|
||||
501Cklfe+GM8VBymwApOjWPLw1cIxVOL/Ex9ADsVMYDubAVh0LnqvDTg8e8bv4gu
|
||||
BhyC2AXsQIUZ9HtixfvLZ6sdsPjstlQj+ZinpTHWthx52jrfcRYOo32cE06BpR3U
|
||||
bQ+mjn6orzZ7Iq5p6aejukCddvlSX381vMaLf1/FGzmu/9f52p7uTLxU7N8sEcqq
|
||||
PlkdRYatwWDeKuGpYVqmXuPvAaPD/sfH6zw0O5JjcNhb5KqTMjcV7IXV+V7QU2F5
|
||||
iH5eYepAFf5uctffFMlCZ2YtCLlISMxHWLLqupIlu/JumTLcUjXUpOMV/sp+v6gD
|
||||
66yx5QQWtVdYT9dYW+EUybjuWlS85T9DJVrPx5GiQfKjgFzuyuEvsbExzVBOwsBP
|
||||
o/pPUWyBNSI6YVrm329U7ybAuDdnTveaMtIxRneN8mM9lhXNWpb8UpvSGnMP0lLI
|
||||
tx58dQjEl3lbis897KDgzHy2pGKQDcvLdj14/xpfjeTWHI6Ut3mZylIKWg==
|
||||
=zWaw
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
+29
-22
@@ -1,29 +1,36 @@
|
||||
# Version Bump Script
|
||||
# Scripts
|
||||
|
||||
Updates the version number across all files in the repo from a single command.
|
||||
Operational scripts for the Triangles project. See also `doc/release-process.md`
|
||||
for the canonical release pipeline documentation.
|
||||
|
||||
## Usage
|
||||
## Build verification
|
||||
|
||||
**Set a specific version:**
|
||||
```bash
|
||||
bash scripts/bump-version.sh 5.7.0
|
||||
```
|
||||
- **`verify-reproducible-build.sh`** — builds the daemon (or another target)
|
||||
twice from the same source tree and verifies the SHA256 hashes match.
|
||||
Catches accidental introduction of non-determinism (e.g. `__DATE__`/`__TIME__`
|
||||
regressions, dirty git state, PIE base-address drift).
|
||||
|
||||
**Or edit `src/clientversion.h` first, then sync everything else:**
|
||||
```bash
|
||||
bash scripts/bump-version.sh
|
||||
```
|
||||
## Release signing
|
||||
|
||||
## What it updates
|
||||
- **`sign-release.sh`** — generates `SHA256SUMS`, writes detached PGP
|
||||
signatures (`.asc`) over each release artifact and over `SHA256SUMS`.
|
||||
Supports `--verify` for independent third-party verification.
|
||||
Uses `TRIANGLES_RELEASE_KEY` env var (defaults to
|
||||
`sami@cryptographic-triangles.org`).
|
||||
|
||||
- `src/clientversion.h` (source of truth)
|
||||
- `src/version.h`
|
||||
- `triangles-qt.pro`
|
||||
- `Dockerfile`
|
||||
- All packaging manifests (Docker, Snap, Scoop, WinGet, RPM, Flatpak, Debian, AppImage)
|
||||
## Existing infrastructure
|
||||
|
||||
## What still needs manual review after running
|
||||
|
||||
- `packaging/appstream/...metainfo.xml` — add a new `<release>` entry
|
||||
- `README.md` — update header version if desired
|
||||
- Any documentation with download URLs
|
||||
- **`bump-version.sh`** — sync version numbers across all manifests from
|
||||
`src/clientversion.h`.
|
||||
- **`sign-snapshot.sh`** — sign a UTXO snapshot file with the wallet's
|
||||
signing address (not a PGP key; this is a chain-level signature, not a
|
||||
release signature).
|
||||
- **`validate_onion_seeds.py`** — validate every `.onion` address in
|
||||
`triangles.conf` against the v3 hidden-service checksum.
|
||||
- **`ibd-smoke-test.sh`** — fresh-datadir IBD smoke test for catching the
|
||||
classic "stalls early / loops around 570" failure mode.
|
||||
- **`ci/build-rocksdb.sh`** — build and install a pinned RocksDB version
|
||||
for CI.
|
||||
- **`ci/package-linux-daemon.sh`** — Linux packaging step (.deb).
|
||||
- **`ci/package-windows-daemon.sh`** — Windows packaging step.
|
||||
- **`tri/`** — operator-facing CLI for node administration.
|
||||
|
||||
@@ -13,14 +13,30 @@
|
||||
# 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.
|
||||
# Pin policy (2026-08-02): chase the LATEST stable 10.x. "Match
|
||||
# DNS2's system librocksdb" reasoning was abandoned: forward
|
||||
# compatibility mattered more than byte-for-byte soname parity.
|
||||
#
|
||||
# Usage: sudo ./scripts/ci/build-rocksdb.sh
|
||||
set -euo pipefail
|
||||
|
||||
ROCKSDB_VERSION="${ROCKSDB_VERSION:-8.9.1}"
|
||||
# 2026-08-02 (Sami directive: "why wouldn't we be using the latest RocksDB"):
|
||||
# Bumped 8.9.1 -> 10.10.1. Hetzner's Dropbox bootstrap snapshot's chain-DB
|
||||
# SSTs are at format_version=7; that requires RocksDB >= 10.4.0 to read.
|
||||
# 10.10.1 is the latest 10.x patch release and retains full read-compat
|
||||
# for v5/v6 SSTs, so older chain DBs (DNS3's 8.9.1 chain DB, the snapshot
|
||||
# fork) open cleanly on the new daemon. The daemon does not pin its own
|
||||
# writes to v7 — see CHANGELOG for why.
|
||||
# Pin policy: default version + commit are set together. Overriding
|
||||
# ROCKSDB_VERSION alone is allowed (e.g. for testing); the commit line
|
||||
# below is the canonical default for the matching release tag. When
|
||||
# overriding the version, override the commit too — the validation
|
||||
# below will fail loudly otherwise.
|
||||
ROCKSDB_VERSION="${ROCKSDB_VERSION:-10.10.1}"
|
||||
ROCKSDB_TAG="v${ROCKSDB_VERSION}"
|
||||
# v10.10.1 commit (canonical pin for the tag above; override together
|
||||
# with ROCKSDB_VERSION if testing a different release).
|
||||
ROCKSDB_COMMIT="${ROCKSDB_COMMIT:-4595a5e95ae8525c42e172a054435782b3479c57}"
|
||||
INSTALL_PREFIX="${INSTALL_PREFIX:-/usr/local}"
|
||||
JOBS="${JOBS:-$(nproc)}"
|
||||
|
||||
@@ -34,6 +50,12 @@ git clone --depth 1 --branch "${ROCKSDB_TAG}" \
|
||||
|
||||
cd "${WORKDIR}/rocksdb"
|
||||
|
||||
ACTUAL_COMMIT="$(git rev-parse HEAD)"
|
||||
if [ "${ACTUAL_COMMIT}" != "${ROCKSDB_COMMIT}" ]; then
|
||||
echo "!!! RocksDB ${ROCKSDB_TAG} resolved to ${ACTUAL_COMMIT}, expected ${ROCKSDB_COMMIT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 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 \
|
||||
@@ -54,12 +76,28 @@ make install-shared PREFIX="${INSTALL_PREFIX}"
|
||||
# consumers see a path that actually exists on disk.
|
||||
PC_FILE="${INSTALL_PREFIX}/lib/pkgconfig/rocksdb.pc"
|
||||
if [ -f "${PC_FILE}" ]; then
|
||||
# Strip the -std=c++XX flag RocksDB writes into Cflags. The flag is
|
||||
# for the rocksdb .cc files themselves, but pkg-config injects it
|
||||
# into every Triangles translation unit — including C files like
|
||||
# src/lz4/lz4.c, which clang refuses to compile with
|
||||
# "invalid argument '-std=c++XX' not allowed with 'C'".
|
||||
# RocksDB 8.x wrote -std=c++17; 10.x bumped to -std=c++20; 11.x is
|
||||
# expected to use -std=c++2b. The regex below strips the whole
|
||||
# family so this fix survives future bumps.
|
||||
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' \
|
||||
-e 's|-std=c++[0-9a-z]\+ ||g' \
|
||||
-e 's|-std=c++[0-9a-z]\+$||g' \
|
||||
"${PC_FILE}"
|
||||
# Sanity: any remaining -std=c++ token means a future RocksDB release
|
||||
# wrote a new variant our regex didn't cover. Fail loudly so the CI
|
||||
# fuzz job doesn't surprise us downstream — fix the regex here.
|
||||
if grep -q -- '-std=c++' "${PC_FILE}"; then
|
||||
echo "!!! rocksdb.pc still contains -std=c++ after stripping:" >&2
|
||||
grep -- '-std=c++' "${PC_FILE}" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
ldconfig
|
||||
@@ -83,4 +121,4 @@ 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"
|
||||
ls -l "${INSTALL_PREFIX}/lib/librocksdb.so"* "${INSTALL_PREFIX}/include/rocksdb/version.h"
|
||||
|
||||
@@ -15,6 +15,7 @@ set -euo pipefail
|
||||
VERSION="${1:-0.0.0}"
|
||||
PKG="cryptographic-triangles-daemon_${VERSION}_amd64"
|
||||
TOR_VERSION="${TOR_VERSION:-15.0.9}"
|
||||
TOR_SHA256="${TOR_SHA256:-7ea13e14cddafb36c6347a9c4f4e639f6010364c16acfd519157c29e226277f2}"
|
||||
|
||||
echo ">>> Building .deb for triangles ${VERSION}"
|
||||
|
||||
@@ -30,8 +31,16 @@ mkdir -p "${PKG}/etc/systemd/system"
|
||||
TOR_TARBALL="tor-expert-bundle-linux-x86_64-${TOR_VERSION}.tar.gz"
|
||||
if [ ! -f "${TOR_TARBALL}" ]; then
|
||||
echo ">>> Downloading Tor ${TOR_VERSION}..."
|
||||
curl -sL "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/${TOR_TARBALL}" -o "${TOR_TARBALL}"
|
||||
# Resilient download: archive.torproject.org occasionally times out from
|
||||
# CI egress (observed 2026-07-03: macOS job exit code 6 after exactly 30s
|
||||
# of curl hang). --retry 3 + --retry-connrefused covers transient network
|
||||
# drops; --fail-with-body surfaces HTTP errors loudly.
|
||||
curl -fSL --connect-timeout 15 --max-time 120 \
|
||||
--retry 3 --retry-delay 5 --retry-connrefused --retry-all-errors \
|
||||
"https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/${TOR_TARBALL}" \
|
||||
-o "${TOR_TARBALL}"
|
||||
fi
|
||||
printf '%s %s\n' "${TOR_SHA256}" "${TOR_TARBALL}" | sha256sum --check --strict -
|
||||
mkdir -p tor-extract
|
||||
tar -xzf "${TOR_TARBALL}" -C tor-extract
|
||||
|
||||
@@ -100,10 +109,35 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=triangles
|
||||
Group=triangles
|
||||
UMask=0077
|
||||
Environment=HOME=/var/lib/triangles
|
||||
Environment=LD_LIBRARY_PATH=/usr/lib/cryptographic-triangles/lib
|
||||
ExecStart=/usr/lib/cryptographic-triangles/trianglesd
|
||||
StateDirectory=triangles
|
||||
StateDirectoryMode=0700
|
||||
WorkingDirectory=/var/lib/triangles
|
||||
ExecStart=/usr/lib/cryptographic-triangles/trianglesd -datadir=/var/lib/triangles -conf=/etc/triangles/triangles.conf -printtoconsole
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
NoNewPrivileges=true
|
||||
PrivateDevices=true
|
||||
PrivateTmp=true
|
||||
ProtectClock=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHome=true
|
||||
ProtectHostname=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/lib/triangles
|
||||
CapabilityBoundingSet=
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
SystemCallArchitectures=native
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -120,11 +154,41 @@ Description: Cryptographic Triangles daemon + CLI with integrated Tor
|
||||
Tor, and systemd service. No external dependencies required.
|
||||
Section: finance
|
||||
Priority: optional
|
||||
Depends: adduser
|
||||
CTRL
|
||||
|
||||
# DEBIAN/postinst
|
||||
cat > "${PKG}/DEBIAN/postinst" << 'POST'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if ! getent group triangles >/dev/null; then
|
||||
addgroup --system triangles
|
||||
fi
|
||||
if ! id triangles >/dev/null 2>&1; then
|
||||
adduser --system --ingroup triangles --home /var/lib/triangles \
|
||||
--no-create-home --disabled-login triangles
|
||||
fi
|
||||
|
||||
install -d -m 0700 -o triangles -g triangles /var/lib/triangles
|
||||
install -d -m 0750 -o root -g triangles /etc/triangles
|
||||
|
||||
if [ ! -e /etc/triangles/triangles.conf ]; then
|
||||
RPC_PASSWORD="$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"
|
||||
CONFIG_TMP="$(mktemp)"
|
||||
trap 'rm -f "${CONFIG_TMP}"' EXIT
|
||||
cat > "${CONFIG_TMP}" << CONF
|
||||
server=1
|
||||
rpcuser=trianglesrpc
|
||||
rpcpassword=${RPC_PASSWORD}
|
||||
rpcbind=127.0.0.1
|
||||
rpcallowip=127.0.0.1
|
||||
rest=0
|
||||
upnp=0
|
||||
CONF
|
||||
install -m 0640 -o root -g triangles "${CONFIG_TMP}" /etc/triangles/triangles.conf
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
echo ""
|
||||
echo "Cryptographic Triangles daemon + CLI installed."
|
||||
|
||||
Executable
+222
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env bash
|
||||
# sign-release.sh
|
||||
#
|
||||
# Sign Triangles release artifacts (the binaries/.debs/.dmgs/.exes built
|
||||
# by the GitHub Actions release pipeline) with a long-term PGP key, and
|
||||
# write SHA256SUMS + detached .asc signatures alongside each artifact.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/sign-release.sh /path/to/release-dir
|
||||
# scripts/sign-release.sh /path/to/release-dir --key 0xDEADBEEF
|
||||
# scripts/sign-release.sh --verify /path/to/release-dir
|
||||
#
|
||||
# Inputs (in the release directory):
|
||||
# - *.tar.gz, *.deb, *.dmg, *.exe, *.zip, *.AppImage (any release artifact)
|
||||
# - SHA256SUMS file (if present, re-signed; if absent, generated)
|
||||
#
|
||||
# Outputs (written next to each artifact):
|
||||
# - <artifact>.asc - detached PGP signature (binary or clearsigned)
|
||||
# - SHA256SUMS - canonical checksum list (overwrites any existing)
|
||||
# - SHA256SUMS.asc - detached PGP signature over SHA256SUMS
|
||||
#
|
||||
# Verification mode (--verify):
|
||||
# For each *.asc, runs `gpg --verify` against the artifact.
|
||||
# Then runs `sha256sum -c SHA256SUMS` if present.
|
||||
# Exits 0 if all artifacts verify; non-zero on any failure.
|
||||
#
|
||||
# Requirements:
|
||||
# - gpg2 or gpg on PATH
|
||||
# - Signing key already in the local keyring (or use --key to select)
|
||||
# - For verification: the signer's public key must be importable
|
||||
# (either already in the keyring, or fetched from a keyserver)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEFAULT_KEY="${TRIANGLES_RELEASE_KEY:-sami@cryptographic-triangles.org}"
|
||||
|
||||
usage() {
|
||||
sed -n '2,30p' "$0"
|
||||
exit "${1:-1}"
|
||||
}
|
||||
|
||||
# ── Parse args ─────────────────────────────────────────────────────────────
|
||||
MODE="sign"
|
||||
RELEASE_DIR=""
|
||||
SIGN_KEY="$DEFAULT_KEY"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--verify)
|
||||
MODE="verify"
|
||||
shift
|
||||
;;
|
||||
--key)
|
||||
SIGN_KEY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage 0
|
||||
;;
|
||||
*)
|
||||
RELEASE_DIR="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$RELEASE_DIR" ]; then
|
||||
echo "ERROR: release directory required" >&2
|
||||
usage 2
|
||||
fi
|
||||
|
||||
if [ ! -d "$RELEASE_DIR" ]; then
|
||||
echo "ERROR: not a directory: $RELEASE_DIR" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cd "$RELEASE_DIR"
|
||||
|
||||
# ── Sign mode ──────────────────────────────────────────────────────────────
|
||||
if [ "$MODE" = "sign" ]; then
|
||||
command -v gpg >/dev/null || { echo "ERROR: gpg not found" >&2; exit 3; }
|
||||
|
||||
# Verify the signing key actually exists in the keyring (don't want to
|
||||
# silently create a new key with the same email).
|
||||
if ! gpg --list-secret-keys "$SIGN_KEY" >/dev/null 2>&1; then
|
||||
echo "ERROR: signing key '$SIGN_KEY' not found in local keyring" >&2
|
||||
echo " import it first: gpg --import <keyfile>" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
echo "Signing artifacts in $RELEASE_DIR with key $SIGN_KEY..."
|
||||
|
||||
# Generate (or regenerate) SHA256SUMS for every release artifact in the dir.
|
||||
# Recognized extensions: .tar.gz, .deb, .dmg, .exe, .zip, .AppImage, .dmg.blockmap
|
||||
# Excludes: .asc files, SHA256SUMS itself, README/notes text files.
|
||||
ARTIFACTS=()
|
||||
while IFS= read -r -d '' f; do
|
||||
case "$f" in
|
||||
*.asc|SHA256SUMS|SHA256SUMS.asc|*.txt|*.md) continue ;;
|
||||
esac
|
||||
ARTIFACTS+=("$f")
|
||||
done < <(find . -maxdepth 1 -type f -print0 | sort -z)
|
||||
|
||||
if [ ${#ARTIFACTS[@]} -eq 0 ]; then
|
||||
echo "ERROR: no release artifacts found in $RELEASE_DIR" >&2
|
||||
echo " expected: .tar.gz, .deb, .dmg, .exe, .zip, .AppImage" >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
echo " Found ${#ARTIFACTS[@]} artifact(s):"
|
||||
for a in "${ARTIFACTS[@]}"; do echo " - $a"; done
|
||||
echo ""
|
||||
|
||||
# Regenerate SHA256SUMS from scratch (deterministic sort).
|
||||
: > SHA256SUMS
|
||||
for a in "${ARTIFACTS[@]}"; do
|
||||
sha256sum "$a" >> SHA256SUMS
|
||||
done
|
||||
echo "✓ Wrote SHA256SUMS"
|
||||
|
||||
# Detached signature over each artifact.
|
||||
for a in "${ARTIFACTS[@]}"; do
|
||||
rm -f "${a}.asc"
|
||||
if gpg --batch --yes \
|
||||
--local-user "$SIGN_KEY" \
|
||||
--armor --detach-sign \
|
||||
--output "${a}.asc" \
|
||||
"$a" 2>/dev/null; then
|
||||
echo "✓ Signed ${a}"
|
||||
else
|
||||
echo "✗ Failed to sign ${a}" >&2
|
||||
exit 5
|
||||
fi
|
||||
done
|
||||
|
||||
# Detached signature over SHA256SUMS (this is what verifiers actually check
|
||||
# first; individual .asc files are belt-and-suspenders).
|
||||
rm -f SHA256SUMS.asc
|
||||
if gpg --batch --yes \
|
||||
--local-user "$SIGN_KEY" \
|
||||
--armor --detach-sign \
|
||||
--output SHA256SUMS.asc \
|
||||
SHA256SUMS 2>/dev/null; then
|
||||
echo "✓ Signed SHA256SUMS"
|
||||
else
|
||||
echo "✗ Failed to sign SHA256SUMS" >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done. To verify from this directory:"
|
||||
echo " gpg --verify SHA256SUMS.asc SHA256SUMS"
|
||||
echo " sha256sum -c SHA256SUMS"
|
||||
echo ""
|
||||
echo "Or run: $0 --verify $RELEASE_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Verify mode ───────────────────────────────────────────────────────────
|
||||
if [ "$MODE" = "verify" ]; then
|
||||
command -v gpg >/dev/null || { echo "ERROR: gpg not found" >&2; exit 3; }
|
||||
|
||||
FAILED=0
|
||||
|
||||
echo "Verifying signatures in $RELEASE_DIR..."
|
||||
echo ""
|
||||
|
||||
# Verify SHA256SUMS.asc if present (this is the master signature).
|
||||
if [ -f SHA256SUMS ] && [ -f SHA256SUMS.asc ]; then
|
||||
if gpg --verify SHA256SUMS.asc SHA256SUMS 2>/dev/null; then
|
||||
echo "✓ SHA256SUMS signature: VALID ($(gpg --list-packets < SHA256SUMS.asc 2>/dev/null | grep -oP 'keyid \K[A-F0-9]+' | head -1 || echo unknown))"
|
||||
else
|
||||
echo "✗ SHA256SUMS signature: INVALID"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
else
|
||||
echo "(no SHA256SUMS / SHA256SUMS.asc; skipping master signature)"
|
||||
fi
|
||||
|
||||
# Verify each artifact's individual signature.
|
||||
while IFS= read -r -d '' asc; do
|
||||
artifact="${asc%.asc}"
|
||||
if [ ! -f "$artifact" ]; then
|
||||
echo "✗ $asc: artifact missing ($artifact)"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
if gpg --verify "$asc" "$artifact" 2>/dev/null; then
|
||||
echo "✓ $artifact signature: VALID"
|
||||
else
|
||||
echo "✗ $artifact signature: INVALID"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done < <(find . -maxdepth 1 -name "*.asc" -not -name "SHA256SUMS.asc" -print0 | sort -z)
|
||||
|
||||
# Verify checksums.
|
||||
if [ -f SHA256SUMS ]; then
|
||||
echo ""
|
||||
echo "Verifying checksums..."
|
||||
if sha256sum -c SHA256SUMS 2>&1 | tail -n +3; then
|
||||
: # sha256sum -c outputs per-file status; aggregate below
|
||||
fi
|
||||
# Count any "FAILED" lines from sha256sum -c output.
|
||||
CHECKSUM_FAILS="$(sha256sum -c SHA256SUMS 2>&1 | grep -c ': FAILED' || true)"
|
||||
if [ "$CHECKSUM_FAILS" -gt 0 ]; then
|
||||
echo "✗ $CHECKSUM_FAILS checksum(s) FAILED"
|
||||
FAILED=$((FAILED + CHECKSUM_FAILS))
|
||||
else
|
||||
echo "✓ All checksums match SHA256SUMS"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
echo "✓ ALL VERIFICATIONS PASSED"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ $FAILED VERIFICATION(S) FAILED"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# tri-pi-test.sh — Run Triangles on emulated Raspberry Pi variants via QEMU
|
||||
#
|
||||
# Usage:
|
||||
# ./tri-pi-test.sh [pi-model] [tri-args...]
|
||||
#
|
||||
# Pi models supported (aarch64):
|
||||
# pi3 Pi 3B/3A+ (Cortex-A53, 64-bit) — user-mode QEMU
|
||||
# pi4 Pi 4B (Cortex-A72, 64-bit) — user-mode QEMU
|
||||
# pi5 Pi 5 (Cortex-A76, 64-bit) — user-mode QEMU
|
||||
# pi3-full Pi 3B — full system emulation (qemu-system-aarch64 -M raspi3b)
|
||||
#
|
||||
# Examples:
|
||||
# ./tri-pi-test.sh pi3 --version
|
||||
# ./tri-pi-test.sh pi4 -regtest -notor -recovery-mode=1 -printtoconsole
|
||||
# ./tri-pi-test.sh pi3-full # boots a full Pi OS (needs rootfs image)
|
||||
#
|
||||
# The aarch64 tri binaries are cross-compiled on DNS2 and run under
|
||||
# qemu-aarch64-static. This tests the ARM binary's correctness — ABI
|
||||
# compatibility, library resolution, crypto operations, database access,
|
||||
# and Tor integration — without needing physical Pi hardware.
|
||||
#
|
||||
# For full-system emulation (testing kernel/hardware/driver interaction),
|
||||
# use pi3-full mode with a Raspberry Pi OS rootfs.
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PI_MODEL="${1:-pi3}"
|
||||
shift || true
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TRI_SRC="/root/triangles_v5"
|
||||
TRI_AARCH64_BIN="${TRI_SRC}/build-aarch64/bin/trianglesd"
|
||||
TRI_AARCH64_CLI="${TRI_SRC}/build-aarch64/bin/triangles-cli"
|
||||
QEMU_USER="/usr/bin/qemu-aarch64-static"
|
||||
QEMU_SYS="/usr/bin/qemu-system-aarch64"
|
||||
ARM_SYSROOT="/usr/aarch64-linux-gnu"
|
||||
|
||||
# Verify binary exists
|
||||
if [[ ! -f "$TRI_AARCH64_BIN" ]]; then
|
||||
echo "ERROR: aarch64 trianglesd not found at $TRI_AARCH64_BIN" >&2
|
||||
echo "Build it with: cd $TRI_SRC && cmake --build build-aarch64 --target trianglesd" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_user_mode() {
|
||||
local binary="$1"
|
||||
shift
|
||||
local model_name="$1"
|
||||
shift
|
||||
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Triangles on Raspberry Pi ${model_name} (QEMU user-mode) ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Binary: $(file "$binary" | cut -d: -f2)"
|
||||
echo "QEMU: $($QEMU_USER --version | head -1)"
|
||||
echo "Args: $*"
|
||||
echo ""
|
||||
|
||||
# QEMU user-mode runs the ARM binary with the host kernel but ARM user-space
|
||||
# -L sets the sysroot for dynamic linker/library resolution
|
||||
exec "$QEMU_USER" -L "$ARM_SYSROOT" "$binary" "$@"
|
||||
}
|
||||
|
||||
run_full_system_pi3() {
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Triangles on Raspberry Pi 3B (QEMU full-system) ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
|
||||
local IMG_DIR="${TRI_SRC}/pi-emulation/images"
|
||||
local KERNEL="${IMG_DIR}/kernel8.img"
|
||||
local DTB="${IMG_DIR}/bcm2710-rpi-3-b.dtb"
|
||||
local ROOTFS="${IMG_DIR}/raspios-trixie-arm64.img"
|
||||
local OVERLAY="/tmp/tri-pi3-overlay.qcow2"
|
||||
|
||||
if [[ ! -f "$KERNEL" ]] || [[ ! -f "$ROOTFS" ]]; then
|
||||
echo "ERROR: Pi 3 full-system images not found in $IMG_DIR" >&2
|
||||
echo "" >&2
|
||||
echo "To set up full-system emulation:" >&2
|
||||
echo " 1. Download Raspberry Pi OS Lite (64-bit) from raspberrypi.com" >&2
|
||||
echo " 2. Extract kernel8.img from the boot partition" >&2
|
||||
echo " 3. Get the DTB: bcm2710-rpi-3-b.dtb from the boot partition" >&2
|
||||
echo " 4. Place all in: $IMG_DIR/" >&2
|
||||
echo "" >&2
|
||||
echo "User-mode testing (default) works without these files." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create overlay so we don't modify the base image
|
||||
qemu-img create -f qcow2 -b "$ROOTFS" "$OVERLAY" 2>/dev/null || true
|
||||
|
||||
exec "$QEMU_SYS" \
|
||||
-M raspi3b \
|
||||
-kernel "$KERNEL" \
|
||||
-dtb "$DTB" \
|
||||
-drive "file=$OVERLAY,if=sd,format=qcow2" \
|
||||
-m 1G \
|
||||
-smp 4 \
|
||||
-nographic \
|
||||
-append "console=ttyAMA0 root=/dev/mmcblk0p2 rootwait rw quiet"
|
||||
}
|
||||
|
||||
case "$PI_MODEL" in
|
||||
pi3|pi4|pi5)
|
||||
# All three use the same aarch64 binary — the binary is
|
||||
# architecture-compatible across Cortex-A53/A72/A76.
|
||||
# The model name documents which hardware variant is being simulated.
|
||||
run_user_mode "$TRI_AARCH64_BIN" "$PI_MODEL (Cortex-A*)"
|
||||
"$@"
|
||||
;;
|
||||
pi3-cli|pi4-cli|pi5-cli)
|
||||
run_user_mode "$TRI_AARCH64_CLI" "$PI_MODEL CLI" "$@"
|
||||
;;
|
||||
pi3-full)
|
||||
run_full_system_pi3
|
||||
;;
|
||||
*)
|
||||
echo "Unknown model: $PI_MODEL" >&2
|
||||
echo "Supported: pi3, pi4, pi5, pi3-cli, pi4-cli, pi5-cli, pi3-full" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env bash
|
||||
# verify-reproducible-build.sh
|
||||
#
|
||||
# Builds the Triangles daemon (trianglesd) twice from the same source tree
|
||||
# into two separate build directories, then compares the resulting
|
||||
# SHA256 hashes. Exits 0 if the two builds produce byte-identical binaries,
|
||||
# non-zero otherwise.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/verify-reproducible-build.sh # default: trianglesd, Release
|
||||
# BUILD_TYPE=Debug scripts/verify-reproducible-build.sh # override build type
|
||||
# TARGET=triangles-qt scripts/verify-reproducible-build.sh # build Qt wallet instead
|
||||
#
|
||||
# What "reproducible" means here:
|
||||
# Given identical source tree, identical compiler toolchain, identical
|
||||
# build flags, identical SOURCE_DATE_EPOCH (if set) -- the resulting
|
||||
# binary must hash identically across separate build directories.
|
||||
#
|
||||
# This script does NOT enforce compiler version pinning. Two different
|
||||
# GCC versions will legitimately produce different binaries even with
|
||||
# identical flags. The verification is "same source + same toolchain =
|
||||
# same binary."
|
||||
#
|
||||
# Pass criteria:
|
||||
# 1. Both builds succeed
|
||||
# 2. Both binaries exist
|
||||
# 3. SHA256 of the two binaries is equal
|
||||
#
|
||||
# On failure: prints the two SHA256s and the diff in size so a reviewer
|
||||
# can investigate. Common causes of non-determinism:
|
||||
# - __DATE__/__TIME__ embedded (we eliminate this in CMakeLists.txt)
|
||||
# - absolute paths in __FILE__ (mitigated by -ffile-prefix-map)
|
||||
# - uninitialized stack/heap contents (should not affect final binary)
|
||||
# - linker adds random base addresses (PIE; deterministic if compiled
|
||||
# with -fno-pie)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Config ─────────────────────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCE_DIR="${SOURCE_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||
BUILD_TYPE="${BUILD_TYPE:-Release}"
|
||||
TARGET="${TARGET:-trianglesd}"
|
||||
# Skip Qt by default -- it's slow and adds CI noise. Override with TARGET=triangles-qt
|
||||
: "${BUILD_QT:=OFF}"
|
||||
BUILD_DIR_A="${BUILD_DIR_A:-/tmp/triangles-repro-A}"
|
||||
BUILD_DIR_B="${BUILD_DIR_B:-/tmp/triangles-repro-B}"
|
||||
LOG_A="${LOG_A:-/tmp/triangles-repro-A.log}"
|
||||
LOG_B="${LOG_B:-/tmp/triangles-repro-B.log}"
|
||||
|
||||
# ── Preflight ──────────────────────────────────────────────────────────────
|
||||
command -v cmake >/dev/null || { echo "ERROR: cmake not found" >&2; exit 2; }
|
||||
command -v ninja >/dev/null || { echo "ERROR: ninja not found (apt install ninja-build)" >&2; exit 2; }
|
||||
command -v sha256sum >/dev/null || { echo "ERROR: sha256sum not found" >&2; exit 2; }
|
||||
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "ERROR: source dir not found: $SOURCE_DIR" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Warn if tracked files are dirty -- git describe --dirty (used by build.h)
|
||||
# ignores untracked files, but includes modified/staged tracked files in the
|
||||
# version string. Untracked notes/build outputs are safe and should not scare
|
||||
# release builders.
|
||||
if [ -n "$(cd "$SOURCE_DIR" && git status --porcelain --untracked-files=no 2>/dev/null)" ]; then
|
||||
echo "WARNING: tracked working tree changes detected." >&2
|
||||
echo " build.h will include a '-dirty' suffix, so the binary will not" >&2
|
||||
echo " match a clean checkout/tag. Commit or stash tracked changes first." >&2
|
||||
fi
|
||||
|
||||
# ── Embedded sub-libraries (Tor, I2P) ─────────────────────────────────────
|
||||
# The daemon statically links libtor.a and libi2pd*.a; both must exist
|
||||
# before cmake's link step. On a fresh checkout they need to be built from
|
||||
# the embedded submodules. CI does this in build-all.yml before the main
|
||||
# build; this script does the same so a local `scripts/verify-reproducible-build.sh`
|
||||
# works out of the box.
|
||||
TOR_LIB="$SOURCE_DIR/src/tor/tor-src/libtor.a"
|
||||
I2P_LIBS=(
|
||||
"$SOURCE_DIR/src/i2p/i2pd-src/libi2pd.a"
|
||||
"$SOURCE_DIR/src/i2p/i2pd-src/libi2pdclient.a"
|
||||
"$SOURCE_DIR/src/i2p/i2pd-src/libi2pdlang.a"
|
||||
)
|
||||
NEED_TOR_BUILD=0
|
||||
NEED_I2P_BUILD=0
|
||||
[ -f "$TOR_LIB" ] || NEED_TOR_BUILD=1
|
||||
for lib in "${I2P_LIBS[@]}"; do [ -f "$lib" ] || NEED_I2P_BUILD=1; done
|
||||
|
||||
if [ "$NEED_TOR_BUILD" = "1" ]; then
|
||||
echo "Building libtor.a (one-time, ~5 min)..." >&2
|
||||
# CI passes /usr paths for native Linux; defaults in build-libtor.sh
|
||||
# are MINGW64 cross-compile paths.
|
||||
LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \
|
||||
bash "$SOURCE_DIR/src/tor/build-libtor.sh" \
|
||||
> /tmp/triangles-build-libtor.log 2>&1 \
|
||||
|| { echo "ERROR: libtor build failed; see /tmp/triangles-build-libtor.log" >&2; exit 5; }
|
||||
fi
|
||||
if [ "$NEED_I2P_BUILD" = "1" ]; then
|
||||
echo "Building libi2pd*.a (one-time, ~3 min)..." >&2
|
||||
bash "$SOURCE_DIR/src/i2p/build-libi2pd.sh" \
|
||||
> /tmp/triangles-build-libi2pd.log 2>&1 \
|
||||
|| { echo "ERROR: libi2pd build failed; see /tmp/triangles-build-libi2pd.log" >&2; exit 5; }
|
||||
fi
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||
build_one() {
|
||||
local dir="$1" log="$2"
|
||||
rm -rf "$dir"
|
||||
mkdir -p "$dir"
|
||||
echo " configuring in $dir (BUILD_TYPE=$BUILD_TYPE BUILD_QT=$BUILD_QT)..." >&2
|
||||
cmake -S "$SOURCE_DIR" -B "$dir" \
|
||||
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
|
||||
-DBUILD_QT="$BUILD_QT" \
|
||||
> "$log" 2>&1 || { echo " configure failed; see $log" >&2; tail -30 "$log" >&2; exit 3; }
|
||||
echo " building target $TARGET..." >&2
|
||||
cmake --build "$dir" --target "$TARGET" -j "$(nproc)" \
|
||||
>> "$log" 2>&1 || { echo " build failed; see $log" >&2; tail -30 "$log" >&2; exit 3; }
|
||||
# ONLY stdout of the find goes to the caller. Progress logs above
|
||||
# were redirected to stderr so they don't pollute the captured path.
|
||||
find "$dir" -name "$TARGET" -type f -executable | head -1
|
||||
}
|
||||
|
||||
# ── Build twice ────────────────────────────────────────────────────────────
|
||||
echo "Building $TARGET ($BUILD_TYPE) twice from $SOURCE_DIR..."
|
||||
echo ""
|
||||
BIN_A="$(build_one "$BUILD_DIR_A" "$LOG_A")"
|
||||
BIN_B="$(build_one "$BUILD_DIR_B" "$LOG_B")"
|
||||
|
||||
if [ -z "$BIN_A" ] || [ -z "$BIN_B" ]; then
|
||||
echo "ERROR: could not find built binary" >&2
|
||||
echo " A: '$BIN_A'" >&2
|
||||
echo " B: '$BIN_B'" >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
# ── Compare ────────────────────────────────────────────────────────────────
|
||||
HASH_A="$(sha256sum "$BIN_A" | awk '{print $1}')"
|
||||
HASH_B="$(sha256sum "$BIN_B" | awk '{print $1}')"
|
||||
SIZE_A="$(stat -c%s "$BIN_A" 2>/dev/null || stat -f%z "$BIN_A")"
|
||||
SIZE_B="$(stat -c%s "$BIN_B" 2>/dev/null || stat -f%z "$BIN_B")"
|
||||
|
||||
echo ""
|
||||
echo "Binary A: $BIN_A"
|
||||
echo " sha256: $HASH_A"
|
||||
echo " size: $SIZE_A bytes"
|
||||
echo "Binary B: $BIN_B"
|
||||
echo " sha256: $HASH_B"
|
||||
echo " size: $SIZE_B bytes"
|
||||
echo ""
|
||||
|
||||
if [ "$HASH_A" = "$HASH_B" ]; then
|
||||
echo "✓ REPRODUCIBLE: both builds produced identical SHA256"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ NOT REPRODUCIBLE: hashes differ"
|
||||
echo ""
|
||||
echo "Likely causes:"
|
||||
echo " - __DATE__/__TIME__ embedded (check src/version.cpp)"
|
||||
echo " - absolute build paths in __FILE__ (check CMakeLists.txt for -ffile-prefix-map)"
|
||||
echo " - dirty git tree (commit/stash and rerun)"
|
||||
echo " - PIE base randomization (compile with -fno-pie -no-pie for testing)"
|
||||
echo " - non-deterministic linker output (linker version mismatch)"
|
||||
exit 1
|
||||
fi
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
name: triangles
|
||||
base: core22
|
||||
version: '5.9.24'
|
||||
version: '6.2.4'
|
||||
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.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-qt
|
||||
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.2.4/Cryptographic-Triangles-v6.2.4-linux-x64-qt
|
||||
source-type: file
|
||||
organize:
|
||||
Cryptographic-Triangles-v5.9.24-linux-x64-qt: bin/triangles-qt
|
||||
Cryptographic-Triangles-v6.2.4-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.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-daemon
|
||||
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.2.4/Cryptographic-Triangles-v6.2.4-linux-x64-daemon
|
||||
source-type: file
|
||||
organize:
|
||||
Cryptographic-Triangles-v5.9.24-linux-x64-daemon: bin/trianglesd
|
||||
Cryptographic-Triangles-v6.2.4-linux-x64-daemon: bin/trianglesd
|
||||
|
||||
desktop-entry:
|
||||
plugin: dump
|
||||
|
||||
+512
-9
@@ -108,8 +108,34 @@ 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})
|
||||
|
||||
# When BUILD_FUZZ=ON, the fuzz target links these .o files directly into
|
||||
# bin/fuzz_script. The link line enables -fsanitize=fuzzer,address,undefined
|
||||
# so EVERY .o referenced from the fuzz binary must also be compiled with the
|
||||
# matching -fsanitize=address,undefined,fuzzer-no-link. Without this, gcc-
|
||||
# built triangles_common objects reference libstdc++-injected ubsan runtime
|
||||
# symbols (e.g. __ubsan_handle_function_type_mismatch_v1_abort) that clang's
|
||||
# libubsan_standalone runtime doesn't provide, and the link fails with
|
||||
# "undefined reference to __ubsan_handle_function_type_mismatch_v1_abort".
|
||||
if(BUILD_FUZZ)
|
||||
target_compile_options(triangles_common PRIVATE
|
||||
-fsanitize=address,undefined,fuzzer-no-link
|
||||
-fno-omit-frame-pointer
|
||||
-fno-sanitize-recover=undefined
|
||||
-fno-sanitize=alignment,signed-integer-overflow,vptr
|
||||
)
|
||||
endif()
|
||||
|
||||
target_include_directories(triangles_common PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/json"
|
||||
@@ -126,13 +152,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
|
||||
@@ -337,12 +361,36 @@ target_precompile_headers(triangles_common PRIVATE
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 4. Headless daemon (trianglesd)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
if(BUILD_DAEMON)
|
||||
add_executable(trianglesd
|
||||
noui.cpp
|
||||
init.cpp
|
||||
wallet.cpp
|
||||
# `trianglesd` is normally an add_executable, but the libFuzzer build only
|
||||
# needs the daemon's object files (init/wallet/noui). Building the executable
|
||||
# under clang-15 with -fsanitize=fuzzer+address+undefined pulls in
|
||||
# undefined references to the libstdc++ runtime built by gcc, which fails
|
||||
# the link step. So we expose the daemon's sources as an OBJECT library and
|
||||
# only attach them to trianglesd when we're not in a fuzz build.
|
||||
set(DAEMON_SOURCES
|
||||
noui.cpp
|
||||
init.cpp
|
||||
wallet.cpp
|
||||
)
|
||||
if(BUILD_FUZZ)
|
||||
add_library(trianglesd_objects OBJECT ${DAEMON_SOURCES})
|
||||
target_link_libraries(trianglesd_objects PRIVATE triangles_common)
|
||||
target_precompile_headers(trianglesd_objects REUSE_FROM triangles_common)
|
||||
# Match triangles_common's sanitizer instrumentation so noui.cpp / init.cpp
|
||||
# / wallet.cpp .o files don't reference the gcc libstdc++ ubsan runtime
|
||||
# when linked into the fuzz binary (see triangles_common compile-options
|
||||
# comment above for the full rationale).
|
||||
target_compile_options(trianglesd_objects PRIVATE
|
||||
-fsanitize=address,undefined,fuzzer-no-link
|
||||
-fno-omit-frame-pointer
|
||||
-fno-sanitize-recover=undefined
|
||||
-fno-sanitize=alignment,signed-integer-overflow,vptr
|
||||
)
|
||||
if(WIN32)
|
||||
set_target_properties(trianglesd_objects PROPERTIES SUFFIX ".obj")
|
||||
endif()
|
||||
elseif(BUILD_DAEMON)
|
||||
add_executable(trianglesd ${DAEMON_SOURCES})
|
||||
# No QT_GUI define — daemon gets the #if !defined(QT_GUI) code paths
|
||||
target_link_libraries(trianglesd PRIVATE triangles_common)
|
||||
target_precompile_headers(trianglesd REUSE_FROM triangles_common)
|
||||
@@ -443,6 +491,7 @@ if(BUILD_QT)
|
||||
qt/qvaluecombobox.cpp
|
||||
qt/askpassphrasedialog.cpp
|
||||
qt/hdseeddialog.cpp
|
||||
qt/outlinedlabel.cpp
|
||||
qt/notificator.cpp
|
||||
qt/qtipcserver.cpp
|
||||
qt/rpcconsole.cpp
|
||||
@@ -522,6 +571,7 @@ if(BUILD_QT)
|
||||
Qt5::Core
|
||||
Qt5::Gui
|
||||
Qt5::Widgets
|
||||
Qt5::Network
|
||||
)
|
||||
|
||||
# Optional: D-Bus notifications (Linux)
|
||||
@@ -588,6 +638,15 @@ if(BUILD_TESTS)
|
||||
# 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$")
|
||||
# These two are standalone test drivers: each #defines its own
|
||||
# BOOST_TEST_MODULE and redefines the wallet/UI globals, and each has
|
||||
# a dedicated executable + add_test below. They must NOT also be
|
||||
# globbed into test_triangles, or the duplicate module/main and global
|
||||
# symbols only link by virtue of -Wl,--allow-multiple-definition (which
|
||||
# silently drops duplicates and can run their suites under the wrong
|
||||
# global fixture). Excluding them keeps each standalone module isolated.
|
||||
list(FILTER TEST_SOURCES EXCLUDE REGEX "chaindb_runtime_tests\\.cpp$")
|
||||
list(FILTER TEST_SOURCES EXCLUDE REGEX "snapshotnet_tests\\.cpp$")
|
||||
|
||||
add_executable(test_triangles
|
||||
${TEST_SOURCES}
|
||||
@@ -598,7 +657,7 @@ if(BUILD_TESTS)
|
||||
# No init.cpp — test_triangles.cpp provides its own StartShutdown() stub
|
||||
|
||||
target_compile_definitions(test_triangles PRIVATE
|
||||
"TEST_DATA_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/test/data\""
|
||||
"TEST_DATA_DIR=${CMAKE_CURRENT_SOURCE_DIR}/test/data"
|
||||
)
|
||||
|
||||
target_include_directories(test_triangles PRIVATE
|
||||
@@ -611,7 +670,16 @@ if(BUILD_TESTS)
|
||||
Boost::unit_test_framework
|
||||
)
|
||||
|
||||
# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}: the consensus_safety_tests
|
||||
# `reindex_reconstruction_is_explicit_and_fail_closed` test reads
|
||||
# src/init.cpp + src/main.cpp via __FILE__-relative path traversal
|
||||
# (3x parent_path() calls). When ctest runs from build/src/ (the
|
||||
# default CMAKE_CURRENT_BINARY_DIR for src/CMakeLists.txt), the
|
||||
# resolved path is build/src/src/init.cpp which doesn't exist.
|
||||
# Pinning WORKING_DIRECTORY to "${CMAKE_SOURCE_DIR}" makes the test
|
||||
# source paths resolve correctly from any environment.
|
||||
add_test(NAME triangles_unit_tests COMMAND test_triangles --log_level=test_suite)
|
||||
set_tests_properties(triangles_unit_tests PROPERTIES WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
# ── Standalone chaindb equivalence tests ─────────────────────────────────
|
||||
# Runs without the TestingSetup global fixture (which would otherwise
|
||||
@@ -679,3 +747,438 @@ if(BUILD_TESTS)
|
||||
add_test(NAME chaindb_runtime_tests
|
||||
COMMAND test_chaindb_runtime --log_level=test_suite)
|
||||
endif()
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 7. Fuzz harness (script interpreter) — opt-in via -DBUILD_FUZZ=ON
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# LibFuzzer is built into clang since version 6; gcc doesn't support
|
||||
# -fsanitize=fuzzer. We compile script_fuzz.cpp + script.cpp with clang++
|
||||
# (so the interpreter itself is ASan/UBSan-instrumented) and link against
|
||||
# the full triangles_common OBJECT library + the same library set trianglesd
|
||||
# uses. Default build (gcc, no sanitizer) is unaffected.
|
||||
#
|
||||
# Build:
|
||||
# cmake -G Ninja -DBUILD_TESTS=ON -DBUILD_FUZZ=ON -DBUILD_DAEMON=ON ..
|
||||
# ninja fuzz_script
|
||||
#
|
||||
# Run:
|
||||
# ./bin/fuzz_script -max_total_time=300 corpus/
|
||||
#
|
||||
# See src/test/fuzz/README.md for corpus seeding and what it covers.
|
||||
option(BUILD_FUZZ "Build libFuzzer harness for the script interpreter" OFF)
|
||||
if(BUILD_FUZZ)
|
||||
find_program(CLANGXX clang++)
|
||||
if(NOT CLANGXX)
|
||||
message(FATAL_ERROR "BUILD_FUZZ=ON requires clang++; not found in PATH")
|
||||
endif()
|
||||
|
||||
set(FUZZ_OBJ_DIR "${CMAKE_CURRENT_BINARY_DIR}/fuzz_objs")
|
||||
file(MAKE_DIRECTORY "${FUZZ_OBJ_DIR}")
|
||||
set(FUZZ_OBJ_SCRIPT_FUZZ "${FUZZ_OBJ_DIR}/script_fuzz.cpp.o")
|
||||
set(FUZZ_OBJ_SCRIPT "${FUZZ_OBJ_DIR}/script.cpp.o")
|
||||
set(FUZZ_OBJ_FUZZ_STUBS "${FUZZ_OBJ_DIR}/fuzz_stubs.cpp.o")
|
||||
set(FUZZ_FUZZ_STUBS_SRC "${FUZZ_OBJ_DIR}/fuzz_stubs.cpp")
|
||||
set(FUZZ_BIN_DIR "${CMAKE_BINARY_DIR}/bin")
|
||||
file(MAKE_DIRECTORY "${FUZZ_BIN_DIR}")
|
||||
set(FUZZ_BIN "${FUZZ_BIN_DIR}/fuzz_script")
|
||||
set(FUZZ_SRC_FUZZ "${CMAKE_CURRENT_SOURCE_DIR}/test/fuzz/script_fuzz.cpp")
|
||||
set(FUZZ_SRC_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/script.cpp")
|
||||
|
||||
# --- Second fuzz target: transaction_deserialize_fuzz ---
|
||||
# CTransaction is declared in main.h and implemented in main.cpp, which is
|
||||
# part of triangles_common. The harness only needs the transaction
|
||||
# deserialize/serialize surface, not the script interpreter, so we don't
|
||||
# need a separate clang-instrumented copy of any .cpp file — we just link
|
||||
# the gcc-built triangles_common .o files directly. libFuzzer's link line
|
||||
# is compatible with gcc .o files for the non-instrumented units; only the
|
||||
# harness entry point itself needs clang + -fsanitize=fuzzer.
|
||||
set(FUZZ_TX_DESER_OBJ "${FUZZ_OBJ_DIR}/transaction_deserialize_fuzz.cpp.o")
|
||||
set(FUZZ_TX_DESER_BIN_DIR "${CMAKE_BINARY_DIR}/bin")
|
||||
set(FUZZ_TX_DESER_BIN "${FUZZ_TX_DESER_BIN_DIR}/transaction_deserialize_fuzz")
|
||||
set(FUZZ_TX_DESER_SRC "${CMAKE_CURRENT_SOURCE_DIR}/test/fuzz/transaction_deserialize_fuzz.cpp")
|
||||
set(FUZZ_TX_DESER_LINK_WRAPPER "${FUZZ_OBJ_DIR}/link_txdeser.sh")
|
||||
set(FUZZ_TX_DESER_LINK_WRAPPER_CONTENT [=[#!/bin/bash
|
||||
# Auto-generated by CMake (BUILD_FUZZ block). Link wrapper for the
|
||||
# transaction_deserialize_fuzz target. Discovers triangles_common +
|
||||
# trianglesd .o files at link time and exec's the clang++ link line.
|
||||
#
|
||||
# Differs from link.sh: this wrapper does NOT exclude script.cpp.o, because
|
||||
# wallet.cpp.o (in trianglesd_objects) calls ExtractDestination,
|
||||
# SignSignature, Solver, IsMine — all defined in script.cpp.o. We only exclude
|
||||
# init.cpp.o (which defines daemon main(), would conflict with libFuzzer's
|
||||
# main). See the BUILD_FUZZ block in src/CMakeLists.txt for full rationale.
|
||||
#
|
||||
# Usage: link_txdeser.sh clang++ [link-args...]
|
||||
# Final exec: clang++ <each .o> <each original link-arg>
|
||||
set -euo pipefail
|
||||
PROG="$1"
|
||||
shift
|
||||
TRIANGLES_COMMON_DIR="@CMAKE_CURRENT_BINARY_DIR@/CMakeFiles/triangles_common.dir"
|
||||
TRIANGLESD_DIR="@CMAKE_CURRENT_BINARY_DIR@/CMakeFiles/trianglesd_objects.dir"
|
||||
declare -a OBJS=()
|
||||
for f in "$TRIANGLES_COMMON_DIR"/*.o "$TRIANGLES_COMMON_DIR"/*/*.o; do
|
||||
[ -f "$f" ] || continue
|
||||
OBJS+=("$f")
|
||||
done
|
||||
if [ -d "$TRIANGLESD_DIR" ]; then
|
||||
for f in "$TRIANGLESD_DIR"/*.o; do
|
||||
[ -f "$f" ] || continue
|
||||
case "$f" in
|
||||
*/init.cpp.o) continue ;;
|
||||
esac
|
||||
OBJS+=("$f")
|
||||
done
|
||||
fi
|
||||
exec "$PROG" "${OBJS[@]}" "$@"
|
||||
]=])
|
||||
string(CONFIGURE "${FUZZ_TX_DESER_LINK_WRAPPER_CONTENT}"
|
||||
FUZZ_TX_DESER_LINK_WRAPPER_CONTENT @ONLY)
|
||||
file(WRITE "${FUZZ_TX_DESER_LINK_WRAPPER}" "${FUZZ_TX_DESER_LINK_WRAPPER_CONTENT}")
|
||||
file(CHMOD "${FUZZ_TX_DESER_LINK_WRAPPER}" PERMISSIONS
|
||||
OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||
GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
||||
# Compile flags shared by both .cpp files. Pull in script.h, secp256k1,
|
||||
# leveldb. Same flags gcc uses for triangles_common (the project defines
|
||||
# HAVE_BUILD_INFO, LINUX, BOOST_THREAD_USE_LIB, etc.) so we don't hit
|
||||
# redefinition errors when linking against the rest of triangles_common.
|
||||
set(FUZZ_COMMON_FLAGS
|
||||
-std=c++20 -g -O1
|
||||
-fsanitize=fuzzer,address,undefined
|
||||
-DHAVE_CONFIG_H
|
||||
-DHAVE_BUILD_INFO
|
||||
-DLINUX
|
||||
-DUSE_IPV6=1
|
||||
-DBOOST_SPIRIT_THREADSAFE
|
||||
-DBOOST_THREAD_PROVIDES_GENERIC_SHARED_MUTEX_ON_WIN
|
||||
-DBOOST_THREAD_USE_LIB
|
||||
-DENABLE_TOR_EMBEDDED
|
||||
-DENABLE_I2P_EMBEDDED
|
||||
-DMINIUPNP_STATICLIB
|
||||
-DSTATICLIB
|
||||
-I${CMAKE_CURRENT_SOURCE_DIR}
|
||||
-I${CMAKE_CURRENT_SOURCE_DIR}/secp256k1/include
|
||||
-I${CMAKE_CURRENT_SOURCE_DIR}/leveldb/include
|
||||
-Wno-unused-parameter
|
||||
-Wno-deprecated-declarations
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${FUZZ_OBJ_SCRIPT_FUZZ}"
|
||||
COMMAND ${CLANGXX} ${FUZZ_COMMON_FLAGS}
|
||||
-c ${FUZZ_SRC_FUZZ} -o ${FUZZ_OBJ_SCRIPT_FUZZ}
|
||||
DEPENDS ${FUZZ_SRC_FUZZ}
|
||||
COMMENT "[fuzz] clang++ script_fuzz.cpp"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_command(
|
||||
OUTPUT "${FUZZ_OBJ_SCRIPT}"
|
||||
COMMAND ${CLANGXX} ${FUZZ_COMMON_FLAGS}
|
||||
-c ${FUZZ_SRC_SCRIPT} -o ${FUZZ_OBJ_SCRIPT}
|
||||
DEPENDS ${FUZZ_SRC_SCRIPT}
|
||||
COMMENT "[fuzz] clang++ script.cpp"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
# fuzz_stubs.cpp — satisfies globals owned by the excluded init.cpp that
|
||||
# triangles_common and trianglesd_objects reference (pwalletMain,
|
||||
# uiInterface, etc.). Keeping these as null/no-ops is the standard fuzzer
|
||||
# pattern — see src/test/test_triangles.cpp and
|
||||
# src/test/snapshotnet_tests.cpp for the same approach.
|
||||
file(MAKE_DIRECTORY "${FUZZ_OBJ_DIR}")
|
||||
file(WRITE "${FUZZ_FUZZ_STUBS_SRC}"
|
||||
"#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include \"checkpoints.h\"
|
||||
#include \"key.h\"
|
||||
#include \"keystore.h\"
|
||||
#include \"script.h\"
|
||||
#include \"ui_interface.h\"
|
||||
#include \"wallet.h\"
|
||||
|
||||
class CBlockIndex;
|
||||
|
||||
bool fUseFastIndex = false;
|
||||
unsigned int nDerivationMethodIndex = 0;
|
||||
bool fEnforceCanonical = true;
|
||||
bool fConfChange = false;
|
||||
|
||||
class CWalletStub : public CKeyStore
|
||||
{
|
||||
public:
|
||||
bool GetPubKey(const CKeyID&, CPubKey&) const override { return false; }
|
||||
bool GetKey(const CKeyID&, CKey&) const override { return false; }
|
||||
bool HaveKey(const CKeyID&) const override { return false; }
|
||||
void GetKeys(std::set<CKeyID>& setAddress) const override { setAddress.clear(); }
|
||||
bool AddKey(const CKey&) override { return false; }
|
||||
bool AddCScript(const CScript&) override { return false; }
|
||||
bool HaveCScript(const CScriptID&) const override { return false; }
|
||||
bool GetCScript(const CScriptID&, CScript&) const override { return false; }
|
||||
};
|
||||
static CWalletStub g_wallet_stub;
|
||||
CWallet* pwalletMain = nullptr;
|
||||
|
||||
CClientUIInterface uiInterface;
|
||||
|
||||
// Checkpoints::CPMode defined in checkpoints.h; default to ADVISORY so the
|
||||
// fuzz target never complains about the missing init.cpp value.
|
||||
enum Checkpoints::CPMode CheckpointsMode = Checkpoints::ADVISORY;
|
||||
|
||||
// Defined in init.cpp; reasonable default so the fuzz link succeeds.
|
||||
unsigned int nNodeLifespan = 7;
|
||||
|
||||
void StartShutdown() {}
|
||||
void MarkShutdownFailure() {}
|
||||
")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${FUZZ_OBJ_FUZZ_STUBS}"
|
||||
COMMAND ${CLANGXX} ${FUZZ_COMMON_FLAGS}
|
||||
-c ${FUZZ_FUZZ_STUBS_SRC} -o ${FUZZ_OBJ_FUZZ_STUBS}
|
||||
DEPENDS ${FUZZ_FUZZ_STUBS_SRC}
|
||||
COMMENT "[fuzz] clang++ fuzz_stubs.cpp"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
# Link using the same library set as trianglesd, but:
|
||||
# - exclude script.cpp.o (we provide our own clang-instrumented one)
|
||||
# - swap gcc for clang++ with -fsanitize=fuzzer,address,undefined
|
||||
# - drop -Wl,-z,relro -Wl,-z,now (incompatible with sanitizer link)
|
||||
# The triangles_common / trianglesd .o file lists are discovered at link
|
||||
# time via the FUZZ_LINK_WRAPPER shell script (defined below). We do NOT
|
||||
# use file(GLOB) here — it runs at configure time when no .o files exist
|
||||
# on a fresh build dir, so the resulting list would always be empty.
|
||||
# The wrapper script does the find at link time and exec's clang++.
|
||||
|
||||
# Build the link command. The triangles_common and trianglesd .o files
|
||||
# are discovered at link time via shell `find` because file(GLOB) only
|
||||
# runs at cmake configure time, when no .o files exist yet on a fresh
|
||||
# build dir. We invoke a small shell wrapper script that does the find
|
||||
# and exec's the link line with all .o files as args. We exclude
|
||||
# script.cpp.o from the triangles_common dir so we don't pull our
|
||||
# standalone copy of script.cpp in twice (we already have it in
|
||||
# ${FUZZ_OBJ_SCRIPT}).
|
||||
set(FUZZ_LINK_WRAPPER "${CMAKE_CURRENT_BINARY_DIR}/fuzz_objs/link.sh")
|
||||
# The wrapper script is invoked with the full link arg list as its
|
||||
# own argv. We pass it via ninja's COMMAND expansion with @{args}.
|
||||
# Strategy: write a here-doc style wrapper that uses bash-style
|
||||
# "$@" preservation. We use bash explicitly (not sh) for "$@" array
|
||||
# semantics — paths may contain spaces, so word-splitting on IFS
|
||||
# would corrupt them.
|
||||
set(FUZZ_LINK_WRAPPER_CONTENT [=[#!/bin/bash
|
||||
# Auto-generated by CMake (BUILD_FUZZ block). Discovers triangles_common +
|
||||
# trianglesd .o files at link time and exec's the clang++ link line.
|
||||
#
|
||||
# Usage: link.sh clang++ [link-args...]
|
||||
# Final exec: clang++ <each .o> <each original link-arg>
|
||||
set -euo pipefail
|
||||
PROG="$1"
|
||||
shift
|
||||
TRIANGLES_COMMON_DIR="@CMAKE_CURRENT_BINARY_DIR@/CMakeFiles/triangles_common.dir"
|
||||
TRIANGLESD_DIR="@CMAKE_CURRENT_BINARY_DIR@/CMakeFiles/trianglesd_objects.dir"
|
||||
# Discover .o files into a bash array. Exclude script.cpp.o (we have our
|
||||
# own clang-instrumented copy in fuzz_objs/ that we want to keep separate
|
||||
# from the main build's copy).
|
||||
declare -a OBJS=()
|
||||
for f in "$TRIANGLES_COMMON_DIR"/*.o "$TRIANGLES_COMMON_DIR"/*/*.o; do
|
||||
[ -f "$f" ] || continue
|
||||
case "$f" in
|
||||
*/script.cpp.o) continue ;;
|
||||
esac
|
||||
OBJS+=("$f")
|
||||
done
|
||||
if [ -d "$TRIANGLESD_DIR" ]; then
|
||||
for f in "$TRIANGLESD_DIR"/*.o; do
|
||||
[ -f "$f" ] || continue
|
||||
# init.cpp defines the daemon's main(); the fuzz harness has its own
|
||||
# (libFuzzer's). wallet.cpp, noui.cpp etc. are safe — they don't
|
||||
# define main and their external references (pwalletMain,
|
||||
# uiInterface, nDerivationMethodIndex) are satisfied by the stub
|
||||
# object file we add at the end of the link line.
|
||||
case "$f" in
|
||||
*/init.cpp.o) continue ;;
|
||||
esac
|
||||
OBJS+=("$f")
|
||||
done
|
||||
fi
|
||||
# Final arg list: PROG, then all .o files, then all original link args.
|
||||
exec "$PROG" "${OBJS[@]}" "$@"
|
||||
]=])
|
||||
string(CONFIGURE "${FUZZ_LINK_WRAPPER_CONTENT}"
|
||||
FUZZ_LINK_WRAPPER_CONTENT @ONLY)
|
||||
file(WRITE "${FUZZ_LINK_WRAPPER}" "${FUZZ_LINK_WRAPPER_CONTENT}")
|
||||
file(CHMOD "${FUZZ_LINK_WRAPPER}" PERMISSIONS
|
||||
OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
set(FUZZ_LINK_CMD
|
||||
"${CLANGXX}"
|
||||
"-fsanitize=fuzzer,address,undefined"
|
||||
"${FUZZ_OBJ_SCRIPT_FUZZ}"
|
||||
"-o" "${FUZZ_BIN}"
|
||||
"${FUZZ_OBJ_SCRIPT}"
|
||||
"${FUZZ_OBJ_FUZZ_STUBS}"
|
||||
"${CMAKE_BINARY_DIR}/lib/libhash9_crypto.a"
|
||||
"${CMAKE_BINARY_DIR}/lib/libleveldb_memenv.a"
|
||||
"${CMAKE_BINARY_DIR}/lib/libleveldb_lib.a"
|
||||
"-lssl" "-lcrypto" "-ldb_cxx" "-levent" "-lsqlite3" "-lminiupnpc"
|
||||
"${CMAKE_BINARY_DIR}/lib/libsecp256k1.a"
|
||||
# RocksDB: build-rocksdb.sh installs librocksdb.so (currently
|
||||
# librocksdb.so.10.10.1) to /usr/local on CI, or it comes from
|
||||
# the distro package. The library search path picks up either
|
||||
# /usr/local/lib or /usr/lib automatically, so a bare
|
||||
# "-lrocksdb" works on both. The previous generator expression
|
||||
# ($<IF:$<TARGET_EXISTS:RocksDB::rocksdb>,-lrocksdb,${ROCKSDB_LIBRARY}>)
|
||||
# failed on CI because:
|
||||
# 1. CMake's find_package(RocksDB CONFIG) does NOT find the .cmake
|
||||
# config RocksDB 10.10.1 ships, only the .pc file.
|
||||
# 2. The pkg-config path exposes PkgConfig::RocksDB (NOT
|
||||
# RocksDB::rocksdb), so $<TARGET_EXISTS:RocksDB::rocksdb> is
|
||||
# FALSE.
|
||||
# 3. The fallback ${ROCKSDB_LIBRARY} is only set inside the manual
|
||||
# find_library() probe at CMakeLists.txt:170-190, which is
|
||||
# skipped when EITHER target exists.
|
||||
# Result on CI: an empty string landed in the link line, and the
|
||||
# fuzz binary linked against every RocksDB symbol it referenced
|
||||
# turned into "undefined reference" errors.
|
||||
"-lrocksdb"
|
||||
"-lz" "-lgflags" "-lsnappy" "-lbz2" "-llz4" "-lzstd"
|
||||
# i2p is inlined into triangles_common as i2p_embedded.cpp.o and is a
|
||||
# NO-OP when USE_I2P_EMBEDDED=OFF (which is the CI default; the
|
||||
# workflow only builds libtor, not libi2pd). Do NOT link any
|
||||
# src/i2p/i2pd-src/lib*.a here — those files are produced by a
|
||||
# separate `make` step in src/i2p/build-libi2pd.sh that the fuzz
|
||||
# job does NOT run, and clang aborts the link with
|
||||
# "no such file or directory" when they're absent.
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tor/tor-src/libtor.a"
|
||||
"-lpthread" "-llzma" "-lubsan"
|
||||
)
|
||||
# Boost target names need real paths on the link line; generator
|
||||
# expressions don't get evaluated by the bash wrapper, so resolve
|
||||
# the imported-target paths at configure time and append them.
|
||||
foreach(_target Boost::program_options Boost::thread Boost::chrono
|
||||
Boost::atomic Boost::filesystem Boost::system)
|
||||
if(TARGET "${_target}")
|
||||
get_target_property(_path "${_target}" IMPORTED_LOCATION_RELEASE)
|
||||
if(NOT _path)
|
||||
get_target_property(_path "${_target}" IMPORTED_LOCATION)
|
||||
endif()
|
||||
if(_path AND EXISTS "${_path}")
|
||||
list(APPEND FUZZ_LINK_CMD "${_path}")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
# Invoke the link wrapper script, passing the actual link line as
|
||||
# args. The wrapper script discovers .o files at link time via find
|
||||
# (file(GLOB) would evaluate empty at configure time when no .o files
|
||||
# exist yet on a fresh build dir) and exec's clang++ with all the
|
||||
# discovered objects prepended to its arg list.
|
||||
add_custom_command(
|
||||
OUTPUT "${FUZZ_BIN}"
|
||||
COMMAND "${FUZZ_LINK_WRAPPER}" ${FUZZ_LINK_CMD}
|
||||
DEPENDS
|
||||
"${FUZZ_OBJ_SCRIPT_FUZZ}"
|
||||
"${FUZZ_OBJ_SCRIPT}"
|
||||
"${FUZZ_OBJ_FUZZ_STUBS}"
|
||||
"${FUZZ_LINK_WRAPPER}"
|
||||
# Static libs the link line references at ${CMAKE_BINARY_DIR}/lib/.
|
||||
# Without these deps, fuzz_script's link step races and fails with
|
||||
# "no such file" errors on first clean build.
|
||||
hash9_crypto
|
||||
leveldb_lib
|
||||
leveldb_memenv
|
||||
secp256k1
|
||||
# trianglesd_objects emits the daemon .o files (noui/init/wallet)
|
||||
# that the link wrapper discovers via find. triangles_common emits
|
||||
# the rest of the .o files we need. Without these deps the wrapper
|
||||
# finds no .o files on first build → undefined references like
|
||||
# CKey::GetPubKey.
|
||||
trianglesd_objects
|
||||
triangles_common
|
||||
COMMENT "[fuzz] clang++ link fuzz_script"
|
||||
)
|
||||
add_custom_target(fuzz_script ALL DEPENDS "${FUZZ_BIN}")
|
||||
|
||||
# ==========================================================================
|
||||
# transaction_deserialize_fuzz — second fuzz target
|
||||
# ==========================================================================
|
||||
# Compile the harness with clang + libFuzzer instrumentation. The harness
|
||||
# only links against the already-instrumented triangles_common /
|
||||
# trianglesd .o files (for CTransaction, CDataStream, etc.) — we do NOT
|
||||
# compile a separate clang-instrumented copy of any .cpp file the way
|
||||
# fuzz_script does for script.cpp.
|
||||
#
|
||||
# Uses its OWN link wrapper (link_txdeser.sh) because the fuzz_script
|
||||
# wrapper excludes script.cpp.o from triangles_common (we replace it
|
||||
# with our own clang-instrumented copy there). For transaction_deserialize
|
||||
# we need script.cpp.o: wallet.cpp.o (in trianglesd_objects) calls
|
||||
# ExtractDestination, SignSignature, Solver, IsMine — all defined in
|
||||
# script.cpp.o. Excluding it produces "undefined reference" link errors.
|
||||
# The new wrapper excludes only init.cpp.o (which defines daemon main()
|
||||
# and would conflict with libFuzzer's main).
|
||||
add_custom_command(
|
||||
OUTPUT "${FUZZ_TX_DESER_OBJ}"
|
||||
COMMAND ${CLANGXX} ${FUZZ_COMMON_FLAGS}
|
||||
-c ${FUZZ_TX_DESER_SRC} -o ${FUZZ_TX_DESER_OBJ}
|
||||
DEPENDS ${FUZZ_TX_DESER_SRC}
|
||||
COMMENT "[fuzz] clang++ transaction_deserialize_fuzz.cpp"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
# Link command — same library set as fuzz_script, but no
|
||||
# ${FUZZ_OBJ_SCRIPT} or ${FUZZ_OBJ_SCRIPT_FUZZ} (we didn't compile
|
||||
# our own clang-instrumented copy). The wrapper script discovers
|
||||
# .o files via find at link time.
|
||||
set(FUZZ_TX_DESER_LINK_CMD
|
||||
"${CLANGXX}"
|
||||
"-fsanitize=fuzzer,address,undefined"
|
||||
"${FUZZ_TX_DESER_OBJ}"
|
||||
"-o" "${FUZZ_TX_DESER_BIN}"
|
||||
"${FUZZ_OBJ_FUZZ_STUBS}"
|
||||
"${CMAKE_BINARY_DIR}/lib/libhash9_crypto.a"
|
||||
"${CMAKE_BINARY_DIR}/lib/libleveldb_memenv.a"
|
||||
"${CMAKE_BINARY_DIR}/lib/libleveldb_lib.a"
|
||||
"-lssl" "-lcrypto" "-ldb_cxx" "-levent" "-lsqlite3" "-lminiupnpc"
|
||||
"${CMAKE_BINARY_DIR}/lib/libsecp256k1.a"
|
||||
"-lrocksdb"
|
||||
"-lz" "-lgflags" "-lsnappy" "-lbz2" "-llz4" "-lzstd"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tor/tor-src/libtor.a"
|
||||
"-lpthread" "-llzma" "-lubsan"
|
||||
)
|
||||
foreach(_target Boost::program_options Boost::thread Boost::chrono
|
||||
Boost::atomic Boost::filesystem Boost::system)
|
||||
if(TARGET "${_target}")
|
||||
get_target_property(_path "${_target}" IMPORTED_LOCATION_RELEASE)
|
||||
if(NOT _path)
|
||||
get_target_property(_path "${_target}" IMPORTED_LOCATION)
|
||||
endif()
|
||||
if(_path AND EXISTS "${_path}")
|
||||
list(APPEND FUZZ_TX_DESER_LINK_CMD "${_path}")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${FUZZ_TX_DESER_BIN}"
|
||||
COMMAND "${FUZZ_TX_DESER_LINK_WRAPPER}" ${FUZZ_TX_DESER_LINK_CMD}
|
||||
DEPENDS
|
||||
"${FUZZ_TX_DESER_OBJ}"
|
||||
"${FUZZ_OBJ_FUZZ_STUBS}"
|
||||
"${FUZZ_TX_DESER_LINK_WRAPPER}"
|
||||
hash9_crypto
|
||||
leveldb_lib
|
||||
leveldb_memenv
|
||||
secp256k1
|
||||
trianglesd_objects
|
||||
triangles_common
|
||||
COMMENT "[fuzz] clang++ link transaction_deserialize_fuzz"
|
||||
)
|
||||
add_custom_target(transaction_deserialize_fuzz ALL
|
||||
DEPENDS "${FUZZ_TX_DESER_BIN}")
|
||||
|
||||
message(STATUS "Fuzz targets enabled:")
|
||||
message(STATUS " ${FUZZ_BIN}")
|
||||
message(STATUS " ${FUZZ_TX_DESER_BIN}")
|
||||
endif()
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <map>
|
||||
// assert() is used in the LockedPageManager implementation below; include
|
||||
// explicitly so this header doesn't rely on transitive includes from
|
||||
// <mutex>/<map> (clang's stricter include resolution surfaces the missing
|
||||
// include even though gcc tolerates it via some other transitive path).
|
||||
#include <cassert>
|
||||
|
||||
#ifdef WIN32
|
||||
#ifdef _WIN32_WINNT
|
||||
|
||||
@@ -67,6 +67,8 @@ inline std::string EncodeBase58(const unsigned char* pbegin, const unsigned char
|
||||
// Encode a byte vector as a base58-encoded string
|
||||
inline std::string EncodeBase58(const std::vector<unsigned char>& vch)
|
||||
{
|
||||
if (vch.empty())
|
||||
return std::string();
|
||||
return EncodeBase58(&vch[0], &vch[0] + vch.size());
|
||||
}
|
||||
|
||||
|
||||
+70
-57
@@ -14,6 +14,8 @@
|
||||
#include <openssl/opensslv.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
@@ -69,16 +71,10 @@ public:
|
||||
throw bignum_error("CBigNum::CBigNum() : BN_new() returned NULL");
|
||||
}
|
||||
|
||||
CBigNum(const CBigNum& b)
|
||||
CBigNum(const CBigNum& b) : CBigNum()
|
||||
{
|
||||
pbn = BN_new();
|
||||
if (pbn == nullptr)
|
||||
throw bignum_error("CBigNum::CBigNum(const CBigNum&) : BN_new() returned NULL");
|
||||
if (!BN_copy(pbn, b.pbn))
|
||||
{
|
||||
BN_clear_free(pbn);
|
||||
throw bignum_error("CBigNum::CBigNum(const CBigNum&) : BN_copy failed");
|
||||
}
|
||||
}
|
||||
|
||||
CBigNum& operator=(const CBigNum& b)
|
||||
@@ -99,21 +95,20 @@ public:
|
||||
const BIGNUM* get() const { return pbn; }
|
||||
|
||||
//CBigNum(char n) is not portable. Use 'signed char' or 'unsigned char'.
|
||||
CBigNum(signed char n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); }
|
||||
CBigNum(short n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); }
|
||||
CBigNum(int n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); }
|
||||
CBigNum(long n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); }
|
||||
CBigNum(long long n) { pbn = BN_new(); setint64(n); }
|
||||
CBigNum(unsigned char n) { pbn = BN_new(); setulong(n); }
|
||||
CBigNum(unsigned short n) { pbn = BN_new(); setulong(n); }
|
||||
CBigNum(unsigned int n) { pbn = BN_new(); setulong(n); }
|
||||
CBigNum(unsigned long n) { pbn = BN_new(); setulong(n); }
|
||||
CBigNum(unsigned long long n) { pbn = BN_new(); setuint64(n); }
|
||||
explicit CBigNum(uint256 n) { pbn = BN_new(); setuint256(n); }
|
||||
CBigNum(signed char n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
|
||||
CBigNum(short n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
|
||||
CBigNum(int n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
|
||||
CBigNum(long n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
|
||||
CBigNum(long long n) : CBigNum() { setint64(n); }
|
||||
CBigNum(unsigned char n) : CBigNum() { setulong(n); }
|
||||
CBigNum(unsigned short n) : CBigNum() { setulong(n); }
|
||||
CBigNum(unsigned int n) : CBigNum() { setulong(n); }
|
||||
CBigNum(unsigned long n) : CBigNum() { setulong(n); }
|
||||
CBigNum(unsigned long long n) : CBigNum() { setuint64(n); }
|
||||
explicit CBigNum(uint256 n) : CBigNum() { setuint256(n); }
|
||||
|
||||
explicit CBigNum(const std::vector<unsigned char>& vch)
|
||||
explicit CBigNum(const std::vector<unsigned char>& vch) : CBigNum()
|
||||
{
|
||||
pbn = BN_new();
|
||||
setvch(vch);
|
||||
}
|
||||
|
||||
@@ -216,21 +211,23 @@ public:
|
||||
pch[1] = (nSize >> 16) & 0xff;
|
||||
pch[2] = (nSize >> 8) & 0xff;
|
||||
pch[3] = (nSize) & 0xff;
|
||||
BN_mpi2bn(pch, p - pch, pbn);
|
||||
if (BN_mpi2bn(pch, static_cast<int>(p - pch), pbn) == nullptr)
|
||||
throw bignum_error("CBigNum::setint64() : BN_mpi2bn failed");
|
||||
}
|
||||
|
||||
uint64_t getuint64()
|
||||
uint64_t getuint64() const
|
||||
{
|
||||
unsigned int nSize = BN_bn2mpi(pbn, nullptr);
|
||||
if (nSize < 4)
|
||||
const int nSize = BN_bn2mpi(pbn, nullptr);
|
||||
if (nSize <= 4)
|
||||
return 0;
|
||||
std::vector<unsigned char> vch(nSize);
|
||||
BN_bn2mpi(pbn, &vch[0]);
|
||||
std::vector<unsigned char> vch(static_cast<size_t>(nSize));
|
||||
if (BN_bn2mpi(pbn, vch.data()) != nSize)
|
||||
throw bignum_error("CBigNum::getuint64() : BN_bn2mpi failed");
|
||||
if (vch.size() > 4)
|
||||
vch[4] &= 0x7f;
|
||||
uint64_t n = 0;
|
||||
for (unsigned int i = 0, j = vch.size()-1; i < sizeof(n) && j >= 4; i++, j--)
|
||||
((unsigned char*)&n)[i] = vch[j];
|
||||
for (size_t i = 0; i < sizeof(n) && i + 4 < vch.size(); ++i)
|
||||
n |= static_cast<uint64_t>(vch[vch.size() - 1 - i]) << (8 * i);
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -258,7 +255,8 @@ public:
|
||||
pch[1] = (nSize >> 16) & 0xff;
|
||||
pch[2] = (nSize >> 8) & 0xff;
|
||||
pch[3] = (nSize) & 0xff;
|
||||
BN_mpi2bn(pch, p - pch, pbn);
|
||||
if (BN_mpi2bn(pch, static_cast<int>(p - pch), pbn) == nullptr)
|
||||
throw bignum_error("CBigNum::setuint64() : BN_mpi2bn failed");
|
||||
}
|
||||
|
||||
void setuint256(uint256 n)
|
||||
@@ -286,29 +284,33 @@ public:
|
||||
pch[1] = (nSize >> 16) & 0xff;
|
||||
pch[2] = (nSize >> 8) & 0xff;
|
||||
pch[3] = (nSize >> 0) & 0xff;
|
||||
BN_mpi2bn(pch, p - pch, pbn);
|
||||
if (BN_mpi2bn(pch, static_cast<int>(p - pch), pbn) == nullptr)
|
||||
throw bignum_error("CBigNum::setuint256() : BN_mpi2bn failed");
|
||||
}
|
||||
|
||||
uint256 getuint256() const
|
||||
{
|
||||
unsigned int nSize = BN_bn2mpi(pbn, nullptr);
|
||||
if (nSize < 4)
|
||||
const int mpiSize = BN_bn2mpi(pbn, nullptr);
|
||||
if (mpiSize <= 4)
|
||||
return 0;
|
||||
std::vector<unsigned char> vch(nSize);
|
||||
BN_bn2mpi(pbn, &vch[0]);
|
||||
if (vch.size() > 4)
|
||||
vch[4] &= 0x7f;
|
||||
std::vector<unsigned char> vch(static_cast<size_t>(mpiSize));
|
||||
if (BN_bn2mpi(pbn, vch.data()) != mpiSize)
|
||||
throw bignum_error("CBigNum::getuint256() : BN_bn2mpi failed");
|
||||
vch[4] &= 0x7f;
|
||||
uint256 n = 0;
|
||||
for (unsigned int i = 0, j = vch.size()-1; i < sizeof(n) && j >= 4; i++, j--)
|
||||
((unsigned char*)&n)[i] = vch[j];
|
||||
for (size_t i = 0; i < sizeof(n) && i + 4 < vch.size(); ++i)
|
||||
reinterpret_cast<unsigned char*>(&n)[i] = vch[vch.size() - 1 - i];
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
void setvch(const std::vector<unsigned char>& vch)
|
||||
{
|
||||
if (vch.size() > static_cast<size_t>(std::numeric_limits<int>::max() - 4))
|
||||
throw bignum_error("CBigNum::setvch() : input is too large");
|
||||
|
||||
std::vector<unsigned char> vch2(vch.size() + 4);
|
||||
unsigned int nSize = vch.size();
|
||||
const uint32_t nSize = static_cast<uint32_t>(vch.size());
|
||||
// BIGNUM's byte stream format expects 4 bytes of
|
||||
// big endian size data info at the front
|
||||
vch2[0] = (nSize >> 24) & 0xff;
|
||||
@@ -316,20 +318,25 @@ public:
|
||||
vch2[2] = (nSize >> 8) & 0xff;
|
||||
vch2[3] = (nSize >> 0) & 0xff;
|
||||
// swap data to big endian
|
||||
reverse_copy(vch.begin(), vch.end(), vch2.begin() + 4);
|
||||
BN_mpi2bn(&vch2[0], vch2.size(), pbn);
|
||||
for (size_t i = 0; i < vch.size(); ++i)
|
||||
vch2.at(i + 4) = vch.at(vch.size() - 1 - i);
|
||||
if (BN_mpi2bn(vch2.data(), static_cast<int>(vch2.size()), pbn) == nullptr)
|
||||
throw bignum_error("CBigNum::setvch() : BN_mpi2bn failed");
|
||||
}
|
||||
|
||||
std::vector<unsigned char> getvch() const
|
||||
{
|
||||
unsigned int nSize = BN_bn2mpi(pbn, nullptr);
|
||||
if (nSize <= 4)
|
||||
const int mpiSize = BN_bn2mpi(pbn, nullptr);
|
||||
if (mpiSize <= 4)
|
||||
return std::vector<unsigned char>();
|
||||
std::vector<unsigned char> vch(nSize);
|
||||
BN_bn2mpi(pbn, &vch[0]);
|
||||
vch.erase(vch.begin(), vch.begin() + 4);
|
||||
reverse(vch.begin(), vch.end());
|
||||
return vch;
|
||||
std::vector<unsigned char> mpi(static_cast<size_t>(mpiSize));
|
||||
if (BN_bn2mpi(pbn, mpi.data()) != mpiSize)
|
||||
throw bignum_error("CBigNum::getvch() : BN_bn2mpi failed");
|
||||
|
||||
std::vector<unsigned char> result(static_cast<size_t>(mpiSize - 4));
|
||||
for (size_t i = 0; i < result.size(); ++i)
|
||||
result.at(i) = mpi.at(mpi.size() - 1 - i);
|
||||
return result;
|
||||
}
|
||||
|
||||
CBigNum& SetCompact(unsigned int nCompact)
|
||||
@@ -340,16 +347,20 @@ public:
|
||||
if (nSize >= 1) vch[4] = (nCompact >> 16) & 0xff;
|
||||
if (nSize >= 2) vch[5] = (nCompact >> 8) & 0xff;
|
||||
if (nSize >= 3) vch[6] = (nCompact >> 0) & 0xff;
|
||||
BN_mpi2bn(&vch[0], vch.size(), pbn);
|
||||
if (BN_mpi2bn(vch.data(), static_cast<int>(vch.size()), pbn) == nullptr)
|
||||
throw bignum_error("CBigNum::SetCompact() : BN_mpi2bn failed");
|
||||
return *this;
|
||||
}
|
||||
|
||||
unsigned int GetCompact() const
|
||||
{
|
||||
unsigned int nSize = BN_bn2mpi(pbn, nullptr);
|
||||
std::vector<unsigned char> vch(nSize);
|
||||
nSize -= 4;
|
||||
BN_bn2mpi(pbn, &vch[0]);
|
||||
const int mpiSize = BN_bn2mpi(pbn, nullptr);
|
||||
if (mpiSize <= 4)
|
||||
return 0;
|
||||
std::vector<unsigned char> vch(static_cast<size_t>(mpiSize));
|
||||
if (BN_bn2mpi(pbn, vch.data()) != mpiSize)
|
||||
throw bignum_error("CBigNum::GetCompact() : BN_bn2mpi failed");
|
||||
const unsigned int nSize = static_cast<unsigned int>(mpiSize - 4);
|
||||
unsigned int nCompact = nSize << 24;
|
||||
if (nSize >= 1) nCompact |= (vch[4] << 16);
|
||||
if (nSize >= 2) nCompact |= (vch[5] << 8);
|
||||
@@ -361,7 +372,7 @@ public:
|
||||
{
|
||||
// skip 0x
|
||||
const char* psz = str.c_str();
|
||||
while (isspace(*psz))
|
||||
while (isspace(static_cast<unsigned char>(*psz)))
|
||||
psz++;
|
||||
bool fNegative = false;
|
||||
if (*psz == '-')
|
||||
@@ -369,15 +380,15 @@ public:
|
||||
fNegative = true;
|
||||
psz++;
|
||||
}
|
||||
if (psz[0] == '0' && tolower(psz[1]) == 'x')
|
||||
if (psz[0] == '0' && tolower(static_cast<unsigned char>(psz[1])) == 'x')
|
||||
psz += 2;
|
||||
while (isspace(*psz))
|
||||
while (isspace(static_cast<unsigned char>(*psz)))
|
||||
psz++;
|
||||
|
||||
// hex string to bignum
|
||||
static constexpr signed char phexdigit[256] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0, 0,0xa,0xb,0xc,0xd,0xe,0xf,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0xa,0xb,0xc,0xd,0xe,0xf,0,0,0,0,0,0,0,0,0 };
|
||||
*this = 0;
|
||||
while (isxdigit(*psz))
|
||||
while (isxdigit(static_cast<unsigned char>(*psz)))
|
||||
{
|
||||
*this <<= 4;
|
||||
int n = phexdigit[(unsigned char)*psz++];
|
||||
@@ -389,6 +400,8 @@ public:
|
||||
|
||||
std::string ToString(int nBase=10) const
|
||||
{
|
||||
if (nBase < 2 || nBase > 16)
|
||||
throw bignum_error("CBigNum::ToString() : base must be in [2, 16]");
|
||||
CAutoBN_CTX pctx;
|
||||
CBigNum bnBase = nBase;
|
||||
CBigNum bn0 = 0;
|
||||
|
||||
+625
-655
File diff suppressed because it is too large
Load Diff
+21
-34
@@ -8,13 +8,14 @@
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <filesystem>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Bootstrap {
|
||||
|
||||
// Bootstrap server configuration
|
||||
static const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org";
|
||||
static const char* BASE_PATH = "/";
|
||||
static const int PORT = 80;
|
||||
inline constexpr const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org";
|
||||
inline constexpr const char* BASE_PATH = "/";
|
||||
inline constexpr int PORT = 443;
|
||||
|
||||
// Progress callback: (bytesDownloaded, totalBytes)
|
||||
typedef std::function<void(int64_t, int64_t)> ProgressCallback;
|
||||
@@ -22,7 +23,7 @@ namespace Bootstrap {
|
||||
// Check if data dir already has blockchain data
|
||||
bool NeedsBootstrap(const std::filesystem::path& dataDir);
|
||||
|
||||
// Download a single file via HTTP GET, write to destPath.
|
||||
// Download a file via HTTP GET, write to destPath.
|
||||
// If noProxy is true, bypass Tor SOCKS proxy and connect directly
|
||||
// (used for clearnet bootstrap downloads).
|
||||
// If portOverride is set (>0), uses that port instead of the default PORT.
|
||||
@@ -31,39 +32,25 @@ namespace Bootstrap {
|
||||
ProgressCallback progressFn,
|
||||
std::string& strError,
|
||||
bool noProxy = false,
|
||||
int portOverride = -1);
|
||||
int portOverride = -1,
|
||||
int64_t maxDownloadBytes = 4LL * 1024 * 1024 * 1024);
|
||||
|
||||
// Fetch the file manifest (list of relative paths to download)
|
||||
bool FetchFileList(const std::string& host,
|
||||
std::vector<std::string>& files,
|
||||
std::string& strError,
|
||||
bool noProxy = false);
|
||||
|
||||
// Download bootstrap.tar.gz and extract to dataDir.
|
||||
// Falls back to filelist.txt + individual file download if tar.gz unavailable.
|
||||
bool DownloadBootstrap(const std::string& host,
|
||||
const std::filesystem::path& dataDir,
|
||||
ProgressCallback progressFn,
|
||||
std::string& strError);
|
||||
|
||||
// Snapshot manifest (parsed from snapshot.manifest in bootstrap archive)
|
||||
struct SnapshotManifest {
|
||||
int format; // format version, must be 1
|
||||
std::string network; // "main" or "test"
|
||||
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)
|
||||
// Advertised identity of a snapshot listed by manifest.json.
|
||||
// The advertised SHA256 is accepted only when it matches the hash compiled
|
||||
// into checkpoints.cpp for the same height.
|
||||
struct RemoteSnapshot {
|
||||
std::string filename;
|
||||
std::string sha256;
|
||||
int height;
|
||||
std::string blockHash;
|
||||
};
|
||||
|
||||
// Parse a snapshot.manifest file into a SnapshotManifest struct.
|
||||
bool ParseManifest(const std::filesystem::path& manifestPath,
|
||||
SnapshotManifest& manifest,
|
||||
std::string& strError);
|
||||
|
||||
// Verify a parsed manifest against compiled-in checkpoints and config.
|
||||
bool VerifyManifest(const SnapshotManifest& manifest,
|
||||
std::string& strError);
|
||||
// Parse and validate the small, untrusted bootstrap manifest. This routine
|
||||
// performs no network I/O and is exposed so malformed-input behavior can
|
||||
// be covered by unit tests.
|
||||
bool ParseRemoteSnapshotManifest(const std::string& manifestText,
|
||||
RemoteSnapshot& snapshot,
|
||||
std::string& strError);
|
||||
|
||||
// Download a UTXO snapshot and load it into a fresh txleveldb.
|
||||
// This is much faster than downloading the full bootstrap archive.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2024-2026 Triangles developers
|
||||
// Distributed under the MIT/X11 software license
|
||||
//
|
||||
// Embedded trust anchors for HTTPS bootstrap. Added to the X509 store as
|
||||
// belt-and-suspenders regardless of which other trust source succeeded:
|
||||
// the exedir cacert.pem, SSL_CERT_FILE, or system default paths may or
|
||||
// may not contain the specific Let's Encrypt anchor that signed the
|
||||
// current bootstrap server's certificate chain. Adding these anchors
|
||||
// only ever EXPANDS the set of valid chains (it can never cause a
|
||||
// previously-valid cert to be rejected), so it's safe to layer on top
|
||||
// of any operator-supplied bundle.
|
||||
//
|
||||
// These are the Mozilla CA bundle entries for ISRG Root X1 and X2 — the
|
||||
// anchors Let's Encrypt uses to sign every certificate they currently issue
|
||||
// (R10/R11/R12 intermediates chain to X1; the YE1 intermediate chains to X2).
|
||||
// Sourced from https://curl.se/ca/cacert.pem and verified via SHA-256 against
|
||||
// the Mozilla NSS bundle.
|
||||
//
|
||||
// Last verified: 2026-08-06 (cacert.pem snapshot).
|
||||
#ifndef TRIANGLES_BOOTSTRAP_ROOTS_H
|
||||
#define TRIANGLES_BOOTSTRAP_ROOTS_H
|
||||
|
||||
const char* const EMBEDDED_ISRG_ROOT_X1_PEM =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE\n"
|
||||
"BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD\n"
|
||||
"EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG\n"
|
||||
"EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT\n"
|
||||
"DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r\n"
|
||||
"Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1\n"
|
||||
"3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K\n"
|
||||
"b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN\n"
|
||||
"Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ\n"
|
||||
"4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf\n"
|
||||
"1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu\n"
|
||||
"hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH\n"
|
||||
"usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r\n"
|
||||
"OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G\n"
|
||||
"A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY\n"
|
||||
"9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n"
|
||||
"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV\n"
|
||||
"0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt\n"
|
||||
"hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw\n"
|
||||
"TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx\n"
|
||||
"e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA\n"
|
||||
"JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD\n"
|
||||
"YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n\n"
|
||||
"JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ\n"
|
||||
"m+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n"
|
||||
"-----END CERTIFICATE-----";
|
||||
|
||||
const char* const EMBEDDED_ISRG_ROOT_X2_PEM =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQswCQYDVQQGEwJV\n"
|
||||
"UzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElT\n"
|
||||
"UkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVT\n"
|
||||
"MSkwJwYDVQQKEyBJbnRlcm5ldCBTZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNS\n"
|
||||
"RyBSb290IFgyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0H\n"
|
||||
"ttwW+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9ItgKbppb\n"
|
||||
"d9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\n"
|
||||
"HQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZIzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtF\n"
|
||||
"cP9Ymd70/aTSVaYgLXTWNLxBo1BfASdWtL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5\n"
|
||||
"U6VR5CmD1/iQMVtCnwr1/q4AaOeMSQ+2b1tbFfLn\n"
|
||||
"-----END CERTIFICATE-----";
|
||||
|
||||
|
||||
#endif // TRIANGLES_BOOTSTRAP_ROOTS_H
|
||||
+87
-24
@@ -109,6 +109,13 @@ bool MaybeMigrateLevelDbToRocksDb(bool fForce, std::string& strError)
|
||||
{
|
||||
std::ofstream marker(markerPath);
|
||||
marker << "RocksDB migration in progress. Safe to delete this directory and retry.\n";
|
||||
marker.flush();
|
||||
if (!marker.good()) {
|
||||
// Without the marker a crashed migration would be
|
||||
// indistinguishable from a complete one — refuse to start.
|
||||
strError = "could not write migration marker " + markerPath.string();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
CTxDB source("r");
|
||||
@@ -129,34 +136,48 @@ bool MaybeMigrateLevelDbToRocksDb(bool fForce, std::string& strError)
|
||||
}
|
||||
|
||||
int64_t nCopied = 0;
|
||||
auto it = source.NewIterator();
|
||||
for (it->Seek(std::string()); it->Valid(); it->Next())
|
||||
bool fCopyOK = true;
|
||||
{
|
||||
if (!destination.WriteRawRecordForMigration(it->KeyStr(), it->ValueStr())) {
|
||||
destination.TxnAbort();
|
||||
strError = "failed to write migrated record to RocksDB";
|
||||
source.Close();
|
||||
destination.Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (++nCopied % 100000 == 0)
|
||||
// W2 root cause: this iterator MUST be destroyed before
|
||||
// source.Close(). Live LevelDB iterators hold a reference to the
|
||||
// current Version; deleting the DB with one outstanding trips
|
||||
// `dummy_versions_.next_ == &dummy_versions_` in
|
||||
// leveldb::VersionSet::~VersionSet (version_set.cc:755) and
|
||||
// aborts the daemon AFTER verification but BEFORE the marker is
|
||||
// removed — which is what produced the original H4 symptom.
|
||||
// Scoping the iterator here guarantees every Close() below runs
|
||||
// with it already dead, on the success AND error paths.
|
||||
auto it = source.NewIterator();
|
||||
for (it->Seek(std::string()); it->Valid(); it->Next())
|
||||
{
|
||||
if (!destination.TxnCommit()) {
|
||||
strError = "failed to commit RocksDB migration batch";
|
||||
source.Close();
|
||||
destination.Close();
|
||||
return false;
|
||||
if (!destination.WriteRawRecordForMigration(it->KeyStr(), it->ValueStr())) {
|
||||
strError = "failed to write migrated record to RocksDB";
|
||||
fCopyOK = false;
|
||||
break;
|
||||
}
|
||||
printf("ChainDB migration: copied %lld / %lld records\n",
|
||||
(long long)nCopied, (long long)srcStats.nRecords);
|
||||
if (!destination.TxnBegin()) {
|
||||
strError = "failed to begin RocksDB migration batch";
|
||||
source.Close();
|
||||
destination.Close();
|
||||
return false;
|
||||
|
||||
if (++nCopied % 100000 == 0)
|
||||
{
|
||||
if (!destination.TxnCommit()) {
|
||||
strError = "failed to commit RocksDB migration batch";
|
||||
fCopyOK = false;
|
||||
break;
|
||||
}
|
||||
printf("ChainDB migration: copied %lld / %lld records\n",
|
||||
(long long)nCopied, (long long)srcStats.nRecords);
|
||||
if (!destination.TxnBegin()) {
|
||||
strError = "failed to begin RocksDB migration batch";
|
||||
fCopyOK = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // iterator destroyed here — before any Close()
|
||||
if (!fCopyOK) {
|
||||
destination.TxnAbort(); // safe no-op if the batch was already consumed
|
||||
source.Close();
|
||||
destination.Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!destination.TxnCommit()) {
|
||||
@@ -185,7 +206,49 @@ bool MaybeMigrateLevelDbToRocksDb(bool fForce, std::string& strError)
|
||||
|
||||
source.Close();
|
||||
destination.Close();
|
||||
fs::remove(markerPath);
|
||||
|
||||
// H4: Marker removal must be verified, not assumed. The previous
|
||||
// implementation called fs::remove() and ignored the return code, which
|
||||
// silently left the marker on disk after a successful migration. On
|
||||
// the next startup init.cpp's fCrashedMigration check would then
|
||||
// trigger a re-migration of the (already-good) RocksDB on every
|
||||
// restart, eventually destroying the chain state.
|
||||
//
|
||||
// Three defenses:
|
||||
// 1. Use the non-throwing error_code overload so a permission
|
||||
// error doesn't propagate as an uncaught exception.
|
||||
// 2. After remove(), confirm the file is actually gone. fs::remove
|
||||
// returns true if the file didn't exist, which is also success
|
||||
// but worth distinguishing.
|
||||
// 3. Retry once with a short delay. On Windows, antivirus and
|
||||
// indexer handles can transiently hold the marker file open
|
||||
// even after our process closed it; a single retry usually
|
||||
// wins. If the second attempt also leaves the file, treat the
|
||||
// migration as FAILED — surface the error to the operator
|
||||
// instead of letting init.cpp's fCrashedMigration logic
|
||||
// destroy working data on the next startup.
|
||||
{
|
||||
std::error_code ec;
|
||||
fs::remove(markerPath, ec);
|
||||
if (ec) {
|
||||
strError = "could not remove migration marker " + markerPath.string() +
|
||||
": " + ec.message();
|
||||
return false;
|
||||
}
|
||||
if (fs::exists(markerPath)) {
|
||||
// Retry once — handles Windows AV/indexer transient locks.
|
||||
MilliSleep(100);
|
||||
std::error_code ec2;
|
||||
fs::remove(markerPath, ec2);
|
||||
if (ec2 || fs::exists(markerPath)) {
|
||||
strError = "migration marker " + markerPath.string() +
|
||||
" could not be removed after retry; refusing to leave it on disk " +
|
||||
"(would trigger re-migration on next startup). " +
|
||||
std::string(ec2 ? ec2.message().c_str() : "");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
strError = e.what();
|
||||
|
||||
@@ -425,7 +425,8 @@ bool LoadSignedCheckpoints(
|
||||
if (Bootstrap::DownloadFile(host, "signed-checkpoints.json",
|
||||
std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp",
|
||||
nullptr, strError,
|
||||
/*noProxy=*/true)) {
|
||||
/*noProxy=*/true, /*portOverride=*/-1,
|
||||
/*maxDownloadBytes=*/10 * 1024 * 1024)) {
|
||||
std::filesystem::path tmp = std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp";
|
||||
FILE* f = fopen(tmp.string().c_str(), "rb");
|
||||
if (f) {
|
||||
|
||||
+81
-55
@@ -32,12 +32,50 @@ namespace Checkpoints
|
||||
{ 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")},
|
||||
};
|
||||
// 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.
|
||||
// Operator rollback canonical (cycle-32, 2026-08-06): the chain was
|
||||
// rolled back to height 2,172,037 (hash 52b12f09...) so the entire
|
||||
// span 2,172,038..2,224,763 no longer exists on the canonical chain.
|
||||
// All pins from 2,205,000..2,224,763 have been REMOVED from the map
|
||||
// (NOT preserved). Their block hashes are not in the canonical chain,
|
||||
// so leaving them as map entries would let GetTotalBlocksEstimate()
|
||||
// return a pre-rollback height — keeping the daemon permanently in
|
||||
// IBD because nBestHeight < GetTotalBlocksEstimate(). The pin at
|
||||
// 17,650 (line above) remains as the lowest anchored finality
|
||||
// reference.
|
||||
// Operator-rollback finality pin (cycle-33, 2026-08-06): the
|
||||
// canonical tip after the operator rollback to 2,172,037. Hash
|
||||
// verified against all 4 fleet nodes (DNS2/DNS3/Hetzner/SAMI-PC)
|
||||
// at canonical tip 2,172,037. Was the highest entry from
|
||||
// 2026-08-06 until the 2026-09-02 checkpoint rebase added the
|
||||
// pins below; retained as a hardened anchor of the rollback span.
|
||||
{ 2172037, uint256("0x52b12f0970191505d9982449875822b78f075d7d76307abed45e7132f5fa2f16")}, // cycle-33 rollback pin
|
||||
// Checkpoint rebase to 2,200,899 (2026-09-02). A strict UTXO
|
||||
// replay of the full on-disk history (genesis..2,224,763) shows
|
||||
// that heights 2,172,038..2,200,899 validate cleanly, while the
|
||||
// canonical chain from height 2,200,900 (2026-04-07) onward
|
||||
// contains 805 coinstake inputs (in 603 blocks) that re-spend
|
||||
// outputs already spent by earlier blocks — accepted at the time
|
||||
// only because of the v5.8.x vSpent tracking bug. No correct
|
||||
// node can ever validate that span, so 2,200,899 is the last
|
||||
// block that can be canonical. Pins below restore 10k-block
|
||||
// spacing across the recovered span. Hashes computed directly
|
||||
// from blk0001.dat headers (X13) and cross-checked against the
|
||||
// chain that all live-network pins (2,222,900..2,224,763) sat on.
|
||||
{ 2180000, uint256("0xe3d2780d838314cb759784757e7e84cd0f18a46d333d3e6aaa4f79d5060104a0")},
|
||||
{ 2190000, uint256("0x682baf783581468ba18f9967254a7f3944e8b8c4cc7101e7d99b68f4f9dd5271")},
|
||||
{ 2200000, uint256("0x0a8d0442f031f1258120f713f34e45f4f9a625fb753558e27b89b32ad5a9a740")},
|
||||
{ 2200500, uint256("0x68fd5eedbefe80431fba92ee4ea37993f3e5f22f88b38a564e582a5c4aa15db2")},
|
||||
{ 2200899, uint256("0x28e57e03c7f48df8ef0dedba2b93fd5176500729c955f86546c381be66952e55")}, // rebase base (last clean block)
|
||||
// Rebase snapshot anchor (2026-09-06): the published canonical
|
||||
// snapshot tip. Hash verified live via sami-pc getblockhash and
|
||||
// byte-reversed against utxo-snapshot-2201018.utx's internal
|
||||
// header blockhash. Highest pin: GetTotalBlocksEstimate()
|
||||
// returns 2,201,018.
|
||||
{ 2201018, uint256("0x2a1894007595acaa5d303554253b3c328ebc870f248ffebf83e09a4c8156a78f")}, // canonical tip (rebase snapshot anchor)
|
||||
};
|
||||
|
||||
// Published UTXO snapshot file SHA256, keyed by snapshot height.
|
||||
// Each entry binds height -> SHA256 of the canonical snapshot file produced by
|
||||
@@ -48,7 +86,23 @@ namespace Checkpoints
|
||||
// here. The corresponding (height, blockhash) must already exist in
|
||||
// mapCheckpoints / mapCheckpointsTestnet.
|
||||
static std::map<int, uint256> mapSnapshotHashes = {
|
||||
{ 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")},
|
||||
// ONLY the canonical entry may live here. GetBestSnapshotHeight()
|
||||
// returns this map's highest key and DownloadUtxoSnapshot trusts the
|
||||
// bootstrap manifest's advertised height when the (height, sha) pair
|
||||
// is present, so a retired entry would let a stale or replayed
|
||||
// manifest hand a fresh wallet an unloadable file. History: the
|
||||
// 2172037 rollback-era snapshot (fc3b2035...) was superseded
|
||||
// 2026-09-02 by the checkpoint rebase; the 2200899 Sep-1 dump
|
||||
// (5374ea23...7a) was retired 2026-09-06 — its writer serialization
|
||||
// is unreadable by the deployed binaries (CDataStream end-of-data).
|
||||
// Do NOT re-add retired entries; full history is in git, not in the
|
||||
// live trust-anchor map.
|
||||
// Canonical rebase snapshot (2026-09-06): dumped live from the
|
||||
// staking node (sami-pc, deployed binary v6.2.6.4), published at
|
||||
// bootstrap.cryptographic-triangles.org/utxo-snapshot.bin with
|
||||
// manifest v3.0. Load-verified end-to-end on DNS2 (all 2,201,019
|
||||
// headers + 17,720 UTXOs + txindex rebuild).
|
||||
{ 2201018, uint256("0xed3fe84ee2388a7083873462af298bd4ba345ceb84e5ac65e3d2906419c0efab")}, // canonical (only entry)
|
||||
};
|
||||
|
||||
static std::map<int, uint256> mapSnapshotHashesTestnet = {
|
||||
@@ -123,6 +177,19 @@ namespace Checkpoints
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Independent of mapBlockIndex: returns the highest compiled checkpoint
|
||||
// height for the current network. Returns -1 if the compiled map is
|
||||
// empty (an unusual, but not impossible, configuration). Used as the
|
||||
// fail-closed reorg floor before pindexLastHardenedCheckpoint has been
|
||||
// resolved against the local block index (early IBD / reindex /
|
||||
// bootstrap before the checkpoint block has been downloaded).
|
||||
int GetLastCheckpointHeight()
|
||||
{
|
||||
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
|
||||
if (checkpoints.empty()) return -1;
|
||||
return checkpoints.rbegin()->first;
|
||||
}
|
||||
|
||||
// triangles: synchronized checkpoint (centrally broadcasted)
|
||||
uint256 hashSyncCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
uint256 hashPendingCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
@@ -325,53 +392,14 @@ namespace Checkpoints
|
||||
|
||||
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;
|
||||
(void)strPrivKey;
|
||||
return error("SetCheckpointPrivKey: synchronized checkpoints are disabled");
|
||||
}
|
||||
|
||||
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;
|
||||
(void)hashCheckpoint;
|
||||
return error("SendSyncCheckpoint: synchronized checkpoints are disabled");
|
||||
}
|
||||
|
||||
// Is the sync-checkpoint outside maturity window?
|
||||
@@ -392,13 +420,11 @@ 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
|
||||
// The master-key system is disabled. Reject these legacy messages instead of
|
||||
// treating unsigned data as authenticated if a dispatcher is added later.
|
||||
bool CSyncCheckpoint::CheckSignature()
|
||||
{
|
||||
// Deserialize the checkpoint data without signature verification
|
||||
CDataStream sMsg(vchMsg, SER_NETWORK, PROTOCOL_VERSION);
|
||||
sMsg >> *(CUnsignedSyncCheckpoint*)this;
|
||||
return true;
|
||||
return error("CSyncCheckpoint::CheckSignature: synchronized checkpoints are disabled");
|
||||
}
|
||||
|
||||
// triangles: process synchronized checkpoint
|
||||
|
||||
@@ -53,6 +53,14 @@ namespace Checkpoints
|
||||
// Returns last CBlockIndex* in mapBlockIndex that is a checkpoint
|
||||
CBlockIndex* GetLastCheckpoint(const std::map<uint256, CBlockIndex*>& mapBlockIndex);
|
||||
|
||||
// Returns the highest *compiled* checkpoint height, independent of
|
||||
// whether mapBlockIndex has loaded the corresponding block yet. Every
|
||||
// node built from the same binary sees the same value. Used as the
|
||||
// fail-closed floor for Reorganize() when pindexLastHardenedCheckpoint
|
||||
// has not yet been resolved (early IBD / reindex / bootstrap before
|
||||
// the checkpoint block has been downloaded).
|
||||
int GetLastCheckpointHeight();
|
||||
|
||||
extern uint256 hashSyncCheckpoint;
|
||||
extern CSyncCheckpoint checkpointMessage;
|
||||
extern uint256 hashInvalidCheckpoint;
|
||||
|
||||
+19
-19
@@ -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 6
|
||||
#define CLIENT_VERSION_MINOR 0
|
||||
#define CLIENT_VERSION_REVISION 0
|
||||
#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
|
||||
#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 2
|
||||
#define CLIENT_VERSION_REVISION 6
|
||||
#define CLIENT_VERSION_BUILD 6
|
||||
|
||||
// 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
|
||||
|
||||
+478
@@ -0,0 +1,478 @@
|
||||
// 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();
|
||||
// The I2P destination private key identifies this node on
|
||||
// the I2P network: owner-only permissions, like Tor's
|
||||
// hidden-service secret key. (No-op semantics differ on
|
||||
// Windows ACLs; harmless there.)
|
||||
std::error_code ec;
|
||||
std::filesystem::permissions(keyPath,
|
||||
std::filesystem::perms::owner_read |
|
||||
std::filesystem::perms::owner_write,
|
||||
std::filesystem::perm_options::replace, ec);
|
||||
if (ec)
|
||||
printf("I2P: WARNING could not restrict permissions on %s: %s\n",
|
||||
keyPath.string().c_str(), ec.message().c_str());
|
||||
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
|
||||
+466
-94
@@ -401,22 +401,53 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
{
|
||||
if (running.load()) return true;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PHASE 0: validate input BEFORE any state mutation.
|
||||
// If validation fails, we must leave the system in a clean state
|
||||
// (running=false, no i2p data dir side effects, no InitI2P call).
|
||||
// ----------------------------------------------------------------
|
||||
if (socks < 1 || socks > 65535) {
|
||||
lastError = strprintf("SOCKS proxy port %d out of range (1-65535)", socks);
|
||||
return false;
|
||||
}
|
||||
if (sam < 1 || sam > 65535) {
|
||||
lastError = strprintf("SAM bridge port %d out of range (1-65535)", sam);
|
||||
return false;
|
||||
}
|
||||
if (server < 0 || server > 65535) {
|
||||
lastError = strprintf("server tunnel port %d out of range (0-65535, 0=disable)", server);
|
||||
return false;
|
||||
}
|
||||
|
||||
lastError.clear();
|
||||
socksPort = socks;
|
||||
samPort = sam;
|
||||
serverPort = server;
|
||||
i2pHostname.clear();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||
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);
|
||||
try {
|
||||
fs::create_directories(i2pDataDir);
|
||||
fs::permissions(i2pDataDir, fs::perms::owner_all, fs::perm_options::replace);
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
lastError = strprintf("Cannot create i2p data dir %s: %s", i2pDataDir.c_str(), e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
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.
|
||||
// NOTE: As of i2pd 2.60.0, the embedded library API (i2p::api::InitI2P)
|
||||
// never calls ParseConfig, so this file is NOT read at runtime. It is
|
||||
// written for documentation/debugging purposes only — operators can
|
||||
// inspect it to see what ports the daemon intends to use. The actual
|
||||
// port bindings are applied programmatically via i2p::config::SetOption
|
||||
// below (before the background thread starts). Keep the file in sync
|
||||
// with the SetOption calls.
|
||||
{
|
||||
fs::path confPath = fs::path(i2pDataDir) / "i2pd.conf";
|
||||
std::ofstream conf(confPath.string());
|
||||
@@ -425,6 +456,8 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
return false;
|
||||
}
|
||||
conf << "# Auto-generated by Triangles embedded I2P\n";
|
||||
conf << "# NOTE: i2pd 2.60.0 library API does NOT read this file.\n";
|
||||
conf << "# Actual port bindings come from i2p::config::SetOption in i2p_embedded.cpp.\n";
|
||||
conf << "datadir = " << i2pDataDir << "\n";
|
||||
conf << "loglevel = info\n";
|
||||
conf << "\n";
|
||||
@@ -441,6 +474,13 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
conf << "address = 127.0.0.1\n";
|
||||
conf << "port = " << samPort << "\n";
|
||||
conf << "\n";
|
||||
// Disable HTTP proxy (port 4444). Cycle-13 fix: the HTTPProxy runs by
|
||||
// default in i2pd 2.60.0 and any HTTP request to its port causes a
|
||||
// nullptr deref in i2p::i18n::Locale::GetString. Conf is dead code in
|
||||
// the embedded library path; SetOption in InitI2P is the real override.
|
||||
conf << "[httpproxy]\n";
|
||||
conf << "enabled = false\n";
|
||||
conf << "\n";
|
||||
// Disable HTTP webconsole (not needed for embedded use)
|
||||
conf << "[http]\n";
|
||||
conf << "enabled = false\n";
|
||||
@@ -463,20 +503,29 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
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);
|
||||
if (!tunnelConf.is_open()) {
|
||||
lastError = strprintf("Failed to write %s for server tunnel configuration",
|
||||
tunnelConfPath.string().c_str());
|
||||
printf("ERROR: %s\n", lastError.c_str());
|
||||
return false;
|
||||
}
|
||||
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();
|
||||
if (tunnelConf.fail()) {
|
||||
lastError = strprintf("Write failed for %s", tunnelConfPath.string().c_str());
|
||||
printf("ERROR: %s\n", lastError.c_str());
|
||||
return false;
|
||||
}
|
||||
printf("Embedded I2P: server tunnel configured on port %d\n", serverPort);
|
||||
}
|
||||
|
||||
// Build argv for i2pd initialization. Pass --datadir and --conf on the
|
||||
@@ -494,113 +543,256 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
argvPtrs.push_back(&s[0]);
|
||||
argvPtrs.push_back(nullptr);
|
||||
|
||||
// The whole post-InitI2P section is wrapped in try/catch so that ANY
|
||||
// failure after i2pd is initialized triggers TerminateI2P. Without this
|
||||
// an exception from SetOption or std::thread construction would leave
|
||||
// running=true but with no router thread to clean up — a leaked i2pd.
|
||||
try {
|
||||
// Initialize i2pd: config parse, filesystem, crypto, router context
|
||||
i2p::api::InitI2P((int)(argvPtrs.size() - 1), argvPtrs.data(), "triangles-i2pd");
|
||||
|
||||
// Start the I2P router: netdb, transports, tunnels, router context
|
||||
// Redirect i2pd logs to our stdout/stderr
|
||||
auto logStream = std::make_shared<std::ostream>(std::cout.rdbuf());
|
||||
i2p::api::StartI2P(logStream);
|
||||
|
||||
printf("Embedded I2P: router started, starting client services...\n");
|
||||
|
||||
// Start the client context — this initializes SAM bridge, SOCKS proxy,
|
||||
// and tunnels based on config. The client context reads the conf we
|
||||
// wrote above to determine which services to start.
|
||||
i2p::client::context.Start();
|
||||
// ----------------------------------------------------------------
|
||||
// Phase 1 (synchronous, < 1s): config parse, crypto, router context.
|
||||
// InitI2P is wrapped in try/catch so a partial-init failure does
|
||||
// not leave i2pd in a half-initialized state with running=true.
|
||||
// ----------------------------------------------------------------
|
||||
try {
|
||||
i2p::api::InitI2P((int)(argvPtrs.size() - 1), argvPtrs.data(), "triangles-i2pd");
|
||||
} catch (const std::exception& e) {
|
||||
lastError = strprintf("InitI2P failed: %s", e.what());
|
||||
// Best-effort cleanup: i2pd's InitI2P may have partially
|
||||
// initialized global state. TerminateI2P is a no-op if no
|
||||
// init happened; it cleans up otherwise.
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
return false;
|
||||
} catch (...) {
|
||||
lastError = "InitI2P failed: unknown exception";
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
return false;
|
||||
}
|
||||
fflush(stdout);
|
||||
|
||||
// Mark running immediately so Qt UI shows I2P as active.
|
||||
// From this point on, any exception thrown by the code below is
|
||||
// caught by the outer try/catch, which calls TerminateI2P to
|
||||
// release the partially-initialized i2pd state.
|
||||
running.store(true);
|
||||
printf("Embedded I2P: SOCKS proxy at 127.0.0.1:%d, SAM at 127.0.0.1:%d\n",
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PROGRAMMATIC OVERRIDE OF SOCKS/SAM PORTS
|
||||
//
|
||||
// i2pd 2.60.0's library API (i2p::api::InitI2P) only calls ParseCmdline
|
||||
// — it never calls ParseConfig. The i2pd.conf file we just wrote is
|
||||
// NEVER READ by the embedded library path. SOCKS proxy falls back to
|
||||
// its built-in default port (4447) regardless of what we put in the
|
||||
// conf file. This is verified by the library's bundled ParseConfig
|
||||
// (called only by the standalone daemon binary at
|
||||
// src/i2p/i2pd-src/daemon/Daemon.cpp:107) — the library API
|
||||
// deliberately omits it.
|
||||
//
|
||||
// The fix: override socksproxy.port + sam.port + socksproxy.address
|
||||
// AFTER i2p::api::InitI2P returns (so all defaults are in m_Options)
|
||||
// but BEFORE the background thread calls i2p::client::context.Start
|
||||
// which calls ReadSocksProxy + ReadSAMBridge. SetOption calls
|
||||
// notify() internally, so the new values are visible to GetOption.
|
||||
//
|
||||
// NOTE: SOCKS/SAM port range validation happens in Start() Phase 0
|
||||
// before any state mutation, so by this point socksPort and samPort
|
||||
// are already known to be 1..65535. No re-validation needed here.
|
||||
// ----------------------------------------------------------------
|
||||
printf("Embedded I2P: overriding socksproxy.port=%d sam.port=%d via SetOption\n",
|
||||
socksPort, samPort);
|
||||
fflush(stdout);
|
||||
{
|
||||
bool socksEnabled = true;
|
||||
std::string socksAddr = "127.0.0.1";
|
||||
uint16_t socksPortVal = (uint16_t)socksPort;
|
||||
std::string socksKeys = "socks-proxy.dat";
|
||||
bool samEnabled = true;
|
||||
std::string samAddr = "127.0.0.1";
|
||||
uint16_t samPortVal = (uint16_t)samPort;
|
||||
// Cycle-13 fix: HTTPProxy runs by default in i2pd 2.60.0 on
|
||||
// port 4444 and any inbound HTTP request crashes the daemon via
|
||||
// nullptr deref in i2p::i18n::Locale::GetString (m_Language is
|
||||
// never initialized). The previous SetOption("http.enabled",...)
|
||||
// targeted the i2pd WEBCONSOLE, not the HTTPProxy. Correct key
|
||||
// is "httpproxy.enabled".
|
||||
bool httpproxyEnabled = false;
|
||||
bool httpWebconsoleEnabled = false;
|
||||
bool i2pcontrolEnabled = false;
|
||||
bool bobEnabled = false;
|
||||
|
||||
// Wait for i2pd's SOCKS proxy AND SAM bridge to become available
|
||||
// (up to 120s — I2P bootstrap is slower than Tor due to floodfill
|
||||
// lookup and tunnel build).
|
||||
printf("Embedded I2P: waiting for SOCKS proxy and SAM bridge...\n");
|
||||
bool socksReady = false;
|
||||
bool samReady = false;
|
||||
i2p::config::SetOption("socksproxy.enabled", socksEnabled);
|
||||
i2p::config::SetOption("socksproxy.address", socksAddr);
|
||||
i2p::config::SetOption("socksproxy.port", socksPortVal);
|
||||
i2p::config::SetOption("socksproxy.keys", socksKeys);
|
||||
i2p::config::SetOption("sam.enabled", samEnabled);
|
||||
i2p::config::SetOption("sam.address", samAddr);
|
||||
i2p::config::SetOption("sam.port", samPortVal);
|
||||
// Cycle-13 fix: was "http.enabled" which targeted webconsole.
|
||||
i2p::config::SetOption("httpproxy.enabled", httpproxyEnabled);
|
||||
i2p::config::SetOption("http.enabled", httpWebconsoleEnabled);
|
||||
i2p::config::SetOption("i2pcontrol.enabled", i2pcontrolEnabled);
|
||||
i2p::config::SetOption("bob.enabled", bobEnabled);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 120; i++) {
|
||||
MilliSleep(1000);
|
||||
if (fShutdown) {
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
// Keep the thread handle so Stop() can join it. A detached thread
|
||||
// that is still running would block the wallet from exiting.
|
||||
routerThread = 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);
|
||||
|
||||
// --- Check SOCKS proxy readiness ---
|
||||
if (!socksReady) {
|
||||
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) {
|
||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock != INVALID_SOCKET) {
|
||||
#else
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock >= 0) {
|
||||
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);
|
||||
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);
|
||||
closesocket(sock);
|
||||
#else
|
||||
close(sock);
|
||||
close(sock);
|
||||
#endif
|
||||
if (up) {
|
||||
socksReady = true;
|
||||
printf("Embedded I2P: SOCKS proxy ready on port %d (took %ds)\n",
|
||||
socksPort, i + 1);
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Check SAM bridge readiness ---
|
||||
if (!samReady) {
|
||||
samReady = IsSamAvailable();
|
||||
if (samReady) {
|
||||
printf("Embedded I2P: SAM v3 bridge ready on port %d (took %ds)\n",
|
||||
samPort, i + 1);
|
||||
}
|
||||
}
|
||||
// ----------------------------------------------------------------
|
||||
// Populate .b32.i2p address — use the SERVER TUNNEL destination,
|
||||
// NOT the embedded router identity. Delegates to
|
||||
// DiscoverServerTunnelDestination() which is also callable
|
||||
// from GetI2PAddress() so the Qt timerI2P retry path picks
|
||||
// up the result once the tunnel registers.
|
||||
// ----------------------------------------------------------------
|
||||
DiscoverServerTunnelDestination();
|
||||
fflush(stdout);
|
||||
|
||||
// Both endpoints are up — router is fully bootstrapped
|
||||
if (socksReady && samReady) {
|
||||
printf("Embedded I2P: all I2P endpoints ready (SOCKS %d + SAM %d)\n",
|
||||
socksPort, samPort);
|
||||
break;
|
||||
} catch (const std::exception& e) {
|
||||
// Background init failure: i2pd router context may be partially
|
||||
// alive (transports listening, netDb half-built). Tear it down,
|
||||
// reset running, and surface the error in lastError so callers
|
||||
// can see the failure rather than seeing running=true forever.
|
||||
printf("ERROR: Embedded I2P background init failed: %s\n", e.what());
|
||||
fflush(stdout);
|
||||
lastError = std::string("i2pd background init failed: ") + e.what();
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
running.store(false);
|
||||
} catch (...) {
|
||||
// Catch-all: any non-std::exception (e.g. structured exception
|
||||
// on Windows) would otherwise invoke std::terminate, killing
|
||||
// the daemon with no useful diagnostic.
|
||||
printf("ERROR: Embedded I2P background init failed: unknown exception\n");
|
||||
fflush(stdout);
|
||||
lastError = "i2pd background init failed: unknown exception";
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
running.store(false);
|
||||
}
|
||||
});
|
||||
|
||||
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 the .b32.i2p address from the router's identity hash.
|
||||
// This is the I2P address that appears in the Qt status bar.
|
||||
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: could not retrieve router address yet\n");
|
||||
}
|
||||
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());
|
||||
// i2pd may be partially or fully initialized by the time we got here.
|
||||
// TerminateI2P is a no-op if InitI2P never ran; otherwise it cleans
|
||||
// up router context, transports, and netDb.
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
running.store(false);
|
||||
return false;
|
||||
} catch (...) {
|
||||
lastError = "i2pd initialization failed: unknown exception";
|
||||
printf("ERROR: Embedded I2P startup failed: unknown exception\n");
|
||||
try { i2p::api::TerminateI2P(); } catch (...) {}
|
||||
running.store(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void CI2PEmbedded::Stop()
|
||||
{
|
||||
if (!running.load()) return;
|
||||
if (!running.load()) {
|
||||
if (routerThread.joinable()) routerThread.join();
|
||||
return;
|
||||
}
|
||||
printf("Requesting embedded I2P shutdown...\n");
|
||||
|
||||
try {
|
||||
@@ -616,9 +808,181 @@ void CI2PEmbedded::Stop()
|
||||
printf("WARNING: error during I2P shutdown: %s\n", e.what());
|
||||
}
|
||||
|
||||
// Wait for the bootstrap thread to finish (up to 5s).
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (routerThread.joinable() && std::chrono::steady_clock::now() < deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
if (!running.load()) {
|
||||
// The thread observes fShutdown and exits its loop on its own
|
||||
// once running is set false by the API teardown above.
|
||||
routerThread.join();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (routerThread.joinable()) {
|
||||
printf("WARNING: embedded I2P did not exit within 5s; detaching thread\n");
|
||||
// Detach as a last resort — the process is about to exit and the OS
|
||||
// will reap the thread.
|
||||
routerThread.detach();
|
||||
}
|
||||
|
||||
running.store(false);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// CI2PEmbedded::DiscoverServerTunnelDestination
|
||||
//
|
||||
// Reads triangles-p2p-keys.dat (binary PrivateKeys blob) and looks for
|
||||
// a matching entry in i2p::client::context.GetServerTunnels(). On
|
||||
// match, sets i2pHostname to the corresponding ".b32.i2p" address.
|
||||
// On no match (or serverPort==0, or read failure), leaves i2pHostname
|
||||
// empty. Idempotent and safe to call repeatedly from the Qt timerI2P
|
||||
// path (qt/trianglesgui.cpp:384-387, default 5s interval).
|
||||
//
|
||||
// Why fail-closed: publishing the keys-file hash while the tunnel is
|
||||
// not yet registered would mean advertising a destination with no
|
||||
// published LeaseSet → peers hit SOCKS code 4 / "LeaseSet not found"
|
||||
// on the floodfill network. Empty hostname → no wrong-identity
|
||||
// connectivity. The Qt timer keeps retrying until the tunnel comes up.
|
||||
// ----------------------------------------------------------------
|
||||
void CI2PEmbedded::DiscoverServerTunnelDestination()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||
|
||||
if (serverPort == 0) {
|
||||
// No P2P server tunnel configured. This is the -nolisten / no
|
||||
// -i2phsport case (pure outbound SOCKS I2P, no inbound service).
|
||||
if (!i2pHostname.empty()) {
|
||||
printf("Embedded I2P: serverPort=0, clearing previously "
|
||||
"discovered destination\n");
|
||||
i2pHostname.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool advertised = false;
|
||||
std::string serverKeysPath = (fs::path(i2pDataDir) / "triangles-p2p-keys.dat").string();
|
||||
|
||||
// Step 1: read expected ident hash from the keys file.
|
||||
std::string keysFileIdentB32;
|
||||
try {
|
||||
std::ifstream ks(serverKeysPath, std::ifstream::binary);
|
||||
if (ks.is_open()) {
|
||||
ks.seekg(0, std::ios::end);
|
||||
size_t len = ks.tellg();
|
||||
ks.seekg(0, std::ios::beg);
|
||||
if (len == 0 || len > 65536) {
|
||||
throw std::runtime_error("implausible keys file size: " +
|
||||
std::to_string(len));
|
||||
}
|
||||
std::vector<uint8_t> buf(len);
|
||||
ks.read(reinterpret_cast<char*>(buf.data()), len);
|
||||
if (!ks) {
|
||||
throw std::runtime_error("short read on keys file");
|
||||
}
|
||||
i2p::data::PrivateKeys pk;
|
||||
if (!pk.FromBuffer(buf.data(), len)) {
|
||||
throw std::runtime_error("PrivateKeys::FromBuffer failed");
|
||||
}
|
||||
auto pub = pk.GetPublic();
|
||||
if (!pub) {
|
||||
throw std::runtime_error("PrivateKeys::GetPublic returned null");
|
||||
}
|
||||
keysFileIdentB32 = pub->GetIdentHash().ToBase32();
|
||||
} else {
|
||||
// Quiet on retry — file-not-found is expected before the
|
||||
// bootstrap thread writes it for the first time.
|
||||
if (i2pHostname.empty()) {
|
||||
// First failure: log at info level so the operator can
|
||||
// see why the hostname is still empty.
|
||||
printf("Embedded I2P: cannot open %s for server tunnel keys "
|
||||
"(will retry on next timerI2P tick)\n",
|
||||
serverKeysPath.c_str());
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
lastDiscoveryError = e.what();
|
||||
printf("Embedded I2P: keys-file ident hash load failed: %s\n", e.what());
|
||||
} catch (...) {
|
||||
lastDiscoveryError = "unknown exception";
|
||||
printf("Embedded I2P: keys-file ident hash load failed: unknown exception\n");
|
||||
}
|
||||
|
||||
// Step 2: only publish if a LIVE registered server tunnel matches
|
||||
// the keys-file hash. Registry membership confirms the tunnel is
|
||||
// active; LeaseSet publication is i2pd's responsibility after that.
|
||||
//
|
||||
// Thread-safety: GetServerTunnels() returns a const reference to
|
||||
// i2pd's internal m_ServerTunnels map, which has NO internal lock.
|
||||
// VisitTunnels(true) (called from ReloadConfig / Stop) can erase
|
||||
// entries concurrently. We make a VALUE COPY of the map (not a
|
||||
// reference) so that iterator invalidation during the copy is a
|
||||
// narrow read-only window, and all string comparisons run on the
|
||||
// local snapshot with no live-map access. The copy constructor of
|
||||
// std::map is exception-safe; if it throws (bad_alloc), the catch
|
||||
// below handles it.
|
||||
if (!keysFileIdentB32.empty()) {
|
||||
try {
|
||||
auto tunnels = i2p::client::context.GetServerTunnels(); // value copy
|
||||
for (const auto& kv : tunnels) {
|
||||
const auto& dest = kv.first.first;
|
||||
if (dest.ToBase32() == keysFileIdentB32) {
|
||||
if (i2pHostname != keysFileIdentB32 + ".b32.i2p") {
|
||||
i2pHostname = keysFileIdentB32 + ".b32.i2p";
|
||||
lastDiscoveryError.clear(); // success — clear stale
|
||||
printf("Embedded I2P: server tunnel address "
|
||||
"(live registry) = %s\n", i2pHostname.c_str());
|
||||
}
|
||||
advertised = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
lastDiscoveryError = e.what();
|
||||
printf("Embedded I2P: server tunnel registry read failed: %s\n", e.what());
|
||||
} catch (...) {
|
||||
lastDiscoveryError = "unknown exception";
|
||||
printf("Embedded I2P: server tunnel registry read failed: "
|
||||
"unknown exception\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (!advertised) {
|
||||
// Not yet in live registry. Leave empty (or clear stale value).
|
||||
if (!i2pHostname.empty()) {
|
||||
printf("Embedded I2P: server tunnel left live registry, "
|
||||
"clearing destination %s\n", i2pHostname.c_str());
|
||||
i2pHostname.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CI2PEmbedded::GetI2PAddress — read the cached destination, retrying
|
||||
// discovery if empty. Called from qt/trianglesgui.cpp:1875
|
||||
// (updateI2PAddress) on every timerI2P tick.
|
||||
//
|
||||
// Threading: read-by-copy under hostnameMutex so concurrent writes by
|
||||
// the bootstrap thread cannot tear the std::string.
|
||||
std::string CI2PEmbedded::GetI2PAddress()
|
||||
{
|
||||
bool needDiscovery = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||
needDiscovery = i2pHostname.empty() && running.load();
|
||||
}
|
||||
|
||||
if (needDiscovery) {
|
||||
// Tunnel may have registered since the bootstrap-thread scan.
|
||||
// Re-scans the registry and the keys file (does file I/O); not
|
||||
// "cheap" on retry, but bounded — single registry walk + one
|
||||
// small file read.
|
||||
DiscoverServerTunnelDestination();
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||
return i2pHostname;
|
||||
}
|
||||
|
||||
#else // !ENABLE_I2P_EMBEDDED
|
||||
|
||||
// ========================================================================
|
||||
@@ -641,6 +1005,14 @@ void CI2PEmbedded::Stop()
|
||||
running.store(false);
|
||||
}
|
||||
|
||||
// Stubs for the new methods (header declares them unconditionally)
|
||||
void CI2PEmbedded::DiscoverServerTunnelDestination() {}
|
||||
std::string CI2PEmbedded::GetI2PAddress()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hostnameMutex);
|
||||
return i2pHostname;
|
||||
}
|
||||
|
||||
#endif // ENABLE_I2P_EMBEDDED
|
||||
|
||||
// ========================================================================
|
||||
|
||||
+40
-3
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
// Cross-platform socket handle for SAM v3 streaming API.
|
||||
// On Windows this is the native SOCKET type; on POSIX it is int (fd).
|
||||
@@ -92,8 +94,40 @@ private:
|
||||
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)
|
||||
// Hostname and discovery-error cache are read by the Qt UI thread
|
||||
// (qt/trianglesgui.cpp:1875 updateI2PAddress) on every 5s timerI2P
|
||||
// tick and written by the bootstrap thread. Mutex-guarded to avoid
|
||||
// a C++ data race on the std::string itself.
|
||||
mutable std::mutex hostnameMutex;
|
||||
std::string i2pHostname; // Our .b32.i2p address (available after router startup)
|
||||
std::string lastError;
|
||||
// I2P bootstrap runs in a background thread; we keep the handle so Stop()
|
||||
// can join it. (A detached thread that is still running blocks process exit.)
|
||||
std::thread routerThread;
|
||||
|
||||
// Server-tunnel destination discovery.
|
||||
//
|
||||
// Scans the live server tunnel registry (i2p::client::context
|
||||
// ::GetServerTunnels()) for an entry whose ident hash matches the
|
||||
// public key in triangles-p2p-keys.dat. Sets i2pHostname to the
|
||||
// corresponding ".b32.i2p" address on success; leaves i2pHostname
|
||||
// empty otherwise. Thread-safe: the registry scan is mutex-guarded
|
||||
// inside libi2pd_client; we only read the resulting map.
|
||||
//
|
||||
// This is a no-op when serverPort == 0 (no inbound server tunnel
|
||||
// configured — pure outbound SOCKS I2P mode).
|
||||
//
|
||||
// Idempotent. Called from the bootstrap thread AND from
|
||||
// GetI2PAddress() when i2pHostname is empty, so the Qt timerI2P
|
||||
// (qt/trianglesgui.cpp:384-387) picks up the result on its next
|
||||
// 5s tick once the tunnel registers.
|
||||
void DiscoverServerTunnelDestination();
|
||||
|
||||
// Cache the most recent discovery failure reason (parsed keys-file
|
||||
// hash, registry-read error, etc.). Visible only to GetStartupError()
|
||||
// callers in the header — no public accessor for lastDiscoveryError
|
||||
// is needed today.
|
||||
std::string lastDiscoveryError;
|
||||
|
||||
public:
|
||||
static CI2PEmbedded* GetInstance();
|
||||
@@ -118,8 +152,11 @@ public:
|
||||
int GetServerPort() const { return serverPort; }
|
||||
const std::string& GetDataDir() const { return i2pDataDir; }
|
||||
|
||||
// Get our .b32.i2p destination address
|
||||
std::string GetI2PAddress() const { return i2pHostname; }
|
||||
// Get our .b32.i2p destination address. Triggers a discovery retry
|
||||
// if the hostname is empty (e.g. first attempt raced the tunnel
|
||||
// registration). Idempotent and cheap when the hostname is already
|
||||
// populated.
|
||||
std::string GetI2PAddress();
|
||||
std::string GetStartupError() const { return lastError; }
|
||||
void SetStartupError(const std::string& value) { lastError = value; }
|
||||
|
||||
@@ -141,4 +178,4 @@ public:
|
||||
bool StartEmbeddedI2P();
|
||||
void StopEmbeddedI2P();
|
||||
|
||||
#endif // TRIANGLES_I2P_EMBEDDED_H
|
||||
#endif // TRIANGLES_I2P_EMBEDDED_H
|
||||
+11
-10
@@ -3,22 +3,23 @@
|
||||
|
||||
// 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.
|
||||
// Nodes must run i2pd (embedded or external) 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.
|
||||
// These addresses were captured from running daemons via getnetworkinfo
|
||||
// on 2026-08-05. See i2pseed-capture-2026-08-05.md for the raw outputs.
|
||||
//
|
||||
// Dynamic seeds will also be available at:
|
||||
// Dynamic seeds are also available at:
|
||||
// https://seeds.cryptographic-triangles.org/i2p-seeds.txt
|
||||
static const char *strMainNetI2PSeed[][1] = {
|
||||
// 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"},
|
||||
// SAMI-PC - authoritative wallet node (main PC). Captured 2026-08-05.
|
||||
{"fecv4pomdm47epuadgrpkvxzjqfqwsjfc7t7xadwaac5bislyrhq.b32.i2p"},
|
||||
// DNS2 - primary bootstrap server (194.233.88.206). Captured 2026-08-05.
|
||||
{"7d5gujh6tw6xbd2uquedhpm3ixoglsgt3nkfqb4b5lvunhjdb2kq.b32.i2p"},
|
||||
// DNS3 - canonical chain reference (74.208.167.19). Captured 2026-08-05.
|
||||
{"jdrpj364rmdule7rw2jdl63wvk3kbaivuje7wyhayugjbxvgbj2a.b32.i2p"},
|
||||
{nullptr}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
+559
-198
@@ -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"
|
||||
@@ -28,8 +30,12 @@
|
||||
#include "addressindex.h"
|
||||
#include "chaindb_migrate.h"
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
|
||||
// Forward declaration: InitError / InitWarning are defined further down
|
||||
// in this file but referenced by AppInit (line ~423) before the definition.
|
||||
@@ -37,12 +43,22 @@ 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>
|
||||
|
||||
#ifdef WIN32
|
||||
// _get_osfhandle lives in <io.h>; FlushFileBuffers / HANDLE live in <windows.h>,
|
||||
// which is transitively included via util.h on Windows builds.
|
||||
#include <io.h>
|
||||
#endif
|
||||
|
||||
#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.
|
||||
@@ -57,9 +73,110 @@ static bool InitWarning(const std::string& str);
|
||||
#endif
|
||||
|
||||
using namespace std;
|
||||
using namespace boost;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
std::atomic<int> g_shutdownExitCode{EXIT_SUCCESS};
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
bool SyncReindexMarker(const fs::path& markerPath)
|
||||
{
|
||||
// POSIX systems guarantee parent-directory durability via fsync(dirfd).
|
||||
// Windows does not expose an equivalent primitive for directory metadata;
|
||||
// `_commit` flushes the file's data to disk and the underlying NTFS
|
||||
// journal commits the directory entry on close. Both paths below flush
|
||||
// before close to maximise durability; Windows users get file-data
|
||||
// durability equivalent to POSIX, with directory metadata committed by
|
||||
// the journal.
|
||||
FILE* marker = std::fopen(markerPath.string().c_str(), "wb");
|
||||
if (!marker)
|
||||
return false;
|
||||
static const char text[] = "Reindex must complete successfully before normal startup.\n";
|
||||
bool ok = std::fwrite(text, 1, sizeof(text) - 1, marker) == sizeof(text) - 1 &&
|
||||
std::fflush(marker) == 0;
|
||||
#ifdef WIN32
|
||||
// FlushFileBuffers on the file handle commits data durably to NTFS.
|
||||
intptr_t osHandle = _get_osfhandle(_fileno(marker));
|
||||
if (osHandle == -1 || FlushFileBuffers(reinterpret_cast<HANDLE>(osHandle)) == FALSE)
|
||||
ok = false;
|
||||
#else
|
||||
if (ok)
|
||||
ok = ::fsync(fileno(marker)) == 0;
|
||||
#endif
|
||||
if (std::fclose(marker) != 0)
|
||||
ok = false;
|
||||
#ifdef WIN32
|
||||
// No directory-fsync primitive on Windows. The journal commit on close
|
||||
// (and the FlushFileBuffers above) is the strongest durability available.
|
||||
// See comment block above.
|
||||
#else
|
||||
if (ok)
|
||||
{
|
||||
int dirFd = ::open(markerPath.parent_path().string().c_str(), O_RDONLY | O_DIRECTORY);
|
||||
if (dirFd < 0)
|
||||
return false;
|
||||
ok = ::fsync(dirFd) == 0;
|
||||
::close(dirFd);
|
||||
}
|
||||
#endif
|
||||
return ok;
|
||||
}
|
||||
|
||||
#ifndef WIN32
|
||||
bool EnsureOwnerOnlyFile(const std::filesystem::path& path, std::string& error)
|
||||
{
|
||||
struct stat fileStat;
|
||||
if (::lstat(path.string().c_str(), &fileStat) != 0)
|
||||
return errno == ENOENT;
|
||||
if (!S_ISREG(fileStat.st_mode) || fileStat.st_uid != geteuid()) {
|
||||
error = path.string() + " must be a regular file owned by the daemon user";
|
||||
return false;
|
||||
}
|
||||
if ((fileStat.st_mode & (S_IRWXG | S_IRWXO)) != 0 &&
|
||||
::chmod(path.string().c_str(), S_IRUSR | S_IWUSR) != 0) {
|
||||
error = "could not restrict permissions on " + path.string();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
void MarkShutdownFailure()
|
||||
{
|
||||
g_shutdownExitCode.store(EXIT_FAILURE, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::unique_ptr<CWallet> pwalletMain;
|
||||
CClientUIInterface uiInterface;
|
||||
std::string strWalletFileName;
|
||||
@@ -106,99 +223,24 @@ void ExitTimeout(void* parg)
|
||||
{
|
||||
#ifdef WIN32
|
||||
MilliSleep(5000);
|
||||
ExitProcess(0);
|
||||
ExitProcess(static_cast<UINT>(
|
||||
g_shutdownExitCode.load(std::memory_order_relaxed)));
|
||||
#endif
|
||||
}
|
||||
|
||||
// Wait up to maxWaitSec for at least minPeers peers to have reported their
|
||||
// chain height via the version handshake. Returns the median peer height, or
|
||||
// -1 if we couldn't get enough peers (timeout, no peers, all nStartingHeight=-1).
|
||||
int WaitForPeerHeights(int minPeers, int maxWaitSec)
|
||||
// Automatic recovery is intentionally non-destructive. Older builds deleted the
|
||||
// chain DB and blk0001.dat when a node lagged its peers, which could turn a
|
||||
// transient peer-height report into permanent local data loss. A privacy coin
|
||||
// must never rewrite historical chain data automatically; recovery remains an
|
||||
// explicit operator action after wallet and chain-state backups.
|
||||
// Legacy hook retained only to surface that -autorerebuild no longer mutates
|
||||
// chain state.
|
||||
static void LogAutoRebuildDisabled(int thresholdBlocks)
|
||||
{
|
||||
const int pollIntervalMs = 500;
|
||||
const int64_t deadline = GetTimeMillis() + (int64_t)maxWaitSec * 1000;
|
||||
|
||||
while (GetTimeMillis() < deadline && !fRequestShutdown) {
|
||||
std::vector<int> heights;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes) {
|
||||
if (pnode && pnode->nStartingHeight > 0)
|
||||
heights.push_back(pnode->nStartingHeight);
|
||||
}
|
||||
}
|
||||
if ((int)heights.size() >= minPeers) {
|
||||
std::sort(heights.begin(), heights.end());
|
||||
int median = heights[heights.size() / 2];
|
||||
printf("AutoRebuild: got %zu peer heights; median=%d\n", heights.size(), median);
|
||||
return median;
|
||||
}
|
||||
MilliSleep(pollIntervalMs);
|
||||
if (thresholdBlocks > 0) {
|
||||
printf("AutoRebuild: -autorerebuild=%d ignored; automatic chain deletion is disabled.\n",
|
||||
thresholdBlocks);
|
||||
}
|
||||
|
||||
std::vector<int> heights;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes) {
|
||||
if (pnode && pnode->nStartingHeight > 0)
|
||||
heights.push_back(pnode->nStartingHeight);
|
||||
}
|
||||
}
|
||||
if (heights.empty()) {
|
||||
printf("AutoRebuild: no peers reported heights after %ds\n", maxWaitSec);
|
||||
return -1;
|
||||
}
|
||||
std::sort(heights.begin(), heights.end());
|
||||
int median = heights[heights.size() / 2];
|
||||
printf("AutoRebuild: timed out with %zu peers; median=%d\n", heights.size(), median);
|
||||
return median;
|
||||
}
|
||||
|
||||
// If -autorerebuild is set and our local chain is more than that many blocks
|
||||
// behind the median peer height, wipe the chain DB (preserving wallet.dat +
|
||||
// onion + smsg state) and request shutdown. On restart, the daemon sees no
|
||||
// chain DB and the snapshot path takes over.
|
||||
void MaybeAutoRebuild(int thresholdBlocks)
|
||||
{
|
||||
if (thresholdBlocks <= 0)
|
||||
return;
|
||||
|
||||
if (nBestHeight < 0) {
|
||||
printf("AutoRebuild: local nBestHeight unset — skipping\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("AutoRebuild: enabled (threshold=%d blocks). Local chain tip: %d\n",
|
||||
thresholdBlocks, nBestHeight);
|
||||
int medianPeer = WaitForPeerHeights(/*minPeers=*/3, /*maxWaitSec=*/60);
|
||||
if (medianPeer <= 0) {
|
||||
printf("AutoRebuild: could not get peer heights — skipping rebuild\n");
|
||||
return;
|
||||
}
|
||||
|
||||
int lag = medianPeer - nBestHeight;
|
||||
printf("AutoRebuild: peer median=%d, local=%d, lag=%d\n",
|
||||
medianPeer, nBestHeight, lag);
|
||||
|
||||
if (lag < thresholdBlocks) {
|
||||
printf("AutoRebuild: lag %d < threshold %d — no rebuild needed\n",
|
||||
lag, thresholdBlocks);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("\n*** AutoRebuild: chain is %d blocks behind — wiping chain DB ***\n", lag);
|
||||
printf("*** Preserving wallet.dat, smsgDB, onion state. ***\n");
|
||||
printf("*** Daemon will shutdown; restart to load signed UTXO snapshot. ***\n\n");
|
||||
|
||||
WipeChainDataDir();
|
||||
|
||||
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||
if (fs::exists(blkPath)) {
|
||||
fs::remove(blkPath);
|
||||
printf("AutoRebuild: removed stale %s\n", blkPath.string().c_str());
|
||||
}
|
||||
|
||||
StartShutdown();
|
||||
}
|
||||
|
||||
void StartShutdown()
|
||||
@@ -295,6 +337,24 @@ void Shutdown(void* parg)
|
||||
// Make this thread recognisable as the shutdown thread
|
||||
RenameThread("Triangles-shutoff");
|
||||
|
||||
// Belt-and-suspenders: spawn a watchdog that force-exits if Shutdown()
|
||||
// doesn't complete in 30 seconds. This protects against deadlock in the
|
||||
// embedded Tor/I2P teardown paths (see notes/wallet-close-hang-fix-2026-07-07.md).
|
||||
std::thread([]()
|
||||
{
|
||||
#ifdef WIN32
|
||||
Sleep(30000);
|
||||
fprintf(stderr, "Shutdown watchdog: 30s elapsed, force-exiting process\n");
|
||||
fflush(stderr);
|
||||
ExitProcess(1);
|
||||
#else
|
||||
sleep(30);
|
||||
fprintf(stderr, "Shutdown watchdog: 30s elapsed, force-exiting process\n");
|
||||
fflush(stderr);
|
||||
_exit(1);
|
||||
#endif
|
||||
}).detach();
|
||||
|
||||
bool fFirstThread = false;
|
||||
{
|
||||
TRY_LOCK(cs_Shutdown, lockShutdown);
|
||||
@@ -340,10 +400,12 @@ void Shutdown(void* parg)
|
||||
pScriptCheckQueue.reset();
|
||||
}
|
||||
|
||||
// Stop the embedded I2P router.
|
||||
StopEmbeddedI2P();
|
||||
|
||||
// NOW safe to destroy Tor state - all threads have stopped
|
||||
ShutdownTorV3();
|
||||
StopEmbeddedTor();
|
||||
StopEmbeddedI2P();
|
||||
|
||||
#ifdef ENABLE_ZMQ
|
||||
if (pzmqNotifier)
|
||||
@@ -363,7 +425,11 @@ void Shutdown(void* parg)
|
||||
// MakeChainDB()->Close();
|
||||
bitdb.Flush(false);
|
||||
bitdb.Flush(true);
|
||||
fs::remove(GetPidFile());
|
||||
std::error_code pidFileError;
|
||||
fs::remove(GetPidFile(), pidFileError);
|
||||
if (pidFileError)
|
||||
printf("Warning: could not remove PID file: %s\n",
|
||||
pidFileError.message().c_str());
|
||||
UnregisterWallet(pwalletMain.get());
|
||||
pwalletMain.reset();
|
||||
// DB is flushed and wallet saved - safe to force-exit if something hangs
|
||||
@@ -373,7 +439,7 @@ void Shutdown(void* parg)
|
||||
fExit = true;
|
||||
#ifndef QT_GUI
|
||||
// ensure non-UI client gets exited here, but let Triangles-Qt reach 'return 0;' in triangles.cpp
|
||||
exit(0);
|
||||
exit(g_shutdownExitCode.load(std::memory_order_relaxed));
|
||||
#endif
|
||||
}
|
||||
else
|
||||
@@ -470,8 +536,10 @@ bool AppInit(int argc, char* argv[])
|
||||
} catch (...) {
|
||||
PrintException(nullptr, "AppInit()");
|
||||
}
|
||||
if (!fRet)
|
||||
if (!fRet) {
|
||||
MarkShutdownFailure();
|
||||
Shutdown(nullptr);
|
||||
}
|
||||
return fRet;
|
||||
}
|
||||
|
||||
@@ -552,8 +620,8 @@ std::string HelpMessage()
|
||||
//" -onlynet=<net> " + _("Only connect to nodes in network <net> (IPv4, IPv6 or Tor)") + "\n" +
|
||||
//" -discover " + _("Discover own IP address (default: 1 when listening and no -externalip)") + "\n" +
|
||||
//" -irc " + _("Find peers using internet relay chat (default: 0)") + "\n" +
|
||||
//" -listen " + _("Accept connections from outside (default: 1 if no -proxy or -connect)") + "\n" +
|
||||
//" -bind=<addr> " + _("Bind to given address. Use [host]:port notation for IPv6") + "\n" +
|
||||
" -listen " + _("Accept inbound peer connections (default: 1 unless -proxy or -connect is set)") + "\n" +
|
||||
" -bind=<addr> " + _("Bind inbound peers to this address. Use [host]:port notation for IPv6") + "\n" +
|
||||
// -dnsseed " + _("Find peers using DNS lookup (default: 1)") + "\n" +
|
||||
" -staking " + _("Stake your coins to support network and gain reward (default: 1)") + "\n" +
|
||||
" -synctime " + _("Sync time with other nodes. Disable if time on your system is precise e.g. syncing with NTP (default: 1)") + "\n" +
|
||||
@@ -561,7 +629,7 @@ std::string HelpMessage()
|
||||
" -onionseed " + _("Find peers using .onion seeds (default: 1 unless -connect)") + "\n" +
|
||||
" -seedurl=<host> " + _("HTTP seed list host (default: seeds.cryptographic-triangles.org)") + "\n" +
|
||||
" -noseedurl " + _("Disable HTTP seed list fetch on startup") + "\n" +
|
||||
" -autorerebuild=<n> " + _("If our chain is more than <n> blocks behind peers, wipe chain DB and shutdown for clean restart (default: 0=disabled)") + "\n" +
|
||||
" -autorerebuild=<n> " + _("Deprecated compatibility option; automatic chain deletion is disabled") + "\n" +
|
||||
" -banscore=<n> " + _("Threshold for disconnecting misbehaving peers (default: 100)") + "\n" +
|
||||
" -bantime=<n> " + _("Number of seconds to keep misbehaving peers from reconnecting (default: 86400)") + "\n" +
|
||||
" -par=<n> " + _("Set the number of script verification threads (default: auto, 0 = auto, 1 = single-threaded)") + "\n" +
|
||||
@@ -594,8 +662,11 @@ std::string HelpMessage()
|
||||
#endif
|
||||
" -rpcuser=<user> " + _("Username for JSON-RPC connections") + "\n" +
|
||||
" -rpcpassword=<pw> " + _("Password for JSON-RPC connections") + "\n" +
|
||||
" -rpcport=<port> " + _("Listen for JSON-RPC connections on <port> (default: 19111 or testnet: 19112)") + "\n" +
|
||||
" -rpcallowip=<ip> " + _("Allow JSON-RPC connections from specified IP address") + "\n" +
|
||||
" -rpcport=<port> " + _("Listen for JSON-RPC connections on <port> (default: 19112 or testnet: 19111)") + "\n" +
|
||||
" -rpcbind=<addr> " + _("Bind JSON-RPC to this address (default: loopback only; use * explicitly for all interfaces)") + "\n" +
|
||||
" -rpcallowip=<ip> " + _("Allow JSON-RPC clients matching this address pattern; does not change the bind address") + "\n" +
|
||||
" -rpcallowmethod=<name> " + _("Allow only this JSON-RPC method (repeat for each method; default: all)") + "\n" +
|
||||
" -rpcservertimeout=<n> " + _("RPC socket read/write timeout in seconds (default: 30, range: 1-600)") + "\n" +
|
||||
" -rpcconnect=<ip> " + _("Send commands to node running on <ip> (default: 127.0.0.1)") + "\n" +
|
||||
" -blocknotify=<cmd> " + _("Execute command when the best block changes (%s in cmd is replaced by block hash)") + "\n" +
|
||||
" -walletnotify=<cmd> " + _("Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)") + "\n" +
|
||||
@@ -610,6 +681,8 @@ std::string HelpMessage()
|
||||
" -checkblocks=<n> " + _("How many blocks to check at startup (default: 2500, 0 = all)") + "\n" +
|
||||
" -checklevel=<n> " + _("How thorough the block verification is (0-6, default: 1)") + "\n" +
|
||||
" -loadblock=<file> " + _("Imports blocks from external blk000?.dat file") + "\n" +
|
||||
" -reindex " + _("Rebuild the derived chain database from the existing blk0001.dat without modifying the raw block file") + "\n" +
|
||||
" -rebuildutxo " + _("Rebuild UTXO set from full block chain (slow, for recovery)") + "\n" +
|
||||
|
||||
"\n" + _("Block creation options:") + "\n" +
|
||||
" -blockminsize=<n> " + _("Set minimum block size in bytes (default: 0)") + "\n" +
|
||||
@@ -884,8 +957,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)
|
||||
@@ -931,6 +1003,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!"
|
||||
@@ -941,33 +1028,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();
|
||||
@@ -1018,15 +1135,19 @@ bool AppInit2()
|
||||
fUseUPnP = GetBoolArg("-upnp", USE_UPNP);
|
||||
#endif
|
||||
bool fBound = false;
|
||||
if (true) {
|
||||
if (true) {
|
||||
do {
|
||||
// Bind to all interfaces so external peers can connect
|
||||
if (!fNoListen) {
|
||||
if (mapArgs.count("-bind")) {
|
||||
for (const std::string& bindAddress : mapMultiArgs["-bind"]) {
|
||||
CService addrBind;
|
||||
if (!Lookup("0.0.0.0", addrBind, GetListenPort(), false))
|
||||
return InitError(strprintf(_("Cannot resolve binding address: '%s'"), "0.0.0.0"));
|
||||
if (!Lookup(bindAddress.c_str(), addrBind, GetListenPort(), false))
|
||||
return InitError(strprintf(_("Cannot resolve -bind address: '%s'"),
|
||||
bindAddress.c_str()));
|
||||
fBound |= Bind(addrBind);
|
||||
} while (false);
|
||||
}
|
||||
} else {
|
||||
struct in_addr any;
|
||||
any.s_addr = htonl(INADDR_ANY);
|
||||
fBound = Bind(CService(any, GetListenPort()));
|
||||
}
|
||||
if (!fBound)
|
||||
return InitError(_("Failed to listen on any port."));
|
||||
@@ -1056,10 +1177,9 @@ bool AppInit2()
|
||||
}
|
||||
}
|
||||
|
||||
if (mapArgs.count("-checkpointkey")) // triangles: checkpoint master priv key
|
||||
if (mapArgs.count("-checkpointkey"))
|
||||
{
|
||||
if (!Checkpoints::SetCheckpointPrivKey(GetArg(std::string_view{"-checkpointkey"}, std::string_view{""})))
|
||||
InitError(_("Unable to sign checkpoint, wrong checkpointkey?\n"));
|
||||
return InitError(_("Synchronized checkpoint signing is disabled."));
|
||||
}
|
||||
|
||||
for (string strDest : mapMultiArgs["-seednode"])
|
||||
@@ -1067,8 +1187,8 @@ bool AppInit2()
|
||||
StartupPerfLog("network_init", GetTimeMillis() - nStart, strprintf("listen=%d seednodes=%" PRIszu, !fNoListen, mapMultiArgs["-seednode"].size()));
|
||||
|
||||
// ********************************************************* Step 6b: bootstrap download (daemon)
|
||||
// Automatic: if data dir has no blockchain, bootstrap without asking.
|
||||
// Can also be forced with -bootstrap flag, or disabled with -nobootstrap.
|
||||
// Remote HTTP bootstrap is opt-in via -bootstrap. Fresh nodes otherwise
|
||||
// use the compiled-hash P2P snapshot path or sync from genesis.
|
||||
//
|
||||
// v5.9.5: P2P UTXO snapshot fetch is the default for fresh installs (Step 11.6).
|
||||
// The legacy clearnet HTTP bootstrap only runs when the user explicitly requests
|
||||
@@ -1076,17 +1196,13 @@ bool AppInit2()
|
||||
// Bootstrap auto-download works for both GUI and daemon.
|
||||
// GUI users get the same automatic bootstrap on fresh installs.
|
||||
{
|
||||
bool wantsBootstrap = GetBoolArg("-bootstrap", false);
|
||||
bool noBootstrap = GetBoolArg("-nobootstrap", false);
|
||||
bool snapshotMode = GetBoolArg("-snapshot", true);
|
||||
bool wantsBootstrap = GetBoolArg("-bootstrap", false) && !noBootstrap;
|
||||
fs::path dataPath = GetDataDir();
|
||||
bool needsBootstrap = Bootstrap::NeedsBootstrap(dataPath);
|
||||
|
||||
if (needsBootstrap && !noBootstrap) {
|
||||
printf("Bootstrap: no blockchain data found — downloading UTXO snapshot automatically.\n");
|
||||
printf("Bootstrap: (use -nobootstrap to skip)\n");
|
||||
uiInterface.InitMessage(_("Downloading UTXO snapshot..."));
|
||||
wantsBootstrap = true;
|
||||
if (needsBootstrap && !wantsBootstrap) {
|
||||
printf("Bootstrap: no blockchain data found; remote bootstrap is disabled unless -bootstrap is set.\n");
|
||||
}
|
||||
|
||||
if (wantsBootstrap)
|
||||
@@ -1094,7 +1210,6 @@ bool AppInit2()
|
||||
int64_t nBootstrapStart = GetTimeMillis();
|
||||
fs::path dataPath = GetDataDir();
|
||||
std::string host = Bootstrap::DEFAULT_HOST;
|
||||
std::string strError;
|
||||
|
||||
int64_t lastGuiUpdate = 0;
|
||||
auto progressFn = [&lastGuiUpdate](int64_t bytesDownloaded, int64_t totalBytes) {
|
||||
@@ -1136,19 +1251,10 @@ bool AppInit2()
|
||||
triedUtxoSnapshot = true;
|
||||
}
|
||||
|
||||
// Fall back to full bootstrap.tar.gz if UTXO snapshot failed
|
||||
// Never consume a server-directed file list. If the authenticated
|
||||
// snapshot is unavailable, normal peer-to-peer sync is the safe fallback.
|
||||
if (!success) {
|
||||
uiInterface.InitMessage(_("Downloading blockchain snapshot..."));
|
||||
printf("Bootstrap: contacting %s...\n", host.c_str());
|
||||
|
||||
success = Bootstrap::DownloadBootstrap(host, dataPath, progressFn, strError);
|
||||
|
||||
if (!success) {
|
||||
printf("\nBootstrap: failed: %s\n", strError.c_str());
|
||||
printf("Bootstrap: skipping, will sync from network.\n");
|
||||
} else {
|
||||
printf("\nBootstrap: done.\n");
|
||||
}
|
||||
printf("Bootstrap: no trusted compiled-hash snapshot available; syncing from peers.\n");
|
||||
}
|
||||
|
||||
StartupPerfLog("bootstrap_download", GetTimeMillis() - nBootstrapStart,
|
||||
@@ -1168,13 +1274,59 @@ bool AppInit2()
|
||||
printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n");
|
||||
uiInterface.InitMessage(_("Loading UTXO snapshot..."));
|
||||
|
||||
// Local file load: skip the checkpoint gate. The operator has
|
||||
// filesystem access, so the trust model is already equivalent
|
||||
// to direct chain state modification — a malicious local file
|
||||
// is no worse than a malicious chain DB. P2P-delivered
|
||||
// snapshots (SnapshotNet) keep the checkpoint gate on.
|
||||
// Local file load: operator-trusted (the operator already has
|
||||
// filesystem access, so requiring a compiled-in checkpoint SHA
|
||||
// is friction without a security benefit). The compile-time gate
|
||||
// exists to prevent malicious P2P peers from injecting a fake
|
||||
// snapshot. Local-file loads skip it via requireCheckpoint=false.
|
||||
// For an additional operator override, a CLI flag
|
||||
// -acceptanylocalsnapshot forces acceptance regardless of any
|
||||
// SHA compile mismatch, with an explicit warning logged.
|
||||
const bool forceAccept = GetBoolArg("-acceptanylocalsnapshot", false);
|
||||
std::string strError;
|
||||
if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError, /*requireCheckpoint=*/false)) {
|
||||
const int snapshotHeight = Checkpoints::GetBestSnapshotHeight();
|
||||
uint256 compiledHash;
|
||||
uint256 actualHash;
|
||||
const bool hasCompiledHash = snapshotHeight > 0 &&
|
||||
Checkpoints::GetSnapshotHash(snapshotHeight, compiledHash);
|
||||
const bool hashVerified = hasCompiledHash &&
|
||||
SnapshotNet::ComputeSnapshotFileHash(snapshotFile, actualHash, strError) &&
|
||||
actualHash == compiledHash;
|
||||
const bool hashMismatchWarning = hasCompiledHash && !hashVerified;
|
||||
int heightInSnapshot = 0;
|
||||
{
|
||||
FILE* hf = fopen(snapshotFile.string().c_str(), "rb");
|
||||
if (hf) {
|
||||
unsigned int magic, version;
|
||||
int height;
|
||||
if (fread(&magic, sizeof(magic), 1, hf) == 1 &&
|
||||
fread(&version, sizeof(version), 1, hf) == 1 &&
|
||||
fread(&height, sizeof(height), 1, hf) == 1) {
|
||||
heightInSnapshot = height;
|
||||
}
|
||||
fclose(hf);
|
||||
}
|
||||
}
|
||||
|
||||
if (forceAccept) {
|
||||
printf("UTXO snapshot SHA256 NOT in compiled map; "
|
||||
"-acceptanylocalsnapshot set, accepting anyway.\n");
|
||||
if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError,
|
||||
/*requireCheckpoint=*/false)) {
|
||||
printf("UTXO snapshot loaded successfully (forced accept).\n");
|
||||
} else {
|
||||
printf("UTXO snapshot load failed: %s\n", strError.c_str());
|
||||
printf("Will proceed with normal sync.\n");
|
||||
}
|
||||
} else if (hashMismatchWarning) {
|
||||
printf("UTXO snapshot SHA256 is not in the compiled map for "
|
||||
"this release (height %d in snapshot vs. height %d "
|
||||
"in compiled map). To load it anyway, restart the "
|
||||
"daemon with -acceptanylocalsnapshot=1.\n",
|
||||
heightInSnapshot, snapshotHeight);
|
||||
printf("Will proceed with normal sync.\n");
|
||||
} else if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError,
|
||||
/*requireCheckpoint=*/false)) {
|
||||
printf("UTXO snapshot loaded successfully.\n");
|
||||
} else {
|
||||
printf("UTXO snapshot load failed: %s\n", strError.c_str());
|
||||
@@ -1183,14 +1335,49 @@ 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);
|
||||
// A rocksdb/ directory containing the MIGRATION_INCOMPLETE marker is a
|
||||
// crashed previous migration, NOT a usable chain DB — treat it the same
|
||||
// as "no rocksdb yet" so the migration is retried instead of silently
|
||||
// opening a truncated database.
|
||||
bool fCrashedMigration = fs::exists(GetDataDir() / "rocksdb" / "MIGRATION_INCOMPLETE");
|
||||
bool fAuto = IsRocksDbChainBackend() &&
|
||||
fs::exists(GetDataDir() / "txleveldb") &&
|
||||
(!fs::exists(GetDataDir() / "rocksdb") || fCrashedMigration);
|
||||
if (fExplicit || fAuto)
|
||||
{
|
||||
uiInterface.InitMessage(_("Migrating chain database to RocksDB..."));
|
||||
if (fAuto && !fExplicit)
|
||||
printf("ChainDB: RocksDB backend active with a legacy LevelDB present%s; "
|
||||
"migrating automatically.\n",
|
||||
fCrashedMigration ? " and a previous migration was interrupted" : "");
|
||||
std::string strMigrateError;
|
||||
bool fForce = GetBoolArg("-migratechaindbforce", false);
|
||||
if (!MaybeMigrateLevelDbToRocksDb(fForce, strMigrateError))
|
||||
return InitError(strprintf(_("Chain DB migration failed: %s"), strMigrateError.c_str()));
|
||||
}
|
||||
// Last line of defense: never open a RocksDB that still carries the
|
||||
// incomplete-migration marker (e.g. the LevelDB source was deleted so
|
||||
// the migration cannot be retried). Opening it would silently run on a
|
||||
// partial chain state.
|
||||
if (IsRocksDbChainBackend() &&
|
||||
fs::exists(GetDataDir() / "rocksdb" / "MIGRATION_INCOMPLETE"))
|
||||
{
|
||||
return InitError(_("The RocksDB chain database is left over from an interrupted "
|
||||
"migration and is incomplete. Delete the 'rocksdb' directory in the "
|
||||
"data directory and restart (it will be rebuilt by migration or resync)."));
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************************************* Step 7: load blockchain
|
||||
@@ -1212,64 +1399,213 @@ bool AppInit2()
|
||||
}
|
||||
|
||||
// Handle -reindex: delete the chain DB so it gets rebuilt from the raw
|
||||
// blk*.dat files. This recalculates money
|
||||
// blk0001.dat file used by this storage format. This recalculates money
|
||||
// supply, tx index, and UTXO set from scratch. Backend-agnostic via
|
||||
// WipeChainDataDir(), which resolves the directory per the configured
|
||||
// -chaindb backend.
|
||||
if (GetBoolArg("-reindex", false))
|
||||
const bool fReindex = GetBoolArg("-reindex", false);
|
||||
fs::path reindexMarker = GetDataDir() / "REINDEX_INCOMPLETE";
|
||||
|
||||
// Validate the immutable source before removing any derived state. A marker
|
||||
// survives crashes/interruption so ordinary startup cannot trust a partial
|
||||
// database left by an earlier recovery attempt.
|
||||
if (fReindex)
|
||||
{
|
||||
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||
if (!fs::exists(blkPath) || !fs::is_regular_file(blkPath))
|
||||
return InitError(_("Reindex requested but blk0001.dat is missing or not a regular file"));
|
||||
if (!SyncReindexMarker(reindexMarker))
|
||||
return InitError(_("Cannot durably create REINDEX_INCOMPLETE marker in the data directory"));
|
||||
|
||||
printf("Reindex requested: removing chain database...\n");
|
||||
uiInterface.InitMessage(_("Removing chain database for reindex..."));
|
||||
WipeChainDataDir();
|
||||
if (fs::exists(GetChainDataDir()))
|
||||
return InitError(_("Reindex could not remove the existing chain database"));
|
||||
}
|
||||
else if (fs::exists(reindexMarker))
|
||||
{
|
||||
return InitError(_("A previous reindex was interrupted. Restart with -reindex to rebuild derived chain state."));
|
||||
}
|
||||
|
||||
uiInterface.InitMessage(_("Loading block index..."));
|
||||
printf("Loading block index...\n");
|
||||
nStart = GetTimeMillis();
|
||||
if (!LoadBlockIndex())
|
||||
return InitError(_("Error loading blkindex.dat"));
|
||||
// Normal startup loads the existing derived index. An explicit -reindex
|
||||
// must NOT call LoadBlockIndex() first: on an empty database that routine
|
||||
// creates and appends a new genesis record to blk0001.dat. Reindex instead
|
||||
// rebuilds directly from the already-existing raw history, keeping the
|
||||
// source block file byte-for-byte unchanged.
|
||||
if (fReindex)
|
||||
{
|
||||
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||
if (!fs::exists(blkPath))
|
||||
return InitError(_("Reindex requested but blk0001.dat is missing"));
|
||||
|
||||
// triangles fix (pitfall #61): initialize pindexFinalized from the
|
||||
// hardcoded checkpoint on startup, BEFORE the daemon opens any peer
|
||||
// connections or processes any block messages.
|
||||
printf("Reindex: rebuilding chain database from existing %s (raw block file will not be modified)\n",
|
||||
blkPath.string().c_str());
|
||||
uiInterface.InitMessage(_("Reindexing blocks from blk0001.dat..."));
|
||||
int64_t nReindexStart = GetTimeMillis();
|
||||
if (!FastImportBlockFile())
|
||||
return InitError(_("Reindex failed while rebuilding from blk0001.dat"));
|
||||
StartupPerfLog("reindex_fast_import", GetTimeMillis() - nReindexStart,
|
||||
strprintf("bestheight=%d indexsize=%" PRIszu,
|
||||
nBestHeight, mapBlockIndex.size()));
|
||||
std::error_code markerError;
|
||||
if (!fs::remove(reindexMarker, markerError) || markerError)
|
||||
return InitError(_("Reindex completed but REINDEX_INCOMPLETE marker could not be removed"));
|
||||
}
|
||||
else if (!LoadBlockIndex())
|
||||
{
|
||||
return InitError(_("Error loading blkindex.dat"));
|
||||
}
|
||||
|
||||
// pindexLastHardenedCheckpoint is initialized from the hardened checkpoint
|
||||
// map on startup, BEFORE the daemon opens any peer connections or
|
||||
// processes any block messages. It is intentionally NOT advanced at
|
||||
// runtime — see fix/consensus-convergence.
|
||||
//
|
||||
// Without this, pindexFinalized stays NULL on a fresh restart even when
|
||||
// we have 2.2M blocks on disk, because the auto-checkpoint code in
|
||||
// ActivateBestChain() at main.cpp:2459 only sets it when
|
||||
// !IsInitialBlockDownload(). If the chain tip is more than 24h stale
|
||||
// (which happens on every restart with a synced chain), IsInitialBlockDownload()
|
||||
// returns true and pindexFinalized never gets set.
|
||||
//
|
||||
// The downstream reorg guard at main.cpp:2198 short-circuits when
|
||||
// pindexFinalized is NULL, which allowed a 3,755-block minority fork
|
||||
// to overwrite a healthy 2,206,004-block chain on 2026-06-16. Loading
|
||||
// the hardcoded checkpoint from checkpoints.cpp (block 2,205,000) on
|
||||
// startup means the reorg guard is always active whenever the
|
||||
// checkpointed block is in our local mapBlockIndex.
|
||||
// GetLastCheckpoint(mapBlockIndex) returns the newest compiled
|
||||
// checkpoint present in this node's local block index. On current
|
||||
// master (2026-07) the newest compiled checkpoint is whatever block
|
||||
// hash is highest in src/checkpoints.cpp::mapCheckpoints and present
|
||||
// in the local index; it is NOT hardcoded to block 2,205,000 here.
|
||||
// The downstream rules that consume this variable are:
|
||||
// - main.cpp Reorganize(): reject reorgs whose fork point is at
|
||||
// or below the checkpoint height. This is THE consensus-validating
|
||||
// guard. It has a bootstrap-time fallback that reads the compiled
|
||||
// map directly via Checkpoints::GetLastCheckpointHeight() when
|
||||
// this pointer is still NULL (early IBD / reindex / bootstrap
|
||||
// before the checkpoint block has been downloaded) — see the
|
||||
// fix/consensus-convergence review notes.
|
||||
// - main.cpp getheaders handler: when the peer's locator contains
|
||||
// the checkpoint, serve canonical headers from the checkpoint
|
||||
// forward; otherwise fall back to the last common ancestor (or
|
||||
// genesis if none). This is the recovery path for forked peers.
|
||||
// SERVING-side only; not a consensus guard.
|
||||
{
|
||||
CBlockIndex* pCheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex);
|
||||
if (pCheckpoint && pCheckpoint != pindexFinalized)
|
||||
if (pCheckpoint && pCheckpoint != pindexLastHardenedCheckpoint)
|
||||
{
|
||||
pindexFinalized = pCheckpoint;
|
||||
printf("STARTUP-CHECKPOINT: pindexFinalized set to block %d (%s) from hardcoded checkpoint\n",
|
||||
pindexFinalized->nHeight, pindexFinalized->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
pindexLastHardenedCheckpoint = pCheckpoint;
|
||||
printf("STARTUP-CHECKPOINT: pindexLastHardenedCheckpoint set to block %d (%s) from compiled hardened checkpoint\n",
|
||||
pindexLastHardenedCheckpoint->nHeight, pindexLastHardenedCheckpoint->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
}
|
||||
else if (!pCheckpoint)
|
||||
{
|
||||
printf("STARTUP-CHECKPOINT: WARNING — hardcoded checkpoint not in local block index, pindexFinalized remains NULL\n");
|
||||
printf("STARTUP-CHECKPOINT: WARNING — no compiled hardened checkpoint present in local block index, pindexLastHardenedCheckpoint remains NULL\n");
|
||||
}
|
||||
}
|
||||
|
||||
// AutoRebuild: if -autorerebuild is set and we are behind peers, wipe chain DB
|
||||
// and shutdown for clean restart.
|
||||
MaybeAutoRebuild(GetArg("-autorerebuild", 0));
|
||||
if (fRequestShutdown) {
|
||||
printf("AutoRebuild: shutdown requested before chain load complete\n");
|
||||
return false;
|
||||
// Handle -rebuildutxo: rebuild UTXO set from full block chain
|
||||
if (GetBoolArg("-rebuildutxo", false))
|
||||
{
|
||||
printf("UTXO rebuild requested: rebuilding UTXO set from full block chain...\n");
|
||||
uiInterface.InitMessage(_("Rebuilding UTXO set from block chain..."));
|
||||
|
||||
auto txdb = MakeChainDB("r+");
|
||||
if (!txdb) {
|
||||
return InitError(_("Failed to open chain database for UTXO rebuild"));
|
||||
}
|
||||
|
||||
// Clear existing UTXO set
|
||||
printf("Clearing existing UTXO set...\n");
|
||||
// Note: We'd need to iterate and erase all UTXOs here
|
||||
// For now, we'll just rebuild on top of existing (will overwrite)
|
||||
|
||||
// Walk all blocks from genesis to tip
|
||||
int nHeight = 0;
|
||||
CBlockIndex* pindex = pindexGenesisBlock;
|
||||
int64_t nStartTime = GetTimeMillis();
|
||||
|
||||
while (pindex && !fRequestShutdown)
|
||||
{
|
||||
// Skip genesis block - it doesn't follow normal PoW rules and has no spendable outputs
|
||||
if (pindex->nHeight == 0)
|
||||
{
|
||||
pindex = pindex->pnext;
|
||||
continue;
|
||||
}
|
||||
|
||||
CBlock block;
|
||||
if (!block.ReadFromDisk(pindex))
|
||||
{
|
||||
printf("ERROR: Failed to read block %d (%s)\n", pindex->nHeight, pindex->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
printf("DEBUG: nBits=%08x, IsPoW=%d, hash=%s\n", pindex->nBits, pindex->IsProofOfWork(), pindex->GetBlockHash().ToString().c_str());
|
||||
return InitError(_("Failed to read block during UTXO rebuild"));
|
||||
}
|
||||
|
||||
// Process all transactions in this block
|
||||
for (const CTransaction& tx : block.vtx)
|
||||
{
|
||||
uint256 hashTx = tx.GetHash();
|
||||
|
||||
// Add all outputs to UTXO set
|
||||
for (unsigned int n = 0; n < tx.vout.size(); n++)
|
||||
{
|
||||
const CTxOut& txout = tx.vout[n];
|
||||
if (txout.IsEmpty())
|
||||
continue;
|
||||
|
||||
CUtxoEntry entry;
|
||||
entry.nValue = txout.nValue;
|
||||
entry.nHeight = pindex->nHeight;
|
||||
entry.scriptPubKey = txout.scriptPubKey;
|
||||
entry.fCoinBase = tx.IsCoinBase();
|
||||
entry.fCoinStake = tx.IsCoinStake();
|
||||
entry.nTxTime = tx.nTime;
|
||||
|
||||
if (!txdb->WriteUtxo(hashTx, n, entry))
|
||||
{
|
||||
printf("ERROR: Failed to write UTXO %s:%d\n", hashTx.ToString().substr(0,20).c_str(), n);
|
||||
return InitError(_("Failed to write UTXO during rebuild"));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove spent inputs from UTXO set (skip coinbase)
|
||||
if (!tx.IsCoinBase())
|
||||
{
|
||||
for (const CTxIn& txin : tx.vin)
|
||||
{
|
||||
if (!txdb->EraseUtxo(txin.prevout.hash, txin.prevout.n))
|
||||
{
|
||||
printf("WARNING: Failed to erase spent UTXO %s:%d (may already be spent)\n",
|
||||
txin.prevout.hash.ToString().substr(0,20).c_str(), txin.prevout.n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nHeight++;
|
||||
if (nHeight % 10000 == 0)
|
||||
{
|
||||
int64_t nElapsed = GetTimeMillis() - nStartTime;
|
||||
printf("UTXO rebuild: processed %d blocks (%.1f blocks/sec)\n",
|
||||
nHeight, nHeight * 1000.0 / nElapsed);
|
||||
}
|
||||
|
||||
pindex = pindex->pnext;
|
||||
}
|
||||
|
||||
if (fRequestShutdown)
|
||||
{
|
||||
printf("UTXO rebuild interrupted by shutdown request\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
int64_t nTotalTime = GetTimeMillis() - nStartTime;
|
||||
printf("UTXO rebuild complete: processed %d blocks in %.1f seconds (%.1f blocks/sec)\n",
|
||||
nHeight, nTotalTime / 1000.0, nHeight * 1000.0 / nTotalTime);
|
||||
|
||||
uiInterface.InitMessage(_("UTXO rebuild complete"));
|
||||
}
|
||||
|
||||
// Block index loaded. With fast-import removed, the only supported sync path
|
||||
// is the UTXO snapshot (auto-downloaded from bootstrap or placed manually in datadir).
|
||||
// Keep the legacy option parse for compatibility, but automatic recovery is
|
||||
// diagnostic-only and never removes chain data.
|
||||
LogAutoRebuildDisabled(GetArg("-autorerebuild", 0));
|
||||
|
||||
// Block index loaded. Normal bootstrap uses the UTXO snapshot; explicit
|
||||
// -reindex is the operator-only recovery path from local blk0001.dat.
|
||||
|
||||
// as LoadBlockIndex can take several minutes, it's possible the user
|
||||
// requested to kill triangles-qt during the last operation. If so, exit.
|
||||
@@ -1346,6 +1682,11 @@ bool AppInit2()
|
||||
{
|
||||
fs::path walletPath = GetDataDir() / strWalletFileName;
|
||||
if (fs::exists(walletPath)) {
|
||||
#ifndef WIN32
|
||||
std::string permissionError;
|
||||
if (!EnsureOwnerOnlyFile(walletPath, permissionError))
|
||||
return InitError(permissionError);
|
||||
#endif
|
||||
uintmax_t wsize = fs::file_size(walletPath);
|
||||
printf("Wallet file size: %llu bytes\n", (unsigned long long)wsize);
|
||||
if (wsize < 1024) {
|
||||
@@ -1670,6 +2011,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
|
||||
@@ -1758,10 +2119,10 @@ bool AppInit2()
|
||||
printf("mapAddressBook.size() = %" PRIszu "\n", pwalletMain->mapAddressBook.size());
|
||||
|
||||
if (!NewThread(StartNode, nullptr))
|
||||
InitError(_("Error: could not start node"));
|
||||
return InitError(_("Error: could not start node"));
|
||||
|
||||
if (fServer)
|
||||
NewThread(ThreadRPCServer, nullptr);
|
||||
if (fServer && !NewThread(ThreadRPCServer, nullptr))
|
||||
return InitError(_("Error: could not start the RPC server"));
|
||||
|
||||
// ********************************************************* Step 11.6: P2P UTXO snapshot fetch
|
||||
// If the chain is empty and snapshot mode is enabled (default), spawn a
|
||||
|
||||
+1
-1
@@ -12,6 +12,7 @@
|
||||
extern std::unique_ptr<CWallet> pwalletMain;
|
||||
extern std::string strWalletFileName;
|
||||
void StartShutdown();
|
||||
void MarkShutdownFailure();
|
||||
bool ShutdownRequested();
|
||||
void Shutdown(void* parg);
|
||||
bool AppInit2();
|
||||
@@ -19,4 +20,3 @@ std::string HelpMessage();
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
+21
-18
@@ -31,24 +31,27 @@ int64_t GetWeight(int64_t nIntervalBeginning, int64_t nIntervalEnd)
|
||||
if (nAge < 0)
|
||||
return 0;
|
||||
|
||||
// After v5 fork: use soft cap of 7 days instead of hard nStakeMaxAge.
|
||||
// This prevents "stake surprise" where a whale who was offline for weeks
|
||||
// comes back with massively amplified staking power and dominates blocks.
|
||||
// The 7-day cap still allows generous accumulation while limiting abuse.
|
||||
static const int64_t STAKE_AGE_SOFT_CAP = 7 * 24 * 60 * 60; // 7 days
|
||||
// Activation gate: the soft cap shipped 2026-04-20 without a height/time
|
||||
// gate, retroactively invalidating earlier blocks staked with long-aged
|
||||
// coins (e.g. coins idle through the 2022-2026 freeze). Apply the cap
|
||||
// only to stakes after the activation timestamp; historical stakes
|
||||
// validate under the rules they were created with (uncapped age).
|
||||
static const int64_t STAKE_AGE_SOFT_CAP_ACTIVATION = 1776000000; // 2026-04-12 ~13:20 UTC
|
||||
if (pindexBest && pindexBest->nHeight >= FORK_HEIGHT_V5)
|
||||
{
|
||||
if (nIntervalEnd >= STAKE_AGE_SOFT_CAP_ACTIVATION)
|
||||
return min(nAge, STAKE_AGE_SOFT_CAP);
|
||||
return nAge;
|
||||
}
|
||||
|
||||
// Original Peercoin/PPCoin behavior: hard cap at nStakeMaxAge.
|
||||
//
|
||||
// Historical context: an earlier V5-fork variant of this function
|
||||
// replaced the cap with a 7-day SOFT cap (STAKE_AGE_SOFT_CAP), with an
|
||||
// activation gate of 2026-04-12. The intent was to limit "stake
|
||||
// surprise" from whales returning after long offline periods. The
|
||||
// side effect was to cap long-dormant coins at the same weight as
|
||||
// freshly-staked coins, eliminating the diamond-hands incentive that
|
||||
// makes PoS economically meaningful for long-term holders.
|
||||
//
|
||||
// The chain froze at block 2,224,763 on 2026-07-18 — over 14 days
|
||||
// later — with no blocks produced during the entire soft-cap window.
|
||||
// Reverting to the original uncapped cap restores the original
|
||||
// Peercoin staking economics for future blocks.
|
||||
//
|
||||
// Validation safety: the soft-cap branch was gated to require
|
||||
// nIntervalEnd >= 1776000000 (2026-04-12), AND pindexBest->nHeight
|
||||
// >= FORK_HEIGHT_V5. The chain never advanced past block 2,224,763
|
||||
// during the soft-cap window, so no historical block was ever
|
||||
// validated under the soft cap. Therefore reverting this branch
|
||||
// changes zero historical block validation results.
|
||||
return min(nAge, (int64_t)nStakeMaxAge);
|
||||
}
|
||||
|
||||
|
||||
+41
-20
@@ -190,28 +190,49 @@ bool CCryptoKeyStore::GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) co
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn)
|
||||
bool CCryptoKeyStore::PrepareKeyEncryption(CKeyingMaterial& vMasterKeyIn,
|
||||
CryptedKeyMap& cryptedKeysOut) const
|
||||
{
|
||||
{
|
||||
LOCK(cs_KeyStore);
|
||||
if (!mapCryptedKeys.empty() || IsCrypted())
|
||||
return false;
|
||||
LOCK(cs_KeyStore);
|
||||
if (!mapCryptedKeys.empty() || IsCrypted())
|
||||
return false;
|
||||
|
||||
fUseCrypto = true;
|
||||
for (KeyMap::value_type& mKey : mapKeys)
|
||||
{
|
||||
CKey key;
|
||||
if (!key.SetSecret(mKey.second.first, mKey.second.second))
|
||||
return false;
|
||||
const CPubKey vchPubKey = key.GetPubKey();
|
||||
std::vector<unsigned char> vchCryptedSecret;
|
||||
bool fCompressed;
|
||||
if (!EncryptSecret(vMasterKeyIn, key.GetSecret(fCompressed), vchPubKey.GetHash(), vchCryptedSecret))
|
||||
return false;
|
||||
if (!AddCryptedKey(vchPubKey, vchCryptedSecret))
|
||||
return false;
|
||||
}
|
||||
mapKeys.clear();
|
||||
cryptedKeysOut.clear();
|
||||
for (const KeyMap::value_type& mKey : mapKeys)
|
||||
{
|
||||
CKey key;
|
||||
if (!key.SetSecret(mKey.second.first, mKey.second.second))
|
||||
return false;
|
||||
const CPubKey vchPubKey = key.GetPubKey();
|
||||
std::vector<unsigned char> vchCryptedSecret;
|
||||
bool fCompressed;
|
||||
if (!EncryptSecret(vMasterKeyIn, key.GetSecret(fCompressed),
|
||||
vchPubKey.GetHash(), vchCryptedSecret))
|
||||
return false;
|
||||
if (!cryptedKeysOut.emplace(vchPubKey.GetID(),
|
||||
std::make_pair(vchPubKey,
|
||||
std::move(vchCryptedSecret))).second)
|
||||
return false;
|
||||
}
|
||||
return cryptedKeysOut.size() == mapKeys.size();
|
||||
}
|
||||
|
||||
bool CCryptoKeyStore::CommitKeyEncryption(CryptedKeyMap&& cryptedKeys)
|
||||
{
|
||||
LOCK(cs_KeyStore);
|
||||
if (!mapCryptedKeys.empty() || IsCrypted() || cryptedKeys.size() != mapKeys.size())
|
||||
return false;
|
||||
|
||||
mapCryptedKeys = std::move(cryptedKeys);
|
||||
mapKeys.clear();
|
||||
fUseCrypto = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn)
|
||||
{
|
||||
CryptedKeyMap cryptedKeys;
|
||||
if (!PrepareKeyEncryption(vMasterKeyIn, cryptedKeys))
|
||||
return false;
|
||||
return CommitKeyEncryption(std::move(cryptedKeys));
|
||||
}
|
||||
|
||||
+9
-1
@@ -9,6 +9,8 @@
|
||||
#include "util_signal.h"
|
||||
#include "sync.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
class CScript;
|
||||
|
||||
/** A virtual base class for key stores */
|
||||
@@ -112,7 +114,13 @@ protected:
|
||||
|
||||
bool SetCrypted();
|
||||
|
||||
// will encrypt previously unencrypted keys
|
||||
// Stage and commit wallet-key encryption separately so callers can make
|
||||
// the on-disk update atomic before discarding plaintext keys in memory.
|
||||
bool PrepareKeyEncryption(CKeyingMaterial& vMasterKeyIn,
|
||||
CryptedKeyMap& cryptedKeysOut) const;
|
||||
bool CommitKeyEncryption(CryptedKeyMap&& cryptedKeys);
|
||||
|
||||
// Encrypt previously unencrypted keys in memory.
|
||||
bool EncryptKeys(CKeyingMaterial& vMasterKeyIn);
|
||||
|
||||
bool Unlock(const CKeyingMaterial& vMasterKeyIn);
|
||||
|
||||
+931
-144
File diff suppressed because it is too large
Load Diff
+86
-4
@@ -42,7 +42,18 @@ constexpr unsigned int MAX_BLOCK_SIGOPS = MAX_BLOCK_SIZE/50;
|
||||
constexpr unsigned int MAX_ORPHAN_TRANSACTIONS = MAX_BLOCK_SIZE/100;
|
||||
constexpr unsigned int MAX_ORPHAN_BLOCKS = 750;
|
||||
constexpr unsigned int MAX_ORPHAN_BLOCKS_IBD = 1500;
|
||||
constexpr unsigned int MAX_REORG_DEPTH = 100; // reject reorgs deeper than this (finality)
|
||||
// MAX_REORG_DEPTH is retained as a compile-time constant for tests and
|
||||
// legacy callers but no longer gates reorgs above the hardened checkpoint.
|
||||
// See Reorganize() in main.cpp for the new convergence rule.
|
||||
constexpr unsigned int MAX_REORG_DEPTH = 100; // historical finality depth (no longer enforced)
|
||||
|
||||
// ASSUME_VALID_BUFFER: how many blocks BACK from the tip to keep fully
|
||||
// validating. Blocks at or below nAssumeValidThreshold take the fast path
|
||||
// (skip sigops/script/UTXO validation) because we've already verified the
|
||||
// entire chain up to that height. We always validate the last BUFFER blocks
|
||||
// so a reorg attack that rewrites the top of the chain is caught immediately.
|
||||
// Lower = safer, higher = faster sync.
|
||||
constexpr unsigned int ASSUME_VALID_BUFFER = 100;
|
||||
constexpr unsigned int MAX_INV_SZ = 50000;
|
||||
constexpr int64_t MIN_TX_FEE = (1 * CENT) / 100;
|
||||
constexpr int64_t MIN_RELAY_TX_FEE = (1 * CENT) / 100;
|
||||
@@ -88,7 +99,8 @@ extern uint256 nBestChainTrust;
|
||||
extern uint256 nBestInvalidTrust;
|
||||
extern uint256 hashBestChain;
|
||||
extern CBlockIndex* pindexBest;
|
||||
extern CBlockIndex* pindexFinalized; // auto-checkpoint: deepest finalized block
|
||||
extern CBlockIndex* pindexLastHardenedCheckpoint; // last compiled hardened checkpoint in our local index (set at startup only; never advanced at runtime)
|
||||
extern int nAssumeValidThreshold; // highest height covered by assumeValid fast path
|
||||
extern unsigned int nTransactionsUpdated;
|
||||
extern uint64_t nLastBlockTx;
|
||||
extern uint64_t nLastBlockSize;
|
||||
@@ -129,6 +141,7 @@ CBlockIndex* FindBlockByHeight(int nHeight);
|
||||
bool ProcessMessages(CNode* pfrom);
|
||||
bool SendMessages(CNode* pto, bool fSendTrickle);
|
||||
bool LoadExternalBlockFile(FILE* fileIn);
|
||||
bool FastImportBlockFile();
|
||||
|
||||
bool CheckProofOfWork(uint256 hash, unsigned int nBits);
|
||||
unsigned int GetNextTargetRequired(const CBlockIndex* pindexLast, bool fProofOfStake);
|
||||
@@ -137,7 +150,34 @@ int64_t GetProofOfStakeReward(int64_t nCoinAge, int64_t nFees);
|
||||
unsigned int ComputeMinWork(unsigned int nBase, int64_t nTime);
|
||||
unsigned int ComputeMinStake(unsigned int nBase, int64_t nTime, unsigned int nBlockTime);
|
||||
int GetNumBlocksOfPeers();
|
||||
|
||||
// IsStakingSafe: continuous safety gate for StakeMiner (fix/consensus-convergence).
|
||||
//
|
||||
// Returns true only when the following conditions ALL hold:
|
||||
// - Not in IBD (IsInitialBlockDownload)
|
||||
// - At least 2 fully connected, non-disconnecting peers
|
||||
// - Our active chain height is at or above the peer median
|
||||
// - We do not have a chain-trust deficit relative to peers we trust
|
||||
//
|
||||
// The chain-trust-vs-peers check is a defensive guard against staking
|
||||
// on an isolated chain while another competing fork has equal or
|
||||
// greater cumulative trust on the network. Without peer-tip-hash
|
||||
// agreement (which is a separate protocol-level follow-up, not in this
|
||||
// branch) the most we can honestly assert is "our height matches or
|
||||
// exceeds the peer median" — that catches the failure mode this gate
|
||||
// was added to prevent (laptop alone minting against an isolated
|
||||
// consensus state). The full chain-trust comparison is left as a
|
||||
// follow-up that requires real peer-tip-hash state.
|
||||
//
|
||||
// Caller may pass an empty peer list to simulate a network outage
|
||||
// (useful from staking_tests).
|
||||
bool IsStakingSafe(const CWallet* pwallet, const std::vector<CNode*>& vNodesSnapshot);
|
||||
[[nodiscard]] bool IsInitialBlockDownload();
|
||||
// Height-based consensus fast path for historical checkpoint / rolling
|
||||
// assume-valid validation. This intentionally excludes operational IBD states
|
||||
// such as a stale tip; stale-tip IBD must not disable live PoS checks.
|
||||
[[nodiscard]] bool IsConsensusAssumeValidHeight(int nHeight);
|
||||
[[nodiscard]] bool IsBlockSignatureRequiredAtHeight(int nHeight);
|
||||
std::string GetWarnings(std::string strFor);
|
||||
bool GetTransaction(const uint256 &hash, CTransaction &tx, uint256 &hashBlock);
|
||||
uint256 WantedByOrphan(const CBlock* pblockOrphan);
|
||||
@@ -1105,8 +1145,17 @@ public:
|
||||
return error("%s() : deserialize or I/O error", __PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
// Check the header
|
||||
if (fReadTransactions && IsProofOfWork() && !CheckProofOfWork(GetHash(), nBits))
|
||||
// Check the header.
|
||||
// Genesis block is a hardcoded trust anchor — its hash is verified
|
||||
// by comparison to hashGenesisBlockOfficial/TestNet, not by PoW.
|
||||
// The genesis block's hash (0x7e7a6e4d...) is intentionally above
|
||||
// the PoW target since it's a network-wide constant, not a mined block.
|
||||
// All peercoin-derived coins (peercoin, triangles, etc.) use this
|
||||
// same exemption for the genesis block.
|
||||
if (fReadTransactions && IsProofOfWork() &&
|
||||
GetHash() != hashGenesisBlockOfficial &&
|
||||
GetHash() != hashGenesisBlockTestNet &&
|
||||
!CheckProofOfWork(GetHash(), nBits))
|
||||
return error("CBlock::ReadFromDisk() : errors in block header");
|
||||
|
||||
return true;
|
||||
@@ -1534,6 +1583,39 @@ public:
|
||||
return vHave.empty();
|
||||
}
|
||||
|
||||
// Return true if this locator's hash list contains the given hash.
|
||||
// Used by getheaders fork-recovery to check whether the peer already
|
||||
// knows the hardened checkpoint before serving from it (see
|
||||
// fix/consensus-convergence in main.cpp).
|
||||
bool Has(const uint256& hash) const
|
||||
{
|
||||
for (const uint256& h : vHave)
|
||||
if (h == hash)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the deepest block in this locator that exists in the given
|
||||
// block index AND is on the main chain. Returns nullptr if no match.
|
||||
// Used by getheaders fork-recovery to compute the last-common-ancestor
|
||||
// when the peer doesn't already know the hardened checkpoint.
|
||||
CBlockIndex* FindCommonAncestorInMainChain() const
|
||||
{
|
||||
CBlockIndex* pCommon = nullptr;
|
||||
for (const uint256& h : vHave)
|
||||
{
|
||||
std::map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(h);
|
||||
if (mi == mapBlockIndex.end())
|
||||
continue;
|
||||
CBlockIndex* pIdx = mi->second;
|
||||
if (!pIdx->IsInMainChain())
|
||||
continue;
|
||||
if (pCommon == nullptr || pIdx->nHeight > pCommon->nHeight)
|
||||
pCommon = pIdx;
|
||||
}
|
||||
return pCommon;
|
||||
}
|
||||
|
||||
// Return the first hash in the locator (peer's tip), or 0 if empty
|
||||
uint256 GetTipHash() const
|
||||
{
|
||||
|
||||
+27
-14
@@ -391,7 +391,6 @@ void StakeMiner(CWallet *pwallet)
|
||||
// Make this thread recognisable as the mining thread
|
||||
RenameThread("Triangles-miner");
|
||||
|
||||
bool fTryToSync = true;
|
||||
bool fForceStaking = GetBoolArg("-forcestaking", false);
|
||||
|
||||
while (true)
|
||||
@@ -407,24 +406,38 @@ void StakeMiner(CWallet *pwallet)
|
||||
return;
|
||||
}
|
||||
|
||||
while (!fForceStaking && (vNodes.empty() || IsInitialBlockDownload()))
|
||||
// Continuous staking safety gate (fix/consensus-convergence).
|
||||
//
|
||||
// Pre-fix: a one-shot strong check ran only once after the inner
|
||||
// wait exited. Losing peers mid-staking left the staker running
|
||||
// on a potentially isolated chain. This gate is evaluated on
|
||||
// EVERY staking attempt.
|
||||
//
|
||||
// Refuses to stake when:
|
||||
// - IBD is active (IsInitialBlockDownload)
|
||||
// - fewer than 2 fully handshaken non-disconnecting peers
|
||||
// - our height is behind the peer median
|
||||
// - a known competing valid fork is at or above our active chain trust
|
||||
//
|
||||
// `-forcestaking` remains an explicit operator override (with the
|
||||
// same warning as before) for stall recovery.
|
||||
if (!fForceStaking)
|
||||
{
|
||||
nLastCoinStakeSearchInterval = 0;
|
||||
fTryToSync = true;
|
||||
MilliSleep(1000);
|
||||
if (fShutdown)
|
||||
return;
|
||||
}
|
||||
|
||||
if (fTryToSync && !fForceStaking)
|
||||
{
|
||||
fTryToSync = false;
|
||||
if (vNodes.size() < 2 || nBestHeight < GetNumBlocksOfPeers())
|
||||
if (!IsStakingSafe(pwallet, vNodes))
|
||||
{
|
||||
MilliSleep(60000);
|
||||
nLastCoinStakeSearchInterval = 0;
|
||||
MilliSleep(1000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (vNodes.empty() || IsInitialBlockDownload())
|
||||
{
|
||||
// Force path still requires wallet connectivity; the rest of
|
||||
// the gate is the operator's responsibility.
|
||||
nLastCoinStakeSearchInterval = 0;
|
||||
MilliSleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
// Update cached stake weight for UI display (avoids heavy work on UI thread)
|
||||
|
||||
+87
-5
@@ -605,7 +605,8 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest)
|
||||
}
|
||||
|
||||
if (fDebug) {
|
||||
printf("ConnectNode(): pszDest: %s\n", pszDest);
|
||||
printf("ConnectNode(): destination: %s\n",
|
||||
pszDest ? pszDest : addrConnect.ToString().c_str());
|
||||
}
|
||||
|
||||
/// debug print
|
||||
@@ -650,6 +651,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;
|
||||
@@ -1690,7 +1739,7 @@ void ThreadOnionSeed(void* parg)
|
||||
|
||||
// Fetch dynamic seeds with retry — up to 4 attempts with increasing backoff.
|
||||
// This is the primary discovery mechanism — seeds.cryptographic-triangles.org
|
||||
{
|
||||
if (!GetBoolArg("-noseedurl", false)) {
|
||||
bool ok = false;
|
||||
int delays[] = {0, 30, 60, 120};
|
||||
for (int attempt = 0; attempt < 4 && !ok && !fShutdown; attempt++) {
|
||||
@@ -1758,7 +1807,8 @@ void ThreadOnionSeed(void* parg)
|
||||
else
|
||||
printf("ThreadOnionSeed: low outbound peers (%d), re-seeding...\n", nOutbound);
|
||||
|
||||
ThreadHTTPSeedFetch2(nullptr);
|
||||
if (!GetBoolArg("-noseedurl", false))
|
||||
ThreadHTTPSeedFetch2(nullptr);
|
||||
|
||||
// Re-queue hardcoded seeds for direct connection
|
||||
for (unsigned int seed_idx = 0; strOnionSeed[seed_idx][0] != nullptr; seed_idx++) {
|
||||
@@ -1843,6 +1893,12 @@ bool ThreadHTTPSeedFetch2(void* parg)
|
||||
seedPath = seedHost.substr(slashPos);
|
||||
seedHost = seedHost.substr(0, slashPos);
|
||||
}
|
||||
if (seedHost.empty() || seedHost.find_first_of("\r\n") != std::string::npos ||
|
||||
seedPath.empty() || seedPath[0] != '/' ||
|
||||
seedPath.find_first_of("\r\n") != std::string::npos) {
|
||||
printf("HTTPS seed fetch: invalid -seedurl value\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("Fetching seed list from https://%s%s (via Tor)...\n", seedHost.c_str(), seedPath.c_str());
|
||||
|
||||
@@ -1881,7 +1937,14 @@ bool ThreadHTTPSeedFetch2(void* parg)
|
||||
}
|
||||
|
||||
// Set SNI hostname (required for Caddy/Let's Encrypt)
|
||||
SSL_set_tlsext_host_name(ssl, seedHost.c_str());
|
||||
if (SSL_set_tlsext_host_name(ssl, seedHost.c_str()) != 1 ||
|
||||
SSL_set1_host(ssl, seedHost.c_str()) != 1) {
|
||||
printf("HTTPS seed fetch: failed to configure TLS hostname verification\n");
|
||||
SSL_free(ssl);
|
||||
SSL_CTX_free(ctx);
|
||||
closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
SSL_set_fd(ssl, (int)hSocket);
|
||||
|
||||
int ret = SSL_connect(ssl);
|
||||
@@ -1896,6 +1959,15 @@ bool ThreadHTTPSeedFetch2(void* parg)
|
||||
closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
if (SSL_get_verify_result(ssl) != X509_V_OK) {
|
||||
printf("HTTPS seed fetch: certificate verification failed for %s\n",
|
||||
seedHost.c_str());
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
SSL_CTX_free(ctx);
|
||||
closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("HTTPS seed fetch: TLS connection established to %s\n", seedHost.c_str());
|
||||
|
||||
@@ -1925,10 +1997,19 @@ bool ThreadHTTPSeedFetch2(void* parg)
|
||||
// Read response over TLS
|
||||
std::string response;
|
||||
char buf[4096];
|
||||
static constexpr size_t MAX_SEED_RESPONSE_SIZE = 1024 * 1024;
|
||||
while (true) {
|
||||
int nBytes = SSL_read(ssl, buf, sizeof(buf));
|
||||
if (nBytes <= 0)
|
||||
break;
|
||||
if (response.size() + static_cast<size_t>(nBytes) > MAX_SEED_RESPONSE_SIZE) {
|
||||
printf("HTTPS seed fetch: response exceeds 1 MiB limit\n");
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
SSL_CTX_free(ctx);
|
||||
closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
response.append(buf, nBytes);
|
||||
}
|
||||
|
||||
@@ -1954,7 +2035,8 @@ bool ThreadHTTPSeedFetch2(void* parg)
|
||||
|
||||
// Check status code
|
||||
std::string statusLine = response.substr(0, response.find("\r\n"));
|
||||
if (statusLine.find("200") == std::string::npos) {
|
||||
if (statusLine.size() < 12 || statusLine.compare(0, 7, "HTTP/1.") != 0 ||
|
||||
statusLine.compare(9, 3, "200") != 0) {
|
||||
printf("HTTPS seed fetch: %s from %s\n", statusLine.c_str(), seedHost.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -37,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()));
|
||||
|
||||
+45
-3
@@ -672,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)
|
||||
@@ -679,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};
|
||||
@@ -734,6 +736,22 @@ bool CNetAddr::SetSpecial(const std::string &strName)
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -856,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
|
||||
@@ -962,6 +980,13 @@ 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) + ".b32.i2p";
|
||||
CService serv(*this, 0);
|
||||
@@ -995,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)
|
||||
@@ -1009,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);
|
||||
}
|
||||
|
||||
@@ -1032,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;
|
||||
@@ -1106,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]);
|
||||
|
||||
+7
-1
@@ -106,8 +106,12 @@ 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();
|
||||
@@ -160,6 +164,7 @@ class CNetAddr
|
||||
READWRITE(FLATDATA(ip));
|
||||
READWRITE(FLATDATA(tor_v3_pubkey));
|
||||
READWRITE(m_is_tor_v3);
|
||||
READWRITE(m_is_i2p);
|
||||
)
|
||||
};
|
||||
|
||||
@@ -203,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)
|
||||
|
||||
@@ -49,7 +49,7 @@ AddressBookPage::AddressBookPage(Mode mode, Tabs tab, QWidget *parent) :
|
||||
connect(ui->tableView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(accept()));
|
||||
ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
ui->tableView->setFocus();
|
||||
ui->borderframe->setStyleSheet("#borderframe {border: 2px solid #f26522;}");
|
||||
ui->borderframe->setStyleSheet("#borderframe {border: 2px solid #e32105;}");
|
||||
break;
|
||||
case ForEditing:
|
||||
ui->buttonBox->setVisible(false);
|
||||
@@ -98,8 +98,8 @@ AddressBookPage::AddressBookPage(Mode mode, Tabs tab, QWidget *parent) :
|
||||
contextMenu->addAction(verifyMessageAction);
|
||||
contextMenu->setStyleSheet("QMenu {\
|
||||
background-color: #000; \
|
||||
border: 1px solid #f26522;\
|
||||
color: #f26522;\
|
||||
border: 1px solid #e32105;\
|
||||
color: #e32105;\
|
||||
}\
|
||||
\
|
||||
QMenu::item {\
|
||||
@@ -107,8 +107,8 @@ AddressBookPage::AddressBookPage(Mode mode, Tabs tab, QWidget *parent) :
|
||||
}\
|
||||
\
|
||||
QMenu::item:selected {\
|
||||
color: #f26522;\
|
||||
background-color: #61280E;\
|
||||
color: #e32105;\
|
||||
background-color: #3d0e04;\
|
||||
}\
|
||||
");
|
||||
// Connect signals for context menu actions
|
||||
|
||||
@@ -122,17 +122,17 @@ void AskPassphraseDialog::accept()
|
||||
msgBox->setIconPixmap(QPixmap(":/msgbox/question"));
|
||||
msgBox->setStyleSheet("QMessageBox { border: 2px solid #e22104;}");
|
||||
msgBox->button(QMessageBox::Yes)->setStyleSheet("\
|
||||
QMessageBox QPushButton {background-color: #000;color: #f26522;border: 1px solid #f26522;\
|
||||
QMessageBox QPushButton {background-color: #000;color: #e32105;border: 1px solid #e32105;\
|
||||
min-width: 120px;max-width: 120px;max-height: 20px;min-height: 20px;}\
|
||||
QMessageBox QPushButton:hover {background-color: #61280E;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #f26522;}\
|
||||
QMessageBox QPushButton:hover {background-color: #3d0e04;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #e32105;}\
|
||||
");
|
||||
|
||||
msgBox->button(QMessageBox::Cancel)->setStyleSheet("\
|
||||
QMessageBox QPushButton {background-color: #000;color: #f26522;border: 1px solid #f26522;\
|
||||
QMessageBox QPushButton {background-color: #000;color: #e32105;border: 1px solid #e32105;\
|
||||
min-width: 120px;max-width: 120px;max-height: 20px;min-height: 20px;}\
|
||||
QMessageBox QPushButton:hover {background-color: #61280E;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #f26522;}\
|
||||
QMessageBox QPushButton:hover {background-color: #3d0e04;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #e32105;}\
|
||||
");
|
||||
int retval = msgBox->exec();
|
||||
|
||||
@@ -161,10 +161,10 @@ void AskPassphraseDialog::accept()
|
||||
msgBox->setIconPixmap(QPixmap(":/msgbox/warning"));
|
||||
msgBox->setStyleSheet("QMessageBox { border: 2px solid #e22104;}");
|
||||
msgBox->button(QMessageBox::Ok)->setStyleSheet("\
|
||||
QMessageBox QPushButton {background-color: #000;color: #f26522;border: 1px solid #f26522;\
|
||||
QMessageBox QPushButton {background-color: #000;color: #e32105;border: 1px solid #e32105;\
|
||||
min-width: 120px;max-width: 120px;max-height: 20px;min-height: 20px;}\
|
||||
QMessageBox QPushButton:hover {background-color: #61280E;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #f26522;}\
|
||||
QMessageBox QPushButton:hover {background-color: #3d0e04;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #e32105;}\
|
||||
");
|
||||
|
||||
msgBox->exec();
|
||||
@@ -183,10 +183,10 @@ void AskPassphraseDialog::accept()
|
||||
msgBox->setIconPixmap(QPixmap(":/msgbox/critical"));
|
||||
msgBox->setStyleSheet("QMessageBox { border: 2px solid #e22104;}");
|
||||
msgBox->button(QMessageBox::Ok)->setStyleSheet("\
|
||||
QMessageBox QPushButton {background-color: #000;color: #f26522;border: 1px solid #f26522;\
|
||||
QMessageBox QPushButton {background-color: #000;color: #e32105;border: 1px solid #e32105;\
|
||||
min-width: 120px;max-width: 120px;max-height: 20px;min-height: 20px;}\
|
||||
QMessageBox QPushButton:hover {background-color: #61280E;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #f26522;}\
|
||||
QMessageBox QPushButton:hover {background-color: #3d0e04;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #e32105;}\
|
||||
");
|
||||
|
||||
msgBox->exec();
|
||||
@@ -205,10 +205,10 @@ void AskPassphraseDialog::accept()
|
||||
msgBox->setIconPixmap(QPixmap(":/msgbox/critical"));
|
||||
msgBox->setStyleSheet("QMessageBox { border: 2px solid #e22104;}");
|
||||
msgBox->button(QMessageBox::Ok)->setStyleSheet("\
|
||||
QMessageBox QPushButton {background-color: #000;color: #f26522;border: 1px solid #f26522;\
|
||||
QMessageBox QPushButton {background-color: #000;color: #e32105;border: 1px solid #e32105;\
|
||||
min-width: 120px;max-width: 120px;max-height: 20px;min-height: 20px;}\
|
||||
QMessageBox QPushButton:hover {background-color: #61280E;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #f26522;}\
|
||||
QMessageBox QPushButton:hover {background-color: #3d0e04;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #e32105;}\
|
||||
");
|
||||
|
||||
msgBox->exec();
|
||||
@@ -233,10 +233,10 @@ void AskPassphraseDialog::accept()
|
||||
msgBox->setIconPixmap(QPixmap(":/msgbox/critical"));
|
||||
msgBox->setStyleSheet("QMessageBox { border: 2px solid #e22104;}");
|
||||
msgBox->button(QMessageBox::Ok)->setStyleSheet("\
|
||||
QMessageBox QPushButton {background-color: #000;color: #f26522;border: 1px solid #f26522;\
|
||||
QMessageBox QPushButton {background-color: #000;color: #e32105;border: 1px solid #e32105;\
|
||||
min-width: 120px;max-width: 120px;max-height: 20px;min-height: 20px;}\
|
||||
QMessageBox QPushButton:hover {background-color: #61280E;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #f26522;}\
|
||||
QMessageBox QPushButton:hover {background-color: #3d0e04;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #e32105;}\
|
||||
");
|
||||
|
||||
msgBox->exec();
|
||||
@@ -259,10 +259,10 @@ void AskPassphraseDialog::accept()
|
||||
msgBox->setIconPixmap(QPixmap(":/msgbox/critical"));
|
||||
msgBox->setStyleSheet("QMessageBox { border: 2px solid #e22104;}");
|
||||
msgBox->button(QMessageBox::Ok)->setStyleSheet("\
|
||||
QMessageBox QPushButton {background-color: #000;color: #f26522;border: 1px solid #f26522;\
|
||||
QMessageBox QPushButton {background-color: #000;color: #e32105;border: 1px solid #e32105;\
|
||||
min-width: 120px;max-width: 120px;max-height: 20px;min-height: 20px;}\
|
||||
QMessageBox QPushButton:hover {background-color: #61280E;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #f26522;}\
|
||||
QMessageBox QPushButton:hover {background-color: #3d0e04;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #e32105;}\
|
||||
");
|
||||
|
||||
msgBox->exec();
|
||||
@@ -287,10 +287,10 @@ void AskPassphraseDialog::accept()
|
||||
msgBox->setIconPixmap(QPixmap(":/msgbox/information"));
|
||||
msgBox->setStyleSheet("QMessageBox { border: 2px solid #e22104;}");
|
||||
msgBox->button(QMessageBox::Ok)->setStyleSheet("\
|
||||
QMessageBox QPushButton {background-color: #000;color: #f26522;border: 1px solid #f26522;\
|
||||
QMessageBox QPushButton {background-color: #000;color: #e32105;border: 1px solid #e32105;\
|
||||
min-width: 120px;max-width: 120px;max-height: 20px;min-height: 20px;}\
|
||||
QMessageBox QPushButton:hover {background-color: #61280E;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #f26522;}\
|
||||
QMessageBox QPushButton:hover {background-color: #3d0e04;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #e32105;}\
|
||||
");
|
||||
|
||||
msgBox->exec();
|
||||
@@ -309,10 +309,10 @@ void AskPassphraseDialog::accept()
|
||||
msgBox->setIconPixmap(QPixmap(":/msgbox/critical"));
|
||||
msgBox->setStyleSheet("QMessageBox { border: 2px solid #e22104;}");
|
||||
msgBox->button(QMessageBox::Ok)->setStyleSheet("\
|
||||
QMessageBox QPushButton {background-color: #000;color: #f26522;border: 1px solid #f26522;\
|
||||
QMessageBox QPushButton {background-color: #000;color: #e32105;border: 1px solid #e32105;\
|
||||
min-width: 120px;max-width: 120px;max-height: 20px;min-height: 20px;}\
|
||||
QMessageBox QPushButton:hover {background-color: #61280E;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #f26522;}\
|
||||
QMessageBox QPushButton:hover {background-color: #3d0e04;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #e32105;}\
|
||||
");
|
||||
|
||||
msgBox->exec();
|
||||
@@ -330,10 +330,10 @@ void AskPassphraseDialog::accept()
|
||||
msgBox->setIconPixmap(QPixmap(":/msgbox/critical"));
|
||||
msgBox->setStyleSheet("QMessageBox { border: 2px solid #e22104;}");
|
||||
msgBox->button(QMessageBox::Ok)->setStyleSheet("\
|
||||
QMessageBox QPushButton {background-color: #000;color: #f26522;border: 1px solid #f26522;\
|
||||
QMessageBox QPushButton {background-color: #000;color: #e32105;border: 1px solid #e32105;\
|
||||
min-width: 120px;max-width: 120px;max-height: 20px;min-height: 20px;}\
|
||||
QMessageBox QPushButton:hover {background-color: #61280E;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #f26522;}\
|
||||
QMessageBox QPushButton:hover {background-color: #3d0e04;}\
|
||||
QMessageBox QPushButton:pressed:flat {color: #000;background-color: #e32105;}\
|
||||
");
|
||||
msgBox->exec();
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ CoinControlDialog::CoinControlDialog(QWidget *parent) :
|
||||
//contextMenu->addAction(unlockAction);
|
||||
contextMenu->setStyleSheet("QMenu {\
|
||||
background-color: #000; \
|
||||
border: 1px solid #f26522;\
|
||||
color: #f26522;\
|
||||
border: 1px solid #e32105;\
|
||||
color: #e32105;\
|
||||
}\
|
||||
\
|
||||
QMenu::item {\
|
||||
@@ -62,11 +62,11 @@ CoinControlDialog::CoinControlDialog(QWidget *parent) :
|
||||
}\
|
||||
\
|
||||
QMenu::item:selected {\
|
||||
color: #f26522;\
|
||||
background-color: #61280E;\
|
||||
color: #e32105;\
|
||||
background-color: #3d0e04;\
|
||||
}\
|
||||
QMenu::item:disabled {\
|
||||
color: #61280E;\
|
||||
color: #3d0e04;\
|
||||
}\
|
||||
");
|
||||
|
||||
@@ -138,7 +138,7 @@ CoinControlDialog::CoinControlDialog(QWidget *parent) :
|
||||
|
||||
ui->treeWidget->setStyleSheet("\
|
||||
CoinControlTreeWidget { \
|
||||
border: 1px solid #f26522; \
|
||||
border: 1px solid #e32105; \
|
||||
} \
|
||||
QTreeView::indicator:unchecked{\
|
||||
image: url(:/icons/stylesheet-checkbox-unchecked) 0;\
|
||||
|
||||
@@ -442,12 +442,12 @@
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLabel {
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
}
|
||||
|
||||
QDialog {
|
||||
background-color: #000;
|
||||
border: 2px solid #f26522;
|
||||
border: 2px solid #e32105;
|
||||
}
|
||||
|
||||
</string>
|
||||
@@ -473,7 +473,7 @@ QDialog {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#frame {
|
||||
background-color: #000;
|
||||
border-style: 2 px solid #f26522;
|
||||
border-style: 2 px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -1138,8 +1138,8 @@ This product includes software developed by the OpenSSL Project for use in the O
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 120px;
|
||||
@@ -1147,12 +1147,12 @@ This product includes software developed by the OpenSSL Project for use in the O
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
<string>Address Book</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
|
||||
/*QTableView {
|
||||
@@ -640,11 +640,11 @@ background-color: #000;
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#tableView {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
|
||||
QHeaderView::section {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
height: 20px;
|
||||
}
|
||||
@@ -659,9 +659,9 @@ QHeaderView::up-arrow {
|
||||
}
|
||||
|
||||
QTableView::item:focus {
|
||||
border: 0px solid #f26522;
|
||||
color: #f26522;
|
||||
background-color: #61280E;
|
||||
border: 0px solid #e32105;
|
||||
color: #e32105;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
|
||||
@@ -710,15 +710,15 @@ QTableView {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -744,15 +744,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -775,15 +775,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -809,15 +809,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -843,15 +843,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -877,15 +877,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -920,22 +920,22 @@ QPushButton:!enabled {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
padding: 3px 20px 3px 20px;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
border: 1px solid #61280E;
|
||||
color: #3d0e04;
|
||||
border: 1px solid #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
@@ -953,22 +953,22 @@ QPushButton:!enabled {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
padding: 3px 20px 3px 20px;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
border: 1px solid #61280E;
|
||||
color: #3d0e04;
|
||||
border: 1px solid #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
<string>Passphrase Dialog</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
|
||||
QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ QLineEdit {
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#frame {
|
||||
border: 2px solid #f26522;
|
||||
border: 2px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -363,7 +363,7 @@ QPushButton:hover {
|
||||
<string notr="true">QLineEdit
|
||||
{
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="echoMode">
|
||||
@@ -397,7 +397,7 @@ QPushButton:hover {
|
||||
<string notr="true">QLineEdit
|
||||
{
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="echoMode">
|
||||
@@ -431,7 +431,7 @@ QPushButton:hover {
|
||||
<string notr="true">QLineEdit
|
||||
{
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="echoMode">
|
||||
@@ -452,7 +452,7 @@ QPushButton:hover {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -507,8 +507,8 @@ QCheckBox::indicator:checked {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 120px;
|
||||
@@ -516,18 +516,18 @@ QCheckBox::indicator:checked {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
background-color: #000;
|
||||
border: 1px solid #61280E;
|
||||
color: #61280E;
|
||||
border: 1px solid #3d0e04;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<string>Coin Control</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
|
||||
|
||||
QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,15 +35,15 @@ QScrollBar:horizontal, QScrollBar:vertical {
|
||||
QScrollBar::handle:horizontal, QScrollBar::handle:vertical
|
||||
{
|
||||
border: 2px solid #491E0A;
|
||||
color #f26522;
|
||||
color #e32105;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal:hover, QScrollBar::handle:vertical:hover
|
||||
{
|
||||
border: 2px solid #f26522;
|
||||
color #f26522;
|
||||
background: #f26522;
|
||||
border: 2px solid #e32105;
|
||||
color #e32105;
|
||||
background: #e32105;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal, QScrollBar::ad
|
||||
<widget class="QFrame" name="borderframe">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#borderframe {
|
||||
border: 2px solid #f26522;
|
||||
border: 2px solid #e32105;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,13 +113,13 @@ QTreeView {
|
||||
|
||||
|
||||
QTreeWidget::item:hover{
|
||||
background-color:#61280E;
|
||||
background-color:#3d0e04;
|
||||
color: #f26526;
|
||||
border: 0px solid #f26522;
|
||||
border: 0px solid #e32105;
|
||||
}
|
||||
|
||||
#treeWidget QHeaderView::section {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
height: 20px;
|
||||
padding: 0px 3px;
|
||||
@@ -136,21 +136,21 @@ QHeaderView::up-arrow {
|
||||
}
|
||||
|
||||
QScrollBar:horizontal {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background: #1c1c1c;
|
||||
height: 15px;
|
||||
margin: 0px 16px 0 16px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background: #1c1c1c;
|
||||
min-height: 20px;
|
||||
/*border-radius: 2px;*/
|
||||
}
|
||||
|
||||
QScrollBar::add-line:horizontal {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
/*border-radius: 2px;*/
|
||||
background: #1c1c1c;
|
||||
width: 14px;
|
||||
@@ -159,7 +159,7 @@ QScrollBar::add-line:horizontal {
|
||||
}
|
||||
|
||||
QScrollBar::sub-line:horizontal {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
/*border-radius: 2px;*/
|
||||
background: #1c1c1c;
|
||||
width: 14px;
|
||||
@@ -168,10 +168,10 @@ QScrollBar::sub-line:horizontal {
|
||||
}
|
||||
|
||||
QScrollBar::right-arrow:horizontal, QScrollBar::left-arrow:horizontal {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
background: #f26522;
|
||||
background: #e32105;
|
||||
}
|
||||
|
||||
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
|
||||
@@ -182,18 +182,18 @@ QScrollBar:vertical {
|
||||
background: #000;
|
||||
width: 15px;
|
||||
margin: 16px 0 16px 0;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background: #1c1c1c;
|
||||
min-height: 20px;
|
||||
/*border-radius: 2px;*/
|
||||
}
|
||||
|
||||
QScrollBar::add-line:vertical {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
/*border-radius: 2px;*/
|
||||
background: #1c1c1c;
|
||||
height: 14px;
|
||||
@@ -202,7 +202,7 @@ QScrollBar::add-line:vertical {
|
||||
}
|
||||
|
||||
QScrollBar::sub-line:vertical {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
/*border-radius: 2px;*/
|
||||
background: #1c1c1c;
|
||||
height: 14px;
|
||||
@@ -211,10 +211,10 @@ QScrollBar::sub-line:vertical {
|
||||
}
|
||||
|
||||
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
background: #f26522;
|
||||
background: #e32105;
|
||||
}
|
||||
|
||||
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||
@@ -720,7 +720,7 @@ QPushButton:hover {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#frame {
|
||||
border: 1 px solid #f26522;
|
||||
border: 1 px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -753,8 +753,8 @@ QPushButton:hover {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 100px;
|
||||
@@ -762,12 +762,12 @@ QPushButton:hover {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -786,26 +786,26 @@ QPushButton:pressed:flat {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QRadioButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
}
|
||||
|
||||
QRadioButton::indicator {
|
||||
border-radius: 6px;
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
}
|
||||
|
||||
QRadioButton::indicator:unchecked {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
QRadioButton::indicator:checked {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: qradialgradient(
|
||||
cx: 0.5, cy: 0.5,
|
||||
fx: 0.5, fy: 0.5,
|
||||
radius: 1.0,
|
||||
stop: 0.15 #f26522,
|
||||
stop: 0.15 #e32105,
|
||||
stop: 0.25 #000
|
||||
);
|
||||
}
|
||||
@@ -813,8 +813,8 @@ QRadioButton::indicator:checked {
|
||||
|
||||
RadioButton::indicator:disabled {
|
||||
background-color: #000;
|
||||
color: #61280E;
|
||||
border: 1px solid #61280E;
|
||||
color: #3d0e04;
|
||||
border: 1px solid #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -839,26 +839,26 @@ RadioButton::indicator:disabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QRadioButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
}
|
||||
|
||||
QRadioButton::indicator {
|
||||
border-radius: 6px;
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
}
|
||||
|
||||
QRadioButton::indicator:unchecked {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
QRadioButton::indicator:checked {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: qradialgradient(
|
||||
cx: 0.5, cy: 0.5,
|
||||
fx: 0.5, fy: 0.5,
|
||||
radius: 1.0,
|
||||
stop: 0.15 #f26522,
|
||||
stop: 0.15 #e32105,
|
||||
stop: 0.25 #000
|
||||
);
|
||||
}
|
||||
@@ -866,8 +866,8 @@ QRadioButton::indicator:checked {
|
||||
|
||||
RadioButton::indicator:disabled {
|
||||
background-color: #000;
|
||||
color: #61280E;
|
||||
border: 1px solid #61280E;
|
||||
color: #3d0e04;
|
||||
border: 1px solid #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1079,15 +1079,15 @@ RadioButton::indicator:disabled {
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
|
||||
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
|
||||
QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
|
||||
</string>
|
||||
@@ -1175,8 +1175,8 @@ QLineEdit {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 120px;
|
||||
@@ -1184,12 +1184,12 @@ QLineEdit {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<string/>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
</string>
|
||||
</property>
|
||||
@@ -38,7 +38,7 @@ background-color: #000;
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#frame {
|
||||
border: 2px solid #f26522;
|
||||
border: 2px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -290,7 +290,7 @@ background-color: #000;
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
margin: 0px 10px;
|
||||
}</string>
|
||||
</property>
|
||||
@@ -354,7 +354,7 @@ background-color: #000;
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
margin: 0px 10px;
|
||||
}</string>
|
||||
</property>
|
||||
@@ -392,15 +392,15 @@ background-color: #000;
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -444,8 +444,8 @@ QPushButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 120px;
|
||||
@@ -453,12 +453,12 @@ QPushButton:!enabled {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
|
||||
+62
-28
@@ -457,7 +457,7 @@ QMenu::item:selected {
|
||||
|
||||
/* ================= combobox */
|
||||
QComboBox {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
@@ -466,7 +466,7 @@ QComboBox::drop-down {
|
||||
subcontrol-position: top right;
|
||||
/*width: 15px;*/
|
||||
border-left-width: 1px;
|
||||
border-left-color: #f26522;
|
||||
border-left-color: #e32105;
|
||||
border-left-style: solid;
|
||||
}
|
||||
|
||||
@@ -476,10 +476,10 @@ QComboBox::down-arrow {
|
||||
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #1c1c1c;
|
||||
selection-background-color:#61280E;
|
||||
selection-color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color:#f26522;
|
||||
selection-background-color:#3d0e04;
|
||||
selection-color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
color:#e32105;
|
||||
}
|
||||
|
||||
/* =========== QLineEdit =============*/
|
||||
@@ -534,7 +534,7 @@ QCheckBox::indicator:checked:pressed {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#centralWidget {
|
||||
background-color: #000;
|
||||
border: 2px solid #f26522;
|
||||
border: 2px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||
@@ -593,7 +593,7 @@ QCheckBox::indicator:checked:pressed {
|
||||
}
|
||||
|
||||
QWidget {
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
@@ -605,7 +605,7 @@ QWidget {
|
||||
}
|
||||
|
||||
QWidget {
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
@@ -682,7 +682,7 @@ QWidget {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -738,7 +738,7 @@ QPushButton:pressed:flat {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -794,7 +794,7 @@ QPushButton:pressed:flat {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -936,11 +936,11 @@ QPushButton:hover {
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -989,11 +989,11 @@ QPushButton:hover {
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1042,11 +1042,11 @@ QPushButton:hover {
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1095,11 +1095,11 @@ QPushButton:hover {
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1148,11 +1148,11 @@ QPushButton:hover {
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1201,11 +1201,11 @@ QPushButton:hover {
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1351,7 +1351,7 @@ QPushButton:hover {
|
||||
</palette>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">background-color: #f26522;</string>
|
||||
<string notr="true">background-color: #e32105;</string>
|
||||
</property>
|
||||
<property name="lineWidth">
|
||||
<number>0</number>
|
||||
@@ -1380,7 +1380,7 @@ QPushButton:hover {
|
||||
background-color: #000;
|
||||
}
|
||||
QLabel {
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,0,0,1,0,1,0,0,0,0,0,0,0">
|
||||
@@ -1602,12 +1602,12 @@ QLabel {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QProgressBar {
|
||||
border: 1px solid#f26522;
|
||||
border: 1px solid#e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
/*width: 20px;*/
|
||||
}</string>
|
||||
</property>
|
||||
@@ -1663,6 +1663,33 @@ QProgressBar::chunk {
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="OutlinedLabel" name="label_hd">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="outlineColor">
|
||||
<color>
|
||||
<red>242</red>
|
||||
<green>101</green>
|
||||
<blue>34</blue>
|
||||
</color>
|
||||
</property>
|
||||
<property name="outlineWidth">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>HD (BIP39) wallet seed status</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">HD</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_staking">
|
||||
<property name="text">
|
||||
@@ -1742,6 +1769,13 @@ QProgressBar::chunk {
|
||||
</widget>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>OutlinedLabel</class>
|
||||
<extends>QLabel</extends>
|
||||
<header>qt/outlinedlabel.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../triangles.qrc"/>
|
||||
</resources>
|
||||
|
||||
+28
-28
@@ -186,7 +186,7 @@
|
||||
<string>Address Book</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
</string>
|
||||
</property>
|
||||
@@ -526,11 +526,11 @@
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#tableView {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
|
||||
QHeaderView::section {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
height: 20px;
|
||||
}
|
||||
@@ -573,7 +573,7 @@ QHeaderView::up-arrow {
|
||||
<widget class="QGroupBox" name="messageDetails">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#messageDetails {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="title">
|
||||
@@ -605,8 +605,8 @@ QHeaderView::up-arrow {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 60px;
|
||||
@@ -614,12 +614,12 @@ QHeaderView::up-arrow {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -917,14 +917,14 @@ QPushButton:pressed:flat {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#listConversation {
|
||||
border: 1px solid #f26522;
|
||||
color: #f26522;
|
||||
border: 1px solid #e32105;
|
||||
color: #e32105;
|
||||
}
|
||||
|
||||
QListView {color:#f26522;}
|
||||
QListView {color:#e32105;}
|
||||
|
||||
QHeaderView::section {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
height: 20px;
|
||||
}
|
||||
@@ -955,7 +955,7 @@ QHeaderView::up-arrow {
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">border: #f26522;
|
||||
<string notr="true">border: #e32105;
|
||||
</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -1145,15 +1145,15 @@ QHeaderView::up-arrow {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1351,15 +1351,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1557,15 +1557,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1763,15 +1763,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1969,15 +1969,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<string>Options</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
</string>
|
||||
</property>
|
||||
@@ -38,7 +38,7 @@
|
||||
<widget class="QFrame" name="borderframe">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#borderframe {
|
||||
border: 2px solid #f26522;
|
||||
border: 2px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -153,7 +153,7 @@
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QTabWidget::pane {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
|
||||
QTabBar::tab {
|
||||
@@ -165,11 +165,11 @@ QTabBar::tab {
|
||||
|
||||
|
||||
QTabBar::tab:!selected {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
margin-right: -1px;
|
||||
border-left: 1px solid #61280E;
|
||||
border-right: 1px solid #61280E;
|
||||
border-top: 1px solid #61280E;
|
||||
border-left: 1px solid #3d0e04;
|
||||
border-right: 1px solid #3d0e04;
|
||||
border-top: 1px solid #3d0e04;
|
||||
}
|
||||
|
||||
QTabBar::tab:!selected:last {
|
||||
@@ -177,11 +177,11 @@ QTabBar::tab:!selected:last {
|
||||
}
|
||||
|
||||
QTabBar::tab:selected {
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
margin-right: -1px;
|
||||
border-left: 1px solid #f26522;
|
||||
border-right: 1px solid #f26522;
|
||||
border-top: 1px solid #f26522;
|
||||
border-left: 1px solid #e32105;
|
||||
border-right: 1px solid #e32105;
|
||||
border-top: 1px solid #e32105;
|
||||
}
|
||||
|
||||
QTabBar::tab:selected:last {
|
||||
@@ -189,25 +189,25 @@ QTabBar::tab:selected:last {
|
||||
}
|
||||
|
||||
QTabBar::tab:!selected:hover {
|
||||
background-color: #61280E;
|
||||
color: #f26522;
|
||||
background-color: #3d0e04;
|
||||
color: #e32105;
|
||||
}
|
||||
|
||||
#transactionFee TrianglesAmountField {
|
||||
background-color: #1c1c1c;
|
||||
selection-background-color:#ff0000;
|
||||
selection-color: #00ff00;
|
||||
border: 1px solid #f26522;
|
||||
color:#f26522;
|
||||
border: 1px solid #e32105;
|
||||
color:#e32105;
|
||||
}
|
||||
|
||||
TrianglesAmountField QAbstractItemView {
|
||||
background-color: #1c1c1c;
|
||||
selection-background-color:#61280E;
|
||||
selection-background-color:#3d0e04;
|
||||
outline: 0px;
|
||||
selection-color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color:#f26522;
|
||||
selection-color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
color:#e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="tabPosition">
|
||||
@@ -460,7 +460,7 @@ TrianglesAmountField QAbstractItemView {
|
||||
<enum>Qt::NoContextMenu</enum>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">border: 1px solid #f26522;
|
||||
<string notr="true">border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
</string>
|
||||
</property>
|
||||
@@ -720,15 +720,15 @@ background-color: #1c1c1c;
|
||||
<enum>Qt::NoContextMenu</enum>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">border: 1px solid #f26522;
|
||||
<string notr="true">border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
|
||||
QAbstractItemView {
|
||||
background-color: #1c1c1c;
|
||||
selection-background-color:#61280E;
|
||||
selection-color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color:#f26522;
|
||||
selection-background-color:#3d0e04;
|
||||
selection-color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
color:#e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -755,7 +755,7 @@ QAbstractItemView {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -776,7 +776,7 @@ QCheckBox::indicator:checked {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -823,7 +823,7 @@ QCheckBox::indicator:checked {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -850,7 +850,7 @@ QCheckBox::indicator:checked {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -906,7 +906,7 @@ QCheckBox::indicator:checked {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -944,7 +944,7 @@ QCheckBox::indicator:checked {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -1237,7 +1237,7 @@ QCheckBox::indicator:checked {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QComboBox {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
@@ -1246,7 +1246,7 @@ QComboBox::drop-down {
|
||||
subcontrol-position: top right;
|
||||
/*width: 15px;*/
|
||||
border-left-width: 1px;
|
||||
border-left-color: #f26522;
|
||||
border-left-color: #e32105;
|
||||
border-left-style: solid;
|
||||
}
|
||||
|
||||
@@ -1256,10 +1256,10 @@ QComboBox::down-arrow {
|
||||
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #1c1c1c;
|
||||
selection-background-color:#61280E;
|
||||
selection-color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color:#f26522;
|
||||
selection-background-color:#3d0e04;
|
||||
selection-color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
color:#e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="editable">
|
||||
@@ -1295,7 +1295,7 @@ QComboBox QAbstractItemView {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -1316,7 +1316,7 @@ QCheckBox::indicator:checked {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -1378,7 +1378,7 @@ QCheckBox::indicator:checked {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QComboBox {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
@@ -1387,7 +1387,7 @@ QComboBox::drop-down {
|
||||
subcontrol-position: top right;
|
||||
/*width: 15px;*/
|
||||
border-left-width: 1px;
|
||||
border-left-color: #f26522;
|
||||
border-left-color: #e32105;
|
||||
border-left-style: solid;
|
||||
}
|
||||
|
||||
@@ -1397,10 +1397,10 @@ QComboBox::down-arrow {
|
||||
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #1c1c1c;
|
||||
selection-background-color:#61280E;
|
||||
selection-color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color:#f26522;
|
||||
selection-background-color:#3d0e04;
|
||||
selection-color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
color:#e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -1435,7 +1435,7 @@ QComboBox QAbstractItemView {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QComboBox {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
@@ -1444,7 +1444,7 @@ QComboBox::drop-down {
|
||||
subcontrol-position: top right;
|
||||
/*width: 15px;*/
|
||||
border-left-width: 1px;
|
||||
border-left-color: #f26522;
|
||||
border-left-color: #e32105;
|
||||
border-left-style: solid;
|
||||
}
|
||||
|
||||
@@ -1454,10 +1454,10 @@ QComboBox::down-arrow {
|
||||
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #1c1c1c;
|
||||
selection-background-color:#61280E;
|
||||
selection-color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color:#f26522;
|
||||
selection-background-color:#3d0e04;
|
||||
selection-color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
color:#e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -1471,7 +1471,7 @@ QComboBox QAbstractItemView {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -1492,7 +1492,7 @@ QCheckBox::indicator:checked {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -1513,7 +1513,7 @@ QCheckBox::indicator:checked {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -1596,8 +1596,8 @@ QCheckBox::indicator:checked {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 80px;
|
||||
@@ -1605,12 +1605,12 @@ QCheckBox::indicator:checked {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1623,8 +1623,8 @@ QPushButton:pressed:flat {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 80px;
|
||||
@@ -1632,12 +1632,12 @@ QPushButton:pressed:flat {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1653,8 +1653,8 @@ QPushButton:pressed:flat {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 80px;
|
||||
@@ -1662,12 +1662,12 @@ QPushButton:pressed:flat {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<colorrole role="WindowText">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>242</red>
|
||||
<green>101</green>
|
||||
<blue>34</blue>
|
||||
<red>255</red>
|
||||
<green>224</green>
|
||||
<blue>102</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -35,44 +35,44 @@
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>255</red>
|
||||
<green>180</green>
|
||||
<blue>144</blue>
|
||||
<green>243</green>
|
||||
<blue>170</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Midlight">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>248</red>
|
||||
<green>140</green>
|
||||
<blue>89</blue>
|
||||
<red>252</red>
|
||||
<green>221</green>
|
||||
<blue>120</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Dark">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>121</red>
|
||||
<green>50</green>
|
||||
<blue>17</blue>
|
||||
<red>140</red>
|
||||
<green>100</green>
|
||||
<blue>30</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Mid">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>161</red>
|
||||
<green>67</green>
|
||||
<blue>22</blue>
|
||||
<red>180</red>
|
||||
<green>140</green>
|
||||
<blue>40</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Text">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>242</red>
|
||||
<green>101</green>
|
||||
<blue>34</blue>
|
||||
<red>255</red>
|
||||
<green>224</green>
|
||||
<blue>102</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -88,9 +88,9 @@
|
||||
<colorrole role="ButtonText">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>242</red>
|
||||
<green>101</green>
|
||||
<blue>34</blue>
|
||||
<red>255</red>
|
||||
<green>224</green>
|
||||
<blue>102</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -124,9 +124,9 @@
|
||||
<colorrole role="AlternateBase">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>248</red>
|
||||
<green>178</green>
|
||||
<blue>144</blue>
|
||||
<red>252</red>
|
||||
<green>231</green>
|
||||
<blue>180</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -153,9 +153,9 @@
|
||||
<colorrole role="WindowText">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>242</red>
|
||||
<green>101</green>
|
||||
<blue>34</blue>
|
||||
<red>255</red>
|
||||
<green>224</green>
|
||||
<blue>102</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -172,44 +172,44 @@
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>255</red>
|
||||
<green>180</green>
|
||||
<blue>144</blue>
|
||||
<green>243</green>
|
||||
<blue>170</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Midlight">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>248</red>
|
||||
<green>140</green>
|
||||
<blue>89</blue>
|
||||
<red>252</red>
|
||||
<green>221</green>
|
||||
<blue>120</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Dark">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>121</red>
|
||||
<green>50</green>
|
||||
<blue>17</blue>
|
||||
<red>140</red>
|
||||
<green>100</green>
|
||||
<blue>30</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Mid">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>161</red>
|
||||
<green>67</green>
|
||||
<blue>22</blue>
|
||||
<red>180</red>
|
||||
<green>140</green>
|
||||
<blue>40</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Text">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>242</red>
|
||||
<green>101</green>
|
||||
<blue>34</blue>
|
||||
<red>255</red>
|
||||
<green>224</green>
|
||||
<blue>102</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -225,9 +225,9 @@
|
||||
<colorrole role="ButtonText">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>242</red>
|
||||
<green>101</green>
|
||||
<blue>34</blue>
|
||||
<red>255</red>
|
||||
<green>224</green>
|
||||
<blue>102</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -261,9 +261,9 @@
|
||||
<colorrole role="AlternateBase">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>248</red>
|
||||
<green>178</green>
|
||||
<blue>144</blue>
|
||||
<red>252</red>
|
||||
<green>231</green>
|
||||
<blue>180</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -290,9 +290,9 @@
|
||||
<colorrole role="WindowText">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>242</red>
|
||||
<green>101</green>
|
||||
<blue>34</blue>
|
||||
<red>255</red>
|
||||
<green>224</green>
|
||||
<blue>102</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -309,44 +309,44 @@
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>255</red>
|
||||
<green>180</green>
|
||||
<blue>144</blue>
|
||||
<green>243</green>
|
||||
<blue>170</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Midlight">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>248</red>
|
||||
<green>140</green>
|
||||
<blue>89</blue>
|
||||
<red>252</red>
|
||||
<green>221</green>
|
||||
<blue>120</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Dark">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>121</red>
|
||||
<green>50</green>
|
||||
<blue>17</blue>
|
||||
<red>140</red>
|
||||
<green>100</green>
|
||||
<blue>30</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Mid">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>161</red>
|
||||
<green>67</green>
|
||||
<blue>22</blue>
|
||||
<red>180</red>
|
||||
<green>140</green>
|
||||
<blue>40</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
<colorrole role="Text">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>242</red>
|
||||
<green>101</green>
|
||||
<blue>34</blue>
|
||||
<red>255</red>
|
||||
<green>224</green>
|
||||
<blue>102</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -362,9 +362,9 @@
|
||||
<colorrole role="ButtonText">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>242</red>
|
||||
<green>101</green>
|
||||
<blue>34</blue>
|
||||
<red>255</red>
|
||||
<green>224</green>
|
||||
<blue>102</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -398,9 +398,9 @@
|
||||
<colorrole role="AlternateBase">
|
||||
<brush brushstyle="SolidPattern">
|
||||
<color alpha="255">
|
||||
<red>242</red>
|
||||
<green>101</green>
|
||||
<blue>34</blue>
|
||||
<red>252</red>
|
||||
<green>231</green>
|
||||
<blue>180</blue>
|
||||
</color>
|
||||
</brush>
|
||||
</colorrole>
|
||||
@@ -429,12 +429,15 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
QWidget#line {
|
||||
border: 2px solid #f26522;
|
||||
border: 2px solid #e32105;
|
||||
}
|
||||
|
||||
QLabel#labelBalance, QLabel#labelStake { color: #7CDB8A; }
|
||||
/* labelTotal color is set dynamically in setBalance() — green when > 0, red when == 0 */
|
||||
QLabel#labelUnconfirmed { color: #A8B847; }
|
||||
QLabel#labelImmature { color: #A8B847; }
|
||||
</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0">
|
||||
@@ -450,7 +453,7 @@ QWidget#line {
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#frame {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -635,7 +638,7 @@ QWidget#line {
|
||||
<widget class="Line" name="line">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#line {
|
||||
border: 2px solid #f26522;
|
||||
border: 2px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
@@ -727,7 +730,7 @@ QWidget#line {
|
||||
<widget class="QFrame" name="frame_2">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#frame_2 {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
</string>
|
||||
</property>
|
||||
@@ -808,10 +811,10 @@ QWidget#line {
|
||||
<property name="text">
|
||||
<string><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -833,10 +836,10 @@ QWidget#line {
|
||||
<property name="text">
|
||||
<string><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -861,10 +864,10 @@ QWidget#line {
|
||||
<property name="text">
|
||||
<string><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+28
-28
@@ -429,7 +429,7 @@
|
||||
<string>Triangles - Debug window</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
</string>
|
||||
</property>
|
||||
@@ -453,7 +453,7 @@
|
||||
<widget class="QFrame" name="borderframe">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#borderframe {
|
||||
border: 2px solid #f26522;
|
||||
border: 2px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -607,7 +607,7 @@ QPushButton:hover {
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QTabWidget::pane {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
|
||||
QTabBar::tab {
|
||||
@@ -619,11 +619,11 @@ QTabBar::tab {
|
||||
|
||||
|
||||
QTabBar::tab:!selected {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
margin-right: -1px;
|
||||
border-left: 1px solid #61280E;
|
||||
border-right: 1px solid #61280E;
|
||||
border-top: 1px solid #61280E;
|
||||
border-left: 1px solid #3d0e04;
|
||||
border-right: 1px solid #3d0e04;
|
||||
border-top: 1px solid #3d0e04;
|
||||
}
|
||||
|
||||
QTabBar::tab:!selected:last {
|
||||
@@ -631,11 +631,11 @@ QTabBar::tab:!selected:last {
|
||||
}
|
||||
|
||||
QTabBar::tab:selected {
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
margin-right: -1px;
|
||||
border-left: 1px solid #f26522;
|
||||
border-right: 1px solid #f26522;
|
||||
border-top: 1px solid #f26522;
|
||||
border-left: 1px solid #e32105;
|
||||
border-right: 1px solid #e32105;
|
||||
border-top: 1px solid #e32105;
|
||||
}
|
||||
|
||||
QTabBar::tab:selected:last {
|
||||
@@ -643,8 +643,8 @@ QTabBar::tab:selected:last {
|
||||
}
|
||||
|
||||
QTabBar::tab:!selected:hover {
|
||||
background-color: #61280E;
|
||||
color: #f26522;
|
||||
background-color: #3d0e04;
|
||||
color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
@@ -839,7 +839,7 @@ QTabBar::tab:!selected:hover {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -968,22 +968,22 @@ QCheckBox::indicator:checked {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
padding: 3px 20px 3px 20px;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
border: 1px solid #61280E;
|
||||
color: #3d0e04;
|
||||
border: 1px solid #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1014,22 +1014,22 @@ QPushButton:!enabled {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
padding: 3px 20px 3px 20px;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
border: 1px solid #61280E;
|
||||
color: #3d0e04;
|
||||
border: 1px solid #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1072,7 +1072,7 @@ QPushButton:!enabled {
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">border: 1px solid #f26522;</string>
|
||||
<string notr="true">border: 1px solid #e32105;</string>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
@@ -1159,7 +1159,7 @@ QPushButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -1184,12 +1184,12 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
|
||||
@@ -429,7 +429,7 @@
|
||||
<string>Send Coins</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
@@ -565,7 +565,7 @@
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#frameCoinControl {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -642,22 +642,22 @@
|
||||
<widget class="QPushButton" name="pushButtonCoinControl">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
padding: 3px 20px 3px 20px;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
border: 1px solid #61280E;
|
||||
color: #3d0e04;
|
||||
border: 1px solid #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1120,7 +1120,7 @@ QPushButton:!enabled {
|
||||
<widget class="QCheckBox" name="checkBoxCoinControlChange">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QCheckBox::indicator {
|
||||
border:1px solid #f26522;
|
||||
border:1px solid #e32105;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@@ -1154,7 +1154,7 @@ QCheckBox::indicator:checked {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -1271,15 +1271,15 @@ QCheckBox::indicator:checked {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1314,15 +1314,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1413,15 +1413,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
|
||||
@@ -431,16 +431,16 @@
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#SendCoinsEntry{
|
||||
background-color: #000;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
|
||||
TrianglesAmountField QAbstractItemView {
|
||||
background-color: #1c1c1c;
|
||||
selection-background-color:#61280E;
|
||||
selection-background-color:#3d0e04;
|
||||
outline: 0px;
|
||||
selection-color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color:#f26522;
|
||||
selection-color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
color:#e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -453,7 +453,7 @@ TrianglesAmountField QAbstractItemView {
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;</string>
|
||||
<string notr="true">color: #e32105;</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Pay &To:</string>
|
||||
@@ -485,7 +485,7 @@ TrianglesAmountField QAbstractItemView {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="maxLength">
|
||||
@@ -507,15 +507,15 @@ TrianglesAmountField QAbstractItemView {
|
||||
|
||||
QToolButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QToolButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QToolButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -544,15 +544,15 @@ QToolButton:!enabled {
|
||||
|
||||
QToolButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QToolButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QToolButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -581,15 +581,15 @@ QToolButton:!enabled {
|
||||
|
||||
QToolButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QToolButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QToolButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -606,7 +606,7 @@ QToolButton:!enabled {
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -637,7 +637,7 @@ QToolButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -651,7 +651,7 @@ QToolButton:!enabled {
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -768,7 +768,7 @@ QToolButton:!enabled {
|
||||
<enum>Qt::NoContextMenu</enum>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">border: 1px solid #f26522;
|
||||
<string notr="true">border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
</string>
|
||||
</property>
|
||||
@@ -792,7 +792,7 @@ QToolButton:!enabled {
|
||||
<string notr="true">QLineEdit
|
||||
{
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<string>Send Messages</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
</string>
|
||||
</property>
|
||||
@@ -38,12 +38,12 @@
|
||||
<widget class="QFrame" name="borderframe">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#borderframe {
|
||||
border: 2px solid #f26522;
|
||||
border: 2px solid #e32105;
|
||||
}
|
||||
|
||||
QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -206,7 +206,7 @@ QPushButton:hover {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#frameAddressFrom {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -258,15 +258,15 @@ QPushButton:hover {
|
||||
|
||||
QToolButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QToolButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QToolButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -710,15 +710,15 @@ QToolButton:!enabled {
|
||||
|
||||
QToolButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QToolButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QToolButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -741,7 +741,7 @@ QToolButton:!enabled {
|
||||
<item>
|
||||
<widget class="QScrollArea" name="scrollArea">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#scrollArea {border: 0px solid #f26522;}</string>
|
||||
<string notr="true">#scrollArea {border: 0px solid #e32105;}</string>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
@@ -804,15 +804,15 @@ QToolButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -847,15 +847,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -905,15 +905,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -939,15 +939,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
|
||||
@@ -431,12 +431,12 @@
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#SendMessagesEntry{
|
||||
background-color: #000;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
|
||||
QLineEdit, QPlainTextEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -468,7 +468,7 @@ QLineEdit, QPlainTextEdit {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="maxLength">
|
||||
@@ -490,15 +490,15 @@ QLineEdit, QPlainTextEdit {
|
||||
|
||||
QToolButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QToolButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QToolButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -527,15 +527,15 @@ QToolButton:!enabled {
|
||||
|
||||
QToolButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QToolButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QToolButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -564,15 +564,15 @@ QToolButton:!enabled {
|
||||
|
||||
QToolButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QToolButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QToolButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -589,7 +589,7 @@ QToolButton:!enabled {
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="messageLabel">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;</string>
|
||||
<string notr="true">color: #e32105;</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Message:</string>
|
||||
@@ -605,7 +605,7 @@ QToolButton:!enabled {
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;</string>
|
||||
<string notr="true">color: #e32105;</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Send &To:</string>
|
||||
@@ -744,7 +744,7 @@ QToolButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -752,7 +752,7 @@ QToolButton:!enabled {
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;</string>
|
||||
<string notr="true">color: #e32105;</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Label:</string>
|
||||
@@ -768,7 +768,7 @@ QToolButton:!enabled {
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="publicKeyLabel">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;</string>
|
||||
<string notr="true">color: #e32105;</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Public Key:</string>
|
||||
@@ -789,7 +789,7 @@ QToolButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
|
||||
@@ -429,11 +429,11 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
|
||||
QLineEdit {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
@@ -655,15 +655,15 @@ QLineEdit {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
@@ -733,15 +733,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
@@ -781,7 +781,7 @@ QPushButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="maxLength">
|
||||
@@ -837,7 +837,7 @@ QPushButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPlainTextEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="plainText">
|
||||
@@ -874,15 +874,15 @@ QPushButton:!enabled {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -919,7 +919,7 @@ QPushButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
@@ -1009,8 +1009,8 @@ QPushButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 120px;
|
||||
@@ -1018,12 +1018,12 @@ QPushButton:!enabled {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -1048,8 +1048,8 @@ QPushButton:pressed:flat {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 120px;
|
||||
@@ -1057,12 +1057,12 @@ QPushButton:pressed:flat {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLabel {
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
}
|
||||
|
||||
QDialog {
|
||||
@@ -44,7 +44,7 @@ QDialog {
|
||||
<widget class="QFrame" name="borderframe">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#borderframe {
|
||||
border: 2px solid #f26522;
|
||||
border: 2px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
@@ -230,8 +230,8 @@ QPushButton:hover {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QTextEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
color: #f26522;
|
||||
border: 1px solid #e32105;
|
||||
color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
@@ -244,8 +244,8 @@ QPushButton:hover {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 120px;
|
||||
@@ -253,12 +253,12 @@ QPushButton:hover {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QWidget {
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
background-color: #000;
|
||||
}</string>
|
||||
</property>
|
||||
@@ -367,7 +367,7 @@
|
||||
}
|
||||
|
||||
QLabel {
|
||||
color: #f26522;
|
||||
color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_10" stretch="0,1">
|
||||
@@ -405,7 +405,7 @@ QLabel {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -426,7 +426,7 @@ QLabel {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QComboBox {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
@@ -435,7 +435,7 @@ QComboBox::drop-down {
|
||||
subcontrol-position: top right;
|
||||
/*width: 15px;*/
|
||||
border-left-width: 1px;
|
||||
border-left-color: #f26522;
|
||||
border-left-color: #e32105;
|
||||
border-left-style: solid;
|
||||
}
|
||||
|
||||
@@ -445,10 +445,10 @@ QComboBox::down-arrow {
|
||||
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #1c1c1c;
|
||||
selection-background-color:#61280E;
|
||||
selection-color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color:#f26522;
|
||||
selection-background-color:#3d0e04;
|
||||
selection-color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
color:#e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -890,7 +890,7 @@ QComboBox QAbstractItemView {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QComboBox {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
@@ -899,7 +899,7 @@ QComboBox::drop-down {
|
||||
subcontrol-position: top right;
|
||||
/*width: 15px;*/
|
||||
border-left-width: 1px;
|
||||
border-left-color: #f26522;
|
||||
border-left-color: #e32105;
|
||||
border-left-style: solid;
|
||||
}
|
||||
|
||||
@@ -909,10 +909,10 @@ QComboBox::down-arrow {
|
||||
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #1c1c1c;
|
||||
selection-background-color:#61280E;
|
||||
selection-color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color:#f26522;
|
||||
selection-background-color:#3d0e04;
|
||||
selection-color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
color:#e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
@@ -946,7 +946,7 @@ QComboBox QAbstractItemView {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
</widget>
|
||||
@@ -1442,11 +1442,11 @@ QComboBox QAbstractItemView {
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#transactionView {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}
|
||||
|
||||
QHeaderView::section {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
background-color: #1c1c1c;
|
||||
height: 20px;
|
||||
padding: 0px 3px;
|
||||
@@ -1462,10 +1462,10 @@ QHeaderView::up-arrow {
|
||||
}
|
||||
|
||||
QTableView::item:focus {
|
||||
border: 0px solid #f26522;
|
||||
color: #f26522;
|
||||
border: 0px solid #e32105;
|
||||
color: #e32105;
|
||||
outline: none;
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QTableView {
|
||||
|
||||
@@ -429,11 +429,11 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f26522;
|
||||
<string notr="true">color: #e32105;
|
||||
background-color: #000;
|
||||
|
||||
QLineEdit {
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
@@ -632,15 +632,15 @@ QLineEdit {
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:!enabled {
|
||||
color: #61280E;
|
||||
color: #3d0e04;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
@@ -706,7 +706,7 @@ QPushButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="maxLength">
|
||||
@@ -730,7 +730,7 @@ QPushButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLineEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
@@ -855,7 +855,7 @@ QPushButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPlainTextEdit {
|
||||
background-color: #1c1c1c;
|
||||
border: 1px solid #f26522;
|
||||
border: 1px solid #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="plainText">
|
||||
@@ -948,8 +948,8 @@ QPushButton:!enabled {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 120px;
|
||||
@@ -957,12 +957,12 @@ QPushButton:!enabled {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -987,8 +987,8 @@ QPushButton:pressed:flat {
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QPushButton {
|
||||
background-color: #000;
|
||||
color: #f26522;
|
||||
border: 1px solid #f26522;
|
||||
color: #e32105;
|
||||
border: 1px solid #e32105;
|
||||
max-height: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 120px;
|
||||
@@ -996,12 +996,12 @@ QPushButton:pressed:flat {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #61280E;
|
||||
background-color: #3d0e04;
|
||||
}
|
||||
|
||||
QPushButton:pressed:flat {
|
||||
color: #000;
|
||||
background-color: #f26522;
|
||||
background-color: #e32105;
|
||||
}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
|
||||
@@ -11,12 +11,18 @@ static const int MAX_PASSPHRASE_SIZE = 1024;
|
||||
static const int STATUSBAR_ICONSIZE = 16;
|
||||
|
||||
/* Invalid field background style */
|
||||
#define STYLE_INVALID "border: 1px solid #ff0000;background:#1c1c1c;color: #f26522;"
|
||||
#define STYLE_INVALID "border: 1px solid #ff0000;background:#1c1c1c;color: #e32105;"
|
||||
|
||||
/* Transaction list -- unconfirmed transaction */
|
||||
/* Transaction list -- unconfirmed transaction (0 confirms: grey, both directions) */
|
||||
#define COLOR_UNCONFIRMED QColor(97, 40, 14)
|
||||
/* Transaction list -- negative amount */
|
||||
/* Transaction list -- negative amount (confirmed: spent) */
|
||||
#define COLOR_NEGATIVE QColor(255, 0, 0)
|
||||
/* Transaction list -- positive amount (fully confirmed, depth >= RecommendedNumConfirmations) */
|
||||
#define COLOR_POSITIVE QColor(124, 219, 138)
|
||||
/* Transaction list -- partially confirmed positive amount (1..RecommendedNumConfirmations-1 confirms)
|
||||
Mid-tone green (#4A8C5E): clearly green but visibly dimmer than the saturated final-state
|
||||
#7CDB8A so the eye reads the difference between "in progress" and "final" at a glance. */
|
||||
#define COLOR_CONFIRMING QColor(74, 140, 94)
|
||||
/* Transaction list -- bare address (without label) */
|
||||
#define COLOR_BAREADDRESS QColor(97, 40, 14)
|
||||
|
||||
|
||||
+267
-49
@@ -1,6 +1,9 @@
|
||||
#include "introdialog.h"
|
||||
#include "util.h"
|
||||
#include "bootstrap.h"
|
||||
#include "utxosnapshot.h"
|
||||
#include "checkpoints.h"
|
||||
#include "snapshotnet.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QVBoxLayout>
|
||||
@@ -14,9 +17,37 @@
|
||||
#include <QCheckBox>
|
||||
#include <QApplication>
|
||||
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <set>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <set>
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Convert QString to fs::path preserving non-ASCII characters on Windows.
|
||||
// On Windows, QString::toStdString() returns UTF-8 but std::filesystem::path
|
||||
// constructed from a narrow string then uses the ANSI code page, which
|
||||
// mangles UTF-8 paths. QString::toStdWString() + fs::path(std::wstring)
|
||||
// preserves them. On non-Windows platforms the UTF-8 path is correct.
|
||||
static fs::path qstringToPath(const QString& s)
|
||||
{
|
||||
#ifdef WIN32
|
||||
return fs::path(std::wstring(s.toStdWString()));
|
||||
#else
|
||||
return fs::path(s.toStdString());
|
||||
#endif
|
||||
}
|
||||
|
||||
// Convert fs::path to a UTF-8 std::string. fs::path::u8string() returns
|
||||
// std::u8string in C++20, which has no implicit conversion to std::string
|
||||
// (clang/gcc reject the functional cast). Reinterpret the char8_t payload:
|
||||
// UTF-8 byte values are preserved exactly.
|
||||
static std::string pathToUtf8String(const fs::path& p)
|
||||
{
|
||||
const std::u8string u8 = p.u8string();
|
||||
return std::string(reinterpret_cast<const char*>(u8.data()), u8.size());
|
||||
}
|
||||
|
||||
IntroDialog::IntroDialog(QWidget *parent) :
|
||||
QDialog(parent)
|
||||
@@ -26,14 +57,14 @@ IntroDialog::IntroDialog(QWidget *parent) :
|
||||
|
||||
// Match existing Triangles dark theme
|
||||
setStyleSheet(
|
||||
"QDialog { background-color: #000; color: #f26522; }"
|
||||
"QLabel { color: #f26522; }"
|
||||
"QRadioButton { color: #f26522; }"
|
||||
"QRadioButton::indicator { border: 1px solid #f26522; background-color: #000; width: 12px; height: 12px; border-radius: 7px; }"
|
||||
"QRadioButton::indicator:checked { background-color: #f26522; }"
|
||||
"QLineEdit { background-color: #1c1c1c; border: 1px solid #f26522; color: #f26522; padding: 4px; }"
|
||||
"QPushButton { background-color: #000; color: #f26522; border: 1px solid #f26522; padding: 4px 16px; min-height: 20px; }"
|
||||
"QPushButton:hover { background-color: #61280E; }"
|
||||
"QDialog { background-color: #000; color: #e32105; }"
|
||||
"QLabel { color: #e32105; }"
|
||||
"QRadioButton { color: #e32105; }"
|
||||
"QRadioButton::indicator { border: 1px solid #e32105; background-color: #000; width: 12px; height: 12px; border-radius: 7px; }"
|
||||
"QRadioButton::indicator:checked { background-color: #e32105; }"
|
||||
"QLineEdit { background-color: #1c1c1c; border: 1px solid #e32105; color: #e32105; padding: 4px; }"
|
||||
"QPushButton { background-color: #000; color: #e32105; border: 1px solid #e32105; padding: 4px 16px; min-height: 20px; }"
|
||||
"QPushButton:hover { background-color: #3d0e04; }"
|
||||
);
|
||||
|
||||
defaultDataDir = QString::fromStdString(GetDefaultDataDir().string());
|
||||
@@ -44,7 +75,7 @@ IntroDialog::IntroDialog(QWidget *parent) :
|
||||
|
||||
// Welcome header
|
||||
QLabel *welcomeLabel = new QLabel(tr("Welcome to Triangles!"));
|
||||
welcomeLabel->setStyleSheet("font-size: 16px; font-weight: bold; color: #f26522;");
|
||||
welcomeLabel->setStyleSheet("font-size: 16px; font-weight: bold; color: #e32105;");
|
||||
mainLayout->addWidget(welcomeLabel);
|
||||
|
||||
// Description
|
||||
@@ -152,7 +183,7 @@ void IntroDialog::on_defaultRadio_toggled(bool checked)
|
||||
void IntroDialog::updateFreeSpace()
|
||||
{
|
||||
QString path = getDataDirectory();
|
||||
std::filesystem::path fsPath(path.toStdString());
|
||||
std::filesystem::path fsPath = qstringToPath(path);
|
||||
|
||||
// Walk up to find an existing parent
|
||||
try {
|
||||
@@ -211,14 +242,18 @@ bool IntroDialog::pickDataDirectory()
|
||||
}
|
||||
|
||||
// If the saved path is the default, don't set -datadir (let normal defaults work)
|
||||
QString defaultDir = QString::fromStdString(GetDefaultDataDir().string());
|
||||
QString defaultDir = QString::fromStdString(
|
||||
pathToUtf8String(GetDefaultDataDir()));
|
||||
if (dataDir != defaultDir) {
|
||||
mapArgs["-datadir"] = dataDir.toStdString();
|
||||
// Pass the data dir to the daemon as UTF-8 bytes so a non-ASCII path
|
||||
// on Windows isn't mangled by the ANSI code page (path::string() does
|
||||
// that). The daemon side uses fs::u8path() to convert back.
|
||||
mapArgs["-datadir"] = pathToUtf8String(qstringToPath(dataDir));
|
||||
}
|
||||
|
||||
// Ensure the directory exists
|
||||
try {
|
||||
fs::create_directories(fs::path(dataDir.toStdString()));
|
||||
fs::create_directories(qstringToPath(dataDir));
|
||||
} catch (const fs::filesystem_error &) {
|
||||
QMessageBox::critical(0, "Triangles",
|
||||
QString("Error: Could not create data directory \"%1\".").arg(dataDir));
|
||||
@@ -227,17 +262,24 @@ bool IntroDialog::pickDataDirectory()
|
||||
|
||||
// Auto-bootstrap: if no blockchain data exists, download automatically.
|
||||
// If data exists, offer optional re-download (unless user checked "don't ask again").
|
||||
fs::path dataDirPath(dataDir.toStdString());
|
||||
fs::path dataDirPath = qstringToPath(dataDir);
|
||||
bool needsBootstrap = Bootstrap::NeedsBootstrap(dataDirPath);
|
||||
bool userWantsBootstrap = false;
|
||||
// Captured local-load error from the staged-snapshot probe below, surfaced
|
||||
// in the HTTPS-failure dialog so users see why their snapshot was rejected.
|
||||
std::string lastLocalLoadError;
|
||||
|
||||
if (needsBootstrap)
|
||||
{
|
||||
// No blockchain data — bootstrap automatically, just inform the user
|
||||
// No blockchain data — bootstrap automatically, just inform the user.
|
||||
// The actual mechanism (local snapshot vs HTTPS download vs network
|
||||
// sync) is decided after probing the data directory; the dialog
|
||||
// intentionally doesn't promise "downloading" because we may find a
|
||||
// staged utxo-snapshot.bin and skip the network entirely.
|
||||
QMessageBox::information(0, "Triangles",
|
||||
"No blockchain data found.\n\n"
|
||||
"Downloading the latest blockchain snapshot automatically.\n"
|
||||
"This will only take a few minutes.");
|
||||
"Setting up the wallet now — this only happens once.\n"
|
||||
"If a local snapshot is available it will be loaded automatically.");
|
||||
userWantsBootstrap = true;
|
||||
}
|
||||
else if (!settings.value("bootstrapDontAsk", false).toBool())
|
||||
@@ -263,44 +305,220 @@ bool IntroDialog::pickDataDirectory()
|
||||
userWantsBootstrap = (ret == QMessageBox::Yes);
|
||||
}
|
||||
|
||||
if (userWantsBootstrap)
|
||||
{
|
||||
std::string host = Bootstrap::DEFAULT_HOST;
|
||||
std::string strError;
|
||||
if (userWantsBootstrap)
|
||||
{
|
||||
std::string host = Bootstrap::DEFAULT_HOST;
|
||||
std::string strError;
|
||||
|
||||
QProgressDialog progress("Downloading blockchain snapshot...", "Cancel",
|
||||
0, 100, 0);
|
||||
progress.setWindowTitle("Triangles - Bootstrap");
|
||||
progress.setWindowModality(Qt::ApplicationModal);
|
||||
progress.setMinimumDuration(0);
|
||||
progress.setValue(0);
|
||||
// First-run local-snapshot probe: if utxo-snapshot.bin is already in the
|
||||
// data directory (placed there by the user, an installer, or a
|
||||
// previous P2P fetch), load it directly. This avoids the HTTPS
|
||||
// bootstrap path entirely on first run.
|
||||
fs::path stagedSnap = dataDirPath / "utxo-snapshot.bin";
|
||||
std::error_code stagedEc;
|
||||
// Use the non-throwing error_code overload and probe symlink
|
||||
// status separately. We reject symlinks: an auto-loaded snapshot
|
||||
// is supposed to be a file the user placed in the data dir, not a
|
||||
// symlink an attacker could redirect to anything; if the user
|
||||
// genuinely wants to symlink, they can resolve it and copy the
|
||||
// file. A symlink_status probe error (broken perms, ENOENT on a
|
||||
// parent component) is treated as "not present" and falls through
|
||||
// to HTTPS bootstrap.
|
||||
fs::file_status symSt = fs::symlink_status(stagedSnap, stagedEc);
|
||||
bool stagedPresent = (!stagedEc &&
|
||||
fs::is_symlink(symSt) == false &&
|
||||
fs::is_regular_file(symSt));
|
||||
if (stagedEc) {
|
||||
printf("IntroDialog: cannot probe staged snapshot path (%s); skipping local load\n",
|
||||
stagedEc.message().c_str());
|
||||
stagedPresent = false;
|
||||
}
|
||||
if (stagedPresent) {
|
||||
printf("IntroDialog: found staged utxo-snapshot.bin in data dir, attempting local load...\n");
|
||||
|
||||
auto progressFn = [&progress](int64_t bytesDownloaded, int64_t totalBytes) {
|
||||
if (totalBytes > 0) {
|
||||
int pct = (int)((bytesDownloaded * 100) / totalBytes);
|
||||
progress.setValue(pct);
|
||||
progress.setLabelText(
|
||||
QString("Downloading blockchain snapshot... %1 MB / %2 MB")
|
||||
.arg(bytesDownloaded / (1024*1024))
|
||||
.arg(totalBytes / (1024*1024)));
|
||||
// Validate the staged file against the compiled-in hash before
|
||||
// touching the chain DB. This prevents loading a stale or wrong
|
||||
// snapshot from a previous install into a fresh data dir.
|
||||
int snapHeight = Checkpoints::GetBestSnapshotHeight();
|
||||
uint256 expectedHash;
|
||||
bool hashOk = (snapHeight > 0) &&
|
||||
Checkpoints::GetSnapshotHash(snapHeight, expectedHash);
|
||||
|
||||
std::string localErr;
|
||||
bool loaded = false;
|
||||
if (hashOk) {
|
||||
uint256 actualHash;
|
||||
std::string hashErr;
|
||||
if (SnapshotNet::ComputeSnapshotFileHash(stagedSnap, actualHash, hashErr)) {
|
||||
if (actualHash != expectedHash) {
|
||||
// Staged file is for a different release or otherwise
|
||||
// doesn't match this build's compiled-in hash. Do NOT
|
||||
// delete it — the user may have staged it intentionally,
|
||||
// or it may belong to another release. Quarantine with
|
||||
// a unique suffix so a previously quarantined file is
|
||||
// never overwritten. If no free name can be found (or
|
||||
// the rename itself fails for perms/locks), leave the
|
||||
// original in place and report the exact error so the
|
||||
// user can recover manually.
|
||||
std::error_code rmEc;
|
||||
fs::path quarantine;
|
||||
bool foundFreeName = false;
|
||||
// Capture the timestamp once — repeated time() calls
|
||||
// inside the loop would just shift the suffix but add
|
||||
// nothing useful, and could overflow on busy systems.
|
||||
const std::string quarantinePrefix =
|
||||
stagedSnap.string() + ".rejected." +
|
||||
std::to_string(::time(nullptr)) + ".";
|
||||
// Collision-safe suffix: timestamp + a small loop
|
||||
// counter. Two rejections within the same second
|
||||
// (e.g. double-clicked bootstrap dialog) still get
|
||||
// distinct destinations. The loop caps at 1000 attempts;
|
||||
// if every candidate is occupied we refuse to rename
|
||||
// (overwriting an earlier quarantined file would lose
|
||||
// the user's data and is worse than just reporting the
|
||||
// conflict).
|
||||
for (int attempt = 0; attempt < 1000; ++attempt) {
|
||||
std::string name = quarantinePrefix +
|
||||
std::to_string(attempt);
|
||||
quarantine = name;
|
||||
std::error_code probeEc;
|
||||
if (!fs::exists(quarantine, probeEc) && !probeEc) {
|
||||
foundFreeName = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundFreeName) {
|
||||
// All 1000 candidate names were already taken.
|
||||
// This is extremely unlikely in normal operation
|
||||
// but if it happens, surface an honest error —
|
||||
// a "no space on device" message would be
|
||||
// misleading here.
|
||||
rmEc = std::make_error_code(std::errc::file_exists);
|
||||
} else {
|
||||
fs::rename(stagedSnap, quarantine, rmEc);
|
||||
}
|
||||
if (rmEc) {
|
||||
localErr = "staged utxo-snapshot.bin hash does not match this release "
|
||||
"(expected " + expectedHash.ToString().substr(0, 16) +
|
||||
", got " + actualHash.ToString().substr(0, 16) +
|
||||
"), AND quarantine failed (" + rmEc.message() +
|
||||
"). Leave the original in place and review it manually: " +
|
||||
stagedSnap.string();
|
||||
} else {
|
||||
localErr = "staged utxo-snapshot.bin hash does not match this release "
|
||||
"(expected " + expectedHash.ToString().substr(0, 16) +
|
||||
", got " + actualHash.ToString().substr(0, 16) +
|
||||
"). File moved to " + quarantine.filename().string() +
|
||||
" — review or delete it manually.";
|
||||
}
|
||||
printf("IntroDialog: %s\n", localErr.c_str());
|
||||
} else {
|
||||
loaded = UtxoSnapshot::LoadSnapshot(stagedSnap, dataDirPath,
|
||||
localErr, /*requireCheckpoint=*/true);
|
||||
}
|
||||
} else {
|
||||
progress.setLabelText(
|
||||
QString("Downloading blockchain snapshot... %1 MB")
|
||||
.arg(bytesDownloaded / (1024*1024)));
|
||||
localErr = "cannot hash staged snapshot: " + hashErr;
|
||||
}
|
||||
QApplication::processEvents();
|
||||
};
|
||||
} else {
|
||||
localErr = "no compiled-in snapshot hash available in this release";
|
||||
}
|
||||
|
||||
bool success = Bootstrap::DownloadBootstrap(host, dataDirPath, progressFn, strError);
|
||||
if (!success) {
|
||||
if (loaded) {
|
||||
printf("IntroDialog: loaded staged utxo-snapshot.bin successfully\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Staged file failed to load — fall through to HTTPS bootstrap.
|
||||
// Remember the local-load reason so we can show it to the user
|
||||
// if HTTPS also fails (the failure dialog below concatenates it).
|
||||
printf("IntroDialog: staged snapshot unusable (%s); falling back to network bootstrap\n",
|
||||
localErr.c_str());
|
||||
lastLocalLoadError = localErr;
|
||||
}
|
||||
|
||||
QProgressDialog progress("Downloading blockchain snapshot...", "Cancel",
|
||||
0, 100, 0);
|
||||
progress.setWindowTitle("Triangles - Bootstrap");
|
||||
progress.setWindowModality(Qt::ApplicationModal);
|
||||
progress.setMinimumDuration(0);
|
||||
progress.setValue(0);
|
||||
|
||||
auto progressFn = [&progress](int64_t bytesDownloaded, int64_t totalBytes) {
|
||||
if (totalBytes > 0) {
|
||||
int pct = (int)((bytesDownloaded * 100) / totalBytes);
|
||||
progress.setValue(pct);
|
||||
progress.setLabelText(
|
||||
QString("Downloading blockchain snapshot... %1 MB / %2 MB")
|
||||
.arg(bytesDownloaded / (1024*1024))
|
||||
.arg(totalBytes / (1024*1024)));
|
||||
} else {
|
||||
progress.setLabelText(
|
||||
QString("Downloading blockchain snapshot... %1 MB")
|
||||
.arg(bytesDownloaded / (1024*1024)));
|
||||
}
|
||||
QApplication::processEvents();
|
||||
};
|
||||
|
||||
// Try the fast UTXO snapshot path. The GUI has already probed the data
|
||||
// dir for a staged utxo-snapshot.bin above; if that didn't find one,
|
||||
// DownloadUtxoSnapshot is the canonical HTTPS path to the bootstrap
|
||||
// server. TLS validation is now handled in bootstrap.cpp's StartTLS
|
||||
// via a layered trust store (exedir cacert.pem → SSL_CERT_FILE →
|
||||
// system default paths → embedded ISRG X1 + X2 as belt-and-suspenders),
|
||||
// so this should succeed on Windows GUI builds where the Qt-bundled
|
||||
// libssl-3-x64.dll ships without a default cert path. The system-path
|
||||
// call is trusted on its return value (OpenSSL's hashed-directory
|
||||
// lookups are lazy and would otherwise show 0 eagerly loaded store
|
||||
// objects even on a valid install); the embedded fallbacks are
|
||||
// always attempted as cross-sign resilience and become load-bearing
|
||||
// on a stripped Windows GUI with no cacert.pem and no usable system
|
||||
// CA directory.
|
||||
std::string utxoError;
|
||||
bool success = Bootstrap::DownloadUtxoSnapshot(host, dataDirPath, progressFn, utxoError);
|
||||
if (!success) {
|
||||
strError = utxoError;
|
||||
}
|
||||
if (!success) {
|
||||
// The TLS-detection strings are matched against the standard error
|
||||
// messages produced by the bootstrap OpenSSL path; they cover
|
||||
// the common GUI-bundled OpenSSL failure modes without dumping
|
||||
// the raw error to the user.
|
||||
bool isTlsError = (strError.find("TLS handshake failed") != std::string::npos ||
|
||||
strError.find("certificate verify failed") != std::string::npos ||
|
||||
strError.find("TLS certificate verification failed") != std::string::npos);
|
||||
if (isTlsError) {
|
||||
QString localNote;
|
||||
if (!lastLocalLoadError.empty()) {
|
||||
localNote = QString("\n\nLocal snapshot note: %1")
|
||||
.arg(QString::fromStdString(lastLocalLoadError));
|
||||
}
|
||||
QMessageBox::warning(0, "Triangles",
|
||||
QString("Could not download blockchain snapshot automatically.\n\n"
|
||||
"The bundled network stack cannot validate the certificate of the\n"
|
||||
"bootstrap server. To skip this, place a file named\n"
|
||||
" utxo-snapshot.bin\n"
|
||||
"in your Triangles data directory:\n"
|
||||
" %1\n\n"
|
||||
"Then restart the wallet — the snapshot will load automatically.\n\n"
|
||||
"Otherwise, the wallet will sync from the network instead.%2")
|
||||
.arg(dataDir)
|
||||
.arg(localNote));
|
||||
} else {
|
||||
QString localNote;
|
||||
if (!lastLocalLoadError.empty()) {
|
||||
localNote = QString("\n\nLocal snapshot note: %1")
|
||||
.arg(QString::fromStdString(lastLocalLoadError));
|
||||
}
|
||||
QMessageBox::warning(0, "Triangles",
|
||||
QString("Could not download blockchain snapshot:\n%1\n\n"
|
||||
"The wallet will sync from the network instead.")
|
||||
.arg(QString::fromStdString(strError)));
|
||||
} else {
|
||||
progress.setValue(100);
|
||||
"The wallet will sync from the network instead.%2")
|
||||
.arg(QString::fromStdString(strError))
|
||||
.arg(localNote));
|
||||
}
|
||||
} else {
|
||||
progress.setValue(100);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -324,8 +542,8 @@ bool IntroDialog::migrateDataDirectory(const QString& oldPath, const QString& ne
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::path srcDir(oldPath.toStdString());
|
||||
fs::path dstDir(newPath.toStdString());
|
||||
fs::path srcDir = qstringToPath(oldPath);
|
||||
fs::path dstDir = qstringToPath(newPath);
|
||||
|
||||
if (!fs::exists(srcDir) || !fs::is_directory(srcDir))
|
||||
return false;
|
||||
|
||||
@@ -1406,10 +1406,10 @@ This label turns red, if the priority is smaller than "medium".
|
||||
<location line="+63"/>
|
||||
<source><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1420,10 +1420,10 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
<location line="+25"/>
|
||||
<source><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1434,10 +1434,10 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
<location line="+28"/>
|
||||
<source><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -3656,7 +3656,7 @@ If the file does not exist, create it with owner-readable-only file permissions.
|
||||
</message>
|
||||
<message>
|
||||
<location line="+108"/>
|
||||
<source><font color='#f26522'>This transaction is over the size limit. You can still send it for a fee of %1, which goes to the nodes that process your transaction and helps to support the network. </font>Do you want to pay the fee?</source>
|
||||
<source><font color='#e32105'>This transaction is over the size limit. You can still send it for a fee of %1, which goes to the nodes that process your transaction and helps to support the network. </font>Do you want to pay the fee?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1409,10 +1409,10 @@ This label turns red, if the priority is smaller than "medium".
|
||||
<location line="+63"/>
|
||||
<source><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1423,10 +1423,10 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
<location line="+25"/>
|
||||
<source><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1437,10 +1437,10 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
<location line="+28"/>
|
||||
<source><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -3679,7 +3679,7 @@ If the file does not exist, create it with owner-readable-only file permissions.
|
||||
</message>
|
||||
<message>
|
||||
<location line="+108"/>
|
||||
<source><font color='#f26522'>This transaction is over the size limit. You can still send it for a fee of %1, which goes to the nodes that process your transaction and helps to support the network. </font>Do you want to pay the fee?</source>
|
||||
<source><font color='#e32105'>This transaction is over the size limit. You can still send it for a fee of %1, which goes to the nodes that process your transaction and helps to support the network. </font>Do you want to pay the fee?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
|
||||
@@ -1406,10 +1406,10 @@ This label turns red, if the priority is smaller than "medium".
|
||||
<location line="+63"/>
|
||||
<source><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1420,10 +1420,10 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
<location line="+25"/>
|
||||
<source><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1434,10 +1434,10 @@ a:active { color:#f26522; text-decoration: underline; }
|
||||
<location line="+28"/>
|
||||
<source><head>
|
||||
<style type="text/css" media="screen">
|
||||
a:link { color:#f26522; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#f26522; text-decoration: none; }
|
||||
a:hover { color:#f26522; text-decoration: underline; }
|
||||
a:active { color:#f26522; text-decoration: underline; }
|
||||
a:link { color:#e32105; text-decoration: none;font-weight:bold; }
|
||||
a:visited { color:#e32105; text-decoration: none; }
|
||||
a:hover { color:#e32105; text-decoration: underline; }
|
||||
a:active { color:#e32105; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -3661,7 +3661,7 @@ If the file does not exist, create it with owner-readable-only file permissions.
|
||||
</message>
|
||||
<message>
|
||||
<location line="+108"/>
|
||||
<source><font color='#f26522'>This transaction is over the size limit. You can still send it for a fee of %1, which goes to the nodes that process your transaction and helps to support the network. </font>Do you want to pay the fee?</source>
|
||||
<source><font color='#e32105'>This transaction is over the size limit. You can still send it for a fee of %1, which goes to the nodes that process your transaction and helps to support the network. </font>Do you want to pay the fee?</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