diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index ff7c1a2..992d493 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: env: - VERSION: "5.3.3" + VERSION: "5.3.4" jobs: build-windows-qt: diff --git a/Makefile b/Makefile index 4e5fcc1..708f431 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # Generated by qmake (3.1) (Qt 5.15.18) # Project: triangles-qt.pro # Template: app -# Command: C:/msys64/mingw64/bin/qmake-qt5.exe -o Makefile triangles-qt.pro USE_QRCODE=1 USE_UPNP=- +# Command: C:/msys64/mingw64/bin/qmake-qt5.exe -o Makefile triangles-qt.pro -spec win32-g++ CONFIG+=release ############################################################################# MAKEFILE = Makefile @@ -156,7 +156,7 @@ Makefile: triangles-qt.pro C:/msys64/mingw64/share/qt5/mkspecs/win32-g++/qmake.c C:/msys64/mingw64/lib/qtmain.prl \ C:/msys64/mingw64/share/qt5/mkspecs/features/build_pass.prf \ src/qt/triangles.qrc - $(QMAKE) -o Makefile triangles-qt.pro USE_QRCODE=1 USE_UPNP=- + $(QMAKE) -o Makefile triangles-qt.pro -spec win32-g++ CONFIG+=release C:/msys64/mingw64/share/qt5/mkspecs/features/spec_pre.prf: C:/msys64/mingw64/share/qt5/mkspecs/qdevice.pri: C:/msys64/mingw64/share/qt5/mkspecs/features/device_config.prf: @@ -244,7 +244,7 @@ C:/msys64/mingw64/lib/qtmain.prl: C:/msys64/mingw64/share/qt5/mkspecs/features/build_pass.prf: src/qt/triangles.qrc: qmake: FORCE - @$(QMAKE) -o Makefile triangles-qt.pro USE_QRCODE=1 USE_UPNP=- + @$(QMAKE) -o Makefile triangles-qt.pro -spec win32-g++ CONFIG+=release qmake_all: FORCE diff --git a/src/bootstrap.cpp b/src/bootstrap.cpp index 420d6c6..e03f627 100644 --- a/src/bootstrap.cpp +++ b/src/bootstrap.cpp @@ -8,9 +8,12 @@ #include #include +#include + #include #include #include +#include namespace fs = boost::filesystem; using boost::asio::ip::tcp; @@ -181,4 +184,195 @@ bool FetchFileList(const std::string& host, return true; } +// --- tar.gz bootstrap support --- + +namespace { + +// Parse a tar octal field (ASCII octal, null/space terminated) +static int64_t ParseTarOctal(const char* field, size_t len) +{ + int64_t result = 0; + for (size_t i = 0; i < len && field[i] != '\0' && field[i] != ' '; i++) { + if (field[i] < '0' || field[i] > '7') continue; + result = (result << 3) | (field[i] - '0'); + } + return result; +} + +// Extract a tar.gz file to a destination directory +static bool ExtractTarGz(const fs::path& tarGzPath, + const fs::path& destDir, + std::string& strError) +{ + gzFile gz = gzopen(tarGzPath.string().c_str(), "rb"); + if (!gz) { + strError = "Cannot open " + tarGzPath.string(); + return false; + } + + gzbuffer(gz, 262144); // 256 KB buffer for performance + + char header[512]; + + while (true) { + int bytesRead = gzread(gz, header, 512); + if (bytesRead == 0) break; // EOF + if (bytesRead != 512) { + strError = "Truncated tar header"; + gzclose(gz); + return false; + } + + // End-of-archive marker (zero block) + bool allZero = true; + for (int i = 0; i < 512; i++) { + if (header[i] != 0) { allZero = false; break; } + } + if (allZero) break; + + // Parse filename: name (offset 0, 100 bytes) + optional prefix (offset 345, 155 bytes) + char name[101] = {0}; + char prefix[156] = {0}; + memcpy(name, header, 100); + memcpy(prefix, header + 345, 155); + + std::string fullName; + if (prefix[0] != '\0') + fullName = std::string(prefix) + "/" + std::string(name); + else + fullName = std::string(name); + + // Security: reject absolute paths and path traversal + if (fullName.empty() || fullName[0] == '/' || fullName.find("..") != std::string::npos) { + strError = "Unsafe path in tar archive: " + fullName; + gzclose(gz); + return false; + } + + char typeflag = header[156]; + int64_t fileSize = ParseTarOctal(header + 124, 12); + + if (typeflag == '5' || (!fullName.empty() && fullName.back() == '/')) { + // Directory entry + fs::create_directories(destDir / fullName); + } else if (typeflag == '0' || typeflag == '\0') { + // Regular file + fs::path filePath = destDir / fullName; + fs::create_directories(filePath.parent_path()); + + FILE* outFile = fopen(filePath.string().c_str(), "wb"); + if (!outFile) { + strError = "Cannot create file: " + filePath.string(); + gzclose(gz); + return false; + } + + int64_t remaining = fileSize; + char buf[65536]; + while (remaining > 0) { + int toRead = (remaining > (int64_t)sizeof(buf)) ? (int)sizeof(buf) : (int)remaining; + int n = gzread(gz, buf, toRead); + if (n <= 0) { + fclose(outFile); + strError = "Truncated tar data for: " + fullName; + gzclose(gz); + return false; + } + fwrite(buf, 1, n, outFile); + remaining -= n; + } + fclose(outFile); + + // Skip padding to next 512-byte boundary + int64_t pad = (512 - (fileSize % 512)) % 512; + if (pad > 0) { + char padBuf[512]; + if (gzread(gz, padBuf, (unsigned)pad) != (int)pad) { + strError = "Truncated tar padding for: " + fullName; + gzclose(gz); + return false; + } + } + } else { + // Unknown entry type - skip its data + int64_t totalSkip = fileSize + ((512 - (fileSize % 512)) % 512); + char skipBuf[512]; + while (totalSkip > 0) { + int toRead = (totalSkip > 512) ? 512 : (int)totalSkip; + if (gzread(gz, skipBuf, toRead) != toRead) break; + totalSkip -= toRead; + } + } + } + + gzclose(gz); + return true; +} + +} // anonymous namespace + +bool DownloadBootstrap(const std::string& host, + const fs::path& dataDir, + ProgressCallback progressFn, + std::string& strError) +{ + bool gotBlockFile = false; + + // Try downloading bootstrap.tar.gz first + fs::path tmpTarGz = dataDir / "bootstrap.tar.gz.tmp"; + std::string tarUrl = std::string(BASE_PATH) + "bootstrap.tar.gz"; + + bool tarDownloaded = DownloadFile(host, tarUrl, tmpTarGz, progressFn, strError); + + if (tarDownloaded) { + bool extractOk = ExtractTarGz(tmpTarGz, dataDir, strError); + fs::remove(tmpTarGz); + + if (extractOk && fs::exists(dataDir / "blk0001.dat")) + gotBlockFile = true; + // If extraction failed, fall through to legacy path + } + + if (!gotBlockFile) { + // Fallback: try filelist.txt + individual file downloads + std::string fallbackError; + std::vector files; + if (!FetchFileList(host, files, fallbackError)) { + if (!tarDownloaded) + strError = strError + " (fallback also failed: " + fallbackError + ")"; + else + strError = "Extraction failed: " + strError + " (fallback also failed: " + fallbackError + ")"; + return false; + } + + for (size_t i = 0; i < files.size(); i++) { + fs::path destPath = dataDir / files[i]; + fs::create_directories(destPath.parent_path()); + + std::string urlPath = std::string(BASE_PATH) + files[i]; + if (!DownloadFile(host, urlPath, destPath, progressFn, strError)) + return false; + } + + gotBlockFile = fs::exists(dataDir / "blk0001.dat"); + } + + if (!gotBlockFile) { + strError = "No blk0001.dat after download"; + return false; + } + + // Remove any extracted txleveldb/ and database/ - they were built on + // a different machine and won't work here. FastImportBlockFile() will + // rebuild the index directly from blk0001.dat on next startup. + fs::path txleveldb = dataDir / "txleveldb"; + fs::path database = dataDir / "database"; + if (fs::exists(txleveldb)) + fs::remove_all(txleveldb); + if (fs::exists(database)) + fs::remove_all(database); + + return true; +} + } // namespace Bootstrap diff --git a/src/bootstrap.h b/src/bootstrap.h index 49149ab..87f3fca 100644 --- a/src/bootstrap.h +++ b/src/bootstrap.h @@ -34,6 +34,13 @@ namespace Bootstrap { std::vector& files, std::string& strError); + // Download bootstrap.tar.gz and extract to dataDir. + // Falls back to filelist.txt + individual file download if tar.gz unavailable. + bool DownloadBootstrap(const std::string& host, + const boost::filesystem::path& dataDir, + ProgressCallback progressFn, + std::string& strError); + } // namespace Bootstrap #endif // TRIANGLES_BOOTSTRAP_H diff --git a/src/clientversion.h b/src/clientversion.h index b1626a2..a6554d1 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 3 -#define CLIENT_VERSION_REVISION 3 +#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. diff --git a/src/init.cpp b/src/init.cpp index 5440e0a..b88233a 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -805,43 +805,32 @@ bool AppInit2() fs::path dataPath = GetDataDir(); std::string host = Bootstrap::DEFAULT_HOST; std::string strError; - std::vector files; - uiInterface.InitMessage(_("Fetching bootstrap file list...")); + uiInterface.InitMessage(_("Downloading blockchain snapshot...")); printf("Bootstrap: contacting %s...\n", host.c_str()); - bool fGotList = Bootstrap::FetchFileList(host, files, strError); - if (!fGotList) { + auto progressFn = [](int64_t bytesDownloaded, int64_t totalBytes) { + if (totalBytes > 0) { + printf("\rBootstrap: %lld / %lld MB (%lld%%)", + (long long)(bytesDownloaded / (1024*1024)), + (long long)(totalBytes / (1024*1024)), + (long long)((bytesDownloaded * 100) / totalBytes)); + fflush(stdout); + } + }; + + bool success = Bootstrap::DownloadBootstrap(host, dataPath, progressFn, strError); + if (!success) { host = Bootstrap::FALLBACK_HOST; - printf("Bootstrap: primary host failed, trying fallback %s...\n", host.c_str()); - fGotList = Bootstrap::FetchFileList(host, files, strError); + printf("\nBootstrap: primary host failed, trying fallback %s...\n", host.c_str()); + success = Bootstrap::DownloadBootstrap(host, dataPath, progressFn, strError); } - if (!fGotList) { - printf("Bootstrap: could not reach server: %s\n", strError.c_str()); + if (!success) { + printf("\nBootstrap: failed: %s\n", strError.c_str()); printf("Bootstrap: skipping, will sync from network.\n"); } else { - printf("Bootstrap: downloading %d files...\n", (int)files.size()); - for (int i = 0; i < (int)files.size(); i++) { - if (fRequestShutdown) break; - - printf("Bootstrap: downloading %s (%d of %d)...\n", - files[i].c_str(), i + 1, (int)files.size()); - uiInterface.InitMessage(strprintf(_("Downloading %s (%d of %d)..."), - files[i].c_str(), i + 1, (int)files.size())); - - fs::path destPath = dataPath / files[i]; - fs::create_directories(destPath.parent_path()); - - std::string urlPath = std::string(Bootstrap::BASE_PATH) + files[i]; - if (!Bootstrap::DownloadFile(host, urlPath, destPath, nullptr, strError)) { - printf("Bootstrap: download failed for %s: %s\n", - files[i].c_str(), strError.c_str()); - printf("Bootstrap: will sync remaining data from network.\n"); - break; - } - } - printf("Bootstrap: done.\n"); + printf("\nBootstrap: done.\n"); } } #endif @@ -870,6 +859,16 @@ bool AppInit2() if (!LoadBlockIndex()) return InitError(_("Error loading blkindex.dat")); + // If the block index is empty but blk0001.dat exists (bootstrap download), + // fast-import: build the index directly from the block file without re-writing + // data. Batches LevelDB commits every 200K blocks for speed. + if (nBestHeight == 0 && boost::filesystem::exists(GetDataDir() / "blk0001.dat") + && mapBlockIndex.size() <= 1) + { + uiInterface.InitMessage(_("Importing bootstrap blocks...")); + printf("Block index empty but blk0001.dat exists - running fast import...\n"); + FastImportBlockFile(); + } // as LoadBlockIndex can take several minutes, it's possible the user // requested to kill triangles-qt during the last operation. If so, exit. diff --git a/src/main.cpp b/src/main.cpp index 86921e1..61ea233 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3023,7 +3023,14 @@ bool LoadExternalBlockFile(FILE* fileIn) { int64_t nStart = GetTimeMillis(); + // Get file size for progress reporting + int64_t nFileSize = 0; + fseek(fileIn, 0, SEEK_END); + nFileSize = ftell(fileIn); + fseek(fileIn, 0, SEEK_SET); + int nLoaded = 0; + int64_t nLastProgressReport = 0; { LOCK(cs_main); try { @@ -3074,6 +3081,20 @@ bool LoadExternalBlockFile(FILE* fileIn) nLoaded++; nPos += 4 + nSize; } + + // Report progress every 1000 blocks + if (nLoaded - nLastProgressReport >= 1000) + { + nLastProgressReport = nLoaded; + if (nFileSize > 0) { + int pct = (int)((int64_t)nPos * 100 / nFileSize); + printf("Importing blocks... %d blocks loaded (%d%%)\n", nLoaded, pct); + uiInterface.InitMessage(strprintf(_("Importing blocks... %d loaded (%d%%)"), nLoaded, pct)); + } else { + printf("Importing blocks... %d blocks loaded\n", nLoaded); + uiInterface.InitMessage(strprintf(_("Importing blocks... %d loaded"), nLoaded)); + } + } } } catch (std::exception &e) { @@ -3085,6 +3106,210 @@ bool LoadExternalBlockFile(FILE* fileIn) return nLoaded > 0; } +bool FastImportBlockFile() +{ + // Fast block import: reads blk0001.dat and builds the block index + // directly without re-writing block data. LevelDB writes are batched + // every 200K blocks for speed. Only used for trusted bootstrap data + // (blocks below the hardcoded checkpoint). + + fs::path blkPath = GetDataDir() / "blk0001.dat"; + if (!fs::exists(blkPath)) + return false; + + printf("FastImportBlockFile: starting from %s\n", blkPath.string().c_str()); + int64_t nStart = GetTimeMillis(); + + FILE* fileIn = fopen(blkPath.string().c_str(), "rb"); + if (!fileIn) + return false; + + // Get file size for progress + fseek(fileIn, 0, SEEK_END); + int64_t nFileSize = ftell(fileIn); + fseek(fileIn, 0, SEEK_SET); + + int nLoaded = 0; + int64_t nLastProgressReport = 0; + + { + LOCK(cs_main); + CAutoFile blkdat(fileIn, SER_DISK, CLIENT_VERSION); + + CTxDB txdb; + txdb.TxnBegin(); + + unsigned int nPos = 0; + while (nPos != (unsigned int)-1 && blkdat.good() && !fRequestShutdown) + { + // Find message start bytes (same scan as LoadExternalBlockFile) + unsigned char pchData[65536]; + do { + fseek(blkdat, nPos, SEEK_SET); + int nRead = fread(pchData, 1, sizeof(pchData), blkdat); + if (nRead <= 8) + { + nPos = (unsigned int)-1; + break; + } + void* nFind = memchr(pchData, pchMessageStart[0], nRead+1-sizeof(pchMessageStart)); + if (nFind) + { + if (memcmp(nFind, pchMessageStart, sizeof(pchMessageStart))==0) + { + nPos += ((unsigned char*)nFind - pchData) + sizeof(pchMessageStart); + break; + } + nPos += ((unsigned char*)nFind - pchData) + 1; + } + else + nPos += sizeof(pchData) - sizeof(pchMessageStart) + 1; + } while(!fRequestShutdown); + + if (nPos == (unsigned int)-1) + break; + + fseek(blkdat, nPos, SEEK_SET); + unsigned int nSize; + blkdat >> nSize; + + if (nSize == 0 || nSize > MAX_BLOCK_SIZE) + { + nPos += 4 + nSize; + continue; + } + + // nBlockPos = file position where the block data starts + // (after 4-byte message start + 4-byte size) + unsigned int nBlockPos = nPos + 4; + + CBlock block; + blkdat >> block; + + uint256 hash = block.GetHash(); + if (mapBlockIndex.count(hash)) + { + nPos += 4 + nSize; + continue; // already indexed + } + + // Create CBlockIndex + CBlockIndex* pindexNew = new CBlockIndex(1, nBlockPos, block); + if (!pindexNew) + break; + + // Link to previous block + map::iterator miPrev = mapBlockIndex.find(block.hashPrevBlock); + if (miPrev != mapBlockIndex.end()) + { + pindexNew->pprev = (*miPrev).second; + pindexNew->nHeight = pindexNew->pprev->nHeight + 1; + } + + // Chain trust + pindexNew->nChainTrust = (pindexNew->pprev ? pindexNew->pprev->nChainTrust : 0) + pindexNew->GetBlockTrust(); + + // Stake entropy bit + pindexNew->SetStakeEntropyBit(block.GetStakeEntropyBit()); + + // Stake modifier (minimal for blocks far below checkpoint) + int nCheckpointHeight = Checkpoints::GetTotalBlocksEstimate(); + if (pindexNew->nHeight >= nCheckpointHeight - 1000) + { + uint64_t nStakeModifier = 0; + bool fGeneratedStakeModifier = false; + ComputeNextStakeModifier(pindexNew->pprev, nStakeModifier, fGeneratedStakeModifier); + pindexNew->SetStakeModifier(nStakeModifier, fGeneratedStakeModifier); + } + else + { + pindexNew->SetStakeModifier(0, pindexNew->nHeight == 0); + } + pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew); + + // Money supply tracking + pindexNew->nMint = 0; + pindexNew->nMoneySupply = (pindexNew->pprev ? pindexNew->pprev->nMoneySupply : 0); + + // PoS stake seen set + if (pindexNew->IsProofOfStake()) + setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime)); + + // Insert into mapBlockIndex + map::iterator mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first; + pindexNew->phashBlock = &((*mi).first); + + // Link pnext for previous block + if (pindexNew->pprev) + pindexNew->pprev->pnext = pindexNew; + + // Write block index to batch + txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew)); + + // Build tx index entries + unsigned int nTxPos = nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION) + - (2 * GetSizeOfCompactSize(0)) + GetSizeOfCompactSize(block.vtx.size()); + for (unsigned int i = 0; i < block.vtx.size(); i++) + { + const CTransaction& tx = block.vtx[i]; + CDiskTxPos posThisTx(1, nBlockPos, nTxPos); + txdb.UpdateTxIndex(tx.GetHash(), CTxIndex(posThisTx, tx.vout.size())); + nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION); + } + + // Update best chain + if (pindexNew->nChainTrust > nBestChainTrust) + { + hashBestChain = hash; + pindexBest = pindexNew; + pblockindexFBBHLast = NULL; + nBestHeight = pindexNew->nHeight; + nBestChainTrust = pindexNew->nChainTrust; + nTimeBestReceived = GetTime(); + } + + // Set genesis block + if (pindexGenesisBlock == NULL && pindexNew->nHeight == 0) + pindexGenesisBlock = pindexNew; + + nLoaded++; + nPos += 4 + nSize; + + // Batch commit every 200K blocks for LevelDB efficiency + if (nLoaded % 200000 == 0) + { + txdb.WriteHashBestChain(hashBestChain); + txdb.TxnCommit(); + txdb.TxnBegin(); + } + + // Report progress every 5000 blocks to keep GUI responsive. + // AppInit2 runs on the GUI thread, so uiInterface.InitMessage + // triggers processEvents() which prevents the window from freezing. + if (nLoaded % 5000 == 0) + { + int pct = (nFileSize > 0) ? (int)((int64_t)nPos * 100 / nFileSize) : 0; + printf("FastImport: %d blocks indexed (%d%%)\n", nLoaded, pct); + uiInterface.InitMessage(strprintf(_("Importing blocks... %d indexed (%d%%)"), nLoaded, pct)); + } + } + + // Final commit + if (pindexBest) + { + txdb.WriteHashBestChain(hashBestChain); + + // Write sync checkpoint + Checkpoints::WriteSyncCheckpoint(hashBestChain); + } + txdb.TxnCommit(); + } + + nTransactionsUpdated++; + printf("FastImportBlockFile: indexed %d blocks in %"PRId64"ms\n", nLoaded, GetTimeMillis() - nStart); + return nLoaded > 0; +} + ////////////////////////////////////////////////////////////////////////////// // // CAlert @@ -3497,11 +3722,12 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) // Trigger them to send a getblocks request for the next batch of inventory if (inv.hash == pfrom->hashContinue) { - // triangles: send latest proof-of-work block to allow the - // download node to accept as orphan (proof-of-stake - // block might be rejected by stake connection check) + // Send the best block hash to trigger the next getblocks. + // Original code sent the last PoW block, but since PoW ended + // at block 9000, that always sent an ancient block causing + // thousands of redundant round-trips through known blocks. vector vInv; - vInv.push_back(CInv(MSG_BLOCK, GetLastBlockIndex(pindexBest, false)->GetBlockHash())); + vInv.push_back(CInv(MSG_BLOCK, hashBestChain)); pfrom->PushMessage("inv", vInv); pfrom->hashContinue = 0; } @@ -3549,7 +3775,11 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) // Send the rest of the chain if (pindex) pindex = pindex->pnext; - int nLimit = IsInitialBlockDownload() ? 20000 : 500; + // Send larger batches when the requester is far behind (syncing). + // The original check used our own IBD state, but we're the seed node + // (fully synced), so it always returned 500. Check how far behind + // the requester is instead. + int nLimit = (pindex && pindexBest && pindexBest->nHeight - pindex->nHeight > 1000) ? 10000 : 500; printf("IBD-DIAG: getblocks request from peer %s: start=%d stop=%s limit=%d\n", pfrom->addr.ToString().c_str(), (pindex ? pindex->nHeight : -1), hashStop.ToString().substr(0,20).c_str(), nLimit); @@ -3775,7 +4005,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) if (IsInitialBlockDownload()) { static int nBlocksSinceRequest = 0; - if (++nBlocksSinceRequest >= 100) + if (++nBlocksSinceRequest >= 5000) { nBlocksSinceRequest = 0; pfrom->pindexLastGetBlocksBegin = NULL; @@ -4231,7 +4461,7 @@ bool SendMessages(CNode* pto, bool fSendTrickle) // - // Stall detection: if IBD and no new blocks for 5 seconds, re-request + // Stall detection: if IBD and no new blocks for 10 seconds, re-request // if (IsInitialBlockDownload() && !pto->fClient) { @@ -4241,8 +4471,8 @@ bool SendMessages(CNode* pto, bool fSendTrickle) if (nBestHeight > nLastHeight) { nLastHeight = nBestHeight; nLastBlockReceived = GetTime(); - } else if (nLastBlockReceived > 0 && GetTime() - nLastBlockReceived > 2) { - if (GetTime() - nLastStallLog >= 10) { // log every 10s max + } else if (nLastBlockReceived > 0 && GetTime() - nLastBlockReceived > 10) { + if (GetTime() - nLastStallLog >= 30) { // log every 30s max printf("IBD-DIAG: STALL at height %d for %ds, peer=%s askfor_queue=%d send_size=%d\n", nBestHeight, (int)(GetTime() - nLastBlockReceived), pto->addr.ToString().c_str(), diff --git a/src/main.h b/src/main.h index b57dd4f..830b393 100644 --- a/src/main.h +++ b/src/main.h @@ -112,6 +112,7 @@ CBlockIndex* FindBlockByHeight(int nHeight); bool ProcessMessages(CNode* pfrom); bool SendMessages(CNode* pto, bool fSendTrickle); bool LoadExternalBlockFile(FILE* fileIn); +bool FastImportBlockFile(); bool CheckProofOfWork(uint256 hash, unsigned int nBits); unsigned int GetNextTargetRequired(const CBlockIndex* pindexLast, bool fProofOfStake); diff --git a/src/miner.cpp b/src/miner.cpp index 73986af..33e3fd4 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -558,6 +558,16 @@ void StakeMiner(CWallet *pwallet) } } + // + // Update cached stake weight for UI display (avoids heavy work on UI thread) + // + { + uint64_t nMinWeight = 0, nMaxWeight = 0, nWeight = 0; + pwallet->GetStakeWeight(*pwallet, nMinWeight, nMaxWeight, nWeight); + pwallet->nCachedStakeWeight = nWeight; + pwallet->nCachedStakeWeightTime = GetTime(); + } + // // Create new block // diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index 6c29e38..f67f51c 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -15,7 +15,7 @@ static const int64_t nClientStartupTime = GetTime(); ClientModel::ClientModel(OptionsModel *optionsModel, QObject *parent) : QObject(parent), optionsModel(optionsModel), - cachedNumBlocks(0), cachedNumBlocksOfPeers(0), pollTimer(0) + cachedNumBlocks(0), cachedNumBlocksOfPeers(0), cachedNumConnections(0), pollTimer(0) { numBlocksAtStartup = -1; @@ -34,7 +34,14 @@ ClientModel::~ClientModel() int ClientModel::getNumConnections() const { - return vNodes.size(); + // Use TRY_LOCK to avoid blocking the UI thread when the network + // thread holds cs_vNodes (e.g. during DNS resolution or connections). + // Return the cached value if the lock is busy. + TRY_LOCK(cs_vNodes, lockNodes); + if (lockNodes) { + cachedNumConnections = vNodes.size(); + } + return cachedNumConnections; } int ClientModel::getNumBlocks() const diff --git a/src/qt/clientmodel.h b/src/qt/clientmodel.h index 837566d..8c90776 100644 --- a/src/qt/clientmodel.h +++ b/src/qt/clientmodel.h @@ -49,6 +49,7 @@ private: int cachedNumBlocks; int cachedNumBlocksOfPeers; + mutable int cachedNumConnections; int numBlocksAtStartup; diff --git a/src/qt/guiconstants.h b/src/qt/guiconstants.h index 98b49c9..5bb8225 100644 --- a/src/qt/guiconstants.h +++ b/src/qt/guiconstants.h @@ -2,7 +2,7 @@ #define GUICONSTANTS_H /* Milliseconds between model updates */ -static const int MODEL_UPDATE_DELAY = 500; +static const int MODEL_UPDATE_DELAY = 2500; /* AskPassphraseDialog -- Maximum passphrase length */ static const int MAX_PASSPHRASE_SIZE = 1024; diff --git a/src/qt/introdialog.cpp b/src/qt/introdialog.cpp index 5b75278..c8d72fe 100644 --- a/src/qt/introdialog.cpp +++ b/src/qt/introdialog.cpp @@ -231,62 +231,45 @@ bool IntroDialog::pickDataDirectory() { std::string host = Bootstrap::DEFAULT_HOST; std::string strError; - std::vector 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); + QProgressDialog progress("Downloading blockchain snapshot...", "Cancel", + 0, 100, 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; + auto progressFn = [&progress](int64_t bytesDownloaded, int64_t totalBytes) { + if (totalBytes > 0) { + int pct = (int)((bytesDownloaded * 100) / totalBytes); + progress.setValue(pct); + progress.setLabelText( + QString("Downloading blockchain snapshot... %1 MB / %2 MB") + .arg(bytesDownloaded / (1024*1024)) + .arg(totalBytes / (1024*1024))); + } else { + progress.setLabelText( + QString("Downloading blockchain snapshot... %1 MB") + .arg(bytesDownloaded / (1024*1024))); } + QApplication::processEvents(); + }; + + bool success = Bootstrap::DownloadBootstrap(host, dataDirPath, progressFn, strError); + if (!success) { + host = Bootstrap::FALLBACK_HOST; + progress.setValue(0); + success = Bootstrap::DownloadBootstrap(host, dataDirPath, progressFn, strError); } - if (!failed) - progress.setValue((int)files.size()); + if (!success) { + QMessageBox::warning(0, "Triangles", + QString("Could not download blockchain snapshot:\n%1\n\n" + "The wallet will sync from the network instead.") + .arg(QString::fromStdString(strError))); + } else { + progress.setValue(100); + } } } diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index f58df31..5815861 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -126,6 +126,8 @@ OverviewPage::~OverviewPage() void OverviewPage::setBalance(qint64 balance, qint64 stake, qint64 unconfirmedBalance, qint64 immatureBalance) { + if (!model || !model->getOptionsModel()) + return; int unit = model->getOptionsModel()->getDisplayUnit(); currentBalance = balance; currentStake = stake; diff --git a/src/qt/trianglesgui.cpp b/src/qt/trianglesgui.cpp index 094bfad..01ef56d 100644 --- a/src/qt/trianglesgui.cpp +++ b/src/qt/trianglesgui.cpp @@ -799,19 +799,23 @@ void TrianglesGUI::setNumConnections(int count) void TrianglesGUI::setNumBlocks(int count, int nTotalBlocks) { - // don't show / hide progress bar and its label if we have no connection to the network - if (!clientModel || clientModel->getNumConnections() == 0) + if (!clientModel) + return; + + int nConnections = clientModel->getNumConnections(); + + // Hide progress bar when disconnected, but don't return early - + // we still need to update sync state and the out-of-sync warning + if (nConnections == 0) { progressBarLabel->setVisible(false); progressBar->setVisible(false); ui->label_blocks->setVisible(false); - - return; } QString tooltip; - if(count < nTotalBlocks) + if(nConnections > 0 && count < nTotalBlocks) { // Calculate blocks/sec - only update rate when new blocks arrive static int lastCount = 0; @@ -899,20 +903,19 @@ void TrianglesGUI::setNumBlocks(int count, int nTotalBlocks) text = tr("%n day(s) ago","",secs/(60*60*24)); } - // Set icon state: spinning if catching up, tick otherwise - if(secs < 90*60 && count >= nTotalBlocks) + // Set icon state: spinning if catching up, tick otherwise. + // Use a generous threshold (6 hours) for PoS chains where block intervals + // can be long during difficulty adjustment with few stakers. + if(secs < 6*60*60 && count >= nTotalBlocks) { tooltip = tr("Up to date") + QString(".
") + tooltip; labelBlocksIcon->setPixmap(QIcon(":/icons/synced").pixmap(STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); - + overviewPage->showOutOfSyncWarning(false); } else { tooltip = tr("Catching up...") + QString("
") + tooltip; - //syncIconMovie doesn't work for some reason - using fallback png - //labelBlocksIcon->setMovie(syncIconMovie); - //syncIconMovie->start(); labelBlocksIcon->setPixmap(QIcon(":/icons/notsynced").pixmap(STATUSBAR_ICONSIZE, STATUSBAR_ICONSIZE)); overviewPage->showOutOfSyncWarning(true); @@ -1590,32 +1593,47 @@ void TrianglesGUI::toggleHidden() void TrianglesGUI::updateStakingIcon() { - uint64_t nMinWeight = 0, nMaxWeight = 0, nWeight = 0; + // Read cached staking info computed by the staking thread. + // No locks needed - these are volatile values written by the miner thread + // and are display-only. This keeps the UI thread completely non-blocking. + + uint64_t nWeight = 0; + bool fWalletLocked = false; + bool fHasPeers = false; + if (pwalletMain) - pwalletMain->GetStakeWeight(*pwalletMain, nMinWeight, nMaxWeight, nWeight); + { + TRY_LOCK(pwalletMain->cs_wallet, lockWallet); + if (lockWallet) + fWalletLocked = pwalletMain->IsLocked(); + else + return; // Skip this cycle, try again in 30 seconds + + // Use cached weight from the staking thread instead of computing on UI thread. + // The staking thread updates this every ~500ms-1s loop iteration. + nWeight = pwalletMain->nCachedStakeWeight; + } + + { + TRY_LOCK(cs_vNodes, lockNodes); + if (lockNodes) + fHasPeers = !vNodes.empty(); + } if (nLastCoinStakeSearchInterval && nWeight) { uint64_t nNetworkWeight = GetPoSKernelPS(); - unsigned nEstimateTime = nTargetSpacing * nNetworkWeight / nWeight; + unsigned nEstimateTime = nWeight > 0 ? nTargetSpacing * nNetworkWeight / nWeight : 0; QString text; if (nEstimateTime < 60) - { text = tr("%n second(s)", "", nEstimateTime); - } else if (nEstimateTime < 60*60) - { text = tr("%n minute(s)", "", nEstimateTime/60); - } else if (nEstimateTime < 24*60*60) - { text = tr("%n hour(s)", "", nEstimateTime/(60*60)); - } else - { text = tr("%n day(s)", "", nEstimateTime/(60*60*24)); - } labelStakingIcon->setPixmap(QIcon(":/icons/staking_on").pixmap(STATUSBAR_ICONSIZE,STATUSBAR_ICONSIZE)); labelStakingIcon->setToolTip(tr("Staking.
Your weight is %1
Network weight is %2
Expected time to earn reward is %3").arg(nWeight).arg(nNetworkWeight).arg(text)); @@ -1623,9 +1641,9 @@ void TrianglesGUI::updateStakingIcon() else { labelStakingIcon->setPixmap(QIcon(":/icons/staking_off").pixmap(STATUSBAR_ICONSIZE,STATUSBAR_ICONSIZE)); - if (pwalletMain && pwalletMain->IsLocked()) + if (fWalletLocked) labelStakingIcon->setToolTip(tr("Not staking because wallet is locked")); - else if (vNodes.empty()) + else if (!fHasPeers) labelStakingIcon->setToolTip(tr("Not staking because wallet is offline")); else if (IsInitialBlockDownload()) labelStakingIcon->setToolTip(tr("Not staking because wallet is syncing")); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 9f6ffd6..13cc767 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -92,10 +92,10 @@ void WalletModel::pollBalanceChanged() void WalletModel::checkBalanceChanged() { - qint64 newBalance = getBalance(); - qint64 newStake = getStake(); - qint64 newUnconfirmedBalance = getUnconfirmedBalance(); - qint64 newImmatureBalance = getImmatureBalance(); + // Get all balances in a single lock acquisition + single pass + // instead of 4 separate lock+iterate cycles + qint64 newBalance = 0, newStake = 0, newUnconfirmedBalance = 0, newImmatureBalance = 0; + wallet->GetAllBalances(newBalance, newStake, newUnconfirmedBalance, newImmatureBalance); if(cachedBalance != newBalance || cachedStake != newStake || cachedUnconfirmedBalance != newUnconfirmedBalance || cachedImmatureBalance != newImmatureBalance) { diff --git a/src/version.h b/src/version.h index f0e5670..b124feb 100644 --- a/src/version.h +++ b/src/version.h @@ -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 3 +#define DISPLAY_VERSION_REVISION 4 #define DISPLAY_VERSION_BUILD 0 #endif diff --git a/src/wallet.cpp b/src/wallet.cpp index f043613..6e0cdbc 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -1476,6 +1476,31 @@ int64_t CWallet::GetNewMint() const return nTotal; } +void CWallet::GetAllBalances(int64_t& nBalance, int64_t& nStake, int64_t& nUnconfirmed, int64_t& nImmature) const +{ + nBalance = 0; + nStake = 0; + nUnconfirmed = 0; + nImmature = 0; + LOCK(cs_wallet); + for (map::const_iterator it = mapWallet.begin(); it != mapWallet.end(); ++it) + { + const CWalletTx& pcoin = (*it).second; + + if (pcoin.IsCoinStake() && pcoin.GetBlocksToMaturity() > 0 && pcoin.GetDepthInMainChain() > 0) + nStake += CWallet::GetCredit(pcoin); + + if (pcoin.IsCoinBase() && pcoin.GetBlocksToMaturity() > 0 && pcoin.IsInMainChain()) + nImmature += GetCredit(pcoin); + + if (pcoin.IsTrusted()) + nBalance += pcoin.GetAvailableCredit(); + + if (!pcoin.IsFinal() || !pcoin.IsTrusted()) + nUnconfirmed += pcoin.GetAvailableCredit(); + } +} + bool CWallet::SelectCoinsMinConf(int64_t nTargetValue, unsigned int nSpendTime, int nConfMine, int nConfTheirs, vector vCoins, set >& setCoinsRet, int64_t& nValueRet) const { setCoinsRet.clear(); diff --git a/src/wallet.h b/src/wallet.h index df7de4c..fa2dab4 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -107,6 +107,8 @@ public: nMasterKeyMaxID = 0; pwalletdbEncryption = NULL; nOrderPosNext = 0; + nCachedStakeWeight = 0; + nCachedStakeWeightTime = 0; } CWallet(std::string strWalletFileIn) { @@ -117,6 +119,8 @@ public: nMasterKeyMaxID = 0; pwalletdbEncryption = NULL; nOrderPosNext = 0; + nCachedStakeWeight = 0; + nCachedStakeWeightTime = 0; } std::map mapWallet; @@ -190,6 +194,8 @@ public: int64_t GetImmatureBalance() const; int64_t GetStake() const; int64_t GetNewMint() const; + // Get all balances in a single lock acquisition + single pass (avoids 4x lock + 4x iteration) + void GetAllBalances(int64_t& nBalance, int64_t& nStake, int64_t& nUnconfirmed, int64_t& nImmature) const; bool CreateTransaction(const std::vector >& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, int64_t& nFeeRet, const CCoinControl *coinControl=NULL); bool CreateTransaction(CScript scriptPubKey, int64_t nValue, std::string& sNarr, CWalletTx& wtxNew, CReserveKey& reservekey, int64_t& nFeeRet, const CCoinControl *coinControl=NULL); bool CommitTransaction(CWalletTx& wtxNew, CReserveKey& reservekey); @@ -197,6 +203,11 @@ public: bool GetStakeWeight(const CKeyStore& keystore, uint64_t& nMinWeight, uint64_t& nMaxWeight, uint64_t& nWeight); bool CreateCoinStake(const CKeyStore& keystore, unsigned int nBits, int64_t nSearchInterval, int64_t nFees, CTransaction& txNew, CKey& key); + // Cached staking info - updated by the staking thread, read by the UI thread. + // Access is safe without locks: written atomically by the miner, read by UI for display only. + volatile uint64_t nCachedStakeWeight; + volatile int64_t nCachedStakeWeightTime; // GetTime() when last updated + std::string SendMoney(CScript scriptPubKey, int64_t nValue, std::string& sNarr, CWalletTx& wtxNew, bool fAskFee=false); std::string SendMoneyToDestination(const CTxDestination& address, int64_t nValue, std::string& sNarr, CWalletTx& wtxNew, bool fAskFee=false);