Compare commits

...

4 Commits

Author SHA1 Message Date
Krystie 207e1ed676 Fix Tor v3 onion address checksum: SHA-256 -> SHA3-256
Build All Platforms / test-linux-unit (push) Failing after 40s
Build All Platforms / build-linux-qt (push) Failing after 40s
Build All Platforms / build-linux-daemon (push) Failing after 40s
Build All Platforms / build-windows-qt (push) Has been cancelled
Build All Platforms / build-windows-daemon (push) Has been cancelled
Build All Platforms / build-macos (push) Has been cancelled
Build All Platforms / release (push) Has been cancelled
The Tor v3 spec requires SHA3-256 (FIPS-202) for the .onion address
checksum computation, but ToStringIP() was using SHA-256 (double-hash).
This caused every reconstructed .onion address to have incorrect suffix
characters, making all outbound Tor connections fail with SOCKS5 'general
failure' - the entire network had 0 Tor peers despite working Tor instances.

Fix: Replace Hash() call with OpenSSL EVP_sha3_256() which is available
in OpenSSL 3.0+ and produces the correct FIPS-202 SHA3-256 checksum.

Tested: All 5 onion seed nodes now connect successfully.
2026-04-02 14:16:41 -07:00
sami7777 2fba88bfc5 Fix LookupHost call to use vector overload in HTTP seed fetch
LookupHost expects std::vector<CNetAddr>& but was passed a single
CNetAddr, breaking compilation on all platforms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 18:52:06 -07:00
sami7777 4563e7952b Add dynamic HTTP seed discovery, remove hardcoded seeds (v5.5.0)
Build All Platforms / build-linux-qt (push) Failing after 3h0m3s
Build All Platforms / test-linux-unit (push) Failing after 3h0m4s
Build All Platforms / build-linux-daemon (push) Failing after 21s
Build All Platforms / build-windows-qt (push) Has been cancelled
Build All Platforms / build-windows-daemon (push) Has been cancelled
Build All Platforms / build-macos (push) Has been cancelled
Build All Platforms / release (push) Has been cancelled
Replace all hardcoded seed addresses (onion, clearnet, DNS) with a
dynamic HTTP-based seed list fetched from seeds.cryptographic-triangles.org
on startup. New getseedlist RPC exposes known .onion peers from the
address manager for a collector script to publish.

