diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 20951c8..ae24811 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -153,14 +153,15 @@ jobs: New-Item -ItemType Directory -Path tor-extract -Force tar -xzf tor-bundle.tar.gz -C tor-extract New-Item -ItemType Directory -Path tor-files -Force - Copy-Item tor-extract/tor/tor.exe tor-files/ - Copy-Item tor-extract/tor/tor-gencert.exe tor-files/ -ErrorAction SilentlyContinue + Copy-Item -Recurse tor-extract/tor/* tor-files/ if (Test-Path tor-extract/tor/pluggable_transports) { - Copy-Item -Recurse tor-extract/tor/pluggable_transports tor-files/ + Copy-Item -Recurse tor-extract/tor/pluggable_transports tor-files/pluggable_transports -Force } if (Test-Path tor-extract/data) { Copy-Item -Recurse tor-extract/data tor-files/data } + Write-Host "Bundled Tor runtime files:" + Get-ChildItem -Recurse tor-files | Select-Object FullName - name: Install NSIS via MSYS2 run: pacman -S --noconfirm mingw-w64-x86_64-nsis @@ -240,7 +241,7 @@ jobs: Invoke-WebRequest -Uri "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-windows-x86_64-${TOR_VERSION}.tar.gz" -OutFile tor-bundle.tar.gz New-Item -ItemType Directory -Path tor-extract -Force tar -xzf tor-bundle.tar.gz -C tor-extract - Copy-Item tor-extract/tor/tor.exe daemon-dist/tor/ + Copy-Item -Recurse tor-extract/tor/* daemon-dist/tor/ if (Test-Path tor-extract/data) { Copy-Item -Recurse tor-extract/data daemon-dist/tor/data } diff --git a/src/clientversion.h b/src/clientversion.h index fac44fd..25698c8 100644 --- a/src/clientversion.h +++ b/src/clientversion.h @@ -8,7 +8,7 @@ // These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it #define CLIENT_VERSION_MAJOR 5 #define CLIENT_VERSION_MINOR 7 -#define CLIENT_VERSION_REVISION 6 +#define CLIENT_VERSION_REVISION 7 #define CLIENT_VERSION_BUILD 0 // Converts the parameter X to a string after macro replacement on X has been performed. diff --git a/src/init.cpp b/src/init.cpp index ee42453..4ea73df 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1223,7 +1223,10 @@ bool AppInit2() fUseUPnP = false; #endif } else { - return InitError(_("Tor failed to start. Triangles requires Tor to operate.")); + std::string torError = CTorEmbedded::GetInstance()->GetLastError(); + if (torError.empty()) + torError = "No detailed Tor startup error was recorded."; + return InitError(strprintf(_("Tor failed to start. Triangles requires Tor to operate.\n\nDetails: %s"), torError)); } // Initialize Tor V3 identity (Ed25519 keys, onion address) diff --git a/src/qt/forms/overviewpage.ui b/src/qt/forms/overviewpage.ui index c200368..0871af7 100644 --- a/src/qt/forms/overviewpage.ui +++ b/src/qt/forms/overviewpage.ui @@ -678,6 +678,43 @@ QWidget#line { + + + + + 10 + + + + Wallet Onion: + + + + + + + + 75 + true + + + + IBeamCursor + + + This wallet's Tor .onion address. + + + Initializing Tor identity... + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 62e92fb..9431b38 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -8,9 +8,12 @@ #include "transactionfilterproxy.h" #include "guiutil.h" #include "guiconstants.h" +#include "tor/tor_embedded.h" +#include "tor/onion_v3.h" #include #include +#include #define DECORATION_SIZE 64 #define NUM_ITEMS 5 @@ -88,6 +91,7 @@ public: OverviewPage::OverviewPage(QWidget *parent) : QWidget(parent), ui(new Ui::OverviewPage), + clientModel(0), currentBalance(-1), currentStake(0), currentUnconfirmedBalance(-1), @@ -112,6 +116,11 @@ OverviewPage::OverviewPage(QWidget *parent) : // start with displaying the "out of sync" warnings showOutOfSyncWarning(true); + + QTimer *onionRefreshTimer = new QTimer(this); + connect(onionRefreshTimer, SIGNAL(timeout()), this, SLOT(updateOnionAddress())); + onionRefreshTimer->start(5000); + updateOnionAddress(); } void OverviewPage::handleTransactionClicked(const QModelIndex &index) @@ -125,6 +134,12 @@ OverviewPage::~OverviewPage() delete ui; } +void OverviewPage::setClientModel(ClientModel *clientModel) +{ + this->clientModel = clientModel; + updateOnionAddress(); +} + void OverviewPage::setBalance(qint64 balance, qint64 stake, qint64 unconfirmedBalance, qint64 immatureBalance) { if (!model || !model->getOptionsModel()) @@ -213,3 +228,20 @@ void OverviewPage::showOutOfSyncWarning(bool fShow) ui->labelWalletStatus->setVisible(fShow); ui->labelTransactionsStatus->setVisible(fShow); } + +void OverviewPage::updateOnionAddress() +{ + std::string onionAddress = CTorV3Manager::GetInstance()->GetWalletOnionAddress(); + if (onionAddress.empty()) { + onionAddress = CTorEmbedded::GetInstance()->GetOnionAddress(); + } + + if (onionAddress.empty()) { + ui->labelOnionAddress->setText(tr("Initializing Tor identity...")); + ui->labelOnionAddress->setToolTip(tr("The wallet's .onion address will appear here once Tor and the wallet identity are ready.")); + return; + } + + ui->labelOnionAddress->setText(QString::fromStdString(onionAddress)); + ui->labelOnionAddress->setToolTip(tr("This wallet's Tor .onion address.")); +} diff --git a/src/qt/overviewpage.h b/src/qt/overviewpage.h index 0a6f038..a6beade 100644 --- a/src/qt/overviewpage.h +++ b/src/qt/overviewpage.h @@ -31,6 +31,7 @@ public: public slots: void setBalance(qint64 balance, qint64 stake, qint64 unconfirmedBalance, qint64 immatureBalance); void setTransactionSyncState(bool syncing); + void updateOnionAddress(); signals: void transactionClicked(const QModelIndex &index); diff --git a/src/qt/trianglesgui.cpp b/src/qt/trianglesgui.cpp index bd4cb2c..f9405eb 100644 --- a/src/qt/trianglesgui.cpp +++ b/src/qt/trianglesgui.cpp @@ -567,6 +567,7 @@ void TrianglesGUI::setClientModel(ClientModel *clientModel) if (rpcConsole) rpcConsole->setClientModel(clientModel); + overviewPage->setClientModel(clientModel); addressBookPage->setOptionsModel(clientModel->getOptionsModel()); receiveCoinsPage->setOptionsModel(clientModel->getOptionsModel()); } diff --git a/src/tor/tor_embedded.cpp b/src/tor/tor_embedded.cpp index f8d4717..fe7b18e 100644 --- a/src/tor/tor_embedded.cpp +++ b/src/tor/tor_embedded.cpp @@ -114,6 +114,7 @@ bool CTorEmbedded::Start(int socks, int hsPort, bool enableHiddenService) { if (running.load()) return true; + lastError.clear(); socksPort = socks; hiddenServiceEnabled = enableHiddenService; hiddenServicePort = hiddenServiceEnabled ? hsPort : 0; @@ -202,11 +203,13 @@ bool CTorEmbedded::Start(int socks, int hsPort, bool enableHiddenService) } if (!running.load()) { + lastError = "Embedded Tor thread exited during bootstrap before the SOCKS proxy became available."; printf("ERROR: Embedded Tor thread exited during bootstrap\n"); return false; } } + lastError = strprintf("Embedded Tor did not expose SOCKS port %d within 60 seconds.", socksPort); printf("WARNING: Embedded Tor started but SOCKS not ready after 60s (still bootstrapping)\n"); return true; } @@ -241,8 +244,12 @@ bool CTorEmbedded::Start(int socks, int hsPort, bool enableHiddenService) hiddenServiceEnabled = enableHiddenService; hiddenServicePort = hiddenServiceEnabled ? hsPort : 0; onionHostname.clear(); + lastError.clear(); torDataDir = (::GetDataDir() / "tor_data").string(); running.store(StartTorProcess(torDataDir, socksPort, hiddenServicePort, hiddenServiceEnabled)); + if (!running.load()) { + lastError = CTorProcess::GetInstance()->GetLastError(); + } return running.load(); } diff --git a/src/tor/tor_embedded.h b/src/tor/tor_embedded.h index 18bd739..abe26a5 100644 --- a/src/tor/tor_embedded.h +++ b/src/tor/tor_embedded.h @@ -19,6 +19,7 @@ private: bool hiddenServiceEnabled; std::string torDataDir; std::string onionHostname; + std::string lastError; public: static CTorEmbedded* GetInstance(); @@ -43,6 +44,8 @@ public: // Get our .onion address (available after bootstrap) std::string GetOnionAddress() const { return onionHostname; } + std::string GetLastError() const { return lastError; } + void SetLastError(const std::string& value) { lastError = value; } // Get the hidden service port int GetHiddenServicePort() const { return hiddenServicePort; } diff --git a/src/tor/tor_process.cpp b/src/tor/tor_process.cpp index 7f32027..2b97e07 100644 --- a/src/tor/tor_process.cpp +++ b/src/tor/tor_process.cpp @@ -19,11 +19,13 @@ #include #include #include +#include #ifdef WIN32 #include #include #include +#include #else #include #include @@ -35,6 +37,28 @@ namespace fs = boost::filesystem; +static std::string ReadTailLines(const fs::path& filePath, size_t maxLines) +{ + std::ifstream in(filePath.string().c_str()); + if (!in.is_open()) return ""; + + std::vector lines; + std::string line; + while (std::getline(in, line)) { + lines.push_back(line); + if (lines.size() > maxLines) { + lines.erase(lines.begin()); + } + } + + std::ostringstream out; + for (size_t i = 0; i < lines.size(); ++i) { + if (i) out << " | "; + out << lines[i]; + } + return out.str(); +} + static CTorProcess* torProcessInstance = nullptr; CTorProcess* CTorProcess::GetInstance() @@ -199,6 +223,10 @@ bool CTorProcess::WriteTorrc() fs::create_directories(torStateDir); torrc << "DataDirectory " << torStateDir.string() << "\n"; + // Persistent Tor log for post-mortem debugging on user machines. + fs::path torLogPath = dataPath / "tor.log"; + torrc << "Log notice file " << torLogPath.string() << "\n"; + if (hiddenServiceEnabled) { // V3 hidden service so this node is reachable via .onion torrc << "HiddenServiceDir " << hsDir.string() << "\n"; @@ -232,6 +260,7 @@ bool CTorProcess::WriteTorrc() bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool enableHiddenService) { + lastError.clear(); socksPort = socks; hiddenServiceEnabled = enableHiddenService; hiddenServicePort = hiddenServiceEnabled ? hsPort : 0; @@ -240,6 +269,7 @@ bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool // Check if something is already listening on our SOCKS port if (IsPortInUse(socksPort)) { printf("Tor SOCKS port %d already in use - assuming Tor is running\n", socksPort); + lastError = strprintf("SOCKS port %d is already in use; assuming an existing Tor instance is serving it.", socksPort); running = true; return true; } @@ -247,6 +277,7 @@ bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool // Find Tor binary torBinaryPath = FindTorBinary(); if (torBinaryPath.empty()) { + lastError = "Tor binary was not found in the bundled install or standard search paths."; printf("WARNING: Tor binary not found. Install Tor for .onion connectivity.\n"); printf(" Windows: Download from https://www.torproject.org/download/tor/\n"); printf(" Linux: apt install tor or yum install tor\n"); @@ -256,6 +287,7 @@ bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool // Write configuration if (!WriteTorrc()) { + lastError = strprintf("Failed to write Tor configuration to %s", torrcPath.c_str()); printf("ERROR: Failed to write Tor configuration\n"); return false; } @@ -282,7 +314,9 @@ bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool NULL, NULL, &si, &pi)) { - printf("ERROR: Failed to start Tor process (error %lu)\n", GetLastError()); + DWORD err = GetLastError(); + lastError = strprintf("CreateProcess failed for Tor binary '%s' with Windows error %lu", torBinaryPath.c_str(), err); + printf("ERROR: Failed to start Tor process (error %lu)\n", err); return false; } @@ -329,6 +363,7 @@ bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool // Defensive check: Tor must not directly occupy the node's hidden-service // virtual port. If it does, node startup will fail with a bind collision. if (hiddenServiceEnabled && IsPortInUse(hiddenServicePort)) { + lastError = strprintf("Port %d is already busy while Tor hidden service is enabled. Another process is likely blocking the node listener.", hiddenServicePort); printf("ERROR: Tor startup collision: hidden service port %d appears busy before node bind.\n", hiddenServicePort); printf(" Refusing to treat Tor as healthy because this would block the node listener.\n"); @@ -353,12 +388,20 @@ bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool // Check if Tor process is still alive if (!IsRunning()) { + fs::path torLogPath = fs::path(torDataDir) / "tor.log"; + std::string torLogTail = ReadTailLines(torLogPath, 8); + if (!torLogTail.empty()) { + lastError = strprintf("Tor process exited during bootstrap before the SOCKS port became ready. Recent tor.log: %s", torLogTail.c_str()); + } else { + lastError = "Tor process exited during bootstrap before the SOCKS port became ready."; + } printf("ERROR: Tor process exited prematurely\n"); running = false; return false; } } + lastError = strprintf("Tor process started from '%s' but SOCKS port %d was not ready after 30 seconds.", torBinaryPath.c_str(), socksPort); printf("WARNING: Tor started but SOCKS proxy not yet ready after 30s\n"); printf(" Tor may still be bootstrapping. .onion connections will work once ready.\n"); return true; diff --git a/src/tor/tor_process.h b/src/tor/tor_process.h index f183d8f..2784e09 100644 --- a/src/tor/tor_process.h +++ b/src/tor/tor_process.h @@ -19,6 +19,7 @@ private: std::string torBinaryPath; std::string torDataDir; std::string torrcPath; + std::string lastError; int socksPort; int hiddenServicePort; bool hiddenServiceEnabled; @@ -59,6 +60,7 @@ public: // Get the Tor binary path (for diagnostics) std::string GetBinaryPath() const { return torBinaryPath; } + std::string GetLastError() const { return lastError; } // Singleton access static CTorProcess* GetInstance();