Compare commits
11 Commits
tri-pi-v5.4.2
...
v5.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 207e1ed676 | |||
| 2fba88bfc5 | |||
| 4563e7952b | |||
| ea86ab077c | |||
| a1b137a7bb | |||
| 939606a5f7 | |||
| 73cecd90d1 | |||
| c74c92c542 | |||
| 97ae675f0a | |||
| b0b591364f | |||
| bf257858a4 |
@@ -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.
|
||||||
@@ -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
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
|
// 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_MAJOR 5
|
||||||
#define CLIENT_VERSION_MINOR 4
|
#define CLIENT_VERSION_MINOR 5
|
||||||
#define CLIENT_VERSION_REVISION 2
|
#define CLIENT_VERSION_REVISION 1
|
||||||
#define CLIENT_VERSION_BUILD 0
|
#define CLIENT_VERSION_BUILD 0
|
||||||
|
|
||||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||||
|
|||||||
+8
-6
@@ -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" +
|
" -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" +
|
" -cppolicy " + _("Sync checkpoints policy (default: strict)") + "\n" +
|
||||||
" -onionseed " + _("Find peers using .onion seeds (default: 1 unless -connect)") + "\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" +
|
" -banscore=<n> " + _("Threshold for disconnecting misbehaving peers (default: 100)") + "\n" +
|
||||||
" -bantime=<n> " + _("Number of seconds to keep misbehaving peers from reconnecting (default: 86400)") + "\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" +
|
" -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" +
|
" -upgradewallet " + _("Upgrade wallet to latest format") + "\n" +
|
||||||
" -keypool=<n> " + _("Set key pool size to <n> (default: 100)") + "\n" +
|
" -keypool=<n> " + _("Set key pool size to <n> (default: 100)") + "\n" +
|
||||||
" -rescan " + _("Rescan the block chain for missing wallet transactions") + "\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" +
|
" -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" +
|
" -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" +
|
" -checklevel=<n> " + _("How thorough the block verification is (0-6, default: 1)") + "\n" +
|
||||||
@@ -629,6 +632,10 @@ bool AppInit2()
|
|||||||
fConfChange = GetBoolArg("-confchange", false);
|
fConfChange = GetBoolArg("-confchange", false);
|
||||||
fEnforceCanonical = GetBoolArg("-enforcecanonical", true);
|
fEnforceCanonical = GetBoolArg("-enforcecanonical", true);
|
||||||
|
|
||||||
|
fAddressIndex = GetBoolArg("-addressindex", false);
|
||||||
|
if (fAddressIndex)
|
||||||
|
printf("Address index enabled\n");
|
||||||
|
|
||||||
if (mapArgs.count("-mininput"))
|
if (mapArgs.count("-mininput"))
|
||||||
{
|
{
|
||||||
if (!ParseMoney(mapArgs["-mininput"], nMinimumInputValue))
|
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);
|
printf("Rescanning last %i blocks (from block %i)...\n", pindexBest->nHeight - pindexRescan->nHeight, pindexRescan->nHeight);
|
||||||
nStart = GetTimeMillis();
|
nStart = GetTimeMillis();
|
||||||
bool fScannedWithIndex = false;
|
bool fScannedWithIndex = false;
|
||||||
if (GetBoolArg("-addressindex", false) && !GetBoolArg("-rescan"))
|
if (fAddressIndex && !GetBoolArg("-rescan"))
|
||||||
{
|
{
|
||||||
CTxDB txdb("r");
|
CTxDB txdb("r");
|
||||||
int nAddressIndexStartHeight = 0;
|
int nAddressIndexStartHeight = 0;
|
||||||
@@ -1298,11 +1305,6 @@ bool AppInit2()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ********************************************************* Step 11.6: Address index
|
|
||||||
fAddressIndex = GetBoolArg("-addressindex", false);
|
|
||||||
if (fAddressIndex)
|
|
||||||
printf("Address index enabled\n");
|
|
||||||
|
|
||||||
// ********************************************************* Step 11.7: SSE notification queue
|
// ********************************************************* Step 11.7: SSE notification queue
|
||||||
if (GetBoolArg("-ssenotify", false))
|
if (GetBoolArg("-ssenotify", false))
|
||||||
{
|
{
|
||||||
|
|||||||
+64
-35
@@ -110,11 +110,54 @@ struct CHeaderSyncNode
|
|||||||
|
|
||||||
static std::map<uint256, CHeaderSyncNode> mapHeaderSync;
|
static std::map<uint256, CHeaderSyncNode> mapHeaderSync;
|
||||||
static uint256 hashBestHeaderSync = 0;
|
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 MAX_HEADER_SYNC_CACHE = 50000;
|
||||||
static const unsigned int HEADER_DOWNLOAD_WINDOW = 128;
|
static const unsigned int HEADER_DOWNLOAD_WINDOW = 128;
|
||||||
static const int64_t HEADER_REQUEST_TIMEOUT_MICROS = 30 * 1000000;
|
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)
|
static uint256 GetHeaderSyncTrust(unsigned int nBits)
|
||||||
{
|
{
|
||||||
CBigNum bnTarget;
|
CBigNum bnTarget;
|
||||||
@@ -2043,8 +2086,8 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
|
|||||||
return error("ConnectBlock() : UpdateTxIndex failed");
|
return error("ConnectBlock() : UpdateTxIndex failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update address index (skip during IBD - will be rebuilt on next start with -reindex)
|
// Update address index
|
||||||
if (fAddressIndex && !fIsInitialDownload)
|
if (fAddressIndex)
|
||||||
{
|
{
|
||||||
for (unsigned int i = 0; i < vtx.size(); i++)
|
for (unsigned int i = 0; i < vtx.size(); i++)
|
||||||
{
|
{
|
||||||
@@ -2406,23 +2449,23 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
|
|||||||
const CBlockLocator locator(pindexBest);
|
const CBlockLocator locator(pindexBest);
|
||||||
::SetBestChain(locator);
|
::SetBestChain(locator);
|
||||||
|
|
||||||
// Wallet rescan: SyncWithWallets was skipped during IBD, so scan
|
// Run expensive post-IBD scans in the background so reaching tip
|
||||||
// the entire chain to pick up all wallet transactions.
|
// is not blocked by wallet/message index rebuild work.
|
||||||
if (pwalletMain)
|
bool fStartPostIbdWork = false;
|
||||||
{
|
{
|
||||||
printf("Starting post-IBD wallet rescan from genesis...\n");
|
LOCK(cs_PostIbdWork);
|
||||||
uiInterface.InitMessage(_("Rescanning wallet..."));
|
if (!fPostIbdWorkStarted)
|
||||||
int nFound = pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
|
{
|
||||||
printf("Post-IBD wallet rescan complete: %d transactions found\n", nFound);
|
fPostIbdWorkStarted = true;
|
||||||
|
fStartPostIbdWork = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secure messaging: scan chain for public keys needed to decrypt messages
|
if (fStartPostIbdWork && !NewThread(ThreadPostIbdWork, NULL))
|
||||||
if (fSecMsgEnabled)
|
|
||||||
{
|
{
|
||||||
printf("Starting post-IBD secure message chain scan...\n");
|
LOCK(cs_PostIbdWork);
|
||||||
uiInterface.InitMessage(_("Scanning for secure messages..."));
|
fPostIbdWorkStarted = false;
|
||||||
SecureMsgScanBlockChain();
|
printf("Warning: post-IBD background work thread could not be started; scans skipped.\n");
|
||||||
printf("Post-IBD secure message chain scan complete\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fWasInitialDownload = fIsInitialDownload;
|
fWasInitialDownload = fIsInitialDownload;
|
||||||
@@ -2788,9 +2831,6 @@ bool CBlock::AcceptBlock()
|
|||||||
pnode->PushInventory(CInv(MSG_BLOCK, hash));
|
pnode->PushInventory(CInv(MSG_BLOCK, hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
// triangles: check pending sync-checkpoint
|
|
||||||
Checkpoints::AcceptPendingSyncCheckpoint();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2829,7 +2869,7 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
|||||||
// triangles: check proof-of-stake
|
// triangles: check proof-of-stake
|
||||||
// Limited duplicity on stake: prevents block flood attack
|
// Limited duplicity on stake: prevents block flood attack
|
||||||
// Duplicate stake allowed only when there is orphan child block
|
// 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());
|
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
|
// Preliminary checks
|
||||||
@@ -2843,12 +2883,12 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Anti-spam: reject blocks with insufficient difficulty to prevent memory flooding.
|
// 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.
|
// Use the most recent hardened checkpoint we know about; fall back to the chain tip.
|
||||||
CBlockIndex* pcheckpoint = Checkpoints::GetLastSyncCheckpoint();
|
CBlockIndex* pcheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex);
|
||||||
if (!pcheckpoint || pcheckpoint->nHeight == 0)
|
if (!pcheckpoint)
|
||||||
pcheckpoint = pindexBest;
|
pcheckpoint = pindexBest;
|
||||||
|
|
||||||
if (pcheckpoint && pblock->hashPrevBlock != hashBestChain && !Checkpoints::WantedByPendingSyncCheckpoint(hash))
|
if (pcheckpoint && pblock->hashPrevBlock != hashBestChain)
|
||||||
{
|
{
|
||||||
int64_t deltaTime = pblock->GetBlockTime() - pcheckpoint->nTime;
|
int64_t deltaTime = pblock->GetBlockTime() - pcheckpoint->nTime;
|
||||||
CBigNum bnNewBlock;
|
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 don't already have its previous block, shunt it off to holding area until we get it
|
||||||
if (!mapBlockIndex.count(pblock->hashPrevBlock))
|
if (!mapBlockIndex.count(pblock->hashPrevBlock))
|
||||||
{
|
{
|
||||||
@@ -2891,7 +2927,7 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
|||||||
{
|
{
|
||||||
// Limited duplicity on stake: prevents block flood attack
|
// Limited duplicity on stake: prevents block flood attack
|
||||||
// Duplicate stake allowed only when there is orphan child block
|
// 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());
|
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
|
else
|
||||||
setStakeSeenOrphan.insert(pblock2->GetProofOfStake());
|
setStakeSeenOrphan.insert(pblock2->GetProofOfStake());
|
||||||
@@ -2979,10 +3015,6 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
|||||||
nQueued, hash.ToString().substr(0,20).c_str());
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3859,9 +3891,6 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
|||||||
|
|
||||||
cPeerBlockCounts.input(pfrom->nStartingHeight);
|
cPeerBlockCounts.input(pfrom->nStartingHeight);
|
||||||
|
|
||||||
// triangles: ask for pending sync-checkpoint if any
|
|
||||||
if (!IsInitialBlockDownload())
|
|
||||||
Checkpoints::AskForPendingSyncCheckpoint(pfrom);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 4000;
|
|||||||
static const unsigned int MAX_INV_SZ = 50000;
|
static const unsigned int MAX_INV_SZ = 50000;
|
||||||
static const int64_t MIN_TX_FEE = (1 * CENT) / 100;
|
static const int64_t MIN_TX_FEE = (1 * CENT) / 100;
|
||||||
static const int64_t MIN_RELAY_TX_FEE = (1 * CENT) / 100;
|
static const int64_t MIN_RELAY_TX_FEE = (1 * CENT) / 100;
|
||||||
static const int64_t MAX_MONEY = 222222 * COIN;
|
static const int64_t MAX_MONEY = 2222222 * COIN;
|
||||||
static const int64_t COIN_YEAR_REWARD = 33 * CENT; // 33% per year
|
static const int64_t COIN_YEAR_REWARD = 33 * CENT; // 33% per year
|
||||||
static const int64_t MAX_TRI_PROOF_OF_STAKE = 0.33 * COIN;
|
static const int64_t MAX_TRI_PROOF_OF_STAKE = 0.33 * COIN;
|
||||||
static const int MODIFIER_INTERVAL_SWITCH = 1;
|
static const int MODIFIER_INTERVAL_SWITCH = 1;
|
||||||
|
|||||||
+155
-62
@@ -13,6 +13,9 @@
|
|||||||
#include "ui_interface.h"
|
#include "ui_interface.h"
|
||||||
#include "onionseed.h"
|
#include "onionseed.h"
|
||||||
|
|
||||||
|
#include <boost/asio.hpp>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
#ifdef WIN32
|
#ifdef WIN32
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -41,8 +44,8 @@ void ThreadOpenAddedConnections2(void* parg);
|
|||||||
#ifdef USE_UPNP
|
#ifdef USE_UPNP
|
||||||
void ThreadMapPort2(void* parg);
|
void ThreadMapPort2(void* parg);
|
||||||
#endif
|
#endif
|
||||||
void ThreadDNSAddressSeed(void* parg);
|
void ThreadHTTPSeedFetch(void* parg);
|
||||||
void ThreadDNSAddressSeed2(void* parg);
|
void ThreadHTTPSeedFetch2(void* parg);
|
||||||
bool OpenNetworkConnection(const CAddress& addrConnect, CSemaphoreGrant *grantOutbound = NULL, const char *strDest = NULL, bool fOneShot = false);
|
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[] = {
|
unsigned int pnSeed[] = {
|
||||||
0xCE58E9C2, // DNS2-OpenClaw: 194.233.88.206
|
|
||||||
0x13A7D04A, // DNS3-Sami: 74.208.167.19
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void DumpAddresses()
|
void DumpAddresses()
|
||||||
@@ -1430,56 +1433,166 @@ void ThreadDumpAddress(void* parg)
|
|||||||
printf("ThreadDumpAddress exited\n");
|
printf("ThreadDumpAddress exited\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThreadDNSAddressSeed2(void* parg)
|
void ThreadHTTPSeedFetch2(void* parg)
|
||||||
{
|
{
|
||||||
static const char* strDNSSeed[] = {
|
static const char* DEFAULT_SEED_URL_HOST = "seeds.cryptographic-triangles.org";
|
||||||
"seed1.cryptographic-triangles.org",
|
static const char* DEFAULT_SEED_URL_PATH = "/seeds.txt";
|
||||||
"seed2.cryptographic-triangles.org",
|
static const int HTTP_PORT = 80;
|
||||||
"seed3.cryptographic-triangles.org",
|
|
||||||
"backup-seed.cryptographic-triangles.org",
|
|
||||||
};
|
|
||||||
|
|
||||||
printf("Loading addresses from DNS seeds...\n");
|
std::string seedHost = GetArg("-seedurl", DEFAULT_SEED_URL_HOST);
|
||||||
int found = 0;
|
std::string seedPath = DEFAULT_SEED_URL_PATH;
|
||||||
|
|
||||||
for (unsigned int seed_idx = 0; seed_idx < ARRAYLEN(strDNSSeed); seed_idx++)
|
// Allow full URL override: -seedurl=myhost.com/path/seeds.txt
|
||||||
{
|
size_t slashPos = seedHost.find('/');
|
||||||
if (fShutdown)
|
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;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
vector<CNetAddr> vaddr;
|
boost::asio::ip::tcp::socket socket(io_context);
|
||||||
if (LookupHost(strDNSSeed[seed_idx], vaddr))
|
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)
|
if (fShutdown)
|
||||||
{
|
return;
|
||||||
CAddress addr(CService(ip, GetDefaultPort()));
|
|
||||||
|
// 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
|
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++;
|
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
|
try
|
||||||
{
|
{
|
||||||
vnThreadsRunning[THREAD_DNSSEED]++;
|
vnThreadsRunning[THREAD_HTTPSEED]++;
|
||||||
ThreadDNSAddressSeed2(parg);
|
ThreadHTTPSeedFetch2(parg);
|
||||||
vnThreadsRunning[THREAD_DNSSEED]--;
|
vnThreadsRunning[THREAD_HTTPSEED]--;
|
||||||
}
|
}
|
||||||
catch (std::exception& e) {
|
catch (std::exception& e) {
|
||||||
vnThreadsRunning[THREAD_DNSSEED]--;
|
vnThreadsRunning[THREAD_HTTPSEED]--;
|
||||||
PrintException(&e, "ThreadDNSAddressSeed()");
|
PrintException(&e, "ThreadHTTPSeedFetch()");
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
vnThreadsRunning[THREAD_DNSSEED]--;
|
vnThreadsRunning[THREAD_HTTPSEED]--;
|
||||||
PrintException(NULL, "ThreadDNSAddressSeed()");
|
PrintException(NULL, "ThreadHTTPSeedFetch()");
|
||||||
}
|
}
|
||||||
printf("ThreadDNSAddressSeed exited\n");
|
printf("ThreadHTTPSeedFetch exited\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThreadOpenConnections(void* parg)
|
void ThreadOpenConnections(void* parg)
|
||||||
@@ -1586,30 +1699,8 @@ void ThreadOpenConnections2(void* parg)
|
|||||||
if (fShutdown)
|
if (fShutdown)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Add hardcoded seed nodes when we have no connections.
|
// Hardcoded seed fallback removed - peer discovery is now fully dynamic
|
||||||
// Original check (addrman.size()==0) was too conservative - stale entries
|
// via HTTP seed list from seeds.cryptographic-triangles.org
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Choose an address to connect to based on most recently seen
|
// Choose an address to connect to based on most recently seen
|
||||||
@@ -2112,9 +2203,11 @@ void StartNode(void* parg)
|
|||||||
if (fUseUPnP)
|
if (fUseUPnP)
|
||||||
MapPort();
|
MapPort();
|
||||||
|
|
||||||
// DNS seed lookup
|
// HTTP seed list fetch (replaces DNS seeds)
|
||||||
if (!NewThread(ThreadDNSAddressSeed, NULL))
|
if (GetBoolArg("-noseedurl", false))
|
||||||
printf("Error: NewThread(ThreadDNSAddressSeed) failed\n");
|
printf("HTTP seed fetch disabled\n");
|
||||||
|
else if (!NewThread(ThreadHTTPSeedFetch, NULL))
|
||||||
|
printf("Error: NewThread(ThreadHTTPSeedFetch) failed\n");
|
||||||
|
|
||||||
// Send and receive from sockets, accept connections
|
// Send and receive from sockets, accept connections
|
||||||
if (!NewThread(ThreadSocketHandler, NULL))
|
if (!NewThread(ThreadSocketHandler, NULL))
|
||||||
@@ -2172,7 +2265,7 @@ bool StopNode()
|
|||||||
#ifdef USE_UPNP
|
#ifdef USE_UPNP
|
||||||
if (vnThreadsRunning[THREAD_UPNP] > 0) printf("ThreadMapPort still running\n");
|
if (vnThreadsRunning[THREAD_UPNP] > 0) printf("ThreadMapPort still running\n");
|
||||||
#endif
|
#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_ADDEDCONNECTIONS] > 0) printf("ThreadOpenAddedConnections still running\n");
|
||||||
if (vnThreadsRunning[THREAD_DUMPADDRESS] > 0) printf("ThreadDumpAddresses still running\n");
|
if (vnThreadsRunning[THREAD_DUMPADDRESS] > 0) printf("ThreadDumpAddresses still running\n");
|
||||||
if (vnThreadsRunning[THREAD_STAKE_MINER] > 0) printf("ThreadStakeMiner still running\n");
|
if (vnThreadsRunning[THREAD_STAKE_MINER] > 0) printf("ThreadStakeMiner still running\n");
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ enum threadId
|
|||||||
THREAD_MESSAGEHANDLER,
|
THREAD_MESSAGEHANDLER,
|
||||||
THREAD_RPCLISTENER,
|
THREAD_RPCLISTENER,
|
||||||
THREAD_UPNP,
|
THREAD_UPNP,
|
||||||
THREAD_DNSSEED,
|
THREAD_HTTPSEED,
|
||||||
THREAD_ADDEDCONNECTIONS,
|
THREAD_ADDEDCONNECTIONS,
|
||||||
THREAD_DUMPADDRESS,
|
THREAD_DUMPADDRESS,
|
||||||
THREAD_RPCHANDLER,
|
THREAD_RPCHANDLER,
|
||||||
|
|||||||
+2
-6
@@ -24,13 +24,9 @@ namespace NetBootstrap {
|
|||||||
NULL
|
NULL
|
||||||
};
|
};
|
||||||
|
|
||||||
// Legacy IP seed nodes for old wallet compatibility
|
// Hardcoded seeds removed - peer discovery is now fully dynamic via HTTP seed list.
|
||||||
// These should be actual IP addresses of stable nodes
|
// See: seeds.cryptographic-triangles.org
|
||||||
static const unsigned int pnSeed[] __attribute__((unused)) = {
|
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
|
// Network protocol compatibility settings
|
||||||
|
|||||||
+10
-5
@@ -6,6 +6,7 @@
|
|||||||
#include "netbase.h"
|
#include "netbase.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "sync.h"
|
#include "sync.h"
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
#ifndef WIN32
|
#ifndef WIN32
|
||||||
#include <sys/fcntl.h>
|
#include <sys/fcntl.h>
|
||||||
@@ -860,15 +861,19 @@ std::string CNetAddr::ToStringIP() const
|
|||||||
unsigned char addr35[35];
|
unsigned char addr35[35];
|
||||||
memcpy(addr35, tor_v3_pubkey, 32);
|
memcpy(addr35, tor_v3_pubkey, 32);
|
||||||
// Compute checksum: SHA3-256(".onion checksum" || pubkey || version)[:2]
|
// 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];
|
unsigned char checksumInput[15 + 32 + 1];
|
||||||
memcpy(checksumInput, ".onion checksum", 15);
|
memcpy(checksumInput, ".onion checksum", 15);
|
||||||
memcpy(checksumInput + 15, tor_v3_pubkey, 32);
|
memcpy(checksumInput + 15, tor_v3_pubkey, 32);
|
||||||
checksumInput[47] = 0x03; // version
|
checksumInput[47] = 0x03; // version
|
||||||
// SHA-256 as fallback (SHA3-256 via tor_crypto_compat.h for full impl)
|
unsigned char sha3hash[32];
|
||||||
uint256 hash = Hash(checksumInput, checksumInput + 48);
|
unsigned int sha3len = 0;
|
||||||
addr35[32] = ((unsigned char*)&hash)[0];
|
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
|
||||||
addr35[33] = ((unsigned char*)&hash)[1];
|
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
|
addr35[34] = 0x03; // version
|
||||||
return EncodeBase32(addr35, 35) + ".onion";
|
return EncodeBase32(addr35, 35) + ".onion";
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-5
@@ -2,12 +2,10 @@
|
|||||||
#ifndef TRIANGLES_ONIONSEED_H
|
#ifndef TRIANGLES_ONIONSEED_H
|
||||||
#define TRIANGLES_ONIONSEED_H
|
#define TRIANGLES_ONIONSEED_H
|
||||||
|
|
||||||
// hidden service seeds
|
// Onion seeds are now fetched dynamically via HTTP seed list.
|
||||||
// v5 hard fork: v2 onion seeds removed (Tor v2 deprecated Oct 2021)
|
// No hardcoded onion addresses - they go stale when Tor services restart.
|
||||||
// v3 onion seeds will be added when bootstrap nodes are deployed
|
// See: seeds.cryptographic-triangles.org
|
||||||
static const char *strMainNetOnionSeed[][1] = {
|
static const char *strMainNetOnionSeed[][1] = {
|
||||||
{"gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion"},
|
|
||||||
{"futmtrvh6j34t7s6yjdxfia6iwuyfzwh4k5eqfof5kfhoqk3xmi3qoqd.onion"},
|
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
|
||||||
IntroDialog::IntroDialog(QWidget *parent) :
|
IntroDialog::IntroDialog(QWidget *parent) :
|
||||||
QDialog(parent)
|
QDialog(parent)
|
||||||
{
|
{
|
||||||
@@ -190,6 +192,24 @@ bool IntroDialog::pickDataDirectory()
|
|||||||
settings.setValue("strDataDir", dataDir);
|
settings.setValue("strDataDir", dataDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for pending data directory migration
|
||||||
|
if (settings.value("fPendingDataDirMigration", false).toBool()) {
|
||||||
|
QString oldDir = settings.value("strDataDirPrevious", "").toString();
|
||||||
|
if (!oldDir.isEmpty() && oldDir != dataDir) {
|
||||||
|
if (!migrateDataDirectory(oldDir, dataDir)) {
|
||||||
|
// Migration failed - revert to old directory
|
||||||
|
QMessageBox::warning(0, "Triangles",
|
||||||
|
QString("Data directory migration failed.\nContinuing with the previous directory:\n%1")
|
||||||
|
.arg(oldDir));
|
||||||
|
dataDir = oldDir;
|
||||||
|
settings.setValue("strDataDir", oldDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear migration state regardless
|
||||||
|
settings.remove("strDataDirPrevious");
|
||||||
|
settings.setValue("fPendingDataDirMigration", false);
|
||||||
|
}
|
||||||
|
|
||||||
// If the saved path is the default, don't set -datadir (let normal defaults work)
|
// If the saved path is the default, don't set -datadir (let normal defaults work)
|
||||||
QString defaultDir = QString::fromStdString(GetDefaultDataDir().string());
|
QString defaultDir = QString::fromStdString(GetDefaultDataDir().string());
|
||||||
if (dataDir != defaultDir) {
|
if (dataDir != defaultDir) {
|
||||||
@@ -275,3 +295,145 @@ bool IntroDialog::pickDataDirectory()
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void copyDirectoryRecursive(const boost::filesystem::path& src,
|
||||||
|
const boost::filesystem::path& dst)
|
||||||
|
{
|
||||||
|
namespace fs = boost::filesystem;
|
||||||
|
fs::create_directories(dst);
|
||||||
|
for (fs::directory_iterator it(src), end; it != end; ++it) {
|
||||||
|
fs::path dstChild = dst / it->path().filename();
|
||||||
|
if (fs::is_directory(it->path())) {
|
||||||
|
copyDirectoryRecursive(it->path(), dstChild);
|
||||||
|
} else {
|
||||||
|
fs::copy_file(it->path(), dstChild, fs::copy_options::overwrite_existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IntroDialog::migrateDataDirectory(const QString& oldPath, const QString& newPath)
|
||||||
|
{
|
||||||
|
namespace fs = boost::filesystem;
|
||||||
|
|
||||||
|
fs::path srcDir(oldPath.toStdString());
|
||||||
|
fs::path dstDir(newPath.toStdString());
|
||||||
|
|
||||||
|
if (!fs::exists(srcDir) || !fs::is_directory(srcDir))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Create destination directory
|
||||||
|
try {
|
||||||
|
fs::create_directories(dstDir);
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
printf("Migration: Cannot create destination directory: %s\n", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check free space
|
||||||
|
try {
|
||||||
|
quint64 srcSize = 0;
|
||||||
|
for (fs::recursive_directory_iterator it(srcDir), end; it != end; ++it) {
|
||||||
|
if (fs::is_regular_file(*it))
|
||||||
|
srcSize += fs::file_size(*it);
|
||||||
|
}
|
||||||
|
fs::space_info si = fs::space(dstDir);
|
||||||
|
if (si.available < srcSize + (50 * 1024 * 1024)) { // 50MB headroom
|
||||||
|
printf("Migration: Insufficient disk space. Need %llu, have %llu\n",
|
||||||
|
(unsigned long long)srcSize, (unsigned long long)si.available);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
printf("Migration: Cannot check disk space: %s\n", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files/directories to skip during copy
|
||||||
|
static const std::set<std::string> skipFiles = {
|
||||||
|
".lock",
|
||||||
|
"debug.log",
|
||||||
|
"db.log",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show progress dialog
|
||||||
|
QProgressDialog progress("Moving data directory...", QString(), 0, 0, 0);
|
||||||
|
progress.setWindowTitle("Triangles - Data Migration");
|
||||||
|
progress.setWindowModality(Qt::ApplicationModal);
|
||||||
|
progress.setMinimumDuration(0);
|
||||||
|
progress.setCancelButton(0);
|
||||||
|
progress.show();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Phase 1: Copy wallet.dat FIRST (most critical file)
|
||||||
|
fs::path walletSrc = srcDir / "wallet.dat";
|
||||||
|
fs::path walletDst = dstDir / "wallet.dat";
|
||||||
|
if (fs::exists(walletSrc)) {
|
||||||
|
progress.setLabelText("Copying wallet.dat...");
|
||||||
|
QApplication::processEvents();
|
||||||
|
try {
|
||||||
|
// Copy to temp name first, then rename for atomicity
|
||||||
|
fs::path walletTmp = dstDir / "wallet.dat.migrating";
|
||||||
|
fs::copy_file(walletSrc, walletTmp, fs::copy_options::overwrite_existing);
|
||||||
|
|
||||||
|
// Verify copy by checking file size
|
||||||
|
if (fs::file_size(walletTmp) != fs::file_size(walletSrc)) {
|
||||||
|
fs::remove(walletTmp);
|
||||||
|
printf("Migration: wallet.dat copy size mismatch!\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename into place
|
||||||
|
if (fs::exists(walletDst))
|
||||||
|
fs::remove(walletDst);
|
||||||
|
fs::rename(walletTmp, walletDst);
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
printf("Migration: Failed to copy wallet.dat: %s\n", e.what());
|
||||||
|
return false; // Abort - wallet is critical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Copy everything else
|
||||||
|
int filesCopied = 0;
|
||||||
|
try {
|
||||||
|
for (fs::directory_iterator it(srcDir), end; it != end; ++it) {
|
||||||
|
std::string filename = it->path().filename().string();
|
||||||
|
|
||||||
|
// Skip special files
|
||||||
|
if (skipFiles.count(filename))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip wallet.dat (already copied)
|
||||||
|
if (filename == "wallet.dat")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
fs::path dst = dstDir / filename;
|
||||||
|
|
||||||
|
progress.setLabelText(QString("Copying %1...").arg(QString::fromStdString(filename)));
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
if (fs::is_directory(it->path())) {
|
||||||
|
copyDirectoryRecursive(it->path(), dst);
|
||||||
|
} else {
|
||||||
|
fs::copy_file(it->path(), dst, fs::copy_options::overwrite_existing);
|
||||||
|
}
|
||||||
|
filesCopied++;
|
||||||
|
}
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
// Non-wallet copy failure: log but don't abort
|
||||||
|
// Chain data can be re-synced; wallet was already safely copied
|
||||||
|
printf("Migration: Warning: failed to copy some files: %s\n", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Rename old wallet.dat as safety backup (don't delete old dir)
|
||||||
|
try {
|
||||||
|
if (fs::exists(walletSrc)) {
|
||||||
|
fs::rename(walletSrc, srcDir / "wallet.dat.bak-migrated");
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// Not critical
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.close();
|
||||||
|
printf("Migration: Successfully copied %d items from %s to %s\n",
|
||||||
|
filesCopied, srcDir.string().c_str(), dstDir.string().c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ public:
|
|||||||
*/
|
*/
|
||||||
static bool pickDataDirectory();
|
static bool pickDataDirectory();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate data directory contents from oldPath to newPath.
|
||||||
|
* Returns true on success, false on failure.
|
||||||
|
* Shows a progress dialog during the copy.
|
||||||
|
*/
|
||||||
|
static bool migrateDataDirectory(const QString& oldPath, const QString& newPath);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void on_browseButton_clicked();
|
void on_browseButton_clicked();
|
||||||
void on_defaultRadio_toggled(bool checked);
|
void on_defaultRadio_toggled(bool checked);
|
||||||
|
|||||||
+202
-1
@@ -7,12 +7,21 @@
|
|||||||
#include "optionsmodel.h"
|
#include "optionsmodel.h"
|
||||||
#include "dialog_move_handler.h"
|
#include "dialog_move_handler.h"
|
||||||
|
|
||||||
|
#include "init.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QGroupBox>
|
||||||
#include <QIntValidator>
|
#include <QIntValidator>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QProcess>
|
||||||
#include <QRegExp>
|
#include <QRegExp>
|
||||||
#include <QRegExpValidator>
|
#include <QRegExpValidator>
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
OptionsDialog::OptionsDialog(QWidget *parent) :
|
OptionsDialog::OptionsDialog(QWidget *parent) :
|
||||||
QDialog(parent),
|
QDialog(parent),
|
||||||
@@ -21,12 +30,54 @@ OptionsDialog::OptionsDialog(QWidget *parent) :
|
|||||||
mapper(0),
|
mapper(0),
|
||||||
fRestartWarningDisplayed_Proxy(false),
|
fRestartWarningDisplayed_Proxy(false),
|
||||||
fRestartWarningDisplayed_Lang(false),
|
fRestartWarningDisplayed_Lang(false),
|
||||||
fProxyIpValid(true)
|
fProxyIpValid(true),
|
||||||
|
dataDirPath(0),
|
||||||
|
dataDirFreeSpaceLabel(0)
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Window);
|
setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Window);
|
||||||
ui->wCaption->installEventFilter(new DialogMoveHandler(this));
|
ui->wCaption->installEventFilter(new DialogMoveHandler(this));
|
||||||
|
|
||||||
|
/* Data Directory section in Main tab */
|
||||||
|
m_currentDataDir = QString::fromStdString(GetDataDir(false).string());
|
||||||
|
m_pendingDataDir.clear();
|
||||||
|
|
||||||
|
QGroupBox *groupDataDir = new QGroupBox(tr("Data Directory"), this);
|
||||||
|
groupDataDir->setStyleSheet(
|
||||||
|
"QGroupBox { border: 1px solid #61280E; margin-top: 8px; padding-top: 16px; color: #f26522; }"
|
||||||
|
"QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; }");
|
||||||
|
|
||||||
|
QVBoxLayout *dataDirLayout = new QVBoxLayout(groupDataDir);
|
||||||
|
|
||||||
|
QHBoxLayout *dataDirPathLayout = new QHBoxLayout();
|
||||||
|
dataDirPath = new QLineEdit(m_currentDataDir, groupDataDir);
|
||||||
|
dataDirPath->setReadOnly(true);
|
||||||
|
dataDirPath->setStyleSheet("QLineEdit { background-color: #1c1c1c; border: 1px solid #f26522; color: #f26522; padding: 2px; }");
|
||||||
|
|
||||||
|
QPushButton *dataDirBrowseButton = new QPushButton(tr("Browse..."), groupDataDir);
|
||||||
|
dataDirBrowseButton->setStyleSheet(
|
||||||
|
"QPushButton { background-color: #000; color: #f26522; border: 1px solid #f26522; padding: 2px 12px; min-height: 20px; }"
|
||||||
|
"QPushButton:hover { background-color: #61280E; }"
|
||||||
|
"QPushButton:pressed:flat { color: #000; background-color: #f26522; }");
|
||||||
|
|
||||||
|
dataDirPathLayout->addWidget(dataDirPath);
|
||||||
|
dataDirPathLayout->addWidget(dataDirBrowseButton);
|
||||||
|
dataDirLayout->addLayout(dataDirPathLayout);
|
||||||
|
|
||||||
|
dataDirFreeSpaceLabel = new QLabel(groupDataDir);
|
||||||
|
dataDirFreeSpaceLabel->setStyleSheet("color: #999; font-size: 11px;");
|
||||||
|
dataDirLayout->addWidget(dataDirFreeSpaceLabel);
|
||||||
|
|
||||||
|
// Insert into Main tab layout, before the vertical spacer (last item)
|
||||||
|
QVBoxLayout *mainTabLayout = qobject_cast<QVBoxLayout*>(ui->tabWidget->widget(0)->layout());
|
||||||
|
if (mainTabLayout) {
|
||||||
|
int spacerIndex = mainTabLayout->count() - 1; // vertical spacer is last
|
||||||
|
mainTabLayout->insertWidget(spacerIndex, groupDataDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(dataDirBrowseButton, SIGNAL(clicked()), this, SLOT(on_dataDirBrowseButton_clicked()));
|
||||||
|
updateDataDirFreeSpace();
|
||||||
|
|
||||||
/* Network elements init */
|
/* Network elements init */
|
||||||
#ifndef USE_UPNP
|
#ifndef USE_UPNP
|
||||||
ui->mapPortUpnp->setEnabled(false);
|
ui->mapPortUpnp->setEnabled(false);
|
||||||
@@ -188,6 +239,8 @@ void OptionsDialog::setSaveButtonState(bool fState)
|
|||||||
void OptionsDialog::on_okButton_clicked()
|
void OptionsDialog::on_okButton_clicked()
|
||||||
{
|
{
|
||||||
mapper->submit();
|
mapper->submit();
|
||||||
|
if (handleDataDirChange())
|
||||||
|
return; // restart flow handles closing
|
||||||
accept();
|
accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +252,7 @@ void OptionsDialog::on_cancelButton_clicked()
|
|||||||
void OptionsDialog::on_applyButton_clicked()
|
void OptionsDialog::on_applyButton_clicked()
|
||||||
{
|
{
|
||||||
mapper->submit();
|
mapper->submit();
|
||||||
|
handleDataDirChange();
|
||||||
disableApplyButton();
|
disableApplyButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,3 +357,150 @@ bool OptionsDialog::eventFilter(QObject *object, QEvent *event)
|
|||||||
}
|
}
|
||||||
return QDialog::eventFilter(object, event);
|
return QDialog::eventFilter(object, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OptionsDialog::on_dataDirBrowseButton_clicked()
|
||||||
|
{
|
||||||
|
QString dir = QFileDialog::getExistingDirectory(
|
||||||
|
this, tr("Choose data directory"), m_currentDataDir);
|
||||||
|
if (!dir.isEmpty() && dir != m_currentDataDir)
|
||||||
|
{
|
||||||
|
m_pendingDataDir = dir;
|
||||||
|
dataDirPath->setText(dir);
|
||||||
|
updateDataDirFreeSpace();
|
||||||
|
enableApplyButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsDialog::updateDataDirFreeSpace()
|
||||||
|
{
|
||||||
|
namespace fs = boost::filesystem;
|
||||||
|
QString path = dataDirPath->text();
|
||||||
|
fs::path fsPath(path.toStdString());
|
||||||
|
try {
|
||||||
|
while (!fsPath.empty() && !fs::exists(fsPath))
|
||||||
|
fsPath = fsPath.parent_path();
|
||||||
|
if (!fsPath.empty()) {
|
||||||
|
fs::space_info si = fs::space(fsPath);
|
||||||
|
double freeGB = (double)si.available / (1024.0 * 1024.0 * 1024.0);
|
||||||
|
dataDirFreeSpaceLabel->setText(
|
||||||
|
tr("Free space: %1 GB").arg(QString::number(freeGB, 'f', 2)));
|
||||||
|
} else {
|
||||||
|
dataDirFreeSpaceLabel->setText(tr("Cannot determine free space"));
|
||||||
|
}
|
||||||
|
} catch (const fs::filesystem_error &) {
|
||||||
|
dataDirFreeSpaceLabel->setText(tr("Cannot determine free space"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quint64 OptionsDialog::calculateDirSize(const QString& path)
|
||||||
|
{
|
||||||
|
namespace fs = boost::filesystem;
|
||||||
|
quint64 totalSize = 0;
|
||||||
|
try {
|
||||||
|
for (fs::recursive_directory_iterator it(path.toStdString()), end; it != end; ++it) {
|
||||||
|
if (fs::is_regular_file(*it))
|
||||||
|
totalSize += fs::file_size(*it);
|
||||||
|
}
|
||||||
|
} catch (...) {}
|
||||||
|
return totalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OptionsDialog::handleDataDirChange()
|
||||||
|
{
|
||||||
|
if (m_pendingDataDir.isEmpty() || m_pendingDataDir == m_currentDataDir)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
namespace fs = boost::filesystem;
|
||||||
|
fs::path destPath(m_pendingDataDir.toStdString());
|
||||||
|
|
||||||
|
// Check destination is writable
|
||||||
|
try {
|
||||||
|
fs::create_directories(destPath);
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
QMessageBox::critical(this, tr("Error"),
|
||||||
|
tr("Cannot create directory: %1").arg(QString::fromStdString(e.what())));
|
||||||
|
m_pendingDataDir.clear();
|
||||||
|
dataDirPath->setText(m_currentDataDir);
|
||||||
|
updateDataDirFreeSpace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check free space vs current data dir size
|
||||||
|
quint64 dataDirSize = calculateDirSize(m_currentDataDir);
|
||||||
|
try {
|
||||||
|
fs::space_info si = fs::space(destPath);
|
||||||
|
quint64 required = dataDirSize + (dataDirSize / 10); // 10% headroom
|
||||||
|
if (si.available < required) {
|
||||||
|
QMessageBox::critical(this, tr("Insufficient Space"),
|
||||||
|
tr("The destination has %1 MB free but the data directory requires approximately %2 MB.")
|
||||||
|
.arg(si.available / (1024*1024))
|
||||||
|
.arg(required / (1024*1024)));
|
||||||
|
m_pendingDataDir.clear();
|
||||||
|
dataDirPath->setText(m_currentDataDir);
|
||||||
|
updateDataDirFreeSpace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (const fs::filesystem_error&) {
|
||||||
|
// If we can't check space, proceed anyway
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save migration state to QSettings
|
||||||
|
QSettings settings;
|
||||||
|
settings.setValue("strDataDirPrevious", m_currentDataDir);
|
||||||
|
settings.setValue("strDataDir", m_pendingDataDir);
|
||||||
|
settings.setValue("fPendingDataDirMigration", true);
|
||||||
|
|
||||||
|
// Ask about restart
|
||||||
|
QMessageBox msgBox(this);
|
||||||
|
msgBox.setWindowFlags(Qt::FramelessWindowHint);
|
||||||
|
msgBox.setWindowTitle(tr("Data Directory Changed"));
|
||||||
|
msgBox.setText(tr("The data directory will be moved from:\n%1\n\nTo:\n%2\n\n"
|
||||||
|
"This will happen when the wallet restarts.")
|
||||||
|
.arg(m_currentDataDir).arg(m_pendingDataDir));
|
||||||
|
msgBox.setIcon(QMessageBox::Information);
|
||||||
|
msgBox.setIconPixmap(QPixmap(":/msgbox/information"));
|
||||||
|
msgBox.setStyleSheet("QMessageBox { border: 2px solid #f26522; background-color: #000; color: #f26522; }");
|
||||||
|
|
||||||
|
QPushButton *restartBtn = msgBox.addButton(tr("Restart Now"), QMessageBox::AcceptRole);
|
||||||
|
QPushButton *laterBtn = msgBox.addButton(tr("Later"), QMessageBox::RejectRole);
|
||||||
|
|
||||||
|
QString btnStyle =
|
||||||
|
"QPushButton { background-color: #000; color: #f26522; border: 1px solid #f26522; "
|
||||||
|
"min-width: 120px; max-width: 120px; max-height: 20px; min-height: 20px; }"
|
||||||
|
"QPushButton:hover { background-color: #61280E; }"
|
||||||
|
"QPushButton:pressed:flat { color: #000; background-color: #f26522; }";
|
||||||
|
restartBtn->setStyleSheet(btnStyle);
|
||||||
|
laterBtn->setStyleSheet(btnStyle);
|
||||||
|
|
||||||
|
msgBox.exec();
|
||||||
|
|
||||||
|
if (msgBox.clickedButton() == restartBtn) {
|
||||||
|
performRestart();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsDialog::performRestart()
|
||||||
|
{
|
||||||
|
// Launch a new instance of ourselves
|
||||||
|
QString exePath = QApplication::applicationFilePath();
|
||||||
|
QStringList args = QApplication::arguments();
|
||||||
|
args.removeFirst(); // remove argv[0]
|
||||||
|
|
||||||
|
// Remove any existing -datadir argument so the new instance
|
||||||
|
// reads strDataDir from QSettings and performs migration
|
||||||
|
QMutableStringListIterator it(args);
|
||||||
|
while (it.hasNext()) {
|
||||||
|
QString arg = it.next();
|
||||||
|
if (arg.startsWith("-datadir") || arg.startsWith("/datadir"))
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new process detached so it survives our shutdown
|
||||||
|
QProcess::startDetached(exePath, args);
|
||||||
|
|
||||||
|
// Close dialog and trigger wallet shutdown
|
||||||
|
accept();
|
||||||
|
StartShutdown();
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
|
|
||||||
|
class QLineEdit;
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class OptionsDialog;
|
class OptionsDialog;
|
||||||
}
|
}
|
||||||
@@ -45,17 +48,29 @@ private slots:
|
|||||||
void updateDisplayUnit();
|
void updateDisplayUnit();
|
||||||
void handleProxyIpValid(QValidatedLineEdit *object, bool fState);
|
void handleProxyIpValid(QValidatedLineEdit *object, bool fState);
|
||||||
void applyTorDefaults(bool enabled);
|
void applyTorDefaults(bool enabled);
|
||||||
|
void on_dataDirBrowseButton_clicked();
|
||||||
|
void updateDataDirFreeSpace();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void proxyIpValid(QValidatedLineEdit *object, bool fValid);
|
void proxyIpValid(QValidatedLineEdit *object, bool fValid);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool handleDataDirChange();
|
||||||
|
void performRestart();
|
||||||
|
quint64 calculateDirSize(const QString& path);
|
||||||
|
|
||||||
Ui::OptionsDialog *ui;
|
Ui::OptionsDialog *ui;
|
||||||
OptionsModel *model;
|
OptionsModel *model;
|
||||||
MonitoredDataMapper *mapper;
|
MonitoredDataMapper *mapper;
|
||||||
bool fRestartWarningDisplayed_Proxy;
|
bool fRestartWarningDisplayed_Proxy;
|
||||||
bool fRestartWarningDisplayed_Lang;
|
bool fRestartWarningDisplayed_Lang;
|
||||||
bool fProxyIpValid;
|
bool fProxyIpValid;
|
||||||
|
|
||||||
|
// Data directory widgets (built programmatically)
|
||||||
|
QLineEdit *dataDirPath;
|
||||||
|
QLabel *dataDirFreeSpaceLabel;
|
||||||
|
QString m_currentDataDir;
|
||||||
|
QString m_pendingDataDir;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // OPTIONSDIALOG_H
|
#endif // OPTIONSDIALOG_H
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
#include "net.h"
|
#include "net.h"
|
||||||
|
#include "addrman.h"
|
||||||
#include "trianglesrpc.h"
|
#include "trianglesrpc.h"
|
||||||
#include "alert.h"
|
#include "alert.h"
|
||||||
#include "wallet.h"
|
#include "wallet.h"
|
||||||
@@ -199,3 +200,28 @@ Value sendalert(const Array& params, bool fHelp)
|
|||||||
result.push_back(Pair("nCancel", alert.nCancel));
|
result.push_back(Pair("nCancel", alert.nCancel));
|
||||||
return result;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ static const CRPCCommand vRPCCommands[] =
|
|||||||
{ "getblockchaininfo", &getblockchaininfo, true, false },
|
{ "getblockchaininfo", &getblockchaininfo, true, false },
|
||||||
{ "getwalletinfo", &getwalletinfo, true, false },
|
{ "getwalletinfo", &getwalletinfo, true, false },
|
||||||
{ "getnetworkinfo", &getnetworkinfo, true, false },
|
{ "getnetworkinfo", &getnetworkinfo, true, false },
|
||||||
|
{ "getseedlist", &getseedlist, true, false },
|
||||||
{ "gettxoutsetinfo", &gettxoutsetinfo, true, false },
|
{ "gettxoutsetinfo", &gettxoutsetinfo, true, false },
|
||||||
{ "estimatefee", &estimatefee, true, false },
|
{ "estimatefee", &estimatefee, true, false },
|
||||||
{ "getaddressbalance", &getaddressbalance, true, false },
|
{ "getaddressbalance", &getaddressbalance, true, false },
|
||||||
|
|||||||
@@ -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 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 getpeerinfo(const json_spirit::Array& params, bool fHelp);
|
||||||
extern json_spirit::Value getnetworkinfo(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 getwalletinfo(const json_spirit::Array& params, bool fHelp);
|
||||||
extern json_spirit::Value dumpwallet(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);
|
extern json_spirit::Value importwallet(const json_spirit::Array& params, bool fHelp);
|
||||||
|
|||||||
+38
-30
@@ -959,39 +959,47 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
|
|||||||
int ret = 0;
|
int ret = 0;
|
||||||
|
|
||||||
CBlockIndex* pindex = pindexStart;
|
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);
|
if (fShutdown)
|
||||||
int nScanned = 0;
|
break;
|
||||||
int nTotal = nBestHeight - (pindexStart ? pindexStart->nHeight : 0);
|
|
||||||
if (nTotal < 1) nTotal = 1;
|
++nScanned;
|
||||||
while (pindex)
|
// Report progress every 500ms to keep UI responsive
|
||||||
|
int64_t nNow = GetTimeMillis();
|
||||||
|
if (nNow - nLastProgressTime > 500)
|
||||||
{
|
{
|
||||||
if (fShutdown)
|
nLastProgressTime = nNow;
|
||||||
break;
|
int nPercent = (nScanned * 100) / nTotal;
|
||||||
|
uiInterface.InitMessage(strprintf(_("Rescanning... %d%%"), nPercent));
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user