Skip to content

Commit

Permalink
wip: Add minimal support for loading signet UTXO snapshots
Browse files Browse the repository at this point in the history
        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 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 is working, could be improved

        Testing:
        1. Start the node
        2. Complete onboarding
        3. Navigate to connection settings
        4. Load snapshot from provided interface
  • Loading branch information
D33r-Gee committed Nov 13, 2024
1 parent c97d2a1 commit a6bede5
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 29 deletions.
6 changes: 6 additions & 0 deletions src/interfaces/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@ class Node
//! List rpc commands.
virtual std::vector<std::string> 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;

Expand Down
7 changes: 7 additions & 0 deletions src/kernel/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,13 @@ class SigNetParams : public CChainParams {

vFixedSeeds.clear();

m_assumeutxo_data = MapAssumeutxo{
{
160000,
{AssumeutxoHash{uint256S("0x5225141cb62dee63ab3be95f9b03d60801f264010b1816d4bd00618b2736e7be")}, 1278002},
},
};

base58Prefixes[PUBKEY_ADDRESS] = std::vector<unsigned char>(1,111);
base58Prefixes[SCRIPT_ADDRESS] = std::vector<unsigned char>(1,196);
base58Prefixes[SECRET_KEY] = std::vector<unsigned char>(1,239);
Expand Down
89 changes: 88 additions & 1 deletion src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <node/context.h>
#include <node/interface_ui.h>
#include <node/transaction.h>
#include <node/utxo_snapshot.h>
#include <policy/feerate.h>
#include <policy/fees.h>
#include <policy/policy.h>
Expand Down Expand Up @@ -395,9 +396,95 @@ 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)) {
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));
}

// Snapshot Progress GUI display
COutPoint outpoint;
Coin coin;
const uint64_t coins_count = metadata.m_coins_count;
uint64_t coins_left = metadata.m_coins_count;

LogPrintf("[loadsnapshot] Loading %d coins from snapshot %s\n", coins_count, base_blockhash.ToString());
int64_t coins_processed{0};
m_snapshot_progress.store(0.0);

while (coins_left > 0) {
--coins_left;
++coins_processed;

if (coins_processed > 0) {
double progress = static_cast<float>(coins_processed) / static_cast<float>(coins_count);
m_snapshot_progress.store(progress);
if (coins_processed % 1000000 == 0) {
LogPrintf("[loadsnapshot] Progress: %.2f%% (%d/%d coins)\n",
progress * 100, coins_processed, coins_count);
}
}
}
m_snapshot_progress.store(1.0);

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};
std::atomic<double> m_snapshot_progress{0.0};
};

bool FillBlock(const CBlockIndex* index, const FoundBlock& block, UniqueLock<RecursiveMutex>& lock, const CChain& active, const BlockManager& blockman)
Expand Down Expand Up @@ -510,7 +597,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<int> getHeight() override
{
const int height{WITH_LOCK(::cs_main, return chainman().ActiveChain().Height())};
Expand Down
18 changes: 16 additions & 2 deletions src/qml/components/ConnectionSettings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down
76 changes: 65 additions & 11 deletions src/qml/components/SnapshotSettings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Dialogs 1.3

import "../controls"

ColumnLayout {
property bool snapshotLoading: nodeModel.snapshotLoading
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
property string snapshotFileName: ""
property var snapshotInfo: ({})

id: columnLayout
width: Math.min(parent.width, 450)
anchors.horizontalCenter: parent.horizontalCenter


// TODO: remove this before release
Timer {
id: snapshotSimulationTimer
interval: 50 // Update every 50ms
Expand All @@ -29,7 +34,7 @@ ColumnLayout {
snapshotVerificationProgress += 0.01
} else {
snapshotVerificationCycles++
if (snapshotVerificationCycles < 1) {
if (snapshotVerificationCycles < 3) {
snapshotVerificationProgress = 0
} else {
running = false
Expand All @@ -42,7 +47,7 @@ ColumnLayout {

StackLayout {
id: settingsStack
currentIndex: 0
currentIndex: onboarding ? 0 : snapshotVerified ? 2 : snapshotLoading ? 1 : 0

ColumnLayout {
Layout.alignment: Qt.AlignHCenter
Expand Down Expand Up @@ -78,8 +83,24 @@ 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)
snapshotFileName = fileDialog.fileUrl.toString()
console.log("Snapshot file name:", snapshotFileName)
if (snapshotFileName.endsWith(".dat")) {
nodeModel.initializeSnapshot(true, snapshotFileName)
// nodeModel.presyncProgress
} else {
console.error("Snapshot loading failed")
}
}
}
}
Expand All @@ -102,17 +123,37 @@ 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
progress: nodeModel.snapshotProgress
Layout.alignment: Qt.AlignCenter
progressColor: Theme.color.blue
}

Connections {
target: nodeModel
function onSnapshotProgressChanged() {
progressIndicator.progress = nodeModel.snapshotProgress
}

function onSnapshotLoaded(success) {
if (success) {
chainModel.isSnapshotActiveChanged()
snapshotVerified = chainModel.isSnapshotActive
snapshotInfo = chainModel.getSnapshotInfo()
settingsStack.currentIndex = 2 // Move to the "Snapshot Loaded" page
} else {
// Handle snapshot loading failure
console.error("Snapshot loading failed")
}
}
}
}

ColumnLayout {
Expand All @@ -137,8 +178,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 {
Expand Down Expand Up @@ -188,16 +232,26 @@ 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...")
// The CoreText component displays the hash of the loaded snapshot.
text: snapshotInfo && snapshotInfo["hashSerialized"] ?
qsTr("Hash: %1").arg(snapshotInfo["hashSerialized"].substring(0, 13) + "...") :
qsTr("Hash: DEBUG")
font.pixelSize: 14
}

Component.onCompleted: {
if (snapshotVerified) {
snapshotInfo = chainModel.getSnapshotInfo()
}
}
}
}
}
Expand Down
22 changes: 22 additions & 0 deletions src/qml/models/chainmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
#include <QThread>
#include <QTime>
#include <interfaces/chain.h>
#include <node/utxo_snapshot.h>
#include <kernel/chainparams.h>
#include <validation.h>

ChainModel::ChainModel(interfaces::Chain& chain)
: m_chain{chain}
// m_params{Params()}
{
QTimer* timer = new QTimer();
connect(timer, &QTimer::timeout, this, &ChainModel::setCurrentTimeRatio);
Expand Down Expand Up @@ -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;
}
6 changes: 5 additions & 1 deletion src/qml/models/chainmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -36,18 +37,21 @@ 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();

Q_SIGNALS:
void timeRatioListChanged();
void currentNetworkNameChanged();
void isSnapshotActiveChanged();

private:
QString m_current_network_name;
Expand Down
Loading

0 comments on commit a6bede5

Please sign in to comment.