From ab5635c3d139b7173ccc54c944b6e3c82ec6dce5 Mon Sep 17 00:00:00 2001 From: D33r-Gee Date: Wed, 2 Oct 2024 14:41:51 -0700 Subject: [PATCH 1/3] qml: Introduce UI Flow for Loading Snapshot This introduce the UI flow to load a AssumeUTXO snapshot into the Bitcoin Core App. It modifies the connection seetings and adds a SnapshotSettings file, Icon, and modified profress bar. It has been rebased --- src/Makefile.qt.include | 5 + src/qml/bitcoin_qml.qrc | 6 + src/qml/components/ConnectionSettings.qml | 28 +++ src/qml/components/SnapshotSettings.qml | 204 ++++++++++++++++++ src/qml/controls/GreenCheckIcon.qml | 11 + src/qml/controls/Header.qml | 2 + src/qml/controls/ProgressIndicator.qml | 3 +- src/qml/controls/Theme.qml | 3 + src/qml/imageprovider.cpp | 15 ++ src/qml/pages/settings/SettingsConnection.qml | 4 + src/qml/pages/settings/SettingsSnapshot.qml | 37 ++++ src/qml/res/icons/circle-file.png | Bin 0 -> 1391 bytes src/qml/res/icons/circle-green-check.png | Bin 0 -> 1503 bytes src/qml/res/icons/green-check.png | Bin 0 -> 788 bytes src/qml/res/src/circle-file.svg | 18 ++ src/qml/res/src/circle-green-check.svg | 18 ++ src/qml/res/src/green-check.svg | 9 + 17 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 src/qml/components/SnapshotSettings.qml create mode 100644 src/qml/controls/GreenCheckIcon.qml create mode 100644 src/qml/pages/settings/SettingsSnapshot.qml create mode 100644 src/qml/res/icons/circle-file.png create mode 100644 src/qml/res/icons/circle-green-check.png create mode 100644 src/qml/res/icons/green-check.png create mode 100644 src/qml/res/src/circle-file.svg create mode 100644 src/qml/res/src/circle-green-check.svg create mode 100644 src/qml/res/src/green-check.svg diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index f2f6e31131..94452aa7b2 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -360,12 +360,15 @@ QML_RES_ICONS = \ qml/res/icons/check.png \ qml/res/icons/copy.png \ qml/res/icons/coinbase.png \ + qml/res/icons/circle-file.png \ + qml/res/icons/circle-green-check.png \ qml/res/icons/cross.png \ qml/res/icons/error.png \ qml/res/icons/export.png \ qml/res/icons/flip-vertical.png \ qml/res/icons/gear.png \ qml/res/icons/gear-outline.png \ + qml/res/icons/green-check.png \ qml/res/icons/hidden.png \ qml/res/icons/info.png \ qml/res/icons/minus.png \ @@ -400,6 +403,7 @@ QML_RES_QML = \ qml/components/NetworkIndicator.qml \ qml/components/ProxySettings.qml \ qml/components/Separator.qml \ + qml/components/SnapshotSettings.qml \ qml/components/StorageLocations.qml \ qml/components/StorageOptions.qml \ qml/components/StorageSettings.qml \ @@ -457,6 +461,7 @@ QML_RES_QML = \ qml/pages/settings/SettingsDeveloper.qml \ qml/pages/settings/SettingsDisplay.qml \ qml/pages/settings/SettingsProxy.qml \ + qml/pages/settings/SettingsSnapshot.qml \ qml/pages/settings/SettingsStorage.qml \ qml/pages/settings/SettingsTheme.qml \ qml/pages/wallet/Activity.qml \ diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index 69a07de568..877469e933 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -15,6 +15,7 @@ components/ProxySettings.qml components/StorageLocations.qml components/Separator.qml + components/SnapshotSettings.qml components/StorageOptions.qml components/StorageSettings.qml components/ThemeSettings.qml @@ -26,6 +27,7 @@ controls/CoreTextField.qml controls/ExternalLink.qml controls/FocusBorder.qml + controls/GreenCheckIcon.qml controls/Header.qml controls/Icon.qml controls/InformationPage.qml @@ -70,6 +72,7 @@ pages/settings/SettingsDeveloper.qml pages/settings/SettingsDisplay.qml pages/settings/SettingsProxy.qml + pages/settings/SettingsSnapshot.qml pages/settings/SettingsStorage.qml pages/settings/SettingsTheme.qml pages/wallet/Activity.qml @@ -103,12 +106,15 @@ res/icons/check.png res/icons/copy.png res/icons/coinbase.png + res/icons/circle-file.png + res/icons/circle-green-check.png res/icons/cross.png res/icons/error.png res/icons/export.png res/icons/flip-vertical.png res/icons/gear.png res/icons/gear-outline.png + res/icons/green-check.png res/icons/hidden.png res/icons/info.png res/icons/minus.png diff --git a/src/qml/components/ConnectionSettings.qml b/src/qml/components/ConnectionSettings.qml index 35e78b7a6e..2362ea12e2 100644 --- a/src/qml/components/ConnectionSettings.qml +++ b/src/qml/components/ConnectionSettings.qml @@ -10,7 +10,35 @@ import "../controls" ColumnLayout { id: root signal next + signal gotoSnapshot + property bool snapshotImported: false + function setSnapshotImported(imported) { + snapshotImported = imported + } spacing: 4 + Setting { + id: gotoSnapshot + 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 + } + GreenCheckIcon { + anchors.centerIn: parent + visible: snapshotImported + color: Theme.color.transparent + size: 30 + } + } + onClicked: root.gotoSnapshot() + } + Separator { 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 new file mode 100644 index 0000000000..ebac415b60 --- /dev/null +++ b/src/qml/components/SnapshotSettings.qml @@ -0,0 +1,204 @@ +// Copyright (c) 2023-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import "../controls" + +ColumnLayout { + signal snapshotImportCompleted() + property int snapshotVerificationCycles: 0 + property real snapshotVerificationProgress: 0 + property bool snapshotVerified: false + + id: columnLayout + width: Math.min(parent.width, 450) + anchors.horizontalCenter: parent.horizontalCenter + + + Timer { + id: snapshotSimulationTimer + interval: 50 // Update every 50ms + running: false + repeat: true + onTriggered: { + if (snapshotVerificationProgress < 1) { + snapshotVerificationProgress += 0.01 + } else { + snapshotVerificationCycles++ + if (snapshotVerificationCycles < 1) { + snapshotVerificationProgress = 0 + } else { + running = false + snapshotVerified = true + settingsStack.currentIndex = 2 + } + } + } + } + + StackLayout { + id: settingsStack + currentIndex: 0 + + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(parent.width, 450) + + Image { + Layout.alignment: Qt.AlignCenter + source: "image://images/circle-file" + + sourceSize.width: 200 + sourceSize.height: 200 + } + + Header { + Layout.fillWidth: true + Layout.topMargin: 20 + headerBold: true + header: qsTr("Load snapshot") + descriptionBold: false + descriptionColor: Theme.color.neutral6 + descriptionSize: 17 + descriptionLineHeight: 1.1 + description: qsTr("You can start using the application more quickly by loading a recent transaction snapshot." + + " It will be automatically verified in the background.") + } + + ContinueButton { + Layout.preferredWidth: Math.min(300, columnLayout.width - 2 * Layout.leftMargin) + Layout.topMargin: 40 + Layout.leftMargin: 20 + Layout.rightMargin: Layout.leftMargin + Layout.bottomMargin: 20 + Layout.alignment: Qt.AlignCenter + text: qsTr("Choose snapshot file") + onClicked: { + settingsStack.currentIndex = 1 + snapshotSimulationTimer.start() + } + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(parent.width, 450) + + Image { + Layout.alignment: Qt.AlignCenter + source: "image://images/circle-file" + + sourceSize.width: 200 + sourceSize.height: 200 + } + + Header { + Layout.fillWidth: true + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + header: qsTr("Loading Snapshot") + } + + ProgressIndicator { + id: progressIndicator + Layout.topMargin: 20 + width: 200 + height: 20 + progress: snapshotVerificationProgress + Layout.alignment: Qt.AlignCenter + progressColor: Theme.color.blue + } + } + + ColumnLayout { + id: loadedSnapshotColumn + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(parent.width, 450) + + Image { + Layout.alignment: Qt.AlignCenter + source: "image://images/circle-green-check" + + sourceSize.width: 60 + sourceSize.height: 60 + } + + Header { + Layout.fillWidth: true + Layout.topMargin: 20 + headerBold: true + header: qsTr("Snapshot Loaded") + descriptionBold: false + 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.") + } + + ContinueButton { + Layout.preferredWidth: Math.min(300, columnLayout.width - 2 * Layout.leftMargin) + Layout.topMargin: 40 + Layout.alignment: Qt.AlignCenter + text: qsTr("Done") + onClicked: { + snapshotImportCompleted() + connectionSwipe.decrementCurrentIndex() + connectionSwipe.decrementCurrentIndex() + } + } + + Setting { + id: viewDetails + Layout.alignment: Qt.AlignCenter + header: qsTr("View details") + actionItem: CaretRightIcon { + id: caretIcon + color: viewDetails.stateColor + rotation: viewDetails.expanded ? 90 : 0 + Behavior on rotation { NumberAnimation { duration: 200 } } + } + + property bool expanded: false + + onClicked: { + expanded = !expanded + } + } + + ColumnLayout { + id: detailsContent + visible: viewDetails.expanded + Layout.preferredWidth: Math.min(300, parent.width - 2 * Layout.leftMargin) + Layout.alignment: Qt.AlignCenter + Layout.leftMargin: 80 + Layout.rightMargin: 80 + Layout.topMargin: 10 + spacing: 10 + // TODO: make sure the block height number aligns right + RowLayout { + CoreText { + text: qsTr("Block Height:") + Layout.alignment: Qt.AlignLeft + font.pixelSize: 14 + } + CoreText { + text: qsTr("200,000") + Layout.alignment: Qt.AlignRight + font.pixelSize: 14 + } + } + Separator { Layout.fillWidth: true } + CoreText { + text: qsTr("Hash: 0x1234567890abcdef...") + font.pixelSize: 14 + } + } + } + } +} diff --git a/src/qml/controls/GreenCheckIcon.qml b/src/qml/controls/GreenCheckIcon.qml new file mode 100644 index 0000000000..02977857b2 --- /dev/null +++ b/src/qml/controls/GreenCheckIcon.qml @@ -0,0 +1,11 @@ +// Copyright (c) 2023 - present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Icon { + source: "image://images/green-check" + size: 30 +} diff --git a/src/qml/controls/Header.qml b/src/qml/controls/Header.qml index f3c4c0c3e3..ece49234d2 100644 --- a/src/qml/controls/Header.qml +++ b/src/qml/controls/Header.qml @@ -25,6 +25,7 @@ ColumnLayout { property int subtextSize: 15 property color subtextColor: Theme.color.neutral9 property bool wrap: true + property real descriptionLineHeight: 1 spacing: 0 Loader { @@ -60,6 +61,7 @@ ColumnLayout { text: root.description horizontalAlignment: root.center ? Text.AlignHCenter : Text.AlignLeft wrapMode: wrap ? Text.WordWrap : Text.NoWrap + lineHeight: root.descriptionLineHeight Behavior on color { ColorAnimation { duration: 150 } diff --git a/src/qml/controls/ProgressIndicator.qml b/src/qml/controls/ProgressIndicator.qml index 117a4baebb..9d6d62d329 100644 --- a/src/qml/controls/ProgressIndicator.qml +++ b/src/qml/controls/ProgressIndicator.qml @@ -7,6 +7,7 @@ import QtQuick.Controls 2.15 Control { property real progress: 0 + property color progressColor: Theme.color.orange Behavior on progress { NumberAnimation { easing.type: Easing.Bezier @@ -26,7 +27,7 @@ Control { width: contentItem.width height: contentItem.height radius: contentItem.radius - color: Theme.color.orange + color: progressColor } } } diff --git a/src/qml/controls/Theme.qml b/src/qml/controls/Theme.qml index f57e152cbd..3c7621c2b5 100644 --- a/src/qml/controls/Theme.qml +++ b/src/qml/controls/Theme.qml @@ -27,6 +27,7 @@ Control { required property color blue required property color amber required property color purple + required property color transparent required property color neutral0 required property color neutral1 required property color neutral2 @@ -59,6 +60,7 @@ Control { blue: "#3CA3DE" amber: "#C9B500" purple: "#C075DC" + transparent: "#00000000" neutral0: "#000000" neutral1: "#1A1A1A" neutral2: "#2D2D2D" @@ -91,6 +93,7 @@ Control { blue: "#2D9CDB" amber: "#C9B500" purple: "#BB6BD9" + transparent: "#00000000" neutral0: "#FFFFFF" neutral1: "#F8F8F8" neutral2: "#F4F4F4" diff --git a/src/qml/imageprovider.cpp b/src/qml/imageprovider.cpp index aeed8a5b97..729cd82617 100644 --- a/src/qml/imageprovider.cpp +++ b/src/qml/imageprovider.cpp @@ -87,6 +87,16 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize return QIcon(":/icons/copy").pixmap(requested_size); } + if (id == "circle-file") { + *size = requested_size; + return QIcon(":/icons/circle-file").pixmap(requested_size); + } + + if (id == "circle-green-check") { + *size = requested_size; + return QIcon(":/icons/circle-green-check").pixmap(requested_size); + } + if (id == "cross") { *size = requested_size; return QIcon(":/icons/cross").pixmap(requested_size); @@ -117,6 +127,11 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize return QIcon(":/icons/gear-outline").pixmap(requested_size); } + if (id == "green-check") { + *size = requested_size; + return QIcon(":/icons/green-check").pixmap(requested_size); + } + if (id == "info") { *size = requested_size; return QIcon(":/icons/info").pixmap(requested_size); diff --git a/src/qml/pages/settings/SettingsConnection.qml b/src/qml/pages/settings/SettingsConnection.qml index d180fa2ff2..443eed72a9 100644 --- a/src/qml/pages/settings/SettingsConnection.qml +++ b/src/qml/pages/settings/SettingsConnection.qml @@ -12,6 +12,7 @@ Page { id: root signal back property bool onboarding: false + property bool snapshotImportCompleted: chainModel.isSnapshotActive background: null PageStack { id: stack @@ -31,6 +32,9 @@ Page { detailActive: true detailItem: ConnectionSettings { onNext: stack.push(proxySettings) + onGotoSnapshot: stack.push(snapshotSettings) + snapshotImportCompleted: root.snapshotImportCompleted + onboarding: root.onboarding } states: [ diff --git a/src/qml/pages/settings/SettingsSnapshot.qml b/src/qml/pages/settings/SettingsSnapshot.qml new file mode 100644 index 0000000000..e6c557a022 --- /dev/null +++ b/src/qml/pages/settings/SettingsSnapshot.qml @@ -0,0 +1,37 @@ +// Copyright (c) 2024-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import "../../controls" +import "../../components" + +Page { + signal backClicked + signal snapshotImportCompleted + + id: root + + background: null + implicitWidth: 450 + leftPadding: 20 + rightPadding: 20 + topPadding: 30 + + header: NavigationBar2 { + leftItem: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: root.backClicked() + } + } + SnapshotSettings { + width: Math.min(parent.width, 450) + anchors.horizontalCenter: parent.horizontalCenter + onSnapshotImportCompleted: { + root.snapshotImportCompleted() + } + } +} diff --git a/src/qml/res/icons/circle-file.png b/src/qml/res/icons/circle-file.png new file mode 100644 index 0000000000000000000000000000000000000000..14a776e6d5ba9ff0d34ecd7178169004c6b00147 GIT binary patch literal 1391 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r53?z4+XPOVB7>k44ofvPP)Tsw@I14-?iy0WW zg+Z8+Vb&Z8paQi3pAc7|q|M@cqX>Tp@YL`63=FBHk|4ie21X76DK!HN2VZkeSt}-K zH&+I3T`pleAs#1YJr8$zGiw`DITvMizK*;9Z-nwT-MkjkckwFg?eP1-QI8~Io*Ku! z@J@UknDXv)`Uk&yjY|*JJ^9%zAKdHjy3=T6C?cvanP_TY%D})};_2cTQZZ-i&DY){ zfg%haim%;t-@WVV)}?3Mv+THnG<6nhaB*=}PrSDLMLqW~Ay3DY+71VSCZ!2o9KkN9 z<)*FsaqH_=>$7Lx^?ly){oKsEFKcGaef?+7&As2}Ri9Tl!R*bLl@V=v=rL1dv`pdc z(y+j*JuITlo=12cgL72(y%A2?5v010TUTg?_hrXr3qJ6j@pz=x|D>^W$BUCMH-`Up zWqe)Oc;sc^?l)CJ;t|WwOtx(Z`Lz0*v9|Lo6i z4ByU2{N5>g7fUILv%jg>86T-74qsIGp>&&$UVX3o`Gp+?-au z?7O^|MJ!YQnS%d|B;PU%t3OTpeO&28S#f(z`{!+kEq8WoEMZ)8w&1q)_BrWFKU}q5 z6;EZ`WcqJL_SV|2#IW5;>Y|3p4a@C5&VKvg%0=I+!EdUTH!pI0&ahB--z(K?{#gpE z%a*Y+a@bZC^|l{Bl8|NE`^fJt)5Df|@#U^6NtZ1S?fjl1;P+@x%o|zP9UrFg9mh#b>>0D%T*ONgzqi;6=snu%VcEuMBXU*Z!ig%ov>1~fh|k3Vb+q2 z1@o3TU1V^y?G+NXk>RrlFxY31)yIXa~XB&kLJLU-MTCA@Zt2#cf&|h49 z&VfUBCLQ&CD8%sT@}13{k0wd5JpOr&{oj+@&Hg){pLMPNaJwtP%I6ZR9W5<6w4@IxH z>2$N+evs6<@1-$Y``V-W?uNPZ|MT6m(AQg*S@?gW|ND2$ztgYR2xSQ@k;q$;HutOC zcb2I>Y|%!nRe|BY1^-OvS?|F9Ll+*h~G&19)-=ttob zae5n8<(!JW7#L!DYW=P1Cb!*!Ui&`$dVKFw@cQfW{~2_6dDogo-qQl+b_P#ZKbLh* G2~7YIypH<- literal 0 HcmV?d00001 diff --git a/src/qml/res/icons/circle-green-check.png b/src/qml/res/icons/circle-green-check.png new file mode 100644 index 0000000000000000000000000000000000000000..25bb20e00fb07a7e6e6d80cf2474744f35e14d2a GIT binary patch literal 1503 zcmc&!{WsHl9RJSDmT;$WH{_u(Z1#XqtgvWXY#uHNbN9q1hH2_jx|z6C=a$n=r7T@! zPT7-ZDb=~`94YInRGd!54T(JVgyw$zeE)!Z&inm&zu%wN`}Kak&*z8F$tV`h+|Cu7{Ebu7OvlxT&}VO)6QFhE;OiRXh-KQ*ykT(>&Dzf>r?b_%w;IDiGp= zIoRN);@~v`clCPdr4bP|DOm7EokG$GaVoxd^8t4y*F()EH3_jw4%qLd6?v%m;7qbs zk#S;w=joce0p2L&#dJoF17Vc#}yrOkDu*!yS-5MfRQXU zlndLV-wg=n-f$bg@JBPVzh4^s8uj)iE3(5NuUirQAYZ*^@@?O%oE0%|v~GvDmz6ci zMy6-J1^{RQBQzv7ud21OKLZtQRjOww7|g*JEc0OnGcifI_a-daDkL<-Ka1Ugv@FJr zLeV$_ya-X_j)1{nf2xcv*&UVzo;~Q;@YbQBePUncd!J+D0e=VwDbTK`F$_v^^Ze>KWZ$Senic=Dc!njuchFV$2#Rns49e2y9U)b!lJZZC zMA?n%wZrDHb3fL=%0AGF^MMTb!(X#Z_%jj*O5vf}`gnm6-=!2SVN@n|>a3lP?95*P za5`_j7c^F6^8^tW9&+LmWN7W+edyd75d_MJrxbS9!OA}ECd%UO^q7e;xqZ+EL%NZl zA@>oq;e8+Q47L(MB3>tF0sa>BH;|`?6|$+RRVp!QzZJ*i!Xx@y$fUHwBv!F_cE?Q# z7A9WaauSaGRs&5oktbB#v5hhpDoR$Zub!&itniC{azfJ?xs|+Z!|VS=E0~(UvdB$F z-*|s*JkFWlzL@rxlW71G#YbmSimoh|6ed{){jJN0o|d;fR4oVlmhnf9LVm`NCf(|_ zSUT;}CGVI!lNDVWEt~EpL@)owE7~85A5o@b??3mnD>}-_f%@l#bDG4%#)Vx-4*B=t zhW6WbV|Z3Oe16ADWASp#=L7f#k!}FeqYyg{z7pQ`W*buS7oA20evwkUTLOQYcDA-V zhu-yqw)x&n2)cJ0Uu-$Y6tV34+l z5K`Jb^Uj;qh4Z;OpK9;0uGQ3%D%<%y9A)jvpqVCR$W`y3l|Z?5cCzsuAz0F9t#1S1 ztyImnw#n%@K{yNL-1?y3WN&tJ+6a2#it7N|$9m(12YWK_R>Ei5rJdg@o<$@%e|+=I zYeIOrzTe+Ce?q(eQayKC~1-RZOtA@ zi0}9wV2!3nOgKnX8a(ZkyndTwPB(NI7e=5L&)D0SAUPjZJ5iTxBN+_haYKjI2ktS$ ZS2(F+W_HU!{3!Sz0E{qJsEW!h`WFKci5vg` literal 0 HcmV?d00001 diff --git a/src/qml/res/icons/green-check.png b/src/qml/res/icons/green-check.png new file mode 100644 index 0000000000000000000000000000000000000000..acce5e1a21539646b08354584cf7a08688613dc9 GIT binary patch literal 788 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX3?zBp#Z3TGjKx9jPK-BC>eK@{oCO|{#S9GG z!XV7ZFl&wkQ1EkrPlzj!R$rGu71VusRsPAT5Ti1VP5A%+|E{NJfLxQ!nQwl4{Pp*@ z>&~+E507+QS^*SHINWvd-Cd1!i9q&?Z|{KQng@q^uB=Kr(lh7QHm&tZiATB*y||>m zG0kXG#X_$t zR1`ep{QsZv5zq$@ffxu50P&6gAHZxJM9R#Vmsz$mE-%}E92oeqo-U3d6?1wg-}h#6 z6gd8zJ9c?oP*z;4MpT&F*9+MKBKtxw$lvCFp{T`C)y=`Ayy#F@P~gc=R;t@Rzx$T= zzWu`Y)bwYY&CPdPGZ(cqb{LvnDo*~U#Xn*9#`WD6c`P1NmUzpG`+qs1zw>CJP*j7E zYvkoalkW<6iDb{P_$%wxc`S=O)Q@;bvVc--Hyl55YFNq<(UtjkvIfmH0b{ypdE2uD-F|ZGxftH+D|9_}Y@73EaR~ OWbkzLb6Mw<&;$U4l7p@Q literal 0 HcmV?d00001 diff --git a/src/qml/res/src/circle-file.svg b/src/qml/res/src/circle-file.svg new file mode 100644 index 0000000000..d8af3949d8 --- /dev/null +++ b/src/qml/res/src/circle-file.svg @@ -0,0 +1,18 @@ + + circle-file-svg + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/qml/res/src/circle-green-check.svg b/src/qml/res/src/circle-green-check.svg new file mode 100644 index 0000000000..d56c175fd4 --- /dev/null +++ b/src/qml/res/src/circle-green-check.svg @@ -0,0 +1,18 @@ + + Circle arrow up-svg + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/qml/res/src/green-check.svg b/src/qml/res/src/green-check.svg new file mode 100644 index 0000000000..95f7c80ead --- /dev/null +++ b/src/qml/res/src/green-check.svg @@ -0,0 +1,9 @@ + + green-check-svg + + + + \ No newline at end of file From b720cded9a96a6b8c20154781555fcab27b2b737 Mon Sep 17 00:00:00 2001 From: D33r-Gee Date: Fri, 31 Jan 2025 10:41:24 -0800 Subject: [PATCH 2/3] cpp: Implement UTXO snapshot loading interface and progress tracking - Extend node interface with virtual functions for UTXO snapshot loading - Add signal mechanism to monitor snapshot loading progress - Include predefined signet UTXO dataset in chainparams for validation --- src/Makefile.qt.include | 3 + src/interfaces/node.h | 8 ++ src/kernel/chainparams.cpp | 7 ++ src/kernel/notifications_interface.h | 1 + src/node/interface_ui.cpp | 3 + src/node/interface_ui.h | 3 + src/node/interfaces.cpp | 11 +- src/node/kernel_notifications.cpp | 5 + src/node/kernel_notifications.h | 2 + src/qml/components/ConnectionSettings.qml | 17 +-- src/qml/components/SnapshotSettings.qml | 107 +++++++++++------- src/qml/models/chainmodel.cpp | 30 +++++ src/qml/models/chainmodel.h | 6 +- src/qml/models/nodemodel.cpp | 58 ++++++++++ src/qml/models/nodemodel.h | 29 ++++- src/qml/models/snapshotqml.cpp | 40 +++++++ src/qml/models/snapshotqml.h | 26 +++++ src/qml/pages/settings/SettingsConnection.qml | 10 +- src/qml/pages/settings/SettingsSnapshot.qml | 13 ++- src/validation.cpp | 4 + 20 files changed, 322 insertions(+), 61 deletions(-) create mode 100644 src/qml/models/snapshotqml.cpp create mode 100644 src/qml/models/snapshotqml.h diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 94452aa7b2..b48db705a7 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -46,6 +46,7 @@ QT_MOC_CPP = \ qml/models/moc_peerlistsortproxy.cpp \ qml/models/moc_transaction.cpp \ qml/models/moc_sendrecipient.cpp \ + qml/models/moc_snapshotqml.cpp \ qml/models/moc_walletlistmodel.cpp \ qml/models/moc_walletqmlmodel.cpp \ qml/models/moc_walletqmlmodel.cpp \ @@ -136,6 +137,7 @@ BITCOIN_QT_H = \ qml/models/peerlistsortproxy.h \ qml/models/transaction.h \ qml/models/sendrecipient.h \ + qml/models/snapshotqml.h \ qml/models/walletlistmodel.h \ qml/models/walletqmlmodel.h \ qml/models/walletqmlmodeltransaction.h \ @@ -335,6 +337,7 @@ BITCOIN_QML_BASE_CPP = \ qml/models/peerlistsortproxy.cpp \ qml/models/transaction.cpp \ qml/models/sendrecipient.cpp \ + qml/models/snapshotqml.cpp \ qml/models/walletlistmodel.cpp \ qml/models/walletqmlmodel.cpp \ qml/models/walletqmlmodeltransaction.cpp \ diff --git a/src/interfaces/node.h b/src/interfaces/node.h index 1aab36927b..b50643db22 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -11,6 +11,7 @@ #include // For banmap_t #include // For Network #include // For ConnectionDirection +#include // For SnapshotMetadata #include // For SecureString #include @@ -208,6 +209,9 @@ class Node //! List rpc commands. virtual std::vector listRpcCommands() = 0; + //! Load UTXO Snapshot. + virtual bool loadSnapshot(AutoFile& afile, const node::SnapshotMetadata& metadata, bool in_memory) = 0; + //! Set RPC timer interface if unset. virtual void rpcSetTimerInterfaceIfUnset(RPCTimerInterface* iface) = 0; @@ -243,6 +247,10 @@ class Node using ShowProgressFn = std::function; virtual std::unique_ptr handleShowProgress(ShowProgressFn fn) = 0; + //! Register handler for snapshot load progress. + using SnapshotLoadProgressFn = std::function; + virtual std::unique_ptr handleSnapshotLoadProgress(SnapshotLoadProgressFn fn) = 0; + //! Register handler for wallet loader constructed messages. using InitWalletFn = std::function; virtual std::unique_ptr handleInitWallet(InitWalletFn fn) = 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/kernel/notifications_interface.h b/src/kernel/notifications_interface.h index c5e77b0df9..b8002dedd2 100644 --- a/src/kernel/notifications_interface.h +++ b/src/kernel/notifications_interface.h @@ -40,6 +40,7 @@ class Notifications [[nodiscard]] virtual InterruptResult blockTip(SynchronizationState state, CBlockIndex& index) { return {}; } virtual void headerTip(SynchronizationState state, int64_t height, int64_t timestamp, bool presync) {} virtual void progress(const bilingual_str& title, int progress_percent, bool resume_possible) {} + virtual void snapshotLoadProgress(double progress) {} virtual void warning(const bilingual_str& warning) {} //! The flush error notification is sent to notify the user that an error diff --git a/src/node/interface_ui.cpp b/src/node/interface_ui.cpp index a1664f1bad..a57a559146 100644 --- a/src/node/interface_ui.cpp +++ b/src/node/interface_ui.cpp @@ -21,6 +21,7 @@ struct UISignals { boost::signals2::signal NotifyNetworkActiveChanged; boost::signals2::signal NotifyAlertChanged; boost::signals2::signal ShowProgress; + boost::signals2::signal SnapshotLoadProgress; boost::signals2::signal NotifyBlockTip; boost::signals2::signal NotifyHeaderTip; boost::signals2::signal BannedListChanged; @@ -44,6 +45,7 @@ ADD_SIGNALS_IMPL_WRAPPER(ShowProgress); ADD_SIGNALS_IMPL_WRAPPER(NotifyBlockTip); ADD_SIGNALS_IMPL_WRAPPER(NotifyHeaderTip); ADD_SIGNALS_IMPL_WRAPPER(BannedListChanged); +ADD_SIGNALS_IMPL_WRAPPER(SnapshotLoadProgress); bool CClientUIInterface::ThreadSafeMessageBox(const bilingual_str& message, const std::string& caption, unsigned int style) { return g_ui_signals.ThreadSafeMessageBox(message, caption, style).value_or(false);} bool CClientUIInterface::ThreadSafeQuestion(const bilingual_str& message, const std::string& non_interactive_message, const std::string& caption, unsigned int style) { return g_ui_signals.ThreadSafeQuestion(message, non_interactive_message, caption, style).value_or(false);} @@ -53,6 +55,7 @@ void CClientUIInterface::NotifyNumConnectionsChanged(PeersNumByType newNumConnec void CClientUIInterface::NotifyNetworkActiveChanged(bool networkActive) { return g_ui_signals.NotifyNetworkActiveChanged(networkActive); } void CClientUIInterface::NotifyAlertChanged() { return g_ui_signals.NotifyAlertChanged(); } void CClientUIInterface::ShowProgress(const std::string& title, int nProgress, bool resume_possible) { return g_ui_signals.ShowProgress(title, nProgress, resume_possible); } +void CClientUIInterface::SnapshotLoadProgress(double progress) { return g_ui_signals.SnapshotLoadProgress(progress); } void CClientUIInterface::NotifyBlockTip(SynchronizationState s, const CBlockIndex* i) { return g_ui_signals.NotifyBlockTip(s, i); } void CClientUIInterface::NotifyHeaderTip(SynchronizationState s, int64_t height, int64_t timestamp, bool presync) { return g_ui_signals.NotifyHeaderTip(s, height, timestamp, presync); } void CClientUIInterface::BannedListChanged() { return g_ui_signals.BannedListChanged(); } diff --git a/src/node/interface_ui.h b/src/node/interface_ui.h index cb5a9cbafd..14d3761898 100644 --- a/src/node/interface_ui.h +++ b/src/node/interface_ui.h @@ -109,6 +109,9 @@ class CClientUIInterface */ ADD_SIGNALS_DECL_WRAPPER(ShowProgress, void, const std::string& title, int nProgress, bool resume_possible); + /** Snapshot load progress. */ + ADD_SIGNALS_DECL_WRAPPER(SnapshotLoadProgress, void, double progress); + /** New block has been accepted */ ADD_SIGNALS_DECL_WRAPPER(NotifyBlockTip, void, SynchronizationState, const CBlockIndex*); diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 3dd867cb8d..4d88822a8a 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -378,6 +379,10 @@ class NodeImpl : public Node { return MakeSignalHandler(::uiInterface.ShowProgress_connect(fn)); } + std::unique_ptr handleSnapshotLoadProgress(SnapshotLoadProgressFn fn) override + { + return MakeSignalHandler(::uiInterface.SnapshotLoadProgress_connect(fn)); + } std::unique_ptr handleInitWallet(InitWalletFn fn) override { return MakeSignalHandler(::uiInterface.InitWallet_connect(fn)); @@ -417,6 +422,10 @@ class NodeImpl : public Node { m_context = context; } + bool loadSnapshot(AutoFile& afile, const node::SnapshotMetadata& metadata, bool in_memory) override + { + return chainman().ActivateSnapshot(afile, metadata, in_memory); + } ArgsManager& args() { return *Assert(Assert(m_context)->args); } ChainstateManager& chainman() { return *Assert(m_context->chainman); } NodeContext* m_context{nullptr}; @@ -532,7 +541,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/node/kernel_notifications.cpp b/src/node/kernel_notifications.cpp index 7224127c72..e41308dd50 100644 --- a/src/node/kernel_notifications.cpp +++ b/src/node/kernel_notifications.cpp @@ -78,6 +78,11 @@ void KernelNotifications::progress(const bilingual_str& title, int progress_perc uiInterface.ShowProgress(title.translated, progress_percent, resume_possible); } +void KernelNotifications::snapshotLoadProgress(double progress) +{ + uiInterface.SnapshotLoadProgress(progress); +} + void KernelNotifications::warning(const bilingual_str& warning) { DoWarning(warning); diff --git a/src/node/kernel_notifications.h b/src/node/kernel_notifications.h index b2dfc03398..4e9b9f77db 100644 --- a/src/node/kernel_notifications.h +++ b/src/node/kernel_notifications.h @@ -31,6 +31,8 @@ class KernelNotifications : public kernel::Notifications void progress(const bilingual_str& title, int progress_percent, bool resume_possible) override; + void snapshotLoadProgress(double progress) override; + void warning(const bilingual_str& warning) override; void flushError(const std::string& debug_message) override; diff --git a/src/qml/components/ConnectionSettings.qml b/src/qml/components/ConnectionSettings.qml index 2362ea12e2..5140b42e73 100644 --- a/src/qml/components/ConnectionSettings.qml +++ b/src/qml/components/ConnectionSettings.qml @@ -11,13 +11,13 @@ ColumnLayout { id: root signal next signal gotoSnapshot - property bool snapshotImported: false - function setSnapshotImported(imported) { - snapshotImported = imported - } + property bool snapshotImportCompleted: onboarding ? false : chainModel.isSnapshotActive + property bool onboarding: false + spacing: 4 Setting { id: gotoSnapshot + visible: !root.onboarding Layout.fillWidth: true header: qsTr("Load snapshot") description: qsTr("Instant use with background sync") @@ -26,19 +26,22 @@ ColumnLayout { height: 26 CaretRightIcon { anchors.centerIn: parent - visible: !snapshotImported + visible: !snapshotImportCompleted color: gotoSnapshot.stateColor } GreenCheckIcon { anchors.centerIn: parent - visible: snapshotImported + visible: snapshotImportCompleted color: Theme.color.transparent size: 30 } } onClicked: root.gotoSnapshot() } - 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..1c73f8af18 100644 --- a/src/qml/components/SnapshotSettings.qml +++ b/src/qml/components/SnapshotSettings.qml @@ -5,44 +5,29 @@ 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 - id: columnLayout + signal back + property bool snapshotLoading: nodeModel.snapshotLoading + property bool snapshotLoaded: nodeModel.isSnapshotLoaded + property bool snapshotImportCompleted: onboarding ? false : chainModel.isSnapshotActive + property bool onboarding: false + property bool snapshotVerified: onboarding ? false : chainModel.isSnapshotActive + property string snapshotFileName: "" + property var snapshotInfo: (snapshotVerified || snapshotLoaded) ? chainModel.getSnapshotInfo() : ({}) + property string selectedFile: "" + property bool headersSynced: nodeModel.headersSynced + width: Math.min(parent.width, 450) anchors.horizontalCenter: parent.horizontalCenter - - Timer { - id: snapshotSimulationTimer - interval: 50 // Update every 50ms - running: false - repeat: true - onTriggered: { - if (snapshotVerificationProgress < 1) { - snapshotVerificationProgress += 0.01 - } else { - snapshotVerificationCycles++ - if (snapshotVerificationCycles < 1) { - snapshotVerificationProgress = 0 - } else { - running = false - snapshotVerified = true - settingsStack.currentIndex = 2 - } - } - } - } - StackLayout { id: settingsStack - currentIndex: 0 + currentIndex: onboarding ? 0 : snapshotLoaded ? 2 : snapshotVerified ? 2 : snapshotLoading ? 1 : 0 ColumnLayout { Layout.alignment: Qt.AlignHCenter @@ -69,6 +54,19 @@ ColumnLayout { " It will be automatically verified in the background.") } + CoreText { + Layout.fillWidth: true + Layout.topMargin: 20 + color: Theme.color.neutral6 + font.pixelSize: 17 + visible: !headersSynced + text: !headersSynced + ? qsTr("Please wait for headers to sync before loading a snapshot.") + : qsTr("") + wrap: true + wrapMode: Text.WordWrap + } + ContinueButton { Layout.preferredWidth: Math.min(300, columnLayout.width - 2 * Layout.leftMargin) Layout.topMargin: 40 @@ -77,11 +75,23 @@ ColumnLayout { Layout.bottomMargin: 20 Layout.alignment: Qt.AlignCenter text: qsTr("Choose snapshot file") - onClicked: { - settingsStack.currentIndex = 1 - snapshotSimulationTimer.start() + enabled: headersSynced + onClicked: fileDialog.open() + } + + FileDialog { + id: fileDialog + folder: shortcuts.home + selectMultiple: false + selectExisting: true + nameFilters: ["Snapshot files (*.dat)", "All files (*)"] + onAccepted: { + selectedFile = fileUrl.toString() + snapshotFileName = selectedFile + nodeModel.snapshotLoadThread(snapshotFileName) } } + // TODO: Handle file error signal } ColumnLayout { @@ -102,6 +112,7 @@ ColumnLayout { Layout.leftMargin: 20 Layout.rightMargin: 20 header: qsTr("Loading Snapshot") + description: qsTr("This might take a while...") } ProgressIndicator { @@ -109,7 +120,7 @@ ColumnLayout { Layout.topMargin: 20 width: 200 height: 20 - progress: snapshotVerificationProgress + progress: nodeModel.snapshotProgress Layout.alignment: Qt.AlignCenter progressColor: Theme.color.blue } @@ -137,8 +148,10 @@ 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 unspent transactions up to %1. Next, transactions will be verified and newer transactions downloaded.").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 { @@ -147,9 +160,8 @@ ColumnLayout { Layout.alignment: Qt.AlignCenter text: qsTr("Done") onClicked: { - snapshotImportCompleted() - connectionSwipe.decrementCurrentIndex() - connectionSwipe.decrementCurrentIndex() + chainModel.isSnapshotActiveChanged() + back() } } @@ -188,15 +200,28 @@ 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...") - font.pixelSize: 14 + ColumnLayout { + Layout.fillWidth: true + spacing: 5 + CoreText { + text: qsTr("Hash:") + font.pixelSize: 14 + } + CoreText { + text: snapshotInfo && snapshotInfo["hashSerializedFirstHalf"] ? + snapshotInfo["hashSerializedFirstHalf"] + "\n" + snapshotInfo["hashSerializedSecondHalf"] : + qsTr("DEBUG") + Layout.fillWidth: true + font.pixelSize: 14 + textFormat: Text.PlainText + } } } } diff --git a/src/qml/models/chainmodel.cpp b/src/qml/models/chainmodel.cpp index aeffe99599..978046ebff 100644 --- a/src/qml/models/chainmodel.cpp +++ b/src/qml/models/chainmodel.cpp @@ -9,9 +9,13 @@ #include #include #include +#include +#include +#include ChainModel::ChainModel(interfaces::Chain& chain) : m_chain{chain} + // m_params{Params()} { QTimer* timer = new QTimer(); connect(timer, &QTimer::timeout, this, &ChainModel::setCurrentTimeRatio); @@ -101,3 +105,29 @@ void ChainModel::setCurrentTimeRatio() Q_EMIT timeRatioListChanged(); } + +// TODO: Change this once a better solution has been found. +// 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); + + QString fullHash = QString::fromStdString(hash_serialized.ToString()); + + int midPoint = fullHash.length() / 2; + QString firstHalf = fullHash.left(midPoint); + QString secondHalf = fullHash.mid(midPoint); + + snapshot_info["height"] = height; + snapshot_info["hashSerializedFirstHalf"] = firstHalf; + snapshot_info["hashSerializedSecondHalf"] = secondHalf; + snapshot_info["date"] = QDateTime::fromSecsSinceEpoch(date).toString("MMMM d yyyy"); + } + + return snapshot_info; +} diff --git a/src/qml/models/chainmodel.h b/src/qml/models/chainmodel.h index 9318510eda..6a5124be7f 100644 --- a/src/qml/models/chainmodel.h +++ b/src/qml/models/chainmodel.h @@ -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); @@ -36,11 +37,13 @@ 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(); @@ -48,6 +51,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 8ccc532016..2078650032 100644 --- a/src/qml/models/nodemodel.cpp +++ b/src/qml/models/nodemodel.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include @@ -14,14 +15,19 @@ #include #include +#include #include #include +#include +#include +#include NodeModel::NodeModel(interfaces::Node& node) : m_node{node} { ConnectToBlockTipSignal(); ConnectToNumConnectionsChangedSignal(); + ConnectToSnapshotLoadProgressSignal(); } void NodeModel::setBlockTipHeight(int new_height) @@ -80,6 +86,10 @@ void NodeModel::setVerificationProgress(double new_progress) if (new_progress != m_verification_progress) { setRemainingSyncTime(new_progress); + if (new_progress >= 0.00001) { + setHeadersSynced(true); + } + m_verification_progress = new_progress; Q_EMIT verificationProgressChanged(); } @@ -176,3 +186,51 @@ QString NodeModel::defaultProxyAddress() { return QString::fromStdString(m_node.defaultProxyAddress()); } + +void NodeModel::ConnectToSnapshotLoadProgressSignal() +{ + assert(!m_handler_snapshot_load_progress); + + m_handler_snapshot_load_progress = m_node.handleSnapshotLoadProgress( + [this](double progress) { + setSnapshotProgress(progress); + }); +} + +void NodeModel::snapshotLoadThread(QString path_file) { + m_snapshot_loading = true; + Q_EMIT snapshotLoadingChanged(); + + path_file = QUrl(path_file).toLocalFile(); + + QThread* snapshot_thread = QThread::create([this, path_file]() { + SnapshotQml loader(m_node, path_file); + bool result = loader.processPath(); + if (!result) { + m_snapshot_loading = false; + Q_EMIT snapshotLoadingChanged(); + } else { + m_snapshot_loaded = true; + Q_EMIT snapshotLoaded(result); + Q_EMIT snapshotLoadingChanged(); + } + }); + + connect(snapshot_thread, &QThread::finished, snapshot_thread, &QThread::deleteLater); + + snapshot_thread->start(); +} + +void NodeModel::setSnapshotProgress(double new_progress) { + if (new_progress != m_snapshot_progress) { + m_snapshot_progress = new_progress; + Q_EMIT snapshotProgressChanged(); + } +} + +void NodeModel::setHeadersSynced(bool new_synced) { + if (new_synced != m_headers_synced) { + m_headers_synced = new_synced; + Q_EMIT headersSyncedChanged(); + } +} diff --git a/src/qml/models/nodemodel.h b/src/qml/models/nodemodel.h index 8603c44d36..8832eeab27 100644 --- a/src/qml/models/nodemodel.h +++ b/src/qml/models/nodemodel.h @@ -34,6 +34,10 @@ 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 WRITE setSnapshotProgress NOTIFY snapshotProgressChanged) + Q_PROPERTY(bool snapshotLoading READ snapshotLoading NOTIFY snapshotLoadingChanged) + Q_PROPERTY(bool isSnapshotLoaded READ isSnapshotLoaded NOTIFY snapshotLoaded) + Q_PROPERTY(bool headersSynced READ headersSynced WRITE setHeadersSynced NOTIFY headersSyncedChanged) public: explicit NodeModel(interfaces::Node& node); @@ -52,6 +56,12 @@ class NodeModel : public QObject void setPause(bool new_pause); bool errorState() const { return m_faulted; } void setErrorState(bool new_error); + bool isSnapshotLoaded() const { return m_snapshot_loaded; } + double snapshotProgress() const { return m_snapshot_progress; } + void setSnapshotProgress(double new_progress); + bool snapshotLoading() const { return m_snapshot_loading; } + bool headersSynced() const { return m_headers_synced; } + void setHeadersSynced(bool new_synced); Q_INVOKABLE float getTotalBytesReceived() const { return (float)m_node.getTotalBytesRecv(); } Q_INVOKABLE float getTotalBytesSent() const { return (float)m_node.getTotalBytesSent(); } @@ -59,6 +69,8 @@ class NodeModel : public QObject Q_INVOKABLE void startNodeInitializionThread(); Q_INVOKABLE void requestShutdown(); + Q_INVOKABLE void snapshotLoadThread(QString path_file); + void startShutdownPolling(); void stopShutdownPolling(); @@ -80,7 +92,12 @@ public Q_SLOTS: void setTimeRatioList(int new_time); void setTimeRatioListInitial(); - + void initializationFinished(); + void snapshotLoaded(bool result); + void snapshotProgressChanged(); + void snapshotLoadingChanged(); + void showProgress(const QString& title, int progress); + void headersSyncedChanged(); protected: void timerEvent(QTimerEvent* event) override; @@ -93,17 +110,21 @@ 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}; - + int m_snapshot_timer_id{0}; + bool m_snapshot_loading{false}; + bool m_snapshot_loaded{false}; + bool m_headers_synced{false}; QVector> m_block_process_time; interfaces::Node& m_node; std::unique_ptr m_handler_notify_block_tip; std::unique_ptr m_handler_notify_num_peers_changed; - + std::unique_ptr m_handler_snapshot_load_progress; void ConnectToBlockTipSignal(); void ConnectToNumConnectionsChangedSignal(); + void ConnectToSnapshotLoadProgressSignal(); }; #endif // BITCOIN_QML_MODELS_NODEMODEL_H diff --git a/src/qml/models/snapshotqml.cpp b/src/qml/models/snapshotqml.cpp new file mode 100644 index 0000000000..6265aba417 --- /dev/null +++ b/src/qml/models/snapshotqml.cpp @@ -0,0 +1,40 @@ +// Copyright (c) 2024 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include + +SnapshotQml::SnapshotQml(interfaces::Node& node, QString path) + : m_node(node), m_path(path) {} + +bool SnapshotQml::processPath() +{ + const fs::path path = fs::u8path(m_path.toStdString()); + if (!fs::exists(path)) { + return false; + } + + FILE* snapshot_file{fsbridge::fopen(path, "rb")}; + AutoFile afile{snapshot_file}; + if (afile.IsNull()) { + return false; + } + + node::SnapshotMetadata metadata; + try { + afile >> metadata; + } catch (const std::exception& e) { + return false; + } + + bool result = m_node.loadSnapshot(afile, metadata, false); + if (!result) { + return false; + } + return true; +} \ No newline at end of file diff --git a/src/qml/models/snapshotqml.h b/src/qml/models/snapshotqml.h new file mode 100644 index 0000000000..75e4758d7e --- /dev/null +++ b/src/qml/models/snapshotqml.h @@ -0,0 +1,26 @@ +// Copyright (c) 2024 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QML_MODELS_SNAPSHOTQML_H +#define BITCOIN_QML_MODELS_SNAPSHOTQML_H + +#include +#include + +#include + +class SnapshotQml : public QObject +{ + Q_OBJECT +public: + SnapshotQml(interfaces::Node& node, QString path); + + bool processPath(); + +private: + interfaces::Node& m_node; + QString m_path; +}; + +#endif // BITCOIN_QML_MODELS_SNAPSHOTQML_H diff --git a/src/qml/pages/settings/SettingsConnection.qml b/src/qml/pages/settings/SettingsConnection.qml index 443eed72a9..da5593c58d 100644 --- a/src/qml/pages/settings/SettingsConnection.qml +++ b/src/qml/pages/settings/SettingsConnection.qml @@ -12,7 +12,7 @@ Page { id: root signal back property bool onboarding: false - property bool snapshotImportCompleted: chainModel.isSnapshotActive + property bool snapshotImportCompleted: onboarding ? false : chainModel.isSnapshotActive background: null PageStack { id: stack @@ -91,5 +91,13 @@ Page { onBack: stack.pop() } } + Component { + id: snapshotSettings + SettingsSnapshot { + onboarding: root.onboarding + snapshotImportCompleted: root.snapshotImportCompleted + onBack: stack.pop() + } + } } } diff --git a/src/qml/pages/settings/SettingsSnapshot.qml b/src/qml/pages/settings/SettingsSnapshot.qml index e6c557a022..2392dfb9c7 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 snapshotImportCompleted + signal back + property bool snapshotImportCompleted: chainModel.isSnapshotActive + property bool onboarding: false id: root @@ -24,14 +25,14 @@ 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 - onSnapshotImportCompleted: { - root.snapshotImportCompleted() - } + onboarding: root.onboarding + snapshotImportCompleted: root.snapshotImportCompleted + onBack: root.back() } } diff --git a/src/validation.cpp b/src/validation.cpp index ed9889d9dd..c67cb91178 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5261,6 +5261,10 @@ bool ChainstateManager::PopulateAndValidateSnapshot( --coins_left; ++coins_processed; + // Show Snapshot Loading progress + double progress = static_cast(coins_processed) / static_cast(coins_count); + GetNotifications().snapshotLoadProgress(progress); + if (coins_processed % 1000000 == 0) { LogPrintf("[snapshot] %d coins loaded (%.2f%%, %.2f MB)\n", coins_processed, From d7523cde16a6805cbe2689b9b7f3991539116c63 Mon Sep 17 00:00:00 2001 From: D33r-Gee Date: Tue, 4 Mar 2025 08:40:12 -0800 Subject: [PATCH 3/3] cpp: Introduce Rewind Progress handler and nofifier This introduces a Rewind Progress notification so that it is available when generating an AssumeUTXO snapshot through the UI. --- src/Makefile.qt.include | 2 + src/interfaces/node.h | 4 + src/kernel/notifications_interface.h | 2 + src/node/interface_ui.cpp | 7 +- src/node/interface_ui.h | 6 + src/node/interfaces.cpp | 4 + src/node/kernel_notifications.cpp | 5 + src/node/kernel_notifications.h | 2 + src/qml/bitcoin_qml.qrc | 2 + src/qml/components/ConnectionSettings.qml | 36 +- src/qml/components/SnapshotGenSettings.qml | 239 ++++++++++++++ src/qml/models/nodemodel.cpp | 89 ++++- src/qml/models/nodemodel.h | 43 ++- src/qml/models/snapshotqml.cpp | 307 +++++++++++++++++- src/qml/models/snapshotqml.h | 27 ++ src/qml/pages/settings/SettingsConnection.qml | 10 + .../pages/settings/SettingsSnapshotGen.qml | 40 +++ src/validation.cpp | 1 + 18 files changed, 815 insertions(+), 11 deletions(-) create mode 100644 src/qml/components/SnapshotGenSettings.qml create mode 100644 src/qml/pages/settings/SettingsSnapshotGen.qml diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index b48db705a7..751605798d 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -406,6 +406,7 @@ QML_RES_QML = \ qml/components/NetworkIndicator.qml \ qml/components/ProxySettings.qml \ qml/components/Separator.qml \ + qml/components/SnapshotGenSettings.qml \ qml/components/SnapshotSettings.qml \ qml/components/StorageLocations.qml \ qml/components/StorageOptions.qml \ @@ -465,6 +466,7 @@ QML_RES_QML = \ qml/pages/settings/SettingsDisplay.qml \ qml/pages/settings/SettingsProxy.qml \ qml/pages/settings/SettingsSnapshot.qml \ + qml/pages/settings/SettingsSnapshotGen.qml \ qml/pages/settings/SettingsStorage.qml \ qml/pages/settings/SettingsTheme.qml \ qml/pages/wallet/Activity.qml \ diff --git a/src/interfaces/node.h b/src/interfaces/node.h index b50643db22..26a1944d5f 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -251,6 +251,10 @@ class Node using SnapshotLoadProgressFn = std::function; virtual std::unique_ptr handleSnapshotLoadProgress(SnapshotLoadProgressFn fn) = 0; + //! Register handler for rewind progress messages. + using RewindProgressFn = std::function; + virtual std::unique_ptr handleRewindProgress(RewindProgressFn fn) = 0; + //! Register handler for wallet loader constructed messages. using InitWalletFn = std::function; virtual std::unique_ptr handleInitWallet(InitWalletFn fn) = 0; diff --git a/src/kernel/notifications_interface.h b/src/kernel/notifications_interface.h index b8002dedd2..7476af64ff 100644 --- a/src/kernel/notifications_interface.h +++ b/src/kernel/notifications_interface.h @@ -39,8 +39,10 @@ class Notifications [[nodiscard]] virtual InterruptResult blockTip(SynchronizationState state, CBlockIndex& index) { return {}; } virtual void headerTip(SynchronizationState state, int64_t height, int64_t timestamp, bool presync) {} + virtual void blockTipDisconnected(SynchronizationState state, CBlockIndex& index) {} virtual void progress(const bilingual_str& title, int progress_percent, bool resume_possible) {} virtual void snapshotLoadProgress(double progress) {} + virtual void rewindProgress(double progress) {} virtual void warning(const bilingual_str& warning) {} //! The flush error notification is sent to notify the user that an error diff --git a/src/node/interface_ui.cpp b/src/node/interface_ui.cpp index a57a559146..ca324adfdd 100644 --- a/src/node/interface_ui.cpp +++ b/src/node/interface_ui.cpp @@ -22,8 +22,10 @@ struct UISignals { boost::signals2::signal NotifyAlertChanged; boost::signals2::signal ShowProgress; boost::signals2::signal SnapshotLoadProgress; + boost::signals2::signal RewindProgress; boost::signals2::signal NotifyBlockTip; boost::signals2::signal NotifyHeaderTip; + boost::signals2::signal NotifyBlockDisconnected; boost::signals2::signal BannedListChanged; }; static UISignals g_ui_signals; @@ -44,9 +46,10 @@ ADD_SIGNALS_IMPL_WRAPPER(NotifyAlertChanged); ADD_SIGNALS_IMPL_WRAPPER(ShowProgress); ADD_SIGNALS_IMPL_WRAPPER(NotifyBlockTip); ADD_SIGNALS_IMPL_WRAPPER(NotifyHeaderTip); +ADD_SIGNALS_IMPL_WRAPPER(NotifyBlockDisconnected); ADD_SIGNALS_IMPL_WRAPPER(BannedListChanged); ADD_SIGNALS_IMPL_WRAPPER(SnapshotLoadProgress); - +ADD_SIGNALS_IMPL_WRAPPER(RewindProgress); bool CClientUIInterface::ThreadSafeMessageBox(const bilingual_str& message, const std::string& caption, unsigned int style) { return g_ui_signals.ThreadSafeMessageBox(message, caption, style).value_or(false);} bool CClientUIInterface::ThreadSafeQuestion(const bilingual_str& message, const std::string& non_interactive_message, const std::string& caption, unsigned int style) { return g_ui_signals.ThreadSafeQuestion(message, non_interactive_message, caption, style).value_or(false);} void CClientUIInterface::InitMessage(const std::string& message) { return g_ui_signals.InitMessage(message); } @@ -56,8 +59,10 @@ void CClientUIInterface::NotifyNetworkActiveChanged(bool networkActive) { return void CClientUIInterface::NotifyAlertChanged() { return g_ui_signals.NotifyAlertChanged(); } void CClientUIInterface::ShowProgress(const std::string& title, int nProgress, bool resume_possible) { return g_ui_signals.ShowProgress(title, nProgress, resume_possible); } void CClientUIInterface::SnapshotLoadProgress(double progress) { return g_ui_signals.SnapshotLoadProgress(progress); } +void CClientUIInterface::RewindProgress(double progress) { return g_ui_signals.RewindProgress(progress); } void CClientUIInterface::NotifyBlockTip(SynchronizationState s, const CBlockIndex* i) { return g_ui_signals.NotifyBlockTip(s, i); } void CClientUIInterface::NotifyHeaderTip(SynchronizationState s, int64_t height, int64_t timestamp, bool presync) { return g_ui_signals.NotifyHeaderTip(s, height, timestamp, presync); } +void CClientUIInterface::NotifyBlockDisconnected(SynchronizationState s, const CBlockIndex* i) { return g_ui_signals.NotifyBlockDisconnected(s, i); } void CClientUIInterface::BannedListChanged() { return g_ui_signals.BannedListChanged(); } bool InitError(const bilingual_str& str) diff --git a/src/node/interface_ui.h b/src/node/interface_ui.h index 14d3761898..2a47f51bf5 100644 --- a/src/node/interface_ui.h +++ b/src/node/interface_ui.h @@ -112,12 +112,18 @@ class CClientUIInterface /** Snapshot load progress. */ ADD_SIGNALS_DECL_WRAPPER(SnapshotLoadProgress, void, double progress); + /** Rewind progress. */ + ADD_SIGNALS_DECL_WRAPPER(RewindProgress, void, double progress); + /** New block has been accepted */ ADD_SIGNALS_DECL_WRAPPER(NotifyBlockTip, void, SynchronizationState, const CBlockIndex*); /** Best header has changed */ ADD_SIGNALS_DECL_WRAPPER(NotifyHeaderTip, void, SynchronizationState, int64_t height, int64_t timestamp, bool presync); + /** Block disconnected */ + ADD_SIGNALS_DECL_WRAPPER(NotifyBlockDisconnected, void, SynchronizationState, const CBlockIndex*); + /** Banlist did change. */ ADD_SIGNALS_DECL_WRAPPER(BannedListChanged, void, void); }; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 4d88822a8a..55c20d6937 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -383,6 +383,10 @@ class NodeImpl : public Node { return MakeSignalHandler(::uiInterface.SnapshotLoadProgress_connect(fn)); } + std::unique_ptr handleRewindProgress(RewindProgressFn fn) override + { + return MakeSignalHandler(::uiInterface.RewindProgress_connect(fn)); + } std::unique_ptr handleInitWallet(InitWalletFn fn) override { return MakeSignalHandler(::uiInterface.InitWallet_connect(fn)); diff --git a/src/node/kernel_notifications.cpp b/src/node/kernel_notifications.cpp index e41308dd50..5b1054ed26 100644 --- a/src/node/kernel_notifications.cpp +++ b/src/node/kernel_notifications.cpp @@ -83,6 +83,11 @@ void KernelNotifications::snapshotLoadProgress(double progress) uiInterface.SnapshotLoadProgress(progress); } +void KernelNotifications::rewindProgress(double progress) +{ + uiInterface.RewindProgress(progress); +} + void KernelNotifications::warning(const bilingual_str& warning) { DoWarning(warning); diff --git a/src/node/kernel_notifications.h b/src/node/kernel_notifications.h index 4e9b9f77db..51fe0cfc59 100644 --- a/src/node/kernel_notifications.h +++ b/src/node/kernel_notifications.h @@ -33,6 +33,8 @@ class KernelNotifications : public kernel::Notifications void snapshotLoadProgress(double progress) override; + void rewindProgress(double progress) override; + void warning(const bilingual_str& warning) override; void flushError(const std::string& debug_message) override; diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index 877469e933..0c5d675eb1 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -15,6 +15,7 @@ components/ProxySettings.qml components/StorageLocations.qml components/Separator.qml + components/SnapshotGenSettings.qml components/SnapshotSettings.qml components/StorageOptions.qml components/StorageSettings.qml @@ -72,6 +73,7 @@ pages/settings/SettingsDeveloper.qml pages/settings/SettingsDisplay.qml pages/settings/SettingsProxy.qml + pages/settings/SettingsSnapshotGen.qml pages/settings/SettingsSnapshot.qml pages/settings/SettingsStorage.qml pages/settings/SettingsTheme.qml diff --git a/src/qml/components/ConnectionSettings.qml b/src/qml/components/ConnectionSettings.qml index 5140b42e73..5cfecad2ca 100644 --- a/src/qml/components/ConnectionSettings.qml +++ b/src/qml/components/ConnectionSettings.qml @@ -11,13 +11,43 @@ ColumnLayout { id: root signal next signal gotoSnapshot + signal gotoGenerateSnapshot property bool snapshotImportCompleted: onboarding ? false : chainModel.isSnapshotActive property bool onboarding: false - + property bool generateSnapshot: false + property bool isSnapshotGenerated: nodeModel.isSnapshotGenerated + property bool isIBDCompleted: nodeModel.isIBDCompleted spacing: 4 + Setting { + id: gotoGenerateSnapshot + visible: !root.onboarding && (root.snapshotImportCompleted || root.isIBDCompleted) + Layout.fillWidth: true + header: qsTr("Generate snapshot") + description: qsTr("Speed up the setup of other nodes") + actionItem: Item { + width: 26 + height: 26 + CaretRightIcon { + anchors.centerIn: parent + color: gotoGenerateSnapshot.stateColor + } + } + onClicked: { + if (!nodeModel.isSnapshotFileExists()) { + root.generateSnapshot = true + root.gotoGenerateSnapshot() + } else { + root.gotoGenerateSnapshot() + } + } + } + Separator { + visible: !root.onboarding && (root.snapshotImportCompleted || root.isIBDCompleted) + Layout.fillWidth: true + } Setting { id: gotoSnapshot - visible: !root.onboarding + visible: !root.onboarding && !snapshotImportCompleted && !root.isIBDCompleted Layout.fillWidth: true header: qsTr("Load snapshot") description: qsTr("Instant use with background sync") @@ -39,7 +69,7 @@ ColumnLayout { onClicked: root.gotoSnapshot() } Separator { - visible: !root.onboarding + visible: !root.onboarding && !snapshotImportCompleted && !root.isIBDCompleted Layout.fillWidth: true } Setting { diff --git a/src/qml/components/SnapshotGenSettings.qml b/src/qml/components/SnapshotGenSettings.qml new file mode 100644 index 0000000000..486806e19a --- /dev/null +++ b/src/qml/components/SnapshotGenSettings.qml @@ -0,0 +1,239 @@ +// Copyright (c) 2023-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Dialogs 1.3 + +import "../controls" +import "../controls/utils.js" as Utils + + +ColumnLayout { + id: columnLayout + signal back + property bool onboarding: false + property bool generateSnapshot: false + property string selectedFile: "" + property bool snapshotGenerating: nodeModel.snapshotGenerating + property bool isPruned: optionsModel.prune + property bool isIBDCompleted: nodeModel.isIBDCompleted + property bool isSnapshotGenerated: nodeModel.isSnapshotGenerated + property var snapshotInfo: ( isSnapshotGenerated || isIBDCompleted ) ? chainModel.getSnapshotInfo() : ({}) + property bool isRewinding: nodeModel.isRewinding + + + width: Math.min(parent.width, 450) + anchors.horizontalCenter: parent.horizontalCenter + + StackLayout { + id: genSettingsStack + currentIndex: snapshotGenerating ? 1 : isSnapshotGenerated ? 2 : generateSnapshot ? 0 : onboarding ? 0 : 0 + + ColumnLayout { + // index: 0 + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(parent.width, 450) + + Image { + Layout.alignment: Qt.AlignCenter + source: "image://images/circle-file" + sourceSize.width: 200 + sourceSize.height: 200 + } + + Header { + Layout.fillWidth: true + Layout.topMargin: 20 + headerBold: true + header: qsTr("Generate snapshot") + descriptionBold: false + descriptionColor: Theme.color.neutral6 + descriptionSize: 17 + descriptionLineHeight: 1.1 + description: isPruned + ? qsTr("A snapshot captures the state of the bitcoin network up to a certain date in the recent past. " + + "It can be imported into other bitcoin nodes to speed up the initial setup.\n\n") + : isIBDCompleted + ? qsTr("A snapshot captures the state of the bitcoin network up to a certain date in the recent past. " + + "It can be imported into other bitcoin nodes to speed up the initial setup.\n\n" + + "You can generate a snapshot at block height %1 and date %2.").arg(snapshotInfo["height"]).arg(snapshotInfo["date"]) + : qsTr("A snapshot captures the state of the bitcoin network up to a certain date in the recent past. " + + "It can be imported into other bitcoin nodes to speed up the initial setup.\n\n" + + "Snapshot generation is available once the initial block download is complete.") + } + + CoreText { + Layout.fillWidth: true + // Layout.topMargin: 20 + color: Theme.color.neutral6 + font.pixelSize: 17 + visible: isPruned + text: isPruned + ? qsTr("Snapshot generation is not available due to current settings. To use this feature, disable \"Store recent blocks only\" in Storage Settings and download the transaction history again.") + : qsTr("") + wrap: true + wrapMode: Text.WordWrap + } + + ContinueButton { + Layout.preferredWidth: Math.min(300, columnLayout.width - 2 * Layout.leftMargin) + Layout.topMargin: 40 + Layout.alignment: Qt.AlignCenter + text: qsTr("Generate") + enabled: !isPruned && isIBDCompleted + onClicked: { + nodeModel.generateSnapshotThread() + } + } + } + + ColumnLayout { + // index: 1 + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(parent.width, 450) + + Image { + Layout.alignment: Qt.AlignCenter + source: "image://images/circle-file" + sourceSize.width: 200 + sourceSize.height: 200 + } + + Header { + Layout.fillWidth: true + Layout.topMargin: 20 + headerBold: true + header: qsTr("Generating Snapshot") + description: isRewinding ? qsTr("Rewinding...\nThis might take a while...") : qsTr("Restoring...\nThis might take a while...") + } + + ProgressIndicator { + id: generatingProgressIndicator + Layout.topMargin: 20 + width: 200 + height: 20 + progress: nodeModel.snapshotGenerating ? nodeModel.rewindProgress : 0 + Layout.alignment: Qt.AlignCenter + progressColor: Theme.color.blue + } + } + + ColumnLayout { + // index: 2 + id: snapshotGeneratedColumn + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(parent.width, 450) + + Image { + Layout.alignment: Qt.AlignCenter + source: "image://images/circle-green-check" + sourceSize.width: 60 + sourceSize.height: 60 + } + + Header { + Layout.fillWidth: true + Layout.topMargin: 20 + headerBold: true + header: qsTr("Snapshot Generated") + descriptionBold: false + descriptionColor: Theme.color.neutral6 + descriptionSize: 17 + descriptionLineHeight: 1.1 + description: snapshotInfo && snapshotInfo["date"] ? + qsTr("It contains transactions up to %1." + + " You can use this snapshot to quickstart other nodes.").arg(snapshotInfo["date"]) + : qsTr("It contains transactions up to DEBUG. You can use this snapshot to quickstart other nodes.") + } + + TextButton { + Layout.alignment: Qt.AlignCenter + text: qsTr("Generate new snapshot") + onClicked: { + nodeModel.generateSnapshotThread() + } + } + + ContinueButton { + Layout.preferredWidth: Math.min(300, columnLayout.width - 2 * Layout.leftMargin) + Layout.topMargin: 20 + Layout.alignment: Qt.AlignCenter + text: qsTr("View file") + borderColor: Theme.color.neutral6 + backgroundColor: "transparent" + onClicked: viewSnapshotFileDialog.open() + } + + FileDialog { + id: viewSnapshotFileDialog + folder: nodeModel.getSnapshotDirectory() + selectMultiple: false + selectExisting: true + nameFilters: ["Snapshot files (*.dat)", "All files (*)"] + } + + Setting { + id: snapshotGeneratedViewDetails + Layout.alignment: Qt.AlignCenter + header: qsTr("View details") + actionItem: CaretRightIcon { + id: snapshotGeneratedCaretIcon + color: snapshotGeneratedViewDetails.stateColor + rotation: snapshotGeneratedViewDetails.expanded ? 90 : 0 + Behavior on rotation { NumberAnimation { duration: 200 } } + } + + property bool expanded: false + + onClicked: { + expanded = !expanded + } + } + + ColumnLayout { + id: snapshotGeneratedDetailsContent + visible: snapshotGeneratedViewDetails.expanded + Layout.preferredWidth: Math.min(300, parent.width - 2 * Layout.leftMargin) + Layout.alignment: Qt.AlignCenter + Layout.leftMargin: 80 + Layout.rightMargin: 80 + Layout.topMargin: 10 + spacing: 10 + // TODO: make sure the block height number aligns right + RowLayout { + CoreText { + text: qsTr("Block Height:") + Layout.alignment: Qt.AlignLeft + font.pixelSize: 14 + } + CoreText { + text: snapshotInfo && snapshotInfo["height"] ? + snapshotInfo["height"] : qsTr("DEBUG") + Layout.alignment: Qt.AlignRight + font.pixelSize: 14 + } + } + Separator { Layout.fillWidth: true } + ColumnLayout { + Layout.fillWidth: true + spacing: 5 + CoreText { + text: qsTr("Hash:") + font.pixelSize: 14 + } + CoreText { + text: snapshotInfo && snapshotInfo["hashSerializedFirstHalf"] ? + snapshotInfo["hashSerializedFirstHalf"] + "\n" + snapshotInfo["hashSerializedSecondHalf"] : + qsTr("DEBUG") + Layout.fillWidth: true + font.pixelSize: 14 + textFormat: Text.PlainText + } + } + } + } + } +} diff --git a/src/qml/models/nodemodel.cpp b/src/qml/models/nodemodel.cpp index 2078650032..3636938870 100644 --- a/src/qml/models/nodemodel.cpp +++ b/src/qml/models/nodemodel.cpp @@ -28,6 +28,7 @@ NodeModel::NodeModel(interfaces::Node& node) ConnectToBlockTipSignal(); ConnectToNumConnectionsChangedSignal(); ConnectToSnapshotLoadProgressSignal(); + ConnectToRewindProgressSignal(); } void NodeModel::setBlockTipHeight(int new_height) @@ -86,6 +87,10 @@ void NodeModel::setVerificationProgress(double new_progress) if (new_progress != m_verification_progress) { setRemainingSyncTime(new_progress); + if (new_progress >= 0.999) { + setIBDCompleted(true); + } + if (new_progress >= 0.00001) { setHeadersSynced(true); } @@ -161,8 +166,6 @@ void NodeModel::ConnectToBlockTipSignal() QMetaObject::invokeMethod(this, [=] { setBlockTipHeight(tip.block_height); setVerificationProgress(verification_progress); - - Q_EMIT setTimeRatioList(tip.block_time); }); }); } @@ -197,6 +200,20 @@ void NodeModel::ConnectToSnapshotLoadProgressSignal() }); } +void NodeModel::ConnectToRewindProgressSignal() +{ + assert(!m_handler_rewind_progress); + + m_handler_rewind_progress = m_node.handleRewindProgress( + [this](double progress) { + if (isRewinding()) { + setRewindProgress(1.0 - progress); + } else { + setRewindProgress(progress); + } + }); +} + void NodeModel::snapshotLoadThread(QString path_file) { m_snapshot_loading = true; Q_EMIT snapshotLoadingChanged(); @@ -234,3 +251,71 @@ void NodeModel::setHeadersSynced(bool new_synced) { Q_EMIT headersSyncedChanged(); } } + +void NodeModel::cancelSnapshotGeneration() { + m_snapshot_cancel = true; + m_snapshot_generating = false; + Q_EMIT snapshotGeneratingChanged(); +} + +void NodeModel::generateSnapshotThread() { + QString path_file = ""; + m_snapshot_generating = true; + Q_EMIT snapshotGeneratingChanged(); + + QThread* generate_snapshot_thread = QThread::create([this, path_file]() { + SnapshotQml generator(m_node, path_file); + generator.setSnapshotCancel(&m_snapshot_cancel); + connect(&generator, &SnapshotQml::isRewindingChanged, this, [this, &generator]() { + setIsRewinding(generator.isRewinding()); + }); + generator.SnapshotGen(); + m_snapshot_generating = false; + Q_EMIT snapshotGeneratingChanged(); + setIsSnapshotGenerated(true); + }); + + connect(generate_snapshot_thread, &QThread::finished, generate_snapshot_thread, &QThread::deleteLater); + + generate_snapshot_thread->start(); +} + +QUrl NodeModel::getSnapshotDirectory() { + return QUrl::fromLocalFile(SnapshotQml(m_node, "").getSnapshotDirectory()); +} + +bool NodeModel::isSnapshotFileExists() { + bool result = SnapshotQml(m_node, "").isSnapshotFileExists(); + if (result) { + setIsSnapshotGenerated(true); + } + return result; +} + +void NodeModel::setIBDCompleted(bool completed) { + if (completed != m_is_ibd_completed) { + m_is_ibd_completed = completed; + Q_EMIT isIBDCompletedChanged(); + } +} + +void NodeModel::setIsRewinding(bool is_rewinding) { + if (is_rewinding != m_is_rewinding) { + m_is_rewinding = is_rewinding; + Q_EMIT isRewindingChanged(); + } +} + +void NodeModel::setRewindProgress(double progress) { + if (progress != m_rewind_progress) { + m_rewind_progress = progress; + Q_EMIT rewindProgressChanged(); + } +} + +void NodeModel::setIsSnapshotGenerated(bool generated) { + if (generated != m_is_snapshot_generated) { + m_is_snapshot_generated = generated; + Q_EMIT isSnapshotGeneratedChanged(); + } +} diff --git a/src/qml/models/nodemodel.h b/src/qml/models/nodemodel.h index 8832eeab27..51fcc40f05 100644 --- a/src/qml/models/nodemodel.h +++ b/src/qml/models/nodemodel.h @@ -5,12 +5,17 @@ #ifndef BITCOIN_QML_MODELS_NODEMODEL_H #define BITCOIN_QML_MODELS_NODEMODEL_H +#include #include #include +#include +#include +#include #include +#include #include - +#include #include #include @@ -38,7 +43,11 @@ class NodeModel : public QObject Q_PROPERTY(bool snapshotLoading READ snapshotLoading NOTIFY snapshotLoadingChanged) Q_PROPERTY(bool isSnapshotLoaded READ isSnapshotLoaded NOTIFY snapshotLoaded) Q_PROPERTY(bool headersSynced READ headersSynced WRITE setHeadersSynced NOTIFY headersSyncedChanged) - + Q_PROPERTY(bool snapshotGenerating READ snapshotGenerating NOTIFY snapshotGeneratingChanged) + Q_PROPERTY(bool isIBDCompleted READ isIBDCompleted WRITE setIBDCompleted NOTIFY isIBDCompletedChanged) + Q_PROPERTY(bool isRewinding READ isRewinding WRITE setIsRewinding NOTIFY isRewindingChanged) + Q_PROPERTY(double rewindProgress READ rewindProgress WRITE setRewindProgress NOTIFY rewindProgressChanged) + Q_PROPERTY(bool isSnapshotGenerated READ isSnapshotGenerated WRITE setIsSnapshotGenerated NOTIFY isSnapshotGeneratedChanged) public: explicit NodeModel(interfaces::Node& node); @@ -62,6 +71,15 @@ class NodeModel : public QObject bool snapshotLoading() const { return m_snapshot_loading; } bool headersSynced() const { return m_headers_synced; } void setHeadersSynced(bool new_synced); + bool snapshotGenerating() const { return m_snapshot_generating; } + bool isIBDCompleted() const { return m_is_ibd_completed; } + void setIBDCompleted(bool completed); + bool isRewinding() const { return m_is_rewinding; } + void setIsRewinding(bool is_rewinding); + double rewindProgress() const { return m_rewind_progress; } + void setRewindProgress(double progress); + bool isSnapshotGenerated() const { return m_is_snapshot_generated; } + void setIsSnapshotGenerated(bool generated); Q_INVOKABLE float getTotalBytesReceived() const { return (float)m_node.getTotalBytesRecv(); } Q_INVOKABLE float getTotalBytesSent() const { return (float)m_node.getTotalBytesSent(); } @@ -70,6 +88,10 @@ class NodeModel : public QObject Q_INVOKABLE void requestShutdown(); Q_INVOKABLE void snapshotLoadThread(QString path_file); + Q_INVOKABLE void generateSnapshotThread(); + Q_INVOKABLE void cancelSnapshotGeneration(); + Q_INVOKABLE bool isSnapshotFileExists(); + Q_INVOKABLE QUrl getSnapshotDirectory(); void startShutdownPolling(); void stopShutdownPolling(); @@ -96,7 +118,11 @@ public Q_SLOTS: void snapshotLoaded(bool result); void snapshotProgressChanged(); void snapshotLoadingChanged(); - void showProgress(const QString& title, int progress); + void snapshotGeneratingChanged(); + void isIBDCompletedChanged(); + void rewindProgressChanged(); + void isSnapshotGeneratedChanged(); + void isRewindingChanged(); void headersSyncedChanged(); protected: void timerEvent(QTimerEvent* event) override; @@ -116,15 +142,24 @@ public Q_SLOTS: bool m_snapshot_loading{false}; bool m_snapshot_loaded{false}; bool m_headers_synced{false}; + bool m_snapshot_generating{false}; QVector> m_block_process_time; - + bool m_is_ibd_completed{false}; + double m_rewind_progress{0.0}; + bool m_is_snapshot_generated{false}; + bool m_is_rewinding{false}; interfaces::Node& m_node; std::unique_ptr m_handler_notify_block_tip; std::unique_ptr m_handler_notify_num_peers_changed; std::unique_ptr m_handler_snapshot_load_progress; + std::unique_ptr m_handler_show_progress; + std::unique_ptr m_handler_notify_block_disconnected; + std::unique_ptr m_handler_rewind_progress; + std::atomic m_snapshot_cancel{false}; void ConnectToBlockTipSignal(); void ConnectToNumConnectionsChangedSignal(); void ConnectToSnapshotLoadProgressSignal(); + void ConnectToRewindProgressSignal(); }; #endif // BITCOIN_QML_MODELS_NODEMODEL_H diff --git a/src/qml/models/snapshotqml.cpp b/src/qml/models/snapshotqml.cpp index 6265aba417..3bd10db56c 100644 --- a/src/qml/models/snapshotqml.cpp +++ b/src/qml/models/snapshotqml.cpp @@ -4,11 +4,23 @@ #include +#include +#include +#include +#include +#include #include +#include +#include #include +#include #include #include +#include + +using kernel::CCoinsStats; + SnapshotQml::SnapshotQml(interfaces::Node& node, QString path) : m_node(node), m_path(path) {} @@ -37,4 +49,297 @@ bool SnapshotQml::processPath() return false; } return true; -} \ No newline at end of file +} + +static void InvalidateBlock(ChainstateManager& chainman, const uint256 block_hash) +{ + BlockValidationState state; + CBlockIndex* pblockindex; + { + LOCK(chainman.GetMutex()); + pblockindex = chainman.m_blockman.LookupBlockIndex(block_hash); + if (!pblockindex) { + LogPrintf("Block not found"); + return; + } + } + + chainman.ActiveChainstate().InvalidateBlock(state, pblockindex); + + if (state.IsValid()) { + chainman.ActiveChainstate().ActivateBestChain(state); + } + + if (!state.IsValid()) { + LogPrintf("Invalidate block failed: %s", state.ToString()); + return; + } +} + +static std::optional GetUTXOStats(CCoinsView* view, node::BlockManager& blockman, + kernel::CoinStatsHashType hash_type, + const std::function& interruption_point = {}, + const CBlockIndex* pindex = nullptr, + bool index_requested = true) +{ + // Use CoinStatsIndex if it is requested and available and a hash_type of Muhash or None was requested + if ((hash_type == kernel::CoinStatsHashType::MUHASH || hash_type == kernel::CoinStatsHashType::NONE) && g_coin_stats_index && index_requested) { + if (pindex) { + return g_coin_stats_index->LookUpStats(*pindex); + } else { + CBlockIndex& block_index = *CHECK_NONFATAL(WITH_LOCK(::cs_main, return blockman.LookupBlockIndex(view->GetBestBlock()))); + return g_coin_stats_index->LookUpStats(block_index); + } + } + + // If the coinstats index isn't requested or is otherwise not usable, the + // pindex should either be null or equal to the view's best block. This is + // because without the coinstats index we can only get coinstats about the + // best block. + CHECK_NONFATAL(!pindex || pindex->GetBlockHash() == view->GetBestBlock()); + + return kernel::ComputeUTXOStats(hash_type, view, blockman, interruption_point); +} + +static std::tuple, CCoinsStats, const CBlockIndex*> +PrepareUTXOSnapshot( + Chainstate& chainstate, + const std::function& interruption_point) +{ + std::unique_ptr pcursor; + std::optional maybe_stats; + const CBlockIndex* tip; + + { + // We need to lock cs_main to ensure that the coinsdb isn't written to + // between (i) flushing coins cache to disk (coinsdb), (ii) getting stats + // based upon the coinsdb, and (iii) constructing a cursor to the + // coinsdb for use in WriteUTXOSnapshot. + // + // Cursors returned by leveldb iterate over snapshots, so the contents + // of the pcursor will not be affected by simultaneous writes during + // use below this block. + // + // See discussion here: + // https://github.com/bitcoin/bitcoin/pull/15606#discussion_r274479369 + // + AssertLockHeld(::cs_main); + + chainstate.ForceFlushStateToDisk(); + + maybe_stats = GetUTXOStats(&chainstate.CoinsDB(), chainstate.m_blockman, kernel::CoinStatsHashType::HASH_SERIALIZED, interruption_point); + if (!maybe_stats) { + LogPrintf("Unable to read UTXO set"); + return std::tuple, CCoinsStats, const CBlockIndex*>{}; + } + + pcursor = chainstate.CoinsDB().Cursor(); + tip = CHECK_NONFATAL(chainstate.m_blockman.LookupBlockIndex(maybe_stats->hashBlock)); + } + + return {std::move(pcursor), *CHECK_NONFATAL(maybe_stats), tip}; +} + +static void WriteUTXOSnapshot( + Chainstate& chainstate, + CCoinsViewCursor* pcursor, + CCoinsStats* maybe_stats, + const CBlockIndex* tip, + AutoFile& afile, + const fs::path& path, + const fs::path& temppath, + const std::function& interruption_point, + std::atomic* snapshot_cancel) +{ + LOG_TIME_SECONDS(strprintf("writing UTXO snapshot at height %s (%s) to file %s (via %s)", + tip->nHeight, tip->GetBlockHash().ToString(), + fs::PathToString(path), fs::PathToString(temppath))); + + node::SnapshotMetadata metadata{tip->GetBlockHash(), maybe_stats->coins_count, tip->nChainTx}; + + afile << metadata; + + COutPoint key; + Coin coin; + unsigned int iter{0}; + + while (pcursor->Valid()) { + if (snapshot_cancel && *snapshot_cancel) { + if (fs::exists(temppath)) { + fs::remove(temppath); + } + return; + } + ++iter; + if (pcursor->GetKey(key) && pcursor->GetValue(coin)) { + afile << key; + afile << coin; + } + + pcursor->Next(); + } + + afile.fclose(); + return; +} + +static void ReconsiderBlock(ChainstateManager& chainman, uint256 block_hash) { + { + LOCK(chainman.GetMutex()); + CBlockIndex* pblockindex = chainman.m_blockman.LookupBlockIndex(block_hash); + if (!pblockindex) { + LogPrintf("Block not found"); + return; + } + + chainman.ActiveChainstate().ResetBlockFailureFlags(pblockindex); + } + + BlockValidationState state; + chainman.ActiveChainstate().ActivateBestChain(state); + + if (!state.IsValid()) { + LogPrintf("Reconsider block failed: %s", state.ToString()); + } +} + +/** + * RAII class that disables the network in its constructor and enables it in its + * destructor. + */ +class NetworkDisable +{ + CConnman& m_connman; +public: + NetworkDisable(CConnman& connman) : m_connman(connman) { + m_connman.SetNetworkActive(false); + if (m_connman.GetNetworkActive()) { + LogPrintf("Network activity could not be suspended."); + } + }; + ~NetworkDisable() { + m_connman.SetNetworkActive(true); + }; +}; + +/** + * RAII class that temporarily rolls back the local chain in it's constructor + * and rolls it forward again in it's destructor. + */ +class TemporaryRollback +{ + ChainstateManager& m_chainman; + const CBlockIndex& m_invalidate_index; + +public: + TemporaryRollback(ChainstateManager& chainman, const CBlockIndex& index) + : m_chainman(chainman) + , m_invalidate_index(index) + { + InvalidateBlock(m_chainman, m_invalidate_index.GetBlockHash()); + } + + ~TemporaryRollback() { + ReconsiderBlock(m_chainman, m_invalidate_index.GetBlockHash()); + } +}; + +void SnapshotQml::setIsPruned(bool is_pruned) { + if (m_is_pruned != is_pruned) { + m_is_pruned = is_pruned; + Q_EMIT isPrunedChanged(); + } +} + +QString SnapshotQml::getSnapshotDirectory() { + const fs::path path = gArgs.GetDataDirNet(); + return QString::fromStdString(fs::PathToString(path)); +} + +bool SnapshotQml::isSnapshotFileExists() { + ChainstateManager& chainman = *Assert(m_node.context()->chainman); + const MapAssumeutxo& assumeutxo_map = chainman.GetParams().Assumeutxo(); + int snapshot_height = 0; + if (!assumeutxo_map.empty()) { + snapshot_height = assumeutxo_map.rbegin()->first; + } + std::string network = chainman.GetParams().GetChainTypeString(); + std::string filename = strprintf("utxo_%s_%d.dat", network, snapshot_height); + const fs::path path = fsbridge::AbsPathJoin(gArgs.GetDataDirNet(), fs::u8path(filename)); + return fs::exists(path); +} + +void SnapshotQml::setIsRewinding(bool is_rewinding) { + if (m_is_rewinding != is_rewinding) { + m_is_rewinding = is_rewinding; + Q_EMIT isRewindingChanged(); + } +} + +void SnapshotQml::SnapshotGen() { + if (!m_snapshot_cancel) return; + ChainstateManager& chainman = *Assert(m_node.context()->chainman); + const CChain& active_chain = chainman.ActiveChain(); + const CBlockIndex* tip{WITH_LOCK(::cs_main, return active_chain.Tip())}; + const MapAssumeutxo& assumeutxo_map = chainman.GetParams().Assumeutxo(); + int snapshot_height = 0; + if (!assumeutxo_map.empty()) { + snapshot_height = assumeutxo_map.rbegin()->first; + } + const CBlockIndex* target_index = active_chain[snapshot_height]; + const CBlockIndex* invalidate_index; + std::string network = chainman.GetParams().GetChainTypeString(); + std::string filename = strprintf("utxo_%s_%d.dat", network, snapshot_height); + const fs::path path = fsbridge::AbsPathJoin(gArgs.GetDataDirNet(), fs::u8path(filename)); + const fs::path temppath = fsbridge::AbsPathJoin(gArgs.GetDataDirNet(), fs::u8path(filename + ".incomplete")); + FILE* file{fsbridge::fopen(temppath, "wb")}; + AutoFile afile{file}; + if (afile.IsNull()) { + return; + } + CConnman& connman = *Assert(m_node.context()->connman); + std::optional disable_network; + std::optional temporary_rollback; + if (chainman.m_blockman.IsPruneMode()) { + LOCK(chainman.GetMutex()); + const CBlockIndex* current_tip = active_chain.Tip(); + const CBlockIndex* first_block = chainman.m_blockman.GetFirstStoredBlock(*current_tip); + if (first_block->nHeight > target_index->nHeight) { + LogPrintf("first_block->nHeight: %d, target_index->nHeight: %d\n", first_block->nHeight, target_index->nHeight); + LogPrintf("Could not roll back to requested height since necessary block data is already pruned. \n"); + return; + } + } + + if (connman.GetNetworkActive()) { + disable_network.emplace(connman); + } + + invalidate_index = WITH_LOCK(::cs_main, return active_chain.Next(target_index)); + setIsRewinding(true); + temporary_rollback.emplace(chainman, *invalidate_index); + + + Chainstate* chainstate; + std::unique_ptr cursor; + CCoinsStats maybe_stats; + { + LOCK(chainman.GetMutex()); + chainstate = &chainman.ActiveChainstate(); + if (m_snapshot_cancel && *m_snapshot_cancel) { + LogPrintf("generateUTXOSnapshot canceled\n"); + return; + } + if (target_index != chainstate->m_chain.Tip()) { + LogPrintf("generateUTXOSnapshot failed to roll back to requested height, reverting to tip.\n"); + return; + } else { + std::tie(cursor, maybe_stats, tip) = PrepareUTXOSnapshot(*chainstate, []() { }); + } + } + + WriteUTXOSnapshot(*chainstate, cursor.get(), &maybe_stats, tip, afile, path, temppath, []() { }, m_snapshot_cancel); + fs::rename(temppath, path); + setIsRewinding(false); + return; +} diff --git a/src/qml/models/snapshotqml.h b/src/qml/models/snapshotqml.h index 75e4758d7e..aee873b947 100644 --- a/src/qml/models/snapshotqml.h +++ b/src/qml/models/snapshotqml.h @@ -7,20 +7,47 @@ #include #include +#include +#include +#include +#include +#include +#include #include + +/** Worker class for UTXO snapshot functions. */ class SnapshotQml : public QObject { + Q_PROPERTY(bool isPruned READ isPruned WRITE setIsPruned NOTIFY isPrunedChanged) + Q_PROPERTY(bool isSnapshotFileExists READ isSnapshotFileExists NOTIFY isSnapshotFileExistsChanged) + Q_PROPERTY(bool isRewinding READ isRewinding WRITE setIsRewinding NOTIFY isRewindingChanged) Q_OBJECT public: SnapshotQml(interfaces::Node& node, QString path); bool processPath(); + void SnapshotGen(); + bool isPruned() const { return m_is_pruned; } + void setIsPruned(bool is_pruned); + void setSnapshotCancel(std::atomic* cancel) { m_snapshot_cancel = cancel; } + bool isSnapshotFileExists(); + QString getSnapshotDirectory(); + bool isRewinding() const { return m_is_rewinding; } + void setIsRewinding(bool is_rewinding); +Q_SIGNALS: + void isPrunedChanged(); + void isSnapshotFileExistsChanged(); + void isRewindingChanged(); private: interfaces::Node& m_node; QString m_path; + bool m_is_pruned; + std::atomic* m_snapshot_cancel; + bool m_is_snapshot_file_exists; + bool m_is_rewinding; }; #endif // BITCOIN_QML_MODELS_SNAPSHOTQML_H diff --git a/src/qml/pages/settings/SettingsConnection.qml b/src/qml/pages/settings/SettingsConnection.qml index da5593c58d..a0bea5dcf4 100644 --- a/src/qml/pages/settings/SettingsConnection.qml +++ b/src/qml/pages/settings/SettingsConnection.qml @@ -35,6 +35,7 @@ Page { onGotoSnapshot: stack.push(snapshotSettings) snapshotImportCompleted: root.snapshotImportCompleted onboarding: root.onboarding + onGotoGenerateSnapshot: stack.push(generateSnapshotSettings) } states: [ @@ -99,5 +100,14 @@ Page { onBack: stack.pop() } } + Component { + id: generateSnapshotSettings + SettingsSnapshotGen { + onboarding: root.onboarding + generateSnapshot: true + isSnapshotGenerated: ( nodeModel.isSnapshotFileExists() || nodeModel.isSnapshotGenerated ) + onBack: stack.pop() + } + } } } diff --git a/src/qml/pages/settings/SettingsSnapshotGen.qml b/src/qml/pages/settings/SettingsSnapshotGen.qml new file mode 100644 index 0000000000..193cc24f28 --- /dev/null +++ b/src/qml/pages/settings/SettingsSnapshotGen.qml @@ -0,0 +1,40 @@ +// Copyright (c) 2024-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import "../../controls" +import "../../components" + +Page { + signal back + property bool onboarding: false + property bool generateSnapshot: false + property bool isSnapshotGenerated: nodeModel.isSnapshotGenerated + + id: root + + background: null + implicitWidth: 450 + leftPadding: 20 + rightPadding: 20 + + header: NavigationBar2 { + leftItem: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: root.back() + } + } + + SnapshotGenSettings { + width: Math.min(parent.width, 450) + anchors.horizontalCenter: parent.horizontalCenter + onboarding: root.onboarding + generateSnapshot: root.generateSnapshot + isSnapshotGenerated: root.isSnapshotGenerated + onBack: root.back() + } +} \ No newline at end of file diff --git a/src/validation.cpp b/src/validation.cpp index c67cb91178..de779d644d 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2661,6 +2661,7 @@ void Chainstate::UpdateTip(const CBlockIndex* pindexNew) } } } + m_chainman.GetNotifications().rewindProgress(GuessVerificationProgress(params.TxData(), pindexNew)); UpdateTipLog(coins_tip, pindexNew, params, __func__, "", warning_messages.original); }