3a6e0470a5
Processing can take several seconds on a mobile, so we should do that in its own thread in order to make the progress icon continue rotating.
351 lines
11 KiB
C++
351 lines
11 KiB
C++
/*
|
|
* This file is part of the Flowee project
|
|
* Copyright (C) 2024 Tom Zander <tom@flowee.org>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "BlockHeadersChecker.h"
|
|
#include "FloweePay.h"
|
|
|
|
#include <utils/Logger.h>
|
|
#include <utils/streaming/BufferPools.h>
|
|
#include <p2p/Blockchain.h>
|
|
#include <QTimer>
|
|
#include <QNetworkReply>
|
|
|
|
static constexpr const char * HEADERFILE = "https://flowee.org/products/pay/blockheaders";
|
|
|
|
#define TEMP_DOWNLOAD_FILENAME "module_blocks_newheaders"
|
|
#define PAY_STATIC_HEADERS "staticHeaders"
|
|
|
|
BlockHeadersChecker::BlockHeadersChecker(QObject *parent)
|
|
: QObject(parent)
|
|
{
|
|
connect (this, &BlockHeadersChecker::finishUp, this, [=](Status status) {
|
|
this->setStatus(status);
|
|
}, Qt::QueuedConnection);
|
|
}
|
|
|
|
int BlockHeadersChecker::wantedHeight() const
|
|
{
|
|
return m_wantedHeight;
|
|
}
|
|
|
|
void BlockHeadersChecker::setWantedHeight(int h)
|
|
{
|
|
if (m_wantedHeight == h)
|
|
return;
|
|
m_wantedHeight = h;
|
|
emit wantedHeightChanged();
|
|
|
|
QTimer::singleShot(0, this, SLOT(startChecking()));
|
|
}
|
|
|
|
void BlockHeadersChecker::startChecking()
|
|
{
|
|
logInfo() << "starting" << m_wantedHeight;
|
|
assert(m_checkpoint >= 0);
|
|
if (m_wantedHeight == 0 || FloweePay::instance()->p2pNet()->chain() != P2PNet::MainChain) {
|
|
setStatus(NoDownloadNeeded);
|
|
return;
|
|
}
|
|
setStatus(Checking);
|
|
const auto &blockchain = FloweePay::instance()->p2pNet()->blockchain();
|
|
bool have = false;
|
|
const auto sources = blockchain.dataSources();
|
|
assert(!sources.empty());
|
|
for (const auto &source : sources) {
|
|
if (source.startBlock <= m_wantedHeight)
|
|
have = true;
|
|
}
|
|
if (have) {
|
|
logDebug() << "Nothing to do, the blockchain has the needed headers";
|
|
setStatus(NoDownloadNeeded);
|
|
return;
|
|
}
|
|
|
|
int startHeight = 0;
|
|
for (const auto &cp : blockchain.checkpoints()) {
|
|
if (cp.height < m_wantedHeight)
|
|
startHeight = cp.height;
|
|
else
|
|
break; // checkpoints are ordered.
|
|
}
|
|
|
|
m_checkpoint = startHeight;
|
|
m_downloadTo = sources.front().startBlock;
|
|
|
|
// do a HEAD
|
|
if (m_headerReply == nullptr) {
|
|
QNetworkRequest headRequest((QUrl(HEADERFILE)));
|
|
headRequest.setTransferTimeout(6000);
|
|
m_headerReply = m_network.head(headRequest);
|
|
connect(m_headerReply, SIGNAL(finished()), this, SLOT(headerReturned()));
|
|
}
|
|
}
|
|
|
|
void BlockHeadersChecker::headerReturned()
|
|
{
|
|
assert(m_headerReply);
|
|
auto reply = m_headerReply;
|
|
m_headerReply->deleteLater();
|
|
m_headerReply = nullptr;
|
|
|
|
if (m_newHeaders) {
|
|
logFatal() << "Already downloading...";
|
|
return;
|
|
}
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
logDebug() << reply->errorString();
|
|
setStatus(NetworkFailure);
|
|
return;
|
|
}
|
|
|
|
uint64_t length = reply->header(QNetworkRequest::ContentLengthHeader).toLongLong();
|
|
auto allowedRange = reply->rawHeader("Accept-Ranges");
|
|
|
|
if (allowedRange != "bytes") {
|
|
setStatus(NetworkFailure);
|
|
logFatal() << "Can't download partial files. Failing";
|
|
return;
|
|
}
|
|
|
|
// Check if this is a continuation download.
|
|
assert(m_downloadTo > m_checkpoint);
|
|
assert(m_checkpoint > 0);
|
|
m_newHeaders = new QFile(TEMP_DOWNLOAD_FILENAME, this);
|
|
bool isContinuation = false;
|
|
if (m_newHeaders->open(QIODevice::ReadOnly)) {
|
|
char bytes[80];
|
|
auto read = m_newHeaders->read(bytes, 80);
|
|
if (read == 80) {
|
|
BlockHeader *bh = reinterpret_cast<BlockHeader*>(bytes);
|
|
auto hash = bh->createHash();
|
|
auto &blockchain = FloweePay::instance()->p2pNet()->blockchain();
|
|
for (const auto &cp : blockchain.checkpoints()) {
|
|
if (cp.height == m_checkpoint) {
|
|
isContinuation = cp.hash == hash;
|
|
logCritical() << "Partial file found of" << m_newHeaders->size()
|
|
<< "bytes. Continuing!";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
m_newHeaders->close();
|
|
}
|
|
|
|
// We're going to download the headers.
|
|
// On the remote server there is one file that has the genesis
|
|
// all the way up to a recent height=((length / 80) - 1)
|
|
|
|
// We use the 'range' feature of the webserver to only download
|
|
// what we need, so lets calculate the offsets here.
|
|
const int from = m_checkpoint * 80 + (isContinuation ? m_newHeaders->size() : 0);
|
|
const int to = m_downloadTo * 80;
|
|
if (to > static_cast<int>(length)) {
|
|
logCritical() << "Remote file does not have the data we want";
|
|
setStatus(NetworkFailure);
|
|
return;
|
|
}
|
|
|
|
QNetworkRequest downloadRequest((QUrl(HEADERFILE)));
|
|
downloadRequest.setTransferTimeout(30000);
|
|
QString range("bytes=%1-%2");
|
|
range = range.arg(from);
|
|
range = range.arg(to - 1); // -1 because ranges are inclusive.
|
|
downloadRequest.setRawHeader("Range", range.toLatin1());
|
|
|
|
if (!m_newHeaders->open(QIODevice::WriteOnly
|
|
| (isContinuation ? QIODevice::Append : QIODevice::Truncate))) {
|
|
logFatal() << "Can not open file...";
|
|
setStatus(DiskFailure);
|
|
return;
|
|
}
|
|
setTotalDownload(to - from);
|
|
setStatus(Downloading);
|
|
|
|
// actually start the download
|
|
m_downloadReply = m_network.get(downloadRequest);
|
|
connect(m_downloadReply, SIGNAL(readyRead()), this, SLOT(onBytesDownloaded()));
|
|
connect(m_downloadReply, SIGNAL(finished()), this, SLOT(downloadFinished()));
|
|
}
|
|
|
|
void BlockHeadersChecker::downloadFinished()
|
|
{
|
|
assert(m_downloadReply);
|
|
auto reply = m_downloadReply;
|
|
m_downloadReply->deleteLater();
|
|
m_downloadReply = nullptr;
|
|
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
logCritical() << "headers download gave:" << reply->errorString();
|
|
setStatus(NetworkFailure);
|
|
m_newHeaders->close();
|
|
m_newHeaders->deleteLater();
|
|
m_newHeaders = nullptr;
|
|
return;
|
|
}
|
|
|
|
assert(m_newHeaders->isOpen());
|
|
logDebug() << "Wrote out:" << m_newHeaders->size() << "bytes" << (m_newHeaders->size() / 80);
|
|
// --- use the file:
|
|
// the new file is the headers I didn't have yet. We need to
|
|
// create a new file that has both the old static headers as well
|
|
// as the new ones concatenated in one.
|
|
// the file we put this in has to have an application-long lifetime.
|
|
// so lets give ownership to the main app.
|
|
|
|
QFile oldStaticHeaders(PAY_STATIC_HEADERS);
|
|
if (!oldStaticHeaders.open(QIODevice::ReadOnly)) {
|
|
logFatal() << "Failed to find old static headers";
|
|
setStatus(DiskFailure);
|
|
return;
|
|
}
|
|
logDebug() << " old ones:" << oldStaticHeaders.size() << "=>" << oldStaticHeaders.size() / 80;
|
|
setStatus(Verifying);
|
|
char buf[4096];
|
|
while (true) {
|
|
auto count = oldStaticHeaders.read(buf, sizeof(buf));
|
|
if (count <= 0)
|
|
break;
|
|
m_newHeaders->write(buf, count);
|
|
}
|
|
oldStaticHeaders.close();
|
|
m_newHeaders->close();
|
|
|
|
// make sure that the UI can update the changes we pushed here
|
|
// before we start the process new headers which is going to take
|
|
// some actual processing time, depending on how big our headers
|
|
// are now.
|
|
// keep the file object from getting garbage collected.
|
|
// the map() requires the QFile to stay alive.
|
|
m_newHeaders->setParent(FloweePay::instance());
|
|
FloweePay::instance()->ioService().post(std::bind(&BlockHeadersChecker::processNewHeaders, this));
|
|
}
|
|
|
|
void BlockHeadersChecker::processNewHeaders()
|
|
{
|
|
// the currenty in use static headers are in the location I want the new ones to be
|
|
// because when we restart, we open by path.
|
|
//
|
|
// After a successful attempt to replace staticChain we'll thus remove the old
|
|
// staticHeaders file and move the one on top of that.
|
|
// "staticHeaders_new" -> finished,
|
|
// static headers path...
|
|
// Knowing that this will never run on Windowz, lets just rename the open file and
|
|
// place the newly created file at the known path.
|
|
|
|
struct RAII {
|
|
explicit RAII(BlockHeadersChecker *dd) : d(dd) {}
|
|
~RAII() {
|
|
emit d->finishUp(status);
|
|
}
|
|
BlockHeadersChecker *d;
|
|
Status status = Success;
|
|
};
|
|
RAII raii(this);
|
|
|
|
if (!m_newHeaders->open(QIODevice::ReadOnly)) {
|
|
logFatal() << "Failed to re-open replacement static headers";
|
|
raii.status = DiskFailure;
|
|
return;
|
|
}
|
|
try {
|
|
logCritical() << "Prepared a new static headers chain. Size:" << m_newHeaders->size()
|
|
<< "Replacing the current one with this new file";
|
|
auto &blockchain = FloweePay::instance()->p2pNet()->blockchain();
|
|
blockchain.replaceStaticChain(m_newHeaders->map(0, m_newHeaders->size()),
|
|
m_newHeaders->size(), TEMP_DOWNLOAD_FILENAME ".info");
|
|
m_newHeaders->close();
|
|
|
|
// make sure we use the new ones after restart.
|
|
QFile::remove(PAY_STATIC_HEADERS);
|
|
QFile::remove(PAY_STATIC_HEADERS ".info");
|
|
QFile::rename(TEMP_DOWNLOAD_FILENAME ".info", PAY_STATIC_HEADERS ".info");
|
|
m_newHeaders->rename(PAY_STATIC_HEADERS);
|
|
} catch (const std::exception &e) {
|
|
logCritical() << "Replace static chain failed. Reason:" << e;
|
|
raii.status = NetworkFailure;
|
|
m_newHeaders->close();
|
|
m_newHeaders->remove();
|
|
m_newHeaders->deleteLater();
|
|
m_newHeaders = nullptr;
|
|
}
|
|
}
|
|
|
|
void BlockHeadersChecker::onBytesDownloaded()
|
|
{
|
|
assert(m_downloadReply);
|
|
assert(m_newHeaders);
|
|
assert(m_newHeaders->isOpen());
|
|
|
|
int downloaded = 0;
|
|
char buf[4096];
|
|
while (true) {
|
|
auto readCount = m_downloadReply->read(buf, sizeof(buf));
|
|
if (readCount <= 0)
|
|
break;
|
|
m_newHeaders->write(buf, readCount);
|
|
downloaded += readCount;
|
|
}
|
|
setBytesDownloaded(m_bytesDownloaded + downloaded);
|
|
}
|
|
|
|
BlockHeadersChecker::Status BlockHeadersChecker::status() const
|
|
{
|
|
return m_status;
|
|
}
|
|
|
|
void BlockHeadersChecker::setStatus(Status newStatus)
|
|
{
|
|
if (m_status == newStatus)
|
|
return;
|
|
m_status = newStatus;
|
|
emit statusChanged();
|
|
}
|
|
|
|
void BlockHeadersChecker::restart()
|
|
{
|
|
if (m_headerReply == nullptr
|
|
&& m_downloadReply == nullptr
|
|
&& m_newHeaders == nullptr)
|
|
startChecking();
|
|
}
|
|
|
|
int BlockHeadersChecker::totalDownload() const
|
|
{
|
|
return m_totalDownload;
|
|
}
|
|
|
|
void BlockHeadersChecker::setTotalDownload(int newTotalDownload)
|
|
{
|
|
if (m_totalDownload == newTotalDownload)
|
|
return;
|
|
m_totalDownload = newTotalDownload;
|
|
emit totalDownloadChanged();
|
|
}
|
|
|
|
void BlockHeadersChecker::setBytesDownloaded(int count)
|
|
{
|
|
if (m_bytesDownloaded == count)
|
|
return;
|
|
m_bytesDownloaded = count;
|
|
emit bytesDownloadedChanged();
|
|
}
|
|
|
|
int BlockHeadersChecker::bytesDownloaded()
|
|
{
|
|
return m_bytesDownloaded;
|
|
}
|