Skip to content

Commit

Permalink
wip: Add initial support for loading signet UTXO snapshots
Browse files Browse the repository at this point in the history
        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
  • Loading branch information
D33r-Gee committed Oct 25, 2024
1 parent 1d08d2c commit e3f8c70
Show file tree
Hide file tree
Showing 13 changed files with 373 additions and 42 deletions.
14 changes: 14 additions & 0 deletions src/interfaces/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ struct CNodeStateStats;
struct bilingual_str;
struct PeersNumByType;
namespace node {
class SnapshotMetadata;
struct NodeContext;
} // namespace node
namespace wallet {
Expand All @@ -45,6 +46,7 @@ namespace interfaces {
class Handler;
class WalletLoader;
struct BlockTip;
struct SnapshotLoadResult;

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

//! Get block time from hash.
virtual int64_t getBlockTime(const uint256& hash) const = 0;

//! Load UTXO Snapshot.
virtual SnapshotLoadResult snapshotLoad(const std::string& path_string, std::function<void(double)> progress_callback, node::SnapshotMetadata& metadata) = 0;

//! Set RPC timer interface if unset.
virtual void rpcSetTimerInterfaceIfUnset(RPCTimerInterface* iface) = 0;

Expand Down Expand Up @@ -280,6 +288,12 @@ struct BlockTip {
uint256 block_hash;
};

//! Snapshot load result.
struct SnapshotLoadResult {
bool success;
int block_height;
};

} // namespace interfaces

#endif // BITCOIN_INTERFACES_NODE_H
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
70 changes: 70 additions & 0 deletions 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 @@ -68,6 +69,7 @@ using interfaces::Handler;
using interfaces::MakeSignalHandler;
using interfaces::Node;
using interfaces::WalletLoader;
using interfaces::SnapshotLoadResult;

namespace node {
// All members of the classes in this namespace are intentionally public, as the
Expand Down Expand Up @@ -395,9 +397,76 @@ class NodeImpl : public Node
{
m_context = context;
}
SnapshotLoadResult snapshotLoad(const std::string& path_string, std::function<void(double)> progress_callback, SnapshotMetadata& metadata) 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, 0};
}

AutoFile afile{fsbridge::fopen(path, "rb")};
if (afile.IsNull()) {
LogPrintf("[loadsnapshot] Failed to open snapshot file %s\n", path.u8string());
return {false, 0};
}

try {
afile >> metadata;
} catch (const std::exception& e) {
LogPrintf("[loadsnapshot] Failed to read snapshot metadata: %s\n", e.what());
return {false, 0};
}

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, 0};
}

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, 0};
}

// Activate the snapshot
if (!chainman.ActivateSnapshot(afile, metadata, false, progress_callback)) {
LogPrintf("[loadsnapshot] Unable to load UTXO snapshot %s\n", path.u8string());
return {false, 0};
}

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, new_tip->nHeight};
}
ArgsManager& args() { return *Assert(Assert(m_context)->args); }
ChainstateManager& chainman() { return *Assert(m_context->chainman); }
NodeContext* m_context{nullptr};
int64_t getBlockTime(const uint256& hash) const override
{
LOCK(cs_main);
const CBlockIndex* pblockindex = m_context->chainman->m_blockman.LookupBlockIndex(hash);
if (!pblockindex) {
return 0;
}
return pblockindex->GetBlockTime();
}
};

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

6 changes: 4 additions & 2 deletions src/qml/bitcoin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ int QmlGuiMain(int argc, char* argv[])

// Default printtoconsole to false for the GUI. GUI programs should not
// print to the console unnecessarily.
gArgs.SoftSetBoolArg("-printtoconsole", false);
// gArgs.SoftSetBoolArg("-printtoconsole", false);
InitLogging(gArgs);
InitParameterInteraction(gArgs);

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

// QObject::connect(&node_model, &NodeModel::snapshotLoaded, &options_model, &OptionsQmlModel::setSnapshotLoadCompleted);

AppMode app_mode = SetupAppMode();

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

// Install qDebug() message handler to route to debug.log
qInstallMessageHandler(DebugMessageHandler);
// qInstallMessageHandler(DebugMessageHandler);

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

Expand Down
65 changes: 44 additions & 21 deletions src/qml/components/ConnectionSettings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,59 @@ import QtQuick.Layouts 1.15
import "../controls"

ColumnLayout {
property bool snapshotImported: false
// TODO: Remove this once storing the snapshot path is implemented
property bool isOnboarding: false
property bool snapshotImported: optionsModel.snapshotLoadCompleted

Component.onCompleted: {
snapshotImported = optionsModel.snapshotLoadCompleted
}

function setSnapshotImported(imported) {
snapshotImported = imported
optionsModel.setSnapshotLoadCompleted(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")
Expand Down
60 changes: 50 additions & 10 deletions src/qml/components/SnapshotSettings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Dialogs 1.3


import "../controls"

ColumnLayout {
signal snapshotImportCompleted()
property int snapshotVerificationCycles: 0
property real snapshotVerificationProgress: 0
property bool snapshotVerified: false
property bool snapshotVerified: optionsModel.snapshotLoadCompleted

id: 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
Expand All @@ -42,7 +44,7 @@ ColumnLayout {

StackLayout {
id: settingsStack
currentIndex: 0
currentIndex: optionsModel.snapshotLoadCompleted ? 2 : 0

ColumnLayout {
Layout.alignment: Qt.AlignHCenter
Expand Down Expand Up @@ -78,8 +80,26 @@ 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())
optionsModel.initializeSnapshot(true, snapshotFileName)
settingsStack.currentIndex = 1
}
}
}
}
Expand Down Expand Up @@ -109,10 +129,27 @@ ColumnLayout {
Layout.topMargin: 20
width: 200
height: 20
progress: snapshotVerificationProgress
progress: optionsModel.snapshotProgress
Layout.alignment: Qt.AlignCenter
progressColor: Theme.color.blue
}

Connections {
target: optionsModel
function onSnapshotProgressChanged() {
progressIndicator.progress = optionsModel.snapshotProgress
}
function onSnapshotLoadCompletedChanged(success) {
if (success) {
progressIndicator.progress = 1
settingsStack.currentIndex = 2 // Move to the "Snapshot Loaded" page
} else {
// Handle snapshot loading failure
console.error("Snapshot loading failed")
// You might want to show an error message or take other actions here
}
}
}
}

ColumnLayout {
Expand All @@ -137,8 +174,8 @@ 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: qsTr("It contains transactions up to %1. Newer transactions still need to be downloaded." +
" The data will be verified in the background.").arg(optionsModel.snapshotDate)
}

ContinueButton {
Expand Down Expand Up @@ -188,14 +225,17 @@ ColumnLayout {
font.pixelSize: 14
}
CoreText {
text: qsTr("200,000")
// text: qsTr("160,000")
text: optionsModel.snapshotBlockHeight.toLocaleString(Qt.locale(), 'f', 0)
Layout.alignment: Qt.AlignRight
font.pixelSize: 14
}
}
Separator { Layout.fillWidth: true }
CoreText {
text: qsTr("Hash: 0x1234567890abcdef...")
// text: qsTr("Hash: 0x1234567890abcdef...")
// TODO: truncate the hash to 8 characters
text: qsTr("Hash: %1").arg(optionsModel.snapshotBlockHash)
font.pixelSize: 14
}
}
Expand Down
Loading

0 comments on commit e3f8c70

Please sign in to comment.