From 670ca05c77c13dceaeecbf3d816400e4942a86f9 Mon Sep 17 00:00:00 2001 From: D33r-Gee Date: Tue, 15 Oct 2024 10:19:14 -0700 Subject: [PATCH] wip: Add initial support for loading signet UTXO snapshots Adds wiring to connect QML GUI to loading a signet UTXO snapshot via the connection settings. Modifies src/interfaces/node.h and src/node/interfaces.cpp to implement snapshot loading functionality. Current limitations: - Not integrated with onboarding process - Requires manual navigation to connection settings after initial startup Testing: 1. Start the node 2. Complete onboarding 3. Navigate to connection settings 4. Load snapshot from provided interface Note: This is a work in progress. Do not merge until fully implemented and tested. --- src/interfaces/node.h | 3 + src/kernel/chainparams.cpp | 7 ++ src/node/interfaces.cpp | 68 +++++++++++++++++++ src/qml/components/ConnectionSettings.qml | 57 ++++++++++------ src/qml/components/SnapshotSettings.qml | 25 ++++++- src/qml/models/nodemodel.cpp | 27 ++++++++ src/qml/models/nodemodel.h | 5 ++ src/qml/pages/node/NodeRunner.qml | 18 ++++- .../pages/onboarding/OnboardingConnection.qml | 4 ++ src/qml/pages/settings/SettingsConnection.qml | 9 ++- 10 files changed, 198 insertions(+), 25 deletions(-) diff --git a/src/interfaces/node.h b/src/interfaces/node.h index f6c79f0c1b..36847a0058 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -199,6 +199,9 @@ class Node //! List rpc commands. virtual std::vector listRpcCommands() = 0; + //! Load UTXO Snapshot. + virtual bool snapshotLoad(const std::string& path_string) = 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..33b5a8372c 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -395,6 +396,73 @@ class NodeImpl : public Node { m_context = context; } + bool snapshotLoad(const std::string& path_string) override + { + fs::path path(fs::u8path(path_string)); + if (!fs::exists(path)) { + LogPrintf("[loadsnapshot] snapshot file %s does not exist\n", path.u8string()); + return false; + } + + FILE* file{fsbridge::fopen(path, "rb")}; + + AutoFile afile{file}; + if (afile.IsNull()) { + LogPrintf("[loadsnapshot] failed to open snapshot file %s\n", path_string); + return false; + } + + // Read the snapshot metadata. + SnapshotMetadata metadata; + afile >> metadata; + + // Get the base blockhash and look up the corresponding CBlockIndex object. + uint256 base_blockhash = metadata.m_base_blockhash; + int max_secs_to_wait_for_headers = 60 * 10; + CBlockIndex* snapshot_start_block = nullptr; + + LogPrintf("[loadsnapshot] waiting to see blockheader %s in headers chain before snapshot activation\n", + base_blockhash.ToString()); + + if (m_context->chainman == nullptr) { + LogPrintf("[loadsnapshot] m_context->chainman is null\n"); + return false; + } + + ChainstateManager& chainman = *m_context->chainman; + + while (max_secs_to_wait_for_headers > 0) { + LogPrintf("[loadsnapshot] base_blockhash = %s\n", base_blockhash.ToString()); + snapshot_start_block = WITH_LOCK(::cs_main, + return chainman.m_blockman.LookupBlockIndex(base_blockhash)); + max_secs_to_wait_for_headers -= 1; + + if (!snapshot_start_block) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + } else { + break; + } + } + + if (!snapshot_start_block) { + LogPrintf("[loadsnapshot] timed out waiting for snapshot start blockheader %s\n", + base_blockhash.ToString()); + return false; + } + + // Activate the snapshot. + if (!chainman.ActivateSnapshot(afile, metadata, false)) { + LogPrintf("[loadsnapshot] Unable to load UTXO snapshot %s\n", path.u8string()); + return false; + } + + // Get the new tip and print a log message. + CBlockIndex* new_tip{WITH_LOCK(::cs_main, return chainman.ActiveTip())}; + LogPrintf("[loadsnashot] Loaded %d coins from snapshot %s at height %d\n", + metadata.m_coins_count, new_tip->GetBlockHash().ToString(), new_tip->nHeight); + + return true; + } ArgsManager& args() { return *Assert(Assert(m_context)->args); } ChainstateManager& chainman() { return *Assert(m_context->chainman); } NodeContext* m_context{nullptr}; diff --git a/src/qml/components/ConnectionSettings.qml b/src/qml/components/ConnectionSettings.qml index fea589685d..ac4024d74c 100644 --- a/src/qml/components/ConnectionSettings.qml +++ b/src/qml/components/ConnectionSettings.qml @@ -8,36 +8,53 @@ import QtQuick.Layouts 1.15 import "../controls" ColumnLayout { + // TODO: Remove this once storing the snapshot path is implemented + property bool isOnboarding: false property bool snapshotImported: false function setSnapshotImported(imported) { snapshotImported = imported } spacing: 4 - Setting { - id: gotoSnapshot + Item { + // TODO: Remove this once storing the snapshot path is implemented + visible: !isOnboarding + height: visible ? implicitHeight : 0 Layout.fillWidth: true - header: qsTr("Load snapshot") - description: qsTr("Instant use with background sync") - actionItem: Item { - width: 26 - height: 26 - CaretRightIcon { - anchors.centerIn: parent - visible: !snapshotImported - color: gotoSnapshot.stateColor + Layout.preferredHeight: gotoSnapshot.height + + Setting { + id: gotoSnapshot + visible: parent.visible + Layout.fillWidth: true + header: qsTr("Load snapshot") + description: qsTr("Instant use with background sync") + actionItem: Item { + width: 26 + height: 26 + CaretRightIcon { + // TODO: aligment will be fixed once Onboarding snapshot works + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + visible: !snapshotImported + color: gotoSnapshot.stateColor + } + GreenCheckIcon { + anchors.centerIn: parent + visible: snapshotImported + color: Theme.color.transparent + } } - GreenCheckIcon { - anchors.centerIn: parent - visible: snapshotImported - color: Theme.color.transparent + onClicked: { + connectionSwipe.incrementCurrentIndex() + connectionSwipe.incrementCurrentIndex() } } - onClicked: { - connectionSwipe.incrementCurrentIndex() - connectionSwipe.incrementCurrentIndex() - } } - Separator { Layout.fillWidth: true } + Separator { + Layout.fillWidth: true + // TODO: Remove this once storing the snapshot path is implemented + visible: !isOnboarding + } Setting { Layout.fillWidth: true header: qsTr("Enable listening") diff --git a/src/qml/components/SnapshotSettings.qml b/src/qml/components/SnapshotSettings.qml index ebac415b60..5f30dd8e3a 100644 --- a/src/qml/components/SnapshotSettings.qml +++ b/src/qml/components/SnapshotSettings.qml @@ -5,6 +5,8 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import QtQuick.Dialogs 1.3 + import "../controls" @@ -18,7 +20,7 @@ ColumnLayout { width: Math.min(parent.width, 450) anchors.horizontalCenter: parent.horizontalCenter - + // TODO: Remove simulation timer before release Timer { id: snapshotSimulationTimer interval: 50 // Update every 50ms @@ -78,8 +80,25 @@ ColumnLayout { Layout.alignment: Qt.AlignCenter text: qsTr("Choose snapshot file") onClicked: { - settingsStack.currentIndex = 1 - snapshotSimulationTimer.start() + // TODO: Connect this to snapshot loading + // settingsStack.currentIndex = 1 + fileDialog.open() + } + } + + FileDialog { + id: fileDialog + folder: shortcuts.home + selectMultiple: false + onAccepted: { + console.log("File chosen:", fileDialog.fileUrls) + var snapshotFileName = fileDialog.fileUrl.toString() + console.log("Snapshot file name:", snapshotFileName) + if (snapshotFileName.endsWith(".dat")) { + // optionsModel.setSnapshotDirectory(snapshotFileName) + // console.log("Snapshot directory set:", optionsModel.getSnapshotDirectory()) + nodeModel.initializeSnapshot(true, snapshotFileName) + } } } } diff --git a/src/qml/models/nodemodel.cpp b/src/qml/models/nodemodel.cpp index 521e5fa1c5..2add8fb764 100644 --- a/src/qml/models/nodemodel.cpp +++ b/src/qml/models/nodemodel.cpp @@ -16,6 +16,9 @@ #include #include #include +#include +#include +#include NodeModel::NodeModel(interfaces::Node& node) : m_node{node} @@ -121,6 +124,8 @@ void NodeModel::initializeResult(bool success, interfaces::BlockAndHeaderTipInfo setVerificationProgress(tip_info.verification_progress); Q_EMIT setTimeRatioListInitial(); + // TODO: fix this so that it works once storing the snapshot path is implemented + Q_EMIT initializationFinished(); } void NodeModel::startShutdownPolling() @@ -166,3 +171,25 @@ 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(); + // TODO: Remove this before release + // qDebug() << "path_file: " << path_file; + QThread* snapshot_thread = new QThread(); + + // Capture path_file by value + auto lambda = [this, path_file]() { + bool result = this->snapshotLoad(path_file); + Q_EMIT snapshotLoaded(result); + }; + + connect(snapshot_thread, &QThread::started, lambda); + connect(snapshot_thread, &QThread::finished, snapshot_thread, &QThread::deleteLater); + + snapshot_thread->start(); + } +} diff --git a/src/qml/models/nodemodel.h b/src/qml/models/nodemodel.h index a17f9b0833..f357062327 100644 --- a/src/qml/models/nodemodel.h +++ b/src/qml/models/nodemodel.h @@ -59,6 +59,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,6 +80,8 @@ public Q_SLOTS: void setTimeRatioList(int new_time); void setTimeRatioListInitial(); + void initializationFinished(); + void snapshotLoaded(bool result); protected: void timerEvent(QTimerEvent* event) override; diff --git a/src/qml/pages/node/NodeRunner.qml b/src/qml/pages/node/NodeRunner.qml index 2372983e21..5c0d0e4503 100644 --- a/src/qml/pages/node/NodeRunner.qml +++ b/src/qml/pages/node/NodeRunner.qml @@ -22,11 +22,27 @@ Page { } } - Component.onCompleted: nodeModel.startNodeInitializionThread(); + Component.onCompleted: { + nodeModel.startNodeInitializionThread(); + // TODO: Fix this so that it works once storing the snapshot path is implemented + // nodeModel.initializationFinished.connect(onInitializationFinished); + nodeModel.initializationFinished.connect(function() { + console.log("Initialization finished, initializing snapshot...") + }); + } BlockClock { parentWidth: parent.width - 40 parentHeight: parent.height anchors.centerIn: parent } + + function onInitializationFinished() { + // TODO: Fix this so that it works once storing the snapshot path is implemented + console.log("Initialization finished, initializing snapshot...") + // if (optionsModel.getLoadUtxo() && !optionsModel.getSnapshotLoaded()) { + // nodeModel.initializeSnapshot(true, optionsModel.getSnapshotDirectory()); + // optionsModel.setSnapshotLoaded(true); + // } + } } diff --git a/src/qml/pages/onboarding/OnboardingConnection.qml b/src/qml/pages/onboarding/OnboardingConnection.qml index d5c5b5875c..6b2d1d6e3e 100644 --- a/src/qml/pages/onboarding/OnboardingConnection.qml +++ b/src/qml/pages/onboarding/OnboardingConnection.qml @@ -17,6 +17,8 @@ Page { anchors.fill: parent interactive: false orientation: Qt.Vertical + // TODO: Remove this once storing the snapshot path is implemented + property bool isOnboarding: true InformationPage { navLeftDetail: NavButton { iconSource: "image://images/caret-left" @@ -49,6 +51,8 @@ Page { buttonMargin: 20 } SettingsConnection { + // TODO: Remove this once storing the snapshot path is implemented + isOnboarding: connections.isOnboarding navRightDetail: NavButton { text: qsTr("Done") onClicked: { diff --git a/src/qml/pages/settings/SettingsConnection.qml b/src/qml/pages/settings/SettingsConnection.qml index 2e36dd4a99..33d2fb2f5b 100644 --- a/src/qml/pages/settings/SettingsConnection.qml +++ b/src/qml/pages/settings/SettingsConnection.qml @@ -14,12 +14,17 @@ Item { property alias navLeftDetail: connectionSwipe.navLeftDetail property alias showHeader: connectionSwipe.showHeader + // TODO: Remove this once storing the snapshot path is implemented + property bool isOnboarding: false + function setSnapshotImported(imported) { connection_settings.loadedDetailItem.setSnapshotImported(imported) } SwipeView { id: connectionSwipe + // TODO: Remove this once storing the snapshot path is implemented + property bool isOnboarding: parent.isOnboarding property alias navRightDetail: connection_settings.navRightDetail property alias navMiddleDetail: connection_settings.navMiddleDetail property alias navLeftDetail: connection_settings.navLeftDetail @@ -36,7 +41,9 @@ Item { headerText: qsTr("Connection settings") headerMargin: 0 detailActive: true - detailItem: ConnectionSettings {} + detailItem: ConnectionSettings { + isOnboarding: connectionSwipe.isOnboarding + } } SettingsProxy { onBackClicked: {