Compare commits

...

2 Commits

Author SHA1 Message Date
sami7777 2abd494fec Bump version to 5.3.3 and update CI version
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-19 18:54:44 -07:00
sami7777 378b0370e3 Improve peer connectivity, UI responsiveness, and add auto-bootstrap
Peer connectivity (small network optimizations):
- Reduce hardcoded seed fallback delay from 30s to 10s
- Reduce peer retry interval from 600s to 120s
- Lower staking minimum peers from 3 to 1
- Relay addr messages to all connected peers instead of just 2

UI responsiveness:
- Add progress reporting to ScanForWalletTransactions (every 10K blocks)
- Use TRY_LOCK in WalletModel::pollBalanceChanged to avoid blocking UI
- Use TRY_LOCK in TransactionTablePriv::refreshWallet with retry

Auto-bootstrap:
- Add bootstrap.h/cpp with HTTP download via boost::asio
- On first run, prompt user to download blockchain snapshot from
  bootstrap.cryptographic-triangles.org directly into data directory
- Downloads filelist.txt manifest then each file with progress dialog
- Falls back to IP 194.233.88.206 if DNS fails
- Gracefully continues to P2P sync if bootstrap unavailable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 18:02:47 -07:00
13 changed files with 336 additions and 9 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ on:
workflow_dispatch:
env:
VERSION: "5.2.0"
VERSION: "5.3.3"
jobs:
build-windows-qt:
+184
View File
@@ -0,0 +1,184 @@
// Copyright (c) 2024 Triangles developers
// Distributed under the MIT/X11 software license
#include "bootstrap.h"
#include <boost/asio.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/algorithm/string.hpp>
#include <fstream>
#include <sstream>
#include <cstdio>
namespace fs = boost::filesystem;
using boost::asio::ip::tcp;
namespace Bootstrap {
bool NeedsBootstrap(const fs::path& dataDir)
{
return !fs::exists(dataDir / "blk0001.dat");
}
bool DownloadFile(const std::string& host, const std::string& urlPath,
const fs::path& destPath,
ProgressCallback progressFn,
std::string& strError)
{
try {
boost::asio::io_context io_context;
tcp::resolver resolver(io_context);
boost::system::error_code resolve_ec;
tcp::resolver::results_type endpoints =
resolver.resolve(host, std::to_string(PORT), resolve_ec);
if (resolve_ec) {
strError = "Cannot resolve host: " + host;
return false;
}
tcp::socket socket(io_context);
boost::asio::connect(socket, endpoints);
// Send HTTP GET request
std::string request =
"GET " + urlPath + " HTTP/1.1\r\n"
"Host: " + host + "\r\n"
"Connection: close\r\n"
"User-Agent: Triangles\r\n"
"\r\n";
boost::asio::write(socket, boost::asio::buffer(request));
// Read response headers
boost::asio::streambuf response_buf;
boost::asio::read_until(socket, response_buf, "\r\n\r\n");
std::istream response_stream(&response_buf);
// Parse status line
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) {
strError = "HTTP error " + std::to_string(status_code) + " for " + urlPath;
return false;
}
// Parse headers for Content-Length
int64_t content_length = 0;
std::string header_line;
while (std::getline(response_stream, header_line) && header_line != "\r") {
std::string lower_header = header_line;
std::transform(lower_header.begin(), lower_header.end(),
lower_header.begin(), ::tolower);
if (lower_header.find("content-length:") == 0) {
content_length = std::stoll(header_line.substr(header_line.find(':') + 1));
}
}
// Open output file
FILE* file = fopen(destPath.string().c_str(), "wb");
if (!file) {
strError = "Cannot create file: " + destPath.string();
return false;
}
int64_t bytes_written = 0;
// Write any data remaining in the header buffer (body starts here)
if (response_buf.size() > 0) {
std::istreambuf_iterator<char> eos;
std::string remaining(std::istreambuf_iterator<char>(response_stream), eos);
if (!remaining.empty()) {
fwrite(remaining.data(), 1, remaining.size(), file);
bytes_written += remaining.size();
}
}
// Read remaining body in chunks
std::vector<char> chunk(65536); // 64 KB
boost::system::error_code ec;
int64_t last_progress = 0;
while (true) {
size_t n = socket.read_some(boost::asio::buffer(chunk), ec);
if (n > 0) {
fwrite(chunk.data(), 1, n, file);
bytes_written += n;
// Report progress every 256 KB
if (progressFn && (bytes_written - last_progress >= 262144)) {
last_progress = bytes_written;
progressFn(bytes_written, content_length);
}
}
if (ec == boost::asio::error::eof)
break;
if (ec) {
fclose(file);
fs::remove(destPath);
strError = "Network error: " + ec.message();
return false;
}
}
fclose(file);
// Verify download size if Content-Length was provided
if (content_length > 0 && bytes_written != content_length) {
fs::remove(destPath);
strError = "Incomplete download: got " + std::to_string(bytes_written)
+ " of " + std::to_string(content_length) + " bytes";
return false;
}
return true;
} catch (std::exception& e) {
strError = std::string("Download failed: ") + e.what();
return false;
}
}
bool FetchFileList(const std::string& host,
std::vector<std::string>& files,
std::string& strError)
{
// Download filelist.txt to a temp file
fs::path tmpPath = fs::temp_directory_path() / "triangles_bootstrap_filelist.txt";
std::string urlPath = std::string(BASE_PATH) + "filelist.txt";
if (!DownloadFile(host, urlPath, tmpPath, nullptr, strError))
return false;
// Read lines
std::ifstream in(tmpPath.string().c_str());
if (!in.is_open()) {
strError = "Cannot read downloaded file list";
return false;
}
files.clear();
std::string line;
while (std::getline(in, line)) {
boost::trim(line);
if (!line.empty() && line[0] != '#')
files.push_back(line);
}
in.close();
fs::remove(tmpPath);
if (files.empty()) {
strError = "File list is empty";
return false;
}
return true;
}
} // namespace Bootstrap
+39
View File
@@ -0,0 +1,39 @@
// Copyright (c) 2024 Triangles developers
// Distributed under the MIT/X11 software license
#ifndef TRIANGLES_BOOTSTRAP_H
#define TRIANGLES_BOOTSTRAP_H
#include <string>
#include <vector>
#include <functional>
#include <boost/filesystem.hpp>
namespace Bootstrap {
// Bootstrap server configuration
static const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org";
static const char* FALLBACK_HOST = "194.233.88.206";
static const char* BASE_PATH = "/";
static const int PORT = 80;
// Progress callback: (bytesDownloaded, totalBytes)
typedef std::function<void(int64_t, int64_t)> ProgressCallback;
// Check if data dir already has blockchain data
bool NeedsBootstrap(const boost::filesystem::path& dataDir);
// Download a single file via HTTP GET, write to destPath
bool DownloadFile(const std::string& host, const std::string& urlPath,
const boost::filesystem::path& destPath,
ProgressCallback progressFn,
std::string& strError);
// Fetch the file manifest (list of relative paths to download)
bool FetchFileList(const std::string& host,
std::vector<std::string>& files,
std::string& strError);
} // namespace Bootstrap
#endif // TRIANGLES_BOOTSTRAP_H
+1 -1
View File
@@ -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 3
#define CLIENT_VERSION_REVISION 2
#define CLIENT_VERSION_REVISION 3
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
+2 -1
View File
@@ -3392,7 +3392,8 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
hashKey = Hash(BEGIN(hashKey), END(hashKey));
mapMix.insert(make_pair(hashKey, pnode));
}
int nRelayNodes = fReachable ? 2 : 1; // limited relaying of addresses outside our network(s)
// Small network: relay to more peers so addresses propagate quickly
int nRelayNodes = fReachable ? (int)mapMix.size() : 1;
for (multimap<uint256, CNode*>::iterator mi = mapMix.begin(); mi != mapMix.end() && nRelayNodes-- > 0; ++mi)
((*mi).second)->PushAddress(addr);
}
+1 -1
View File
@@ -551,7 +551,7 @@ void StakeMiner(CWallet *pwallet)
if (fTryToSync)
{
fTryToSync = false;
if (vNodes.size() < 3 || nBestHeight < GetNumBlocksOfPeers())
if (vNodes.size() < 1 || nBestHeight < GetNumBlocksOfPeers())
{
MilliSleep(60000);
continue;
+3 -3
View File
@@ -1584,7 +1584,7 @@ void ThreadOpenConnections2(void* parg)
for (CNode* pnode : vNodes) {
if (!pnode->fInbound) { fNoOutbound = false; break; }
}
if (fNoOutbound && (GetTime() - nStart > 30) && !fTestNet)
if (fNoOutbound && (GetTime() - nStart > 10) && !fTestNet)
{
std::vector<CAddress> vAdd;
for (unsigned int i = 0; i < ARRAYLEN(pnSeed); i++)
@@ -1596,7 +1596,7 @@ void ThreadOpenConnections2(void* parg)
vAdd.push_back(addr);
}
addrman.Add(vAdd, CNetAddr("127.0.0.1"));
printf("No outbound connections after 30s, added %d hardcoded seeds\n", (int)vAdd.size());
printf("No outbound connections after 10s, added %d hardcoded seeds\n", (int)vAdd.size());
}
}
@@ -1642,7 +1642,7 @@ void ThreadOpenConnections2(void* parg)
continue;
// only consider very recently tried nodes after 30 failed attempts
if (nANow - addr.nLastTry < 600 && nTries < 30)
if (nANow - addr.nLastTry < 120 && nTries < 30)
continue;
// do not allow non-default ports, unless after 50 invalid addresses selected already
+79
View File
@@ -1,5 +1,6 @@
#include "introdialog.h"
#include "util.h"
#include "bootstrap.h"
#include <QSettings>
#include <QVBoxLayout>
@@ -9,6 +10,8 @@
#include <QDir>
#include <QMessageBox>
#include <QDialogButtonBox>
#include <QProgressDialog>
#include <QApplication>
#include <boost/filesystem.hpp>
@@ -201,5 +204,81 @@ bool IntroDialog::pickDataDirectory()
return false;
}
// Offer bootstrap download once (tracks via QSettings so it only asks once)
fs::path dataDirPath(dataDir.toStdString());
if (!settings.value("bootstrapOffered", false).toBool())
{
settings.setValue("bootstrapOffered", true);
int ret = QMessageBox::question(0, "Triangles",
"Would you like to download the latest blockchain snapshot?\n\n"
"This will download the blockchain data from the Triangles network "
"and replace any existing chain data in your data directory.\n\n"
"Click Yes to download, or No to sync from the network.",
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (ret == QMessageBox::Yes)
{
std::string host = Bootstrap::DEFAULT_HOST;
std::string strError;
std::vector<std::string> files;
// Fetch file list (try domain first, then fallback to IP)
if (!Bootstrap::FetchFileList(host, files, strError)) {
host = Bootstrap::FALLBACK_HOST;
if (!Bootstrap::FetchFileList(host, files, strError)) {
QMessageBox::warning(0, "Triangles",
QString("Could not reach bootstrap server:\n%1\n\n"
"The wallet will sync from the network instead.")
.arg(QString::fromStdString(strError)));
return true;
}
}
// Show progress dialog
QProgressDialog progress("Downloading blockchain data...", "Cancel",
0, (int)files.size(), 0);
progress.setWindowTitle("Triangles - Bootstrap");
progress.setWindowModality(Qt::ApplicationModal);
progress.setMinimumDuration(0);
progress.setValue(0);
bool failed = false;
for (int i = 0; i < (int)files.size(); i++) {
if (progress.wasCanceled())
break;
QString filename = QString::fromStdString(files[i]);
progress.setLabelText(
QString("Downloading %1 (%2 of %3)...")
.arg(filename).arg(i + 1).arg((int)files.size()));
progress.setValue(i);
QApplication::processEvents();
// Create subdirectories if needed (e.g. txleveldb/)
fs::path destPath = dataDirPath / files[i];
fs::create_directories(destPath.parent_path());
// Download this file
std::string urlPath = std::string(Bootstrap::BASE_PATH) + files[i];
if (!Bootstrap::DownloadFile(host, urlPath, destPath,
[](int64_t, int64_t) {
QApplication::processEvents();
}, strError))
{
QMessageBox::warning(0, "Triangles",
QString("Download failed for %1:\n%2\n\n"
"The wallet will sync remaining data from the network.")
.arg(filename).arg(QString::fromStdString(strError)));
failed = true;
break;
}
}
if (!failed)
progress.setValue((int)files.size());
}
}
return true;
}
+7 -1
View File
@@ -71,7 +71,13 @@ public:
OutputDebugStringF("refreshWallet\n");
cachedWallet.clear();
{
LOCK(wallet->cs_wallet);
TRY_LOCK(wallet->cs_wallet, lockWallet);
if(!lockWallet)
{
// Lock busy (block processing), retry in 500ms
QTimer::singleShot(500, parent, SLOT(refreshWallet()));
return;
}
for(std::map<uint256, CWalletTx>::iterator it = wallet->mapWallet.begin(); it != wallet->mapWallet.end(); ++it)
{
if(TransactionRecord::showTransaction(it->second))
+6
View File
@@ -78,6 +78,12 @@ void WalletModel::pollBalanceChanged()
{
if(nBestHeight != cachedNumBlocks)
{
// Don't block the UI thread waiting for cs_wallet - skip this
// update cycle if the lock is held by the block processing thread
TRY_LOCK(wallet->cs_wallet, lockWallet);
if(!lockWallet)
return;
// Balance and number of transactions might have changed
cachedNumBlocks = nBestHeight;
checkBalanceChanged();
+1 -1
View File
@@ -53,7 +53,7 @@ static const int MEMPOOL_GD_VERSION = 60002;
#define DISPLAY_VERSION_MAJOR 5
#define DISPLAY_VERSION_MINOR 3
#define DISPLAY_VERSION_REVISION 2
#define DISPLAY_VERSION_REVISION 3
#define DISPLAY_VERSION_BUILD 0
#endif
+10
View File
@@ -958,11 +958,21 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
CBlockIndex* pindex = pindexStart;
{
LOCK(cs_wallet);
int nScanned = 0;
int nTotal = nBestHeight - (pindexStart ? pindexStart->nHeight : 0);
if (nTotal < 1) nTotal = 1;
while (pindex)
{
if (fShutdown)
break;
// Report progress every 10000 blocks to keep UI responsive
if (++nScanned % 10000 == 0)
{
int nPercent = (nScanned * 100) / nTotal;
uiInterface.InitMessage(strprintf(_("Rescanning... %d%%"), nPercent));
}
// no need to read and scan block, if block was created before
// our wallet birthday (as adjusted for block time variability)
if (nTimeFirstKey && (pindex->nTime < (nTimeFirstKey - 7200))) {
+2
View File
@@ -204,6 +204,7 @@ HEADERS += src/qt/trianglesgui.h \
src/qt/addressbookpage.h \
src/qt/aboutdialog.h \
src/qt/introdialog.h \
src/bootstrap.h \
src/qt/editaddressdialog.h \
src/qt/trianglesaddressvalidator.h \
src/alert.h \
@@ -322,6 +323,7 @@ SOURCES += src/qt/triangles.cpp src/qt/trianglesgui.cpp \
src/qt/addressbookpage.cpp \
src/qt/aboutdialog.cpp \
src/qt/introdialog.cpp \
src/bootstrap.cpp \
src/qt/editaddressdialog.cpp \
src/qt/trianglesaddressvalidator.cpp \
# Old embedded Tor v2 client removed - incompatible with OpenSSL 3.x