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
204 changed files with 1171 additions and 90677 deletions
-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)"
+147 -207
View File
@@ -13,24 +13,20 @@ jobs:
runs-on: ubuntu-22.04
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Checkout (with history for submodule)
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Init submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build \
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev
- name: Build RocksDB from source
# Ubuntu 22.04's librocksdb-dev is 6.11.4 which CMakeLists.txt now
# refuses to configure against (need >= 7.4 for XXH3 per-block
# checksum). Build 8.9.1 from source — same version DNS2 ships —
# into /usr/local so CMake's find_library picks it up first.
run: sudo bash scripts/ci/build-rocksdb.sh
librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
- name: Configure
run: |
@@ -41,49 +37,9 @@ jobs:
-DBUILD_TESTS=ON \
-DUSE_UPNP=OFF
- name: Build libtor (embedded Tor static lib)
# USE_TOR_EMBEDDED defaults to ON and the daemon/Qt GUI both
# link -ltor. The Tor source is a git submodule but libtor.a
# is NOT built by cmake. build-libtor.sh defaults to /mingw64
# paths which don't exist on the ubuntu-22.04 runner; pass
# /usr where libevent-dev/libssl-dev/zlib1g-dev install.
run: |
sudo apt-get install -y libevent-dev libssl-dev zlib1g-dev
LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \
bash src/tor/build-libtor.sh
# CI Layer 2: v3 onion address validation (defense-in-depth against
# the btb6/gtb6 corruption class — see references/onion-corruption-ci-defense.md).
# Validates: (a) src/onionseed.h hardcoded seeds, (b) contrib/triangles.conf.example
# operator-facing example. Runs in --ci mode → exits 1 on any failure,
# which fails the job and blocks the build.
- name: Validate .onion addresses (CI gate)
run: |
python3 scripts/validate_onion_seeds.py \
--ci \
--against src/onionseed.h \
src/onionseed.h \
contrib/triangles.conf.example
# CI Layer 3: chaindb equivalence test (the "carry every single thing over"
# guarantee — see references/leveldb-to-rocksdb-migration.md Phase A).
# Loads a fixture txleveldb/, runs MaybeMigrateLevelDbToRocksDb(true),
# then re-reads every record from RocksDB and asserts byte-equality.
# This is the proof that no data is lost in the LevelDB→RocksDB migration.
- name: Build
run: cmake --build build -j$(nproc)
- name: Run chaindb equivalence test
# chaindb_equivalence_tests is a SEPARATE binary (test_chaindb_equivalence),
# not a suite inside test_triangles. Run the right binary.
run: |
if [ -x build/bin/test_chaindb_equivalence ]; then
./build/bin/test_chaindb_equivalence --log_level=test_suite
else
echo "test_chaindb_equivalence not built — skipping chaindb equivalence"
exit 0
fi
- name: Run unit tests
run: cd build && ctest --output-on-failure || true
@@ -103,20 +59,20 @@ jobs:
# and BDB until they're fixed file-by-file.
SAN_FLAGS: "-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined -fno-sanitize=alignment,signed-integer-overflow,vptr"
steps:
- uses: actions/checkout@v4
- name: Checkout (with history for submodule)
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Init submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build \
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev
- name: Build RocksDB from source
run: sudo bash scripts/ci/build-rocksdb.sh
librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
- name: Configure with sanitizers
run: |
@@ -131,17 +87,6 @@ jobs:
-DBUILD_TESTS=ON \
-DUSE_UPNP=OFF
- name: Build libtor (embedded Tor static lib)
# USE_TOR_EMBEDDED defaults to ON and the daemon/Qt GUI both
# link -ltor. The Tor source is a git submodule but libtor.a
# is NOT built by cmake. build-libtor.sh defaults to /mingw64
# paths which don't exist on the ubuntu-22.04 runner; pass
# /usr where libevent-dev/libssl-dev/zlib1g-dev install.
run: |
sudo apt-get install -y libevent-dev libssl-dev zlib1g-dev
LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \
bash src/tor/build-libtor.sh
- name: Build
run: cmake --build build-san -j$(nproc)
@@ -154,9 +99,14 @@ jobs:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4
- name: Checkout (with history for submodule)
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Init submodules
run: git submodule update --init --recursive
shell: bash
- uses: msys2/setup-msys2@v2
with:
@@ -175,7 +125,6 @@ jobs:
mingw-w64-x86_64-miniupnpc
mingw-w64-x86_64-zlib
mingw-w64-x86_64-rocksdb
mingw-w64-x86_64-autotools
- name: Set VERSION
run: |
@@ -196,16 +145,7 @@ jobs:
-DBUILD_DAEMON=OFF \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON \
-DUSE_QRCODE=OFF \
-DUSE_I2P_EMBEDDED=ON
- name: Build libtor (embedded Tor static lib)
# Windows Qt GUI also transitively links -ltor via triangles_common.
# msys2 default install puts everything in /mingw64.
run: bash src/tor/build-libtor.sh
- name: Build libi2pd (embedded I2P static lib)
run: bash src/i2p/build-libi2pd.sh
-DUSE_QRCODE=OFF
- name: Build
run: cmake --build build -j$(nproc)
@@ -268,22 +208,6 @@ jobs:
echo "=== dist/ contents ==="
find dist/ -type f | head -50
- name: Upload portable wallet zip
# Portable Windows GUI wallet ZIP — what users extract to a folder
# and run triangles-qt.exe directly. This is what the Chocolatey
# package and most manual downloads expect.
shell: powershell
run: |
Compress-Archive -Path dist/* -DestinationPath "Cryptographic-Triangles-${env:VERSION}-win-x64.zip" -Force
echo "Created Cryptographic-Triangles-${env:VERSION}-win-x64.zip"
Get-Item "Cryptographic-Triangles-${env:VERSION}-win-x64.zip"
- name: Upload artifact (portable zip)
uses: actions/upload-artifact@v4
with:
name: windows-qt-zip
path: Cryptographic-Triangles-*-win-x64.zip
- name: Download Tor
shell: powershell
run: |
@@ -333,9 +257,14 @@ jobs:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4
- name: Checkout (with history for submodule)
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Init submodules
run: git submodule update --init --recursive
shell: bash
- uses: msys2/setup-msys2@v2
with:
@@ -352,7 +281,6 @@ jobs:
mingw-w64-x86_64-miniupnpc
mingw-w64-x86_64-zlib
mingw-w64-x86_64-rocksdb
mingw-w64-x86_64-autotools
- name: Configure
run: |
@@ -360,28 +288,23 @@ jobs:
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_QT=OFF \
-DBUILD_DAEMON=ON \
-DBUILD_CLI=ON \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON \
-DUSE_I2P_EMBEDDED=ON
- name: Build libtor (embedded Tor static lib)
# Windows: msys2 default install puts everything in /mingw64,
# which is exactly the script's default. Just invoke it.
# See v5.9.25-fork-detection run #466 for why this is needed.
run: bash src/tor/build-libtor.sh
- name: Build libi2pd (embedded I2P static lib)
run: bash src/i2p/build-libi2pd.sh
-DUSE_UPNP=ON
- name: Build
run: |
cmake --build build -j$(nproc)
strip --strip-all build/bin/trianglesd.exe
strip --strip-all build/bin/triangles-cli.exe
- name: Package daemon with DLLs
run: bash scripts/ci/package-windows-daemon.sh daemon-dist trianglesd triangles-cli
run: |
mkdir -p daemon-dist/tor
cp build/bin/trianglesd.exe daemon-dist/
# Copy all linked DLLs from MSYS2
ldd build/bin/trianglesd.exe | grep '/mingw64' | awk '{print $3}' | while read dll; do
cp "$dll" daemon-dist/ 2>/dev/null || true
done
- name: Bundle Tor for daemon
shell: powershell
@@ -404,9 +327,13 @@ jobs:
build-linux-qt:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Checkout (with history for submodule)
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Init submodules
run: git submodule update --init --recursive
- name: Set VERSION
run: |
@@ -425,11 +352,7 @@ jobs:
sudo apt-get install -y build-essential cmake ninja-build \
qtbase5-dev qttools5-dev-tools \
libboost-all-dev libssl-dev libdb++-dev \
libleveldb-dev libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev
- name: Build RocksDB from source
run: sudo bash scripts/ci/build-rocksdb.sh
libleveldb-dev librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
- name: Configure
run: |
@@ -438,20 +361,7 @@ jobs:
-DBUILD_QT=ON \
-DBUILD_DAEMON=OFF \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON \
-DUSE_I2P_EMBEDDED=ON
- name: Build libtor (embedded Tor static lib)
# Linux Qt GUI also transitively links -ltor via triangles_common.
# build-libtor.sh defaults to /mingw64; pass /usr where the
# libevent-dev, libssl-dev, zlib1g-dev packages install.
run: |
sudo apt-get install -y libevent-dev libssl-dev zlib1g-dev
LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \
bash src/tor/build-libtor.sh
- name: Build libi2pd (embedded I2P static lib)
run: bash src/i2p/build-libi2pd.sh
-DUSE_UPNP=ON
- name: Build
run: cmake --build build -j$(nproc)
@@ -540,9 +450,13 @@ jobs:
build-linux-daemon:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Checkout (with history for submodule)
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Init submodules
run: git submodule update --init --recursive
- name: Set VERSION
run: |
@@ -560,11 +474,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build \
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev
- name: Build RocksDB from source
run: sudo bash scripts/ci/build-rocksdb.sh
librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
- name: Configure
run: |
@@ -572,35 +482,100 @@ jobs:
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_QT=OFF \
-DBUILD_DAEMON=ON \
-DBUILD_CLI=ON \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON \
-DUSE_I2P_EMBEDDED=ON
- name: Build libtor (embedded Tor static lib)
# USE_TOR_EMBEDDED defaults to ON and the daemon/Qt GUI both
# link -ltor. The Tor source is a git submodule but libtor.a
# is NOT built by cmake. build-libtor.sh defaults to /mingw64
# paths which don't exist on the ubuntu-22.04 runner; pass
# /usr where libevent-dev/libssl-dev/zlib1g-dev install.
run: |
sudo apt-get install -y libevent-dev libssl-dev zlib1g-dev
LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \
bash src/tor/build-libtor.sh
- name: Build libi2pd (embedded I2P static lib)
run: bash src/i2p/build-libi2pd.sh
-DUSE_UPNP=ON
- name: Build
run: cmake --build build -j$(nproc)
- name: Strip binary
run: |
strip --strip-all build/bin/trianglesd
strip --strip-all build/bin/triangles-cli
run: strip --strip-all build/bin/trianglesd
- name: Build .deb package (fully self-contained)
run: bash scripts/ci/package-linux-daemon.sh "${VERSION}"
run: |
TOR_VERSION="15.0.9"
curl -sL "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-linux-x86_64-${TOR_VERSION}.tar.gz" -o tor-bundle.tar.gz
mkdir -p tor-extract && tar -xzf tor-bundle.tar.gz -C tor-extract
PKG="cryptographic-triangles-daemon_${VERSION}_amd64"
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
cp build/bin/trianglesd ${PKG}/usr/lib/cryptographic-triangles/
cp tor-extract/tor/tor ${PKG}/usr/lib/cryptographic-triangles/tor/
chmod +x ${PKG}/usr/lib/cryptographic-triangles/tor/tor
[ -d tor-extract/data ] && cp -r tor-extract/data ${PKG}/usr/lib/cryptographic-triangles/tor/data
# Bundle ALL shared library dependencies (except glibc/kernel)
ldd build/bin/trianglesd | grep '=> /' | awk '{print $3}' | while read lib; do
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 — always present
*)
cp -L "$lib" ${PKG}/usr/lib/cryptographic-triangles/lib/ 2>/dev/null || true
;;
esac
done
echo "=== Bundled libs ==="
ls ${PKG}/usr/lib/cryptographic-triangles/lib/ | wc -l
ls ${PKG}/usr/lib/cryptographic-triangles/lib/
# Launcher with LD_LIBRARY_PATH
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
sed -i 's/^ //' ${PKG}/usr/bin/trianglesd
chmod +x ${PKG}/usr/bin/trianglesd
cat > ${PKG}/etc/systemd/system/trianglesd.service << 'SVC'
[Unit]
Description=Cryptographic Triangles Daemon
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=LD_LIBRARY_PATH=/usr/lib/cryptographic-triangles/lib
ExecStart=/usr/lib/cryptographic-triangles/trianglesd
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
SVC
sed -i 's/^ //' ${PKG}/etc/systemd/system/trianglesd.service
cat > ${PKG}/DEBIAN/control << CTRL
Package: cryptographic-triangles-daemon
Version: ${VERSION}
Architecture: amd64
Maintainer: Cryptographic Triangles <dev@cryptographic-triangles.org>
Description: Cryptographic Triangles daemon with integrated Tor
Fully self-contained headless node with all libraries, Tor, and systemd service.
No external dependencies required — runs on any x86_64 Linux.
Section: finance
Priority: optional
CTRL
sed -i 's/^ //' ${PKG}/DEBIAN/control
cat > ${PKG}/DEBIAN/postinst << 'POST'
#!/bin/bash
systemctl daemon-reload
echo ""
echo "Cryptographic Triangles daemon installed."
echo " Start: sudo systemctl start trianglesd"
echo " On boot: sudo systemctl enable trianglesd"
echo ""
POST
chmod +x ${PKG}/DEBIAN/postinst
dpkg-deb --build ${PKG}
- name: Upload .deb
uses: actions/upload-artifact@v4
@@ -611,9 +586,13 @@ jobs:
build-macos:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Checkout (with history for submodule)
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Init submodules
run: git submodule update --init --recursive
- name: Set VERSION
run: |
@@ -628,14 +607,9 @@ jobs:
- name: Install dependencies
run: |
brew install cmake ninja qt@5 openssl@3 boost berkeley-db@5 leveldb rocksdb libevent miniupnpc zstd
brew install cmake ninja qt@5 openssl@3 boost berkeley-db@5 leveldb rocksdb libevent miniupnpc
- name: Configure
# Add -L/opt/homebrew/lib to the link line so rocksdb's
# transitive -lzstd resolves. /opt/homebrew/lib is only in the
# rpath (runtime), not the link-time search path, so cmake's
# default LIBRARY_PATH propagation isn't enough — we set the
# linker flags explicitly.
run: |
export PATH="/opt/homebrew/opt/qt@5/bin:$PATH"
cmake -B build -G Ninja \
@@ -644,7 +618,6 @@ jobs:
-DBUILD_DAEMON=OFF \
-DBUILD_TESTS=OFF \
-DUSE_UPNP=ON \
-DUSE_I2P_EMBEDDED=ON \
-DBOOST_ROOT=/opt/homebrew/opt/boost \
-DBDB_INCLUDE_PATH=/opt/homebrew/opt/berkeley-db@5/include \
-DBDB_LIB_PATH=/opt/homebrew/opt/berkeley-db@5/lib \
@@ -653,38 +626,7 @@ jobs:
-DEVENT_LIB_PATH=/opt/homebrew/opt/libevent/lib \
-DMINIUPNPC_INCLUDE_PATH=/opt/homebrew/opt/miniupnpc/include \
-DMINIUPNPC_LIB_PATH=/opt/homebrew/opt/miniupnpc/lib \
-DQt5_DIR=/opt/homebrew/opt/qt@5/lib/cmake/Qt5 \
-DCMAKE_LIBRARY_PATH=/opt/homebrew/lib \
-DCMAKE_EXE_LINKER_FLAGS="-L/opt/homebrew/lib" \
-DCMAKE_SHARED_LINKER_FLAGS="-L/opt/homebrew/lib"
- name: Build libtor (embedded Tor static lib)
# macOS Qt GUI also transitively links -ltor via triangles_common.
# macOS Qt is built with @rpath embedded, so libtor needs to be
# at the configured TOR_SOURCE_ROOT location.
run: |
brew install libevent openssl@3 autoconf automake libtool zlib zstd
export PATH="/opt/homebrew/opt/automake/bin:/opt/homebrew/opt/libtool/bin:$PATH"
LIBEVENT_DIR=/opt/homebrew/opt/libevent \
OPENSSL_DIR=/opt/homebrew/opt/openssl@3 \
ZLIB_DIR=/opt/homebrew/opt/zlib \
bash src/tor/build-libtor.sh
- name: Build libtor (embedded Tor static lib)
# macOS Qt GUI also transitively links -ltor via triangles_common.
# macOS Qt is built with @rpath embedded, so libtor needs to be
# at the configured TOR_SOURCE_ROOT location.
run: |
brew install libevent openssl@3 autoconf automake libtool zlib
export PATH="/opt/homebrew/opt/automake/bin:/opt/homebrew/opt/libtool/bin:$PATH"
LIBEVENT_DIR=/opt/homebrew/opt/libevent \
OPENSSL_DIR=/opt/homebrew/opt/openssl@3 \
ZLIB_DIR=/opt/homebrew/opt/zlib \
bash src/tor/build-libtor.sh
- name: Build libi2pd (embedded I2P static lib)
# HOMEBREW=1 tells the i2pd Makefile to use Homebrew paths.
run: HOMEBREW=1 bash src/i2p/build-libi2pd.sh
-DQt5_DIR=/opt/homebrew/opt/qt@5/lib/cmake/Qt5
- name: Build
run: cmake --build build -j$(sysctl -n hw.ncpu)
@@ -777,8 +719,6 @@ jobs:
mkdir -p release
# Windows Qt installer (setup.exe — includes Tor, Start Menu shortcuts, uninstaller)
cp artifacts/windows-qt-setup/*.exe release/
# Windows Qt portable zip (extract & run — no install required)
cp artifacts/windows-qt-zip/*.zip release/
# Windows daemon (zip with DLLs + Tor)
cd artifacts/windows-daemon && zip -r "../../release/Cryptographic-Triangles-${VERSION}-win-x64-daemon.zip" . && cd ../..
# Linux Qt .deb (dpkg -i to install — includes Tor, desktop entry, icon)
-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..30}; do
URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/cryptographic-triangles-daemon_${VERSION}_amd64.deb"
if curl -fsSL --head "$URL" >/dev/null 2>&1; then
echo "✓ Release .deb available: $URL"
exit 0
fi
echo " waiting for release v${VERSION} daemon .deb... ($i/30)"
sleep 20
done
echo "::error::Release v${VERSION} daemon .deb never became available after 10 minutes"
exit 1
- name: Build and push
run: |
if [ -z "$DOCKERHUB_TOKEN" ]; then exit 0; fi
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..30}; do
URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/cryptographic-triangles_${VERSION}_amd64.deb"
if curl -fsSL --head "$URL" >/dev/null 2>&1; then
echo "✓ Release .deb available: $URL"
exit 0
fi
echo " waiting for release v${VERSION}... ($i/30)"
sleep 20
done
echo "::error::Release v${VERSION} .deb never became available after 10 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..30}; do
URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/Cryptographic-Triangles-v${VERSION}-macos-arm64.dmg"
if curl -fsSL --head "$URL" >/dev/null 2>&1; then
echo "✓ Release .dmg available: $URL"
exit 0
fi
echo " waiting for release v${VERSION}... ($i/30)"
sleep 20
done
echo "::error::Release v${VERSION} macOS .dmg never became available"
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..30}; do
URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/Cryptographic-Triangles-${VERSION}-win-x64-setup.exe"
if curl -fsSL --head "$URL" >/dev/null 2>&1; then
echo "✓ Release .exe available: $URL"
exit 0
fi
echo " waiting for release v${VERSION}... ($i/30)"
sleep 20
done
echo "::error::Release v${VERSION} Windows installer never became available"
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..30}; do
URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/Cryptographic-Triangles-${VERSION}-win-x64-setup.exe"
if curl -fsSL --head "$URL" >/dev/null 2>&1; then
echo "✓ Release .exe available: $URL"
exit 0
fi
echo " waiting for release v${VERSION}... ($i/30)"
sleep 20
done
echo "::error::Release v${VERSION} Windows installer never became available"
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
+1 -10
View File
@@ -49,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 \
@@ -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>
+2 -94
View File
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
endif()
project(Triangles
VERSION 6.0.0
VERSION 6.0.1
DESCRIPTION "Cryptographic Triangles Wallet"
LANGUAGES C CXX
)
@@ -47,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" 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)" 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" 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")
@@ -94,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)
@@ -154,75 +133,6 @@ if(NOT TARGET RocksDB::rocksdb AND NOT TARGET PkgConfig::RocksDB)
message(STATUS "Found RocksDB (manual probe): ${ROCKSDB_LIBRARY}")
endif()
# 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:
@@ -275,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}")
@@ -283,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}")
+1 -1
View File
@@ -2,7 +2,7 @@ FROM ubuntu:22.04
LABEL maintainer="Cryptographic Triangles Team"
LABEL description="Cryptographic Triangles (TRI) headless daemon"
LABEL version="5.9.24"
LABEL version="5.7.6"
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
-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
-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.
-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)
-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
-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
+1 -1
View File
@@ -3,7 +3,7 @@
# Run on a Linux x64 system with appimagetool installed
set -e
VERSION="5.9.24"
VERSION="5.7.6"
APPDIR="Triangles-x86_64.AppDir"
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
-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="5.9.24"
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=5.9.24
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=5.9.24
LABEL maintainer="Cryptographic Triangles Team"
LABEL description="Cryptographic Triangles (TRI) headless daemon"
LABEL version="5.9.24"
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:5.9.24
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/v5.9.24/Cryptographic-Triangles-v5.9.24-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/v5.9.24/Cryptographic-Triangles-v5.9.24-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="5.9.24"
VERSION="5.7.6"
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
echo "Building RPM for Triangles v${VERSION}..."
+1 -1
View File
@@ -1,5 +1,5 @@
Name: triangles
Version: 5.9.24
Version: 5.7.6
Release: 1%{?dist}
Summary: Cryptographic Triangles (TRI) cryptocurrency wallet
License: MIT
+2 -2
View File
@@ -1,11 +1,11 @@
{
"version": "5.9.24",
"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/v5.9.24/Cryptographic-Triangles-5.9.24-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: 5.9.24
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/v5.9.24/Cryptographic-Triangles-5.9.24-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
-86
View File
@@ -1,86 +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.
#
# Pinned version matches DNS2's system librocksdb (8.9.1) so test
# coverage matches production.
#
# Usage: sudo ./scripts/ci/build-rocksdb.sh
set -euo pipefail
ROCKSDB_VERSION="${ROCKSDB_VERSION:-8.9.1}"
ROCKSDB_TAG="v${ROCKSDB_VERSION}"
INSTALL_PREFIX="${INSTALL_PREFIX:-/usr/local}"
JOBS="${JOBS:-$(nproc)}"
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
echo ">>> Building RocksDB ${ROCKSDB_TAG} (${JOBS} jobs) into ${INSTALL_PREFIX}"
git clone --depth 1 --branch "${ROCKSDB_TAG}" \
https://github.com/facebook/rocksdb.git "${WORKDIR}/rocksdb"
cd "${WORKDIR}/rocksdb"
# Shared library only — Triangles links dynamically. Statically linking
# rocksdb.a would also work but balloons the daemon binary by ~50 MB.
make -j"${JOBS}" shared_lib PORTABLE=1 USE_RTTI=1 \
EXTRA_CXXFLAGS="-Wno-error=deprecated-declarations"
make install-shared PREFIX="${INSTALL_PREFIX}"
# Scrub the rocksdb.pc that install-shared just wrote. RocksDB's
# Makefile unconditionally appends `-isystem third-party/gtest-1.8.1/
# fused-src` to Cflags, which is a RELATIVE path baked in from the build
# directory. Modern CMake (>= 3.27) refuses to consume imported targets
# with non-existent relative paths in INTERFACE_INCLUDE_DIRECTORIES,
# so pkg_check_modules(rocksdb) on a Triangles configure errors out
# with: 'Imported target "PkgConfig::RocksDB" includes non-existent
# path "third-party/gtest-1.8.1/fused-src"'.
#
# Replace the bad flag with the absolute include dir so pkg-config
# consumers see a path that actually exists on disk.
PC_FILE="${INSTALL_PREFIX}/lib/pkgconfig/rocksdb.pc"
if [ -f "${PC_FILE}" ]; then
sed -i \
-e "s|-isystem third-party/gtest-1.8.1/fused-src|-I${INSTALL_PREFIX}/include|g" \
-e "s|-isystem \\\${prefix}/third-party/gtest-1.8.1/fused-src|-I${INSTALL_PREFIX}/include|g" \
-e 's|-std=c++17 ||g' \
-e 's|-std=c++17$||g' \
"${PC_FILE}"
fi
ldconfig
# Sanity: installed library should be on disk and registered with ldconfig.
# ldconfig strips the patch version from its output, so we check both:
# 1. File exists at the versioned path (definitive).
# 2. ldconfig shows a matching major.minor (sanity for runtime linker).
ROCKSDB_MAJOR_MINOR="${ROCKSDB_VERSION%.*}"
if [ ! -f "${INSTALL_PREFIX}/lib/librocksdb.so.${ROCKSDB_VERSION}" ]; then
echo "!!! librocksdb.so.${ROCKSDB_VERSION} not found at ${INSTALL_PREFIX}/lib/" >&2
ls -l "${INSTALL_PREFIX}/lib/librocksdb"* 2>&1 || true
exit 1
fi
if ! ldconfig -p | grep -q "librocksdb.so.${ROCKSDB_MAJOR_MINOR}"; then
echo "!!! ldconfig did not register librocksdb.so.${ROCKSDB_MAJOR_MINOR}" >&2
ldconfig -p | grep -i rocksdb >&2 || true
exit 1
fi
echo ">>> RocksDB ${ROCKSDB_TAG} installed to ${INSTALL_PREFIX}"
echo ">>> - library: ${INSTALL_PREFIX}/lib/librocksdb.so.${ROCKSDB_VERSION}"
echo ">>> - headers: ${INSTALL_PREFIX}/include/rocksdb/version.h"
ls -l "${INSTALL_PREFIX}/lib/librocksdb.so"* "${INSTALL_PREFIX}/include/rocksdb/version.h"
-142
View File
@@ -1,142 +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}"
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}..."
curl -sL "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/${TOR_TARBALL}" -o "${TOR_TARBALL}"
fi
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
Environment=LD_LIBRARY_PATH=/usr/lib/cryptographic-triangles/lib
ExecStart=/usr/lib/cryptographic-triangles/trianglesd
Restart=on-failure
RestartSec=10
[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
CTRL
# DEBIAN/postinst
cat > "${PKG}/DEBIAN/postinst" << 'POST'
#!/bin/bash
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
-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"
-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())
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: '5.9.24'
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/v5.9.24/Cryptographic-Triangles-v5.9.24-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-v5.9.24-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/v5.9.24/Cryptographic-Triangles-v5.9.24-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-v5.9.24-linux-x64-daemon: bin/trianglesd
Cryptographic-Triangles-v5.7.6-linux-x64-daemon: bin/trianglesd
desktop-entry:
plugin: dump
+3 -209
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
@@ -114,7 +111,6 @@ 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
)
@@ -181,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
@@ -352,39 +249,6 @@ if(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)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -442,7 +306,6 @@ if(BUILD_QT)
qt/trianglesunits.cpp
qt/qvaluecombobox.cpp
qt/askpassphrasedialog.cpp
qt/hdseeddialog.cpp
qt/notificator.cpp
qt/qtipcserver.cpp
qt/rpcconsole.cpp
@@ -585,9 +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$")
add_executable(test_triangles
${TEST_SOURCES}
@@ -612,70 +472,4 @@ if(BUILD_TESTS)
)
add_test(NAME triangles_unit_tests COMMAND test_triangles --log_level=test_suite)
# ── Standalone chaindb equivalence tests ─────────────────────────────────
# Runs without the TestingSetup global fixture (which would otherwise
# open the real chain DB and lock it for the process). Sets a fresh
# temp -datadir via its own global fixture, then runs the
# chaindb_equivalence_tests suite.
add_executable(test_chaindb_equivalence
"${CMAKE_CURRENT_SOURCE_DIR}/test/chaindb_equivalence_tests_main.cpp"
# wallet.cpp provides the CWallet symbols that triangles_common
# (txdb-rocksdb, net, etc.) references, even though the chaindb
# tests themselves don't use the wallet.
wallet.cpp
)
target_include_directories(test_chaindb_equivalence PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/test"
"${CMAKE_CURRENT_SOURCE_DIR}/leveldb/include"
)
target_link_libraries(test_chaindb_equivalence PRIVATE
triangles_common
Boost::unit_test_framework
)
add_test(NAME chaindb_equivalence_tests
COMMAND test_chaindb_equivalence --log_level=test_suite)
# ── Standalone snapshotnet P2P tests ────────────────────────────────────
# Same rationale as test_chaindb_equivalence: snapshotnet needs filesystem
# and threading globals and its own tmp datadir fixture, which would
# conflict with test_triangles' heavy TestingSetup. Runs independently.
add_executable(test_snapshotnet
"${CMAKE_CURRENT_SOURCE_DIR}/test/snapshotnet_tests.cpp"
wallet.cpp
)
target_include_directories(test_snapshotnet PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/test"
"${CMAKE_CURRENT_SOURCE_DIR}/leveldb/include"
)
target_link_libraries(test_snapshotnet PRIVATE
triangles_common
Boost::unit_test_framework
)
add_test(NAME snapshotnet_tests
COMMAND test_snapshotnet --log_level=test_suite)
# ── Standalone chaindb runtime tests (CRocksTxDB wrapper layer) ─────────
# Exercises MakeChainDB / WipeChainDataDir / IsRocksDbChainBackend and
# the CRocksTxDB write/read/batch/iterator wrapper — the same code path
# the daemon uses when launched with `-chaindb=rocksdb`. The
# chaindb_equivalence_tests (above) only verify the byte-copy migration
# via the raw leveldb/rocksdb APIs; this one verifies the wrapper class.
add_executable(test_chaindb_runtime
"${CMAKE_CURRENT_SOURCE_DIR}/test/chaindb_runtime_tests.cpp"
wallet.cpp
)
target_include_directories(test_chaindb_runtime PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/test"
"${CMAKE_CURRENT_SOURCE_DIR}/leveldb/include"
)
target_link_libraries(test_chaindb_runtime PRIVATE
triangles_common
Boost::unit_test_framework
)
add_test(NAME chaindb_runtime_tests
COMMAND test_chaindb_runtime --log_level=test_suite)
endif()
-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
+139 -437
View File
@@ -17,13 +17,6 @@
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/sha.h>
#include "key.h"
#include "base58.h"
#include "util.h"
extern const std::string strMessageMagic;
#include <fstream>
#include <sstream>
@@ -50,14 +43,7 @@ namespace Bootstrap {
bool NeedsBootstrap(const fs::path& dataDir)
{
// Need bootstrap if there's no chain database (the UTXO set / block index).
// blk0001.dat alone is NOT sufficient — it's raw block data that requires
// (fast-import was removed; UTXO snapshot is the only sync path)
// Check for both LevelDB (txleveldb/) and RocksDB (chainstate/) backends.
bool hasChainDb = fs::exists(dataDir / "txleveldb")
|| fs::exists(dataDir / "blocks" / "chainstate")
|| fs::exists(dataDir / "chainstate");
return !hasChainDb;
return !fs::exists(dataDir / "blk0001.dat");
}
// Direct TCP connection bypassing Tor SOCKS proxy.
@@ -460,6 +446,114 @@ static int64_t ParseTarOctal(const char* field, size_t len)
}
// Extract a tar.gz file to a destination directory
static bool ExtractTarGz(const fs::path& tarGzPath,
const fs::path& destDir,
std::string& strError)
{
gzFile gz = gzopen(tarGzPath.string().c_str(), "rb");
if (!gz) {
strError = "Cannot open " + tarGzPath.string();
return false;
}
gzbuffer(gz, 262144); // 256 KB buffer for performance
char header[512];
while (true) {
int bytesRead = gzread(gz, header, 512);
if (bytesRead == 0) break; // EOF
if (bytesRead != 512) {
strError = "Truncated tar header";
gzclose(gz);
return false;
}
// End-of-archive marker (zero block)
bool allZero = true;
for (int i = 0; i < 512; i++) {
if (header[i] != 0) { allZero = false; break; }
}
if (allZero) break;
// Parse filename: name (offset 0, 100 bytes) + optional prefix (offset 345, 155 bytes)
char name[101] = {0};
char prefix[156] = {0};
memcpy(name, header, 100);
memcpy(prefix, header + 345, 155);
std::string fullName;
if (prefix[0] != '\0')
fullName = std::string(prefix) + "/" + std::string(name);
else
fullName = std::string(name);
// Security: reject absolute paths and path traversal
if (fullName.empty() || fullName[0] == '/' || fullName.find("..") != std::string::npos) {
strError = "Unsafe path in tar archive: " + fullName;
gzclose(gz);
return false;
}
char typeflag = header[156];
int64_t fileSize = ParseTarOctal(header + 124, 12);
if (typeflag == '5' || (!fullName.empty() && fullName.back() == '/')) {
// Directory entry
fs::create_directories(destDir / fullName);
} else if (typeflag == '0' || typeflag == '\0') {
// Regular file
fs::path filePath = destDir / fullName;
fs::create_directories(filePath.parent_path());
FILE* outFile = fopen(filePath.string().c_str(), "wb");
if (!outFile) {
strError = "Cannot create file: " + filePath.string();
gzclose(gz);
return false;
}
int64_t remaining = fileSize;
char buf[65536];
while (remaining > 0) {
int toRead = (remaining > (int64_t)sizeof(buf)) ? (int)sizeof(buf) : (int)remaining;
int n = gzread(gz, buf, toRead);
if (n <= 0) {
fclose(outFile);
strError = "Truncated tar data for: " + fullName;
gzclose(gz);
return false;
}
fwrite(buf, 1, n, outFile);
remaining -= n;
}
fclose(outFile);
// Skip padding to next 512-byte boundary
int64_t pad = (512 - (fileSize % 512)) % 512;
if (pad > 0) {
char padBuf[512];
if (gzread(gz, padBuf, (unsigned)pad) != (int)pad) {
strError = "Truncated tar padding for: " + fullName;
gzclose(gz);
return false;
}
}
} else {
// Unknown entry type - skip its data
int64_t totalSkip = fileSize + ((512 - (fileSize % 512)) % 512);
char skipBuf[512];
while (totalSkip > 0) {
int toRead = (totalSkip > 512) ? 512 : (int)totalSkip;
if (gzread(gz, skipBuf, toRead) != toRead) break;
totalSkip -= toRead;
}
}
}
gzclose(gz);
return true;
}
} // anonymous namespace
@@ -504,8 +598,6 @@ bool ParseManifest(const fs::path& manifestPath,
manifest.hash = val;
else if (key == "dbversion")
manifest.dbversion = std::atoi(val.c_str());
else if (key == "signature")
manifest.signature = val;
}
in.close();
@@ -568,96 +660,6 @@ bool VerifyManifest(const SnapshotManifest& manifest,
return false;
}
// ─── Signature verification (#11) ─────────────────────────────────────
// If the manifest includes a signature, verify it against the
// compiled-in snapshot signing key. This prevents MITM attacks
// where an attacker replaces the snapshot file on the bootstrap server.
//
// If no signature is present, print a warning but continue (backward
// compatibility with older snapshots that pre-date signing).
if (!manifest.signature.empty()) {
// Build the message that was signed: "height||hash" (ASCII)
std::string message = std::to_string(manifest.height) + "||" + manifest.hash;
// Decode the hex-encoded signature (64 bytes for Ed25519)
std::vector<unsigned char> sigBytes;
if (manifest.signature.size() != 128) { // 64 bytes hex = 128 chars
strError = "Invalid signature length in manifest (expected 128 hex chars, got "
+ std::to_string(manifest.signature.size()) + ")";
return false;
}
for (size_t i = 0; i < manifest.signature.size(); i += 2) {
auto hexVal = [](char c) -> int {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
};
int hi = hexVal(manifest.signature[i]);
int lo = hexVal(manifest.signature[i + 1]);
if (hi < 0 || lo < 0) {
strError = "Invalid hex in manifest signature";
return false;
}
sigBytes.push_back((hi << 4) | lo);
}
// Snapshot signing public key (Ed25519, 32 bytes).
// This is the public half of the key used to sign snapshots on the
// bootstrap server. The private key never leaves the build machine.
// To rotate: generate new keypair, update this constant, re-sign
// all snapshots, update manifest files.
static const unsigned char snapshotPubkey[32] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
}; // Placeholder: replace with actual pubkey when signing is deployed
// Use OpenSSL Ed25519 verification
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (!mdctx) {
strError = "Failed to allocate EVP context for signature verification";
return false;
}
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr,
snapshotPubkey, 32);
if (!pkey) {
EVP_MD_CTX_free(mdctx);
strError = "Failed to load snapshot signing public key";
return false;
}
int rc = EVP_DigestVerifyInit(mdctx, nullptr, nullptr, nullptr, pkey);
if (rc != 1) {
EVP_PKEY_free(pkey);
EVP_MD_CTX_free(mdctx);
strError = "Failed to init signature verification";
return false;
}
rc = EVP_DigestVerify(mdctx,
sigBytes.data(), sigBytes.size(),
(const unsigned char*)message.data(), message.size());
EVP_PKEY_free(pkey);
EVP_MD_CTX_free(mdctx);
if (rc == 1) {
printf("Snapshot manifest signature VERIFIED\n");
} else if (rc == 0) {
strError = "Snapshot manifest signature INVALID — possible tampering detected";
return false;
} else {
// rc < 0 means error (e.g., placeholder zero pubkey not yet deployed)
printf("WARNING: Snapshot manifest signature verification error (rc=%d). "
"Signing key may not be deployed yet. Proceeding without verification.\n", rc);
}
} else {
printf("WARNING: Snapshot manifest has no signature — loading WITHOUT signature verification\n");
}
return true;
}
@@ -668,19 +670,34 @@ bool DownloadBootstrap(const std::string& host,
{
bool gotBlockFile = false;
// FastImport removed (commit bdb7253). v2 UTXO snapshot is the ONLY
// supported sync path. Skip the legacy tarball fallback entirely so we
// never hit /triangles-bootstrap.tar.gz (404 since 2026-06-19 cleanup)
// or /tri-bootstrap.tar.gz (also gone; was the URL in the old filelist.txt).
// The remaining path below reads filelist.txt → downloads utxo-snapshot.bin.
// Try downloading bootstrap.tar.gz first
// Bootstrap server is on clearnet — bypass Tor proxy for DNS + HTTP
const bool noProxy = true;
fs::path tmpTarGz = dataDir / "bootstrap.tar.gz.tmp";
std::string tarUrl = std::string(BASE_PATH) + "triangles-bootstrap.tar.gz";
printf("DownloadBootstrap(): attempting tar.gz download from %s%s\n", host.c_str(), tarUrl.c_str());
bool tarDownloaded = DownloadFile(host, tarUrl, tmpTarGz, progressFn, strError, noProxy);
printf("DownloadBootstrap(): tarDownloaded=%d result=%s\n", tarDownloaded, strError.c_str());
if (tarDownloaded) {
bool extractOk = ExtractTarGz(tmpTarGz, dataDir, strError);
fs::remove(tmpTarGz);
if (extractOk && fs::exists(dataDir / "blk0001.dat"))
gotBlockFile = true;
// If extraction failed, fall through to legacy path
}
if (!gotBlockFile) {
// Try filelist.txt — should contain only utxo-snapshot.bin (v2).
// Fallback: try filelist.txt + individual file downloads
std::string fallbackError;
std::vector<std::string> files;
if (!FetchFileList(host, files, fallbackError, noProxy)) {
strError = "filelist.txt unavailable: " + fallbackError;
if (!tarDownloaded)
strError = strError + " (fallback also failed: " + fallbackError + ")";
else
strError = "Extraction failed: " + strError + " (fallback also failed: " + fallbackError + ")";
return false;
}
@@ -703,7 +720,7 @@ bool DownloadBootstrap(const std::string& host,
// Check if the archive included a trusted pre-built index for the active
// backend with a valid snapshot.manifest. If verified, keep it to skip the
// multi-hour rebuild (fast-import removed; UTXO snapshot is the only sync path).
// multi-hour FastImportBlockFile() rebuild.
fs::path chainDbPath = GetChainDataDir();
fs::path database = dataDir / "database";
fs::path manifestPath = dataDir / "snapshot.manifest";
@@ -736,7 +753,7 @@ bool DownloadBootstrap(const std::string& host,
if (!keepIndex) {
// No valid manifest or verification failed - delete the index.
// The block index will be rebuilt from the UTXO snapshot on next startup.
// FastImportBlockFile() will rebuild from blk0001.dat on next startup.
printf("Bootstrap: removing extracted %s/ (will rebuild index from blk0001.dat)\n",
GetChainDataDir().filename().string().c_str());
if (fs::exists(chainDbPath))
@@ -754,312 +771,15 @@ bool DownloadBootstrap(const std::string& host,
return true;
}
namespace {
// Try to find the canonical UTXO snapshot entry in the bootstrap server's
// manifest.json. Looks for an entry of type "utxo_snapshot" and extracts
// its filename + expected SHA256. Returns true on success.
//
// We deliberately do a simple substring scan rather than full JSON parsing:
// the manifest is operator-controlled, the format is stable, and adding a
// JSON dependency for ~50 lines of code isn't worth it.
//
// On failure, the caller falls back to the legacy "utxo-snapshot.bin" URL,
// which the bootstrap server symlinks to the canonical file.
// Trusted signer addresses for snapshot manifests. A snapshot is accepted
// iff its manifest's signing_address matches one of these AND its signature
// verifies under Triangles' compact-message protocol.
static const char* TRUSTED_SNAPSHOT_SIGNERS[] = {
"TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX", // Sami's snapshot publisher key
};
static const size_t NUM_TRUSTED_SNAPSHOT_SIGNERS =
sizeof(TRUSTED_SNAPSHOT_SIGNERS) / sizeof(TRUSTED_SNAPSHOT_SIGNERS[0]);
bool IsTrustedSnapshotSigner(const std::string& addr)
{
for (size_t i = 0; i < NUM_TRUSTED_SNAPSHOT_SIGNERS; ++i)
if (addr == TRUSTED_SNAPSHOT_SIGNERS[i])
return true;
return false;
}
// Verify a Triangles signed-message compact signature. Returns true iff:
// - The address is valid
// - The signature is valid base64
// - The compact signature recovers to a public key whose hash160 matches
// the address's keyID
// - The hash being verified is Hash(strMessageMagic || message)
//
// Mirrors verifymessage RPC. Caller separately checks trust.
bool VerifySignedMessage(const std::string& strAddress,
const std::string& strSignatureB64,
const std::string& strMessage,
std::string& strError)
{
CTrianglesAddress addr(strAddress);
if (!addr.IsValid()) {
strError = "Invalid signer address: " + strAddress;
return false;
}
CKeyID keyID;
if (!addr.GetKeyID(keyID)) {
strError = "Address does not refer to a key: " + strAddress;
return false;
}
bool fInvalid = false;
std::vector<unsigned char> vchSig = DecodeBase64(strSignatureB64.c_str(), &fInvalid);
if (fInvalid) {
strError = "Malformed base64 in signature";
return false;
}
CDataStream ss(SER_GETHASH, 0);
ss << strMessageMagic;
ss << strMessage;
CKey key;
if (!key.SetCompactSignature(Hash(ss.begin(), ss.end()), vchSig)) {
strError = "Signature does not verify (recovered key mismatch or malformed sig)";
return false;
}
if (key.GetPubKey().GetID() != keyID) {
strError = "Signature recovered to a different key than the claimed signer";
return false;
}
return true;
}
// Extract a string field value from a small JSON object (subset).
std::string ExtractJsonString(const std::string& json, const std::string& field)
{
std::string key = "\"" + field + "\"";
size_t pos = json.find(key);
if (pos == std::string::npos) return "";
pos += key.size();
while (pos < json.size() && (json[pos] == ' ' || json[pos] == ':' || json[pos] == '\t'))
pos++;
if (pos >= json.size() || json[pos] != '\"') return "";
pos++;
size_t end = json.find('\"', pos);
if (end == std::string::npos) return "";
return json.substr(pos, end - pos);
}
bool FindCanonicalSnapshotInManifest(const std::string& manifestText,
std::string& outFilename,
std::string& outSha256,
std::string& outManifestFilename,
std::string& strError)
{
// Look for the "utxo_snapshot" file entry, e.g.:
// "utxo-snapshot-2207680.utx": {
// ...
// "type": "utxo_snapshot",
// "sha256": "eeefe107...",
// ...
// }
size_t typePos = manifestText.find("\"utxo_snapshot\"");
if (typePos == std::string::npos) {
strError = "manifest.json has no utxo_snapshot entry";
return false;
}
// Walk backwards from the typePos to find the start of this file's block.
// Format: "filename": { ... "type": "utxo_snapshot" ...
// We scan for the nearest preceding '"' followed by ':' that introduces a
// top-level file entry. Simple heuristic: find the line containing the
// type marker, then search backwards for the file key.
size_t entryStart = manifestText.rfind('"', typePos);
if (entryStart == std::string::npos || entryStart == 0) {
strError = "malformed manifest.json (no filename before utxo_snapshot entry)";
return false;
}
// Skip the opening quote
size_t filenameStart = entryStart + 1;
size_t filenameEnd = manifestText.find('"', filenameStart);
if (filenameEnd == std::string::npos) {
strError = "malformed manifest.json (unterminated filename)";
return false;
}
outFilename = manifestText.substr(filenameStart, filenameEnd - filenameStart);
// Within this block, extract the sha256.
// Walk forward from the typePos to find the matching closing brace of the
// entry. (Manifest is shallow, so a naive brace-count is fine.)
size_t braceStart = manifestText.find('{', filenameEnd);
if (braceStart == std::string::npos) {
strError = "malformed manifest.json (no body after filename)";
return false;
}
int depth = 0;
size_t bodyEnd = braceStart;
for (size_t i = braceStart; i < manifestText.size(); ++i) {
if (manifestText[i] == '{') depth++;
else if (manifestText[i] == '}') {
depth--;
if (depth == 0) { bodyEnd = i; break; }
}
}
if (depth != 0) {
strError = "malformed manifest.json (unbalanced braces in entry)";
return false;
}
std::string entry = manifestText.substr(braceStart, bodyEnd - braceStart);
size_t shaPos = entry.find("\"sha256\"");
if (shaPos == std::string::npos) {
strError = "manifest entry has no sha256 field";
return false;
}
size_t valStart = entry.find('"', shaPos + 8);
if (valStart == std::string::npos) {
strError = "malformed manifest.json (no sha256 value)";
return false;
}
valStart++;
size_t valEnd = entry.find('"', valStart);
if (valEnd == std::string::npos) {
strError = "malformed manifest.json (unterminated sha256 value)";
return false;
}
outSha256 = entry.substr(valStart, valEnd - valStart);
// Extract manifest filename (optional).
outManifestFilename.clear();
size_t manPos = entry.find("\"manifest\"");
if (manPos != std::string::npos) {
size_t mvStart = entry.find('\"', manPos + 10);
if (mvStart != std::string::npos) {
mvStart++;
size_t mvEnd = entry.find('\"', mvStart);
if (mvEnd != std::string::npos)
outManifestFilename = entry.substr(mvStart, mvEnd - mvStart);
}
}
return true;
}
// Read an entire file into a string. Empty string on error.
std::string ReadFileToString(const fs::path& path)
{
FILE* f = fopen(path.string().c_str(), "rb");
if (!f) return "";
fseek(f, 0, SEEK_END);
long sz = ftell(f);
if (sz < 0) { fclose(f); return ""; }
fseek(f, 0, SEEK_SET);
std::string s(sz, '\0');
size_t nread = fread(&s[0], 1, sz, f);
s.resize(nread);
fclose(f);
return s;
}
// Compute the SHA256 of a file, return as lowercase hex string.
std::string Sha256OfFile(const fs::path& path)
{
FILE* f = fopen(path.string().c_str(), "rb");
if (!f) return "";
SHA256_CTX ctx;
SHA256_Init(&ctx);
unsigned char buf[64 * 1024];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0)
SHA256_Update(&ctx, buf, n);
fclose(f);
unsigned char out[SHA256_DIGEST_LENGTH];
SHA256_Final(out, &ctx);
static const char hex[] = "0123456789abcdef";
std::string s(SHA256_DIGEST_LENGTH * 2, '0');
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
s[2*i] = hex[(out[i] >> 4) & 0xF];
s[2*i + 1] = hex[out[i] & 0xF];
}
return s;
}
} // anonymous namespace
bool DownloadUtxoSnapshot(const std::string& host,
const fs::path& dataDir,
ProgressCallback progressFn,
std::string& strError)
{
const bool noProxy = true;
const char* snapshotFilename = "utxo-snapshot.bin";
// Step 1: discover the canonical snapshot filename + expected SHA256 +
// per-snapshot manifest filename from the big manifest.json. Falls back
// to legacy URL if manifest unavailable.
std::string snapshotFilename = "utxo-snapshot.bin";
std::string expectedSha256;
std::string snapshotManifestFilename;
bool haveManifest = false;
fs::path tmpManifest = dataDir / "manifest.json.tmp";
if (DownloadFile(host, "manifest.json", tmpManifest, nullptr, strError, noProxy)) {
std::string text = ReadFileToString(tmpManifest);
fs::remove(tmpManifest);
std::string mFile, mSha, mManifest;
std::string mErr;
if (FindCanonicalSnapshotInManifest(text, mFile, mSha, mManifest, mErr)) {
snapshotFilename = mFile;
expectedSha256 = mSha;
snapshotManifestFilename = mManifest;
haveManifest = true;
printf("Bootstrap: manifest declares canonical snapshot %s (sha256=%s)\n",
snapshotFilename.c_str(), expectedSha256.substr(0, 16).c_str());
} else {
printf("Bootstrap: manifest parse failed (%s) — falling back to legacy URL\n",
mErr.c_str());
}
} else {
printf("Bootstrap: no manifest.json available — falling back to legacy URL\n");
strError.clear();
}
// Step 2: verify the per-snapshot manifest's signature. This is the
// AUTHENTICATION gate — the signature attests that the listed snapshot
// file came from a trusted operator. No checkpoint required; signature
// alone proves authenticity.
if (!snapshotManifestFilename.empty()) {
fs::path tmpSnapManifest = dataDir / "snapshot-manifest.tmp";
if (!DownloadFile(host, snapshotManifestFilename, tmpSnapManifest, nullptr, strError, noProxy)) {
fs::remove(tmpSnapManifest);
return false;
}
std::string snapManifestText = ReadFileToString(tmpSnapManifest);
fs::remove(tmpSnapManifest);
std::string signerAddr = ExtractJsonString(snapManifestText, "signing_address");
std::string message = ExtractJsonString(snapManifestText, "message");
std::string signature = ExtractJsonString(snapManifestText, "signature");
std::string declaredSha = ExtractJsonString(snapManifestText, "snapshot_sha256");
if (signerAddr.empty() || message.empty() || signature.empty()) {
strError = "per-snapshot manifest missing required fields (signing_address/message/signature)";
return false;
}
if (!IsTrustedSnapshotSigner(signerAddr)) {
strError = "snapshot manifest signer " + signerAddr + " is not in trusted signers list";
return false;
}
std::string vErr;
if (!VerifySignedMessage(signerAddr, signature, message, vErr)) {
strError = "snapshot signature verification failed: " + vErr;
return false;
}
if (!declaredSha.empty())
expectedSha256 = declaredSha;
printf("Bootstrap: snapshot signature verified (signer=%s)\n", signerAddr.c_str());
} else {
printf("Bootstrap: WARNING — no per-snapshot manifest available; "
"loading snapshot WITHOUT signature verification\n");
}
// Step 3: download the canonical snapshot file.
// Download utxo-snapshot.bin to a temp file
fs::path tmpPath = dataDir / "utxo-snapshot.bin.tmp";
std::string urlPath = std::string(BASE_PATH) + snapshotFilename;
@@ -1070,35 +790,17 @@ bool DownloadUtxoSnapshot(const std::string& host,
return false;
}
// Step 4: verify the downloaded file's SHA256 against the manifest.
if (!expectedSha256.empty()) {
std::string actualSha = Sha256OfFile(tmpPath);
if (actualSha.empty()) {
strError = "Cannot read downloaded snapshot for SHA256 verification";
fs::remove(tmpPath);
return false;
}
if (actualSha != expectedSha256) {
strError = "Snapshot SHA256 mismatch: expected " + expectedSha256
+ ", got " + actualSha
+ " (manifest/snapshot tampering or server misconfiguration)";
fs::remove(tmpPath);
return false;
}
printf("Bootstrap: snapshot SHA256 verified (%s)\n", actualSha.substr(0, 16).c_str());
}
printf("Bootstrap: UTXO snapshot downloaded, loading into database...\n");
// Step 5: load the snapshot. requireCheckpoint is FALSE — signature is
// the authentication gate; checkpoints would force snapshots only at
// specific heights. Signature alone is sufficient.
if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError, /*requireCheckpoint=*/false)) {
// Load the snapshot into a fresh active chain DB
if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError)) {
fs::remove(tmpPath);
return false;
}
// Clean up the temp file
fs::remove(tmpPath);
printf("Bootstrap: UTXO snapshot loaded successfully.\n");
return true;
}
-1
View File
@@ -53,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.
-473
View File
@@ -1,473 +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)) {
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
+2 -8
View File
@@ -25,18 +25,13 @@ 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")},
};
// Published UTXO snapshot file SHA256, keyed by snapshot height.
@@ -48,7 +43,6 @@ namespace Checkpoints
// here. The corresponding (height, blockhash) must already exist in
// mapCheckpoints / mapCheckpointsTestnet.
static std::map<int, uint256> mapSnapshotHashes = {
{ 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")},
};
static std::map<int, uint256> mapSnapshotHashesTestnet = {
@@ -60,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")},
+1 -1
View File
@@ -8,7 +8,7 @@
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
#define CLIENT_VERSION_MAJOR 6
#define CLIENT_VERSION_MINOR 0
#define CLIENT_VERSION_REVISION 0
#define CLIENT_VERSION_REVISION 1
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
-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
-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 .."
-668
View File
@@ -1,668 +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;
lastError.clear();
socksPort = socks;
samPort = sam;
serverPort = server;
i2pHostname.clear();
// Prepare i2pd data directory under the wallet's data dir
i2pDataDir = (::GetDataDir() / "i2p_data").string();
fs::create_directories(i2pDataDir);
fs::permissions(i2pDataDir, fs::perms::owner_all, fs::perm_options::replace);
printf("Embedded I2P: starting i2pd router...\n");
// Write an i2pd.conf configuration file that enables SAM + SOCKS proxy.
// i2pd's config system reads from a file; programmatic option setting is
// fragile across i2pd versions. Writing a minimal conf is robust.
{
fs::path confPath = fs::path(i2pDataDir) / "i2pd.conf";
std::ofstream conf(confPath.string());
if (!conf.is_open()) {
lastError = "Failed to write i2pd.conf";
return false;
}
conf << "# Auto-generated by Triangles embedded I2P\n";
conf << "datadir = " << i2pDataDir << "\n";
conf << "loglevel = info\n";
conf << "\n";
// SOCKS proxy for outbound .i2p connections (P2P transport)
conf << "[socksproxy]\n";
conf << "enabled = true\n";
conf << "address = 127.0.0.1\n";
conf << "port = " << socksPort << "\n";
conf << "keys = socks-proxy.dat\n";
conf << "\n";
// SAM bridge for SAM v3 direct streaming API
conf << "[sam]\n";
conf << "enabled = true\n";
conf << "address = 127.0.0.1\n";
conf << "port = " << samPort << "\n";
conf << "\n";
// Disable HTTP webconsole (not needed for embedded use)
conf << "[http]\n";
conf << "enabled = false\n";
conf << "\n";
// Disable I2P control protocol
conf << "[i2pcontrol]\n";
conf << "enabled = false\n";
conf << "\n";
// Disable BOB
conf << "[bob]\n";
conf << "enabled = false\n";
conf << "\n";
conf.close();
}
// Write tunnels.conf BEFORE Start() — ClientContext::Start() reads this
// file to create server/client tunnels. The server tunnel is the I2P
// equivalent of a Tor hidden service: it forwards inbound I2P connections
// to the Triangles P2P listen port.
if (serverPort > 0) {
fs::path tunnelConfPath = fs::path(i2pDataDir) / "tunnels.conf";
std::ofstream tunnelConf(tunnelConfPath.string());
if (tunnelConf.is_open()) {
tunnelConf << "# Auto-generated by Triangles embedded I2P\n";
tunnelConf << "[triangles-p2p]\n";
tunnelConf << "type = server\n";
tunnelConf << "host = 127.0.0.1\n";
tunnelConf << "port = " << serverPort << "\n";
tunnelConf << "keys = triangles-p2p-keys.dat\n";
tunnelConf << "inbound.length = 3\n";
tunnelConf << "outbound.length = 3\n";
tunnelConf << "inbound.quantity = 5\n";
tunnelConf << "outbound.quantity = 5\n";
tunnelConf.close();
printf("Embedded I2P: server tunnel configured on port %d\n", serverPort);
}
}
// Build argv for i2pd initialization. Pass --datadir and --conf on the
// command line (not just in the conf file) because i2pd's ParseCmdline
// runs BEFORE ParseConfig, and DetectDataDir needs the datadir early.
std::vector<std::string> argvStrings;
argvStrings.push_back("i2pd");
argvStrings.push_back("--datadir");
argvStrings.push_back(i2pDataDir);
argvStrings.push_back("--conf");
argvStrings.push_back((fs::path(i2pDataDir) / "i2pd.conf").string());
std::vector<char*> argvPtrs;
for (auto& s : argvStrings)
argvPtrs.push_back(&s[0]);
argvPtrs.push_back(nullptr);
try {
// Initialize i2pd: config parse, filesystem, crypto, router context
i2p::api::InitI2P((int)(argvPtrs.size() - 1), argvPtrs.data(), "triangles-i2pd");
// Start the I2P router: netdb, transports, tunnels, router context
// Redirect i2pd logs to our stdout/stderr
auto logStream = std::make_shared<std::ostream>(std::cout.rdbuf());
i2p::api::StartI2P(logStream);
printf("Embedded I2P: router started, starting client services...\n");
// Start the client context — this initializes SAM bridge, SOCKS proxy,
// and tunnels based on config. The client context reads the conf we
// wrote above to determine which services to start.
i2p::client::context.Start();
running.store(true);
printf("Embedded I2P: SOCKS proxy at 127.0.0.1:%d, SAM at 127.0.0.1:%d\n",
socksPort, samPort);
// Wait for i2pd's SOCKS proxy AND SAM bridge to become available
// (up to 120s — I2P bootstrap is slower than Tor due to floodfill
// lookup and tunnel build).
printf("Embedded I2P: waiting for SOCKS proxy and SAM bridge...\n");
bool socksReady = false;
bool samReady = false;
for (int i = 0; i < 120; i++) {
MilliSleep(1000);
if (fShutdown) {
Stop();
return false;
}
// --- Check SOCKS proxy readiness ---
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);
}
}
}
// --- Check SAM bridge readiness ---
if (!samReady) {
samReady = IsSamAvailable();
if (samReady) {
printf("Embedded I2P: SAM v3 bridge ready on port %d (took %ds)\n",
samPort, i + 1);
}
}
// Both endpoints are up — router is fully bootstrapped
if (socksReady && samReady) {
printf("Embedded I2P: all I2P endpoints ready (SOCKS %d + SAM %d)\n",
socksPort, samPort);
break;
}
if (i > 0 && i % 30 == 0) {
printf("Embedded I2P: still bootstrapping (%ds elapsed, SOCKS:%s SAM:%s)...\n",
i, socksReady ? "ready" : "wait",
samReady ? "ready" : "wait");
}
}
// Populate the .b32.i2p address from the router's identity hash.
// This is the I2P address that appears in the Qt status bar.
try {
auto identHash = i2p::context.GetRouterInfo().GetIdentHash();
i2pHostname = identHash.ToBase32() + ".b32.i2p";
printf("Embedded I2P: router address = %s\n", i2pHostname.c_str());
} catch (...) {
printf("Embedded I2P: could not retrieve router address yet\n");
}
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());
return false;
}
}
void CI2PEmbedded::Stop()
{
if (!running.load()) return;
printf("Requesting embedded I2P shutdown...\n");
try {
// Stop client context (SAM, SOCKS, tunnels)
i2p::client::context.Stop();
// Stop the router
i2p::api::StopI2P();
// Terminate crypto
i2p::api::TerminateI2P();
} catch (const std::exception& e) {
printf("WARNING: error during I2P shutdown: %s\n", e.what());
}
running.store(false);
}
#else // !ENABLE_I2P_EMBEDDED
// ========================================================================
// Fallback stubs: embedded I2P not compiled in
// ========================================================================
bool CI2PEmbedded::Start(int socks, int sam, int server)
{
printf("Embedded I2P not compiled in (ENABLE_I2P_EMBEDDED not defined).\n");
socksPort = socks;
samPort = sam;
serverPort = server;
i2pDataDir = (::GetDataDir() / "i2p_data").string();
lastError = "I2P support not compiled in. Build with -DUSE_I2P_EMBEDDED=ON";
return false;
}
void CI2PEmbedded::Stop()
{
running.store(false);
}
#endif // ENABLE_I2P_EMBEDDED
// ========================================================================
// Global hooks (called from init.cpp)
// ========================================================================
bool StartEmbeddedI2P()
{
bool enableI2P = GetBoolArg("-i2p", true);
if (!enableI2P) {
printf("I2P disabled by -i2p=0 flag\n");
return false;
}
int socksPort = GetArg("-i2psocks", 19100);
int samPort = GetArg("-i2psam", 7656);
int serverPort = GetArg("-i2phsport", GetListenPort());
return CI2PEmbedded::GetInstance()->Start(socksPort, samPort, serverPort);
}
void StopEmbeddedI2P()
{
CI2PEmbedded::GetInstance()->Stop();
}
-144
View File
@@ -1,144 +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;
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
-29
View File
@@ -1,29 +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 with a server tunnel forwarding to the Triangles P2P port.
//
// NOTE: .b32.i2p addresses are derived from the destination's public key.
// They are generated when the node first creates its I2P tunnel keys.
// Replace these placeholders with actual seed node addresses once deployed.
//
// Dynamic seeds will also be available at:
// https://seeds.cryptographic-triangles.org/i2p-seeds.txt
static const char *strMainNetI2PSeed[][1] = {
// DNS2 - primary bootstrap server (194.233.88.206)
// Generated by embedded i2pd on first run, keys persist in i2p_data/
{"hnupgkbtcn4hlo6sunhbp6uuz4k6bkgsa5jtcruyyt7y6q7qsoda.b32.i2p"},
// DNS3 - canonical chain reference (74.208.167.19)
{"hvvr2yys3nll4l6fdywecvn3baw6h5i7bsa2ldbz2e5xwangnn7q.b32.i2p"},
// Hetzner Helsinki - ARM64 staking node (46.62.249.20)
{"2hyeunnkax5du4snip4gdsdicxtmlnagtlkatv57rjpx2kvfssma.b32.i2p"},
{nullptr}
};
static const char *strTestNetI2PSeed[][1] = {
{nullptr}
};
#endif
+23 -278
View File
@@ -19,8 +19,6 @@
#include "tor/tor_embedded.h"
#include "tor/onion_v3.h"
#include "tor/tor_process.h"
#include "i2p/i2p_embedded.h"
#include "i2p/i2pseed.h"
#ifdef ENABLE_ZMQ
#include "zmqpublishnotifier.h"
#endif
@@ -30,15 +28,9 @@
#include <memory>
#include <thread>
#include <vector>
// Forward declaration: InitError / InitWarning are defined further down
// in this file but referenced by AppInit (line ~423) before the definition.
static bool InitError(const std::string& str);
static bool InitWarning(const std::string& str);
#include <filesystem>
#include <fstream>
#include <boost/interprocess/sync/file_lock.hpp>
#include <algorithm>
#include <openssl/crypto.h>
#ifndef WIN32
@@ -110,97 +102,6 @@ void ExitTimeout(void* parg)
#endif
}
// Wait up to maxWaitSec for at least minPeers peers to have reported their
// chain height via the version handshake. Returns the median peer height, or
// -1 if we couldn't get enough peers (timeout, no peers, all nStartingHeight=-1).
int WaitForPeerHeights(int minPeers, int maxWaitSec)
{
const int pollIntervalMs = 500;
const int64_t deadline = GetTimeMillis() + (int64_t)maxWaitSec * 1000;
while (GetTimeMillis() < deadline && !fRequestShutdown) {
std::vector<int> heights;
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes) {
if (pnode && pnode->nStartingHeight > 0)
heights.push_back(pnode->nStartingHeight);
}
}
if ((int)heights.size() >= minPeers) {
std::sort(heights.begin(), heights.end());
int median = heights[heights.size() / 2];
printf("AutoRebuild: got %zu peer heights; median=%d\n", heights.size(), median);
return median;
}
MilliSleep(pollIntervalMs);
}
std::vector<int> heights;
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes) {
if (pnode && pnode->nStartingHeight > 0)
heights.push_back(pnode->nStartingHeight);
}
}
if (heights.empty()) {
printf("AutoRebuild: no peers reported heights after %ds\n", maxWaitSec);
return -1;
}
std::sort(heights.begin(), heights.end());
int median = heights[heights.size() / 2];
printf("AutoRebuild: timed out with %zu peers; median=%d\n", heights.size(), median);
return median;
}
// If -autorerebuild is set and our local chain is more than that many blocks
// behind the median peer height, wipe the chain DB (preserving wallet.dat +
// onion + smsg state) and request shutdown. On restart, the daemon sees no
// chain DB and the snapshot path takes over.
void MaybeAutoRebuild(int thresholdBlocks)
{
if (thresholdBlocks <= 0)
return;
if (nBestHeight < 0) {
printf("AutoRebuild: local nBestHeight unset — skipping\n");
return;
}
printf("AutoRebuild: enabled (threshold=%d blocks). Local chain tip: %d\n",
thresholdBlocks, nBestHeight);
int medianPeer = WaitForPeerHeights(/*minPeers=*/3, /*maxWaitSec=*/60);
if (medianPeer <= 0) {
printf("AutoRebuild: could not get peer heights — skipping rebuild\n");
return;
}
int lag = medianPeer - nBestHeight;
printf("AutoRebuild: peer median=%d, local=%d, lag=%d\n",
medianPeer, nBestHeight, lag);
if (lag < thresholdBlocks) {
printf("AutoRebuild: lag %d < threshold %d — no rebuild needed\n",
lag, thresholdBlocks);
return;
}
printf("\n*** AutoRebuild: chain is %d blocks behind — wiping chain DB ***\n", lag);
printf("*** Preserving wallet.dat, smsgDB, onion state. ***\n");
printf("*** Daemon will shutdown; restart to load signed UTXO snapshot. ***\n\n");
WipeChainDataDir();
fs::path blkPath = GetDataDir() / "blk0001.dat";
if (fs::exists(blkPath)) {
fs::remove(blkPath);
printf("AutoRebuild: removed stale %s\n", blkPath.string().c_str());
}
StartShutdown();
}
void StartShutdown()
{
fRequestShutdown = true;
@@ -343,7 +244,6 @@ void Shutdown(void* parg)
// NOW safe to destroy Tor state - all threads have stopped
ShutdownTorV3();
StopEmbeddedTor();
StopEmbeddedI2P();
#ifdef ENABLE_ZMQ
if (pzmqNotifier)
@@ -421,21 +321,6 @@ bool AppInit(int argc, char* argv[])
}
ReadConfigFile(mapArgs, mapMultiArgs);
// AUDIT: If notorious=1 or -notor was set in triangles.conf, scream
// loudly. This is the silent path that put DNS2 on a 5+ day clearnet
// fork in 2026-06-23 — operator flipped it for troubleshooting, never
// reverted it, and the daemon happily started in clearnet-only mode.
// We refuse to proceed unless -recovery-mode=1 is ALSO set, even if
// the flag was set in the config file rather than on the command line.
if (mapArgs.count("-notor") && !GetBoolArg("-recovery-mode", false)) {
return InitError(_(
"-notor=1 found in triangles.conf or command line. Triangles is "
"Tor-native; running without Tor is unsafe and produces silent "
"clearnet forks (see 2026-06-23 DNS2 incident). If this is an "
"explicit recovery operation, pass -recovery-mode=1 on the command "
"line (in addition to the config file setting) to acknowledge."));
}
if (mapArgs.count("-?") || mapArgs.count("--help"))
{
// First part of help message is specific to trianglesd / RPC client
@@ -529,22 +414,16 @@ std::string HelpMessage()
" -dbcache=<n> " + _("Set database cache size in megabytes (default: 25)") + "\n" +
" -dblogsize=<n> " + _("Set database disk log size in megabytes (default: 100)") + "\n" +
" -timeout=<n> " + _("Specify connection timeout in milliseconds (default: 5000)") + "\n" +
" -torconnecttimeout=<n> " + _("Max time (ms) for the SOCKS5 handshake with the Tor proxy (send+recv of SOCKS5 init/auth/connect). Bounds how long a dead/slow .onion can stall the connector thread (default: 60000, range 5000-180000)") + "\n" +
//" -proxy=<ip:port> " + _("Connect through socks proxy") + "\n" +
//" -socks=<n> " + _("Select the version of socks proxy to use (4-5, default: 5)") + "\n" +
" -tor=<ip:port> " + _("Use proxy to reach tor hidden services (default: same as -proxy)") + "\n"
" -notor " + _("Disable Tor - run in clearnet-only mode (no .onion connectivity)") + "\n" +
" -torsocks=<port> " + _("Set embedded or managed Tor SOCKS proxy port (default: 19099)") + "\n" +
" -torhiddenservice " + _("Enable the managed Tor hidden service (default: 1)") + "\n" +
" -torhsport=<port> " + _("Set embedded or managed Tor hidden service port (default: wallet listen port)") + "\n"
" -i2p " + _("Enable embedded I2P router for .b32.i2p connectivity (default: 1)") + "\n"
" -i2psocks=<port> " + _("Set embedded I2P SOCKS proxy port (default: 19100)") + "\n"
" -i2psam=<port> " + _("Set embedded I2P SAM bridge port (default: 7656)") + "\n"
" -i2phsport=<port> " + _("Set I2P server tunnel forward port (default: wallet listen port)") + "\n" +
" -torhsport=<port> " + _("Set embedded or managed Tor hidden service port (default: wallet listen port)") + "\n" +
//" -dns " + _("Allow DNS lookups for -addnode, -seednode and -connect") + "\n" +
" -port=<port> " + _("Listen for connections on <port> (default: 24112 or testnet: 24111)") + "\n" +
" -maxconnections=<n> " + _("Maintain at most <n> connections to peers (default: 125)") + "\n" +
" -maxoutboundconnections=<n> " + _("Maximum outbound connections (default: 8, range 4-32)") + "\n" +
" -addnode=<ip> " + _("Add a node to connect to and attempt to keep the connection open") + "\n" +
" -connect=<ip> " + _("Connect only to the specified node(s)") + "\n" +
" -seednode=<ip> " + _("Connect to a node to retrieve peer addresses, and disconnect") + "\n" +
@@ -561,7 +440,6 @@ std::string HelpMessage()
" -onionseed " + _("Find peers using .onion seeds (default: 1 unless -connect)") + "\n" +
" -seedurl=<host> " + _("HTTP seed list host (default: seeds.cryptographic-triangles.org)") + "\n" +
" -noseedurl " + _("Disable HTTP seed list fetch on startup") + "\n" +
" -autorerebuild=<n> " + _("If our chain is more than <n> blocks behind peers, wipe chain DB and shutdown for clean restart (default: 0=disabled)") + "\n" +
" -banscore=<n> " + _("Threshold for disconnecting misbehaving peers (default: 100)") + "\n" +
" -bantime=<n> " + _("Number of seconds to keep misbehaving peers from reconnecting (default: 86400)") + "\n" +
" -par=<n> " + _("Set the number of script verification threads (default: auto, 0 = auto, 1 = single-threaded)") + "\n" +
@@ -809,21 +687,6 @@ bool AppInit2()
nConnectTimeout = nNewTimeout;
}
// SOCKS5/Tor negotiation timeout. Separate from -timeout (which only covers
// the instant local connect to the Tor SOCKS proxy); this bounds the
// SOCKS5 handshake (send+recv of init/auth/connect). On a dead/slow .onion
// the recv() in Socks5() would otherwise block until Tor's own ~120s
// SocksTimeout fires, holding an outbound connection slot.
if (mapArgs.count("-torconnecttimeout"))
{
int nTorTimeout = GetArg("-torconnecttimeout", 60000);
if (IsValidSocksNegotiationTimeout(nTorTimeout))
nSocksNegotiationTimeout = nTorTimeout;
else
InitWarning("Ignoring -torconnecttimeout=" + mapArgs["-torconnecttimeout"] +
": out of range (5000..180000 ms), using default 60000");
}
if (mapArgs.count("-paytxfee"))
{
if (!ParseMoney(mapArgs["-paytxfee"], nTransactionFee))
@@ -835,15 +698,6 @@ bool AppInit2()
fConfChange = GetBoolArg("-confchange", false);
fEnforceCanonical = GetBoolArg("-enforcecanonical", true);
// Validate -maxoutboundconnections (range 4-32, default 8)
if (mapArgs.count("-maxoutboundconnections"))
{
int nMaxOutboundConn = GetArg("-maxoutboundconnections", 8);
if (nMaxOutboundConn < 4 || nMaxOutboundConn > 32)
InitWarning("Ignoring -maxoutboundconnections=" + mapArgs["-maxoutboundconnections"] +
": out of range (4..32), using default 8");
}
int nScriptCheckThreads = GetArg("-par", 0);
if (nScriptCheckThreads <= 0)
nScriptCheckThreads = std::thread::hardware_concurrency();
@@ -1082,11 +936,14 @@ bool AppInit2()
fs::path dataPath = GetDataDir();
bool needsBootstrap = Bootstrap::NeedsBootstrap(dataPath);
if (needsBootstrap && !noBootstrap) {
printf("Bootstrap: no blockchain data found — downloading UTXO snapshot automatically.\n");
if (needsBootstrap && !noBootstrap && !snapshotMode) {
printf("Bootstrap: no blockchain data found — downloading automatically.\n");
printf("Bootstrap: (use -nobootstrap to skip)\n");
uiInterface.InitMessage(_("Downloading UTXO snapshot..."));
uiInterface.InitMessage(_("Downloading blockchain data..."));
wantsBootstrap = true;
} else if (needsBootstrap && snapshotMode && !wantsBootstrap) {
printf("Bootstrap: no blockchain data found — will fetch UTXO snapshot via P2P after network start.\n");
printf("Bootstrap: (use -bootstrap for legacy clearnet HTTP bootstrap, or -snapshot=0 to disable P2P fetcher)\n");
}
if (wantsBootstrap)
@@ -1168,13 +1025,8 @@ bool AppInit2()
printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n");
uiInterface.InitMessage(_("Loading UTXO snapshot..."));
// Local file load: skip the checkpoint gate. The operator has
// filesystem access, so the trust model is already equivalent
// to direct chain state modification — a malicious local file
// is no worse than a malicious chain DB. P2P-delivered
// snapshots (SnapshotNet) keep the checkpoint gate on.
std::string strError;
if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError, /*requireCheckpoint=*/false)) {
if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError)) {
printf("UTXO snapshot loaded successfully.\n");
} else {
printf("UTXO snapshot load failed: %s\n", strError.c_str());
@@ -1212,7 +1064,7 @@ bool AppInit2()
}
// Handle -reindex: delete the chain DB so it gets rebuilt from the raw
// blk*.dat files. This recalculates money
// blk*.dat files via FastImportBlockFile(). This recalculates money
// supply, tx index, and UTXO set from scratch. Backend-agnostic via
// WipeChainDataDir(), which resolves the directory per the configured
// -chaindb backend.
@@ -1229,48 +1081,19 @@ bool AppInit2()
if (!LoadBlockIndex())
return InitError(_("Error loading blkindex.dat"));
// triangles fix (pitfall #61): initialize pindexFinalized from the
// hardcoded checkpoint on startup, BEFORE the daemon opens any peer
// connections or processes any block messages.
//
// Without this, pindexFinalized stays NULL on a fresh restart even when
// we have 2.2M blocks on disk, because the auto-checkpoint code in
// ActivateBestChain() at main.cpp:2459 only sets it when
// !IsInitialBlockDownload(). If the chain tip is more than 24h stale
// (which happens on every restart with a synced chain), IsInitialBlockDownload()
// returns true and pindexFinalized never gets set.
//
// The downstream reorg guard at main.cpp:2198 short-circuits when
// pindexFinalized is NULL, which allowed a 3,755-block minority fork
// to overwrite a healthy 2,206,004-block chain on 2026-06-16. Loading
// the hardcoded checkpoint from checkpoints.cpp (block 2,205,000) on
// startup means the reorg guard is always active whenever the
// checkpointed block is in our local mapBlockIndex.
// If the block index is empty but blk0001.dat exists (bootstrap download),
// fast-import: build the index directly from the block file without re-writing
// data. Batches LevelDB commits every 200K blocks for speed.
if (nBestHeight == 0 && std::filesystem::exists(GetDataDir() / "blk0001.dat")
&& mapBlockIndex.size() <= 1)
{
CBlockIndex* pCheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex);
if (pCheckpoint && pCheckpoint != pindexFinalized)
{
pindexFinalized = pCheckpoint;
printf("STARTUP-CHECKPOINT: pindexFinalized set to block %d (%s) from hardcoded checkpoint\n",
pindexFinalized->nHeight, pindexFinalized->GetBlockHash().ToString().substr(0,20).c_str());
}
else if (!pCheckpoint)
{
printf("STARTUP-CHECKPOINT: WARNING — hardcoded checkpoint not in local block index, pindexFinalized remains NULL\n");
}
uiInterface.InitMessage(_("Importing bootstrap blocks..."));
printf("Block index empty but blk0001.dat exists - running fast import...\n");
int64_t nFastImportStart = GetTimeMillis();
FastImportBlockFile();
StartupPerfLog("bootstrap_fast_import", GetTimeMillis() - nFastImportStart, strprintf("bestheight=%d", nBestHeight));
}
// AutoRebuild: if -autorerebuild is set and we are behind peers, wipe chain DB
// and shutdown for clean restart.
MaybeAutoRebuild(GetArg("-autorerebuild", 0));
if (fRequestShutdown) {
printf("AutoRebuild: shutdown requested before chain load complete\n");
return false;
}
// Block index loaded. With fast-import removed, the only supported sync path
// is the UTXO snapshot (auto-downloaded from bootstrap or placed manually in datadir).
// as LoadBlockIndex can take several minutes, it's possible the user
// requested to kill triangles-qt during the last operation. If so, exit.
// As the program has not fully started yet, Shutdown() is possibly overkill.
@@ -1533,26 +1356,11 @@ bool AppInit2()
fUseUPnP = false;
#endif
} else if (GetBoolArg("-notor", false)) {
// -notor: explicit clearnet mode. Triangles is Tor-native and
// running without Tor is unsafe for normal operation — it can
// produce silent clearnet forks (see 2026-06-23 DNS2 incident,
// 5+ days on a parallel chain because -notor=1 was left on after
// troubleshooting). The flag is preserved for explicit recovery
// workflows (e.g. dumputxoset-from-clearnet when bootstrapping
// a new node) but requires an additional -recovery-mode=1
// confirmation flag so it cannot be flipped by accident.
if (!GetBoolArg("-recovery-mode", false)) {
return InitError(_(
"-notor requires -recovery-mode=1 confirmation. Triangles is Tor-native; "
"running without Tor is unsafe and produces silent clearnet forks. "
"If you need clearnet mode for bootstrap recovery or diagnostics, "
"pass BOTH -notor=1 -recovery-mode=1 on the command line."));
}
printf("WARNING: Tor disabled via -notor AND -recovery-mode=1 set. "
"Running in clearnet-only mode.\n");
// -notor: user explicitly disabled Tor. Allow the daemon to start
// in clearnet-only mode (useful for diagnostics, benchmarking, and
// recovery). .onion connectivity will not be available.
printf("NOTICE: Tor disabled via -notor. Running in clearnet-only mode.\n");
printf(" .onion connections will NOT be available.\n");
printf(" This mode is for RECOVERY ONLY — exit and restart without these\n"
" flags as soon as the recovery operation completes.\n");
SetReachable(NET_IPV4, true);
SetReachable(NET_IPV6, true);
SetReachable(NET_TOR, false);
@@ -1563,47 +1371,6 @@ bool AppInit2()
return InitError(strprintf(_("Tor failed to start. Triangles requires Tor to operate.\n\nDetails: %s"), torError.c_str()));
}
// ════════════════════════════════════════════════════════════════
// Embedded I2P (i2pd) startup
//
// I2P runs as a co-equal anonymity network alongside Tor. When Tor
// starts successfully (tor-native mode), I2P provides an alternative
// anonymous transport via .b32.i2p destinations. When Tor is disabled
// (-notor recovery mode), I2P is still started to maintain anonymity.
//
// I2P's SOCKS proxy (default 19100) handles outbound .i2p connections.
// A server tunnel forwards incoming I2P connections to the P2P port.
// ════════════════════════════════════════════════════════════════
if (torStarted || GetBoolArg("-notor", false)) {
uiInterface.InitMessage(_("Starting embedded I2P router..."));
int64_t nI2PStart = GetTimeMillis();
bool i2pStarted = StartEmbeddedI2P();
StartupPerfLog("i2p_start", GetTimeMillis() - nI2PStart,
strprintf("started=%d", i2pStarted));
if (i2pStarted) {
int i2pSocksPort = CI2PEmbedded::GetInstance()->GetSocksPort();
CService i2pProxyAddr("127.0.0.1", i2pSocksPort);
// Route I2P traffic through i2pd's SOCKS proxy
SetProxy(NET_I2P, i2pProxyAddr, 5);
SetReachable(NET_I2P, true);
printf("I2P-NATIVE MODE: I2P router running\n");
printf(" SOCKS proxy at 127.0.0.1:%d for .b32.i2p connections\n",
i2pSocksPort);
printf(" Dual-network anonymity: Tor (.onion) + I2P (.b32.i2p)\n");
} else {
// I2P failure is non-fatal — Tor-only operation continues.
// The daemon still works with .onion peers.
std::string i2pError = CI2PEmbedded::GetInstance()->GetStartupError();
printf("WARNING: Embedded I2P did not start. Running Tor-only.\n");
if (!i2pError.empty())
printf(" I2P error: %s\n", i2pError.c_str());
SetReachable(NET_I2P, false);
}
}
// Initialize Tor V3 identity (Ed25519 keys, onion address)
uiInterface.InitMessage(_("Initializing Tor V3 identity..."));
printf("Initializing Tor V3 onion identity...\n");
@@ -1718,28 +1485,6 @@ bool AppInit2()
printf("Loaded %i addresses from peers.dat %" PRId64 "ms\n",
addrman.size(), GetTimeMillis() - nStart);
StartupPerfLog("peers_load", GetTimeMillis() - nStart, strprintf("count=%d", addrman.size()));
// Add hardcoded I2P (.b32.i2p) seed addresses to the address manager.
// This enables cross-network peer discovery: Tor-connected nodes can learn
// about I2P peers and vice versa. Onion seeds are loaded separately in
// ThreadOnionSeed (net.cpp), but we add I2P seeds here during init so they
// are available immediately for the outbound connector.
{
static const char *(*strI2PSeed)[1] = fTestNet ? strTestNetI2PSeed : strMainNetI2PSeed;
int nI2PSeeds = 0;
for (unsigned int si = 0; strI2PSeed[si][0] != nullptr; si++) {
CNetAddr parsed;
if (parsed.SetSpecial(strI2PSeed[si][0])) {
int nOneDay = 24 * 3600;
CAddress addr = CAddress(CService(parsed, GetDefaultPort()));
addr.nTime = GetTime() - 3 * nOneDay - GetRand(4 * nOneDay);
addrman.Add(addr, parsed);
nI2PSeeds++;
}
}
if (nI2PSeeds > 0)
printf("Added %d hardcoded I2P (.b32.i2p) seed addresses to addrman\n", nI2PSeeds);
}
// ********************************************************* Step 11: start node
+1 -11
View File
@@ -36,18 +36,8 @@ int64_t GetWeight(int64_t nIntervalBeginning, int64_t nIntervalEnd)
// comes back with massively amplified staking power and dominates blocks.
// The 7-day cap still allows generous accumulation while limiting abuse.
static const int64_t STAKE_AGE_SOFT_CAP = 7 * 24 * 60 * 60; // 7 days
// Activation gate: the soft cap shipped 2026-04-20 without a height/time
// gate, retroactively invalidating earlier blocks staked with long-aged
// coins (e.g. coins idle through the 2022-2026 freeze). Apply the cap
// only to stakes after the activation timestamp; historical stakes
// validate under the rules they were created with (uncapped age).
static const int64_t STAKE_AGE_SOFT_CAP_ACTIVATION = 1776000000; // 2026-04-12 ~13:20 UTC
if (pindexBest && pindexBest->nHeight >= FORK_HEIGHT_V5)
{
if (nIntervalEnd >= STAKE_AGE_SOFT_CAP_ACTIVATION)
return min(nAge, STAKE_AGE_SOFT_CAP);
return nAge;
}
return min(nAge, STAKE_AGE_SOFT_CAP);
return min(nAge, (int64_t)nStakeMaxAge);
}
+422 -560
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -83,7 +83,6 @@ 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;
@@ -129,6 +128,7 @@ CBlockIndex* FindBlockByHeight(int nHeight);
bool ProcessMessages(CNode* pfrom);
bool SendMessages(CNode* pto, bool fSendTrickle);
bool LoadExternalBlockFile(FILE* fileIn);
bool FastImportBlockFile();
bool CheckProofOfWork(uint256 hash, unsigned int nBits);
unsigned int GetNextTargetRequired(const CBlockIndex* pindexLast, bool fProofOfStake);
+2 -8
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
{
+66 -466
View File
@@ -11,9 +11,6 @@
#include "addrman.h"
#include "ui_interface.h"
#include "onionseed.h"
#include "tor/onion_v3.h"
#include "snapshotnet.h"
#include "i2p/i2pseed.h"
#include <openssl/ssl.h>
#include <openssl/err.h>
@@ -22,8 +19,6 @@
#ifdef WIN32
#include <string.h>
#else
#include <sys/uio.h>
#endif
#ifdef USE_UPNP
@@ -41,9 +36,7 @@ extern "C" {
// int tor_main(int argc, char *argv[]);
}
// Configurable max outbound connections. Set from -maxoutboundconnections
// during network init (StartNode). Default 8, configurable range 4-32.
static int MAX_OUTBOUND_CONNECTIONS = 8;
static const int MAX_OUTBOUND_CONNECTIONS = 8; // reduced from 16 for Tor-only small networks
void ThreadMessageHandler2(void* parg);
void ThreadSocketHandler2(void* parg);
@@ -334,86 +327,6 @@ bool IsReachable(const CNetAddr& addr)
return vfReachable[net] && !vfLimited[net];
}
// ────────────────────────────────────────────────────────────────────────────
// Cross-network Tor ↔ I2P peer discovery helpers
// ────────────────────────────────────────────────────────────────────────────
/**
* Check whether a CAddress refers to an I2P (.b32.i2p) endpoint.
* Returns true if the string representation of the address contains ".i2p".
*/
bool IsI2PAddr(const CAddress& addr)
{
std::string addrStr = addr.ToStringIP();
return (addrStr.find(".i2p") != std::string::npos);
}
/**
* Check whether a CAddress refers to a Tor (.onion) endpoint.
*/
static bool IsOnionAddr(const CAddress& addr)
{
std::string addrStr = addr.ToStringIP();
return (addrStr.find(".onion") != std::string::npos);
}
/**
* Cross-network address relay: when an 'addr' message is received from a
* peer on one anonymity network, this function bridges addresses belonging
* to the *other* network to the appropriate peers.
*
* - .b32.i2p addresses received from any peer → relay to I2P-connected peers
* - .onion addresses received from any peer → relay to Tor-connected peers
*
* This breaks the isolation between Tor and I2P peer sets so that a Tor
* node can learn about I2P peers and vice versa.
*/
void RelayCrossNetworkAddr(const std::vector<CAddress>& vAddr)
{
bool hasI2P = false;
bool hasOnion = false;
for (const CAddress& addr : vAddr) {
if (IsI2PAddr(addr)) hasI2P = true;
if (IsOnionAddr(addr)) hasOnion = true;
}
if (!hasI2P && !hasOnion)
return;
LOCK(cs_vNodes);
for (CNode* pnode : vNodes) {
if (pnode->fDisconnect)
continue;
std::string peerAddr = pnode->addr.ToStringIP();
bool peerIsI2P = (peerAddr.find(".i2p") != std::string::npos);
bool peerIsOnion = (peerAddr.find(".onion") != std::string::npos);
for (const CAddress& addr : vAddr) {
// Bridge I2P addresses to I2P peers
if (hasI2P && IsI2PAddr(addr) && peerIsI2P) {
pnode->PushAddress(addr);
}
// Bridge .onion addresses to Tor peers
if (hasOnion && IsOnionAddr(addr) && peerIsOnion) {
pnode->PushAddress(addr);
}
// Cross-bridge: also push I2P addresses to Tor peers and
// .onion addresses to I2P peers so each network learns about
// the other's peers.
if (hasI2P && IsI2PAddr(addr) && peerIsOnion) {
pnode->PushAddress(addr);
}
if (hasOnion && IsOnionAddr(addr) && peerIsI2P) {
pnode->PushAddress(addr);
}
}
}
if (fDebug && (hasI2P || hasOnion))
printf("RelayCrossNetworkAddr: bridged %s%s%s addresses across networks\n",
hasOnion ? ".onion " : "", hasI2P ? ".i2p " : "",
(hasOnion && hasI2P) ? "(both)" : "");
}
bool GetMyExternalIP2(const CService& addrConnect, const char* pszGet, const char* pszKeyword, CNetAddr& ipRet)
{
SOCKET hSocket;
@@ -581,13 +494,11 @@ CNode* FindNode(const CService& addr)
CNode* ConnectNode(CAddress addrConnect, const char *pszDest)
{
// TOR+I2P NATIVE: Reject all clearnet (non-.onion, non-.b32.i2p) addresses
// TOR-NATIVE: Reject all non-.onion addresses
std::string addrStr = pszDest ? std::string(pszDest) : addrConnect.ToStringIP();
bool isOnion = (addrStr.find(".onion") != std::string::npos);
bool isI2P = (addrStr.find(".i2p") != std::string::npos);
if (!isOnion && !isI2P) {
if (addrStr.find(".onion") == std::string::npos) {
if (fDebug)
printf("ConnectNode(): REJECTED clearnet address: %s (Tor/I2P native mode)\n", addrStr.c_str());
printf("ConnectNode(): REJECTED non-onion address: %s (Tor-native mode)\n", addrStr.c_str());
return nullptr;
}
@@ -653,14 +564,6 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest)
void CNode::CloseSocketDisconnect()
{
fDisconnect = true;
// Option C: track this disconnect for the reliability score. We increment
// BEFORE closing the socket so a flurry of disconnects from one peer is
// visible to the next sync manager tick (which iterates cs_vNodes).
++nDisconnectCount;
nLastDisconnectTime = GetTime();
// Penalize the score by 25 per disconnect. Flapping peers (5+ in 5min) get
// an extra 50 penalty applied in the score recompute.
nReliabilityScore = std::max(0, nReliabilityScore - 25);
if (hSocket != INVALID_SOCKET)
{
printf("disconnecting node %s\n", addrName.c_str());
@@ -678,40 +581,6 @@ void CNode::Cleanup()
{
}
int CNode::RecomputeReliabilityScore()
{
// Option C: compute reliability score from current counters.
//
// Base: 100
// -10 per connect failure (host unreachable on attempt)
// -25 per disconnect (also applied immediately in CloseSocketDisconnect,
// but we re-apply here so a fresh CNode that started with a low score
// can recover)
// +5 per block delivered, capped at +200
// -50 if the peer has flapped (5+ disconnects in the last 5 minutes)
//
// Floor: 0 (peer effectively banned from sync)
// Ceiling: 500
int score = 100;
score -= 10 * nConnectFailures;
score -= 25 * nDisconnectCount;
int deliveryBonus = std::min(200, 5 * nBlocksDelivered);
score += deliveryBonus;
if (nDisconnectCount >= 5) {
// Flapping detection: 5+ disconnects in the peer's lifetime.
// We can't easily check "last 5 min" without history, so we use
// total count as a proxy. A peer that connects/disconnects a lot
// is unreliable regardless of timing.
score -= 50;
}
if (score < 0) score = 0;
if (score > 500) score = 500;
nReliabilityScore = score;
return score;
}
void CNode::PushVersion()
{
@@ -916,96 +785,36 @@ void SocketSendData(CNode *pnode)
std::deque<CSerializeData>::iterator it = pnode->vSendMsg.begin();
while (it != pnode->vSendMsg.end()) {
#ifndef WIN32
// Coalesce up to MAX_IOV queued messages into a single syscall using
// scatter-gather I/O. On Linux we use sendmsg() so we can pass
// MSG_NOSIGNAL | MSG_DONTWAIT; on other POSIX systems (e.g. BSD where
// SO_NOSIGPIPE is already set on the socket) we fall back to writev().
static const int MAX_IOV = 16;
struct iovec iov[MAX_IOV];
int iovcnt = 0;
std::deque<CSerializeData>::iterator batchEnd = it;
for (; batchEnd != pnode->vSendMsg.end() && iovcnt < MAX_IOV; ++batchEnd, ++iovcnt) {
const CSerializeData &data = *batchEnd;
size_t off = (batchEnd == it) ? pnode->nSendOffset : 0;
assert(data.size() > off);
iov[iovcnt].iov_base = const_cast<char*>(&data[off]);
iov[iovcnt].iov_len = data.size() - off;
}
if (iovcnt == 0)
break;
ssize_t nBytes;
#ifdef MSG_NOSIGNAL
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = iov;
msg.msg_iovlen = iovcnt;
nBytes = sendmsg(pnode->hSocket, &msg, MSG_NOSIGNAL | MSG_DONTWAIT);
#else
nBytes = writev(pnode->hSocket, iov, iovcnt);
#endif
if (nBytes > 0) {
pnode->nLastSend = GetTime();
pnode->nSendBytes += nBytes;
// Consume nBytes across the coalesced messages
while (it != batchEnd && nBytes > 0) {
const CSerializeData &data = *it;
size_t remaining = data.size() - pnode->nSendOffset;
if ((size_t)nBytes >= remaining) {
nBytes -= remaining;
pnode->nSendSize -= data.size();
pnode->nSendOffset = 0;
++it;
} else {
pnode->nSendOffset += nBytes;
nBytes = 0;
}
}
// Socket buffer full mid-batch — wait for next cycle
if (it != batchEnd)
break;
} else if (nBytes < 0) {
int nErr = WSAGetLastError();
if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) {
printf("socket send error %d\n", nErr);
pnode->CloseSocketDisconnect();
}
break;
} else {
// nBytes == 0: peer closed
break;
}
#else
// Windows: individual send() calls
const CSerializeData &data = *it;
assert(data.size() > pnode->nSendOffset);
int nBytes = send(pnode->hSocket, &data[pnode->nSendOffset], data.size() - pnode->nSendOffset, MSG_NOSIGNAL | MSG_DONTWAIT);
if (nBytes > 0) {
pnode->nLastSend = GetTime();
pnode->nSendOffset += nBytes;
pnode->nSendBytes += nBytes;
pnode->nSendBytes += nBytes;
if (pnode->nSendOffset == data.size()) {
pnode->nSendOffset = 0;
pnode->nSendSize -= data.size();
it++;
} else {
// could not send full message; stop sending more
break;
}
} else {
if (nBytes < 0) {
// error
int nErr = WSAGetLastError();
if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) {
if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS)
{
printf("socket send error %d\n", nErr);
pnode->CloseSocketDisconnect();
}
}
// couldn't send anything at all
break;
}
#endif
}
if (it == pnode->vSendMsg.end()) {
@@ -1239,16 +1048,6 @@ void ThreadSocketHandler2(void* parg)
break;
}
}
// Also check I2P seed addresses
if (!fIsSeed) {
static const char *(*strI2PSeedCheck)[1] = fTestNet ? strTestNetI2PSeed : strMainNetI2PSeed;
for (unsigned int si = 0; strI2PSeedCheck[si][0] != nullptr; si++) {
if (incomingAddr.find(strI2PSeedCheck[si][0]) != std::string::npos) {
fIsSeed = true;
break;
}
}
}
if (fIsSeed && nInbound < nMaxInbound + 2) {
fAccept = true;
printf("accepted seed node %s (reserved slot)\n", addr.ToString().c_str());
@@ -1376,7 +1175,7 @@ void ThreadSocketHandler2(void* parg)
if (fShutdown)
return;
MilliSleep(IsInitialBlockDownload() ? 1 : 10);
MilliSleep(10);
}
}
@@ -1604,39 +1403,6 @@ void ThreadOnionSeed(void* parg)
static const char *(*strOnionSeed)[1] = fTestNet ? strTestNetOnionSeed : strMainNetOnionSeed;
int found = 0;
// Defense-in-depth (2026-06-22): Validate every hardcoded seed against the
// v3 onion checksum BEFORE we hand it to Tor. The btb6/gtb6 incident
// (4,842 "No more HSDir" errors over a 12h from-zero sync test) was caused
// by a single-character corruption that Tor rejected with a cryptic
// "ed25519 validation failed" warning. Catching it here gives the operator
// a clear, actionable error at startup with no wasted network/CPU.
// See references/onion-corruption-ci-defense.md (CI Layers 2-3) for the
// static-analysis side of this defense.
{
int nInvalid = 0;
int nTotal = 0;
std::string strFirstBad;
for (unsigned int si = 0; strOnionSeed[si][0] != nullptr; si++) {
nTotal++;
if (!CTorV3Service::ValidateOnionAddress(strOnionSeed[si][0])) {
if (strFirstBad.empty()) strFirstBad = strOnionSeed[si][0];
nInvalid++;
}
}
if (nInvalid > 0) {
std::string strErr = strprintf(
"ThreadOnionSeed() : %d of %d hardcoded .onion seed(s) failed v3 "
"checksum validation. First bad address: %s. "
"This is the btb6/gtb6 class of bug (see references/onion-corruption-ci-defense.md). "
"Fix src/onionseed.h before starting the daemon — Tor would "
"have wasted hours producing cryptic 'ed25519 validation failed' "
"warnings otherwise.",
nInvalid, nTotal, strFirstBad.c_str());
printf("ERROR: %s\n", strErr.c_str());
throw runtime_error(strErr);
}
}
for (unsigned int seed_idx = 0; strOnionSeed[seed_idx][0] != nullptr; seed_idx++) {
CNetAddr parsed;
if (!parsed.SetSpecial(strOnionSeed[seed_idx][0]))
@@ -1657,31 +1423,6 @@ void ThreadOnionSeed(void* parg)
printf("%d addresses from hardcoded .onion seeds (queued as OneShot)\n", found);
// Load hardcoded I2P (.b32.i2p) seeds for cross-network peer discovery.
// These are added to the address manager so that I2P-connected peers can
// be discovered. Unlike onion seeds, we don't queue them as OneShot
// connections here — they're connected via the normal outbound connector
// through the I2P SOCKS proxy.
{
static const char *(*strI2PSeed)[1] = fTestNet ? strTestNetI2PSeed : strMainNetI2PSeed;
int i2pFound = 0;
for (unsigned int si = 0; strI2PSeed[si][0] != nullptr; si++) {
CNetAddr parsed;
if (!parsed.SetSpecial(strI2PSeed[si][0])) {
printf("WARNING: ThreadOnionSeed() : invalid .b32.i2p seed: %s\n",
strI2PSeed[si][0]);
continue;
}
int nOneDay = 24*3600;
CAddress addr = CAddress(CService(parsed, GetDefaultPort()));
addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay);
addrman.Add(addr, parsed);
i2pFound++;
}
if (i2pFound > 0)
printf("%d addresses from hardcoded .b32.i2p seeds added to addrman\n", i2pFound);
}
// Wait for Tor to establish circuits before attempting HTTPS seed fetch.
// The hardcoded OneShot connections can race ahead meanwhile.
printf("ThreadOnionSeed: waiting 20s for Tor circuits before HTTPS seed fetch...\n");
@@ -1959,114 +1700,67 @@ bool ThreadHTTPSeedFetch2(void* parg)
return false;
}
std::string headers = response.substr(0, headerEnd);
std::string body = response.substr(headerEnd + 4);
// Some servers (e.g. Caddy / Let's Encrypt fronting the seed list) reply
// with Transfer-Encoding: chunked even on HTTP/1.1 + Connection: close. The
// body then carries hex chunk-size lines interleaved with the data; parsing
// it raw fuses a chunk marker onto an address and we lose most of the list
// (the classic "only 1 address" symptom). De-chunk first when present.
//
// v5.9.22 hardening: the parser is now strict and reports a distinct
// failure code for each kind of malformed framing. See DechunkResult in
// netbase.h and the unit tests in src/test/http_seed_tests.cpp.
{
std::string h = headers;
for (char& c : h) c = (char)tolower((unsigned char)c);
if (h.find("transfer-encoding:") != std::string::npos &&
h.find("chunked") != std::string::npos)
{
std::string decoded;
int rc = DechunkTransferEncoding(body, decoded);
if (rc != DECHUNK_OK) {
const char* reason = "unknown";
switch (rc) {
case DECHUNK_EMPTY: reason = "empty body"; break;
case DECHUNK_NO_CHUNK_TERMINATOR: reason = "missing chunk terminator (CRLF)"; break;
case DECHUNK_INVALID_HEX: reason = "malformed chunk-size (not valid hex)"; break;
case DECHUNK_OVERSIZE_CHUNK: reason = "chunk size exceeds remaining input (truncated)"; break;
case DECHUNK_MISSING_DATA_CRLF: reason = "missing CRLF after chunk data"; break;
default: reason = "unknown"; break;
}
printf("HTTPS seed fetch: malformed chunked transfer encoding (%s) from %s\n",
reason, seedHost.c_str());
return false;
}
body.swap(decoded);
}
}
if (fDebug)
printf("HTTPS seed fetch: %d body bytes to parse\n", (int)body.size());
// Tolerant parse: accept one-per-line OR several addresses on one line
// (whitespace / comma / semicolon separated), and ignore inline '#' comments.
// v5.9.22: the splitting logic is now a pure function in netbase.cpp so
// we can unit-test every line format. The CNetAddr/CService/addrman
// validation stays here because it touches globals.
// Parse one address per line: "address:port" or just "address"
int found = 0;
int skipped = 0;
auto addSeed = [&](std::string addrStr) -> void {
while (!addrStr.empty() && (addrStr.back()=='\r' || addrStr.back()==' ' || addrStr.back()=='\t'))
addrStr.pop_back();
while (!addrStr.empty() && (addrStr.front()==' ' || addrStr.front()=='\t'))
addrStr.erase(addrStr.begin());
if (addrStr.empty())
return;
int port = GetDefaultPort();
size_t onionPos = addrStr.find(".onion:");
size_t i2pPos = addrStr.find(".i2p:");
if (onionPos != std::string::npos) {
port = atoi(addrStr.substr(onionPos + 7).c_str());
addrStr = addrStr.substr(0, onionPos + 6); // keep ".onion"
} else if (i2pPos != std::string::npos) {
port = atoi(addrStr.substr(i2pPos + 5).c_str());
// keep the ".i2p" suffix
} else if (addrStr.find(".onion") == std::string::npos &&
addrStr.find(".i2p") == std::string::npos) {
return; // Tor/I2P-native: skip clearnet addresses
}
if (port <= 0 || port > 65535)
port = GetDefaultPort();
CService service(addrStr, port);
if (service.IsValid()) {
CAddress addr(service);
addr.nTime = GetTime() - 3*24*60*60; // 3 days ago
addrman.Add(addr, service);
printf("HTTPS seed: added %s:%d\n", addrStr.c_str(), port);
found++;
} else {
skipped++;
}
};
// Use the pure helper to split the body. If it returns nothing, that
// means the body was entirely comments / blank lines / whitespace —
// distinct failure mode worth logging separately from "no valid
// addresses after parsing".
std::vector<std::string> tokens = ParseSeedListBody(body);
if (tokens.empty()) {
printf("HTTPS seed fetch: parsed response contained zero valid addresses from %s\n", seedHost.c_str());
return false;
}
for (const std::string& tok : tokens)
std::istringstream lines(body);
std::string line;
while (std::getline(lines, line))
{
if (fShutdown)
return false;
addSeed(tok);
// Trim whitespace and carriage returns
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
line.pop_back();
while (!line.empty() && (line.front() == ' ' || line.front() == '\t'))
line.erase(line.begin());
if (line.empty() || line[0] == '#')
continue;
// Parse address:port
std::string addrStr = line;
int port = GetDefaultPort();
// For .onion addresses, the last colon before port is after ".onion"
size_t onionPos = addrStr.find(".onion:");
if (onionPos != std::string::npos) {
port = atoi(addrStr.substr(onionPos + 7).c_str());
addrStr = addrStr.substr(0, onionPos + 6); // keep ".onion"
} else if (addrStr.find(".onion") == std::string::npos) {
// Tor-native: skip non-.onion addresses
continue;
}
if (port <= 0 || port > 65535)
port = GetDefaultPort();
CNetAddr parsed;
bool resolved = parsed.SetSpecial(addrStr);
if (!resolved) {
std::vector<CNetAddr> vIP;
if (LookupHost(addrStr.c_str(), vIP, 1, false) && !vIP.empty()) {
parsed = vIP[0];
resolved = true;
}
}
if (resolved) {
CAddress addr(CService(parsed, port));
addr.nTime = GetTime() - 3*24*60*60; // 3 days ago
addrman.Add(addr, CNetAddr("https-seed", true));
// Queue the first 8 seeds for immediate direct connection
if (found < 8) {
std::string oneShotAddr = addrStr + ":" + std::to_string(port);
AddOneShot(oneShotAddr);
}
found++;
}
}
printf("%d addresses found from HTTPS seed list (%s)\n", found, seedHost.c_str());
if (found == 0) {
printf("HTTPS seed fetch: parsed response contained zero valid addresses from %s\n", seedHost.c_str());
return false;
}
return true;
return found > 0;
} catch (std::exception& e) {
printf("HTTPS seed fetch failed: %s\n", e.what());
@@ -2200,57 +1894,10 @@ void ThreadOpenConnections2(void* parg)
// Initiate network connections
int64_t nStart = GetTime();
int64_t nLastDiscoveryRound = 0; // signed peer discovery: re-trigger getaddr+getseederlist+getwalletaddr
const int64_t DISCOVERY_COOLDOWN = 300; // 5min between rounds (peer count < threshold)
const int DISCOVERY_THRESHOLD = 4; // if we have fewer than this many connected peers, re-trigger
while (true)
{
ProcessOneShot();
// Signed peer discovery: when our connected-peer count drops, re-trigger
// the full signing + discovery round on every peer. Triangles already has
// getaddr / getseederlist / getwalletaddr in onion_v3.cpp — this just
// re-fires them periodically instead of only at startup.
int nConnectedOnion = 0;
int nSignedPeers = 0;
int64_t nNow = GetTime();
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes) {
if (!pnode->fInbound && pnode->fSuccessfullyConnected) {
std::string ip = pnode->addr.ToStringIP();
if (ip.find(".onion") != std::string::npos) {
nConnectedOnion++;
if (pnode->nSignedPeerBonus > 0) nSignedPeers++;
}
}
}
}
if (nConnectedOnion < DISCOVERY_THRESHOLD &&
nNow - nLastDiscoveryRound > DISCOVERY_COOLDOWN)
{
nLastDiscoveryRound = nNow;
printf("SYNC-SIGN: low peer count (%d < %d), re-firing discovery round on all peers\n",
nConnectedOnion, DISCOVERY_THRESHOLD);
LOCK(cs_vNodes);
for (CNode* pnode : vNodes) {
if (!pnode->fInbound && pnode->fSuccessfullyConnected) {
std::string ip = pnode->addr.ToStringIP();
if (ip.find(".onion") != std::string::npos &&
nNow - pnode->nLastGetaddrTrigger > DISCOVERY_COOLDOWN)
{
pnode->nLastGetaddrTrigger = nNow;
pnode->PushMessage("getaddr");
pnode->PushMessage("getseederlist");
// getwalletaddr is only sent on version handshake (main.cpp:3941);
// we don't re-fire it here because it generates a new receiving
// key on the peer each call, which is wasteful. Signed peers
// are cached for 24h (onion_v3.cpp:2308) so they'll be reused.
}
}
}
}
vnThreadsRunning[THREAD_OPENCONNECTIONS]--;
MilliSleep(500);
vnThreadsRunning[THREAD_OPENCONNECTIONS]++;
@@ -2739,25 +2386,8 @@ void StartNode(void* parg)
// Make this thread recognisable as the startup thread
RenameThread("Triangles-start");
// Configurable outbound connections via -maxoutboundconnections (default 8, range 4-32)
MAX_OUTBOUND_CONNECTIONS = GetArg("-maxoutboundconnections", 8);
if (MAX_OUTBOUND_CONNECTIONS < 4) MAX_OUTBOUND_CONNECTIONS = 4;
if (MAX_OUTBOUND_CONNECTIONS > 32) MAX_OUTBOUND_CONNECTIONS = 32;
printf("Configured max outbound connections: %d (from -maxoutboundconnections)\n", MAX_OUTBOUND_CONNECTIONS);
// If a canonical UTXO snapshot file is already present at startup,
// advertise NODE_SNAPSHOT to peers BEFORE the first outbound connection.
// EnsureLocalSnapshot() also sets this flag post-IBD, but at that point
// already-connected peers have already cached our version message and
// won't re-read our service bits — so for the "place canonical file in
// datadir before launch" operator workflow this pre-handshake OR is the
// load-bearing one.
if (!fClient) {
SnapshotNet::EnsureLocalSnapshot();
}
if (semOutbound == nullptr) {
// initialize semaphore — use -maxoutboundconnections (set above), fall back to -maxoutbound
// initialize semaphore — use -maxoutbound if specified, else default
int nMaxOutbound = (int)GetArg("-maxoutbound", MAX_OUTBOUND_CONNECTIONS);
nMaxOutbound = min(nMaxOutbound, (int)GetArg("-maxconnections", 125));
nMaxOutbound = max(nMaxOutbound, 1); // at least 1 outbound
@@ -2809,10 +2439,6 @@ void StartNode(void* parg)
if (!NewThread(ThreadOpenConnections, nullptr))
printf("Error: NewThread(ThreadOpenConnections) failed\n");
// Start fork detector (post-IBD background monitor)
if (!NewThread(ThreadForkDetector, nullptr))
printf("Error: NewThread(ThreadForkDetector) failed\n");
// Process messages
if (!NewThread(ThreadMessageHandler, nullptr))
printf("Error: NewThread(ThreadMessageHandler) failed\n");
@@ -2947,29 +2573,3 @@ void RelayTransaction(const CTransaction& tx, const uint256& hash, const CDataSt
RelayInventory(inv);
}
// ---------------------------------------------------------------------------
// BIP152 Compact Block relay — net-layer integration
// ---------------------------------------------------------------------------
/** Advertise a new block to all connected peers.
*
* For peers that have negotiated compact block relay (fSendCmpct), the
* inventory is sent as MSG_CMPCT_BLOCK so they know to request the compact
* form. For legacy peers, standard MSG_BLOCK inventory is sent.
*
* The actual compact block construction and sending happens in main.cpp
* (SendCompactBlock / ProcessCompactBlock). This function only handles
* the inventory advertisement at the net layer.
*/
void RelayBlockInventory(const uint256& hash)
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
{
// Use MSG_CMPCT_BLOCK for peers that support compact relay,
// MSG_BLOCK for legacy peers.
int nType = pnode->fSendCmpct ? MSG_CMPCT_BLOCK : MSG_BLOCK;
pnode->PushInventory(CInv(nType, hash));
}
}
-19
View File
@@ -21,9 +21,7 @@
class CNode;
class CBlockIndex;
bool IsInitialBlockDownload();
void ThreadForkDetector(void*);
extern int nBestHeight;
extern int nForkAlertCount;
@@ -263,15 +261,6 @@ public:
int nBestKnownHeight; // highest block height known to this peer (updated from inv/block msgs)
int nIncompatibleGetblocks; // count of getblocks with no common blocks (fork detection)
// Option C: peer reliability scoring. Higher = more reliable.
// Starts at 100 (neutral), grows with successful block delivery, shrinks with
// disconnects and unreachable-on-connect. Used by sync manager to prefer
// reliable peers for header/block requests and to demote flaky ones.
int nReliabilityScore = 100;
int nDisconnectCount = 0; // disconnects since startup
int nConnectFailures = 0; // host-unreachable on connect attempts
int64_t nLastDisconnectTime = 0; // for flapping detection (many disconnects in short window)
// BIP 31 ping/pong latency tracking
uint64_t nPingNonceSent; // nonce of last ping sent (0 = no outstanding ping)
int64_t nPingUsecStart; // microsecond timestamp when last ping was sent
@@ -282,8 +271,6 @@ public:
std::vector<CAddress> vAddrToSend;
mruset<CAddress> setAddrKnown;
bool fGetAddr;
int64_t nLastGetaddrTrigger; // last time we sent this peer a discovery round (getaddr+getseederlist+getwalletaddr)
int nSignedPeerBonus; // +N reputation when peer completed walletaddr handshake (signed identity)
std::set<uint256> setKnown;
uint256 hashCheckpointKnown; // triangles: known sent sync-checkpoint
@@ -338,8 +325,6 @@ public:
nPingUsecTime = 0;
nPingRetryCount = 0;
fGetAddr = false;
nLastGetaddrTrigger = 0;
nSignedPeerBonus = 0;
nMisbehavior = 0;
hashCheckpointKnown = 0;
setInventoryKnown.max_size(SendBufferSize() / 1000);
@@ -564,10 +549,6 @@ public:
void CancelSubscribe(unsigned int nChannel);
void CloseSocketDisconnect();
void Cleanup();
// Option C: recompute reliability score from current counters.
// Call this periodically (e.g. in sync manager tick) to apply the
// flapping penalty (5+ disconnects in 5min = extra 50 penalty).
int RecomputeReliabilityScore();
// Denial-of-service detection/prevention
+9 -241
View File
@@ -10,15 +10,8 @@
#ifndef WIN32
#include <sys/fcntl.h>
#include <netinet/tcp.h>
#endif
#include <cstdlib>
#include <cctype>
#include <cerrno>
#include <limits>
#include <sstream>
#include "strlcpy.h"
using namespace std;
@@ -28,13 +21,6 @@ static proxyType proxyInfo[NET_MAX];
static proxyType nameproxyInfo;
static CCriticalSection cs_proxyInfos;
int nConnectTimeout = 5000;
// Bound for the SOCKS5 negotiation over Tor (ms). The recv() calls in Socks5()
// wait for Tor to build a circuit and fetch the v3 hidden-service descriptor for
// the target .onion; with no timeout a dead/slow onion blocks the connecting
// thread (holding an outbound slot) until Tor's own ~120s SocksTimeout fires.
// Configurable via -torconnecttimeout. Default 60s: long enough for a healthy
// onion to answer, short enough that bad peers don't starve a from-zero node.
int nSocksNegotiationTimeout = 60000;
bool fNameLookup = false;
static const unsigned char pchIPv4[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff };
@@ -237,24 +223,6 @@ bool static Socks5(string strDest, int port, SOCKET& hSocket)
closesocket(hSocket);
return error("Hostname too long");
}
// Bound the blocking SOCKS5 handshake so a slow/dead .onion can't stall this
// thread (and hold an outbound connection slot) waiting on Tor. A timeout makes
// the recv() below return < expected, which the existing checks treat as a
// clean failure so the connector moves on to the next peer.
{
#ifdef WIN32
DWORD tv = (DWORD)nSocksNegotiationTimeout;
setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
#else
struct timeval tv;
tv.tv_sec = nSocksNegotiationTimeout / 1000;
tv.tv_usec = (nSocksNegotiationTimeout % 1000) * 1000;
setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (const void*)&tv, sizeof(tv));
setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (const void*)&tv, sizeof(tv));
#endif
}
char pszSocks5Init[] = "\5\1\0";
if (fDebug)
{
@@ -458,19 +426,6 @@ bool static ConnectSocketDirectly(const CService &addrConnect, SOCKET& hSocketRe
}
}
// TCP_NODELAY: disable Nagle's algorithm for low-latency P2P messaging.
// SO_KEEPALIVE: detect dead connections faster (important for Tor/I2P
// tunnels that can silently drop without RST/FIN).
{
int one = 1;
#ifdef WIN32
setsockopt(hSocket, IPPROTO_TCP, TCP_NODELAY, (char*)&one, sizeof(one));
#else
setsockopt(hSocket, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
#endif
setsockopt(hSocket, SOL_SOCKET, SO_KEEPALIVE, (char*)&one, sizeof(one));
}
// this isn't even strictly necessary
// CNode::ConnectNode immediately turns the socket back to non-blocking
// but we'll turn it back to blocking just in case
@@ -605,33 +560,6 @@ bool ConnectSocketByName(CService &addr, SOCKET& hSocketRet, const char *pszDest
SOCKET hSocket = INVALID_SOCKET;
// I2P routing: .b32.i2p destinations go through i2pd's SOCKS proxy, not
// the Tor name proxy. This is the key routing decision for dual-network
// anonymity — Tor handles .onion, i2pd handles .b32.i2p.
bool isI2PDest = (strDest.size() > 7 &&
strDest.substr(strDest.size() - 7, 7) == ".b32.i2p");
if (isI2PDest) {
// Route through the I2P SOCKS proxy
proxyType i2pProxy;
if (GetProxy(NET_I2P, i2pProxy)) {
addr = CService("0.0.0.0:0");
printf("ConnectSocketByName(): routing .b32.i2p via I2P SOCKS proxy\n");
if (!ConnectSocketDirectly(i2pProxy.first, hSocket, nTimeout))
return false;
// i2pd's SOCKS proxy accepts .b32.i2p domain names via SOCKS5 ATYP=domain
if (!Socks5(strDest, port, hSocket)) {
printf("ConnectSocketByName(): I2P SOCKS5 handshake failed\n");
return false;
}
printf("ConnectSocketByName(): connected via I2P SOCKS5\n");
hSocketRet = hSocket;
return true;
}
// No I2P proxy configured — fall through to nameproxy (will likely fail)
printf("ConnectSocketByName(): WARNING - .b32.i2p dest but no I2P proxy set\n");
}
proxyType nameproxy;
GetNameProxy(nameproxy);
@@ -713,26 +641,14 @@ bool CNetAddr::SetSpecial(const std::string &strName)
m_is_tor_v3 = false;
return true;
}
// Standard I2P b32 address: <52 base32 chars>.b32.i2p
// (SHA-256 hash of destination key, base32-encoded)
if (strName.size()>7 && strName.substr(strName.size() - 7, 7) == ".b32.i2p") {
std::string b32Part = strName.substr(0, strName.size() - 7);
std::vector<unsigned char> vchAddr = DecodeBase32(b32Part.c_str());
if (vchAddr.size() == 32) {
// Standard 32-byte I2P destination hash
memcpy(ip, pchGarliCat, sizeof(pchGarliCat));
// Store as many bytes as fit (16 - prefix_size)
for (unsigned int i = 0; i < 16 - sizeof(pchGarliCat) && i < vchAddr.size(); i++)
ip[i + sizeof(pchGarliCat)] = vchAddr[i];
return true;
}
// Also handle the legacy .oc.b32.i2p format (10 bytes)
if (vchAddr.size() == 16 - sizeof(pchGarliCat)) {
memcpy(ip, pchGarliCat, sizeof(pchGarliCat));
for (unsigned int i = 0; i < 16 - sizeof(pchGarliCat); i++)
ip[i + sizeof(pchGarliCat)] = vchAddr[i];
return true;
}
if (strName.size()>11 && strName.substr(strName.size() - 11, 11) == ".oc.b32.i2p") {
std::vector<unsigned char> vchAddr = DecodeBase32(strName.substr(0, strName.size() - 11).c_str());
if (vchAddr.size() != 16-sizeof(pchGarliCat))
return false;
memcpy(ip, pchOnionCat, sizeof(pchGarliCat));
for (unsigned int i=0; i<16-sizeof(pchGarliCat); i++)
ip[i + sizeof(pchGarliCat)] = vchAddr[i];
return true;
}
return false;
}
@@ -963,7 +879,7 @@ std::string CNetAddr::ToStringIP() const
if (IsTor())
return EncodeBase32(&ip[6], 10) + ".onion";
if (IsI2P())
return EncodeBase32(&ip[6], 10) + ".b32.i2p";
return EncodeBase32(&ip[6], 10) + ".oc.b32.i2p";
CService serv(*this, 0);
#ifdef USE_IPV6
struct sockaddr_storage sockaddr;
@@ -1371,151 +1287,3 @@ void CService::SetPort(unsigned short portIn)
{
port = portIn;
}
// ═══════════════════════════════════════════════════════════════════════════════
// v5.9.22 hardening: pure helper functions for the HTTPS seed-list path.
// See netbase.h for the contract. These are intentionally free of SSL/Tor
// dependencies so they can be unit-tested in isolation.
// ═══════════════════════════════════════════════════════════════════════════════
bool IsValidSocksNegotiationTimeout(int nMs)
{
// Range bounds match the documented -torconnecttimeout contract. 5000ms
// is the lower edge that still tolerates a slow SOCKS handshake over a
// congested link; 180000ms (3 min) is the upper edge to prevent a stuck
// thread from holding an outbound connection slot indefinitely. These
// constants are duplicated in src/init.cpp's HelpMessage text and the
// test suite — keep all three in sync.
return nMs >= 5000 && nMs <= 180000;
}
int DechunkTransferEncoding(const std::string& body, std::string& decoded)
{
decoded.clear();
if (body.empty())
return DECHUNK_EMPTY;
// HTTP chunked framing requires every chunk-size line to be terminated
// by CRLF. We walk the body one chunk at a time and validate each piece.
// The previous implementation silently dropped malformed chunks and
// treated them as the last-chunk marker, which lost the entire seed list
// for any non-conforming server. This version returns an explicit error
// code for each failure mode.
size_t pos = 0;
const size_t n = body.size();
bool sawLastChunk = false;
while (pos < n) {
// Find end of chunk-size line. Required: CRLF.
size_t eol = body.find("\r\n", pos);
if (eol == std::string::npos)
return DECHUNK_NO_CHUNK_TERMINATOR;
std::string sizeLine = body.substr(pos, eol - pos);
pos = eol + 2; // consume CRLF
// Strip chunk extensions per RFC 7230 §4.1.1: ";name[=value]" after
// the hex size. Extensions are part of the framing protocol, not
// data, so we drop them here.
size_t semi = sizeLine.find(';');
std::string hexSize = (semi == std::string::npos) ? sizeLine : sizeLine.substr(0, semi);
// Strict hex validation: every character must be [0-9A-Fa-f]. Empty
// size lines (e.g. a stray CRLF) are rejected as malformed, not
// silently treated as 0. strtoul alone would also accept leading
// whitespace, '+', and '-' which we don't want.
if (hexSize.empty())
return DECHUNK_INVALID_HEX;
for (size_t i = 0; i < hexSize.size(); ++i) {
if (!isxdigit(static_cast<unsigned char>(hexSize[i])))
return DECHUNK_INVALID_HEX;
}
// strtoul returns ULONG_MAX on overflow. We also need to guard
// against chunks larger than the remaining input, which the old
// code clamped silently. Use strtoull so we can detect overflow
// without truncation surprises on 32-bit builds.
errno = 0;
char* endp = nullptr;
unsigned long long chunkSize = strtoull(hexSize.c_str(), &endp, 16);
if (errno == ERANGE || chunkSize > std::numeric_limits<size_t>::max())
return DECHUNK_INVALID_HEX;
if (endp == hexSize.c_str())
return DECHUNK_INVALID_HEX;
if (chunkSize == 0) {
// Last-chunk: payload is empty, trailer part (which we ignore)
// follows and is terminated by a final CRLF on its own line.
sawLastChunk = true;
break;
}
// Bounds check before reading the chunk data. Catching this
// explicitly (rather than clamping) is what lets callers
// distinguish "truncated network read" from "server sent us junk".
if (chunkSize > n - pos)
return DECHUNK_OVERSIZE_CHUNK;
decoded.append(body, pos, static_cast<size_t>(chunkSize));
pos += static_cast<size_t>(chunkSize);
// Per RFC 7230 each chunk's data must be followed by a CRLF. We
// tolerate the final chunk missing its trailing CRLF (some clients
// do this when the connection is being closed anyway), but for any
// non-final chunk a missing CRLF is a hard framing error.
if (pos + 1 < n && body[pos] == '\r' && body[pos + 1] == '\n') {
pos += 2;
} else if (pos >= n) {
// End of input immediately after chunk data — no CRLF, but
// nothing left to misframe. Reject to be strict.
return DECHUNK_MISSING_DATA_CRLF;
} else {
return DECHUNK_MISSING_DATA_CRLF;
}
}
if (!sawLastChunk) {
// Body ended without a last-chunk marker. Treat as malformed
// rather than accepting a truncated body.
return DECHUNK_NO_CHUNK_TERMINATOR;
}
return DECHUNK_OK;
}
std::vector<std::string> ParseSeedListBody(const std::string& body)
{
std::vector<std::string> out;
std::istringstream lines(body);
std::string line;
while (std::getline(lines, line)) {
// Strip inline '#' comments. Per common seed-list convention, the
// first '#' to end-of-line is comment.
size_t hashPos = line.find('#');
if (hashPos != std::string::npos)
line = line.substr(0, hashPos);
// Split on whitespace, comma, or semicolon so multiple addresses
// on one line are all captured. CR/LF are already consumed by
// std::getline but a trailing CR (LF-only line endings) is trimmed
// implicitly by skipping it as a separator below.
size_t start = 0;
while (start <= line.size()) {
size_t sep = line.find_first_of(" \t,;", start);
std::string tok = (sep == std::string::npos)
? line.substr(start)
: line.substr(start, sep - start);
// Trim CR and any leftover whitespace from the token. The
// 'sep' loop above eats spaces/tabs but a bare CR survives.
while (!tok.empty() && (tok.back() == '\r' || tok.back() == ' ' || tok.back() == '\t'))
tok.pop_back();
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
tok.erase(tok.begin());
if (!tok.empty())
out.push_back(tok);
if (sep == std::string::npos) break;
start = sep + 1;
}
}
return out;
}
-70
View File
@@ -29,78 +29,8 @@ enum Network
};
extern int nConnectTimeout;
extern int nSocksNegotiationTimeout;
extern bool fNameLookup;
// ═══════════════════════════════════════════════════════════════════════════════
// v5.9.22 hardening: pure helper functions for the HTTPS seed-list path.
// Extracted from net.cpp ThreadHTTPSeedFetch2 so they can be unit-tested
// without the SSL/Tor network stack. All functions are side-effect free and
// operate on std::string/std::vector<std::string> only.
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Result of dechunking an HTTP/1.1 chunked body. The daemon used to silently
* treat malformed framing as a zero-length chunk, which dropped the entire
* seed list. This enum lets the caller distinguish each failure mode and
* surface it in logs.
*/
enum DechunkResult {
DECHUNK_OK = 0, // success
DECHUNK_EMPTY, // body is empty
DECHUNK_NO_CHUNK_TERMINATOR, // missing CRLF after a chunk-size line
DECHUNK_INVALID_HEX, // chunk-size line is not valid hex
DECHUNK_OVERSIZE_CHUNK, // declared chunk size exceeds remaining input
DECHUNK_MISSING_DATA_CRLF, // CRLF missing after a chunk's data
};
/**
* Decode an HTTP/1.1 Transfer-Encoding: chunked body.
*
* chunked-body = *chunk last-chunk trailer-part CRLF
* chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF
* chunk-size = 1*HEXDIG
* last-chunk = 1*("0") [ chunk-ext ] CRLF
* chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
*
* @param[in] body the raw body bytes after the header terminator
* @param[out] decoded the dechunked payload on success
* @return status code (DECHUNK_OK or one of the failure modes)
*
* The implementation is intentionally strict: a malformed hex digit, a
* missing CRLF, or a chunk whose declared size is larger than the remaining
* input all return an explicit error code rather than silently clamping.
* Chunk extensions ("a;foo=bar") are preserved (stripped from the size
* line) so legitimate servers that attach metadata to chunks are still
* accepted.
*/
int DechunkTransferEncoding(const std::string& body, std::string& decoded);
/**
* Parse a tolerant HTTPS seed-list body into individual host entries.
*
* Accepted per line:
* - one or more addresses separated by whitespace, commas, or semicolons
* - inline "#" comments (everything after '#' is dropped)
* - blank lines
* - CRLF or LF line endings
*
* Each returned entry is the address string (e.g. "abcd...onion:24112" or
* "abcd...onion"). Empty/whitespace-only entries are omitted. The result is
* a list of candidate strings suitable for CNetAddr/CService validation
* downstream.
*/
std::vector<std::string> ParseSeedListBody(const std::string& body);
/**
* Validate the -torconnecttimeout / nSocksNegotiationTimeout value.
*
* Accepts 5000..180000 ms inclusive. Returns true for in-range, false for
* out-of-range. This is the central policy so callers and tests stay in
* sync; do not duplicate the literal numbers elsewhere.
*/
bool IsValidSocksNegotiationTimeout(int nMs);
/** IP address (IPv6, or IPv4 using mapped IPv6 range (::FFFF:0:0/96)) */
class CNetAddr
{
-12
View File
@@ -72,18 +72,6 @@ enum
NODE_SNAPSHOT = (1 << 1), // peer can serve UTXO snapshot chunks
};
/** Inventory type constants for CInv.
*
* MSG_TX and MSG_BLOCK are the legacy inventory types used for
* transaction and block relay. MSG_CMPCT_BLOCK (BIP152) signals
* that the sender wants the block delivered as a compact block
* instead of a full serialized block.
*/
enum
{
MSG_CMPCT_BLOCK = 4, // BIP152 compact block inventory type
};
/** A CService with information about it as peer */
class CAddress : public CService
{
+22 -142
View File
@@ -1366,13 +1366,13 @@ QPushButton:hover {
<property name="minimumSize">
<size>
<width>0</width>
<height>52</height>
<height>37</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>52</height>
<height>37</height>
</size>
</property>
<property name="styleSheet">
@@ -1413,146 +1413,26 @@ QLabel {
</spacer>
</item>
<item>
<widget class="QWidget" name="wAddressStack" native="true">
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="wI2PRow" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_i2p">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="label_i2p_icon">
<property name="font">
<font>
<pointsize>8</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>I2P router status</string>
</property>
<property name="text">
<string notr="true">[I2P]</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_i2p">
<property name="font">
<font>
<pointsize>8</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="toolTip">
<string>Click to copy .b32.i2p address</string>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="wTorRow" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_tor">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="label_tor_icon">
<property name="font">
<font>
<pointsize>8</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>Tor V3 hidden service status</string>
</property>
<property name="text">
<string notr="true">[Tor]</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_onion">
<property name="font">
<font>
<pointsize>8</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="toolTip">
<string>Click to copy .onion address</string>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
<widget class="QLabel" name="label_onion">
<property name="font">
<font>
<pointsize>8</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="toolTip">
<string>Click to copy .onion address</string>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
</widget>
</item>
<item>
+1 -1
View File
@@ -815,7 +815,7 @@ QWidget#line {
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</string>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
-142
View File
@@ -1,142 +0,0 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license.
#include "hdseeddialog.h"
#include "walletmodel.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QPlainTextEdit>
#include <QLabel>
#include <QMessageBox>
#include <QFont>
HDSeedDialog::HDSeedDialog(QWidget *parent)
: QDialog(parent), model(0), seedText(0), statusLabel(0)
{
setWindowTitle(tr("HD Seed Phrase (BIP39)"));
resize(560, 360);
QVBoxLayout *layout = new QVBoxLayout(this);
QLabel *intro = new QLabel(tr(
"A 24-word seed phrase is a complete backup of this wallet. Anyone who has it "
"can spend your coins. Write it down on paper and keep it offline."), this);
intro->setWordWrap(true);
layout->addWidget(intro);
seedText = new QPlainTextEdit(this);
seedText->setPlaceholderText(tr(
"Your 24-word phrase appears here when you generate or reveal it. "
"To restore, paste an existing 24-word phrase here and click 'Restore from Phrase'."));
QFont mono("monospace");
mono.setStyleHint(QFont::Monospace);
seedText->setFont(mono);
layout->addWidget(seedText);
statusLabel = new QLabel(this);
statusLabel->setWordWrap(true);
layout->addWidget(statusLabel);
QHBoxLayout *btns = new QHBoxLayout();
QPushButton *genBtn = new QPushButton(tr("Generate New"), this);
QPushButton *showBtn = new QPushButton(tr("Reveal for Backup"), this);
QPushButton *restoreBtn = new QPushButton(tr("Restore from Phrase"), this);
QPushButton *closeBtn = new QPushButton(tr("Close"), this);
btns->addWidget(genBtn);
btns->addWidget(showBtn);
btns->addWidget(restoreBtn);
btns->addStretch();
btns->addWidget(closeBtn);
layout->addLayout(btns);
connect(genBtn, SIGNAL(clicked()), this, SLOT(onGenerate()));
connect(showBtn, SIGNAL(clicked()), this, SLOT(onShow()));
connect(restoreBtn, SIGNAL(clicked()), this, SLOT(onRestore()));
connect(closeBtn, SIGNAL(clicked()), this, SLOT(accept()));
}
void HDSeedDialog::setModel(WalletModel *modelIn)
{
model = modelIn;
refreshStatus();
}
void HDSeedDialog::refreshStatus()
{
if (!model || !statusLabel) return;
if (model->hdEnabled())
statusLabel->setText(tr("Status: HD seed is ACTIVE. Use 'Reveal for Backup' to view your phrase."));
else
statusLabel->setText(tr("Status: no HD seed yet. Use 'Generate New' to create one."));
}
void HDSeedDialog::onGenerate()
{
if (!model) return;
if (model->hdEnabled()) {
QMessageBox::warning(this, tr("HD seed already set"),
tr("This wallet already has an HD seed. Use 'Reveal for Backup' to view it."));
return;
}
if (QMessageBox::question(this, tr("Generate new seed"),
tr("Generate a new 24-word HD seed for this wallet?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
return;
WalletModel::UnlockContext ctx(model->requestUnlock());
if (!ctx.isValid()) return;
QString mnemonic, err;
if (!model->hdNew(mnemonic, err)) {
QMessageBox::critical(this, tr("Error"), err);
return;
}
seedText->setPlainText(mnemonic);
QMessageBox::information(this, tr("Write this down"),
tr("Your new 24-word seed phrase is shown above. Write it on paper and store it safely "
"and offline. This is the only backup of this wallet."));
refreshStatus();
}
void HDSeedDialog::onShow()
{
if (!model) return;
WalletModel::UnlockContext ctx(model->requestUnlock());
if (!ctx.isValid()) return;
QString mnemonic, err;
if (!model->hdShow(mnemonic, err)) {
QMessageBox::critical(this, tr("Error"), err);
return;
}
seedText->setPlainText(mnemonic);
}
void HDSeedDialog::onRestore()
{
if (!model) return;
QString phrase = seedText->toPlainText().trimmed();
if (phrase.isEmpty()) {
QMessageBox::warning(this, tr("No phrase"),
tr("Paste a 24-word phrase into the box first."));
return;
}
if (QMessageBox::question(this, tr("Restore from phrase"),
tr("Restore the HD seed from the phrase in the box and rescan the chain? "
"This replaces the current HD seed."),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
return;
WalletModel::UnlockContext ctx(model->requestUnlock());
if (!ctx.isValid()) return;
QString err;
if (!model->hdRestore(phrase, err)) {
QMessageBox::critical(this, tr("Error"), err);
return;
}
QMessageBox::information(this, tr("Restored"),
tr("HD seed restored and the chain was rescanned for your funds."));
refreshStatus();
}
-34
View File
@@ -1,34 +0,0 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license.
#ifndef HDSEEDDIALOG_H
#define HDSEEDDIALOG_H
#include <QDialog>
class WalletModel;
QT_BEGIN_NAMESPACE
class QPlainTextEdit;
class QLabel;
QT_END_NAMESPACE
/** Generate, reveal (for backup), and restore the wallet's BIP39 HD seed phrase. */
class HDSeedDialog : public QDialog
{
Q_OBJECT
public:
explicit HDSeedDialog(QWidget *parent = 0);
void setModel(WalletModel *model);
private:
WalletModel *model;
QPlainTextEdit *seedText;
QLabel *statusLabel;
void refreshStatus();
private slots:
void onGenerate();
void onShow();
void onRestore();
};
#endif // HDSEEDDIALOG_H
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1416,7 +1416,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1444,7 +1444,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1412,7 +1412,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1440,7 +1440,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1412,7 +1412,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1440,7 +1440,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1429,7 +1429,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1457,7 +1457,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1414,7 +1414,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1442,7 +1442,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1427,7 +1427,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1455,7 +1455,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>

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