diff --git a/src/interfaces/node.h b/src/interfaces/node.h index f6c79f0c1b..108eed5f38 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -199,6 +199,16 @@ class Node //! List rpc commands. virtual std::vector listRpcCommands() = 0; + //! Load UTXO Snapshot. + virtual bool snapshotLoad(const std::string& path_string) = 0; + + //! Set snapshot progress callback. + using SnapshotProgressFn = std::function; + virtual void setSnapshotProgressCallback(SnapshotProgressFn fn) = 0; + + //! Notify snapshot progress. + virtual void notifySnapshotProgress(double progress) = 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..62f340b325 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -58,7 +59,7 @@ #include #include #include - +#include #include using interfaces::BlockTip; @@ -395,6 +396,99 @@ class NodeImpl : public Node { m_context = context; } + SnapshotProgressFn m_snapshot_progress_callback{nullptr}; + void setSnapshotProgressCallback(SnapshotProgressFn fn) override + { + m_snapshot_progress_callback = std::move(fn); + } + void notifySnapshotProgress(double progress) override + { + if (m_snapshot_progress_callback) m_snapshot_progress_callback(progress); + } + bool snapshotLoad(const std::string& path_string) override + { + // Set up log message parsing + LogInstance().PushBackCallback([this](const std::string& str) { + static const std::regex progress_regex(R"(\[snapshot\] (\d+) coins loaded \(([0-9.]+)%.*\))"); + static const std::regex sync_progress_regex(R"(Synchronizing blockheaders, height: (\d+) \(~([\d.]+)%\))"); + + std::smatch matches; + if (std::regex_search(str, matches, progress_regex)) { + try { + double percentage = std::stod(matches[2]); + // Convert percentage to 0-1 range and notify through callback + notifySnapshotProgress(percentage / 100.0); + } catch (...) { + // Handle parsing errors + } + } else if (std::regex_search(str, matches, sync_progress_regex)) { + try { + double percentage = std::stod(matches[2]); + // Convert percentage to 0-1 range and notify through callback + notifySnapshotProgress(percentage / 100.0); + } catch (...) { + // Handle parsing errors + } + } + }); + + const fs::path path = fs::u8path(path_string); + if (!fs::exists(path)) { + LogPrintf("[loadsnapshot] Snapshot file %s does not exist\n", path.u8string()); + return false; + } + + AutoFile afile{fsbridge::fopen(path, "rb")}; + if (afile.IsNull()) { + LogPrintf("[loadsnapshot] Failed to open snapshot file %s\n", path.u8string()); + return false; + } + + SnapshotMetadata metadata; + try { + afile >> metadata; + } catch (const std::exception& e) { + LogPrintf("[loadsnapshot] Failed to read snapshot metadata: %s\n", e.what()); + return false; + } + + const uint256& base_blockhash = metadata.m_base_blockhash; + LogPrintf("[loadsnapshot] Waiting for blockheader %s in headers chain before snapshot activation\n", + base_blockhash.ToString()); + + if (!m_context->chainman) { + LogPrintf("[loadsnapshot] Chainman is null\n"); + return false; + } + + ChainstateManager& chainman = *m_context->chainman; + CBlockIndex* snapshot_start_block = nullptr; + + // Wait for the block to appear in the block index + 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)); + } + + 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; + } + + CBlockIndex* new_tip = WITH_LOCK(::cs_main, return chainman.ActiveTip()); + LogPrintf("[loadsnapshot] 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}; @@ -510,7 +604,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 e2519d4122..2efd9a2514 100644 --- a/src/qml/components/ConnectionSettings.qml +++ b/src/qml/components/ConnectionSettings.qml @@ -10,13 +10,24 @@ import "../controls" ColumnLayout { id: root signal next - property bool snapshotImported: false + property bool snapshotImported: onboarding ? false : chainModel.isSnapshotActive + property bool onboarding: false + + Component.onCompleted: { + if (!onboarding) { + snapshotImported = chainModel.isSnapshotActive + } else { + snapshotImported = false + } + } + function setSnapshotImported(imported) { snapshotImported = imported } spacing: 4 Setting { id: gotoSnapshot + visible: !root.onboarding Layout.fillWidth: true header: qsTr("Load snapshot") description: qsTr("Instant use with background sync") @@ -40,7 +51,10 @@ ColumnLayout { connectionSwipe.incrementCurrentIndex() } } - 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..cf7d956691 100644 --- a/src/qml/components/SnapshotSettings.qml +++ b/src/qml/components/SnapshotSettings.qml @@ -5,6 +5,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import QtQuick.Dialogs 1.3 import "../controls" @@ -12,13 +13,14 @@ ColumnLayout { signal snapshotImportCompleted() property int snapshotVerificationCycles: 0 property real snapshotVerificationProgress: 0 - property bool snapshotVerified: false + property bool onboarding: false + property bool snapshotVerified: onboarding ? false : chainModel.isSnapshotActive id: columnLayout width: Math.min(parent.width, 450) anchors.horizontalCenter: parent.horizontalCenter - + // TODO: Remove this once the verification progress is available Timer { id: snapshotSimulationTimer interval: 50 // Update every 50ms @@ -29,7 +31,7 @@ ColumnLayout { snapshotVerificationProgress += 0.01 } else { snapshotVerificationCycles++ - if (snapshotVerificationCycles < 1) { + if (snapshotVerificationCycles < 3) { snapshotVerificationProgress = 0 } else { running = false @@ -42,7 +44,7 @@ ColumnLayout { StackLayout { id: settingsStack - currentIndex: 0 + currentIndex: onboarding ? 0 : snapshotVerified ? 2 : 0 ColumnLayout { Layout.alignment: Qt.AlignHCenter @@ -78,8 +80,22 @@ ColumnLayout { Layout.alignment: Qt.AlignCenter text: qsTr("Choose snapshot file") onClicked: { - settingsStack.currentIndex = 1 - snapshotSimulationTimer.start() + 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")) { + nodeModel.initializeSnapshot(true, snapshotFileName) + settingsStack.currentIndex = 1 + } } } } @@ -109,10 +125,34 @@ ColumnLayout { Layout.topMargin: 20 width: 200 height: 20 - progress: snapshotVerificationProgress + // TODO: uncomment this once the verification progress is available + // progress: nodeModel.verificationProgress + progress: 0 Layout.alignment: Qt.AlignCenter progressColor: Theme.color.blue } + + Connections { + target: nodeModel + // TODO: uncomment this once the verification progress is available + // function onVerificationProgressChanged() { + // progressIndicator.progress = nodeModel.verificationProgress + // } + function onSnapshotProgressChanged() { + progressIndicator.progress = nodeModel.snapshotProgress + } + function onSnapshotLoaded(success) { + if (success) { + chainModel.isSnapshotActiveChanged() + snapshotVerified = chainModel.isSnapshotActive + progressIndicator.progress = 1 + settingsStack.currentIndex = 2 // Move to the "Snapshot Loaded" page + } else { + // Handle snapshot loading failure + console.error("Snapshot loading failed") + } + } + } } ColumnLayout { @@ -137,6 +177,7 @@ ColumnLayout { descriptionColor: Theme.color.neutral6 descriptionSize: 17 descriptionLineHeight: 1.1 + // TODO: Update this description once the snapshot is verified 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.") } @@ -153,6 +194,9 @@ ColumnLayout { } } + // TODO: Update this with the actual snapshot details + // TODO: uncomment this once the snapshot details are available + /* Setting { id: viewDetails Layout.alignment: Qt.AlignCenter @@ -188,6 +232,7 @@ ColumnLayout { font.pixelSize: 14 } CoreText { + // TODO: Update this with the actual block height text: qsTr("200,000") Layout.alignment: Qt.AlignRight font.pixelSize: 14 @@ -195,10 +240,12 @@ ColumnLayout { } Separator { Layout.fillWidth: true } CoreText { + // TODO: Update this with the actual snapshot file hash text: qsTr("Hash: 0x1234567890abcdef...") font.pixelSize: 14 } } + */ } } } diff --git a/src/qml/models/chainmodel.h b/src/qml/models/chainmodel.h index 9318510eda..adc1f9dca7 100644 --- a/src/qml/models/chainmodel.h +++ b/src/qml/models/chainmodel.h @@ -12,6 +12,7 @@ #include #include #include +#include namespace interfaces { class FoundBlock; @@ -27,6 +28,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,7 +38,7 @@ 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(); @@ -48,6 +50,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..44b027c0ba 100644 --- a/src/qml/models/nodemodel.cpp +++ b/src/qml/models/nodemodel.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -16,6 +17,8 @@ #include #include #include +#include +#include NodeModel::NodeModel(interfaces::Node& node) : m_node{node} @@ -166,3 +169,39 @@ 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]() { + m_node.setSnapshotProgressCallback([this](double progress) { + QMetaObject::invokeMethod(this, [this, progress]() { + setSnapshotProgress(progress); + }, Qt::QueuedConnection); + }); + + 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(); + } +} + +void NodeModel::setSnapshotProgress(double new_snapshot_progress) +{ + if (new_snapshot_progress != m_snapshot_progress) { + m_snapshot_progress = new_snapshot_progress; + Q_EMIT snapshotProgressChanged(); + } +} diff --git a/src/qml/models/nodemodel.h b/src/qml/models/nodemodel.h index a17f9b0833..a9989c6382 100644 --- a/src/qml/models/nodemodel.h +++ b/src/qml/models/nodemodel.h @@ -34,6 +34,7 @@ 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 NOTIFY snapshotProgressChanged) public: explicit NodeModel(interfaces::Node& node); @@ -52,6 +53,9 @@ class NodeModel : public QObject void setPause(bool new_pause); bool errorState() const { return m_faulted; } void setErrorState(bool new_error); + double snapshotProgress() const { return m_snapshot_progress; } + void setSnapshotProgress(double new_snapshot_progress); + bool isSnapshotLoaded() const; Q_INVOKABLE float getTotalBytesReceived() const { return (float)m_node.getTotalBytesRecv(); } Q_INVOKABLE float getTotalBytesSent() const { return (float)m_node.getTotalBytesSent(); } @@ -59,6 +63,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 +84,9 @@ public Q_SLOTS: void setTimeRatioList(int new_time); void setTimeRatioListInitial(); + void initializationFinished(); + void snapshotLoaded(bool result); + void snapshotProgressChanged(); protected: void timerEvent(QTimerEvent* event) override; @@ -90,7 +100,7 @@ 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}; QVector> m_block_process_time; diff --git a/src/qml/pages/settings/SettingsConnection.qml b/src/qml/pages/settings/SettingsConnection.qml index 1b2d9959d2..5ad0cb4dee 100644 --- a/src/qml/pages/settings/SettingsConnection.qml +++ b/src/qml/pages/settings/SettingsConnection.qml @@ -35,6 +35,7 @@ Item { detailActive: true detailItem: ConnectionSettings { onNext: connectionSwipe.incrementCurrentIndex() + onboarding: root.onboarding } states: [ @@ -89,19 +90,11 @@ Item { } } SettingsSnapshot { + onboarding: root.onboarding onSnapshotImportCompleted: { setSnapshotImported(true) } - onBackClicked: { - connectionSwipe.decrementCurrentIndex() - connectionSwipe.decrementCurrentIndex() - } - } - SettingsSnapshot { - onSnapshotImportCompleted: { - setSnapshotImported(true) - } - onBackClicked: { + onBack: { connectionSwipe.decrementCurrentIndex() connectionSwipe.decrementCurrentIndex() } diff --git a/src/qml/pages/settings/SettingsSnapshot.qml b/src/qml/pages/settings/SettingsSnapshot.qml index e6c557a022..9f2b27b247 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 back signal snapshotImportCompleted + property bool onboarding: false id: root @@ -24,12 +25,13 @@ 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 + onboarding: root.onboarding onSnapshotImportCompleted: { root.snapshotImportCompleted() }