Any wallet that comes online with an onion address is automatically
discovered by peers via P2P addr exchange and appears in the seed list
within minutes. No binary rebuilds needed when addresses change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 18:25:43 -07:00
sami7777 ea86ab077c Optimize wallet rescan and address indexing during IBD
Move wallet rescan to a background thread after IBD completes instead
of blocking on the main thread. Address index is now built during IBD
rather than skipped and rebuilt later. Wallet scan releases cs_wallet
lock while reading blocks from disk to improve concurrency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 18:10:00 -07:00
14 changed files with 508 additions and 159 deletions
+147
View File
@@ -0,0 +1,147 @@
# Triangles Dynamic Seed Node - Setup Guide
## Overview
Triangles v5.5.0+ uses a dynamic HTTP seed list instead of hardcoded addresses.
A collector script runs on a VPS alongside a Triangles node, periodically
querying the node for known .onion peers and publishing them to a static file.
New wallets fetch this file on startup to bootstrap peer discovery.
Once any wallet syncs and obtains its own .onion address, other nodes learn
about it via P2P address exchange. The collector picks it up automatically
on its next run. No manual intervention is needed after initial setup.
## Requirements
- Linux VPS
- Triangles daemon (`trianglesd`) running with Tor enabled
- A web server (Caddy, nginx, Apache, etc.)
- DNS control for the domain serving the seed list
- `jq` and `curl` (`apt install jq curl`)
## Step 1: DNS
Create an A record for the seed list hostname pointing to the VPS IP address.
The default hostname the wallet fetches is `seeds.cryptographic-triangles.org`.
This can be overridden per-node with the `-seedurl` flag.
## Step 2: Web Server
Create a directory for the seed file:
```bash
sudo mkdir -p /var/www/seeds
sudo chown $USER:$USER /var/www/seeds
```
Configure the web server to serve that directory on the seed list hostname.
**Caddy example** (add to Caddyfile):
```
seeds.cryptographic-triangles.org {
root * /var/www/seeds
file_server
}
```
**nginx example** (add server block):
```
server {
listen 80;
server_name seeds.cryptographic-triangles.org;
root /var/www/seeds;
}
```
Reload the web server after making changes.
## Step 3: Install the Collector Script
```bash
sudo cp contrib/seeds/collect-seeds.sh /usr/local/bin/collect-seeds.sh
sudo chmod +x /usr/local/bin/collect-seeds.sh
```
## Step 4: Configure and Test
The script communicates with `trianglesd` via JSON-RPC. It reads credentials
from environment variables. Check `triangles.conf` for `rpcuser` and `rpcpassword`.
Run it manually to verify:
```bash
export RPC_USER="your_rpc_username"
export RPC_PASSWORD="your_rpc_password"
export RPC_PORT="19112"
export OUTPUT_FILE="/var/www/seeds/seeds.txt"
/usr/local/bin/collect-seeds.sh
```
Expected output: `Updated /var/www/seeds/seeds.txt with N seeds`
The resulting file should contain one `.onion:port` entry per line:
```
# Triangles seed nodes - auto-generated 2026-04-01T12:00:00Z
exampleaddress1234567890abcdefghijklmnopqrstuvwxyz234567.onion:24112
anotheraddress1234567890abcdefghijklmnopqrstuvwxyz23456.onion:24112
```
## Step 5: Cron Job
Schedule the collector to run every 5 minutes:
```bash
crontab -e
```
Add:
```
*/5 * * * * RPC_USER="your_rpc_username" RPC_PASSWORD="your_rpc_password" OUTPUT_FILE="/var/www/seeds/seeds.txt" /usr/local/bin/collect-seeds.sh >> /var/log/triangles-seeds.log 2>&1
```
## Step 6: Verify End-to-End
From any machine:
```bash
curl http://seeds.cryptographic-triangles.org/seeds.txt
```
The response should list .onion addresses.
## Troubleshooting
**"no onion seeds found"**
The node has not yet learned any .onion peer addresses. Ensure Tor is enabled
and the node has at least one connected peer. Check with `trianglesd getpeerinfo`.
**"RPC call failed"**
Verify `trianglesd` is running and RPC credentials are correct:
```bash
curl -s --user "user:pass" --data-binary \
'{"jsonrpc":"1.0","method":"getinfo","params":[]}' \
http://127.0.0.1:19112/
```
**seeds.txt not updating**
Check the cron log: `tail /var/log/triangles-seeds.log`
## How It Works
1. The collector calls the `getseedlist` RPC, which returns all known .onion
addresses from the node's address manager
2. Results are written to a static text file served by the web server
3. On startup, Triangles wallets fetch this file and add the addresses to
their peer database
4. As wallets connect and exchange addresses via P2P, new .onion addresses
propagate across the network
5. The collector discovers newly-propagated addresses on its next run
This creates a fully automatic cycle where every online wallet with a Tor
hidden service becomes a discoverable seed node.
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# Triangles Dynamic Seed Collector
# Run via cron on a VPS that runs a Triangles node.
# Queries the local node's getseedlist RPC for known .onion peers
# and writes them to a static file served by a web server.
#
# Example cron (every 5 minutes):
# */5 * * * * /path/to/collect-seeds.sh
#
# The web server (Caddy, nginx, etc.) serves the output file at:
# http://seeds.cryptographic-triangles.org/seeds.txt
# Configuration
RPC_USER="${RPC_USER:-trianglesrpc}"
RPC_PASSWORD="${RPC_PASSWORD:-}"
RPC_PORT="${RPC_PORT:-19112}"
OUTPUT_FILE="${OUTPUT_FILE:-/var/www/seeds/seeds.txt}"
if [ -z "$RPC_PASSWORD" ]; then
echo "Error: RPC_PASSWORD not set" >&2
exit 1
fi
# Query the node for known onion seeds
RESPONSE=$(curl -s --user "${RPC_USER}:${RPC_PASSWORD}" \
--data-binary '{"jsonrpc":"1.0","id":"seedcollect","method":"getseedlist","params":[]}' \
-H 'content-type: text/plain;' \
"http://127.0.0.1:${RPC_PORT}/" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$RESPONSE" ]; then
echo "Error: RPC call failed" >&2
exit 1
fi
# Extract addresses and write to temp file, then atomically move
TMPFILE=$(mktemp)
echo "# Triangles seed nodes - auto-generated $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$TMPFILE"
echo "$RESPONSE" | jq -r '.result[] | .address + ":" + (.port|tostring)' >> "$TMPFILE" 2>/dev/null
SEED_COUNT=$(grep -c '.onion' "$TMPFILE" 2>/dev/null || echo 0)
if [ "$SEED_COUNT" -gt 0 ]; then
mv "$TMPFILE" "$OUTPUT_FILE"
echo "Updated ${OUTPUT_FILE} with ${SEED_COUNT} seeds"
else
rm -f "$TMPFILE"
echo "Warning: no onion seeds found, keeping previous file" >&2
fi
+2 -2
View File
@@ -7,8 +7,8 @@
// 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 4
#define CLIENT_VERSION_REVISION 4
#define CLIENT_VERSION_MINOR 5
#define CLIENT_VERSION_REVISION 1
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
+8 -6
View File
@@ -378,6 +378,8 @@ std::string HelpMessage()
" -synctime " + _("Sync time with other nodes. Disable if time on your system is precise e.g. syncing with NTP (default: 1)") + "\n" +
" -cppolicy " + _("Sync checkpoints policy (default: strict)") + "\n" +
" -onionseed " + _("Find peers using .onion seeds (default: 1 unless -connect)") + "\n" +
" -seedurl=<host> " + _("HTTP seed list host (default: seeds.cryptographic-triangles.org)") + "\n" +
" -noseedurl " + _("Disable HTTP seed list fetch on startup") + "\n" +
" -banscore=<n> " + _("Threshold for disconnecting misbehaving peers (default: 100)") + "\n" +
" -bantime=<n> " + _("Number of seconds to keep misbehaving peers from reconnecting (default: 86400)") + "\n" +
" -maxreceivebuffer=<n> " + _("Maximum per-connection receive buffer, <n>*1000 bytes (default: 5000)") + "\n" +
@@ -420,6 +422,7 @@ std::string HelpMessage()
" -upgradewallet " + _("Upgrade wallet to latest format") + "\n" +
" -keypool=<n> " + _("Set key pool size to <n> (default: 100)") + "\n" +
" -rescan " + _("Rescan the block chain for missing wallet transactions") + "\n" +
" -postibdrescan " + _("Run the wallet rescan after initial sync in a background thread (default: 1)") + "\n" +
" -salvagewallet " + _("Attempt to recover private keys from a corrupt wallet.dat") + "\n" +
" -checkblocks=<n> " + _("How many blocks to check at startup (default: 2500, 0 = all)") + "\n" +
" -checklevel=<n> " + _("How thorough the block verification is (0-6, default: 1)") + "\n" +
@@ -629,6 +632,10 @@ bool AppInit2()
fConfChange = GetBoolArg("-confchange", false);
fEnforceCanonical = GetBoolArg("-enforcecanonical", true);
fAddressIndex = GetBoolArg("-addressindex", false);
if (fAddressIndex)
printf("Address index enabled\n");
if (mapArgs.count("-mininput"))
{
if (!ParseMoney(mapArgs["-mininput"], nMinimumInputValue))
@@ -1045,7 +1052,7 @@ bool AppInit2()
printf("Rescanning last %i blocks (from block %i)...\n", pindexBest->nHeight - pindexRescan->nHeight, pindexRescan->nHeight);
nStart = GetTimeMillis();
bool fScannedWithIndex = false;
if (GetBoolArg("-addressindex", false) && !GetBoolArg("-rescan"))
if (fAddressIndex && !GetBoolArg("-rescan"))
{
CTxDB txdb("r");
int nAddressIndexStartHeight = 0;
@@ -1298,11 +1305,6 @@ bool AppInit2()
}
#endif
// ********************************************************* Step 11.6: Address index
fAddressIndex = GetBoolArg("-addressindex", false);
if (fAddressIndex)
printf("Address index enabled\n");
// ********************************************************* Step 11.7: SSE notification queue
if (GetBoolArg("-ssenotify", false))
{
+64 -35
View File
@@ -110,11 +110,54 @@ struct CHeaderSyncNode
static std::map<uint256, CHeaderSyncNode> mapHeaderSync;
static uint256 hashBestHeaderSync = 0;
static CCriticalSection cs_PostIbdWork;
static bool fPostIbdWorkStarted = false;
static const unsigned int MAX_HEADER_SYNC_CACHE = 50000;
static const unsigned int HEADER_DOWNLOAD_WINDOW = 128;
static const int64_t HEADER_REQUEST_TIMEOUT_MICROS = 30 * 1000000;
static void ThreadPostIbdWork(void* parg)
{
RenameThread("Triangles-postibd");
try
{
if (!fShutdown && pwalletMain && GetBoolArg("-postibdrescan", true))
{
printf("Starting post-IBD wallet rescan from genesis in background...\n");
uiInterface.InitMessage(_("Rescanning wallet in background..."));
int nFound = 0;
bool fUsedIndex = false;
if (fAddressIndex)
{
fUsedIndex = pwalletMain->ScanForWalletTransactionsFromIndex(pindexGenesisBlock, true, &nFound);
if (!fUsedIndex)
printf("Indexed wallet rescan failed, falling back to full rescan.\n");
}
if (!fUsedIndex)
nFound = pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
printf("Post-IBD wallet rescan complete: %d transactions found (indexed=%d)\n", nFound, fUsedIndex);
}
if (!fShutdown && fSecMsgEnabled)
{
printf("Starting post-IBD secure message chain scan in background...\n");
uiInterface.InitMessage(_("Scanning for secure messages in background..."));
SecureMsgScanBlockChain();
printf("Post-IBD secure message chain scan complete\n");
}
}
catch (std::exception& e)
{
PrintExceptionContinue(&e, "ThreadPostIbdWork()");
}
catch (...)
{
PrintExceptionContinue(NULL, "ThreadPostIbdWork()");
}
}
static uint256 GetHeaderSyncTrust(unsigned int nBits)
{
CBigNum bnTarget;
@@ -2043,8 +2086,8 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
return error("ConnectBlock() : UpdateTxIndex failed");
}
// Update address index (skip during IBD - will be rebuilt on next start with -reindex)
if (fAddressIndex && !fIsInitialDownload)
// Update address index
if (fAddressIndex)
{
for (unsigned int i = 0; i < vtx.size(); i++)
{
@@ -2406,23 +2449,23 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
const CBlockLocator locator(pindexBest);
::SetBestChain(locator);
// Wallet rescan: SyncWithWallets was skipped during IBD, so scan
// the entire chain to pick up all wallet transactions.
if (pwalletMain)
// Run expensive post-IBD scans in the background so reaching tip
// is not blocked by wallet/message index rebuild work.
bool fStartPostIbdWork = false;
{
printf("Starting post-IBD wallet rescan from genesis...\n");
uiInterface.InitMessage(_("Rescanning wallet..."));
int nFound = pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
printf("Post-IBD wallet rescan complete: %d transactions found\n", nFound);
LOCK(cs_PostIbdWork);
if (!fPostIbdWorkStarted)
{
fPostIbdWorkStarted = true;
fStartPostIbdWork = true;
}
}
// Secure messaging: scan chain for public keys needed to decrypt messages
if (fSecMsgEnabled)
if (fStartPostIbdWork && !NewThread(ThreadPostIbdWork, NULL))
{
printf("Starting post-IBD secure message chain scan...\n");
uiInterface.InitMessage(_("Scanning for secure messages..."));
SecureMsgScanBlockChain();
printf("Post-IBD secure message chain scan complete\n");
LOCK(cs_PostIbdWork);
fPostIbdWorkStarted = false;
printf("Warning: post-IBD background work thread could not be started; scans skipped.\n");
}
}
fWasInitialDownload = fIsInitialDownload;
@@ -2788,9 +2831,6 @@ bool CBlock::AcceptBlock()
pnode->PushInventory(CInv(MSG_BLOCK, hash));
}
// triangles: check pending sync-checkpoint
Checkpoints::AcceptPendingSyncCheckpoint();
return true;
}
@@ -2829,7 +2869,7 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
// triangles: check proof-of-stake
// Limited duplicity on stake: prevents block flood attack
// Duplicate stake allowed only when there is orphan child block
if (pblock->IsProofOfStake() && setStakeSeen.count(pblock->GetProofOfStake()) && !mapOrphanBlocksByPrev.count(hash) && !Checkpoints::WantedByPendingSyncCheckpoint(hash))
if (pblock->IsProofOfStake() && setStakeSeen.count(pblock->GetProofOfStake()) && !mapOrphanBlocksByPrev.count(hash))
return error("ProcessBlock() : duplicate proof-of-stake (%s, %d) for block %s", pblock->GetProofOfStake().first.ToString().c_str(), pblock->GetProofOfStake().second, hash.ToString().c_str());
// Preliminary checks
@@ -2843,12 +2883,12 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
}
// Anti-spam: reject blocks with insufficient difficulty to prevent memory flooding.
// Use sync checkpoint as reference; fall back to chain tip if checkpoint is genesis.
CBlockIndex* pcheckpoint = Checkpoints::GetLastSyncCheckpoint();
if (!pcheckpoint || pcheckpoint->nHeight == 0)
// Use the most recent hardened checkpoint we know about; fall back to the chain tip.
CBlockIndex* pcheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex);
if (!pcheckpoint)
pcheckpoint = pindexBest;
if (pcheckpoint && pblock->hashPrevBlock != hashBestChain && !Checkpoints::WantedByPendingSyncCheckpoint(hash))
if (pcheckpoint && pblock->hashPrevBlock != hashBestChain)
{
int64_t deltaTime = pblock->GetBlockTime() - pcheckpoint->nTime;
CBigNum bnNewBlock;
@@ -2877,10 +2917,6 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
}
}
// triangles: ask for pending sync-checkpoint if any
if (!IsInitialBlockDownload())
Checkpoints::AskForPendingSyncCheckpoint(pfrom);
// If don't already have its previous block, shunt it off to holding area until we get it
if (!mapBlockIndex.count(pblock->hashPrevBlock))
{
@@ -2891,7 +2927,7 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
{
// Limited duplicity on stake: prevents block flood attack
// Duplicate stake allowed only when there is orphan child block
if (setStakeSeenOrphan.count(pblock2->GetProofOfStake()) && !mapOrphanBlocksByPrev.count(hash) && !Checkpoints::WantedByPendingSyncCheckpoint(hash))
if (setStakeSeenOrphan.count(pblock2->GetProofOfStake()) && !mapOrphanBlocksByPrev.count(hash))
return error("ProcessBlock() : duplicate proof-of-stake (%s, %d) for orphan block %s", pblock2->GetProofOfStake().first.ToString().c_str(), pblock2->GetProofOfStake().second, hash.ToString().c_str());
else
setStakeSeenOrphan.insert(pblock2->GetProofOfStake());
@@ -2979,10 +3015,6 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
nQueued, hash.ToString().substr(0,20).c_str());
}
// triangles: if responsible for sync-checkpoint send it
if (pfrom && !CSyncCheckpoint::strMasterPrivKey.empty())
Checkpoints::SendSyncCheckpoint(Checkpoints::AutoSelectSyncCheckpoint());
return true;
}
@@ -3859,9 +3891,6 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
cPeerBlockCounts.input(pfrom->nStartingHeight);
// triangles: ask for pending sync-checkpoint if any
if (!IsInitialBlockDownload())
Checkpoints::AskForPendingSyncCheckpoint(pfrom);
}
+155 -62
View File
@@ -13,6 +13,9 @@
#include "ui_interface.h"
#include "onionseed.h"
#include <boost/asio.hpp>
#include <sstream>
#ifdef WIN32
#include <string.h>
#endif
@@ -41,8 +44,8 @@ void ThreadOpenAddedConnections2(void* parg);
#ifdef USE_UPNP
void ThreadMapPort2(void* parg);
#endif
void ThreadDNSAddressSeed(void* parg);
void ThreadDNSAddressSeed2(void* parg);
void ThreadHTTPSeedFetch(void* parg);
void ThreadHTTPSeedFetch2(void* parg);
bool OpenNetworkConnection(const CAddress& addrConnect, CSemaphoreGrant *grantOutbound = NULL, const char *strDest = NULL, bool fOneShot = false);
@@ -1386,9 +1389,9 @@ void ThreadOnionSeed(void* parg)
// Hardcoded seeds removed - peer discovery is now fully dynamic via HTTP seed list.
// See: seeds.cryptographic-triangles.org
unsigned int pnSeed[] = {
0xCE58E9C2, // DNS2-OpenClaw: 194.233.88.206
0x13A7D04A, // DNS3-Sami: 74.208.167.19
};
void DumpAddresses()
@@ -1430,56 +1433,166 @@ void ThreadDumpAddress(void* parg)
printf("ThreadDumpAddress exited\n");
}
void ThreadDNSAddressSeed2(void* parg)
void ThreadHTTPSeedFetch2(void* parg)
{
static const char* strDNSSeed[] = {
"seed1.cryptographic-triangles.org",
"seed2.cryptographic-triangles.org",
"seed3.cryptographic-triangles.org",
"backup-seed.cryptographic-triangles.org",
};
static const char* DEFAULT_SEED_URL_HOST = "seeds.cryptographic-triangles.org";
static const char* DEFAULT_SEED_URL_PATH = "/seeds.txt";
static const int HTTP_PORT = 80;
printf("Loading addresses from DNS seeds...\n");
int found = 0;
std::string seedHost = GetArg("-seedurl", DEFAULT_SEED_URL_HOST);
std::string seedPath = DEFAULT_SEED_URL_PATH;
for (unsigned int seed_idx = 0; seed_idx < ARRAYLEN(strDNSSeed); seed_idx++)
{
if (fShutdown)
// Allow full URL override: -seedurl=myhost.com/path/seeds.txt
size_t slashPos = seedHost.find('/');
if (slashPos != std::string::npos) {
seedPath = seedHost.substr(slashPos);
seedHost = seedHost.substr(0, slashPos);
}
printf("Fetching seed list from http://%s%s ...\n", seedHost.c_str(), seedPath.c_str());
try {
boost::asio::io_context io_context;
boost::asio::ip::tcp::resolver resolver(io_context);
boost::system::error_code resolve_ec;
auto endpoints = resolver.resolve(seedHost, std::to_string(HTTP_PORT), resolve_ec);
if (resolve_ec) {
printf("HTTP seed fetch: cannot resolve %s (%s)\n", seedHost.c_str(), resolve_ec.message().c_str());
return;
}
vector<CNetAddr> vaddr;
if (LookupHost(strDNSSeed[seed_idx], vaddr))
boost::asio::ip::tcp::socket socket(io_context);
boost::asio::connect(socket, endpoints);
std::string request =
"GET " + seedPath + " HTTP/1.1\r\n"
"Host: " + seedHost + "\r\n"
"Connection: close\r\n"
"User-Agent: Triangles\r\n"
"\r\n";
boost::asio::write(socket, boost::asio::buffer(request));
// Read response
boost::asio::streambuf response_buf;
boost::asio::read_until(socket, response_buf, "\r\n\r\n");
std::istream response_stream(&response_buf);
std::string http_version;
unsigned int status_code = 0;
response_stream >> http_version >> status_code;
std::string status_message;
std::getline(response_stream, status_message);
if (status_code != 200) {
printf("HTTP seed fetch: got status %u from %s\n", status_code, seedHost.c_str());
return;
}
// Skip remaining headers
std::string header_line;
while (std::getline(response_stream, header_line) && header_line != "\r") {}
// Read body (remainder in buffer + rest from socket)
std::string body;
// First, grab anything already buffered past the headers
if (response_buf.size() > 0) {
std::istream body_stream(&response_buf);
std::ostringstream oss;
oss << body_stream.rdbuf();
body = oss.str();
}
// Read rest until EOF
boost::system::error_code ec;
while (boost::asio::read(socket, response_buf, boost::asio::transfer_at_least(1), ec)) {
std::istream s(&response_buf);
std::ostringstream oss;
oss << s.rdbuf();
body += oss.str();
}
// 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))
{
for (CNetAddr& ip : vaddr)
{
CAddress addr(CService(ip, GetDefaultPort()));
if (fShutdown)
return;
// Trim whitespace and carriage returns
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
line.pop_back();
while (!line.empty() && (line.front() == ' ' || line.front() == '\t'))
line.erase(line.begin());
if (line.empty() || line[0] == '#')
continue;
// Parse address:port
std::string addrStr = line;
int port = GetDefaultPort();
// For .onion addresses, the last colon before port is after ".onion"
size_t onionPos = addrStr.find(".onion:");
if (onionPos != std::string::npos) {
port = atoi(addrStr.substr(onionPos + 7).c_str());
addrStr = addrStr.substr(0, onionPos + 6); // keep ".onion"
} else if (addrStr.find(".onion") == std::string::npos) {
// Clearnet address - find last colon for port
size_t colonPos = addrStr.rfind(':');
if (colonPos != std::string::npos) {
port = atoi(addrStr.substr(colonPos + 1).c_str());
addrStr = addrStr.substr(0, colonPos);
}
}
if (port <= 0 || port > 65535)
port = GetDefaultPort();
CNetAddr parsed;
bool resolved = parsed.SetSpecial(addrStr);
if (!resolved) {
std::vector<CNetAddr> vIP;
if (LookupHost(addrStr.c_str(), vIP, 1, false) && !vIP.empty()) {
parsed = vIP[0];
resolved = true;
}
}
if (resolved) {
CAddress addr(CService(parsed, port));
addr.nTime = GetTime() - 3*24*60*60; // 3 days ago
addrman.Add(addr, CNetAddr(strDNSSeed[seed_idx], true));
addrman.Add(addr, CNetAddr("http-seed", true));
found++;
}
}
}
printf("%d addresses found from DNS seeds\n", found);
printf("%d addresses found from HTTP seed list (%s)\n", found, seedHost.c_str());
} catch (std::exception& e) {
printf("HTTP seed fetch failed: %s\n", e.what());
}
}
void ThreadDNSAddressSeed(void* parg)
void ThreadHTTPSeedFetch(void* parg)
{
RenameThread("Triangles-dnsseed");
RenameThread("Triangles-httpseed");
try
{
vnThreadsRunning[THREAD_DNSSEED]++;
ThreadDNSAddressSeed2(parg);
vnThreadsRunning[THREAD_DNSSEED]--;
vnThreadsRunning[THREAD_HTTPSEED]++;
ThreadHTTPSeedFetch2(parg);
vnThreadsRunning[THREAD_HTTPSEED]--;
}
catch (std::exception& e) {
vnThreadsRunning[THREAD_DNSSEED]--;
PrintException(&e, "ThreadDNSAddressSeed()");
vnThreadsRunning[THREAD_HTTPSEED]--;
PrintException(&e, "ThreadHTTPSeedFetch()");
} catch (...) {
vnThreadsRunning[THREAD_DNSSEED]--;
PrintException(NULL, "ThreadDNSAddressSeed()");
vnThreadsRunning[THREAD_HTTPSEED]--;
PrintException(NULL, "ThreadHTTPSeedFetch()");
}
printf("ThreadDNSAddressSeed exited\n");
printf("ThreadHTTPSeedFetch exited\n");
}
void ThreadOpenConnections(void* parg)
@@ -1586,30 +1699,8 @@ void ThreadOpenConnections2(void* parg)
if (fShutdown)
return;
// Add hardcoded seed nodes when we have no connections.
// Original check (addrman.size()==0) was too conservative - stale entries
// in peers.dat would prevent fallback to working hardcoded IPs forever.
{
LOCK(cs_vNodes);
bool fNoOutbound = true;
for (CNode* pnode : vNodes) {
if (!pnode->fInbound) { fNoOutbound = false; break; }
}
if (fNoOutbound && (GetTime() - nStart > 10) && !fTestNet)
{
std::vector<CAddress> vAdd;
for (unsigned int i = 0; i < ARRAYLEN(pnSeed); i++)
{
struct in_addr ip;
memcpy(&ip, &pnSeed[i], sizeof(ip));
CAddress addr(CService(ip, GetDefaultPort()));
addr.nTime = GetTime() - GetRand(60*60); // seen recently
vAdd.push_back(addr);
}
addrman.Add(vAdd, CNetAddr("127.0.0.1"));
printf("No outbound connections after 10s, added %d hardcoded seeds\n", (int)vAdd.size());
}
}
// Hardcoded seed fallback removed - peer discovery is now fully dynamic
// via HTTP seed list from seeds.cryptographic-triangles.org
//
// Choose an address to connect to based on most recently seen
@@ -2112,9 +2203,11 @@ void StartNode(void* parg)
if (fUseUPnP)
MapPort();
// DNS seed lookup
if (!NewThread(ThreadDNSAddressSeed, NULL))
printf("Error: NewThread(ThreadDNSAddressSeed) failed\n");
// HTTP seed list fetch (replaces DNS seeds)
if (GetBoolArg("-noseedurl", false))
printf("HTTP seed fetch disabled\n");
else if (!NewThread(ThreadHTTPSeedFetch, NULL))
printf("Error: NewThread(ThreadHTTPSeedFetch) failed\n");
// Send and receive from sockets, accept connections
if (!NewThread(ThreadSocketHandler, NULL))
@@ -2172,7 +2265,7 @@ bool StopNode()
#ifdef USE_UPNP
if (vnThreadsRunning[THREAD_UPNP] > 0) printf("ThreadMapPort still running\n");
#endif
if (vnThreadsRunning[THREAD_DNSSEED] > 0) printf("ThreadDNSAddressSeed still running\n");
if (vnThreadsRunning[THREAD_HTTPSEED] > 0) printf("ThreadHTTPSeedFetch still running\n");
if (vnThreadsRunning[THREAD_ADDEDCONNECTIONS] > 0) printf("ThreadOpenAddedConnections still running\n");
if (vnThreadsRunning[THREAD_DUMPADDRESS] > 0) printf("ThreadDumpAddresses still running\n");
if (vnThreadsRunning[THREAD_STAKE_MINER] > 0) printf("ThreadStakeMiner still running\n");
+1 -1
View File
@@ -103,7 +103,7 @@ enum threadId
THREAD_MESSAGEHANDLER,
THREAD_RPCLISTENER,
THREAD_UPNP,
THREAD_DNSSEED,
THREAD_HTTPSEED,
THREAD_ADDEDCONNECTIONS,
THREAD_DUMPADDRESS,
THREAD_RPCHANDLER,
+2 -6
View File
@@ -24,13 +24,9 @@ namespace NetBootstrap {
NULL
};
// Legacy IP seed nodes for old wallet compatibility
// These should be actual IP addresses of stable nodes
// Hardcoded seeds removed - peer discovery is now fully dynamic via HTTP seed list.
// See: seeds.cryptographic-triangles.org
static const unsigned int pnSeed[] __attribute__((unused)) = {
// Format: IP addresses in network byte order (little-endian)
// For IP a.b.c.d: (d << 24) | (c << 16) | (b << 8) | a
0xCE58E9C2, // DNS2-OpenClaw: 194.233.88.206
0x13A7D04A, // DNS3-Sami: 74.208.167.19
};
// Network protocol compatibility settings
+10 -5
View File
@@ -6,6 +6,7 @@
#include "netbase.h"
#include "util.h"
#include "sync.h"
#include <openssl/evp.h>
#ifndef WIN32
#include <sys/fcntl.h>
@@ -860,15 +861,19 @@ std::string CNetAddr::ToStringIP() const
unsigned char addr35[35];
memcpy(addr35, tor_v3_pubkey, 32);
// Compute checksum: SHA3-256(".onion checksum" || pubkey || version)[:2]
// For now use a simplified checksum from the stored data
unsigned char checksumInput[15 + 32 + 1];
memcpy(checksumInput, ".onion checksum", 15);
memcpy(checksumInput + 15, tor_v3_pubkey, 32);
checksumInput[47] = 0x03; // version
// SHA-256 as fallback (SHA3-256 via tor_crypto_compat.h for full impl)
uint256 hash = Hash(checksumInput, checksumInput + 48);
addr35[32] = ((unsigned char*)&hash)[0];
addr35[33] = ((unsigned char*)&hash)[1];
unsigned char sha3hash[32];
unsigned int sha3len = 0;
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
EVP_DigestInit_ex(mdctx, EVP_sha3_256(), NULL);
EVP_DigestUpdate(mdctx, checksumInput, 48);
EVP_DigestFinal_ex(mdctx, sha3hash, &sha3len);
EVP_MD_CTX_free(mdctx);
addr35[32] = sha3hash[0];
addr35[33] = sha3hash[1];
addr35[34] = 0x03; // version
return EncodeBase32(addr35, 35) + ".onion";
}
+4 -12
View File
@@ -1,19 +1,11 @@
#ifndef TRIANGLES_ONIONSEED_H
#define TRIANGLES_ONIONSEED_H
// hidden service seeds
// v5 hard fork: v2 onion seeds removed (Tor v2 deprecated Oct 2021)
// v3 onion seeds - bootstrap nodes deployed 2026-03-30
// Onion seeds are now fetched dynamically via HTTP seed list.
// No hardcoded onion addresses - they go stale when Tor services restart.
// See: seeds.cryptographic-triangles.org
static const char *strMainNetOnionSeed[][1] = {
// Main nodes
{"gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion"}, // DNS2
{"futmtrvh6j34t7s6yjdxfia6iwuyfzwh4k5eqfof5kfhoqk3xmi3qoqd.onion"}, // Original seed
{"i6tk7soznftvoibtskwlezviskiererhjndpsmrff4kaxw7jnd5izfqd.onion"}, // DNS3
// Docker seed nodes (contabo-de)
{"uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion"}, // seed-1
{"el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion"}, // seed-2
{"sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion"}, // seed-3
{"i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion"}, // seed-4
{NULL}
};
+26
View File
@@ -3,6 +3,7 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "net.h"
#include "addrman.h"
#include "trianglesrpc.h"
#include "alert.h"
#include "wallet.h"
@@ -199,3 +200,28 @@ Value sendalert(const Array& params, bool fHelp)
result.push_back(Pair("nCancel", alert.nCancel));
return result;
}
Value getseedlist(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 0)
throw runtime_error(
"getseedlist\n"
"Returns known .onion peer addresses from the address manager.\n"
"Used by the seed collector to build the dynamic seed list.");
vector<CAddress> vAddr = addrman.GetAddr();
Array ret;
for (const CAddress& addr : vAddr) {
if (!addr.IsTor())
continue;
Object obj;
obj.push_back(Pair("address", addr.ToStringIP()));
obj.push_back(Pair("port", (int)addr.GetPort()));
obj.push_back(Pair("lastseen", (boost::int64_t)addr.nTime));
ret.push_back(obj);
}
return ret;
}
+1
View File
@@ -253,6 +253,7 @@ static const CRPCCommand vRPCCommands[] =
{ "getblockchaininfo", &getblockchaininfo, true, false },
{ "getwalletinfo", &getwalletinfo, true, false },
{ "getnetworkinfo", &getnetworkinfo, true, false },
{ "getseedlist", &getseedlist, true, false },
{ "gettxoutsetinfo", &gettxoutsetinfo, true, false },
{ "estimatefee", &estimatefee, true, false },
{ "getaddressbalance", &getaddressbalance, true, false },
+1
View File
@@ -149,6 +149,7 @@ extern std::vector<unsigned char> ParseHexO(const json_spirit::Object& o, std::s
extern json_spirit::Value getconnectioncount(const json_spirit::Array& params, bool fHelp); // in rpcnet.cpp
extern json_spirit::Value getpeerinfo(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getnetworkinfo(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getseedlist(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getwalletinfo(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value dumpwallet(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value importwallet(const json_spirit::Array& params, bool fHelp);
+38 -30
View File
@@ -959,39 +959,47 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
int ret = 0;
CBlockIndex* pindex = pindexStart;
int nScanned = 0;
int nTotal = nBestHeight - (pindexStart ? pindexStart->nHeight : 0);
if (nTotal < 1) nTotal = 1;
int64_t nLastProgressTime = GetTimeMillis();
// Cache wallet birthday outside the loop (only written during key import)
int64_t nBirthTime = nTimeFirstKey;
while (pindex)
{
LOCK(cs_wallet);
int nScanned = 0;
int nTotal = nBestHeight - (pindexStart ? pindexStart->nHeight : 0);
if (nTotal < 1) nTotal = 1;
while (pindex)
if (fShutdown)
break;
++nScanned;
// Report progress every 500ms to keep UI responsive
int64_t nNow = GetTimeMillis();
if (nNow - nLastProgressTime > 500)
{
if (fShutdown)
break;
// Report progress every 10000 blocks to keep UI responsive
if (++nScanned % 10000 == 0)
{
int nPercent = (nScanned * 100) / nTotal;
uiInterface.InitMessage(strprintf(_("Rescanning... %d%%"), nPercent));
}
// no need to read and scan block, if block was created before
// our wallet birthday (as adjusted for block time variability)
if (nTimeFirstKey && (pindex->nTime < (nTimeFirstKey - 7200))) {
pindex = pindex->pnext;
continue;
}
CBlock block;
block.ReadFromDisk(pindex, true);
for (CTransaction& tx : block.vtx)
{
if (AddToWalletIfInvolvingMe(tx, &block, fUpdate))
ret++;
}
pindex = pindex->pnext;
nLastProgressTime = nNow;
int nPercent = (nScanned * 100) / nTotal;
uiInterface.InitMessage(strprintf(_("Rescanning... %d%%"), nPercent));
}
// no need to read and scan block, if block was created before
// our wallet birthday (as adjusted for block time variability)
if (nBirthTime && (pindex->nTime < (nBirthTime - 7200))) {
pindex = pindex->pnext;
continue;
}
// Read block from disk WITHOUT holding wallet lock
CBlock block;
block.ReadFromDisk(pindex, true);
// AddToWalletIfInvolvingMe acquires cs_wallet internally
for (CTransaction& tx : block.vtx)
{
if (AddToWalletIfInvolvingMe(tx, &block, fUpdate))
ret++;
}
pindex = pindex->pnext;
}
return ret;
}