From 26897857efb75c397685c6a123829d2ff91a4376 Mon Sep 17 00:00:00 2001 From: D33r-Gee Date: Mon, 28 Apr 2025 10:02:05 -0700 Subject: [PATCH 1/2] poc: Load snapshot through the options dialog This is a quick and dirty POC to showcase and test the load snapshot functionality. This is by no means meant to be a robust approach... --- src/qt/CMakeLists.txt | 2 ++ src/qt/optionsdialog.cpp | 34 ++++++++++++++++++++++++++++++ src/qt/optionsdialog.h | 2 ++ src/qt/snapshotmodel.cpp | 45 ++++++++++++++++++++++++++++++++++++++++ src/qt/snapshotmodel.h | 28 +++++++++++++++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 src/qt/snapshotmodel.cpp create mode 100644 src/qt/snapshotmodel.h diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index 6c853cbf2f8..d373b4a4f87 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -116,6 +116,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..d3d6d343ab4 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -121,6 +123,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 +406,34 @@ void OptionsDialog::on_showTrayIcon_stateChanged(int state) } } +void OptionsDialog::on_loadSnapshotButton_clicked() +{ + QString snapshotFile = QFileDialog::getOpenFileName(this, + tr("Select Snapshot File"), "", + tr("Bitcoin Snapshot Files (*.dat);;All Files (*)")); + + if (snapshotFile.isEmpty()) + return; + + QMessageBox::StandardButton reply = QMessageBox::question(this, + tr("Load Snapshot"), + tr("Loading a snapshot will require restarting Bitcoin. The current blockchain data will be replaced with the snapshot data. Are you sure you want to proceed?"), + QMessageBox::Yes|QMessageBox::No); + + if (reply == QMessageBox::Yes) { + SnapshotModel snapshotModel(model->node(), snapshotFile); + if (snapshotModel.processPath()) { + QMessageBox::information(this, + tr("Snapshot loaded successfully"), + tr("The snapshot has been loaded successfully. You can now start the client with the new snapshot.")); + } else { + QMessageBox::critical(this, + tr("Error"), + tr("Failed to load the snapshot. Please ensure the file is a valid snapshot and try again.")); + } + } +} + 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..408f62d0eeb 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -58,6 +58,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); 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 From 25bfc724076ace4af4e299e70290515565b7edbc Mon Sep 17 00:00:00 2001 From: D33r-Gee Date: Mon, 28 Apr 2025 14:59:44 -0700 Subject: [PATCH 2/2] poc: Add notification handler to display UTXO snapshot Load Progress This serves as a quick and dirty POC to display a progress bar when an AssumeUTXO snapshot is loading. The gui code is by no means production or deployment ready. For backend evaluation pruposes only... --- src/interfaces/node.h | 4 ++ src/kernel/notifications_interface.h | 1 + src/node/interface_ui.cpp | 3 ++ src/node/interface_ui.h | 3 ++ src/node/interfaces.cpp | 4 ++ src/node/kernel_notifications.cpp | 5 +++ src/node/kernel_notifications.h | 2 + src/qt/optionsdialog.cpp | 62 ++++++++++++++++++---------- src/qt/optionsdialog.h | 5 +++ src/validation.cpp | 4 ++ 10 files changed, 72 insertions(+), 21 deletions(-) diff --git a/src/interfaces/node.h b/src/interfaces/node.h index a18c868d40a..745e602cdd9 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -244,6 +244,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 ef72d9bdb6e..6fcd1243806 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) { 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 c9dcd7a7fb9..6a0b1df2058 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* i) { return g_ui_signals.NotifyBlockTip(s, i); } 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 85c34f58342..5d12612692b 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*); diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 4eb4a99ce38..21670e7ba92 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -391,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 550ffe74c4a..ea3ca3e6135 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 f4174381daf..4708c0bfecd 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/optionsdialog.cpp b/src/qt/optionsdialog.cpp index d3d6d343ab4..853e4339cd0 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -33,6 +33,9 @@ #include #include #include +#include + +#include int setFontChoice(QComboBox* cb, const OptionsModel::FontChoice& fc) { @@ -408,29 +411,46 @@ void OptionsDialog::on_showTrayIcon_stateChanged(int state) void OptionsDialog::on_loadSnapshotButton_clicked() { - QString snapshotFile = QFileDialog::getOpenFileName(this, - tr("Select Snapshot File"), "", - tr("Bitcoin Snapshot Files (*.dat);;All Files (*)")); + QString filename = QFileDialog::getOpenFileName(this, + tr("Load Snapshot"), + tr("Bitcoin Snapshot Files (*.dat);;")); - if (snapshotFile.isEmpty()) - return; + if (filename.isEmpty()) return; - QMessageBox::StandardButton reply = QMessageBox::question(this, - tr("Load Snapshot"), - tr("Loading a snapshot will require restarting Bitcoin. The current blockchain data will be replaced with the snapshot data. Are you sure you want to proceed?"), - QMessageBox::Yes|QMessageBox::No); - - if (reply == QMessageBox::Yes) { - SnapshotModel snapshotModel(model->node(), snapshotFile); - if (snapshotModel.processPath()) { - QMessageBox::information(this, - tr("Snapshot loaded successfully"), - tr("The snapshot has been loaded successfully. You can now start the client with the new snapshot.")); - } else { - QMessageBox::critical(this, - tr("Error"), - tr("Failed to load the snapshot. Please ensure the file is a valid snapshot and try again.")); - } + 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")); } } diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index 408f62d0eeb..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 @@ -79,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/validation.cpp b/src/validation.cpp index 13dbe83f806..a9d5ed0a740 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5909,6 +5909,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) { LogPrintf("[snapshot] %d coins loaded (%.2f%%, %.2f MB)\n", coins_processed,