Compare commits

..

16 Commits

Author SHA1 Message Date
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
sami7777 16611efe72 Bump version to v5.4.2
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
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 01:00:43 -07:00
sami7777 1f3deacb7a Fix persistent PoS chain forks with deterministic tiebreaker and tighter timestamps
PoS blocks at the same height have identical difficulty, producing equal chain
trust scores. The old "strictly greater" comparison meant first-seen-wins,
causing permanent forks when nodes received competing blocks in different order.

v5.4 fork (block 2186941) adds:
- Deterministic tiebreaker: equal-trust chains resolve to the lower tip hash
- Tighter time drift: ±3 min (was ±10 min), reducing the competing block window

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 00:59:38 -07:00
SamiAhmed7777 8847571193 Merge pull request #3 from SamiAhmed7777/fix/version-detection
Fix version detection: prioritize exact tag match in genbuild.sh
2026-03-27 23:03:53 -07:00
Krystie a58eb3e9ef Fix version detection: prioritize exact tag match in genbuild.sh
When building from a release tag (e.g. v5.4.1), git describe was finding
the nearest ancestor tag (v5.3.8) instead of the exact tag, resulting in
version strings like 'v5.3.8-9-gdfb4b22' instead of 'v5.4.1'.

Now genbuild.sh tries --exact-match first, falling back to distance-based
describe only when not on a tagged commit.
2026-03-27 23:02:04 -07:00
sami7777 dfb4b221dd Fix shutdown race conditions causing bad_weak_ptr crash (v5.4.1)
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
Fixes multiple concurrency bugs exposed during shutdown when Tor proxy
connections are failing:

- Reorder shutdown: stop network threads before destroying Tor V3 services
- Make RPC listener responsive to fShutdown (poll_one+sleep vs blocking run_one)
- Wrap StopRequests() in try/catch and drain io_service on exit
- Fix leaked CNode AddRef in ThreadSocketHandler2 and ThreadMessageHandler2
  (return→break so Release loop executes)
- Guard vNodes.size() read with cs_vNodes lock (data race)
- Guard Qt UI signal callbacks with fShutdown check (use-after-free)
- Add cs_vNodes lock in CNetCleanup global destructor
- Force-disconnect remaining nodes in StopNode() after threads stop
- Make Tor maintenance thread sleep in 500ms intervals for prompt shutdown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 21:57:34 -07:00
sami7777 b6013edbe4 Suppress UI transaction notifications during initial block download
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
During IBD, every wallet transaction triggers NotifyTransactionChanged
which repaints the Qt transaction list. With thousands of staking
rewards across 2M blocks, this floods the event loop and makes the
wallet appear frozen ("not responding") for hours.

Skip NotifyTransactionChanged during IsInitialBlockDownload(). The UI
catches up naturally via refreshWallet() once sync completes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 02:54:31 -07:00
sami7777 d42c5aa799 Fix CService constructor ambiguity in GetEffectiveTorProxy()
Cast GetArg() return (int64_t) to unsigned short for the port
parameter to resolve overload ambiguity across all platforms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 01:26:46 -07:00
sami7777 e8b2339339 Bump version to v5.4.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 01:03:53 -07:00
sami7777 d242c2f37e Tor v3: fix hidden service backend, add key persistence and health monitoring
Codex changes: delegate hidden service management to the actual Tor
backend instead of generating keys the wallet never served. The new
AttachToBackendService() reads the hostname Tor creates, and the
torrc/process plumbing properly gates HiddenService directives behind
the -torhiddenservice flag.

Additional fixes:
- Back up hs_ed25519_secret_key (96 bytes) to wallet.dat so the onion
  identity survives deletion of tor_data/
- Restore the key before Tor starts so the same .onion address is
  regenerated automatically
- Add ThreadTorMaintenance: checks Tor health every 30s, auto-restarts
  with exponential backoff on crash, re-attaches the hidden service
  and re-registers the onion address with AddLocal()
- Seeder maintenance: every 30 min re-announces to peers and refreshes
  known seeder lists (when -torseeder is enabled)
