Skip to content

Commit e3f8c70

Browse files
committed
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, src/node/interfaces.cpp and src/validation.h and src/validation.cpp to implement snapshot loading functionality through the options model. 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
1 parent 1d08d2c commit e3f8c70

13 files changed

+373
-42
lines changed

src/interfaces/node.h

+14
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ struct CNodeStateStats;
3535
struct bilingual_str;
3636
struct PeersNumByType;
3737
namespace node {
38+
class SnapshotMetadata;
3839
struct NodeContext;
3940
} // namespace node
4041
namespace wallet {
@@ -45,6 +46,7 @@ namespace interfaces {
4546
class Handler;
4647
class WalletLoader;
4748
struct BlockTip;
49+
struct SnapshotLoadResult;
4850

4951
//! Block and header tip information
5052
struct BlockAndHeaderTipInfo
@@ -199,6 +201,12 @@ class Node
199201
//! List rpc commands.
200202
virtual std::vector<std::string> listRpcCommands() = 0;
201203

204+
//! Get block time from hash.
205+
virtual int64_t getBlockTime(const uint256& hash) const = 0;
206+
207+
//! Load UTXO Snapshot.
208+
virtual SnapshotLoadResult snapshotLoad(const std::string& path_string, std::function<void(double)> progress_callback, node::SnapshotMetadata& metadata) = 0;
209+
202210
//! Set RPC timer interface if unset.
203211
virtual void rpcSetTimerInterfaceIfUnset(RPCTimerInterface* iface) = 0;
204212

@@ -280,6 +288,12 @@ struct BlockTip {
280288
uint256 block_hash;
281289
};
282290

291+
//! Snapshot load result.
292+
struct SnapshotLoadResult {
293+
bool success;
294+
int block_height;
295+
};
296+
283297
} // namespace interfaces
284298

285299
#endif // BITCOIN_INTERFACES_NODE_H

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);

src/node/interfaces.cpp

+70
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>
@@ -68,6 +69,7 @@ using interfaces::Handler;
6869
using interfaces::MakeSignalHandler;
6970
using interfaces::Node;
7071
using interfaces::WalletLoader;
72+
using interfaces::SnapshotLoadResult;
7173

7274
namespace node {
7375
// All members of the classes in this namespace are intentionally public, as the
@@ -395,9 +397,76 @@ class NodeImpl : public Node
395397
{
396398
m_context = context;
397399
}
400+
SnapshotLoadResult snapshotLoad(const std::string& path_string, std::function<void(double)> progress_callback, SnapshotMetadata& metadata) override
401+
{
402+
const fs::path path = fs::u8path(path_string);
403+
if (!fs::exists(path)) {
404+
LogPrintf("[loadsnapshot] Snapshot file %s does not exist\n", path.u8string());
405+
return {false, 0};
406+
}
407+
408+
AutoFile afile{fsbridge::fopen(path, "rb")};
409+
if (afile.IsNull()) {
410+
LogPrintf("[loadsnapshot] Failed to open snapshot file %s\n", path.u8string());
411+
return {false, 0};
412+
}
413+
414+
try {
415+
afile >> metadata;
416+
} catch (const std::exception& e) {
417+
LogPrintf("[loadsnapshot] Failed to read snapshot metadata: %s\n", e.what());
418+
return {false, 0};
419+
}
420+
421+
const uint256& base_blockhash = metadata.m_base_blockhash;
422+
LogPrintf("[loadsnapshot] Waiting for blockheader %s in headers chain before snapshot activation\n",
423+
base_blockhash.ToString());
424+
425+
if (!m_context->chainman) {
426+
LogPrintf("[loadsnapshot] Chainman is null\n");
427+
return {false, 0};
428+
}
429+
430+
ChainstateManager& chainman = *m_context->chainman;
431+
CBlockIndex* snapshot_start_block = nullptr;
432+
433+
// Wait for the block to appear in the block index
434+
constexpr int max_wait_seconds = 600; // 10 minutes
435+
for (int i = 0; i < max_wait_seconds; ++i) {
436+
snapshot_start_block = WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(base_blockhash));
437+
if (snapshot_start_block) break;
438+
std::this_thread::sleep_for(std::chrono::seconds(1));
439+
}
440+
441+
if (!snapshot_start_block) {
442+
LogPrintf("[loadsnapshot] Timed out waiting for snapshot start blockheader %s\n", base_blockhash.ToString());
443+
return {false, 0};
444+
}
445+
446+
// Activate the snapshot
447+
if (!chainman.ActivateSnapshot(afile, metadata, false, progress_callback)) {
448+
LogPrintf("[loadsnapshot] Unable to load UTXO snapshot %s\n", path.u8string());
449+
return {false, 0};
450+
}
451+
452+
CBlockIndex* new_tip = WITH_LOCK(::cs_main, return chainman.ActiveTip());
453+
LogPrintf("[loadsnapshot] Loaded %d coins from snapshot %s at height %d\n",
454+
metadata.m_coins_count, new_tip->GetBlockHash().ToString(), new_tip->nHeight);
455+
456+
return {true, new_tip->nHeight};
457+
}
398458
ArgsManager& args() { return *Assert(Assert(m_context)->args); }
399459
ChainstateManager& chainman() { return *Assert(m_context->chainman); }
400460
NodeContext* m_context{nullptr};
461+
int64_t getBlockTime(const uint256& hash) const override
462+
{
463+
LOCK(cs_main);
464+
const CBlockIndex* pblockindex = m_context->chainman->m_blockman.LookupBlockIndex(hash);
465+
if (!pblockindex) {
466+
return 0;
467+
}
468+
return pblockindex->GetBlockTime();
469+
}
401470
};
402471

