Compare commits

...

30 Commits

Author SHA1 Message Date
Krystie 53f003aef1 v5.9.24: update TRI home + explorer links, networking fixes, checkpoint publisher
- qt: TRI home → https://cryptographic-triangles.org/ (UI + 65 locales)
- qt: block explorer → https://blocks.cryptographic-triangles.org (65 locales)
- net: networking hardening + checkpoint publisher support
- build: MinGW cross-compilation toolchain, CI tridock rebuild trigger
- test: checkpoint publisher + onion v3 test updates
- test: chaindb equivalence test suite (LevelDB↔RocksDB migration parity)
- util: expose ResetDataDirCache() for test fixture datadir switching
- txdb: WriteRawPublic/ReadRawPublic test seam for raw byte-level access
- version bump 5.9.23 → 5.9.24
2026-06-23 20:13:02 -07:00
Krystie 9762c741b7 distribute: fix $schema aka.ms URL + add NSIS Silent switches
Two errors from PR #391813 manifest validation (build 349844):

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

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

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

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

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

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

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

What changed in the template heredocs:

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

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

Two new safeguards:

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

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

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

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

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

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

DechunkTransferEncoding is now strict (was lenient):

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

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

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

Six distinct failure-mode log messages in ThreadHTTPSeedFetch2:

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

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

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

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

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

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

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

Test results: 183 test cases total, *** No errors detected. Existing
onion_v3_tests (8) and netbase_tests (10) still pass.
2026-06-22 00:55:51 -07:00
Sami Ahmed 43db0138c6 merge: Tor/HTTP resilience fixes (v5.9.22)
Brings v5.9.22 to master for CI build and distribution.
- -torconnecttimeout config (5-180s, default 60s)
- Chunked-encoding aware HTTP seed body parser
- Tolerant seed parser (whitespace, commas, semicolons, comments)

3 bugs in original Claude diff fixed before merge:
- Removed orphan code referencing undefined parsed/addrStr
- Replaced non-existent AddSeed() with CService service(addr,port)
- Correct addrman.Add signature: CAddress + CService
2026-06-21 19:49:15 -07:00
Sami Ahmed 78256e65d7 net: 3 Tor/HTTP resilience fixes from experimental patch
1. -torconnecttimeout config option (init.cpp, netbase.h, netbase.cpp)
   SOCKS5/Tor negotiation bound. Default 60s. Range 5-180s. Without this, a
   dead/slow .onion blocks the connecting thread (holding an outbound slot)
   until Tor's own ~120s SocksTimeout fires, starving a from-zero node.

   Implementation: SO_RCVTIMEO + SO_SNDTIMEO on the SOCKS5 socket only,
   inside Socks5(). Both Linux/BSD and Win32 paths. Configurable because
   consensus-validating nodes may want a longer ceiling than IBD nodes.

