Skip to content

Commit 955c65d

Browse files
committed
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. Modifies src/interfaces/node.h, src/node/interfaces.cpp and chainparams.cpp (temporarily for signet snapshot testing) to implement 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 uses a regex to extract Pre-Synchronizing progress Testing: 1. Start the node 2. Complete onboarding 3. Navigate to connection settings 4. Load snapshot from provided interface
1 parent c97d2a1 commit 955c65d

File tree

10 files changed

+244
-25
lines changed

10 files changed

+244
-25
lines changed

Diff for: src/interfaces/node.h

+10
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ class Node
199199
//! List rpc commands.
200200
virtual std::vector<std::string> listRpcCommands() = 0;
201201

202+
//! Load UTXO Snapshot.
203+
virtual bool snapshotLoad(const std::string& path_string) = 0;
204+
205+
//! Set snapshot progress callback.
206+
using SnapshotProgressFn = std::function<void(double progress)>;
207+
virtual void setSnapshotProgressCallback(SnapshotProgressFn fn) = 0;
208+
209+
//! Notify snapshot progress.
210+
virtual void notifySnapshotProgress(double progress) = 0;
211+
202212
//! Set RPC timer interface if unset.
203213
virtual void rpcSetTimerInterfaceIfUnset(RPCTimerInterface* iface) = 0;
204214

