From 8beb50ab504c6a76daf2b164d6786c2bafa6cba3 Mon Sep 17 00:00:00 2001 From: D33r-Gee Date: Fri, 8 Nov 2024 10:28:58 -0800 Subject: [PATCH] wip: Add minimal support for loading signet UTXO snapshots Adds minimal wiring to connect QML GUI to loading a signet UTXO snapshot via the connection settings. Uses SnapshotSettings.qml to allow user interaction. Modifies src/interfaces/node.h, src/node/interfaces.cpp and chainparams.cpp (temporarily for signet snapshot testing) to expose snapshot loading functionality through the node model. Current limitations: - Not integrated with onboarding process - Requires manual navigation to connection settings after initial startup - Snapshot verification progress is not implemented yet Testing: 1. Start the node 2. Complete onboarding 3. Navigate to connection settings 4. Load snapshot from provided interface --- src/interfaces/node.h | 6 ++ src/kernel/chainparams.cpp | 7 ++ src/node/interfaces.cpp | 36 ++++++- src/qml/components/ConnectionSettings.qml | 23 +++-- src/qml/components/SnapshotSettings.qml | 94 +++++++++---------- src/qml/models/chainmodel.cpp | 22 +++++ src/qml/models/chainmodel.h | 6 +- src/qml/models/nodemodel.cpp | 39 ++++++++ src/qml/models/nodemodel.h | 21 ++++- src/qml/pages/settings/SettingsConnection.qml | 8 ++ src/qml/pages/settings/SettingsSnapshot.qml | 13 +-- 11 files changed, 209 insertions(+), 66 deletions(-) diff --git a/src/interfaces/node.h b/src/interfaces/node.h index f6c79f0c1b..cf5033b877 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -199,6 +199,12 @@ class Node //! List rpc commands. virtual std::vector listRpcCommands() = 0; + //! Load UTXO Snapshot. + virtual bool snapshotLoad(const std::string& path_string) = 0; + + //! Get snapshot progress. + virtual double getSnapshotProgress() = 0; + //! Set RPC timer interface if unset. virtual void rpcSetTimerInterfaceIfUnset(RPCTimerInterface* iface) = 0; diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 733a3339b3..635dc40f9f 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -371,6 +371,13 @@ class SigNetParams : public CChainParams { vFixedSeeds.clear(); + m_assumeutxo_data = MapAssumeutxo{ + { + 160000, + {AssumeutxoHash{uint256S("0x5225141cb62dee63ab3be95f9b03d60801f264010b1816d4bd00618b2736e7be")}, 1278002}, + }, + }; + base58Prefixes[PUBKEY_ADDRESS] = std::vector(1,111); base58Prefixes[SCRIPT_ADDRESS] = std::vector(1,196); base58Prefixes[SECRET_KEY] = std::vector(1,239); diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index f1fe42206e..f9d421ce68 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -395,9 +396,42 @@ class NodeImpl : public Node { m_context = context; } + double getSnapshotProgress() override { return m_snapshot_progress.load(); } + bool snapshotLoad(const std::string& path_string) override + { + const fs::path path = fs::u8path(path_string); + if (!fs::exists(path)) { return false; } + + AutoFile afile{fsbridge::fopen(path, "rb")}; + if (afile.IsNull()) { return false; } + + SnapshotMetadata metadata; + try { + afile >> metadata; + } catch (const std::exception& e) { return false; } + + const uint256& base_blockhash = metadata.m_base_blockhash; + + if (!m_context->chainman) { return false; } + + ChainstateManager& chainman = *m_context->chainman; + CBlockIndex* snapshot_start_block = nullptr; + + // Wait for the block to appear in the block index + //TODO: remove this once another method is implemented + constexpr int max_wait_seconds = 600; // 10 minutes + for (int i = 0; i < max_wait_seconds; ++i) { + snapshot_start_block = WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(base_blockhash)); + if (snapshot_start_block) break; + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + return chainman.ActivateSnapshot(afile, metadata, false); + } ArgsManager& args() { return *Assert(Assert(m_context)->args); } ChainstateManager& chainman() { return *Assert(m_context->chainman); } NodeContext* m_context{nullptr}; + std::atomic m_snapshot_progress{0.0}; }; bool FillBlock(const CBlockIndex* index, const FoundBlock& block, UniqueLock& lock, const CChain& active, const BlockManager& blockman) @@ -510,7 +544,7 @@ class RpcHandlerImpl : public Handler class ChainImpl : public Chain { public: - explicit ChainImpl(NodeContext& node) : m_node(node) {} + explicit ChainImpl(node::NodeContext& node) : m_node(node) {} std::optional getHeight() override { const int height{WITH_LOCK(::cs_main, return chainman().ActiveChain().Height())}; diff --git a/src/qml/components/ConnectionSettings.qml b/src/qml/components/ConnectionSettings.qml index 2362ea12e2..df18fe8d40 100644 --- a/src/qml/components/ConnectionSettings.qml +++ b/src/qml/components/ConnectionSettings.qml @@ -11,13 +11,21 @@ ColumnLayout { id: root signal next signal gotoSnapshot - property bool snapshotImported: false - function setSnapshotImported(imported) { - snapshotImported = imported + property bool snapshotImportCompleted: false + property bool onboarding: false + + Component.onCompleted: { + if (!onboarding) { + snapshotImportCompleted = chainModel.isSnapshotActive + } else { + snapshotImportCompleted = false + } } + spacing: 4 Setting { id: gotoSnapshot + visible: !root.onboarding Layout.fillWidth: true header: qsTr("Load snapshot") description: qsTr("Instant use with background sync") @@ -26,19 +34,22 @@ ColumnLayout { height: 26 CaretRightIcon { anchors.centerIn: parent - visible: !snapshotImported + visible: !snapshotImportCompleted color: gotoSnapshot.stateColor } GreenCheckIcon { anchors.centerIn: parent - visible: snapshotImported + visible: snapshotImportCompleted color: Theme.color.transparent size: 30 } } onClicked: root.gotoSnapshot() } - Separator { Layout.fillWidth: true } + Separator { + visible: !root.onboarding + Layout.fillWidth: true + } Setting { Layout.fillWidth: true header: qsTr("Enable listening") diff --git a/src/qml/components/SnapshotSettings.qml b/src/qml/components/SnapshotSettings.qml index ebac415b60..311beb3d6e 100644 --- a/src/qml/components/SnapshotSettings.qml +++ b/src/qml/components/SnapshotSettings.qml @@ -5,44 +5,28 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import QtQuick.Dialogs 1.3 import "../controls" ColumnLayout { - signal snapshotImportCompleted() - property int snapshotVerificationCycles: 0 - property real snapshotVerificationProgress: 0 - property bool snapshotVerified: false - id: columnLayout + signal back + property bool snapshotLoading: nodeModel.snapshotLoading + property bool snapshotLoaded: nodeModel.isSnapshotLoaded + property bool snapshotImportCompleted: chainModel.isSnapshotActive + property bool onboarding: false + property bool snapshotVerified: onboarding ? false : chainModel.isSnapshotActive + property string snapshotFileName: "" + property var snapshotInfo: ({}) + property string selectedFile: "" + width: Math.min(parent.width, 450) anchors.horizontalCenter: parent.horizontalCenter - - Timer { - id: snapshotSimulationTimer - interval: 50 // Update every 50ms - running: false - repeat: true - onTriggered: { - if (snapshotVerificationProgress < 1) { - snapshotVerificationProgress += 0.01 - } else { - snapshotVerificationCycles++ - if (snapshotVerificationCycles < 1) { - snapshotVerificationProgress = 0 - } else { - running = false - snapshotVerified = true - settingsStack.currentIndex = 2 - } - } - } - } - StackLayout { id: settingsStack - currentIndex: 0 + currentIndex: onboarding ? 0 : snapshotLoaded ? 2 : snapshotVerified ? 2 : snapshotLoading ? 1 : 0 ColumnLayout { Layout.alignment: Qt.AlignHCenter @@ -77,11 +61,22 @@ ColumnLayout { Layout.bottomMargin: 20 Layout.alignment: Qt.AlignCenter text: qsTr("Choose snapshot file") - onClicked: { - settingsStack.currentIndex = 1 - snapshotSimulationTimer.start() + onClicked: fileDialog.open() + } + + FileDialog { + id: fileDialog + folder: shortcuts.home + selectMultiple: false + selectExisting: true + nameFilters: ["Snapshot files (*.dat)", "All files (*)"] + onAccepted: { + selectedFile = fileUrl.toString() + snapshotFileName = selectedFile + nodeModel.initializeSnapshot(true, snapshotFileName) } } + // TODO: Handle file error signal } ColumnLayout { @@ -102,17 +97,10 @@ ColumnLayout { Layout.leftMargin: 20 Layout.rightMargin: 20 header: qsTr("Loading Snapshot") + description: qsTr("This might take a while...") } - ProgressIndicator { - id: progressIndicator - Layout.topMargin: 20 - width: 200 - height: 20 - progress: snapshotVerificationProgress - Layout.alignment: Qt.AlignCenter - progressColor: Theme.color.blue - } + // TODO: add progress indicator once the snapshot progress is implemented } ColumnLayout { @@ -137,8 +125,11 @@ ColumnLayout { descriptionColor: Theme.color.neutral6 descriptionSize: 17 descriptionLineHeight: 1.1 - description: qsTr("It contains transactions up to January 12, 2024. Newer transactions still need to be downloaded." + - " The data will be verified in the background.") + description: snapshotInfo && snapshotInfo["date"] ? + qsTr("It contains transactions up to %1. Newer transactions still need to be downloaded." + + " The data will be verified in the background.").arg(snapshotInfo["date"]) : + qsTr("It contains transactions up to DEBUG. Newer transactions still need to be downloaded." + + " The data will be verified in the background.") } ContinueButton { @@ -146,11 +137,7 @@ ColumnLayout { Layout.topMargin: 40 Layout.alignment: Qt.AlignCenter text: qsTr("Done") - onClicked: { - snapshotImportCompleted() - connectionSwipe.decrementCurrentIndex() - connectionSwipe.decrementCurrentIndex() - } + onClicked: back() } Setting { @@ -188,16 +175,25 @@ ColumnLayout { font.pixelSize: 14 } CoreText { - text: qsTr("200,000") + text: snapshotInfo && snapshotInfo["height"] ? + snapshotInfo["height"] : qsTr("DEBUG") Layout.alignment: Qt.AlignRight font.pixelSize: 14 } } Separator { Layout.fillWidth: true } CoreText { - text: qsTr("Hash: 0x1234567890abcdef...") + text: snapshotInfo && snapshotInfo["hashSerialized"] ? + qsTr("Hash: %1").arg(snapshotInfo["hashSerialized"].substring(0, 13) + "...") : + qsTr("Hash: DEBUG") font.pixelSize: 14 } + + Component.onCompleted: { + if (snapshotVerified || snapshotLoaded) { + snapshotInfo = chainModel.getSnapshotInfo() + } + } } } } diff --git a/src/qml/models/chainmodel.cpp b/src/qml/models/chainmodel.cpp index aeffe99599..d5161df273 100644 --- a/src/qml/models/chainmodel.cpp +++ b/src/qml/models/chainmodel.cpp @@ -9,9 +9,13 @@ #include #include #include +#include +#include +#include ChainModel::ChainModel(interfaces::Chain& chain) : m_chain{chain} + // m_params{Params()} { QTimer* timer = new QTimer(); connect(timer, &QTimer::timeout, this, &ChainModel::setCurrentTimeRatio); @@ -101,3 +105,21 @@ void ChainModel::setCurrentTimeRatio() Q_EMIT timeRatioListChanged(); } + +// Using hardcoded snapshot info to display in SnapshotSettings.qml +QVariantMap ChainModel::getSnapshotInfo() { + QVariantMap snapshot_info; + + const MapAssumeutxo& valid_assumeutxos_map = Params().Assumeutxo(); + if (!valid_assumeutxos_map.empty()) { + const int height = valid_assumeutxos_map.rbegin()->first; + const auto& hash_serialized = valid_assumeutxos_map.rbegin()->second.hash_serialized; + int64_t date = m_chain.getBlockTime(height); + + snapshot_info["height"] = height; + snapshot_info["hashSerialized"] = QString::fromStdString(hash_serialized.ToString()); + snapshot_info["date"] = QDateTime::fromSecsSinceEpoch(date).toString("MMMM d yyyy"); + } + + return snapshot_info; +} diff --git a/src/qml/models/chainmodel.h b/src/qml/models/chainmodel.h index 9318510eda..6a5124be7f 100644 --- a/src/qml/models/chainmodel.h +++ b/src/qml/models/chainmodel.h @@ -27,6 +27,7 @@ class ChainModel : public QObject Q_PROPERTY(quint64 assumedBlockchainSize READ assumedBlockchainSize CONSTANT) Q_PROPERTY(quint64 assumedChainstateSize READ assumedChainstateSize CONSTANT) Q_PROPERTY(QVariantList timeRatioList READ timeRatioList NOTIFY timeRatioListChanged) + Q_PROPERTY(bool isSnapshotActive READ isSnapshotActive NOTIFY isSnapshotActiveChanged) public: explicit ChainModel(interfaces::Chain& chain); @@ -36,11 +37,13 @@ class ChainModel : public QObject quint64 assumedBlockchainSize() const { return m_assumed_blockchain_size; }; quint64 assumedChainstateSize() const { return m_assumed_chainstate_size; }; QVariantList timeRatioList() const { return m_time_ratio_list; }; - + bool isSnapshotActive() const { return m_chain.hasAssumedValidChain(); }; int timestampAtMeridian(); void setCurrentTimeRatio(); + Q_INVOKABLE QVariantMap getSnapshotInfo(); + public Q_SLOTS: void setTimeRatioList(int new_time); void setTimeRatioListInitial(); @@ -48,6 +51,7 @@ public Q_SLOTS: Q_SIGNALS: void timeRatioListChanged(); void currentNetworkNameChanged(); + void isSnapshotActiveChanged(); private: QString m_current_network_name; diff --git a/src/qml/models/nodemodel.cpp b/src/qml/models/nodemodel.cpp index 521e5fa1c5..57fc016294 100644 --- a/src/qml/models/nodemodel.cpp +++ b/src/qml/models/nodemodel.cpp @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include #include #include @@ -16,6 +19,9 @@ #include #include #include +#include +#include +#include NodeModel::NodeModel(interfaces::Node& node) : m_node{node} @@ -166,3 +172,36 @@ void NodeModel::ConnectToNumConnectionsChangedSignal() setNumOutboundPeers(new_num_peers.outbound_full_relay + new_num_peers.block_relay); }); } + +// Loads a snapshot from a given path using FileDialog +void NodeModel::initializeSnapshot(bool initLoadSnapshot, QString path_file) { + if (initLoadSnapshot) { + // TODO: this is to deal with FileDialog returning a QUrl + path_file = QUrl(path_file).toLocalFile(); + m_snapshot_loading = true; + Q_EMIT snapshotLoadingChanged(); + // TODO: Remove this before release + // qDebug() << "path_file: " << path_file; + QThread* snapshot_thread = new QThread(); + + auto lambda = [this, path_file]() { + bool result = this->snapshotLoad(path_file); + m_snapshot_loading = false; + m_snapshot_loaded = result; + Q_EMIT snapshotLoaded(result); + Q_EMIT snapshotLoadingChanged(); + }; + + connect(snapshot_thread, &QThread::started, lambda); + connect(snapshot_thread, &QThread::finished, snapshot_thread, &QThread::deleteLater); + + snapshot_thread->start(); + } +} + +void NodeModel::setSnapshotProgress(double new_progress) { + if (new_progress != m_snapshot_progress) { + m_snapshot_progress = new_progress; + Q_EMIT snapshotProgressChanged(); + } +} diff --git a/src/qml/models/nodemodel.h b/src/qml/models/nodemodel.h index a17f9b0833..55d267cee6 100644 --- a/src/qml/models/nodemodel.h +++ b/src/qml/models/nodemodel.h @@ -34,6 +34,9 @@ class NodeModel : public QObject Q_PROPERTY(double verificationProgress READ verificationProgress NOTIFY verificationProgressChanged) Q_PROPERTY(bool pause READ pause WRITE setPause NOTIFY pauseChanged) Q_PROPERTY(bool faulted READ errorState WRITE setErrorState NOTIFY errorStateChanged) + Q_PROPERTY(double snapshotProgress READ snapshotProgress WRITE setSnapshotProgress NOTIFY snapshotProgressChanged) + Q_PROPERTY(bool snapshotLoading READ snapshotLoading NOTIFY snapshotLoadingChanged) + Q_PROPERTY(bool isSnapshotLoaded READ isSnapshotLoaded NOTIFY snapshotLoaded) public: explicit NodeModel(interfaces::Node& node); @@ -52,6 +55,10 @@ class NodeModel : public QObject void setPause(bool new_pause); bool errorState() const { return m_faulted; } void setErrorState(bool new_error); + bool isSnapshotLoaded() const { return m_snapshot_loaded; } + double snapshotProgress() const { return m_snapshot_progress; } + void setSnapshotProgress(double new_progress); + bool snapshotLoading() const { return m_snapshot_loading; } Q_INVOKABLE float getTotalBytesReceived() const { return (float)m_node.getTotalBytesRecv(); } Q_INVOKABLE float getTotalBytesSent() const { return (float)m_node.getTotalBytesSent(); } @@ -59,6 +66,9 @@ class NodeModel : public QObject Q_INVOKABLE void startNodeInitializionThread(); Q_INVOKABLE void requestShutdown(); + Q_INVOKABLE void initializeSnapshot(bool initLoadSnapshot, QString path_file); + Q_INVOKABLE bool snapshotLoad(QString path_file) const { return m_node.snapshotLoad(path_file.toStdString()); } + void startShutdownPolling(); void stopShutdownPolling(); @@ -77,7 +87,10 @@ public Q_SLOTS: void setTimeRatioList(int new_time); void setTimeRatioListInitial(); - + void initializationFinished(); + void snapshotLoaded(bool result); + void snapshotProgressChanged(); + void snapshotLoadingChanged(); protected: void timerEvent(QTimerEvent* event) override; @@ -90,9 +103,11 @@ public Q_SLOTS: double m_verification_progress{0.0}; bool m_pause{false}; bool m_faulted{false}; - + double m_snapshot_progress{0.0}; int m_shutdown_polling_timer_id{0}; - + int m_snapshot_timer_id{0}; + bool m_snapshot_loading{false}; + bool m_snapshot_loaded{false}; QVector> m_block_process_time; interfaces::Node& m_node; diff --git a/src/qml/pages/settings/SettingsConnection.qml b/src/qml/pages/settings/SettingsConnection.qml index 443eed72a9..7d5c00496a 100644 --- a/src/qml/pages/settings/SettingsConnection.qml +++ b/src/qml/pages/settings/SettingsConnection.qml @@ -91,5 +91,13 @@ Page { onBack: stack.pop() } } + Component { + id: snapshotSettings + SettingsSnapshot { + onboarding: root.onboarding + snapshotImportCompleted: root.snapshotImportCompleted + onBack: stack.pop() + } + } } } diff --git a/src/qml/pages/settings/SettingsSnapshot.qml b/src/qml/pages/settings/SettingsSnapshot.qml index e6c557a022..2392dfb9c7 100644 --- a/src/qml/pages/settings/SettingsSnapshot.qml +++ b/src/qml/pages/settings/SettingsSnapshot.qml @@ -9,8 +9,9 @@ import "../../controls" import "../../components" Page { - signal backClicked - signal snapshotImportCompleted + signal back + property bool snapshotImportCompleted: chainModel.isSnapshotActive + property bool onboarding: false id: root @@ -24,14 +25,14 @@ Page { leftItem: NavButton { iconSource: "image://images/caret-left" text: qsTr("Back") - onClicked: root.backClicked() + onClicked: root.back() } } SnapshotSettings { width: Math.min(parent.width, 450) anchors.horizontalCenter: parent.horizontalCenter - onSnapshotImportCompleted: { - root.snapshotImportCompleted() - } + onboarding: root.onboarding + snapshotImportCompleted: root.snapshotImportCompleted + onBack: root.back() } }