2. HTTP seed fetch: chunked-encoding support (net.cpp ThreadHTTPSeedFetch2)
   Some servers (Caddy, Let's Encrypt proxies) reply with
   Transfer-Encoding: chunked even on HTTP/1.1 + Connection: close. The
   previous parser read the body raw and saw hex chunk-size lines
   interleaved with addresses, fusing a chunk marker onto the first
   address and dropping the rest of the list (the 'only 1 address'
   symptom). De-chunk first when header advertises chunked, then parse.

3. Tolerant seed parser: whitespace/comma/semicolon separated, inline
   comments, multi-address-per-line (net.cpp)
   Real seed lists are often formatted for humans (multiple per line,
   inline comments) or older scripts (semicolons). The previous one-per-
   line, no-comments, no-inline parser lost any address that broke the
   strict format. Now strips inline '#' comments, splits on any of
   ' \t,;' so a single line can yield N addresses, and trims each.

Bugs caught and fixed before this commit (so the patch as-shipped is
clean):
- Removed orphan code referencing undefined 'parsed' and 'addrStr' vars
  from a copy-paste of an earlier draft
- Replaced non-existent 'AddSeed()' with direct 'CService service(...)'
  construction followed by 'addrman.Add(CAddress, CService)' (correct
  addrman.Add signature, not CNetAddr)
- Tightened 'addrman.Add' call to the actual signature: address + source
2026-06-21 19:48:48 -07:00
Sami Ahmed 55f1b03848 build: bump version to 5.9.21 — signed peer discovery + v3 onion validator
Release 5.9.21 includes:
  * Signed peer discovery (commit 9e9d17e) — periodic re-fire of
    getaddr/getseederlist when peer count drops, signed-peer bonus
    preference in syncmanager
  * scripts/validate_onion_seeds.py — Python validator for v3 onion
    checksums with 'did you mean' suggestions
  * scripts/pre-commit — auto-validates any triangles.conf edit
  * src/test/onion_v3_tests.cpp — 8-case Boost.Test suite
  * contrib/triangles.conf.example — pre-validated starting config
  * SYNC-SECURITY-AUDIT-2026-06-21.md addendum covering the
    corrupted .onion discovery + signed-peer architecture
2026-06-21 18:17:40 -07:00
Sami Ahmed 21ab4bb4c3 contrib: add triangles.conf.example with all 7 hardcoded seeds
A canonical starting point for new operators. Pre-validated against
the v3 onion checksum, so anyone copying this file gets a known-good
config out of the box. Documents:

  * The 7 hardcoded seeds from src/onionseed.h (with port 24112)
  * How to add the 7 dynamic seeds from seeds.cryptographic-triangles.org
    (commented out, since the daemon fetches them automatically)
  * The pre-commit hook installation instructions
  * The Tor-only requirement (notor=0 must stay)
  * Standard index flags (txindex, addressindex, spentindex, timestampindex)
  * dbcache sizing guidance

The 7 hardcoded seeds were taken verbatim from src/onionseed.h and
verified by scripts/validate_onion_seeds.py. The C++ test suite
src/test/onion_v3_tests.cpp also re-validates them at every build.

Bonus: this file gets auto-validated by the pre-commit hook on every
commit, so any future edit that introduces a corrupt .onion will be
caught before it can reach a deployment.
2026-06-21 16:58:32 -07:00
Sami Ahmed fe61e34da6 docs: addendum to SYNC-SECURITY-AUDIT covering corruption + signed peers
Adds Finding 8 (corrupted v3 .onion address in test config) and
Finding 9 (signed peer discovery) to the security audit. Documents
the full chain:

  4,842 Tor 'No more HSDir' errors
    → identified as bad .onion (btb6 vs gtb6)
    → root-caused to one-character config typo
    → fixed in triangles.conf
    → built validator tool (scripts/validate_onion_seeds.py)
    → built pre-commit hook (scripts/pre-commit)
    → built C++ test suite (src/test/onion_v3_tests.cpp)
    → shipped signed peer discovery (commit 9e9d17e)

Includes a defense-in-depth table showing the 4 layers of protection
now in place (Tor checksum, Python validator, C++ tests, signed peers).

Also documents 3 remaining gaps for future work:
  1. No signing on seeds.cryptographic-triangles.org seed list
  2. No audit log of when the btb6 typo was introduced
  3. getwalletaddr creates a new key per call (should use stable node identity)
2026-06-21 16:49:54 -07:00
Sami Ahmed 20bb571690 tests: add v3 onion address validator + fix Phase 1.5 build break
Adds src/test/onion_v3_tests.cpp with 8 Boost.Test cases that validate
every hardcoded seed in src/onionseed.h against the v3 hidden service
checksum algorithm (SHA3-256 of ".onion checksum" || pubkey || version).

Test cases:
  * onion_v3_valid_known_seeds - all 7 hardcoded seeds must validate
  * onion_v3_detects_transposition - catches the btb6/gtb6 bug from 2026-06-21
  * onion_v3_detects_wrong_length - too short, too long
  * onion_v3_detects_missing_suffix - .com instead of .onion
  * onion_v3_detects_invalid_base32 - chars 0,1,8,9 + uppercase rejected
  * onion_v3_detects_bad_version_byte - all-'a' body has invalid checksum
  * onion_v3_round_trip_encoding - base32 encode/decode is deterministic
  * onion_v3_audit_summary - overall summary check

The C++ validator mirrors scripts/validate_onion_seeds.py exactly so the
two implementations stay in sync. Catches corruption at CI/build time
instead of daemon runtime.

Also fixes an unrelated build break: GetPeerInflightCap() was called from
syncmanager.cpp:533 but never declared in syncmanager.h. The function
intent was 'windowSize / peerCount + 1' - inlined that here so the test
build can succeed.
2026-06-21 16:48:58 -07:00
Sami Ahmed f58d0a5a15 scripts: add pre-commit hook that auto-validates .onion addresses
The hook scans every staged file for:
  1. Filename matches: triangles.conf, *.onion
  2. Content matches: lines starting with 'addnode=' followed by a
     base32-encoded .onion address

If any address fails v3 onion checksum validation, the commit is blocked
with a clear diagnostic showing the bad address, the reason, and (when
possible) a suggestion of the correct address.

Run with --ci mode on the validator so it exits 1 on any failure.

Install:
  cp scripts/pre-commit .git/hooks/pre-commit
  chmod +x .git/hooks/pre-commit

Bypass (NEVER do this for normal commits):
  git commit --no-verify

Tested:
  ✓ Clean config: commit allowed, validator says PASSED
  ✓ Corrupted config (btb6 vs gtb6): commit blocked with full
    diagnostic + 'did you mean: gtb6?' suggestion
2026-06-21 16:41:14 -07:00
Sami Ahmed 2bc69cd9e3 scripts: add v3 onion address validator for triangles.conf
Detects corrupted .onion addresses by validating the v3 hidden service
checksum (SHA3-256 of ".onion checksum" || pubkey || version).

Background: 2026-06-21 from-zero sync test produced 4,842 Tor
"No more HSDir" 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.1s.

Usage:
  ./scripts/validate_onion_seeds.py /root/.triangles/triangles.conf
  ./scripts/validate_onion_seeds.py /path/to/triangles.conf --ci
  ./scripts/validate_onion_seeds.py /path/to/triangles.conf \
    --against /root/triangles_v5/src/onionseed.h

Features:
  * Validates every addnode= line against v3 onion checksum
  * Suggests the correct address if 1-2 char transposition detected
  * Detects truncated/extended/non-base32 addresses
  * Cross-checks multiple configs (catches test vs prod mismatches)
  * CI mode exits 1 on any failure (gates deploys)
  * Pure stdlib, no pip deps (works in any Python 3.8+ env)
2026-06-21 16:36:48 -07:00
Sami Ahmed 9e9d17e1e0 sync: signed peer discovery — re-fire getaddr/getseederlist when peer count drops
Triangles already has a node-identity signing system (getwalletaddr/walletaddr
in onion_v3.cpp:4793-4848) that lets peers cryptographically prove they own
their .onion address. The problem: that handshake only fires at startup, so
a long-running sync daemon that takes 12+ hours to bootstrap gets exactly ONE
discovery round at minute 0 — and then never asks again.

This commit wires the existing signing + discovery machinery into the main
peer-connection loop, not just startup:

  * src/net.h: add nLastGetaddrTrigger + nSignedPeerBonus fields to CNode
  * src/net.cpp: in ThreadOpenConnections2, when connected onion peers < 4
    AND 5min cooldown elapsed, re-fire getaddr + getseederlist on every
    connected .onion peer. getwalletaddr is left alone (it generates a new
    receiving key per call; signed peers are cached 24h anyway).
  * src/tor/onion_v3.cpp: when HandleWalletAddrResponse verifies a peer's
    signature, set nSignedPeerBonus=1 so sync peer selection prefers them.
  * src/syncmanager.cpp: signed-peer bonus used as tiebreaker in peer sort
    (after reliability score, before blocks-delivered).

Why this matters: real-world from-zero sync of the Triangles chain took
~18 hours because only 2-3 of the 14 seed .onion nodes were reliably
reachable from any given Tor instance. With periodic re-discovery, the
daemon now has a chance to find the 12 others when the 2-3 drop.

Verified: built clean (15:59), test daemon climbed from 70,828 → 73,997+
at ~1.9 blk/s with new binary, SYNC-SIGN message confirmed firing.
2026-06-21 16:35:27 -07:00
Krystie (TRI packaging) 7de1595647 ci: fix WinGet PR creation (use 'owner:branch' not 'owner/repo:branch') 2026-06-21 02:16:41 -07:00
Krystie (TRI packaging) 758c22e5b2 ci: use unique branch per run for WinGet (triangles-VERSION-RUN#)
Avoid 'fetch first' errors when the same version gets re-distributed
(multiple tags or workflow re-runs). Each run uses its own branch in
the winget-pkgs fork.
2026-06-21 02:14:05 -07:00
Krystie (TRI packaging) b58bb2ce5f ci: fix WinGet gh pr create auth (set GH_TOKEN) 2026-06-21 02:10:55 -07:00
Krystie (TRI packaging) a548aad96c ci: fix distribute.yml chocolatey + winget step bugs
- Chocolatey 'Check' step: add shell: bash so the [ -z ] syntax parses
- WinGet fork: remove --fork flag (renamed), use --remote=false instead
  which omits the clone in the same step
2026-06-21 02:08:38 -07:00
Krystie (TRI packaging) 17b5119d40 packaging + ci: add Chocolatey auto-push + WinGet auto-PR jobs
distribute.yml:
- New 'chocolatey' job: updates nuspec version + install script SHA256,
  packs .nupkg, pushes to chocolatey.org. Gated by CHOCO_SKIP_WACATAC
  env var so it can be disabled while the Microsoft false-positive is
  still active (set CHOCO_SKIP_WACATAC=true on the repo, flip to empty
  after Microsoft clears the detection).
- New 'winget' job: forks microsoft/winget-pkgs (auto-creates fork if
  needed), generates the three manifest files (version/locale/installer)
  in the winget-pkgs v1.6.0 format, opens a PR.

Both jobs use the Windows setup.exe as the installer source.
Both jobs skip gracefully with a warning if their respective GitHub
secrets aren't set.

packaging/chocolatey/tools/chocolateyInstall.ps1:
- Rewritten to use the NSIS installer (.exe) instead of the old .zip
  format (the v5.9.x release ships an NSIS .exe setup)
- Uses $env:ChocolateyPackageVersion so the workflow can substitute the
  version at pack time
- checksum64 is '__CHECKSUM_PLACEHOLDER__' which the workflow replaces
  with the computed SHA256

Required GitHub secrets (all added):
  CHOCO_API_KEY  - Chocolatey API key
  WINGET_TOKEN   - GitHub PAT with public_repo scope
2026-06-21 02:04:59 -07:00
Krystie (TRI packaging) 794b840cdc ci: fix redacted HOMEBREW_GITHUB_TOKEN env value
The previous commit had a literal '***' placeholder where the GitHub
Actions expression ${{ secrets.HOMEBREW_GITHUB_TOKEN }} should have
been. The workflow couldn't parse, so runs showed as 'failure' with
zero jobs and the display name fell back to the file path.

Fixed by writing the correct expression directly.
2026-06-21 01:48:00 -07:00
Krystie (TRI packaging) 7213dddcf1 ci: add job-level guards to distribute.yml
Observed the workflow firing on regular push-to-master events, not just
tag pushes. GitHub is sometimes over-eager about workflow re-runs on
commits that touch the workflow file. Add an explicit job-level guard

  if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')

to all four jobs so the distribute jobs only run on tag pushes or
manual workflow_dispatch events.
2026-06-21 01:43:44 -07:00
Krystie (TRI packaging) 8b147317d5 ci: auto-distribute releases to Homebrew tap on tag
New 'homebrew' job in distribute.yml:
- Waits for the macOS .dmg to be available on the GitHub release
- Computes the new SHA256
- Clones SamiAhmed7777/homebrew-triangles
- Updates version + sha256 in both Formula/triangles.rb and
  Casks/cryptographic-triangles.rb
- Commits and pushes to main
- Skips gracefully with a warning if HOMEBREW_GITHUB_TOKEN is not set

Required GitHub secret: HOMEBREW_GITHUB_TOKEN (added)
2026-06-21 01:39:45 -07:00
Krystie (TRI packaging) 2abb72ed0e ci: fix distribute.yml to handle missing secrets per step
GitHub Actions doesn't allow 'secrets' context in 'if:' conditionals,
only in 'env:'. Reworked the workflow to:

- Capture DOCKERHUB_TOKEN and AUR_SSH_KEY into env vars at job level
- Each step that needs a secret checks env.* and exits 0 with a
  ::warning:: annotation if not set
- Skipped steps display a final summary in the job log

Same behavior, just no parser errors.
2026-06-21 01:33:23 -07:00
Krystie (TRI packaging) 06fea513d8 ci: auto-distribute releases to Docker Hub + AUR on tag
New workflow .github/workflows/distribute.yml:
- Triggers on v* tag push (and workflow_dispatch for manual runs)
- Docker job: builds + pushes to samiahmed7777/trianglesd with both
  :VERSION and :latest tags, plus a post-push smoke test
- AUR job: runs in archlinux container, downloads the release .debs,
  updates PKGBUILD with new version + SHA256s, regenerates .SRCINFO
  via makepkg, commits and pushes to AUR via SSH
- Both jobs skip gracefully (with a clear warning) if their respective
  GitHub secrets aren't set, so the workflow can be merged and tested
  before secrets are configured
- Waits up to 10 minutes for the build-all release artifacts to be
  available (build-all and distribute run in parallel on the same tag)

Required GitHub secrets:
  DOCKERHUB_TOKEN — Docker Hub access token (have in vault)
  AUR_SSH_KEY     — Private key of the AUR packager (~/.ssh/aur_key)
2026-06-21 01:30:32 -07:00
Krystie (TRI packaging) 3ddf6536e5 packaging: bump Docker + AUR to v5.9.20
Docker:
- Dockerfile now extracts from cryptographic-triangles-daemon_5.9.20_amd64.deb
  (release no longer ships raw linux-x64 binaries)
- Multi-stage build with .deb extraction
- Includes triangles-cli alongside trianglesd
- LD_LIBRARY_PATH wrapper for the bundled lib/ dir

AUR:
- Bump triangles-qt-bin to 5.9.20
- Switch from raw linux-x64 binary download (no longer published) to
  extracting the official .deb packages
- Bundle version-pinned libs in /opt/triangles/lib
- Add triangles-cli to provides
2026-06-21 01:19:09 -07:00
Sami Ahmed adbbad3121 Merge sync-freeze-fix: resolves IBD freeze at 15k + PoS header rejection at 1026
From-zero sync test confirmed: chain advances past 15k freeze zone
to 17k+ with no stall. Build clean (149/149 Ninja targets).
132/132 unit tests pass.
2026-06-20 20:56:57 -07:00
Sami Ahmed 7ba8d8b8c9 Fix sync-freeze: backpressure, prune protection, eviction direction, bridge-repair + PoS header guard
Sync-freeze patch (original):
- Backpressure ceiling HEADER_FRONT_MAX_AHEAD=8000
- PruneHeaders protects live sync window (nProtectFloor)
- Hard-cap eviction from highest-height first
- Bridge-repair getheaders from connected tip via PathReachesChain

Additional fix:
- Skip PoW check on PoS headers (nonce=0) in AddHeaderNode
  Block 1026 is PoS but within the 0-9000 PoW range — old code
  rejected valid PoS headers and severed the chain at height 1025

Verified: from-zero no-snapshot sync reached block 17k+ past the
old 15k freeze zone. 132/132 unit tests pass.
2026-06-20 20:56:47 -07:00
Sami Ahmed 6b49dd9e62 Remove legacy bootstrap.tar.gz fallback path (v2 snapshot is now the only sync)
FastImport removal in commit bdb7253 made the v2 UTXO snapshot the
canonical sync start. The legacy DownloadBootstrap() function still
attempted to fetch /triangles-bootstrap.tar.gz first, then fell back to
filelist.txt — which still contained tri-bootstrap.tar.gz. Both legacy
URLs return 404 (cleaned up 2026-06-19), so the wallet wasted a request
on a dead path before reaching the v2 snapshot URL.

Changes:
- DownloadBootstrap() no longer tries /triangles-bootstrap.tar.gz.
- Goes straight to filelist.txt → downloads the URL listed there (now
  utxo-snapshot.bin only, after the bootstrap server fix).
- Removed unused ExtractTarGz() helper function (~110 lines).
- Kept DEFAULT_HOST in bootstrap.h — init.cpp still references it
  for the SnapshotNet P2P fetch.

No version bump. v5.9.20 binary built locally; SHA
ad34764e28fb0c922a3f3570e830ba5707fdc2f7f7a11301e8c0f60356048fd3.

Bootstrap server fix landed first:
- /var/www/triangles-bootstrap/filelist.txt now contains only
  'utxo-snapshot.bin' (was tri-bootstrap.tar.gz + triangles-bootstrap.tar.gz).
This means existing laptop wallets (no rebuild needed) will now read the
updated filelist.txt on next bootstrap attempt and go straight to the
v2 snapshot URL.
2026-06-20 04:07:07 -07:00
114 changed files with 4931 additions and 392 deletions
+27
View File
@@ -33,9 +33,36 @@ jobs:
-DBUILD_TESTS=ON \
-DUSE_UPNP=OFF
# 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
run: |
if [ -x build/bin/test_triangles ]; then
./build/bin/test_triangles --run_test=chaindb_equivalence_tests --log_level=test_suite
else
echo "test_triangles not built — skipping chaindb equivalence"
exit 0
fi
- name: Run unit tests
run: cd build && ctest --output-on-failure || true
+650
View File
@@ -0,0 +1,650 @@
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: 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
@@ -0,0 +1,104 @@
# trigger-tridock-rebuild.yml
#
# Triangles v5.9.24 — release → tridock rebuild dispatcher
#
# Purpose
# -------
# When a new Triangles release is published (e.g. v5.9.24) this workflow
# fires a `repository_dispatch` event at the `samiahmed7777/tridock`
# repository, which in turn triggers that repo's build-and-publish.yml to
# bake the new Triangles binary into a fresh `samiahmed7777/tridock` image.
#
# Why this exists
# ---------------
# Before this workflow, tridock's Docker Hub `latest` tag only updated
# when somebody manually edited the Dockerfile and pushed to master. That
# made it easy to forget — DNS2 ran a 6-days-out-of-date image, and the
# tridock-dev container ended up running v5.9.9 while DNS2 prod ran v5.9.23.
# This workflow closes the gap: every Tri release auto-triggers a tridock
# rebuild, and DNS2's self-hosted runner auto-deploys the result.
#
# Required GitHub Secrets / Vars on triangles_v5 repo
# --------------------------------------------------
# - TRIDOCK_DISPATCH_TOKEN: a GitHub PAT with `repo` scope on the
# samiahmed7777/tridock repository. NOT the same token as
# GITEA_SAMI_TOKEN / GITEA_DASHCADDY_TOKEN / DOCKERHUB_TOKEN.
name: Trigger tridock rebuild on Tri release
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Override version (e.g. 5.9.24). Leave blank to use the published release tag.'
required: false
type: string
permissions:
contents: read
jobs:
dispatch:
name: Notify tridock repo
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Resolve version
id: version
run: |
# On release:published, github.event.release.tag_name is like "v5.9.24"
# Strip the leading "v" so the dispatched payload uses "5.9.24"
if [ "${{ github.event_name }}" = "release" ]; then
TAG="${{ github.event.release.tag_name }}"
VERSION="${TAG#v}"
else
VERSION="${{ inputs.version }}"
fi
if [ -z "$VERSION" ]; then
echo "::error::Could not resolve a version (event=${{ github.event_name }}, tag=${{ github.event.release.tag_name }})"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Dispatching tridock rebuild for Triangles v$VERSION"
- name: Dispatch to samiahmed7777/tridock
run: |
curl -fsSL --max-time 30 \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.TRIDOCK_DISPATCH_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-X POST \
https://api.github.com/repos/SamiAhmed7777/tridock/dispatches \
-d "{\"event_type\": \"tri-release-published\", \"client_payload\": {\"version\": \"${{ steps.version.outputs.version }}\", \"source_repo\": \"SamiAhmed7777/triangles_v5\", \"source_sha\": \"${{ github.sha }}\"}}"
# Verify the dispatch landed
RC=$?
if [ $RC -ne 0 ]; then
echo "::error::Failed to dispatch to tridock repo (curl exit=$RC)"
exit 1
fi
echo "Dispatch OK — tridock build-and-publish.yml will pick this up."
- name: Send Telegram alert
if: always()
continue-on-error: true
env:
TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TG_CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
if [ -z "$TG_TOKEN" ] || [ -z "$TG_CHAT" ]; then
echo "Telegram secrets not set — skipping alert"
exit 0
fi
STATUS="${{ job.status }}"
VERSION="${{ steps.version.outputs.version }}"
MSG="Tri release v$VERSION → tridock dispatch: $STATUS"
curl -fsSL --max-time 10 \
"https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
-d "chat_id=${TG_CHAT}" \
-d "text=${MSG}" \
-d "parse_mode=HTML" \
> /dev/null || echo "Telegram send failed (non-fatal)"
+69
View File
@@ -0,0 +1,69 @@
name: WinGet PR watchdog
# Catches failing WinGet submissions within an hour of opening them.
# Goal: don't leave "needs-author-feedback" or "PullRequest-Error" PRs
# sitting open for days — moderators read sustained unfixed PRs as spam.
#
# Behaviour:
# - Every 30 min, scan open SamiAhmed7777 PRs against microsoft/winget-pkgs
# - For each one, look at recent wingetbot comments to detect validation result
# - If validation FAILED, post a comment summarising the error, close the PR,
# and surface the failure on the workflow summary so it's easy to spot.
on:
schedule:
- cron: '*/30 * * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
watchdog:
name: Scan + auto-close failed WinGet PRs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install gh CLI
run: |
which gh >/dev/null 2>&1 || (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null && sudo apt update && sudo apt install -y gh jq)
- name: Scan + auto-close
env:
GH_TOKEN: ${{ secrets.WINGET_TOKEN }}
run: |
set -e
if [ -z "$GH_TOKEN" ]; then
echo "::warning::WINGET_TOKEN not set — watchdog can scan but cannot close PRs."
fi
echo "Fetching open SamiAhmed7777 PRs against microsoft/winget-pkgs..."
PRS=$(gh api 'repos/microsoft/winget-pkgs/pulls?state=open&per_page=30' --jq '.[] | select(.user.login=="SamiAhmed7777") | "\(.number)|\(.head.ref)|\(.title)|\(.created_at)"')
if [ -z "$PRS" ]; then
echo "OK no open SamiAhmed7777 PRs."
exit 0
fi
echo "$PRS" | while IFS='|' read -r NUM BRANCH TITLE CREATED; do
echo ""
echo "--- PR #$NUM: $TITLE (branch $BRANCH, created $CREATED) ---"
LAST_VALIDATION=$(gh api "repos/microsoft/winget-pkgs/issues/$NUM/comments?per_page=20" --jq '[.[] | select(.user.login=="wingetbot" or .user.login=="stephengillie") | select(.body | test("Result: Failed|Invalid file|Automatic Validation ended"))] | first')
if [ -n "$LAST_VALIDATION" ]; then
echo " X Validation FAILED detected."
SUMMARY=$(echo "$LAST_VALIDATION" | jq -r '.body' | head -40)
echo " Summary:"
echo "$SUMMARY" | sed 's/^/ /'
if [ -n "$GH_TOKEN" ]; then
printf 'Auto-closing: automatic validation failed within the watchdog window.\n\n```\n%s\n```\n\nThe watchdog (winget-watchdog.yml) closed this PR so it does not sit in the moderator queue with a needs-author-feedback flag. Reopen after fixing the issue, or open a fresh PR for a known-good version.\n' "$SUMMARY" > /tmp/watchdog-comment.txt
gh api -X POST "repos/microsoft/winget-pkgs/issues/$NUM/comments" -f body=@/tmp/watchdog-comment.txt || echo " (comment failed, continuing)"
gh api -X PATCH "repos/microsoft/winget-pkgs/pulls/$NUM" -f state=closed || echo " (close failed, continuing)"
echo " OK Closed PR #$NUM"
echo "::warning::Closed failing PR #$NUM -- $TITLE"
else
echo " (no WINGET_TOKEN, skipping close)"
fi
elif gh api "repos/microsoft/winget-pkgs/issues/$NUM/comments?per_page=20" --jq '[.[] | select(.user.login=="wingetbot") | select(.body | test("Validation Pipeline Run"))] | first' | grep -q .; then
echo " ? Validation has been triggered but no failure detected yet — leaving PR open."
else
echo " ? No validation result yet — leaving PR open."
fi
done
+12
View File
@@ -88,3 +88,15 @@ bench-results.csv
/build-latest/
/build-bench/
/.qmake.stash
# MinGW cross-compilation deps (local build environment)
/deps-mingw/
# Snapshot files
*.utx
# Merge artifacts
*.orig
# Dev patches
*.patch
+1 -1
View File
@@ -2,7 +2,7 @@ FROM ubuntu:22.04
LABEL maintainer="Cryptographic Triangles Team"
LABEL description="Cryptographic Triangles (TRI) headless daemon"
LABEL version="5.7.6"
LABEL version="5.9.24"
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
+279
View File
@@ -0,0 +1,279 @@
# 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
@@ -0,0 +1,70 @@
# CMake toolchain file for cross-compiling Triangles for Windows x64 using MinGW on Linux
# Usage: cmake -DCMAKE_TOOLCHAIN_FILE=cmake/mingw64.cmake -B build-mingw -S .
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
# MinGW toolchain
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
# Search for programs only in the build host directories
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# Search for libraries and headers only in the staging directory
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# Staging prefix — all dependencies installed here
set(DEP_PREFIX "${CMAKE_SOURCE_DIR}/deps-mingw")
# Windows libraries
set(CMAKE_LIBRARY_PATH "${DEP_PREFIX}/lib")
# Include directories
set(CMAKE_INCLUDE_PATH "${DEP_PREFIX}/include")
# Windows sysroot (MinGW libraries, headers, and tools)
set(MINGW_SYSROOT /usr/x86_64-w64-mingw32)
# Don't search the host system for programs
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32 ${DEP_PREFIX})
# For find_package(OpenSSL), find_package(Boost), etc.
# Only search deps-mingw and MinGW sysroot — NOT the host system
set(CMAKE_SYSROOT "${MINGW_SYSROOT}")
set(OPENSSL_ROOT_DIR "${DEP_PREFIX}")
set(BOOST_ROOT "${DEP_PREFIX}")
set(CMAKE_PREFIX_PATH "${DEP_PREFIX}")
# Critical: prevent Linux host headers from leaking into MinGW compilation
# The MinGW cross-compiler should ONLY see MinGW and deps headers
set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES "")
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES "")
# Add MinGW and deps include paths explicitly
include_directories(BEFORE SYSTEM
"${DEP_PREFIX}/include"
"${MINGW_SYSROOT}/include"
"${MINGW_SYSROOT}/include/c++"
"${MINGW_SYSROOT}/include/sec_api"
)
# Set output directories
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
# C++20 for the project
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Build settings
set(BUILD_DAEMON ON)
set(BUILD_QT OFF)
set(BUILD_TESTS OFF)
set(USE_UPNP OFF)
set(USE_QRCODE OFF)
set(USE_ZMQ OFF)
set(USE_DBUS OFF)
set(USE_TOR_EMBEDDED OFF)
+88
View File
@@ -0,0 +1,88 @@
# 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
+1 -1
View File
@@ -3,7 +3,7 @@
# Run on a Linux x64 system with appimagetool installed
set -e
VERSION="5.7.6"
VERSION="5.9.24"
APPDIR="Triangles-x86_64.AppDir"
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
+30
View File
@@ -0,0 +1,30 @@
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
+57 -14
View File
@@ -1,6 +1,6 @@
# Maintainer: Cryptographic Triangles Team
# Maintainer: Sami Ahmed <https://github.com/SamiAhmed7777>
pkgname=triangles-qt-bin
pkgver=5.5.6
pkgver=5.9.20
pkgrel=1
pkgdesc="Cryptographic Triangles (TRI) cryptocurrency wallet - Qt GUI"
arch=('x86_64')
@@ -8,21 +8,64 @@ 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')
conflicts=('triangles-qt' 'trianglesd')
provides=('triangles-qt' 'trianglesd' 'triangles-cli')
conflicts=('triangles-qt' 'trianglesd' 'triangles-cli')
source=(
"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"
"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.desktop"
)
sha256sums=(
'ed220eb8d0b403f62cdac28988541fd1a27864491e233216f9c00a4c2537b4a3'
'4d2ab25d61127d6aff3e6f3069556d04f4b823f8849e97629c12871ad4779517'
'SKIP'
)
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
}
package() {
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"
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"
}
@@ -1,18 +1,14 @@
$ErrorActionPreference = 'Stop'
$packageArgs = @{
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'
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__'
checksumType64 = 'sha256'
silentArgs = '/S'
validExitCodes = @(0, 3010, 1641)
}
Install-ChocolateyZipPackage @packageArgs
$installDir = $packageArgs.unzipLocation
$desktopPath = [Environment]::GetFolderPath('Desktop')
Install-ChocolateyShortcut `
-ShortcutFilePath "$desktopPath\Cryptographic Triangles.lnk" `
-TargetPath "$installDir\triangles-qt.exe"
Install-ChocolateyPackage @packageArgs
+1 -1
View File
@@ -3,7 +3,7 @@
# Run from the packaging/debian directory
set -e
VERSION="5.7.6"
VERSION="5.9.24"
PKGDIR="triangles_${VERSION}-1_amd64"
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
+41 -23
View File
@@ -1,39 +1,57 @@
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.7.6"
ARG VERSION=5.7.6
LABEL version="5.9.24"
RUN apt-get update && apt-get install -y --no-install-recommends \
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 \
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 \
&& rm -rf /var/lib/apt/lists/*
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
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 useradd -m -s /bin/bash triangles
# 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
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.7.6
image: cryptographic-triangles/trianglesd:5.9.24
container_name: trianglesd
restart: unless-stopped
ports:
@@ -25,7 +25,7 @@ modules:
- install -Dm644 org.cryptographic_triangles.TrianglesQt.metainfo.xml /app/share/metainfo/org.cryptographic_triangles.TrianglesQt.metainfo.xml
sources:
- type: file
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-linux-x64-qt
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-qt
sha256: ed220eb8d0b403f62cdac28988541fd1a27864491e233216f9c00a4c2537b4a3
dest-filename: triangles-qt-linux
- type: file
@@ -55,6 +55,6 @@ modules:
- install -Dm755 trianglesd-linux /app/bin/trianglesd
sources:
- type: file
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-linux-x64-daemon
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-daemon
sha256: 4d2ab25d61127d6aff3e6f3069556d04f4b823f8849e97629c12871ad4779517
dest-filename: trianglesd-linux
+1 -1
View File
@@ -4,7 +4,7 @@
# Install build tools: sudo dnf install rpm-build rpmdevtools
set -e
VERSION="5.7.6"
VERSION="5.9.24"
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
echo "Building RPM for Triangles v${VERSION}..."
+1 -1
View File
@@ -1,5 +1,5 @@
Name: triangles
Version: 5.7.6
Version: 5.9.24
Release: 1%{?dist}
Summary: Cryptographic Triangles (TRI) cryptocurrency wallet
License: MIT
+2 -2
View File
@@ -1,11 +1,11 @@
{
"version": "5.7.6",
"version": "5.9.24",
"description": "Cryptographic Triangles (TRI) cryptocurrency wallet with PoS staking and encrypted messaging",
"homepage": "https://cryptographic-triangles.org",
"license": "MIT",
"architecture": {
"64bit": {
"url": "https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-5.7.6-win-x64.zip",
"url": "https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-5.9.24-win-x64.zip",
"hash": "6f002a669a7e92aaf3d8dd7b1ae80f06a086c99a15ca05cf107665009ffc06b7"
}
},
@@ -1,5 +1,5 @@
PackageIdentifier: CryptographicTriangles.TrianglesQt
PackageVersion: 5.7.6
PackageVersion: 5.9.24
PackageLocale: en-US
Publisher: Cryptographic Triangles
PublisherUrl: https://cryptographic-triangles.org
@@ -27,7 +27,7 @@ Installers:
- RelativeFilePath: triangles-qt.exe
PortableCommandAlias: triangles-qt
ArchiveBinariesDependOnPath: true
InstallerUrl: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-5.7.6-win-x64.zip
InstallerUrl: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-5.9.24-win-x64.zip
InstallerSha256: 6F002A669A7E92AAF3D8DD7B1AE80F06A086C99A15CA05CF107665009FFC06B7
ManifestType: singleton
ManifestVersion: 1.6.0
+106
View File
@@ -0,0 +1,106 @@
#!/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
+391
View File
@@ -0,0 +1,391 @@
#!/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())
+5 -5
View File
@@ -1,6 +1,6 @@
name: triangles
base: core22
version: '5.7.6'
version: '5.9.24'
summary: Cryptographic Triangles (TRI) cryptocurrency wallet
description: |
Privacy-focused cryptocurrency featuring Proof-of-Stake consensus,
@@ -51,10 +51,10 @@ apps:
parts:
triangles:
plugin: dump
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-linux-x64-qt
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-qt
source-type: file
organize:
Cryptographic-Triangles-v5.7.6-linux-x64-qt: bin/triangles-qt
Cryptographic-Triangles-v5.9.24-linux-x64-qt: bin/triangles-qt
stage-packages:
- libqt5widgets5
- libqt5gui5
@@ -73,10 +73,10 @@ parts:
trianglesd:
plugin: dump
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.7.6/Cryptographic-Triangles-v5.7.6-linux-x64-daemon
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-daemon
source-type: file
organize:
Cryptographic-Triangles-v5.7.6-linux-x64-daemon: bin/trianglesd
Cryptographic-Triangles-v5.9.24-linux-x64-daemon: bin/trianglesd
desktop-entry:
plugin: dump
+28
View File
@@ -40,6 +40,7 @@ target_include_directories(json_compat INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/js
set(CORE_SOURCES
addrman.cpp
bootstrap.cpp
checkpointpublisher.cpp
checkpoints.cpp
crypter.cpp
hdwallet.cpp
@@ -483,6 +484,9 @@ if(BUILD_TESTS)
file(GLOB TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/test/*.cpp")
# Exclude miner_tests.cpp (never ported from Bitcoin)
list(FILTER TEST_SOURCES EXCLUDE REGEX "miner_tests\\.cpp$")
# Exclude the standalone chaindb test driver — it gets its own target
# because it needs to run without the TestingSetup global fixture.
list(FILTER TEST_SOURCES EXCLUDE REGEX "chaindb_equivalence_tests_main\\.cpp$")
add_executable(test_triangles
${TEST_SOURCES}
@@ -507,4 +511,28 @@ 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)
endif()
+7 -130
View File
@@ -460,114 +460,6 @@ 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
@@ -684,34 +576,19 @@ bool DownloadBootstrap(const std::string& host,
{
bool gotBlockFile = false;
// Try downloading bootstrap.tar.gz first
// Bootstrap server is on clearnet — bypass Tor proxy for DNS + HTTP
// 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.
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) {
// Fallback: try filelist.txt + individual file downloads
// Try filelist.txt — should contain only utxo-snapshot.bin (v2).
std::string fallbackError;
std::vector<std::string> files;
if (!FetchFileList(host, files, fallbackError, noProxy)) {
if (!tarDownloaded)
strError = strError + " (fallback also failed: " + fallbackError + ")";
else
strError = "Extraction failed: " + strError + " (fallback also failed: " + fallbackError + ")";
strError = "filelist.txt unavailable: " + fallbackError;
return false;
}
+473
View File
@@ -0,0 +1,473 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
// Signed Checkpoint Publisher (Triangles v5.9.24) — implementation.
//
// See checkpointpublisher.h for the design. This file holds:
// - The in-memory signed-checkpoint cache (a CCriticalSection-guarded
// std::map keyed by height; values are block hashes)
// - The canonical serialization used by both producer and consumer
// - The JSON parsing/building helpers (small subset, no third-party deps)
// - The trusted signers list (mirrors IsTrustedSnapshotSigner)
#include "checkpointpublisher.h"
#include <algorithm>
#include <cstdio>
#include <map>
#include <set>
#include <sstream>
#include <vector>
#include "sync.h"
#include "util.h"
#include "base58.h"
#include "key.h"
#include "serialize.h"
#include "net.h" // for CCriticalSection
#include "main.h" // for strMessageMagic
#include "bootstrap.h" // for Bootstrap::DownloadFile
namespace Checkpoints {
// ============================================================================
// Trusted signers
// ============================================================================
//
// Mirrors Bootstrap::TRUSTED_SNAPSHOT_SIGNERS but kept SEPARATE so the two
// lists can be managed independently. The default trust list contains the
// project operator's address. Operators can extend via a future -trustedcheckpointsigner
// conf option (not yet implemented — see Phase 2 in checkpointpublisher.h).
static const char* TRUSTED_CHECKPOINT_SIGNERS[] = {
"TG8f76yktTxDrT7JJymY3wVAusXiD3fVvX", // Sami's wallet (DNS2 default)
};
static const size_t NUM_TRUSTED_CHECKPOINT_SIGNERS =
sizeof(TRUSTED_CHECKPOINT_SIGNERS) / sizeof(TRUSTED_CHECKPOINT_SIGNERS[0]);
bool IsTrustedCheckpointSigner(const std::string& addr)
{
for (size_t i = 0; i < NUM_TRUSTED_CHECKPOINT_SIGNERS; ++i) {
if (addr == TRUSTED_CHECKPOINT_SIGNERS[i]) return true;
}
return false;
}
// ============================================================================
// In-memory cache of loaded signed checkpoints
// ============================================================================
//
// Guarded by a single CCriticalSection. The cache is small (a few thousand
// entries max — operator publishes one every N=5000 blocks, so for a 2.2M
// chain that's ~440 entries per active signer). Lookup is O(log n).
static CCriticalSection cs_signedCheckpoints;
static std::map<int, std::string> mapSignedCheckpoints;
bool IsKnownSignedCheckpoint(int nHeight, const std::string& hashHex)
{
LOCK(cs_signedCheckpoints);
auto it = mapSignedCheckpoints.find(nHeight);
if (it == mapSignedCheckpoints.end()) return false;
// case-insensitive compare — JSON parsers sometimes downcase hex
if (it->second.size() != hashHex.size()) return false;
for (size_t i = 0; i < it->second.size(); i++) {
if (std::tolower(static_cast<unsigned char>(it->second[i])) !=
std::tolower(static_cast<unsigned char>(hashHex[i]))) {
return false;
}
}
return true;
}
void AddSignedCheckpoints(const std::vector<SignedCheckpoint>& entries)
{
LOCK(cs_signedCheckpoints);
for (const auto& e : entries) {
// Don't overwrite compiled-in mapCheckpoints — that gate runs FIRST
// in AcceptBlock. The signed set is a SUPPLEMENT, not a replacement.
mapSignedCheckpoints[e.nHeight] = e.hashHex;
}
printf("Checkpoints: added %lu signed-remote checkpoints to cache\n", (unsigned long)entries.size());
}
void ClearSignedCheckpoints()
{
LOCK(cs_signedCheckpoints);
mapSignedCheckpoints.clear();
}
// ============================================================================
// Canonical serialization — producer + consumer MUST agree on this byte sequence
// ============================================================================
//
// Format: "<height1>:<hash1>:<ts1>;<height2>:<hash2>:<ts2>;..."
//
// Properties:
// - Entries in DESCENDING order (tip first)
// - Lowercase hex, no 0x prefix, no leading zeros
// - Timestamps are unix seconds, decimal
// - Field separator ':' — guaranteed not to appear in hex
// - Entry separator ';' — guaranteed not to appear in either
// - Trailing newline is NOT part of the signed payload (producers MUST NOT
// add one to the message before signing; consumers MUST NOT trim it off
// the fetched JSON's message field before verifying)
//
// This function is PURE — no I/O, no globals. Tested in checkpoint_tests.cpp.
std::string SerializeEntriesForSigning(const std::vector<SignedCheckpoint>& entries)
{
std::string out;
for (size_t i = 0; i < entries.size(); i++) {
if (i > 0) out += ";";
out += std::to_string(entries[i].nHeight);
out += ":";
out += entries[i].hashHex;
out += ":";
out += std::to_string(entries[i].nTimestamp);
}
return out;
}
// ============================================================================
// Producer — build the JSON document
// ============================================================================
//
// This is intentionally a thin wrapper: the wallet signing happens in the
// caller (rpcwallet.cpp / daemon loop), which has the unlocked key. Here we
// just escape + format.
bool BuildSignedCheckpointsJson(
const std::vector<SignedCheckpoint>& entries,
const std::string& signingAddress,
const std::string& signatureBase64,
const std::string& message,
std::string& outJson,
std::string& strError)
{
if (entries.empty()) {
strError = "BuildSignedCheckpointsJson: entries vector is empty";
return false;
}
if (signingAddress.empty()) {
strError = "BuildSignedCheckpointsJson: signingAddress is empty";
return false;
}
if (signatureBase64.empty()) {
strError = "BuildSignedCheckpointsJson: signature is empty";
return false;
}
// Sort entries DESCENDING by height — canonical form. Producers and
// consumers both depend on this so verification is deterministic.
std::vector<SignedCheckpoint> sorted = entries;
std::sort(sorted.begin(), sorted.end(),
[](const SignedCheckpoint& a, const SignedCheckpoint& b) {
return a.nHeight > b.nHeight;
});
// Build JSON manually — no third-party deps. Format is intentionally
// simple (no nested objects beyond the entries array).
std::ostringstream oss;
oss << "{\n";
oss << " \"format_version\": 1,\n";
oss << " \"signing_address\": \"" << signingAddress << "\",\n";
oss << " \"message\": \"" << message << "\",\n";
oss << " \"signature\": \"" << signatureBase64 << "\",\n";
oss << " \"entries\": [\n";
for (size_t i = 0; i < sorted.size(); i++) {
oss << " {\"height\": " << sorted[i].nHeight
<< ", \"hash\": \"" << sorted[i].hashHex << "\""
<< ", \"timestamp\": " << sorted[i].nTimestamp << "}";
if (i + 1 < sorted.size()) oss << ",";
oss << "\n";
}
oss << " ]\n";
oss << "}\n";
outJson = oss.str();
return true;
}
// ============================================================================
// Consumer — verify a JSON document
// ============================================================================
// Small JSON helper — extract a top-level array of objects from the
// "entries" field. We don't need full JSON parsing; the format is fixed.
static std::vector<std::string> ExtractJsonObjectArray(
const std::string& json, const std::string& field)
{
std::vector<std::string> objs;
std::string key = "\"" + field + "\"";
size_t pos = json.find(key);
if (pos == std::string::npos) return objs;
pos += key.size();
while (pos < json.size() && (json[pos] == ' ' || json[pos] == ':' ||
json[pos] == '\t' || json[pos] == '\n' || json[pos] == '\r'))
pos++;
if (pos >= json.size() || json[pos] != '[') return objs;
pos++; // past '['
while (pos < json.size()) {
while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' ||
json[pos] == '\n' || json[pos] == '\r' || json[pos] == ','))
pos++;
if (pos >= json.size() || json[pos] == ']') break;
if (json[pos] != '{') break;
// Find matching closing brace (shallow — no nested objects in entries)
int depth = 1;
size_t start = pos;
pos++;
while (pos < json.size() && depth > 0) {
if (json[pos] == '{') depth++;
else if (json[pos] == '}') depth--;
pos++;
}
if (depth != 0) break;
objs.push_back(json.substr(start, pos - start));
}
return objs;
}
// Extract an integer field from an entry object like:
// {"height": 12345, "hash": "...", "timestamp": 1700000000}
static int ExtractJsonInt(const std::string& obj, const std::string& field)
{
std::string key = "\"" + field + "\"";
size_t pos = obj.find(key);
if (pos == std::string::npos) return 0;
pos += key.size();
while (pos < obj.size() && (obj[pos] == ' ' || obj[pos] == ':' ||
obj[pos] == '\t')) pos++;
// Parse a non-negative integer
int n = 0;
bool foundAny = false;
while (pos < obj.size() && obj[pos] >= '0' && obj[pos] <= '9') {
n = n * 10 + (obj[pos] - '0');
pos++;
foundAny = true;
}
if (!foundAny) return 0;
return n;
}
// Extract a string field from a small JSON object — mirrors ExtractJsonString
// in bootstrap.cpp. Duplicated here to keep checkpointpublisher.cpp standalone
// (no link dependency on bootstrap.cpp internals).
static std::string ExtractJsonString(const std::string& obj, const std::string& field)
{
std::string key = "\"" + field + "\"";
size_t pos = obj.find(key);
if (pos == std::string::npos) return "";
pos += key.size();
while (pos < obj.size() && (obj[pos] == ' ' || obj[pos] == ':' ||
obj[pos] == '\t')) pos++;
if (pos >= obj.size() || obj[pos] != '\"') return "";
pos++;
size_t end = obj.find('\"', pos);
if (end == std::string::npos) return "";
return obj.substr(pos, end - pos);
}
bool VerifySignedCheckpoints(
const std::string& jsonText,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError)
{
outEntries.clear();
outSigningAddress.clear();
// 1. Extract signing fields
outSigningAddress = ExtractJsonString(jsonText, "signing_address");
std::string signature = ExtractJsonString(jsonText, "signature");
std::string message = ExtractJsonString(jsonText, "message");
if (outSigningAddress.empty() || signature.empty() || message.empty()) {
strError = "signed-checkpoints JSON missing required top-level fields "
"(signing_address/signature/message)";
return false;
}
// 2. Verify signer is trusted
if (!IsTrustedCheckpointSigner(outSigningAddress)) {
strError = "signing_address " + outSigningAddress +
" is not in the trusted checkpoint signers list";
return false;
}
// 3. Verify the address is well-formed (catches typos early)
CTrianglesAddress addr(outSigningAddress);
if (!addr.IsValid()) {
strError = "signing_address " + outSigningAddress + " is not a valid Triangles address";
return false;
}
CKeyID keyID;
if (!addr.GetKeyID(keyID)) {
strError = "signing_address " + outSigningAddress + " does not refer to a key";
return false;
}
// 4. Decode and verify the signature (same code path as verifymessage RPC)
bool fInvalid = false;
std::vector<unsigned char> vchSig = DecodeBase64(signature.c_str(), &fInvalid);
if (fInvalid) {
strError = "signed-checkpoints signature is not valid base64";
return false;
}
CDataStream ss(SER_GETHASH, 0);
ss << strMessageMagic;
ss << message;
CKey key;
if (!key.SetCompactSignature(Hash(ss.begin(), ss.end()), vchSig)) {
strError = "signed-checkpoints signature failed to recover (bad sig or "
"message tampered)";
return false;
}
if (key.GetPubKey().GetID() != keyID) {
strError = "signed-checkpoints signature recovered to a key that does "
"not match the claimed signer address";
return false;
}
// 5. Extract entries and verify they match the signed message
std::vector<std::string> entryObjs = ExtractJsonObjectArray(jsonText, "entries");
if (entryObjs.empty()) {
strError = "signed-checkpoints JSON has no entries array or entries is empty";
return false;
}
outEntries.reserve(entryObjs.size());
for (const auto& obj : entryObjs) {
SignedCheckpoint e;
e.nHeight = ExtractJsonInt(obj, "height");
e.hashHex = ExtractJsonString(obj, "hash");
e.nTimestamp = ExtractJsonInt(obj, "timestamp");
if (e.nHeight <= 0 || e.hashHex.empty() || e.nTimestamp <= 0) {
strError = "malformed entry (height/hash/timestamp invalid): " + obj;
return false;
}
// hashHex sanity: must be exactly 64 lowercase hex chars
if (e.hashHex.size() != 64) {
strError = "entry hash at height " + std::to_string(e.nHeight) +
" is not 64 chars: " + e.hashHex;
return false;
}
for (char c : e.hashHex) {
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
strError = "entry hash at height " + std::to_string(e.nHeight) +
" contains non-lowercase-hex character";
return false;
}
}
outEntries.push_back(e);
}
// 6. Verify the signed message exactly matches the canonical serialization
// of the entries. This is the cross-check that proves the entries
// weren't tampered with after signing.
std::string expectedMessage = SerializeEntriesForSigning(outEntries);
if (expectedMessage != message) {
strError = "signed-checkpoints message does not match canonical entry "
"serialization — entries were tampered with after signing";
return false;
}
printf("Checkpoints: signed-remote verified — %lu entries signed by %s\n",
(unsigned long)outEntries.size(), outSigningAddress.c_str());
return true;
}
// ============================================================================
// Network fetch — keep it simple. The signed-checkpoints doc is tiny (~5 KB
// for a year of entries at 5000-block intervals), so a plain HTTP GET is
// fine. We DO NOT go through Tor for this fetch: the bootstrap server is
// already a known clearnet endpoint (same model as the existing UTXO
// snapshot download, which uses ConnectDirectTCP per bootstrap.cpp).
// ============================================================================
bool LoadSignedCheckpoints(
const std::string& host,
const std::string& onDiskPath,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError)
{
outEntries.clear();
outSigningAddress.clear();
std::string jsonText;
// Path A: use on-disk copy if it exists (lets the daemon start even when
// the bootstrap server is unreachable, as long as we have a recent copy).
if (!onDiskPath.empty()) {
FILE* f = fopen(onDiskPath.c_str(), "rb");
if (f) {
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz > 0 && sz < 10 * 1024 * 1024) { // 10 MB cap — sanity
jsonText.resize(sz);
size_t got = fread(&jsonText[0], 1, sz, f);
jsonText.resize(got);
}
fclose(f);
if (!jsonText.empty()) {
printf("Checkpoints: loaded on-disk signed-checkpoints from %s (%lu bytes)\n",
onDiskPath.c_str(), (unsigned long)jsonText.size());
}
}
}
// Path B: fetch from bootstrap server. We always try this — if it
// succeeds, prefer the freshest doc over the on-disk copy.
if (host.empty()) {
strError = "LoadSignedCheckpoints: no host provided and no on-disk copy found";
return !jsonText.empty(); // if we have disk content, still try to verify it
}
// Use Bootstrap::DownloadFile — already handles clearnet HTTPS, timeouts,
// and redirects. We do NOT proxy through Tor.
if (Bootstrap::DownloadFile(host, "signed-checkpoints.json",
std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp",
nullptr, strError,
/*noProxy=*/true)) {
std::filesystem::path tmp = std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp";
FILE* f = fopen(tmp.string().c_str(), "rb");
if (f) {
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz > 0 && sz < 10 * 1024 * 1024) {
jsonText.resize(sz);
size_t got = fread(&jsonText[0], 1, sz, f);
jsonText.resize(got);
}
fclose(f);
}
std::error_code ec;
std::filesystem::remove(tmp, ec);
if (!jsonText.empty()) {
printf("Checkpoints: fetched fresh signed-checkpoints from %s (%lu bytes)\n",
host.c_str(), (unsigned long)jsonText.size());
// Persist to disk for next startup (only if onDiskPath was given)
if (!onDiskPath.empty()) {
FILE* f2 = fopen(onDiskPath.c_str(), "wb");
if (f2) {
fwrite(jsonText.data(), 1, (unsigned long)jsonText.size(), f2);
fclose(f2);
printf("Checkpoints: persisted signed-checkpoints to %s\n", onDiskPath.c_str());
}
}
}
} else {
printf("Checkpoints: WARNING — fetch from %s failed (%s)",
host.c_str(), strError.c_str());
if (jsonText.empty()) {
strError = "could not fetch signed-checkpoints and no on-disk copy: " + strError;
return false;
}
printf(" — falling back to on-disk copy\n");
strError.clear();
}
// Verify whatever we ended up with
return VerifySignedCheckpoints(jsonText, outEntries, outSigningAddress, strError);
}
} // namespace Checkpoints
+164
View File
@@ -0,0 +1,164 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
// Signed Checkpoint Publisher (Triangles v5.9.24)
//
// Background
// ----------
// Triangles' existing CSyncCheckpoint (src/checkpoints.cpp) is Bitcoin-era
// P2P-broadcast code that uses a HARDCODED master pubkey. That model does
// not match how the project actually operates today (one operator with
// multiple keys, snapshot publishing on the bootstrap server, no master
// hierarchy). Instead we layer a *new* signed-checkpoint scheme on top of
// the bootstrap server, using the same compact-message primitive the UTXO
// snapshot trust model already uses (see src/bootstrap.cpp:IsTrustedSnapshotSigner).
//
// Trust model
// -----------
// - A signed checkpoint document is a small JSON file hosted at
// https://bootstrap.cryptographic-triangles.org/signed-checkpoints.json
// - It contains a list of (height, block_hash, unix_timestamp) entries,
// followed by a single signing_address + signature covering the canonical
// serialization of the entry list.
// - The signing_address must appear in the trusted signers list
// (Checkpoints::IsTrustedCheckpointSigner, see checkpoints.cpp). The
// default trust list is the same as IsTrustedSnapshotSigner but kept
// separate so they can be managed independently.
// - Verification uses the existing CKey::SignCompact / SetCompactSignature
// code path through the wallet's verifymessage-style flow — no new
// cryptography is introduced.
//
// Producer
// --------
// - The daemon operator runs `triangles-cli publishcheckpoint [interval]`
// which builds the entry list from pindexBest, signs with the wallet's
// default key, and writes the JSON document to a path the operator
// uploads to the bootstrap server (or a cron job uploads automatically
// when -autopublishcheckpoint is set).
// - Default interval = every 5000 blocks; can be set to every N.
// - The first entry is always the chain tip at publish time.
//
// Consumer
// --------
// - On startup, the daemon can call
// Checkpoints::LoadSignedCheckpoints(host, dataDir, strError)
// which fetches, verifies, and merges the trusted entries into the
// compiled-in mapCheckpoints (lower priority — compiled-in wins on
// conflict to defend against remote-rollback).
// - Checkpoints::IsKnownSignedCheckpoint(height, hash) returns true if
// either compiled-in OR signed-remote knows about (height, hash).
//
// Relationship to existing code
// -----------------------------
// - mapCheckpoints in src/checkpoints.cpp is UNCHANGED — the compiled-in
// list is still the primary trust anchor.
// - Signed checkpoints EXTEND the trust anchor with operator-published
// ones, useful when the operator wants to publish a checkpoint at
// height 2,210,000 without waiting for a code release.
// - mapSnapshotHashes is unaffected.
#ifndef TRIANGLES_CHECKPOINT_PUBLISHER_H
#define TRIANGLES_CHECKPOINT_PUBLISHER_H
#include <string>
#include <vector>
#include <cstdint>
namespace Checkpoints {
// One signed checkpoint entry. Compact, serializable, no JSON inside the
// struct — JSON wrapping happens in the publisher.
struct SignedCheckpoint {
int nHeight; // block height
std::string hashHex; // block hash, lowercase hex, NO 0x prefix, NO leading zeros
int64_t nTimestamp; // unix seconds when published (signed over)
};
// Result of a publish or verify operation. Used for human-readable errors
// and structured logging.
struct SignedCheckpointResult {
bool ok; // overall success
std::string error; // populated if !ok
int nEntriesWritten; // for publish: how many entries went into the JSON
int nEntriesVerified; // for verify: how many entries passed signature check
};
// Default URL for the bootstrap server's signed-checkpoints document.
static const char* SIGNED_CHECKPOINTS_URL =
"https://bootstrap.cryptographic-triangles.org/signed-checkpoints.json";
// Default local output path the daemon writes to on publish.
static const char* SIGNED_CHECKPOINTS_DEFAULT_OUT =
"/var/www/triangles-bootstrap/signed-checkpoints.json";
// ---- Producer ----
// Build the JSON document for the entries [heights[0], heights[1], ...]
// (in DESCENDING order — tip first) using the wallet's default key.
// Returns true on success; outJson/outputPath written. Wallet must be
// unlocked (signmessage requires it).
//
// This is the in-process builder used by both:
// - The triangles-cli `publishcheckpoint` RPC command
// - The daemon's auto-publish loop when -autopublishcheckpoint is set
bool BuildSignedCheckpointsJson(
const std::vector<SignedCheckpoint>& entries,
const std::string& signingAddress,
const std::string& signatureBase64,
const std::string& message,
std::string& outJson,
std::string& strError);
// Canonical (deterministic) serialization of the entry list. The signature
// is over this exact byte sequence — both producer and consumer MUST use
// this function so verification is reproducible across platforms.
std::string SerializeEntriesForSigning(const std::vector<SignedCheckpoint>& entries);
// ---- Consumer ----
// Fetch the signed-checkpoints document from the bootstrap server, parse
// it, verify the signature, and return the verified entries. Does NOT
// merge into mapCheckpoints — caller decides what to do with the entries.
//
// onDiskPath: optional. If non-empty and the file already exists locally,
// skip the network fetch and verify the on-disk copy. This makes startup
// robust against bootstrap-server outages.
bool LoadSignedCheckpoints(
const std::string& host,
const std::string& onDiskPath,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError);
// Verify the signature on a parsed JSON document. Pure function — no
// network, no filesystem.
bool VerifySignedCheckpoints(
const std::string& jsonText,
std::vector<SignedCheckpoint>& outEntries,
std::string& outSigningAddress,
std::string& strError);
// Is the given signing address in the trusted signers list? Mirrors
// Bootstrap::IsTrustedSnapshotSigner but kept separate for independent
// governance.
bool IsTrustedCheckpointSigner(const std::string& addr);
// ---- Merged lookup ----
// Is (height, hash) known to either the compiled-in OR the
// signed-remote set? This is what AcceptBlock / fork-detection should call.
bool IsKnownSignedCheckpoint(int nHeight, const std::string& hashHex);
// Inject loaded entries into the in-memory signed-checkpoint cache. Called
// by init.cpp after LoadSignedCheckpoints returns successfully. Subsequent
// IsKnownSignedCheckpoint() calls will return true for any (height, hash)
// in the loaded set.
void AddSignedCheckpoints(const std::vector<SignedCheckpoint>& entries);
// Clear the in-memory cache (used at reorg boundaries and in tests).
void ClearSignedCheckpoints();
} // namespace Checkpoints
#endif // TRIANGLES_CHECKPOINT_PUBLISHER_H
+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 5
#define CLIENT_VERSION_MINOR 9
#define CLIENT_VERSION_REVISION 20
#define CLIENT_VERSION_REVISION 24
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
+16
View File
@@ -506,6 +506,7 @@ 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"
@@ -780,6 +781,21 @@ 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))
+204 -39
View File
@@ -11,6 +11,7 @@
#include "addrman.h"
#include "ui_interface.h"
#include "onionseed.h"
#include "tor/onion_v3.h"
#include <openssl/ssl.h>
#include <openssl/err.h>
@@ -564,6 +565,14 @@ 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());
@@ -581,6 +590,40 @@ 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()
{
@@ -1403,6 +1446,39 @@ void ThreadOnionSeed(void* parg)
static const char *(*strOnionSeed)[1] = fTestNet ? strTestNetOnionSeed : strMainNetOnionSeed;
int found = 0;
// Defense-in-depth (2026-06-22): Validate every hardcoded seed against the
// v3 onion checksum BEFORE we hand it to Tor. The btb6/gtb6 incident
// (4,842 "No more HSDir" errors over a 12h from-zero sync test) was caused
// by a single-character corruption that Tor rejected with a cryptic
// "ed25519 validation failed" warning. Catching it here gives the operator
// a clear, actionable error at startup with no wasted network/CPU.
// See references/onion-corruption-ci-defense.md (CI Layers 2-3) for the
// static-analysis side of this defense.
{
int nInvalid = 0;
int nTotal = 0;
std::string strFirstBad;
for (unsigned int si = 0; strOnionSeed[si][0] != nullptr; si++) {
nTotal++;
if (!CTorV3Service::ValidateOnionAddress(strOnionSeed[si][0])) {
if (strFirstBad.empty()) strFirstBad = strOnionSeed[si][0];
nInvalid++;
}
}
if (nInvalid > 0) {
std::string strErr = strprintf(
"ThreadOnionSeed() : %d of %d hardcoded .onion seed(s) failed v3 "
"checksum validation. First bad address: %s. "
"This is the btb6/gtb6 class of bug (see references/onion-corruption-ci-defense.md). "
"Fix src/onionseed.h before starting the daemon — Tor would "
"have wasted hours producing cryptic 'ed25519 validation failed' "
"warnings otherwise.",
nInvalid, nTotal, strFirstBad.c_str());
printf("ERROR: %s\n", strErr.c_str());
throw runtime_error(strErr);
}
}
for (unsigned int seed_idx = 0; strOnionSeed[seed_idx][0] != nullptr; seed_idx++) {
CNetAddr parsed;
if (!parsed.SetSpecial(strOnionSeed[seed_idx][0]))
@@ -1700,67 +1776,109 @@ bool ThreadHTTPSeedFetch2(void* parg)
return false;
}
std::string headers = response.substr(0, headerEnd);
std::string body = response.substr(headerEnd + 4);
// Parse one address per line: "address:port" or just "address"
int found = 0;
std::istringstream lines(body);
std::string line;
while (std::getline(lines, line))
// 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.
{
if (fShutdown)
return false;
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);
}
}
// 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 (fDebug)
printf("HTTPS seed fetch: %d body bytes to parse\n", (int)body.size());
if (line.empty() || line[0] == '#')
continue;
// Tolerant parse: accept one-per-line OR several addresses on one line
// (whitespace / comma / semicolon separated), and ignore inline '#' comments.
// v5.9.22: the splitting logic is now a pure function in netbase.cpp so
// we can unit-test every line format. The CNetAddr/CService/addrman
// validation stays here because it touches globals.
int found = 0;
int skipped = 0;
auto addSeed = [&](std::string addrStr) -> void {
while (!addrStr.empty() && (addrStr.back()=='\r' || addrStr.back()==' ' || addrStr.back()=='\t'))
addrStr.pop_back();
while (!addrStr.empty() && (addrStr.front()==' ' || addrStr.front()=='\t'))
addrStr.erase(addrStr.begin());
if (addrStr.empty())
return;
// 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;
return; // Tor-native: skip non-.onion addresses
}
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));
CService service(addrStr, port);
if (service.IsValid()) {
CAddress addr(service);
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);
}
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)
{
if (fShutdown)
return false;
addSeed(tok);
}
printf("%d addresses found from HTTPS seed list (%s)\n", found, seedHost.c_str());
return found > 0;
if (found == 0) {
printf("HTTPS seed fetch: parsed response contained zero valid addresses from %s\n", seedHost.c_str());
return false;
}
return true;
} catch (std::exception& e) {
printf("HTTPS seed fetch failed: %s\n", e.what());
@@ -1894,10 +2012,57 @@ 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]++;
+17
View File
@@ -261,6 +261,15 @@ 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
@@ -271,6 +280,8 @@ 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
@@ -325,6 +336,8 @@ public:
nPingUsecTime = 0;
nPingRetryCount = 0;
fGetAddr = false;
nLastGetaddrTrigger = 0;
nSignedPeerBonus = 0;
nMisbehavior = 0;
hashCheckpointKnown = 0;
setInventoryKnown.max_size(SendBufferSize() / 1000);
@@ -549,6 +562,10 @@ 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
+179
View File
@@ -12,6 +12,12 @@
#include <sys/fcntl.h>
#endif
#include <cstdlib>
#include <cctype>
#include <cerrno>
#include <limits>
#include <sstream>
#include "strlcpy.h"
using namespace std;
@@ -21,6 +27,13 @@ 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 };
@@ -223,6 +236,24 @@ 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)
{
@@ -1287,3 +1318,151 @@ void CService::SetPort(unsigned short portIn)
{
port = portIn;
}
// ═══════════════════════════════════════════════════════════════════════════════
// v5.9.22 hardening: pure helper functions for the HTTPS seed-list path.
// See netbase.h for the contract. These are intentionally free of SSL/Tor
// dependencies so they can be unit-tested in isolation.
// ═══════════════════════════════════════════════════════════════════════════════
bool IsValidSocksNegotiationTimeout(int nMs)
{
// Range bounds match the documented -torconnecttimeout contract. 5000ms
// is the lower edge that still tolerates a slow SOCKS handshake over a
// congested link; 180000ms (3 min) is the upper edge to prevent a stuck
// thread from holding an outbound connection slot indefinitely. These
// constants are duplicated in src/init.cpp's HelpMessage text and the
// test suite — keep all three in sync.
return nMs >= 5000 && nMs <= 180000;
}
int DechunkTransferEncoding(const std::string& body, std::string& decoded)
{
decoded.clear();
if (body.empty())
return DECHUNK_EMPTY;
// HTTP chunked framing requires every chunk-size line to be terminated
// by CRLF. We walk the body one chunk at a time and validate each piece.
// The previous implementation silently dropped malformed chunks and
// treated them as the last-chunk marker, which lost the entire seed list
// for any non-conforming server. This version returns an explicit error
// code for each failure mode.
size_t pos = 0;
const size_t n = body.size();
bool sawLastChunk = false;
while (pos < n) {
// Find end of chunk-size line. Required: CRLF.
size_t eol = body.find("\r\n", pos);
if (eol == std::string::npos)
return DECHUNK_NO_CHUNK_TERMINATOR;
std::string sizeLine = body.substr(pos, eol - pos);
pos = eol + 2; // consume CRLF
// Strip chunk extensions per RFC 7230 §4.1.1: ";name[=value]" after
// the hex size. Extensions are part of the framing protocol, not
// data, so we drop them here.
size_t semi = sizeLine.find(';');
std::string hexSize = (semi == std::string::npos) ? sizeLine : sizeLine.substr(0, semi);
// Strict hex validation: every character must be [0-9A-Fa-f]. Empty
// size lines (e.g. a stray CRLF) are rejected as malformed, not
// silently treated as 0. strtoul alone would also accept leading
// whitespace, '+', and '-' which we don't want.
if (hexSize.empty())
return DECHUNK_INVALID_HEX;
for (size_t i = 0; i < hexSize.size(); ++i) {
if (!isxdigit(static_cast<unsigned char>(hexSize[i])))
return DECHUNK_INVALID_HEX;
}
// strtoul returns ULONG_MAX on overflow. We also need to guard
// against chunks larger than the remaining input, which the old
// code clamped silently. Use strtoull so we can detect overflow
// without truncation surprises on 32-bit builds.
errno = 0;
char* endp = nullptr;
unsigned long long chunkSize = strtoull(hexSize.c_str(), &endp, 16);
if (errno == ERANGE || chunkSize > std::numeric_limits<size_t>::max())
return DECHUNK_INVALID_HEX;
if (endp == hexSize.c_str())
return DECHUNK_INVALID_HEX;
if (chunkSize == 0) {
// Last-chunk: payload is empty, trailer part (which we ignore)
// follows and is terminated by a final CRLF on its own line.
sawLastChunk = true;
break;
}
// Bounds check before reading the chunk data. Catching this
// explicitly (rather than clamping) is what lets callers
// distinguish "truncated network read" from "server sent us junk".
if (chunkSize > n - pos)
return DECHUNK_OVERSIZE_CHUNK;
decoded.append(body, pos, static_cast<size_t>(chunkSize));
pos += static_cast<size_t>(chunkSize);
// Per RFC 7230 each chunk's data must be followed by a CRLF. We
// tolerate the final chunk missing its trailing CRLF (some clients
// do this when the connection is being closed anyway), but for any
// non-final chunk a missing CRLF is a hard framing error.
if (pos + 1 < n && body[pos] == '\r' && body[pos + 1] == '\n') {
pos += 2;
} else if (pos >= n) {
// End of input immediately after chunk data — no CRLF, but
// nothing left to misframe. Reject to be strict.
return DECHUNK_MISSING_DATA_CRLF;
} else {
return DECHUNK_MISSING_DATA_CRLF;
}
}
if (!sawLastChunk) {
// Body ended without a last-chunk marker. Treat as malformed
// rather than accepting a truncated body.
return DECHUNK_NO_CHUNK_TERMINATOR;
}
return DECHUNK_OK;
}
std::vector<std::string> ParseSeedListBody(const std::string& body)
{
std::vector<std::string> out;
std::istringstream lines(body);
std::string line;
while (std::getline(lines, line)) {
// Strip inline '#' comments. Per common seed-list convention, the
// first '#' to end-of-line is comment.
size_t hashPos = line.find('#');
if (hashPos != std::string::npos)
line = line.substr(0, hashPos);
// Split on whitespace, comma, or semicolon so multiple addresses
// on one line are all captured. CR/LF are already consumed by
// std::getline but a trailing CR (LF-only line endings) is trimmed
// implicitly by skipping it as a separator below.
size_t start = 0;
while (start <= line.size()) {
size_t sep = line.find_first_of(" \t,;", start);
std::string tok = (sep == std::string::npos)
? line.substr(start)
: line.substr(start, sep - start);
// Trim CR and any leftover whitespace from the token. The
// 'sep' loop above eats spaces/tabs but a bare CR survives.
while (!tok.empty() && (tok.back() == '\r' || tok.back() == ' ' || tok.back() == '\t'))
tok.pop_back();
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
tok.erase(tok.begin());
if (!tok.empty())
out.push_back(tok);
if (sep == std::string::npos) break;
start = sep + 1;
}
}
return out;
}
+70
View File
@@ -29,8 +29,78 @@ 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
{
+1 -1
View File
@@ -815,7 +815,7 @@ QWidget#line {
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</string>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1416,7 +1416,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1444,7 +1444,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1412,7 +1412,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1440,7 +1440,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1412,7 +1412,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1440,7 +1440,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1429,7 +1429,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1457,7 +1457,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1421,7 +1421,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1449,7 +1449,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1414,7 +1414,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1442,7 +1442,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1427,7 +1427,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1455,7 +1455,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1417,7 +1417,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1445,7 +1445,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1417,7 +1417,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1445,7 +1445,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1417,7 +1417,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1445,7 +1445,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1416,7 +1416,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1444,7 +1444,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1412,7 +1412,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1440,7 +1440,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1426,7 +1426,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1454,7 +1454,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1427,7 +1427,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1455,7 +1455,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1432,7 +1432,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1460,7 +1460,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1418,7 +1418,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1446,7 +1446,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1413,7 +1413,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1441,7 +1441,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1419,7 +1419,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1447,7 +1447,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
+2 -2
View File
@@ -1420,7 +1420,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://triangles.technology&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://cryptographic-triangles.org/&quot;&gt; &amp;#187; TRI home&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
@@ -1448,7 +1448,7 @@ a:active { color:#f26522; text-decoration: underline; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;a href=&quot;http://explorer.triangles.technology&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
&lt;a href=&quot;https://blocks.cryptographic-triangles.org&quot;&gt; &amp;#187; TRI block explorer&lt;/a&gt;&lt;/body&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>

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