- Clean up ScheduleSeederReannouncement() stub (real work now in thread)
- Respect -torsocks port in onion proxy registration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 00:27:22 -07:00
21 changed files with 852 additions and 135 deletions
Regular → Executable
+7 -2
View File
@@ -15,8 +15,13 @@ if [ -e "$(which git)" ]; then
# clean 'dirty' status of touched files that haven't been modified
git diff >/dev/null 2>/dev/null
# get a string like "v0.6.0-66-g59887e8-dirty"
DESC="$(git describe --dirty 2>/dev/null)"
# Try exact tag match first (when building from a release tag)
DESC="$(git describe --tags --exact-match 2>/dev/null)"
# If no exact match, fall back to git describe with commit distance
if [ -z "$DESC" ]; then
DESC="$(git describe --tags --dirty 2>/dev/null)"
fi
# get a string like "2012-04-10 16:27:19 +0200"
TIME="$(git log -n 1 --format="%ci")"
+2 -2
View File
@@ -7,8 +7,8 @@
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
#define CLIENT_VERSION_MAJOR 5
#define CLIENT_VERSION_MINOR 3
#define CLIENT_VERSION_REVISION 9
#define CLIENT_VERSION_MINOR 4
#define CLIENT_VERSION_REVISION 4
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
+59 -10
View File
@@ -176,6 +176,12 @@ void Shutdown(void* parg)
}
SecureMsgShutdown();
// Stop network threads FIRST so nothing references Tor objects
nTransactionsUpdated++;
StopNode();
// NOW safe to destroy Tor state - all threads have stopped
ShutdownTorV3();
StopEmbeddedTor();
@@ -194,10 +200,8 @@ void Shutdown(void* parg)
pNotificationQueue = NULL;
}
nTransactionsUpdated++;
// CTxDB().Close();
bitdb.Flush(false);
StopNode();
bitdb.Flush(true);
fs::remove(GetPidFile());
UnregisterWallet(pwalletMain);
@@ -355,6 +359,7 @@ std::string HelpMessage()
" -tor=<ip:port> " + _("Use proxy to reach tor hidden services (default: same as -proxy)") + "\n"
" -notor " + _("Disable Tor startup and .onion connectivity") + "\n" +
" -torsocks=<port> " + _("Set embedded or managed Tor SOCKS proxy port (default: 19099)") + "\n" +
" -torhiddenservice " + _("Enable the managed Tor hidden service (default: 1)") + "\n" +
" -torhsport=<port> " + _("Set embedded or managed Tor hidden service port (default: wallet listen port)") + "\n" +
//" -dns " + _("Allow DNS lookups for -addnode, -seednode and -connect") + "\n" +
" -port=<port> " + _("Listen for connections on <port> (default: 24112 or testnet: 24111)") + "\n" +
@@ -752,7 +757,7 @@ bool AppInit2()
// Tor proxy: always configured for .onion connectivity
CService addrOnion;
unsigned short const onion_port = 19099;
unsigned short const onion_port = static_cast<unsigned short>(GetArg("-torsocks", 19099));
if (mapArgs.count("-tor") && mapArgs["-tor"] != "0") {
addrOnion = CService(mapArgs["-tor"], onion_port);
@@ -1078,6 +1083,37 @@ bool AppInit2()
uiInterface.InitMessage(_("Starting Tor..."));
printf("Starting Tor process...\n");
// Restore hidden service secret key from wallet backup if the key
// file is missing on disk. This preserves the .onion identity even
// if the tor_data directory was deleted.
if (pwalletMain && !GetBoolArg("-notor", false)) {
std::string restoreDataPath = GetArg("-tordatadir", (GetDataDir() / "tor_data").string());
fs::path secretKeyPath = fs::path(restoreDataPath) / "hidden_service" / "hs_ed25519_secret_key";
if (!fs::exists(secretKeyPath)) {
CWalletDB walletdb(pwalletMain->strWalletFile);
std::vector<unsigned char> backedUpKey;
if (walletdb.ReadSetting("tor_v3_hs_secret_key_backup", backedUpKey) &&
backedUpKey.size() == 96) {
fs::create_directories(secretKeyPath.parent_path());
std::ofstream keyFile(secretKeyPath.string().c_str(), std::ios::binary);
if (keyFile.is_open()) {
keyFile.write(reinterpret_cast<const char*>(backedUpKey.data()),
backedUpKey.size());
keyFile.close();
printf("Restored Tor hidden service secret key from wallet backup\n");
} else {
printf("WARNING: Failed to write restored hs_ed25519_secret_key to %s\n",
secretKeyPath.string().c_str());
}
}
OPENSSL_cleanse(backedUpKey.data(), backedUpKey.size());
}
}
int64_t nTorStart = GetTimeMillis();
bool torStarted = StartEmbeddedTor();
StartupPerfLog("tor_start", GetTimeMillis() - nTorStart, strprintf("started=%d", torStarted));
@@ -1100,13 +1136,14 @@ bool AppInit2()
int64_t nTorIdentityStart = GetTimeMillis();
LoadTorV3Config();
TorV3Config& torConfig = GetTorV3Config();
torConfig.enableTor = true;
torConfig.enableHiddenService = true;
torConfig.hiddenServicePort = GetListenPort();
torConfig.enableTor = torStarted;
torConfig.enableHiddenService = torStarted && CTorEmbedded::GetInstance()->IsHiddenServiceEnabled();
torConfig.hiddenServicePort = CTorEmbedded::GetInstance()->GetHiddenServicePort();
torConfig.torDataDirectory = torDataPath;
std::string onionAddr;
if (InitTorV3()) {
string onionAddr = CTorV3Manager::GetInstance()->GetWalletOnionAddress();
if (torConfig.enableTor && torConfig.enableHiddenService && InitTorV3()) {
onionAddr = CTorV3Manager::GetInstance()->GetWalletOnionAddress();
if (!onionAddr.empty()) {
// Write onion/hostname for compatibility with existing code paths
fs::path onionDir = GetDataDir() / "onion";
@@ -1118,11 +1155,15 @@ bool AppInit2()
}
// Register onion address as local address for peer discovery
AddLocal(CService(onionAddr, GetListenPort(), fNameLookup), LOCAL_MANUAL);
AddLocal(CService(onionAddr, torConfig.hiddenServicePort, fNameLookup), LOCAL_MANUAL);
printf("Tor V3 identity: %s\n", onionAddr.c_str());
} else {
printf("WARNING: Tor V3 initialized but no onion address available\n");
}
} else if (torStarted && !torConfig.enableHiddenService) {
printf("Tor hidden service disabled by configuration\n");
} else if (!torStarted) {
printf("Skipping Tor V3 identity because the Tor backend is unavailable\n");
} else {
printf("WARNING: Failed to initialize Tor V3 identity\n");
}
@@ -1139,13 +1180,21 @@ bool AppInit2()
while (!torOnion.empty() && (torOnion.back() == '\n' || torOnion.back() == '\r' || torOnion.back() == ' '))
torOnion.pop_back();
if (!torOnion.empty()) {
AddLocal(CService(torOnion, GetListenPort(), fNameLookup), LOCAL_MANUAL);
if (torOnion != onionAddr) {
AddLocal(CService(torOnion, torConfig.hiddenServicePort, fNameLookup), LOCAL_MANUAL);
}
printf("Tor hidden service (from Tor process): %s\n", torOnion.c_str());
}
}
}
}
StartupPerfLog("tor_setup_total", GetTimeMillis() - nTorStart);
// Launch background thread for Tor health monitoring and seeder maintenance
if (torStarted) {
if (!NewThread(ThreadTorMaintenance, NULL))
printf("Warning: ThreadTorMaintenance could not be started\n");
}
}
// ********************************************************* Step 9: import blocks
+13 -3
View File
@@ -2568,7 +2568,17 @@ bool CBlock::AddToBlockIndex(unsigned int nFile, unsigned int nBlockPos, const u
// New best — keep the batch open so SetBestChain can add ConnectBlock
// writes to the same transaction, cutting the per-block commit count in half.
// v5.4: deterministic tiebreaker — when two chains have equal trust,
// all nodes agree on the one whose tip has the lower block hash.
// This prevents permanent forks from PoS blocks with identical difficulty.
bool fNewBest = false;
if (pindexNew->nChainTrust > nBestChainTrust)
fNewBest = true;
else if (pindexNew->nChainTrust == nBestChainTrust && pindexBest &&
pindexNew->GetBlockHash() < pindexBest->GetBlockHash())
fNewBest = true;
if (fNewBest)
{
if (!SetBestChain(txdb, pindexNew))
return false;
@@ -2719,7 +2729,7 @@ bool CBlock::AcceptBlock()
if (nHeight % 10000 == 0 || nHeight > 2186900)
printf("ProcessBlock(): Check proof-of-stake/work OK for block %d\n", nHeight);
// Check timestamp against prev
if (GetBlockTime() <= pindexPrev->GetPastTimeLimit() || FutureDrift(GetBlockTime()) < pindexPrev->GetBlockTime())
if (GetBlockTime() <= pindexPrev->GetPastTimeLimit() || FutureDrift(GetBlockTime(), nHeight) < pindexPrev->GetBlockTime())
return error("AcceptBlock() : block's timestamp is too early");
// Check that all transactions are finalized
@@ -2999,13 +3009,13 @@ bool CBlock::SignBlock(CWallet& wallet, int64_t nFees)
{
if (wallet.CreateCoinStake(wallet, nBits, nSearchTime-nLastCoinStakeSearchTime, nFees, txCoinStake, key))
{
if (txCoinStake.nTime >= max(pindexBest->GetPastTimeLimit()+1, PastDrift(pindexBest->GetBlockTime())))
if (txCoinStake.nTime >= max(pindexBest->GetPastTimeLimit()+1, PastDrift(pindexBest->GetBlockTime(), pindexBest->nHeight + 1)))
{
// make sure coinstake would meet timestamp protocol
// as it would be the same as the block timestamp
vtx[0].nTime = nTime = txCoinStake.nTime;
nTime = max(pindexBest->GetPastTimeLimit()+1, GetMaxTransactionTime());
nTime = max(GetBlockTime(), PastDrift(pindexBest->GetBlockTime()));
nTime = max(GetBlockTime(), PastDrift(pindexBest->GetBlockTime(), pindexBest->nHeight + 1));
// we have to make sure that we have no future timestamps in
// our transactions set
+7 -3
View File
@@ -29,6 +29,7 @@ class CNode;
static const int CUTOFF_POW_BLOCK = 9000;
static const int CRAPCHAIN_CUTOFF_BLOCK = 17691; // pre-Pharao (version 4) blockchain until block 17691
static const int FORK_HEIGHT_V5 = 17651; // v5 hard fork: decentralization + Tor v3 (next block after last checkpoint)
static const int FORK_HEIGHT_V5_4 = 2186941; // v5.4: tighter timestamps, deterministic fork resolution
static const unsigned int MAX_BLOCK_SIZE = 1000000;
static const unsigned int MAX_BLOCK_SIZE_GEN = MAX_BLOCK_SIZE/2;
@@ -39,7 +40,7 @@ static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 4000;
static const unsigned int MAX_INV_SZ = 50000;
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 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 MAX_TRI_PROOF_OF_STAKE = 0.33 * COIN;
static const int MODIFIER_INTERVAL_SWITCH = 1;
@@ -56,8 +57,11 @@ static const int fHaveUPnP = false;
static const uint256 hashGenesisBlockOfficial("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
static const uint256 hashGenesisBlockTestNet ("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
inline int64_t PastDrift(int64_t nTime) { return nTime - 10 * 60; } // up to 10 minutes from the past
inline int64_t FutureDrift(int64_t nTime) { return nTime + 10 * 60; } // up to 10 minutes from the future
inline int64_t GetMaxTimeDrift(int nHeight) { return (nHeight >= FORK_HEIGHT_V5_4) ? 3 * 60 : 10 * 60; }
inline int64_t PastDrift(int64_t nTime, int nHeight) { return nTime - GetMaxTimeDrift(nHeight); }
inline int64_t FutureDrift(int64_t nTime, int nHeight) { return nTime + GetMaxTimeDrift(nHeight); }
inline int64_t PastDrift(int64_t nTime) { return PastDrift(nTime, nBestHeight); }
inline int64_t FutureDrift(int64_t nTime) { return FutureDrift(nTime, nBestHeight); }
extern CScript COINBASE_FLAGS;
+1 -1
View File
@@ -365,7 +365,7 @@ CBlock* CreateNewBlock(CWallet* pwallet, bool fProofOfStake, int64_t* pFees)
// Fill in header
pblock->hashPrevBlock = pindexPrev->GetBlockHash();
pblock->nTime = max(pindexPrev->GetPastTimeLimit()+1, pblock->GetMaxTransactionTime());
pblock->nTime = max(pblock->GetBlockTime(), PastDrift(pindexPrev->GetBlockTime()));
pblock->nTime = max(pblock->GetBlockTime(), PastDrift(pindexPrev->GetBlockTime(), pindexPrev->nHeight + 1));
if (!fProofOfStake)
pblock->UpdateTime(pindexPrev);
pblock->nNonce = 0;
+33 -10
View File
@@ -878,10 +878,19 @@ void ThreadSocketHandler2(void* parg)
}
}
}
if (vNodes.size() != nPrevNodeCount)
{
nPrevNodeCount = vNodes.size();
uiInterface.NotifyNumConnectionsChanged(vNodes.size());
// Read vNodes.size() under the lock to avoid data race
unsigned int nNodeCount;
{
LOCK(cs_vNodes);
nNodeCount = vNodes.size();
}
if (nNodeCount != nPrevNodeCount)
{
nPrevNodeCount = nNodeCount;
if (!fShutdown)
uiInterface.NotifyNumConnectionsChanged(nNodeCount);
}
}
@@ -1013,7 +1022,7 @@ void ThreadSocketHandler2(void* parg)
for (CNode* pnode : vNodesCopy)
{
if (fShutdown)
return;
break;
//
// Receive
@@ -1039,7 +1048,7 @@ void ThreadSocketHandler2(void* parg)
if (!pnode->ReceiveMsgBytes(pchBuf, nBytes))
pnode->CloseSocketDisconnect();
pnode->nLastRecv = GetTime();
pnode->nRecvBytes += nBytes;
pnode->nRecvBytes += nBytes;
}
else if (nBytes == 0)
{
@@ -1104,6 +1113,8 @@ void ThreadSocketHandler2(void* parg)
pnode->Release();
}
if (fShutdown)
return;
MilliSleep(10);
}
}
@@ -1832,6 +1843,9 @@ void ThreadMessageHandler2(void* parg)
pnodeTrickle = vNodesCopy[GetRand(vNodesCopy.size())];
for (CNode* pnode : vNodesCopy)
{
if (fShutdown)
break;
// Receive messages
{
TRY_LOCK(pnode->cs_vRecvMsg, lockRecv);
@@ -1839,8 +1853,6 @@ void ThreadMessageHandler2(void* parg)
if (!ProcessMessages(pnode))
pnode->CloseSocketDisconnect();
}
if (fShutdown)
return;
// Send messages
{
@@ -1848,8 +1860,6 @@ void ThreadMessageHandler2(void* parg)
if (lockSend)
SendMessages(pnode, pnode == pnodeTrickle);
}
if (fShutdown)
return;
}
{
@@ -2180,6 +2190,18 @@ bool StopNode()
}
MilliSleep(50);
DumpAddresses();
// Force-disconnect and clean up all remaining nodes now that threads have stopped.
// Close sockets first so any lingering I/O fails immediately.
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
{
pnode->CloseSocketDisconnect();
pnode->Cleanup();
}
}
return true;
}
@@ -2191,7 +2213,8 @@ public:
}
~CNetCleanup()
{
// Close sockets
// Close sockets - acquire lock in case other threads are still winding down
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
if (pnode->hSocket != INVALID_SOCKET)
closesocket(pnode->hSocket);
+10 -4
View File
@@ -1,13 +1,19 @@
#ifndef TRIANGLES_ONIONSEED_H
#define TRIANGLES_ONIONSEED_H
// hidden service seeds
// v5 hard fork: v2 onion seeds removed (Tor v2 deprecated Oct 2021)
// v3 onion seeds will be added when bootstrap nodes are deployed
// v3 onion seeds - bootstrap nodes deployed 2026-03-30
static const char *strMainNetOnionSeed[][1] = {
{"gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion"},
{"futmtrvh6j34t7s6yjdxfia6iwuyfzwh4k5eqfof5kfhoqk3xmi3qoqd.onion"},
// Main nodes
{"gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion"}, // DNS2
{"futmtrvh6j34t7s6yjdxfia6iwuyfzwh4k5eqfof5kfhoqk3xmi3qoqd.onion"}, // Original seed
{"i6tk7soznftvoibtskwlezviskiererhjndpsmrff4kaxw7jnd5izfqd.onion"}, // DNS3
// Docker seed nodes (contabo-de)
{"uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion"}, // seed-1
{"el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion"}, // seed-2
{"sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion"}, // seed-3
{"i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion"}, // seed-4
{NULL}
};
+4 -1
View File
@@ -193,13 +193,16 @@ static void NotifyBlocksChanged(ClientModel *clientmodel)
static void NotifyNumConnectionsChanged(ClientModel *clientmodel, int newNumConnections)
{
// Too noisy: OutputDebugStringF("NotifyNumConnectionsChanged %i\n", newNumConnections);
// Don't queue UI updates during shutdown - ClientModel may be destroyed
// before Qt processes the queued invocation, causing use-after-free.
if (fShutdown) return;
QMetaObject::invokeMethod(clientmodel, "updateNumConnections", Qt::QueuedConnection,
Q_ARG(int, newNumConnections));
}
static void NotifyAlertChanged(ClientModel *clientmodel, const uint256 &hash, ChangeType status)
{
if (fShutdown) return;
OutputDebugStringF("NotifyAlertChanged %s status=%i\n", hash.GetHex().c_str(), status);
QMetaObject::invokeMethod(clientmodel, "updateAlert", Qt::QueuedConnection,
Q_ARG(QString, QString::fromStdString(hash.GetHex())),
+162
View File
@@ -16,6 +16,8 @@
#include <boost/filesystem.hpp>
#include <set>
IntroDialog::IntroDialog(QWidget *parent) :
QDialog(parent)
{
@@ -190,6 +192,24 @@ bool IntroDialog::pickDataDirectory()
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)
QString defaultDir = QString::fromStdString(GetDefaultDataDir().string());
if (dataDir != defaultDir) {
@@ -275,3 +295,145 @@ bool IntroDialog::pickDataDirectory()
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();
/**
* 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:
void on_browseButton_clicked();
void on_defaultRadio_toggled(bool checked);
+202 -1
View File
@@ -7,12 +7,21 @@
#include "optionsmodel.h"
#include "dialog_move_handler.h"
#include "init.h"
#include "util.h"
#include <boost/filesystem.hpp>
#include <QDir>
#include <QFileDialog>
#include <QGroupBox>
#include <QIntValidator>
#include <QLocale>
#include <QMessageBox>
#include <QProcess>
#include <QRegExp>
#include <QRegExpValidator>
#include <QSettings>
OptionsDialog::OptionsDialog(QWidget *parent) :
QDialog(parent),
@@ -21,12 +30,54 @@ OptionsDialog::OptionsDialog(QWidget *parent) :
mapper(0),
fRestartWarningDisplayed_Proxy(false),
fRestartWarningDisplayed_Lang(false),
fProxyIpValid(true)
fProxyIpValid(true),
dataDirPath(0),
dataDirFreeSpaceLabel(0)
{
ui->setupUi(this);
setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Window);
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 */
#ifndef USE_UPNP
ui->mapPortUpnp->setEnabled(false);
@@ -188,6 +239,8 @@ void OptionsDialog::setSaveButtonState(bool fState)
void OptionsDialog::on_okButton_clicked()
{
mapper->submit();
if (handleDataDirChange())
return; // restart flow handles closing
accept();
}
@@ -199,6 +252,7 @@ void OptionsDialog::on_cancelButton_clicked()
void OptionsDialog::on_applyButton_clicked()
{
mapper->submit();
handleDataDirChange();
disableApplyButton();
}
@@ -303,3 +357,150 @@ bool OptionsDialog::eventFilter(QObject *object, QEvent *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>
class QLineEdit;
class QLabel;
namespace Ui {
class OptionsDialog;
}
@@ -45,17 +48,29 @@ private slots:
void updateDisplayUnit();
void handleProxyIpValid(QValidatedLineEdit *object, bool fState);
void applyTorDefaults(bool enabled);
void on_dataDirBrowseButton_clicked();
void updateDataDirFreeSpace();
signals:
void proxyIpValid(QValidatedLineEdit *object, bool fValid);
private:
bool handleDataDirChange();
void performRestart();
quint64 calculateDirSize(const QString& path);
Ui::OptionsDialog *ui;
OptionsModel *model;
MonitoredDataMapper *mapper;
bool fRestartWarningDisplayed_Proxy;
bool fRestartWarningDisplayed_Lang;
bool fProxyIpValid;
// Data directory widgets (built programmatically)
QLineEdit *dataDirPath;
QLabel *dataDirFreeSpaceLabel;
QString m_currentDataDir;
QString m_pendingDataDir;
};
#endif // OPTIONSDIALOG_H
+230 -46
View File
@@ -18,6 +18,7 @@
#endif
#include "onion_v3.h"
#include "tor_embedded.h"
#include "tor_crypto_compat.h"
#include "../util.h"
#include "../net.h"
@@ -59,6 +60,53 @@ extern CWallet* pwalletMain;
CTorV3Manager* CTorV3Manager::instance = nullptr;
static TorV3Config torV3Config;
static boost::filesystem::path GetBackendHiddenServiceDir(const std::string& torDataDir)
{
return boost::filesystem::path(torDataDir) / "hidden_service";
}
static bool ReadTrimmedFirstLine(const boost::filesystem::path& path, std::string& valueOut)
{
valueOut.clear();
std::ifstream file(path.string().c_str());
if (!file.is_open() || !std::getline(file, valueOut)) {
return false;
}
while (!valueOut.empty()) {
const char ch = valueOut[valueOut.size() - 1];
if (ch != '\n' && ch != '\r' && ch != ' ' && ch != '\t') {
break;
}
valueOut.erase(valueOut.size() - 1);
}
return !valueOut.empty();
}
static std::string GetEffectiveTorProxy()
{
proxyType proxy;
if (GetProxy(NET_TOR, proxy)) {
return proxy.first.ToStringIPPort();
}
if (mapArgs.count("-tor") && mapArgs["-tor"] != "0") {
CService torProxy(mapArgs["-tor"], (unsigned short)GetArg("-torsocks", 19099));
if (torProxy.IsValid()) {
return torProxy.ToStringIPPort();
}
}
std::string explicitProxy = GetArg("-torproxy", "");
if (!explicitProxy.empty()) {
return explicitProxy;
}
return strprintf("127.0.0.1:%d", GetArg("-torsocks", 19099));
}
// Utility function for proper base32 encoding (RFC 4648) - Tor variant
std::string EncodeBase32Proper(const unsigned char* data, size_t len)
{
@@ -503,40 +551,95 @@ bool CTorV3Service::LoadFromPrivateKey(const std::string& privKey)
bool CTorV3Service::StartOnionService()
{
if (onionAddress.empty()) {
printf("ERROR: No onion address generated\n");
if (!torV3Config.enableTor || !torV3Config.enableHiddenService) {
printf("ERROR: Tor hidden service backend is disabled\n");
return false;
}
// Use data directory for Tor service files
boost::filesystem::path serviceDir = GetDataDir() / "tor_data" / "triangles_v3";
boost::filesystem::create_directories(serviceDir);
return AttachToBackendService(torV3Config.torDataDirectory, port);
}
// Create Tor configuration for hidden service
std::string torrcContent = strprintf(
"HiddenServiceDir %s\n"
"HiddenServiceVersion 3\n"
"HiddenServicePort %d 127.0.0.1:%d\n",
serviceDir.string().c_str(), port, port
);
// Write torrc file
std::ofstream torrcFile((serviceDir / "torrc").string().c_str());
if (torrcFile.is_open()) {
torrcFile << torrcContent;
torrcFile.close();
bool CTorV3Service::AttachToBackendService(const std::string& torDataDir, int servicePort, int waitSeconds)
{
if (servicePort <= 0 || servicePort > 65535) {
printf("ERROR: Invalid backend hidden service port: %d\n", servicePort);
return false;
}
// Write private key
std::ofstream keyFile((serviceDir / "hs_ed25519_secret_key").string().c_str());
if (keyFile.is_open()) {
keyFile << "== ed25519v1-secret: type0 ==\n";
keyFile << privateKey << "\n";
keyFile.close();
if (torDataDir.empty()) {
printf("ERROR: Tor data directory is empty\n");
return false;
}
port = servicePort;
const boost::filesystem::path serviceDir = GetBackendHiddenServiceDir(torDataDir);
const boost::filesystem::path hostnamePath = serviceDir / "hostname";
std::string backendOnion;
for (int waited = 0; waited <= waitSeconds; ++waited) {
if (boost::filesystem::exists(hostnamePath) &&
ReadTrimmedFirstLine(hostnamePath, backendOnion)) {
break;
}
if (waited == waitSeconds) {
printf("ERROR: Timed out waiting for Tor hidden service hostname at %s\n",
hostnamePath.string().c_str());
return false;
}
if (fShutdown) {
printf("ERROR: Shutdown requested while waiting for Tor hidden service hostname\n");
return false;
}
MilliSleep(1000);
}
if (!ValidateOnionAddress(backendOnion)) {
printf("ERROR: Tor backend produced invalid onion address: %s\n", backendOnion.c_str());
return false;
}
if (!onionAddress.empty() && onionAddress != backendOnion) {
printf("WARNING: Replacing wallet-managed onion address %s with Tor backend address %s\n",
onionAddress.c_str(), backendOnion.c_str());
}
onionAddress = backendOnion;
isActive = true;
printf("Started V3 onion service on %s:%d\n", onionAddress.c_str(), port);
if (pwalletMain) {
CWalletDB walletdb(pwalletMain->strWalletFile);
walletdb.WriteSetting("tor_v3_onion_address", onionAddress);
// Back up the Tor-generated secret key to wallet.dat so the onion
// identity survives deletion of the tor_data directory.
boost::filesystem::path secretKeyPath = serviceDir / "hs_ed25519_secret_key";
if (boost::filesystem::exists(secretKeyPath)) {
std::ifstream keyFile(secretKeyPath.string().c_str(), std::ios::binary);
if (keyFile.is_open()) {
std::vector<unsigned char> keyData(
(std::istreambuf_iterator<char>(keyFile)),
std::istreambuf_iterator<char>());
keyFile.close();
if (keyData.size() == 96) {
walletdb.WriteSetting("tor_v3_hs_secret_key_backup", keyData);
printf("Backed up Tor hidden service secret key to wallet (%d bytes)\n",
(int)keyData.size());
} else {
printf("WARNING: hs_ed25519_secret_key has unexpected size %d (expected 96), not backing up\n",
(int)keyData.size());
}
OPENSSL_cleanse(keyData.data(), keyData.size());
}
}
}
printf("Attached V3 onion service to Tor backend at %s:%d\n", onionAddress.c_str(), port);
return true;
}
@@ -1142,20 +1245,10 @@ void CTorV3Manager::ShutdownTor()
bool CTorV3Manager::CreateWalletHiddenService(int port)
{
CTorV3Service* service = new CTorV3Service();
// Try to load existing service first
if (!service->LoadFromWallet()) {
// Generate new service
if (!service->GenerateV3Service(port)) {
delete service;
return false;
}
service->SaveToWallet();
}
if (service->StartOnionService()) {
if (service->AttachToBackendService(torDataDir, port)) {
services[port] = service;
printf("Wallet hidden service created: %s\n", service->GetOnionAddress().c_str());
printf("Wallet hidden service attached: %s\n", service->GetOnionAddress().c_str());
// If seeder mode is enabled, automatically register as seeder
if (torV3Config.enableSeederMode) {
@@ -1968,12 +2061,12 @@ void CTorV3Manager::UpdateDiscoveryStats(int connected, int attempted)
bool LoadTorV3Config()
{
// Tor V3 identity is innate to Triangles — enabled by default
torV3Config.enableTor = GetBoolArg("-tor", true);
torV3Config.enableHiddenService = GetBoolArg("-torhiddenservice", true);
torV3Config.enableTor = !GetBoolArg("-notor", false);
torV3Config.enableHiddenService = torV3Config.enableTor && GetBoolArg("-torhiddenservice", true);
torV3Config.enableSeederMode = GetBoolArg("-torseeder", false);
torV3Config.hiddenServicePort = GetArg("-torhiddenserviceport", GetDefaultPort());
torV3Config.hiddenServicePort = GetArg("-torhsport", GetListenPort());
torV3Config.torDataDirectory = GetArg("-tordatadir", (GetDataDir() / "tor_data").string());
torV3Config.socksProxy = GetArg("-torproxy", "127.0.0.1:9050");
torV3Config.socksProxy = GetEffectiveTorProxy();
torV3Config.maxConnections = GetArg("-tormaxconnections", 8);
printf("Loaded Tor V3 configuration: enabled=%s, hidden_service=%s, seeder=%s, proxy=%s\n",
@@ -2093,15 +2186,106 @@ void CTorV3Manager::RequestSeederListFromPeer(const std::string& peerAddress)
// Schedule periodic seeder re-announcements
void CTorV3Manager::ScheduleSeederReannouncement()
{
// This would typically be handled by a timer or scheduler
// For now, we'll just update the last announcement time
if (pwalletMain) {
CWalletDB walletdb(pwalletMain->strWalletFile);
walletdb.WriteSetting("seeder_last_announcement", (int64_t)GetTime());
printf("Scheduled seeder re-announcement\n");
}
}
// ---------------------------------------------------------------------------
// Background thread: Tor health monitoring + seeder maintenance
// ---------------------------------------------------------------------------
void ThreadTorMaintenance(void* parg)
{
RenameThread("Triangles-tormaint");
printf("Tor maintenance thread started\n");
int restartBackoffSec = 30;
int64_t lastSeederMaint = GetTime();
static const int SEEDER_INTERVAL = 1800; // 30 minutes
while (!fShutdown)
{
// Sleep in short intervals so the thread exits promptly on shutdown
for (int i = 0; i < 60 && !fShutdown; i++)
MilliSleep(500);
if (fShutdown) break;
// --- Tor health check & auto-restart ---
if (!CTorEmbedded::GetInstance()->IsRunning())
{
printf("WARNING: Tor process is no longer running, attempting restart...\n");
if (StartEmbeddedTor())
{
printf("Tor restarted successfully\n");
restartBackoffSec = 30;
// Re-attach the hidden service identity
TorV3Config& torConfig = GetTorV3Config();
std::string torDataPath = CTorEmbedded::GetInstance()->GetDataDir();
if (torDataPath.empty())
torDataPath = torConfig.torDataDirectory;
torConfig.enableTor = true;
torConfig.enableHiddenService = CTorEmbedded::GetInstance()->IsHiddenServiceEnabled();
torConfig.hiddenServicePort = CTorEmbedded::GetInstance()->GetHiddenServicePort();
torConfig.torDataDirectory = torDataPath;
if (torConfig.enableHiddenService && InitTorV3())
{
std::string onionAddr = CTorV3Manager::GetInstance()->GetWalletOnionAddress();
if (!onionAddr.empty())
{
AddLocal(CService(onionAddr, torConfig.hiddenServicePort), LOCAL_MANUAL);
printf("Re-registered Tor V3 identity after restart: %s\n", onionAddr.c_str());
}
}
}
else
{
printf("WARNING: Tor restart failed, retrying in %d seconds\n", restartBackoffSec);
for (int i = 0; i < restartBackoffSec * 2 && !fShutdown; i++)
MilliSleep(500);
if (restartBackoffSec < 300)
restartBackoffSec *= 2;
}
continue;
}
// --- Seeder maintenance (every 30 minutes) ---
TorV3Config& cfg = GetTorV3Config();
if (cfg.enableSeederMode && (GetTime() - lastSeederMaint) >= SEEDER_INTERVAL)
{
CTorV3Manager* mgr = CTorV3Manager::GetInstance();
std::string ownAddr = mgr->GetWalletOnionAddress();
if (!ownAddr.empty())
{
// Re-announce ourselves as a seeder to all peers
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
{
try {
pnode->PushMessage("seeder", ownAddr, cfg.hiddenServicePort);
} catch (...) {}
}
}
printf("Seeder re-announcement sent to %d peers\n", (int)vNodes.size());
}
// Refresh our knowledge of other seeders
mgr->RequestSeederListFromPeers();
mgr->ScheduleSeederReannouncement();
lastSeederMaint = GetTime();
}
}
printf("Tor maintenance thread exited\n");
}
// Global functions
bool InitTorV3()
{
@@ -2111,4 +2295,4 @@ bool InitTorV3()
void ShutdownTorV3()
{
CTorV3Manager::GetInstance()->ShutdownTor();
}
}
+6 -2
View File
@@ -34,7 +34,10 @@ public:
// Start the onion service
bool StartOnionService();
// Attach to the hidden service managed by the running Tor backend
bool AttachToBackendService(const std::string& torDataDir, int servicePort, int waitSeconds = 30);
// Stop the onion service
bool StopService();
@@ -169,5 +172,6 @@ void ShutdownTorV3();
TorV3Config& GetTorV3Config();
bool SaveTorV3Config();
bool LoadTorV3Config();
void ThreadTorMaintenance(void* parg);
#endif // TRIANGLES_TOR_ONION_V3_H
#endif // TRIANGLES_TOR_ONION_V3_H
+35 -23
View File
@@ -52,6 +52,7 @@ CTorEmbedded::CTorEmbedded()
: running(false)
, socksPort(19099)
, hiddenServicePort(24112)
, hiddenServiceEnabled(true)
{
}
@@ -109,20 +110,24 @@ static void TorThreadFunc(std::vector<std::string> argv_strings)
CTorEmbedded::GetInstance()->SetRunning(false);
}
bool CTorEmbedded::Start(int socks, int hsPort)
bool CTorEmbedded::Start(int socks, int hsPort, bool enableHiddenService)
{
if (running.load()) return true;
socksPort = socks;
hiddenServicePort = hsPort;
hiddenServiceEnabled = enableHiddenService;
hiddenServicePort = hiddenServiceEnabled ? hsPort : 0;
onionHostname.clear();
// Prepare Tor data directory under the wallet's data dir
torDataDir = (::GetDataDir() / "tor_data").string();
fs::create_directories(torDataDir);
// Hidden service directory
std::string hsDir = (fs::path(torDataDir) / "hidden_service").string();
fs::create_directories(hsDir);
std::string hsDir;
if (hiddenServiceEnabled) {
hsDir = (fs::path(torDataDir) / "hidden_service").string();
fs::create_directories(hsDir);
}
// Build the argv for tor_run_main
std::vector<std::string> argv;
@@ -131,12 +136,14 @@ bool CTorEmbedded::Start(int socks, int hsPort)
argv.push_back(std::to_string(socksPort));
argv.push_back("--DataDirectory");
argv.push_back(torDataDir);
argv.push_back("--HiddenServiceDir");
argv.push_back(hsDir);
argv.push_back("--HiddenServiceVersion");
argv.push_back("3");
argv.push_back("--HiddenServicePort");
argv.push_back(std::to_string(hiddenServicePort) + " 127.0.0.1:" + std::to_string(hiddenServicePort));
if (hiddenServiceEnabled) {
argv.push_back("--HiddenServiceDir");
argv.push_back(hsDir);
argv.push_back("--HiddenServiceVersion");
argv.push_back("3");
argv.push_back("--HiddenServicePort");
argv.push_back(std::to_string(hiddenServicePort) + " 127.0.0.1:" + std::to_string(hiddenServicePort));
}
argv.push_back("--AvoidDiskWrites");
argv.push_back("1");
argv.push_back("--Log");
@@ -175,13 +182,15 @@ bool CTorEmbedded::Start(int socks, int hsPort)
printf("Embedded Tor SOCKS proxy ready on port %d (took %ds)\n", socksPort, i + 1);
// Read .onion hostname if available
fs::path hostnameFile = fs::path(hsDir) / "hostname";
if (fs::exists(hostnameFile)) {
std::ifstream f(hostnameFile.string().c_str());
if (f.is_open())
std::getline(f, onionHostname);
if (!onionHostname.empty())
printf("Tor hidden service: %s\n", onionHostname.c_str());
if (hiddenServiceEnabled) {
fs::path hostnameFile = fs::path(hsDir) / "hostname";
if (fs::exists(hostnameFile)) {
std::ifstream f(hostnameFile.string().c_str());
if (f.is_open())
std::getline(f, onionHostname);
if (!onionHostname.empty())
printf("Tor hidden service: %s\n", onionHostname.c_str());
}
}
return true;
}
@@ -219,14 +228,16 @@ void CTorEmbedded::Stop()
#include "tor_process.h"
bool CTorEmbedded::Start(int socks, int hsPort)
bool CTorEmbedded::Start(int socks, int hsPort, bool enableHiddenService)
{
printf("Embedded Tor not compiled in. Using external Tor process.\n");
// Delegate to the external process manager
socksPort = socks;
hiddenServicePort = hsPort;
hiddenServiceEnabled = enableHiddenService;
hiddenServicePort = hiddenServiceEnabled ? hsPort : 0;
onionHostname.clear();
torDataDir = (::GetDataDir() / "tor_data").string();
running.store(StartTorProcess(torDataDir, socksPort, hiddenServicePort));
running.store(StartTorProcess(torDataDir, socksPort, hiddenServicePort, hiddenServiceEnabled));
return running.load();
}
@@ -249,10 +260,11 @@ bool StartEmbeddedTor()
return false;
}
bool enableHiddenService = GetBoolArg("-torhiddenservice", true);
int socksPort = GetArg("-torsocks", 19099);
int hsPort = GetArg("-torhsport", GetListenPort());
int hsPort = enableHiddenService ? GetArg("-torhsport", GetListenPort()) : 0;
return CTorEmbedded::GetInstance()->Start(socksPort, hsPort);
return CTorEmbedded::GetInstance()->Start(socksPort, hsPort, enableHiddenService);
}
void StopEmbeddedTor()
+3 -1
View File
@@ -16,6 +16,7 @@ private:
std::atomic<bool> running;
int socksPort;
int hiddenServicePort;
bool hiddenServiceEnabled;
std::string torDataDir;
std::string onionHostname;
@@ -26,7 +27,7 @@ public:
~CTorEmbedded();
// Start Tor in a background thread (blocks that thread until shutdown)
bool Start(int socksPort = 19099, int hsPort = 24112);
bool Start(int socksPort = 19099, int hsPort = 24112, bool enableHiddenService = true);
// Request Tor to shut down
void Stop();
@@ -45,6 +46,7 @@ public:
// Get the hidden service port
int GetHiddenServicePort() const { return hiddenServicePort; }
bool IsHiddenServiceEnabled() const { return hiddenServiceEnabled; }
};
// Global init/shutdown hooks (called from init.cpp)
+28 -17
View File
@@ -48,6 +48,7 @@ CTorProcess* CTorProcess::GetInstance()
CTorProcess::CTorProcess()
: socksPort(19099)
, hiddenServicePort(24112)
, hiddenServiceEnabled(true)
, running(false)
#ifdef WIN32
, hProcess(NULL)
@@ -198,11 +199,13 @@ bool CTorProcess::WriteTorrc()
fs::create_directories(torStateDir);
torrc << "DataDirectory " << torStateDir.string() << "\n";
// V3 hidden service so this node is reachable via .onion
torrc << "HiddenServiceDir " << hsDir.string() << "\n";
torrc << "HiddenServiceVersion 3\n";
torrc << "HiddenServicePort " << hiddenServicePort
<< " 127.0.0.1:" << hiddenServicePort << "\n";
if (hiddenServiceEnabled) {
// V3 hidden service so this node is reachable via .onion
torrc << "HiddenServiceDir " << hsDir.string() << "\n";
torrc << "HiddenServiceVersion 3\n";
torrc << "HiddenServicePort " << hiddenServicePort
<< " 127.0.0.1:" << hiddenServicePort << "\n";
}
// Reduce bandwidth/resource usage for wallet use
torrc << "ClientOnly 1\n";
@@ -213,15 +216,21 @@ bool CTorProcess::WriteTorrc()
torrc.close();
printf("Wrote torrc to %s (SOCKS %d, HS port %d)\n",
torrcPath.c_str(), socksPort, hiddenServicePort);
if (hiddenServiceEnabled) {
printf("Wrote torrc to %s (SOCKS %d, HS port %d)\n",
torrcPath.c_str(), socksPort, hiddenServicePort);
} else {
printf("Wrote torrc to %s (SOCKS %d, hidden service disabled)\n",
torrcPath.c_str(), socksPort);
}
return true;
}
bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort)
bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool enableHiddenService)
{
socksPort = socks;
hiddenServicePort = hsPort;
hiddenServiceEnabled = enableHiddenService;
hiddenServicePort = hiddenServiceEnabled ? hsPort : 0;
torDataDir = dataDir;
// Check if something is already listening on our SOCKS port
@@ -314,12 +323,14 @@ bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort)
printf("Tor SOCKS proxy ready on port %d (took %ds)\n", socksPort, i + 1);
// Read and display the hidden service hostname if available
fs::path hsHostname = fs::path(torDataDir) / "hidden_service" / "hostname";
if (fs::exists(hsHostname)) {
std::ifstream f(hsHostname.string().c_str());
std::string hostname;
if (f.is_open() && std::getline(f, hostname)) {
printf("Tor hidden service: %s\n", hostname.c_str());
if (hiddenServiceEnabled) {
fs::path hsHostname = fs::path(torDataDir) / "hidden_service" / "hostname";
if (fs::exists(hsHostname)) {
std::ifstream f(hsHostname.string().c_str());
std::string hostname;
if (f.is_open() && std::getline(f, hostname)) {
printf("Tor hidden service: %s\n", hostname.c_str());
}
}
}
return true;
@@ -402,9 +413,9 @@ std::string CTorProcess::GetSocksProxy() const
}
// Global convenience functions
bool StartTorProcess(const std::string& dataDir, int socksPort, int hsPort)
bool StartTorProcess(const std::string& dataDir, int socksPort, int hsPort, bool enableHiddenService)
{
return CTorProcess::GetInstance()->Start(dataDir, socksPort, hsPort);
return CTorProcess::GetInstance()->Start(dataDir, socksPort, hsPort, enableHiddenService);
}
void StopTorProcess()
+3 -2
View File
@@ -21,6 +21,7 @@ private:
std::string torrcPath;
int socksPort;
int hiddenServicePort;
bool hiddenServiceEnabled;
bool running;
#ifdef WIN32
@@ -45,7 +46,7 @@ public:
// Start the Tor process
// Returns true if Tor was started or is already running
bool Start(const std::string& dataDir, int socksPort = 19099, int hsPort = 24112);
bool Start(const std::string& dataDir, int socksPort = 19099, int hsPort = 24112, bool enableHiddenService = true);
// Stop the Tor process
void Stop();
@@ -64,7 +65,7 @@ public:
};
// Global convenience functions
bool StartTorProcess(const std::string& dataDir, int socksPort = 19099, int hsPort = 24112);
bool StartTorProcess(const std::string& dataDir, int socksPort = 19099, int hsPort = 24112, bool enableHiddenService = true);
void StopTorProcess();
#endif // TRIANGLES_TOR_PROCESS_H
+17 -2
View File
@@ -946,9 +946,24 @@ void ThreadRPCServer2(void* parg)
vnThreadsRunning[THREAD_RPCLISTENER]--;
while (!fShutdown)
io_service.run_one();
{
// Use poll_one + sleep instead of blocking run_one so the thread
// remains responsive to fShutdown and can exit promptly.
if (!io_service.poll_one())
{
io_service.restart();
MilliSleep(50);
}
}
vnThreadsRunning[THREAD_RPCLISTENER]++;
StopRequests();
// Safely shut down: close acceptors, then drain any remaining handlers
try {
StopRequests();
} catch (...) {
// Absorb bad_weak_ptr or other exceptions from stale tracked slots
}
io_service.poll(); // process cancellation callbacks so shared_ptrs are released
}
class JSONRequest
+8 -5
View File
@@ -486,7 +486,8 @@ void CWallet::WalletUpdateSpent(const CTransaction &tx, bool fBlock)
printf("WalletUpdateSpent found spent coin %s TRI %s\n", FormatMoney(wtx.GetCredit()).c_str(), wtx.GetHash().ToString().c_str());
wtx.MarkSpent(txin.prevout.n);
wtx.WriteToDisk();
NotifyTransactionChanged(this, txin.prevout.hash, CT_UPDATED);
if (!IsInitialBlockDownload())
NotifyTransactionChanged(this, txin.prevout.hash, CT_UPDATED);
}
}
}
@@ -503,7 +504,8 @@ void CWallet::WalletUpdateSpent(const CTransaction &tx, bool fBlock)
{
wtx.MarkUnspent(&txout - &tx.vout[0]);
wtx.WriteToDisk();
NotifyTransactionChanged(this, hash, CT_UPDATED);
if (!IsInitialBlockDownload())
NotifyTransactionChanged(this, hash, CT_UPDATED);
}
}
}
@@ -634,8 +636,9 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn)
// since AddToWallet is called directly for self-originating transactions, check for consumption of own coins
WalletUpdateSpent(wtx, (wtxIn.hashBlock != 0));
// Notify UI of new or updated transaction
NotifyTransactionChanged(this, hash, fInsertedNew ? CT_NEW : CT_UPDATED);
// Notify UI of new or updated transaction (skip during IBD to avoid flooding the event loop)
if (!IsInitialBlockDownload())
NotifyTransactionChanged(this, hash, fInsertedNew ? CT_NEW : CT_UPDATED);
// notify an external script when a wallet transaction comes in or is updated
std::string strCmd = GetArg("-walletnotify", "");
@@ -2775,7 +2778,7 @@ void CWallet::UpdatedTransaction(const uint256 &hashTx)
LOCK(cs_wallet);
// Only notify UI if this transaction is in this wallet
map<uint256, CWalletTx>::const_iterator mi = mapWallet.find(hashTx);
if (mi != mapWallet.end())
if (mi != mapWallet.end() && !IsInitialBlockDownload())
NotifyTransactionChanged(this, hashTx, CT_UPDATED);
}
}