403472
bool FillBlock(const CBlockIndex* index, const FoundBlock& block, UniqueLock<RecursiveMutex>& lock, const CChain& active, const BlockManager& blockman)
@@ -809,3 +878,4 @@ namespace interfaces {
809878
std::unique_ptr<Node> MakeNode(node::NodeContext& context) { return std::make_unique<node::NodeImpl>(context); }
810879
std::unique_ptr<Chain> MakeChain(node::NodeContext& context) { return std::make_unique<node::ChainImpl>(context); }
811880
} // namespace interfaces
881+

src/qml/bitcoin.cpp

+4-2
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ int QmlGuiMain(int argc, char* argv[])
242242

243243
// Default printtoconsole to false for the GUI. GUI programs should not
244244
// print to the console unnecessarily.
245-
gArgs.SoftSetBoolArg("-printtoconsole", false);
245+
// gArgs.SoftSetBoolArg("-printtoconsole", false);
246246
InitLogging(gArgs);
247247
InitParameterInteraction(gArgs);
248248

@@ -311,6 +311,8 @@ int QmlGuiMain(int argc, char* argv[])
311311
engine.rootContext()->setContextProperty("optionsModel", &options_model);
312312
engine.rootContext()->setContextProperty("needOnboarding", need_onboarding);
313313

314+
// QObject::connect(&node_model, &NodeModel::snapshotLoaded, &options_model, &OptionsQmlModel::setSnapshotLoadCompleted);
315+
314316
AppMode app_mode = SetupAppMode();
315317

316318
qmlRegisterSingletonInstance<AppMode>("org.bitcoincore.qt", 1, 0, "AppMode", &app_mode);
@@ -330,7 +332,7 @@ int QmlGuiMain(int argc, char* argv[])
330332
}
331333

332334
// Install qDebug() message handler to route to debug.log
333-
qInstallMessageHandler(DebugMessageHandler);
335+
// qInstallMessageHandler(DebugMessageHandler);
334336

335337
qInfo() << "Graphics API in use:" << QmlUtil::GraphicsApi(window);
336338

src/qml/components/ConnectionSettings.qml

+44-21
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,59 @@ import QtQuick.Layouts 1.15
88
import "../controls"
99

