Compare commits

...

11 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
Krystie a1b137a7bb Bump version to 5.4.4 - Updated seed nodes
Build All Platforms / build-linux-qt (push) Failing after 7s
Build All Platforms / build-linux-daemon (push) Failing after 1h10m35s
Build All Platforms / test-linux-unit (push) Failing after 1h10m43s
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
2026-03-31 17:07:54 -07:00
Krystie 939606a5f7 Update Docker seed node onion addresses (contabo-de deployment) 2026-03-31 02:32:41 -07:00
Krystie 73cecd90d1 Add v3 onion seed nodes for network bootstrap
Added 5 new .onion v3 seed addresses:
- DNS3 main node
- 4 Docker-based seed nodes running on DNS2

Total seed count: 2 -> 7 onion seeds for improved
network connectivity and peer discovery.
2026-03-30 19:08:43 -07:00
sami7777 c74c92c542 Fix Boost filesystem API for modern Boost (copy_options)
Build All Platforms / test-linux-unit (push) Waiting to run
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-linux-daemon (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
copy_option::overwrite_if_exists was removed in Boost 1.90+,
replaced with copy_options::overwrite_existing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 12:49:17 -07:00
sami7777 97ae675f0a Bump version to v5.4.3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 12:40:33 -07:00
sami7777 b0b591364f Raise MAX_MONEY from 222222 to 2222222
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 12:38:22 -07:00
sami7777 bf257858a4 Add data directory change feature to Options dialog
Adds a "Data Directory" section to Options > Main tab that lets users
browse for a new data directory. On confirmation, files are automatically
migrated to the new location on restart (wallet.dat copied first with
atomic rename for safety). Supports "Restart Now" or "Later" workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 12:38:15 -07:00
19 changed files with 894 additions and 154 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 // 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
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" + " -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
View File
@@ -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
View File
@@ -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
View File
@@ -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");
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}
}; };
+162
View File
@@ -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;
}
+7
View File
@@ -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
View File
@@ -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();
}
+15
View File
@@ -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
+26
View File
@@ -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;
}
+1
View File
@@ -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 },
+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 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
View File
@@ -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;
} }