diff --git a/src/interfaces/node.h b/src/interfaces/node.h index 78a186c5d96..ed58b2f4ce3 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -204,6 +205,9 @@ class Node //! List rpc commands. virtual std::vector listRpcCommands() = 0; + //! Load UTXO Snapshot. + virtual bool loadSnapshot(AutoFile& afile, const node::SnapshotMetadata& metadata, bool in_memory) = 0; + //! Get unspent output associated with a transaction. virtual std::optional getUnspentOutput(const COutPoint& output) = 0; @@ -233,6 +237,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/kernel/notifications_interface.h b/src/kernel/notifications_interface.h index 3e97e3b45e9..e6a5a1dd3fc 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, 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 62172930dca..49e946f0a7d 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -356,6 +357,11 @@ class NodeImpl : public Node return ::tableRPC.execute(req); } std::vector listRpcCommands() override { return ::tableRPC.listCommands(); } + bool loadSnapshot(AutoFile& coins_file, const SnapshotMetadata& metadata, bool in_memory) override + { + auto activation_result{chainman().ActivateSnapshot(coins_file, metadata, in_memory)}; + return activation_result.has_value(); + } std::optional getUnspentOutput(const COutPoint& output) override { LOCK(::cs_main); @@ -385,6 +391,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 4a6f8444dac..a1ae75ecc45 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 10ee3e18d74..d873d34febf 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/CMakeLists.txt b/src/qt/CMakeLists.txt index 87a5a2bef3b..1927b58bf55 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -111,6 +111,8 @@ add_library(bitcoinqt STATIC EXCLUDE_FROM_ALL qvaluecombobox.h rpcconsole.cpp rpcconsole.h + snapshotmodel.cpp + snapshotmodel.h splashscreen.cpp splashscreen.h trafficgraphwidget.cpp diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 0a0e44f6f5b..853e4339cd0 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -25,12 +26,16 @@ #include #include #include +#include #include #include #include #include #include #include +#include + +#include int setFontChoice(QComboBox* cb, const OptionsModel::FontChoice& fc) { @@ -121,6 +126,10 @@ 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); + 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 */ @@ -400,6 +409,51 @@ 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; + + 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; + + QProgressDialog* progress = new QProgressDialog(tr("Loading snapshot..."), tr("Cancel"), 0, 100, this); + progress->setWindowModality(Qt::WindowModal); + progress->setMinimumDuration(0); + progress->setValue(0); + progress->show(); + + // Store the handler in the member variable to keep it alive + m_snapshot_load_handler = model->node().handleSnapshotLoadProgress( + [progress](double p) { + progress->setValue(static_cast(p * 100)); + QApplication::processEvents(); + }); + + SnapshotModel snapshotModel(model->node(), filename); + bool success = snapshotModel.processPath(); + + // Clean up the progress dialog + progress->close(); + progress->deleteLater(); + + // Clean up the handler + m_snapshot_load_handler.reset(); + + if (success) { + QMessageBox::information(this, tr("Success"), tr("Snapshot loaded successfully")); + } else { + QMessageBox::critical(this, tr("Error"), tr("Error loading snapshot")); + } +} + 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..6eb0a29196c 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -20,6 +20,10 @@ namespace Ui { class OptionsDialog; } +namespace interfaces { +class Handler; +} + /** Proxy address widget validator, checks for a valid proxy address. */ class ProxyAddressValidator : public QValidator @@ -58,6 +62,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 +83,7 @@ private Q_SLOTS: ClientModel* m_client_model{nullptr}; OptionsModel* model{nullptr}; QDataWidgetMapper* mapper{nullptr}; + std::unique_ptr m_snapshot_load_handler; }; #endif // BITCOIN_QT_OPTIONSDIALOG_H diff --git a/src/qt/snapshotmodel.cpp b/src/qt/snapshotmodel.cpp new file mode 100644 index 00000000000..f2ea450753d --- /dev/null +++ b/src/qt/snapshotmodel.cpp @@ -0,0 +1,45 @@ +// Copyright (c) 2024 - present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include + +SnapshotModel::SnapshotModel(interfaces::Node& node, QString path) + : m_node(node), m_path(path) {} + +bool SnapshotModel::processPath() +{ + ChainstateManager& chainman = *m_node.context()->chainman; + const fs::path snapshot_path = fs::u8path(m_path.toStdString()); + if (!fs::exists(snapshot_path)) { + return false; + } + + FILE* snapshot_file{fsbridge::fopen(snapshot_path, "rb")}; + AutoFile coins_file{snapshot_file}; + if (coins_file.IsNull()) { + return false; + } + + node::SnapshotMetadata metadata{chainman.GetParams().MessageStart()}; + try { + coins_file >> metadata; + } catch (const std::exception& e) { + return false; + } + + bool result = m_node.loadSnapshot(coins_file, metadata, false); + if (!result) { + return false; + } + + return true; +} diff --git a/src/qt/snapshotmodel.h b/src/qt/snapshotmodel.h new file mode 100644 index 00000000000..c7d1adf34ce --- /dev/null +++ b/src/qt/snapshotmodel.h @@ -0,0 +1,28 @@ +// Copyright (c) 2024 - present 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_QT_SNAPSHOTMODEL_H +#define BITCOIN_QT_SNAPSHOTMODEL_H + +#include +#include +#include +#include + +/** Model for snapshot operations. */ +class SnapshotModel : public QObject +{ + Q_OBJECT + +public: + SnapshotModel(interfaces::Node& node, QString path); + + bool processPath(); + +private: + interfaces::Node& m_node; + QString m_path; +}; + +#endif // BITCOIN_QT_SNAPSHOTMODEL_H diff --git a/src/validation.cpp b/src/validation.cpp index 09e04ff0ddb..46ba62b2d27 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5980,6 +5980,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,