1010
ColumnLayout {
11-
property bool snapshotImported: false
11+
// TODO: Remove this once storing the snapshot path is implemented
12+
property bool isOnboarding: false
13+
property bool snapshotImported: optionsModel.snapshotLoadCompleted
14+
15+
Component.onCompleted: {
16+
snapshotImported = optionsModel.snapshotLoadCompleted
17+
}
18+
1219
function setSnapshotImported(imported) {
1320
snapshotImported = imported
21+
optionsModel.setSnapshotLoadCompleted(imported)
1422
}
1523
spacing: 4
16-
Setting {
17-
id: gotoSnapshot
24+
Item {
25+
// TODO: Remove this once storing the snapshot path is implemented
26+
visible: !isOnboarding
27+
height: visible ? implicitHeight : 0
1828
Layout.fillWidth: true
19-
header: qsTr("Load snapshot")
20-
description: qsTr("Instant use with background sync")
21-
actionItem: Item {
22-
width: 26
23-
height: 26
24-
CaretRightIcon {
25-
anchors.centerIn: parent
26-
visible: !snapshotImported
27-
color: gotoSnapshot.stateColor
29+
Layout.preferredHeight: gotoSnapshot.height
30+
31+
Setting {
32+
id: gotoSnapshot
33+
visible: parent.visible
34+
Layout.fillWidth: true
35+
header: qsTr("Load snapshot")
36+
description: qsTr("Instant use with background sync")
37+
actionItem: Item {
38+
width: 26
39+
height: 26
40+
CaretRightIcon {
41+
// TODO: aligment will be fixed once Onboarding snapshot works
42+
anchors.right: parent.right
43+
anchors.verticalCenter: parent.verticalCenter
44+
visible: !snapshotImported
45+
color: gotoSnapshot.stateColor
46+
}
47+
GreenCheckIcon {
48+
anchors.centerIn: parent
49+
visible: snapshotImported
50+
color: Theme.color.transparent
51+
}
2852
}
29-
GreenCheckIcon {
30-
anchors.centerIn: parent
31-
visible: snapshotImported
32-
color: Theme.color.transparent
53+
onClicked: {
54+
connectionSwipe.incrementCurrentIndex()
55+
connectionSwipe.incrementCurrentIndex()
3356
}
3457
}
35-
onClicked: {
36-
connectionSwipe.incrementCurrentIndex()
37-
connectionSwipe.incrementCurrentIndex()
38-
}
3958
}
40-
Separator { Layout.fillWidth: true }
59+
Separator {
60+
Layout.fillWidth: true
61+
// TODO: Remove this once storing the snapshot path is implemented
62+
visible: !isOnboarding
63+
}
4164
Setting {
4265
Layout.fillWidth: true
4366
header: qsTr("Enable listening")

src/qml/components/SnapshotSettings.qml

+50-10
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
9+
810

911
import "../controls"
1012

1113
ColumnLayout {
1214
signal snapshotImportCompleted()
1315
property int snapshotVerificationCycles: 0
1416
property real snapshotVerificationProgress: 0
15-
property bool snapshotVerified: false
17+
property bool snapshotVerified: optionsModel.snapshotLoadCompleted
1618

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

21-
23+
// TODO: Remove simulation timer before release
2224
Timer {
2325
id: snapshotSimulationTimer
2426
interval: 50 // Update every 50ms
@@ -42,7 +44,7 @@ ColumnLayout {
4244

4345
StackLayout {
4446
id: settingsStack
45-
currentIndex: 0
47+
currentIndex: optionsModel.snapshotLoadCompleted ? 2 : 0
4648

4749
ColumnLayout {
4850
Layout.alignment: Qt.AlignHCenter
@@ -78,8 +80,26 @@ ColumnLayout {
7880
Layout.alignment: Qt.AlignCenter
7981
text: qsTr("Choose snapshot file")
8082
onClicked: {
81-
settingsStack.currentIndex = 1
82-
snapshotSimulationTimer.start()
83+
// TODO: Connect this to snapshot loading
84+
// settingsStack.currentIndex = 1
85+
fileDialog.open()
86+
}
87+
}
88+
89+
FileDialog {
90+
id: fileDialog
91+
folder: shortcuts.home
92+
selectMultiple: false
93+
onAccepted: {
94+
console.log("File chosen:", fileDialog.fileUrls)
95+
var snapshotFileName = fileDialog.fileUrl.toString()
96+
console.log("Snapshot file name:", snapshotFileName)
97+
if (snapshotFileName.endsWith(".dat")) {
98+
// optionsModel.setSnapshotDirectory(snapshotFileName)
99+
// console.log("Snapshot directory set:", optionsModel.getSnapshotDirectory())
100+
optionsModel.initializeSnapshot(true, snapshotFileName)
101+
settingsStack.currentIndex = 1
102+
}
83103
}
84104
}
85105
}
@@ -109,10 +129,27 @@ ColumnLayout {
109129
Layout.topMargin: 20
110130
width: 200
111131
height: 20
112-
progress: snapshotVerificationProgress
132+
progress: optionsModel.snapshotProgress
113133
Layout.alignment: Qt.AlignCenter
114134
progressColor: Theme.color.blue
115135
}
136+
137+
Connections {
138+
target: optionsModel
139+
function onSnapshotProgressChanged() {
140+
progressIndicator.progress = optionsModel.snapshotProgress
141+
}
142+
function onSnapshotLoadCompletedChanged(success) {
143+
if (success) {
144+
progressIndicator.progress = 1
145+
settingsStack.currentIndex = 2 // Move to the "Snapshot Loaded" page
146+
} else {
147+
// Handle snapshot loading failure
148+
console.error("Snapshot loading failed")
149+
// You might want to show an error message or take other actions here
150+
}
151+
}
152+
}
116153
}
117154

118155
ColumnLayout {
@@ -137,8 +174,8 @@ ColumnLayout {
137174
descriptionColor: Theme.color.neutral6
138175
descriptionSize: 17
139176
descriptionLineHeight: 1.1
140-
description: qsTr("It contains transactions up to January 12, 2024. Newer transactions still need to be downloaded." +
141-
" The data will be verified in the background.")
177+
description: qsTr("It contains transactions up to %1. Newer transactions still need to be downloaded." +
178+
" The data will be verified in the background.").arg(optionsModel.snapshotDate)
142179
}
143180

144181
ContinueButton {
@@ -188,14 +225,17 @@ ColumnLayout {
188225
font.pixelSize: 14
189226
}
190227
CoreText {
191-
text: qsTr("200,000")
228+
// text: qsTr("160,000")
229+
text: optionsModel.snapshotBlockHeight.toLocaleString(Qt.locale(), 'f', 0)
192230
Layout.alignment: Qt.AlignRight
193231
font.pixelSize: 14
194232
}
195233
}
196234
Separator { Layout.fillWidth: true }
197235
CoreText {
198-
text: qsTr("Hash: 0x1234567890abcdef...")
236+
// text: qsTr("Hash: 0x1234567890abcdef...")
237+
// TODO: truncate the hash to 8 characters
238+
text: qsTr("Hash: %1").arg(optionsModel.snapshotBlockHash)
199239
font.pixelSize: 14
200240
}
201241
}

0 commit comments

Comments
 (0)