diff --git a/src/interfaces/node.h b/src/interfaces/node.h index 78a186c5d96..10e607b6113 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include @@ -204,6 +206,9 @@ class Node //! List rpc commands. virtual std::vector listRpcCommands() = 0; + //! UTXO Snapshot interface. + virtual std::unique_ptr snapshot(const fs::path& path) = 0; + //! Get unspent output associated with a transaction. virtual std::optional getUnspentOutput(const COutPoint& output) = 0; @@ -233,6 +238,10 @@ class Node using ShowProgressFn = std::function; virtual std::unique_ptr handleShowProgress(ShowProgressFn fn) = 0; + //! Register handler for snapshot load progress. + using SnapshotLoadProgressFn = std::function; + virtual std::unique_ptr handleSnapshotLoadProgress(SnapshotLoadProgressFn fn) = 0; + //! Register handler for wallet loader constructed messages. using InitWalletFn = std::function; virtual std::unique_ptr handleInitWallet(InitWalletFn fn) = 0; diff --git a/src/interfaces/snapshot.h b/src/interfaces/snapshot.h new file mode 100644 index 00000000000..0005a520e47 --- /dev/null +++ b/src/interfaces/snapshot.h @@ -0,0 +1,43 @@ +// Copyright (c) 2024 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_INTERFACES_SNAPSHOT_H +#define BITCOIN_INTERFACES_SNAPSHOT_H + +#include +#include +#include + +class CBlockIndex; +namespace node { +class SnapshotMetadata; +} + +namespace interfaces { + +//! Interface for managing UTXO snapshots. +class Snapshot +{ +public: + virtual ~Snapshot() = default; + + //! Activate the snapshot, making it the active chainstate. + virtual bool activate() = 0; + + //! Get the snapshot metadata. + virtual const node::SnapshotMetadata& getMetadata() const = 0; + + //! Get the activation result (block index of the snapshot base). + virtual std::optional getActivationResult() const = 0; + + //! Get the last error message from activation attempt. + virtual std::string getLastError() const = 0; + + //! Get the path of the snapshot. + virtual fs::path getPath() const = 0; +}; + +} // namespace interfaces + +#endif // BITCOIN_INTERFACES_SNAPSHOT_H diff --git a/src/kernel/notifications_interface.h b/src/kernel/notifications_interface.h index 816f06307be..58cd761d8f7 100644 --- a/src/kernel/notifications_interface.h +++ b/src/kernel/notifications_interface.h @@ -40,6 +40,7 @@ class Notifications [[nodiscard]] virtual InterruptResult blockTip(SynchronizationState state, const CBlockIndex& index, double verification_progress) { return {}; } virtual void headerTip(SynchronizationState state, int64_t height, int64_t timestamp, bool presync) {} virtual void progress(const bilingual_str& title, int progress_percent, bool resume_possible) {} + virtual void snapshotLoadProgress(double progress) {} virtual void warningSet(Warning id, const bilingual_str& message) {} virtual void warningUnset(Warning id) {} diff --git a/src/node/interface_ui.cpp b/src/node/interface_ui.cpp index 273e51974e3..c26d101b8b4 100644 --- a/src/node/interface_ui.cpp +++ b/src/node/interface_ui.cpp @@ -23,6 +23,7 @@ struct UISignals { boost::signals2::signal NotifyNetworkActiveChanged; boost::signals2::signal NotifyAlertChanged; boost::signals2::signal ShowProgress; + boost::signals2::signal SnapshotLoadProgress; boost::signals2::signal NotifyBlockTip; boost::signals2::signal NotifyHeaderTip; boost::signals2::signal BannedListChanged; @@ -43,6 +44,7 @@ ADD_SIGNALS_IMPL_WRAPPER(NotifyNumConnectionsChanged); ADD_SIGNALS_IMPL_WRAPPER(NotifyNetworkActiveChanged); ADD_SIGNALS_IMPL_WRAPPER(NotifyAlertChanged); ADD_SIGNALS_IMPL_WRAPPER(ShowProgress); +ADD_SIGNALS_IMPL_WRAPPER(SnapshotLoadProgress); ADD_SIGNALS_IMPL_WRAPPER(NotifyBlockTip); ADD_SIGNALS_IMPL_WRAPPER(NotifyHeaderTip); ADD_SIGNALS_IMPL_WRAPPER(BannedListChanged); @@ -55,6 +57,7 @@ void CClientUIInterface::NotifyNumConnectionsChanged(int newNumConnections) { re void CClientUIInterface::NotifyNetworkActiveChanged(bool networkActive) { return g_ui_signals.NotifyNetworkActiveChanged(networkActive); } void CClientUIInterface::NotifyAlertChanged() { return g_ui_signals.NotifyAlertChanged(); } void CClientUIInterface::ShowProgress(const std::string& title, int nProgress, bool resume_possible) { return g_ui_signals.ShowProgress(title, nProgress, resume_possible); } +void CClientUIInterface::SnapshotLoadProgress(double progress) { return g_ui_signals.SnapshotLoadProgress(progress); } void CClientUIInterface::NotifyBlockTip(SynchronizationState s, const CBlockIndex& block, double verification_progress) { return g_ui_signals.NotifyBlockTip(s, block, verification_progress); } void CClientUIInterface::NotifyHeaderTip(SynchronizationState s, int64_t height, int64_t timestamp, bool presync) { return g_ui_signals.NotifyHeaderTip(s, height, timestamp, presync); } void CClientUIInterface::BannedListChanged() { return g_ui_signals.BannedListChanged(); } diff --git a/src/node/interface_ui.h b/src/node/interface_ui.h index 7732cf47977..6f25870c49d 100644 --- a/src/node/interface_ui.h +++ b/src/node/interface_ui.h @@ -102,6 +102,9 @@ class CClientUIInterface */ ADD_SIGNALS_DECL_WRAPPER(ShowProgress, void, const std::string& title, int nProgress, bool resume_possible); + /** Snapshot load progress. */ + ADD_SIGNALS_DECL_WRAPPER(SnapshotLoadProgress, void, double progress); + /** New block has been accepted */ ADD_SIGNALS_DECL_WRAPPER(NotifyBlockTip, void, SynchronizationState, const CBlockIndex& block, double verification_progress); diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index fd3fa226cae..21ec886a18f 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -80,6 +82,7 @@ using interfaces::Handler; using interfaces::MakeSignalHandler; using interfaces::Mining; using interfaces::Node; +using interfaces::Snapshot; using interfaces::WalletLoader; using node::BlockAssembler; using node::BlockWaitOptions; @@ -89,6 +92,78 @@ namespace node { // All members of the classes in this namespace are intentionally public, as the // classes themselves are private. namespace { +class SnapshotImpl : public interfaces::Snapshot +{ +public: + SnapshotImpl(ChainstateManager& chainman, const fs::path& path, bool in_memory) + : m_chainman(chainman), m_path(path), m_in_memory(in_memory), m_metadata(chainman.GetParams().MessageStart()) {} + + bool activate() override + { + m_last_error.clear(); + + if (!fs::exists(m_path)) { + m_last_error = "Snapshot file does not exist"; + return false; + } + + FILE* snapshot_file{fsbridge::fopen(m_path, "rb")}; + AutoFile afile{snapshot_file}; + if (afile.IsNull()) { + m_last_error = "Unable to open snapshot file for reading"; + return false; + } + + try { + afile >> m_metadata; + } catch (const std::ios_base::failure& e) { + m_last_error = strprintf("Unable to parse metadata: %s", e.what()); + return false; + } + + auto result = m_chainman.ActivateSnapshot(afile, m_metadata, m_in_memory); + if (result.has_value()) { + m_activation_result = result.value(); + return true; + } else { + m_activation_result = std::nullopt; + m_last_error = util::ErrorString(result).original; + return false; + } + } + + const node::SnapshotMetadata& getMetadata() const override + { + return m_metadata; + } + + std::optional getActivationResult() const override + { + if (m_activation_result.has_value()) { + return m_activation_result; + } + return std::nullopt; + } + + std::string getLastError() const override + { + return m_last_error; + } + + fs::path getPath() const override + { + return m_path; + } + +private: + ChainstateManager& m_chainman; + fs::path m_path; + bool m_in_memory; + node::SnapshotMetadata m_metadata; + std::optional m_activation_result; + std::string m_last_error; +}; + #ifdef ENABLE_EXTERNAL_SIGNER class ExternalSignerImpl : public interfaces::ExternalSigner { @@ -356,6 +431,10 @@ class NodeImpl : public Node return ::tableRPC.execute(req); } std::vector listRpcCommands() override { return ::tableRPC.listCommands(); } + std::unique_ptr snapshot(const fs::path& path) override + { + return std::make_unique(chainman(), path, /*in_memory=*/ false); + } std::optional getUnspentOutput(const COutPoint& output) override { LOCK(::cs_main); @@ -385,6 +464,10 @@ class NodeImpl : public Node { return MakeSignalHandler(::uiInterface.ShowProgress_connect(fn)); } + std::unique_ptr handleSnapshotLoadProgress(SnapshotLoadProgressFn fn) override + { + return MakeSignalHandler(::uiInterface.SnapshotLoadProgress_connect(fn)); + } std::unique_ptr handleInitWallet(InitWalletFn fn) override { return MakeSignalHandler(::uiInterface.InitWallet_connect(fn)); diff --git a/src/node/kernel_notifications.cpp b/src/node/kernel_notifications.cpp index 56c7188ac1a..ee7dc7b90e7 100644 --- a/src/node/kernel_notifications.cpp +++ b/src/node/kernel_notifications.cpp @@ -77,6 +77,11 @@ void KernelNotifications::progress(const bilingual_str& title, int progress_perc uiInterface.ShowProgress(title.translated, progress_percent, resume_possible); } +void KernelNotifications::snapshotLoadProgress(double progress) +{ + uiInterface.SnapshotLoadProgress(progress); +} + void KernelNotifications::warningSet(kernel::Warning id, const bilingual_str& message) { if (m_warnings.Set(id, message)) { diff --git a/src/node/kernel_notifications.h b/src/node/kernel_notifications.h index 21d2ea43241..4e78c6714e5 100644 --- a/src/node/kernel_notifications.h +++ b/src/node/kernel_notifications.h @@ -41,6 +41,8 @@ class KernelNotifications : public kernel::Notifications void progress(const bilingual_str& title, int progress_percent, bool resume_possible) override; + void snapshotLoadProgress(double progress) override; + void warningSet(kernel::Warning id, const bilingual_str& message) override; void warningUnset(kernel::Warning id) override; diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index fe552a574a2..03ef0905367 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -14,7 +14,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -35,7 +37,6 @@ #include #include #include -#include #ifdef ENABLE_WALLET #include @@ -319,6 +320,12 @@ void BitcoinApplication::InitPruneSetting(int64_t prune_MiB) optionsModel->SetPruneTargetGB(PruneMiBtoGB(prune_MiB)); } +void BitcoinApplication::setSnapshotPath(const QString& snapshot_path) +{ + qDebug() << "setSnapshotPath called with:" << snapshot_path; + m_snapshot_path = snapshot_path; +} + void BitcoinApplication::requestInitialize() { qDebug() << __func__ << ": Requesting initialize"; @@ -386,7 +393,7 @@ void BitcoinApplication::initializeResult(bool success, interfaces::BlockAndHead // Log this only after AppInitMain finishes, as then logging setup is guaranteed complete qInfo() << "Platform customization:" << platformStyle->getName(); - clientModel = new ClientModel(node(), optionsModel); + clientModel = new ClientModel(node(), optionsModel, this, m_snapshot_path); window->setClientModel(clientModel, &tip_info); // If '-min' option passed, start window minimized (iconified) or minimized to tray @@ -580,8 +587,9 @@ int GuiMain(int argc, char* argv[]) // User language is set up: pick a data directory bool did_show_intro = false; int64_t prune_MiB = 0; // Intro dialog prune configuration + QString snapshot_path; // Intro dialog snapshot path // Gracefully exit if the user cancels - if (!Intro::showIfNeeded(did_show_intro, prune_MiB)) return EXIT_SUCCESS; + if (!Intro::showIfNeeded(did_show_intro, prune_MiB, snapshot_path)) return EXIT_SUCCESS; /// 6-7. Parse bitcoin.conf, determine network, switch to network specific /// options, and create datadir and settings.json. @@ -662,6 +670,11 @@ int GuiMain(int argc, char* argv[]) app.InitPruneSetting(prune_MiB); } + // Store snapshot path for later loading after node initialization + if (!snapshot_path.isEmpty()) { + app.setSnapshotPath(snapshot_path); + } + try { app.createWindow(networkStyle.data()); diff --git a/src/qt/bitcoin.h b/src/qt/bitcoin.h index abc478bd456..fdb726695c2 100644 --- a/src/qt/bitcoin.h +++ b/src/qt/bitcoin.h @@ -16,6 +16,8 @@ #include +enum class SynchronizationState; + class BitcoinGUI; class ClientModel; class NetworkStyle; @@ -48,6 +50,12 @@ class BitcoinApplication: public QApplication [[nodiscard]] bool createOptionsModel(bool resetSettings); /// Initialize prune setting void InitPruneSetting(int64_t prune_MiB); + /// Set snapshot path for loading after node initialization + void setSnapshotPath(const QString& snapshot_path); + /// Test method to manually trigger snapshot loading (for debugging) + void testLoadSnapshot(); + /// Force load snapshot regardless of sync status (for debugging) + void forceLoadSnapshot(); /// Create main window void createWindow(const NetworkStyle *networkStyle); /// Create splash screen @@ -66,6 +74,10 @@ class BitcoinApplication: public QApplication /// Setup platform style void setupPlatformStyle(); + void loadSnapshotIfNeeded(); + bool areHeadersSynced() const; + void onHeaderTipChanged(SynchronizationState sync_state, interfaces::BlockTip tip, bool presync); + interfaces::Node& node() const { assert(m_node); return *m_node; } public Q_SLOTS: @@ -103,6 +115,9 @@ public Q_SLOTS: std::unique_ptr shutdownWindow; SplashScreen* m_splash = nullptr; std::unique_ptr m_node; + QString m_snapshot_path; + std::unique_ptr m_header_tip_handler; + QTimer* m_sync_check_timer; void startThread(); }; diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index dda26fa059b..157c78e8fa3 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -17,8 +17,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -26,18 +28,57 @@ #include #include +#include #include #include #include +// SnapshotLoadWorker implementation +SnapshotLoadWorker::SnapshotLoadWorker(const fs::path& path, interfaces::Node& node, QObject* parent) + : QObject(parent), m_path(path), m_node(node) +{ +} + +void SnapshotLoadWorker::loadSnapshot() +{ + try { + // Create snapshot object using the node interface + auto snapshot = m_node.snapshot(m_path); + if (!snapshot) { + Q_EMIT finished(false, tr("Failed to create snapshot object")); + return; + } + + // Set up progress handler - it will be kept alive by the node until the operation completes + auto progress_handler = m_node.handleSnapshotLoadProgress( + [this](double progress) { + Q_EMIT progressUpdated(progress); + }); + + // Activate the snapshot + bool success = snapshot->activate(); + + if (success) { + Q_EMIT finished(true, QString()); + } else { + QString error_msg = QString::fromStdString(snapshot->getLastError()); + Q_EMIT finished(false, error_msg); + } + } catch (const std::exception& e) { + Q_EMIT finished(false, tr("Exception during snapshot loading: %1").arg(QString::fromStdString(e.what()))); + } +} + + static SteadyClock::time_point g_last_header_tip_update_notification{}; static SteadyClock::time_point g_last_block_tip_update_notification{}; -ClientModel::ClientModel(interfaces::Node& node, OptionsModel *_optionsModel, QObject *parent) : +ClientModel::ClientModel(interfaces::Node& node, OptionsModel *_optionsModel, QObject *parent, const QString& snapshot_path) : QObject(parent), m_node(node), optionsModel(_optionsModel), - m_thread(new QThread(this)) + m_thread(new QThread(this)), + m_snapshot_path(snapshot_path) { cachedBestHeaderHeight = -1; cachedBestHeaderTime = -1; @@ -72,6 +113,18 @@ void ClientModel::stop() { unsubscribeFromCoreSignals(); + // Clean up snapshot thread and worker + if (m_snapshot_thread) { + m_snapshot_thread->quit(); + m_snapshot_thread->wait(); + delete m_snapshot_thread; + m_snapshot_thread = nullptr; + } + if (m_snapshot_worker) { + delete m_snapshot_worker; + m_snapshot_worker = nullptr; + } + m_thread->quit(); m_thread->wait(); } @@ -234,6 +287,9 @@ void ClientModel::TipChanged(SynchronizationState sync_state, interfaces::BlockT WITH_LOCK(m_cached_tip_mutex, m_cached_tip_blocks = tip.block_hash;); } + // Check if we should load snapshot based on verification progress + setVerificationProgress(verification_progress); + // Throttle GUI notifications about (a) blocks during initial sync, and (b) both blocks and headers during reindex. const bool throttle = (sync_state != SynchronizationState::POST_INIT && synctype == SyncType::BLOCK_SYNC) || sync_state == SynchronizationState::INIT_REINDEX; const auto now{throttle ? SteadyClock::now() : SteadyClock::time_point{}}; @@ -294,3 +350,76 @@ bool ClientModel::getProxyInfo(std::string& ip_port) const } return false; } + +bool ClientModel::loadSnapshot() +{ + QString path_to_use = m_snapshot_path; + + if (path_to_use.isEmpty()) { + qDebug() << "ClientModel::loadSnapshot: No snapshot path provided (neither parameter nor stored path)"; + return false; + } + + // Convert QString to fs::path + const fs::path snapshot_path_fs = fs::u8path(path_to_use.toStdString()); + + qDebug() << "ClientModel::loadSnapshot: Attempting to load snapshot from:" << QString::fromStdString(snapshot_path_fs.utf8string()); + + // Check if file exists + if (!fs::exists(snapshot_path_fs)) { + qDebug() << "ClientModel::loadSnapshot: Snapshot file does not exist:" << QString::fromStdString(snapshot_path_fs.utf8string()); + return false; + } + + qDebug() << "ClientModel::loadSnapshot: Snapshot file exists, proceeding with threaded loading..."; + + // Clean up any existing thread and worker + if (m_snapshot_thread) { + m_snapshot_thread->quit(); + m_snapshot_thread->wait(); + delete m_snapshot_thread; + m_snapshot_thread = nullptr; + } + if (m_snapshot_worker) { + delete m_snapshot_worker; + m_snapshot_worker = nullptr; + } + + // Create worker thread + m_snapshot_thread = new QThread(this); + m_snapshot_worker = new SnapshotLoadWorker(snapshot_path_fs, m_node); + m_snapshot_worker->moveToThread(m_snapshot_thread); + + // Connect signals + connect(m_snapshot_thread, &QThread::started, m_snapshot_worker, &SnapshotLoadWorker::loadSnapshot); + connect(m_snapshot_worker, &SnapshotLoadWorker::progressUpdated, this, &ClientModel::snapshotLoadProgress); + connect(m_snapshot_worker, &SnapshotLoadWorker::finished, this, &ClientModel::snapshotLoadFinished); + connect(m_snapshot_worker, &SnapshotLoadWorker::finished, this, [this](bool success, const QString& error) { + // Clean up worker and thread + m_snapshot_worker->deleteLater(); + m_snapshot_worker = nullptr; + m_snapshot_thread->quit(); + m_snapshot_thread->wait(); + m_snapshot_thread->deleteLater(); + m_snapshot_thread = nullptr; + }); + + // Start the thread + m_snapshot_thread->start(); + + return true; // Threaded operation started successfully +} + +void ClientModel::setVerificationProgress(double verification_progress) +{ + m_verification_progress = verification_progress; + + // Check if we should load snapshot based on verification progress + const double SNAPSHOT_LOAD_THRESHOLD = 0.00014; + if (verification_progress > SNAPSHOT_LOAD_THRESHOLD && !m_snapshot_path.isEmpty() && !m_snapshot_loaded) { + qDebug() << "ClientModel::setVerificationProgress: Verification progress" << verification_progress + << "exceeds threshold" << SNAPSHOT_LOAD_THRESHOLD << "- starting threaded snapshot loading"; + m_snapshot_loaded = true; // Set flag before attempting to load + loadSnapshot(); // This now uses threaded loading with progress updates + } +} diff --git a/src/qt/clientmodel.h b/src/qt/clientmodel.h index 7d0e35e7f96..ec3aa3cc34e 100644 --- a/src/qt/clientmodel.h +++ b/src/qt/clientmodel.h @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -14,6 +15,7 @@ #include #include +#include class BanTableModel; class CBlockIndex; @@ -29,6 +31,26 @@ class Node; struct BlockTip; } +/** Worker class for loading snapshots in a separate thread */ +class SnapshotLoadWorker : public QObject +{ + Q_OBJECT + +public: + explicit SnapshotLoadWorker(const fs::path& path, interfaces::Node& node, QObject* parent = nullptr); + +public Q_SLOTS: + void loadSnapshot(); + +Q_SIGNALS: + void progressUpdated(double progress); + void finished(bool success, const QString& errorMessage); + +private: + fs::path m_path; + interfaces::Node& m_node; +}; + QT_BEGIN_NAMESPACE class QTimer; QT_END_NAMESPACE @@ -58,7 +80,7 @@ class ClientModel : public QObject Q_OBJECT public: - explicit ClientModel(interfaces::Node& node, OptionsModel *optionsModel, QObject *parent = nullptr); + explicit ClientModel(interfaces::Node& node, OptionsModel *optionsModel, QObject *parent = nullptr, const QString& snapshot_path = QString()); ~ClientModel(); void stop(); @@ -91,6 +113,13 @@ class ClientModel : public QObject bool getProxyInfo(std::string& ip_port) const; + //! Load snapshot from the specified path + bool loadSnapshot(const QString& snapshot_path); + //! Load snapshot using the stored snapshot path + bool loadSnapshot(); + //! Set verification progress and trigger snapshot loading if threshold reached + void setVerificationProgress(double verification_progress); + // caches for the best header: hash, number of blocks and block time mutable std::atomic cachedBestHeaderHeight; mutable std::atomic cachedBestHeaderTime; @@ -110,6 +139,19 @@ class ClientModel : public QObject //! A thread to interact with m_node asynchronously QThread* const m_thread; + //! Snapshot path for loading + QString m_snapshot_path; + + //! Verification progress for snapshot loading trigger + double m_verification_progress{0.0}; + + //! Flag to prevent multiple snapshot loading attempts + bool m_snapshot_loaded{false}; + + //! Thread and worker for snapshot loading + QThread* m_snapshot_thread{nullptr}; + SnapshotLoadWorker* m_snapshot_worker{nullptr}; + void TipChanged(SynchronizationState sync_state, interfaces::BlockTip tip, double verification_progress, SyncType synctype) EXCLUSIVE_LOCKS_REQUIRED(!m_cached_tip_mutex); void subscribeToCoreSignals(); void unsubscribeFromCoreSignals(); @@ -127,6 +169,10 @@ class ClientModel : public QObject // Show progress dialog e.g. for verifychain void showProgress(const QString &title, int nProgress); + + //! Snapshot loading progress signals + void snapshotLoadProgress(double progress); + void snapshotLoadFinished(bool success, const QString& errorMessage); }; #endif // BITCOIN_QT_CLIENTMODEL_H diff --git a/src/qt/forms/intro.ui b/src/qt/forms/intro.ui index 9ab91f6aa91..7c88efb3e2d 100644 --- a/src/qt/forms/intro.ui +++ b/src/qt/forms/intro.ui @@ -271,6 +271,33 @@ + + + + + + Snapshot Path: + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/src/qt/intro.cpp b/src/qt/intro.cpp index 91078b7cc1b..c5acd702650 100644 --- a/src/qt/intro.cpp +++ b/src/qt/intro.cpp @@ -17,12 +17,15 @@ #include #include +#include #include #include #include #include #include +#include +#include #include @@ -119,9 +122,16 @@ int64_t Intro::getPruneMiB() const } } -bool Intro::showIfNeeded(bool& did_show_intro, int64_t& prune_MiB) +QString Intro::getSnapshotPath() const +{ + QString snapshot_path = ui->snapshotPath->text(); + return snapshot_path; +} + +bool Intro::showIfNeeded(bool& did_show_intro, int64_t& prune_MiB, QString& snapshot_path) { did_show_intro = false; + snapshot_path.clear(); QSettings settings; /* If data directory provided on command line, no need to look at settings @@ -171,6 +181,7 @@ bool Intro::showIfNeeded(bool& did_show_intro, int64_t& prune_MiB) // Additional preferences: prune_MiB = intro.getPruneMiB(); + snapshot_path = intro.getSnapshotPath(); settings.setValue("strDataDir", dataDir); settings.setValue("fReset", false); @@ -253,6 +264,13 @@ void Intro::on_dataDirCustom_clicked() ui->ellipsisButton->setEnabled(true); } +void Intro::on_getSnapshotPathButton_clicked() +{ + QString m_snapshot_path = QFileDialog::getOpenFileName(nullptr, tr("Choose snapshot file"), "", tr("Snapshot Files (*.dat);;")); + if(!m_snapshot_path.isEmpty()) + ui->snapshotPath->setText(m_snapshot_path); +} + void Intro::startThread() { thread = new QThread(this); diff --git a/src/qt/intro.h b/src/qt/intro.h index db6c1d50b23..56d5f72718d 100644 --- a/src/qt/intro.h +++ b/src/qt/intro.h @@ -37,6 +37,7 @@ class Intro : public QDialog, public FreespaceChecker::PathQuery QString getDataDirectory(); void setDataDirectory(const QString &dataDir); int64_t getPruneMiB() const; + QString getSnapshotPath() const; /** * Determine data directory. Let the user choose if the current one doesn't exist. @@ -48,7 +49,7 @@ class Intro : public QDialog, public FreespaceChecker::PathQuery * @note do NOT call global gArgs.GetDataDirNet() before calling this function, this * will cause the wrong path to be cached. */ - static bool showIfNeeded(bool& did_show_intro, int64_t& prune_MiB); + static bool showIfNeeded(bool& did_show_intro, int64_t& prune_MiB, QString& snapshot_path); Q_SIGNALS: void requestCheck(); @@ -61,6 +62,7 @@ private Q_SLOTS: void on_ellipsisButton_clicked(); void on_dataDirDefault_clicked(); void on_dataDirCustom_clicked(); + void on_getSnapshotPathButton_clicked(); private: Ui::Intro *ui; @@ -75,6 +77,7 @@ private Q_SLOTS: int64_t m_required_space_gb{0}; uint64_t m_bytes_available{0}; int64_t m_prune_target_gb; + QString m_snapshot_path; void startThread(); void checkPath(const QString &dataDir); diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 0a0e44f6f5b..dd073f70c9e 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -25,12 +26,18 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include + +#include + int setFontChoice(QComboBox* cb, const OptionsModel::FontChoice& fc) { @@ -121,6 +128,12 @@ OptionsDialog::OptionsDialog(QWidget* parent, bool enableWallet) connect(ui->connectSocksTor, &QPushButton::toggled, ui->proxyPortTor, &QWidget::setEnabled); connect(ui->connectSocksTor, &QPushButton::toggled, this, &OptionsDialog::updateProxyValidationState); + QPushButton* loadSnapshotButton = new QPushButton(tr("Load Snapshot..."), this); + loadSnapshotButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + loadSnapshotButton->setMaximumWidth(200); // Match the width of other buttons + ui->verticalLayout_Main->insertWidget(ui->verticalLayout_Main->indexOf(ui->enableServer) + 1, loadSnapshotButton); + connect(loadSnapshotButton, &QPushButton::clicked, this, &OptionsDialog::on_loadSnapshotButton_clicked); + /* Window elements init */ #ifdef Q_OS_MACOS /* remove Window tab on Mac */ @@ -207,6 +220,16 @@ OptionsDialog::OptionsDialog(QWidget* parent, bool enableWallet) OptionsDialog::~OptionsDialog() { + // Clean up snapshot thread and worker if they exist + if (m_snapshot_thread) { + m_snapshot_thread->quit(); + m_snapshot_thread->wait(); + delete m_snapshot_thread; + } + if (m_snapshot_worker) { + delete m_snapshot_worker; + } + delete ui; } @@ -400,6 +423,80 @@ void OptionsDialog::on_showTrayIcon_stateChanged(int state) } } +void OptionsDialog::on_loadSnapshotButton_clicked() +{ + QString filename = QFileDialog::getOpenFileName(this, + tr("Load Snapshot"), + tr("Bitcoin Snapshot Files (*.dat);;")); + + if (filename.isEmpty()) return; + + const fs::path path_file_fs = fs::u8path(filename.toStdString()); + + QMessageBox::StandardButton retval = QMessageBox::question(this, tr("Confirm snapshot load"), + tr("Are you sure you want to load this snapshot? This will delete your current blockchain data."), + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel); + + if (retval != QMessageBox::Yes) return; + + // Create progress dialog + QProgressDialog* progress = new QProgressDialog(tr("Loading snapshot..."), tr("Cancel"), 0, 100, this); + progress->setWindowModality(Qt::WindowModal); + progress->setMinimumDuration(0); + progress->setValue(0); + progress->show(); + + // Note: Progress updates are handled via Qt signals from the worker thread + // No direct progress handler needed here to avoid thread safety issues + + // Clean up any existing thread and worker + if (m_snapshot_thread) { + m_snapshot_thread->quit(); + m_snapshot_thread->wait(); + delete m_snapshot_thread; + m_snapshot_thread = nullptr; + } + if (m_snapshot_worker) { + delete m_snapshot_worker; + m_snapshot_worker = nullptr; + } + + // Create worker thread + m_snapshot_thread = new QThread(this); + m_snapshot_worker = new SnapshotLoadWorker(path_file_fs, model->node()); + m_snapshot_worker->moveToThread(m_snapshot_thread); + + // Connect signals + connect(m_snapshot_thread, &QThread::started, m_snapshot_worker, &SnapshotLoadWorker::loadSnapshot); + connect(m_snapshot_worker, &SnapshotLoadWorker::progressUpdated, this, [progress](double progress_value) { + // Convert progress from 0.0-1.0 range to 0-100 integer range + int progress_percent = static_cast(progress_value * 100); + progress->setValue(progress_percent); + }); + connect(m_snapshot_worker, &SnapshotLoadWorker::finished, this, [this, progress](bool success, const QString& error) { + progress->close(); + progress->deleteLater(); + + if (success) { + QMessageBox::information(this, tr("Success"), tr("Snapshot loaded successfully")); + } else { + QMessageBox::critical(this, tr("Error"), tr("Error loading snapshot: %1").arg(error)); + } + + // Clean up worker and thread + m_snapshot_worker->deleteLater(); + m_snapshot_worker = nullptr; + m_snapshot_thread->quit(); + m_snapshot_thread->wait(); + m_snapshot_thread->deleteLater(); + m_snapshot_thread = nullptr; + }); + + // Start the thread + m_snapshot_thread->start(); +} + void OptionsDialog::togglePruneWarning(bool enabled) { ui->pruneWarning->setVisible(!ui->pruneWarning->isVisible()); diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index 031e4d31638..bd464aa979a 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -7,6 +7,10 @@ #include #include +#include +#include +#include +#include class ClientModel; class OptionsModel; @@ -20,6 +24,13 @@ namespace Ui { class OptionsDialog; } +namespace interfaces { +class Handler; +class Node; +} + +class SnapshotLoadWorker; + /** Proxy address widget validator, checks for a valid proxy address. */ class ProxyAddressValidator : public QValidator @@ -58,6 +69,8 @@ private Q_SLOTS: void on_openBitcoinConfButton_clicked(); void on_okButton_clicked(); void on_cancelButton_clicked(); + void on_loadSnapshotButton_clicked(); + void on_showTrayIcon_stateChanged(int state); @@ -77,6 +90,9 @@ private Q_SLOTS: ClientModel* m_client_model{nullptr}; OptionsModel* model{nullptr}; QDataWidgetMapper* mapper{nullptr}; + // std::unique_ptr m_snapshot_load_handler; + QThread* m_snapshot_thread{nullptr}; + SnapshotLoadWorker* m_snapshot_worker{nullptr}; }; #endif // BITCOIN_QT_OPTIONSDIALOG_H diff --git a/src/validation.cpp b/src/validation.cpp index 8fcc719a684..daf101ee0da 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5922,6 +5922,10 @@ util::Result ChainstateManager::PopulateAndValidateSnapshot( --coins_left; ++coins_processed; + // Show Snapshot Loading progress + double progress = static_cast(coins_processed) / static_cast(coins_count); + GetNotifications().snapshotLoadProgress(progress); + if (coins_processed % 1000000 == 0) { LogInfo("[snapshot] %d coins loaded (%.2f%%, %.2f MB)", coins_processed,