Compare commits

..

3 Commits

Author SHA1 Message Date
Krystie 9bdb1b21b1 Fix .mailmap: use primary GitHub email
Build All Platforms / test-linux-unit (push) Failing after 40s
Build All Platforms / test-linux-sanitizers (push) Failing after 40s
Build All Platforms / build-linux-qt (push) Failing after 40s
Build All Platforms / build-linux-daemon (push) Failing after 40s
Build All Platforms / build-windows-qt (push) Has been cancelled
Build All Platforms / build-windows-daemon (push) Has been cancelled
Build All Platforms / build-macos (push) Has been cancelled
Build All Platforms / release (push) Has been cancelled
Build All Platforms / Trigger TRI-PI ARM64 Build (push) Has been cancelled
2026-05-23 16:51:59 -07:00
Krystie 309f529218 Add .mailmap to consolidate contributor identities 2026-05-23 16:48:47 -07:00
sami7777 af55bfac80 Merge branch 'cpp20-modernization' into master
Brings in the cpp20 modernization stream (28+ commits) plus the
CSyncManager extraction and LevelDB->RocksDB chain DB migration.

Conflict resolutions:
  - clientversion.h: bumped to v6.0.1 (cpp20's v6.0.0 superseded
    master's v5.9.7.0; bumped one revision to mark the merged release)
  - .github/workflows/build-all.yml: kept master's explicit submodule
    init (Windows CI hardening from 372b252/6c56e41)
  - src/trianglesrpc.cpp: combined master's bad-auth crash fix (79b0c4a)
    with cpp20's TrimString (de-boost), preserving both defenses

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:32:22 -07:00
321 changed files with 6216 additions and 108352 deletions
-16
View File
@@ -1,16 +0,0 @@
.git
.github
build
build-*
cmake-build-*
*.dat
*.log
*.pid
*.conf
*.key
*.pem
*.sqlite
*.sqlite3
.triangles
wallet.dat
wallet.dat.*
-65
View File
@@ -1,65 +0,0 @@
-----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-----
-262
View File
@@ -1,262 +0,0 @@
#!/usr/bin/env python3
"""Krystie Gate — static check stage of the CI gate.
Runs inside the Gitea Actions runner. Inspects all commits that were just
pushed to a krystie-wip/* branch and rejects if any violates the gate rules.
Decision per commit:
* If signed by Krystie's GPG key (fingerprint DCF2579968107984), apply the
full per-repo gate.
* If signed by a different key OR unsigned, allow (Sami's authority).
Per-repo enforcement:
* triangles_v5 : red-list (consensus paths) + test-first + no-clearnet
* triangles-explorer, triangles-api, tridock-web-wallet, sami-chat, tri-pi:
test-first only
* homebrew-triangles: formula syntax check only
Outputs:
* On reject, prints REJECTED lines to stderr and exits 1.
* On accept, sets `is_krystie_commit` GH-actions output to true/false.
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
# Krystie's GPG identity. We accept both the primary long-ID and the
# signing subkey because `git log %GK` returns the subkey that was actually
# used to sign, not the primary. The full primary fingerprint is also
# included so a paranoid future check can validate the chain.
KRYSTIE_PRIMARY_FP = "523A81833EB7201573E1EFE1DCF2579968107984"
KRYSTIE_KEY_IDS = {
"DCF2579968107984", # primary long-ID
"C2DC60618C85A159", # signing subkey long-ID
}
RED_LIST_TRIANGLES_V5 = [
re.compile(r"^src/main\.(cpp|h)$"),
re.compile(r"^src/validation.*"),
re.compile(r"^src/kernel\.(cpp|h)$"),
re.compile(r"^src/checkpoints\.(cpp|h)$"),
re.compile(r"^src/consensus/"),
re.compile(r"^src/protocol\.(cpp|h)$"),
re.compile(r"^src/net\.(cpp|h)$"),
re.compile(r"^src/netbase\.(cpp|h)$"),
re.compile(r"^src/net_bootstrap\.(cpp|h)$"),
re.compile(r"^src/chainparams.*"),
re.compile(r"^src/clientversion\.h$"),
re.compile(r"^src/key\.(cpp|h)$"),
re.compile(r"^src/keystore\.(cpp|h)$"),
re.compile(r"^src/onionseed\.h$"),
re.compile(r"^contrib/seeds/"),
re.compile(r"^contrib/devtools/release.*"),
re.compile(r"^doc/release-process\.txt$"),
]
TEST_DIRS = {
"triangles_v5": ["src/test/", "test/"],
"triangles-explorer": ["src/__tests__/", "tests/", "test/"],
"triangles-api": ["test/", "__tests__/", "tests/"],
"tridock-web-wallet": ["test/", "__tests__/", "tests/"],
"sami-chat": ["test/", "__tests__/", "tests/"],
"tri-pi": ["test/", "tests/"],
"homebrew-triangles": [],
}
SOURCE_EXTS = {
"triangles_v5": {".cpp", ".h", ".c"},
"triangles-explorer": {".ts", ".tsx", ".js", ".svelte"},
"triangles-api": {".js", ".ts"},
"tridock-web-wallet": {".ts", ".tsx", ".js", ".svelte", ".vue"},
"sami-chat": {".ts", ".tsx", ".js", ".svelte", ".vue"},
"tri-pi": {".py", ".sh", ".ts", ".js"},
"homebrew-triangles": set(),
}
RED_LIST_REPOS = {"triangles_v5"}
PEER_CONFIG_PATHS = [
re.compile(r"^contrib/seeds/"),
re.compile(r"^src/chainparams.*"),
re.compile(r".*triangles\.conf(\.example)?$"),
]
@dataclass
class GateResult:
ok: bool
reason: str = ""
def repo_name() -> str:
repo = os.environ.get("GITHUB_REPOSITORY", "")
return repo.split("/", 1)[1] if "/" in repo else repo
def commit_signer(sha: str) -> str | None:
try:
out = subprocess.run(
["git", "log", "-1", "--format=%GK", sha],
check=True, capture_output=True, text=True,
).stdout.strip()
return out or None
except subprocess.CalledProcessError:
return None
def is_krystie_commit(sha: str) -> bool:
fp = commit_signer(sha)
if not fp:
return False
# Accept any key ID we know belongs to Krystie. `git log %GK` returns the
# signing subkey, so we have to whitelist both primary and subkey.
return any(fp == known or known.endswith(fp) for known in KRYSTIE_KEY_IDS)
def commits_in_push() -> list[str]:
before = os.environ.get("GITHUB_BEFORE", "")
sha = os.environ.get("GITHUB_SHA", "")
if not sha:
return []
if not before or set(before) == {"0"}:
# New branch — only inspect the head commit (don't walk history)
return [sha]
# On force-push, `before` may have been orphaned and is unreachable in the
# checked-out repo. `git rev-list before..sha` then exits 128. Fall back
# to inspecting the new head only — that's the safest guarantee we can
# make about what just landed.
try:
out = subprocess.run(
["git", "rev-list", f"{before}..{sha}"],
check=True, capture_output=True, text=True,
).stdout
return [c for c in out.split() if c]
except subprocess.CalledProcessError:
return [sha]
def changed_files(sha: str) -> list[str]:
out = subprocess.run(
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", sha],
check=True, capture_output=True, text=True,
).stdout
return [f for f in out.split("\n") if f]
def commit_diff_text(sha: str, paths: list[str]) -> str:
if not paths:
return ""
out = subprocess.run(
["git", "show", "--no-color", sha, "--"] + paths,
check=True, capture_output=True, text=True,
).stdout
return out
def red_list_check(repo: str, files: list[str]) -> GateResult:
if repo not in RED_LIST_REPOS:
return GateResult(True)
for f in files:
for pat in RED_LIST_TRIANGLES_V5:
if pat.match(f):
return GateResult(False, f"red-list violation: '{f}' is consensus/critical-path; needs Sami review (open red-list-labeled issue)")
return GateResult(True)
def _is_test_path(f: str, test_dirs: list[str]) -> bool:
return any(f.startswith(d) for d in test_dirs) or "/test/" in f or "/tests/" in f or "/__tests__/" in f
def test_first_check(repo: str, files: list[str]) -> GateResult:
src_exts = SOURCE_EXTS.get(repo, set())
test_dirs = TEST_DIRS.get(repo, [])
if not src_exts or not test_dirs:
return GateResult(True)
src_changed = any(any(f.endswith(e) for e in src_exts) and not _is_test_path(f, test_dirs) for f in files)
test_changed = any(_is_test_path(f, test_dirs) for f in files)
if src_changed and not test_changed:
return GateResult(False, f"test-first violation: source changed without paired test; expected test under {test_dirs}")
return GateResult(True)
def no_clearnet_check(repo: str, sha: str, files: list[str]) -> GateResult:
if repo != "triangles_v5":
return GateResult(True)
peer_files = [f for f in files if any(p.match(f) for p in PEER_CONFIG_PATHS)]
if not peer_files:
return GateResult(True)
diff = commit_diff_text(sha, peer_files)
for line in diff.split("\n"):
if not line.startswith("+") or line.startswith("+++"):
continue
body = line[1:].strip()
if re.search(r"\b(addnode|seednode|connect)\s*=", body, re.IGNORECASE):
if ".onion" not in body.lower():
return GateResult(False, f"no-clearnet: added peer/seed without .onion: {body[:120]}")
if re.match(r"^\s*(\d{1,3}\.){3}\d{1,3}\b", body) or re.match(r"^\s*[0-9a-fA-F:]{4,}\b", body):
return GateResult(False, f"no-clearnet: clearnet address added: {body[:120]}")
return GateResult(True)
def gate_commit(repo: str, sha: str) -> list[str]:
files = changed_files(sha)
failures = []
for check, args in [
(red_list_check, (repo, files)),
(test_first_check, (repo, files)),
(no_clearnet_check, (repo, sha, files)),
]:
r = check(*args)
if not r.ok:
failures.append(f"commit {sha[:12]}: {r.reason}")
return failures
def emit_output(name: str, value: str):
out_file = os.environ.get("GITHUB_OUTPUT", "")
if out_file:
with open(out_file, "a") as fh:
fh.write(f"{name}={value}\n")
def main() -> int:
repo = repo_name()
if not repo:
print("ERROR: GITHUB_REPOSITORY not set", file=sys.stderr)
return 2
commits = commits_in_push()
if not commits:
print("No commits to inspect", file=sys.stdout)
emit_output("is_krystie_commit", "false")
return 0
krystie_count = 0
all_failures: list[str] = []
for sha in commits:
if not is_krystie_commit(sha):
print(f" {sha[:12]}: not Krystie-signed (allow)")
continue
krystie_count += 1
print(f" {sha[:12]}: Krystie-signed; running gate")
failures = gate_commit(repo, sha)
all_failures.extend(failures)
emit_output("is_krystie_commit", "true" if krystie_count > 0 else "false")
if all_failures:
print(f"\n[KRYSTIE GATE] REJECTED on {repo}:", file=sys.stderr)
for f in all_failures:
print(f" - {f}", file=sys.stderr)
return 1
print(f"[KRYSTIE GATE] PASS on {repo} ({krystie_count} Krystie commit(s) inspected, {len(commits) - krystie_count} non-Krystie)")
return 0
if __name__ == "__main__":
sys.exit(main())
-132
View File
@@ -1,132 +0,0 @@
name: Krystie Gate
# Runs on every push to krystie-wip/* branches.
# Static checks first (cheap), then build + tests.
# If everything green AND the commit is Krystie's, fast-forwards master.
# Sami's pushes (admin) bypass this entire flow — he goes direct to master.
on:
push:
branches:
- 'krystie-wip/**'
jobs:
static-gate:
name: "Static gate (red-list / test-first / no-clearnet)"
runs-on: ubuntu-latest
outputs:
is_krystie_commit: ${{ steps.gate.outputs.is_krystie_commit }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Import Krystie public key (for verification)
run: |
mkdir -p ~/.gnupg && chmod 700 ~/.gnupg
if [ -f .gitea/krystie-release.pub.asc ]; then
gpg --import .gitea/krystie-release.pub.asc
# Mark the key as ultimately trusted so `git log %GK` will consider
# signatures valid. Without this, %GK returns empty and the gate
# treats Krystie's commits as unsigned, defeating the whole point.
FP=$(gpg --list-keys --with-colons | awk -F: '/^fpr:/ {print $10; exit}')
echo "${FP}:6:" | gpg --import-ownertrust
echo "Imported and trusted Krystie public key: ${FP}"
# Configure git to call gpg for verification (it does by default,
# but explicit doesn't hurt) and not to require signed-by-default.
git config --global gpg.program gpg
else
echo "WARN: .gitea/krystie-release.pub.asc not found — gate will treat all commits as non-Krystie (i.e. allow)"
fi
- name: Run gate
id: gate
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_BEFORE: ${{ github.event.before }}
run: |
python3 .gitea/krystie_gate.py
build-and-test:
name: "Build + ctest"
needs: static-gate
runs-on: ubuntu-latest
if: ${{ needs.static-gate.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake ninja-build pkg-config \
libssl-dev libboost-all-dev libdb++-dev libleveldb-dev \
librocksdb-dev libevent-dev libsodium-dev \
libsecp256k1-dev || true
# Some packages may not be available; the C++20 / RocksDB modernization
# is in flight, so missing deps are tolerable for v1 of the gate.
- name: Configure (daemon-only, no Qt)
run: |
mkdir -p build && cd build
cmake .. -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_QT=OFF \
-DBUILD_TESTS=ON \
-DBUILD_ROCKSDB=OFF \
|| (echo "::warning::CMake configure failed — likely WIP modernization. Allowing build skip for v1." && exit 0)
- name: Build
run: |
if [ -f build/build.ninja ]; then
cd build && ninja -j$(nproc) 2>&1 | tail -100 || (echo "::warning::Build failed — flagging for Sami review" && exit 1)
else
echo "::warning::No build.ninja produced; skipping for v1"
fi
- name: ctest
run: |
if [ -f build/CTestTestfile.cmake ]; then
cd build && ctest --output-on-failure -j$(nproc) || exit 1
else
echo "::warning::No ctest produced; skipping for v1 — Krystie should add tests in src/test/"
fi
auto-merge:
name: "Auto-merge to master"
needs: [static-gate, build-and-test]
runs-on: ubuntu-latest
if: ${{ needs.static-gate.result == 'success' && needs.build-and-test.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.KRYSTIE_GITEA_TOKEN }}
- name: Fast-forward master to this branch
env:
GITEA_TOKEN: ${{ secrets.KRYSTIE_GITEA_TOKEN }}
BRANCH: ${{ github.ref_name }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
# The wip branch is master + N Krystie commits. A plain push with
# the wip sha onto refs/heads/master succeeds iff the update is a
# fast-forward — which is exactly the safety we want. (Earlier
# versions called PATCH /branches/master which is Gitea's branch-
# rename endpoint, not a ref-update endpoint, and always failed.)
REPO="${GITHUB_REPOSITORY}" # owner/name
GIT_URL="http://localhost:3030/${REPO}.git"
git -c "http.extraHeader=Authorization: token ${GITEA_TOKEN}" \
push "${GIT_URL}" "${SHA}:refs/heads/master" \
&& echo "Master fast-forwarded to ${SHA:0:12}" \
|| (echo "::error::Fast-forward push refused — master has likely diverged" && exit 1)
# Clean up the wip branch via the same push channel (delete = empty source).
git -c "http.extraHeader=Authorization: token ${GITEA_TOKEN}" \
push "${GIT_URL}" ":refs/heads/${BRANCH}" \
&& echo "Cleaned up wip branch ${BRANCH}" \
|| echo "::warning::Could not delete wip branch (it'll get pruned later)"
File diff suppressed because it is too large Load Diff
-670
View File
@@ -1,670 +0,0 @@
name: Distribute Release
# Auto-pushes new releases to package managers. Triggers on:
# - tag push (e.g. v5.9.21) — the normal release flow
# - workflow_dispatch — manual run for testing or backports
#
# Each step that needs a secret checks for it and skips gracefully with a
# clear warning if it's not set, so the workflow can be merged and tested
# before secrets are configured.
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
version:
description: 'Override version (e.g. 5.9.21). Leave blank to use tag.'
required: false
type: string
permissions:
contents: read
jobs:
version:
name: Resolve version
runs-on: ubuntu-22.04
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
outputs:
version: ${{ steps.v.outputs.version }}
steps:
- id: v
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.version }}" ]; then
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
else
echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
fi
- run: echo "Distributing v${{ steps.v.outputs.version }}"
docker:
name: Docker Hub
needs: version
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-22.04
permissions:
contents: read
packages: write
env:
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
run: |
if [ -z "$DOCKERHUB_TOKEN" ]; then
echo "::warning::DOCKERHUB_TOKEN secret not set — skipping Docker push. Add it at Settings → Secrets → Actions."
exit 0
fi
echo "$DOCKERHUB_TOKEN" | docker login -u samiahmed7777 --password-stdin
- name: Wait for release artifacts
run: |
# The Dockerfile downloads the daemon .deb from the release URL.
# On tag-push the release is created first, but the assets get
# uploaded a few seconds/minutes later by the build job — without
# this wait, the Docker build races and fails with curl 22 / 404
# (saw this on v5.9.24 run #24, dist #24, Docker Hub job
# step #5 — release was published 8 min after the workflow fired).
for i in {1..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/90)"
sleep 20
done
echo "::error::Release v${VERSION} daemon .deb never became available after 30 minutes"
exit 1
- name: Build and push
run: |
if [ -z "$DOCKERHUB_TOKEN" ]; then exit 0; fi
docker buildx build \
--push \
--tag samiahmed7777/trianglesd:$VERSION \
--tag samiahmed7777/trianglesd:latest \
--cache-from type=gha \
--cache-to type=gha,mode=max \
--provenance=false \
./packaging/docker
- name: Verify pushed image
run: |
if [ -z "$DOCKERHUB_TOKEN" ]; then exit 0; fi
docker pull samiahmed7777/trianglesd:$VERSION
echo "--- trianglesd -version ---"
docker run --rm samiahmed7777/trianglesd:$VERSION trianglesd -version 2>&1 | head -3
echo "--- triangles-cli getinfo (will fail without RPC, expected) ---"
docker run --rm samiahmed7777/trianglesd:$VERSION triangles-cli getinfo 2>&1 | head -3
aur:
name: AUR (triangles-qt-bin)
needs: version
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-22.04
container:
image: archlinux:latest
options: --privileged
env:
AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }}
VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Check AUR_SSH_KEY
run: |
if [ -z "$AUR_SSH_KEY" ]; then
echo "::warning::AUR_SSH_KEY secret not set — skipping AUR push. Add it at Settings → Secrets → Actions."
echo "::warning::The key should be the contents of ~/.ssh/aur_key (private key, not .pub)."
fi
- name: Install build tools + create non-root user
if: env.AUR_SSH_KEY != ''
run: |
pacman -Syu --noconfirm --needed git openssh base-devel python sudo
# makepkg refuses to run as root — create a build user
useradd -m -s /bin/bash build
echo 'build ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
chown -R build:build "$GITHUB_WORKSPACE"
- name: Wait for release artifacts
if: env.AUR_SSH_KEY != ''
run: |
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/90)"
sleep 20
done
echo "::error::Release v${VERSION} .deb never became available after 30 minutes"
exit 1
- name: Download source .debs
if: env.AUR_SSH_KEY != ''
run: |
cd /tmp
curl -fsSL -o full.deb "https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/cryptographic-triangles_${VERSION}_amd64.deb"
curl -fsSL -o daemon.deb "https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/cryptographic-triangles-daemon_${VERSION}_amd64.deb"
ls -la /tmp/*.deb
sha256sum /tmp/full.deb /tmp/daemon.deb
- name: Update PKGBUILD with version + SHA256s
if: env.AUR_SSH_KEY != ''
run: |
cp "$GITHUB_WORKSPACE/packaging/aur/PKGBUILD" /tmp/PKGBUILD
chown build:build /tmp/PKGBUILD /tmp/full.deb /tmp/daemon.deb
sudo -u build bash -c '
set -e
cd /tmp
FULL_SHA=$(sha256sum full.deb | awk "{print \$1}")
DAEMON_SHA=$(sha256sum daemon.deb | awk "{print \$1}")
echo "version='"$VERSION"' full=$FULL_SHA daemon=$DAEMON_SHA"
python3 - <<PYEOF
import re
with open("/tmp/PKGBUILD") as f:
content = f.read()
content = re.sub(r"^pkgver=.*", "pkgver='"$VERSION"'", content, count=1, flags=re.MULTILINE)
new_shas = """sha256sums=(
'"'"'$FULL_SHA'"'"'
'"'"'$DAEMON_SHA'"'"'
'"'"'SKIP'"'"'
)"""
content = re.sub(r"sha256sums=\(.*?\)", new_shas, content, count=1, flags=re.DOTALL)
with open("/tmp/PKGBUILD", "w") as f:
f.write(content)
PYEOF
echo "--- updated PKGBUILD (pkgver + sha256sums) ---"
grep -E "^(pkgver|sha256sums)" /tmp/PKGBUILD
'
- name: Generate .SRCINFO via makepkg
if: env.AUR_SSH_KEY != ''
run: |
cp /tmp/full.deb "/tmp/cryptographic-triangles_${VERSION}_amd64.deb"
cp /tmp/daemon.deb "/tmp/cryptographic-triangles-daemon_${VERSION}_amd64.deb"
chown build:build /tmp/PKGBUILD /tmp/cryptographic-triangles-*.deb
sudo -u build bash -c '
cd /tmp
makepkg --printsrcinfo > .SRCINFO
echo "--- generated .SRCINFO ---"
cat .SRCINFO
'
- name: Setup SSH key for AUR
if: env.AUR_SSH_KEY != ''
run: |
mkdir -p /home/build/.ssh
printf '%s\n' "$AUR_SSH_KEY" > /home/build/.ssh/aur_key
chmod 600 /home/build/.ssh/aur_key
ssh-keyscan -t ed25519 aur.archlinux.org > /home/build/.ssh/known_hosts 2>/dev/null
chown -R build:build /home/build/.ssh
- name: Clone AUR repo
if: env.AUR_SSH_KEY != ''
run: |
sudo -u build bash -c '
cd /tmp
GIT_SSH_COMMAND="ssh -i ~/.ssh/aur_key -o IdentitiesOnly=yes" \
git clone ssh://aur@aur.archlinux.org/triangles-qt-bin.git
ls -la /tmp/triangles-qt-bin
'
- name: Stage updated files
if: env.AUR_SSH_KEY != ''
run: |
cp /tmp/PKGBUILD /tmp/triangles-qt-bin/PKGBUILD
cp /tmp/.SRCINFO /tmp/triangles-qt-bin/.SRCINFO
cp "$GITHUB_WORKSPACE/packaging/aur/triangles-qt.desktop" /tmp/triangles-qt-bin/triangles-qt.desktop
chown -R build:build /tmp/triangles-qt-bin
sudo -u build bash -c '
cd /tmp/triangles-qt-bin
git --no-pager diff --stat
'
- name: Commit and push to AUR
if: env.AUR_SSH_KEY != ''
run: |
sudo -u build bash -c '
cd /tmp/triangles-qt-bin
git config user.name "Sami Ahmed"
git config user.email "SamiAhmed7777@users.noreply.github.com"
git add PKGBUILD .SRCINFO triangles-qt.desktop
if git diff --cached --quiet; then
echo "No changes to commit (AUR already at this version)"
exit 0
fi
git commit -m "triangles-qt-bin '"$VERSION"'-1"
GIT_SSH_COMMAND="ssh -i ~/.ssh/aur_key -o IdentitiesOnly=yes" \
git push origin master
'
- name: ✓ Summary
if: always()
run: |
if [ -z "$AUR_SSH_KEY" ]; then
echo "::notice::AUR job was skipped because AUR_SSH_KEY is not set."
else
echo "::notice::AUR distribution completed."
fi
homebrew:
name: Homebrew tap (SamiAhmed7777/homebrew-triangles)
needs: version
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-22.04
env:
HOMEBREW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
VERSION: ${{ needs.version.outputs.version }}
steps:
- name: Check HOMEBREW_GITHUB_TOKEN
run: |
if [ -z "$HOMEBREW_GITHUB_TOKEN" ]; then
echo "::warning::HOMEBREW_GITHUB_TOKEN secret not set — skipping Homebrew push. Add it at Settings → Secrets → Actions."
echo "::warning::Use a GitHub PAT with 'repo' scope for SamiAhmed7777/homebrew-triangles."
fi
- name: Wait for release artifacts
if: env.HOMEBREW_GITHUB_TOKEN != ''
run: |
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/90)"
sleep 20
done
echo "::error::Release v${VERSION} macOS .dmg never became available after 30 minutes"
exit 1
- name: Compute macOS .dmg SHA256
if: env.HOMEBREW_GITHUB_TOKEN != ''
id: sha
run: |
curl -fsSL -o /tmp/triangles.dmg \
"https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/Cryptographic-Triangles-v${VERSION}-macos-arm64.dmg"
SHA=$(sha256sum /tmp/triangles.dmg | awk '{print $1}')
echo "sha=$SHA" >> $GITHUB_OUTPUT
echo "macOS .dmg SHA256: $SHA"
- name: Clone homebrew-triangles
if: env.HOMEBREW_GITHUB_TOKEN != ''
run: |
git clone https://x-access-token:$HOMEBREW_GITHUB_TOKEN@github.com/SamiAhmed7777/homebrew-triangles.git /tmp/homebrew-triangles
cd /tmp/homebrew-triangles
git --no-pager log --oneline | head -3
- name: Update Formula and Cask
if: env.HOMEBREW_GITHUB_TOKEN != ''
env:
VERSION: ${{ needs.version.outputs.version }}
SHA: ${{ steps.sha.outputs.sha }}
run: |
cd /tmp/homebrew-triangles
# Update Casks/cryptographic-triangles.rb
python3 - <<PYEOF
import re
for path, old_v_pat, old_sha_pat in [
('Casks/cryptographic-triangles.rb', r'^\s*version\s+"[\d.]+"', r'^\s*sha256\s+"[a-f0-9]+"'),
('Formula/triangles.rb', r'^\s*version\s+"[\d.]+"', r'^\s*sha256\s+"[a-f0-9]+"'),
]:
with open(path) as f: content = f.read()
content = re.sub(old_v_pat, f' version "$VERSION"', content, count=1, flags=re.MULTILINE)
content = re.sub(old_sha_pat, f' sha256 "$SHA"', content, count=1, flags=re.MULTILINE)
with open(path, 'w') as f: f.write(content)
PYEOF
cat Formula/triangles.rb | head -5
echo "---"
cat Casks/cryptographic-triangles.rb | head -5
git --no-pager diff --stat
- name: Commit and push
if: env.HOMEBREW_GITHUB_TOKEN != ''
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
cd /tmp/homebrew-triangles
git config user.name "Sami Ahmed"
git config user.email "SamiAhmed7777@users.noreply.github.com"
git add Formula/triangles.rb Casks/cryptographic-triangles.rb
if git diff --cached --quiet; then
echo "No changes to commit (Homebrew tap already at this version)"
exit 0
fi
git commit -m "triangles ${VERSION}"
git push origin main
- name: ✓ Summary
if: always()
run: |
if [ -z "$HOMEBREW_GITHUB_TOKEN" ]; then
echo "::notice::Homebrew job was skipped because HOMEBREW_GITHUB_TOKEN is not set."
else
echo "::notice::Homebrew distribution completed."
fi
chocolatey:
name: Chocolatey (triangles)
needs: version
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
runs-on: windows-latest
env:
CHOCO_API_KEY: ${{ secrets.CHOCO_API_KEY }}
VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Check CHOCO_API_KEY + CHOCO_SKIP_WACATAC
shell: bash
run: |
if [ -z "$CHOCO_API_KEY" ]; then
echo "::warning::CHOCO_API_KEY not set — skipping Chocolatey push."
fi
if [ "$CHOCO_SKIP_WACATAC" != "" ]; then
echo "::warning::CHOCO_SKIP_WACATAC=$CHOCO_SKIP_WACATAC — skipping Chocolatey push (Wacatac still active)."
fi
- name: Wait for release artifacts
if: env.CHOCO_API_KEY != '' && env.CHOCO_SKIP_WACATAC != ''
shell: bash
run: |
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/90)"
sleep 20
done
echo "::error::Release v${VERSION} Windows installer never became available after 30 minutes"
exit 1
- name: Compute installer SHA256
if: env.CHOCO_API_KEY != '' && env.CHOCO_SKIP_WACATAC != ''
shell: bash
id: sha
run: |
curl -fsSL -o /tmp/triangles-setup.exe \
"https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/Cryptographic-Triangles-${VERSION}-win-x64-setup.exe"
SHA=$(sha256sum /tmp/triangles-setup.exe | awk '{print $1}')
echo "sha=$SHA" >> $GITHUB_OUTPUT
echo "Chocolatey installer SHA256: $SHA"
- name: Update nuspec version
if: env.CHOCO_API_KEY != '' && env.CHOCO_SKIP_WACATAC != ''
shell: bash
working-directory: ${{ github.workspace }}/packaging/chocolatey
run: |
python3 -c "
import re
with open('triangles.nuspec') as f: c = f.read()
c = re.sub(r'<version>[\d.]+</version>', f'<version>${VERSION}</version>', c)
with open('triangles.nuspec', 'w') as f: f.write(c)
print('updated nuspec version to', '${VERSION}')
"
grep -E "<version>|<id>" triangles.nuspec
- name: Update nuspec version + install script SHA
if: env.CHOCO_API_KEY != '' && env.CHOCO_SKIP_WACATAC != ''
shell: bash
working-directory: ${{ github.workspace }}/packaging/chocolatey
run: |
python3 -c "
import re
with open('triangles.nuspec') as f: c = f.read()
c = re.sub(r'<version>[\d.]+</version>', f'<version>${VERSION}</version>', c)
with open('triangles.nuspec', 'w') as f: f.write(c)
with open('tools/chocolateyInstall.ps1') as f: c = f.read()
c = c.replace('__CHECKSUM_PLACEHOLDER__', '${{ steps.sha.outputs.sha }}')
with open('tools/chocolateyInstall.ps1', 'w') as f: f.write(c)
print('updated nuspec version + install script checksum')
"
grep -E "<version>|<id>" triangles.nuspec
grep checksum64 tools/chocolateyInstall.ps1
- name: Pack Chocolatey package
if: env.CHOCO_API_KEY != '' && env.CHOCO_SKIP_WACATAC != ''
shell: pwsh
working-directory: ${{ github.workspace }}/packaging/chocolatey
run: |
choco pack
Get-ChildItem *.nupkg
- name: Push to Chocolatey
if: env.CHOCO_API_KEY != '' && env.CHOCO_SKIP_WACATAC != ''
shell: pwsh
working-directory: ${{ github.workspace }}/packaging/chocolatey
run: |
$apiKey = [System.Environment]::GetEnvironmentVariable('CHOCO_API_KEY', 'Process')
choco apikey add --key="$apiKey" --source='https://push.chocolatey.org/'
Get-ChildItem *.nupkg | ForEach-Object {
Write-Host "Pushing $($_.Name)..."
choco push $_.Name --source='https://push.chocolatey.org/'
}
- name: ✓ Summary
if: always()
shell: bash
run: |
if [ -z "$CHOCO_API_KEY" ]; then
echo "::notice::Chocolatey job skipped (CHOCO_API_KEY not set)."
elif [ -n "$CHOCO_SKIP_WACATAC" ]; then
echo "::notice::Chocolatey job skipped (Wacatac detection still active). Set CHOCO_SKIP_WACATAC='' and re-run after Microsoft clears the false-positive."
else
echo "::notice::Chocolatey push completed (subject to moderator review)."
fi
winget:
name: WinGet (CryptographicTriangles.TrianglesQt)
needs: version
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-22.04
env:
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Check WINGET_TOKEN
run: |
if [ -z "$WINGET_TOKEN" ]; then
echo "::warning::WINGET_TOKEN not set — skipping WinGet PR. Add a GitHub PAT with 'public_repo' scope at Settings → Secrets → Actions."
fi
- name: Wait for release artifacts
if: env.WINGET_TOKEN != ''
run: |
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/90)"
sleep 20
done
echo "::error::Release v${VERSION} Windows installer never became available after 30 minutes"
exit 1
- name: Compute installer SHA256
if: env.WINGET_TOKEN != ''
id: sha
run: |
curl -fsSL -o /tmp/triangles-setup.exe \
"https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/Cryptographic-Triangles-${VERSION}-win-x64-setup.exe"
SHA=$(sha256sum /tmp/triangles-setup.exe | awk '{print $1}')
echo "sha=$SHA" >> $GITHUB_OUTPUT
echo "WinGet installer SHA256: $SHA"
- name: "Pre-flight check for existing failed WinGet PRs"
if: env.WINGET_TOKEN != ''
env:
GH_TOKEN: ${{ secrets.WINGET_TOKEN }}
run: |
set -e
# Don't pile up PRs if previous ones still have author-action-needed flags.
# winget-pkgs moderators can read repeated unfixed failures as spam.
# Skip the PR for this release if any existing SamiAhmed7777 PR against
# microsoft/winget-pkgs has a blocker label.
echo "Checking existing open PRs from SamiAhmed7777 on microsoft/winget-pkgs..."
BLOCKING=$(gh api -X GET \
'repos/microsoft/winget-pkgs/issues?state=open&labels=PullRequest-Error,Needs-Author-Feedback&per_page=30' \
--jq '.[] | select(.user.login=="SamiAhmed7777") | "#\(.number) [\(.state)] \(.title)"' \
|| echo "")
if [ -n "$BLOCKING" ]; then
echo "::error::Existing WinGet PR(s) with blocker labels — fix or close those first:"
echo "$BLOCKING"
echo "::error::Aborting this WinGet submission to avoid piling up failed PRs."
exit 1
fi
echo "✓ No blocker-labelled PRs found — safe to submit."
- name: Fork + update WinGet manifest + open PR
if: env.WINGET_TOKEN != ''
env:
GH_TOKEN: ${{ secrets.WINGET_TOKEN }}
SHA: ${{ steps.sha.outputs.sha }}
PUBLISHER_INITIAL: c
PACKAGE_ID: CryptographicTriangles.TrianglesQt
PACKAGE_SHORT: TrianglesQt
INSTALLER_URL: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${{ env.VERSION }}/Cryptographic-Triangles-${{ env.VERSION }}-win-x64-setup.exe
run: |
set -e
# Install gh + jq if missing
which gh >/dev/null 2>&1 || (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null && sudo apt update && sudo apt install -y gh jq)
# Skip if a PR for THIS version already exists (avoid duplicate submissions).
echo "Checking for existing PR for version ${VERSION}..."
if gh api 'repos/microsoft/winget-pkgs/pulls?state=open&per_page=30' \
--jq ".[] | select(.head.ref | startswith(\"triangles-${VERSION}-\")) | .number" \
| grep -q .; then
echo "::notice::PR for v${VERSION} already exists — skipping to avoid duplicate."
exit 0
fi
echo "✓ No existing PR for v${VERSION}."
VERSION="$VERSION"
# Path convention (winget-pkgs): lowercase first letter of publisher,
# then publisher folder (PascalCase), then short package folder name.
# Example: manifests/c/CryptographicTriangles/TrianglesQt/5.9.20/
MANIFEST_DIR="manifests/$PUBLISHER_INITIAL/CryptographicTriangles/$PACKAGE_SHORT/$VERSION"
# TrianglesQt is built with NSIS (Nullsoft). Standard silent flag is /S.
# If the installer tech ever changes, update InstallerSwitches here.
NSIS_SILENT="/S"
# 1. Clone the winget-pkgs repo (Sami's fork) — auto-create fork if needed
echo "Forking microsoft/winget-pkgs..."
GH_REPO="SamiAhmed7777/winget-pkgs"
if ! gh repo view "$GH_REPO" >/dev/null 2>&1; then
gh repo fork microsoft/winget-pkgs --remote=false || true
fi
rm -rf winget-pkgs
git clone --depth 1 "https://x-access-token:${WINGET_TOKEN}@github.com/${GH_REPO}.git" winget-pkgs
cd winget-pkgs
git config user.name "Sami Ahmed"
git config user.email "SamiAhmed7777@users.noreply.github.com"
BRANCH="triangles-${VERSION}-${{ github.run_number }}"
git checkout -b "$BRANCH"
mkdir -p "$MANIFEST_DIR"
# 2. Generate the three manifest files (winget-pkgs schema 1.12.0)
#
# Schema rules (see doc/manifest/schema/1.12.0/*.md and
# doc/ValidationFailureGuide.md):
# - version file: PackageIdentifier, PackageVersion, DefaultLocale
# (NOT PackageLocale — that's the old field name), ManifestType
# "version", ManifestVersion "1.12.0"
# - defaultLocale file: Publisher, PackageName, License,
# ShortDescription are REQUIRED (no Publisher in version file)
# - installer file: InstallModes array (not "InstallerMode:
# interactive" — that's the old field name); ManifestVersion 1.12.0
# - All files: include # yaml-language-server: $schema=... comment
# for editor + validator support
SCHEMA_BASE="https://raw.githubusercontent.com/microsoft/winget-cli/master/schemas/JSON/manifests/v1.12.0"
cat > "$MANIFEST_DIR/${PACKAGE_ID}.yaml" <<EOF
# yaml-language-server: \$schema=https://aka.ms/winget-manifest.version.1.12.0.schema.json
PackageIdentifier: ${PACKAGE_ID}
PackageVersion: ${VERSION}
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.12.0
EOF
cat > "$MANIFEST_DIR/${PACKAGE_ID}.locale.en-US.yaml" <<EOF
# yaml-language-server: \$schema=https://aka.ms/winget-manifest.defaultLocale.1.12.0.schema.json
PackageIdentifier: ${PACKAGE_ID}
PackageVersion: ${VERSION}
PackageLocale: en-US
Publisher: Cryptographic Triangles
PublisherUrl: https://cryptographic-triangles.org
PackageName: Cryptographic Triangles Qt Wallet
License: MIT
ShortDescription: Privacy-focused cryptocurrency wallet with PoS staking, Tor v3, and encrypted messaging.
Description: |-
Cryptographic Triangles (TRI) is a privacy-focused cryptocurrency
featuring Proof-of-Stake consensus with 33% annual staking rewards,
Tor v3 onion routing, and built-in encrypted peer-to-peer messaging.
Originally launched in July 2014, featuring the unique Hash9 algorithm
(13-step hash cascade).
ManifestType: defaultLocale
ManifestVersion: 1.12.0
EOF
cat > "$MANIFEST_DIR/${PACKAGE_ID}.installer.yaml" <<EOF
# yaml-language-server: \$schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json
PackageIdentifier: ${PACKAGE_ID}
PackageVersion: ${VERSION}
InstallModes:
- interactive
- silent
InstallerSwitches:
Silent: /S
SilentWithProgress: /S
Installers:
- Architecture: x64
InstallerType: exe
InstallerUrl: ${INSTALLER_URL}
InstallerSha256: ${SHA}
ManifestType: installer
ManifestVersion: 1.12.0
EOF
git add "$MANIFEST_DIR"
git commit -m "${PACKAGE_ID} version ${VERSION}"
git push origin "$BRANCH"
# 3. Open PR
gh pr create \
--repo microsoft/winget-pkgs \
--head "SamiAhmed7777:${BRANCH}" \
--base master \
--title "${PACKAGE_ID} version ${VERSION}" \
--body "Automated update of ${PACKAGE_ID} to v${VERSION}. Artifacts at ${INSTALLER_URL} (SHA256: ${SHA})."
echo "✓ PR opened"
- name: ✓ Summary
if: always()
run: |
if [ -z "$WINGET_TOKEN" ]; then
echo "::notice::WinGet job skipped (WINGET_TOKEN not set)."
else
echo "::notice::WinGet PR opened."
fi
+17 -56
View File
@@ -26,20 +26,12 @@ jobs:
- name: Check format on changed lines
run: |
# 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"
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)
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
# 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)
if [ -z "$OUTPUT" ] || [ "$OUTPUT" = "no modified files to format" ] || [ "$OUTPUT" = "clang-format did not modify any files" ]; then
echo "clang-format: clean"
@@ -57,24 +49,15 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Install dependencies + clang-tidy
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build clang-tidy-15 \
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev
librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
sudo ln -sf /usr/bin/clang-tidy-15 /usr/local/bin/clang-tidy
- name: Build RocksDB from source
# Ubuntu 22.04's librocksdb-dev is 6.11.4 which CMakeLists.txt now
# refuses to configure against (need >= 7.4 for XXH3 per-block
# checksum). Build 8.9.1 from source — same version DNS2 ships —
# into /usr/local so CMake's find_library picks it up first.
run: sudo bash scripts/ci/build-rocksdb.sh
- name: Configure (export compile_commands.json)
run: |
cmake -B build -G Ninja \
@@ -91,6 +74,9 @@ 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
@@ -98,42 +84,17 @@ 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).
cat /tmp/changes.diff | python3 "$DIFF_SCRIPT" -p1 -path build \
-iregex '.*\.(cpp|cc|h|hpp)$' \
-j$(nproc) || EXIT=$?
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=$?
# Warn-only initially. Flip this to `exit ${EXIT:-0}` once we're clean.
exit 0
@@ -1,104 +0,0 @@
# trigger-tridock-rebuild.yml
#
# Triangles v5.9.24 — release → tridock rebuild dispatcher
#
# Purpose
# -------
# When a new Triangles release is published (e.g. v5.9.24) this workflow
# fires a `repository_dispatch` event at the `samiahmed7777/tridock`
# repository, which in turn triggers that repo's build-and-publish.yml to
# bake the new Triangles binary into a fresh `samiahmed7777/tridock` image.
#
# Why this exists
# ---------------
# Before this workflow, tridock's Docker Hub `latest` tag only updated
# when somebody manually edited the Dockerfile and pushed to master. That
# made it easy to forget — DNS2 ran a 6-days-out-of-date image, and the
# tridock-dev container ended up running v5.9.9 while DNS2 prod ran v5.9.23.
# This workflow closes the gap: every Tri release auto-triggers a tridock
# rebuild, and DNS2's self-hosted runner auto-deploys the result.
#
# Required GitHub Secrets / Vars on triangles_v5 repo
# --------------------------------------------------
# - TRIDOCK_DISPATCH_TOKEN: a GitHub PAT with `repo` scope on the
# samiahmed7777/tridock repository. NOT the same token as
# GITEA_SAMI_TOKEN / GITEA_DASHCADDY_TOKEN / DOCKERHUB_TOKEN.
name: Trigger tridock rebuild on Tri release
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Override version (e.g. 5.9.24). Leave blank to use the published release tag.'
required: false
type: string
permissions:
contents: read
jobs:
dispatch:
name: Notify tridock repo
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Resolve version
id: version
run: |
# On release:published, github.event.release.tag_name is like "v5.9.24"
# Strip the leading "v" so the dispatched payload uses "5.9.24"
if [ "${{ github.event_name }}" = "release" ]; then
TAG="${{ github.event.release.tag_name }}"
VERSION="${TAG#v}"
else
VERSION="${{ inputs.version }}"
fi
if [ -z "$VERSION" ]; then
echo "::error::Could not resolve a version (event=${{ github.event_name }}, tag=${{ github.event.release.tag_name }})"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Dispatching tridock rebuild for Triangles v$VERSION"
- name: Dispatch to samiahmed7777/tridock
run: |
curl -fsSL --max-time 30 \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.TRIDOCK_DISPATCH_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-X POST \
https://api.github.com/repos/SamiAhmed7777/tridock/dispatches \
-d "{\"event_type\": \"tri-release-published\", \"client_payload\": {\"version\": \"${{ steps.version.outputs.version }}\", \"source_repo\": \"SamiAhmed7777/triangles_v5\", \"source_sha\": \"${{ github.sha }}\"}}"
# Verify the dispatch landed
RC=$?
if [ $RC -ne 0 ]; then
echo "::error::Failed to dispatch to tridock repo (curl exit=$RC)"
exit 1
fi
echo "Dispatch OK — tridock build-and-publish.yml will pick this up."
- name: Send Telegram alert
if: always()
continue-on-error: true
env:
TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TG_CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
if [ -z "$TG_TOKEN" ] || [ -z "$TG_CHAT" ]; then
echo "Telegram secrets not set — skipping alert"
exit 0
fi
STATUS="${{ job.status }}"
VERSION="${{ steps.version.outputs.version }}"
MSG="Tri release v$VERSION → tridock dispatch: $STATUS"
curl -fsSL --max-time 10 \
"https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
-d "chat_id=${TG_CHAT}" \
-d "text=${MSG}" \
-d "parse_mode=HTML" \
> /dev/null || echo "Telegram send failed (non-fatal)"
-69
View File
@@ -1,69 +0,0 @@
name: WinGet PR watchdog
# Catches failing WinGet submissions within an hour of opening them.
# Goal: don't leave "needs-author-feedback" or "PullRequest-Error" PRs
# sitting open for days — moderators read sustained unfixed PRs as spam.
#
# Behaviour:
# - Every 30 min, scan open SamiAhmed7777 PRs against microsoft/winget-pkgs
# - For each one, look at recent wingetbot comments to detect validation result
# - If validation FAILED, post a comment summarising the error, close the PR,
# and surface the failure on the workflow summary so it's easy to spot.
on:
schedule:
- cron: '*/30 * * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
watchdog:
name: Scan + auto-close failed WinGet PRs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install gh CLI
run: |
which gh >/dev/null 2>&1 || (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null && sudo apt update && sudo apt install -y gh jq)
- name: Scan + auto-close
env:
GH_TOKEN: ${{ secrets.WINGET_TOKEN }}
run: |
set -e
if [ -z "$GH_TOKEN" ]; then
echo "::warning::WINGET_TOKEN not set — watchdog can scan but cannot close PRs."
fi
echo "Fetching open SamiAhmed7777 PRs against microsoft/winget-pkgs..."
PRS=$(gh api 'repos/microsoft/winget-pkgs/pulls?state=open&per_page=30' --jq '.[] | select(.user.login=="SamiAhmed7777") | "\(.number)|\(.head.ref)|\(.title)|\(.created_at)"')
if [ -z "$PRS" ]; then
echo "OK no open SamiAhmed7777 PRs."
exit 0
fi
echo "$PRS" | while IFS='|' read -r NUM BRANCH TITLE CREATED; do
echo ""
echo "--- PR #$NUM: $TITLE (branch $BRANCH, created $CREATED) ---"
LAST_VALIDATION=$(gh api "repos/microsoft/winget-pkgs/issues/$NUM/comments?per_page=20" --jq '[.[] | select(.user.login=="wingetbot" or .user.login=="stephengillie") | select(.body | test("Result: Failed|Invalid file|Automatic Validation ended"))] | first')
if [ -n "$LAST_VALIDATION" ]; then
echo " X Validation FAILED detected."
SUMMARY=$(echo "$LAST_VALIDATION" | jq -r '.body' | head -40)
echo " Summary:"
echo "$SUMMARY" | sed 's/^/ /'
if [ -n "$GH_TOKEN" ]; then
printf 'Auto-closing: automatic validation failed within the watchdog window.\n\n```\n%s\n```\n\nThe watchdog (winget-watchdog.yml) closed this PR so it does not sit in the moderator queue with a needs-author-feedback flag. Reopen after fixing the issue, or open a fresh PR for a known-good version.\n' "$SUMMARY" > /tmp/watchdog-comment.txt
gh api -X POST "repos/microsoft/winget-pkgs/issues/$NUM/comments" -f body=@/tmp/watchdog-comment.txt || echo " (comment failed, continuing)"
gh api -X PATCH "repos/microsoft/winget-pkgs/pulls/$NUM" -f state=closed || echo " (close failed, continuing)"
echo " OK Closed PR #$NUM"
echo "::warning::Closed failing PR #$NUM -- $TITLE"
else
echo " (no WINGET_TOKEN, skipping close)"
fi
elif gh api "repos/microsoft/winget-pkgs/issues/$NUM/comments?per_page=20" --jq '[.[] | select(.user.login=="wingetbot") | select(.body | test("Validation Pipeline Run"))] | first' | grep -q .; then
echo " ? Validation has been triggered but no failure detected yet — leaving PR open."
else
echo " ? No validation result yet — leaving PR open."
fi
done
+2 -24
View File
@@ -49,6 +49,7 @@ blocks/
# IDE
.vscode/
.idea/
.claude/
*.swp
*.swo
*~
@@ -67,6 +68,7 @@ triangles.conf
*.key
*.cert
*.gpg
*.o
src/trianglesd
src/obj/
build-bench/
@@ -76,27 +78,3 @@ build-latest/
build-rocks-probe/
build-rocksdb/
bench-results.csv
# Local build dirs (krystie)
/build-*/
/build/
/bench-results.csv
/build-rocks-probe/
/build-rocksdb/
/build-cmake/
/build-cmake-test/
/build-latest/
/build-bench/
/.qmake.stash
# MinGW cross-compilation deps (local build environment)
/deps-mingw/
# Snapshot files
*.utx
# Merge artifacts
*.orig
# Dev patches
*.patch
-3
View File
@@ -4,6 +4,3 @@
[submodule "src/secp256k1"]
path = src/secp256k1
url = https://github.com/bitcoin-core/secp256k1
[submodule "src/i2p/i2pd-src"]
path = src/i2p/i2pd-src
url = https://github.com/PurpleI2P/i2pd.git
+5
View File
@@ -0,0 +1,5 @@
Sami Ahmed <tweet@sami-ahmed.net> Sami <sami@dashcaddy.net>
Sami Ahmed <tweet@sami-ahmed.net> SamiAhmed7777 <sami@users.noreply.github.com>
Sami Ahmed <tweet@sami-ahmed.net> SamiAhmed7777 <79177212+SamiAhmed7777@users.noreply.github.com>
Sami Ahmed <tweet@sami-ahmed.net> Sami Ahmed <hello@sami-ahmed.net>
Sami Ahmed <tweet@sami-ahmed.net> Sami Ahmed <samiahmed7777@gmail.com>
-91
View File
@@ -1,91 +0,0 @@
# 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
View File
@@ -1,284 +0,0 @@
# 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`)
- 13 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
+6 -147
View File
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
endif()
project(Triangles
VERSION 6.2.5
VERSION 6.0.1
DESCRIPTION "Cryptographic Triangles Wallet"
LANGUAGES C CXX
)
@@ -37,41 +37,6 @@ 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")
@@ -82,37 +47,17 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# ── User-facing options ──
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" OFF)
option(USE_UPNP "Enable UPnP support via miniupnpc" ON)
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)" OFF)
option(USE_DBUS "Enable D-Bus notifications (Linux only)" ON)
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
# (5+ days on a parallel chain because someone flipped -notor=1 for
# troubleshooting and never reverted it) motivated this. We keep the option
# for legacy recovery workflows, but default it ON and abort the build if
# anyone explicitly disables it.
option(USE_TOR_EMBEDDED "Enable embedded Tor library linking" ON)
if(DEFINED USE_TOR_EMBEDDED AND NOT USE_TOR_EMBEDDED)
message(FATAL_ERROR
"USE_TOR_EMBEDDED=OFF is not supported. Triangles is Tor-native. "
"If you need clearnet mode for bootstrap recovery, build with "
"USE_TOR_EMBEDDED=ON and pass -notor=1 -recovery-mode=1 at runtime "
"instead.")
endif()
option(USE_TOR_EMBEDDED "Enable embedded Tor library linking" OFF)
option(USE_O3 "Use -O3 optimization instead of -O2" OFF)
option(ENABLE_PIE "Build position-independent executables" ON)
option(ENABLE_PIE "Build position-independent executables" OFF)
option(ENABLE_STATIC "Prefer static linking (Linux release builds)" OFF)
# Embedded I2P (i2pd) — runs an I2P router in-process alongside Tor.
# When enabled, Triangles supports dual-network anonymity: Tor (.onion) +
# I2P (.b32.i2p). Disabled by default until seed nodes are deployed.
option(USE_I2P_EMBEDDED "Enable embedded I2P (i2pd) library linking" OFF)
set(I2P_SOURCE_ROOT "" CACHE PATH "Path to i2pd source tree (for USE_I2P_EMBEDDED)")
# Cache variables for custom dependency paths
set(BDB_INCLUDE_PATH "" CACHE PATH "Path to Berkeley DB headers")
set(BDB_LIB_PATH "" CACHE PATH "Path to Berkeley DB libraries")
@@ -129,7 +74,6 @@ include(AddCompilerFlags)
find_package(OpenSSL REQUIRED)
find_package(Boost 1.71 REQUIRED COMPONENTS
program_options thread chrono
OPTIONAL_COMPONENTS filesystem system
)
if(BUILD_TESTS)
find_package(Boost REQUIRED COMPONENTS unit_test_framework)
@@ -189,78 +133,6 @@ if(NOT TARGET RocksDB::rocksdb AND NOT TARGET PkgConfig::RocksDB)
message(STATUS "Found RocksDB (manual probe): ${ROCKSDB_LIBRARY}")
endif()
# Modernization: SQLite3 for the new wallet DB backend.
find_package(SQLite3 REQUIRED)
# Triangles uses RocksDB features that only exist in 7.4+ (XXH3 per-block
# checksum, type 4). Building against an older RocksDB produces a binary
# whose smsgDB Open() fails on any SST file written by RocksDB 7.4+ —
# instead of just bailing, src/smessage.cpp::SecMsgDB::Open now
# quarantines the offending file and recovers. We still fail loudly at
# configure time so this drift doesn't sneak back in unnoticed.
# rocksdb/version.h ships with every RocksDB release (3.x onward) and
# defines ROCKSDB_MAJOR / ROCKSDB_MINOR / ROCKSDB_PATCH. If neither
# find_package nor pkg-config exposed RocksDB_VERSION (e.g. Ubuntu 22.04's
# librocksdb-dev, which ships no CMake config and no .pc file), we can
# still recover the version directly from the header. This closes the
# "manual probe silently allows old RocksDB" gap that let v5.9.24 ship
# linked to librocksdb 6.11.
function(_tri_detect_rocksdb_version_from_header)
if(RocksDB_VERSION)
return()
endif()
foreach(_dir ${ARGN})
if(NOT IS_DIRECTORY "${_dir}")
continue()
endif()
set(_vh "${_dir}/rocksdb/version.h")
if(EXISTS "${_vh}")
file(STRINGS "${_vh}" _maj REGEX "^#define ROCKSDB_MAJOR ")
file(STRINGS "${_vh}" _min REGEX "^#define ROCKSDB_MINOR ")
file(STRINGS "${_vh}" _pat REGEX "^#define ROCKSDB_PATCH ")
if(_maj AND _min AND _pat)
string(REGEX MATCH "[0-9]+" _maj "${_maj}")
string(REGEX MATCH "[0-9]+" _min "${_min}")
string(REGEX MATCH "[0-9]+" _pat "${_pat}")
set(RocksDB_VERSION "${_maj}.${_min}.${_pat}")
set(RocksDB_VERSION "${_maj}.${_min}.${_pat}" PARENT_SCOPE)
message(STATUS "Detected RocksDB version from version.h: ${RocksDB_VERSION}")
return()
endif()
endif()
endforeach()
endfunction()
if(NOT RocksDB_VERSION AND TARGET RocksDB::rocksdb)
get_target_property(_rocksdb_inc RocksDB::rocksdb INTERFACE_INCLUDE_DIRECTORIES)
if(_rocksdb_inc)
_tri_detect_rocksdb_version_from_header(${_rocksdb_inc})
endif()
endif()
if(NOT RocksDB_VERSION AND ROCKSDB_INCLUDE_DIR)
_tri_detect_rocksdb_version_from_header(${ROCKSDB_INCLUDE_DIR})
endif()
if(RocksDB_VERSION AND RocksDB_VERSION VERSION_LESS "7.4.0")
message(FATAL_ERROR
"Triangles requires RocksDB >= 7.4.0 (got ${RocksDB_VERSION}). "
"Older versions cannot read smsgDB files written by RocksDB 7.4+ "
"(XXH3 per-block checksum). "
"On Debian/Ubuntu: install librocksdb-dev >= 7.4 from a backports "
"repo or build RocksDB from source into /usr/local.")
elseif(NOT RocksDB_VERSION)
# No version detectable: headers missing entirely, or ROCKSDB_INCLUDE_DIR
# not pointing at one with rocksdb/version.h. Runtime fallback in
# SecMsgDB::Open covers the gap; print WARNING so build logs flag it.
message(WARNING
"Could not determine RocksDB version (no CMake config, no "
"pkg-config metadata, and no rocksdb/version.h found). "
"Triangles prefers RocksDB >= 7.4.0; older versions are recovered "
"at runtime via SecMsgDB::Open's quarantine fallback.")
endif()
# libsecp256k1 — vendored as a git submodule under src/secp256k1. Provides
# ECDSA signing/verification, pubkey recovery (via the recovery module), and
# ECDH for secure messaging. Configure the submodule's build for our needs:
@@ -286,7 +158,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 Network)
find_package(Qt5 5.9 REQUIRED COMPONENTS Core Gui Widgets)
find_package(Qt5 COMPONENTS LinguistTools QUIET)
if(USE_DBUS AND UNIX AND NOT APPLE)
find_package(Qt5 COMPONENTS DBus QUIET)
@@ -305,17 +177,6 @@ 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)
@@ -324,7 +185,6 @@ message(STATUS "")
message(STATUS "Triangles ${PROJECT_VERSION} build configuration:")
message(STATUS " Build Qt GUI: ${BUILD_QT}")
message(STATUS " Build daemon: ${BUILD_DAEMON}")
message(STATUS " Build CLI: ${BUILD_CLI}")
message(STATUS " Build tests: ${BUILD_TESTS}")
message(STATUS " UPnP: ${USE_UPNP}")
message(STATUS " IPv6: ${USE_IPV6}")
@@ -332,7 +192,6 @@ message(STATUS " QR code: ${USE_QRCODE}")
message(STATUS " D-Bus: ${USE_DBUS}")
message(STATUS " ZMQ: ${USE_ZMQ}")
message(STATUS " Embedded Tor: ${USE_TOR_EMBEDDED}")
message(STATUS " Embedded I2P: ${USE_I2P_EMBEDDED}")
message(STATUS " Static linking: ${ENABLE_STATIC}")
message(STATUS " ccache: ${CCACHE_PROGRAM}")
message(STATUS " Unity build: ${ENABLE_UNITY_BUILD}")
+28 -73
View File
@@ -1,83 +1,38 @@
FROM ubuntu:24.04 AS builder
FROM ubuntu:22.04
ARG DEBIAN_FRONTEND=noninteractive
ARG SOURCE_DATE_EPOCH=1700000000
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
LABEL maintainer="Cryptographic Triangles Team"
LABEL description="Cryptographic Triangles (TRI) headless daemon"
LABEL version="5.7.6"
RUN apt-get update && apt-get install -y --no-install-recommends \
autoconf \
automake \
build-essential \
ca-certificates \
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 \
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 \
&& rm -rf /var/lib/apt/lists/*
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
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
RUN useradd -m -s /bin/bash triangles
USER triangles
WORKDIR /var/lib/triangles
WORKDIR /home/triangles
EXPOSE 24112
VOLUME ["/var/lib/triangles"]
STOPSIGNAL SIGTERM
RUN mkdir -p .triangles
ENTRYPOINT ["/usr/local/bin/trianglesd"]
CMD ["-datadir=/var/lib/triangles", "-printtoconsole", "-upnp=0", "-rest=0", "-rpcbind=127.0.0.1"]
EXPOSE 24112 19112
VOLUME ["/home/triangles/.triangles"]
ENTRYPOINT ["trianglesd"]
CMD ["-printtoconsole", "-txindex=1"]
-237
View File
@@ -1,237 +0,0 @@
# I2P Embedded Architecture (Level 3)
**Date:** 2026-06-27
**Status:** ✅ IMPLEMENTED & WORKING
---
## What This Is
Triangles now runs **two embedded anonymity networks simultaneously**:
1. **Tor** — Every node is a .onion hidden service (existing, unchanged)
2. **I2P** — Every node is a .b32.i2p destination (new)
Both routers run **in-process** as static libraries. No external dependencies, no separate daemons to install.
### What I2P Adds Over Tor-Only
| Property | Tor | I2P |
|----------|-----|-----|
| Routing | Onion (3-hop circuits) | Garlic (variable-hop tunnels) |
| Directory | Centralized authorities | Distributed floodfills |
| Service discovery | Hidden service descriptors | Network database (KadDHT) |
| Designed for | Exit to clearnet | Peer-to-peer services |
| Peer correlation resistance | Moderate | Strong (ephemeral tunnels) |
I2P was designed from the ground up for **peer-to-peer anonymous services** — exactly what a cryptocurrency P2P network needs. Tor's hidden services work, but Tor is optimized for anonymous web browsing (exit traffic). I2P's garlic routing, distributed network database, and short-lived tunnels make it inherently better suited for P2P mesh communication.
---
## Architecture
### Dual-Network Routing
```
┌─────────────────────────────────┐
│ trianglesd (process) │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ libtor │ │ libi2pd │ │
│ │ (Tor) │ │ (I2P) │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
.onion peers ─────┼───────┘ │ │
│ SOCKS 19099 │ │
│ │ │
.b32.i2p peers ───┼──────────────────────┘ │
│ SOCKS 19100 │
└─────────────────────────────────┘
```
### Traffic Flow
| Destination | Route | Proxy |
|-------------|-------|-------|
| `*.onion` | Tor SOCKS5 → Tor circuit → hidden service | 127.0.0.1:19099 |
| `*.b32.i2p` | I2P SOCKS5 → I2P tunnel → destination | 127.0.0.1:19100 |
| Clearnet (IPv4/IPv6) | **BLOCKED** | — |
The routing decision happens in `ConnectSocketByName()` (netbase.cpp):
- `.b32.i2p` suffix → I2P SOCKS proxy (NET_I2P)
- Everything else → Tor name proxy (SetNameProxy)
---
## Implementation
### Files Added
```
src/i2p/
├── i2pd-src/ # PurpleI2P/i2pd git submodule
├── i2p_embedded.h # CI2PEmbedded class declaration
├── i2p_embedded.cpp # Embedded router start/stop logic
├── i2pseed.h # Hardcoded .b32.i2p seed nodes
└── build-libi2pd.sh # Static library build script
```
### Files Modified
| File | Change |
|------|--------|
| `CMakeLists.txt` | `USE_I2P_EMBEDDED` option + config summary |
| `src/CMakeLists.txt` | I2P source, includes, library linking |
| `src/init.cpp` | I2P startup (after Tor), shutdown, CLI flags |
| `src/net.cpp` | Allow `.b32.i2p` in `ConnectNode()` and seed parser |
| `src/netbase.cpp` | I2P SOCKS routing, fixed `.b32.i2p` address parsing |
### CI2PEmbedded Class
Singleton pattern (mirrors `CTorEmbedded`):
```cpp
class CI2PEmbedded {
bool Start(int socksPort, int samPort, int serverPort);
void Stop();
bool IsRunning() const;
std::string GetSocksProxy() const; // "127.0.0.1:19100"
std::string GetI2PAddress() const; // .b32.i2p destination
};
```
### Startup Sequence (init.cpp)
```
1. StartEmbeddedTor() → Tor SOCKS on 19099
2. TOR-NATIVE MODE → all traffic forced through Tor
3. StartEmbeddedI2P() → i2pd SOCKS on 19100
4. I2P-NATIVE MODE → .b32.i2p routed through i2pd
5. Dual-network anonymity → Tor + I2P co-equal
```
If I2P fails to start, the daemon continues in Tor-only mode (non-fatal).
### How i2pd Integrates
i2pd provides a C++ API (`libi2pd/api.h`) for in-process embedding:
```cpp
i2p::api::InitI2P(argc, argv, "triangles-i2pd");
i2p::api::StartI2P(logStream);
i2p::client::context.Start(); // SAM, SOCKS, tunnels
```
The auto-generated `i2pd.conf` enables:
- SOCKS proxy on 19100 (for outbound .b32.i2p)
- SAM bridge on 7656 (for future SAM v3 protocol)
- Server tunnel in `tunnels.conf` (I2P hidden service)
The `tunnels.conf` is written before `Start()`:
```ini
[triangles-p2p]
type = server
host = 127.0.0.1
port = <P2P_PORT>
keys = triangles-p2p-keys.dat
inbound.length = 3
outbound.length = 3
```
This creates a persistent `.b32.i2p` destination that survives restarts.
---
## Build Instructions
### Prerequisites
Same as existing Tor build + Boost (already required).
### Build with I2P
```bash
# 1. Initialize the i2pd submodule
git submodule update --init --recursive src/i2p/i2pd-src
# 2. Build i2pd static libraries
cd src/i2p && bash build-libi2pd.sh
# 3. Configure and build Triangles
mkdir build && cd build
cmake -G Ninja -DUSE_I2P_EMBEDDED=ON ..
ninja trianglesd
```
### Build without I2P (Tor-only, existing behavior)
```bash
cmake -G Ninja .. # USE_I2P_EMBEDDED defaults to OFF
ninja trianglesd
```
---
## CLI Flags
| Flag | Default | Description |
|------|---------|-------------|
| `-i2p` | `1` | Enable embedded I2P router |
| `-i2psocks=<port>` | `19100` | I2P SOCKS proxy port |
| `-i2psam=<port>` | `7656` | I2P SAM bridge port |
| `-i2phsport=<port>` | P2P port | I2P server tunnel forward port |
---
## Testing Verification
### Expected Startup Output
```
Embedded I2P: starting i2pd router...
Embedded I2P: server tunnel configured on port 24112
...
Clients: New private keys file .../triangles-p2p-keys.dat for <b32>.b32.i2p created
Clients: 1 I2P server tunnels created
Embedded I2P: SOCKS proxy at 127.0.0.1:19100, SAM at 127.0.0.1:7656
...
I2P-NATIVE MODE: I2P router running
SOCKS proxy at 127.0.0.1:19100 for .b32.i2p connections
Dual-network anonymity: Tor (.onion) + I2P (.b32.i2p)
```
---
## Seed Node Deployment
To deploy an I2P seed node:
1. Build with `-DUSE_I2P_EMBEDDED=ON`
2. Start the daemon — it auto-generates a `.b32.i2p` destination
3. Read the address from the log: `grep "b32.i2p" debug.log`
4. Add the address to `src/i2p/i2pseed.h`
5. Add the address to `seeds.cryptographic-triangles.org/i2p-seeds.txt`
The destination keys persist in `<datadir>/i2p_data/triangles-p2p-keys.dat`.
---
## Comparison to Other Projects
| Project | Tor | I2P | Embedded | Dual-Network |
|---------|-----|-----|----------|-------------|
| **Triangles** | ✅ Embedded | ✅ Embedded | Both in-process | ✅ |
| Bitcoin Core | Optional | Optional (SAM) | No | No |
| Monero | Optional | No | No | No |
| Kovri (Monero I2P) | N/A | Planned | Planned | No |
Triangles is the only cryptocurrency with **both** Tor and I2P embedded as in-process routers.
---
## Future Work
- **I2P seed nodes:** Deploy stable .b32.i2p seeds (parallel to onion seeds)
- **SAM v3 direct:** Use SAM bridge for native I2P streaming (bypass SOCKS overhead)
- **I2P address in RPC:** Expose `.b32.i2p` address via `getnetworkinfo`
- **Cross-network bridging:** Allow Tor nodes to discover I2P peers and vice versa
+250 -314
View File
@@ -1,314 +1,250 @@
# 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)
# 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)
-132
View File
@@ -1,132 +0,0 @@
# 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
View File
@@ -1,66 +0,0 @@
# 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.
-279
View File
@@ -1,279 +0,0 @@
# Sync Security Audit — 2026-06-21 (Phase 1.5 Hardened, per-peer cap reverted)
**Audited by:** Hermes
**Code under audit:** orphan SetBestChain fix (main.cpp:3177-3201) and network pipeline changes (syncmanager.h, syncmanager.cpp) + Phase 1.5 hardening (per-peer inflight cap, DoS attribution at orphan surfacing)
**Per-peer orphan eviction cap:** REMOVED on 2026-06-21 per operator concern about evicting legitimate orphan blocks
**Test daemon:** PID 2229166, height 61,584+ at ~18 blk/s sustained, climbing through 55k-60k freeze zones
**Production daemon:** PID 3652708, untouched
## Audit Checklist Results (Phase 1.5 Hardened)
### 1. DoS scoring still fires on bad peer data
- **PASS** — main.cpp:4446-4449: `if (block.nDoS) pfrom->Misbehaving(block.nDoS);` runs after every block receive
- **PASS** — main.cpp:3260-3274: **NEW** — Phase 1.5: orphan-rejected-at-AcceptBlock now resolves the original sending peer via `mapOrphanBlockPeer[hash]` and `Misbehaving(pblockOrphan->nDoS)` with LOCK(cs_vNodes) for thread safety. The peer attribution gap is CLOSED.
- **PASS** — main.cpp:3115-3117: PoW/PoS anti-spam check exists (currently disabled behind `if (false && ...)` for sync)
### 2. Per-peer orphan cap exists and is enforced
- **REVERTED 2026-06-21** — main.cpp:3160-3241 (Phase 1.5 per-peer cap block) REMOVED
- **REASON** — Operator concern: even with correct subtree eviction, an over-eager eviction policy could drop legitimate blocks. The global FIFO cap (1500/IBD) is sufficient defense against memory exhaustion; honest peers don't fill it.
- **RETAINED** — main.h:45: `MAX_ORPHAN_BLOCKS_PER_PEER = 50` constant remains defined (unused) so the rationale is preserved in the code
- **PASS (unchanged)** — main.cpp:1099-1140: `LimitOrphanBlocks` evicts oldest first via `dequeOrphanOrder` FIFO (only fires at global cap of 1500)
### 3. Rate-limit by peer, not globally
- **PASS** — syncmanager.h:28-36: **NEW**`GetPeerInflightCap(nPeers)` divides `HEADER_DOWNLOAD_WINDOW` by peer count with a 32-block floor
- **PASS** — syncmanager.cpp:520-530: **NEW** — per-peer inflight counter computed at start of `QueueBlocksParallel`
- **PASS** — syncmanager.cpp:548-577: **NEW** — peer selection tries weighted candidates in order, falls back to next if at cap
- **PASS** — syncmanager.h:38 + syncmanager.cpp:13-25: **NEW**`HeaderNode.pnodeLastRequest` tracks which peer each header was last requested from
- **NET EFFECT** — One .onion peer cannot claim more than ~4096 of the 8192-block window (with 2 peers). Malicious peer's damage is capped.
### 4. New write paths go through the same validation
- **PASS** — Orphan SetBestChain only fires AFTER `pblockOrphan->AcceptBlock()` returns true (main.cpp:3177)
- **PASS** — main.cpp:3079: `pblock->CheckBlock(true, true, !IsInitialBlockDownload())` — full validation when not in IBD
- **PASS** — main.cpp:2705-2722: `AddToBlockIndex` runs stake modifier checksum, rejected if mismatch
- **NOT CHANGED** — Hardcoded checkpoint at height 2,206,004 still enforced in checkpoints.cpp
- **CONCERN (unchanged)** — During IBD, PoS kernel check is skipped via `SKIP: PoS kernel check skipped for block N` log lines. This is correct for the hardcoded checkpoint window.
### 5. Persistent state integrity during reorgs
- **PASS** — main.cpp:2414: `Reorganize(txdb, pindexIntermediate)` called for non-`hashPrevBlock==hashBestChain` reorgs
- **PASS** — main.cpp:2354: `if (!ConnectBlock(...) || !txdb.WriteHashBestChain(hash) || !UpdateAddressIndexSyncState(...))` — atomic write
- **PASS** — main.cpp:3192-3194: orphan SetBestChain uses `MakeChainDB()` (writable), with TxnAbort on failure
### 6. Error path doesn't leak resources
- **PASS** — main.cpp:3146: `LimitOrphanBlocks` runs on every insert
- **PASS** — main.cpp:3276: **NEW** — Phase 1.5: `mapOrphanBlockPeer.erase(pblockOrphan->GetHash())` runs in both success and failure paths
- **PASS** — main.cpp:1145: **NEW** — Phase 1.5: `mapOrphanBlockPeer.erase(evictHash)` added to LimitOrphanBlocks eviction path
- **PASS** — main.cpp:3204-3205: **NEW** — Phase 1.5: per-peer cap eviction also clears `mapOrphanBlockPeer` and `setStakeSeenOrphan`
- **NOT RE-AUDITED** — Async writer flusher thread (txdb-leveldb.cpp) not re-audited in this pass. The flusher thread's error-path safety should be reviewed separately.
### 7. Information disclosure via timing
- **N/A** — Tor onion service, not a clear-net endpoint. Attack model mitigated by Tor design.
- **RESIDUAL** — Block delivery latency to a specific peer is measurable. Mitigation is non-trivial; out of scope.
## Summary (Phase 1.5 — per-peer cap reverted)
| Item | Before Phase 1.5 | After Phase 1.5 (reverted) |
|------|------------------|----------------------------|
| 1. DoS scoring on bad data | Pass+concern (orphan attribution) | **Pass** (orphan attribution fixed) |
| 2. Per-peer orphan cap | Pass (global 1500 only) | **Reverted** (revert reason logged; global cap retained) |
| 3. Per-peer rate limit | Not implemented | **Pass** (per-peer inflight cap + tracking) |
| 4. New writes go through validation | Pass | Pass |
| 5. Reorg safety | Pass | Pass |
| 6. Error path resource leaks | Pass | **Pass** (added peer tracking cleanup) |
| 7. Timing fingerprinting | N/A | N/A |
## Test Results
- **Test daemon resumed at height 55,584** (preserved progress from earlier runs)
- **First 5 minutes with reverted-cap binary:** chain climbed 55,584 → 61,584 (+6,000 blocks)
- **Sustained rate:** ~18 blk/s (vs ~1 blk/s pre-hardening, vs 174 blk/s burst with cap)
- **0 per-peer cap firings** in 5 minutes (cap is gone — no eviction of legitimate blocks)
- **0 errors**, **0 crashes**, **production daemon untouched**
- **ACCEPTED events:** 60,000 (60k freeze zone passed cleanly)
- **SetBestChain events:** 60,000 (chain extended successfully)
- **3 peers** connected, **0 orphaned-from-cap blocks**
## Speedup Source Analysis
The 18 blk/s sustained rate (vs 1 blk/s pre-hardening) comes from:
1. **Per-peer inflight cap** (syncmanager) — caps each peer's claim on the 8192-block window
2. **Peer-weighted request distribution** (syncmanager) — better peer utilization
3. **Network pipeline changes** (syncmanager.h) — HEADER_DOWNLOAD_WINDOW 1024→8192
4. **DoS attribution** (main.cpp) — no impact on speed, just better logging
The reverted per-peer orphan cap was defense-in-depth that was dormant in practice. Its absence has no impact on throughput.
## Option B Investigation: Tor Stall Pattern (2026-06-21)
The 41s sync stall was traced to two compounding issues:
### Issue 1: Fork-peer inv flood (FIXED)
Peer `i6tk7soznftvoibtskwlezviskiererhjndpsmrff4kaxw7jnd5izfqd.onion:24112` was on a fork and kept sending `getblocks` requests with locators that didn't match our chain. The fork-detection code served them 10,000 invs per request. The counter went 1→2→3→...→10 and reset, repeating indefinitely. **Cumulative cost: 100,000+ invs** flooding our outgoing queue, preventing us from sending getdata to the main node.
**Fix applied** (main.cpp:4255-4264): scale the response limit by `nIncompatibleGetblocks`:
- counter=0 (honest peer): 10000 / 500 based on distance
- counter=1: 10000 / 2 = 5000
- counter=2: 10000 / 4 = 2500
- counter=3: 10000 / 8 = 1250
- ...
- counter≥7: floor at 100
**Verified working:** 690+ reductions fired in a 3-minute test window. The fork peer can no longer flood our outgoing queue.
### Issue 2: Main node connection flapping (NOT FIXABLE IN CODEBASE)
The main node `gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion:24113` (the well-connected node that was delivering blocks) repeatedly disconnects with `ERROR: Proxy error: host unreachable` and `connection refused`. The daemon then has to wait for Tor to re-establish the hidden service. While re-establishing, we lose the only peer that was feeding us new blocks.
When blocks DO arrive, they have `prev` hashes not in our `mapBlockIndex`, causing them to be queued as orphans. After 723 unique orphans accumulated with no chain advance, the daemon is effectively stalled.
**Root cause:** Tor hidden service reliability for the main node. This is a network/deployment issue, not a Triangles code issue.
### Conclusion
- **Issue 1 fix is in main.cpp and working.** Sync is more resilient to fork peers.
- **Issue 2 cannot be fixed in the Triangles codebase.** The main node's Tor hidden service needs to be more reliable (or we need to add more reliable .onion peers to the seed list).
- **The 18 blk/s sustained rate is the actual ceiling** for this Tor peer set. The fork-peer fix prevents stalls from inv floods but doesn't help when the main node is unreachable.
### Recommended Next Steps (beyond code)
1. Add more reliable .onion peers to the seed list in `seeds.cryptographic-triangles.org`
2. Improve the main node's Tor hidden service uptime (deploy tor v3 with longer liveness, multiple introduction points)
3. Add a peer-scoring system that downgrades flaky peers and prefers reliable ones
These are operational improvements, not code changes.
---
## Addendum (2026-06-21, end-of-day): Corrupted .onion Address & Signed Peer Discovery
After the above audit was written, two more findings emerged that warrant
their own section.
### Finding 8: Corrupted v3 onion address in test config (real bug, production-safe)
**Symptom:** During the running from-zero sync test (PID 2394385), the
embedded Tor log at `/root/.triangles-synctest/tor_data/tor.log` produced:
4,842 occurrences of: "Closed streams for service [scrubbed].onion for reason resolve failed. Fetch status: No more HSDir available to query."
181 occurrences of: "ed25519 validation failed"
181 occurrences of: "Service address [scrubbed] has bad pubkey"
181 occurrences of: "Invalid onion hostname [scrubbed]; rejecting"
The first instinct was "Tor is broken" — but the same Tor instance
worked fine for clearnet (`https://check.torproject.org/api/ip` returned
`{"IsTor":true,"IP":"192.42.116.60"}`) and for known .onion services
(`duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion`
returned HTTP 301 in 3.5s).
**Root cause:** One of the 14 addnodes in `/root/.triangles-synctest/triangles.conf`
had a 1-character transposition:
| Source | Address |
|---|---|
| `src/onionseed.h` (source of truth) | `vmepp7plxngv4qpyngb**gtb6**njwnmlwy4api64xnwkhaf6fm3qlqtpfad.onion` |
| `/root/.triangles/triangles.conf` (production) | `vmepp7plxngv4qpyngb**gtb6**njwnmlwy4api64xnwkhaf6fm3qlqtpfad.onion` ✓ |
| `/root/.triangles-synctest/triangles.conf` (test, BUGGY) | `vmepp7plxngv4qpyngb**btb6**njwnmlwy4api64xnwkhaf6fm3qlqtpfad.onion` ✗ |
The character `g` was corrupted to `b` at position 21. Tor's v3 onion
checksum validation (`SHA3-256(".onion checksum" || pubkey || version)`)
correctly rejected the corrupted address, but the error messages
("ed25519 validation failed" / "No more HSDir available") are Tor's
standard messages for ANY onion-resolution failure, so they don't
immediately point to "your config has a typo".
**Why this matters more than the immediate symptom:**
This is exactly the kind of silent corruption that a signed peer
discovery system would catch at the daemon layer. The Tor layer's
checksum catches it, but only if the corrupted address is actually
attempted — and with 14 addnodes and 1 being bad, the daemon wasted
~25% of its connection attempts on a guaranteed-fail target. A signed
peer system (where peers' .onion addresses are cryptographically bound
to their wallet key) would reject the address before the connection
attempt even happened.
**Fixes deployed:**
1. **One-character config fix** in `/root/.triangles-synctest/triangles.conf`:
`btb6``gtb6`. Production was never affected.
2. **New tool: `scripts/validate_onion_seeds.py`** — validates every
`.onion` in a `triangles.conf` against the v3 hidden service checksum.
Detects the `btb6` corruption in 0.1s with full diagnostic including
"did you mean: gtb6?" suggestion. Pure stdlib, no pip deps.
3. **New pre-commit hook: `scripts/pre-commit`** — auto-runs the
validator on any staged file containing `addnode=` entries. Blocks
the commit if any address fails. Installed at
`.git/hooks/pre-commit`. Bypass with `git commit --no-verify` (NEVER
do this for normal commits).
4. **New C++ test: `src/test/onion_v3_tests.cpp`** — 8 Boost.Test cases
that validate every hardcoded seed in `src/onionseed.h` against the
v3 onion checksum. Runs in CI on every build. Catches corruption at
compile time, not daemon runtime.
### Finding 9: Signed peer discovery (real architectural improvement)
The above finding surfaced a bigger gap: Triangles HAS a node-identity
signing system (`getwalletaddr`/`walletaddr` in `src/tor/onion_v3.cpp:4793-4848`)
but it only fires at startup. After 18 hours of sync, the daemon has
zero ability to find new peers.
**The existing system (already in place, just under-used):**
1. **Node identity proof** (`main.cpp:3935-3941`): On outbound version
handshake, the daemon sends `getwalletaddr` to every connected .onion
peer. The peer responds with their TRI wallet address + an ECDSA
signature over `(strMessageMagic || onion_address)`. The daemon
verifies the signature and caches the `onion → TRI` mapping for 24h
(`onion_v3.cpp:2308`).
2. **Seeder list exchange** (`main.cpp:4866-4888`): `getseederlist` /
`seederlist` messages let peers share known good .onion seeders.
3. **Standard `getaddr`/`addr`** (`main.cpp:4720, 3869, 5090-5093`):
Bitcoin-style peer address discovery, gated by `fGetAddr` flag to
prevent spam.
**The fix shipped in commit `9e9d17e`:**
1. **`src/net.h`** — added `nLastGetaddrTrigger` + `nSignedPeerBonus`
fields to `CNode`.
2. **`src/net.cpp:1944-1985`** — in `ThreadOpenConnections2`, when
`connected onion peers < 4` AND `5min cooldown elapsed`, re-fire
`getaddr` + `getseederlist` on every connected .onion peer. Logs
`SYNC-SIGN: low peer count (X < 4), re-firing discovery round on all peers`.
3. **`src/tor/onion_v3.cpp:2372-2377`** — when `HandleWalletAddrResponse`
verifies a peer's signature, set `pfrom->nSignedPeerBonus = 1`. Logs
`SYNC-SIGN: marked X as signed peer (proved identity via walletaddr)`.
4. **`src/syncmanager.cpp:495`** — peer selection now prefers signed
peers over unsigned peers as a tiebreaker (after reliability score,
before blocks-delivered).
**Verified at runtime:**
SYNC-SIGN: low peer count (0 < 4), re-firing discovery round on all peers
SYNC-SIGN: low peer count (1 < 4), re-firing discovery round on all peers
SYNC-SIGN: marked X as signed peer (proved identity via walletaddr)
The signed peer bonus means that once a peer completes the walletaddr
handshake, they're preferred in block delivery — making the network
self-strengthening: nodes that prove identity get more traffic, which
incentivizes more nodes to prove identity.
### Defense-in-depth summary (end of 2026-06-21)
The from-zero sync test, the corruption bug, and the signed-peer
improvement together produced 4 layers of defense against the same
class of problem (peer discovery / address corruption):
| Layer | Mechanism | What it catches | When |
|---|---|---|---|
| 1. Tor v3 checksum | Tor itself rejects addresses with bad SHA3-256 checksum | Corrupted .onion addresses | Always (network layer) |
| 2. `scripts/validate_onion_seeds.py` | Python validator checks v3 checksum, suggests fix | Same as #1, but with actionable diagnostic + "did you mean?" | Pre-commit / pre-deploy |
| 3. `src/test/onion_v3_tests.cpp` | 8 Boost.Test cases run in CI | Hardcoded seed corruption in `onionseed.h` | Every build |
| 4. Signed peer discovery | `getwalletaddr` ECDSA handshake + `nSignedPeerBonus` preference | Sybil attackers + ephemeral malicious peers | At runtime |
### Remaining gaps (2026-06-21)
1. **The `btb6` corruption was a one-time data entry error** that
snuck in via manual config edit. There's no audit log of when/who
introduced it. A signing system would have caught it because the
signature wouldn't have matched — but we still don't have signing
for *seed list entries* (only for live peers).
2. **The seed list at `seeds.cryptographic-triangles.org` is not
cryptographically signed.** A future improvement would be to sign
the seed list with the Triangles team key, ship the public key in
the binary, and have the daemon verify the signature before
importing new seeds. This is the same pattern Bitcoin Core uses
for its `chainparams.cpp` checkpoints.
3. **The `getwalletaddr` handshake generates a new receiving key on
the peer each call** (see `main.cpp:4814: pwalletMain->GetKeyFromPool`).
This is wasteful — we only re-fire it once per peer per connection,
but the cost is a new key pool entry. Future work: use a stable
node identity key separate from the wallet.
-3
View File
@@ -31,9 +31,6 @@ 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. |
-7
View File
@@ -107,13 +107,6 @@
## 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
-98
View File
@@ -1,98 +0,0 @@
# 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.
-57
View File
@@ -7,15 +7,6 @@ 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
@@ -56,54 +47,6 @@ 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)
-16
View File
@@ -1,16 +0,0 @@
# 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)
-15
View File
@@ -1,15 +0,0 @@
# 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)
-70
View File
@@ -1,70 +0,0 @@
# CMake toolchain file for cross-compiling Triangles for Windows x64 using MinGW on Linux
# Usage: cmake -DCMAKE_TOOLCHAIN_FILE=cmake/mingw64.cmake -B build-mingw -S .
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
# MinGW toolchain
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
# Search for programs only in the build host directories
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# Search for libraries and headers only in the staging directory
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# Staging prefix — all dependencies installed here
set(DEP_PREFIX "${CMAKE_SOURCE_DIR}/deps-mingw")
# Windows libraries
set(CMAKE_LIBRARY_PATH "${DEP_PREFIX}/lib")
# Include directories
set(CMAKE_INCLUDE_PATH "${DEP_PREFIX}/include")
# Windows sysroot (MinGW libraries, headers, and tools)
set(MINGW_SYSROOT /usr/x86_64-w64-mingw32)
# Don't search the host system for programs
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32 ${DEP_PREFIX})
# For find_package(OpenSSL), find_package(Boost), etc.
# Only search deps-mingw and MinGW sysroot — NOT the host system
set(CMAKE_SYSROOT "${MINGW_SYSROOT}")
set(OPENSSL_ROOT_DIR "${DEP_PREFIX}")
set(BOOST_ROOT "${DEP_PREFIX}")
set(CMAKE_PREFIX_PATH "${DEP_PREFIX}")
# Critical: prevent Linux host headers from leaking into MinGW compilation
# The MinGW cross-compiler should ONLY see MinGW and deps headers
set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES "")
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES "")
# Add MinGW and deps include paths explicitly
include_directories(BEFORE SYSTEM
"${DEP_PREFIX}/include"
"${MINGW_SYSROOT}/include"
"${MINGW_SYSROOT}/include/c++"
"${MINGW_SYSROOT}/include/sec_api"
)
# Set output directories
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
# C++20 for the project
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Build settings
set(BUILD_DAEMON ON)
set(BUILD_QT OFF)
set(BUILD_TESTS OFF)
set(USE_UPNP OFF)
set(USE_QRCODE OFF)
set(USE_ZMQ OFF)
set(USE_DBUS OFF)
set(USE_TOR_EMBEDDED OFF)
+51
View File
@@ -31,6 +31,10 @@ RequestExecutionLevel user
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
; Bootstrap page
Page custom BootstrapPage
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
@@ -39,6 +43,34 @@ RequestExecutionLevel user
!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"
@@ -52,6 +84,25 @@ 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"
-88
View File
@@ -1,88 +0,0 @@
# triangles.conf.example — Cryptographic Triangles daemon configuration
#
# Copy this to ~/.triangles/triangles.conf and customize for your node.
# Run scripts/validate_onion_seeds.py against your config before starting
# the daemon to catch any .onion address corruption.
#
# Run order for a fresh operator:
# 1. cp contrib/triangles.conf.example ~/.triangles/triangles.conf
# 2. Edit credentials, port numbers, addnode list as needed
# 3. python3 scripts/validate_onion_seeds.py ~/.triangles/triangles.conf
# 4. /usr/lib/cryptographic-triangles/trianglesd -daemon
#
# The pre-commit hook at scripts/pre-commit will auto-validate this file
# on every commit if you install it via:
# cp scripts/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
# ─── Network ─────────────────────────────────────────────────────────────────
# port=24112 is the mainnet P2P default. Pick an alternate (e.g. 24118) for
# test/parallel nodes to avoid clashing with production.
port=24112
listen=1
discover=1
# ─── RPC ─────────────────────────────────────────────────────────────────────
# Bind RPC to localhost only. The triangles-cli tool connects here.
rpcuser=trianglesrpc
rpcpassword=CHANGE_ME_TO_A_STRONG_RANDOM_PASSWORD
rpcport=19112
rpcallowip=127.0.0.1
server=1
# ─── Tor (MANDATORY — Triangles is Tor-only) ─────────────────────────────────
# Triangles peers are exclusively .onion addresses. Never use clearnet IPs
# in addnode= entries. See:
# * src/onionseed.h — hardcoded seed list (source of truth)
# * src/test/onion_v3_tests.cpp — validates the hardcoded list at CI
# * scripts/validate_onion_seeds.py — validates your config at pre-commit
#
# proxy= can point at:
# * Embedded Tor: 127.0.0.1:19099 (started automatically by the daemon)
# * System Tor: 127.0.0.1:9050
# * Tor Browser: 127.0.0.1:9150
proxy=127.0.0.1:19099
# ─── Hardcoded seed nodes (src/onionseed.h, v3 onion only) ──────────────────
# These 7 are the source-of-truth seeds. The C++ test suite validates
# every one of them at build time.
addnode=gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion:24112
addnode=i6tk7soznftvoibtskwlezviskiererhjndpsmrff4kaxw7jnd5izfqd.onion:24112
addnode=nawqqoazk2hhaglygulpeg6kh7hsgnvi2fursdvpvkantu4ojj26taid.onion:24112
addnode=vmepp7plxngv4qpyngbgtb6njwnmlwy4api64xnwkhaf6fm3qlqtpfad.onion:24112
addnode=nsldmfujkiwsfha42ajp5zx7gz3ekwdk4nvowdpf56mayuxnzshuykqd.onion:24112
addnode=on4noksywc7b6cdbbxsp535l7j4cugunvlyz3iyhf6sfcg2qzaoy3eqd.onion:24112
addnode=3uyzltm5cy7xzunncp3d7ariw75erabdnj4l3cxwvsxb6h4orc7eiqad.onion:24112
# ─── Dynamic seeds (fetched from seeds.cryptographic-triangles.org) ─────────
# These are populated at runtime by the daemon from the HTTP seed list. You
# can also pin them here as a fallback for offline operation. They MUST be
# valid v3 onions — validate with scripts/validate_onion_seeds.py.
# addnode=6ygpphp2qsucwvhwefv6h6ehvk6zjf7b7zdp4ggkzjjwe76cg6jwm7id.onion:24112
# addnode=uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112
# addnode=el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112
# addnode=sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112
# addnode=i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112
# addnode=odtiwh6d2mqweztjrp45g5ogf4ikwtl5gotpjcbtax2qzkztrqcqieid.onion:24112
# addnode=jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112
# ─── Indexes ─────────────────────────────────────────────────────────────────
# Required for getaddressbalance / getaddressutxos / getaddresstxids RPCs
# and for the bootstrap server to serve UTXO snapshots. Costs ~5GB disk.
txindex=1
addressindex=1
spentindex=1
timestampindex=1
# ─── Staking ─────────────────────────────────────────────────────────────────
# Set staking=0 to disable stake mining (recommended for sync-test / archive
# nodes that don't need to produce blocks).
staking=1
stakegen=1
# ─── Performance ────────────────────────────────────────────────────────────
# dbcache in MB. 512 is reasonable for sync nodes. 1024+ for archival nodes.
dbcache=512
# ─── Security ───────────────────────────────────────────────────────────────
# Disable Tor — DO NOT REMOVE THIS. Triangles is Tor-only by design.
notor=0
-35
View File
@@ -1,35 +0,0 @@
# 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
View File
@@ -1,68 +0,0 @@
# 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.
-244
View File
@@ -1,244 +0,0 @@
# 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).
-301
View File
@@ -1,301 +0,0 @@
# 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
-7
View File
@@ -1,7 +0,0 @@
# Krystie runner log
This file records autonomous-runner activity. Each entry is a doc-only
edit produced by the demo worker; once OpenClaw is wired in this log
will be replaced by real work.
- [2026-04-29T06:57:30Z] triangles_v5#1 — Smoke-test the Krystie loop runner
-240
View File
@@ -1,240 +0,0 @@
# 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.
+1 -1
View File
@@ -3,7 +3,7 @@
# Run on a Linux x64 system with appimagetool installed
set -e
VERSION="6.2.4"
VERSION="5.7.6"
APPDIR="Triangles-x86_64.AppDir"
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
@@ -41,31 +41,6 @@
</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>
-30
View File
@@ -1,30 +0,0 @@
pkgbase = triangles-qt-bin
pkgdesc = Cryptographic Triangles (TRI) cryptocurrency wallet - Qt GUI
pkgver = 5.9.20
pkgrel = 1
url = https://cryptographic-triangles.org
arch = x86_64
license = MIT
depends = qt5-base
depends = openssl
depends = boost-libs
depends = db
depends = leveldb
depends = libevent
depends = miniupnpc
depends = tor
optdepend = tor: anonymous networking support
provides = triangles-qt
provides = trianglesd
provides = triangles-cli
conflicts = triangles-qt
conflicts = trianglesd
conflicts = triangles-cli
source = https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.20/cryptographic-triangles_5.9.20_amd64.deb
source = https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.20/cryptographic-triangles-daemon_5.9.20_amd64.deb
source = triangles-qt.desktop
sha256sums = b4afcf758f55c8fb256f4742917971414078ce37c0fe346383ccda5251917bde
sha256sums = 068d015cf73206f3f3604b0c8fbf60db307c20234cbe06e236996fb9a336df51
sha256sums = SKIP
pkgname = triangles-qt-bin
+14 -57
View File
@@ -1,6 +1,6 @@
# Maintainer: Sami Ahmed <https://github.com/SamiAhmed7777>
# Maintainer: Cryptographic Triangles Team
pkgname=triangles-qt-bin
pkgver=5.9.20
pkgver=5.5.6
pkgrel=1
pkgdesc="Cryptographic Triangles (TRI) cryptocurrency wallet - Qt GUI"
arch=('x86_64')
@@ -8,64 +8,21 @@ url="https://cryptographic-triangles.org"
license=('MIT')
depends=('qt5-base' 'openssl' 'boost-libs' 'db' 'leveldb' 'libevent' 'miniupnpc' 'tor')
optdepends=('tor: anonymous networking support')
provides=('triangles-qt' 'trianglesd' 'triangles-cli')
conflicts=('triangles-qt' 'trianglesd' 'triangles-cli')
provides=('triangles-qt' 'trianglesd')
conflicts=('triangles-qt' 'trianglesd')
source=(
"https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${pkgver}/cryptographic-triangles_${pkgver}_amd64.deb"
"https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${pkgver}/cryptographic-triangles-daemon_${pkgver}_amd64.deb"
"triangles-qt-${pkgver}::https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${pkgver}/Cryptographic-Triangles-v${pkgver}-linux-x64-qt"
"trianglesd-${pkgver}::https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${pkgver}/Cryptographic-Triangles-v${pkgver}-linux-x64-daemon"
"triangles-qt.desktop"
)
sha256sums=('b4afcf758f55c8fb256f4742917971414078ce37c0fe346383ccda5251917bde'
'068d015cf73206f3f3604b0c8fbf60db307c20234cbe06e236996fb9a336df51'
'SKIP')
prepare() {
cd "$srcdir"
# Qt GUI + bundled Qt/libs come from the full wallet .deb
ar x "cryptographic-triangles_${pkgver}_amd64.deb"
tar --use-compress-program=unzstd -xf data.tar.zst
rm -f control.tar.zst data.tar.zst debian-binary
# Headless daemon + JSON-RPC client come from the daemon .deb
ar x "cryptographic-triangles-daemon_${pkgver}_amd64.deb"
tar --use-compress-program=unzstd -xf data.tar.zst
rm -f control.tar.zst data.tar.zst debian-binary
}
sha256sums=(
'ed220eb8d0b403f62cdac28988541fd1a27864491e233216f9c00a4c2537b4a3'
'4d2ab25d61127d6aff3e6f3069556d04f4b823f8849e97629c12871ad4779517'
'SKIP'
)
package() {
cd "$srcdir"
# Install the actual binaries to /opt/triangles
install -dm755 "${pkgdir}/opt/triangles"
install -m755 usr/lib/cryptographic-triangles/triangles-qt \
"${pkgdir}/opt/triangles/triangles-qt"
install -m755 usr/lib/cryptographic-triangles/trianglesd \
"${pkgdir}/opt/triangles/trianglesd"
install -m755 usr/lib/cryptographic-triangles/triangles-cli \
"${pkgdir}/opt/triangles/triangles-cli"
# Install bundled shared libraries to /opt/triangles/lib.
# Many are version-pinned (librocksdb.so.6.11, libgflags.so.2.2,
# libdb_cxx-5.3.so, libboost_program_options.so.1.74.0) and are not
# available at the right version on Arch, so we ship them ourselves.
install -dm755 "${pkgdir}/opt/triangles/lib"
# Use GUI .deb libs (it has the full Qt set + everything daemon needs)
install -m644 usr/lib/cryptographic-triangles/lib/* \
"${pkgdir}/opt/triangles/lib/"
# Wrapper scripts in /usr/bin set LD_LIBRARY_PATH and exec the real binary.
# System Qt5/openssl/etc. are still on the default loader path and take
# precedence for libs NOT in our private directory.
install -dm755 "${pkgdir}/usr/bin"
for bin in triangles-qt trianglesd triangles-cli; do
install -m755 /dev/stdin "${pkgdir}/usr/bin/${bin}" <<EOF
#!/bin/bash
export LD_LIBRARY_PATH=/opt/triangles/lib\${LD_LIBRARY_PATH:+:\${LD_LIBRARY_PATH}}
exec /opt/triangles/${bin} "\$@"
EOF
done
# .desktop file
install -Dm644 triangles-qt.desktop \
"${pkgdir}/usr/share/applications/triangles-qt.desktop"
install -Dm755 "triangles-qt-${pkgver}" "${pkgdir}/usr/bin/triangles-qt"
install -Dm755 "trianglesd-${pkgver}" "${pkgdir}/usr/bin/trianglesd"
install -Dm644 "triangles-qt.desktop" "${pkgdir}/usr/share/applications/triangles-qt.desktop"
}
@@ -1,14 +1,18 @@
$ErrorActionPreference = 'Stop'
$packageArgs = @{
packageName = $env:ChocolateyPackageName
fileType = 'exe'
softwareName = 'Cryptographic Triangles*'
url64bit = "https://github.com/SamiAhmed7777/triangles_v5/releases/download/v$env:ChocolateyPackageVersion/Cryptographic-Triangles-$env:ChocolateyPackageVersion-win-x64-setup.exe"
checksum64 = '__CHECKSUM_PLACEHOLDER__'
packageName = 'triangles'
unzipLocation = "$(Split-Path -Parent $MyInvocation.MyCommand.Definition)"
url64bit = 'https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.3.7/Cryptographic-Triangles-5.3.7-win-x64.zip'
checksum64 = '6f002a669a7e92aaf3d8dd7b1ae80f06a086c99a15ca05cf107665009ffc06b7'
checksumType64 = 'sha256'
silentArgs = '/S'
validExitCodes = @(0, 3010, 1641)
}
Install-ChocolateyPackage @packageArgs
Install-ChocolateyZipPackage @packageArgs
$installDir = $packageArgs.unzipLocation
$desktopPath = [Environment]::GetFolderPath('Desktop')
Install-ChocolateyShortcut `
-ShortcutFilePath "$desktopPath\Cryptographic Triangles.lnk" `
-TargetPath "$installDir\triangles-qt.exe"
+1 -1
View File
@@ -3,7 +3,7 @@
# Run from the packaging/debian directory
set -e
VERSION="6.2.4"
VERSION="5.7.6"
PKGDIR="triangles_${VERSION}-1_amd64"
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
+23 -41
View File
@@ -1,57 +1,39 @@
FROM ubuntu:22.04 AS builder
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 \
curl ca-certificates binutils zstd && \
curl -fsSL -o /tmp/triangles.deb "${DEB_URL}" && \
cd /tmp && ar x /tmp/triangles.deb && \
tar --use-compress-program=unzstd -xf data.tar.zst && \
rm -f /tmp/triangles.deb /tmp/control.tar.zst /tmp/debian-binary /tmp/data.tar.zst
# ---------- Runtime ----------
FROM ubuntu:22.04
ARG VERSION=6.2.4
LABEL maintainer="Cryptographic Triangles Team"
LABEL description="Cryptographic Triangles (TRI) headless daemon"
LABEL version="6.2.4"
LABEL version="5.7.6"
ARG VERSION=5.7.6
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libssl3 \
libevent-2.1-7 \
libboost-system1.74.0 \
libboost-filesystem1.74.0 \
libboost-program-options1.74.0 \
libboost-thread1.74.0 \
libboost-chrono1.74.0 \
libdb5.3++ \
libminiupnpc17 \
curl \
ca-certificates \
libssl3 \
libevent-2.1-7 \
libboost-system1.74.0 \
libboost-filesystem1.74.0 \
libboost-program-options1.74.0 \
libboost-thread1.74.0 \
libboost-chrono1.74.0 \
libdb5.3++ \
libminiupnpc17 \
tor \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /tmp/usr/lib/cryptographic-triangles/ /opt/triangles/
COPY --from=builder /tmp/usr/bin/trianglesd /usr/local/bin/trianglesd
COPY --from=builder /tmp/usr/bin/triangles-cli /usr/local/bin/triangles-cli
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
# Wrapper sets LD_LIBRARY_PATH so the dynamic libs resolve
RUN printf '#!/bin/bash\nexport LD_LIBRARY_PATH=/opt/triangles/lib:${LD_LIBRARY_PATH}\nexec /opt/triangles/%s "$@"\n' trianglesd \
> /usr/local/bin/trianglesd-wrap && \
printf '#!/bin/bash\nexport LD_LIBRARY_PATH=/opt/triangles/lib:${LD_LIBRARY_PATH}\nexec /opt/triangles/%s "$@"\n' triangles-cli \
> /usr/local/bin/triangles-cli-wrap && \
mv /usr/local/bin/trianglesd-wrap /usr/local/bin/trianglesd && \
mv /usr/local/bin/triangles-cli-wrap /usr/local/bin/triangles-cli && \
chmod +x /usr/local/bin/trianglesd /usr/local/bin/triangles-cli
RUN useradd -m -s /bin/bash triangles && \
mkdir -p /home/triangles/.triangles && \
chown -R triangles:triangles /home/triangles
RUN useradd -m -s /bin/bash triangles
USER triangles
WORKDIR /home/triangles
RUN mkdir -p /home/triangles/.triangles
VOLUME /home/triangles/.triangles
EXPOSE 24112 19112
ENTRYPOINT ["trianglesd"]
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
trianglesd:
build: .
image: cryptographic-triangles/trianglesd:6.2.4
image: cryptographic-triangles/trianglesd:5.7.6
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/v6.2.4/Cryptographic-Triangles-v6.2.4-linux-x64-qt
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-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/v6.2.4/Cryptographic-Triangles-v6.2.4-linux-x64-daemon
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-linux-x64-daemon
sha256: 4d2ab25d61127d6aff3e6f3069556d04f4b823f8849e97629c12871ad4779517
dest-filename: trianglesd-linux
+1 -1
View File
@@ -4,7 +4,7 @@
# Install build tools: sudo dnf install rpm-build rpmdevtools
set -e
VERSION="6.2.4"
VERSION="5.7.6"
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
echo "Building RPM for Triangles v${VERSION}..."
+1 -47
View File
@@ -1,5 +1,5 @@
Name: triangles
Version: 6.2.4
Version: 5.7.6
Release: 1%{?dist}
Summary: Cryptographic Triangles (TRI) cryptocurrency wallet
License: MIT
@@ -42,49 +42,3 @@ 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.
+2 -2
View File
@@ -1,11 +1,11 @@
{
"version": "6.2.4",
"version": "5.7.6",
"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/v6.2.4/Cryptographic-Triangles-6.2.4-win-x64.zip",
"url": "https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-5.7.6-win-x64.zip",
"hash": "6f002a669a7e92aaf3d8dd7b1ae80f06a086c99a15ca05cf107665009ffc06b7"
}
},
@@ -1,5 +1,5 @@
PackageIdentifier: CryptographicTriangles.TrianglesQt
PackageVersion: 6.2.4
PackageVersion: 5.7.6
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/v6.2.4/Cryptographic-Triangles-6.2.4-win-x64.zip
InstallerUrl: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-5.7.6-win-x64.zip
InstallerSha256: 6F002A669A7E92AAF3D8DD7B1AE80F06A086C99A15CA05CF107665009FFC06B7
ManifestType: singleton
ManifestVersion: 1.6.0
-184
View File
@@ -1,184 +0,0 @@
# 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.
-65
View File
@@ -1,65 +0,0 @@
-----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-----
+22 -29
View File
@@ -1,36 +1,29 @@
# Scripts
# Version Bump Script
Operational scripts for the Triangles project. See also `doc/release-process.md`
for the canonical release pipeline documentation.
Updates the version number across all files in the repo from a single command.
## Build verification
## Usage
- **`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).
**Set a specific version:**
```bash
bash scripts/bump-version.sh 5.7.0
```
## Release signing
**Or edit `src/clientversion.h` first, then sync everything else:**
```bash
bash scripts/bump-version.sh
```
- **`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`).
## What it updates
## Existing infrastructure
- `src/clientversion.h` (source of truth)
- `src/version.h`
- `triangles-qt.pro`
- `Dockerfile`
- All packaging manifests (Docker, Snap, Scoop, WinGet, RPM, Flatpak, Debian, AppImage)
- **`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.
## 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
-124
View File
@@ -1,124 +0,0 @@
#!/usr/bin/env bash
#
# build-rocksdb.sh — Build and install a pinned RocksDB version for CI.
#
# Ubuntu 22.04's librocksdb-dev is 6.11.4 (the same version that bit
# DNS2 — see PR #10). Triangles requires RocksDB >= 7.4.0 for the XXH3
# per-block checksum used in modern smsgDB SST files; src/smessage.cpp's
# SecMsgDB::Open has a runtime quarantine fallback, but the build-time
# check in CMakeLists.txt refuses to configure against < 7.4.
#
# This script clones RocksDB at a pinned tag, builds only the shared
# library (fast), installs to /usr/local, and refreshes ldconfig.
# Triangles' CMake find_library probes /usr/local before /usr/lib so
# the just-built copy is picked up first.
#
# 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
# 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)}"
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
echo ">>> Building RocksDB ${ROCKSDB_TAG} (${JOBS} jobs) into ${INSTALL_PREFIX}"
git clone --depth 1 --branch "${ROCKSDB_TAG}" \
https://github.com/facebook/rocksdb.git "${WORKDIR}/rocksdb"
cd "${WORKDIR}/rocksdb"
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 \
EXTRA_CXXFLAGS="-Wno-error=deprecated-declarations"
make install-shared PREFIX="${INSTALL_PREFIX}"
# Scrub the rocksdb.pc that install-shared just wrote. RocksDB's
# Makefile unconditionally appends `-isystem third-party/gtest-1.8.1/
# fused-src` to Cflags, which is a RELATIVE path baked in from the build
# directory. Modern CMake (>= 3.27) refuses to consume imported targets
# with non-existent relative paths in INTERFACE_INCLUDE_DIRECTORIES,
# so pkg_check_modules(rocksdb) on a Triangles configure errors out
# with: 'Imported target "PkgConfig::RocksDB" includes non-existent
# path "third-party/gtest-1.8.1/fused-src"'.
#
# Replace the bad flag with the absolute include dir so pkg-config
# consumers see a path that actually exists on disk.
PC_FILE="${INSTALL_PREFIX}/lib/pkgconfig/rocksdb.pc"
if [ -f "${PC_FILE}" ]; then
# 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++[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
# Sanity: installed library should be on disk and registered with ldconfig.
# ldconfig strips the patch version from its output, so we check both:
# 1. File exists at the versioned path (definitive).
# 2. ldconfig shows a matching major.minor (sanity for runtime linker).
ROCKSDB_MAJOR_MINOR="${ROCKSDB_VERSION%.*}"
if [ ! -f "${INSTALL_PREFIX}/lib/librocksdb.so.${ROCKSDB_VERSION}" ]; then
echo "!!! librocksdb.so.${ROCKSDB_VERSION} not found at ${INSTALL_PREFIX}/lib/" >&2
ls -l "${INSTALL_PREFIX}/lib/librocksdb"* 2>&1 || true
exit 1
fi
if ! ldconfig -p | grep -q "librocksdb.so.${ROCKSDB_MAJOR_MINOR}"; then
echo "!!! ldconfig did not register librocksdb.so.${ROCKSDB_MAJOR_MINOR}" >&2
ldconfig -p | grep -i rocksdb >&2 || true
exit 1
fi
echo ">>> RocksDB ${ROCKSDB_TAG} installed to ${INSTALL_PREFIX}"
echo ">>> - library: ${INSTALL_PREFIX}/lib/librocksdb.so.${ROCKSDB_VERSION}"
echo ">>> - headers: ${INSTALL_PREFIX}/include/rocksdb/version.h"
ls -l "${INSTALL_PREFIX}/lib/librocksdb.so"* "${INSTALL_PREFIX}/include/rocksdb/version.h"
-206
View File
@@ -1,206 +0,0 @@
#!/usr/bin/env bash
# scripts/ci/package-linux-daemon.sh
#
# Linux packaging step for the triangles daemon + CLI .deb.
# Called from .github/workflows/build-all.yml build-linux-daemon step.
#
# Builds a self-contained .deb with trianglesd, triangles-cli, bundled libs,
# Tor, systemd service, and CLI launchers. Designed to be reproducible and
# debuggable outside the CI environment.
#
# Usage: bash scripts/ci/package-linux-daemon.sh <version>
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}"
# Stage directories
rm -rf "${PKG}"
mkdir -p "${PKG}/DEBIAN"
mkdir -p "${PKG}/usr/lib/cryptographic-triangles/lib"
mkdir -p "${PKG}/usr/lib/cryptographic-triangles/tor"
mkdir -p "${PKG}/usr/bin"
mkdir -p "${PKG}/etc/systemd/system"
# Download + extract Tor
TOR_TARBALL="tor-expert-bundle-linux-x86_64-${TOR_VERSION}.tar.gz"
if [ ! -f "${TOR_TARBALL}" ]; then
echo ">>> Downloading Tor ${TOR_VERSION}..."
# 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
# Copy binaries
cp "build/bin/trianglesd" "${PKG}/usr/lib/cryptographic-triangles/"
cp "build/bin/triangles-cli" "${PKG}/usr/lib/cryptographic-triangles/"
# Copy Tor
cp "tor-extract/tor/tor" "${PKG}/usr/lib/cryptographic-triangles/tor/"
chmod +x "${PKG}/usr/lib/cryptographic-triangles/tor/tor"
if [ -d "tor-extract/data" ]; then
cp -r "tor-extract/data" "${PKG}/usr/lib/cryptographic-triangles/tor/data"
fi
# Bundle shared library dependencies (skip glibc/kernel — always present)
echo ">>> Bundling shared library dependencies..."
ALL_LIBS="$(mktemp)"
trap 'rm -f "${ALL_LIBS}"' EXIT
for bin in trianglesd triangles-cli; do
ldd "build/bin/${bin}" 2>/dev/null \
| grep '=> /' \
| awk '{print $3}' \
>> "${ALL_LIBS}" || true
done
if [ -s "${ALL_LIBS}" ]; then
sort -u "${ALL_LIBS}" | while IFS= read -r lib; do
if [ -z "${lib}" ]; then continue; fi
case "${lib}" in
/lib/x86_64-linux-gnu/libc.so*|/lib/x86_64-linux-gnu/libm.so*|/lib/x86_64-linux-gnu/libpthread.so*|/lib/x86_64-linux-gnu/libdl.so*|/lib/x86_64-linux-gnu/librt.so*|/lib/x86_64-linux-gnu/ld-linux*|/lib64/ld-linux*)
;; # Skip glibc core
*)
cp -L "${lib}" "${PKG}/usr/lib/cryptographic-triangles/lib/" 2>/dev/null || true
;;
esac
done
fi
echo ">>> Bundled libs:"
ls -la "${PKG}/usr/lib/cryptographic-triangles/lib/" | tail -n +2 | wc -l
# Launchers (set LD_LIBRARY_PATH for bundled libs)
cat > "${PKG}/usr/bin/trianglesd" << 'LAUNCHER'
#!/bin/bash
INSTALL_DIR=/usr/lib/cryptographic-triangles
export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}"
exec "${INSTALL_DIR}/trianglesd" "$@"
LAUNCHER
chmod +x "${PKG}/usr/bin/trianglesd"
cat > "${PKG}/usr/bin/triangles-cli" << 'LAUNCHER'
#!/bin/bash
INSTALL_DIR=/usr/lib/cryptographic-triangles
export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}"
exec "${INSTALL_DIR}/triangles-cli" "$@"
LAUNCHER
chmod +x "${PKG}/usr/bin/triangles-cli"
# systemd unit
cat > "${PKG}/etc/systemd/system/trianglesd.service" << 'SVC'
[Unit]
Description=Cryptographic Triangles Daemon
After=network-online.target
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
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
SVC
# DEBIAN/control
cat > "${PKG}/DEBIAN/control" << CTRL
Package: cryptographic-triangles-daemon
Version: ${VERSION}
Architecture: amd64
Maintainer: Cryptographic Triangles <dev@cryptographic-triangles.org>
Description: Cryptographic Triangles daemon + CLI with integrated Tor
Fully self-contained headless node + JSON-RPC client with all libraries,
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."
echo " Start daemon: sudo systemctl start trianglesd"
echo " On boot: sudo systemctl enable trianglesd"
echo " Use CLI: triangles-cli getinfo"
echo ""
POST
chmod +x "${PKG}/DEBIAN/postinst"
# Build the .deb
dpkg-deb --build "${PKG}"
echo ">>> Built: ${PKG}.deb"
ls -la "${PKG}.deb"
exit 0
-71
View File
@@ -1,71 +0,0 @@
#!/usr/bin/env bash
# scripts/ci/package-windows-daemon.sh
#
# Windows MSYS2 packaging step for the triangles daemon + CLI.
# Called from .github/workflows/build-all.yml build-windows-daemon step.
#
# Why a script file instead of inline YAML:
# The GitHub Actions msys2 shell wrapper has shown inconsistent handling of
# multi-line inline run: blocks under `set -e -o pipefail` (silent exits with
# code 1). A committed script file bypasses the YAML → shell translation
# quirks and gives us a known-good artifact that we can also run locally in
# MSYS2 for debugging.
#
# Usage: bash scripts/ci/package-windows-daemon.sh <dist-dir> <bin> [<bin> ...]
# Example: bash scripts/ci/package-windows-daemon.sh daemon-dist trianglesd triangles-cli
set -euo pipefail
DIST="${1:-daemon-dist}"
shift
BINS=("$@")
if [ "${#BINS[@]}" -eq 0 ]; then
echo "Usage: $0 <dist-dir> <bin> [<bin> ...]" >&2
echo " e.g. $0 daemon-dist trianglesd triangles-cli" >&2
exit 2
fi
echo ">>> Package step: bins=${BINS[*]} dist=${DIST}"
# Make the dist directory
mkdir -p "${DIST}/tor"
# Copy each binary to dist/
for bin in "${BINS[@]}"; do
src="build/bin/${bin}.exe"
if [ ! -f "${src}" ]; then
echo "ERROR: ${src} not found" >&2
exit 3
fi
cp "${src}" "${DIST}/"
echo " copied ${src} -> ${DIST}/"
done
# Copy linked DLLs (union of all binaries' dependencies, deduped)
echo ">>> Collecting DLLs from ldd output..."
ALL_DLLS="$(mktemp)"
trap 'rm -f "${ALL_DLLS}"' EXIT
for bin in "${BINS[@]}"; do
src="build/bin/${bin}.exe"
ldd "${src}" 2>/dev/null \
| grep '/mingw64' \
| awk '{print $3}' \
>> "${ALL_DLLS}" || true
done
if [ ! -s "${ALL_DLLS}" ]; then
echo "WARNING: no /mingw64 DLLs found in ldd output for ${BINS[*]}" >&2
else
echo ">>> Copying $(sort -u "${ALL_DLLS}" | wc -l) unique DLLs..."
sort -u "${ALL_DLLS}" | while IFS= read -r dll; do
if [ -n "${dll}" ] && [ -f "${dll}" ]; then
cp "${dll}" "${DIST}/" || echo "WARN: failed to copy ${dll}" >&2
fi
done
fi
echo ">>> Package complete: $(ls -1 "${DIST}" | wc -l) files in ${DIST}/"
ls -la "${DIST}/"
exit 0
-106
View File
@@ -1,106 +0,0 @@
#!/usr/bin/env bash
# .git/hooks/pre-commit — Cryptographic Triangles
#
# Auto-runs scripts/validate_onion_seeds.py against any staged file that
# contains .onion addresses. Blocks the commit if any address fails v3
# onion checksum validation.
#
# This is the primary defense against the "1-character .onion transposition
# bug" that caused 4,842 Tor "No more HSDir" errors during the 2026-06-21
# from-zero sync test. See scripts/validate_onion_seeds.py for the validator
# and references/sync-security-audit-2026-06-21.md for the full story.
#
# The hook scans staged files for two patterns:
# 1. Filename matches: triangles.conf, *.onion
# 2. Content contains addnode= entries with .onion addresses
#
# To install:
# cp scripts/pre-commit .git/hooks/pre-commit
# chmod +x .git/hooks/pre-commit
#
# To bypass (in emergencies only — NEVER do this for normal commits):
# git commit --no-verify
set -e
REPO_ROOT="$(git rev-parse --show-toplevel)"
VALIDATOR="${REPO_ROOT}/scripts/validate_onion_seeds.py"
# Find the validator
if [[ ! -x "$VALIDATOR" ]]; then
echo "pre-commit: WARNING: $VALIDATOR not found or not executable" >&2
echo "pre-commit: skipping v3 onion validation" >&2
echo "pre-commit: install with: chmod +x $VALIDATOR" >&2
exit 0
fi
# Two-pass detection:
# Pass 1: filename-based — files named triangles.conf or *.onion
# Pass 2: content-based — any file containing "addnode=" + .onion address
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR)
# Pass 1: filename-based
NAME_MATCHES=$(echo "$STAGED_FILES" | grep -E '(triangles\.conf$|\.onion$)' || true)
# Pass 2: content-based — find staged files containing addnode= with .onion addresses
CONTENT_MATCHES=""
for f in $STAGED_FILES; do
if [[ -f "$f" ]] && grep -qE '^[[:space:]]*addnode=[a-z2-7]{56}\.onion' "$f" 2>/dev/null; then
CONTENT_MATCHES="$CONTENT_MATCHES $f"
fi
done
# Combine and dedupe
ALL_MATCHES=$(printf "%s\n%s\n" "$NAME_MATCHES" "$CONTENT_MATCHES" | sort -u | grep -v '^$' || true)
if [[ -z "$ALL_MATCHES" ]]; then
# Nothing to validate
exit 0
fi
# Filter to only files that exist (skip deletions)
EXISTING_CONFIGS=""
for f in $ALL_MATCHES; do
if [[ -f "$f" ]]; then
EXISTING_CONFIGS="$EXISTING_CONFIGS $f"
fi
done
if [[ -z "$EXISTING_CONFIGS" ]]; then
exit 0
fi
COUNT=$(echo $EXISTING_CONFIGS | wc -w)
echo "pre-commit: validating $COUNT staged file(s) with .onion addresses..."
# Build the validator command
CMD="python3 \"$VALIDATOR\" --no-color --ci"
if [[ -f "${REPO_ROOT}/src/onionseed.h" ]]; then
CMD="$CMD --against \"${REPO_ROOT}/src/onionseed.h\""
fi
# Run the validator
if eval $CMD $EXISTING_CONFIGS; then
echo "pre-commit: v3 onion validation PASSED"
exit 0
else
EXIT_CODE=$?
echo "" >&2
echo "pre-commit: v3 onion validation FAILED (exit $EXIT_CODE)" >&2
echo "" >&2
echo " The commit was blocked because one or more .onion addresses failed" >&2
echo " v3 hidden service checksum validation. This means the .onion address" >&2
echo " has a typo or character transposition that Tor will reject at runtime" >&2
echo " with 'ed25519 validation failed' / 'No more HSDir available to query'." >&2
echo "" >&2
echo " Fix the .onion address in the affected file, then re-stage and commit." >&2
echo "" >&2
echo " To inspect the failure in detail, run manually:" >&2
echo " python3 $VALIDATOR --against ${REPO_ROOT}/src/onionseed.h \\" >&2
echo " $EXISTING_CONFIGS" >&2
echo "" >&2
echo " To bypass this check (DO NOT do this for normal commits):" >&2
echo " git commit --no-verify" >&2
exit 1
fi
-222
View File
@@ -1,222 +0,0 @@
#!/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
-182
View File
@@ -1,182 +0,0 @@
#!/usr/bin/env bash
# ============================================================================
# Triangles UTXO Snapshot Signer
# ============================================================================
# Generates a UTXO snapshot from the current node, signs its provenance
# message with the wallet's signing address, and writes the signed manifest.
#
# Usage:
# ./sign-snapshot.sh [snapshot-name]
#
# Default snapshot name: tri-utxo-snapshot-<timestamp>.utx
# Output (in this dir):
# <snapshot-name> - the UTXO snapshot binary
# <snapshot-name>.sig - base64 signature
# <snapshot-name>.msg - signed message (human-readable provenance)
# <snapshot-name>.manifest.json - signed manifest (drop into bootstrap dir)
# <snapshot-name>.pubkey - signing address
#
# Requirements:
# - trianglesd running with RPC enabled
# - wallet unlocked (or passphrase set in triangles.conf)
# - jq installed (apt: jq / brew: jq)
#
# Verification:
# ./sign-snapshot.sh verify <manifest.json> <snapshot-file>
# OR via RPC:
# verifymessage <addr> <sig> <msg>
# ============================================================================
set -euo pipefail
# ----- Config (override via env) -----
RPC_USER="${RPC_USER:-trianglesrpc}"
RPC_PASS="${RPC_PASS:-2KVK2FvLZBW9Hxv4a2Uj3dMRDAXdh4ei6S5tdZ3z2Mme}"
RPC_HOST="${RPC_HOST:-127.0.0.1}"
RPC_PORT="${RPC_PORT:-19112}"
SIGN_ACCOUNT="${SIGN_ACCOUNT:-}" # blank = use default account
NHEADERS="${NHEADERS:-2000}"
SNAP_DIR="${SNAP_DIR:-.}"
# ----- Helpers -----
rpc() {
local method="$1"; shift
local params="$1"; shift || true
curl -s --user "${RPC_USER}:${RPC_PASS}" \
-X POST -H 'Content-Type: application/json' \
--data "{\"jsonrpc\":\"1.0\",\"method\":\"${method}\",\"params\":${params}}" \
"http://${RPC_HOST}:${RPC_PORT}/"
}
rpc_field() {
local method="$1"; shift
local params="$1"; shift || true
local field="$1"; shift
rpc "$method" "$params" | jq -r ".result.${field} // empty"
}
sha256_file() { sha256sum "$1" | awk '{print $1}'; }
# ----- Verify mode -----
if [[ "${1:-}" == "verify" ]]; then
MANIFEST="${2:?usage: $0 verify <manifest.json> <snapshot-file>}"
SNAP="${3:?usage: $0 verify <manifest.json> <snapshot-file>}"
ADDR=$(jq -r '.signing_address' "$MANIFEST")
SIG=$(jq -r '.signature' "$MANIFEST")
MSG=$(jq -r '.message' "$MANIFEST")
EXPECTED_SHA=$(jq -r '.snapshot_sha256' "$MANIFEST")
echo "==> Verifying snapshot provenance..."
echo " Address: $ADDR"
echo " Message: $MSG"
ACTUAL_SHA=$(sha256_file "$SNAP")
if [[ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]]; then
echo "FAIL: snapshot sha256 mismatch"
echo " expected: $EXPECTED_SHA"
echo " actual: $ACTUAL_SHA"
exit 1
fi
echo "OK: sha256 matches"
PARAMS=$(jq -nc --arg a "$ADDR" --arg s "$SIG" --arg m "$MSG" \
'[$a, $s, $m]')
RESULT=$(rpc verifymessage "$PARAMS" | jq -r '.result')
if [[ "$RESULT" == "true" ]]; then
echo "OK: signature valid — snapshot was signed by $ADDR"
exit 0
else
echo "FAIL: signature does not verify"
exit 1
fi
fi
# ----- Generate + sign -----
SNAP_NAME="${1:-tri-utxo-snapshot-$(date -u +%Y%m%dT%H%M%SZ).utx}"
SNAP_PATH="${SNAP_DIR}/${SNAP_NAME}"
echo "==> Step 1/5: querying chain state..."
HEIGHT=$(rpc_field getblockcount '[]' '' || echo "")
if [[ -z "$HEIGHT" ]]; then
rpc_field getblockcount '[]' '' # re-run for error visibility
echo "FAIL: RPC getblockcount failed"; exit 1
fi
HEIGHT=$(rpc getblockcount '[]' | jq -r '.result')
BLOCKHASH=$(rpc getbestblockhash '[]' | jq -r '.result')
echo " height: $HEIGHT"
echo " blockhash:$BLOCKHASH"
echo "==> Step 2/5: selecting signing address..."
if [[ -n "$SIGN_ACCOUNT" ]]; then
PARAMS=$(jq -nc --arg a "$SIGN_ACCOUNT" '[$a]')
else
PARAMS='[""]'
fi
ADDR=$(rpc getaccountaddress "$PARAMS" | jq -r '.result')
echo " signer: $ADDR"
echo "==> Step 3/5: dumping UTXO snapshot..."
PARAMS=$(jq -nc --arg f "$SNAP_PATH" --argjson n "$NHEADERS" '[$f, $n]')
DUMP_RESULT=$(rpc dumputxoset "$PARAMS")
echo "$DUMP_RESULT" | jq -r '.result // .error.message // .'
SIZE=$(echo "$DUMP_RESULT" | jq -r '.result.file_size // empty')
if [[ -z "$SIZE" ]]; then
echo "FAIL: dumputxoset failed"; exit 1
fi
echo " size: $SIZE bytes"
echo "==> Step 4/5: signing provenance message..."
SHA=$(sha256_file "$SNAP_PATH")
MSG="Triangles UTXO Snapshot $(date -u +%Y-%m-%d): height=$HEIGHT hash=$BLOCKHASH sha256=$SHA"
echo " message: $MSG"
PARAMS=$(jq -nc --arg a "$ADDR" --arg m "$MSG" '[$a, $m]')
SIG=$(rpc signmessage "$PARAMS" | jq -r '.result')
echo " sig: $SIG"
echo "==> Step 5/5: writing manifest + sidecars..."
MANIFEST_PATH="${SNAP_PATH}.manifest.json"
jq -n \
--arg name "$SNAP_NAME" \
--arg height "$HEIGHT" \
--arg hash "$BLOCKHASH" \
--arg sha "$SHA" \
--arg size "$SIZE" \
--arg msg "$MSG" \
--arg sig "$SIG" \
--arg addr "$ADDR" \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg ver "$(rpc getnetworkinfo '[]' | jq -r '.result.version // "unknown"')" \
'{
schema: "triangles-utxo-snapshot-signed/v1",
name: $name,
generated_utc: $ts,
daemon_version: $ver,
chain_tip: { height: ($height | tonumber), blockhash: $hash },
snapshot_sha256: $sha,
snapshot_bytes: ($size | tonumber),
signing_address: $addr,
message: $msg,
signature: $sig
}' > "$MANIFEST_PATH"
# Sidecar files for easy reading
echo "$ADDR" > "${SNAP_PATH}.pubkey"
echo "$MSG" > "${SNAP_PATH}.msg"
echo "$SIG" > "${SNAP_PATH}.sig"
echo ""
echo "============================================================"
echo "Snapshot signed."
echo " snapshot: $SNAP_PATH"
echo " signature: ${SNAP_PATH}.sig"
echo " manifest: $MANIFEST_PATH"
echo " signer: $ADDR"
echo " sha256: $SHA"
echo "============================================================"
echo ""
echo "To verify on any node:"
echo " verifymessage $ADDR \\"
echo " '$SIG' \\"
echo " '$MSG'"
echo ""
echo "Or run: $0 verify $MANIFEST_PATH $SNAP_PATH"
-125
View File
@@ -1,125 +0,0 @@
#!/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
-77
View File
@@ -1,77 +0,0 @@
# tri — Cryptographic Triangles CLI
A friendly bash wrapper around `trianglesd` RPC for humans and agents.
## Install
```bash
# System-wide
sudo cp tri /usr/local/bin/tri
sudo chmod +x /usr/local/bin/tri
sudo mkdir -p /etc/tri
sudo cp nodes.conf.example /etc/tri/nodes.conf
# Edit /etc/tri/nodes.conf with your node's RPC credentials
# Bash completion
sudo cp tri-completion.bash /etc/bash_completion.d/
# Zsh completion
sudo cp _tri_zsh_completion /usr/local/share/zsh/site-functions/_tri
```
## Config
Edit `/etc/tri/nodes.conf`:
```bash
TRI_SSH_HOST="100.81.59.99" # Node IP (or remove for local)
TRI_SSH_USER="root"
TRI_RPC_PORT="19112"
TRI_RPC_USER="your-rpc-user"
TRI_RPC_PASS="your-rpc-password"
# TRI_WALLET_PASSPHRASE="wallet-passphrase" # If wallet is encrypted
```
## Commands
### Info
- `tri` — Status overview
- `tri status` — Detailed node status
- `tri balance` — Wallet balance + UTXO count
- `tri peers` — Connected peers
- `tri stake` — Staking info
### Wallet
- `tri address new` — New address
- `tri address list` — List addresses
- `tri address balance` — Per-address balances
- `tri send <addr> <amt> [memo]` — Send TRI
- `tri tx [N]` — Recent transactions
- `tri tx <txid>` — Transaction details
### Secure Messaging
- `tri msg inbox` — Read messages
- `tri msg outbox` — Sent messages
- `tri msg send <from> <to> <msg>` — Send encrypted message
- `tri msg anon <to> <msg>` — Anonymous message
- `tri msg keys` — Messaging keys
- `tri msg enable` — Enable secure messaging
- `tri msg pubkey <addr>` — Get public key
### Advanced
- `tri raw <method> [params...]` — Raw RPC passthrough
## Agent Integration (Hermes, Krystie)
Both agents on DNS2 share the same `/etc/tri/nodes.conf` and can execute all commands.
For inter-agent messaging via TRI's encrypted P2P network:
1. Each agent needs a TRI address: `tri address new`
2. Enable messaging: `tri msg enable`
3. Register key: `tri raw smsglocalkeys recv + <address>`
4. Exchange addresses between agents
5. Send: `tri msg send <hermes_addr> <krystie_addr> "message"`
6. Read: `tri msg inbox`
Messages are encrypted (ECDH), routed through the Tor P2P network,
stored for 48 hours, max 4096 bytes each.
-39
View File
@@ -1,39 +0,0 @@
#compdef tri
_tri() {
local -a commands
commands=(
'status:Detailed node status'
'balance:Wallet balance'
'peers:Connected peers'
'stake:Staking info'
'address:Address management'
'send:Send TRI'
'tx:Transactions'
'msg:Secure messaging'
'raw:Raw RPC passthrough'
'help:Show help'
)
_arguments -C \
"1:command:->command" \
"*::arg:->args"
case "$state" in
command)
_describe 'tri command' commands
;;
args)
case ${words[1]} in
address|addr)
_values 'subcommand' 'new' 'list' 'balance'
;;
msg|message|messages)
_values 'subcommand' 'inbox' 'outbox' 'send' 'anon' 'keys' 'enable' 'pubkey' 'unlock'
;;
esac
;;
esac
}
_tri "$@"
-32
View File
@@ -1,32 +0,0 @@
# /etc/tri/nodes.conf — Triangles node configuration
#
# Shared by Hermes and Krystie. Both agents on DNS2 tunnel RPC
# to the trianglesd node on DNS3 via SSH.
#
# Node: DNS3 (100.81.59.99)
# ─── Connection ──────────────────────────────────────────────────────────────
# RPC is only accessible on localhost at the node, so we SSH-tunnel
TRI_SSH_HOST="your-node-ip-here"
TRI_SSH_USER="root"
# RPC credentials (as set in triangles.conf on the node)
TRI_RPC_HOST="127.0.0.1"
TRI_RPC_PORT="19112"
TRI_RPC_USER="your-rpc-user-here"
TRI_RPC_PASS="your-rpc-password-here"
# ─── Wallet ──────────────────────────────────────────────────────────────────
# Wallet passphrase for unlocking (needed for messaging + sending)
# Leave empty if wallet is unencrypted or set via env var TRI_WALLET_PASSPHRASE
# TRI_WALLET_PASSPHRASE=""
# Default sender address for messages (set after creating addresses)
# TRI_DEFAULT_FROM=""
# ─── Agent Addresses ─────────────────────────────────────────────────────────
# When agents have their own TRI addresses, register them here:
# HERMES_TRI_ADDR="T..."
# KRYSTIE_TRI_ADDR="T..."
-691
View File
@@ -1,691 +0,0 @@
#!/usr/bin/env bash
#
# tri — Cryptographic Triangles command interface
#
# A friendly wrapper around trianglesd RPC for both human and agent use.
# Designed for Hermes, Krystie, and Sami to manage TRI wallets, monitor
# nodes, and communicate via the built-in secure messaging system.
#
# Config: /etc/tri/nodes.conf (or ~/.config/tri/nodes.conf)
# Completion: /etc/bash_completion.d/tri-completion.bash
#
# Usage: tri <command> [subcommand] [args]
# tri Status overview
# tri help Full command list
# tri status Detailed node status
# tri balance Wallet balance
# tri peers Connected peers
# tri stake Staking info
# tri address new Generate new wallet address
# tri address list List wallet addresses
# tri address balance Per-address balances
# tri send <addr> <amt> [memo] Send TRI
# tri tx [N] Recent N transactions (default 10)
# tri tx <txid> Transaction details
# tri msg inbox Secure message inbox
# tri msg outbox Sent messages
# tri msg send <from> <to> <msg> Send encrypted message
# tri msg anon <to> <msg> Send anonymous message
# tri msg keys List messaging keys
# tri msg enable Enable secure messaging
# tri msg pubkey <addr> Get public key for address
# tri msg unlock [secs] Unlock wallet for messaging (default 60s)
# tri raw <method> [params...] Raw RPC passthrough
#
set -euo pipefail
# ─── Config ──────────────────────────────────────────────────────────────────
TRI_CONFIG="/etc/tri/nodes.conf"
[[ -f "$HOME/.config/tri/nodes.conf" ]] && TRI_CONFIG="$HOME/.config/tri/nodes.conf"
# Defaults (overridden by config file)
TRI_RPC_HOST="127.0.0.1"
TRI_RPC_PORT="19112"
TRI_RPC_USER=""
TRI_RPC_PASS=""
TRI_SSH_HOST="" # If set, RPC calls are tunneled via SSH to this host
TRI_SSH_USER="root"
TRI_WALLET_PASSPHRASE="" # For unlocking wallet when sending/messages
TRI_DEFAULT_FROM="" # Default sender address for messages
# Load config
if [[ -f "$TRI_CONFIG" ]]; then
source "$TRI_CONFIG"
fi
# Allow env overrides
[[ -n "${TRI_RPC_HOST_ENV:-}" ]] && TRI_RPC_HOST="$TRI_RPC_HOST_ENV"
[[ -n "${TRI_RPC_PORT_ENV:-}" ]] && TRI_RPC_PORT="$TRI_RPC_PORT_ENV"
[[ -n "${TRI_SSH_HOST_ENV:-}" ]] && TRI_SSH_HOST="$TRI_SSH_HOST_ENV"
# ─── Colors ──────────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then
C_RESET="\033[0m"
C_BOLD="\033[1m"
C_DIM="\033[2m"
C_RED="\033[31m"
C_GREEN="\033[32m"
C_YELLOW="\033[33m"
C_BLUE="\033[34m"
C_CYAN="\033[36m"
C_MAGENTA="\033[35m"
else
C_RESET=""; C_BOLD=""; C_DIM=""; C_RED=""; C_GREEN=""; C_YELLOW=""
C_BLUE=""; C_CYAN=""; C_MAGENTA=""
fi
# ─── Helpers ─────────────────────────────────────────────────────────────────
# Core RPC call function. Executes JSON-RPC against the node.
# Usage: _tri_rpc <method> [param1] [param2] ...
_tri_rpc() {
local method="$1"; shift
local params="[]"
if [[ $# -gt 0 ]]; then
# Build JSON params array
local json_params=()
for p in "$@"; do
# Try to detect numbers and booleans
if [[ "$p" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
json_params+=("$p")
elif [[ "$p" == "true" || "$p" == "false" || "$p" == "null" ]]; then
json_params+=("\"$p\"")
else
# Escape for JSON string
local escaped="${p//\\/\\\\}"
escaped="${escaped//\"/\\\"}"
json_params+=("\"$escaped\"")
fi
done
params="[$(IFS=,; echo "${json_params[*]}")]"
fi
local payload="{\"jsonrpc\":\"1.0\",\"id\":\"tri\",\"method\":\"$method\",\"params\":$params}"
if [[ -n "$TRI_SSH_HOST" ]]; then
# Tunnel via SSH
local auth="$TRI_RPC_USER:$TRI_RPC_PASS"
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no \
"${TRI_SSH_USER}@${TRI_SSH_HOST}" \
"curl -s --connect-timeout 10 http://127.0.0.1:${TRI_RPC_PORT}/ \
-u '${auth}' \
-H 'Content-Type: application/json' \
-d '${payload//\'/\'\\\'\'}'" 2>/dev/null
else
# Local connection
curl -s --connect-timeout 10 "http://${TRI_RPC_HOST}:${TRI_RPC_PORT}/" \
-u "${TRI_RPC_USER}:${TRI_RPC_PASS}" \
-H 'Content-Type: application/json' \
-d "$payload" 2>/dev/null
fi
}
# Pretty RPC call — extracts .result and pretty-prints JSON
# Usage: _tri_rpc_pretty <method> [param1] [param2] ...
_tri_rpc_pretty() {
local raw
raw=$(_tri_rpc "$@")
if [[ -z "$raw" ]]; then
echo -e "${C_RED}Error: No response from node${C_RESET}" >&2
return 1
fi
# Check for error
local err
err=$(echo "$raw" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error',{}).get('message','') if d.get('error') else '',end='')" 2>/dev/null || echo "")
if [[ -n "$err" ]]; then
echo -e "${C_RED}RPC Error: ${err}${C_RESET}" >&2
return 1
fi
echo "$raw" | python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin).get('result',''),indent=2))" 2>/dev/null
}
# Raw RPC call — print full JSON response as-is
_tri_rpc_raw() {
_tri_rpc "$@"
}
# Extract a single field from RPC result
# Usage: _tri_rpc_field <method> <field> [params...]
_tri_rpc_field() {
local method="$1"; shift
local field="$1"; shift
_tri_rpc "$method" "$@" | python3 -c "
import sys,json
d=json.load(sys.stdin)
r=d.get('result',{})
if isinstance(r,dict):
print(r.get('$field',''))
else:
print(r)
" 2>/dev/null
}
# Extract multiple fields
_tri_rpc_fields() {
local method="$1"; shift
_tri_rpc "$method" "$@" | python3 -c "
import sys,json
d=json.load(sys.stdin)
r=d.get('result',{})
if isinstance(r, dict):
for k,v in r.items():
if isinstance(v,(str,int,float,bool)) or v is None:
print(f'{k}: {v}')
" 2>/dev/null
}
# Unlock wallet for messaging
_tri_unlock() {
local duration="${1:-60}"
if [[ -z "$TRI_WALLET_PASSPHRASE" ]]; then
echo -e "${C_YELLOW}Warning: TRI_WALLET_PASSPHRASE not set in config${C_RESET}" >&2
return 1
fi
_tri_rpc walletpassphrase "$TRI_WALLET_PASSPHRASE" "$duration" >/dev/null 2>&1
}
# ─── Commands: Info ──────────────────────────────────────────────────────────
cmd_status() {
echo -e "${C_BOLD}${C_CYAN}Triangles Node Status${C_RESET}"
echo -e "${C_DIM}$(date -u '+%Y-%m-%d %H:%M:%S UTC')${C_RESET}"
echo ""
local info
info=$(_tri_rpc getinfo 2>/dev/null)
if [[ -z "$info" ]]; then
echo -e "${C_RED}Cannot connect to node${C_RESET}"
if [[ -n "$TRI_SSH_HOST" ]]; then
echo -e " Target: ${TRI_SSH_USER}@${TRI_SSH_HOST} → RPC ${TRI_RPC_PORT}"
else
echo -e " Target: ${TRI_RPC_HOST}:${TRI_RPC_PORT}"
fi
return 1
fi
echo "$info" | python3 -c "
import sys,json
d=json.load(sys.stdin)['result']
print(f\" Version: {d.get('version','?')}\")
print(f\" Blocks: {d.get('blocks','?'):,}\")
print(f\" Connections: {d.get('connections','?')}\")
print(f\" Balance: {d.get('balance',0):.4f} TRI\")
print(f\" Stake: {d.get('stake',0):.4f} TRI\")
print(f\" Money Supply: {d.get('moneysupply',0):,.2f} TRI\")
print(f\" Difficulty: {d.get('difficulty','?')}\")
print(f\" Testnet: {d.get('testnet',False)}\")
" 2>/dev/null
# Peer summary
local peer_count
peer_count=$(_tri_rpc_field getconnectioncount "result" 2>/dev/null || echo "?")
echo ""
echo -e " ${C_DIM}Node: ${TRI_SSH_HOST:-${TRI_RPC_HOST}}:${TRI_RPC_PORT}${C_RESET}"
}
cmd_balance() {
local balance
balance=$(_tri_rpc_field getbalance "balance" 2>/dev/null || echo "error")
if [[ "$balance" == "error" ]]; then
echo -e "${C_RED}Cannot connect to node${C_RESET}" >&2
return 1
fi
local stake
stake=$(_tri_rpc_field getinfo "stake" 2>/dev/null || echo "0")
echo -e "${C_BOLD}Wallet Balance${C_RESET}"
echo -e " Available: ${C_GREEN}${balance} TRI${C_RESET}"
echo -e " Staking: ${C_YELLOW}${stake} TRI${C_RESET}"
# UTXO count
local utxo_count
utxo_count=$(_tri_rpc listunspent 2>/dev/null | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('result',[])))" 2>/dev/null || echo "?")
[[ "$utxo_count" != "?" ]] && echo -e " UTXOs: ${utxo_count}"
}
cmd_peers() {
local raw
raw=$(_tri_rpc getpeerinfo 2>/dev/null)
echo -e "${C_BOLD}Connected Peers${C_RESET}"
echo "$raw" | python3 -c "
import sys,json
d=json.load(sys.stdin)
peers=d.get('result',[])
if not peers:
print(' (no peers connected)')
else:
for p in peers:
addr = p.get('addr','?')
subver = p.get('subver','?').replace('/','')
height = p.get('startingheight','?')
ping = p.get('pingtime',0)
if isinstance(ping,(int,float)) and ping > 0:
ping_ms = ping * 1000
print(f' {addr:30s} {subver:25s} height={height} ping={ping_ms:.0f}ms')
else:
print(f' {addr:30s} {subver:25s} height={height}')
print(f'\n Total: {len(peers)} peer(s)')
" 2>/dev/null
}
cmd_stake() {
echo -e "${C_BOLD}Staking Information${C_RESET}"
_tri_rpc_fields getstakinginfo 2>/dev/null | while read -r line; do
echo " $line"
done
}
# ─── Commands: Wallet ────────────────────────────────────────────────────────
cmd_address() {
local sub="${1:-list}"; shift || true
case "$sub" in
new)
local addr
addr=$(_tri_rpc_field getnewaddress "result" 2>/dev/null)
if [[ -n "$addr" ]]; then
echo "$addr"
else
echo -e "${C_RED}Failed to generate address${C_RESET}" >&2
return 1
fi
;;
list)
echo -e "${C_BOLD}Wallet Addresses${C_RESET}"
_tri_rpc getaddressesbyaccount "" 2>/dev/null | python3 -c "
import sys,json
d=json.load(sys.stdin)
addrs=d.get('result',[])
if not addrs:
print(' (no addresses)')
else:
for a in addrs:
print(f' {a}')
print(f'\n Total: {len(addrs)}')
" 2>/dev/null
;;
balance)
echo -e "${C_BOLD}Address Balances${C_RESET}"
_tri_rpc listaddressgroupings 2>/dev/null | python3 -c "
import sys,json
d=json.load(sys.stdin)
groups=d.get('result',[])
if not groups:
print(' (no address balances)')
else:
for group in groups:
for item in group:
addr=item[0] if isinstance(item,list) and len(item)>0 else '?'
amt=item[1] if isinstance(item,list) and len(item)>1 else '?'
print(f' {addr:40s} {amt} TRI')
" 2>/dev/null
;;
*)
echo -e "${C_RED}Unknown subcommand: $sub${C_RESET}" >&2
echo "Usage: tri address [new|list|balance]" >&2
return 1
;;
esac
}
cmd_send() {
if [[ $# -lt 2 ]]; then
echo -e "${C_RED}Usage: tri send <address> <amount> [memo]${C_RESET}" >&2
return 1
fi
local addr="$1"
local amount="$2"
local memo="${3:-}"
echo -e "${C_YELLOW}Sending ${amount} TRI to ${addr}...${C_RESET}"
local result
if [[ -n "$memo" ]]; then
result=$(_tri_rpc sendtoaddress "$addr" "$amount" "$memo" 2>/dev/null)
else
result=$(_tri_rpc sendtoaddress "$addr" "$amount" 2>/dev/null)
fi
local txid
txid=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result','') if d.get('result') else d.get('error',{}).get('message','FAILED'),end='')" 2>/dev/null)
if [[ "$txid" == "FAILED" ]] || [[ -z "$txid" ]]; then
echo -e "${C_RED}Send failed: $txid${C_RESET}" >&2
return 1
fi
echo -e "${C_GREEN}Sent! TXID: ${txid}${C_RESET}"
}
cmd_tx() {
if [[ $# -eq 0 ]]; then
# Recent transactions
echo -e "${C_BOLD}Recent Transactions${C_RESET}"
_tri_rpc listtransactions "*" 10 2>/dev/null | python3 -c "
import sys,json
d=json.load(sys.stdin)
txs=d.get('result',[])
if not txs:
print(' (no transactions)')
else:
for t in reversed(txs):
category = t.get('category','?')
amount = t.get('amount',0)
addr = t.get('address','?')
confirmations = t.get('confirmations',0)
txid = t.get('txid','?')
time = t.get('time',0)
from datetime import datetime
dt = datetime.fromtimestamp(time) if time else None
datestr = dt.strftime('%Y-%m-%d %H:%M') if dt else '???'
# Color by category
if category == 'receive' or category == 'generate' or category == 'mint':
amt_str = f'+{amount} TRI'
else:
amt_str = f'-{amount} TRI'
conf_str = f'{confirmations} conf' if confirmations > 0 else 'unconfirmed'
print(f' {datestr} {amt_str:>15s} {category:10s} {conf_str:>12s} {addr}')
print(f' {txid}')
" 2>/dev/null
else
# Transaction details
local txid="$1"
echo -e "${C_BOLD}Transaction: ${txid}${C_RESET}"
_tri_rpc_fields gettransaction "$txid" 2>/dev/null | while read -r line; do
echo " $line"
done
fi
}
# ─── Commands: Secure Messaging ──────────────────────────────────────────────
cmd_msg() {
local sub="${1:-inbox}"; shift || true
case "$sub" in
inbox)
# Unlock wallet first if passphrase is configured
if [[ -n "$TRI_WALLET_PASSPHRASE" ]]; then
_tri_unlock 60 2>/dev/null || true
fi
echo -e "${C_BOLD}${C_MAGENTA}Secure Message Inbox${C_RESET}"
_tri_rpc smsginbox "all" 2>/dev/null | python3 -c "
import sys,json
raw=json.load(sys.stdin)
d=raw.get('result',{})
msg = d.get('message')
count_str = d.get('result','0 messages shown.')
# Extract count from result string like 'N messages shown.'
try:
count = int(count_str.split()[0])
except:
count = 0
if count == 0 or msg is None:
print(' (inbox is empty)')
else:
# The daemon returns one message per RPC call (last one only).
# For full inbox dump, use: tri raw smsginbox all
frm = msg.get('from','?')
to = msg.get('to','?')
text = msg.get('text','(no text)')
sent = msg.get('sent','')
rcvd = msg.get('received','')
print(f' Latest message (of {count}):')
print(f' Sent: {sent}')
print(f' Received: {rcvd}')
print(f' From: {frm}')
print(f' To: {to}')
print(f' Text: {text[:200]}')
if count > 1:
print(f'')
print(f' ({count-1} more messages — use: tri raw smsginbox all)')
" 2>/dev/null
;;
outbox)
if [[ -n "$TRI_WALLET_PASSPHRASE" ]]; then
_tri_unlock 60 2>/dev/null || true
fi
echo -e "${C_BOLD}${C_MAGENTA}Sent Messages${C_RESET}"
_tri_rpc smsgoutbox "all" 2>/dev/null | python3 -c "
import sys,json
raw=json.load(sys.stdin)
d=raw.get('result',{})
msg = d.get('message')
count_str = d.get('result','0 sent messages shown.')
try:
count = int(count_str.split()[0])
except:
count = 0
if count == 0 or msg is None:
print(' (outbox is empty)')
else:
to = msg.get('to','?')
frm = msg.get('from','?')
text = msg.get('text','(no text)')
sent = msg.get('sent','')
print(f' Latest sent (of {count}):')
print(f' Sent: {sent}')
print(f' From: {frm}')
print(f' To: {to}')
print(f' Text: {text[:200]}')
if count > 1:
print(f'')
print(f' ({count-1} more — use: tri raw smsgoutbox all)')
" 2>/dev/null
;;
send)
if [[ $# -lt 3 ]]; then
echo -e "${C_RED}Usage: tri msg send <from_address> <to_address> <message>${C_RESET}" >&2
return 1
fi
local from_addr="$1"
local to_addr="$2"
shift 2
local message="$*"
# Unlock for send
if [[ -n "$TRI_WALLET_PASSPHRASE" ]]; then
_tri_unlock 60 2>/dev/null || true
fi
echo -e "${C_YELLOW}Sending encrypted message...${C_RESET}"
local result
result=$(_tri_rpc smsgsend "$from_addr" "$to_addr" "$message" 2>/dev/null)
local status
status=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',{}); print(r.get('result','') if isinstance(r,dict) else str(r),end='')" 2>/dev/null)
if [[ "$status" == "Sent." ]]; then
echo -e "${C_GREEN}Message sent to ${to_addr}${C_RESET}"
else
local err
err=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',{}); print(r.get('error','unknown error') if isinstance(r,dict) else str(r),end='')" 2>/dev/null)
echo -e "${C_RED}Send failed: ${err}${C_RESET}" >&2
return 1
fi
;;
anon)
if [[ $# -lt 2 ]]; then
echo -e "${C_RED}Usage: tri msg anon <to_address> <message>${C_RESET}" >&2
return 1
fi
local to_addr="$1"
shift
local message="$*"
echo -e "${C_YELLOW}Sending anonymous encrypted message...${C_RESET}"
local result
result=$(_tri_rpc smsgsendanon "$to_addr" "$message" 2>/dev/null)
local status
status=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',{}); print(r.get('result','') if isinstance(r,dict) else str(r),end='')" 2>/dev/null)
if [[ "$status" == "Sent." ]]; then
echo -e "${C_GREEN}Anonymous message sent to ${to_addr}${C_RESET}"
else
echo -e "${C_RED}Send failed${C_RESET}" >&2
return 1
fi
;;
keys)
echo -e "${C_BOLD}${C_MAGENTA}Messaging Keys${C_RESET}"
_tri_rpc smsglocalkeys "all" 2>/dev/null | python3 -c "
import sys,json
raw=json.load(sys.stdin)
d=raw.get('result',{})
if isinstance(d, dict):
key_line = d.get('key','')
count_line = d.get('result','')
if key_line:
print(f' {key_line}')
if count_line:
print(f' {count_line}')
elif isinstance(d, str):
print(f' {d}')
else:
print(' (no keys registered)')
" 2>/dev/null
;;
enable)
echo -e "${C_YELLOW}Enabling secure messaging...${C_RESET}"
_tri_rpc_pretty smsgenable 2>/dev/null
;;
pubkey)
if [[ $# -lt 1 ]]; then
echo -e "${C_RED}Usage: tri msg pubkey <address>${C_RESET}" >&2
return 1
fi
_tri_rpc_pretty smsggetpubkey "$1" 2>/dev/null
;;
unlock)
local duration="${1:-60}"
if [[ -z "$TRI_WALLET_PASSPHRASE" ]]; then
echo -e "${C_RED}TRI_WALLET_PASSPHRASE not set in config${C_RESET}" >&2
return 1
fi
_tri_rpc walletpassphrase "$TRI_WALLET_PASSPHRASE" "$duration" >/dev/null 2>&1
echo -e "${C_GREEN}Wallet unlocked for ${duration}s${C_RESET}"
;;
*)
echo -e "${C_RED}Unknown msg subcommand: $sub${C_RESET}" >&2
echo "Usage: tri msg [inbox|outbox|send|anon|keys|enable|pubkey|unlock]" >&2
return 1
;;
esac
}
# ─── Commands: Raw RPC ───────────────────────────────────────────────────────
cmd_raw() {
if [[ $# -eq 0 ]]; then
echo -e "${C_RED}Usage: tri raw <method> [params...]${C_RESET}" >&2
echo "Example: tri raw getblockhash 2200000" >&2
return 1
fi
_tri_rpc_pretty "$@"
}
# ─── Help ────────────────────────────────────────────────────────────────────
cmd_help() {
cat << 'EOF'
tri — Cryptographic Triangles Command Interface
INFO
tri Status overview (blocks, connections, balance)
tri status Detailed node status
tri balance Wallet balance + UTXO count
tri peers Connected peers with ping times
tri stake Staking information
WALLET
tri address new Generate new wallet address
tri address list List all wallet addresses
tri address balance Per-address balance breakdown
tri send <addr> <amt> [memo] Send TRI to address
tri tx [N] Recent N transactions (default 10)
tri tx <txid> Transaction details
SECURE MESSAGING
tri msg inbox Read inbox messages (wallet auto-unlocks)
tri msg outbox Read sent messages
tri msg send <from> <to> <msg> Send encrypted message
tri msg anon <to> <msg> Send anonymous message
tri msg keys List messaging keys
tri msg enable Enable secure messaging
tri msg pubkey <addr> Get public key for an address
tri msg unlock [secs] Unlock wallet for messaging (default 60s)
ADVANCED
tri raw <method> [params...] Raw RPC passthrough
tri help This help screen
CONFIG
/etc/tri/nodes.conf System-wide config
~/.config/tri/nodes.conf Per-user config override
AGENTS (Hermes, Krystie)
Both agents use the same config and can execute all commands.
For messaging between agents, each needs its own TRI address
registered in the wallet. Use 'tri msg keys' to verify.
EOF
}
# ─── Main ────────────────────────────────────────────────────────────────────
main() {
local cmd="${1:-status}"; shift || true
case "$cmd" in
status|info) cmd_status "$@" ;;
balance) cmd_balance "$@" ;;
peers) cmd_peers "$@" ;;
stake|staking) cmd_stake "$@" ;;
address|addr) cmd_address "$@" ;;
send) cmd_send "$@" ;;
tx|transactions) cmd_tx "$@" ;;
msg|message|messages) cmd_msg "$@" ;;
raw) cmd_raw "$@" ;;
help|-h|--help) cmd_help "$@" ;;
*)
echo -e "${C_RED}Unknown command: $cmd${C_RESET}" >&2
echo "Run 'tri help' for available commands" >&2
exit 1
;;
esac
}
main "$@"
-40
View File
@@ -1,40 +0,0 @@
# bash/zsh completion for tri command
# Install: source this file or place in /etc/bash_completion.d/
_tri_complete() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Top-level commands
local top_cmds="status balance peers stake address send tx msg raw help"
local addr_subcmds="new list balance"
local msg_subcmds="inbox outbox send anon keys enable pubkey unlock"
if [[ ${COMP_CWORD} -eq 1 ]]; then
COMPREPLY=($(compgen -W "${top_cmds}" -- "${cur}"))
return 0
fi
# Subcommand completion
if [[ ${COMP_CWORD} -eq 2 ]]; then
case "${COMP_WORDS[1]}" in
address|addr)
COMPREPLY=($(compgen -W "${addr_subcmds}" -- "${cur}"))
return 0
;;
msg|message|messages)
COMPREPLY=($(compgen -W "${msg_subcmds}" -- "${cur}"))
return 0
;;
esac
fi
# Address completion for send/msg send (would need wallet addresses in practice)
# For now, no further completion
return 0
}
complete -F _tri_complete tri
-391
View File
@@ -1,391 +0,0 @@
#!/usr/bin/env python3
"""
validate_onion_seeds.py - Cryptographic Triangles v3 onion address validator
Validates every .onion address in a triangles.conf (or any text file) against
the v3 hidden service checksum algorithm:
v3 onion = base32( version[2] || pubkey[32] || checksum[2] )
where checksum = SHA3-256( ".onion checksum" || version || pubkey )[:2]
and version = 0x03 0x00
A corrupted v3 onion (e.g. one character transposed) will have a valid base32
shape but a failing checksum. Tor rejects these with:
[warn] ed25519 validation failed
[warn] Service address has bad pubkey
[warn] Invalid onion hostname; rejecting
[notice] ... resolve failed. No more HSDir available to query.
This tool is designed to be run as a pre-flight check before deploying
a triangles.conf, and as a CI gate to prevent corrupted .onion addresses
from ever reaching production. It can also be used to audit an existing
config for inconsistencies against the hardcoded seed list in
src/onionseed.h.
USAGE
# Validate the production config
./validate_onion_seeds.py /root/.triangles/triangles.conf
# Validate multiple configs
./validate_onion_seeds.py /root/.triangles/triangles.conf \\
/root/.triangles-synctest/triangles.conf
# Audit a config against the hardcoded source-of-truth
./validate_onion_seeds.py /root/.triangles/triangles.conf \\
--against /root/triangles_v5/src/onionseed.h
# CI mode (exit 1 on any error)
./validate_onion_seeds.py /root/.triangles/triangles.conf --ci
EXIT CODES
0 all addresses valid, no warnings
1 one or more addresses failed validation
2 usage error / file not found
DETECTION CAPABILITIES
* Bad v3 checksum (1-2 char transposition, missing char, etc.)
* Truncated or extended .onion addresses
* Non-base32 characters in .onion
* Cross-config diff (or test vs production mismatch)
* addnode referencing a .onion that's not in the source seed list
BACKGROUND
During a from-zero sync test on 2026-06-21, the test daemon's Tor log
produced 4,842 "No more HSDir available" errors and 181 "ed25519
validation failed" warnings. Root cause: a 1-character transposition
(btb6 vs gtb6) in the test config's vmepp seed address. This tool
would have caught it in 0.1 seconds.
"""
import argparse
import base64
import hashlib
import os
import re
import sys
from pathlib import Path
# v3 onion constants
V3_VERSION = b'\x03\x00' # 2 bytes
V3_CHECKSUM_INPUT = b'.onion checksum' # 15 bytes
V3_PUBKEY_LENGTH = 32
V3_CHECKSUM_LENGTH = 2
V3_DECODED_LENGTH = 35 # 2 + 32 + 2 + ...wait that's 36
# Actually v3 onion base32-decodes to 35 bytes:
# 1 byte version (0x03) + 1 byte checksum-type (0x00) +
# 32 bytes pubkey + 2 bytes checksum -- no wait
# Per official spec: onion_address = base32(pubkey || checksum || version)
# Total = 32 (ed25519) + 2 (checksum) + 1 (version) = 35 bytes
# But some implementations use:
# version(2) || pubkey(32) || checksum(2) = 36
# The actual spec from rfc7686 says:
# onion_address = base32(PUBKEY || CHECKSUM || VERSION)
# PUBKEY = ed25519 public key (32 bytes)
# CHECKSUM = H(".onion checksum" || PUBKEY || VERSION)[:2]
# VERSION = 0x03
# So total = 32 + 2 + 1 = 35 bytes (not 36)
# We'll use the official spec (35 bytes)
# ANSI color codes (only if stdout is a TTY)
class C:
RESET = '\033[0m'
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
BOLD = '\033[1m'
DIM = '\033[2m'
@classmethod
def disable(cls):
for attr in dir(cls):
if attr.isupper() and not attr.startswith('_'):
setattr(cls, attr, '')
def decode_v3_onion(address: str) -> tuple[bool, str, bytes | None]:
"""
Validate a v3 onion address.
Returns:
(valid, reason, decoded_bytes_or_None)
"""
if not isinstance(address, str):
return False, f"not a string (got {type(address).__name__})", None
if not address.endswith('.onion'):
return False, "missing .onion suffix", None
onion_body = address[:-6] # strip .onion
expected_len = 56 # base32(35 bytes) = 56 chars
if len(onion_body) != expected_len:
return False, f"wrong length: {len(onion_body)} chars (expected {expected_len})", None
# Validate base32 alphabet
if not re.match(r'^[a-z2-7]+$', onion_body):
# Find first bad char
for i, c in enumerate(onion_body):
if not re.match(r'[a-z2-7]', c):
return False, f"non-base32 char '{c}' at position {i}", None
# Decode
try:
# Add padding
padding_needed = (8 - len(onion_body) % 8) % 8
decoded = base64.b32decode(onion_body.upper() + '=' * padding_needed)
except Exception as e:
return False, f"base32 decode failed: {e}", None
if len(decoded) != 35:
return False, f"decoded to {len(decoded)} bytes, expected 35", None
# v3 spec: PUBKEY(32) || CHECKSUM(2) || VERSION(1)
pubkey = decoded[0:32]
checksum = decoded[32:34]
version = decoded[34:35]
if version != b'\x03':
return False, f"version byte is 0x{version[0]:02x}, expected 0x03", decoded
# Compute expected checksum
expected_checksum = hashlib.sha3_256(
V3_CHECKSUM_INPUT + pubkey + version
).digest()[:2]
if checksum != expected_checksum:
return False, (
f"checksum mismatch: got 0x{checksum.hex()}, "
f"expected 0x{expected_checksum.hex()}"
), decoded
return True, "valid v3 onion", decoded
def parse_config_addnodes(config_path: Path) -> list[tuple[str, str, int]]:
"""
Extract (line_no, address, port) tuples for all addnode= lines in a config.
Also handles addnode=onion:port and just addnode=onion (port defaults to 24112).
"""
addnodes = []
if not config_path.exists():
return addnodes
for line_no, raw_line in enumerate(config_path.read_text().splitlines(), 1):
line = raw_line.strip()
if not line or line.startswith('#'):
continue
m = re.match(r'^addnode=([^:]+)(?::(\d+))?$', line)
if m:
addr = m.group(1)
port = int(m.group(2)) if m.group(2) else 24112
addnodes.append((line_no, addr, port))
return addnodes
def parse_source_seeds(source_path: Path) -> set[str]:
"""
Extract all .onion addresses from the hardcoded seed list in onionseed.h.
Matches the strMainNetOnionSeed and strTestNetOnionSeed arrays.
"""
seeds = set()
if not source_path.exists():
return seeds
for m in re.finditer(r'"([a-z2-7]{56}\.onion)"', source_path.read_text()):
seeds.add(m.group(1))
return seeds
def levenshtein_1(a: str, b: str) -> int:
"""Return number of positions where a and b differ (assumes same length)."""
if len(a) != len(b):
return -1
return sum(1 for x, y in zip(a, b) if x != b.count(x))
def find_near_match(target: str, candidates: set[str]) -> str | None:
"""Find a candidate that's 1-2 char different from target (for diff hints)."""
for c in candidates:
if len(c) == len(target):
d = sum(1 for x, y in zip(c, target) if x != y)
if 0 < d <= 2:
return c
return None
def colorize(s: str, color: str, enabled: bool) -> str:
return f"{color}{s}{C.RESET}" if enabled else s
def validate_config(
config_path: Path,
source_seeds: set[str] | None = None,
other_configs: dict[Path, set[str]] | None = None,
use_color: bool = True,
) -> tuple[int, int, int, int]:
"""
Validate all .onion addresses in a config file.
Returns:
(valid_count, invalid_count, missing_count, extra_count)
"""
addnodes = parse_config_addnodes(config_path)
if not addnodes:
print(colorize(f" (no addnode= entries found in {config_path})",
C.YELLOW, use_color))
return (0, 0, 0, 0)
valid = invalid = 0
invalid_addrs = set()
print(colorize(f"\n=== {config_path} ===", C.BOLD + C.BLUE, use_color))
print(colorize(f" {len(addnodes)} addnode entries found", C.DIM, use_color))
for line_no, addr, port in addnodes:
ok, reason, _ = decode_v3_onion(addr)
if ok:
print(f" {colorize('[OK]', C.GREEN, use_color):>14} line {line_no:>4} {addr}")
valid += 1
else:
print(f" {colorize('[BAD]', C.RED, use_color):>14} line {line_no:>4} {addr}")
print(f" {'':<14} {'':>4} reason: {reason}")
# Try to suggest a similar address
if source_seeds:
near = find_near_match(addr, source_seeds)
if near:
print(f" {'':<14} {'':>4} {colorize(f'did you mean: {near}?', C.YELLOW, use_color)}")
invalid += 1
invalid_addrs.add(addr)
# Cross-check against other configs
missing = extra = 0
if other_configs and source_seeds is not None:
config_addrs = {addr for _, addr, _ in addnodes}
# Note: this just reports on relationships; doesn't fail the test
for other_path, other_addrs in other_configs.items():
only_in_this = config_addrs - other_addrs - invalid_addrs
only_in_other = other_addrs - config_addrs
if only_in_this:
print(colorize(
f"\n {colorize('[DIFF]', C.YELLOW, use_color)} addresses only in {config_path.name} "
f"(missing from {other_path.name}):",
C.YELLOW, use_color))
for a in sorted(only_in_this):
print(f" {a}")
extra += len(only_in_this)
if only_in_other:
print(colorize(
f"\n {colorize('[DIFF]', C.YELLOW, use_color)} addresses only in {other_path.name} "
f"(missing from {config_path.name}):",
C.YELLOW, use_color))
for a in sorted(only_in_other):
print(f" {a}")
missing += len(only_in_other)
return valid, invalid, missing, extra
def main():
parser = argparse.ArgumentParser(
description="Validate v3 .onion addresses in Triangles config files",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
'configs',
nargs='+',
type=Path,
help='One or more triangles.conf files to validate',
)
parser.add_argument(
'--against',
type=Path,
default=None,
help='Path to src/onionseed.h to use as source of truth for diff hints',
)
parser.add_argument(
'--ci',
action='store_true',
help='CI mode: exit 1 if any address fails validation',
)
parser.add_argument(
'--no-color',
action='store_true',
help='Disable colored output (also auto-disabled when stdout is not a TTY)',
)
args = parser.parse_args()
# Color detection
use_color = not args.no_color and sys.stdout.isatty()
if not use_color:
C.disable()
# Validate inputs exist
for p in args.configs:
if not p.exists():
print(colorize(f"ERROR: file not found: {p}", C.RED, use_color),
file=sys.stderr)
return 2
# Load source seeds if provided
source_seeds = None
if args.against:
if not args.against.exists():
print(colorize(f"WARNING: source seed file not found: {args.against}",
C.YELLOW, use_color), file=sys.stderr)
else:
source_seeds = parse_source_seeds(args.against)
print(colorize(
f"Loaded {len(source_seeds)} hardcoded seeds from {args.against}",
C.DIM, use_color))
# Pre-load all configs for cross-checking
all_configs: dict[Path, set[str]] = {}
for p in args.configs:
addnodes = parse_config_addnodes(p)
all_configs[p] = {addr for _, addr, _ in addnodes}
# Validate each config
total_valid = total_invalid = total_missing = total_extra = 0
for p in args.configs:
if len(args.configs) > 1:
other = {k: v for k, v in all_configs.items() if k != p}
else:
other = None
v, i, m, e = validate_config(p, source_seeds, other, use_color)
total_valid += v
total_invalid += i
total_missing += m
total_extra += e
# Summary
print(colorize("\n=== SUMMARY ===", C.BOLD, use_color))
print(f" Valid: {colorize(str(total_valid), C.GREEN, use_color)}")
if total_invalid:
print(f" Invalid: {colorize(str(total_invalid), C.RED, use_color)}")
else:
print(f" Invalid: {total_invalid}")
if total_missing:
print(f" Missing: {colorize(str(total_missing), C.YELLOW, use_color)} "
f"(in other configs, not this one)")
if total_extra:
print(f" Extra: {colorize(str(total_extra), C.YELLOW, use_color)} "
f"(in this config, not others)")
if total_invalid == 0 and total_missing == 0:
print(colorize("\n All addresses valid.", C.GREEN + C.BOLD, use_color))
return 0
else:
print(colorize(
f"\n {total_invalid} address(es) failed v3 onion checksum validation.",
C.RED + C.BOLD, use_color))
if args.ci:
return 1
return 1 if total_invalid else 0
if __name__ == '__main__':
sys.exit(main())
-163
View File
@@ -1,163 +0,0 @@
#!/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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

+5 -5
View File
@@ -1,6 +1,6 @@
name: triangles
base: core22
version: '6.2.4'
version: '5.7.6'
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/v6.2.4/Cryptographic-Triangles-v6.2.4-linux-x64-qt
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-linux-x64-qt
source-type: file
organize:
Cryptographic-Triangles-v6.2.4-linux-x64-qt: bin/triangles-qt
Cryptographic-Triangles-v5.7.6-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/v6.2.4/Cryptographic-Triangles-v6.2.4-linux-x64-daemon
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-linux-x64-daemon
source-type: file
organize:
Cryptographic-Triangles-v6.2.4-linux-x64-daemon: bin/trianglesd
Cryptographic-Triangles-v5.7.6-linux-x64-daemon: bin/trianglesd
desktop-entry:
plugin: dump
+12 -721
View File
@@ -40,10 +40,8 @@ target_include_directories(json_compat INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/js
set(CORE_SOURCES
addrman.cpp
bootstrap.cpp
checkpointpublisher.cpp
checkpoints.cpp
crypter.cpp
hdwallet.cpp
crypto_ecdh.cpp
crypto_ecdsa.cpp
db.cpp
@@ -86,7 +84,6 @@ set(CORE_SOURCES
tor/onion_v3.cpp
tor/tor_process.cpp
tor/tor_embedded.cpp
i2p/i2p_embedded.cpp
)
# Scrypt assembly — platform-specific
@@ -108,39 +105,12 @@ 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"
"${CMAKE_CURRENT_SOURCE_DIR}/tor"
"${CMAKE_CURRENT_SOURCE_DIR}/i2p"
"${CMAKE_BINARY_DIR}/generated" # for build.h
)
@@ -152,11 +122,13 @@ 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
@@ -205,118 +177,19 @@ if(USE_TOR_EMBEDDED)
# and its dependencies.
# Use --allow-multiple-definition because libtor.a may pull in static
# OpenSSL objects that duplicate the DLL import lib already linked above.
# These GNU ld options are not supported on macOS (which uses lld) —
# guard with NOT APPLE so the build still works on macOS.
# On macOS, the libevent/openssl/zlib install paths are not on the
# default linker search path. Pull them in from the standard
# homebrew locations so -levent / -lssl / -lssl etc. resolve.
if(APPLE)
target_link_directories(triangles_common PUBLIC
/opt/homebrew/opt/libevent/lib
/opt/homebrew/opt/openssl@3/lib
/opt/homebrew/opt/zlib/lib
)
endif()
if(NOT APPLE)
target_link_libraries(triangles_common PUBLIC
-Wl,--allow-multiple-definition
-Wl,--start-group
)
endif()
target_link_libraries(triangles_common PUBLIC
-Wl,--allow-multiple-definition
-Wl,--start-group
-ltor
-levent -levent_core -levent_extra -levent_openssl
-lssl -lcrypto -lz -llzma -lzstd
-Wl,--end-group
)
if(NOT APPLE)
target_link_libraries(triangles_common PUBLIC
-Wl,--end-group
)
endif()
if(WIN32)
target_link_libraries(triangles_common PUBLIC iphlpapi shlwapi crypt32)
endif()
endif()
# Optional: Embedded I2P (i2pd)
if(USE_I2P_EMBEDDED)
if(I2P_SOURCE_ROOT STREQUAL "")
set(I2P_SOURCE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/i2p/i2pd-src")
endif()
if(NOT EXISTS "${I2P_SOURCE_ROOT}/libi2pd/Crypto.h")
message(FATAL_ERROR
"USE_I2P_EMBEDDED=ON but i2pd source not found at ${I2P_SOURCE_ROOT}.\n"
"Run: git submodule update --init --recursive\n"
"Or set -DI2P_SOURCE_ROOT=/path/to/i2pd")
endif()
target_compile_definitions(triangles_common PUBLIC ENABLE_I2P_EMBEDDED)
target_include_directories(triangles_common PUBLIC
"${I2P_SOURCE_ROOT}"
"${I2P_SOURCE_ROOT}/libi2pd"
"${I2P_SOURCE_ROOT}/libi2pd_client"
"${I2P_SOURCE_ROOT}/i18n"
)
# i2pd builds as two static libraries: libi2pd.a (core router) and
# libi2pd_client.a (SAM, SOCKS, tunnels, client context). Both are needed.
# i2pd's own Makefile.mingw links by full static .a paths rather than
# -l flags because MinGW's linker is single-pass and CMake imported
# targets (Boost::) may not exist on MSYS2. We follow the same pattern:
# link the archives, then their Boost/zlib deps as full paths, then
# the archives again to resolve the second-pass references.
target_link_libraries(triangles_common PUBLIC
"${I2P_SOURCE_ROOT}/libi2pdclient.a"
"${I2P_SOURCE_ROOT}/libi2pd.a"
"${I2P_SOURCE_ROOT}/libi2pdlang.a"
)
if(WIN32)
# MinGW/MSYS2: Boost:: CMake imported targets are unreliable here.
# Use find_library to locate the actual .a/.dll files. Some Boost
# libs (e.g. boost_system) are header-only in newer versions and
# won't have a .a file at all — that's fine, we skip them.
if(NOT MINGW_PREFIX)
if(DEFINED ENV{MINGW_PREFIX})
set(MINGW_PREFIX "$ENV{MINGW_PREFIX}")
else()
set(MINGW_PREFIX "/mingw64")
endif()
endif()
find_library(I2P_BOOST_FS NAMES boost_filesystem-mt boost_filesystem libboost_filesystem-mt HINTS "${MINGW_PREFIX}/lib")
find_library(I2P_BOOST_PO NAMES boost_program_options-mt boost_program_options libboost_program_options-mt HINTS "${MINGW_PREFIX}/lib")
find_library(I2P_BOOST_SYS NAMES boost_system-mt boost_system libboost_system-mt HINTS "${MINGW_PREFIX}/lib")
find_library(I2P_SSL NAMES ssl libssl HINTS "${MINGW_PREFIX}/lib")
find_library(I2P_CRYPTO NAMES crypto libcrypto HINTS "${MINGW_PREFIX}/lib")
find_library(I2P_Z NAMES z libz zlib HINTS "${MINGW_PREFIX}/lib")
set(I2P_WIN_LIBS "")
foreach(lib I2P_BOOST_FS I2P_BOOST_PO I2P_BOOST_SYS I2P_SSL I2P_CRYPTO I2P_Z)
if(${lib})
list(APPEND I2P_WIN_LIBS "${${lib}}")
message(STATUS " I2P link: ${lib} = ${${lib}}")
else()
message(STATUS " I2P link: ${lib} = (not found, header-only?)")
endif()
endforeach()
target_link_libraries(triangles_common PUBLIC ${I2P_WIN_LIBS} -Wl,--allow-multiple-definition)
else()
target_link_libraries(triangles_common PUBLIC
Boost::program_options Boost::thread Boost::chrono
OpenSSL::SSL OpenSSL::Crypto
ZLIB::ZLIB
)
if(TARGET Boost::filesystem)
target_link_libraries(triangles_common PUBLIC Boost::filesystem)
endif()
if(TARGET Boost::system)
target_link_libraries(triangles_common PUBLIC Boost::system)
endif()
endif()
# Second pass: list archives again so linker resolves i2pd→Boost refs
# that were unsatisfied in the first left-to-right pass.
target_link_libraries(triangles_common PUBLIC
"${I2P_SOURCE_ROOT}/libi2pd.a"
"${I2P_SOURCE_ROOT}/libi2pdclient.a"
)
endif()
# Platform-specific libraries
if(WIN32)
target_link_libraries(triangles_common PUBLIC
@@ -361,36 +234,12 @@ target_precompile_headers(triangles_common PRIVATE
# ═══════════════════════════════════════════════════════════════════════════════
# 4. Headless daemon (trianglesd)
# ═══════════════════════════════════════════════════════════════════════════════
# `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(BUILD_DAEMON)
add_executable(trianglesd
noui.cpp
init.cpp
wallet.cpp
)
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)
@@ -400,39 +249,6 @@ elseif(BUILD_DAEMON)
endif()
endif()
# ═══════════════════════════════════════════════════════════════════════════════
# 4b. JSON-RPC client (triangles-cli)
#
# Self-contained: only links univalue + boost::asio + boost::program_options
# + boost::filesystem + OpenSSL (for base64 / future TLS). Does NOT link
# triangles_common, wallet, or net — keeps the binary small.
# ═══════════════════════════════════════════════════════════════════════════════
if(BUILD_CLI)
add_executable(triangles-cli
triangles-cli.cpp
)
# No Boost dependency: uses raw POSIX/Winsock sockets for HTTP. Only links
# the json_compat header-only shim and the platform's native socket lib
# (Winsock ws2_32 on Windows; libc on POSIX). Keeps the binary small and
# avoids per-platform Boost linking pain (MSYS2 uses versioned -mt- names;
# Homebrew doesn't ship the boost_system CMake config).
target_link_libraries(triangles-cli
PRIVATE
json_compat
)
if(WIN32)
set_target_properties(triangles-cli PROPERTIES SUFFIX ".exe")
target_link_libraries(triangles-cli PRIVATE ws2_32)
endif()
if(MSVC)
set_target_properties(triangles-cli PROPERTIES
VS_WINRT_COMPONENT "console"
)
endif()
endif()
# ═══════════════════════════════════════════════════════════════════════════════
# 5. Qt5 GUI wallet (triangles-qt)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -490,8 +306,6 @@ if(BUILD_QT)
qt/trianglesunits.cpp
qt/qvaluecombobox.cpp
qt/askpassphrasedialog.cpp
qt/hdseeddialog.cpp
qt/outlinedlabel.cpp
qt/notificator.cpp
qt/qtipcserver.cpp
qt/rpcconsole.cpp
@@ -571,7 +385,6 @@ if(BUILD_QT)
Qt5::Core
Qt5::Gui
Qt5::Widgets
Qt5::Network
)
# Optional: D-Bus notifications (Linux)
@@ -635,18 +448,6 @@ if(BUILD_TESTS)
file(GLOB TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/test/*.cpp")
# Exclude miner_tests.cpp (never ported from Bitcoin)
list(FILTER TEST_SOURCES EXCLUDE REGEX "miner_tests\\.cpp$")
# Exclude the standalone chaindb test driver — it gets its own target
# because it needs to run without the TestingSetup global fixture.
list(FILTER TEST_SOURCES EXCLUDE REGEX "chaindb_equivalence_tests_main\\.cpp$")
# 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}
@@ -657,7 +458,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
@@ -670,515 +471,5 @@ 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
# open the real chain DB and lock it for the process). Sets a fresh
# temp -datadir via its own global fixture, then runs the
# chaindb_equivalence_tests suite.
add_executable(test_chaindb_equivalence
"${CMAKE_CURRENT_SOURCE_DIR}/test/chaindb_equivalence_tests_main.cpp"
# wallet.cpp provides the CWallet symbols that triangles_common
# (txdb-rocksdb, net, etc.) references, even though the chaindb
# tests themselves don't use the wallet.
wallet.cpp
)
target_include_directories(test_chaindb_equivalence PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/test"
"${CMAKE_CURRENT_SOURCE_DIR}/leveldb/include"
)
target_link_libraries(test_chaindb_equivalence PRIVATE
triangles_common
Boost::unit_test_framework
)
add_test(NAME chaindb_equivalence_tests
COMMAND test_chaindb_equivalence --log_level=test_suite)
# ── Standalone snapshotnet P2P tests ────────────────────────────────────
# Same rationale as test_chaindb_equivalence: snapshotnet needs filesystem
# and threading globals and its own tmp datadir fixture, which would
# conflict with test_triangles' heavy TestingSetup. Runs independently.
add_executable(test_snapshotnet
"${CMAKE_CURRENT_SOURCE_DIR}/test/snapshotnet_tests.cpp"
wallet.cpp
)
target_include_directories(test_snapshotnet PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/test"
"${CMAKE_CURRENT_SOURCE_DIR}/leveldb/include"
)
target_link_libraries(test_snapshotnet PRIVATE
triangles_common
Boost::unit_test_framework
)
add_test(NAME snapshotnet_tests
COMMAND test_snapshotnet --log_level=test_suite)
# ── Standalone chaindb runtime tests (CRocksTxDB wrapper layer) ─────────
# Exercises MakeChainDB / WipeChainDataDir / IsRocksDbChainBackend and
# the CRocksTxDB write/read/batch/iterator wrapper — the same code path
# the daemon uses when launched with `-chaindb=rocksdb`. The
# chaindb_equivalence_tests (above) only verify the byte-copy migration
# via the raw leveldb/rocksdb APIs; this one verifies the wrapper class.
add_executable(test_chaindb_runtime
"${CMAKE_CURRENT_SOURCE_DIR}/test/chaindb_runtime_tests.cpp"
wallet.cpp
)
target_include_directories(test_chaindb_runtime PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/test"
"${CMAKE_CURRENT_SOURCE_DIR}/leveldb/include"
)
target_link_libraries(test_chaindb_runtime PRIVATE
triangles_common
Boost::unit_test_framework
)
add_test(NAME chaindb_runtime_tests
COMMAND test_chaindb_runtime --log_level=test_suite)
endif()
# ═══════════════════════════════════════════════════════════════════════════════
# 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()
-5
View File
@@ -9,11 +9,6 @@
#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
-2
View File
@@ -67,8 +67,6 @@ 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());
}
+57 -70
View File
@@ -14,8 +14,6 @@
#include <openssl/opensslv.h>
#include <algorithm>
#include <cctype>
#include <limits>
#include <stdexcept>
#include <vector>
@@ -71,10 +69,16 @@ public:
throw bignum_error("CBigNum::CBigNum() : BN_new() returned NULL");
}
CBigNum(const CBigNum& b) : CBigNum()
CBigNum(const CBigNum& b)
{
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)
@@ -95,20 +99,21 @@ public:
const BIGNUM* get() const { return pbn; }
//CBigNum(char n) is not portable. Use 'signed char' or 'unsigned char'.
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); }
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); }
explicit CBigNum(const std::vector<unsigned char>& vch) : CBigNum()
explicit CBigNum(const std::vector<unsigned char>& vch)
{
pbn = BN_new();
setvch(vch);
}
@@ -211,23 +216,21 @@ public:
pch[1] = (nSize >> 16) & 0xff;
pch[2] = (nSize >> 8) & 0xff;
pch[3] = (nSize) & 0xff;
if (BN_mpi2bn(pch, static_cast<int>(p - pch), pbn) == nullptr)
throw bignum_error("CBigNum::setint64() : BN_mpi2bn failed");
BN_mpi2bn(pch, p - pch, pbn);
}
uint64_t getuint64() const
uint64_t getuint64()
{
const int nSize = BN_bn2mpi(pbn, nullptr);
if (nSize <= 4)
unsigned int nSize = BN_bn2mpi(pbn, nullptr);
if (nSize < 4)
return 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");
std::vector<unsigned char> vch(nSize);
BN_bn2mpi(pbn, &vch[0]);
if (vch.size() > 4)
vch[4] &= 0x7f;
uint64_t n = 0;
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);
for (unsigned int i = 0, j = vch.size()-1; i < sizeof(n) && j >= 4; i++, j--)
((unsigned char*)&n)[i] = vch[j];
return n;
}
@@ -255,8 +258,7 @@ public:
pch[1] = (nSize >> 16) & 0xff;
pch[2] = (nSize >> 8) & 0xff;
pch[3] = (nSize) & 0xff;
if (BN_mpi2bn(pch, static_cast<int>(p - pch), pbn) == nullptr)
throw bignum_error("CBigNum::setuint64() : BN_mpi2bn failed");
BN_mpi2bn(pch, p - pch, pbn);
}
void setuint256(uint256 n)
@@ -284,33 +286,29 @@ public:
pch[1] = (nSize >> 16) & 0xff;
pch[2] = (nSize >> 8) & 0xff;
pch[3] = (nSize >> 0) & 0xff;
if (BN_mpi2bn(pch, static_cast<int>(p - pch), pbn) == nullptr)
throw bignum_error("CBigNum::setuint256() : BN_mpi2bn failed");
BN_mpi2bn(pch, p - pch, pbn);
}
uint256 getuint256() const
{
const int mpiSize = BN_bn2mpi(pbn, nullptr);
if (mpiSize <= 4)
unsigned int nSize = BN_bn2mpi(pbn, nullptr);
if (nSize < 4)
return 0;
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;
std::vector<unsigned char> vch(nSize);
BN_bn2mpi(pbn, &vch[0]);
if (vch.size() > 4)
vch[4] &= 0x7f;
uint256 n = 0;
for (size_t i = 0; i < sizeof(n) && i + 4 < vch.size(); ++i)
reinterpret_cast<unsigned char*>(&n)[i] = vch[vch.size() - 1 - i];
for (unsigned int i = 0, j = vch.size()-1; i < sizeof(n) && j >= 4; i++, j--)
((unsigned char*)&n)[i] = vch[j];
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);
const uint32_t nSize = static_cast<uint32_t>(vch.size());
unsigned int nSize = vch.size();
// BIGNUM's byte stream format expects 4 bytes of
// big endian size data info at the front
vch2[0] = (nSize >> 24) & 0xff;
@@ -318,25 +316,20 @@ public:
vch2[2] = (nSize >> 8) & 0xff;
vch2[3] = (nSize >> 0) & 0xff;
// swap data to big endian
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");
reverse_copy(vch.begin(), vch.end(), vch2.begin() + 4);
BN_mpi2bn(&vch2[0], vch2.size(), pbn);
}
std::vector<unsigned char> getvch() const
{
const int mpiSize = BN_bn2mpi(pbn, nullptr);
if (mpiSize <= 4)
unsigned int nSize = BN_bn2mpi(pbn, nullptr);
if (nSize <= 4)
return std::vector<unsigned char>();
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;
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;
}
CBigNum& SetCompact(unsigned int nCompact)
@@ -347,20 +340,16 @@ 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;
if (BN_mpi2bn(vch.data(), static_cast<int>(vch.size()), pbn) == nullptr)
throw bignum_error("CBigNum::SetCompact() : BN_mpi2bn failed");
BN_mpi2bn(&vch[0], vch.size(), pbn);
return *this;
}
unsigned int GetCompact() const
{
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 nSize = BN_bn2mpi(pbn, nullptr);
std::vector<unsigned char> vch(nSize);
nSize -= 4;
BN_bn2mpi(pbn, &vch[0]);
unsigned int nCompact = nSize << 24;
if (nSize >= 1) nCompact |= (vch[4] << 16);
if (nSize >= 2) nCompact |= (vch[5] << 8);
@@ -372,7 +361,7 @@ public:
{
// skip 0x
const char* psz = str.c_str();
while (isspace(static_cast<unsigned char>(*psz)))
while (isspace(*psz))
psz++;
bool fNegative = false;
if (*psz == '-')
@@ -380,15 +369,15 @@ public:
fNegative = true;
psz++;
}
if (psz[0] == '0' && tolower(static_cast<unsigned char>(psz[1])) == 'x')
if (psz[0] == '0' && tolower(psz[1]) == 'x')
psz += 2;
while (isspace(static_cast<unsigned char>(*psz)))
while (isspace(*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(static_cast<unsigned char>(*psz)))
while (isxdigit(*psz))
{
*this <<= 4;
int n = phexdigit[(unsigned char)*psz++];
@@ -400,8 +389,6 @@ 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;
-263
View File
@@ -1,263 +0,0 @@
// BIP39 English wordlist (2048 words, canonical). Auto-generated; do not edit.
#ifndef TRIANGLES_BIP39_ENGLISH_H
#define TRIANGLES_BIP39_ENGLISH_H
static const char* const BIP39_WORDLIST_EN[2048] = {
"abandon","ability","able","about","above","absent","absorb","abstract",
"absurd","abuse","access","accident","account","accuse","achieve","acid",
"acoustic","acquire","across","act","action","actor","actress","actual",
"adapt","add","addict","address","adjust","admit","adult","advance",
"advice","aerobic","affair","afford","afraid","again","age","agent",
"agree","ahead","aim","air","airport","aisle","alarm","album",
"alcohol","alert","alien","all","alley","allow","almost","alone",
"alpha","already","also","alter","always","amateur","amazing","among",
"amount","amused","analyst","anchor","ancient","anger","angle","angry",
"animal","ankle","announce","annual","another","answer","antenna","antique",
"anxiety","any","apart","apology","appear","apple","approve","april",
"arch","arctic","area","arena","argue","arm","armed","armor",
"army","around","arrange","arrest","arrive","arrow","art","artefact",
"artist","artwork","ask","aspect","assault","asset","assist","assume",
"asthma","athlete","atom","attack","attend","attitude","attract","auction",
"audit","august","aunt","author","auto","autumn","average","avocado",
"avoid","awake","aware","away","awesome","awful","awkward","axis",
"baby","bachelor","bacon","badge","bag","balance","balcony","ball",
"bamboo","banana","banner","bar","barely","bargain","barrel","base",
"basic","basket","battle","beach","bean","beauty","because","become",
"beef","before","begin","behave","behind","believe","below","belt",
"bench","benefit","best","betray","better","between","beyond","bicycle",
"bid","bike","bind","biology","bird","birth","bitter","black",
"blade","blame","blanket","blast","bleak","bless","blind","blood",
"blossom","blouse","blue","blur","blush","board","boat","body",
"boil","bomb","bone","bonus","book","boost","border","boring",
"borrow","boss","bottom","bounce","box","boy","bracket","brain",
"brand","brass","brave","bread","breeze","brick","bridge","brief",
"bright","bring","brisk","broccoli","broken","bronze","broom","brother",
"brown","brush","bubble","buddy","budget","buffalo","build","bulb",
"bulk","bullet","bundle","bunker","burden","burger","burst","bus",
"business","busy","butter","buyer","buzz","cabbage","cabin","cable",
"cactus","cage","cake","call","calm","camera","camp","can",
"canal","cancel","candy","cannon","canoe","canvas","canyon","capable",
"capital","captain","car","carbon","card","cargo","carpet","carry",
"cart","case","cash","casino","castle","casual","cat","catalog",
"catch","category","cattle","caught","cause","caution","cave","ceiling",
"celery","cement","census","century","cereal","certain","chair","chalk",
"champion","change","chaos","chapter","charge","chase","chat","cheap",
"check","cheese","chef","cherry","chest","chicken","chief","child",
"chimney","choice","choose","chronic","chuckle","chunk","churn","cigar",
"cinnamon","circle","citizen","city","civil","claim","clap","clarify",
"claw","clay","clean","clerk","clever","click","client","cliff",
"climb","clinic","clip","clock","clog","close","cloth","cloud",
"clown","club","clump","cluster","clutch","coach","coast","coconut",
"code","coffee","coil","coin","collect","color","column","combine",
"come","comfort","comic","common","company","concert","conduct","confirm",
"congress","connect","consider","control","convince","cook","cool","copper",
"copy","coral","core","corn","correct","cost","cotton","couch",
"country","couple","course","cousin","cover","coyote","crack","cradle",
"craft","cram","crane","crash","crater","crawl","crazy","cream",
"credit","creek","crew","cricket","crime","crisp","critic","crop",
"cross","crouch","crowd","crucial","cruel","cruise","crumble","crunch",
"crush","cry","crystal","cube","culture","cup","cupboard","curious",
"current","curtain","curve","cushion","custom","cute","cycle","dad",
"damage","damp","dance","danger","daring","dash","daughter","dawn",
"day","deal","debate","debris","decade","december","decide","decline",
"decorate","decrease","deer","defense","define","defy","degree","delay",
"deliver","demand","demise","denial","dentist","deny","depart","depend",
"deposit","depth","deputy","derive","describe","desert","design","desk",
"despair","destroy","detail","detect","develop","device","devote","diagram",
"dial","diamond","diary","dice","diesel","diet","differ","digital",
"dignity","dilemma","dinner","dinosaur","direct","dirt","disagree","discover",
"disease","dish","dismiss","disorder","display","distance","divert","divide",
"divorce","dizzy","doctor","document","dog","doll","dolphin","domain",
"donate","donkey","donor","door","dose","double","dove","draft",
"dragon","drama","drastic","draw","dream","dress","drift","drill",
"drink","drip","drive","drop","drum","dry","duck","dumb",
"dune","during","dust","dutch","duty","dwarf","dynamic","eager",
"eagle","early","earn","earth","easily","east","easy","echo",
"ecology","economy","edge","edit","educate","effort","egg","eight",
"either","elbow","elder","electric","elegant","element","elephant","elevator",
"elite","else","embark","embody","embrace","emerge","emotion","employ",
"empower","empty","enable","enact","end","endless","endorse","enemy",
"energy","enforce","engage","engine","enhance","enjoy","enlist","enough",
"enrich","enroll","ensure","enter","entire","entry","envelope","episode",
"equal","equip","era","erase","erode","erosion","error","erupt",
"escape","essay","essence","estate","eternal","ethics","evidence","evil",
"evoke","evolve","exact","example","excess","exchange","excite","exclude",
"excuse","execute","exercise","exhaust","exhibit","exile","exist","exit",
"exotic","expand","expect","expire","explain","expose","express","extend",
"extra","eye","eyebrow","fabric","face","faculty","fade","faint",
"faith","fall","false","fame","family","famous","fan","fancy",
"fantasy","farm","fashion","fat","fatal","father","fatigue","fault",
"favorite","feature","february","federal","fee","feed","feel","female",
"fence","festival","fetch","fever","few","fiber","fiction","field",
"figure","file","film","filter","final","find","fine","finger",
"finish","fire","firm","first","fiscal","fish","fit","fitness",
"fix","flag","flame","flash","flat","flavor","flee","flight",
"flip","float","flock","floor","flower","fluid","flush","fly",
"foam","focus","fog","foil","fold","follow","food","foot",
"force","forest","forget","fork","fortune","forum","forward","fossil",
"foster","found","fox","fragile","frame","frequent","fresh","friend",
"fringe","frog","front","frost","frown","frozen","fruit","fuel",
"fun","funny","furnace","fury","future","gadget","gain","galaxy",
"gallery","game","gap","garage","garbage","garden","garlic","garment",
"gas","gasp","gate","gather","gauge","gaze","general","genius",
"genre","gentle","genuine","gesture","ghost","giant","gift","giggle",
"ginger","giraffe","girl","give","glad","glance","glare","glass",
"glide","glimpse","globe","gloom","glory","glove","glow","glue",
"goat","goddess","gold","good","goose","gorilla","gospel","gossip",
"govern","gown","grab","grace","grain","grant","grape","grass",
"gravity","great","green","grid","grief","grit","grocery","group",
"grow","grunt","guard","guess","guide","guilt","guitar","gun",
"gym","habit","hair","half","hammer","hamster","hand","happy",
"harbor","hard","harsh","harvest","hat","have","hawk","hazard",
"head","health","heart","heavy","hedgehog","height","hello","helmet",
"help","hen","hero","hidden","high","hill","hint","hip",
"hire","history","hobby","hockey","hold","hole","holiday","hollow",
"home","honey","hood","hope","horn","horror","horse","hospital",
"host","hotel","hour","hover","hub","huge","human","humble",
"humor","hundred","hungry","hunt","hurdle","hurry","hurt","husband",
"hybrid","ice","icon","idea","identify","idle","ignore","ill",
"illegal","illness","image","imitate","immense","immune","impact","impose",
"improve","impulse","inch","include","income","increase","index","indicate",
"indoor","industry","infant","inflict","inform","inhale","inherit","initial",
"inject","injury","inmate","inner","innocent","input","inquiry","insane",
"insect","inside","inspire","install","intact","interest","into","invest",
"invite","involve","iron","island","isolate","issue","item","ivory",
"jacket","jaguar","jar","jazz","jealous","jeans","jelly","jewel",
"job","join","joke","journey","joy","judge","juice","jump",
"jungle","junior","junk","just","kangaroo","keen","keep","ketchup",
"key","kick","kid","kidney","kind","kingdom","kiss","kit",
"kitchen","kite","kitten","kiwi","knee","knife","knock","know",
"lab","label","labor","ladder","lady","lake","lamp","language",
"laptop","large","later","latin","laugh","laundry","lava","law",
"lawn","lawsuit","layer","lazy","leader","leaf","learn","leave",
"lecture","left","leg","legal","legend","leisure","lemon","lend",
"length","lens","leopard","lesson","letter","level","liar","liberty",
"library","license","life","lift","light","like","limb","limit",
"link","lion","liquid","list","little","live","lizard","load",
"loan","lobster","local","lock","logic","lonely","long","loop",
"lottery","loud","lounge","love","loyal","lucky","luggage","lumber",
"lunar","lunch","luxury","lyrics","machine","mad","magic","magnet",
"maid","mail","main","major","make","mammal","man","manage",
"mandate","mango","mansion","manual","maple","marble","march","margin",
"marine","market","marriage","mask","mass","master","match","material",
"math","matrix","matter","maximum","maze","meadow","mean","measure",
"meat","mechanic","medal","media","melody","melt","member","memory",
"mention","menu","mercy","merge","merit","merry","mesh","message",
"metal","method","middle","midnight","milk","million","mimic","mind",
"minimum","minor","minute","miracle","mirror","misery","miss","mistake",
"mix","mixed","mixture","mobile","model","modify","mom","moment",
"monitor","monkey","monster","month","moon","moral","more","morning",
"mosquito","mother","motion","motor","mountain","mouse","move","movie",
"much","muffin","mule","multiply","muscle","museum","mushroom","music",
"must","mutual","myself","mystery","myth","naive","name","napkin",
"narrow","nasty","nation","nature","near","neck","need","negative",
"neglect","neither","nephew","nerve","nest","net","network","neutral",
"never","news","next","nice","night","noble","noise","nominee",
"noodle","normal","north","nose","notable","note","nothing","notice",
"novel","now","nuclear","number","nurse","nut","oak","obey",
"object","oblige","obscure","observe","obtain","obvious","occur","ocean",
"october","odor","off","offer","office","often","oil","okay",
"old","olive","olympic","omit","once","one","onion","online",
"only","open","opera","opinion","oppose","option","orange","orbit",
"orchard","order","ordinary","organ","orient","original","orphan","ostrich",
"other","outdoor","outer","output","outside","oval","oven","over",
"own","owner","oxygen","oyster","ozone","pact","paddle","page",
"pair","palace","palm","panda","panel","panic","panther","paper",
"parade","parent","park","parrot","party","pass","patch","path",
"patient","patrol","pattern","pause","pave","payment","peace","peanut",
"pear","peasant","pelican","pen","penalty","pencil","people","pepper",
"perfect","permit","person","pet","phone","photo","phrase","physical",
"piano","picnic","picture","piece","pig","pigeon","pill","pilot",
"pink","pioneer","pipe","pistol","pitch","pizza","place","planet",
"plastic","plate","play","please","pledge","pluck","plug","plunge",
"poem","poet","point","polar","pole","police","pond","pony",
"pool","popular","portion","position","possible","post","potato","pottery",
"poverty","powder","power","practice","praise","predict","prefer","prepare",
"present","pretty","prevent","price","pride","primary","print","priority",
"prison","private","prize","problem","process","produce","profit","program",
"project","promote","proof","property","prosper","protect","proud","provide",
"public","pudding","pull","pulp","pulse","pumpkin","punch","pupil",
"puppy","purchase","purity","purpose","purse","push","put","puzzle",
"pyramid","quality","quantum","quarter","question","quick","quit","quiz",
"quote","rabbit","raccoon","race","rack","radar","radio","rail",
"rain","raise","rally","ramp","ranch","random","range","rapid",
"rare","rate","rather","raven","raw","razor","ready","real",
"reason","rebel","rebuild","recall","receive","recipe","record","recycle",
"reduce","reflect","reform","refuse","region","regret","regular","reject",
"relax","release","relief","rely","remain","remember","remind","remove",
"render","renew","rent","reopen","repair","repeat","replace","report",
"require","rescue","resemble","resist","resource","response","result","retire",
"retreat","return","reunion","reveal","review","reward","rhythm","rib",
"ribbon","rice","rich","ride","ridge","rifle","right","rigid",
"ring","riot","ripple","risk","ritual","rival","river","road",
"roast","robot","robust","rocket","romance","roof","rookie","room",
"rose","rotate","rough","round","route","royal","rubber","rude",
"rug","rule","run","runway","rural","sad","saddle","sadness",
"safe","sail","salad","salmon","salon","salt","salute","same",
"sample","sand","satisfy","satoshi","sauce","sausage","save","say",
"scale","scan","scare","scatter","scene","scheme","school","science",
"scissors","scorpion","scout","scrap","screen","script","scrub","sea",
"search","season","seat","second","secret","section","security","seed",
"seek","segment","select","sell","seminar","senior","sense","sentence",
"series","service","session","settle","setup","seven","shadow","shaft",
"shallow","share","shed","shell","sheriff","shield","shift","shine",
"ship","shiver","shock","shoe","shoot","shop","short","shoulder",
"shove","shrimp","shrug","shuffle","shy","sibling","sick","side",
"siege","sight","sign","silent","silk","silly","silver","similar",
"simple","since","sing","siren","sister","situate","six","size",
"skate","sketch","ski","skill","skin","skirt","skull","slab",
"slam","sleep","slender","slice","slide","slight","slim","slogan",
"slot","slow","slush","small","smart","smile","smoke","smooth",
"snack","snake","snap","sniff","snow","soap","soccer","social",
"sock","soda","soft","solar","soldier","solid","solution","solve",
"someone","song","soon","sorry","sort","soul","sound","soup",
"source","south","space","spare","spatial","spawn","speak","special",
"speed","spell","spend","sphere","spice","spider","spike","spin",
"spirit","split","spoil","sponsor","spoon","sport","spot","spray",
"spread","spring","spy","square","squeeze","squirrel","stable","stadium",
"staff","stage","stairs","stamp","stand","start","state","stay",
"steak","steel","stem","step","stereo","stick","still","sting",
"stock","stomach","stone","stool","story","stove","strategy","street",
"strike","strong","struggle","student","stuff","stumble","style","subject",
"submit","subway","success","such","sudden","suffer","sugar","suggest",
"suit","summer","sun","sunny","sunset","super","supply","supreme",
"sure","surface","surge","surprise","surround","survey","suspect","sustain",
"swallow","swamp","swap","swarm","swear","sweet","swift","swim",
"swing","switch","sword","symbol","symptom","syrup","system","table",
"tackle","tag","tail","talent","talk","tank","tape","target",
"task","taste","tattoo","taxi","teach","team","tell","ten",
"tenant","tennis","tent","term","test","text","thank","that",
"theme","then","theory","there","they","thing","this","thought",
"three","thrive","throw","thumb","thunder","ticket","tide","tiger",
"tilt","timber","time","tiny","tip","tired","tissue","title",
"toast","tobacco","today","toddler","toe","together","toilet","token",
"tomato","tomorrow","tone","tongue","tonight","tool","tooth","top",
"topic","topple","torch","tornado","tortoise","toss","total","tourist",
"toward","tower","town","toy","track","trade","traffic","tragic",
"train","transfer","trap","trash","travel","tray","treat","tree",
"trend","trial","tribe","trick","trigger","trim","trip","trophy",
"trouble","truck","true","truly","trumpet","trust","truth","try",
"tube","tuition","tumble","tuna","tunnel","turkey","turn","turtle",
"twelve","twenty","twice","twin","twist","two","type","typical",
"ugly","umbrella","unable","unaware","uncle","uncover","under","undo",
"unfair","unfold","unhappy","uniform","unique","unit","universe","unknown",
"unlock","until","unusual","unveil","update","upgrade","uphold","upon",
"upper","upset","urban","urge","usage","use","used","useful",
"useless","usual","utility","vacant","vacuum","vague","valid","valley",
"valve","van","vanish","vapor","various","vast","vault","vehicle",
"velvet","vendor","venture","venue","verb","verify","version","very",
"vessel","veteran","viable","vibrant","vicious","victory","video","view",
"village","vintage","violin","virtual","virus","visa","visit","visual",
"vital","vivid","vocal","voice","void","volcano","volume","vote",
"voyage","wage","wagon","wait","walk","wall","walnut","want",
"warfare","warm","warrior","wash","wasp","waste","water","wave",
"way","wealth","weapon","wear","weasel","weather","web","wedding",
"weekend","weird","welcome","west","wet","whale","what","wheat",
"wheel","when","where","whip","whisper","wide","width","wife",
"wild","will","win","window","wine","wing","wink","winner",
"winter","wire","wisdom","wise","wish","witness","wolf","woman",
"wonder","wood","wool","word","work","world","worry","worth",
"wrap","wreck","wrestle","wrist","write","wrong","yard","year",
"yellow","you","young","youth","zebra","zero","zone","zoo",
};
#endif
+166 -691
View File
File diff suppressed because it is too large Load Diff
+4 -24
View File
@@ -8,14 +8,13 @@
#include <vector>
#include <functional>
#include <filesystem>
#include <cstdint>
namespace Bootstrap {
// Bootstrap server configuration
inline constexpr const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org";
inline constexpr const char* BASE_PATH = "/";
inline constexpr int PORT = 443;
static const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org";
static const char* BASE_PATH = "/";
static const int PORT = 80;
// Progress callback: (bytesDownloaded, totalBytes)
typedef std::function<void(int64_t, int64_t)> ProgressCallback;
@@ -32,8 +31,7 @@ namespace Bootstrap {
ProgressCallback progressFn,
std::string& strError,
bool noProxy = false,
int portOverride = -1,
int64_t maxDownloadBytes = 4LL * 1024 * 1024 * 1024);
int portOverride = -1);
// Fetch the file manifest (list of relative paths to download)
bool FetchFileList(const std::string& host,
@@ -48,23 +46,6 @@ namespace Bootstrap {
ProgressCallback progressFn,
std::string& strError);
// 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 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);
// Snapshot manifest (parsed from snapshot.manifest in bootstrap archive)
struct SnapshotManifest {
int format; // format version, must be 1
@@ -72,7 +53,6 @@ namespace Bootstrap {
int height; // block height of the snapshot tip
std::string hash; // block hash at that height (hex, no 0x prefix)
int dbversion; // DATABASE_VERSION the txleveldb was built with
std::string signature; // Ed25519 signature of (height || hash), hex-encoded (empty if unsigned)
};
// Parse a snapshot.manifest file into a SnapshotManifest struct.
+25 -88
View File
@@ -109,13 +109,6 @@ 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");
@@ -136,48 +129,34 @@ bool MaybeMigrateLevelDbToRocksDb(bool fForce, std::string& strError)
}
int64_t nCopied = 0;
bool fCopyOK = true;
auto it = source.NewIterator();
for (it->Seek(std::string()); it->Valid(); it->Next())
{
// 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.WriteRawRecordForMigration(it->KeyStr(), it->ValueStr())) {
strError = "failed to write migrated record to RocksDB";
fCopyOK = false;
break;
}
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)
{
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;
}
if (++nCopied % 100000 == 0)
{
if (!destination.TxnCommit()) {
strError = "failed to commit RocksDB migration batch";
source.Close();
destination.Close();
return false;
}
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;
}
}
} // 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()) {
@@ -206,49 +185,7 @@ bool MaybeMigrateLevelDbToRocksDb(bool fForce, std::string& strError)
source.Close();
destination.Close();
// 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;
}
}
}
fs::remove(markerPath);
}
catch (std::exception& e) {
strError = e.what();
-474
View File
@@ -1,474 +0,0 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
// Signed Checkpoint Publisher (Triangles v5.9.24) — implementation.
//
// See checkpointpublisher.h for the design. This file holds:
// - The in-memory signed-checkpoint cache (a CCriticalSection-guarded
// std::map keyed by height; values are block hashes)
// - The canonical serialization used by both producer and consumer
// - The JSON parsing/building helpers (small subset, no third-party deps)
// - The trusted signers list (mirrors IsTrustedSnapshotSigner)
#include "checkpointpublisher.h"
#include <algorithm>
#include <cstdio>
#include <map>
#include <set>
#include <sstream>
#include <vector>
#include "sync.h"
#include "util.h"
#include "base58.h"
#include "key.h"
#include "serialize.h"
#include "net.h" // for CCriticalSection
#include "main.h" // for strMessageMagic
#include "bootstrap.h" // for Bootstrap::DownloadFile
namespace Checkpoints {
// ============================================================================
// Trusted signers
// ============================================================================
//
// Mirrors Bootstrap::TRUSTED_SNAPSHOT_SIGNERS but kept SEPARATE so the two
// lists can be managed independently. The default trust list contains the
// project operator's address. Operators can extend via a future -trustedcheckpointsigner
// conf option (not yet implemented — see Phase 2 in checkpointpublisher.h).
static const char* TRUSTED_CHECKPOINT_SIGNERS[] = {
"TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX", // Sami's wallet (DNS2 default)
};
static const size_t NUM_TRUSTED_CHECKPOINT_SIGNERS =
sizeof(TRUSTED_CHECKPOINT_SIGNERS) / sizeof(TRUSTED_CHECKPOINT_SIGNERS[0]);
bool IsTrustedCheckpointSigner(const std::string& addr)
{
for (size_t i = 0; i < NUM_TRUSTED_CHECKPOINT_SIGNERS; ++i) {
if (addr == TRUSTED_CHECKPOINT_SIGNERS[i]) return true;
}
return false;
}
// ============================================================================
// In-memory cache of loaded signed checkpoints
// ============================================================================
//
// Guarded by a single CCriticalSection. The cache is small (a few thousand
// entries max — operator publishes one every N=5000 blocks, so for a 2.2M
// chain that's ~440 entries per active signer). Lookup is O(log n).
static CCriticalSection cs_signedCheckpoints;
static std::map<int, std::string> mapSignedCheckpoints;
bool IsKnownSignedCheckpoint(int nHeight, const std::string& hashHex)
{
LOCK(cs_signedCheckpoints);
auto it = mapSignedCheckpoints.find(nHeight);
if (it == mapSignedCheckpoints.end()) return false;
// case-insensitive compare — JSON parsers sometimes downcase hex
if (it->second.size() != hashHex.size()) return false;
for (size_t i = 0; i < it->second.size(); i++) {
if (std::tolower(static_cast<unsigned char>(it->second[i])) !=
std::tolower(static_cast<unsigned char>(hashHex[i]))) {
return false;
}
}
return true;
}
void AddSignedCheckpoints(const std::vector<SignedCheckpoint>& entries)
{
LOCK(cs_signedCheckpoints);
for (const auto& e : entries) {
// Don't overwrite compiled-in mapCheckpoints — that gate runs FIRST
// in AcceptBlock. The signed set is a SUPPLEMENT, not a replacement.
mapSignedCheckpoints[e.nHeight] = e.hashHex;
}
printf("Checkpoints: added %lu signed-remote checkpoints to cache\n", (unsigned long)entries.size());
}
void ClearSignedCheckpoints()
{
LOCK(cs_signedCheckpoints);
mapSignedCheckpoints.clear();
}
// ============================================================================
// Canonical serialization — producer + consumer MUST agree on this byte sequence
// ============================================================================
//
// Format: "<height1>:<hash1>:<ts1>;<height2>:<hash2>:<ts2>;..."
//
// Properties:
// - Entries in DESCENDING order (tip first)
// - Lowercase hex, no 0x prefix, no leading zeros
// - Timestamps are unix seconds, decimal
// - Field separator ':' — guaranteed not to appear in hex
// - Entry separator ';' — guaranteed not to appear in either
// - Trailing newline is NOT part of the signed payload (producers MUST NOT
// add one to the message before signing; consumers MUST NOT trim it off
// the fetched JSON's message field before verifying)
//
// This function is PURE — no I/O, no globals. Tested in checkpoint_tests.cpp.
std::string SerializeEntriesForSigning(const std::vector<SignedCheckpoint>& entries)
{
std::string out;
for (size_t i = 0; i < entries.size(); i++) {
if (i > 0) out += ";";
out += std::to_string(entries[i].nHeight);
out += ":";
out += entries[i].hashHex;
out += ":";
out += std::to_string(entries[i].nTimestamp);
}
return out;
}
// ============================================================================
// Producer — build the JSON document
// ============================================================================
//
// This is intentionally a thin wrapper: the wallet signing happens in the
// caller (rpcwallet.cpp / daemon loop), which has the unlocked key. Here we
// just escape + format.
bool BuildSignedCheckpointsJson(
const std::vector<SignedCheckpoint>& entries,
const std::string& signingAddress,
const std::string& signatureBase64,
const std::string& message,
std::string& outJson,
std::string& strError)
{
if (entries.empty()) {
strError = "BuildSignedCheckpointsJson: entries vector is empty";
return false;
}
if (signingAddress.empty()) {
strError = "BuildSignedCheckpointsJson: signingAddress is empty";
return false;
}
if (signatureBase64.empty()) {
strError = "BuildSignedCheckpointsJson: signature is empty";
return false;
}
// Sort entries DESCENDING by height — canonical form. Producers and
// consumers both depend on this so verification is deterministic.
std::vector<SignedCheckpoint> sorted = entries;
std::sort(sorted.begin(), sorted.end(),
[](const SignedCheckpoint& a, const SignedCheckpoint& b) {
return a.nHeight > b.nHeight;
});
// Build JSON manually — no third-party deps. Format is intentionally
// simple (no nested objects beyond the entries array).
std::ostringstream oss;
oss << "{\n";
oss << " \"format_version\": 1,\n";
oss << " \"signing_address\": \"" << signingAddress << "\",\n";
oss << " \"message\": \"" << message << "\",\n";
oss << " \"signature\": \"" << signatureBase64 << "\",\n";
oss << " \"entries\": [\n";
for (size_t i = 0; i < sorted.size(); i++) {
oss << " {\"height\": " << sorted[i].nHeight
<< ", \"hash\": \"" << sorted[i].hashHex << "\""
<< ", \"timestamp\": " << sorted[i].nTimestamp << "}";
if (i + 1 < sorted.size()) oss << ",";
oss << "\n";
}
oss << " ]\n";
oss << "}\n";
outJson = oss.str();
return true;
}
// ============================================================================
// Consumer — verify a JSON document
// ============================================================================
// Small JSON helper — extract a top-level array of objects from the
// "entries" field. We don't need full JSON parsing; the format is fixed.
static std::vector<std::string> ExtractJsonObjectArray(
const std::string& json, const std::string& field)
{
std::vector<std::string> objs;
std::string key = "\"" + field + "\"";
size_t pos = json.find(key);
if (pos == std::string::npos) return objs;
pos += key.size();
while (pos < json.size() && (json[pos] == ' ' || json[pos] == ':' ||
json[pos] == '\t' || json[pos] == '\n' || json[pos] == '\r'))
pos++;
if (pos >= json.size() || json[pos] != '[') return objs;
pos++; // past '['
while (pos < json.size()) {
while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' ||
json[pos] == '\n' || json[pos] == '\r' || json[pos] == ','))
pos++;
if (pos >= json.size() || json[pos] == ']') break;
if (json[pos] != '{') break;
// Find matching closing brace (shallow — no nested objects in entries)
int depth = 1;
size_t start = pos;
pos++;
while (pos < json.size() && depth > 0) {
if (json[pos] == '{') depth++;
else if (json[pos] == '}') depth--;
pos++;
}
if (depth != 0) break;
objs.push_back(json.substr(start, pos - start));
}
return objs;
}
// Extract an integer field from an entry object like:
// {"height": 12345, "hash": "...", "timestamp": 1700000000}
static int ExtractJsonInt(const std::string& obj, const std::string& field)
{
std::string key = "\"" + field + "\"";
size_t pos = obj.find(key);
if (pos == std::string::npos) return 0;
pos += key.size();
while (pos < obj.size() && (obj[pos] == ' ' || obj[pos] == ':' ||
obj[pos] == '\t')) pos++;
// Parse a non-negative integer
int n = 0;
bool foundAny = false;
while (pos < obj.size() && obj[pos] >= '0' && obj[pos] <= '9') {
n = n * 10 + (obj[pos] - '0');
pos++;
foundAny = true;
}
if (!foundAny) return 0;
return n;
}
// Extract a string field from a small JSON object — mirrors ExtractJsonString
// in bootstrap.cpp. Duplicated here to keep checkpointpublisher.cpp standalone
// (no link dependency on bootstrap.cpp internals).
static std::string ExtractJsonString(const std::string& obj, const std::string& field)
{
std::string key = "\"" + field + "\"";
size_t pos = obj.find(key);
if (pos == std::string::npos) return "";
pos += key.size();
while (pos < obj.size() && (obj[pos] == ' ' || obj[pos] == ':' ||
obj[pos] == '\t')) pos++;
if (pos >= obj.size() || obj[pos] != '\"') return "";
pos++;
size_t end = obj.find('\"', pos);
if (end == std::string::npos) return "";
return obj.substr(pos, end - pos);
}
bool VerifySignedCheckpoints(
const std::string& jsonText,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError)
{
outEntries.clear();
outSigningAddress.clear();
// 1. Extract signing fields
outSigningAddress = ExtractJsonString(jsonText, "signing_address");
std::string signature = ExtractJsonString(jsonText, "signature");
std::string message = ExtractJsonString(jsonText, "message");
if (outSigningAddress.empty() || signature.empty() || message.empty()) {
strError = "signed-checkpoints JSON missing required top-level fields "
"(signing_address/signature/message)";
return false;
}
// 2. Verify signer is trusted
if (!IsTrustedCheckpointSigner(outSigningAddress)) {
strError = "signing_address " + outSigningAddress +
" is not in the trusted checkpoint signers list";
return false;
}
// 3. Verify the address is well-formed (catches typos early)
CTrianglesAddress addr(outSigningAddress);
if (!addr.IsValid()) {
strError = "signing_address " + outSigningAddress + " is not a valid Triangles address";
return false;
}
CKeyID keyID;
if (!addr.GetKeyID(keyID)) {
strError = "signing_address " + outSigningAddress + " does not refer to a key";
return false;
}
// 4. Decode and verify the signature (same code path as verifymessage RPC)
bool fInvalid = false;
std::vector<unsigned char> vchSig = DecodeBase64(signature.c_str(), &fInvalid);
if (fInvalid) {
strError = "signed-checkpoints signature is not valid base64";
return false;
}
CDataStream ss(SER_GETHASH, 0);
ss << strMessageMagic;
ss << message;
CKey key;
if (!key.SetCompactSignature(Hash(ss.begin(), ss.end()), vchSig)) {
strError = "signed-checkpoints signature failed to recover (bad sig or "
"message tampered)";
return false;
}
if (key.GetPubKey().GetID() != keyID) {
strError = "signed-checkpoints signature recovered to a key that does "
"not match the claimed signer address";
return false;
}
// 5. Extract entries and verify they match the signed message
std::vector<std::string> entryObjs = ExtractJsonObjectArray(jsonText, "entries");
if (entryObjs.empty()) {
strError = "signed-checkpoints JSON has no entries array or entries is empty";
return false;
}
outEntries.reserve(entryObjs.size());
for (const auto& obj : entryObjs) {
SignedCheckpoint e;
e.nHeight = ExtractJsonInt(obj, "height");
e.hashHex = ExtractJsonString(obj, "hash");
e.nTimestamp = ExtractJsonInt(obj, "timestamp");
if (e.nHeight <= 0 || e.hashHex.empty() || e.nTimestamp <= 0) {
strError = "malformed entry (height/hash/timestamp invalid): " + obj;
return false;
}
// hashHex sanity: must be exactly 64 lowercase hex chars
if (e.hashHex.size() != 64) {
strError = "entry hash at height " + std::to_string(e.nHeight) +
" is not 64 chars: " + e.hashHex;
return false;
}
for (char c : e.hashHex) {
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
strError = "entry hash at height " + std::to_string(e.nHeight) +
" contains non-lowercase-hex character";
return false;
}
}
outEntries.push_back(e);
}
// 6. Verify the signed message exactly matches the canonical serialization
// of the entries. This is the cross-check that proves the entries
// weren't tampered with after signing.
std::string expectedMessage = SerializeEntriesForSigning(outEntries);
if (expectedMessage != message) {
strError = "signed-checkpoints message does not match canonical entry "
"serialization — entries were tampered with after signing";
return false;
}
printf("Checkpoints: signed-remote verified — %lu entries signed by %s\n",
(unsigned long)outEntries.size(), outSigningAddress.c_str());
return true;
}
// ============================================================================
// Network fetch — keep it simple. The signed-checkpoints doc is tiny (~5 KB
// for a year of entries at 5000-block intervals), so a plain HTTP GET is
// fine. We DO NOT go through Tor for this fetch: the bootstrap server is
// already a known clearnet endpoint (same model as the existing UTXO
// snapshot download, which uses ConnectDirectTCP per bootstrap.cpp).
// ============================================================================
bool LoadSignedCheckpoints(
const std::string& host,
const std::string& onDiskPath,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError)
{
outEntries.clear();
outSigningAddress.clear();
std::string jsonText;
// Path A: use on-disk copy if it exists (lets the daemon start even when
// the bootstrap server is unreachable, as long as we have a recent copy).
if (!onDiskPath.empty()) {
FILE* f = fopen(onDiskPath.c_str(), "rb");
if (f) {
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz > 0 && sz < 10 * 1024 * 1024) { // 10 MB cap — sanity
jsonText.resize(sz);
size_t got = fread(&jsonText[0], 1, sz, f);
jsonText.resize(got);
}
fclose(f);
if (!jsonText.empty()) {
printf("Checkpoints: loaded on-disk signed-checkpoints from %s (%lu bytes)\n",
onDiskPath.c_str(), (unsigned long)jsonText.size());
}
}
}
// Path B: fetch from bootstrap server. We always try this — if it
// succeeds, prefer the freshest doc over the on-disk copy.
if (host.empty()) {
strError = "LoadSignedCheckpoints: no host provided and no on-disk copy found";
return !jsonText.empty(); // if we have disk content, still try to verify it
}
// Use Bootstrap::DownloadFile — already handles clearnet HTTPS, timeouts,
// and redirects. We do NOT proxy through Tor.
if (Bootstrap::DownloadFile(host, "signed-checkpoints.json",
std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp",
nullptr, strError,
/*noProxy=*/true, /*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) {
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz > 0 && sz < 10 * 1024 * 1024) {
jsonText.resize(sz);
size_t got = fread(&jsonText[0], 1, sz, f);
jsonText.resize(got);
}
fclose(f);
}
std::error_code ec;
std::filesystem::remove(tmp, ec);
if (!jsonText.empty()) {
printf("Checkpoints: fetched fresh signed-checkpoints from %s (%lu bytes)\n",
host.c_str(), (unsigned long)jsonText.size());
// Persist to disk for next startup (only if onDiskPath was given)
if (!onDiskPath.empty()) {
FILE* f2 = fopen(onDiskPath.c_str(), "wb");
if (f2) {
fwrite(jsonText.data(), 1, (unsigned long)jsonText.size(), f2);
fclose(f2);
printf("Checkpoints: persisted signed-checkpoints to %s\n", onDiskPath.c_str());
}
}
}
} else {
printf("Checkpoints: WARNING — fetch from %s failed (%s)",
host.c_str(), strError.c_str());
if (jsonText.empty()) {
strError = "could not fetch signed-checkpoints and no on-disk copy: " + strError;
return false;
}
printf(" — falling back to on-disk copy\n");
strError.clear();
}
// Verify whatever we ended up with
return VerifySignedCheckpoints(jsonText, outEntries, outSigningAddress, strError);
}
} // namespace Checkpoints
-164
View File
@@ -1,164 +0,0 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
// Signed Checkpoint Publisher (Triangles v5.9.24)
//
// Background
// ----------
// Triangles' existing CSyncCheckpoint (src/checkpoints.cpp) is Bitcoin-era
// P2P-broadcast code that uses a HARDCODED master pubkey. That model does
// not match how the project actually operates today (one operator with
// multiple keys, snapshot publishing on the bootstrap server, no master
// hierarchy). Instead we layer a *new* signed-checkpoint scheme on top of
// the bootstrap server, using the same compact-message primitive the UTXO
// snapshot trust model already uses (see src/bootstrap.cpp:IsTrustedSnapshotSigner).
//
// Trust model
// -----------
// - A signed checkpoint document is a small JSON file hosted at
// https://bootstrap.cryptographic-triangles.org/signed-checkpoints.json
// - It contains a list of (height, block_hash, unix_timestamp) entries,
// followed by a single signing_address + signature covering the canonical
// serialization of the entry list.
// - The signing_address must appear in the trusted signers list
// (Checkpoints::IsTrustedCheckpointSigner, see checkpoints.cpp). The
// default trust list is the same as IsTrustedSnapshotSigner but kept
// separate so they can be managed independently.
// - Verification uses the existing CKey::SignCompact / SetCompactSignature
// code path through the wallet's verifymessage-style flow — no new
// cryptography is introduced.
//
// Producer
// --------
// - The daemon operator runs `triangles-cli publishcheckpoint [interval]`
// which builds the entry list from pindexBest, signs with the wallet's
// default key, and writes the JSON document to a path the operator
// uploads to the bootstrap server (or a cron job uploads automatically
// when -autopublishcheckpoint is set).
// - Default interval = every 5000 blocks; can be set to every N.
// - The first entry is always the chain tip at publish time.
//
// Consumer
// --------
// - On startup, the daemon can call
// Checkpoints::LoadSignedCheckpoints(host, dataDir, strError)
// which fetches, verifies, and merges the trusted entries into the
// compiled-in mapCheckpoints (lower priority — compiled-in wins on
// conflict to defend against remote-rollback).
// - Checkpoints::IsKnownSignedCheckpoint(height, hash) returns true if
// either compiled-in OR signed-remote knows about (height, hash).
//
// Relationship to existing code
// -----------------------------
// - mapCheckpoints in src/checkpoints.cpp is UNCHANGED — the compiled-in
// list is still the primary trust anchor.
// - Signed checkpoints EXTEND the trust anchor with operator-published
// ones, useful when the operator wants to publish a checkpoint at
// height 2,210,000 without waiting for a code release.
// - mapSnapshotHashes is unaffected.
#ifndef TRIANGLES_CHECKPOINT_PUBLISHER_H
#define TRIANGLES_CHECKPOINT_PUBLISHER_H
#include <string>
#include <vector>
#include <cstdint>
namespace Checkpoints {
// One signed checkpoint entry. Compact, serializable, no JSON inside the
// struct — JSON wrapping happens in the publisher.
struct SignedCheckpoint {
int nHeight; // block height
std::string hashHex; // block hash, lowercase hex, NO 0x prefix, NO leading zeros
int64_t nTimestamp; // unix seconds when published (signed over)
};
// Result of a publish or verify operation. Used for human-readable errors
// and structured logging.
struct SignedCheckpointResult {
bool ok; // overall success
std::string error; // populated if !ok
int nEntriesWritten; // for publish: how many entries went into the JSON
int nEntriesVerified; // for verify: how many entries passed signature check
};
// Default URL for the bootstrap server's signed-checkpoints document.
static const char* SIGNED_CHECKPOINTS_URL =
"https://bootstrap.cryptographic-triangles.org/signed-checkpoints.json";
// Default local output path the daemon writes to on publish.
static const char* SIGNED_CHECKPOINTS_DEFAULT_OUT =
"/var/www/triangles-bootstrap/signed-checkpoints.json";
// ---- Producer ----
// Build the JSON document for the entries [heights[0], heights[1], ...]
// (in DESCENDING order — tip first) using the wallet's default key.
// Returns true on success; outJson/outputPath written. Wallet must be
// unlocked (signmessage requires it).
//
// This is the in-process builder used by both:
// - The triangles-cli `publishcheckpoint` RPC command
// - The daemon's auto-publish loop when -autopublishcheckpoint is set
bool BuildSignedCheckpointsJson(
const std::vector<SignedCheckpoint>& entries,
const std::string& signingAddress,
const std::string& signatureBase64,
const std::string& message,
std::string& outJson,
std::string& strError);
// Canonical (deterministic) serialization of the entry list. The signature
// is over this exact byte sequence — both producer and consumer MUST use
// this function so verification is reproducible across platforms.
std::string SerializeEntriesForSigning(const std::vector<SignedCheckpoint>& entries);
// ---- Consumer ----
// Fetch the signed-checkpoints document from the bootstrap server, parse
// it, verify the signature, and return the verified entries. Does NOT
// merge into mapCheckpoints — caller decides what to do with the entries.
//
// onDiskPath: optional. If non-empty and the file already exists locally,
// skip the network fetch and verify the on-disk copy. This makes startup
// robust against bootstrap-server outages.
bool LoadSignedCheckpoints(
const std::string& host,
const std::string& onDiskPath,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError);
// Verify the signature on a parsed JSON document. Pure function — no
// network, no filesystem.
bool VerifySignedCheckpoints(
const std::string& jsonText,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError);
// Is the given signing address in the trusted signers list? Mirrors
// Bootstrap::IsTrustedSnapshotSigner but kept separate for independent
// governance.
bool IsTrustedCheckpointSigner(const std::string& addr);
// ---- Merged lookup ----
// Is (height, hash) known to either the compiled-in OR the
// signed-remote set? This is what AcceptBlock / fork-detection should call.
bool IsKnownSignedCheckpoint(int nHeight, const std::string& hashHex);
// Inject loaded entries into the in-memory signed-checkpoint cache. Called
// by init.cpp after LoadSignedCheckpoints returns successfully. Subsequent
// IsKnownSignedCheckpoint() calls will return true for any (height, hash)
// in the loaded set.
void AddSignedCheckpoints(const std::vector<SignedCheckpoint>& entries);
// Clear the in-memory cache (used at reorg boundaries and in tests).
void ClearSignedCheckpoints();
} // namespace Checkpoints
#endif // TRIANGLES_CHECKPOINT_PUBLISHER_H
+51 -75
View File
@@ -25,60 +25,14 @@ namespace Checkpoints
{ 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")},
{ 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")},
{ 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")},
{ 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")},
{ 3935, uint256("0xe16290c9757d1368b8d7c35de4f8f70c2c9f9c785b667df0c3bff85086ca6")},
{ 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")},
{ 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")},
{ 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")},
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
// Recent finality pin (PoS era). Closes the long unchecked span from
// 17650 to the live tip so stale-bootstrap / low-trust forks below
// this height are rejected outright. Hash from the canonical chain.
{ 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")},
{ 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")},
// Continuous finality pins: every 1000 blocks from 2206500 onward so the
// gap between the last hardcoded checkpoint and the live tip stays bounded.
// Without these, a fresh node syncing from zero (no snapshot) has 8,400+
// unverified blocks at tip — a peer feeding fork blocks at those heights
// could trick an IBD node into accepting a divergent chain. With these
// pins, any divergence >1000 blocks is rejected at AcceptBlock time.
// All hashes verified against the canonical chain on 2026-07-01.
{ 2206500, uint256("0x707ea288242227e9b36ceeeecd5a16a6c918f8b6f7e6375128cba908ebfcbf27")},
{ 2207000, uint256("0x7af1cc23fdffb3a9ed2eb9aa5a8697e8af2f98c67c4f6baa9f4d7899cbfaf4ca")},
{ 2210000, uint256("0xe2dc2e55c6e1b3d2ea9d8a1f2b274bf64053ddd6a61335dc6896aa9c056956be")},
{ 2211000, uint256("0x61c8a179c928a1f0bbffa029b4f1aea67b04a98227a6d02e6137280404ed29dc")},
{ 2212000, uint256("0xf4df2b5d0d1de326b97ed5a3eeefef307a51791e03af401373e142f00453a9a8")},
{ 2213000, uint256("0x7bc9652d423676c52ba8b0a287e0b46e1eca6e8eecc51d3f30e0d665d3b236f5")},
{ 2214000, uint256("0x17e61ceb45db36358aaabe91b094a77ecba32370a467185fa9af75eef6c8e414")},
{ 2214400, uint256("0x8ebb818f7280850c5a3916b7c8a2bca603f7c4f9926d3cdc2262f726035d96ed")},
// Post-rebuild finality pin (v6.2.5.0). Closes the gap between
// the last hardcoded checkpoint and the live tip after -rebuildutxo.
// Hash from the canonical chain on DNS2 after fresh UTXO rebuild.
{ 2219922, uint256("0x9ed3e1d38317950927f37f2867e3fc29e239fc1f4c57b182f55c6e04b73b52ec")},
// Live-tip finality pins (v6.2.6.0). Verified against DNS3 chain state
// on 2026-08-04. Closes the 4,841-block unchecked span between the
// last hardcoded pin (2,219,922) and the live tip (2,224,763).
// All hashes verified against the canonical chain on DNS3 (running
// v6.2.3.0-geb02f34) at block 2,224,763. Verification transcript
// (DNS3 getblockhash output) is archived in the v6.2.6.0 release
// notes on bootstrap.cryptographic-triangles.org.
//
// Note: the gap from 2,219,922 to 2,222,900 is 2,978 blocks (larger than the
// 1,000-block standard spacing), because block 2,220,000 etc. were
// not indexed in DNS3's local block index when this release was
// prepared. The 2,222,900+ pins restore the 1,000-block spacing
// guarantee from that point to the live tip.
{ 2222900, uint256("0xe104c29d6a6ff983d9a02a9854a86c221a1f400f0116cb255cee2b8d5c7ced9f")},
{ 2223000, uint256("0x41926ba6dc9147e361ffd1ffc1a0357d7d7b66550ed05864d1ae103c6332371a")},
{ 2223500, uint256("0x998e65941f200359ca0c1f53ea128c27f83111e8bbb1db38b7ed2ed7a48b8e32")},
{ 2223700, uint256("0x97d3a70d258c34429c15b430e654fa1270e4de635ecec3c72ace92a0d04679c3")},
{ 2224000, uint256("0x4dddc0b555266a1207fef70af17db9a7b14ab5e1d7cf27882ea35cc77923841f")},
{ 2224500, uint256("0xe0fea543829dd0e8c02b7c657468cff775c7993658c16c1feaf1418b4080ba27")},
{ 2224700, uint256("0x2a8ea5ef954adb707286bc468fdf43d8d99d23a1d15cf4f17a35d58dd51b0944")},
{ 2224750, uint256("0x0f117fe05befb6d8a93c6e45bc3b3d48889208e2785ba6a3d723c8ad7c9d649f")},
{ 2224763, uint256("0x9d3575ac5428e64911e698ba0a8f773954b17b214a044d4b244fa2ec83c06674")}, // live tip
};
};
// Published UTXO snapshot file SHA256, keyed by snapshot height.
// Each entry binds height -> SHA256 of the canonical snapshot file produced by
@@ -89,12 +43,6 @@ namespace Checkpoints
// here. The corresponding (height, blockhash) must already exist in
// mapCheckpoints / mapCheckpointsTestnet.
static std::map<int, uint256> mapSnapshotHashes = {
{ 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")},
{ 2219922, uint256("0x6dd8d782a04bb8dc4ccd5e88a4bc7726fe26bdebaed96b79242de1e2949b6ee6")},
// Live-tip snapshot (v6.2.6.0). Generated from DNS3 (Samihost) at the
// canonical tip 2,224,763, blockhash 9d3575ac...06674. Verified against
// the canonical chain on 2026-08-04.
{ 2224763, uint256("0xa7ea62ad4e158faf07973e5cd1539c1895154c4e28685a3eb7af458a001037b7")},
};
static std::map<int, uint256> mapSnapshotHashesTestnet = {
@@ -106,7 +54,7 @@ namespace Checkpoints
{ 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")},
{ 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")},
{ 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")},
{ 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")},
{ 3935, uint256("0xe16290c9757d1368b8d7c35de4f8f70c2c9f9c785b667df0c3bff85086ca6")},
{ 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")},
{ 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")},
{ 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")},
@@ -169,19 +117,6 @@ 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");
@@ -384,14 +319,53 @@ namespace Checkpoints
bool SetCheckpointPrivKey(std::string strPrivKey)
{
(void)strPrivKey;
return error("SetCheckpointPrivKey: synchronized checkpoints are disabled");
// Test signing a sync-checkpoint with genesis block
CSyncCheckpoint checkpoint;
checkpoint.hashCheckpoint = !fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet;
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
std::vector<unsigned char> vchPrivKey = ParseHex(strPrivKey);
CKey key;
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
return false;
// Test signing successful, proceed
CSyncCheckpoint::strMasterPrivKey = strPrivKey;
return true;
}
bool SendSyncCheckpoint(uint256 hashCheckpoint)
{
(void)hashCheckpoint;
return error("SendSyncCheckpoint: synchronized checkpoints are disabled");
CSyncCheckpoint checkpoint;
checkpoint.hashCheckpoint = hashCheckpoint;
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
if (CSyncCheckpoint::strMasterPrivKey.empty())
return error("SendSyncCheckpoint: Checkpoint master key unavailable.");
std::vector<unsigned char> vchPrivKey = ParseHex(CSyncCheckpoint::strMasterPrivKey);
CKey key;
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
return error("SendSyncCheckpoint: Unable to sign checkpoint, check private key?");
if(!checkpoint.ProcessSyncCheckpoint(nullptr))
{
printf("WARNING: SendSyncCheckpoint: Failed to process checkpoint.\n");
return false;
}
// Relay checkpoint
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
checkpoint.RelayTo(pnode);
}
return true;
}
// Is the sync-checkpoint outside maturity window?
@@ -412,11 +386,13 @@ const std::string CSyncCheckpoint::strMasterPubKey = "";
std::string CSyncCheckpoint::strMasterPrivKey = "";
// triangles: verify signature of sync-checkpoint message
// The master-key system is disabled. Reject these legacy messages instead of
// treating unsigned data as authenticated if a dispatcher is added later.
// Master key system disabled - checkpoint signatures are no longer required
bool CSyncCheckpoint::CheckSignature()
{
return error("CSyncCheckpoint::CheckSignature: synchronized checkpoints are disabled");
// Deserialize the checkpoint data without signature verification
CDataStream sMsg(vchMsg, SER_NETWORK, PROTOCOL_VERSION);
sMsg >> *(CUnsignedSyncCheckpoint*)this;
return true;
}
// triangles: process synchronized checkpoint
-8
View File
@@ -53,14 +53,6 @@ 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
View File
@@ -1,19 +1,19 @@
#ifndef CLIENTVERSION_H
#define CLIENTVERSION_H
//
// client versioning
//
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
#define CLIENT_VERSION_MAJOR 6
#define CLIENT_VERSION_MINOR 2
#define CLIENT_VERSION_REVISION 6
#define CLIENT_VERSION_BUILD 2
// 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 0
#define CLIENT_VERSION_REVISION 1
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
// Don't merge these into one macro!
#define STRINGIZE(X) DO_STRINGIZE(X)
#define DO_STRINGIZE(X) #X
#endif // CLIENTVERSION_H
-223
View File
@@ -1,223 +0,0 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license.
#include "hdwallet.h"
#include "bip39_english.h"
#include <cstring>
#include <algorithm>
#include <openssl/sha.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <secp256k1.h>
namespace hd {
// ---- secp256k1 context (self-contained; independent of crypto_ecdsa) ------
static secp256k1_context* HDContext()
{
static secp256k1_context* ctx = NULL;
if (!ctx)
ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
return ctx;
}
static void HmacSha512(const unsigned char* key, size_t keylen,
const unsigned char* data, size_t datalen,
unsigned char out[64])
{
unsigned int len = 64;
HMAC(EVP_sha512(), key, (int)keylen, data, datalen, out, &len);
}
// Binary search the (lexicographically sorted) BIP39 English wordlist.
static int WordIndex(const std::string& w)
{
int lo = 0, hi = 2047;
while (lo <= hi) {
int mid = (lo + hi) / 2;
int c = w.compare(BIP39_WORDLIST_EN[mid]);
if (c == 0) return mid;
if (c < 0) hi = mid - 1; else lo = mid + 1;
}
return -1;
}
static std::vector<std::string> SplitWords(const std::string& s)
{
std::vector<std::string> out;
size_t i = 0, n = s.size();
while (i < n) {
while (i < n && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) i++;
size_t j = i;
while (j < n && !(s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r')) j++;
if (j > i) out.push_back(s.substr(i, j - i));
i = j;
}
return out;
}
// ---- BIP39 ----------------------------------------------------------------
std::string GenerateMnemonic(int strengthBits)
{
if (strengthBits != 128 && strengthBits != 256) strengthBits = 256;
int entBytes = strengthBits / 8;
std::vector<unsigned char> ent(entBytes);
if (RAND_bytes(&ent[0], entBytes) != 1) return std::string();
// checksum = first (ENT/32) bits of SHA256(entropy)
unsigned char hash[32];
SHA256(&ent[0], entBytes, hash);
int csBits = strengthBits / 32;
// bit buffer = entropy || checksum bits
std::vector<unsigned char> bits = ent;
bits.push_back(hash[0]); // up to 8 checksum bits live in hash[0]
int totalBits = strengthBits + csBits;
int words = totalBits / 11;
std::string out;
for (int i = 0; i < words; i++) {
int idx = 0;
for (int b = 0; b < 11; b++) {
int bitpos = i * 11 + b;
int byte = bitpos / 8, off = 7 - (bitpos % 8);
int bit = (bits[byte] >> off) & 1;
idx = (idx << 1) | bit;
}
if (i) out += ' ';
out += BIP39_WORDLIST_EN[idx];
}
return out;
}
bool CheckMnemonic(const std::string& mnemonic)
{
std::vector<std::string> w = SplitWords(mnemonic);
size_t nw = w.size();
if (nw != 12 && nw != 15 && nw != 18 && nw != 21 && nw != 24) return false;
int totalBits = (int)nw * 11;
int csBits = totalBits / 33;
int entBits = totalBits - csBits;
if (entBits % 8 != 0) return false;
int entBytes = entBits / 8;
// unpack 11-bit indices into a bit buffer
std::vector<unsigned char> buf((totalBits + 7) / 8, 0);
for (size_t i = 0; i < nw; i++) {
int idx = WordIndex(w[i]);
if (idx < 0) return false;
for (int b = 0; b < 11; b++) {
int bit = (idx >> (10 - b)) & 1;
int bitpos = (int)i * 11 + b;
int byte = bitpos / 8, off = 7 - (bitpos % 8);
if (bit) buf[byte] |= (1 << off);
}
}
std::vector<unsigned char> ent(buf.begin(), buf.begin() + entBytes);
unsigned char hash[32];
SHA256(&ent[0], entBytes, hash);
// compare csBits checksum bits
for (int b = 0; b < csBits; b++) {
int bitpos = entBits + b;
int byte = bitpos / 8, off = 7 - (bitpos % 8);
int got = (buf[byte] >> off) & 1;
int want = (hash[b / 8] >> (7 - (b % 8))) & 1;
if (got != want) return false;
}
return true;
}
bool MnemonicToSeed(const std::string& mnemonic, const std::string& passphrase,
unsigned char seed64[64])
{
std::string salt = "mnemonic" + passphrase;
int rc = PKCS5_PBKDF2_HMAC(mnemonic.c_str(), (int)mnemonic.size(),
(const unsigned char*)salt.c_str(), (int)salt.size(),
2048, EVP_sha512(), 64, seed64);
return rc == 1;
}
// ---- BIP32 ----------------------------------------------------------------
bool MasterFromSeed(const unsigned char* seed, size_t seedlen, ExtKey& out)
{
unsigned char I[64];
HmacSha512((const unsigned char*)"Bitcoin seed", 12, seed, seedlen, I);
memcpy(out.key, I, 32);
memcpy(out.chaincode, I + 32, 32);
if (!secp256k1_ec_seckey_verify(HDContext(), out.key)) return false;
out.valid = true;
return true;
}
bool CKDpriv(const ExtKey& parent, uint32_t index, ExtKey& child)
{
if (!parent.valid) return false;
secp256k1_context* ctx = HDContext();
unsigned char data[37];
size_t dlen = 0;
if (index & HARDENED) {
data[0] = 0x00;
memcpy(data + 1, parent.key, 32);
dlen = 33;
} else {
// serP(point(parent.key)) = 33-byte compressed pubkey
secp256k1_pubkey pk;
if (!secp256k1_ec_pubkey_create(ctx, &pk, parent.key)) return false;
size_t plen = 33;
secp256k1_ec_pubkey_serialize(ctx, data, &plen, &pk, SECP256K1_EC_COMPRESSED);
dlen = 33;
}
data[dlen + 0] = (index >> 24) & 0xff;
data[dlen + 1] = (index >> 16) & 0xff;
data[dlen + 2] = (index >> 8) & 0xff;
data[dlen + 3] = index & 0xff;
dlen += 4;
unsigned char I[64];
HmacSha512(parent.chaincode, 32, data, dlen, I);
memcpy(child.key, parent.key, 32);
// child = (IL + parent) mod n ; rejects invalid (IL>=n or result 0)
if (!secp256k1_ec_seckey_tweak_add(ctx, child.key, I)) return false;
memcpy(child.chaincode, I + 32, 32);
child.valid = true;
return true;
}
bool DerivePath(const ExtKey& master, const std::vector<uint32_t>& path, ExtKey& out)
{
ExtKey cur = master;
for (size_t i = 0; i < path.size(); i++) {
ExtKey nxt;
if (!CKDpriv(cur, path[i], nxt)) return false;
cur = nxt;
}
out = cur;
return true;
}
bool DeriveTriangles(const std::string& mnemonic, const std::string& passphrase,
uint32_t account, uint32_t change, uint32_t index,
unsigned char privOut[32])
{
unsigned char seed[64];
if (!MnemonicToSeed(mnemonic, passphrase, seed)) return false;
ExtKey master;
if (!MasterFromSeed(seed, 64, master)) return false;
std::vector<uint32_t> path;
path.push_back(44u | HARDENED);
path.push_back(TRI_COIN_TYPE | HARDENED);
path.push_back(account | HARDENED);
path.push_back(change);
path.push_back(index);
ExtKey leaf;
if (!DerivePath(master, path, leaf)) return false;
memcpy(privOut, leaf.key, 32);
return true;
}
} // namespace hd
-50
View File
@@ -1,50 +0,0 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license.
//
// Native BIP39 (mnemonic) + BIP32 (HD) key derivation for Triangles.
// Produces keys identical to the TRIdock web wallet (derivation path
// m/44'/2222'/0'/0/i, coin type 2222), so a 24-word phrase round-trips
// between the Qt/daemon wallet and the web wallet.
#ifndef TRIANGLES_HDWALLET_H
#define TRIANGLES_HDWALLET_H
#include <string>
#include <vector>
#include <cstdint>
#include <cstddef>
namespace hd {
static const uint32_t HARDENED = 0x80000000u;
static const uint32_t TRI_COIN_TYPE = 2222u; // matches triWallet.js
// A BIP32 extended private key (private scalar + chain code).
struct ExtKey {
unsigned char key[32];
unsigned char chaincode[32];
bool valid;
ExtKey() : valid(false) { }
};
// ---- BIP39 ----------------------------------------------------------------
// Generate a new mnemonic. strengthBits must be 128 (12 words) or 256 (24).
std::string GenerateMnemonic(int strengthBits = 256);
// Validate word membership + checksum.
bool CheckMnemonic(const std::string& mnemonic);
// PBKDF2-HMAC-SHA512(mnemonic, "mnemonic"+passphrase, 2048) -> 64-byte seed.
bool MnemonicToSeed(const std::string& mnemonic, const std::string& passphrase,
unsigned char seed64[64]);
// ---- BIP32 ----------------------------------------------------------------
bool MasterFromSeed(const unsigned char* seed, size_t seedlen, ExtKey& out);
bool CKDpriv(const ExtKey& parent, uint32_t index, ExtKey& child);
bool DerivePath(const ExtKey& master, const std::vector<uint32_t>& path, ExtKey& out);
// ---- High level -----------------------------------------------------------
// Derive the 32-byte private scalar for m/44'/coinType'/account'/change/index.
bool DeriveTriangles(const std::string& mnemonic, const std::string& passphrase,
uint32_t account, uint32_t change, uint32_t index,
unsigned char privOut[32]);
} // namespace hd
#endif // TRIANGLES_HDWALLET_H
-478
View File
@@ -1,478 +0,0 @@
// 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();
}
-95
View File
@@ -1,95 +0,0 @@
// Copyright (c) 2024 Triangles developers
// I2P (SAM v3) transport support
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
// This module gives Triangles real I2P connectivity that mirrors the existing
// embedded-Tor design: instead of a SOCKS proxy it talks the SAM v3 protocol
// to a locally running I2P router (i2pd or Java I2P) and obtains a persistent
// I2P destination whose ".b32.i2p" address is shown alongside the .onion
// address. The wallet:
// * creates / loads a persistent destination (i2p_private_key in datadir),
// * runs a STREAM session so peers can dial us,
// * accepts inbound I2P streams and feeds them to the net layer,
// * dials outbound ".b32.i2p" peers through the same session.
//
// A running I2P router with its SAM bridge enabled (default 127.0.0.1:7656) is
// required; nothing is bundled. Enable with -i2p and optionally -i2psam=host:port.
#ifndef TRIANGLES_I2P_H
#define TRIANGLES_I2P_H
#include <atomic>
#include <mutex>
#include <string>
#include <thread>
#include "compat.h" // SOCKET / INVALID_SOCKET
// Default SAM bridge endpoint exposed by i2pd / Java I2P.
#define I2P_DEFAULT_SAM_HOST "127.0.0.1"
#define I2P_DEFAULT_SAM_PORT 7656
// Manages a single persistent I2P STREAM session over SAM v3.
class CI2PSession
{
public:
static CI2PSession* GetInstance();
// Bring the session up: connect to the SAM bridge, load/generate the
// persistent destination and start accepting inbound streams.
// Returns false (and logs) if no router/SAM bridge is reachable.
bool Start();
// Tear the session down and stop the accept loop.
void Stop();
bool IsEnabled() const { return fEnabled.load(); }
bool IsActive() const { return fActive.load(); }
// Our own ".b32.i2p" address (empty until the session is up).
std::string GetB32Address();
// Dial a remote ".b32.i2p" (or full base64 destination) through the
// session. On success hSocketRet is a connected, blocking data socket the
// caller can hand to a CNode. The caller takes ownership of the socket.
bool Connect(const std::string& strDest, SOCKET& hSocketRet);
private:
CI2PSession();
~CI2PSession();
// --- low level SAM helpers ---
bool SamConnect(SOCKET& hSocketRet); // raw TCP to the bridge
bool SamHandshake(SOCKET hSocket); // HELLO VERSION
bool SamSendLine(SOCKET hSocket, const std::string& strLine);
bool SamRecvLine(SOCKET hSocket, std::string& strLineRet);
static std::string SamGetValue(const std::string& strReply, const std::string& strKey);
bool LoadOrCreateDestination(std::string& strPrivKeyRet);
bool CreateSession(); // SESSION CREATE
bool ResolveMyB32(); // NAMING LOOKUP ME
void AcceptLoop(); // inbound STREAM ACCEPT
// Compute the ".b32.i2p" address from a base64 (I2P alphabet) destination.
static std::string DestToB32(const std::string& strB64Dest);
std::string samHost;
int samPort;
std::string sessionId;
std::string privateKey; // persistent destination private key (base64)
std::string b32Address; // our own .b32.i2p
SOCKET hSession; // long-lived control socket owning the session
std::atomic<bool> fEnabled;
std::atomic<bool> fActive;
std::atomic<bool> fShutdown;
std::thread acceptThread;
std::mutex cs;
};
// Convenience: start/stop from init.cpp.
bool StartI2P();
void StopI2P();
#endif // TRIANGLES_I2P_H
-46
View File
@@ -1,46 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
I2PD_SRC_DIR="${I2PD_SRC_DIR:-$ROOT_DIR/i2pd-src}"
if [[ ! -d "$I2PD_SRC_DIR" ]]; then
echo "i2pd source tree not found at: $I2PD_SRC_DIR" >&2
exit 1
fi
cd "$I2PD_SRC_DIR"
echo "Building libi2pd static libraries from: $I2PD_SRC_DIR"
NPROC_VAL="${NPROC:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)}"
# Detect the correct OpenSSL formula path on macOS. The i2pd
# Makefile.homebrew hardcodes openssl@3.5 but Homebrew may install
# openssl@3 instead. Command-line make variables override Makefile
# assignments, so passing SSLROOT=<detected> fixes the include path.
EXTRA_MAKE_ARGS=()
if [[ "$(uname -s)" == "Darwin" ]]; then
if [[ -d "/opt/homebrew/opt/openssl@3" ]]; then
SSLROOT="/opt/homebrew/opt/openssl@3"
elif [[ -d "/usr/local/opt/openssl@3" ]]; then
SSLROOT="/usr/local/opt/openssl@3"
fi
if [[ -n "${SSLROOT:-}" ]]; then
echo "Detected OpenSSL at: $SSLROOT (overriding Makefile.homebrew)"
EXTRA_MAKE_ARGS+=("SSLROOT=${SSLROOT}")
fi
fi
# i2pd uses a hand-written Makefile system. We build only the static library
# targets (libi2pd.a, libi2pdclient.a, libi2pdlang.a), NOT the standalone
# i2pd daemon binary, which pulls in HTTPServer/I2PControl deps we don't need
# and can OOM on memory-constrained build machines.
make -j"$NPROC_VAL" USE_STATIC=no "${EXTRA_MAKE_ARGS[@]}" libi2pd.a libi2pdclient.a libi2pdlang.a
echo
echo "Build finished. Static libraries:"
ls -lh libi2pd*.a
echo
echo "Suggested next step for Triangles:"
echo " cmake -DUSE_I2P_EMBEDDED=ON -DI2P_SOURCE_ROOT=src/i2p/i2pd-src .."
-969
View File
@@ -1,969 +0,0 @@
// Copyright (c) 2025-2026 Triangles developers
// Embedded I2P (i2pd) integration - runs an I2P router in-process
// Distributed under the MIT/X11 software license
//
// BUILD REQUIREMENT: Link against libi2pd.a + libi2pd_client.a built from
// the PurpleI2P/i2pd source tree (src/i2p/i2pd-src).
//
// This file compiles in two modes:
// 1. ENABLE_I2P_EMBEDDED defined: full embedded i2pd via i2p::api
// 2. ENABLE_I2P_EMBEDDED not defined: stubs that report I2P unavailable
#include "i2p_embedded.h"
#include "../util.h"
#include "../net.h"
#include <filesystem>
#include <thread>
#include <fstream>
#include <cstring>
#include <chrono>
#include <ctime>
#include <vector>
#include <string>
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#endif
namespace fs = std::filesystem;
// ===========================================================================
// CI2PSamSocket — SAM v3 direct streaming implementation
// ===========================================================================
//
// Protocol reference: https://geti2p.net/en/docs/api/samv3
//
// The SAM bridge is a simple line-oriented text protocol over TCP. After
// HELLO + SESSION CREATE + STREAM CONNECT succeed, the socket becomes a
// raw bidirectional byte stream to the I2P destination — no further SAM
// framing is needed and there is zero SOCKS overhead.
static std::atomic<unsigned int> g_samSessionSeq{0};
CI2PSamSocket::CI2PSamSocket()
: rawSocket(I2P_INVALID_SOCKET)
{
}
CI2PSamSocket::~CI2PSamSocket()
{
CloseSocket();
}
void CI2PSamSocket::CloseSocket()
{
if (rawSocket != I2P_INVALID_SOCKET) {
#ifdef WIN32
closesocket(rawSocket);
#else
close(rawSocket);
#endif
rawSocket = I2P_INVALID_SOCKET;
}
}
I2pSocket_t CI2PSamSocket::GetRawSocket()
{
I2pSocket_t fd = rawSocket;
rawSocket = I2P_INVALID_SOCKET; // transfer ownership
return fd;
}
bool CI2PSamSocket::SamConnect(const std::string& host, int port)
{
CloseSocket();
#ifdef WIN32
rawSocket = (I2pSocket_t)::socket(AF_INET, SOCK_STREAM, 0);
if (rawSocket == INVALID_SOCKET) {
#else
rawSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (rawSocket < 0) {
#endif
lastError = "SAM: failed to create socket";
return false;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // SAM is always local
addr.sin_port = htons((uint16_t)port);
if (::connect(rawSocket, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
lastError = "SAM: cannot connect to bridge at 127.0.0.1:" + std::to_string(port);
CloseSocket();
return false;
}
return true;
}
bool CI2PSamSocket::SendLine(const std::string& line)
{
std::string msg = line + "\n";
const char* data = msg.data();
size_t remaining = msg.size();
while (remaining > 0) {
#ifdef WIN32
int n = ::send(rawSocket, data, (int)remaining, 0);
#else
ssize_t n = ::send(rawSocket, data, remaining, MSG_NOSIGNAL);
#endif
if (n <= 0) {
lastError = "SAM: send failed";
return false;
}
data += n;
remaining -= (size_t)n;
}
return true;
}
bool CI2PSamSocket::ReadLine(std::string& lineOut)
{
// Look for a complete line (terminated by \n) in recvBuffer first.
for (;;) {
size_t nl = recvBuffer.find('\n');
if (nl != std::string::npos) {
lineOut = recvBuffer.substr(0, nl);
// Strip trailing \r (SAM bridge always uses \n, but be tolerant)
if (!lineOut.empty() && lineOut.back() == '\r')
lineOut.pop_back();
recvBuffer.erase(0, nl + 1);
return true;
}
char buf[4096];
#ifdef WIN32
int n = ::recv(rawSocket, buf, sizeof(buf), 0);
#else
ssize_t n = ::recv(rawSocket, buf, sizeof(buf), 0);
#endif
if (n <= 0) {
lastError = "SAM: connection closed while waiting for reply";
return false;
}
recvBuffer.append(buf, (size_t)n);
}
}
std::string CI2PSamSocket::ParseValue(const std::string& line, const std::string& key)
{
// Find KEY=VALUE token within a space-separated SAM response line.
std::string needle = key + "=";
size_t pos = line.find(needle);
if (pos == std::string::npos)
return {};
pos += needle.size();
size_t end = line.find(' ', pos);
if (end == std::string::npos)
return line.substr(pos);
return line.substr(pos, end - pos);
}
bool CI2PSamSocket::Connect(const std::string& dest_b32, int port,
const std::string& samHost, int samPort)
{
CloseSocket();
lastError.clear();
recvBuffer.clear();
if (dest_b32.empty()) {
lastError = "SAM: empty destination";
return false;
}
// Generate a unique session ID for this connection.
unsigned int seq = ++g_samSessionSeq;
sessionId = "triangles-" + std::to_string(seq) + "-" +
std::to_string((unsigned long)std::time(nullptr));
// ----------------------------------------------------------------
// Step 0: TCP connect to the SAM bridge
// ----------------------------------------------------------------
if (!SamConnect(samHost, samPort)) {
// lastError already set by SamConnect
return false;
}
// ----------------------------------------------------------------
// Step 1: HELLO handshake
// C → S: HELLO VERSION MIN=3.1 MAX=3.1
// S → C: HELLO REPLY RESULT=OK VERSION=3.1
// ----------------------------------------------------------------
if (!SendLine("HELLO VERSION MIN=3.1 MAX=3.1")) {
return false;
}
{
std::string reply;
if (!ReadLine(reply)) {
return false;
}
std::string result = ParseValue(reply, "RESULT");
if (result != "OK") {
lastError = "SAM HELLO failed: " + reply;
CloseSocket();
return false;
}
}
// ----------------------------------------------------------------
// Step 2: SESSION CREATE (transient destination)
// C → S: SESSION CREATE STYLE=STREAM ID=<id> DESTINATION=TRANSIENT
// S → C: SESSION STATUS RESULT=OK DESTINATION=<base64>
// ----------------------------------------------------------------
if (!SendLine("SESSION CREATE STYLE=STREAM ID=" + sessionId +
" DESTINATION=TRANSIENT")) {
return false;
}
{
std::string reply;
if (!ReadLine(reply)) {
return false;
}
std::string result = ParseValue(reply, "RESULT");
if (result != "OK") {
lastError = "SAM SESSION CREATE failed: " + reply;
CloseSocket();
return false;
}
// Save the transient local destination (base64) for diagnostics.
localDestination = ParseValue(reply, "DESTINATION");
}
// ----------------------------------------------------------------
// Step 3: STREAM CONNECT to the remote destination
// C → S: STREAM CONNECT ID=<id> DESTINATION=<b32>.i2p
// S → C: STREAM STATUS RESULT=OK
//
// After RESULT=OK the socket is a raw byte stream — no more SAM
// framing is needed.
// ----------------------------------------------------------------
// Ensure destination has the .b32.i2p suffix (accept bare b32 hash too)
std::string dest = dest_b32;
if (dest.find(".i2p") == std::string::npos && dest.find(".b32") == std::string::npos) {
// Looks like a bare b32 hash — append the standard suffix
dest += ".b32.i2p";
}
if (!SendLine("STREAM CONNECT ID=" + sessionId + " DESTINATION=" + dest)) {
return false;
}
{
std::string reply;
if (!ReadLine(reply)) {
return false;
}
std::string result = ParseValue(reply, "RESULT");
if (result != "OK") {
lastError = "SAM STREAM CONNECT to " + dest + " failed: " + reply;
CloseSocket();
return false;
}
}
// Socket is now a raw I2P stream. Any residual bytes in recvBuffer
// belong to the application layer — leave them for the caller.
return true;
}
// ===========================================================================
// CI2PEmbedded — singleton router management
// ===========================================================================
// Singleton
CI2PEmbedded* CI2PEmbedded::instance = nullptr;
CI2PEmbedded* CI2PEmbedded::GetInstance()
{
if (!instance)
instance = new CI2PEmbedded();
return instance;
}
CI2PEmbedded::CI2PEmbedded()
: running(false)
, socksPort(19100)
, samPort(7656)
, serverPort(0)
{
}
CI2PEmbedded::~CI2PEmbedded()
{
Stop();
}
std::string CI2PEmbedded::GetSocksProxy() const
{
return "127.0.0.1:" + std::to_string(socksPort);
}
// ---------------------------------------------------------------------------
// IsSamAvailable — quick TCP probe of the SAM bridge port
// ---------------------------------------------------------------------------
bool CI2PEmbedded::IsSamAvailable() const
{
#ifdef WIN32
SOCKET sock = ::socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET)
return false;
#else
int sock = ::socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
return false;
#endif
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = htons((uint16_t)samPort);
bool ok = (::connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0);
#ifdef WIN32
closesocket(sock);
#else
close(sock);
#endif
return ok;
}
// ---------------------------------------------------------------------------
// CreateConnection — factory for SAM v3 direct streaming connections
// ---------------------------------------------------------------------------
CI2PSamSocket* CI2PEmbedded::CreateConnection(const std::string& dest_b32, int port)
{
if (!running.load()) {
return nullptr;
}
auto* sam = new CI2PSamSocket();
if (!sam->Connect(dest_b32, port, "127.0.0.1", samPort)) {
// Caller can inspect via the object — but they don't have it yet,
// so log the error and clean up.
printf("I2P SAM connect failed: %s\n", sam->GetLastError().c_str());
delete sam;
return nullptr;
}
printf("I2P SAM stream connected to %s (raw socket, no SOCKS overhead)\n",
dest_b32.c_str());
return sam;
}
#ifdef ENABLE_I2P_EMBEDDED
// ========================================================================
// Embedded mode: i2pd runs in-process via libi2pd / i2p::api
// ========================================================================
#ifdef WIN32
// MinGW's rpcndr.h (pulled in by winsock2.h/windows.h) #defines
// 'interface' as 'struct' for COM support. i2pd's I2CP.h uses it as a
// parameter name (I2CPServer(const std::string& interface, ...)),
// causing a parse error. Undef before including any i2pd headers.
#undef interface
#endif
// i2pd C++ API
#include "Config.h"
#include "Log.h"
#include "FS.h"
#include "Crypto.h"
#include "NetDb.hpp"
#include "Transports.h"
#include "Tunnel.h"
#include "RouterContext.h"
#include "Streaming.h"
#include "Destination.h"
#include "ClientContext.h"
#include "I2PTunnel.h"
#include "api.h"
static std::unique_ptr<i2p::client::I2PServerTunnel> g_i2pServerTunnel;
static std::shared_ptr<i2p::client::ClientDestination> g_i2pServerDestination;
bool CI2PEmbedded::Start(int socks, int sam, int server)
{
if (running.load()) return true;
// ----------------------------------------------------------------
// 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();
// Prepare i2pd data directory under the wallet's data dir
i2pDataDir = (::GetDataDir() / "i2p_data").string();
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.
// 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());
if (!conf.is_open()) {
lastError = "Failed to write i2pd.conf";
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";
// SOCKS proxy for outbound .i2p connections (P2P transport)
conf << "[socksproxy]\n";
conf << "enabled = true\n";
conf << "address = 127.0.0.1\n";
conf << "port = " << socksPort << "\n";
conf << "keys = socks-proxy.dat\n";
conf << "\n";
// SAM bridge for SAM v3 direct streaming API
conf << "[sam]\n";
conf << "enabled = true\n";
conf << "address = 127.0.0.1\n";
conf << "port = " << samPort << "\n";
conf << "\n";
// Disable HTTP 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";
conf << "\n";
// Disable I2P control protocol
conf << "[i2pcontrol]\n";
conf << "enabled = false\n";
conf << "\n";
// Disable BOB
conf << "[bob]\n";
conf << "enabled = false\n";
conf << "\n";
conf.close();
}
// Write tunnels.conf BEFORE Start() — ClientContext::Start() reads this
// file to create server/client tunnels. The server tunnel is the I2P
// equivalent of a Tor hidden service: it forwards inbound I2P connections
// to the Triangles P2P listen port.
if (serverPort > 0) {
fs::path tunnelConfPath = fs::path(i2pDataDir) / "tunnels.conf";
std::ofstream tunnelConf(tunnelConfPath.string());
if (tunnelConf.is_open()) {
tunnelConf << "# Auto-generated by Triangles embedded I2P\n";
tunnelConf << "[triangles-p2p]\n";
tunnelConf << "type = server\n";
tunnelConf << "host = 127.0.0.1\n";
tunnelConf << "port = " << serverPort << "\n";
tunnelConf << "keys = triangles-p2p-keys.dat\n";
tunnelConf << "inbound.length = 3\n";
tunnelConf << "outbound.length = 3\n";
tunnelConf << "inbound.quantity = 5\n";
tunnelConf << "outbound.quantity = 5\n";
tunnelConf.close();
printf("Embedded I2P: server tunnel configured on port %d\n", serverPort);
}
}
// Build argv for i2pd initialization. Pass --datadir and --conf on the
// command line (not just in the conf file) because i2pd's ParseCmdline
// runs BEFORE ParseConfig, and DetectDataDir needs the datadir early.
std::vector<std::string> argvStrings;
argvStrings.push_back("i2pd");
argvStrings.push_back("--datadir");
argvStrings.push_back(i2pDataDir);
argvStrings.push_back("--conf");
argvStrings.push_back((fs::path(i2pDataDir) / "i2pd.conf").string());
std::vector<char*> argvPtrs;
for (auto& s : argvStrings)
argvPtrs.push_back(&s[0]);
argvPtrs.push_back(nullptr);
// 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 {
// ----------------------------------------------------------------
// 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);
// ----------------------------------------------------------------
// 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;
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);
}
// 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);
printf("Embedded I2P: router started, starting client services...\n");
fflush(stdout);
// Start SAM bridge, SOCKS proxy, and server tunnel
i2p::client::context.Start();
printf("Embedded I2P: SOCKS proxy at 127.0.0.1:%d, SAM at 127.0.0.1:%d\n",
socksPort, samPort);
fflush(stdout);
// Wait for SOCKS proxy + SAM bridge to become available
printf("Embedded I2P: waiting for SOCKS proxy and SAM bridge...\n");
bool socksReady = false;
bool samReady = false;
for (int i = 0; i < 300; i++) {
MilliSleep(1000);
if (fShutdown) {
Stop();
return;
}
if (!socksReady) {
#ifdef WIN32
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock != INVALID_SOCKET) {
#else
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock >= 0) {
#endif
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = htons(socksPort);
bool up = (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0);
#ifdef WIN32
closesocket(sock);
#else
close(sock);
#endif
if (up) {
socksReady = true;
printf("Embedded I2P: SOCKS proxy ready on port %d (took %ds)\n",
socksPort, i + 1);
}
}
}
if (!samReady) {
samReady = IsSamAvailable();
if (samReady) {
printf("Embedded I2P: SAM v3 bridge ready on port %d (took %ds)\n",
samPort, i + 1);
}
}
if (socksReady && samReady) {
printf("Embedded I2P: all I2P endpoints ready (SOCKS %d + SAM %d)\n",
socksPort, samPort);
break;
}
if (i > 0 && i % 30 == 0) {
printf("Embedded I2P: still bootstrapping (%ds elapsed, SOCKS:%s SAM:%s)...\n",
i, socksReady ? "ready" : "wait",
samReady ? "ready" : "wait");
}
}
// ----------------------------------------------------------------
// Populate .b32.i2p address — use the SERVER TUNNEL destination,
// NOT the embedded router identity.
//
// The Triangles P2P layer listens on the local port via the
// server tunnel loaded from `triangles-p2p-keys.dat`. That tunnel
// publishes a LeaseSet whose destination is the ident hash of
// the keys file (a separate identity from the i2pd router
// itself). Peers that dial the address we advertise must hit
// THAT LeaseSet, or they get SOCKS code 4 / "LeaseSet not found"
// from the floodfill network.
//
// Strategy (preferred first):
// 1. Walk i2p::client::context.GetServerTunnels() and pick the
// server tunnel whose keys file matches
// triangles-p2p-keys.dat — presence in the registry confirms
// the tunnel has registered, so the LeaseSet will be
// published and reachable once the i2pd netDb has it.
// 2. Fall back to parsing triangles-p2p-keys.dat directly via
// i2p::data::PrivateKeys::FromBuffer (binary blob format,
// length == PrivateKeys::GetFullLen()) if the tunnel hasn't
// registered yet (race during the same startup pass).
// 3. Last-resort error log if neither works — better to leave
// i2pHostname empty than advertise the wrong identity.
// ----------------------------------------------------------------
bool advertised = false;
std::string serverKeysPath = (fs::path(i2pDataDir) / "triangles-p2p-keys.dat").string();
// Step 1+2 (combined): authoritative ident hash comes from the
// keys file the server tunnel was loaded from. The tunnel
// registry's GetServerTunnels() maps (IdentHash, port) → tunnel,
// so we just compare each registered tunnel's ident hash
// against what triangles-p2p-keys.dat actually contains. If
// any registered tunnel matches, that's our address. Otherwise
// we fall back to publishing the keys-file ident hash directly
// (the tunnel will register a moment later — the keys file is
// the source of truth either way).
//
// File format: binary blob, length = PrivateKeys::GetFullLen().
// i2pd reads it with FromBuffer() in libi2pd_client/ClientContext.cpp:285-313.
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 {
printf("Embedded I2P: cannot open %s for server tunnel keys\n",
serverKeysPath.c_str());
}
} catch (const std::exception& e) {
printf("Embedded I2P: keys-file ident hash load failed: %s\n", e.what());
} catch (...) {
printf("Embedded I2P: keys-file ident hash load failed: unknown exception\n");
}
// Try the live registry first — if a registered server tunnel
// matches the keys-file hash, the LeaseSet will be published and
// inbound peers can reach us via that destination.
if (!keysFileIdentB32.empty()) {
try {
for (const auto& kv : i2p::client::context.GetServerTunnels()) {
const i2p::data::IdentHash& dest = kv.first.first;
if (dest.ToBase32() == keysFileIdentB32) {
i2pHostname = keysFileIdentB32 + ".b32.i2p";
advertised = true;
printf("Embedded I2P: server tunnel address (live registry) = %s\n",
i2pHostname.c_str());
break;
}
}
} catch (const std::exception& e) {
printf("Embedded I2P: server tunnel registry read failed: %s\n", e.what());
} catch (...) {
printf("Embedded I2P: server tunnel registry read failed: unknown exception\n");
}
}
// Fall back: trust the keys file even before the tunnel registers.
if (!advertised && !keysFileIdentB32.empty()) {
i2pHostname = keysFileIdentB32 + ".b32.i2p";
advertised = true;
printf("Embedded I2P: server tunnel address (from keys file) = %s\n",
i2pHostname.c_str());
}
// Step 3: explicit failure rather than advertise router identity.
if (!advertised) {
i2pHostname.clear();
printf("Embedded I2P: server tunnel destination not available yet, "
"Qt timer will retry\n");
}
fflush(stdout);
} 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);
}
});
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()) {
if (routerThread.joinable()) routerThread.join();
return;
}
printf("Requesting embedded I2P shutdown...\n");
try {
// Stop client context (SAM, SOCKS, tunnels)
i2p::client::context.Stop();
// Stop the router
i2p::api::StopI2P();
// Terminate crypto
i2p::api::TerminateI2P();
} catch (const std::exception& e) {
printf("WARNING: error during I2P shutdown: %s\n", e.what());
}
// 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);
}
#else // !ENABLE_I2P_EMBEDDED
// ========================================================================
// Fallback stubs: embedded I2P not compiled in
// ========================================================================
bool CI2PEmbedded::Start(int socks, int sam, int server)
{
printf("Embedded I2P not compiled in (ENABLE_I2P_EMBEDDED not defined).\n");
socksPort = socks;
samPort = sam;
serverPort = server;
i2pDataDir = (::GetDataDir() / "i2p_data").string();
lastError = "I2P support not compiled in. Build with -DUSE_I2P_EMBEDDED=ON";
return false;
}
void CI2PEmbedded::Stop()
{
running.store(false);
}
#endif // ENABLE_I2P_EMBEDDED
// ========================================================================
// Global hooks (called from init.cpp)
// ========================================================================
bool StartEmbeddedI2P()
{
bool enableI2P = GetBoolArg("-i2p", true);
if (!enableI2P) {
printf("I2P disabled by -i2p=0 flag\n");
return false;
}
int socksPort = GetArg("-i2psocks", 19100);
int samPort = GetArg("-i2psam", 7656);
int serverPort = GetArg("-i2phsport", GetListenPort());
return CI2PEmbedded::GetInstance()->Start(socksPort, samPort, serverPort);
}
void StopEmbeddedI2P()
{
CI2PEmbedded::GetInstance()->Stop();
}
-147
View File
@@ -1,147 +0,0 @@
// Copyright (c) 2025-2026 Triangles developers
// Embedded I2P (i2pd) integration - runs an I2P router in-process
// Distributed under the MIT/X11 software license
#ifndef TRIANGLES_I2P_EMBEDDED_H
#define TRIANGLES_I2P_EMBEDDED_H
#include <string>
#include <atomic>
// Cross-platform socket handle for SAM v3 streaming API.
// On Windows this is the native SOCKET type; on POSIX it is int (fd).
#ifdef WIN32
# include <winsock2.h>
typedef SOCKET I2pSocket_t;
# define I2P_INVALID_SOCKET INVALID_SOCKET
#else
typedef int I2pSocket_t;
# define I2P_INVALID_SOCKET (-1)
#endif
// ---------------------------------------------------------------------------
// CI2PSamSocket — SAM v3 direct streaming socket
//
// Wraps a raw TCP socket to the i2pd SAM bridge. After Connect() succeeds,
// the underlying socket is a bidirectional byte stream to the I2P
// destination with NO SOCKS overhead. The Triangles P2P layer can read and
// write directly once ownership is taken via GetRawSocket().
//
// Lifecycle:
// 1. Construct
// 2. Connect(dest_b32, port) — performs SAM SESSION CREATE + STREAM CONNECT
// 3. GetRawSocket() — take the fd for direct read/write
// 4. The fd must be closed by the caller (e.g. via CloseSocket())
//
// If Connect() fails, GetLastError() returns a human-readable diagnostic.
// ---------------------------------------------------------------------------
class CI2PSamSocket
{
public:
CI2PSamSocket();
~CI2PSamSocket();
CI2PSamSocket(const CI2PSamSocket&) = delete;
CI2PSamSocket& operator=(const CI2PSamSocket&) = delete;
// Perform the full SAM v3 handshake (HELLO → SESSION CREATE → STREAM CONNECT)
// to reach dest_b32 (a .b32.i2p hostname). samHost/samPort identify the
// local SAM bridge (default 127.0.0.1:7656).
//
// The |port| argument is accepted for API symmetry with the Tor SOCKS
// connection factory but is not part of the SAM v3 STREAM CONNECT request
// (I2P destinations are address-only; there is no TCP-style port).
bool Connect(const std::string& dest_b32, int port,
const std::string& samHost = "127.0.0.1", int samPort = 7656);
// Release ownership of the raw socket fd. After this call the object
// will not close it and the caller is responsible for cleanup.
// Returns I2P_INVALID_SOCKET if not connected.
I2pSocket_t GetRawSocket();
// Close the socket if still owned (no-op after GetRawSocket()).
void CloseSocket();
bool IsValid() const { return rawSocket != I2P_INVALID_SOCKET; }
std::string GetLastError() const { return lastError; }
// The base64 local destination returned by SESSION STATUS (may be empty).
const std::string& GetLocalDestination() const { return localDestination; }
private:
I2pSocket_t rawSocket;
std::string sessionId;
std::string localDestination;
std::string lastError;
std::string recvBuffer; // partial SAM response buffering
// --- SAM protocol helpers ---
bool SamConnect(const std::string& host, int port);
bool SendLine(const std::string& line);
bool ReadLine(std::string& lineOut);
static std::string ParseValue(const std::string& line, const std::string& key);
};
// Embedded I2P router state
class CI2PEmbedded
{
private:
static CI2PEmbedded* instance;
std::atomic<bool> running;
int socksPort; // i2pd SOCKS proxy port (for outbound .i2p connections)
int samPort; // i2pd SAM bridge port (for SAM v3 protocol)
int serverPort; // Triangles P2P listen port (for incoming I2P connections)
std::string i2pDataDir; // i2pd data directory (under wallet datadir)
std::string i2pHostname; // Our .b32.i2p address (available after router startup)
std::string lastError;
// 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;
public:
static CI2PEmbedded* GetInstance();
CI2PEmbedded();
~CI2PEmbedded();
// Start embedded i2pd router (blocks calling thread briefly during init)
bool Start(int socksPort = 19100, int samPort = 7656, int serverPort = 0);
// Request i2pd to shut down
void Stop();
// Check if i2pd is running
bool IsRunning() const { return running.load(); }
void SetRunning(bool value) { running.store(value); }
// Get the SOCKS5 proxy address for outbound .i2p connections
std::string GetSocksProxy() const;
int GetSocksPort() const { return socksPort; }
int GetSamPort() const { return samPort; }
int GetServerPort() const { return serverPort; }
const std::string& GetDataDir() const { return i2pDataDir; }
// Get our .b32.i2p destination address
std::string GetI2PAddress() const { return i2pHostname; }
std::string GetStartupError() const { return lastError; }
void SetStartupError(const std::string& value) { lastError = value; }
// -------------------------------------------------------------------
// SAM v3 direct streaming API
// -------------------------------------------------------------------
// Create a SAM v3 connection to a .b32.i2p destination.
// Returns a heap-allocated CI2PSamSocket on success (caller owns it
// and must CloseSocket / delete), or nullptr on failure. Use
// GetLastError() on the returned object for diagnostics.
CI2PSamSocket* CreateConnection(const std::string& dest_b32, int port);
// Probe whether the SAM bridge port is accepting TCP connections.
bool IsSamAvailable() const;
};
// Global init/shutdown hooks (called from init.cpp)
bool StartEmbeddedI2P();
void StopEmbeddedI2P();
#endif // TRIANGLES_I2P_EMBEDDED_H
-30
View File
@@ -1,30 +0,0 @@
#ifndef TRIANGLES_I2PSEED_H
#define TRIANGLES_I2PSEED_H
// Hardcoded I2P seed nodes for initial peer discovery.
// These are .b32.i2p addresses (Destination hashes).
// Nodes must run i2pd (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.
// 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 are also available at:
// https://seeds.cryptographic-triangles.org/i2p-seeds.txt
static const char *strMainNetI2PSeed[][1] = {
// 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}
};
static const char *strTestNetI2PSeed[][1] = {
{nullptr}
};
#endif
-368
View File
@@ -1,368 +0,0 @@
// Copyright (c) 2024 Triangles developers
// I2P Router Process Manager - launches and manages a bundled i2pd binary
// Distributed under the MIT/X11 software license
#ifdef WIN32
#define NOMINMAX
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif
#endif
#include "i2p_process.h"
#include "util.h"
#include <filesystem>
#include <fstream>
#include <sstream>
#include <vector>
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <tlhelp32.h>
#include <windows.h>
#else
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#endif
namespace fs = std::filesystem;
static CI2PProcess* i2pProcessInstance = nullptr;
CI2PProcess* CI2PProcess::GetInstance()
{
if (!i2pProcessInstance)
i2pProcessInstance = new CI2PProcess();
return i2pProcessInstance;
}
CI2PProcess::CI2PProcess()
: samPort(7656)
, running(false)
, fExternal(false)
#ifdef WIN32
, hProcess(nullptr)
, hJob(nullptr)
, processId(0)
#else
, processId(0)
#endif
{
}
CI2PProcess::~CI2PProcess()
{
Stop();
}
// Try a quick TCP connect; success means something is already listening
// (e.g. the SAM bridge is up, or an external router is running).
bool CI2PProcess::CanConnect(const std::string& host, int port)
{
#ifdef WIN32
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET) return false;
#else
int s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s < 0) return false;
#endif
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((unsigned short)port);
addr.sin_addr.s_addr = inet_addr(host.c_str());
bool ok = (connect(s, (struct sockaddr*)&addr, sizeof(addr)) == 0);
#ifdef WIN32
closesocket(s);
#else
close(s);
#endif
return ok;
}
std::string CI2PProcess::FindI2pdBinary()
{
std::vector<std::string> candidates;
#ifdef WIN32
const char* exeName = "i2pd.exe";
#else
const char* exeName = "i2pd";
#endif
// 1. Next to the wallet executable (this is how tor.exe is shipped).
try {
fs::path exeDir;
#ifdef WIN32
char buf[MAX_PATH];
if (GetModuleFileNameA(nullptr, buf, MAX_PATH) > 0)
exeDir = fs::path(buf).parent_path();
#else
exeDir = fs::current_path();
#endif
if (!exeDir.empty()) {
candidates.push_back((exeDir / exeName).string());
candidates.push_back((exeDir / "i2pd" / exeName).string());
candidates.push_back((exeDir / "I2P" / exeName).string());
}
} catch (...) {}
// 2. In / next to the data directory.
candidates.push_back((GetDataDir() / exeName).string());
candidates.push_back((GetDataDir() / "i2pd" / exeName).string());
// 3. Common system locations.
#ifdef WIN32
if (const char* pf = getenv("ProgramFiles"))
candidates.push_back(std::string(pf) + "\\i2pd\\" + exeName);
if (const char* pfx = getenv("ProgramFiles(x86)"))
candidates.push_back(std::string(pfx) + "\\i2pd\\" + exeName);
candidates.push_back(std::string("C:\\i2pd\\") + exeName);
#else
candidates.push_back("/usr/bin/i2pd");
candidates.push_back("/usr/local/bin/i2pd");
candidates.push_back("/opt/i2pd/bin/i2pd");
candidates.push_back("/opt/homebrew/bin/i2pd");
candidates.push_back("/usr/local/opt/i2pd/bin/i2pd");
#endif
for (const std::string& c : candidates) {
try {
if (fs::exists(c) && fs::is_regular_file(c)) {
printf("I2P: found i2pd binary at %s\n", c.c_str());
return c;
}
} catch (...) {}
}
return "";
}
bool CI2PProcess::WriteConfig()
{
fs::path dir(dataDir);
try {
fs::create_directories(dir);
} catch (const std::exception& e) {
lastError = std::string("Cannot create i2pd data directory: ") + e.what();
return false;
}
confPath = (dir / "i2pd.conf").string();
fs::path logPath = dir / "i2pd.log";
std::ofstream conf(confPath.c_str(), std::ios::trunc);
if (!conf.is_open()) {
lastError = "Cannot write i2pd.conf to " + confPath;
return false;
}
conf << "# Triangles Wallet I2P configuration (auto-generated)\n";
conf << "# Do not edit - this file is overwritten on startup\n\n";
conf << "daemon = false\n";
conf << "log = file\n";
conf << "logfile = " << logPath.string() << "\n";
conf << "datadir = " << dir.string() << "\n\n";
// The bridge our SAM client talks to.
conf << "[sam]\n";
conf << "enabled = true\n";
conf << "address = 127.0.0.1\n";
conf << "port = " << samPort << "\n\n";
// We only need SAM; keep everything else off to minimise footprint.
conf << "[httpproxy]\nenabled = false\n\n";
conf << "[socksproxy]\nenabled = false\n\n";
conf << "[http]\nenabled = false\n\n";
conf << "[i2pcontrol]\nenabled = false\n";
conf.close();
printf("I2P: wrote i2pd config to %s (SAM port %d)\n", confPath.c_str(), samPort);
return true;
}
bool CI2PProcess::Start(const std::string& dataDirIn, int samPortIn)
{
dataDir = dataDirIn;
samPort = samPortIn;
fExternal = false;
lastError.clear();
// If a SAM bridge is already up, use it instead of launching our own.
if (CanConnect("127.0.0.1", samPort)) {
printf("I2P: detected an I2P router already listening on SAM port %d; using it\n", samPort);
fExternal = true;
return true;
}
binaryPath = FindI2pdBinary();
if (binaryPath.empty()) {
lastError = "No i2pd binary found (ship i2pd alongside the wallet, like tor)";
printf("I2P: %s\n", lastError.c_str());
return false;
}
if (!WriteConfig())
return false;
printf("I2P: starting i2pd: %s --conf %s\n", binaryPath.c_str(), confPath.c_str());
#ifdef WIN32
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
ZeroMemory(&pi, sizeof(pi));
std::string cmdLine = "\"" + binaryPath + "\" --conf \"" + confPath + "\"";
if (!CreateProcessA(nullptr, (LPSTR)cmdLine.c_str(), nullptr, nullptr,
FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) {
DWORD err = ::GetLastError();
lastError = strprintf("CreateProcess failed for i2pd '%s' (Windows error %lu)", binaryPath.c_str(), err);
printf("I2P: ERROR %s\n", lastError.c_str());
return false;
}
hProcess = pi.hProcess;
processId = pi.dwProcessId;
CloseHandle(pi.hThread);
// Kill i2pd if the wallet dies (matches the embedded Tor behaviour).
hJob = CreateJobObject(nullptr, nullptr);
if (hJob) {
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {};
jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo));
if (!AssignProcessToJobObject(hJob, hProcess))
printf("I2P: WARNING could not assign i2pd to Job Object (error %lu)\n", GetLastError());
}
printf("I2P: i2pd started (PID %lu)\n", processId);
#else
pid_t pid = fork();
if (pid < 0) {
lastError = "Failed to fork for i2pd process";
printf("I2P: ERROR %s\n", lastError.c_str());
return false;
}
if (pid == 0) {
freopen("/dev/null", "w", stdout);
freopen("/dev/null", "w", stderr);
execl(binaryPath.c_str(), binaryPath.c_str(),
"--conf", confPath.c_str(), (char*)nullptr);
_exit(1);
}
processId = pid;
printf("I2P: i2pd started (PID %d)\n", processId);
#endif
running = true;
// Wait for the SAM bridge to come up. The bridge opens quickly; tunnel
// build (needed for actual connectivity) continues in the background.
printf("I2P: waiting for SAM bridge on port %d...\n", samPort);
for (int i = 0; i < 45; i++) {
MilliSleep(1000);
if (fShutdown) {
Stop();
return false;
}
if (CanConnect("127.0.0.1", samPort)) {
printf("I2P: SAM bridge ready on port %d (took %ds)\n", samPort, i + 1);
return true;
}
if (!IsRunning()) {
lastError = "i2pd exited during start-up before the SAM bridge became ready";
printf("I2P: ERROR %s\n", lastError.c_str());
running = false;
return false;
}
}
lastError = strprintf("i2pd started but SAM port %d not ready after 45s", samPort);
printf("I2P: WARNING %s (it may still be building tunnels)\n", lastError.c_str());
return true;
}
void CI2PProcess::Stop()
{
if (fExternal) {
// We never launched it; leave the user's router running.
running = false;
return;
}
if (!running) return;
#ifdef WIN32
if (hProcess != nullptr) {
printf("I2P: stopping i2pd (PID %lu)...\n", processId);
TerminateProcess(hProcess, 0);
WaitForSingleObject(hProcess, 5000);
CloseHandle(hProcess);
hProcess = nullptr;
}
if (hJob != nullptr) {
CloseHandle(hJob);
hJob = nullptr;
}
#else
if (processId > 0) {
printf("I2P: stopping i2pd (PID %d)...\n", processId);
kill(processId, SIGTERM);
for (int i = 0; i < 50; i++) {
int status;
pid_t result = waitpid(processId, &status, WNOHANG);
if (result != 0) break;
MilliSleep(100);
}
kill(processId, SIGKILL);
waitpid(processId, nullptr, 0);
}
#endif
processId = 0;
running = false;
printf("I2P: i2pd stopped\n");
}
bool CI2PProcess::IsRunning()
{
if (fExternal) return true;
if (!running) return false;
#ifdef WIN32
if (hProcess == nullptr) return false;
DWORD exitCode;
if (GetExitCodeProcess(hProcess, &exitCode))
return (exitCode == STILL_ACTIVE);
return false;
#else
if (processId <= 0) return false;
int status;
pid_t result = waitpid(processId, &status, WNOHANG);
return (result == 0); // 0 => still running
#endif
}
bool StartEmbeddedI2P(const std::string& dataDir, int samPort)
{
return CI2PProcess::GetInstance()->Start(dataDir, samPort);
}
void StopEmbeddedI2P()
{
CI2PProcess::GetInstance()->Stop();
}
-70
View File
@@ -1,70 +0,0 @@
// 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
+90 -706
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -12,7 +12,6 @@
extern std::unique_ptr<CWallet> pwalletMain;
extern std::string strWalletFileName;
void StartShutdown();
void MarkShutdownFailure();
bool ShutdownRequested();
void Shutdown(void* parg);
bool AppInit2();
@@ -20,3 +19,4 @@ std::string HelpMessage();
#endif
+8 -21
View File
@@ -31,27 +31,14 @@ int64_t GetWeight(int64_t nIntervalBeginning, int64_t nIntervalEnd)
if (nAge < 0)
return 0;
// 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.
// 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
if (pindexBest && pindexBest->nHeight >= FORK_HEIGHT_V5)
return min(nAge, STAKE_AGE_SOFT_CAP);
return min(nAge, (int64_t)nStakeMaxAge);
}
+22 -43
View File
@@ -190,49 +190,28 @@ bool CCryptoKeyStore::GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) co
return false;
}
bool CCryptoKeyStore::PrepareKeyEncryption(CKeyingMaterial& vMasterKeyIn,
CryptedKeyMap& cryptedKeysOut) const
{
LOCK(cs_KeyStore);
if (!mapCryptedKeys.empty() || IsCrypted())
return false;
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));
{
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();
}
return true;
}
+1 -9
View File
@@ -9,8 +9,6 @@
#include "util_signal.h"
#include "sync.h"
#include <utility>
class CScript;
/** A virtual base class for key stores */
@@ -114,13 +112,7 @@ protected:
bool SetCrypted();
// 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.
// will encrypt previously unencrypted keys
bool EncryptKeys(CKeyingMaterial& vMasterKeyIn);
bool Unlock(const CKeyingMaterial& vMasterKeyIn);
+423 -1338
View File
File diff suppressed because it is too large Load Diff
+4 -86
View File
@@ -42,18 +42,7 @@ 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;
// 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_REORG_DEPTH = 100; // reject reorgs deeper than this (finality)
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;
@@ -94,13 +83,11 @@ extern unsigned int nStakeMinAge;
extern unsigned int nNodeLifespan;
extern int nCoinbaseMaturity;
extern int nBestHeight;
extern bool fLoadedFromSnapshot; // true after successful UtxoSnapshot::LoadSnapshot
extern uint256 nBestChainTrust;
extern uint256 nBestInvalidTrust;
extern uint256 hashBestChain;
extern CBlockIndex* pindexBest;
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 CBlockIndex* pindexFinalized; // auto-checkpoint: deepest finalized block
extern unsigned int nTransactionsUpdated;
extern uint64_t nLastBlockTx;
extern uint64_t nLastBlockSize;
@@ -150,34 +137,7 @@ 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);
@@ -1145,17 +1105,8 @@ public:
return error("%s() : deserialize or I/O error", __PRETTY_FUNCTION__);
}
// 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))
// Check the header
if (fReadTransactions && IsProofOfWork() && !CheckProofOfWork(GetHash(), nBits))
return error("CBlock::ReadFromDisk() : errors in block header");
return true;
@@ -1583,39 +1534,6 @@ 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
{
+16 -35
View File
@@ -58,17 +58,11 @@ public:
TxPriorityCompare(bool _byFee) : byFee(_byFee) { }
bool operator()(const TxPriority& a, const TxPriority& b)
{
// #8: Fee-weighted priority for PoS staking.
// When sorting by fee (PoS mode), apply a 2x weight to fees so
// higher-fee transactions are prioritized over coin-age-only ones.
// This maximizes staking rewards for the minter.
if (byFee)
{
double feeA = std::get<1>(a) * 2.0; // fee boost
double feeB = std::get<1>(b) * 2.0;
if (feeA == feeB)
if (std::get<1>(a) == std::get<1>(b))
return std::get<0>(a) < std::get<0>(b);
return feeA < feeB;
return std::get<1>(a) < std::get<1>(b);
}
else
{
@@ -391,6 +385,7 @@ 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)
@@ -406,38 +401,24 @@ void StakeMiner(CWallet *pwallet)
return;
}
// 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)
while (!fForceStaking && (vNodes.empty() || IsInitialBlockDownload()))
{
if (!IsStakingSafe(pwallet, vNodes))
nLastCoinStakeSearchInterval = 0;
fTryToSync = true;
MilliSleep(1000);
if (fShutdown)
return;
}
if (fTryToSync && !fForceStaking)
{
fTryToSync = false;
if (vNodes.size() < 2 || nBestHeight < GetNumBlocksOfPeers())
{
nLastCoinStakeSearchInterval = 0;
MilliSleep(1000);
MilliSleep(60000);
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)

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