Diff for: src/kernel/chainparams.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,13 @@ class SigNetParams : public CChainParams {
371371

372372
vFixedSeeds.clear();
373373

374+
m_assumeutxo_data = MapAssumeutxo{
375+
{
376+
160000,
377+
{AssumeutxoHash{uint256S("0x5225141cb62dee63ab3be95f9b03d60801f264010b1816d4bd00618b2736e7be")}, 1278002},
378+
},
379+
};
380+
374381
base58Prefixes[PUBKEY_ADDRESS] = std::vector<unsigned char>(1,111);
375382
base58Prefixes[SCRIPT_ADDRESS] = std::vector<unsigned char>(1,196);
376383
base58Prefixes[SECRET_KEY] = std::vector<unsigned char>(1,239);

Diff for: src/node/interfaces.cpp

+96-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <node/context.h>
3030
#include <node/interface_ui.h>
3131
#include <node/transaction.h>
32+
#include <node/utxo_snapshot.h>
3233
#include <policy/feerate.h>
3334
#include <policy/fees.h>
3435
#include <policy/policy.h>
@@ -58,7 +59,7 @@
5859
#include <memory>
5960
#include <optional>
6061
#include <utility>
61-
62+
#include <regex>
6263
#include <boost/signals2/signal.hpp>
6364

6465
using interfaces::BlockTip;
@@ -395,6 +396,99 @@ class NodeImpl : public Node
395396
{
396397
m_context = context;
397398
}
399+
SnapshotProgressFn m_snapshot_progress_callback{nullptr};
400+
void setSnapshotProgressCallback(SnapshotProgressFn fn) override
401+
{
402+
m_snapshot_progress_callback = std::move(fn);
403+
}
404+
void notifySnapshotProgress(double progress) override
405+
{
406+
if (m_snapshot_progress_callback) m_snapshot_progress_callback(progress);
407+
}
408+
bool snapshotLoad(const std::string& path_string) override
409+
{
410+
// Set up log message parsing
411+
LogInstance().PushBackCallback([this](const std::string& str) {
412+
static const std::regex progress_regex(R"(\[snapshot\] (\d+) coins loaded \(([0-9.]+)%.*\))");
413+
static const std::regex sync_progress_regex(R"(Synchronizing blockheaders, height: (\d+) \(~([\d.]+)%\))");
414+
415+
std::smatch matches;
416+
if (std::regex_search(str, matches, progress_regex)) {
417+
try {
418+
double percentage = std::stod(matches[2]);
419+
// Convert percentage to 0-1 range and notify through callback
420+
notifySnapshotProgress(percentage / 100.0);
421+
} catch (...) {
422+
// Handle parsing errors
423+
}
424+
} else if (std::regex_search(str, matches, sync_progress_regex)) {
425+
try {
426+
double percentage = std::stod(matches[2]);
427+
// Convert percentage to 0-1 range and notify through callback
428+
notifySnapshotProgress(percentage / 100.0);
429+
} catch (...) {
430+
// Handle parsing errors
431+
}
432+
}
433+
});
434+
435+
const fs::path path = fs::u8path(path_string);
436+
if (!fs::exists(path)) {
437+
LogPrintf("[loadsnapshot] Snapshot file %s does not exist\n", path.u8string());
438+
return false;
439+
}
440+
441+
AutoFile afile{fsbridge::fopen(path, "rb")};
442+
if (afile.IsNull()) {
443+
LogPrintf("[loadsnapshot] Failed to open snapshot file %s\n", path.u8string());
444+
return false;
445+
}
446+
447+
SnapshotMetadata metadata;
448+
try {
449+
afile >> metadata;
450+
} catch (const std::exception& e) {
451+
LogPrintf("[loadsnapshot] Failed to read snapshot metadata: %s\n", e.what());
452+
return false;
453+
}
454+
455+
const uint256& base_blockhash = metadata.m_base_blockhash;
456+
LogPrintf("[loadsnapshot] Waiting for blockheader %s in headers chain before snapshot activation\n",
457+
base_blockhash.ToString());
458+
459+
if (!m_context->chainman) {
460+
LogPrintf("[loadsnapshot] Chainman is null\n");
461+
return false;
462+
}
463+
464+
ChainstateManager& chainman = *m_context->chainman;
465+
CBlockIndex* snapshot_start_block = nullptr;
466+
467+
// Wait for the block to appear in the block index
468+
constexpr int max_wait_seconds = 600; // 10 minutes
469+
for (int i = 0; i < max_wait_seconds; ++i) {
470+
snapshot_start_block = WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(base_blockhash));
471+
if (snapshot_start_block) break;
472+
std::this_thread::sleep_for(std::chrono::seconds(1));
473+
}
474+
475+
if (!snapshot_start_block) {
476+
LogPrintf("[loadsnapshot] Timed out waiting for snapshot start blockheader %s\n", base_blockhash.ToString());
477+
return false;
478+
}
479+
480+
// Activate the snapshot
481+
if (!chainman.ActivateSnapshot(afile, metadata, false)) {
482+
LogPrintf("[loadsnapshot] Unable to load UTXO snapshot %s\n", path.u8string());
483+
return false;
484+
}
485+
486+
CBlockIndex* new_tip = WITH_LOCK(::cs_main, return chainman.ActiveTip());
487+
LogPrintf("[loadsnapshot] Loaded %d coins from snapshot %s at height %d\n",
488+
metadata.m_coins_count, new_tip->GetBlockHash().ToString(), new_tip->nHeight);
489+
490+
return true;
491+
}
398492
ArgsManager& args() { return *Assert(Assert(m_context)->args); }
399493
ChainstateManager& chainman() { return *Assert(m_context->chainman); }
400494
NodeContext* m_context{nullptr};
@@ -510,7 +604,7 @@ class RpcHandlerImpl : public Handler
510604
class ChainImpl : public Chain
511605
{
512606
public:
513-
explicit ChainImpl(NodeContext& node) : m_node(node) {}
607+
explicit ChainImpl(node::NodeContext& node) : m_node(node) {}
514608
std::optional<int> getHeight() override
515609
{
516610
const int height{WITH_LOCK(::cs_main, return chainman().ActiveChain().Height())};

Diff for: src/qml/components/ConnectionSettings.qml

+16-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,24 @@ import "../controls"
1010
ColumnLayout {
1111
id: root
1212
signal next
13-
property bool snapshotImported: false
13+
property bool snapshotImported: onboarding ? false : chainModel.isSnapshotActive
14+
property bool onboarding: false
15+
16+
Component.onCompleted: {
17+
if (!onboarding) {
18+
snapshotImported = chainModel.isSnapshotActive
19+
} else {
20+
snapshotImported = false
21+
}
22+
}
23+
1424
function setSnapshotImported(imported) {
1525
snapshotImported = imported
1626
}
1727
spacing: 4
1828
Setting {
1929
id: gotoSnapshot
30+
visible: !root.onboarding
2031
Layout.fillWidth: true
2132
header: qsTr("Load snapshot")
2233
description: qsTr("Instant use with background sync")
@@ -40,7 +51,10 @@ ColumnLayout {
4051
connectionSwipe.incrementCurrentIndex()
4152
}
4253
}
43-
Separator { Layout.fillWidth: true }
54+
Separator {
55+
visible: !root.onboarding
56+
Layout.fillWidth: true
57+
}
4458
Setting {
4559
Layout.fillWidth: true
4660
header: qsTr("Enable listening")

Diff for: src/qml/components/SnapshotSettings.qml

+54-7
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,22 @@
55
import QtQuick 2.15
66
import QtQuick.Controls 2.15
77
import QtQuick.Layouts 1.15
8+
import QtQuick.Dialogs 1.3
89

910
import "../controls"
1011

1112
ColumnLayout {
1213
signal snapshotImportCompleted()
1314
property int snapshotVerificationCycles: 0
1415
property real snapshotVerificationProgress: 0
15-
property bool snapshotVerified: false
16+
property bool onboarding: false
17+
property bool snapshotVerified: onboarding ? false : chainModel.isSnapshotActive
1618

1719
id: columnLayout
1820
width: Math.min(parent.width, 450)
1921
anchors.horizontalCenter: parent.horizontalCenter
2022

21-
23+
// TODO: Remove this once the verification progress is available
2224
Timer {
2325
id: snapshotSimulationTimer
2426
interval: 50 // Update every 50ms
@@ -29,7 +31,7 @@ ColumnLayout {
2931
snapshotVerificationProgress += 0.01
3032
} else {
3133
snapshotVerificationCycles++
32-
if (snapshotVerificationCycles < 1) {
34+
if (snapshotVerificationCycles < 3) {
3335
snapshotVerificationProgress = 0
3436
} else {
3537
running = false
@@ -42,7 +44,7 @@ ColumnLayout {
4244

4345
StackLayout {
4446
id: settingsStack
45-
currentIndex: 0
47+
currentIndex: onboarding ? 0 : snapshotVerified ? 2 : 0
4648

4749
ColumnLayout {
4850
Layout.alignment: Qt.AlignHCenter
@@ -78,8 +80,22 @@ ColumnLayout {
7880
Layout.alignment: Qt.AlignCenter
7981
text: qsTr("Choose snapshot file")
8082
onClicked: {
81-
settingsStack.currentIndex = 1
82-
snapshotSimulationTimer.start()
83+
fileDialog.open()
84+
}
85+
}
86+
87+
FileDialog {
88+
id: fileDialog
89+
folder: shortcuts.home
90+
selectMultiple: false
91+
onAccepted: {
92+
console.log("File chosen:", fileDialog.fileUrls)
93+
var snapshotFileName = fileDialog.fileUrl.toString()
94+
console.log("Snapshot file name:", snapshotFileName)
95+
if (snapshotFileName.endsWith(".dat")) {
96+
nodeModel.initializeSnapshot(true, snapshotFileName)
97+
settingsStack.currentIndex = 1
98+
}
8399
}
84100
}
85101
}
@@ -109,10 +125,34 @@ ColumnLayout {
109125
Layout.topMargin: 20
110126
width: 200
111127
height: 20
112-
progress: snapshotVerificationProgress
128+
// TODO: uncomment this once the verification progress is available
129+
// progress: nodeModel.verificationProgress
130+
progress: 0
113131
Layout.alignment: Qt.AlignCenter
114132
progressColor: Theme.color.blue
115133
}
134+
135+
Connections {
136+
target: nodeModel
137+
// TODO: uncomment this once the verification progress is available
138+
// function onVerificationProgressChanged() {
139+
// progressIndicator.progress = nodeModel.verificationProgress
140+
// }
141+
function onSnapshotProgressChanged() {
142+
progressIndicator.progress = nodeModel.snapshotProgress
143+
}
144+
function onSnapshotLoaded(success) {
145+
if (success) {
146+
chainModel.isSnapshotActiveChanged()
147+
snapshotVerified = chainModel.isSnapshotActive
148+
progressIndicator.progress = 1
149+
settingsStack.currentIndex = 2 // Move to the "Snapshot Loaded" page
150+
} else {
151+
// Handle snapshot loading failure
152+
console.error("Snapshot loading failed")
153+
}
154+
}
155+
}
116156
}
117157

118158
ColumnLayout {
@@ -137,6 +177,7 @@ ColumnLayout {
137177
descriptionColor: Theme.color.neutral6
138178
descriptionSize: 17
139179
descriptionLineHeight: 1.1
180+
// TODO: Update this description once the snapshot is verified
140181
description: qsTr("It contains transactions up to January 12, 2024. Newer transactions still need to be downloaded." +
141182
" The data will be verified in the background.")
142183
}
@@ -153,6 +194,9 @@ ColumnLayout {
153194
}
154195
}
155196

197+
// TODO: Update this with the actual snapshot details
198+
// TODO: uncomment this once the snapshot details are available
199+
/*
156200
Setting {
157201
id: viewDetails
158202
Layout.alignment: Qt.AlignCenter
@@ -188,17 +232,20 @@ ColumnLayout {
188232
font.pixelSize: 14
189233
}
190234
CoreText {
235+
// TODO: Update this with the actual block height
191236
text: qsTr("200,000")
192237
Layout.alignment: Qt.AlignRight
193238
font.pixelSize: 14
194239
}
195240
}
196241
Separator { Layout.fillWidth: true }
197242
CoreText {
243+
// TODO: Update this with the actual snapshot file hash
198244
text: qsTr("Hash: 0x1234567890abcdef...")
199245
font.pixelSize: 14
200246
}
201247
}
248+
*/
202249
}
203250
}
204251
}

Diff for: src/qml/models/chainmodel.h

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <QString>
1313
#include <QTimer>
1414
#include <QVariant>
15+
#include <QDebug>
1516

1617
namespace interfaces {
1718
class FoundBlock;
@@ -27,6 +28,7 @@ class ChainModel : public QObject
2728
Q_PROPERTY(quint64 assumedBlockchainSize READ assumedBlockchainSize CONSTANT)
2829
Q_PROPERTY(quint64 assumedChainstateSize READ assumedChainstateSize CONSTANT)
2930
Q_PROPERTY(QVariantList timeRatioList READ timeRatioList NOTIFY timeRatioListChanged)
31+
Q_PROPERTY(bool isSnapshotActive READ isSnapshotActive NOTIFY isSnapshotActiveChanged)
3032

3133
public:
3234
explicit ChainModel(interfaces::Chain& chain);
@@ -36,7 +38,7 @@ class ChainModel : public QObject
3638
quint64 assumedBlockchainSize() const { return m_assumed_blockchain_size; };
3739
quint64 assumedChainstateSize() const { return m_assumed_chainstate_size; };
3840
QVariantList timeRatioList() const { return m_time_ratio_list; };
39-
41+
bool isSnapshotActive() const { return m_chain.hasAssumedValidChain(); };
4042
int timestampAtMeridian();
4143

4244
void setCurrentTimeRatio();
@@ -48,6 +50,7 @@ public Q_SLOTS:
4850
Q_SIGNALS:
4951
void timeRatioListChanged();
5052
void currentNetworkNameChanged();
53+
void isSnapshotActiveChanged();
5154

5255
private:
5356
QString m_current_network_name;

0 commit comments

Comments
 (0)