From 9638e3e81c49b5e5b12819c22444eeeefaa5b9d8 Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 01:13:48 -0500 Subject: [PATCH 01/33] feat(qt): add Dash Platform GUI scaffolding behind --enable-platform-gui Introduce the build and UI skeleton for Dash Platform (usernames / DashPay) support in dash-qt, keeping dashd and consensus code fully independent of Platform: - new configure flag --enable-platform-gui (default off), gating everything - depends: new optional mbedtls package (PLATFORM_GUI=1), for the future DAPI TLS client; config.site auto-enables the flag - vendor BLAKE3 1.8.5 (portable C only) under src/crypto/blake3/, used exclusively for GroveDB merk proof verification in the GUI client library - new src/platform/ Qt-free client library (libdash_platform.a, linked into dash-qt and test binaries only) seeded with per-network parameters and the well-known DPNS/DashPay system contract ids - new DashPay tab (placeholder page) wired through BitcoinGUI/WalletFrame/ WalletView following the Masternodes tab pattern, with an OptionsModel ShowPlatformTab toggle and options dialog checkbox; the tab is only offered on networks where Platform is deployed - lint/non-backported list updates for the new Dash-specific paths Co-Authored-By: Claude Fable 5 --- configure.ac | 43 ++ contrib/devtools/copyright_header.py | 1 + depends/Makefile | 7 + depends/config.site.in | 4 + depends/packages/mbedtls.mk | 27 ++ depends/packages/packages.mk | 2 + src/Makefile.am | 23 +- src/Makefile.qt.include | 22 +- src/Makefile.qttest.include | 3 + src/crypto/blake3/LICENSE | 202 +++++++++ src/crypto/blake3/README.md | 17 + src/crypto/blake3/blake3.c | 651 +++++++++++++++++++++++++++ src/crypto/blake3/blake3.h | 86 ++++ src/crypto/blake3/blake3_dispatch.c | 332 ++++++++++++++ src/crypto/blake3/blake3_impl.h | 333 ++++++++++++++ src/crypto/blake3/blake3_portable.c | 160 +++++++ src/platform/README.md | 29 ++ src/platform/params.cpp | 40 ++ src/platform/params.h | 65 +++ src/qt/bitcoingui.cpp | 58 +++ src/qt/bitcoingui.h | 13 + src/qt/forms/optionsdialog.ui | 10 + src/qt/optionsdialog.cpp | 7 + src/qt/optionsmodel.cpp | 13 + src/qt/optionsmodel.h | 4 + src/qt/platform/moc_platformpage.cpp | 95 ++++ src/qt/platform/platformpage.cpp | 49 ++ src/qt/platform/platformpage.h | 38 ++ src/qt/walletframe.cpp | 9 + src/qt/walletframe.h | 8 + src/qt/walletview.cpp | 21 + src/qt/walletview.h | 14 + test/lint/lint-include-guards.py | 3 +- test/lint/lint-includes.py | 1 + test/lint/lint-locale-dependence.py | 1 + test/lint/lint-whitespace.py | 1 + test/util/data/non-backported.txt | 4 + 37 files changed, 2393 insertions(+), 3 deletions(-) create mode 100644 depends/packages/mbedtls.mk create mode 100644 src/crypto/blake3/LICENSE create mode 100644 src/crypto/blake3/README.md create mode 100644 src/crypto/blake3/blake3.c create mode 100644 src/crypto/blake3/blake3.h create mode 100644 src/crypto/blake3/blake3_dispatch.c create mode 100644 src/crypto/blake3/blake3_impl.h create mode 100644 src/crypto/blake3/blake3_portable.c create mode 100644 src/platform/README.md create mode 100644 src/platform/params.cpp create mode 100644 src/platform/params.h create mode 100644 src/qt/platform/moc_platformpage.cpp create mode 100644 src/qt/platform/platformpage.cpp create mode 100644 src/qt/platform/platformpage.h diff --git a/configure.ac b/configure.ac index 0b5ab13da6e4..38b470abbfcb 100644 --- a/configure.ac +++ b/configure.ac @@ -301,6 +301,16 @@ if test "$enable_miner" = "yes"; then AC_DEFINE(ENABLE_MINER, 1, [Define this symbol if in-wallet miner should be enabled]) fi +dnl Enable Dash Platform support (usernames / DashPay contacts) in the GUI. +dnl This only affects dash-qt; dashd and the other binaries never link any of it. +AC_ARG_ENABLE([platform-gui], + [AS_HELP_STRING([--enable-platform-gui], + [enable Dash Platform (usernames/DashPay) support in the GUI (default is no)])], + [enable_platform_gui=$enableval], + [enable_platform_gui=no]) +AC_ARG_VAR([MBEDTLS_CFLAGS], [C compiler flags for mbedtls, bypasses autodetection]) +AC_ARG_VAR([MBEDTLS_LIBS], [Linker flags for mbedtls, bypasses autodetection]) + dnl Enable different -fsanitize options AC_ARG_WITH([sanitizers], [AS_HELP_STRING([--with-sanitizers], @@ -887,6 +897,16 @@ case $host in export PKG_CONFIG_PATH="$($BREW --prefix qt@5 2>/dev/null)/lib/pkgconfig:$PKG_CONFIG_PATH" fi + if test "$enable_platform_gui" = "yes" && $BREW list --versions mbedtls >/dev/null && test "$MBEDTLS_CFLAGS" = "" && test "$MBEDTLS_LIBS" = ""; then + mbedtls_prefix=$($BREW --prefix mbedtls 2>/dev/null) + if test "$suppress_external_warnings" != "no"; then + MBEDTLS_CFLAGS="-isystem $mbedtls_prefix/include" + else + MBEDTLS_CFLAGS="-I$mbedtls_prefix/include" + fi + MBEDTLS_LIBS="-L$mbedtls_prefix/lib -lmbedtls -lmbedx509 -lmbedcrypto" + fi + gmp_prefix=$($BREW --prefix gmp 2>/dev/null) if test "$gmp_prefix" != ""; then if test "$suppress_external_warnings" != "no"; then @@ -1966,6 +1986,28 @@ if test "$build_bitcoin_wallet$build_bitcoin_cli$build_bitcoin_tx$build_bitcoin_ AC_MSG_ERROR([No targets! Please specify at least one of: --with-utils --with-libs --with-daemon --with-gui --enable-fuzz(-binary) --enable-bench or --enable-tests]) fi +dnl Dash Platform GUI support needs the GUI and the wallet, and mbedtls for the +dnl DAPI TLS client. The platform client library is linked into dash-qt (and +dnl test binaries) only. +if test "$enable_platform_gui" = "yes"; then + if test "$bitcoin_enable_qt" != "yes"; then + AC_MSG_ERROR([--enable-platform-gui requires the GUI (--with-gui)]) + fi + if test "$enable_wallet" != "yes"; then + AC_MSG_ERROR([--enable-platform-gui requires wallet support (--enable-wallet)]) + fi + if test "$MBEDTLS_CFLAGS$MBEDTLS_LIBS" = ""; then + AC_CHECK_HEADER([mbedtls/ssl.h], [], [AC_MSG_ERROR([mbedtls headers not found (required by --enable-platform-gui)])]) + AC_CHECK_LIB([mbedcrypto], [main], [MBEDTLS_LIBS="-lmbedcrypto"], [AC_MSG_ERROR([libmbedcrypto not found (required by --enable-platform-gui)])]) + AC_CHECK_LIB([mbedx509], [main], [MBEDTLS_LIBS="-lmbedx509 $MBEDTLS_LIBS"], [AC_MSG_ERROR([libmbedx509 not found (required by --enable-platform-gui)])], [$MBEDTLS_LIBS]) + AC_CHECK_LIB([mbedtls], [mbedtls_ssl_init], [MBEDTLS_LIBS="-lmbedtls $MBEDTLS_LIBS"], [AC_MSG_ERROR([libmbedtls not found (required by --enable-platform-gui)])], [$MBEDTLS_LIBS]) + fi + AC_DEFINE([ENABLE_PLATFORM_GUI], [1], [Define this symbol to enable Dash Platform support in the GUI]) +fi +AM_CONDITIONAL([ENABLE_PLATFORM_GUI], [test "$enable_platform_gui" = "yes"]) +AC_SUBST(MBEDTLS_CFLAGS) +AC_SUBST(MBEDTLS_LIBS) + AM_CONDITIONAL([TARGET_DARWIN], [test "$TARGET_OS" = "darwin"]) AM_CONDITIONAL([BUILD_DARWIN], [test "$BUILD_OS" = "darwin"]) AM_CONDITIONAL([TARGET_LINUX], [test "$TARGET_OS" = "linux"]) @@ -2160,6 +2202,7 @@ echo " debug enabled = $enable_debug" echo " stacktraces = $enable_stacktraces" echo " crash hooks = $enable_crashhooks" echo " miner enabled = $enable_miner" +echo " platform gui = $enable_platform_gui" echo " gprof enabled = $enable_gprof" echo " werror = $enable_werror" echo diff --git a/contrib/devtools/copyright_header.py b/contrib/devtools/copyright_header.py index effd8633aa10..a834b48c84c8 100755 --- a/contrib/devtools/copyright_header.py +++ b/contrib/devtools/copyright_header.py @@ -36,6 +36,7 @@ EXCLUDE_DIRS = [ # git subtrees "src/crc32c/", + "src/crypto/blake3/", "src/crypto/ctaes/", "src/dashbls/", "src/gsl/", diff --git a/depends/Makefile b/depends/Makefile index c8510f4cc010..b80bc27b0c0b 100644 --- a/depends/Makefile +++ b/depends/Makefile @@ -44,6 +44,7 @@ NO_UPNP ?= NO_USDT ?= NO_NATPMP ?= MULTIPROCESS ?= +PLATFORM_GUI ?= LTO ?= NO_HARDEN ?= FALLBACK_DOWNLOAD_PATH ?= http://dash-depends-sources.s3-website-us-west-2.amazonaws.com @@ -175,6 +176,7 @@ natpmp_packages_$(NO_NATPMP) = $(natpmp_packages) zmq_packages_$(NO_ZMQ) = $(zmq_packages) multiprocess_packages_$(MULTIPROCESS) = $(multiprocess_packages) +platform_packages_$(PLATFORM_GUI) = $(platform_packages) usdt_packages_$(NO_USDT) = $(usdt_$(host_os)_packages) packages += $($(host_arch)_$(host_os)_packages) $($(host_os)_packages) $(boost_packages_) $(libevent_packages_) $(qt_packages_) $(wallet_packages_) $(upnp_packages_) $(natpmp_packages_) $(usdt_packages_) @@ -189,6 +191,10 @@ packages += $(multiprocess_packages) native_packages += $(multiprocess_native_packages) endif +ifeq ($(platform_packages_),) +packages += $(platform_packages) +endif + all_packages = $(packages) $(native_packages) meta_depends = Makefile config.guess config.sub funcs.mk builders/default.mk hosts/default.mk hosts/$(host_os).mk builders/$(build_os).mk @@ -257,6 +263,7 @@ $(host_prefix)/share/config.site : config.site.in $(host_prefix)/.stamp_$(final_ -e 's|@no_usdt@|$(NO_USDT)|' \ -e 's|@no_natpmp@|$(NO_NATPMP)|' \ -e 's|@multiprocess@|$(MULTIPROCESS)|' \ + -e 's|@platform_gui@|$(PLATFORM_GUI)|' \ -e 's|@lto@|$(LTO)|' \ -e 's|@no_harden@|$(NO_HARDEN)|' \ -e 's|@debug@|$(DEBUG)|' \ diff --git a/depends/config.site.in b/depends/config.site.in index 398a09b74c63..ff54ed3db74b 100644 --- a/depends/config.site.in +++ b/depends/config.site.in @@ -50,6 +50,10 @@ if test -z "$enable_multiprocess" && test -n "@multiprocess@"; then enable_multiprocess=yes fi +if test -z "$enable_platform_gui" && test -n "@platform_gui@"; then + enable_platform_gui=yes +fi + if test -z "$with_miniupnpc" && test -n "@no_upnp@"; then with_miniupnpc=no fi diff --git a/depends/packages/mbedtls.mk b/depends/packages/mbedtls.mk new file mode 100644 index 000000000000..cea4466c167d --- /dev/null +++ b/depends/packages/mbedtls.mk @@ -0,0 +1,27 @@ +package=mbedtls +$(package)_version=3.6.3.1 +$(package)_download_path=https://github.com/Mbed-TLS/mbedtls/releases/download/v$($(package)_version)/ +$(package)_file_name=$(package)-$($(package)_version).tar.bz2 +$(package)_sha256_hash=243ed496d5f88a5b3791021be2800aac821b9a4cc16e7134aa413c58b4c20e0c + +define $(package)_set_vars +$(package)_config_opts := -DENABLE_PROGRAMS=OFF -DENABLE_TESTING=OFF +$(package)_config_opts += -DUSE_SHARED_MBEDTLS_LIBRARY=OFF -DUSE_STATIC_MBEDTLS_LIBRARY=ON +$(package)_config_opts += -DMBEDTLS_FATAL_WARNINGS=OFF -DGEN_FILES=OFF +endef + +define $(package)_config_cmds + $($(package)_cmake) -S . -B . +endef + +define $(package)_build_cmds + $(MAKE) +endef + +define $(package)_stage_cmds + $(MAKE) DESTDIR=$($(package)_staging_dir) install +endef + +define $(package)_postprocess_cmds + rm -rf lib/cmake +endef diff --git a/depends/packages/packages.mk b/depends/packages/packages.mk index 7e0bb2633219..05371494fc49 100644 --- a/depends/packages/packages.mk +++ b/depends/packages/packages.mk @@ -26,4 +26,6 @@ natpmp_packages=libnatpmp multiprocess_packages = libmultiprocess capnp multiprocess_native_packages = native_libmultiprocess native_capnp +platform_packages = mbedtls + usdt_linux_packages=systemtap diff --git a/src/Makefile.am b/src/Makefile.am index 4b446675291e..9f678fd5a805 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -58,6 +58,9 @@ LIBSECP256K1=secp256k1/libsecp256k1.la if ENABLE_ZMQ LIBBITCOIN_ZMQ=libbitcoin_zmq.a endif +if ENABLE_PLATFORM_GUI +LIBDASH_PLATFORM=libdash_platform.a +endif if BUILD_BITCOIN_LIBS LIBBITCOINCONSENSUS=libdashconsensus.la endif @@ -123,7 +126,8 @@ EXTRA_LIBRARIES += \ $(LIBBITCOIN_IPC) \ $(LIBBITCOIN_WALLET) \ $(LIBBITCOIN_WALLET_TOOL) \ - $(LIBBITCOIN_ZMQ) + $(LIBBITCOIN_ZMQ) \ + $(LIBDASH_PLATFORM) if BUILD_BITCOIND bin_PROGRAMS += dashd @@ -675,6 +679,23 @@ libbitcoin_zmq_a_SOURCES = \ endif # +# platform (Dash Platform client, linked into dash-qt and test_dash only) # +if ENABLE_PLATFORM_GUI +libdash_platform_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(BOOST_CPPFLAGS) $(MBEDTLS_CFLAGS) \ + -DBLAKE3_NO_SSE2 -DBLAKE3_NO_SSE41 -DBLAKE3_NO_AVX2 -DBLAKE3_NO_AVX512 +libdash_platform_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) +libdash_platform_a_CFLAGS = $(AM_CFLAGS) $(PIE_FLAGS) +libdash_platform_a_SOURCES = \ + crypto/blake3/blake3.c \ + crypto/blake3/blake3.h \ + crypto/blake3/blake3_dispatch.c \ + crypto/blake3/blake3_impl.h \ + crypto/blake3/blake3_portable.c \ + platform/params.cpp \ + platform/params.h +endif +# + # wallet # libbitcoin_wallet_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(BOOST_CPPFLAGS) $(BDB_CPPFLAGS) $(SQLITE_CFLAGS) libbitcoin_wallet_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 16296207eca1..93d1367bc47f 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -114,6 +114,11 @@ QT_MOC_CPP = \ qt/moc_walletmodel.cpp \ qt/moc_walletview.cpp +if ENABLE_PLATFORM_GUI +QT_MOC_CPP += \ + qt/platform/moc_platformpage.cpp +endif + BITCOIN_MM = \ qt/macdockiconhandler.mm \ qt/macnotificationhandler.mm \ @@ -207,6 +212,11 @@ BITCOIN_QT_H = \ qt/walletview.h \ qt/winshutdownmonitor.h +if ENABLE_PLATFORM_GUI +BITCOIN_QT_H += \ + qt/platform/platformpage.h +endif + QT_RES_ICONS = \ qt/res/icons/address-book.png \ qt/res/icons/connect1_16.png \ @@ -331,12 +341,18 @@ BITCOIN_QT_WALLET_CPP = \ qt/walletmodeltransaction.cpp \ qt/walletview.cpp +BITCOIN_QT_PLATFORM_CPP = \ + qt/platform/platformpage.cpp + BITCOIN_QT_CPP = $(BITCOIN_QT_BASE_CPP) if TARGET_WINDOWS BITCOIN_QT_CPP += $(BITCOIN_QT_WINDOWS_CPP) endif if ENABLE_WALLET BITCOIN_QT_CPP += $(BITCOIN_QT_WALLET_CPP) +if ENABLE_PLATFORM_GUI +BITCOIN_QT_CPP += $(BITCOIN_QT_PLATFORM_CPP) +endif # ENABLE_PLATFORM_GUI endif # ENABLE_WALLET QT_RES_IMAGES = \ @@ -461,6 +477,9 @@ endif if ENABLE_ZMQ bitcoin_qt_ldadd += $(LIBBITCOIN_ZMQ) $(ZMQ_LIBS) endif +if ENABLE_PLATFORM_GUI +bitcoin_qt_ldadd += $(LIBDASH_PLATFORM) $(MBEDTLS_LIBS) +endif bitcoin_qt_ldadd += $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON) $(LIBBITCOIN_UTIL) $(LIBBITCOIN_CONSENSUS) $(LIBBITCOIN_CRYPTO) $(LIBDASHBLS) $(LIBUNIVALUE) $(LIBLEVELDB) $(LIBMEMENV) \ $(BACKTRACE_LIBS) $(QT_LIBS) $(QT_DBUS_LIBS) $(QR_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(LIBSECP256K1) \ $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) @@ -492,7 +511,7 @@ $(srcdir)/qt/dashstrings.cpp: FORCE # The resulted dash_en.xlf source file should follow Transifex requirements. # See: https://docs.transifex.com/formats/xliff#how-to-distinguish-between-a-source-file-and-a-translation-file -translate: $(srcdir)/qt/dashstrings.cpp $(QT_FORMS_UI) $(QT_FORMS_UI) $(BITCOIN_QT_BASE_CPP) qt/bitcoin.cpp $(BITCOIN_QT_WINDOWS_CPP) $(BITCOIN_QT_WALLET_CPP) $(BITCOIN_QT_H) $(BITCOIN_MM) +translate: $(srcdir)/qt/dashstrings.cpp $(QT_FORMS_UI) $(QT_FORMS_UI) $(BITCOIN_QT_BASE_CPP) qt/bitcoin.cpp $(BITCOIN_QT_WINDOWS_CPP) $(BITCOIN_QT_WALLET_CPP) $(BITCOIN_QT_PLATFORM_CPP) $(BITCOIN_QT_H) $(BITCOIN_MM) @test -n $(LUPDATE) || echo "lupdate is required for updating translations" $(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(LUPDATE) -no-obsolete -I $(srcdir) -locations relative $^ -ts $(srcdir)/qt/locale/dash_en.ts @test -n $(LCONVERT) || echo "lconvert is required for updating translations" @@ -528,6 +547,7 @@ ui_%.h: %.ui $(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(MOC) $(DEFAULT_INCLUDES) $(QT_INCLUDES_UNSUPPRESSED) $(MOC_DEFS) $< > $@ moc_%.cpp: %.h + @$(MKDIR_P) $(@D) $(AM_V_GEN) QT_SELECT=$(QT_SELECT) $(MOC) $(DEFAULT_INCLUDES) $(QT_INCLUDES_UNSUPPRESSED) $(MOC_DEFS) $< > $@ %.qm: %.ts diff --git a/src/Makefile.qttest.include b/src/Makefile.qttest.include index 62071d40e5a5..9a0d1ffab488 100644 --- a/src/Makefile.qttest.include +++ b/src/Makefile.qttest.include @@ -58,6 +58,9 @@ endif if ENABLE_ZMQ qt_test_test_dash_qt_LDADD += $(LIBBITCOIN_ZMQ) $(ZMQ_LIBS) endif +if ENABLE_PLATFORM_GUI +qt_test_test_dash_qt_LDADD += $(LIBDASH_PLATFORM) $(MBEDTLS_LIBS) +endif qt_test_test_dash_qt_LDADD += $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON) $(LIBBITCOIN_UTIL) $(LIBBITCOIN_CONSENSUS) $(LIBBITCOIN_CRYPTO) $(LIBDASHBLS) $(LIBUNIVALUE) $(LIBLEVELDB) \ $(LIBMEMENV) $(BACKTRACE_LIBS) $(QT_LIBS) $(QT_DBUS_LIBS) $(QT_TEST_LIBS) \ $(QR_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(LIBSECP256K1) \ diff --git a/src/crypto/blake3/LICENSE b/src/crypto/blake3/LICENSE new file mode 100644 index 000000000000..d512ca94d0ca --- /dev/null +++ b/src/crypto/blake3/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Jack O'Connor and Samuel Neves + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/crypto/blake3/README.md b/src/crypto/blake3/README.md new file mode 100644 index 000000000000..7c6c93d33a34 --- /dev/null +++ b/src/crypto/blake3/README.md @@ -0,0 +1,17 @@ +# BLAKE3 (vendored) + +Portable C implementation of BLAKE3, vendored from +https://github.com/BLAKE3-team/BLAKE3 at tag `1.8.5` (`c/` directory). + +Only the portable backend is vendored (`blake3.c`, `blake3_dispatch.c`, +`blake3_portable.c`); it is compiled with the `BLAKE3_NO_SSE2`, +`BLAKE3_NO_SSE41`, `BLAKE3_NO_AVX2`, `BLAKE3_NO_AVX512` and (implicitly, by not +defining `BLAKE3_USE_NEON`) no-NEON configuration, so no SIMD sources are +required. BLAKE3 is used exclusively by the Dash Platform GUI client library +(`--enable-platform-gui`) for GroveDB merk proof verification; it is not linked +into `dashd` or any consensus code. + +Do not modify these files. To update, re-copy from a tagged upstream release +and update this README and the build definitions in `src/Makefile.am`. + +License: Apache-2.0 OR CC0-1.0 (see `LICENSE`). diff --git a/src/crypto/blake3/blake3.c b/src/crypto/blake3/blake3.c new file mode 100644 index 000000000000..00f91f444922 --- /dev/null +++ b/src/crypto/blake3/blake3.c @@ -0,0 +1,651 @@ +#include +#include +#include +#include + +#include "blake3.h" +#include "blake3_impl.h" + +const char *blake3_version(void) { return BLAKE3_VERSION_STRING; } + +INLINE void chunk_state_init(blake3_chunk_state *self, const uint32_t key[8], + uint8_t flags) { + memcpy(self->cv, key, BLAKE3_KEY_LEN); + self->chunk_counter = 0; + memset(self->buf, 0, BLAKE3_BLOCK_LEN); + self->buf_len = 0; + self->blocks_compressed = 0; + self->flags = flags; +} + +INLINE void chunk_state_reset(blake3_chunk_state *self, const uint32_t key[8], + uint64_t chunk_counter) { + memcpy(self->cv, key, BLAKE3_KEY_LEN); + self->chunk_counter = chunk_counter; + self->blocks_compressed = 0; + memset(self->buf, 0, BLAKE3_BLOCK_LEN); + self->buf_len = 0; +} + +INLINE size_t chunk_state_len(const blake3_chunk_state *self) { + return (BLAKE3_BLOCK_LEN * (size_t)self->blocks_compressed) + + ((size_t)self->buf_len); +} + +INLINE size_t chunk_state_fill_buf(blake3_chunk_state *self, + const uint8_t *input, size_t input_len) { + size_t take = BLAKE3_BLOCK_LEN - ((size_t)self->buf_len); + if (take > input_len) { + take = input_len; + } + uint8_t *dest = self->buf + ((size_t)self->buf_len); + memcpy(dest, input, take); + self->buf_len += (uint8_t)take; + return take; +} + +INLINE uint8_t chunk_state_maybe_start_flag(const blake3_chunk_state *self) { + if (self->blocks_compressed == 0) { + return CHUNK_START; + } else { + return 0; + } +} + +typedef struct { + uint32_t input_cv[8]; + uint64_t counter; + uint8_t block[BLAKE3_BLOCK_LEN]; + uint8_t block_len; + uint8_t flags; +} output_t; + +INLINE output_t make_output(const uint32_t input_cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags) { + output_t ret; + memcpy(ret.input_cv, input_cv, 32); + memcpy(ret.block, block, BLAKE3_BLOCK_LEN); + ret.block_len = block_len; + ret.counter = counter; + ret.flags = flags; + return ret; +} + +// Chaining values within a given chunk (specifically the compress_in_place +// interface) are represented as words. This avoids unnecessary bytes<->words +// conversion overhead in the portable implementation. However, the hash_many +// interface handles both user input and parent node blocks, so it accepts +// bytes. For that reason, chaining values in the CV stack are represented as +// bytes. +INLINE void output_chaining_value(const output_t *self, uint8_t cv[32]) { + uint32_t cv_words[8]; + memcpy(cv_words, self->input_cv, 32); + blake3_compress_in_place(cv_words, self->block, self->block_len, + self->counter, self->flags); + store_cv_words(cv, cv_words); +} + +INLINE void output_root_bytes(const output_t *self, uint64_t seek, uint8_t *out, + size_t out_len) { + if (out_len == 0) { + return; + } + uint64_t output_block_counter = seek / 64; + size_t offset_within_block = seek % 64; + uint8_t wide_buf[64]; + if(offset_within_block) { + blake3_compress_xof(self->input_cv, self->block, self->block_len, output_block_counter, self->flags | ROOT, wide_buf); + const size_t available_bytes = 64 - offset_within_block; + const size_t bytes = out_len > available_bytes ? available_bytes : out_len; + memcpy(out, wide_buf + offset_within_block, bytes); + out += bytes; + out_len -= bytes; + output_block_counter += 1; + } + if(out_len / 64) { + blake3_xof_many(self->input_cv, self->block, self->block_len, output_block_counter, self->flags | ROOT, out, out_len / 64); + } + output_block_counter += out_len / 64; + out += out_len & -64; + out_len -= out_len & -64; + if(out_len) { + blake3_compress_xof(self->input_cv, self->block, self->block_len, output_block_counter, self->flags | ROOT, wide_buf); + memcpy(out, wide_buf, out_len); + } +} + +INLINE void chunk_state_update(blake3_chunk_state *self, const uint8_t *input, + size_t input_len) { + if (self->buf_len > 0) { + size_t take = chunk_state_fill_buf(self, input, input_len); + input += take; + input_len -= take; + if (input_len > 0) { + blake3_compress_in_place( + self->cv, self->buf, BLAKE3_BLOCK_LEN, self->chunk_counter, + self->flags | chunk_state_maybe_start_flag(self)); + self->blocks_compressed += 1; + self->buf_len = 0; + memset(self->buf, 0, BLAKE3_BLOCK_LEN); + } + } + + while (input_len > BLAKE3_BLOCK_LEN) { + blake3_compress_in_place(self->cv, input, BLAKE3_BLOCK_LEN, + self->chunk_counter, + self->flags | chunk_state_maybe_start_flag(self)); + self->blocks_compressed += 1; + input += BLAKE3_BLOCK_LEN; + input_len -= BLAKE3_BLOCK_LEN; + } + + chunk_state_fill_buf(self, input, input_len); +} + +INLINE output_t chunk_state_output(const blake3_chunk_state *self) { + uint8_t block_flags = + self->flags | chunk_state_maybe_start_flag(self) | CHUNK_END; + return make_output(self->cv, self->buf, self->buf_len, self->chunk_counter, + block_flags); +} + +INLINE output_t parent_output(const uint8_t block[BLAKE3_BLOCK_LEN], + const uint32_t key[8], uint8_t flags) { + return make_output(key, block, BLAKE3_BLOCK_LEN, 0, flags | PARENT); +} + +// Given some input larger than one chunk, return the number of bytes that +// should go in the left subtree. This is the largest power-of-2 number of +// chunks that leaves at least 1 byte for the right subtree. +INLINE size_t left_subtree_len(size_t input_len) { + // Subtract 1 to reserve at least one byte for the right side. input_len + // should always be greater than BLAKE3_CHUNK_LEN. + size_t full_chunks = (input_len - 1) / BLAKE3_CHUNK_LEN; + return round_down_to_power_of_2(full_chunks) * BLAKE3_CHUNK_LEN; +} + +// Use SIMD parallelism to hash up to MAX_SIMD_DEGREE chunks at the same time +// on a single thread. Write out the chunk chaining values and return the +// number of chunks hashed. These chunks are never the root and never empty; +// those cases use a different codepath. +INLINE size_t compress_chunks_parallel(const uint8_t *input, size_t input_len, + const uint32_t key[8], + uint64_t chunk_counter, uint8_t flags, + uint8_t *out) { +#if defined(BLAKE3_TESTING) + assert(0 < input_len); + assert(input_len <= MAX_SIMD_DEGREE * BLAKE3_CHUNK_LEN); +#endif + + const uint8_t *chunks_array[MAX_SIMD_DEGREE]; + size_t input_position = 0; + size_t chunks_array_len = 0; + while (input_len - input_position >= BLAKE3_CHUNK_LEN) { + chunks_array[chunks_array_len] = &input[input_position]; + input_position += BLAKE3_CHUNK_LEN; + chunks_array_len += 1; + } + + blake3_hash_many(chunks_array, chunks_array_len, + BLAKE3_CHUNK_LEN / BLAKE3_BLOCK_LEN, key, chunk_counter, + true, flags, CHUNK_START, CHUNK_END, out); + + // Hash the remaining partial chunk, if there is one. Note that the empty + // chunk (meaning the empty message) is a different codepath. + if (input_len > input_position) { + uint64_t counter = chunk_counter + (uint64_t)chunks_array_len; + blake3_chunk_state chunk_state; + chunk_state_init(&chunk_state, key, flags); + chunk_state.chunk_counter = counter; + chunk_state_update(&chunk_state, &input[input_position], + input_len - input_position); + output_t output = chunk_state_output(&chunk_state); + output_chaining_value(&output, &out[chunks_array_len * BLAKE3_OUT_LEN]); + return chunks_array_len + 1; + } else { + return chunks_array_len; + } +} + +// Use SIMD parallelism to hash up to MAX_SIMD_DEGREE parents at the same time +// on a single thread. Write out the parent chaining values and return the +// number of parents hashed. (If there's an odd input chaining value left over, +// return it as an additional output.) These parents are never the root and +// never empty; those cases use a different codepath. +INLINE size_t compress_parents_parallel(const uint8_t *child_chaining_values, + size_t num_chaining_values, + const uint32_t key[8], uint8_t flags, + uint8_t *out) { +#if defined(BLAKE3_TESTING) + assert(2 <= num_chaining_values); + assert(num_chaining_values <= 2 * MAX_SIMD_DEGREE_OR_2); +#endif + + const uint8_t *parents_array[MAX_SIMD_DEGREE_OR_2]; + size_t parents_array_len = 0; + while (num_chaining_values - (2 * parents_array_len) >= 2) { + parents_array[parents_array_len] = + &child_chaining_values[2 * parents_array_len * BLAKE3_OUT_LEN]; + parents_array_len += 1; + } + + blake3_hash_many(parents_array, parents_array_len, 1, key, + 0, // Parents always use counter 0. + false, flags | PARENT, + 0, // Parents have no start flags. + 0, // Parents have no end flags. + out); + + // If there's an odd child left over, it becomes an output. + if (num_chaining_values > 2 * parents_array_len) { + memcpy(&out[parents_array_len * BLAKE3_OUT_LEN], + &child_chaining_values[2 * parents_array_len * BLAKE3_OUT_LEN], + BLAKE3_OUT_LEN); + return parents_array_len + 1; + } else { + return parents_array_len; + } +} + +// The wide helper function returns (writes out) an array of chaining values +// and returns the length of that array. The number of chaining values returned +// is the dynamically detected SIMD degree, at most MAX_SIMD_DEGREE. Or fewer, +// if the input is shorter than that many chunks. The reason for maintaining a +// wide array of chaining values going back up the tree, is to allow the +// implementation to hash as many parents in parallel as possible. +// +// As a special case when the SIMD degree is 1, this function will still return +// at least 2 outputs. This guarantees that this function doesn't perform the +// root compression. (If it did, it would use the wrong flags, and also we +// wouldn't be able to implement extendable output.) Note that this function is +// not used when the whole input is only 1 chunk long; that's a different +// codepath. +// +// Why not just have the caller split the input on the first update(), instead +// of implementing this special rule? Because we don't want to limit SIMD or +// multi-threading parallelism for that update(). +size_t blake3_compress_subtree_wide(const uint8_t *input, size_t input_len, + const uint32_t key[8], + uint64_t chunk_counter, uint8_t flags, + uint8_t *out, bool use_tbb) { + // Note that the single chunk case does *not* bump the SIMD degree up to 2 + // when it is 1. If this implementation adds multi-threading in the future, + // this gives us the option of multi-threading even the 2-chunk case, which + // can help performance on smaller platforms. + if (input_len <= blake3_simd_degree() * BLAKE3_CHUNK_LEN) { + return compress_chunks_parallel(input, input_len, key, chunk_counter, flags, + out); + } + + // With more than simd_degree chunks, we need to recurse. Start by dividing + // the input into left and right subtrees. (Note that this is only optimal + // as long as the SIMD degree is a power of 2. If we ever get a SIMD degree + // of 3 or something, we'll need a more complicated strategy.) + size_t left_input_len = left_subtree_len(input_len); + size_t right_input_len = input_len - left_input_len; + const uint8_t *right_input = &input[left_input_len]; + uint64_t right_chunk_counter = + chunk_counter + (uint64_t)(left_input_len / BLAKE3_CHUNK_LEN); + + // Make space for the child outputs. Here we use MAX_SIMD_DEGREE_OR_2 to + // account for the special case of returning 2 outputs when the SIMD degree + // is 1. + uint8_t cv_array[2 * MAX_SIMD_DEGREE_OR_2 * BLAKE3_OUT_LEN]; + size_t degree = blake3_simd_degree(); + if (left_input_len > BLAKE3_CHUNK_LEN && degree == 1) { + // The special case: We always use a degree of at least two, to make + // sure there are two outputs. Except, as noted above, at the chunk + // level, where we allow degree=1. (Note that the 1-chunk-input case is + // a different codepath.) + degree = 2; + } + uint8_t *right_cvs = &cv_array[degree * BLAKE3_OUT_LEN]; + + // Recurse! + size_t left_n = SIZE_MAX; + size_t right_n = SIZE_MAX; + +#if defined(BLAKE3_USE_TBB) + blake3_compress_subtree_wide_join_tbb( + key, flags, use_tbb, + // left-hand side + input, left_input_len, chunk_counter, cv_array, &left_n, + // right-hand side + right_input, right_input_len, right_chunk_counter, right_cvs, &right_n); +#else + left_n = blake3_compress_subtree_wide( + input, left_input_len, key, chunk_counter, flags, cv_array, use_tbb); + right_n = blake3_compress_subtree_wide(right_input, right_input_len, key, + right_chunk_counter, flags, right_cvs, + use_tbb); +#endif // BLAKE3_USE_TBB + + // The special case again. If simd_degree=1, then we'll have left_n=1 and + // right_n=1. Rather than compressing them into a single output, return + // them directly, to make sure we always have at least two outputs. + if (left_n == 1) { + memcpy(out, cv_array, 2 * BLAKE3_OUT_LEN); + return 2; + } + + // Otherwise, do one layer of parent node compression. + size_t num_chaining_values = left_n + right_n; + return compress_parents_parallel(cv_array, num_chaining_values, key, flags, + out); +} + +// Hash a subtree with compress_subtree_wide(), and then condense the resulting +// list of chaining values down to a single parent node. Don't compress that +// last parent node, however. Instead, return its message bytes (the +// concatenated chaining values of its children). This is necessary when the +// first call to update() supplies a complete subtree, because the topmost +// parent node of that subtree could end up being the root. It's also necessary +// for extended output in the general case. +// +// As with compress_subtree_wide(), this function is not used on inputs of 1 +// chunk or less. That's a different codepath. +INLINE void +compress_subtree_to_parent_node(const uint8_t *input, size_t input_len, + const uint32_t key[8], uint64_t chunk_counter, + uint8_t flags, uint8_t out[2 * BLAKE3_OUT_LEN], + bool use_tbb) { +#if defined(BLAKE3_TESTING) + assert(input_len > BLAKE3_CHUNK_LEN); +#endif + + uint8_t cv_array[MAX_SIMD_DEGREE_OR_2 * BLAKE3_OUT_LEN]; + size_t num_cvs = blake3_compress_subtree_wide(input, input_len, key, + chunk_counter, flags, cv_array, use_tbb); + assert(num_cvs <= MAX_SIMD_DEGREE_OR_2); + // The following loop never executes when MAX_SIMD_DEGREE_OR_2 is 2, because + // as we just asserted, num_cvs will always be <=2 in that case. But GCC + // (particularly GCC 8.5) can't tell that it never executes, and if NDEBUG is + // set then it emits incorrect warnings here. We tried a few different + // hacks to silence these, but in the end our hacks just produced different + // warnings (see https://github.com/BLAKE3-team/BLAKE3/pull/380). Out of + // desperation, we ifdef out this entire loop when we know it's not needed. +#if MAX_SIMD_DEGREE_OR_2 > 2 + // If MAX_SIMD_DEGREE_OR_2 is greater than 2 and there's enough input, + // compress_subtree_wide() returns more than 2 chaining values. Condense + // them into 2 by forming parent nodes repeatedly. + uint8_t out_array[MAX_SIMD_DEGREE_OR_2 * BLAKE3_OUT_LEN / 2]; + while (num_cvs > 2) { + num_cvs = + compress_parents_parallel(cv_array, num_cvs, key, flags, out_array); + memcpy(cv_array, out_array, num_cvs * BLAKE3_OUT_LEN); + } +#endif + memcpy(out, cv_array, 2 * BLAKE3_OUT_LEN); +} + +INLINE void hasher_init_base(blake3_hasher *self, const uint32_t key[8], + uint8_t flags) { + memcpy(self->key, key, BLAKE3_KEY_LEN); + chunk_state_init(&self->chunk, key, flags); + self->cv_stack_len = 0; +} + +void blake3_hasher_init(blake3_hasher *self) { hasher_init_base(self, IV, 0); } + +void blake3_hasher_init_keyed(blake3_hasher *self, + const uint8_t key[BLAKE3_KEY_LEN]) { + uint32_t key_words[8]; + load_key_words(key, key_words); + hasher_init_base(self, key_words, KEYED_HASH); +} + +void blake3_hasher_init_derive_key_raw(blake3_hasher *self, const void *context, + size_t context_len) { + blake3_hasher context_hasher; + hasher_init_base(&context_hasher, IV, DERIVE_KEY_CONTEXT); + blake3_hasher_update(&context_hasher, context, context_len); + uint8_t context_key[BLAKE3_KEY_LEN]; + blake3_hasher_finalize(&context_hasher, context_key, BLAKE3_KEY_LEN); + uint32_t context_key_words[8]; + load_key_words(context_key, context_key_words); + hasher_init_base(self, context_key_words, DERIVE_KEY_MATERIAL); +} + +void blake3_hasher_init_derive_key(blake3_hasher *self, const char *context) { + blake3_hasher_init_derive_key_raw(self, context, strlen(context)); +} + +// As described in hasher_push_cv() below, we do "lazy merging", delaying +// merges until right before the next CV is about to be added. This is +// different from the reference implementation. Another difference is that we +// aren't always merging 1 chunk at a time. Instead, each CV might represent +// any power-of-two number of chunks, as long as the smaller-above-larger stack +// order is maintained. Instead of the "count the trailing 0-bits" algorithm +// described in the spec, we use a "count the total number of 1-bits" variant +// that doesn't require us to retain the subtree size of the CV on top of the +// stack. The principle is the same: each CV that should remain in the stack is +// represented by a 1-bit in the total number of chunks (or bytes) so far. +INLINE void hasher_merge_cv_stack(blake3_hasher *self, uint64_t total_len) { + size_t post_merge_stack_len = (size_t)popcnt(total_len); + while (self->cv_stack_len > post_merge_stack_len) { + uint8_t *parent_node = + &self->cv_stack[(self->cv_stack_len - 2) * BLAKE3_OUT_LEN]; + output_t output = parent_output(parent_node, self->key, self->chunk.flags); + output_chaining_value(&output, parent_node); + self->cv_stack_len -= 1; + } +} + +// In reference_impl.rs, we merge the new CV with existing CVs from the stack +// before pushing it. We can do that because we know more input is coming, so +// we know none of the merges are root. +// +// This setting is different. We want to feed as much input as possible to +// compress_subtree_wide(), without setting aside anything for the chunk_state. +// If the user gives us 64 KiB, we want to parallelize over all 64 KiB at once +// as a single subtree, if at all possible. +// +// This leads to two problems: +// 1) This 64 KiB input might be the only call that ever gets made to update. +// In this case, the root node of the 64 KiB subtree would be the root node +// of the whole tree, and it would need to be ROOT finalized. We can't +// compress it until we know. +// 2) This 64 KiB input might complete a larger tree, whose root node is +// similarly going to be the root of the whole tree. For example, maybe +// we have 196 KiB (that is, 128 + 64) hashed so far. We can't compress the +// node at the root of the 256 KiB subtree until we know how to finalize it. +// +// The second problem is solved with "lazy merging". That is, when we're about +// to add a CV to the stack, we don't merge it with anything first, as the +// reference impl does. Instead we do merges using the *previous* CV that was +// added, which is sitting on top of the stack, and we put the new CV +// (unmerged) on top of the stack afterwards. This guarantees that we never +// merge the root node until finalize(). +// +// Solving the first problem requires an additional tool, +// compress_subtree_to_parent_node(). That function always returns the top +// *two* chaining values of the subtree it's compressing. We then do lazy +// merging with each of them separately, so that the second CV will always +// remain unmerged. (That also helps us support extendable output when we're +// hashing an input all-at-once.) +INLINE void hasher_push_cv(blake3_hasher *self, uint8_t new_cv[BLAKE3_OUT_LEN], + uint64_t chunk_counter) { + hasher_merge_cv_stack(self, chunk_counter); + memcpy(&self->cv_stack[self->cv_stack_len * BLAKE3_OUT_LEN], new_cv, + BLAKE3_OUT_LEN); + self->cv_stack_len += 1; +} + +INLINE void blake3_hasher_update_base(blake3_hasher *self, const void *input, + size_t input_len, bool use_tbb) { + // Explicitly checking for zero avoids causing UB by passing a null pointer + // to memcpy. This comes up in practice with things like: + // std::vector v; + // blake3_hasher_update(&hasher, v.data(), v.size()); + if (input_len == 0) { + return; + } + + const uint8_t *input_bytes = (const uint8_t *)input; + + // If we have some partial chunk bytes in the internal chunk_state, we need + // to finish that chunk first. + if (chunk_state_len(&self->chunk) > 0) { + size_t take = BLAKE3_CHUNK_LEN - chunk_state_len(&self->chunk); + if (take > input_len) { + take = input_len; + } + chunk_state_update(&self->chunk, input_bytes, take); + input_bytes += take; + input_len -= take; + // If we've filled the current chunk and there's more coming, finalize this + // chunk and proceed. In this case we know it's not the root. + if (input_len > 0) { + output_t output = chunk_state_output(&self->chunk); + uint8_t chunk_cv[32]; + output_chaining_value(&output, chunk_cv); + hasher_push_cv(self, chunk_cv, self->chunk.chunk_counter); + chunk_state_reset(&self->chunk, self->key, self->chunk.chunk_counter + 1); + } else { + return; + } + } + + // Now the chunk_state is clear, and we have more input. If there's more than + // a single chunk (so, definitely not the root chunk), hash the largest whole + // subtree we can, with the full benefits of SIMD (and maybe in the future, + // multi-threading) parallelism. Two restrictions: + // - The subtree has to be a power-of-2 number of chunks. Only subtrees along + // the right edge can be incomplete, and we don't know where the right edge + // is going to be until we get to finalize(). + // - The subtree must evenly divide the total number of chunks up until this + // point (if total is not 0). If the current incomplete subtree is only + // waiting for 1 more chunk, we can't hash a subtree of 4 chunks. We have + // to complete the current subtree first. + // Because we might need to break up the input to form powers of 2, or to + // evenly divide what we already have, this part runs in a loop. + while (input_len > BLAKE3_CHUNK_LEN) { + size_t subtree_len = round_down_to_power_of_2(input_len); + uint64_t count_so_far = self->chunk.chunk_counter * BLAKE3_CHUNK_LEN; + // Shrink the subtree_len until it evenly divides the count so far. We know + // that subtree_len itself is a power of 2, so we can use a bitmasking + // trick instead of an actual remainder operation. (Note that if the caller + // consistently passes power-of-2 inputs of the same size, as is hopefully + // typical, this loop condition will always fail, and subtree_len will + // always be the full length of the input.) + // + // An aside: We don't have to shrink subtree_len quite this much. For + // example, if count_so_far is 1, we could pass 2 chunks to + // compress_subtree_to_parent_node. Since we'll get 2 CVs back, we'll still + // get the right answer in the end, and we might get to use 2-way SIMD + // parallelism. The problem with this optimization, is that it gets us + // stuck always hashing 2 chunks. The total number of chunks will remain + // odd, and we'll never graduate to higher degrees of parallelism. See + // https://github.com/BLAKE3-team/BLAKE3/issues/69. + while ((((uint64_t)(subtree_len - 1)) & count_so_far) != 0) { + subtree_len /= 2; + } + // The shrunken subtree_len might now be 1 chunk long. If so, hash that one + // chunk by itself. Otherwise, compress the subtree into a pair of CVs. + uint64_t subtree_chunks = subtree_len / BLAKE3_CHUNK_LEN; + if (subtree_len <= BLAKE3_CHUNK_LEN) { + blake3_chunk_state chunk_state; + chunk_state_init(&chunk_state, self->key, self->chunk.flags); + chunk_state.chunk_counter = self->chunk.chunk_counter; + chunk_state_update(&chunk_state, input_bytes, subtree_len); + output_t output = chunk_state_output(&chunk_state); + uint8_t cv[BLAKE3_OUT_LEN]; + output_chaining_value(&output, cv); + hasher_push_cv(self, cv, chunk_state.chunk_counter); + } else { + // This is the high-performance happy path, though getting here depends + // on the caller giving us a long enough input. + uint8_t cv_pair[2 * BLAKE3_OUT_LEN]; + compress_subtree_to_parent_node(input_bytes, subtree_len, self->key, + self->chunk.chunk_counter, + self->chunk.flags, cv_pair, use_tbb); + hasher_push_cv(self, cv_pair, self->chunk.chunk_counter); + hasher_push_cv(self, &cv_pair[BLAKE3_OUT_LEN], + self->chunk.chunk_counter + (subtree_chunks / 2)); + } + self->chunk.chunk_counter += subtree_chunks; + input_bytes += subtree_len; + input_len -= subtree_len; + } + + // If there's any remaining input less than a full chunk, add it to the chunk + // state. In that case, also do a final merge loop to make sure the subtree + // stack doesn't contain any unmerged pairs. The remaining input means we + // know these merges are non-root. This merge loop isn't strictly necessary + // here, because hasher_push_chunk_cv already does its own merge loop, but it + // simplifies blake3_hasher_finalize below. + if (input_len > 0) { + chunk_state_update(&self->chunk, input_bytes, input_len); + hasher_merge_cv_stack(self, self->chunk.chunk_counter); + } +} + +void blake3_hasher_update(blake3_hasher *self, const void *input, + size_t input_len) { + bool use_tbb = false; + blake3_hasher_update_base(self, input, input_len, use_tbb); +} + +#if defined(BLAKE3_USE_TBB) +void blake3_hasher_update_tbb(blake3_hasher *self, const void *input, + size_t input_len) { + bool use_tbb = true; + blake3_hasher_update_base(self, input, input_len, use_tbb); +} +#endif // BLAKE3_USE_TBB + +void blake3_hasher_finalize(const blake3_hasher *self, uint8_t *out, + size_t out_len) { + blake3_hasher_finalize_seek(self, 0, out, out_len); +} + +void blake3_hasher_finalize_seek(const blake3_hasher *self, uint64_t seek, + uint8_t *out, size_t out_len) { + // Explicitly checking for zero avoids causing UB by passing a null pointer + // to memcpy. This comes up in practice with things like: + // std::vector v; + // blake3_hasher_finalize(&hasher, v.data(), v.size()); + if (out_len == 0) { + return; + } + + // If the subtree stack is empty, then the current chunk is the root. + if (self->cv_stack_len == 0) { + output_t output = chunk_state_output(&self->chunk); + output_root_bytes(&output, seek, out, out_len); + return; + } + // If there are any bytes in the chunk state, finalize that chunk and do a + // roll-up merge between that chunk hash and every subtree in the stack. In + // this case, the extra merge loop at the end of blake3_hasher_update + // guarantees that none of the subtrees in the stack need to be merged with + // each other first. Otherwise, if there are no bytes in the chunk state, + // then the top of the stack is a chunk hash, and we start the merge from + // that. + output_t output; + size_t cvs_remaining; + if (chunk_state_len(&self->chunk) > 0) { + cvs_remaining = self->cv_stack_len; + output = chunk_state_output(&self->chunk); + } else { + // There are always at least 2 CVs in the stack in this case. + cvs_remaining = self->cv_stack_len - 2; + output = parent_output(&self->cv_stack[cvs_remaining * 32], self->key, + self->chunk.flags); + } + while (cvs_remaining > 0) { + cvs_remaining -= 1; + uint8_t parent_block[BLAKE3_BLOCK_LEN]; + memcpy(parent_block, &self->cv_stack[cvs_remaining * 32], 32); + output_chaining_value(&output, &parent_block[32]); + output = parent_output(parent_block, self->key, self->chunk.flags); + } + output_root_bytes(&output, seek, out, out_len); +} + +void blake3_hasher_reset(blake3_hasher *self) { + chunk_state_reset(&self->chunk, self->key, 0); + self->cv_stack_len = 0; +} diff --git a/src/crypto/blake3/blake3.h b/src/crypto/blake3/blake3.h new file mode 100644 index 000000000000..423154ff7585 --- /dev/null +++ b/src/crypto/blake3/blake3.h @@ -0,0 +1,86 @@ +#ifndef BLAKE3_H +#define BLAKE3_H + +#include +#include + +#if !defined(BLAKE3_API) +# if defined(_WIN32) || defined(__CYGWIN__) +# if defined(BLAKE3_DLL) +# if defined(BLAKE3_DLL_EXPORTS) +# define BLAKE3_API __declspec(dllexport) +# else +# define BLAKE3_API __declspec(dllimport) +# endif +# define BLAKE3_PRIVATE +# else +# define BLAKE3_API +# define BLAKE3_PRIVATE +# endif +# elif __GNUC__ >= 4 +# define BLAKE3_API __attribute__((visibility("default"))) +# define BLAKE3_PRIVATE __attribute__((visibility("hidden"))) +# else +# define BLAKE3_API +# define BLAKE3_PRIVATE +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define BLAKE3_VERSION_STRING "1.8.5" +#define BLAKE3_KEY_LEN 32 +#define BLAKE3_OUT_LEN 32 +#define BLAKE3_BLOCK_LEN 64 +#define BLAKE3_CHUNK_LEN 1024 +#define BLAKE3_MAX_DEPTH 54 + +// This struct is a private implementation detail. It has to be here because +// it's part of the blake3_hasher structure defined below. +typedef struct { + uint32_t cv[8]; + uint64_t chunk_counter; + uint8_t buf[BLAKE3_BLOCK_LEN]; + uint8_t buf_len; + uint8_t blocks_compressed; + uint8_t flags; +} blake3_chunk_state; + +typedef struct { + uint32_t key[8]; + blake3_chunk_state chunk; + uint8_t cv_stack_len; + // The stack size is MAX_DEPTH + 1 because we do lazy merging. For example, + // with 7 chunks, we have 3 entries in the stack. Adding an 8th chunk + // requires a 4th entry, rather than merging everything down to 1, because we + // don't know whether more input is coming. This is different from how the + // reference implementation does things. + uint8_t cv_stack[(BLAKE3_MAX_DEPTH + 1) * BLAKE3_OUT_LEN]; +} blake3_hasher; + +BLAKE3_API const char *blake3_version(void); +BLAKE3_API void blake3_hasher_init(blake3_hasher *self); +BLAKE3_API void blake3_hasher_init_keyed(blake3_hasher *self, + const uint8_t key[BLAKE3_KEY_LEN]); +BLAKE3_API void blake3_hasher_init_derive_key(blake3_hasher *self, const char *context); +BLAKE3_API void blake3_hasher_init_derive_key_raw(blake3_hasher *self, const void *context, + size_t context_len); +BLAKE3_API void blake3_hasher_update(blake3_hasher *self, const void *input, + size_t input_len); +#if defined(BLAKE3_USE_TBB) +BLAKE3_API void blake3_hasher_update_tbb(blake3_hasher *self, const void *input, + size_t input_len); +#endif // BLAKE3_USE_TBB +BLAKE3_API void blake3_hasher_finalize(const blake3_hasher *self, uint8_t *out, + size_t out_len); +BLAKE3_API void blake3_hasher_finalize_seek(const blake3_hasher *self, uint64_t seek, + uint8_t *out, size_t out_len); +BLAKE3_API void blake3_hasher_reset(blake3_hasher *self); + +#ifdef __cplusplus +} +#endif + +#endif /* BLAKE3_H */ diff --git a/src/crypto/blake3/blake3_dispatch.c b/src/crypto/blake3/blake3_dispatch.c new file mode 100644 index 000000000000..14dfbbe0c8f3 --- /dev/null +++ b/src/crypto/blake3/blake3_dispatch.c @@ -0,0 +1,332 @@ +#include +#include +#include + +#include "blake3_impl.h" + +#if defined(_MSC_VER) +#include +#endif + +#if defined(IS_X86) +#if defined(_MSC_VER) +#include +#elif defined(__GNUC__) +#include +#else +#undef IS_X86 /* Unimplemented! */ +#endif +#endif + +#if !defined(BLAKE3_ATOMICS) +#if defined(__has_include) +#if __has_include() && !defined(_MSC_VER) +#define BLAKE3_ATOMICS 1 +#else +#define BLAKE3_ATOMICS 0 +#endif /* __has_include() && !defined(_MSC_VER) */ +#else +#define BLAKE3_ATOMICS 0 +#endif /* defined(__has_include) */ +#endif /* BLAKE3_ATOMICS */ + +#if BLAKE3_ATOMICS +#define ATOMIC_INT _Atomic int +#define ATOMIC_LOAD(x) x +#define ATOMIC_STORE(x, y) x = y +#elif defined(_MSC_VER) +#define ATOMIC_INT LONG +#define ATOMIC_LOAD(x) InterlockedOr(&x, 0) +#define ATOMIC_STORE(x, y) InterlockedExchange(&x, y) +#else +#define ATOMIC_INT int +#define ATOMIC_LOAD(x) x +#define ATOMIC_STORE(x, y) x = y +#endif + +#define MAYBE_UNUSED(x) (void)((x)) + +#if defined(IS_X86) +static uint64_t xgetbv(void) { +#if defined(_MSC_VER) + return _xgetbv(0); +#else + uint32_t eax = 0, edx = 0; + __asm__ __volatile__("xgetbv\n" : "=a"(eax), "=d"(edx) : "c"(0)); + return ((uint64_t)edx << 32) | eax; +#endif +} + +static void cpuid(uint32_t out[4], uint32_t id) { +#if defined(_MSC_VER) + __cpuid((int *)out, id); +#elif defined(__i386__) || defined(_M_IX86) + __asm__ __volatile__("movl %%ebx, %1\n" + "cpuid\n" + "xchgl %1, %%ebx\n" + : "=a"(out[0]), "=r"(out[1]), "=c"(out[2]), "=d"(out[3]) + : "a"(id)); +#else + __asm__ __volatile__("cpuid\n" + : "=a"(out[0]), "=b"(out[1]), "=c"(out[2]), "=d"(out[3]) + : "a"(id)); +#endif +} + +static void cpuidex(uint32_t out[4], uint32_t id, uint32_t sid) { +#if defined(_MSC_VER) + __cpuidex((int *)out, id, sid); +#elif defined(__i386__) || defined(_M_IX86) + __asm__ __volatile__("movl %%ebx, %1\n" + "cpuid\n" + "xchgl %1, %%ebx\n" + : "=a"(out[0]), "=r"(out[1]), "=c"(out[2]), "=d"(out[3]) + : "a"(id), "c"(sid)); +#else + __asm__ __volatile__("cpuid\n" + : "=a"(out[0]), "=b"(out[1]), "=c"(out[2]), "=d"(out[3]) + : "a"(id), "c"(sid)); +#endif +} + + +enum cpu_feature { + SSE2 = 1 << 0, + SSSE3 = 1 << 1, + SSE41 = 1 << 2, + AVX = 1 << 3, + AVX2 = 1 << 4, + AVX512F = 1 << 5, + AVX512VL = 1 << 6, + /* ... */ + UNDEFINED = 1 << 30 +}; + +#if !defined(BLAKE3_TESTING) +static /* Allow the variable to be controlled manually for testing */ +#endif + ATOMIC_INT g_cpu_features = UNDEFINED; + +#if !defined(BLAKE3_TESTING) +static +#endif + enum cpu_feature + get_cpu_features(void) { + + /* If TSAN detects a data race here, try compiling with -DBLAKE3_ATOMICS=1 */ + enum cpu_feature features = ATOMIC_LOAD(g_cpu_features); + if (features != UNDEFINED) { + return features; + } else { +#if defined(IS_X86) + uint32_t regs[4] = {0}; + uint32_t *eax = ®s[0], *ebx = ®s[1], *ecx = ®s[2], *edx = ®s[3]; + (void)edx; + features = 0; + cpuid(regs, 0); + const int max_id = *eax; + cpuid(regs, 1); +#if defined(__amd64__) || defined(_M_X64) + features |= SSE2; +#else + if (*edx & (1UL << 26)) + features |= SSE2; +#endif + if (*ecx & (1UL << 9)) + features |= SSSE3; + if (*ecx & (1UL << 19)) + features |= SSE41; + + if (*ecx & (1UL << 27)) { // OSXSAVE + const uint64_t mask = xgetbv(); + if ((mask & 6) == 6) { // SSE and AVX states + if (*ecx & (1UL << 28)) + features |= AVX; + if (max_id >= 7) { + cpuidex(regs, 7, 0); + if (*ebx & (1UL << 5)) + features |= AVX2; + if ((mask & 224) == 224) { // Opmask, ZMM_Hi256, Hi16_Zmm + if (*ebx & (1UL << 31)) + features |= AVX512VL; + if (*ebx & (1UL << 16)) + features |= AVX512F; + } + } + } + } + ATOMIC_STORE(g_cpu_features, features); + return features; +#else + /* How to detect NEON? */ + return 0; +#endif + } +} +#endif + +void blake3_compress_in_place(uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags) { +#if defined(IS_X86) + const enum cpu_feature features = get_cpu_features(); + MAYBE_UNUSED(features); +#if !defined(BLAKE3_NO_AVX512) + if (features & AVX512VL) { + blake3_compress_in_place_avx512(cv, block, block_len, counter, flags); + return; + } +#endif +#if !defined(BLAKE3_NO_SSE41) + if (features & SSE41) { + blake3_compress_in_place_sse41(cv, block, block_len, counter, flags); + return; + } +#endif +#if !defined(BLAKE3_NO_SSE2) + if (features & SSE2) { + blake3_compress_in_place_sse2(cv, block, block_len, counter, flags); + return; + } +#endif +#endif + blake3_compress_in_place_portable(cv, block, block_len, counter, flags); +} + +void blake3_compress_xof(const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, uint8_t flags, + uint8_t out[64]) { +#if defined(IS_X86) + const enum cpu_feature features = get_cpu_features(); + MAYBE_UNUSED(features); +#if !defined(BLAKE3_NO_AVX512) + if (features & AVX512VL) { + blake3_compress_xof_avx512(cv, block, block_len, counter, flags, out); + return; + } +#endif +#if !defined(BLAKE3_NO_SSE41) + if (features & SSE41) { + blake3_compress_xof_sse41(cv, block, block_len, counter, flags, out); + return; + } +#endif +#if !defined(BLAKE3_NO_SSE2) + if (features & SSE2) { + blake3_compress_xof_sse2(cv, block, block_len, counter, flags, out); + return; + } +#endif +#endif + blake3_compress_xof_portable(cv, block, block_len, counter, flags, out); +} + + +void blake3_xof_many(const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, uint8_t flags, + uint8_t out[64], size_t outblocks) { + if (outblocks == 0) { + // The current assembly implementation always outputs at least 1 block. + return; + } +#if defined(IS_X86) + const enum cpu_feature features = get_cpu_features(); + MAYBE_UNUSED(features); +#if !defined(_WIN32) && !defined(__CYGWIN__) && !defined(BLAKE3_NO_AVX512) + if (features & AVX512VL) { + blake3_xof_many_avx512(cv, block, block_len, counter, flags, out, outblocks); + return; + } +#endif +#endif + for(size_t i = 0; i < outblocks; ++i) { + blake3_compress_xof(cv, block, block_len, counter + i, flags, out + 64*i); + } +} + +void blake3_hash_many(const uint8_t *const *inputs, size_t num_inputs, + size_t blocks, const uint32_t key[8], uint64_t counter, + bool increment_counter, uint8_t flags, + uint8_t flags_start, uint8_t flags_end, uint8_t *out) { +#if defined(IS_X86) + const enum cpu_feature features = get_cpu_features(); + MAYBE_UNUSED(features); +#if !defined(BLAKE3_NO_AVX512) + if ((features & (AVX512F|AVX512VL)) == (AVX512F|AVX512VL)) { + blake3_hash_many_avx512(inputs, num_inputs, blocks, key, counter, + increment_counter, flags, flags_start, flags_end, + out); + return; + } +#endif +#if !defined(BLAKE3_NO_AVX2) + if (features & AVX2) { + blake3_hash_many_avx2(inputs, num_inputs, blocks, key, counter, + increment_counter, flags, flags_start, flags_end, + out); + return; + } +#endif +#if !defined(BLAKE3_NO_SSE41) + if (features & SSE41) { + blake3_hash_many_sse41(inputs, num_inputs, blocks, key, counter, + increment_counter, flags, flags_start, flags_end, + out); + return; + } +#endif +#if !defined(BLAKE3_NO_SSE2) + if (features & SSE2) { + blake3_hash_many_sse2(inputs, num_inputs, blocks, key, counter, + increment_counter, flags, flags_start, flags_end, + out); + return; + } +#endif +#endif + +#if BLAKE3_USE_NEON == 1 + blake3_hash_many_neon(inputs, num_inputs, blocks, key, counter, + increment_counter, flags, flags_start, flags_end, out); + return; +#endif + + blake3_hash_many_portable(inputs, num_inputs, blocks, key, counter, + increment_counter, flags, flags_start, flags_end, + out); +} + +// The dynamically detected SIMD degree of the current platform. +size_t blake3_simd_degree(void) { +#if defined(IS_X86) + const enum cpu_feature features = get_cpu_features(); + MAYBE_UNUSED(features); +#if !defined(BLAKE3_NO_AVX512) + if ((features & (AVX512F|AVX512VL)) == (AVX512F|AVX512VL)) { + return 16; + } +#endif +#if !defined(BLAKE3_NO_AVX2) + if (features & AVX2) { + return 8; + } +#endif +#if !defined(BLAKE3_NO_SSE41) + if (features & SSE41) { + return 4; + } +#endif +#if !defined(BLAKE3_NO_SSE2) + if (features & SSE2) { + return 4; + } +#endif +#endif +#if BLAKE3_USE_NEON == 1 + return 4; +#endif + return 1; +} diff --git a/src/crypto/blake3/blake3_impl.h b/src/crypto/blake3/blake3_impl.h new file mode 100644 index 000000000000..88e71e41e90f --- /dev/null +++ b/src/crypto/blake3/blake3_impl.h @@ -0,0 +1,333 @@ +#ifndef BLAKE3_IMPL_H +#define BLAKE3_IMPL_H + +#include +#include +#include +#include +#include + +#include "blake3.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// internal flags +enum blake3_flags { + CHUNK_START = 1 << 0, + CHUNK_END = 1 << 1, + PARENT = 1 << 2, + ROOT = 1 << 3, + KEYED_HASH = 1 << 4, + DERIVE_KEY_CONTEXT = 1 << 5, + DERIVE_KEY_MATERIAL = 1 << 6, +}; + +// This C implementation tries to support recent versions of GCC, Clang, and +// MSVC. +#if defined(_MSC_VER) +#define INLINE static __forceinline +#else +#define INLINE static inline __attribute__((always_inline)) +#endif + +#ifdef __cplusplus +#define NOEXCEPT noexcept +#else +#define NOEXCEPT +#endif + +#if (defined(__x86_64__) || defined(_M_X64)) && !defined(_M_ARM64EC) +#define IS_X86 +#define IS_X86_64 +#endif + +#if defined(__i386__) || defined(_M_IX86) +#define IS_X86 +#define IS_X86_32 +#endif + +#if defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC) +#define IS_AARCH64 +#endif + +#if defined(IS_X86) +#if defined(_MSC_VER) +#include +#endif +#endif + +#if !defined(BLAKE3_USE_NEON) + // If BLAKE3_USE_NEON not manually set, autodetect based on AArch64ness + #if defined(IS_AARCH64) + #if defined(__ARM_BIG_ENDIAN) + #define BLAKE3_USE_NEON 0 + #else + #define BLAKE3_USE_NEON 1 + #endif + #else + #define BLAKE3_USE_NEON 0 + #endif +#endif + +#if defined(IS_X86) +#define MAX_SIMD_DEGREE 16 +#elif BLAKE3_USE_NEON == 1 +#define MAX_SIMD_DEGREE 4 +#else +#define MAX_SIMD_DEGREE 1 +#endif + +// There are some places where we want a static size that's equal to the +// MAX_SIMD_DEGREE, but also at least 2. +#define MAX_SIMD_DEGREE_OR_2 (MAX_SIMD_DEGREE > 2 ? MAX_SIMD_DEGREE : 2) + +static const uint32_t IV[8] = {0x6A09E667UL, 0xBB67AE85UL, 0x3C6EF372UL, + 0xA54FF53AUL, 0x510E527FUL, 0x9B05688CUL, + 0x1F83D9ABUL, 0x5BE0CD19UL}; + +static const uint8_t MSG_SCHEDULE[7][16] = { + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + {2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8}, + {3, 4, 10, 12, 13, 2, 7, 14, 6, 5, 9, 0, 11, 15, 8, 1}, + {10, 7, 12, 9, 14, 3, 13, 15, 4, 0, 11, 2, 5, 8, 1, 6}, + {12, 13, 9, 11, 15, 10, 14, 8, 7, 2, 5, 3, 0, 1, 6, 4}, + {9, 14, 11, 5, 8, 12, 15, 1, 13, 3, 0, 10, 2, 6, 4, 7}, + {11, 15, 5, 0, 1, 9, 8, 6, 14, 10, 2, 12, 3, 4, 7, 13}, +}; + +/* Find index of the highest set bit */ +/* x is assumed to be nonzero. */ +static unsigned int highest_one(uint64_t x) { +#if defined(__GNUC__) || defined(__clang__) + return 63 ^ (unsigned int)__builtin_clzll(x); +#elif defined(_MSC_VER) && defined(IS_X86_64) + unsigned long index; + _BitScanReverse64(&index, x); + return index; +#elif defined(_MSC_VER) && defined(IS_X86_32) + if(x >> 32) { + unsigned long index; + _BitScanReverse(&index, (unsigned long)(x >> 32)); + return 32 + index; + } else { + unsigned long index; + _BitScanReverse(&index, (unsigned long)x); + return index; + } +#else + unsigned int c = 0; + if(x & 0xffffffff00000000ULL) { x >>= 32; c += 32; } + if(x & 0x00000000ffff0000ULL) { x >>= 16; c += 16; } + if(x & 0x000000000000ff00ULL) { x >>= 8; c += 8; } + if(x & 0x00000000000000f0ULL) { x >>= 4; c += 4; } + if(x & 0x000000000000000cULL) { x >>= 2; c += 2; } + if(x & 0x0000000000000002ULL) { c += 1; } + return c; +#endif +} + +// Count the number of 1 bits. +INLINE unsigned int popcnt(uint64_t x) { +#if defined(__GNUC__) || defined(__clang__) + return (unsigned int)__builtin_popcountll(x); +#else + unsigned int count = 0; + while (x != 0) { + count += 1; + x &= x - 1; + } + return count; +#endif +} + +// Largest power of two less than or equal to x. As a special case, returns 1 +// when x is 0. +INLINE uint64_t round_down_to_power_of_2(uint64_t x) { + return 1ULL << highest_one(x | 1); +} + +INLINE uint32_t counter_low(uint64_t counter) { return (uint32_t)counter; } + +INLINE uint32_t counter_high(uint64_t counter) { + return (uint32_t)(counter >> 32); +} + +INLINE uint32_t load32(const void *src) { + const uint8_t *p = (const uint8_t *)src; + return ((uint32_t)(p[0]) << 0) | ((uint32_t)(p[1]) << 8) | + ((uint32_t)(p[2]) << 16) | ((uint32_t)(p[3]) << 24); +} + +INLINE void load_key_words(const uint8_t key[BLAKE3_KEY_LEN], + uint32_t key_words[8]) { + key_words[0] = load32(&key[0 * 4]); + key_words[1] = load32(&key[1 * 4]); + key_words[2] = load32(&key[2 * 4]); + key_words[3] = load32(&key[3 * 4]); + key_words[4] = load32(&key[4 * 4]); + key_words[5] = load32(&key[5 * 4]); + key_words[6] = load32(&key[6 * 4]); + key_words[7] = load32(&key[7 * 4]); +} + +INLINE void load_block_words(const uint8_t block[BLAKE3_BLOCK_LEN], + uint32_t block_words[16]) { + for (size_t i = 0; i < 16; i++) { + block_words[i] = load32(&block[i * 4]); + } +} + +INLINE void store32(void *dst, uint32_t w) { + uint8_t *p = (uint8_t *)dst; + p[0] = (uint8_t)(w >> 0); + p[1] = (uint8_t)(w >> 8); + p[2] = (uint8_t)(w >> 16); + p[3] = (uint8_t)(w >> 24); +} + +INLINE void store_cv_words(uint8_t bytes_out[32], uint32_t cv_words[8]) { + store32(&bytes_out[0 * 4], cv_words[0]); + store32(&bytes_out[1 * 4], cv_words[1]); + store32(&bytes_out[2 * 4], cv_words[2]); + store32(&bytes_out[3 * 4], cv_words[3]); + store32(&bytes_out[4 * 4], cv_words[4]); + store32(&bytes_out[5 * 4], cv_words[5]); + store32(&bytes_out[6 * 4], cv_words[6]); + store32(&bytes_out[7 * 4], cv_words[7]); +} + +void blake3_compress_in_place(uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags); + +void blake3_compress_xof(const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, uint8_t flags, + uint8_t out[64]); + +void blake3_xof_many(const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, uint8_t flags, + uint8_t out[64], size_t outblocks); + +void blake3_hash_many(const uint8_t *const *inputs, size_t num_inputs, + size_t blocks, const uint32_t key[8], uint64_t counter, + bool increment_counter, uint8_t flags, + uint8_t flags_start, uint8_t flags_end, uint8_t *out); + +size_t blake3_simd_degree(void); + +BLAKE3_PRIVATE size_t blake3_compress_subtree_wide(const uint8_t *input, size_t input_len, + const uint32_t key[8], + uint64_t chunk_counter, uint8_t flags, + uint8_t *out, bool use_tbb); + +#if defined(BLAKE3_USE_TBB) +BLAKE3_PRIVATE void blake3_compress_subtree_wide_join_tbb( + // shared params + const uint32_t key[8], uint8_t flags, bool use_tbb, + // left-hand side params + const uint8_t *l_input, size_t l_input_len, uint64_t l_chunk_counter, + uint8_t *l_cvs, size_t *l_n, + // right-hand side params + const uint8_t *r_input, size_t r_input_len, uint64_t r_chunk_counter, + uint8_t *r_cvs, size_t *r_n) NOEXCEPT; +#endif + +// Declarations for implementation-specific functions. +void blake3_compress_in_place_portable(uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags); + +void blake3_compress_xof_portable(const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags, uint8_t out[64]); + +void blake3_hash_many_portable(const uint8_t *const *inputs, size_t num_inputs, + size_t blocks, const uint32_t key[8], + uint64_t counter, bool increment_counter, + uint8_t flags, uint8_t flags_start, + uint8_t flags_end, uint8_t *out); + +#if defined(IS_X86) +#if !defined(BLAKE3_NO_SSE2) +void blake3_compress_in_place_sse2(uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags); +void blake3_compress_xof_sse2(const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags, uint8_t out[64]); +void blake3_hash_many_sse2(const uint8_t *const *inputs, size_t num_inputs, + size_t blocks, const uint32_t key[8], + uint64_t counter, bool increment_counter, + uint8_t flags, uint8_t flags_start, + uint8_t flags_end, uint8_t *out); +#endif +#if !defined(BLAKE3_NO_SSE41) +void blake3_compress_in_place_sse41(uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags); +void blake3_compress_xof_sse41(const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags, uint8_t out[64]); +void blake3_hash_many_sse41(const uint8_t *const *inputs, size_t num_inputs, + size_t blocks, const uint32_t key[8], + uint64_t counter, bool increment_counter, + uint8_t flags, uint8_t flags_start, + uint8_t flags_end, uint8_t *out); +#endif +#if !defined(BLAKE3_NO_AVX2) +void blake3_hash_many_avx2(const uint8_t *const *inputs, size_t num_inputs, + size_t blocks, const uint32_t key[8], + uint64_t counter, bool increment_counter, + uint8_t flags, uint8_t flags_start, + uint8_t flags_end, uint8_t *out); +#endif +#if !defined(BLAKE3_NO_AVX512) +void blake3_compress_in_place_avx512(uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags); + +void blake3_compress_xof_avx512(const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags, uint8_t out[64]); + +void blake3_hash_many_avx512(const uint8_t *const *inputs, size_t num_inputs, + size_t blocks, const uint32_t key[8], + uint64_t counter, bool increment_counter, + uint8_t flags, uint8_t flags_start, + uint8_t flags_end, uint8_t *out); + +#if !defined(_WIN32) && !defined(__CYGWIN__) +void blake3_xof_many_avx512(const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, uint8_t flags, + uint8_t* out, size_t outblocks); +#endif +#endif +#endif + +#if BLAKE3_USE_NEON == 1 +void blake3_hash_many_neon(const uint8_t *const *inputs, size_t num_inputs, + size_t blocks, const uint32_t key[8], + uint64_t counter, bool increment_counter, + uint8_t flags, uint8_t flags_start, + uint8_t flags_end, uint8_t *out); +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* BLAKE3_IMPL_H */ diff --git a/src/crypto/blake3/blake3_portable.c b/src/crypto/blake3/blake3_portable.c new file mode 100644 index 000000000000..062dd1b47fb6 --- /dev/null +++ b/src/crypto/blake3/blake3_portable.c @@ -0,0 +1,160 @@ +#include "blake3_impl.h" +#include + +INLINE uint32_t rotr32(uint32_t w, uint32_t c) { + return (w >> c) | (w << (32 - c)); +} + +INLINE void g(uint32_t *state, size_t a, size_t b, size_t c, size_t d, + uint32_t x, uint32_t y) { + state[a] = state[a] + state[b] + x; + state[d] = rotr32(state[d] ^ state[a], 16); + state[c] = state[c] + state[d]; + state[b] = rotr32(state[b] ^ state[c], 12); + state[a] = state[a] + state[b] + y; + state[d] = rotr32(state[d] ^ state[a], 8); + state[c] = state[c] + state[d]; + state[b] = rotr32(state[b] ^ state[c], 7); +} + +INLINE void round_fn(uint32_t state[16], const uint32_t *msg, size_t round) { + // Select the message schedule based on the round. + const uint8_t *schedule = MSG_SCHEDULE[round]; + + // Mix the columns. + g(state, 0, 4, 8, 12, msg[schedule[0]], msg[schedule[1]]); + g(state, 1, 5, 9, 13, msg[schedule[2]], msg[schedule[3]]); + g(state, 2, 6, 10, 14, msg[schedule[4]], msg[schedule[5]]); + g(state, 3, 7, 11, 15, msg[schedule[6]], msg[schedule[7]]); + + // Mix the rows. + g(state, 0, 5, 10, 15, msg[schedule[8]], msg[schedule[9]]); + g(state, 1, 6, 11, 12, msg[schedule[10]], msg[schedule[11]]); + g(state, 2, 7, 8, 13, msg[schedule[12]], msg[schedule[13]]); + g(state, 3, 4, 9, 14, msg[schedule[14]], msg[schedule[15]]); +} + +INLINE void compress_pre(uint32_t state[16], const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, uint8_t flags) { + uint32_t block_words[16]; + block_words[0] = load32(block + 4 * 0); + block_words[1] = load32(block + 4 * 1); + block_words[2] = load32(block + 4 * 2); + block_words[3] = load32(block + 4 * 3); + block_words[4] = load32(block + 4 * 4); + block_words[5] = load32(block + 4 * 5); + block_words[6] = load32(block + 4 * 6); + block_words[7] = load32(block + 4 * 7); + block_words[8] = load32(block + 4 * 8); + block_words[9] = load32(block + 4 * 9); + block_words[10] = load32(block + 4 * 10); + block_words[11] = load32(block + 4 * 11); + block_words[12] = load32(block + 4 * 12); + block_words[13] = load32(block + 4 * 13); + block_words[14] = load32(block + 4 * 14); + block_words[15] = load32(block + 4 * 15); + + state[0] = cv[0]; + state[1] = cv[1]; + state[2] = cv[2]; + state[3] = cv[3]; + state[4] = cv[4]; + state[5] = cv[5]; + state[6] = cv[6]; + state[7] = cv[7]; + state[8] = IV[0]; + state[9] = IV[1]; + state[10] = IV[2]; + state[11] = IV[3]; + state[12] = counter_low(counter); + state[13] = counter_high(counter); + state[14] = (uint32_t)block_len; + state[15] = (uint32_t)flags; + + round_fn(state, &block_words[0], 0); + round_fn(state, &block_words[0], 1); + round_fn(state, &block_words[0], 2); + round_fn(state, &block_words[0], 3); + round_fn(state, &block_words[0], 4); + round_fn(state, &block_words[0], 5); + round_fn(state, &block_words[0], 6); +} + +void blake3_compress_in_place_portable(uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags) { + uint32_t state[16]; + compress_pre(state, cv, block, block_len, counter, flags); + cv[0] = state[0] ^ state[8]; + cv[1] = state[1] ^ state[9]; + cv[2] = state[2] ^ state[10]; + cv[3] = state[3] ^ state[11]; + cv[4] = state[4] ^ state[12]; + cv[5] = state[5] ^ state[13]; + cv[6] = state[6] ^ state[14]; + cv[7] = state[7] ^ state[15]; +} + +void blake3_compress_xof_portable(const uint32_t cv[8], + const uint8_t block[BLAKE3_BLOCK_LEN], + uint8_t block_len, uint64_t counter, + uint8_t flags, uint8_t out[64]) { + uint32_t state[16]; + compress_pre(state, cv, block, block_len, counter, flags); + + store32(&out[0 * 4], state[0] ^ state[8]); + store32(&out[1 * 4], state[1] ^ state[9]); + store32(&out[2 * 4], state[2] ^ state[10]); + store32(&out[3 * 4], state[3] ^ state[11]); + store32(&out[4 * 4], state[4] ^ state[12]); + store32(&out[5 * 4], state[5] ^ state[13]); + store32(&out[6 * 4], state[6] ^ state[14]); + store32(&out[7 * 4], state[7] ^ state[15]); + store32(&out[8 * 4], state[8] ^ cv[0]); + store32(&out[9 * 4], state[9] ^ cv[1]); + store32(&out[10 * 4], state[10] ^ cv[2]); + store32(&out[11 * 4], state[11] ^ cv[3]); + store32(&out[12 * 4], state[12] ^ cv[4]); + store32(&out[13 * 4], state[13] ^ cv[5]); + store32(&out[14 * 4], state[14] ^ cv[6]); + store32(&out[15 * 4], state[15] ^ cv[7]); +} + +INLINE void hash_one_portable(const uint8_t *input, size_t blocks, + const uint32_t key[8], uint64_t counter, + uint8_t flags, uint8_t flags_start, + uint8_t flags_end, uint8_t out[BLAKE3_OUT_LEN]) { + uint32_t cv[8]; + memcpy(cv, key, BLAKE3_KEY_LEN); + uint8_t block_flags = flags | flags_start; + while (blocks > 0) { + if (blocks == 1) { + block_flags |= flags_end; + } + blake3_compress_in_place_portable(cv, input, BLAKE3_BLOCK_LEN, counter, + block_flags); + input = &input[BLAKE3_BLOCK_LEN]; + blocks -= 1; + block_flags = flags; + } + store_cv_words(out, cv); +} + +void blake3_hash_many_portable(const uint8_t *const *inputs, size_t num_inputs, + size_t blocks, const uint32_t key[8], + uint64_t counter, bool increment_counter, + uint8_t flags, uint8_t flags_start, + uint8_t flags_end, uint8_t *out) { + while (num_inputs > 0) { + hash_one_portable(inputs[0], blocks, key, counter, flags, flags_start, + flags_end, out); + if (increment_counter) { + counter += 1; + } + inputs += 1; + num_inputs -= 1; + out = &out[BLAKE3_OUT_LEN]; + } +} diff --git a/src/platform/README.md b/src/platform/README.md new file mode 100644 index 000000000000..7a0168f92c5b --- /dev/null +++ b/src/platform/README.md @@ -0,0 +1,29 @@ +# Dash Platform client library (GUI-only) + +This directory contains a Qt-free C++ client for Dash Platform (Evolution), +used exclusively by the dash-qt GUI when configured with +`--enable-platform-gui`. It provides: + +- per-network parameters and the well-known system data contract IDs + (`params.*`); +- codecs for the wire formats Platform uses: a bincode-v2 subset, a protobuf + wire-format subset for the DAPI gRPC messages, and DPP (Dash Platform + Protocol) object serialization; +- a GroveDB/merk proof verifier (blake3-based) mirroring the upstream Rust + `verify` feature slice, plus the Tenderdash quorum-signature check that + binds a proof's root hash to a Platform block signed by an LLMQ quorum; +- a DAPI client speaking gRPC-Web over HTTP/1.1 + TLS (mbedtls) to evonodes. + +## Isolation rules + +- Nothing in this directory may be linked into `dashd`, `dash-cli`, + `dash-tx`, `dash-wallet` or any consensus/wallet library. It is linked into + `dash-qt` and `test_dash` only, and only under `--enable-platform-gui`. +- Consensus, wallet and node code must not include headers from here. The GUI + (`src/qt/platform/`) is the only consumer. +- Code here may depend on `src/crypto`, `src/util`, `src/bls` (dashbls), + `src/secp256k1` and the standard library. It must not depend on Qt. + +Upstream references are pinned in code comments (dashpay/platform, grovedb). +All protocol logic is pinned to a single Platform protocol version and must be +re-validated against Rust-generated test vectors when Platform upgrades. diff --git a/src/platform/params.cpp b/src/platform/params.cpp new file mode 100644 index 000000000000..a80896e8eb61 --- /dev/null +++ b/src/platform/params.cpp @@ -0,0 +1,40 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include + +namespace platform { + +std::optional GetParams(const std::string& network_id) +{ + // Tenderdash chain ids from dashpay/platform + // packages/dashmate/configs/defaults/get{Mainnet,Testnet}ConfigFactory.js. + // The testnet chain id changes when testnet Platform is reset; it is kept + // overridable through the GUI-only -platformchainid argument (see + // qt/platform/). + if (network_id == CBaseChainParams::MAIN) { + return Params{ + .network_id = network_id, + .tenderdash_chain_id = "evo1", + // 0.01 DASH; matches the DashPay mobile wallet default for an + // uncontested username registration. Contested (premium) names + // require additional prefunded balance, handled by the GUI flow. + .default_identity_funding_amount = 1000000, + }; + } + if (network_id == CBaseChainParams::TESTNET) { + return Params{ + .network_id = network_id, + .tenderdash_chain_id = "dash-testnet-51", + .default_identity_funding_amount = 1000000, + }; + } + // Platform is not deployed on this network (or, for devnets, the GUI + // requires explicit -platformchainid configuration). + return std::nullopt; +} + +} // namespace platform diff --git a/src/platform/params.h b/src/platform/params.h new file mode 100644 index 000000000000..f06e2c14537a --- /dev/null +++ b/src/platform/params.h @@ -0,0 +1,65 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_PARAMS_H +#define BITCOIN_PLATFORM_PARAMS_H + +#include +#include +#include +#include + +namespace platform { + +using Identifier = std::array; + +//! Well-known system data contract identifiers. These are protocol constants +//! created at Platform genesis and identical on every network. +//! Source: dashpay/platform packages/dpns-contract/src/lib.rs and +//! packages/dashpay-contract/src/lib.rs (ID_BYTES). +inline constexpr Identifier DPNS_CONTRACT_ID{ + 230, 104, 198, 89, 175, 102, 174, 225, 231, 44, 24, 109, 222, 123, 91, 126, + 10, 29, 113, 42, 9, 196, 13, 87, 33, 246, 34, 191, 83, 197, 49, 85}; + +inline constexpr Identifier DASHPAY_CONTRACT_ID{ + 162, 161, 180, 172, 111, 239, 34, 234, 42, 26, 104, 232, 18, 54, 68, 179, + 87, 135, 95, 107, 65, 44, 24, 16, 146, 129, 193, 70, 231, 178, 113, 188}; + +//! Document id of the pre-registered "dash" top level domain DPNS document. +//! Source: packages/dpns-contract/src/lib.rs DPNS_DASH_TLD_DOCUMENT_ID. +inline constexpr Identifier DPNS_DASH_TLD_DOCUMENT_ID{ + 215, 242, 197, 63, 70, 169, 23, 171, 110, 91, 57, 162, 215, 188, 38, 11, + 100, 146, 137, 69, 55, 68, 209, 224, 212, 242, 106, 141, 142, 255, 55, 207}; + +//! Preorder salt of the "dash" TLD document. +//! Source: packages/dpns-contract/src/lib.rs DPNS_DASH_TLD_PREORDER_SALT. +inline constexpr Identifier DPNS_DASH_TLD_PREORDER_SALT{ + 224, 181, 8, 197, 163, 104, 37, 162, 6, 105, 58, 31, 65, 74, 161, 62, + 219, 236, 244, 60, 65, 227, 199, 153, 234, 158, 115, 123, 79, 154, 162, 38}; + +//! Per-network Platform parameters for networks where Platform is deployed. +struct Params { + //! Chain name as in CBaseChainParams ("main", "test", ...). + std::string network_id; + //! Tenderdash chain id, part of the quorum signature preimage + //! (CanonicalVote.chain_id). The LLMQ type used by Platform to sign + //! state roots is not duplicated here; read it from + //! Consensus::Params::llmqTypePlatform. + std::string tenderdash_chain_id; + //! Default amount (in duffs) locked to fund a new identity when + //! registering a username. Matches the DashPay mobile wallet defaults. + int64_t default_identity_funding_amount; + + //! Minimum credit conversion: 1 duff == 1000 platform credits. + static constexpr int64_t CREDITS_PER_DUFF{1000}; +}; + +//! Returns the Platform parameters for the given chain name +//! (CBaseChainParams::MAIN etc.), or std::nullopt if Platform is not +//! deployed on that network (in which case the GUI feature is disabled). +std::optional GetParams(const std::string& network_id); + +} // namespace platform + +#endif // BITCOIN_PLATFORM_PARAMS_H diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 4c581111961a..548b7796946f 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -7,6 +7,9 @@ #include #include +#ifdef ENABLE_PLATFORM_GUI +#include +#endif #include #include #include @@ -762,6 +765,13 @@ void BitcoinGUI::createToolBars() coinJoinCoinsButton->setStatusTip(coinJoinCoinsAction->statusTip()); tabGroup->addButton(coinJoinCoinsButton); +#ifdef ENABLE_PLATFORM_GUI + platformButton = new QToolButton(this); + platformButton->setText(tr("&DashPay")); + platformButton->setStatusTip(tr("Usernames, profiles and contacts on Dash Platform")); + tabGroup->addButton(platformButton); +#endif + masternodeButton = new QToolButton(this); masternodeButton->setText(tr("&Masternodes")); masternodeButton->setStatusTip(tr("Browse masternodes")); @@ -779,6 +789,9 @@ void BitcoinGUI::createToolBars() connect(historyButton, &QToolButton::clicked, this, &BitcoinGUI::gotoHistoryPage); connect(governanceButton, &QToolButton::clicked, this, &BitcoinGUI::gotoGovernancePage); connect(masternodeButton, &QToolButton::clicked, this, &BitcoinGUI::gotoMasternodePage); +#ifdef ENABLE_PLATFORM_GUI + connect(platformButton, &QToolButton::clicked, this, &BitcoinGUI::gotoPlatformPage); +#endif // Give the selected tab button a bolder font. connect(tabGroup, qOverload(&QButtonGroup::buttonToggled), this, &BitcoinGUI::highlightTabButton); @@ -792,6 +805,9 @@ void BitcoinGUI::createToolBars() if (button == coinJoinCoinsButton) { m_coinjoin_action = action; } else if (button == governanceButton) { m_governance_action = action; } else if (button == masternodeButton) { m_masternode_action = action; } +#ifdef ENABLE_PLATFORM_GUI + else if (button == platformButton) { m_platform_action = action; } +#endif } overviewButton->setChecked(true); @@ -918,6 +934,9 @@ void BitcoinGUI::setClientModel(ClientModel *_clientModel, interfaces::BlockAndH connect(optionsModel, &OptionsModel::showGovernanceChanged, this, &BitcoinGUI::updateGovernanceVisibility); connect(optionsModel, &OptionsModel::showGovernanceClockChanged, this, &BitcoinGUI::updateGovernanceCycleIcon); connect(optionsModel, &OptionsModel::showMasternodesChanged, this, &BitcoinGUI::updateMasternodesVisibility); +#ifdef ENABLE_PLATFORM_GUI + connect(optionsModel, &OptionsModel::showPlatformChanged, this, &BitcoinGUI::updatePlatformVisibility); +#endif if (trayIcon) { // be aware of the tray icon disable state change reported by the OptionsModel object. @@ -955,6 +974,9 @@ void BitcoinGUI::setClientModel(ClientModel *_clientModel, interfaces::BlockAndH updateCoinJoinVisibility(); updateGovernanceVisibility(); updateMasternodesVisibility(); +#ifdef ENABLE_PLATFORM_GUI + updatePlatformVisibility(); +#endif } #ifdef ENABLE_WALLET @@ -1326,6 +1348,16 @@ void BitcoinGUI::gotoMasternodePage() } } +#ifdef ENABLE_PLATFORM_GUI +void BitcoinGUI::gotoPlatformPage() +{ + if (platformButton) { + platformButton->setChecked(true); + if (walletFrame) walletFrame->gotoPlatformPage(); + } +} +#endif + void BitcoinGUI::gotoReceiveCoinsPage() { receiveCoinsButton->setChecked(true); @@ -1562,6 +1594,32 @@ void BitcoinGUI::updateMasternodesVisibility() updateWidth(); } +#ifdef ENABLE_PLATFORM_GUI +void BitcoinGUI::updatePlatformVisibility() +{ + if (!clientModel || !clientModel->getOptionsModel()) return; + // Only offer the tab on networks where Platform is deployed (or explicitly + // configured), and only when the user opted in. + const bool fShow = clientModel->getOptionsModel()->getShowPlatformTab() && + platform::GetParams(Params().NetworkIDString()).has_value(); + + // Show/hide the underlying QAction, hiding the QToolButton itself doesn't + // work for the GUI part but is still needed for shortcuts to work properly. + if (m_platform_action) m_platform_action->setVisible(fShow); + if (platformButton) { +#ifdef ENABLE_WALLET + if (!fShow && platformButton->isChecked()) { + gotoOverviewPage(); + } +#endif // ENABLE_WALLET + platformButton->setVisible(fShow); + } + + GUIUtil::updateButtonGroupShortcuts(tabGroup); + updateWidth(); +} +#endif // ENABLE_PLATFORM_GUI + void BitcoinGUI::updateWidth() { if (walletFrame == nullptr) { diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 333ef538d412..4bc7c0034802 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -143,6 +143,9 @@ class BitcoinGUI : public QMainWindow QToolButton* historyButton = nullptr; QToolButton* masternodeButton = nullptr; QToolButton* governanceButton = nullptr; +#ifdef ENABLE_PLATFORM_GUI + QToolButton* platformButton = nullptr; +#endif QAction* appToolBarLogoAction = nullptr; QAction* quitAction = nullptr; QAction* sendCoinsAction = nullptr; @@ -154,6 +157,9 @@ class BitcoinGUI : public QMainWindow QAction* m_coinjoin_action = nullptr; QAction* m_governance_action = nullptr; QAction* m_masternode_action = nullptr; +#ifdef ENABLE_PLATFORM_GUI + QAction* m_platform_action = nullptr; +#endif QAction* m_load_psbt_action = nullptr; QAction* m_load_psbt_clipboard_action = nullptr; QAction* aboutAction = nullptr; @@ -354,6 +360,10 @@ public Q_SLOTS: void gotoHistoryPage(); /** Switch to masternode page */ void gotoMasternodePage(); +#ifdef ENABLE_PLATFORM_GUI + /** Switch to DashPay (Dash Platform) page */ + void gotoPlatformPage(); +#endif /** Switch to receive coins page */ void gotoReceiveCoinsPage(); /** Switch to send coins page */ @@ -415,6 +425,9 @@ public Q_SLOTS: void updateCoinJoinVisibility(); void updateGovernanceVisibility(); void updateMasternodesVisibility(); +#ifdef ENABLE_PLATFORM_GUI + void updatePlatformVisibility(); +#endif void updateWidth(); }; diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index dd31289875ed..67547734eb4e 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -1080,6 +1080,16 @@ https://explore.transifex.com/dash/dash/ + + + + Show additional tab for DashPay usernames, profiles and contacts on Dash Platform. + + + Show DashPay Tab + + + diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 76d0e5934bae..89f9ef6b4fef 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -111,6 +111,9 @@ OptionsDialog::OptionsDialog(QWidget* parent, bool enableWallet) pageButtons = new QButtonGroup(this); pageButtons->addButton(ui->btnMain, pageButtons->buttons().size()); +#ifndef ENABLE_PLATFORM_GUI + ui->showPlatformTab->hide(); +#endif /* Remove Wallet/CoinJoin tabs and 3rd party-URL textbox in case of -disablewallet */ if (!m_enable_wallet) { ui->stackedWidgetOptions->removeWidget(ui->pageWallet); @@ -120,6 +123,7 @@ OptionsDialog::OptionsDialog(QWidget* parent, bool enableWallet) ui->thirdPartyTxUrlsLabel->setVisible(false); ui->thirdPartyTxUrls->setVisible(false); ui->showMasternodesTab->hide(); + ui->showPlatformTab->hide(); ui->showGovernanceTab->hide(); ui->showGovernanceCycleIcon->hide(); } else { @@ -394,6 +398,9 @@ void OptionsDialog::setMapper() /* Display */ mapper->addMapping(ui->showMasternodesTab, OptionsModel::ShowMasternodesTab); +#ifdef ENABLE_PLATFORM_GUI + mapper->addMapping(ui->showPlatformTab, OptionsModel::ShowPlatformTab); +#endif mapper->addMapping(ui->showGovernanceCycleIcon, OptionsModel::ShowGovernanceClock); mapper->addMapping(ui->showGovernanceTab, OptionsModel::ShowGovernanceTab); mapper->addMapping(ui->digits, OptionsModel::Digits); diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 3ab17fc63eb8..9b6c0317defa 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -324,6 +324,10 @@ bool OptionsModel::Init(bilingual_str& error) settings.setValue("fShowMasternodesTab", false); m_enable_masternodes = settings.value("fShowMasternodesTab", false).toBool(); + if (!settings.contains("fShowPlatformTab")) + settings.setValue("fShowPlatformTab", false); + m_enable_platform = settings.value("fShowPlatformTab", false).toBool(); + if (!settings.contains("show_governance_clock")) settings.setValue("show_governance_clock", false); m_show_governance_clock = settings.value("show_governance_clock", false).toBool(); @@ -660,6 +664,8 @@ QVariant OptionsModel::getOption(OptionID option, const std::string& suffix) con return m_sub_fee_from_amount; case ShowMasternodesTab: return m_enable_masternodes; + case ShowPlatformTab: + return m_enable_platform; case ShowGovernanceClock: return m_show_governance_clock; case ShowGovernanceTab: @@ -861,6 +867,13 @@ bool OptionsModel::setOption(OptionID option, const QVariant& value, const std:: Q_EMIT showMasternodesChanged(); } break; + case ShowPlatformTab: + if (changed()) { + m_enable_platform = value.toBool(); + settings.setValue("fShowPlatformTab", m_enable_platform); + Q_EMIT showPlatformChanged(); + } + break; case SubFeeFromAmount: m_sub_fee_from_amount = value.toBool(); settings.setValue("SubFeeFromAmount", m_sub_fee_from_amount); diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index ac157c2e4fce..2625e936d271 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -85,6 +85,7 @@ class OptionsModel : public QAbstractListModel ExternalSignerPath, // QString SpendZeroConfChange, // bool ShowMasternodesTab, // bool + ShowPlatformTab, // bool ShowGovernanceClock, // bool ShowGovernanceTab, // bool CoinJoinEnabled, // bool @@ -138,6 +139,7 @@ class OptionsModel : public QAbstractListModel bool getEnablePSBTControls() const { return m_enable_psbt_controls; } bool getKeepChangeAddress() const { return fKeepChangeAddress; } bool getShowMasternodesTab() const { return m_enable_masternodes; } + bool getShowPlatformTab() const { return m_enable_platform; } bool getShowGovernanceClock() const { return m_show_governance_clock; } bool getShowGovernanceTab() const { return m_enable_governance; } bool getShowAdvancedCJUI() { return fShowAdvancedCJUI; } @@ -175,6 +177,7 @@ class OptionsModel : public QAbstractListModel bool m_mask_values; bool fKeepChangeAddress; bool m_enable_masternodes; + bool m_enable_platform; bool m_enable_governance; bool m_show_governance_clock; bool fShowAdvancedCJUI; @@ -202,6 +205,7 @@ class OptionsModel : public QAbstractListModel void showGovernanceClockChanged(); void showGovernanceChanged(); void showMasternodesChanged(); + void showPlatformChanged(); void showTrayIconChanged(bool); void fontForMoneyChanged(const QFont&); void dustProtectionChanged(); diff --git a/src/qt/platform/moc_platformpage.cpp b/src/qt/platform/moc_platformpage.cpp new file mode 100644 index 000000000000..0e9e185e6815 --- /dev/null +++ b/src/qt/platform/moc_platformpage.cpp @@ -0,0 +1,95 @@ +/**************************************************************************** +** Meta object code from reading C++ file 'platformpage.h' +** +** Created by: The Qt Meta Object Compiler version 67 (Qt 5.15.19) +** +** WARNING! All changes made in this file will be lost! +*****************************************************************************/ + +#include +#include "qt/platform/platformpage.h" +#include +#include +#if !defined(Q_MOC_OUTPUT_REVISION) +#error "The header file 'platformpage.h' doesn't include ." +#elif Q_MOC_OUTPUT_REVISION != 67 +#error "This file was generated using the moc from 5.15.19. It" +#error "cannot be used with the include files from this version of Qt." +#error "(The moc has changed too much.)" +#endif + +QT_BEGIN_MOC_NAMESPACE +QT_WARNING_PUSH +QT_WARNING_DISABLE_DEPRECATED +struct qt_meta_stringdata_PlatformPage_t { + QByteArrayData data[1]; + char stringdata0[13]; +}; +#define QT_MOC_LITERAL(idx, ofs, len) \ + Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \ + qptrdiff(offsetof(qt_meta_stringdata_PlatformPage_t, stringdata0) + ofs \ + - idx * sizeof(QByteArrayData)) \ + ) +static const qt_meta_stringdata_PlatformPage_t qt_meta_stringdata_PlatformPage = { + { +QT_MOC_LITERAL(0, 0, 12) // "PlatformPage" + + }, + "PlatformPage" +}; +#undef QT_MOC_LITERAL + +static const uint qt_meta_data_PlatformPage[] = { + + // content: + 8, // revision + 0, // classname + 0, 0, // classinfo + 0, 0, // methods + 0, 0, // properties + 0, 0, // enums/sets + 0, 0, // constructors + 0, // flags + 0, // signalCount + + 0 // eod +}; + +void PlatformPage::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a) +{ + (void)_o; + (void)_id; + (void)_c; + (void)_a; +} + +QT_INIT_METAOBJECT const QMetaObject PlatformPage::staticMetaObject = { { + QMetaObject::SuperData::link(), + qt_meta_stringdata_PlatformPage.data, + qt_meta_data_PlatformPage, + qt_static_metacall, + nullptr, + nullptr +} }; + + +const QMetaObject *PlatformPage::metaObject() const +{ + return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject; +} + +void *PlatformPage::qt_metacast(const char *_clname) +{ + if (!_clname) return nullptr; + if (!strcmp(_clname, qt_meta_stringdata_PlatformPage.stringdata0)) + return static_cast(this); + return QWidget::qt_metacast(_clname); +} + +int PlatformPage::qt_metacall(QMetaObject::Call _c, int _id, void **_a) +{ + _id = QWidget::qt_metacall(_c, _id, _a); + return _id; +} +QT_WARNING_POP +QT_END_MOC_NAMESPACE diff --git a/src/qt/platform/platformpage.cpp b/src/qt/platform/platformpage.cpp new file mode 100644 index 000000000000..3216d20c1dc1 --- /dev/null +++ b/src/qt/platform/platformpage.cpp @@ -0,0 +1,49 @@ +// Copyright (c) 2026 The Dash 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 +#include + +PlatformPage::PlatformPage(QWidget* parent) : + QWidget(parent) +{ + auto* layout = new QVBoxLayout(this); + + auto* titleLabel = new QLabel(tr("DashPay"), this); + GUIUtil::setFont({titleLabel}, GUIUtil::FontWeight::Bold, 20); + + statusLabel = new QLabel(this); + statusLabel->setWordWrap(true); + if (platform::GetParams(Params().NetworkIDString())) { + statusLabel->setText(tr("Usernames, profiles and contacts are coming to this wallet. " + "This page is under construction.")); + } else { + statusLabel->setText(tr("Dash Platform is not available on this network.")); + } + + layout->addStretch(); + layout->addWidget(titleLabel, 0, Qt::AlignHCenter); + layout->addWidget(statusLabel, 0, Qt::AlignHCenter); + layout->addStretch(); + + GUIUtil::updateFonts(); +} + +PlatformPage::~PlatformPage() = default; + +void PlatformPage::setWalletModel(WalletModel* wallet_model) +{ + walletModel = wallet_model; +} + +void PlatformPage::setClientModel(ClientModel* client_model) +{ + clientModel = client_model; +} diff --git a/src/qt/platform/platformpage.h b/src/qt/platform/platformpage.h new file mode 100644 index 000000000000..f8baa5c1323a --- /dev/null +++ b/src/qt/platform/platformpage.h @@ -0,0 +1,38 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_PLATFORM_PLATFORMPAGE_H +#define BITCOIN_QT_PLATFORM_PLATFORMPAGE_H + +#include + +class ClientModel; +class WalletModel; + +QT_BEGIN_NAMESPACE +class QLabel; +QT_END_NAMESPACE + +/** DashPay (Dash Platform) page: usernames, profiles and contacts for a + * single wallet. Only built with --enable-platform-gui. + */ +class PlatformPage : public QWidget +{ + Q_OBJECT + +public: + explicit PlatformPage(QWidget* parent = nullptr); + ~PlatformPage() override; + + void setWalletModel(WalletModel* wallet_model); + void setClientModel(ClientModel* client_model); + +private: + WalletModel* walletModel{nullptr}; + ClientModel* clientModel{nullptr}; + + QLabel* statusLabel{nullptr}; +}; + +#endif // BITCOIN_QT_PLATFORM_PLATFORMPAGE_H diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index a47dc431e683..69c3aca1a0bc 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -210,6 +210,15 @@ void WalletFrame::gotoMasternodePage() i.value()->gotoMasternodePage(); } +#ifdef ENABLE_PLATFORM_GUI +void WalletFrame::gotoPlatformPage() +{ + QMap::const_iterator i; + for (i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) + i.value()->gotoPlatformPage(); +} +#endif + void WalletFrame::gotoReceiveCoinsPage() { QMap::const_iterator i; diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index 42993d7827cc..f38a7db1696d 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -5,6 +5,10 @@ #ifndef BITCOIN_QT_WALLETFRAME_H #define BITCOIN_QT_WALLETFRAME_H +#if defined(HAVE_CONFIG_H) +#include +#endif + #include #include #include @@ -80,6 +84,10 @@ public Q_SLOTS: void gotoHistoryPage(); /** Switch to masternode page */ void gotoMasternodePage(); +#ifdef ENABLE_PLATFORM_GUI + /** Switch to DashPay (Dash Platform) page */ + void gotoPlatformPage(); +#endif /** Switch to receive coins page */ void gotoReceiveCoinsPage(); /** Switch to send coins page */ diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index face06ba7ade..1ee235157b54 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -14,6 +14,9 @@ #include #include #include +#ifdef ENABLE_PLATFORM_GUI +#include +#endif #include #include #include @@ -106,6 +109,12 @@ WalletView::WalletView(WalletModel* wallet_model, QWidget* parent) proposalListPage->setWalletModel(walletModel); addWidget(proposalListPage); +#ifdef ENABLE_PLATFORM_GUI + platformPage = new PlatformPage(); + platformPage->setWalletModel(walletModel); + addWidget(platformPage); +#endif + connect(proposalListPage, &ProposalList::showProposalInfo, this, &WalletView::showProposalInfo); connect(overviewPage, &OverviewPage::transactionClicked, this, &WalletView::transactionClicked); @@ -173,6 +182,11 @@ void WalletView::setClientModel(ClientModel *_clientModel) if (proposalListPage != nullptr) { proposalListPage->setClientModel(_clientModel); } +#ifdef ENABLE_PLATFORM_GUI + if (platformPage != nullptr) { + platformPage->setClientModel(_clientModel); + } +#endif walletModel->setClientModel(_clientModel); } @@ -226,6 +240,13 @@ void WalletView::gotoMasternodePage() setCurrentWidget(masternodeListPage); } +#ifdef ENABLE_PLATFORM_GUI +void WalletView::gotoPlatformPage() +{ + setCurrentWidget(platformPage); +} +#endif + void WalletView::gotoReceiveCoinsPage() { setCurrentWidget(receiveCoinsPage); diff --git a/src/qt/walletview.h b/src/qt/walletview.h index de47272f5762..fa3012a91c2f 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -5,6 +5,10 @@ #ifndef BITCOIN_QT_WALLETVIEW_H #define BITCOIN_QT_WALLETVIEW_H +#if defined(HAVE_CONFIG_H) +#include +#endif + #include #include @@ -15,6 +19,9 @@ class ClientModel; class OverviewPage; +#ifdef ENABLE_PLATFORM_GUI +class PlatformPage; +#endif class ReceiveCoinsDialog; class SendCoinsDialog; class SendCoinsRecipient; @@ -70,6 +77,9 @@ class WalletView : public QStackedWidget AddressBookPage *usedReceivingAddressesPage; MasternodeList* masternodeListPage{nullptr}; ProposalList* proposalListPage{nullptr}; +#ifdef ENABLE_PLATFORM_GUI + PlatformPage* platformPage{nullptr}; +#endif TransactionView *transactionView; @@ -85,6 +95,10 @@ public Q_SLOTS: void gotoHistoryPage(); /** Switch to masternode page */ void gotoMasternodePage(); +#ifdef ENABLE_PLATFORM_GUI + /** Switch to DashPay (Dash Platform) page */ + void gotoPlatformPage(); +#endif /** Switch to receive coins page */ void gotoReceiveCoinsPage(); /** Switch to send coins page */ diff --git a/test/lint/lint-include-guards.py b/test/lint/lint-include-guards.py index 6e59c83f7dbc..f71c99effbe4 100755 --- a/test/lint/lint-include-guards.py +++ b/test/lint/lint-include-guards.py @@ -17,7 +17,8 @@ HEADER_ID_PREFIX = 'BITCOIN_' HEADER_ID_SUFFIX = '_H' -EXCLUDE_FILES_WITH_PREFIX = ['src/crypto/ctaes', +EXCLUDE_FILES_WITH_PREFIX = ['src/crypto/blake3', + 'src/crypto/ctaes', 'src/leveldb', 'src/crc32c', 'src/secp256k1', diff --git a/test/lint/lint-includes.py b/test/lint/lint-includes.py index 9a0cfa127dde..81f336edd422 100755 --- a/test/lint/lint-includes.py +++ b/test/lint/lint-includes.py @@ -21,6 +21,7 @@ "src/minisketch/", "src/dashbls/", "src/immer/", + "src/crypto/blake3/", "src/crypto/x11/"] EXPECTED_BOOST_INCLUDES = ["boost/date_time/posix_time/posix_time.hpp", diff --git a/test/lint/lint-locale-dependence.py b/test/lint/lint-locale-dependence.py index 32cef3cb1248..aca632c28ed9 100755 --- a/test/lint/lint-locale-dependence.py +++ b/test/lint/lint-locale-dependence.py @@ -57,6 +57,7 @@ ] REGEXP_EXTERNAL_DEPENDENCIES_EXCLUSIONS = [ + "src/crypto/blake3/", "src/crypto/ctaes/", "src/leveldb/", "src/secp256k1/", diff --git a/test/lint/lint-whitespace.py b/test/lint/lint-whitespace.py index 78c145bd004e..3bcfbf0b53d4 100755 --- a/test/lint/lint-whitespace.py +++ b/test/lint/lint-whitespace.py @@ -18,6 +18,7 @@ EXCLUDED_DIRS = ["depends/patches/", "contrib/guix/patches/", + "src/crypto/blake3/", "src/crypto/x11/", "src/leveldb/", "src/crc32c/", diff --git a/test/util/data/non-backported.txt b/test/util/data/non-backported.txt index b259b0793f40..df791e5fd602 100644 --- a/test/util/data/non-backported.txt +++ b/test/util/data/non-backported.txt @@ -37,8 +37,12 @@ src/qt/descriptiondialog.* src/qt/donutchart.* src/qt/guiutil_font.* src/qt/informationwidget.* +src/platform/*.cpp +src/platform/*.h src/qt/masternodelist.* src/qt/masternodemodel.* +src/qt/platform/*.cpp +src/qt/platform/*.h src/qt/mnemonicverificationdialog.* src/qt/networkwidget.* src/qt/proposalcreate.* From c1fdabf1d4fdb36889df5d08e31406aa80923acc Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 01:14:23 -0500 Subject: [PATCH 02/33] feat(wallet): DIP-13/14/15 platform key provider Add Dash Platform HD key derivation to the wallet, exposed to the GUI through new interfaces::Wallet methods that never hand out raw private keys (sign/pubkey/ECDH only, precedent: signSpecialTxPayload): - DIP-14 (256-bit child index) derivation: CKey::Derive256 / CPubKey::Derive256 + DIP14Hash, with BIP32 compatibility mode for sub-2^32 indexes; verified against all four official DIP-14 test vectors - wallet/platformkeys: DIP-13 identity authentication and funding paths (m/9'/coin'/5'/...) and DIP-15 friendship paths (m/9'/coin'/15'/account'/idA/idB, ids non-hardened per dashj), path walking from the wallet BIP39 seed (legacy CHDChain and descriptor wallets), and ECDH secrets using the libsecp256k1 KDF (matches dashj KeyCrypterECDH used for DashPay contact request encryption) - enable the vendored secp256k1 ECDH module when --enable-platform-gui - interfaces::Wallet: getPlatformPubKey, signPlatformDigest, platformECDHSecret, getFriendshipXpub (stubbed when the feature is compiled out); seed access follows the getMnemonic precedent and respects wallet encryption/locking Co-Authored-By: Claude Fable 5 --- configure.ac | 9 +- src/Makefile.am | 8 +- src/Makefile.test.include | 7 ++ src/hash.cpp | 5 + src/hash.h | 5 + src/interfaces/wallet.h | 32 ++++++ src/key.cpp | 28 +++++ src/key.h | 5 + src/pubkey.cpp | 28 +++++ src/pubkey.h | 5 + src/wallet/interfaces.cpp | 98 +++++++++++++++++ src/wallet/platformkeys.cpp | 127 ++++++++++++++++++++++ src/wallet/platformkeys.h | 106 ++++++++++++++++++ src/wallet/test/platformkeys_tests.cpp | 144 +++++++++++++++++++++++++ 14 files changed, 605 insertions(+), 2 deletions(-) create mode 100644 src/wallet/platformkeys.cpp create mode 100644 src/wallet/platformkeys.h create mode 100644 src/wallet/test/platformkeys_tests.cpp diff --git a/configure.ac b/configure.ac index 38b470abbfcb..ff49400acfd6 100644 --- a/configure.ac +++ b/configure.ac @@ -2148,7 +2148,14 @@ CPPFLAGS_TEMP="$CPPFLAGS" unset CPPFLAGS CPPFLAGS="$CPPFLAGS_TEMP" -ac_configure_args="${ac_configure_args} --disable-shared --with-pic --enable-benchmark=no --enable-module-recovery --disable-module-ecdh --disable-openssl-tests" +dnl The ECDH module is required by the Dash Platform GUI (DashPay contact +dnl request encryption, DIP-15); it is compiled but unused otherwise. +if test "$enable_platform_gui" = "yes"; then + secp_ecdh_arg="--enable-module-ecdh" +else + secp_ecdh_arg="--disable-module-ecdh" +fi +ac_configure_args="${ac_configure_args} --disable-shared --with-pic --enable-benchmark=no --enable-module-recovery ${secp_ecdh_arg} --disable-openssl-tests" AC_CONFIG_SUBDIRS([src/dashbls src/secp256k1]) AC_OUTPUT diff --git a/src/Makefile.am b/src/Makefile.am index 9f678fd5a805..0d5ca41200e0 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -472,6 +472,7 @@ BITCOIN_CORE_H = \ wallet/hdchain.h \ wallet/ismine.h \ wallet/load.h \ + wallet/platformkeys.h \ wallet/receive.h \ wallet/rpc/util.h \ wallet/rpc/wallet.h \ @@ -691,8 +692,10 @@ libdash_platform_a_SOURCES = \ crypto/blake3/blake3_dispatch.c \ crypto/blake3/blake3_impl.h \ crypto/blake3/blake3_portable.c \ + platform/client.h \ platform/params.cpp \ - platform/params.h + platform/params.h \ + platform/types.h endif # @@ -740,6 +743,9 @@ endif if USE_BDB libbitcoin_wallet_a_SOURCES += wallet/bdb.cpp wallet/salvage.cpp endif +if ENABLE_PLATFORM_GUI +libbitcoin_wallet_a_SOURCES += wallet/platformkeys.cpp +endif # # wallet tool # diff --git a/src/Makefile.test.include b/src/Makefile.test.include index e2f1782e5d42..373998acc478 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -225,6 +225,10 @@ BITCOIN_TESTS += \ wallet/test/rpc_util_tests.cpp \ wallet/test/scriptpubkeyman_tests.cpp +if ENABLE_PLATFORM_GUI +BITCOIN_TESTS += wallet/test/platformkeys_tests.cpp +endif + FUZZ_SUITE_LD_COMMON +=\ $(SQLITE_LIBS) \ $(BDB_LIBS) @@ -259,6 +263,9 @@ if ENABLE_WALLET test_test_dash_LDADD += $(LIBBITCOIN_WALLET) test_test_dash_CPPFLAGS += $(BDB_CPPFLAGS) endif +if ENABLE_PLATFORM_GUI +test_test_dash_LDADD += $(LIBDASH_PLATFORM) $(MBEDTLS_LIBS) +endif test_test_dash_LDADD += $(LIBBITCOIN_NODE) $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON) $(LIBBITCOIN_UTIL) $(LIBBITCOIN_CONSENSUS) $(LIBBITCOIN_CRYPTO) $(LIBUNIVALUE) \ $(LIBDASHBLS) $(LIBLEVELDB) $(LIBMEMENV) $(BACKTRACE_LIBS) $(LIBSECP256K1) $(EVENT_LIBS) $(EVENT_PTHREADS_LIBS) $(MINISKETCH_LIBS) test_test_dash_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) diff --git a/src/hash.cpp b/src/hash.cpp index 0544468bfd94..bbb652f597b0 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -75,6 +75,11 @@ void BIP32Hash(const ChainCode &chainCode, unsigned int nChild, unsigned char he CHMAC_SHA512(chainCode.begin(), chainCode.size()).Write(&header, 1).Write(data, 32).Write(num, 4).Finalize(output); } +void DIP14Hash(const ChainCode& chainCode, const unsigned char nChild[32], unsigned char header, const unsigned char data[32], unsigned char output[64]) +{ + CHMAC_SHA512(chainCode.begin(), chainCode.size()).Write(&header, 1).Write(data, 32).Write(nChild, 32).Finalize(output); +} + uint256 SHA256Uint256(const uint256& input) { uint256 result; diff --git a/src/hash.h b/src/hash.h index 8db12e811cd4..bba188207066 100644 --- a/src/hash.h +++ b/src/hash.h @@ -242,6 +242,11 @@ unsigned int MurmurHash3(unsigned int nHashSeed, Span vData void BIP32Hash(const ChainCode &chainCode, unsigned int nChild, unsigned char header, const unsigned char data[32], unsigned char output[64]); +/** DIP-14 child key derivation HMAC: like BIP32Hash but with a 256-bit child + * index, serialized big-endian (ser256). Used for Dash Platform (DashPay) + * derivation paths. */ +void DIP14Hash(const ChainCode& chainCode, const unsigned char nChild[32], unsigned char header, const unsigned char data[32], unsigned char output[64]); + /** Return a HashWriter primed for tagged hashes (as specified in BIP 340). * * The returned object will have SHA256(tag) written to it twice (= 64 bytes). diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 134180f3728a..efd99952b818 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -130,6 +130,38 @@ class Wallet //! Sign special transaction payload virtual bool signSpecialTxPayload(const uint256& hash, const CKeyID& keyid, std::vector& vchSig) = 0; + //! Dash Platform (DIP-13) key classes served by the platform key provider + //! methods below. Raw private keys never cross this interface; the wallet + //! derives keys from its HD seed on demand and only returns public keys, + //! signatures and ECDH secrets. All methods fail when the wallet is + //! locked or has no HD seed, and always fail unless the wallet was built + //! with --enable-platform-gui. + enum class PlatformKeyType { + IdentityAuth, //!< m/9'/coin'/5'/0'/0'/'/' + RegistrationFunding, //!< m/9'/coin'/5'/1'/' + TopupFunding, //!< m/9'/coin'/5'/2'/' + InvitationFunding, //!< m/9'/coin'/5'/3'/' + }; + + //! Derive and return the public key for a platform key. For + //! IdentityAuth, account is the identity index; for funding keys it is + //! ignored. + virtual bool getPlatformPubKey(PlatformKeyType type, uint32_t account, uint32_t index, CPubKey& pubkey_out) = 0; + + //! Sign a 32-byte digest with a platform key (compact/recoverable ECDSA, + //! as used by Platform state transitions). + virtual bool signPlatformDigest(PlatformKeyType type, uint32_t account, uint32_t index, const uint256& digest, std::vector& vchSig) = 0; + + //! ECDH shared secret between the identity authentication key + //! (identity_index, key_index) and a counterparty public key, using the + //! libsecp256k1 ECDH KDF. Used for DashPay contact request encryption. + virtual bool platformECDHSecret(uint32_t identity_index, uint32_t key_index, const CPubKey& counterparty, SecureVector& secret_out) = 0; + + //! DIP-15 friendship extended public key (pubkey + chain code) at + //! m/9'/coin'/15'/'//. For our receiving chain + //! user_a is our identity id and user_b the contact's. + virtual bool getFriendshipXpub(uint32_t account, const uint256& user_a_id, const uint256& user_b_id, CPubKey& pubkey_out, uint256& chaincode_out) = 0; + //! Return whether wallet has private key. virtual bool isSpendable(const CScript& script) = 0; virtual bool isSpendable(const CTxDestination& dest) = 0; diff --git a/src/key.cpp b/src/key.cpp index 84a068d71454..19bbe8bbfbf0 100644 --- a/src/key.cpp +++ b/src/key.cpp @@ -13,6 +13,8 @@ #include #include +#include + static secp256k1_context* secp256k1_context_sign = nullptr; /** These functions are taken from the libsecp256k1 distribution and are very ugly. */ @@ -305,6 +307,32 @@ bool CKey::Derive(CKey& keyChild, ChainCode &ccChild, unsigned int nChild, const return ret; } +bool CKey::Derive256(CKey& keyChild, ChainCode& ccChild, Span nChild, bool hardened, const ChainCode& cc) const { + assert(IsValid()); + assert(IsCompressed()); + assert(nChild.size() == 32); + // DIP-14 compatibility mode: indexes below 2^32 derive exactly as BIP32, + // with the hardened flag folded into the high bit of the 32-bit index. + if (std::all_of(nChild.begin(), nChild.begin() + 28, [](unsigned char c) { return c == 0; })) { + uint32_t child32 = ReadBE32(nChild.data() + 28); + return Derive(keyChild, ccChild, child32 | (hardened ? 0x80000000u : 0), cc); + } + std::vector> vout(64); + if (!hardened) { + CPubKey pubkey = GetPubKey(); + assert(pubkey.size() == CPubKey::COMPRESSED_SIZE); + DIP14Hash(cc, nChild.data(), *pubkey.begin(), pubkey.begin() + 1, vout.data()); + } else { + assert(size() == 32); + DIP14Hash(cc, nChild.data(), 0, begin(), vout.data()); + } + memcpy(ccChild.begin(), vout.data() + 32, 32); + keyChild.Set(begin(), begin() + 32, true); + bool ret = secp256k1_ec_seckey_tweak_add(secp256k1_context_sign, (unsigned char*)keyChild.begin(), vout.data()); + if (!ret) keyChild.ClearKeyData(); + return ret; +} + EllSwiftPubKey CKey::EllSwiftCreate(Span ent32) const { assert(keydata); diff --git a/src/key.h b/src/key.h index c4c713c02f24..25c4277dd328 100644 --- a/src/key.h +++ b/src/key.h @@ -157,6 +157,11 @@ class CKey //! Derive BIP32 child key. [[nodiscard]] bool Derive(CKey& keyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const; + //! Derive DIP-14 child key with a 256-bit index (32 bytes, big-endian). + //! Indexes below 2^32 fall back to BIP32 derivation for compatibility + //! (the hardened flag is then folded into the 32-bit index). + [[nodiscard]] bool Derive256(CKey& keyChild, ChainCode& ccChild, Span nChild, bool hardened, const ChainCode& cc) const; + /** * Verify thoroughly whether a private key and a public key match. * This is done using a different mechanism than just regenerating it. diff --git a/src/pubkey.cpp b/src/pubkey.cpp index 8de5143dbb0b..6583fb9da837 100644 --- a/src/pubkey.cpp +++ b/src/pubkey.cpp @@ -258,6 +258,34 @@ bool CPubKey::Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChi return true; } +bool CPubKey::Derive256(CPubKey& pubkeyChild, ChainCode& ccChild, Span nChild, const ChainCode& cc) const { + assert(IsValid()); + assert(nChild.size() == 32); + assert(size() == COMPRESSED_SIZE); + // DIP-14 compatibility mode: indexes below 2^32 derive exactly as BIP32. + // A hardened (high bit set) 32-bit index cannot be derived from a pubkey. + if (std::all_of(nChild.begin(), nChild.begin() + 28, [](unsigned char c) { return c == 0; })) { + uint32_t child32 = ReadBE32(nChild.data() + 28); + if (child32 >> 31) return false; + return Derive(pubkeyChild, ccChild, child32, cc); + } + unsigned char out[64]; + DIP14Hash(cc, nChild.data(), *begin(), begin() + 1, out); + memcpy(ccChild.begin(), out + 32, 32); + secp256k1_pubkey pubkey; + if (!secp256k1_ec_pubkey_parse(secp256k1_context_static, &pubkey, vch, size())) { + return false; + } + if (!secp256k1_ec_pubkey_tweak_add(secp256k1_context_static, &pubkey, out)) { + return false; + } + unsigned char pub[COMPRESSED_SIZE]; + size_t publen = COMPRESSED_SIZE; + secp256k1_ec_pubkey_serialize(secp256k1_context_static, pub, &publen, &pubkey, SECP256K1_EC_COMPRESSED); + pubkeyChild.Set(pub, pub + publen); + return true; +} + EllSwiftPubKey::EllSwiftPubKey(Span ellswift) noexcept { assert(ellswift.size() == SIZE); diff --git a/src/pubkey.h b/src/pubkey.h index 990a33ccef99..a9304ffcd087 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -209,6 +209,11 @@ class CPubKey //! Derive BIP32 child pubkey. [[nodiscard]] bool Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const; + + //! Derive DIP-14 (non-hardened) child pubkey with a 256-bit index + //! (32 bytes, big-endian). Indexes below 2^32 fall back to BIP32 + //! derivation for compatibility; a hardened 32-bit index fails. + [[nodiscard]] bool Derive256(CPubKey& pubkeyChild, ChainCode& ccChild, Span nChild, const ChainCode& cc) const; }; /** An ElligatorSwift-encoded public key. */ diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index d79475001c88..a97720d9ac52 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -2,9 +2,14 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#if defined(HAVE_CONFIG_H) +#include +#endif + #include #include +#include #include #include #include @@ -31,7 +36,11 @@ #include #include #include +#include #include +#ifdef ENABLE_PLATFORM_GUI +#include +#endif #include #include #include @@ -235,6 +244,95 @@ class WalletImpl : public Wallet { return m_wallet->SignSpecialTxPayload(hash, keyid, vchSig); } +#ifdef ENABLE_PLATFORM_GUI + //! Fetch the BIP39 seed backing this wallet's HD chain (requires the + //! wallet to be unlocked). Precedent: getMnemonic() below. + bool getPlatformSeed(SecureVector& seed_out) + { + LOCK(m_wallet->cs_wallet); + if (m_wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) return false; + + if (m_wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { + SecureString mnemonic, mnemonic_passphrase; + for (auto spk_man : m_wallet->GetActiveScriptPubKeyMans()) { + if (auto desc_spk_man = dynamic_cast(spk_man)) { + if (desc_spk_man->GetMnemonicString(mnemonic, mnemonic_passphrase)) { + CMnemonic::ToSeed(mnemonic, mnemonic_passphrase, seed_out); + return !seed_out.empty(); + } + } + } + return false; + } + + auto spk_man = m_wallet->GetLegacyScriptPubKeyMan(); + if (!spk_man) return false; + CHDChain hd_chain; + if (!spk_man->GetHDChain(hd_chain)) return false; + if (m_wallet->IsCrypted() && !spk_man->GetDecryptedHDChain(hd_chain)) return false; + seed_out = hd_chain.GetSeed(); + return !seed_out.empty(); + } + bool derivePlatformKey(PlatformKeyType type, uint32_t account, uint32_t index, platformkeys::ExtKey256& out) + { + SecureVector seed; + if (!getPlatformSeed(seed)) return false; + const auto coin_type{static_cast(Params().ExtCoinType())}; + platformkeys::Path path; + switch (type) { + case PlatformKeyType::IdentityAuth: + path = platformkeys::IdentityAuthKeyPath(coin_type, account, index); + break; + case PlatformKeyType::RegistrationFunding: + path = platformkeys::IdentityFundingPath(coin_type, platformkeys::IDENTITY_REGISTRATION_FUNDING, index); + break; + case PlatformKeyType::TopupFunding: + path = platformkeys::IdentityFundingPath(coin_type, platformkeys::IDENTITY_TOPUP_FUNDING, index); + break; + case PlatformKeyType::InvitationFunding: + path = platformkeys::IdentityFundingPath(coin_type, platformkeys::IDENTITY_INVITATION_FUNDING, index); + break; + } + return platformkeys::DeriveExtKey(seed, path, out); + } + bool getPlatformPubKey(PlatformKeyType type, uint32_t account, uint32_t index, CPubKey& pubkey_out) override + { + platformkeys::ExtKey256 ext_key; + if (!derivePlatformKey(type, account, index, ext_key)) return false; + pubkey_out = ext_key.key.GetPubKey(); + return true; + } + bool signPlatformDigest(PlatformKeyType type, uint32_t account, uint32_t index, const uint256& digest, std::vector& vchSig) override + { + platformkeys::ExtKey256 ext_key; + if (!derivePlatformKey(type, account, index, ext_key)) return false; + return ext_key.key.SignCompact(digest, vchSig); + } + bool platformECDHSecret(uint32_t identity_index, uint32_t key_index, const CPubKey& counterparty, SecureVector& secret_out) override + { + platformkeys::ExtKey256 ext_key; + if (!derivePlatformKey(PlatformKeyType::IdentityAuth, identity_index, key_index, ext_key)) return false; + return platformkeys::ComputeECDHSecret(ext_key.key, counterparty, secret_out); + } + bool getFriendshipXpub(uint32_t account, const uint256& user_a_id, const uint256& user_b_id, CPubKey& pubkey_out, uint256& chaincode_out) override + { + SecureVector seed; + if (!getPlatformSeed(seed)) return false; + const auto path = platformkeys::FriendshipPath(Params().ExtCoinType(), account, + Span{user_a_id.begin(), uint256::size()}, + Span{user_b_id.begin(), uint256::size()}); + platformkeys::ExtKey256 ext_key; + if (!platformkeys::DeriveExtKey(seed, path, ext_key)) return false; + pubkey_out = ext_key.key.GetPubKey(); + chaincode_out = ext_key.chaincode; + return true; + } +#else + bool getPlatformPubKey(PlatformKeyType, uint32_t, uint32_t, CPubKey&) override { return false; } + bool signPlatformDigest(PlatformKeyType, uint32_t, uint32_t, const uint256&, std::vector&) override { return false; } + bool platformECDHSecret(uint32_t, uint32_t, const CPubKey&, SecureVector&) override { return false; } + bool getFriendshipXpub(uint32_t, const uint256&, const uint256&, CPubKey&, uint256&) override { return false; } +#endif // ENABLE_PLATFORM_GUI bool isSpendable(const CScript& script) override { LOCK(m_wallet->cs_wallet); diff --git a/src/wallet/platformkeys.cpp b/src/wallet/platformkeys.cpp new file mode 100644 index 000000000000..521f033dfaf2 --- /dev/null +++ b/src/wallet/platformkeys.cpp @@ -0,0 +1,127 @@ +// Copyright (c) 2026 The Dash 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 + +namespace wallet::platformkeys { + +Path IdentityAuthKeyPath(uint32_t coin_type, uint32_t identity_index, uint32_t key_index) +{ + // dashj DerivationPathFactory.blockchainIdentityECDSADerivationPath(index): + // m/9'/coin'/5'/0'(sub-feature)/0'(key type ECDSA)/identity'/key' + return { + PathElement::Hardened(FEATURE_PURPOSE), + PathElement::Hardened(coin_type), + PathElement::Hardened(FEATURE_IDENTITIES), + PathElement::Hardened(IDENTITY_AUTHENTICATION), + PathElement::Hardened(AUTH_KEY_TYPE_ECDSA), + PathElement::Hardened(identity_index), + PathElement::Hardened(key_index), + }; +} + +Path IdentityFundingPath(uint32_t coin_type, uint32_t subfeature, uint32_t index) +{ + // dashj DerivationPathFactory.blockchainIdentity{Registration,Topup}Funding- + // DerivationPath() / identityInvitationFundingDerivationPath(), plus the + // hardened address index appended by AuthenticationKeyChain: + // m/9'/coin'/5'/{1,2,3}'/index' + assert(subfeature == IDENTITY_REGISTRATION_FUNDING || subfeature == IDENTITY_TOPUP_FUNDING || + subfeature == IDENTITY_INVITATION_FUNDING); + return { + PathElement::Hardened(FEATURE_PURPOSE), + PathElement::Hardened(coin_type), + PathElement::Hardened(FEATURE_IDENTITIES), + PathElement::Hardened(subfeature), + PathElement::Hardened(index), + }; +} + +Path FriendshipPath(uint32_t coin_type, uint32_t account, Span user_a_id, Span user_b_id) +{ + // dashj FriendKeyChain.getContactPath(): + // m/9'/coin'/15'/account'/userA/userB with the two 256-bit identity ids + // NOT hardened (DIP-14/DIP-15), enabling watch-only xpub derivation. + assert(user_a_id.size() == 32); + assert(user_b_id.size() == 32); + std::array a, b; + std::copy(user_a_id.begin(), user_a_id.end(), a.begin()); + std::copy(user_b_id.begin(), user_b_id.end(), b.begin()); + return { + PathElement::Hardened(FEATURE_PURPOSE), + PathElement::Hardened(coin_type), + PathElement::Hardened(FEATURE_DASHPAY), + PathElement::Hardened(account), + PathElement::Normal256(a), + PathElement::Normal256(b), + }; +} + +bool DeriveExtKey(Span seed, const Path& path, ExtKey256& out) +{ + CExtKey master; + master.SetSeed(MakeByteSpan(seed)); + if (!master.key.IsValid()) return false; + + CKey key{master.key}; + ChainCode chaincode{master.chaincode}; + + for (const auto& element : path) { + CKey child_key; + ChainCode child_cc; + bool ok{false}; + if (const auto* index32 = std::get_if(&element.index)) { + if (*index32 >> 31) return false; // must use the hardened flag instead + ok = key.Derive(child_key, child_cc, *index32 | (element.hardened ? 0x80000000u : 0), chaincode); + } else { + const auto& index256 = std::get>(element.index); + ok = key.Derive256(child_key, child_cc, index256, element.hardened, chaincode); + } + if (!ok) return false; + key = child_key; + chaincode = child_cc; + } + + out.key = key; + out.chaincode = chaincode; + return true; +} + +bool DerivePubKey(const ExtPubKey256& parent, const PathElement& element, ExtPubKey256& out) +{ + if (element.hardened) return false; + if (const auto* index32 = std::get_if(&element.index)) { + if (*index32 >> 31) return false; + return parent.pubkey.Derive(out.pubkey, out.chaincode, *index32, parent.chaincode); + } + const auto& index256 = std::get>(element.index); + return parent.pubkey.Derive256(out.pubkey, out.chaincode, index256, parent.chaincode); +} + +bool ComputeECDHSecret(const CKey& key, const CPubKey& counterparty, SecureVector& secret_out) +{ + if (!key.IsValid() || !counterparty.IsValid()) return false; + + secp256k1_pubkey pubkey; + if (!secp256k1_ec_pubkey_parse(secp256k1_context_static, &pubkey, counterparty.data(), counterparty.size())) { + return false; + } + + secret_out.assign(32, 0); + // Default KDF: SHA256 of the compressed shared point — identical to + // dashj's Secp256k1ECDHAgreement (DashPay contact request encryption). + if (!secp256k1_ecdh(secp256k1_context_static, secret_out.data(), &pubkey, + UCharCast(key.begin()), nullptr, nullptr)) { + secret_out.clear(); + return false; + } + return true; +} + +} // namespace wallet::platformkeys diff --git a/src/wallet/platformkeys.h b/src/wallet/platformkeys.h new file mode 100644 index 000000000000..40196ec3305d --- /dev/null +++ b/src/wallet/platformkeys.h @@ -0,0 +1,106 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_WALLET_PLATFORMKEYS_H +#define BITCOIN_WALLET_PLATFORMKEYS_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +/** + * Dash Platform (DIP-9/13/14/15) key derivation for the wallet. + * + * This is pure BIP32/DIP-14 key math: it knows nothing about Platform + * documents, contracts or the network. It is only compiled with + * --enable-platform-gui and only used through the interfaces::Wallet + * platform key provider methods. + * + * References: + * - DIP-9 feature-purpose derivation (m/9'/...) + * - DIP-13 identity keys (m/9'/coin'/5'/...) + * - DIP-14 256-bit child indexes (CKey::Derive256) + * - DIP-15 DashPay friendship keychains (m/9'/coin'/15'/account'/idA/idB) + * - dashj DerivationPathFactory.java / FriendKeyChain.java (reference impl) + */ +namespace wallet::platformkeys { + +//! One component of a derivation path: a 31-bit BIP32 index or a DIP-14 +//! 256-bit index (big-endian bytes), plus a hardened flag. +struct PathElement { + std::variant> index; + bool hardened{false}; + + static PathElement Hardened(uint32_t i) { return {i, true}; } + static PathElement Normal(uint32_t i) { return {i, false}; } + static PathElement Normal256(const std::array& i) { return {i, false}; } +}; +using Path = std::vector; + +// DIP-9 feature purpose, and DIP-13/DIP-15 feature types under it. +inline constexpr uint32_t FEATURE_PURPOSE{9}; +inline constexpr uint32_t FEATURE_IDENTITIES{5}; // DIP-13 +inline constexpr uint32_t FEATURE_DASHPAY{15}; // DIP-15 + +// DIP-13 sub-features: m/9'/coin'/5'/' +inline constexpr uint32_t IDENTITY_AUTHENTICATION{0}; +inline constexpr uint32_t IDENTITY_REGISTRATION_FUNDING{1}; +inline constexpr uint32_t IDENTITY_TOPUP_FUNDING{2}; +inline constexpr uint32_t IDENTITY_INVITATION_FUNDING{3}; + +// DIP-13 authentication key types: m/9'/coin'/5'/0'/' +inline constexpr uint32_t AUTH_KEY_TYPE_ECDSA{0}; +inline constexpr uint32_t AUTH_KEY_TYPE_BLS{1}; + +//! An extended key produced by walking a (possibly 256-bit) derivation path. +//! Unlike CExtKey this does not carry BIP32 serialization metadata; DIP-14 +//! extended-key serialization tracks the path separately. +struct ExtKey256 { + CKey key; + ChainCode chaincode; + + ExtKey256() = default; +}; + +struct ExtPubKey256 { + CPubKey pubkey; + ChainCode chaincode; +}; + +//! m/9'/coin'/5'/0'/0'/'/' — ECDSA identity authentication key +//! (all components hardened; key 0 = MASTER, key 1 = HIGH security level). +Path IdentityAuthKeyPath(uint32_t coin_type, uint32_t identity_index, uint32_t key_index); + +//! m/9'/coin'/5'/'/' — L1 funding keys for identity +//! registration (1'), top-ups (2') and invitations (3'). +Path IdentityFundingPath(uint32_t coin_type, uint32_t subfeature, uint32_t index); + +//! m/9'/coin'/15'/'// — DIP-15 friendship keychain +//! root. The two 256-bit identity ids are NOT hardened (this is what allows +//! watch-only derivation from an exported xpub); the receiving chain is +//! (userA = my id, userB = their id), the sending chain is the reverse. +Path FriendshipPath(uint32_t coin_type, uint32_t account, Span user_a_id, Span user_b_id); + +//! Derive the extended key at `path` from a BIP39 seed (the same 64-byte +//! seed CHDChain stores). Returns false if any derivation step fails. +[[nodiscard]] bool DeriveExtKey(Span seed, const Path& path, ExtKey256& out); + +//! Derive a (non-hardened) child extended pubkey. Fails on hardened steps. +[[nodiscard]] bool DerivePubKey(const ExtPubKey256& parent, const PathElement& element, ExtPubKey256& out); + +//! ECDH shared secret between `key` and `counterparty`, using the libsecp256k1 +//! ECDH KDF (SHA256 of the compressed shared point). This matches dashj's +//! KeyCrypterECDH / Secp256k1ECDHAgreement, used for DashPay contact request +//! encryption. Returns a 32-byte secret. +[[nodiscard]] bool ComputeECDHSecret(const CKey& key, const CPubKey& counterparty, SecureVector& secret_out); + +} // namespace wallet::platformkeys + +#endif // BITCOIN_WALLET_PLATFORMKEYS_H diff --git a/src/wallet/test/platformkeys_tests.cpp b/src/wallet/test/platformkeys_tests.cpp new file mode 100644 index 000000000000..c884c79c9216 --- /dev/null +++ b/src/wallet/test/platformkeys_tests.cpp @@ -0,0 +1,144 @@ +// Copyright (c) 2026 The Dash 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 + +using namespace wallet::platformkeys; + +BOOST_FIXTURE_TEST_SUITE(platformkeys_tests, BasicTestingSetup) + +//! Seed shared by all DIP-14 test vectors (dashpay/dips dip-0014.md), from +//! mnemonic "birth kingdom trash renew flavor utility donkey gasp regular +//! alert pave layer". +static SecureVector Dip14Seed() +{ + const std::vector seed{ParseHex( + "b16d3782e714da7c55a397d5f19104cfed7ffa8036ac514509bbb50807f8ac59" + "8eeb26f0797bd8cc221a6cbff2168d90a5e9ee025a5bd977977b9eccd97894bb")}; + return SecureVector{seed.begin(), seed.end()}; +} + +static std::array Arr32(const std::string& hex) +{ + const std::vector v{ParseHex(hex)}; + BOOST_REQUIRE_EQUAL(v.size(), 32U); + std::array out; + std::copy(v.begin(), v.end(), out.begin()); + return out; +} + +static std::string DerivedKeyHex(const Path& path) +{ + ExtKey256 out; + BOOST_REQUIRE(DeriveExtKey(Dip14Seed(), path, out)); + return HexStr(Span{out.key.begin(), out.key.size()}); +} + +// DIP-14 test vector 1: m//'//0 +BOOST_AUTO_TEST_CASE(dip14_vector_1) +{ + const Path path{ + PathElement::Normal256(Arr32("775d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3b")), + PathElement{Arr32("f537439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89a6"), true}, + PathElement::Normal256(Arr32("4c4592ca670c983fc43397dfd21a6f427fac9b4ac53cb4dcdc6522ec51e81e79")), + PathElement::Normal(0), + }; + BOOST_CHECK_EQUAL(DerivedKeyHex(path), "e8781fdef72862968cd9a4d2df34edaf9dcc5b17629ec505f0d2d1a8ed6f9f09"); +} + +// DIP-14 test vector 2: m/9'/5'/15'/0'/'/'/0 (DIP-15 shape) +BOOST_AUTO_TEST_CASE(dip14_vector_2) +{ + const Path path{ + PathElement::Hardened(9), + PathElement::Hardened(5), + PathElement::Hardened(15), + PathElement::Hardened(0), + PathElement{Arr32("555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a"), true}, + PathElement{Arr32("a137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5"), true}, + PathElement::Normal(0), + }; + BOOST_CHECK_EQUAL(DerivedKeyHex(path), "fac40790776d171ee1db90899b5eb2df2f7d2aaf35ad56f07ffb8ed2c57f8e60"); +} + +// DIP-14 test vector 3: m/ (single 256-bit non-hardened step) +BOOST_AUTO_TEST_CASE(dip14_vector_3) +{ + const Path path{ + PathElement::Normal256(Arr32("775d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3b")), + }; + BOOST_CHECK_EQUAL(DerivedKeyHex(path), "f6a95ae75ea8362d9478932f71b262b3d981918fe030316686a475dea4889938"); +} + +// DIP-14 test vector 4: m//' +BOOST_AUTO_TEST_CASE(dip14_vector_4) +{ + const Path path{ + PathElement::Normal256(Arr32("775d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3b")), + PathElement{Arr32("f537439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89a6"), true}, + }; + BOOST_CHECK_EQUAL(DerivedKeyHex(path), "b898ad92d3a0698bc3117d3777d82676673816ce52f4fc2f1263a2f676825f90"); +} + +//! Non-hardened 256-bit public derivation must match private derivation +//! (this is what lets a contact derive our friendship addresses from an +//! exported xpub). +BOOST_AUTO_TEST_CASE(dip14_public_derivation_matches) +{ + const Path parent_path{ + PathElement::Hardened(9), + PathElement::Hardened(1), + PathElement::Hardened(15), + PathElement::Hardened(0), + }; + ExtKey256 parent; + BOOST_REQUIRE(DeriveExtKey(Dip14Seed(), parent_path, parent)); + + const auto id_a{Arr32("555d3854c910b7dee436869c4724bed2fe0784e198b8a39f02bbb49d8ebcfc3a")}; + const auto id_b{Arr32("a137439f36d04a15474ff7423e4b904a14373fafb37a41db74c84f1dbb5c89b5")}; + + // Private side: parent/idA/idB + Path leaf_path{parent_path}; + leaf_path.push_back(PathElement::Normal256(id_a)); + leaf_path.push_back(PathElement::Normal256(id_b)); + ExtKey256 leaf; + BOOST_REQUIRE(DeriveExtKey(Dip14Seed(), leaf_path, leaf)); + + // Public side: neuter parent, then derive idA/idB + ExtPubKey256 pub_parent{parent.key.GetPubKey(), parent.chaincode}; + ExtPubKey256 pub_mid, pub_leaf; + BOOST_REQUIRE(DerivePubKey(pub_parent, PathElement::Normal256(id_a), pub_mid)); + BOOST_REQUIRE(DerivePubKey(pub_mid, PathElement::Normal256(id_b), pub_leaf)); + + BOOST_CHECK(pub_leaf.pubkey == leaf.key.GetPubKey()); + BOOST_CHECK(pub_leaf.chaincode == leaf.chaincode); + + // Hardened steps must be rejected on the public side. + ExtPubKey256 unused; + BOOST_CHECK(!DerivePubKey(pub_parent, PathElement{id_a, true}, unused)); + BOOST_CHECK(!DerivePubKey(pub_parent, PathElement::Hardened(0), unused)); +} + +//! ECDH secrets must be symmetric and match the libsecp256k1 KDF +//! (SHA256 of compressed shared point), as used by DashPay contact requests. +BOOST_AUTO_TEST_CASE(ecdh_symmetry) +{ + CKey a, b; + a.MakeNewKey(/*fCompressed=*/true); + b.MakeNewKey(/*fCompressed=*/true); + + SecureVector s_ab, s_ba; + BOOST_REQUIRE(ComputeECDHSecret(a, b.GetPubKey(), s_ab)); + BOOST_REQUIRE(ComputeECDHSecret(b, a.GetPubKey(), s_ba)); + BOOST_CHECK_EQUAL(s_ab.size(), 32U); + BOOST_CHECK(s_ab == s_ba); +} + +BOOST_AUTO_TEST_SUITE_END() From 37e089bc2d2dc41a3e44764141ffdbfc33a4747f Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 01:14:38 -0500 Subject: [PATCH 03/33] feat: asset lock creation and node platform data seams Wallet: interfaces::Wallet::createAssetLockTransaction builds a signed TRANSACTION_ASSET_LOCK special transaction (single OP_RETURN burn output carrying the credit amount, CAssetLockPayload with one P2PKH credit output) funded via CreateTransaction and signed as a whole, ready for commitTransaction. This is the L1 leg of Platform identity funding. Node: expose locally synced data the GUI Platform client needs for verification and asset lock proofs: - LLMQ::getPlatformQuorums(llmq_type): active quorum hashes + BLS public keys (basic scheme) so Platform state-root quorum signatures can be verified against the node's own quorum list (no external trust source) - LLMQ::getInstantSendLock(txid): serialized islock for building InstantAssetLockProofs - MnEntry::getPlatformHTTPSAddrs(): evonode DAPI gateway endpoints from the extended masternode address list Co-Authored-By: Claude Fable 5 --- src/interfaces/node.h | 15 ++++++++++++ src/interfaces/wallet.h | 9 ++++++++ src/node/interfaces.cpp | 48 +++++++++++++++++++++++++++++++++++++++ src/wallet/interfaces.cpp | 38 +++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+) diff --git a/src/interfaces/node.h b/src/interfaces/node.h index 21bab8a95ec3..51a76e524348 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -74,6 +74,9 @@ class MnEntry virtual bool isBanned() const = 0; virtual CService getNetInfoPrimary() const = 0; + //! Platform HTTPS (DAPI gateway) endpoints from the extended address + //! list; empty for non-evo masternodes. + virtual std::vector getPlatformHTTPSAddrs() const = 0; virtual MnType getType() const = 0; virtual UniValue toJson() const = 0; virtual const CKeyID& getKeyIdOwner() const = 0; @@ -213,6 +216,18 @@ class LLMQ int32_t m_expiry_height{0}; }; virtual std::vector getQuorumStats() = 0; + struct PlatformQuorum { + uint256 m_quorum_hash{}; + std::vector m_pubkey{}; //!< serialized BLS public key (basic scheme) + int32_t m_height{0}; + }; + //! Active quorums of the given LLMQ type with their public keys. Used by + //! the GUI Platform client to verify Platform state-root quorum + //! signatures against locally synced quorum data. + virtual std::vector getPlatformQuorums(uint8_t llmq_type) = 0; + //! Serialized InstantSend lock for the given txid, or empty if the + //! transaction has no islock (used to build asset lock proofs). + virtual std::vector getInstantSendLock(const uint256& txid) = 0; virtual void setContext(node::NodeContext* context) {} }; diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index efd99952b818..6bb283805b72 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -227,6 +227,15 @@ class Wallet int& change_pos, CAmount& fee) = 0; + //! Create a signed (uncommitted) asset lock transaction converting + //! credit_amount duffs into Platform credits, with a single credit + //! output paying P2PKH to credit_pubkey (typically a registration + //! funding key from getPlatformPubKey). Broadcast the result with + //! commitTransaction(). Fails unless built with --enable-platform-gui. + virtual util::Result createAssetLockTransaction(CAmount credit_amount, + const CPubKey& credit_pubkey, + const wallet::CCoinControl& coin_control) = 0; + //! Commit transaction. virtual void commitTransaction(CTransactionRef tx, WalletValueMap value_map, diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 08587e77a973..5c77cf0e90bb 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -131,6 +131,16 @@ class MnEntryImpl : public MnEntry bool isBanned() const override { return m_dmn->pdmnState->IsBanned(); } CService getNetInfoPrimary() const override { return m_dmn->pdmnState->netInfo->GetPrimary(); } + std::vector getPlatformHTTPSAddrs() const override + { + std::vector ret; + for (const auto& entry : m_dmn->pdmnState->netInfo->GetEntries(NetInfoPurpose::PLATFORM_HTTPS)) { + if (const auto service_opt{entry.GetAddrPort()}) { + ret.push_back(*service_opt); + } + } + return ret; + } MnType getType() const override { return m_dmn->nType; } UniValue toJson() const override { return m_dmn->ToJson(); } const CKeyID& getKeyIdOwner() const override { return m_dmn->pdmnState->keyIDOwner; } @@ -532,6 +542,44 @@ class LLMQImpl : public LLMQ } return stats; } + std::vector getPlatformQuorums(uint8_t llmq_type) override + { + std::vector ret; + if (!context().llmq_ctx || !context().llmq_ctx->qman || !context().chainman) { + return ret; + } + const auto* pindex{WITH_LOCK(::cs_main, return context().chainman->ActiveChain().Tip())}; + if (!pindex) { + return ret; + } + const auto type{static_cast(llmq_type)}; + const auto llmq_params{Params().GetLLMQ(type)}; + if (!llmq_params.has_value()) { + return ret; + } + for (const auto& q : context().llmq_ctx->qman->ScanQuorums(type, pindex, llmq_params->signingActiveQuorumCount)) { + if (!q->qc->quorumPublicKey.IsValid()) continue; + ret.emplace_back(PlatformQuorum{ + .m_quorum_hash = q->qc->quorumHash, + .m_pubkey = q->qc->quorumPublicKey.ToByteVector(/*specificLegacyScheme=*/false), + .m_height = q->m_quorum_base_block_index ? q->m_quorum_base_block_index->nHeight : 0, + }); + } + return ret; + } + std::vector getInstantSendLock(const uint256& txid) override + { + if (!context().llmq_ctx || !context().llmq_ctx->isman) { + return {}; + } + const auto islock{context().llmq_ctx->isman->GetInstantSendLockByTxid(txid)}; + if (!islock) { + return {}; + } + CDataStream ds(SER_NETWORK, PROTOCOL_VERSION); + ds << *islock; + return {UCharCast(ds.data()), UCharCast(ds.data()) + ds.size()}; + } void setContext(NodeContext* context) override { m_context = context; diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index a97720d9ac52..8c252ea5b1e6 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -42,7 +42,9 @@ #include #endif #include +#include #include +#include #include #include #include @@ -485,6 +487,42 @@ class WalletImpl : public Wallet return txr.tx; } + util::Result createAssetLockTransaction(CAmount credit_amount, + const CPubKey& credit_pubkey, + const CCoinControl& coin_control) override + { +#ifdef ENABLE_PLATFORM_GUI + if (credit_amount <= 0 || !credit_pubkey.IsValid()) { + return util::Error{Untranslated("invalid asset lock parameters")}; + } + + LOCK(m_wallet->cs_wallet); + + CMutableTransaction mtx; + mtx.nVersion = 3; + mtx.nType = TRANSACTION_ASSET_LOCK; + const CAssetLockPayload payload{{CTxOut{credit_amount, GetScriptForDestination(PKHash{credit_pubkey})}}}; + SetTxPayload(mtx, payload); + + // The single OP_RETURN "burn" output must carry the total credit + // amount (see CheckAssetLockTx). + const std::vector recipients{{CScript() << OP_RETURN << OP_0, credit_amount, /*fSubtractFeeFromAmount=*/false}}; + auto res = CreateTransaction(*m_wallet, recipients, RANDOM_CHANGE_POSITION, coin_control, + /*sign=*/false, static_cast(mtx.vExtraPayload.size())); + if (!res) return util::Error{util::ErrorString(res)}; + + // Graft funded inputs/outputs onto the special tx and sign it as a + // whole (input signatures commit to nType and vExtraPayload). + mtx.vin = res->tx->vin; + mtx.vout = res->tx->vout; + if (!m_wallet->SignTransaction(mtx)) { + return util::Error{Untranslated("failed to sign asset lock transaction")}; + } + return MakeTransactionRef(std::move(mtx)); +#else + return util::Error{Untranslated("platform support is not compiled in")}; +#endif // ENABLE_PLATFORM_GUI + } void commitTransaction(CTransactionRef tx, WalletValueMap value_map, WalletOrderForm order_form) override From 69336a1fddc865d1ad2d57546138613632a919c0 Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 01:14:51 -0500 Subject: [PATCH 04/33] feat(platform): define PlatformClient interface and shared GUI types Qt-free contract between the DAPI transport implementation and the GUI service layer: async PlatformClient (every query issued with prove=true and verified before callbacks fire; endpoints from the deterministic MN list, quorum keys injected from the node) and the plain structs the GUI consumes (Identity, DpnsName, Profile, ContactRequest, contested name state, broadcast results). Co-Authored-By: Claude Fable 5 --- src/platform/client.h | 107 +++++++++++++++++++++++++++++++++++++++++ src/platform/types.h | 108 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 src/platform/client.h create mode 100644 src/platform/types.h diff --git a/src/platform/client.h b/src/platform/client.h new file mode 100644 index 000000000000..8afd468b5e5c --- /dev/null +++ b/src/platform/client.h @@ -0,0 +1,107 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_CLIENT_H +#define BITCOIN_PLATFORM_CLIENT_H + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace platform { + +//! A quorum public key the client may verify proofs against, fed from the +//! node's locally synced LLMQ data (interfaces::Node::LLMQ). +struct QuorumKey { + uint256 quorum_hash; + std::vector pubkey; //!< serialized BLS public key (basic scheme) + int32_t height{0}; +}; + +//! An evonode DAPI endpoint, fed from the deterministic masternode list. +struct Endpoint { + CService service; //!< platform HTTPS (gRPC-Web gateway) addr:port + uint256 pro_tx_hash{}; +}; + +template +struct Result { + std::optional value; + std::string error; + ResponseMetadata metadata; + + bool ok() const { return value.has_value(); } +}; + +//! Abstract asynchronous Dash Platform (DAPI) client. +//! +//! Implementations own their I/O threads; callbacks fire on client threads +//! and consumers must marshal to their own thread (the Qt layer uses +//! QMetaObject::invokeMethod). Every query is issued with prove=true and the +//! response is verified (GroveDB proof replay to the root hash, then the +//! Tenderdash quorum signature against the quorum keys supplied via +//! updateQuorumKeys) before the callback sees it; unverifiable responses +//! surface as errors and the offending endpoint is rotated out. +class PlatformClient +{ +public: + template + using Callback = std::function)>; + + virtual ~PlatformClient() = default; + + //! Resolve an exact normalized label under a parent domain ("dash"). + //! Absence (name available) yields ok() with std::nullopt-like empty + //! optional inside the vector-free overloads below; here: value with + //! std::optional empty means proven absent. + virtual void resolveName(const std::string& normalized_label, Callback> cb) = 0; + + //! Prefix search over normalizedLabel (startsWith), limited. + virtual void searchNames(const std::string& prefix, uint32_t limit, Callback> cb) = 0; + + //! Reverse lookup: all names whose records.identity == identity. + virtual void namesOfIdentity(const Identifier& identity, Callback> cb) = 0; + + virtual void getIdentity(const Identifier& id, Callback> cb) = 0; + virtual void getIdentityByPublicKeyHash(const std::array& pubkey_hash, Callback> cb) = 0; + virtual void getIdentityNonce(const Identifier& id, Callback cb) = 0; + virtual void getIdentityContractNonce(const Identifier& id, const Identifier& contract_id, Callback cb) = 0; + + virtual void getProfile(const Identifier& owner_id, Callback> cb) = 0; + //! Contact requests sent to (to_me=true) or by (to_me=false) the given + //! identity, created at or after since_ms (0 = all). + virtual void getContactRequests(const Identifier& identity, bool to_me, uint64_t since_ms, Callback> cb) = 0; + + virtual void getContestedNameState(const std::string& normalized_label, Callback cb) = 0; + + //! Broadcast a serialized state transition. The result's error channel is + //! unverified; confirm success with a proved re-query of the created + //! object (waitForStateTransitionResult is used internally to surface + //! consensus errors quickly). + virtual void broadcastStateTransition(const std::vector& state_transition, Callback cb) = 0; + + //! Node-local trust/context injection. + virtual void updateEndpoints(std::vector endpoints) = 0; + virtual void updateQuorumKeys(uint8_t llmq_type, std::vector keys) = 0; + + //! Stop all I/O and drop pending callbacks (must be called before the + //! consumer is destroyed). + virtual void shutdown() = 0; +}; + +//! Create the production gRPC-Web/TLS client (implemented in +//! platform/transport/, Phase 7). +std::unique_ptr MakeGrpcWebPlatformClient(const Params& params); + +} // namespace platform + +#endif // BITCOIN_PLATFORM_CLIENT_H diff --git a/src/platform/types.h b/src/platform/types.h new file mode 100644 index 000000000000..0ecc7de7434d --- /dev/null +++ b/src/platform/types.h @@ -0,0 +1,108 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_TYPES_H +#define BITCOIN_PLATFORM_TYPES_H + +#include + +#include +#include +#include +#include +#include + +namespace platform { + +//! Identity public key record (DPP IdentityPublicKey, subset the GUI needs). +struct IdentityPublicKey { + enum class Type : uint8_t { ECDSA_SECP256K1 = 0, BLS12_381 = 1, ECDSA_HASH160 = 2, BIP13_SCRIPT_HASH = 3, EDDSA_25519_HASH160 = 4 }; + enum class Purpose : uint8_t { AUTHENTICATION = 0, ENCRYPTION = 1, DECRYPTION = 2, TRANSFER = 3, VOTING = 5 }; + enum class SecurityLevel : uint8_t { MASTER = 0, CRITICAL = 1, HIGH = 2, MEDIUM = 3 }; + + uint32_t id{0}; + Purpose purpose{Purpose::AUTHENTICATION}; + SecurityLevel security_level{SecurityLevel::MASTER}; + Type type{Type::ECDSA_SECP256K1}; + bool read_only{false}; + std::vector data; //!< serialized public key + std::optional disabled_at; +}; + +//! A Platform identity as the GUI sees it. +struct Identity { + Identifier id{}; + uint64_t balance{0}; //!< platform credits + uint64_t revision{0}; + std::vector public_keys; +}; + +//! A resolved DPNS name (domain document subset). +struct DpnsName { + std::string label; //!< as registered, e.g. "Alice" + std::string normalized_label; //!< homograph-safe lower-case, e.g. "al1ce" + std::string parent_domain; //!< normalized parent, e.g. "dash" + Identifier identity{}; //!< records.identity + Identifier document_id{}; +}; + +//! DashPay profile document (all fields optional per schema). +struct Profile { + Identifier owner_id{}; + std::string display_name; + std::string public_message; + std::string avatar_url; + std::vector avatar_hash; //!< SHA256 of avatar (32B) if set + std::vector avatar_fingerprint; //!< dHash (8B) if set + uint64_t created_at{0}; //!< ms since epoch + uint64_t updated_at{0}; + uint64_t revision{0}; +}; + +//! DashPay contactRequest document. +struct ContactRequest { + Identifier owner_id{}; //!< sender identity + Identifier to_user_id{}; //!< recipient identity + std::vector encrypted_public_key; //!< 96B: IV(16) || AES-CBC(xpub) + uint32_t sender_key_index{0}; + uint32_t recipient_key_index{0}; + uint32_t account_reference{0}; + std::vector encrypted_account_label; //!< optional, 48-80B + uint32_t core_height_created_at{0}; + uint64_t created_at{0}; //!< ms since epoch + Identifier document_id{}; +}; + +//! Contested-resource (premium username) vote state. +struct ContestedNameState { + enum class Status { UNKNOWN, CONTEST_IN_PROGRESS, WON, LOST, LOCKED }; + Status status{Status::UNKNOWN}; + std::string normalized_label; + std::vector> contenders; //!< identity -> votes + uint32_t abstain_votes{0}; + uint32_t lock_votes{0}; + std::optional ends_at; //!< ms since epoch +}; + +//! Result of broadcasting a state transition. +struct BroadcastResult { + bool accepted{false}; + //! When not accepted: a platform consensus error description. Note this + //! error channel is informational (not proof-backed); success is + //! confirmed separately through a proved re-query of the created object. + std::string error; + uint32_t error_code{0}; +}; + +//! Metadata every proved response is verified against. +struct ResponseMetadata { + uint64_t height{0}; //!< platform block height + uint32_t core_chain_locked_height{0}; + uint64_t time_ms{0}; + uint32_t protocol_version{0}; +}; + +} // namespace platform + +#endif // BITCOIN_PLATFORM_TYPES_H From dddbddb1ae7158c58c67224cc6dbc181b488b42b Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 01:20:00 -0500 Subject: [PATCH 05/33] feat(wallet): generic platform data records; define state transition builder contract - Wallet DB: new DBKeys::PLATFORM_DATA generic key/value records (opaque to the wallet, travel with backups) with CWallet load/write/prefix-scan and interfaces::Wallet::writePlatformData/getPlatformData. The Platform GUI persists its identity/username/contact flow state here so multi-step flows survive restarts and are recoverable from backups. - src/platform/statetransitions.h: contract for DPP state transition construction (identity create from asset lock proofs, DPNS preorder/domain, DashPay profile/contactRequest), with signing delegated to wallet callbacks so keys never leave the wallet. Co-Authored-By: Claude Fable 5 --- src/Makefile.am | 1 + src/interfaces/wallet.h | 8 ++ src/platform/statetransitions.h | 140 ++++++++++++++++++++++++++++++++ src/wallet/interfaces.cpp | 10 +++ src/wallet/wallet.cpp | 30 +++++++ src/wallet/wallet.h | 11 +++ src/wallet/walletdb.cpp | 17 ++++ src/wallet/walletdb.h | 6 ++ 8 files changed, 223 insertions(+) create mode 100644 src/platform/statetransitions.h diff --git a/src/Makefile.am b/src/Makefile.am index 0d5ca41200e0..515a4358d3cb 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -695,6 +695,7 @@ libdash_platform_a_SOURCES = \ platform/client.h \ platform/params.cpp \ platform/params.h \ + platform/statetransitions.h \ platform/types.h endif # diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 6bb283805b72..87f1541aba10 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -190,6 +190,14 @@ class Wallet //! Save or remove receive request. virtual bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) = 0; + //! Write (or, with an empty value, erase) a generic Platform GUI data + //! record. Records are persisted in the wallet database and travel with + //! backups; they are opaque to the wallet itself. + virtual bool writePlatformData(const std::string& key, const std::vector& value) = 0; + + //! All Platform GUI data records whose key starts with prefix. + virtual std::map> getPlatformData(const std::string& prefix) = 0; + //! Display address on external signer virtual bool displayAddress(const CTxDestination& dest) = 0; diff --git a/src/platform/statetransitions.h b/src/platform/statetransitions.h new file mode 100644 index 000000000000..ebef3cfa57f5 --- /dev/null +++ b/src/platform/statetransitions.h @@ -0,0 +1,140 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_STATETRANSITIONS_H +#define BITCOIN_PLATFORM_STATETRANSITIONS_H + +#include +#include + +#include +#include +#include +#include +#include +#include + +/** + * Construction and signing of DPP state transitions (bincode + * platform-serialization, pinned to a single Platform protocol version). + * Mirrors dashpay/platform packages/rs-dpp state_transition types; every + * builder returns the exact bytes to hand to + * PlatformClient::broadcastStateTransition. + * + * Signing is delegated through callbacks so private keys stay in the wallet + * (interfaces::Wallet::signPlatformDigest): the callback receives the + * double-SHA256 digest of the transition's signable bytes and must return a + * compact/recoverable ECDSA signature (65 bytes). + */ +namespace platform::st { + +//! Signs a 32-byte digest, returning a 65-byte compact recoverable ECDSA +//! signature. Returns false on failure (e.g. locked wallet). +using Signer = std::function& sig_out)>; + +template +struct Result { + std::optional value; + std::string error; + bool ok() const { return value.has_value(); } +}; + +//! Asset lock proof for identity create/top-up. +struct InstantAssetLockProof { + std::vector transaction; //!< serialized asset lock tx + std::vector instant_lock; //!< serialized islock message + uint32_t output_index{0}; //!< index of the OP_RETURN output in tx.vout +}; +struct ChainAssetLockProof { + uint32_t core_chain_locked_height{0}; + std::array out_point{}; //!< txid (big-endian) || index (LE u32) +}; + +//! Identity public key to register with a new identity. +struct NewIdentityKey { + uint32_t id{0}; + IdentityPublicKey::Purpose purpose{IdentityPublicKey::Purpose::AUTHENTICATION}; + IdentityPublicKey::SecurityLevel security_level{IdentityPublicKey::SecurityLevel::MASTER}; + std::vector pubkey; //!< compressed secp256k1 public key (33B) + //! Signs with the corresponding private key (each registered key must + //! prove ownership by signing the transition's signable bytes). + Signer signer; +}; + +struct BuiltTransition { + std::vector bytes; //!< serialized signed state transition + uint256 hash; //!< sha256(bytes) — wait handle for waitForStateTransitionResult +}; + +//! Compute the identity id for an asset lock outpoint +//! (DSHA256 of the 36-byte outpoint), as InstantAssetLockProof::createIdentifier. +Identifier IdentityIdFromOutpoint(const std::array& out_point); + +//! IdentityCreateTransition: registers public keys funded by the asset lock; +//! signed by the asset-lock one-time private key (asset_lock_signer). +Result BuildIdentityCreate( + const std::variant& proof, + const std::vector& keys, + const Signer& asset_lock_signer); + +//! DPNS preorder document create (documents batch transition), signed by the +//! identity HIGH-level authentication key. +Result BuildDpnsPreorder( + const Identifier& identity, + uint64_t identity_contract_nonce, + const std::array& salted_domain_hash, + uint32_t signature_public_key_id, + const Signer& signer); + +//! DPNS domain document create. entropy is the 32-byte document entropy used +//! for the document id. preorder_salt must match the earlier preorder: +//! salted_domain_hash = DSHA256(salt || normalized_label || "." || parent). +Result BuildDpnsDomain( + const Identifier& identity, + uint64_t identity_contract_nonce, + const std::string& label, + const std::string& normalized_label, + const std::string& parent_domain, //!< "dash" + const std::array& preorder_salt, + uint32_t signature_public_key_id, + const Signer& signer); + +//! DashPay profile create (revision 1) or replace (revision > 1). +Result BuildProfile( + const Identifier& identity, + uint64_t identity_contract_nonce, + const Profile& profile, + uint64_t revision, + const std::optional& existing_document_id, //!< set for replace + uint32_t signature_public_key_id, + const Signer& signer); + +//! DashPay contactRequest create. +Result BuildContactRequest( + const Identifier& identity, + uint64_t identity_contract_nonce, + const ContactRequest& request, + uint32_t signature_public_key_id, + const Signer& signer); + +//! Compute the salted domain hash for a DPNS (pre)order: +//! DSHA256(salt || ) per +//! packages/rs-dpp DPNS logic (verify the exact concatenation against the +//! Rust implementation when implementing). +std::array SaltedDomainHash( + const std::array& salt, + const std::string& normalized_label, + const std::string& parent_domain); + +//! Homograph-safe normalization for DPNS labels (o/O->0, i/I/l/L->1, +//! lower-case) per platform convertToHomographSafeChars. +std::string NormalizeLabel(const std::string& label); + +//! True if the normalized label is contested (masternode vote) per the DPNS +//! v1 contract: length 3..19 and matches [a-hj-km-np-z0-1-]+ style rules. +bool IsContestedLabel(const std::string& normalized_label); + +} // namespace platform::st + +#endif // BITCOIN_PLATFORM_STATETRANSITIONS_H diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 8c252ea5b1e6..839b8498b2e5 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -412,6 +412,16 @@ class WalletImpl : public Wallet return value.empty() ? m_wallet->EraseAddressReceiveRequest(batch, dest, id) : m_wallet->SetAddressReceiveRequest(batch, dest, id, value); } + bool writePlatformData(const std::string& key, const std::vector& value) override + { + LOCK(m_wallet->cs_wallet); + return m_wallet->WritePlatformData(key, value); + } + std::map> getPlatformData(const std::string& prefix) override + { + LOCK(m_wallet->cs_wallet); + return m_wallet->GetPlatformData(prefix); + } bool displayAddress(const CTxDestination& dest) override { LOCK(m_wallet->cs_wallet); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 350b18a640c8..991f0796085d 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3792,6 +3792,36 @@ bool CWallet::WriteGovernanceObject(const Governance::Object& obj) return batch.WriteGovernanceObject(obj) && LoadGovernanceObject(obj); } +void CWallet::LoadPlatformData(const std::string& key, const std::vector& value) +{ + AssertLockHeld(cs_wallet); + m_platform_data[key] = value; +} + +bool CWallet::WritePlatformData(const std::string& key, const std::vector& value) +{ + AssertLockHeld(cs_wallet); + WalletBatch batch(GetDatabase()); + if (value.empty()) { + m_platform_data.erase(key); + return batch.ErasePlatformData(key); + } + if (!batch.WritePlatformData(key, value)) return false; + m_platform_data[key] = value; + return true; +} + +std::map> CWallet::GetPlatformData(const std::string& prefix) const +{ + AssertLockHeld(cs_wallet); + std::map> ret; + for (auto it = m_platform_data.lower_bound(prefix); it != m_platform_data.end(); ++it) { + if (it->first.compare(0, prefix.size(), prefix) != 0) break; + ret.emplace(it->first, it->second); + } + return ret; +} + std::vector CWallet::GetGovernanceObjects() { AssertLockHeld(cs_wallet); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 564f3d2559e3..ae0d51745d60 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -476,6 +476,10 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati // Map from governance object hash to governance object, they are added by gobject_prepare. std::map m_gobjects; + // Generic per-wallet key/value records used by the Dash Platform GUI + // (opaque to the wallet; persisted as DBKeys::PLATFORM_DATA). + std::map> m_platform_data; + typedef std::map MasterKeyMap; MasterKeyMap mapMasterKeys; unsigned int nMasterKeyMaxID = 0; @@ -988,6 +992,13 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati /** Returns a vector containing pointers to the governance objects in m_gobjects */ std::vector GetGovernanceObjects() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /** Load a Platform GUI data record into m_platform_data (wallet load). */ + void LoadPlatformData(const std::string& key, const std::vector& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /** Write (or, with an empty value, erase) a Platform GUI data record. */ + bool WritePlatformData(const std::string& key, const std::vector& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /** All Platform GUI data records whose key starts with prefix. */ + std::map> GetPlatformData(const std::string& prefix) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /** * Blocks until the wallet state is up-to-date to /at least/ the current * chain at the time this function is entered diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index dd7366ca30b7..b6a4587a73ea 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -54,6 +54,7 @@ const std::string MINVERSION{"minversion"}; const std::string NAME{"name"}; const std::string OLD_KEY{"wkey"}; const std::string ORDERPOSNEXT{"orderposnext"}; +const std::string PLATFORM_DATA{"platform_data"}; const std::string POOL{"pool"}; const std::string PURPOSE{"purpose"}; const std::string PRIVATESEND_SALT{"ps_salt"}; @@ -231,6 +232,16 @@ bool WalletBatch::WriteGovernanceObject(const Governance::Object& obj) return WriteIC(std::make_pair(DBKeys::G_OBJECT, obj.GetHash()), obj, false); } +bool WalletBatch::WritePlatformData(const std::string& key, const std::vector& value) +{ + return WriteIC(std::make_pair(DBKeys::PLATFORM_DATA, key), value, true); +} + +bool WalletBatch::ErasePlatformData(const std::string& key) +{ + return EraseIC(std::make_pair(DBKeys::PLATFORM_DATA, key)); +} + bool WalletBatch::WriteActiveScriptPubKeyMan(const uint256& id, bool internal) { std::string key = internal ? DBKeys::ACTIVEINTERNALSPK : DBKeys::ACTIVEEXTERNALSPK; @@ -625,6 +636,12 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, strErr = "Invalid governance object: LoadGovernanceObject"; return false; } + } else if (strType == DBKeys::PLATFORM_DATA) { + std::string strKey; + std::vector vchValue; + ssKey >> strKey; + ssValue >> vchValue; + pwallet->LoadPlatformData(strKey, vchValue); } else if (strType == DBKeys::OLD_KEY) { strErr = "Found unsupported 'wkey' record, try loading with version 0.17"; return false; diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index eb5ce4b37d9a..b16e9c56b51a 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -84,6 +84,7 @@ extern const std::string MINVERSION; extern const std::string NAME; extern const std::string OLD_KEY; extern const std::string ORDERPOSNEXT; +extern const std::string PLATFORM_DATA; extern const std::string POOL; extern const std::string PURPOSE; extern const std::string PRIVATESEND_SALT; @@ -219,6 +220,11 @@ class WalletBatch /** Write a CGovernanceObject to the database */ bool WriteGovernanceObject(const Governance::Object& obj); + //! Generic per-wallet key/value records used by the Dash Platform GUI + //! (flow state, identity metadata). Opaque to the wallet. + bool WritePlatformData(const std::string& key, const std::vector& value); + bool ErasePlatformData(const std::string& key); + bool WriteDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const CPrivKey& privkey, const SecureString& mnemonic, const SecureString& mnemonic_passphrase); bool WriteCryptedDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const std::vector& secret, const std::vector& crypted_mnemonic, const std::vector& crypted_mnemonic_passphrase); bool WriteDescriptor(const uint256& desc_id, const WalletDescriptor& descriptor); From f103e1e4dadc16819842b561a357d5d7c5c5fd6b Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 10:27:45 -0500 Subject: [PATCH 06/33] feat(platform): GroveDB merk proof verifier (C++ port of grovedb v5.0.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure-C++ verification of DAPI GroveDB proofs, mirroring the storage-free verify slice of dashpay/grovedb v5.0.0 and validated byte-for-byte against vectors generated from the real Rust grovedb: - serialize.{h,cpp}: bincode-v2 big-endian varint reader (+zigzag), LEB128 helpers, fixed-width BE readers, exception-free Result-style cursor - proof/merk.{h,cpp}: blake3 node hashing (value/kv/kv_digest/node/combine, sum variant), the full op-stack decoder and replay machine, and query-completeness + absence-proof verification (a node cannot silently omit matching results) - proof/grovedb.{h,cpp}: Element decode (Item/Reference/Tree/SumItem/ SumTree + reference-path resolution), GroveDBProof V0/V1 envelope decode, and recursive layered verification binding each subtree to its parent via combine_hash(H(element), child_root) - contrib/devtools/platform-test-vectors: Rust generator (developer tool, not built by CI) emitting deterministic element/merk/grovedb vectors - tests: 8 cases incl. ~2500 single-byte-flip forgery attempts, wrong-root, truncation, and query-completeness attacks — all must fail verification Also drop an accidentally committed generated moc file and gitignore it. Notable finding: V0 tree-result proofs do not bind the tree element bytes; the GUI must require V1 envelopes when a queried result is itself a tree. Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + .../devtools/platform-test-vectors/.gitignore | 1 + .../devtools/platform-test-vectors/Cargo.lock | 1905 +++++++++++++++++ .../devtools/platform-test-vectors/Cargo.toml | 17 + .../devtools/platform-test-vectors/README.md | 36 + .../platform-test-vectors/src/main.rs | 644 ++++++ src/Makefile.am | 8 +- src/Makefile.test.include | 9 + src/platform/proof/grovedb.cpp | 675 ++++++ src/platform/proof/grovedb.h | 167 ++ src/platform/proof/merk.cpp | 783 +++++++ src/platform/proof/merk.h | 177 ++ src/platform/serialize.cpp | 172 ++ src/platform/serialize.h | 77 + src/qt/platform/moc_platformpage.cpp | 95 - src/test/data/platform/element_vectors.json | 104 + .../data/platform/grovedb_proof_vectors.json | 426 ++++ .../data/platform/merk_proof_vectors.json | 253 +++ src/test/platform_proof_tests.cpp | 580 +++++ test/util/data/non-backported.txt | 3 + 20 files changed, 6037 insertions(+), 96 deletions(-) create mode 100644 contrib/devtools/platform-test-vectors/.gitignore create mode 100644 contrib/devtools/platform-test-vectors/Cargo.lock create mode 100644 contrib/devtools/platform-test-vectors/Cargo.toml create mode 100644 contrib/devtools/platform-test-vectors/README.md create mode 100644 contrib/devtools/platform-test-vectors/src/main.rs create mode 100644 src/platform/proof/grovedb.cpp create mode 100644 src/platform/proof/grovedb.h create mode 100644 src/platform/proof/merk.cpp create mode 100644 src/platform/proof/merk.h create mode 100644 src/platform/serialize.cpp create mode 100644 src/platform/serialize.h delete mode 100644 src/qt/platform/moc_platformpage.cpp create mode 100644 src/test/data/platform/element_vectors.json create mode 100644 src/test/data/platform/grovedb_proof_vectors.json create mode 100644 src/test/data/platform/merk_proof_vectors.json create mode 100644 src/test/platform_proof_tests.cpp diff --git a/.gitignore b/.gitignore index eab9f3f83ec1..8ffaccfc73ce 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,4 @@ compile_commands.json # Linux perf profiling artifacts perf.data perf.data.old +src/qt/platform/moc_*.cpp diff --git a/contrib/devtools/platform-test-vectors/.gitignore b/contrib/devtools/platform-test-vectors/.gitignore new file mode 100644 index 000000000000..2f7896d1d136 --- /dev/null +++ b/contrib/devtools/platform-test-vectors/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/contrib/devtools/platform-test-vectors/Cargo.lock b/contrib/devtools/platform-test-vectors/Cargo.lock new file mode 100644 index 000000000000..109649c2fcd1 --- /dev/null +++ b/contrib/devtools/platform-test-vectors/Cargo.lock @@ -0,0 +1,1905 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.118", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "const-crc32-nostd" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808ac43170e95b11dd23d78aa9eaac5bea45776a602955552c4e833f3f0f823d" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "corez" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df6f98652d30167eaeea34d77b730e07c8caba6df17bd4551842b9b8da01deb" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derive-getters" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "ed" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c8d6ea916fadcd87e3d1ff4802b696d717c83519b47e76f267ab77e536dd5a" +dependencies = [ + "ed-derive", + "thiserror 1.0.69", +] + +[[package]] +name = "ed-derive" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06a91d774f4b861acaa791bc6165e66d72d3a5d1aa85fc8c0956f5580f863161" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fpe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c4b37de5ae15812a764c958297cfc50f5c010438f60c6ce75d11b802abd404" +dependencies = [ + "cbc", + "cipher", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "frost-core" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ef2787af391c7e8bedc037a3b9ea03dde803fbd93e778e6bb369547800e5cd" +dependencies = [ + "byteorder", + "const-crc32-nostd", + "derive-getters", + "document-features", + "hex", + "itertools 0.14.0", + "postcard", + "rand_core 0.6.4", + "serde", + "serdect", + "thiserror 2.0.18", + "visibility", + "zeroize", + "zeroize_derive", +] + +[[package]] +name = "frost-rerandomized" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4c5cedd2426728adef2c0b1720f57676354c473836d1ccc50d0f0d1c91942b" +dependencies = [ + "derive-getters", + "document-features", + "frost-core", + "hex", + "rand_core 0.6.4", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.1", +] + +[[package]] +name = "getset" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "memuse", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "grovedb" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "bincode", + "bincode_derive", + "blake3", + "grovedb-bulk-append-tree", + "grovedb-commitment-tree", + "grovedb-costs", + "grovedb-dense-fixed-sized-merkle-tree", + "grovedb-element", + "grovedb-merk", + "grovedb-merkle-mountain-range", + "grovedb-path", + "grovedb-query", + "grovedb-storage", + "grovedb-version", + "grovedb-visualize", + "hex", + "indexmap 2.14.0", + "integer-encoding", + "intmap", + "itertools 0.14.0", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "grovedb-bulk-append-tree" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "bincode", + "blake3", + "grovedb-costs", + "grovedb-dense-fixed-sized-merkle-tree", + "grovedb-merkle-mountain-range", + "grovedb-query", + "grovedb-storage", + "hex", + "thiserror 2.0.18", +] + +[[package]] +name = "grovedb-commitment-tree" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "blake3", + "grovedb-bulk-append-tree", + "grovedb-costs", + "grovedb-storage", + "incrementalmerkletree", + "orchard", + "thiserror 2.0.18", +] + +[[package]] +name = "grovedb-costs" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "integer-encoding", + "intmap", + "thiserror 2.0.18", +] + +[[package]] +name = "grovedb-dense-fixed-sized-merkle-tree" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "bincode", + "blake3", + "grovedb-costs", + "grovedb-query", + "grovedb-storage", + "thiserror 2.0.18", +] + +[[package]] +name = "grovedb-element" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "bincode", + "bincode_derive", + "grovedb-path", + "grovedb-version", + "grovedb-visualize", + "hex", + "integer-encoding", + "thiserror 2.0.18", +] + +[[package]] +name = "grovedb-merk" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "bincode", + "bincode_derive", + "blake3", + "byteorder", + "colored", + "ed", + "grovedb-costs", + "grovedb-element", + "grovedb-path", + "grovedb-query", + "grovedb-storage", + "grovedb-version", + "grovedb-visualize", + "hex", + "indexmap 2.14.0", + "integer-encoding", + "num_cpus", + "rand 0.10.2", + "thiserror 2.0.18", +] + +[[package]] +name = "grovedb-merkle-mountain-range" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "bincode", + "blake3", + "grovedb-costs", + "grovedb-storage", +] + +[[package]] +name = "grovedb-path" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "hex", +] + +[[package]] +name = "grovedb-query" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "bincode", + "byteorder", + "ed", + "grovedb-costs", + "grovedb-storage", + "hex", + "indexmap 2.14.0", + "integer-encoding", + "thiserror 2.0.18", +] + +[[package]] +name = "grovedb-storage" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "blake3", + "grovedb-costs", + "grovedb-path", + "grovedb-visualize", + "hex", + "integer-encoding", + "lazy_static", + "num_cpus", + "rocksdb", + "strum", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "grovedb-version" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "thiserror 2.0.18", + "versioned-feature-core", +] + +[[package]] +name = "grovedb-visualize" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "hex", + "itertools 0.14.0", +] + +[[package]] +name = "halo2_gadgets" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb2a697cad929f706b7987fe804ad57d43622cd37463ba7e4d662a926fdcfea3" +dependencies = [ + "arrayvec", + "bitvec", + "ff", + "group", + "halo2_poseidon", + "halo2_proofs", + "lazy_static", + "pasta_curves", + "rand 0.8.6", + "sinsemilla", + "subtle", + "uint", +] + +[[package]] +name = "halo2_legacy_pdqsort" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47716fe1ae67969c5e0b2ef826f32db8c3be72be325e1aa3c1951d06b5575ec5" + +[[package]] +name = "halo2_poseidon" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa3da60b81f02f9b33ebc6252d766f843291fb4d2247a07ae73d20b791fc56f" +dependencies = [ + "bitvec", + "ff", + "group", + "pasta_curves", +] + +[[package]] +name = "halo2_proofs" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05713f117155643ce10975e0bee44a274bcda2f4bb5ef29a999ad67c1fa8d4d3" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "halo2_legacy_pdqsort", + "indexmap 1.9.3", + "maybe-rayon", + "pasta_curves", + "rand_core 0.6.4", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "incrementalmerkletree" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30821f91f0fa8660edca547918dc59812893b497d07c1144f326f07fdd94aba9" +dependencies = [ + "either", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "integer-encoding" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c00403deb17c3221a1fe4fb571b9ed0370b3dcd116553c77fa294a3d918699" + +[[package]] +name = "intmap" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2e611826a1868311677fdcdfbec9e8621d104c732d080f546a854530232f0ee" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "jubjub" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8499f7a74008aafbecb2a2e608a3e13e4dd3e84df198b604451efe93f2de6e61" +dependencies = [ + "bitvec", + "bls12_381", + "ff", + "group", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "librocksdb-sys" +version = "0.18.0+10.7.5" +source = "git+https://github.com/QuantumExplorer/rust-rocksdb.git?rev=52772eea7bcd214d1d07d80aa538b1d24e5015b7#52772eea7bcd214d1d07d80aa538b1d24e5015b7" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "libc", + "libz-sys", + "lz4-sys", + "zstd-sys", +] + +[[package]] +name = "libz-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memuse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonempty" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "orchard" +version = "0.14.0" +source = "git+https://github.com/dashpay/orchard.git?tag=dashified-0.14.0#f05557390a5843bc4eb04c66d8140bc9ef0fe9b7" +dependencies = [ + "aes", + "bitvec", + "blake2b_simd", + "corez", + "ff", + "fpe", + "getset", + "group", + "halo2_gadgets", + "halo2_poseidon", + "halo2_proofs", + "hex", + "incrementalmerkletree", + "lazy_static", + "memuse", + "nonempty", + "pasta_curves", + "rand 0.8.6", + "rand_core 0.6.4", + "reddsa", + "serde", + "sinsemilla", + "subtle", + "tracing", + "visibility", + "zcash_note_encryption", + "zcash_spec", + "zip32", +] + +[[package]] +name = "pasta_curves" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "lazy_static", + "rand 0.8.6", + "static_assertions", + "subtle", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "platform-test-vectors" +version = "0.1.0" +dependencies = [ + "bincode", + "grovedb", + "grovedb-merk", + "grovedb-version", + "hex", + "serde_json", + "tempfile", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20 0.10.1", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reddsa" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4784b85c8bfd17b36b86e664e6e504ecdb586001086ee23749e4a633bbb84832" +dependencies = [ + "blake2b_simd", + "byteorder", + "frost-rerandomized", + "group", + "hex", + "jubjub", + "pasta_curves", + "rand_core 0.6.4", + "serde", + "thiserror 2.0.18", + "zeroize", +] + +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rocksdb" +version = "0.24.0" +source = "git+https://github.com/QuantumExplorer/rust-rocksdb.git?rev=52772eea7bcd214d1d07d80aa538b1d24e5015b7#52772eea7bcd214d1d07d80aa538b1d24e5015b7" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sinsemilla" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d268ae0ea06faafe1662e9967cd4f9022014f5eeb798e0c302c876df8b7af9c" +dependencies = [ + "group", + "pasta_curves", + "subtle", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "versioned-feature-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898c0ad500fdb1914df465a2c729fce33646ef65dfbbbd16a6d8050e0d2404df" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zcash_note_encryption" +version = "0.4.1" +source = "git+https://github.com/dashpay/zcash_note_encryption?rev=9f7e93d#9f7e93d42cef839d02b9d75918117941d453f8cb" +dependencies = [ + "chacha20 0.9.1", + "chacha20poly1305", + "cipher", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "zcash_spec" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded3f58b93486aa79b85acba1001f5298f27a46489859934954d262533ee2915" +dependencies = [ + "blake2b_simd", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zip32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64bf5186a8916f7a48f2a98ef599bf9c099e2458b36b819e393db1c0e768c4b" +dependencies = [ + "bech32", + "blake2b_simd", + "memuse", + "subtle", + "zcash_spec", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/contrib/devtools/platform-test-vectors/Cargo.toml b/contrib/devtools/platform-test-vectors/Cargo.toml new file mode 100644 index 000000000000..b9f6ac72a2f2 --- /dev/null +++ b/contrib/devtools/platform-test-vectors/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "platform-test-vectors" +version = "0.1.0" +edition = "2021" +publish = false +description = "Generates GroveDB proof test vectors for src/test/platform_proof_tests.cpp" + +[dependencies] +grovedb = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.0" } +grovedb-merk = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.0", features = ["full"] } +grovedb-version = { git = "https://github.com/dashpay/grovedb", tag = "v5.0.0" } +bincode = "=2.0.1" +hex = "0.4" +serde_json = { version = "1", features = ["preserve_order"] } +tempfile = "3" + +# This crate is a developer tool, not part of the Dash Core build. See README.md. diff --git a/contrib/devtools/platform-test-vectors/README.md b/contrib/devtools/platform-test-vectors/README.md new file mode 100644 index 000000000000..c1f912f4f708 --- /dev/null +++ b/contrib/devtools/platform-test-vectors/README.md @@ -0,0 +1,36 @@ +# platform-test-vectors + +Developer tool that generates GroveDB proof test vectors for the pure-C++ +GroveDB proof verifier in `src/platform/proof/` (unit tests: +`src/test/platform_proof_tests.cpp`, suite `platform_proof_tests`). + +This crate is **not** built by CI or by the normal Dash Core build. It exists +so the committed JSON vectors in `src/test/data/platform/` can be regenerated +from the real Rust GroveDB implementation (github.com/dashpay/grovedb, pinned +to tag `v5.0.0`) whenever the C++ port needs new coverage. + +## Usage + +Requires a recent stable Rust toolchain (edition 2024 support, Rust >= 1.85) +and the usual native build dependencies for RocksDB (clang, cmake). + +```bash +cd contrib/devtools/platform-test-vectors +cargo run --release -- ../../../src/test/data/platform +``` + +This rewrites: + +- `element_vectors.json` — serialized `Element` bytes (bincode + standard/big-endian config) for Item, SumItem, Tree, SumTree and Reference + variants. +- `merk_proof_vectors.json` — single-subtree merk proof op streams (extracted + from full GroveDB proofs) with query descriptions, expected root hashes and + expected result sets. +- `grovedb_proof_vectors.json` — full layered `GroveDBProof` envelopes (V0 and + V1) with path-query descriptions, expected root hashes and expected result + sets (as returned by `GroveDb::verify_query_raw`). + +All fixture contents are deterministic (fixed keys/values, no randomness or +timestamps), so regenerating without changing this crate must produce +byte-identical JSON. diff --git a/contrib/devtools/platform-test-vectors/src/main.rs b/contrib/devtools/platform-test-vectors/src/main.rs new file mode 100644 index 000000000000..ef7b662d75b0 --- /dev/null +++ b/contrib/devtools/platform-test-vectors/src/main.rs @@ -0,0 +1,644 @@ +//! Generates GroveDB proof test vectors for the C++ verifier in +//! src/platform/proof/ (tested by src/test/platform_proof_tests.cpp). +//! +//! Everything here is deterministic: fixed keys and values, no randomness, +//! no timestamps. Output JSON files are written to src/test/data/platform/. +//! +//! Usage (from the repo root): +//! cd contrib/devtools/platform-test-vectors +//! cargo run --release -- ../../../src/test/data/platform + +use std::collections::BTreeMap; +use std::path::Path; + +use grovedb::operations::proof::{GroveDBProof, LayerProof, MerkOnlyLayerProof, ProofBytes}; +use grovedb::reference_path::ReferencePathType; +use grovedb::{Element, GroveDb, PathQuery, Query, QueryItem, SizedQuery}; +use grovedb_merk::proofs::query::QueryProofVerify; +use grovedb_version::version::GroveVersion; +use serde_json::{json, Value}; + +fn hex_of(b: &[u8]) -> String { + hex::encode(b) +} + +/// JSON description of a QueryItem, mirrored by the C++ test loader. +fn query_item_json(item: &QueryItem) -> Value { + match item { + QueryItem::Key(k) => json!({"type": "key", "key": hex_of(k)}), + QueryItem::Range(r) => { + json!({"type": "range", "start": hex_of(&r.start), "end": hex_of(&r.end)}) + } + QueryItem::RangeInclusive(r) => { + json!({"type": "range_inclusive", "start": hex_of(r.start()), "end": hex_of(r.end())}) + } + QueryItem::RangeFull(_) => json!({"type": "range_full"}), + other => panic!("unsupported query item in vector generator: {:?}", other), + } +} + +/// JSON description of a Query (items + default subquery branch), mirrored by +/// the C++ test loader. Conditional subquery branches are not used by these +/// vectors. +fn query_json(q: &Query) -> Value { + assert!( + q.conditional_subquery_branches.is_none(), + "vector generator does not describe conditional subquery branches" + ); + let items: Vec = q.items.iter().map(query_item_json).collect(); + let subquery_path: Value = match &q.default_subquery_branch.subquery_path { + Some(p) => Value::Array(p.iter().map(|s| Value::String(hex_of(s))).collect()), + None => Value::Null, + }; + let subquery: Value = match &q.default_subquery_branch.subquery { + Some(sq) => query_json(sq), + None => Value::Null, + }; + json!({ + "items": items, + "left_to_right": q.left_to_right, + "default_subquery_path": subquery_path, + "default_subquery": subquery, + }) +} + +fn path_query_json(pq: &PathQuery) -> Value { + let path: Vec = pq.path.iter().map(|s| Value::String(hex_of(s))).collect(); + json!({ + "path": path, + "query": query_json(&pq.query.query), + "limit": pq.query.limit, + }) +} + +/// Build the fixture database. All contents are deterministic. +fn build_db(db: &GroveDb, v: &GroveVersion) { + let insert = |path: &[&[u8]], key: &[u8], element: Element| { + db.insert(path, key, element, None, None, v) + .unwrap() + .expect("insert failed"); + }; + + // Top-level trees + insert(&[], b"items", Element::empty_tree()); + insert(&[], b"index", Element::empty_tree()); + insert(&[], b"balances", Element::new_sum_tree(None)); + insert(&[], b"docs", Element::empty_tree()); + insert(&[], b"empty", Element::empty_tree()); + + // items: plain items i1..i5, i3 carries flags + insert(&[b"items"], b"i1", Element::new_item(b"value one".to_vec())); + insert(&[b"items"], b"i2", Element::new_item(b"value two".to_vec())); + insert( + &[b"items"], + b"i3", + Element::new_item_with_flags(b"value three".to_vec(), Some(vec![0x0d, 0x0e])), + ); + insert(&[b"items"], b"i4", Element::new_item(b"value four".to_vec())); + insert(&[b"items"], b"i5", Element::new_item(b"value five".to_vec())); + + // index: references into items + insert( + &[b"index"], + b"r1", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + b"items".to_vec(), + b"i1".to_vec(), + ])), + ); + insert( + &[b"index"], + b"r2", + Element::new_reference(ReferencePathType::UpstreamRootHeightReference( + 0, + vec![b"items".to_vec(), b"i2".to_vec()], + )), + ); + + // balances: a sum tree with sum items + insert(&[b"balances"], b"alice", Element::new_sum_item(1000)); + insert(&[b"balances"], b"bob", Element::new_sum_item(-250)); + insert(&[b"balances"], b"carol", Element::new_sum_item(42)); + + // docs: two user subtrees with documents + insert(&[b"docs"], b"user1", Element::empty_tree()); + insert(&[b"docs"], b"user2", Element::empty_tree()); + insert( + &[b"docs", b"user1"], + b"d1", + Element::new_item(b"doc one".to_vec()), + ); + insert( + &[b"docs", b"user1"], + b"d2", + Element::new_item(b"doc two".to_vec()), + ); + insert( + &[b"docs", b"user2"], + b"d3", + Element::new_item(b"doc three".to_vec()), + ); +} + +// --------------------------------------------------------------------------- +// element vectors +// --------------------------------------------------------------------------- + +fn element_vectors(v: &GroveVersion) -> Value { + let root_key: Vec = (0u8..32).collect(); + let cases: Vec<(&str, Element)> = vec![ + ("item_no_flags", Element::new_item(b"hello".to_vec())), + ("item_empty_value", Element::new_item(vec![])), + ( + "item_with_flags", + Element::new_item_with_flags(vec![1, 2, 3], Some(vec![7, 8])), + ), + ( + "item_large_value", + // 300-byte value: exercises the >=251 bincode varint length form + // (0xfb marker + big-endian u16). + Element::new_item(vec![0xab; 300]), + ), + ("sum_item_positive", Element::new_sum_item(1000)), + ("sum_item_negative", Element::new_sum_item(-42)), + ( + "sum_item_with_flags", + Element::new_sum_item_with_flags(5, Some(vec![1])), + ), + ("tree_empty_root_key", Element::empty_tree()), + ("tree_nonempty", Element::new_tree(Some(root_key.clone()))), + ( + "tree_with_flags", + Element::new_tree_with_flags(None, Some(vec![5])), + ), + ("sum_tree_empty", Element::new_sum_tree(None)), + ( + "sum_tree_nonempty", + Element::new_sum_tree_with_flags_and_sum_value(Some(root_key.clone()), 123, None), + ), + ( + "reference_absolute", + Element::new_reference(ReferencePathType::AbsolutePathReference(vec![ + vec![0], + b"abcd".to_vec(), + vec![5], + ])), + ), + ( + "reference_absolute_hops_flags", + Element::new_reference_with_max_hops_and_flags( + ReferencePathType::AbsolutePathReference(vec![b"items".to_vec(), b"i1".to_vec()]), + Some(3), + Some(vec![9]), + ), + ), + ( + "reference_upstream_root_height", + Element::new_reference(ReferencePathType::UpstreamRootHeightReference( + 1, + vec![b"p".to_vec(), b"q".to_vec()], + )), + ), + ( + "reference_upstream_root_height_parent_addition", + Element::new_reference( + ReferencePathType::UpstreamRootHeightWithParentPathAdditionReference( + 2, + vec![b"x".to_vec(), b"y".to_vec()], + ), + ), + ), + ( + "reference_upstream_from_element_height", + Element::new_reference(ReferencePathType::UpstreamFromElementHeightReference( + 1, + vec![b"p".to_vec(), b"q".to_vec()], + )), + ), + ( + "reference_cousin", + Element::new_reference(ReferencePathType::CousinReference(b"c".to_vec())), + ), + ( + "reference_removed_cousin", + Element::new_reference(ReferencePathType::RemovedCousinReference(vec![ + b"m".to_vec(), + b"n".to_vec(), + ])), + ), + ( + "reference_sibling", + Element::new_reference(ReferencePathType::SiblingReference(b"s".to_vec())), + ), + ]; + + let vectors: Vec = cases + .into_iter() + .map(|(name, element)| { + let serialized = element.serialize(v).expect("serialize element"); + json!({ + "name": name, + "description": format!("{}", element), + "serialized_hex": hex_of(&serialized), + }) + }) + .collect(); + json!({ "vectors": vectors }) +} + +// --------------------------------------------------------------------------- +// merk proof vectors (single-subtree proofs, extracted from GroveDB proofs) +// --------------------------------------------------------------------------- + +const BINCODE_CONFIG: bincode::config::Configuration< + bincode::config::BigEndian, + bincode::config::Varint, + bincode::config::NoLimit, +> = bincode::config::standard().with_big_endian().with_no_limit(); + +fn decode_envelope(proof: &[u8]) -> GroveDBProof { + let (decoded, consumed): (GroveDBProof, usize) = + bincode::decode_from_slice(proof, BINCODE_CONFIG).expect("decode envelope"); + assert_eq!(consumed, proof.len(), "trailing envelope bytes"); + decoded +} + +/// Walk the envelope down the given path and return the merk proof bytes for +/// that layer. +fn merk_proof_at_path(proof: &GroveDBProof, path: &[&[u8]]) -> Vec { + fn walk_v0<'a>(layer: &'a MerkOnlyLayerProof, path: &[&[u8]]) -> &'a Vec { + match path.split_first() { + None => &layer.merk_proof, + Some((seg, rest)) => walk_v0( + layer + .lower_layers + .get(*seg) + .expect("path segment missing in v0 proof"), + rest, + ), + } + } + fn walk_v1<'a>(layer: &'a LayerProof, path: &[&[u8]]) -> &'a Vec { + match path.split_first() { + None => match &layer.merk_proof { + ProofBytes::Merk(bytes) => bytes, + _ => panic!("expected merk proof bytes"), + }, + Some((seg, rest)) => walk_v1( + layer + .lower_layers + .get(*seg) + .expect("path segment missing in v1 proof"), + rest, + ), + } + } + match proof { + GroveDBProof::V0(p) => walk_v0(&p.root_layer, path).clone(), + GroveDBProof::V1(p) => walk_v1(&p.root_layer, path).clone(), + } +} + +struct MerkCase<'a> { + name: &'a str, + path: Vec<&'a [u8]>, + items: Vec, + limit: Option, + /// GroveVersion used for proving; GROVE_V1/GROVE_V2 produce V0 envelopes + /// (lenient merk proof version 0), GROVE_V3 produces V1 (strict version 1). + grove_version: &'a GroveVersion, + proof_version: u16, +} + +fn merk_vectors(db: &GroveDb, latest: &GroveVersion, first: &GroveVersion) -> Value { + let cases = vec![ + MerkCase { + name: "items_single_key_present", + path: vec![b"items"], + items: vec![QueryItem::Key(b"i2".to_vec())], + limit: None, + grove_version: latest, + proof_version: 1, + }, + MerkCase { + name: "items_single_key_absent", + path: vec![b"items"], + items: vec![QueryItem::Key(b"i9".to_vec())], + limit: None, + grove_version: latest, + proof_version: 1, + }, + MerkCase { + name: "items_multiple_keys", + path: vec![b"items"], + items: vec![ + QueryItem::Key(b"i1".to_vec()), + QueryItem::Key(b"i3".to_vec()), + QueryItem::Key(b"i5".to_vec()), + ], + limit: None, + grove_version: latest, + proof_version: 1, + }, + MerkCase { + name: "items_range", + path: vec![b"items"], + items: vec![QueryItem::Range(b"i2".to_vec()..b"i4".to_vec())], + limit: None, + grove_version: latest, + proof_version: 1, + }, + MerkCase { + name: "items_range_inclusive", + path: vec![b"items"], + items: vec![QueryItem::RangeInclusive(b"i2".to_vec()..=b"i4".to_vec())], + limit: None, + grove_version: latest, + proof_version: 1, + }, + MerkCase { + name: "items_range_full_limit_2", + path: vec![b"items"], + items: vec![QueryItem::RangeFull(std::ops::RangeFull)], + limit: Some(2), + grove_version: latest, + proof_version: 1, + }, + MerkCase { + name: "sum_tree_keys", + path: vec![b"balances"], + items: vec![ + QueryItem::Key(b"alice".to_vec()), + QueryItem::Key(b"bob".to_vec()), + ], + limit: None, + grove_version: latest, + proof_version: 1, + }, + MerkCase { + name: "sum_tree_range_full", + path: vec![b"balances"], + items: vec![QueryItem::RangeFull(std::ops::RangeFull)], + limit: None, + grove_version: latest, + proof_version: 1, + }, + MerkCase { + name: "items_multiple_keys_v0", + path: vec![b"items"], + items: vec![ + QueryItem::Key(b"i1".to_vec()), + QueryItem::Key(b"i3".to_vec()), + ], + limit: None, + grove_version: first, + proof_version: 0, + }, + ]; + + let mut vectors = Vec::new(); + for case in cases { + let mut query = Query::new(); + for item in case.items.clone() { + query.insert_item(item); + } + let path_query = PathQuery::new( + case.path.iter().map(|p| p.to_vec()).collect(), + SizedQuery::new(query.clone(), case.limit, None), + ); + let proof_bytes = db + .prove_query(&path_query, None, case.grove_version) + .unwrap() + .expect("prove"); + // Sanity: the full proof verifies against the db root hash. + let root_hash = db.root_hash(None, case.grove_version).unwrap().unwrap(); + let (verified_root, _) = + GroveDb::verify_query_raw(&proof_bytes, &path_query, case.grove_version) + .expect("full proof verifies"); + assert_eq!(verified_root, root_hash, "{}", case.name); + + // Extract the single-subtree merk proof and verify it standalone to + // obtain the expected merk root hash and result set. + let envelope = decode_envelope(&proof_bytes); + let merk_bytes = merk_proof_at_path(&envelope, &case.path); + let (merk_root, merk_result) = query + .execute_proof(&merk_bytes, case.limit, true, case.proof_version) + .unwrap() + .expect("merk proof verifies"); + + let results: Vec = merk_result + .result_set + .iter() + .map(|r| { + json!({ + "key_hex": hex_of(&r.key), + "value_hex": r.value.as_ref().map(|v| hex_of(v)), + }) + }) + .collect(); + + let items: Vec = case.items.iter().map(query_item_json).collect(); + vectors.push(json!({ + "name": case.name, + "query": { + "items": items, + "limit": case.limit, + "left_to_right": true, + }, + "proof_version": case.proof_version, + "proof_hex": hex_of(&merk_bytes), + "expected_root_hash_hex": hex_of(&merk_root), + "expected_results": results, + })); + } + json!({ "vectors": vectors }) +} + +// --------------------------------------------------------------------------- +// grovedb (layered) proof vectors +// --------------------------------------------------------------------------- + +struct GroveCase<'a> { + name: &'a str, + path_query: PathQuery, + grove_version: &'a GroveVersion, +} + +fn grove_vectors(db: &GroveDb, latest: &GroveVersion, first: &GroveVersion) -> Value { + let key_query = |path: Vec>, keys: Vec<&[u8]>, limit: Option| -> PathQuery { + let mut query = Query::new(); + for key in keys { + query.insert_item(QueryItem::Key(key.to_vec())); + } + PathQuery::new(path, SizedQuery::new(query, limit, None)) + }; + + // Three-level query using a default subquery: + // path ["docs"], items [Key("user1")], subquery: all items. + let subquery_case = { + let mut subquery = Query::new(); + subquery.insert_item(QueryItem::RangeFull(std::ops::RangeFull)); + let mut query = Query::new(); + query.insert_item(QueryItem::Key(b"user1".to_vec())); + query.set_subquery(subquery); + PathQuery::new(vec![b"docs".to_vec()], SizedQuery::new(query, None, None)) + }; + + // Sum tree range query + let sum_tree_case = { + let mut query = Query::new(); + query.insert_item(QueryItem::RangeFull(std::ops::RangeFull)); + PathQuery::new( + vec![b"balances".to_vec()], + SizedQuery::new(query, None, None), + ) + }; + + let cases = vec![ + GroveCase { + name: "two_level_items_v1", + path_query: key_query(vec![b"items".to_vec()], vec![b"i1", b"i3"], None), + grove_version: latest, + }, + GroveCase { + name: "two_level_items_v0", + path_query: key_query(vec![b"items".to_vec()], vec![b"i1", b"i3"], None), + grove_version: first, + }, + GroveCase { + name: "absence_v1", + path_query: key_query(vec![b"items".to_vec()], vec![b"i9"], None), + grove_version: latest, + }, + GroveCase { + name: "absence_between_keys_v1", + path_query: key_query(vec![b"items".to_vec()], vec![b"i2", b"i25", b"i3"], None), + grove_version: latest, + }, + GroveCase { + name: "reference_absolute_v1", + path_query: key_query(vec![b"index".to_vec()], vec![b"r1"], None), + grove_version: latest, + }, + GroveCase { + name: "reference_upstream_v1", + path_query: key_query(vec![b"index".to_vec()], vec![b"r2"], None), + grove_version: latest, + }, + GroveCase { + name: "sum_tree_v1", + path_query: sum_tree_case, + grove_version: latest, + }, + GroveCase { + name: "three_level_path_v1", + path_query: key_query( + vec![b"docs".to_vec(), b"user1".to_vec()], + vec![b"d1"], + None, + ), + grove_version: latest, + }, + GroveCase { + name: "three_level_subquery_v1", + path_query: subquery_case, + grove_version: latest, + }, + GroveCase { + name: "root_empty_tree_v1", + path_query: key_query(vec![], vec![b"empty"], None), + grove_version: latest, + }, + GroveCase { + name: "root_tree_no_subquery_v1", + path_query: key_query(vec![], vec![b"items"], None), + grove_version: latest, + }, + GroveCase { + name: "root_tree_no_subquery_v0", + path_query: key_query(vec![], vec![b"items"], None), + grove_version: first, + }, + ]; + + let mut vectors = Vec::new(); + for case in cases { + let proof_bytes = db + .prove_query(&case.path_query, None, case.grove_version) + .unwrap() + .expect("prove"); + let root_hash = db.root_hash(None, case.grove_version).unwrap().unwrap(); + let (verified_root, results) = + GroveDb::verify_query_raw(&proof_bytes, &case.path_query, case.grove_version) + .expect("proof verifies raw"); + assert_eq!(verified_root, root_hash, "{}", case.name); + + // Cross-check with the deserializing verifier as well (skips + // element deserialization errors would surface here). + let (verified_root2, _) = GroveDb::verify_query_with_options( + &proof_bytes, + &case.path_query, + grovedb::VerifyOptions { + absence_proofs_for_non_existing_searched_keys: false, + verify_proof_succinctness: false, + include_empty_trees_in_result: true, + }, + case.grove_version, + ) + .expect("proof verifies deserialized"); + assert_eq!(verified_root2, root_hash, "{}", case.name); + + let results_json: Vec = results + .iter() + .map(|r| { + json!({ + "path": r.path.iter().map(|s| Value::String(hex_of(s))).collect::>(), + "key_hex": hex_of(&r.key), + "element_hex": hex_of(&r.value), + }) + }) + .collect(); + + vectors.push(json!({ + "name": case.name, + "path_query": path_query_json(&case.path_query), + "proof_hex": hex_of(&proof_bytes), + "expected_root_hash_hex": hex_of(&root_hash), + "expected_results": results_json, + })); + } + json!({ "vectors": vectors }) +} + +fn main() { + let out_dir = std::env::args() + .nth(1) + .unwrap_or_else(|| "../../../src/test/data/platform".to_string()); + let out_dir = Path::new(&out_dir); + std::fs::create_dir_all(out_dir).expect("create output dir"); + + let latest = GroveVersion::latest(); + let first = &grovedb_version::version::GROVE_VERSIONS[0]; + + let tmp = tempfile::tempdir().expect("tempdir"); + let db = GroveDb::open(tmp.path()).expect("open grovedb"); + build_db(&db, latest); + + let write = |name: &str, value: Value| { + let path = out_dir.join(name); + let mut text = serde_json::to_string_pretty(&value).expect("serialize json"); + text.push('\n'); + std::fs::write(&path, text).expect("write json"); + println!("wrote {}", path.display()); + }; + + write("element_vectors.json", element_vectors(latest)); + write("merk_proof_vectors.json", merk_vectors(&db, latest, first)); + write( + "grovedb_proof_vectors.json", + grove_vectors(&db, latest, first), + ); + + // Keep BTreeMap import used in both envelope variants. + let _: Option, ()>> = None; +} diff --git a/src/Makefile.am b/src/Makefile.am index 515a4358d3cb..8cd27c48bcbf 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -683,7 +683,7 @@ endif # platform (Dash Platform client, linked into dash-qt and test_dash only) # if ENABLE_PLATFORM_GUI libdash_platform_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(BOOST_CPPFLAGS) $(MBEDTLS_CFLAGS) \ - -DBLAKE3_NO_SSE2 -DBLAKE3_NO_SSE41 -DBLAKE3_NO_AVX2 -DBLAKE3_NO_AVX512 + -DBLAKE3_NO_SSE2 -DBLAKE3_NO_SSE41 -DBLAKE3_NO_AVX2 -DBLAKE3_NO_AVX512 -DBLAKE3_USE_NEON=0 libdash_platform_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) libdash_platform_a_CFLAGS = $(AM_CFLAGS) $(PIE_FLAGS) libdash_platform_a_SOURCES = \ @@ -695,6 +695,12 @@ libdash_platform_a_SOURCES = \ platform/client.h \ platform/params.cpp \ platform/params.h \ + platform/proof/grovedb.cpp \ + platform/proof/grovedb.h \ + platform/proof/merk.cpp \ + platform/proof/merk.h \ + platform/serialize.cpp \ + platform/serialize.h \ platform/statetransitions.h \ platform/types.h endif diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 373998acc478..556b29081258 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -208,6 +208,15 @@ BITCOIN_TESTS =\ test/versionbits_tests.cpp \ test/xoroshiro128plusplus_tests.cpp +if ENABLE_PLATFORM_GUI +BITCOIN_TESTS += \ + test/platform_proof_tests.cpp +JSON_TEST_FILES += \ + test/data/platform/element_vectors.json \ + test/data/platform/grovedb_proof_vectors.json \ + test/data/platform/merk_proof_vectors.json +endif + if ENABLE_WALLET BITCOIN_TESTS += \ wallet/test/bip39_tests.cpp \ diff --git a/src/platform/proof/grovedb.cpp b/src/platform/proof/grovedb.cpp new file mode 100644 index 000000000000..0783590ce3a9 --- /dev/null +++ b/src/platform/proof/grovedb.cpp @@ -0,0 +1,675 @@ +// Copyright (c) 2026 The Dash 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 + +namespace platform::grove { + +namespace { + +//! grovedb/src/operations/proof/mod.rs MAX_PROOF_DEPTH: limit on layer +//! nesting depth and on the number of children per layer. +constexpr size_t MAX_PROOF_DEPTH{128}; +//! grovedb decodes envelopes with a 256 MiB bincode limit +//! (grovedb/src/operations/proof/mod.rs decode_grovedb_proof_canonical). +constexpr size_t MAX_PROOF_BYTES{256 * 1024 * 1024}; +//! Sanity cap on decoded path segment counts (no grovedb equivalent; real +//! reference paths are a handful of segments deep). +constexpr size_t MAX_PATH_SEGMENTS{1024}; + +} // namespace + +bool ReferencePath::Resolve(const std::vector& current_path, const Bytes& key, + std::vector& out, std::string& error) const +{ + // grovedb-element/src/reference_path/mod.rs path_from_reference_path_type + out.clear(); + switch (type) { + case Type::ABSOLUTE_PATH: + out = path; + return true; + case Type::UPSTREAM_ROOT_HEIGHT: { + if (height > current_path.size()) { + error = "reference stored path cannot satisfy reference constraints"; + return false; + } + out.assign(current_path.begin(), current_path.begin() + height); + out.insert(out.end(), path.begin(), path.end()); + return true; + } + case Type::UPSTREAM_ROOT_HEIGHT_WITH_PARENT_PATH_ADDITION: { + if (height > current_path.size() || current_path.empty()) { + error = "reference stored path cannot satisfy reference constraints"; + return false; + } + out.assign(current_path.begin(), current_path.begin() + height); + out.insert(out.end(), path.begin(), path.end()); + out.push_back(current_path.back()); + return true; + } + case Type::UPSTREAM_FROM_ELEMENT_HEIGHT: { + if (height > current_path.size()) { + error = "reference stored path cannot satisfy reference constraints"; + return false; + } + out.assign(current_path.begin(), current_path.end() - height); + out.insert(out.end(), path.begin(), path.end()); + return true; + } + case Type::COUSIN: { + if (current_path.empty() || path.size() != 1) { + error = "reference stored path cannot satisfy reference constraints"; + return false; + } + out.assign(current_path.begin(), current_path.end() - 1); + out.push_back(path[0]); + out.push_back(key); + return true; + } + case Type::REMOVED_COUSIN: { + if (current_path.empty()) { + error = "reference stored path cannot satisfy reference constraints"; + return false; + } + out.assign(current_path.begin(), current_path.end() - 1); + out.insert(out.end(), path.begin(), path.end()); + out.push_back(key); + return true; + } + case Type::SIBLING: { + if (path.size() != 1) { + error = "reference stored path cannot satisfy reference constraints"; + return false; + } + out = current_path; + out.push_back(path[0]); + return true; + } + } + error = "unknown reference path type"; + return false; +} + +namespace { + +bool ReadPathSegments(BytesReader& reader, std::vector& out) +{ + uint64_t count; + if (!reader.ReadBincodeVarint(count)) return false; + if (count > MAX_PATH_SEGMENTS) return reader.SetError(strprintf("path has too many segments (%u)", count)); + out.clear(); + out.reserve(static_cast(count)); + for (uint64_t i = 0; i < count; ++i) { + Bytes segment; + if (!reader.ReadBincodeByteVec(segment, MAX_PROOF_BYTES)) return false; + out.push_back(std::move(segment)); + } + return true; +} + +//! grovedb-element/src/reference_path/mod.rs ReferencePathType, bincode +//! standard()+big_endian(): varint discriminant in declaration order. +bool DecodeReferencePath(BytesReader& reader, ReferencePath& out) +{ + uint64_t discriminant; + if (!reader.ReadBincodeVarint(discriminant)) return false; + out = ReferencePath{}; + switch (discriminant) { + case 0: + out.type = ReferencePath::Type::ABSOLUTE_PATH; + return ReadPathSegments(reader, out.path); + case 1: + case 2: + case 3: + out.type = discriminant == 1 ? ReferencePath::Type::UPSTREAM_ROOT_HEIGHT + : discriminant == 2 + ? ReferencePath::Type::UPSTREAM_ROOT_HEIGHT_WITH_PARENT_PATH_ADDITION + : ReferencePath::Type::UPSTREAM_FROM_ELEMENT_HEIGHT; + return reader.ReadU8(out.height) && ReadPathSegments(reader, out.path); + case 4: + case 6: { + out.type = discriminant == 4 ? ReferencePath::Type::COUSIN : ReferencePath::Type::SIBLING; + Bytes key; + if (!reader.ReadBincodeByteVec(key, MAX_PROOF_BYTES)) return false; + out.path.push_back(std::move(key)); + return true; + } + case 5: + out.type = ReferencePath::Type::REMOVED_COUSIN; + return ReadPathSegments(reader, out.path); + default: + return reader.SetError(strprintf("unknown reference path type discriminant %u", discriminant)); + } +} + +bool ReadOptionalByteVec(BytesReader& reader, std::optional& out) +{ + bool has_value; + if (!reader.ReadBincodeOptionTag(has_value)) return false; + if (!has_value) { + out.reset(); + return true; + } + Bytes value; + if (!reader.ReadBincodeByteVec(value, MAX_PROOF_BYTES)) return false; + out = std::move(value); + return true; +} + +const char* UnsupportedElementName(uint64_t discriminant) +{ + switch (discriminant) { + case 5: return "BigSumTree"; + case 6: return "CountTree"; + case 7: return "CountSumTree"; + case 8: return "ProvableCountTree"; + case 9: return "ItemWithSumItem"; + case 10: return "ProvableCountSumTree"; + case 11: return "CommitmentTree"; + case 12: return "MmrTree"; + case 13: return "BulkAppendTree"; + case 14: return "DenseAppendOnlyFixedSizeTree"; + case 15: return "NonCounted"; + case 16: return "NotSummed"; + case 17: return "NotCountedOrSummed"; + case 18: return "ReferenceWithSumItem"; + case 19: return "ProvableSumTree"; + case 20: return "ProvableCountProvableSumTree"; + default: return nullptr; + } +} + +} // namespace + +bool DecodeElement(Span bytes, Element& out, std::string& error) +{ + // grovedb-element/src/element/serialize.rs Element::deserialize with the + // bincode standard()+big_endian()+no_limit configuration. Enum + // discriminants and collection lengths are bincode varints; Option is a + // 0/1 tag byte; integer payloads are zigzag varints. + BytesReader reader{bytes}; + out = Element{}; + uint64_t discriminant; + bool ok{reader.ReadBincodeVarint(discriminant)}; + if (ok) { + switch (discriminant) { + case 0: + out.type = Element::Type::ITEM; + ok = reader.ReadBincodeByteVec(out.item_value, MAX_PROOF_BYTES) && + ReadOptionalByteVec(reader, out.flags); + break; + case 1: { + out.type = Element::Type::REFERENCE; + ok = DecodeReferencePath(reader, out.reference); + if (ok) { + bool has_hops; + ok = reader.ReadBincodeOptionTag(has_hops); + if (ok && has_hops) { + uint8_t hops; + ok = reader.ReadU8(hops); + out.max_hops = hops; + } + } + ok = ok && ReadOptionalByteVec(reader, out.flags); + break; + } + case 2: { + out.type = Element::Type::TREE; + ok = ReadOptionalByteVec(reader, out.root_key) && ReadOptionalByteVec(reader, out.flags); + break; + } + case 3: + out.type = Element::Type::SUM_ITEM; + ok = reader.ReadBincodeVarintSigned(out.sum_value) && ReadOptionalByteVec(reader, out.flags); + break; + case 4: + out.type = Element::Type::SUM_TREE; + ok = ReadOptionalByteVec(reader, out.root_key) && + reader.ReadBincodeVarintSigned(out.sum_value) && ReadOptionalByteVec(reader, out.flags); + break; + default: { + const char* name{UnsupportedElementName(discriminant)}; + if (name != nullptr) { + error = strprintf("unsupported element type %s", name); + } else { + error = strprintf("unknown element discriminant %u", discriminant); + } + return false; + } + } + } + if (!ok || reader.HasError()) { + error = strprintf("unable to deserialize element: %s", reader.GetError()); + return false; + } + if (!reader.IsEof()) { + error = strprintf("element deserialization did not consume all bytes: %u left", reader.Remaining()); + return false; + } + return true; +} + +namespace { + +// --------------------------------------------------------------------------- +// GroveDBProof envelope decoding +// (grovedb/src/operations/proof/mod.rs, bincode standard()+big_endian()) +// --------------------------------------------------------------------------- + +struct LayerProofNode { + Bytes merk_proof; + std::map lower_layers; +}; + +struct EnvelopeProof { + //! 0 = GroveDBProofV0 (MerkOnlyLayerProof + embedded ProveOptions, + //! lenient merk proof version), 1 = GroveDBProofV1 (LayerProof with + //! ProofBytes, strict merk proof version, default ProveOptions). + uint16_t version{1}; + //! ProveOptions::decrease_limit_on_empty_sub_query_result. Embedded in + //! the proof for V0 (prover controlled); the default (true) for V1. + bool decrease_limit_on_empty_sub_query_result{true}; + LayerProofNode root_layer; +}; + +bool DecodeLayerProof(BytesReader& reader, bool v1, size_t depth, LayerProofNode& out) +{ + // Mirrors MerkOnlyLayerProof::decode_with_depth (V0) and + // LayerProof::decode_with_depth (V1). + if (depth > MAX_PROOF_DEPTH) { + return reader.SetError("proof layer nesting depth exceeded maximum"); + } + if (v1) { + // ProofBytes enum: only the Merk variant is verifiable here. + uint64_t proof_type; + if (!reader.ReadBincodeVarint(proof_type)) return false; + switch (proof_type) { + case 0: + break; // Merk + case 1: + return reader.SetError("unsupported proof bytes type MMR"); + case 2: + return reader.SetError("unsupported proof bytes type BulkAppendTree"); + case 3: + return reader.SetError("unsupported proof bytes type DenseTree"); + case 4: + return reader.SetError("unsupported proof bytes type CommitmentTree"); + default: + return reader.SetError(strprintf("unknown proof bytes discriminant %u", proof_type)); + } + } + if (!reader.ReadBincodeByteVec(out.merk_proof, MAX_PROOF_BYTES)) return false; + uint64_t child_count; + if (!reader.ReadBincodeVarint(child_count)) return false; + if (child_count > MAX_PROOF_DEPTH) return reader.SetError("proof layer has too many children"); + for (uint64_t i = 0; i < child_count; ++i) { + Bytes key; + if (!reader.ReadBincodeByteVec(key, MAX_PROOF_BYTES)) return false; + LayerProofNode child; + if (!DecodeLayerProof(reader, v1, depth + 1, child)) return false; + out.lower_layers[std::move(key)] = std::move(child); + } + return true; +} + +bool DecodeEnvelope(Span proof, EnvelopeProof& out, std::string& error) +{ + // grovedb/src/operations/proof/mod.rs decode_grovedb_proof_canonical: + // bincode standard()+big_endian(), trailing bytes rejected. + BytesReader reader{proof}; + uint64_t version; + if (!reader.ReadBincodeVarint(version)) { + error = reader.GetError(); + return false; + } + out = EnvelopeProof{}; + switch (version) { + case 0: { + out.version = 0; + if (!DecodeLayerProof(reader, /*v1=*/false, 0, out.root_layer)) { + error = reader.GetError(); + return false; + } + // Embedded ProveOptions { decrease_limit_on_empty_sub_query_result: + // bool }. Prover controlled; see the V0 security note upstream. + uint8_t flag; + if (!reader.ReadU8(flag) || flag > 1) { + error = reader.HasError() ? reader.GetError() : "invalid ProveOptions bool encoding"; + return false; + } + out.decrease_limit_on_empty_sub_query_result = flag == 1; + break; + } + case 1: { + out.version = 1; + // V1 does not embed ProveOptions; the verifier uses the default. + if (!DecodeLayerProof(reader, /*v1=*/true, 0, out.root_layer)) { + error = reader.GetError(); + return false; + } + break; + } + default: + error = strprintf("unknown GroveDBProof version discriminant %u", version); + return false; + } + if (!reader.IsEof()) { + error = strprintf("proof has %u trailing bytes after the encoded envelope", reader.Remaining()); + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// PathQuery::query_items_at_path (grovedb/src/query/mod.rs) +// --------------------------------------------------------------------------- + +//! Mirror of grovedb/src/query/mod.rs SinglePathSubquery, resolved for one +//! specific path. +struct LevelQuery { + std::vector items; + bool left_to_right{true}; + enum class SubqueryKind : uint8_t { NONE, ALWAYS, CONDITIONAL } subquery_kind{SubqueryKind::NONE}; + const std::vector* conditional{nullptr}; + std::optional in_path; + + //! SinglePathSubquery::has_subquery_or_matching_in_path_on_key. + bool HasSubqueryOrMatchingInPathOnKey(const Bytes& key) const + { + switch (subquery_kind) { + case SubqueryKind::ALWAYS: + return true; + case SubqueryKind::CONDITIONAL: + if (conditional != nullptr) { + for (const auto& branch : *conditional) { + if (branch.item.Contains(key)) return true; + } + } + break; + case SubqueryKind::NONE: + break; + } + return in_path.has_value() && *in_path == key; + } +}; + +LevelQuery LevelFromQuery(const GroveQuery& query) +{ + // SinglePathSubquery::from_query + LevelQuery out; + out.items = query.items; + out.left_to_right = query.left_to_right; + if (!query.default_subquery_branch.Empty()) { + out.subquery_kind = LevelQuery::SubqueryKind::ALWAYS; + } else if (!query.conditional_subquery_branches.empty()) { + out.subquery_kind = LevelQuery::SubqueryKind::CONDITIONAL; + out.conditional = &query.conditional_subquery_branches; + } + return out; +} + +LevelQuery LevelFromKeyInPath(const Bytes& key, bool subquery_is_last_path_item, + bool subquery_has_inner_subquery) +{ + // SinglePathSubquery::from_key_when_in_path + LevelQuery out; + out.items.push_back(merk::QueryItem::Key(key)); + if (!(subquery_is_last_path_item && !subquery_has_inner_subquery)) { + out.in_path = key; + } + return out; +} + +using PathSpan = Span; + +bool PathsMatchPrefix(PathSpan path, const std::vector& prefix_source, size_t count) +{ + for (size_t i = 0; i < count; ++i) { + if (path[i] != prefix_source[i]) return false; + } + return true; +} + +//! recursive_query_items in PathQuery::query_items_at_path. `path` is the +//! path relative to the layer this query applies to. Returns false when the +//! query has nothing to say about this path (Rust None). +bool RecursiveQueryItems(const GroveQuery& query, PathSpan path, LevelQuery& out) +{ + if (path.empty()) { + out = LevelFromQuery(query); + return true; + } + + const Bytes& key{path.front()}; + const PathSpan rest{path.subspan(1)}; + + const auto resolve_branch = [&](const GroveQuery::SubqueryBranch& branch) -> bool { + // Shared logic between conditional and default subquery branches. + if (branch.subquery_path.has_value()) { + const std::vector& subquery_path{*branch.subquery_path}; + if (rest.size() <= subquery_path.size()) { + if (PathsMatchPrefix(rest, subquery_path, rest.size())) { + if (rest.size() == subquery_path.size()) { + if (branch.subquery) { + out = LevelFromQuery(*branch.subquery); + return true; + } + return false; + } + const bool last_path_item{path.size() == subquery_path.size()}; + out = LevelFromKeyInPath(subquery_path[rest.size()], last_path_item, + branch.subquery != nullptr); + return true; + } + } else if (PathsMatchPrefix(rest, subquery_path, subquery_path.size()) && branch.subquery) { + return RecursiveQueryItems(*branch.subquery, rest.subspan(subquery_path.size()), out); + } + } else if (branch.subquery) { + return RecursiveQueryItems(*branch.subquery, rest, out); + } + return false; + }; + + for (const auto& conditional : query.conditional_subquery_branches) { + if (conditional.item.Contains(key)) { + // Once a conditional item matches the key the outcome is final. + return resolve_branch(conditional.branch); + } + } + + return resolve_branch(query.default_subquery_branch); +} + +//! PathQuery::query_items_at_path. `path` is absolute. Returns false when +//! the path is not covered by the query. +bool QueryItemsAtPath(const PathQuery& query, PathSpan path, LevelQuery& out) +{ + const size_t query_path_len{query.path.size()}; + if (path.size() < query_path_len) { + if (!PathsMatchPrefix(path, query.path, path.size())) return false; + out = LevelFromKeyInPath(query.path[path.size()], /*subquery_is_last_path_item=*/false, + /*subquery_has_inner_subquery=*/true); + return true; + } + if (!PathsMatchPrefix(path, query.path, query_path_len)) return false; + if (path.size() == query_path_len) { + out = LevelFromQuery(query.query); + return true; + } + return RecursiveQueryItems(query.query, path.subspan(query_path_len), out); +} + +// --------------------------------------------------------------------------- +// Recursive layer verification +// (grovedb/src/operations/proof/verify.rs verify_layer_proof{,_v1}) +// --------------------------------------------------------------------------- + +struct VerifyContext { + const PathQuery& query; + const VerifyOptions& options; + const EnvelopeProof& envelope; + std::optional limit_left; + std::vector results; +}; + +bool VerifyLayer(VerifyContext& ctx, const LayerProofNode& layer, std::vector& current_path, + size_t depth, Hash256& out_root_hash, std::string& error) +{ + if (depth > MAX_PROOF_DEPTH) { + error = "proof verification exceeded maximum depth limit"; + return false; + } + + LevelQuery level; + if (!QueryItemsAtPath(ctx.query, PathSpan{current_path}, level)) { + error = "proof layer path is not covered by the path query"; + return false; + } + + // V0 envelopes verify with the lenient merk proof version (0); V1 with + // the strict version (1). + merk::VerifyResult merk_result; + if (!merk::ExecuteProof(layer.merk_proof, level.items, ctx.limit_left, level.left_to_right, + ctx.envelope.version, merk_result, error)) { + return false; + } + + std::vector verified_keys; + + if (merk_result.result_set.empty()) { + if (ctx.envelope.decrease_limit_on_empty_sub_query_result && ctx.limit_left.has_value() && + *ctx.limit_left > 0) { + --*ctx.limit_left; + } + } else { + for (const merk::ProvedKeyValue& proved : merk_result.result_set) { + const Bytes& key{proved.key}; + const Bytes& value_bytes{proved.value}; + Element element; + if (!DecodeElement(value_bytes, element, error)) return false; + verified_keys.push_back(key); + + const auto lower_it{layer.lower_layers.find(key)}; + if (lower_it != layer.lower_layers.end()) { + if (!element.IsTree() || element.IsEmptyTree()) { + error = "proof has lower layer for a non-tree element"; + return false; + } + current_path.push_back(key); + LevelQuery deeper; + if (!QueryItemsAtPath(ctx.query, PathSpan{current_path}, deeper)) { + // The query targets the tree element itself, not its + // contents. + ctx.results.push_back( + ProvedPathKeyValue{current_path, key, value_bytes, proved.value_hash}); + if (ctx.limit_left.has_value() && *ctx.limit_left > 0) --*ctx.limit_left; + current_path.pop_back(); + if (ctx.limit_left == std::optional{0}) break; + } else { + Hash256 lower_hash; + if (!VerifyLayer(ctx, lower_it->second, current_path, depth + 1, lower_hash, error)) { + return false; + } + current_path.pop_back(); + // Subtree link check: the parent's committed value_hash + // must equal combine_hash(H(serialized element bytes), + // child layer root hash). + const Hash256 combined{merk::CombineHash(merk::ValueHash(value_bytes), lower_hash)}; + if (proved.value_hash != combined) { + error = strprintf("mismatch in lower layer hash, expected %s, got %s", + HexStr(proved.value_hash), HexStr(combined)); + return false; + } + if (ctx.limit_left == std::optional{0}) break; + } + } else if (element.IsItem() || + (!level.HasSubqueryOrMatchingInPathOnKey(key) && + (ctx.options.include_empty_trees_in_result || + !(element.type == Element::Type::TREE && element.IsEmptyTree())))) { + if (element.IsTree() && element.IsEmptyTree()) { + // Empty trees carry no lower layer; their committed + // value_hash must be combine_hash(H(value), NULL_HASH). + // Without this an attacker could swap tree types inside + // KVValueHash nodes. + const Hash256 expected{merk::CombineHash(merk::ValueHash(value_bytes), merk::NULL_HASH)}; + if (proved.value_hash != expected) { + error = strprintf("empty tree value hash mismatch at key %s", HexStr(key)); + return false; + } + } + // V1 strict rule: a non-empty merk tree without a subquery + // must come from a KVValueHashFeatureTypeWithChildHash node + // so the merk verifier confirmed combine_hash(H(value), + // child_hash) == value_hash. + if (ctx.envelope.version >= 1 && element.IsTree() && !element.IsEmptyTree() && + !proved.child_hash_verified) { + error = strprintf("non-empty tree at key %s without subquery must use a " + "child-hash-verified proof node", + HexStr(key)); + return false; + } + ctx.results.push_back(ProvedPathKeyValue{current_path, key, value_bytes, proved.value_hash}); + if (ctx.limit_left.has_value() && *ctx.limit_left > 0) --*ctx.limit_left; + if (ctx.limit_left == std::optional{0}) break; + } else if (element.IsTree() && !element.IsEmptyTree() && + level.HasSubqueryOrMatchingInPathOnKey(key)) { + error = strprintf("proof is missing lower layer for non-empty tree at key %s", HexStr(key)); + return false; + } + } + } + + if (ctx.options.verify_proof_succinctness) { + for (const auto& [key, unused_child] : layer.lower_layers) { + if (std::find(verified_keys.begin(), verified_keys.end(), key) == verified_keys.end()) { + error = strprintf("proof contains extra lower layer for key %s not required by query", + HexStr(key)); + return false; + } + } + } + + out_root_hash = merk_result.root_hash; + return true; +} + +} // namespace + +bool VerifyQuery(Span proof, const PathQuery& query, const VerifyOptions& options, + GroveVerifyResult& out, std::string& error) +{ + out = GroveVerifyResult{}; + EnvelopeProof envelope; + if (!DecodeEnvelope(proof, envelope, error)) return false; + + VerifyContext ctx{query, options, envelope, query.limit, {}}; + std::vector current_path; + Hash256 root_hash; + if (!VerifyLayer(ctx, envelope.root_layer, current_path, 0, root_hash, error)) return false; + + out.root_hash = root_hash; + out.results = std::move(ctx.results); + return true; +} + +bool VerifyQueryWithRootHash(Span proof, const PathQuery& query, + const VerifyOptions& options, const Hash256& expected_root_hash, + GroveVerifyResult& out, std::string& error) +{ + if (!VerifyQuery(proof, query, options, out, error)) return false; + if (out.root_hash != expected_root_hash) { + error = strprintf("proof did not match expected root hash: expected %s, got %s", + HexStr(expected_root_hash), HexStr(out.root_hash)); + return false; + } + return true; +} + +} // namespace platform::grove diff --git a/src/platform/proof/grovedb.h b/src/platform/proof/grovedb.h new file mode 100644 index 000000000000..5b1639be2a75 --- /dev/null +++ b/src/platform/proof/grovedb.h @@ -0,0 +1,167 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_PROOF_GROVEDB_H +#define BITCOIN_PLATFORM_PROOF_GROVEDB_H + +#include +#include + +#include +#include +#include +#include +#include +#include + +//! Layered GroveDB proof verification, a pure C++ port of the "verify" +//! feature slice of dashpay/grovedb tag v5.0.0: +//! - grovedb/src/operations/proof/mod.rs (GroveDBProof envelope, bincode +//! standard()+big_endian() config, V0/V1 layer proofs) +//! - grovedb/src/operations/proof/verify.rs (recursive layer verification) +//! - grovedb-element/src/element/mod.rs + serialize.rs (Element decoding) +//! - grovedb-element/src/reference_path/mod.rs (ReferencePathType) +//! - grovedb/src/query/mod.rs (PathQuery::query_items_at_path) +namespace platform::grove { + +using merk::Hash256; + +//! ReferencePathType (grovedb-element/src/reference_path/mod.rs). Variant +//! order matches the Rust enum declaration (bincode discriminants 0..6). +struct ReferencePath { + enum class Type : uint8_t { + ABSOLUTE_PATH = 0, + UPSTREAM_ROOT_HEIGHT = 1, + UPSTREAM_ROOT_HEIGHT_WITH_PARENT_PATH_ADDITION = 2, + UPSTREAM_FROM_ELEMENT_HEIGHT = 3, + COUSIN = 4, + REMOVED_COUSIN = 5, + SIBLING = 6, + }; + Type type{Type::ABSOLUTE_PATH}; + //! First tuple field of the upstream variants. + uint8_t height{0}; + //! Path segments; for COUSIN / SIBLING the single key lives in path[0]. + std::vector path; + + //! Resolves the reference to an absolute path, given the path of the + //! tree that holds the reference and the reference's own key. Mirrors + //! grovedb-element/src/reference_path/mod.rs + //! path_from_reference_path_type. Returns false on invalid input (e.g. + //! upstream height exceeding the current path length). + bool Resolve(const std::vector& current_path, const Bytes& key, + std::vector& out, std::string& error) const; +}; + +//! GroveDB Element (grovedb-element/src/element/mod.rs). Only the variants +//! the platform GUI needs are decoded fully (Item, Reference, Tree, SumItem, +//! SumTree); the remaining variants are recognized and rejected with clear +//! errors. Bincode discriminants follow the Rust declaration order. +struct Element { + enum class Type : uint8_t { + ITEM = 0, + REFERENCE = 1, + TREE = 2, + SUM_ITEM = 3, + SUM_TREE = 4, + // Recognized but unsupported: BigSumTree=5, CountTree=6, + // CountSumTree=7, ProvableCountTree=8, ItemWithSumItem=9, + // ProvableCountSumTree=10, CommitmentTree=11, MmrTree=12, + // BulkAppendTree=13, DenseAppendOnlyFixedSizeTree=14, + // NonCounted=15, NotSummed=16, NotCountedOrSummed=17, + // ReferenceWithSumItem=18, ProvableSumTree=19, + // ProvableCountProvableSumTree=20. + }; + Type type{Type::ITEM}; + Bytes item_value; //!< ITEM payload + int64_t sum_value{0}; //!< SUM_ITEM value / SUM_TREE total + std::optional root_key; //!< TREE / SUM_TREE root key + ReferencePath reference; //!< REFERENCE target + std::optional max_hops; //!< REFERENCE MaxReferenceHop + std::optional flags; //!< element flags (all variants) + + bool IsTree() const { return type == Type::TREE || type == Type::SUM_TREE; } + bool IsEmptyTree() const { return IsTree() && !root_key.has_value(); } + bool IsItem() const { return type == Type::ITEM || type == Type::SUM_ITEM; } +}; + +//! Decodes a serialized Element (bincode standard()+big_endian()+no_limit, +//! grovedb-element/src/element/serialize.rs Element::deserialize). The whole +//! input must be consumed. Returns false with error set on failure or when +//! the element variant is not supported by this verifier. +bool DecodeElement(Span bytes, Element& out, std::string& error); + +//! Query with optional default subquery branch +//! (grovedb-query/src/query.rs Query + subquery_branch.rs SubqueryBranch). +//! Conditional subquery branches are supported through `conditional`. +struct GroveQuery { + struct SubqueryBranch { + //! Optional path of intermediate keys leading to the subquery. + std::optional> subquery_path; + std::shared_ptr subquery; + + bool Empty() const { return !subquery_path.has_value() && !subquery; } + }; + struct ConditionalBranch { + merk::QueryItem item; + SubqueryBranch branch; + }; + + std::vector items; + bool left_to_right{true}; + SubqueryBranch default_subquery_branch; + std::vector conditional_subquery_branches; +}; + +//! PathQuery (grovedb/src/query/mod.rs PathQuery + SizedQuery). Offsets are +//! not supported by the proof system and are therefore not modeled. +struct PathQuery { + std::vector path; + GroveQuery query; + std::optional limit; +}; + +//! One verified result entry (grovedb/src/operations/proof/util.rs +//! ProvedPathKeyValue): the path of the subtree that holds the entry, the +//! key, and the serialized element bytes. +struct ProvedPathKeyValue { + std::vector path; + Bytes key; + Bytes value; + Hash256 value_hash{}; +}; + +struct GroveVerifyResult { + Hash256 root_hash{}; + std::vector results; +}; + +//! Verification options (merk/src/proofs/query/verify.rs VerifyOptions +//! subset; absence_proofs_for_non_existing_searched_keys is handled by the +//! caller since raw results already prove absences cryptographically). +struct VerifyOptions { + //! Reject proofs with lower layers the query did not require. + bool verify_proof_succinctness{false}; + //! Include empty tree elements in the result set. + bool include_empty_trees_in_result{true}; +}; + +//! Verifies a full layered GroveDB proof (V0 or V1 envelope) against a path +//! query, mirroring GroveDb::verify_query_raw / +//! grovedb/src/operations/proof/verify.rs. On success out.root_hash is the +//! GroveDB root hash the proof commits to and out.results contains the +//! proven (path, key, element bytes) entries in proof order. Any layer whose +//! parent link hash does not match, any incomplete query answer and any +//! malformed encoding is a hard error. +bool VerifyQuery(Span proof, const PathQuery& query, const VerifyOptions& options, + GroveVerifyResult& out, std::string& error); + +//! VerifyQuery + comparison against an expected root hash. +bool VerifyQueryWithRootHash(Span proof, const PathQuery& query, + const VerifyOptions& options, const Hash256& expected_root_hash, + GroveVerifyResult& out, std::string& error); + +} // namespace platform::grove + +#endif // BITCOIN_PLATFORM_PROOF_GROVEDB_H diff --git a/src/platform/proof/merk.cpp b/src/platform/proof/merk.cpp new file mode 100644 index 000000000000..25cb79c712d4 --- /dev/null +++ b/src/platform/proof/merk.cpp @@ -0,0 +1,783 @@ +// Copyright (c) 2026 The Dash 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 +#include + +namespace platform::merk { + +namespace { + +//! merk/src/proofs/tree.rs MAX_PROOF_OPS. +constexpr size_t MAX_PROOF_OPS{50'000}; +//! merk/src/proofs/tree.rs MAX_PROOF_STACK_DEPTH. +constexpr size_t MAX_PROOF_STACK_DEPTH{10'000}; +//! merk/src/proofs/tree.rs MAX_PROOF_TREE_HEIGHT. +constexpr size_t MAX_PROOF_TREE_HEIGHT{92}; +//! grovedb-query/src/proofs/encoding.rs MAX_VALUE_LEN (64 MiB). +constexpr uint32_t MAX_VALUE_LEN{64 * 1024 * 1024}; +//! GroveDB enforces a 255 byte maximum key length, so u8 key lengths in the +//! proof encoding are exact (grovedb-query/src/proofs/encoding.rs). +constexpr size_t MAX_KEY_LEN{255}; + +class Blake3 +{ +public: + Blake3() { blake3_hasher_init(&m_hasher); } + Blake3& Update(Span data) + { + blake3_hasher_update(&m_hasher, data.data(), data.size()); + return *this; + } + Hash256 Finalize() + { + Hash256 out; + blake3_hasher_finalize(&m_hasher, out.data(), out.size()); + return out; + } + +private: + blake3_hasher m_hasher; +}; + +Bytes LEB128(uint64_t value) +{ + Bytes out; + WriteLEB128(out, value); + return out; +} + +} // namespace + +Hash256 ValueHash(Span value) +{ + // merk/src/tree/hash.rs value_hash + return Blake3{}.Update(LEB128(value.size())).Update(value).Finalize(); +} + +Hash256 KvHash(Span key, Span value) +{ + // merk/src/tree/hash.rs kv_hash + const Hash256 vh{ValueHash(value)}; + return Blake3{}.Update(LEB128(key.size())).Update(key).Update(vh).Finalize(); +} + +Hash256 KvDigestToKvHash(Span key, const Hash256& value_hash) +{ + // merk/src/tree/hash.rs kv_digest_to_kv_hash + return Blake3{}.Update(LEB128(key.size())).Update(key).Update(value_hash).Finalize(); +} + +Hash256 NodeHash(const Hash256& kv, const Hash256& left, const Hash256& right) +{ + // merk/src/tree/hash.rs node_hash + return Blake3{}.Update(kv).Update(left).Update(right).Finalize(); +} + +Hash256 CombineHash(const Hash256& a, const Hash256& b) +{ + // merk/src/tree/hash.rs combine_hash + return Blake3{}.Update(a).Update(b).Finalize(); +} + +Hash256 NodeHashWithSum(const Hash256& kv, const Hash256& left, const Hash256& right, int64_t sum) +{ + // merk/src/tree/hash.rs node_hash_with_sum: i64 sum appended big-endian. + std::array sum_be; + const auto usum = static_cast(sum); + for (int i = 0; i < 8; ++i) { + sum_be[i] = static_cast(usum >> (8 * (7 - i))); + } + return Blake3{}.Update(kv).Update(left).Update(right).Update(sum_be).Finalize(); +} + +bool Node::IsKeyBearing() const +{ + switch (variant) { + case NodeVariant::KV: + case NodeVariant::KV_VALUE_HASH: + case NodeVariant::KV_DIGEST: + case NodeVariant::KV_REF_VALUE_HASH: + case NodeVariant::KV_VALUE_HASH_FEATURE_TYPE: + case NodeVariant::KV_VALUE_HASH_FEATURE_TYPE_WITH_CHILD_HASH: + return true; + case NodeVariant::HASH: + case NodeVariant::KV_HASH: + return false; + } + return false; +} + +namespace { + +//! Decodes a TreeFeatureType (grovedb-query/src/proofs/tree_feature_type.rs +//! impl Decode). Tag byte then, for SummedMerkNode, an LEB128 zigzag i64 +//! (integer-encoding crate write_varint). Only Basic (0) and Summed (1) are +//! supported. +bool DecodeTreeFeature(BytesReader& reader, TreeFeature& out) +{ + uint8_t tag; + if (!reader.ReadU8(tag)) return false; + switch (tag) { + case 0: + out = TreeFeature{}; + return true; + case 1: + out.summed = true; + return reader.ReadLEB128Signed(out.sum); + case 2: + return reader.SetError("unsupported tree feature type BigSummedMerkNode"); + case 3: + return reader.SetError("unsupported tree feature type CountedMerkNode"); + case 4: + return reader.SetError("unsupported tree feature type CountedSummedMerkNode"); + case 5: + return reader.SetError("unsupported tree feature type ProvableCountedMerkNode"); + case 6: + return reader.SetError("unsupported tree feature type ProvableCountedSummedMerkNode"); + case 7: + return reader.SetError("unsupported tree feature type ProvableSummedMerkNode"); + case 8: + return reader.SetError("unsupported tree feature type ProvableCountedAndProvableSummedMerkNode"); + default: + return reader.SetError(strprintf("unknown tree feature type tag %u", tag)); + } +} + +bool ReadKey(BytesReader& reader, Bytes& key) +{ + uint8_t key_len; + if (!reader.ReadU8(key_len)) return false; + static_assert(MAX_KEY_LEN == 255, "u8 key length covers the whole key range"); + return reader.ReadBytes(key_len, key); +} + +//! large=false: u16 big-endian value length; large=true: u32 big-endian +//! value length that must not fit in the small encoding (canonical form) and +//! must respect MAX_VALUE_LEN. +bool ReadValue(BytesReader& reader, bool large, Bytes& value) +{ + if (!large) { + uint16_t len; + if (!reader.ReadU16BE(len)) return false; + return reader.ReadBytes(len, value); + } + uint32_t len; + if (!reader.ReadU32BE(len)) return false; + if (len < 65536) return reader.SetError("non-canonical large value length in proof op"); + if (len > MAX_VALUE_LEN) return reader.SetError(strprintf("proof value length %u exceeds maximum %u", len, MAX_VALUE_LEN)); + return reader.ReadBytes(len, value); +} + +bool ReadHash(BytesReader& reader, Hash256& out) +{ + return reader.ReadInto(Span{out.data(), out.size()}); +} + +} // namespace + +bool DecodeOp(BytesReader& reader, Op& op) +{ + // grovedb-query/src/proofs/encoding.rs impl Decode for Op. + uint8_t opcode; + if (!reader.ReadU8(opcode)) return false; + + op.node = Node{}; + const auto push = [&](OpType type, NodeVariant variant) { + op.type = type; + op.node.variant = variant; + }; + + switch (opcode) { + case 0x01: + case 0x08: + push(opcode == 0x01 ? OpType::PUSH : OpType::PUSH_INVERTED, NodeVariant::HASH); + return ReadHash(reader, op.node.hash); + case 0x02: + case 0x09: + push(opcode == 0x02 ? OpType::PUSH : OpType::PUSH_INVERTED, NodeVariant::KV_HASH); + return ReadHash(reader, op.node.hash); + case 0x03: + case 0x0a: + case 0x20: + case 0x28: { + const bool large{opcode == 0x20 || opcode == 0x28}; + push((opcode == 0x03 || opcode == 0x20) ? OpType::PUSH : OpType::PUSH_INVERTED, NodeVariant::KV); + return ReadKey(reader, op.node.key) && ReadValue(reader, large, op.node.value); + } + case 0x04: + case 0x0b: + case 0x21: + case 0x29: { + const bool large{opcode == 0x21 || opcode == 0x29}; + push((opcode == 0x04 || opcode == 0x21) ? OpType::PUSH : OpType::PUSH_INVERTED, NodeVariant::KV_VALUE_HASH); + return ReadKey(reader, op.node.key) && ReadValue(reader, large, op.node.value) && + ReadHash(reader, op.node.value_hash); + } + case 0x05: + case 0x0c: + push(opcode == 0x05 ? OpType::PUSH : OpType::PUSH_INVERTED, NodeVariant::KV_DIGEST); + return ReadKey(reader, op.node.key) && ReadHash(reader, op.node.value_hash); + case 0x06: + case 0x0d: + case 0x22: + case 0x2a: { + const bool large{opcode == 0x22 || opcode == 0x2a}; + push((opcode == 0x06 || opcode == 0x22) ? OpType::PUSH : OpType::PUSH_INVERTED, NodeVariant::KV_REF_VALUE_HASH); + return ReadKey(reader, op.node.key) && ReadValue(reader, large, op.node.value) && + ReadHash(reader, op.node.value_hash); + } + case 0x07: + case 0x0e: + case 0x23: + case 0x2b: { + const bool large{opcode == 0x23 || opcode == 0x2b}; + push((opcode == 0x07 || opcode == 0x23) ? OpType::PUSH : OpType::PUSH_INVERTED, NodeVariant::KV_VALUE_HASH_FEATURE_TYPE); + return ReadKey(reader, op.node.key) && ReadValue(reader, large, op.node.value) && + ReadHash(reader, op.node.value_hash) && DecodeTreeFeature(reader, op.node.feature); + } + case 0x1c: + case 0x1d: + case 0x2e: + case 0x2f: { + const bool large{opcode == 0x2e || opcode == 0x2f}; + push((opcode == 0x1c || opcode == 0x2e) ? OpType::PUSH : OpType::PUSH_INVERTED, + NodeVariant::KV_VALUE_HASH_FEATURE_TYPE_WITH_CHILD_HASH); + return ReadKey(reader, op.node.key) && ReadValue(reader, large, op.node.value) && + ReadHash(reader, op.node.value_hash) && DecodeTreeFeature(reader, op.node.feature) && + ReadHash(reader, op.node.child_hash); + } + case 0x10: + op.type = OpType::PARENT; + return true; + case 0x11: + op.type = OpType::CHILD; + return true; + case 0x12: + op.type = OpType::PARENT_INVERTED; + return true; + case 0x13: + op.type = OpType::CHILD_INVERTED; + return true; + default: + break; + } + + // Recognized but unsupported op families; reject with explicit errors so + // callers can tell "malformed" from "unsupported tree type". + if ((opcode >= 0x14 && opcode <= 0x1b) || opcode == 0x1e || opcode == 0x1f || + opcode == 0x24 || opcode == 0x25 || opcode == 0x2c || opcode == 0x2d) { + return reader.SetError(strprintf("unsupported count-tree proof op 0x%02x", opcode)); + } + if (opcode >= 0x30 && opcode <= 0x3d) { + return reader.SetError(strprintf("unsupported provable-sum-tree proof op 0x%02x", opcode)); + } + if (opcode >= 0x40 && opcode <= 0x4d) { + return reader.SetError(strprintf("unsupported provable-count-and-sum-tree proof op 0x%02x", opcode)); + } + return reader.SetError(strprintf("unknown proof op 0x%02x", opcode)); +} + +QueryItem QueryItem::Key(Bytes key) +{ + QueryItem item; + item.type = Type::KEY; + item.key = std::move(key); + return item; +} + +QueryItem QueryItem::Range(Bytes start, Bytes end) +{ + QueryItem item; + item.type = Type::RANGE; + item.start = std::move(start); + item.end = std::move(end); + return item; +} + +QueryItem QueryItem::RangeInclusive(Bytes start, Bytes end) +{ + QueryItem item; + item.type = Type::RANGE_INCLUSIVE; + item.start = std::move(start); + item.end = std::move(end); + return item; +} + +QueryItem QueryItem::RangeFull() +{ + QueryItem item; + item.type = Type::RANGE_FULL; + return item; +} + +std::pair>, bool> QueryItem::LowerBound() const +{ + // grovedb-query/src/query_item/mod.rs lower_bound + switch (type) { + case Type::KEY: + return {Span{key}, false}; + case Type::RANGE: + case Type::RANGE_INCLUSIVE: + return {Span{start}, false}; + case Type::RANGE_FULL: + return {std::nullopt, false}; + } + return {std::nullopt, false}; +} + +std::pair>, bool> QueryItem::UpperBound() const +{ + // grovedb-query/src/query_item/mod.rs upper_bound + switch (type) { + case Type::KEY: + return {Span{key}, true}; + case Type::RANGE: + return {Span{end}, false}; + case Type::RANGE_INCLUSIVE: + return {Span{end}, true}; + case Type::RANGE_FULL: + return {std::nullopt, true}; + } + return {std::nullopt, true}; +} + +namespace { + +int CompareBytes(Span a, Span b) +{ + const size_t common{std::min(a.size(), b.size())}; + const int r{common == 0 ? 0 : std::memcmp(a.data(), b.data(), common)}; + if (r != 0) return r; + if (a.size() == b.size()) return 0; + return a.size() < b.size() ? -1 : 1; +} + +} // namespace + +bool QueryItem::Contains(Span candidate) const +{ + // grovedb-query/src/query_item/mod.rs contains + const auto [lower, lower_non_inclusive] = LowerBound(); + const auto [upper, upper_inclusive] = UpperBound(); + const bool lower_ok{LowerUnbounded() || + (lower && CompareBytes(candidate, *lower) > 0) || + (lower && CompareBytes(candidate, *lower) == 0 && !lower_non_inclusive)}; + const bool upper_ok{UpperUnbounded() || + (upper && CompareBytes(candidate, *upper) < 0) || + (upper && CompareBytes(candidate, *upper) == 0 && upper_inclusive)}; + return lower_ok && upper_ok; +} + +namespace { + +//! Peeks at the element type discriminant of serialized element bytes and +//! reports whether the type has a "simple" value hash (H(value) with no +//! child-hash combination), mirroring +//! grovedb-element/src/element_type.rs from_serialized_value + +//! has_simple_value_hash. Simple types: Item (0), SumItem (3), +//! ItemWithSumItem (9), including under a NonCounted (15) wrapper. +bool ElementHasSimpleValueHash(Span value, bool& simple, std::string& error) +{ + if (value.empty()) { + error = "cannot get element type from empty value"; + return false; + } + uint8_t base{value[0]}; + if (base == 15 || base == 16 || base == 17) { + // Wrapper discriminants (NonCounted / NotSummed / NotCountedOrSummed) + if (value.size() < 2) { + error = "element wrapper has no inner discriminant byte"; + return false; + } + const uint8_t inner{value[1]}; + if (base == 15) { + if (!(inner <= 14 || inner == 18 || inner == 19 || inner == 20)) { + error = strprintf("invalid NonCounted inner element discriminant %u", inner); + return false; + } + } else { + if (!(inner == 4 || inner == 5 || inner == 7 || inner == 10 || inner == 19 || inner == 20)) { + error = strprintf("invalid NotSummed/NotCountedOrSummed inner element discriminant %u", inner); + return false; + } + } + base = inner; + } else if (base > 20) { + error = strprintf("unknown element discriminant %u", base); + return false; + } + simple = base == 0 /* Item */ || base == 3 /* SumItem */ || base == 9 /* ItemWithSumItem */; + return true; +} + +//! A reconstructed proof tree node. Proofs are executed with the equivalent +//! of merk/src/proofs/tree.rs execute(ops, /*collapse=*/true, visit): child +//! subtrees are collapsed to their hash (and, matching the Rust +//! Tree::into_hash, their height resets to 1). +struct StackTree { + Node node; + bool has_left{false}; + bool has_right{false}; + Hash256 left_hash{}; + Hash256 right_hash{}; + size_t height{1}; + size_t left_child_height{0}; + size_t right_child_height{0}; + + const Hash256& ChildHash(bool left) const + { + static constexpr Hash256 null_hash{}; + if (left) return has_left ? left_hash : null_hash; + return has_right ? right_hash : null_hash; + } + + //! merk/src/proofs/tree.rs Tree::hash for the supported node variants. + Hash256 TreeHash() const + { + switch (node.variant) { + case NodeVariant::HASH: + return node.hash; + case NodeVariant::KV_HASH: + return NodeHash(node.hash, ChildHash(true), ChildHash(false)); + case NodeVariant::KV: + return NodeHash(KvHash(node.key, node.value), ChildHash(true), ChildHash(false)); + case NodeVariant::KV_VALUE_HASH: + case NodeVariant::KV_DIGEST: + case NodeVariant::KV_VALUE_HASH_FEATURE_TYPE: + case NodeVariant::KV_VALUE_HASH_FEATURE_TYPE_WITH_CHILD_HASH: + // Basic and Summed feature types hash like plain nodes; the sum + // is not part of the node hash for non-provable sum trees. The + // child_hash of the WithChildHash variant is GroveDB-level + // metadata and does not participate in the merk hash. + return NodeHash(KvDigestToKvHash(node.key, node.value_hash), ChildHash(true), ChildHash(false)); + case NodeVariant::KV_REF_VALUE_HASH: { + // value_hash field holds H(serialized reference element); the + // node's kv hash commits to + // combine_hash(value_hash, H(referenced value)). + const Hash256 combined{CombineHash(node.value_hash, ValueHash(node.value))}; + return NodeHash(KvDigestToKvHash(node.key, combined), ChildHash(true), ChildHash(false)); + } + } + return NULL_HASH; + } +}; + +//! Validates that query items are sorted ascending and non-overlapping (the +//! invariant grovedb's Query::insert_item maintains). +bool ValidateQueryItems(const std::vector& items, std::string& error) +{ + for (size_t i = 1; i < items.size(); ++i) { + const auto [prev_upper, prev_upper_inclusive] = items[i - 1].UpperBound(); + const auto [next_lower, next_lower_non_inclusive] = items[i].LowerBound(); + if (!prev_upper || !next_lower) { + error = "unbounded query items cannot be combined with other items"; + return false; + } + const int cmp{CompareBytes(*prev_upper, *next_lower)}; + if (cmp > 0 || (cmp == 0 && prev_upper_inclusive && !next_lower_non_inclusive)) { + error = "query items must be sorted ascending and non-overlapping"; + return false; + } + } + return true; +} + +} // namespace + +bool ExecuteProof(Span proof, const std::vector& items, + std::optional limit, bool left_to_right, uint16_t proof_version, + VerifyResult& out, std::string& error) +{ + // Mirrors merk/src/proofs/query/verify.rs execute_proof combined with + // merk/src/proofs/tree.rs execute (collapse = true). + out = VerifyResult{}; + if (!ValidateQueryItems(items, error)) return false; + + BytesReader reader{proof}; + std::vector stack; + stack.reserve(32); + std::optional last_key; + size_t op_count{0}; + + // Query walk state (verify.rs execute_proof) + size_t query_index{0}; // index into items in traversal direction + const auto query_peek = [&]() -> const QueryItem* { + if (query_index >= items.size()) return nullptr; + return left_to_right ? &items[query_index] : &items[items.size() - 1 - query_index]; + }; + bool in_range{false}; + std::optional current_limit{limit}; + // Last pushed node: nullopt until the first push; then whether it was a + // key-bearing variant (KV / KVDigest / KVValueHash / KVRefValueHash / + // feature-type variants) as opposed to Hash / KVHash. + std::optional last_push_key_bearing; + + const auto execute_node = [&](const Bytes& key, const Bytes* value, const Hash256& value_hash, + bool child_hash_verified) -> bool { + while (const QueryItem* query_item = query_peek()) { + const auto [lower_bound, start_non_inclusive] = query_item->LowerBound(); + const auto [upper_bound, end_inclusive] = query_item->UpperBound(); + + // Terminate if we encounter a node before the current query item. + const bool terminate = left_to_right + ? (!query_item->LowerUnbounded() && + (CompareBytes(*lower_bound, key) > 0 || + (start_non_inclusive && CompareBytes(*lower_bound, key) == 0))) + : (!query_item->UpperUnbounded() && + (CompareBytes(*upper_bound, key) < 0 || + (!end_inclusive && CompareBytes(*upper_bound, key) == 0))); + if (terminate) break; + + if (!in_range) { + // This is the first data we encounter for this query item: + // the bound facing the traversal direction must be proven, + // either by an exact match or by a preceding key-bearing + // node (or this being the first node in the tree). + const auto bound = left_to_right ? lower_bound : upper_bound; + const bool exact_match{bound && CompareBytes(key, *bound) == 0}; + if (!exact_match && last_push_key_bearing.has_value() && !*last_push_key_bearing) { + error = left_to_right ? "cannot verify lower bound of queried range" + : "cannot verify upper bound of queried range"; + return false; + } + } + + if (left_to_right) { + if (upper_bound && CompareBytes(key, *upper_bound) >= 0) { + // At or past the upper bound (or exact single-key match): + // advance to the next query item. + ++query_index; + in_range = false; + } else { + in_range = true; + } + } else if (lower_bound && CompareBytes(key, *lower_bound) <= 0) { + ++query_index; + in_range = false; + } else { + in_range = true; + } + + if (query_item->Contains(key)) { + if (value == nullptr) { + error = "proof is missing data for query"; + return false; + } + if (current_limit.has_value()) { + if (*current_limit == 0) { + error = "proof returns more data than the query limit"; + return false; + } + --*current_limit; + if (*current_limit == 0) in_range = false; + } + out.result_set.push_back(ProvedKeyValue{key, *value, value_hash, child_hash_verified}); + break; // continue to next push + } + // otherwise: continue with the next query item for the same key + } + return true; + }; + + const auto visit_node = [&](const Node& node) -> bool { + switch (node.variant) { + case NodeVariant::KV: + return execute_node(node.key, &node.value, ValueHash(node.value), false); + case NodeVariant::KV_VALUE_HASH: + case NodeVariant::KV_VALUE_HASH_FEATURE_TYPE: + case NodeVariant::KV_VALUE_HASH_FEATURE_TYPE_WITH_CHILD_HASH: { + // Strict (V1+) mode: these node types exist for elements whose + // value_hash is a combine_hash (trees and references). Reject + // item elements to prevent KV -> KVValueHash forgery. + if (proof_version >= 1) { + bool simple{false}; + std::string element_error; + if (!ElementHasSimpleValueHash(node.value, simple, element_error)) { + error = strprintf("cannot determine element type in proof node: %s", element_error); + return false; + } + if (simple) { + error = "value-hash proof node must not contain an item element"; + return false; + } + } + if (node.variant == NodeVariant::KV_VALUE_HASH_FEATURE_TYPE_WITH_CHILD_HASH) { + // Verify value integrity: combine_hash(H(value), child_hash) + // must equal the node's value_hash + // (merk/src/proofs/query/verify.rs). + const Hash256 computed{CombineHash(ValueHash(node.value), node.child_hash)}; + if (computed != node.value_hash) { + error = "value/child hash mismatch in proof node"; + return false; + } + return execute_node(node.key, &node.value, node.value_hash, true); + } + return execute_node(node.key, &node.value, node.value_hash, false); + } + case NodeVariant::KV_DIGEST: + return execute_node(node.key, nullptr, node.value_hash, false); + case NodeVariant::KV_REF_VALUE_HASH: + return execute_node(node.key, &node.value, node.value_hash, false); + case NodeVariant::HASH: + case NodeVariant::KV_HASH: + if (in_range) { + error = "proof is missing data for query range (abridged node inside range)"; + return false; + } + return true; + } + error = "unhandled proof node variant"; + return false; + }; + + // merk/src/proofs/tree.rs execute: op replay stack machine. + const auto attach = [&](StackTree& parent, bool left, const StackTree& child) -> bool { + if ((left && parent.has_left) || (!left && parent.has_right)) { + error = "proof op tried to attach to an already occupied child slot"; + return false; + } + // Collapse the child to its hash; matching Rust Tree::into_hash the + // collapsed child's height is 1. + const Hash256 child_hash{child.TreeHash()}; + constexpr size_t collapsed_child_height{1}; + const size_t new_height{std::max(parent.height, collapsed_child_height + 1)}; + if (new_height > MAX_PROOF_TREE_HEIGHT) { + error = "proof tree exceeds maximum height"; + return false; + } + parent.height = new_height; + if (left) { + parent.has_left = true; + parent.left_hash = child_hash; + parent.left_child_height = collapsed_child_height; + } else { + parent.has_right = true; + parent.right_hash = child_hash; + parent.right_child_height = collapsed_child_height; + } + return true; + }; + + const auto pop = [&](StackTree& out_tree) -> bool { + if (stack.empty()) { + error = "proof op stack underflow"; + return false; + } + out_tree = std::move(stack.back()); + stack.pop_back(); + return true; + }; + + while (!reader.IsEof()) { + if (++op_count > MAX_PROOF_OPS) { + error = "proof exceeds maximum operation count"; + return false; + } + Op op; + if (!DecodeOp(reader, op)) { + error = reader.GetError(); + return false; + } + switch (op.type) { + case OpType::PARENT: + case OpType::CHILD_INVERTED: { + StackTree parent, child; + if (!pop(parent) || !pop(child)) return false; + if (op.type == OpType::CHILD_INVERTED) std::swap(parent, child); + if (!attach(parent, /*left=*/true, child)) return false; + stack.push_back(std::move(parent)); + break; + } + case OpType::CHILD: + case OpType::PARENT_INVERTED: { + // CHILD pops the child from the top of the stack, then the + // parent; PARENT_INVERTED pops the parent first. Both attach the + // child on the right (grovedb-query/src/proofs/mod.rs). + StackTree parent, child; + if (op.type == OpType::CHILD) { + if (!pop(child) || !pop(parent)) return false; + } else { + if (!pop(parent) || !pop(child)) return false; + } + if (!attach(parent, /*left=*/false, child)) return false; + stack.push_back(std::move(parent)); + break; + } + case OpType::PUSH: + case OpType::PUSH_INVERTED: { + if (op.node.IsKeyBearing() && last_key.has_value()) { + const int cmp{CompareBytes(op.node.key, *last_key)}; + if (op.type == OpType::PUSH && cmp <= 0) { + error = "incorrect key ordering in proof"; + return false; + } + if (op.type == OpType::PUSH_INVERTED && cmp >= 0) { + error = "incorrect key ordering in inverted proof"; + return false; + } + } + if (op.node.IsKeyBearing()) last_key = op.node.key; + if (!visit_node(op.node)) return false; + last_push_key_bearing = op.node.IsKeyBearing(); + StackTree tree; + tree.node = std::move(op.node); + stack.push_back(std::move(tree)); + if (stack.size() > MAX_PROOF_STACK_DEPTH) { + error = "proof exceeds maximum stack depth"; + return false; + } + break; + } + } + } + + if (stack.size() != 1) { + error = "expected proof to result in exactly one stack item"; + return false; + } + const StackTree root{std::move(stack.back())}; + stack.pop_back(); + + // Root-level AVL balance check (merk/src/proofs/tree.rs execute). + const size_t max_child{std::max(root.left_child_height, root.right_child_height)}; + const size_t min_child{std::min(root.left_child_height, root.right_child_height)}; + if (max_child - min_child > 1) { + error = "expected proof to result in a valid avl tree"; + return false; + } + + // Remaining query items must be proven absent against the tree edge. + if (query_peek() != nullptr && !(current_limit.has_value() && *current_limit == 0)) { + if (!last_push_key_bearing.has_value() || !*last_push_key_bearing) { + error = "proof is missing data for query"; + return false; + } + } + + out.root_hash = root.TreeHash(); + out.limit_left = current_limit; + return true; +} + +bool VerifyProof(Span proof, const std::vector& items, + std::optional limit, bool left_to_right, const Hash256& expected_hash, + VerifyResult& out, std::string& error) +{ + if (!ExecuteProof(proof, items, limit, left_to_right, PROOF_VERSION_LATEST, out, error)) return false; + if (out.root_hash != expected_hash) { + error = strprintf("proof did not match expected hash: expected %s, got %s", + HexStr(expected_hash), HexStr(out.root_hash)); + return false; + } + return true; +} + +} // namespace platform::merk diff --git a/src/platform/proof/merk.h b/src/platform/proof/merk.h new file mode 100644 index 000000000000..c7a7e64d08eb --- /dev/null +++ b/src/platform/proof/merk.h @@ -0,0 +1,177 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_PROOF_MERK_H +#define BITCOIN_PLATFORM_PROOF_MERK_H + +#include + +#include +#include +#include +#include +#include + +//! Merk (Merkle AVL tree) proof verification, a pure C++ port of the "verify" +//! feature slice of dashpay/grovedb tag v5.0.0: +//! - merk/src/tree/hash.rs (blake3 node hashing) +//! - grovedb-query/src/proofs/mod.rs (Op / Node variants) +//! - grovedb-query/src/proofs/encoding.rs (op stream decoding) +//! - grovedb-query/src/proofs/tree_feature_type.rs +//! - merk/src/proofs/tree.rs (op replay stack machine) +//! - merk/src/proofs/query/verify.rs (query matching + absence proofs) +namespace platform::merk { + +using Hash256 = std::array; + +//! merk/src/tree/hash.rs NULL_HASH: 32 zero bytes. +inline constexpr Hash256 NULL_HASH{}; + +//! blake3(varint(len(value)) || value); varint is LEB128 +//! (merk/src/tree/hash.rs value_hash). +Hash256 ValueHash(Span value); +//! blake3(varint(len(key)) || key || value_hash(value)) +//! (merk/src/tree/hash.rs kv_hash). +Hash256 KvHash(Span key, Span value); +//! blake3(varint(len(key)) || key || value_hash) +//! (merk/src/tree/hash.rs kv_digest_to_kv_hash). +Hash256 KvDigestToKvHash(Span key, const Hash256& value_hash); +//! blake3(kv || left || right) (merk/src/tree/hash.rs node_hash). +Hash256 NodeHash(const Hash256& kv, const Hash256& left, const Hash256& right); +//! blake3(a || b) (merk/src/tree/hash.rs combine_hash). +Hash256 CombineHash(const Hash256& a, const Hash256& b); +//! blake3(kv || left || right || sum_be8) +//! (merk/src/tree/hash.rs node_hash_with_sum; only used by ProvableSumTree +//! proofs, which this verifier rejects, but kept for completeness). +Hash256 NodeHashWithSum(const Hash256& kv, const Hash256& left, const Hash256& right, int64_t sum); + +//! TreeFeatureType (grovedb-query/src/proofs/tree_feature_type.rs). +//! Only BasicMerkNode and SummedMerkNode are supported; the count / big-sum / +//! provable variants are rejected at decode time. +struct TreeFeature { + bool summed{false}; + int64_t sum{0}; +}; + +//! Node variants (grovedb-query/src/proofs/mod.rs enum Node). Only the +//! variants emitted for plain trees and (non-provable) sum trees are +//! supported; the count / MMR / dense / commitment / provable-aggregate op +//! families are rejected with explicit errors. +enum class NodeVariant : uint8_t { + HASH, + KV_HASH, + KV, + KV_VALUE_HASH, + KV_DIGEST, + KV_REF_VALUE_HASH, + KV_VALUE_HASH_FEATURE_TYPE, + KV_VALUE_HASH_FEATURE_TYPE_WITH_CHILD_HASH, +}; + +struct Node { + NodeVariant variant{NodeVariant::HASH}; + Bytes key; + Bytes value; + Hash256 hash{}; //!< HASH / KV_HASH payload + Hash256 value_hash{}; //!< KV_VALUE_HASH / KV_DIGEST / KV_REF_VALUE_HASH / feature type variants + TreeFeature feature{}; + Hash256 child_hash{}; //!< KV_VALUE_HASH_FEATURE_TYPE_WITH_CHILD_HASH only + + bool IsKeyBearing() const; +}; + +//! Proof operators (grovedb-query/src/proofs/mod.rs enum Op). +enum class OpType : uint8_t { + PUSH, + PUSH_INVERTED, + PARENT, + CHILD, + PARENT_INVERTED, + CHILD_INVERTED, +}; + +struct Op { + OpType type{OpType::PUSH}; + Node node; +}; + +//! Decodes one proof operator from the reader +//! (grovedb-query/src/proofs/encoding.rs impl Decode for Op). Key length is +//! a u8; value lengths are fixed-width big-endian u16 (small ops) or u32 +//! (large ops). Returns false with reader error set on failure. +bool DecodeOp(BytesReader& reader, Op& op); + +//! Query item shapes needed by the platform GUI +//! (grovedb-query/src/query_item/mod.rs enum QueryItem subset). +struct QueryItem { + enum class Type : uint8_t { + KEY, + RANGE, //!< start..end (end exclusive) + RANGE_INCLUSIVE, //!< start..=end + RANGE_FULL, + }; + Type type{Type::KEY}; + Bytes key; //!< KEY + Bytes start; //!< RANGE / RANGE_INCLUSIVE + Bytes end; //!< RANGE / RANGE_INCLUSIVE + + static QueryItem Key(Bytes key); + static QueryItem Range(Bytes start, Bytes end); + static QueryItem RangeInclusive(Bytes start, Bytes end); + static QueryItem RangeFull(); + + //! (bound, is_exclusive); nullopt bound means unbounded. + std::pair>, bool> LowerBound() const; + //! (bound, is_inclusive); nullopt bound means unbounded. + std::pair>, bool> UpperBound() const; + bool LowerUnbounded() const { return type == Type::RANGE_FULL; } + bool UpperUnbounded() const { return type == Type::RANGE_FULL; } + bool Contains(Span key) const; +}; + +//! One proven result entry (merk/src/proofs/query/verify.rs +//! ProvedKeyOptionalValue). +struct ProvedKeyValue { + Bytes key; + Bytes value; + Hash256 value_hash{}; + //! True only for KVValueHashFeatureTypeWithChildHash nodes where + //! combine_hash(H(value), child_hash) == value_hash was checked. + bool child_hash_verified{false}; +}; + +struct VerifyResult { + Hash256 root_hash{}; + std::vector result_set; + std::optional limit_left; +}; + +//! Strict proof version constant (merk/src/proofs/query/verify.rs +//! PROOF_VERSION_LATEST). Version 0 permits item elements in KVValueHash +//! nodes (legacy V0 envelopes); version 1 rejects them. +inline constexpr uint16_t PROOF_VERSION_LATEST{1}; + +//! Executes and verifies a merk proof against a query, mirroring +//! merk/src/proofs/query/verify.rs execute_proof. `items` must be sorted +//! ascending and non-overlapping (grovedb's Query::insert_item invariant); +//! this is validated and violations are errors. +//! +//! On success `out.root_hash` is the reconstructed merk root hash and +//! `out.result_set` contains exactly the entries that answer the query. +//! Absent keys are proven absent via boundary nodes; a proof that omits data +//! required by the query fails verification (security critical: a node must +//! not be able to silently drop matching results). +bool ExecuteProof(Span proof, const std::vector& items, + std::optional limit, bool left_to_right, uint16_t proof_version, + VerifyResult& out, std::string& error); + +//! ExecuteProof + comparison of the reconstructed root hash against +//! expected_hash (merk/src/proofs/query/verify.rs verify_proof). +bool VerifyProof(Span proof, const std::vector& items, + std::optional limit, bool left_to_right, const Hash256& expected_hash, + VerifyResult& out, std::string& error); + +} // namespace platform::merk + +#endif // BITCOIN_PLATFORM_PROOF_MERK_H diff --git a/src/platform/serialize.cpp b/src/platform/serialize.cpp new file mode 100644 index 000000000000..16d2d364b8c6 --- /dev/null +++ b/src/platform/serialize.cpp @@ -0,0 +1,172 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include + +namespace platform { + +void WriteLEB128(Bytes& out, uint64_t value) +{ + while (true) { + uint8_t byte = value & 0x7f; + value >>= 7; + if (value != 0) byte |= 0x80; + out.push_back(byte); + if (value == 0) return; + } +} + +bool BytesReader::SetError(const std::string& error) +{ + if (m_error.empty()) m_error = error; + return false; +} + +bool BytesReader::ReadBytes(size_t len, Bytes& out) +{ + if (HasError()) return false; + if (Remaining() < len) return SetError(strprintf("unexpected end of data: wanted %u bytes, have %u", len, Remaining())); + out.assign(m_data.begin() + m_pos, m_data.begin() + m_pos + len); + m_pos += len; + return true; +} + +bool BytesReader::ReadInto(Span out) +{ + if (HasError()) return false; + if (Remaining() < out.size()) return SetError(strprintf("unexpected end of data: wanted %u bytes, have %u", out.size(), Remaining())); + std::copy(m_data.begin() + m_pos, m_data.begin() + m_pos + out.size(), out.begin()); + m_pos += out.size(); + return true; +} + +bool BytesReader::ReadU8(uint8_t& out) +{ + if (HasError()) return false; + if (Remaining() < 1) return SetError("unexpected end of data reading u8"); + out = m_data[m_pos++]; + return true; +} + +bool BytesReader::ReadU16BE(uint16_t& out) +{ + if (HasError()) return false; + if (Remaining() < 2) return SetError("unexpected end of data reading u16"); + out = (uint16_t{m_data[m_pos]} << 8) | uint16_t{m_data[m_pos + 1]}; + m_pos += 2; + return true; +} + +bool BytesReader::ReadU32BE(uint32_t& out) +{ + if (HasError()) return false; + if (Remaining() < 4) return SetError("unexpected end of data reading u32"); + out = 0; + for (int i = 0; i < 4; ++i) { + out = (out << 8) | m_data[m_pos + i]; + } + m_pos += 4; + return true; +} + +bool BytesReader::ReadU64BE(uint64_t& out) +{ + if (HasError()) return false; + if (Remaining() < 8) return SetError("unexpected end of data reading u64"); + out = 0; + for (int i = 0; i < 8; ++i) { + out = (out << 8) | m_data[m_pos + i]; + } + m_pos += 8; + return true; +} + +bool BytesReader::ReadI64BE(int64_t& out) +{ + uint64_t value; + if (!ReadU64BE(value)) return false; + out = static_cast(value); + return true; +} + +bool BytesReader::ReadLEB128(uint64_t& out) +{ + if (HasError()) return false; + out = 0; + for (int shift = 0; shift < 64; shift += 7) { + uint8_t byte; + if (!ReadU8(byte)) return false; + if (shift == 63 && (byte & 0x7e) != 0) return SetError("LEB128 varint overflows 64 bits"); + out |= uint64_t{static_cast(byte & 0x7f)} << shift; + if ((byte & 0x80) == 0) return true; + } + return SetError("LEB128 varint too long"); +} + +bool BytesReader::ReadLEB128Signed(int64_t& out) +{ + uint64_t zigzag; + if (!ReadLEB128(zigzag)) return false; + out = static_cast((zigzag >> 1) ^ (~(zigzag & 1) + 1)); + return true; +} + +bool BytesReader::ReadBincodeVarint(uint64_t& out) +{ + if (HasError()) return false; + uint8_t first; + if (!ReadU8(first)) return false; + if (first < 251) { + out = first; + return true; + } + switch (first) { + case 251: { + uint16_t v; + if (!ReadU16BE(v)) return false; + out = v; + return true; + } + case 252: { + uint32_t v; + if (!ReadU32BE(v)) return false; + out = v; + return true; + } + case 253: + return ReadU64BE(out); + default: + return SetError(strprintf("unsupported bincode varint marker %u", first)); + } +} + +bool BytesReader::ReadBincodeVarintSigned(int64_t& out) +{ + uint64_t zigzag; + if (!ReadBincodeVarint(zigzag)) return false; + out = static_cast((zigzag >> 1) ^ (~(zigzag & 1) + 1)); + return true; +} + +bool BytesReader::ReadBincodeOptionTag(bool& has_value) +{ + uint8_t tag; + if (!ReadU8(tag)) return false; + if (tag > 1) return SetError(strprintf("invalid bincode Option tag %u", tag)); + has_value = tag == 1; + return true; +} + +bool BytesReader::ReadBincodeByteVec(Bytes& out, size_t max_len) +{ + uint64_t len; + if (!ReadBincodeVarint(len)) return false; + if (len > max_len) return SetError(strprintf("byte vector length %u exceeds limit %u", len, max_len)); + if (len > Remaining()) return SetError(strprintf("byte vector length %u exceeds remaining data %u", len, Remaining())); + return ReadBytes(static_cast(len), out); +} + +} // namespace platform diff --git a/src/platform/serialize.h b/src/platform/serialize.h new file mode 100644 index 000000000000..c8e2dffb52c5 --- /dev/null +++ b/src/platform/serialize.h @@ -0,0 +1,77 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_SERIALIZE_H +#define BITCOIN_PLATFORM_SERIALIZE_H + +#include + +#include +#include +#include + +namespace platform { + +using Bytes = std::vector; + +//! Appends an unsigned LEB128 varint (integer-encoding crate compatible) to +//! out. Used inside blake3 hash preimages, mirroring +//! grovedb merk/src/tree/hash.rs (integer_encoding::VarInt::encode_var). +void WriteLEB128(Bytes& out, uint64_t value); + +//! Safe forward-only cursor over a byte span. All Read* methods return false +//! on truncation or malformed input and record a human readable error; once +//! an error is set every further read fails. No exceptions are thrown. +class BytesReader +{ +public: + explicit BytesReader(Span data) : m_data(data) {} + + size_t Remaining() const { return m_data.size() - m_pos; } + bool IsEof() const { return m_pos == m_data.size(); } + bool HasError() const { return !m_error.empty(); } + const std::string& GetError() const { return m_error; } + //! Records an error (first error wins) and returns false. + bool SetError(const std::string& error); + + //! Reads len raw bytes into out (overwriting it). + bool ReadBytes(size_t len, Bytes& out); + //! Reads exactly out.size() bytes into a fixed-size buffer. + bool ReadInto(Span out); + bool ReadU8(uint8_t& out); + //! Fixed-width big-endian reads ("ed" crate style, used by merk proof ops). + bool ReadU16BE(uint16_t& out); + bool ReadU32BE(uint32_t& out); + bool ReadU64BE(uint64_t& out); + bool ReadI64BE(int64_t& out); + + //! Unsigned LEB128 varint (integer-encoding crate). Rejects encodings + //! longer than 10 bytes or overflowing 64 bits. + bool ReadLEB128(uint64_t& out); + //! Signed LEB128 varint with zigzag (integer-encoding crate signed ints). + bool ReadLEB128Signed(int64_t& out); + + //! bincode-v2 varint in the big-endian configuration + //! (bincode::config::standard().with_big_endian()): values < 251 are a + //! single byte; marker 251 is followed by a big-endian u16, 252 by a + //! big-endian u32, 253 by a big-endian u64 (marker 254/u128 rejected). + //! Used for collection lengths and enum discriminants in grovedb + //! envelopes and elements. + bool ReadBincodeVarint(uint64_t& out); + //! bincode-v2 signed varint: zigzag then ReadBincodeVarint. + bool ReadBincodeVarintSigned(int64_t& out); + //! bincode Option tag: one byte, 0 (nullopt) or 1 (value follows). + bool ReadBincodeOptionTag(bool& has_value); + //! bincode Vec: varint length + raw bytes. max_len guards allocation. + bool ReadBincodeByteVec(Bytes& out, size_t max_len); + +private: + Span m_data; + size_t m_pos{0}; + std::string m_error; +}; + +} // namespace platform + +#endif // BITCOIN_PLATFORM_SERIALIZE_H diff --git a/src/qt/platform/moc_platformpage.cpp b/src/qt/platform/moc_platformpage.cpp deleted file mode 100644 index 0e9e185e6815..000000000000 --- a/src/qt/platform/moc_platformpage.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/**************************************************************************** -** Meta object code from reading C++ file 'platformpage.h' -** -** Created by: The Qt Meta Object Compiler version 67 (Qt 5.15.19) -** -** WARNING! All changes made in this file will be lost! -*****************************************************************************/ - -#include -#include "qt/platform/platformpage.h" -#include -#include -#if !defined(Q_MOC_OUTPUT_REVISION) -#error "The header file 'platformpage.h' doesn't include ." -#elif Q_MOC_OUTPUT_REVISION != 67 -#error "This file was generated using the moc from 5.15.19. It" -#error "cannot be used with the include files from this version of Qt." -#error "(The moc has changed too much.)" -#endif - -QT_BEGIN_MOC_NAMESPACE -QT_WARNING_PUSH -QT_WARNING_DISABLE_DEPRECATED -struct qt_meta_stringdata_PlatformPage_t { - QByteArrayData data[1]; - char stringdata0[13]; -}; -#define QT_MOC_LITERAL(idx, ofs, len) \ - Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \ - qptrdiff(offsetof(qt_meta_stringdata_PlatformPage_t, stringdata0) + ofs \ - - idx * sizeof(QByteArrayData)) \ - ) -static const qt_meta_stringdata_PlatformPage_t qt_meta_stringdata_PlatformPage = { - { -QT_MOC_LITERAL(0, 0, 12) // "PlatformPage" - - }, - "PlatformPage" -}; -#undef QT_MOC_LITERAL - -static const uint qt_meta_data_PlatformPage[] = { - - // content: - 8, // revision - 0, // classname - 0, 0, // classinfo - 0, 0, // methods - 0, 0, // properties - 0, 0, // enums/sets - 0, 0, // constructors - 0, // flags - 0, // signalCount - - 0 // eod -}; - -void PlatformPage::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a) -{ - (void)_o; - (void)_id; - (void)_c; - (void)_a; -} - -QT_INIT_METAOBJECT const QMetaObject PlatformPage::staticMetaObject = { { - QMetaObject::SuperData::link(), - qt_meta_stringdata_PlatformPage.data, - qt_meta_data_PlatformPage, - qt_static_metacall, - nullptr, - nullptr -} }; - - -const QMetaObject *PlatformPage::metaObject() const -{ - return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject; -} - -void *PlatformPage::qt_metacast(const char *_clname) -{ - if (!_clname) return nullptr; - if (!strcmp(_clname, qt_meta_stringdata_PlatformPage.stringdata0)) - return static_cast(this); - return QWidget::qt_metacast(_clname); -} - -int PlatformPage::qt_metacall(QMetaObject::Call _c, int _id, void **_a) -{ - _id = QWidget::qt_metacall(_c, _id, _a); - return _id; -} -QT_WARNING_POP -QT_END_MOC_NAMESPACE diff --git a/src/test/data/platform/element_vectors.json b/src/test/data/platform/element_vectors.json new file mode 100644 index 000000000000..38c6faef6ad5 --- /dev/null +++ b/src/test/data/platform/element_vectors.json @@ -0,0 +1,104 @@ +{ + "vectors": [ + { + "name": "item_no_flags", + "description": "Item(hello)", + "serialized_hex": "000568656c6c6f00" + }, + { + "name": "item_empty_value", + "description": "Item()", + "serialized_hex": "000000" + }, + { + "name": "item_with_flags", + "description": "Item(0x010203, flags: [7, 8])", + "serialized_hex": "000301020301020708" + }, + { + "name": "item_large_value", + "description": "Item(0xabababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab)", + "serialized_hex": "00fb012cabababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab00" + }, + { + "name": "sum_item_positive", + "description": "SumItem(1000)", + "serialized_hex": "03fb07d000" + }, + { + "name": "sum_item_negative", + "description": "SumItem(-42)", + "serialized_hex": "035300" + }, + { + "name": "sum_item_with_flags", + "description": "SumItem(5, flags: [1])", + "serialized_hex": "030a010101" + }, + { + "name": "tree_empty_root_key", + "description": "Tree(None)", + "serialized_hex": "020000" + }, + { + "name": "tree_nonempty", + "description": "Tree(000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f)", + "serialized_hex": "020120000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f00" + }, + { + "name": "tree_with_flags", + "description": "Tree(None, flags: [5])", + "serialized_hex": "0200010105" + }, + { + "name": "sum_tree_empty", + "description": "SumTree(None, 0)", + "serialized_hex": "04000000" + }, + { + "name": "sum_tree_nonempty", + "description": "SumTree(000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f, 123)", + "serialized_hex": "040120000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1ff600" + }, + { + "name": "reference_absolute", + "description": "Reference(AbsolutePathReference(00/61626364(abcd)/05), max_hop: None)", + "serialized_hex": "0100030100046162636401050000" + }, + { + "name": "reference_absolute_hops_flags", + "description": "Reference(AbsolutePathReference(6974656d73(items)/6931(i1)), max_hop: 3, flags: [9])", + "serialized_hex": "010002056974656d730269310103010109" + }, + { + "name": "reference_upstream_root_height", + "description": "Reference(UpstreamRootHeightReference(1, 70(p)/71(q)), max_hop: None)", + "serialized_hex": "01010102017001710000" + }, + { + "name": "reference_upstream_root_height_parent_addition", + "description": "Reference(UpstreamRootHeightWithParentPathAdditionReference(2, 78(x)/79(y)), max_hop: None)", + "serialized_hex": "01020202017801790000" + }, + { + "name": "reference_upstream_from_element_height", + "description": "Reference(UpstreamFromElementHeightReference(1, 70(p)/71(q)), max_hop: None)", + "serialized_hex": "01030102017001710000" + }, + { + "name": "reference_cousin", + "description": "Reference(CousinReference(63), max_hop: None)", + "serialized_hex": "010401630000" + }, + { + "name": "reference_removed_cousin", + "description": "Reference(RemovedCousinReference(6d(m)/6e(n)), max_hop: None)", + "serialized_hex": "010502016d016e0000" + }, + { + "name": "reference_sibling", + "description": "Reference(SiblingReference(73), max_hop: None)", + "serialized_hex": "010601730000" + } + ] +} diff --git a/src/test/data/platform/grovedb_proof_vectors.json b/src/test/data/platform/grovedb_proof_vectors.json new file mode 100644 index 000000000000..466aa60eaa2b --- /dev/null +++ b/src/test/data/platform/grovedb_proof_vectors.json @@ -0,0 +1,426 @@ +{ + "vectors": [ + { + "name": "two_level_items_v1", + "path_query": { + "path": [ + "6974656d73" + ], + "query": { + "items": [ + { + "type": "key", + "key": "6931" + }, + { + "type": "key", + "key": "6933" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + }, + "limit": null + }, + "proof_hex": "010073017c9cea6b09368598059458fca2282022c905e48221ed492ba46b75c42c9b942a02053f9d933087d993b66dacebc15afc605f319fa38c14028b5e25a93351d24cf91004056974656d730006020102693200319f2ca4aadc32872b17db9b9eb646e3854b45277671335cfb2efdbcef9d06e91101056974656d73009003026931000c000976616c7565206f6e650002709c6b09fd73e12f3fc564de16e2b5b8a38f01faed59c65701ac778e3cd714a610030269330011000b76616c756520746872656501020d0e02aa22b6fb3b5b8d48903b28ec935a4fce1aab8f90078404ac56e4bd216b0d9ff110019d32337bff3f5260b6af28b6de573c5cc903ce38fe4fe7ab2629eb531a36d175111100", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [ + { + "path": [ + "6974656d73" + ], + "key_hex": "6931", + "element_hex": "000976616c7565206f6e6500" + }, + { + "path": [ + "6974656d73" + ], + "key_hex": "6933", + "element_hex": "000b76616c756520746872656501020d0e" + } + ] + }, + { + "name": "two_level_items_v0", + "path_query": { + "path": [ + "6974656d73" + ], + "query": { + "items": [ + { + "type": "key", + "key": "6931" + }, + { + "type": "key", + "key": "6933" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + }, + "limit": null + }, + "proof_hex": "0073017c9cea6b09368598059458fca2282022c905e48221ed492ba46b75c42c9b942a02053f9d933087d993b66dacebc15afc605f319fa38c14028b5e25a93351d24cf91004056974656d730006020102693200319f2ca4aadc32872b17db9b9eb646e3854b45277671335cfb2efdbcef9d06e91101056974656d739003026931000c000976616c7565206f6e650002709c6b09fd73e12f3fc564de16e2b5b8a38f01faed59c65701ac778e3cd714a610030269330011000b76616c756520746872656501020d0e02aa22b6fb3b5b8d48903b28ec935a4fce1aab8f90078404ac56e4bd216b0d9ff110019d32337bff3f5260b6af28b6de573c5cc903ce38fe4fe7ab2629eb531a36d17511110001", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [ + { + "path": [ + "6974656d73" + ], + "key_hex": "6931", + "element_hex": "000976616c7565206f6e6500" + }, + { + "path": [ + "6974656d73" + ], + "key_hex": "6933", + "element_hex": "000b76616c756520746872656501020d0e" + } + ] + }, + { + "name": "absence_v1", + "path_query": { + "path": [ + "6974656d73" + ], + "query": { + "items": [ + { + "type": "key", + "key": "6939" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + }, + "limit": null + }, + "proof_hex": "010073017c9cea6b09368598059458fca2282022c905e48221ed492ba46b75c42c9b942a02053f9d933087d993b66dacebc15afc605f319fa38c14028b5e25a93351d24cf91004056974656d730006020102693200319f2ca4aadc32872b17db9b9eb646e3854b45277671335cfb2efdbcef9d06e91101056974656d7300ac01a973d24c105b6feda08a6cb2489a7de0ff90a659833ce08f975480c53b9a227d02709c6b09fd73e12f3fc564de16e2b5b8a38f01faed59c65701ac778e3cd714a610019cc36ded1853ebbc8f89e028c7d8767cdb6019d3a1e741bea748cb885e7e4f4a02aa22b6fb3b5b8d48903b28ec935a4fce1aab8f90078404ac56e4bd216b0d9ff11005026935216216a5a6cdbeae18233885660b80c2192cc4b4520090d18efd1a2ef91a762c111100", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [] + }, + { + "name": "absence_between_keys_v1", + "path_query": { + "path": [ + "6974656d73" + ], + "query": { + "items": [ + { + "type": "key", + "key": "6932" + }, + { + "type": "key", + "key": "693235" + }, + { + "type": "key", + "key": "6933" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + }, + "limit": null + }, + "proof_hex": "010073017c9cea6b09368598059458fca2282022c905e48221ed492ba46b75c42c9b942a02053f9d933087d993b66dacebc15afc605f319fa38c14028b5e25a93351d24cf91004056974656d730006020102693200319f2ca4aadc32872b17db9b9eb646e3854b45277671335cfb2efdbcef9d06e91101056974656d73009001a973d24c105b6feda08a6cb2489a7de0ff90a659833ce08f975480c53b9a227d03026932000c000976616c75652074776f0010030269330011000b76616c756520746872656501020d0e02aa22b6fb3b5b8d48903b28ec935a4fce1aab8f90078404ac56e4bd216b0d9ff110019d32337bff3f5260b6af28b6de573c5cc903ce38fe4fe7ab2629eb531a36d175111100", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [ + { + "path": [ + "6974656d73" + ], + "key_hex": "6932", + "element_hex": "000976616c75652074776f00" + }, + { + "path": [ + "6974656d73" + ], + "key_hex": "6933", + "element_hex": "000b76616c756520746872656501020d0e" + } + ] + }, + { + "name": "reference_absolute_v1", + "path_query": { + "path": [ + "696e646578" + ], + "query": { + "items": [ + { + "type": "key", + "key": "7231" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + }, + "limit": null + }, + "proof_hex": "010073017c9cea6b09368598059458fca2282022c905e48221ed492ba46b75c42c9b942a0405696e646578000602010272310096e6250fa24e3b8a444687463d20f636b60f1cd37998f5fd93f4019657b40fe81001b8cd7d96ee6ef1856993d233edeb311527d162a692122c89867484e5ff4ad3c0110105696e646578005406027231000c000976616c7565206f6e6500d77c3741e58ffe033eca0854849704de7a3a6e8d4c6496343343f6bae3b24c770163681f49f0bf0cfd08b8e5a83bc954906a251e0dfb7eacb7768ec16f0b84fe2f1100", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [ + { + "path": [ + "696e646578" + ], + "key_hex": "7231", + "element_hex": "000976616c7565206f6e6500" + } + ] + }, + { + "name": "reference_upstream_v1", + "path_query": { + "path": [ + "696e646578" + ], + "query": { + "items": [ + { + "type": "key", + "key": "7232" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + }, + "limit": null + }, + "proof_hex": "010073017c9cea6b09368598059458fca2282022c905e48221ed492ba46b75c42c9b942a0405696e646578000602010272310096e6250fa24e3b8a444687463d20f636b60f1cd37998f5fd93f4019657b40fe81001b8cd7d96ee6ef1856993d233edeb311527d162a692122c89867484e5ff4ad3c0110105696e646578005402b9b5452ac0347efe5048b009f77ae88208ed9565d22fc27cdb83fb16d8acea1606027232000c000976616c75652074776f00499c79a0a8d45d4632bc8ea16963a1d638000f5b32586cfa29a29a682d84c0f61100", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [ + { + "path": [ + "696e646578" + ], + "key_hex": "7232", + "element_hex": "000976616c75652074776f00" + } + ] + }, + { + "name": "sum_tree_v1", + "path_query": { + "path": [ + "62616c616e636573" + ], + "query": { + "items": [ + { + "type": "range_full" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + }, + "limit": null + }, + "proof_hex": "0100be040862616c616e636573000a040103626f62fb0630006077d7151f0e9aa688856e7f8ccf40860293aa2f6f0366892382478c38d18f2d0295f4e0ba6c1dd1958e7953a09b049711de9e10b7e7ae0de50532de46e17f833f1001026cf5261c59a0142c12b68ea73e9232ba69df7d815969420ca22c193c4a760d1102053f9d933087d993b66dacebc15afc605f319fa38c14028b5e25a93351d24cf91001b8cd7d96ee6ef1856993d233edeb311527d162a692122c89867484e5ff4ad3c011010862616c616e63657300280305616c696365000503fb07d0000303626f62000503fb01f3001003056361726f6c00030354001100", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [ + { + "path": [ + "62616c616e636573" + ], + "key_hex": "616c696365", + "element_hex": "03fb07d000" + }, + { + "path": [ + "62616c616e636573" + ], + "key_hex": "626f62", + "element_hex": "03fb01f300" + }, + { + "path": [ + "62616c616e636573" + ], + "key_hex": "6361726f6c", + "element_hex": "035400" + } + ] + }, + { + "name": "three_level_path_v1", + "path_query": { + "path": [ + "646f6373", + "7573657231" + ], + "query": { + "items": [ + { + "type": "key", + "key": "6431" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + }, + "limit": null + }, + "proof_hex": "0100b90143bee683a1ef20a63dcac14981335ca47f6a323cd3243d33d53e4861529c1ba90404646f63730009020105757365723100ddb89ecdade4897f8bdecbf8a2d984747c81be35a977e23ef1d48c7fa408ea411001026cf5261c59a0142c12b68ea73e9232ba69df7d815969420ca22c193c4a760d1102053f9d933087d993b66dacebc15afc605f319fa38c14028b5e25a93351d24cf91001b8cd7d96ee6ef1856993d233edeb311527d162a692122c89867484e5ff4ad3c0110104646f63730051040575736572310006020102643100b2de5431f42011a4b0ad2f61d4cb8b350ad0b766f0803bb4e37610132c0da67c011db3ac01d3e97327e53c1293235f0ffb8a75906d2ab87a40bbcb95aff66ab2f11101057573657231003203026431000a0007646f63206f6e650001e5bce65bdf527aaf8094ce5d747203deda06ca9515cee46d47696a205574d6271100", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [ + { + "path": [ + "646f6373", + "7573657231" + ], + "key_hex": "6431", + "element_hex": "0007646f63206f6e6500" + } + ] + }, + { + "name": "three_level_subquery_v1", + "path_query": { + "path": [ + "646f6373" + ], + "query": { + "items": [ + { + "type": "key", + "key": "7573657231" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": { + "items": [ + { + "type": "range_full" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + } + }, + "limit": null + }, + "proof_hex": "0100b90143bee683a1ef20a63dcac14981335ca47f6a323cd3243d33d53e4861529c1ba90404646f63730009020105757365723100ddb89ecdade4897f8bdecbf8a2d984747c81be35a977e23ef1d48c7fa408ea411001026cf5261c59a0142c12b68ea73e9232ba69df7d815969420ca22c193c4a760d1102053f9d933087d993b66dacebc15afc605f319fa38c14028b5e25a93351d24cf91001b8cd7d96ee6ef1856993d233edeb311527d162a692122c89867484e5ff4ad3c0110104646f63730051040575736572310006020102643100b2de5431f42011a4b0ad2f61d4cb8b350ad0b766f0803bb4e37610132c0da67c011db3ac01d3e97327e53c1293235f0ffb8a75906d2ab87a40bbcb95aff66ab2f11101057573657231002103026431000a0007646f63206f6e650003026432000a0007646f632074776f001100", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [ + { + "path": [ + "646f6373", + "7573657231" + ], + "key_hex": "6431", + "element_hex": "0007646f63206f6e6500" + }, + { + "path": [ + "646f6373", + "7573657231" + ], + "key_hex": "6432", + "element_hex": "0007646f632074776f00" + } + ] + }, + { + "name": "root_empty_tree_v1", + "path_query": { + "path": [], + "query": { + "items": [ + { + "type": "key", + "key": "656d707479" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + }, + "limit": null + }, + "proof_hex": "0100b40143bee683a1ef20a63dcac14981335ca47f6a323cd3243d33d53e4861529c1ba90295f4e0ba6c1dd1958e7953a09b049711de9e10b7e7ae0de50532de46e17f833f100405656d7074790003020000651929e1747381a16157515e5447625502f3a79843859a0a929d24c605c0b23a1102053f9d933087d993b66dacebc15afc605f319fa38c14028b5e25a93351d24cf91001b8cd7d96ee6ef1856993d233edeb311527d162a692122c89867484e5ff4ad3c01100", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [ + { + "path": [], + "key_hex": "656d707479", + "element_hex": "020000" + } + ] + }, + { + "name": "root_tree_no_subquery_v1", + "path_query": { + "path": [], + "query": { + "items": [ + { + "type": "key", + "key": "6974656d73" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + }, + "limit": null + }, + "proof_hex": "010094017c9cea6b09368598059458fca2282022c905e48221ed492ba46b75c42c9b942a02053f9d933087d993b66dacebc15afc605f319fa38c14028b5e25a93351d24cf9101c056974656d730006020102693200319f2ca4aadc32872b17db9b9eb646e3854b45277671335cfb2efdbcef9d06e900a63c56e5cd38e8ca2a02eac214bf03d6d75d3a1233d6cc8c15b9bb68600a4cdf1100", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [ + { + "path": [], + "key_hex": "6974656d73", + "element_hex": "020102693200" + } + ] + }, + { + "name": "root_tree_no_subquery_v0", + "path_query": { + "path": [], + "query": { + "items": [ + { + "type": "key", + "key": "6974656d73" + } + ], + "left_to_right": true, + "default_subquery_path": null, + "default_subquery": null + }, + "limit": null + }, + "proof_hex": "0073017c9cea6b09368598059458fca2282022c905e48221ed492ba46b75c42c9b942a02053f9d933087d993b66dacebc15afc605f319fa38c14028b5e25a93351d24cf91004056974656d730006020102693200319f2ca4aadc32872b17db9b9eb646e3854b45277671335cfb2efdbcef9d06e9110001", + "expected_root_hash_hex": "7fe907293e808349296e0c4dd4c6a9c4159022911d1c98a15f013aefbec70b51", + "expected_results": [ + { + "path": [], + "key_hex": "6974656d73", + "element_hex": "020102693200" + } + ] + } + ] +} diff --git a/src/test/data/platform/merk_proof_vectors.json b/src/test/data/platform/merk_proof_vectors.json new file mode 100644 index 000000000000..eb3cb311504a --- /dev/null +++ b/src/test/data/platform/merk_proof_vectors.json @@ -0,0 +1,253 @@ +{ + "vectors": [ + { + "name": "items_single_key_present", + "query": { + "items": [ + { + "type": "key", + "key": "6932" + } + ], + "limit": null, + "left_to_right": true + }, + "proof_version": 1, + "proof_hex": "01a973d24c105b6feda08a6cb2489a7de0ff90a659833ce08f975480c53b9a227d03026932000c000976616c75652074776f00100108f0444967d67daa6549f7abeafb62f6496be4c342d6d6ab45d77e366d5070e011", + "expected_root_hash_hex": "a63c56e5cd38e8ca2a02eac214bf03d6d75d3a1233d6cc8c15b9bb68600a4cdf", + "expected_results": [ + { + "key_hex": "6932", + "value_hex": "000976616c75652074776f00" + } + ] + }, + { + "name": "items_single_key_absent", + "query": { + "items": [ + { + "type": "key", + "key": "6939" + } + ], + "limit": null, + "left_to_right": true + }, + "proof_version": 1, + "proof_hex": "01a973d24c105b6feda08a6cb2489a7de0ff90a659833ce08f975480c53b9a227d02709c6b09fd73e12f3fc564de16e2b5b8a38f01faed59c65701ac778e3cd714a610019cc36ded1853ebbc8f89e028c7d8767cdb6019d3a1e741bea748cb885e7e4f4a02aa22b6fb3b5b8d48903b28ec935a4fce1aab8f90078404ac56e4bd216b0d9ff11005026935216216a5a6cdbeae18233885660b80c2192cc4b4520090d18efd1a2ef91a762c1111", + "expected_root_hash_hex": "a63c56e5cd38e8ca2a02eac214bf03d6d75d3a1233d6cc8c15b9bb68600a4cdf", + "expected_results": [] + }, + { + "name": "items_multiple_keys", + "query": { + "items": [ + { + "type": "key", + "key": "6931" + }, + { + "type": "key", + "key": "6933" + }, + { + "type": "key", + "key": "6935" + } + ], + "limit": null, + "left_to_right": true + }, + "proof_version": 1, + "proof_hex": "03026931000c000976616c7565206f6e650002709c6b09fd73e12f3fc564de16e2b5b8a38f01faed59c65701ac778e3cd714a610030269330011000b76616c756520746872656501020d0e02aa22b6fb3b5b8d48903b28ec935a4fce1aab8f90078404ac56e4bd216b0d9ff11003026935000d000a76616c75652066697665001111", + "expected_root_hash_hex": "a63c56e5cd38e8ca2a02eac214bf03d6d75d3a1233d6cc8c15b9bb68600a4cdf", + "expected_results": [ + { + "key_hex": "6931", + "value_hex": "000976616c7565206f6e6500" + }, + { + "key_hex": "6933", + "value_hex": "000b76616c756520746872656501020d0e" + }, + { + "key_hex": "6935", + "value_hex": "000a76616c7565206669766500" + } + ] + }, + { + "name": "items_range", + "query": { + "items": [ + { + "type": "range", + "start": "6932", + "end": "6934" + } + ], + "limit": null, + "left_to_right": true + }, + "proof_version": 1, + "proof_hex": "01a973d24c105b6feda08a6cb2489a7de0ff90a659833ce08f975480c53b9a227d03026932000c000976616c75652074776f0010030269330011000b76616c756520746872656501020d0e05026934f5135e4d860941360b38d15ed5983a078c2976649398ea52439352d726ea911a10019d32337bff3f5260b6af28b6de573c5cc903ce38fe4fe7ab2629eb531a36d1751111", + "expected_root_hash_hex": "a63c56e5cd38e8ca2a02eac214bf03d6d75d3a1233d6cc8c15b9bb68600a4cdf", + "expected_results": [ + { + "key_hex": "6932", + "value_hex": "000976616c75652074776f00" + }, + { + "key_hex": "6933", + "value_hex": "000b76616c756520746872656501020d0e" + } + ] + }, + { + "name": "items_range_inclusive", + "query": { + "items": [ + { + "type": "range_inclusive", + "start": "6932", + "end": "6934" + } + ], + "limit": null, + "left_to_right": true + }, + "proof_version": 1, + "proof_hex": "01a973d24c105b6feda08a6cb2489a7de0ff90a659833ce08f975480c53b9a227d03026932000c000976616c75652074776f0010030269330011000b76616c756520746872656501020d0e03026934000d000a76616c756520666f75720010019d32337bff3f5260b6af28b6de573c5cc903ce38fe4fe7ab2629eb531a36d1751111", + "expected_root_hash_hex": "a63c56e5cd38e8ca2a02eac214bf03d6d75d3a1233d6cc8c15b9bb68600a4cdf", + "expected_results": [ + { + "key_hex": "6932", + "value_hex": "000976616c75652074776f00" + }, + { + "key_hex": "6933", + "value_hex": "000b76616c756520746872656501020d0e" + }, + { + "key_hex": "6934", + "value_hex": "000a76616c756520666f757200" + } + ] + }, + { + "name": "items_range_full_limit_2", + "query": { + "items": [ + { + "type": "range_full" + } + ], + "limit": 2, + "left_to_right": true + }, + "proof_version": 1, + "proof_hex": "03026931000c000976616c7565206f6e650003026932000c000976616c75652074776f00100108f0444967d67daa6549f7abeafb62f6496be4c342d6d6ab45d77e366d5070e011", + "expected_root_hash_hex": "a63c56e5cd38e8ca2a02eac214bf03d6d75d3a1233d6cc8c15b9bb68600a4cdf", + "expected_results": [ + { + "key_hex": "6931", + "value_hex": "000976616c7565206f6e6500" + }, + { + "key_hex": "6932", + "value_hex": "000976616c75652074776f00" + } + ] + }, + { + "name": "sum_tree_keys", + "query": { + "items": [ + { + "type": "key", + "key": "616c696365" + }, + { + "type": "key", + "key": "626f62" + } + ], + "limit": null, + "left_to_right": true + }, + "proof_version": 1, + "proof_hex": "0305616c696365000503fb07d0000303626f62000503fb01f3001001297d18b922322ae40043c408e7bd6687fa8d8de75ecb09dcd89e1f6d1b2495e811", + "expected_root_hash_hex": "e8c4f4a7d3d009e37a20bddaa9ed54bd03fbc30fde7d7cb4b0f4d3e232dfeb30", + "expected_results": [ + { + "key_hex": "616c696365", + "value_hex": "03fb07d000" + }, + { + "key_hex": "626f62", + "value_hex": "03fb01f300" + } + ] + }, + { + "name": "sum_tree_range_full", + "query": { + "items": [ + { + "type": "range_full" + } + ], + "limit": null, + "left_to_right": true + }, + "proof_version": 1, + "proof_hex": "0305616c696365000503fb07d0000303626f62000503fb01f3001003056361726f6c000303540011", + "expected_root_hash_hex": "e8c4f4a7d3d009e37a20bddaa9ed54bd03fbc30fde7d7cb4b0f4d3e232dfeb30", + "expected_results": [ + { + "key_hex": "616c696365", + "value_hex": "03fb07d000" + }, + { + "key_hex": "626f62", + "value_hex": "03fb01f300" + }, + { + "key_hex": "6361726f6c", + "value_hex": "035400" + } + ] + }, + { + "name": "items_multiple_keys_v0", + "query": { + "items": [ + { + "type": "key", + "key": "6931" + }, + { + "type": "key", + "key": "6933" + } + ], + "limit": null, + "left_to_right": true + }, + "proof_version": 0, + "proof_hex": "03026931000c000976616c7565206f6e650002709c6b09fd73e12f3fc564de16e2b5b8a38f01faed59c65701ac778e3cd714a610030269330011000b76616c756520746872656501020d0e02aa22b6fb3b5b8d48903b28ec935a4fce1aab8f90078404ac56e4bd216b0d9ff110019d32337bff3f5260b6af28b6de573c5cc903ce38fe4fe7ab2629eb531a36d1751111", + "expected_root_hash_hex": "a63c56e5cd38e8ca2a02eac214bf03d6d75d3a1233d6cc8c15b9bb68600a4cdf", + "expected_results": [ + { + "key_hex": "6931", + "value_hex": "000976616c7565206f6e6500" + }, + { + "key_hex": "6933", + "value_hex": "000b76616c756520746872656501020d0e" + } + ] + } + ] +} diff --git a/src/test/platform_proof_tests.cpp b/src/test/platform_proof_tests.cpp new file mode 100644 index 000000000000..78f0b6c03f93 --- /dev/null +++ b/src/test/platform_proof_tests.cpp @@ -0,0 +1,580 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! Tests for the pure C++ GroveDB proof verifier (src/platform/proof/) +//! against JSON vectors generated from the Rust dashpay/grovedb v5.0.0 +//! implementation by contrib/devtools/platform-test-vectors. + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include + +using platform::Bytes; +using platform::grove::Element; +using platform::grove::GroveQuery; +using platform::grove::PathQuery; +using platform::merk::Hash256; +using platform::merk::QueryItem; + +BOOST_FIXTURE_TEST_SUITE(platform_proof_tests, BasicTestingSetup) + +namespace { + +UniValue ReadJsonObject(const unsigned char* data, size_t size) +{ + UniValue v; + BOOST_REQUIRE(v.read(std::string(data, data + size))); + BOOST_REQUIRE(v.isObject()); + return v; +} + +Bytes FromHex(const std::string& hex) +{ + const auto parsed{TryParseHex(hex)}; + BOOST_REQUIRE_MESSAGE(parsed.has_value(), "invalid hex in test vector: " + hex); + return *parsed; +} + +Hash256 HashFromHex(const std::string& hex) +{ + const Bytes bytes{FromHex(hex)}; + BOOST_REQUIRE_EQUAL(bytes.size(), 32U); + Hash256 out; + std::copy(bytes.begin(), bytes.end(), out.begin()); + return out; +} + +QueryItem ParseQueryItem(const UniValue& json) +{ + const std::string type{json["type"].get_str()}; + if (type == "key") return QueryItem::Key(FromHex(json["key"].get_str())); + if (type == "range") return QueryItem::Range(FromHex(json["start"].get_str()), FromHex(json["end"].get_str())); + if (type == "range_inclusive") { + return QueryItem::RangeInclusive(FromHex(json["start"].get_str()), FromHex(json["end"].get_str())); + } + if (type == "range_full") return QueryItem::RangeFull(); + BOOST_REQUIRE_MESSAGE(false, "unknown query item type " + type); + return QueryItem::RangeFull(); +} + +std::vector ParseQueryItems(const UniValue& json) +{ + std::vector items; + for (size_t i = 0; i < json.size(); ++i) { + items.push_back(ParseQueryItem(json[i])); + } + return items; +} + +GroveQuery ParseGroveQuery(const UniValue& json) +{ + GroveQuery query; + query.items = ParseQueryItems(json["items"]); + query.left_to_right = json["left_to_right"].get_bool(); + const UniValue& subquery_path{json["default_subquery_path"]}; + if (!subquery_path.isNull()) { + std::vector segments; + for (size_t i = 0; i < subquery_path.size(); ++i) { + segments.push_back(FromHex(subquery_path[i].get_str())); + } + query.default_subquery_branch.subquery_path = std::move(segments); + } + const UniValue& subquery{json["default_subquery"]}; + if (!subquery.isNull()) { + query.default_subquery_branch.subquery = std::make_shared(ParseGroveQuery(subquery)); + } + return query; +} + +PathQuery ParsePathQuery(const UniValue& json) +{ + PathQuery query; + const UniValue& path{json["path"]}; + for (size_t i = 0; i < path.size(); ++i) { + query.path.push_back(FromHex(path[i].get_str())); + } + query.query = ParseGroveQuery(json["query"]); + const UniValue& limit{json["limit"]}; + if (!limit.isNull()) query.limit = static_cast(limit.getInt()); + return query; +} + +//! Outcome of verifying a (possibly corrupted) proof against a vector's +//! query and expected output. +enum class VectorOutcome { + //! Verification returned an error, or the reconstructed root hash does + //! not match the trusted one (a verifier bound to a trusted root hash + //! rejects either way). + DETECTED, + //! Verification succeeded and root hash + results match the vector. + MATCHES, + //! Verification succeeded with the expected root hash but different + //! results — for a corrupted proof this would be a forgery. + DIFFERS, +}; + +//! Runs a merk vector and classifies the outcome. +VectorOutcome RunMerkVector(const UniValue& vec, const Bytes& proof, std::string& error) +{ + const std::vector items{ParseQueryItems(vec["query"]["items"])}; + std::optional limit; + if (!vec["query"]["limit"].isNull()) limit = static_cast(vec["query"]["limit"].getInt()); + const bool left_to_right{vec["query"]["left_to_right"].get_bool()}; + const auto proof_version{static_cast(vec["proof_version"].getInt())}; + + platform::merk::VerifyResult result; + if (!platform::merk::ExecuteProof(proof, items, limit, left_to_right, proof_version, result, error)) { + return VectorOutcome::DETECTED; + } + if (result.root_hash != HashFromHex(vec["expected_root_hash_hex"].get_str())) { + error = "root hash mismatch"; + return VectorOutcome::DETECTED; + } + const UniValue& expected{vec["expected_results"]}; + if (result.result_set.size() != expected.size()) { + error = "result count mismatch"; + return VectorOutcome::DIFFERS; + } + for (size_t i = 0; i < expected.size(); ++i) { + if (result.result_set[i].key != FromHex(expected[i]["key_hex"].get_str()) || + result.result_set[i].value != FromHex(expected[i]["value_hex"].get_str())) { + error = "result entry mismatch"; + return VectorOutcome::DIFFERS; + } + } + return VectorOutcome::MATCHES; +} + +//! Runs a grovedb vector and classifies the outcome. +VectorOutcome RunGroveVector(const UniValue& vec, const Bytes& proof, std::string& error) +{ + const PathQuery query{ParsePathQuery(vec["path_query"])}; + // Options matching Rust GroveDb::verify_query_raw, which produced the + // expected results. + const platform::grove::VerifyOptions options{ + .verify_proof_succinctness = false, + .include_empty_trees_in_result = true, + }; + platform::grove::GroveVerifyResult result; + if (!platform::grove::VerifyQuery(proof, query, options, result, error)) return VectorOutcome::DETECTED; + if (result.root_hash != HashFromHex(vec["expected_root_hash_hex"].get_str())) { + error = "root hash mismatch"; + return VectorOutcome::DETECTED; + } + const UniValue& expected{vec["expected_results"]}; + if (result.results.size() != expected.size()) { + error = "result count mismatch"; + return VectorOutcome::DIFFERS; + } + for (size_t i = 0; i < expected.size(); ++i) { + const UniValue& entry{expected[i]}; + std::vector expected_path; + for (size_t j = 0; j < entry["path"].size(); ++j) { + expected_path.push_back(FromHex(entry["path"][j].get_str())); + } + if (result.results[i].path != expected_path || + result.results[i].key != FromHex(entry["key_hex"].get_str()) || + result.results[i].value != FromHex(entry["element_hex"].get_str())) { + error = "result entry mismatch"; + return VectorOutcome::DIFFERS; + } + } + return VectorOutcome::MATCHES; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(element_vectors) +{ + const UniValue doc{ReadJsonObject(json_tests::element_vectors, sizeof(json_tests::element_vectors))}; + const UniValue& vectors{doc["vectors"]}; + BOOST_REQUIRE(vectors.size() > 0); + for (size_t i = 0; i < vectors.size(); ++i) { + const UniValue& vec{vectors[i]}; + const std::string name{vec["name"].get_str()}; + const Bytes serialized{FromHex(vec["serialized_hex"].get_str())}; + + Element element; + std::string error; + BOOST_CHECK_MESSAGE(platform::grove::DecodeElement(serialized, element, error), + name + ": " + error); + + // Spot checks on semantics for known vectors. + if (name == "item_no_flags") { + BOOST_CHECK(element.type == Element::Type::ITEM); + BOOST_CHECK(element.item_value == Bytes({'h', 'e', 'l', 'l', 'o'})); + BOOST_CHECK(!element.flags.has_value()); + } else if (name == "item_with_flags") { + BOOST_CHECK(element.type == Element::Type::ITEM); + BOOST_CHECK(element.item_value == Bytes({1, 2, 3})); + BOOST_CHECK((element.flags == std::optional{{7, 8}})); + } else if (name == "item_large_value") { + BOOST_CHECK(element.type == Element::Type::ITEM); + BOOST_CHECK_EQUAL(element.item_value.size(), 300U); + } else if (name == "sum_item_positive") { + BOOST_CHECK(element.type == Element::Type::SUM_ITEM); + BOOST_CHECK_EQUAL(element.sum_value, 1000); + } else if (name == "sum_item_negative") { + BOOST_CHECK(element.type == Element::Type::SUM_ITEM); + BOOST_CHECK_EQUAL(element.sum_value, -42); + } else if (name == "tree_empty_root_key") { + BOOST_CHECK(element.type == Element::Type::TREE); + BOOST_CHECK(!element.root_key.has_value()); + } else if (name == "tree_nonempty") { + BOOST_CHECK(element.type == Element::Type::TREE); + BOOST_REQUIRE(element.root_key.has_value()); + BOOST_CHECK_EQUAL(element.root_key->size(), 32U); + } else if (name == "sum_tree_nonempty") { + BOOST_CHECK(element.type == Element::Type::SUM_TREE); + BOOST_CHECK(element.root_key.has_value()); + BOOST_CHECK_EQUAL(element.sum_value, 123); + } else if (name == "reference_absolute") { + BOOST_CHECK(element.type == Element::Type::REFERENCE); + BOOST_CHECK(element.reference.type == platform::grove::ReferencePath::Type::ABSOLUTE_PATH); + BOOST_CHECK_EQUAL(element.reference.path.size(), 3U); + } else if (name == "reference_absolute_hops_flags") { + BOOST_CHECK(element.type == Element::Type::REFERENCE); + BOOST_CHECK(element.max_hops == std::optional{3}); + BOOST_CHECK(element.flags == std::optional{{9}}); + } else if (name == "reference_upstream_root_height") { + BOOST_CHECK(element.reference.type == + platform::grove::ReferencePath::Type::UPSTREAM_ROOT_HEIGHT); + BOOST_CHECK_EQUAL(element.reference.height, 1); + } else if (name == "reference_sibling") { + BOOST_CHECK(element.reference.type == platform::grove::ReferencePath::Type::SIBLING); + BOOST_REQUIRE_EQUAL(element.reference.path.size(), 1U); + BOOST_CHECK(element.reference.path[0] == Bytes({'s'})); + } + + // Truncation must be rejected. + if (serialized.size() > 1) { + const Bytes truncated(serialized.begin(), serialized.end() - 1); + Element ignored; + BOOST_CHECK_MESSAGE(!platform::grove::DecodeElement(truncated, ignored, error), + name + ": truncated bytes must not decode"); + } + // Trailing garbage must be rejected. + Bytes extended{serialized}; + extended.push_back(0xff); + Element ignored; + BOOST_CHECK_MESSAGE(!platform::grove::DecodeElement(extended, ignored, error), + name + ": trailing bytes must not decode"); + } + + // Unknown / unsupported discriminants must fail with clear errors. + for (const uint8_t discriminant : {uint8_t{5}, uint8_t{11}, uint8_t{15}, uint8_t{21}, uint8_t{255}}) { + Element ignored; + std::string error; + BOOST_CHECK(!platform::grove::DecodeElement(Bytes{discriminant, 0x00}, ignored, error)); + BOOST_CHECK(!error.empty()); + } +} + +BOOST_AUTO_TEST_CASE(merk_vectors) +{ + const UniValue doc{ReadJsonObject(json_tests::merk_proof_vectors, sizeof(json_tests::merk_proof_vectors))}; + const UniValue& vectors{doc["vectors"]}; + BOOST_REQUIRE(vectors.size() > 0); + for (size_t i = 0; i < vectors.size(); ++i) { + const UniValue& vec{vectors[i]}; + const std::string name{vec["name"].get_str()}; + const Bytes proof{FromHex(vec["proof_hex"].get_str())}; + + std::string error; + BOOST_CHECK_MESSAGE(RunMerkVector(vec, proof, error) == VectorOutcome::MATCHES, + name + ": " + error); + + // Negative: flipping any single byte of a merk proof must be + // detected (decode error, root hash mismatch or result mismatch). + for (size_t pos = 0; pos < proof.size(); ++pos) { + Bytes corrupted{proof}; + corrupted[pos] ^= 0x01; + std::string ignored_error; + BOOST_CHECK_MESSAGE(RunMerkVector(vec, corrupted, ignored_error) == VectorOutcome::DETECTED, + name + ": corrupting byte " + std::to_string(pos) + " must fail"); + } + + // Negative: wrong expected root hash must fail. + const std::vector items{ParseQueryItems(vec["query"]["items"])}; + std::optional limit; + if (!vec["query"]["limit"].isNull()) limit = static_cast(vec["query"]["limit"].getInt()); + Hash256 wrong_root{HashFromHex(vec["expected_root_hash_hex"].get_str())}; + wrong_root[0] ^= 0x01; + platform::merk::VerifyResult result; + BOOST_CHECK_MESSAGE(!platform::merk::VerifyProof(proof, items, limit, + vec["query"]["left_to_right"].get_bool(), + wrong_root, result, error), + name + ": wrong expected root must fail"); + } +} + +BOOST_AUTO_TEST_CASE(merk_query_completeness) +{ + // Security critical: a proof must not be able to silently omit results + // that match the query. Take the proof that only covers key "i2" and + // verify it against a query that also asks for "i4" (which exists in + // the tree but is abridged in this proof): verification must fail. + const UniValue doc{ReadJsonObject(json_tests::merk_proof_vectors, sizeof(json_tests::merk_proof_vectors))}; + const UniValue& vectors{doc["vectors"]}; + const UniValue* single_key{nullptr}; + for (size_t i = 0; i < vectors.size(); ++i) { + if (vectors[i]["name"].get_str() == "items_single_key_present") single_key = &vectors[i]; + } + BOOST_REQUIRE(single_key != nullptr); + const Bytes proof{FromHex((*single_key)["proof_hex"].get_str())}; + const Hash256 expected_root{HashFromHex((*single_key)["expected_root_hash_hex"].get_str())}; + + // The original query verifies. + std::vector items{QueryItem::Key(Bytes{'i', '2'})}; + platform::merk::VerifyResult result; + std::string error; + BOOST_CHECK_MESSAGE( + platform::merk::VerifyProof(proof, items, std::nullopt, true, expected_root, result, error), error); + BOOST_CHECK_EQUAL(result.result_set.size(), 1U); + + // Asking for an additional key the proof does not cover must fail even + // though the proof itself is untampered. + items.push_back(QueryItem::Key(Bytes{'i', '4'})); + BOOST_CHECK(!platform::merk::VerifyProof(proof, items, std::nullopt, true, expected_root, result, error)); + + // Same for a range that the proof cannot answer completely. + items = {QueryItem::Range(Bytes{'i', '1'}, Bytes{'i', '5'})}; + BOOST_CHECK(!platform::merk::VerifyProof(proof, items, std::nullopt, true, expected_root, result, error)); +} + +BOOST_AUTO_TEST_CASE(grovedb_vectors) +{ + const UniValue doc{ + ReadJsonObject(json_tests::grovedb_proof_vectors, sizeof(json_tests::grovedb_proof_vectors))}; + const UniValue& vectors{doc["vectors"]}; + BOOST_REQUIRE(vectors.size() > 0); + for (size_t i = 0; i < vectors.size(); ++i) { + const UniValue& vec{vectors[i]}; + const std::string name{vec["name"].get_str()}; + const Bytes proof{FromHex(vec["proof_hex"].get_str())}; + + std::string error; + BOOST_CHECK_MESSAGE(RunGroveVector(vec, proof, error) == VectorOutcome::MATCHES, + name + ": " + error); + + // Negative: flipping any single byte of the envelope must never + // verify to a different root hash or result set. A few positions + // are genuinely malleable in the Rust implementation as well (each + // was confirmed against grovedb v5.0.0 verify_query_raw): + // - the trailing ProveOptions bool of V0 envelopes (prover + // controlled by design, see the upstream security note) and a + // Push <-> PushInverted twin opcode pair (0x1c/0x1d) on a + // single-node layer may still verify to the *identical* output; + // - lenient V0 proofs do not bind the serialized element bytes of a + // non-empty tree returned without descending into it (the very + // forgery vector the V1 strict mode closes with + // KVValueHashFeatureTypeWithChildHash nodes), so for + // root_tree_no_subquery_v0 some flips inside the element bytes + // verify with the same root hash but a different element. + const bool is_v0{name.size() >= 3 && name.compare(name.size() - 3, 3, "_v0") == 0}; + const bool v0_tree_element_hole{name == "root_tree_no_subquery_v0"}; + for (size_t pos = 0; pos < proof.size(); ++pos) { + Bytes corrupted{proof}; + corrupted[pos] ^= 0x01; + std::string ignored_error; + const VectorOutcome outcome{RunGroveVector(vec, corrupted, ignored_error)}; + if (outcome == VectorOutcome::DIFFERS) { + BOOST_CHECK_MESSAGE(v0_tree_element_hole, + name + ": corrupting byte " + std::to_string(pos) + + " must not verify to different results"); + } else if (outcome == VectorOutcome::MATCHES) { + const bool malleable{(is_v0 && pos == proof.size() - 1) || proof[pos] == 0x1c || + proof[pos] == 0x1d}; + BOOST_CHECK_MESSAGE(malleable, name + ": corrupting byte " + std::to_string(pos) + + " must fail"); + } + } + + // Negative: wrong expected root hash must fail. + const PathQuery query{ParsePathQuery(vec["path_query"])}; + Hash256 wrong_root{HashFromHex(vec["expected_root_hash_hex"].get_str())}; + wrong_root[31] ^= 0x80; + platform::grove::GroveVerifyResult result; + BOOST_CHECK_MESSAGE( + !platform::grove::VerifyQueryWithRootHash(proof, query, platform::grove::VerifyOptions{}, + wrong_root, result, error), + name + ": wrong expected root must fail"); + } +} + +BOOST_AUTO_TEST_CASE(grovedb_query_completeness) +{ + // A layered proof covering keys {i1, i3} must not verify for a query + // that additionally asks for i4 (present in the tree, abridged in the + // proof). + const UniValue doc{ + ReadJsonObject(json_tests::grovedb_proof_vectors, sizeof(json_tests::grovedb_proof_vectors))}; + const UniValue& vectors{doc["vectors"]}; + const UniValue* two_level{nullptr}; + for (size_t i = 0; i < vectors.size(); ++i) { + if (vectors[i]["name"].get_str() == "two_level_items_v1") two_level = &vectors[i]; + } + BOOST_REQUIRE(two_level != nullptr); + const Bytes proof{FromHex((*two_level)["proof_hex"].get_str())}; + + PathQuery query{ParsePathQuery((*two_level)["path_query"])}; + query.query.items.push_back(QueryItem::Key(Bytes{'i', '4'})); + std::sort(query.query.items.begin(), query.query.items.end(), + [](const QueryItem& a, const QueryItem& b) { return a.key < b.key; }); + + platform::grove::GroveVerifyResult result; + std::string error; + BOOST_CHECK(!platform::grove::VerifyQuery(proof, query, platform::grove::VerifyOptions{}, result, error)); +} + +BOOST_AUTO_TEST_CASE(grovedb_succinctness_option) +{ + // With the succinctness option enabled, exact-match vectors still verify + // (their lower layers are all required by the query). + const UniValue doc{ + ReadJsonObject(json_tests::grovedb_proof_vectors, sizeof(json_tests::grovedb_proof_vectors))}; + const UniValue& vectors{doc["vectors"]}; + for (size_t i = 0; i < vectors.size(); ++i) { + const UniValue& vec{vectors[i]}; + const std::string name{vec["name"].get_str()}; + const Bytes proof{FromHex(vec["proof_hex"].get_str())}; + const PathQuery query{ParsePathQuery(vec["path_query"])}; + const platform::grove::VerifyOptions options{ + .verify_proof_succinctness = true, + .include_empty_trees_in_result = true, + }; + platform::grove::GroveVerifyResult result; + std::string error; + BOOST_CHECK_MESSAGE(platform::grove::VerifyQuery(proof, query, options, result, error), + name + ": " + error); + BOOST_CHECK(result.root_hash == HashFromHex(vec["expected_root_hash_hex"].get_str())); + } +} + +BOOST_AUTO_TEST_CASE(reference_path_resolution) +{ + // grovedb-element/src/reference_path/mod.rs path_from_reference_path_type + using platform::grove::ReferencePath; + const std::vector current_path{Bytes{'a'}, Bytes{'b'}, Bytes{'c'}}; + const Bytes key{'k'}; + std::vector resolved; + std::string error; + + ReferencePath absolute; + absolute.type = ReferencePath::Type::ABSOLUTE_PATH; + absolute.path = {Bytes{'x'}, Bytes{'y'}}; + BOOST_REQUIRE(absolute.Resolve(current_path, key, resolved, error)); + BOOST_CHECK(resolved == std::vector({Bytes{'x'}, Bytes{'y'}})); + + ReferencePath upstream_root; + upstream_root.type = ReferencePath::Type::UPSTREAM_ROOT_HEIGHT; + upstream_root.height = 2; + upstream_root.path = {Bytes{'p'}, Bytes{'q'}}; + BOOST_REQUIRE(upstream_root.Resolve(current_path, key, resolved, error)); + BOOST_CHECK(resolved == std::vector({Bytes{'a'}, Bytes{'b'}, Bytes{'p'}, Bytes{'q'}})); + + ReferencePath parent_addition; + parent_addition.type = ReferencePath::Type::UPSTREAM_ROOT_HEIGHT_WITH_PARENT_PATH_ADDITION; + parent_addition.height = 2; + parent_addition.path = {Bytes{'x'}, Bytes{'y'}}; + BOOST_REQUIRE(parent_addition.Resolve(current_path, key, resolved, error)); + BOOST_CHECK(resolved == + std::vector({Bytes{'a'}, Bytes{'b'}, Bytes{'x'}, Bytes{'y'}, Bytes{'c'}})); + + ReferencePath from_element; + from_element.type = ReferencePath::Type::UPSTREAM_FROM_ELEMENT_HEIGHT; + from_element.height = 1; + from_element.path = {Bytes{'p'}, Bytes{'q'}}; + BOOST_REQUIRE(from_element.Resolve(current_path, key, resolved, error)); + BOOST_CHECK(resolved == std::vector({Bytes{'a'}, Bytes{'b'}, Bytes{'p'}, Bytes{'q'}})); + + ReferencePath cousin; + cousin.type = ReferencePath::Type::COUSIN; + cousin.path = {Bytes{'z'}}; + BOOST_REQUIRE(cousin.Resolve(current_path, key, resolved, error)); + BOOST_CHECK(resolved == std::vector({Bytes{'a'}, Bytes{'b'}, Bytes{'z'}, Bytes{'k'}})); + + ReferencePath removed_cousin; + removed_cousin.type = ReferencePath::Type::REMOVED_COUSIN; + removed_cousin.path = {Bytes{'m'}, Bytes{'n'}}; + BOOST_REQUIRE(removed_cousin.Resolve(current_path, key, resolved, error)); + BOOST_CHECK(resolved == + std::vector({Bytes{'a'}, Bytes{'b'}, Bytes{'m'}, Bytes{'n'}, Bytes{'k'}})); + + ReferencePath sibling; + sibling.type = ReferencePath::Type::SIBLING; + sibling.path = {Bytes{'s'}}; + BOOST_REQUIRE(sibling.Resolve(current_path, key, resolved, error)); + BOOST_CHECK(resolved == std::vector({Bytes{'a'}, Bytes{'b'}, Bytes{'c'}, Bytes{'s'}})); + + // Upstream height beyond the current path must fail. + upstream_root.height = 4; + BOOST_CHECK(!upstream_root.Resolve(current_path, key, resolved, error)); +} + +BOOST_AUTO_TEST_CASE(leb128_and_bincode_varints) +{ + // LEB128 write/read round trip (integer-encoding crate compatible). + for (const uint64_t value : {uint64_t{0}, uint64_t{1}, uint64_t{127}, uint64_t{128}, + uint64_t{300}, uint64_t{1'000'000}, + std::numeric_limits::max()}) { + Bytes encoded; + platform::WriteLEB128(encoded, value); + platform::BytesReader reader{encoded}; + uint64_t decoded; + BOOST_REQUIRE(reader.ReadLEB128(decoded)); + BOOST_CHECK_EQUAL(decoded, value); + BOOST_CHECK(reader.IsEof()); + } + + // bincode-v2 big-endian varints: values below 251 are one byte; 251 + // introduces a big-endian u16 (spot values match the Rust encoder, cf. + // SumItem(1000) serializing to 03fb07d000 with zigzag(1000) = 2000 = + // 0x07d0). + { + const Bytes data{0xfa}; + platform::BytesReader reader{data}; + uint64_t v; + BOOST_REQUIRE(reader.ReadBincodeVarint(v)); + BOOST_CHECK_EQUAL(v, 250U); + } + { + const Bytes data{0xfb, 0x07, 0xd0}; + platform::BytesReader reader{data}; + int64_t v; + BOOST_REQUIRE(reader.ReadBincodeVarintSigned(v)); + BOOST_CHECK_EQUAL(v, 1000); + } + { + const Bytes data{0xfc, 0x00, 0x01, 0x00, 0x00}; + platform::BytesReader reader{data}; + uint64_t v; + BOOST_REQUIRE(reader.ReadBincodeVarint(v)); + BOOST_CHECK_EQUAL(v, 65536U); + } + { + // u128 marker is unsupported. + const Bytes data{0xfe}; + platform::BytesReader reader{data}; + uint64_t v; + BOOST_CHECK(!reader.ReadBincodeVarint(v)); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/util/data/non-backported.txt b/test/util/data/non-backported.txt index df791e5fd602..bde6ac5d29ac 100644 --- a/test/util/data/non-backported.txt +++ b/test/util/data/non-backported.txt @@ -39,6 +39,8 @@ src/qt/guiutil_font.* src/qt/informationwidget.* src/platform/*.cpp src/platform/*.h +src/platform/proof/*.cpp +src/platform/proof/*.h src/qt/masternodelist.* src/qt/masternodemodel.* src/qt/platform/*.cpp @@ -69,6 +71,7 @@ src/test/dynamic_activation*.cpp src/test/evo*.cpp src/test/llmq*.cpp src/test/masternode_payments_tests.cpp +src/test/platform_proof_tests.cpp src/test/util/llmq_tests.h src/test/governance*.cpp src/unordered_lru_cache.h From 2657ba52bf1447ceb30319890f61956f1e0c2fed Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 10:29:51 -0500 Subject: [PATCH 07/33] feat(platform): DPP codecs and state transition construction Pure-C++ encode/decode of Dash Platform Protocol objects and the state transitions the GUI broadcasts, pinned to dashpay/platform v4.0.0 (protocol version 12) and validated byte-for-byte against vectors from the real Rust rs-dpp: - dpp/bincode: bincode-v2 big-endian varint reader/writer (zigzag signed, versioned-enum discriminants) matching rs-platform-serialization - dpp/document: platform Value / document property encoding and the document id derivation used by the four GUI document types - dpp/identity: Identity / IdentityPublicKey decode into GUI types - dpp/statetransitions: IdentityCreate (instant + chain asset lock proofs), DPNS preorder/domain, DashPay profile create+replace and contactRequest; signing delegated to wallet callbacks (double-SHA256 of signable bytes, 65-byte compact recoverable ECDSA, header 27+recid+4); plus salted domain hash, homograph-safe label normalization, and the DPNS v1 contested-name rule (normalizedLabel ^[a-zA-Z01-]{3,19}$) - contrib/devtools/platform-dpp-vectors: Rust vector generator (dev tool) - tests: byte-exact signable bytes/digests/serialized STs, document ids, salted hashes, identity ids, identity decode + tamper negatives (9 cases) Key correction over initial notes: DPP bincode is big-endian (same config as the grovedb envelope), and batch transitions serialize as BatchTransition::V1 / DocumentBaseTransition::V1. Co-Authored-By: Claude Fable 5 --- .../devtools/platform-dpp-vectors/.gitignore | 1 + .../devtools/platform-dpp-vectors/Cargo.lock | 2331 +++++++++++++++++ .../devtools/platform-dpp-vectors/Cargo.toml | 28 + .../devtools/platform-dpp-vectors/README.md | 25 + .../devtools/platform-dpp-vectors/src/main.rs | 718 +++++ src/Makefile.am | 7 + src/Makefile.test.include | 3 + src/platform/dpp/bincode.cpp | 164 ++ src/platform/dpp/bincode.h | 101 + src/platform/dpp/document.cpp | 139 + src/platform/dpp/document.h | 106 + src/platform/dpp/identity.cpp | 159 ++ src/platform/dpp/identity.h | 43 + src/platform/dpp/statetransitions.cpp | 528 ++++ .../data/platform/dpp_identity_vectors.json | 52 + src/test/data/platform/dpp_st_vectors.json | 201 ++ src/test/platform_dpp_tests.cpp | 429 +++ test/util/data/non-backported.txt | 3 + 18 files changed, 5038 insertions(+) create mode 100644 contrib/devtools/platform-dpp-vectors/.gitignore create mode 100644 contrib/devtools/platform-dpp-vectors/Cargo.lock create mode 100644 contrib/devtools/platform-dpp-vectors/Cargo.toml create mode 100644 contrib/devtools/platform-dpp-vectors/README.md create mode 100644 contrib/devtools/platform-dpp-vectors/src/main.rs create mode 100644 src/platform/dpp/bincode.cpp create mode 100644 src/platform/dpp/bincode.h create mode 100644 src/platform/dpp/document.cpp create mode 100644 src/platform/dpp/document.h create mode 100644 src/platform/dpp/identity.cpp create mode 100644 src/platform/dpp/identity.h create mode 100644 src/platform/dpp/statetransitions.cpp create mode 100644 src/test/data/platform/dpp_identity_vectors.json create mode 100644 src/test/data/platform/dpp_st_vectors.json create mode 100644 src/test/platform_dpp_tests.cpp diff --git a/contrib/devtools/platform-dpp-vectors/.gitignore b/contrib/devtools/platform-dpp-vectors/.gitignore new file mode 100644 index 000000000000..2f7896d1d136 --- /dev/null +++ b/contrib/devtools/platform-dpp-vectors/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/contrib/devtools/platform-dpp-vectors/Cargo.lock b/contrib/devtools/platform-dpp-vectors/Cargo.lock new file mode 100644 index 000000000000..54044ca20ff6 --- /dev/null +++ b/contrib/devtools/platform-dpp-vectors/Cargo.lock @@ -0,0 +1,2331 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-compat" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" +dependencies = [ + "byteorder", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue 0.0.18", +] + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d6094e2a1ba3c93b5a596fe5a10d1a10c3c6e06785cde89f693a044c01aa40" +dependencies = [ + "bitcoin-internals", +] + +[[package]] +name = "bitcoin-internals" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" +dependencies = [ + "hex-conservative 0.3.2", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "blsful" +version = "3.0.0" +source = "git+https://github.com/dashpay/agora-blsful?rev=0c34a7a488a0bd1c9a9a2196e793b303ad35c900#0c34a7a488a0bd1c9a9a2196e793b303ad35c900" +dependencies = [ + "anyhow", + "blstrs_plus", + "hex", + "hkdf", + "merlin", + "pairing", + "rand", + "rand_chacha", + "rand_core", + "serde", + "serde_bare", + "sha2", + "sha3", + "subtle", + "thiserror", + "uint-zigzag", + "vsss-rs", + "zeroize", +] + +[[package]] +name = "blst" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62dc83a094a71d43eeadd254b1ec2d24cb6a0bb6cadce00df51f0db594711a32" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "blstrs_plus" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a16dd4b0d6b4538e1fa0388843acb186363082713a8fc8416d802a04d013818" +dependencies = [ + "arrayref", + "blst", + "elliptic-curve", + "ff", + "group", + "pairing", + "rand_core", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cbindgen" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ecb53484c9c167ba674026b656d8a27d7657a58e6066aa902bfb1a4aa00ae20" +dependencies = [ + "clap", + "heck", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", + "tempfile", + "toml", +] + +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.9", + "rand_core", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.9", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dash-network" +version = "0.44.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +dependencies = [ + "bincode", + "bincode_derive", + "cbindgen", + "serde", +] + +[[package]] +name = "dashcore" +version = "0.44.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +dependencies = [ + "anyhow", + "base64-compat", + "bech32 0.9.1", + "bincode", + "bincode_derive", + "bitvec", + "blake3", + "blsful", + "dash-network", + "dashcore-private", + "dashcore_hashes", + "ed25519-dalek", + "hex", + "hex_lit", + "rustversion", + "secp256k1", + "serde", + "thiserror", + "tracing", +] + +[[package]] +name = "dashcore-private" +version = "0.44.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" + +[[package]] +name = "dashcore_hashes" +version = "0.44.0" +source = "git+https://github.com/dashpay/rust-dashcore?rev=991c6ebe24d7ea8ba7d900a052b25be8c5498409#991c6ebe24d7ea8ba7d900a052b25be8c5498409" +dependencies = [ + "bincode", + "dashcore-private", + "serde", +] + +[[package]] +name = "dashpay-contract" +version = "4.0.0" +source = "git+https://github.com/dashpay/platform?tag=v4.0.0#9f9092cc910809fd5c415b74fe939864d7bfa7ed" +dependencies = [ + "platform-value", + "platform-version", + "serde_json", + "thiserror", +] + +[[package]] +name = "data-contracts" +version = "4.0.0" +source = "git+https://github.com/dashpay/platform?tag=v4.0.0#9f9092cc910809fd5c415b74fe939864d7bfa7ed" +dependencies = [ + "dashpay-contract", + "dpns-contract", + "platform-value", + "platform-version", + "serde_json", + "thiserror", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dpns-contract" +version = "4.0.0" +source = "git+https://github.com/dashpay/platform?tag=v4.0.0#9f9092cc910809fd5c415b74fe939864d7bfa7ed" +dependencies = [ + "platform-value", + "platform-version", + "serde_json", + "thiserror", +] + +[[package]] +name = "dpp" +version = "4.0.0" +source = "git+https://github.com/dashpay/platform?tag=v4.0.0#9f9092cc910809fd5c415b74fe939864d7bfa7ed" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "bech32 0.11.1", + "bincode", + "bs58", + "byteorder", + "chrono", + "dashcore", + "data-contracts", + "derive_more", + "dpp-json-convertible-derive", + "env_logger", + "getrandom 0.2.17", + "hex", + "indexmap", + "integer-encoding", + "itertools", + "lazy_static", + "nohash-hasher", + "num_enum", + "once_cell", + "platform-serialization", + "platform-serialization-derive", + "platform-value", + "platform-version", + "platform-versioning", + "rand", + "regex", + "serde", + "serde_json", + "serde_repr", + "sha2", + "strum", + "thiserror", + "tracing", +] + +[[package]] +name = "dpp-json-convertible-derive" +version = "4.0.0" +source = "git+https://github.com/dashpay/platform?tag=v4.0.0#9f9092cc910809fd5c415b74fe939864d7bfa7ed" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array 0.14.9", + "group", + "hkdf", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "tap", + "zeroize", +] + +[[package]] +name = "elliptic-curve-tools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de2b6fae800f08032a6ea32995b52925b1d451bff9d445c8ab2932323277faf" +dependencies = [ + "elliptic-curve", + "heapless", + "hex", + "multiexp", + "serde", + "zeroize", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "generic-array" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351" +dependencies = [ + "rustversion", + "serde_core", + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand", + "rand_core", + "rand_xorshift", + "subtle", +] + +[[package]] +name = "grovedb-version" +version = "5.0.0" +source = "git+https://github.com/dashpay/grovedb?tag=v5.0.0#9b98a35644cdea73cc1b21d7c122cb58ae9fafd8" +dependencies = [ + "thiserror", + "versioned-feature-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830e599c2904b08f0834ee6337d8fe8f0ed4a63b5d9e7a7f49c0ffa06d08d360" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "integer-encoding" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c00403deb17c3221a1fe4fb571b9ed0370b3dcd116553c77fa294a3d918699" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" +dependencies = [ + "defmt", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "multiexp" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec2ce93a6f06ac6cae04c1da3f2a6a24fcfc1f0eb0b4e0f3d302f0df45326cb" +dependencies = [ + "ff", + "group", + "rand_core", + "rustversion", + "std-shims", + "zeroize", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", + "rand", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", + "rand", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "platform-dpp-vectors" +version = "0.1.0" +dependencies = [ + "async-trait", + "dpp", + "futures", + "hex", + "platform-value", + "platform-version", + "serde_json", +] + +[[package]] +name = "platform-serialization" +version = "4.0.0" +source = "git+https://github.com/dashpay/platform?tag=v4.0.0#9f9092cc910809fd5c415b74fe939864d7bfa7ed" +dependencies = [ + "bincode", + "platform-version", +] + +[[package]] +name = "platform-serialization-derive" +version = "4.0.0" +source = "git+https://github.com/dashpay/platform?tag=v4.0.0#9f9092cc910809fd5c415b74fe939864d7bfa7ed" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "virtue 0.0.17", +] + +[[package]] +name = "platform-value" +version = "4.0.0" +source = "git+https://github.com/dashpay/platform?tag=v4.0.0#9f9092cc910809fd5c415b74fe939864d7bfa7ed" +dependencies = [ + "base64", + "bincode", + "bs58", + "hex", + "indexmap", + "platform-serialization", + "platform-version", + "rand", + "serde", + "serde_json", + "thiserror", + "treediff", +] + +[[package]] +name = "platform-version" +version = "4.0.0" +source = "git+https://github.com/dashpay/platform?tag=v4.0.0#9f9092cc910809fd5c415b74fe939864d7bfa7ed" +dependencies = [ + "bincode", + "grovedb-version", + "thiserror", + "versioned-feature-core 1.0.0 (git+https://github.com/dashpay/versioned-feature-core)", +] + +[[package]] +name = "platform-versioning" +version = "4.0.0" +source = "git+https://github.com/dashpay/platform?tag=v4.0.0#9f9092cc910809fd5c415b74fe939864d7bfa7ed" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.9", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bare" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51c55386eed0f1ae957b091dc2ca8122f287b60c79c774cbe3d5f2b69fded660" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "std-shims" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227c4f8561598188d0df96dbe749824576174bba278b5b6bb2eacff1066067d0" +dependencies = [ + "hashbrown 0.16.1", + "rustversion", + "spin", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "treediff" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ce481b2b7c2534fe7b5242cccebf37f9084392665c6a3783c414a1bada5432" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uint-zigzag" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61faa33dc26b2851a37da5390a1a4cac015887b1e97ecd77ce7b4f987431de9f" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "versioned-feature-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898c0ad500fdb1914df465a2c729fce33646ef65dfbbbd16a6d8050e0d2404df" + +[[package]] +name = "versioned-feature-core" +version = "1.0.0" +source = "git+https://github.com/dashpay/versioned-feature-core#560157096c8405a46ce0f21a2e7e1bd11d6625b4" + +[[package]] +name = "virtue" +version = "0.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7302ac74a033bf17b6e609ceec0f891ca9200d502d31f02dc7908d3d98767c9d" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "vsss-rs" +version = "5.1.0" +source = "git+https://github.com/dashpay/vsss-rs?branch=main#668f1406bf25a4b9a95cd97c9069f7a1632897c3" +dependencies = [ + "crypto-bigint", + "elliptic-curve", + "elliptic-curve-tools", + "generic-array 1.4.3", + "hex", + "num", + "rand_core", + "serde", + "sha3", + "subtle", + "zeroize", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/contrib/devtools/platform-dpp-vectors/Cargo.toml b/contrib/devtools/platform-dpp-vectors/Cargo.toml new file mode 100644 index 000000000000..839bb73e2c86 --- /dev/null +++ b/contrib/devtools/platform-dpp-vectors/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "platform-dpp-vectors" +version = "0.1.0" +edition = "2021" +publish = false +description = "Generates DPP object / state transition test vectors for src/test/platform_dpp_tests.cpp" + +[dependencies] +# Pinned to the dashpay/platform release the C++ port in src/platform/dpp/ +# mirrors (protocol version 12). When bumping the tag, regenerate the JSON +# vectors and re-run test_dash --run_test=platform_dpp_tests. +dpp = { git = "https://github.com/dashpay/platform", tag = "v4.0.0", package = "dpp", features = [ + "state-transitions", + "state-transition-signing", + "identity-serialization", + "system_contracts", + "dpns-contract", + "dashpay-contract", +] } +platform-value = { git = "https://github.com/dashpay/platform", tag = "v4.0.0", package = "platform-value" } +platform-version = { git = "https://github.com/dashpay/platform", tag = "v4.0.0", package = "platform-version" } + +async-trait = "0.1" +futures = "0.3" +hex = "0.4" +serde_json = { version = "1", features = ["preserve_order"] } + +# This crate is a developer tool, not part of the Dash Core build. See README.md. diff --git a/contrib/devtools/platform-dpp-vectors/README.md b/contrib/devtools/platform-dpp-vectors/README.md new file mode 100644 index 000000000000..b06dbec93dca --- /dev/null +++ b/contrib/devtools/platform-dpp-vectors/README.md @@ -0,0 +1,25 @@ +# platform-dpp-vectors + +Developer tool that generates the DPP (Dash Platform Protocol) test vectors in +`src/test/data/platform/dpp_identity_vectors.json` and +`src/test/data/platform/dpp_st_vectors.json`, consumed by +`src/test/platform_dpp_tests.cpp` to validate the pure-C++ DPP port in +`src/platform/dpp/`. + +The vectors are ground truth: every state transition is constructed, signed +and serialized by the real `rs-dpp` crate from +[dashpay/platform](https://github.com/dashpay/platform), pinned to tag +**v4.0.0** (Platform protocol version **12**). Signing uses +`dashcore::signer` (double SHA256 digest, 65-byte compact recoverable ECDSA, +header byte `27 + recovery_id + 4`). + +This crate is not part of the Dash Core build or CI. Regenerate the vectors +only when bumping the pinned platform tag: + +```bash +cd contrib/devtools/platform-dpp-vectors +cargo run --release -- ../../../src/test/data/platform +``` + +Then rerun `./src/test/test_dash --run_test=platform_dpp_tests` and fix the +C++ port for any protocol changes. diff --git a/contrib/devtools/platform-dpp-vectors/src/main.rs b/contrib/devtools/platform-dpp-vectors/src/main.rs new file mode 100644 index 000000000000..36f842811403 --- /dev/null +++ b/contrib/devtools/platform-dpp-vectors/src/main.rs @@ -0,0 +1,718 @@ +//! Generates DPP test vectors for the C++ port in src/platform/dpp/. +//! +//! Everything is produced by the real rs-dpp code (dashpay/platform tag +//! v4.0.0, protocol version 12): state transitions are built through the same +//! factory methods the Rust SDK uses, signed with dashcore::signer (double +//! SHA256 + 65-byte compact recoverable ECDSA), and serialized with +//! rs-platform-serialization (bincode standard() + big-endian + varint). +//! +//! Usage: cargo run -- +//! Writes dpp_identity_vectors.json and dpp_st_vectors.json. + +use std::collections::BTreeMap; + +use dpp::dashcore::bls_sig_utils::BLSSignature; +use dpp::dashcore::consensus::serialize as consensus_serialize; +use dpp::dashcore::hashes::Hash; +use dpp::dashcore::secp256k1::{PublicKey as SecpPublicKey, Secp256k1, SecretKey}; +use dpp::dashcore::transaction::special_transaction::asset_lock::AssetLockPayload; +use dpp::dashcore::transaction::special_transaction::TransactionPayload; +use dpp::dashcore::{signer, InstantLock, OutPoint, ScriptBuf, Transaction, TxIn, Txid, TxOut}; +use dpp::document::{Document, DocumentV0}; +use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dpp::identity::signer::Signer; +use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; +use dpp::identity::state_transition::asset_lock_proof::{AssetLockProof, InstantAssetLockProof}; +use dpp::identity::contract_bounds::ContractBounds; +use dpp::identity::{ + Identity, IdentityPublicKey, IdentityV0, KeyType, Purpose, SecurityLevel, +}; +use dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; +use dpp::data_contract::DataContract; +use dpp::serialization::{PlatformSerializable, Signable}; +use dpp::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::identity_create_transition::methods::IdentityCreateTransitionMethodsV0; +use dpp::state_transition::identity_create_transition::IdentityCreateTransition; +use dpp::state_transition::StateTransition; +use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; +use dpp::util::hash::hash_double; +use dpp::util::strings::convert_to_homograph_safe_chars; +use dpp::address_funds::AddressWitness; +use dpp::{BlsModule, ProtocolError, PublicKeyValidationError}; +use platform_value::{BinaryData, Identifier, Value}; +use platform_version::version::PlatformVersion; +use serde_json::{json, Value as JsonValue}; + +const MASTER_KEY_PRIV: [u8; 32] = [0x11; 32]; +const HIGH_KEY_PRIV: [u8; 32] = [0x22; 32]; +const ASSET_LOCK_PRIV: [u8; 32] = [0x33; 32]; +const PREORDER_SALT: [u8; 32] = [0x55; 32]; + +/// Document entropy convention of the C++ builders (src/platform/dpp/ +/// statetransitions.cpp): rs-dpp uses caller-provided random entropy, the +/// dash-qt builders derive it deterministically as +/// DSHA256(owner_id || document_type_name || identity_contract_nonce (LE u64)) +/// so that retries of the same (identity, nonce) rebuild the identical +/// transition. DPNS documents use flow-specific 32-byte values instead +/// (preorder: the salted domain hash; domain: the preorder salt). +fn derived_entropy(owner: &Identifier, document_type_name: &str, nonce: u64) -> [u8; 32] { + let mut buf: Vec = Vec::new(); + buf.extend_from_slice(owner.as_slice()); + buf.extend_from_slice(document_type_name.as_bytes()); + buf.extend_from_slice(&nonce.to_le_bytes()); + hash_double(buf.as_slice()) +} + +fn hex_of(bytes: &[u8]) -> String { + hex::encode(bytes) +} + +fn pubkey_of(priv_bytes: &[u8; 32]) -> Vec { + let secp = Secp256k1::new(); + let sk = SecretKey::from_slice(priv_bytes).expect("valid secp256k1 secret key"); + SecpPublicKey::from_secret_key(&secp, &sk).serialize().to_vec() +} + +/// Signs with fixed private keys, looked up by public key data. Mirrors what +/// interfaces::Wallet::signPlatformDigest does in dash-qt: RFC6979 +/// deterministic ECDSA, 65-byte compact recoverable signature over +/// double-SHA256 of the payload. +#[derive(Debug)] +struct FixedSigner { + keys: BTreeMap, [u8; 32]>, +} + +#[async_trait::async_trait] +impl Signer for FixedSigner { + async fn sign( + &self, + key: &IdentityPublicKey, + data: &[u8], + ) -> Result { + let sk = self + .keys + .get(key.data().as_slice()) + .expect("signer asked for an unknown key"); + let sig = signer::sign(data, sk).map_err(|e| ProtocolError::Generic(e.to_string()))?; + Ok(BinaryData::new(sig.to_vec())) + } + + async fn sign_create_witness( + &self, + _key: &IdentityPublicKey, + _data: &[u8], + ) -> Result { + Err(ProtocolError::Generic("not supported".to_string())) + } + + fn can_sign_with(&self, key: &IdentityPublicKey) -> bool { + self.keys.contains_key(key.data().as_slice()) + } +} + +/// BLS is never exercised (all vector keys are ECDSA_SECP256K1); the module +/// only satisfies the try_from_identity_with_signer_and_private_key signature. +#[derive(Debug)] +struct NoBls; + +impl BlsModule for NoBls { + fn validate_public_key(&self, _pk: &[u8]) -> Result<(), PublicKeyValidationError> { + Ok(()) + } + fn verify_signature( + &self, + _signature: &[u8], + _data: &[u8], + _public_key: &[u8], + ) -> Result { + Err(ProtocolError::Generic("bls not supported".to_string())) + } + fn private_key_to_public_key(&self, _private_key: &[u8]) -> Result, ProtocolError> { + Err(ProtocolError::Generic("bls not supported".to_string())) + } + fn sign(&self, _data: &[u8], _private_key: &[u8]) -> Result, ProtocolError> { + Err(ProtocolError::Generic("bls not supported".to_string())) + } +} + +fn identity_key( + id: u32, + purpose: Purpose, + security_level: SecurityLevel, + data: Vec, + contract_bounds: Option, + disabled_at: Option, +) -> IdentityPublicKey { + IdentityPublicKey::V0(IdentityPublicKeyV0 { + id, + purpose, + security_level, + contract_bounds, + key_type: KeyType::ECDSA_SECP256K1, + read_only: false, + data: BinaryData::new(data), + disabled_at, + }) +} + +fn key_json(key: &IdentityPublicKey) -> JsonValue { + json!({ + "id": key.id(), + "purpose": key.purpose() as u8, + "security_level": key.security_level() as u8, + "key_type": key.key_type() as u8, + "read_only": key.read_only(), + "data": hex_of(key.data().as_slice()), + "disabled_at": key.disabled_at(), + }) +} + +fn st_json(st: &StateTransition, extra: JsonValue) -> JsonValue { + let signable = st.signable_bytes().expect("signable bytes"); + let digest = hash_double(signable.as_slice()); + let serialized = st.serialize_to_bytes().expect("serialize"); + let mut obj = json!({ + "signable_hex": hex_of(&signable), + "digest_hex": hex_of(&digest), + "serialized_hex": hex_of(&serialized), + }); + if let (Some(base), Some(add)) = (obj.as_object_mut(), extra.as_object()) { + for (k, v) in add { + base.insert(k.clone(), v.clone()); + } + } + obj +} + +fn asset_lock_transaction() -> Transaction { + // One credit output inside the asset-lock payload; tx.vout carries the + // OP_RETURN burn output as on the wire. + let credit_output = TxOut { + value: 100_000, + script_pubkey: ScriptBuf::from_bytes(vec![ + 0x76, 0xa9, 0x14, // OP_DUP OP_HASH160 push20 + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, // + 0x88, 0xac, // OP_EQUALVERIFY OP_CHECKSIG + ]), + }; + Transaction { + version: 3, + lock_time: 0, + input: vec![TxIn { + previous_output: OutPoint { + txid: Txid::from_byte_array([0xdd; 32]), + vout: 1, + }, + script_sig: ScriptBuf::new(), + sequence: 0xffffffff, + witness: Default::default(), + }], + output: vec![TxOut { + value: 100_000, + script_pubkey: ScriptBuf::from_bytes(vec![0x6a]), // OP_RETURN + }], + special_transaction_payload: Some(TransactionPayload::AssetLockPayloadType( + AssetLockPayload { + version: 1, + credit_outputs: vec![credit_output], + }, + )), + } +} + +fn document(id: Identifier, owner: Identifier, properties: BTreeMap, revision: Option) -> Document { + Document::V0(DocumentV0 { + id, + owner_id: owner, + properties, + revision, + created_at: None, + updated_at: None, + transferred_at: None, + created_at_block_height: None, + updated_at_block_height: None, + transferred_at_block_height: None, + created_at_core_block_height: None, + updated_at_core_block_height: None, + transferred_at_core_block_height: None, + creator_id: None, + }) +} + +#[allow(clippy::too_many_arguments)] +fn document_create_st( + contract: &DataContract, + document_type_name: &str, + owner: Identifier, + properties: BTreeMap, + entropy: [u8; 32], + nonce: u64, + key: &IdentityPublicKey, + signer: &FixedSigner, + platform_version: &PlatformVersion, +) -> (Identifier, StateTransition) { + let document_type = contract + .document_type_for_name(document_type_name) + .expect("document type"); + let id = Document::generate_document_id_v0( + &contract.id(), + &owner, + document_type_name, + entropy.as_slice(), + ); + let doc = document(id, owner, properties, Some(1)); + let st = futures::executor::block_on( + BatchTransition::new_document_creation_transition_from_document( + doc, + document_type, + entropy, + key, + nonce, + 0, // user fee increase + None, + signer, + platform_version, + None, + ), + ) + .expect("create document transition"); + (id, st) +} + +fn main() { + let out_dir = std::env::args().nth(1).unwrap_or_else(|| ".".to_string()); + let platform_version = PlatformVersion::latest(); + let protocol_version = platform_version.protocol_version; + + let master_pub = pubkey_of(&MASTER_KEY_PRIV); + let high_pub = pubkey_of(&HIGH_KEY_PRIV); + let asset_lock_pub = pubkey_of(&ASSET_LOCK_PRIV); + + let signer = FixedSigner { + keys: BTreeMap::from([ + (master_pub.clone(), MASTER_KEY_PRIV), + (high_pub.clone(), HIGH_KEY_PRIV), + (asset_lock_pub.clone(), ASSET_LOCK_PRIV), + ]), + }; + + // ---------------------------------------------------------------- identity vectors + let master_key = identity_key( + 0, + Purpose::AUTHENTICATION, + SecurityLevel::MASTER, + master_pub.clone(), + None, + None, + ); + let high_key = identity_key( + 1, + Purpose::AUTHENTICATION, + SecurityLevel::HIGH, + high_pub.clone(), + None, + None, + ); + // Exercises Option and Option. + let bounded_key = identity_key( + 2, + Purpose::ENCRYPTION, + SecurityLevel::MEDIUM, + pubkey_of(&[0x66; 32]), + Some(ContractBounds::SingleContract { + id: Identifier::from(dpp::system_data_contracts::SystemDataContract::Dashpay.id()), + }), + Some(1_700_000_000_123), + ); + + let identity_id = Identifier::from([0x77; 32]); + let identity = Identity::V0(IdentityV0 { + id: identity_id, + public_keys: BTreeMap::from([ + (0, master_key.clone()), + (1, high_key.clone()), + (2, bounded_key.clone()), + ]), + // > u32::MAX so the u64 varint marker + big-endian payload is + // exercised in the vector. + balance: 5_000_000_000, + revision: 3, + }); + let identity_bytes = identity.serialize_to_bytes().expect("identity serialize"); + + let identity_vectors = json!({ + "platform_repo_tag": "v4.0.0", + "protocol_version": protocol_version, + "identity": { + "serialized_hex": hex_of(&identity_bytes), + "id": hex_of(identity_id.as_slice()), + "balance": 5_000_000_000u64, + "revision": 3, + "public_keys": [key_json(&master_key), key_json(&high_key), key_json(&bounded_key)], + "bounded_key_contract_id": hex_of(dpp::system_data_contracts::SystemDataContract::Dashpay.id().as_slice()), + }, + "identity_public_key": { + "serialized_hex": hex_of(&high_key.serialize_to_bytes().expect("key serialize")), + "fields": key_json(&high_key), + }, + }); + + // ---------------------------------------------------------------- identity create + let tx = asset_lock_transaction(); + let txid = tx.txid(); + let islock = InstantLock { + version: 1, + inputs: vec![OutPoint { + txid: Txid::from_byte_array([0xaa; 32]), + vout: 0, + }], + txid, + cyclehash: dpp::dashcore::hash_types::CycleHash::from_byte_array([0xbb; 32]), + signature: BLSSignature::from([0xcc; 96]), + }; + let tx_bytes = consensus_serialize(&tx); + let islock_bytes = consensus_serialize(&islock); + + // The identity registered by both create transitions (master + high key). + let created_identity = Identity::V0(IdentityV0 { + id: Identifier::from([0u8; 32]), // replaced by proof-derived id + public_keys: BTreeMap::from([(0, master_key.clone()), (1, high_key.clone())]), + balance: 0, + revision: 0, + }); + + let instant_proof = AssetLockProof::Instant(InstantAssetLockProof::new( + islock.clone(), + tx.clone(), + 0, + )); + let instant_outpoint: [u8; 36] = instant_proof.out_point().expect("outpoint").into(); + let instant_identity_id = instant_proof.create_identifier().expect("identifier"); + + let st_instant = futures::executor::block_on( + IdentityCreateTransition::try_from_identity_with_signer_and_private_key( + &created_identity, + instant_proof, + &ASSET_LOCK_PRIV, + &signer, + &NoBls, + 0, + platform_version, + ), + ) + .expect("identity create (instant)"); + + let chain_outpoint_raw: [u8; 36] = { + let mut buf = [0u8; 36]; + buf[..32].copy_from_slice(&txid.to_byte_array()); + buf[32..].copy_from_slice(&0u32.to_le_bytes()); + buf + }; + let chain_proof = AssetLockProof::Chain(ChainAssetLockProof::new(1_050_000, chain_outpoint_raw)); + let chain_identity_id = chain_proof.create_identifier().expect("identifier"); + + let st_chain = futures::executor::block_on( + IdentityCreateTransition::try_from_identity_with_signer_and_private_key( + &created_identity, + chain_proof, + &ASSET_LOCK_PRIV, + &signer, + &NoBls, + 0, + platform_version, + ), + ) + .expect("identity create (chain)"); + + // ---------------------------------------------------------------- documents + let dpns = load_system_data_contract(SystemDataContract::DPNS, platform_version) + .expect("dpns contract"); + let dashpay = load_system_data_contract(SystemDataContract::Dashpay, platform_version) + .expect("dashpay contract"); + + let owner = instant_identity_id; + + // DPNS preorder. + let label = "Alice"; + let normalized_label = convert_to_homograph_safe_chars(label); + let mut salted: Vec = Vec::new(); + salted.extend_from_slice(&PREORDER_SALT); + salted.extend_from_slice((normalized_label.clone() + ".dash").as_bytes()); + let salted_domain_hash = hash_double(salted.as_slice()); + + let (preorder_id, st_preorder) = document_create_st( + &dpns, + "preorder", + owner, + BTreeMap::from([( + "saltedDomainHash".to_string(), + Value::Bytes32(salted_domain_hash), + )]), + salted_domain_hash, + 2, + &high_key, + &signer, + platform_version, + ); + + // DPNS domain (label "Alice" normalizes to "a11ce": contested, so the + // factory adds the prefunded voting balance tuple). + let domain_properties = |label: &str, normalized: &str| { + BTreeMap::from([ + ("parentDomainName".to_string(), Value::Text("dash".to_string())), + ( + "normalizedParentDomainName".to_string(), + Value::Text("dash".to_string()), + ), + ("label".to_string(), Value::Text(label.to_string())), + ("normalizedLabel".to_string(), Value::Text(normalized.to_string())), + ("preorderSalt".to_string(), Value::Bytes32(PREORDER_SALT)), + ( + "records".to_string(), + Value::Map(vec![( + Value::Text("identity".to_string()), + Value::Identifier(owner.to_buffer()), + )]), + ), + ( + "subdomainRules".to_string(), + Value::Map(vec![( + Value::Text("allowSubdomains".to_string()), + Value::Bool(false), + )]), + ), + ]) + }; + + // identity_contract_nonce 0x1234 pins the big-endian u16 varint encoding. + let (domain_id, st_domain) = document_create_st( + &dpns, + "domain", + owner, + domain_properties(label, &normalized_label), + PREORDER_SALT, + 0x1234, + &high_key, + &signer, + platform_version, + ); + + // Non-contested domain: digits 2-9 fall outside the contested character + // set of the DPNS contract's parentNameAndLabel contested index. + let label2 = "quantum42"; + let normalized_label2 = convert_to_homograph_safe_chars(label2); + let (domain2_id, st_domain2) = document_create_st( + &dpns, + "domain", + owner, + domain_properties(label2, &normalized_label2), + PREORDER_SALT, + 5, + &high_key, + &signer, + platform_version, + ); + + // Contested-label truth table straight from the DPNS document type + // (prefunded_voting_balance_for_document over the real contract). + let domain_type = dpns.document_type_for_name("domain").expect("domain type"); + let contested_cases: Vec = [ + "alice", "a11ce", "bob", "b0b", "ab", "abc", "x2y", "up", "-ab-", + "aaaaaaaaaaaaaaaaaaa", // 19 chars + "aaaaaaaaaaaaaaaaaaaa", // 20 chars + "quantum42", "dash", "test-name", "name2", + ] + .iter() + .map(|raw| { + let normalized = convert_to_homograph_safe_chars(raw); + let doc = document( + Identifier::from([1u8; 32]), + owner, + domain_properties(raw, &normalized), + Some(1), + ); + let contested = domain_type + .prefunded_voting_balance_for_document(&doc, platform_version) + .expect("prefunded voting balance") + .is_some(); + json!({"label": raw, "normalized": normalized, "contested": contested}) + }) + .collect(); + + // DashPay profile create + replace. + let profile_properties = BTreeMap::from([ + ("displayName".to_string(), Value::Text("Alice в Wonderland".to_string())), + ("publicMessage".to_string(), Value::Text("hello platform".to_string())), + ("avatarUrl".to_string(), Value::Text("https://example.com/a.png".to_string())), + ("avatarHash".to_string(), Value::Bytes32([0x88; 32])), + ("avatarFingerprint".to_string(), Value::Bytes(vec![0x99; 8])), + ]); + let profile_entropy = derived_entropy(&owner, "profile", 3); + let (profile_id, st_profile_create) = document_create_st( + &dashpay, + "profile", + owner, + profile_properties.clone(), + profile_entropy, + 3, + &high_key, + &signer, + platform_version, + ); + + let mut replace_properties = profile_properties.clone(); + replace_properties.insert( + "publicMessage".to_string(), + Value::Text("updated message".to_string()), + ); + let profile_doc_replace = document(profile_id, owner, replace_properties.clone(), Some(2)); + let profile_type = dashpay.document_type_for_name("profile").expect("profile type"); + let st_profile_replace = futures::executor::block_on( + BatchTransition::new_document_replacement_transition_from_document( + profile_doc_replace, + profile_type, + &high_key, + 4, + 0, + None, + &signer, + platform_version, + None, + ), + ) + .expect("profile replace transition"); + + // DashPay contactRequest create. + let to_user_id = Identifier::from([0xee; 32]); + let contact_properties = BTreeMap::from([ + ("toUserId".to_string(), Value::Identifier(to_user_id.to_buffer())), + ("encryptedPublicKey".to_string(), Value::Bytes(vec![0xab; 96])), + ("senderKeyIndex".to_string(), Value::U32(2)), + ("recipientKeyIndex".to_string(), Value::U32(3)), + ("accountReference".to_string(), Value::U32(0x0badc0de)), + ("encryptedAccountLabel".to_string(), Value::Bytes(vec![0xcd; 48])), + ]); + let contact_entropy = derived_entropy(&owner, "contactRequest", 6); + let (contact_id, st_contact) = document_create_st( + &dashpay, + "contactRequest", + owner, + contact_properties, + contact_entropy, + 6, + &high_key, + &signer, + platform_version, + ); + + let st_vectors = json!({ + "platform_repo_tag": "v4.0.0", + "protocol_version": protocol_version, + "signature_scheme": "double-SHA256 of signable bytes; 65-byte compact recoverable ECDSA; header byte = 27 + recovery_id + 4 (compressed)", + "keys": { + "master": {"id": 0, "private_key_hex": hex_of(&MASTER_KEY_PRIV), "public_key_hex": hex_of(&master_pub)}, + "high": {"id": 1, "private_key_hex": hex_of(&HIGH_KEY_PRIV), "public_key_hex": hex_of(&high_pub)}, + "asset_lock": {"private_key_hex": hex_of(&ASSET_LOCK_PRIV), "public_key_hex": hex_of(&asset_lock_pub)}, + }, + "identity_create_instant": st_json(&st_instant, json!({ + "transaction_hex": hex_of(&tx_bytes), + "instant_lock_hex": hex_of(&islock_bytes), + "output_index": 0, + "out_point_hex": hex_of(&instant_outpoint), + "identity_id_hex": hex_of(instant_identity_id.as_slice()), + })), + "identity_create_chain": st_json(&st_chain, json!({ + "core_chain_locked_height": 1_050_000, + "out_point_hex": hex_of(&chain_outpoint_raw), + "identity_id_hex": hex_of(chain_identity_id.as_slice()), + })), + "dpns_preorder": st_json(&st_preorder, json!({ + "owner_id_hex": hex_of(owner.as_slice()), + "identity_contract_nonce": 2, + "entropy_hex": hex_of(&salted_domain_hash), + "salt_hex": hex_of(&PREORDER_SALT), + "label": label, + "normalized_label": normalized_label, + "salted_domain_hash_hex": hex_of(&salted_domain_hash), + "document_id_hex": hex_of(preorder_id.as_slice()), + "signature_public_key_id": 1, + })), + "dpns_domain_contested": st_json(&st_domain, json!({ + "owner_id_hex": hex_of(owner.as_slice()), + "identity_contract_nonce": 0x1234, + "entropy_hex": hex_of(&PREORDER_SALT), + "salt_hex": hex_of(&PREORDER_SALT), + "label": label, + "normalized_label": normalized_label, + "document_id_hex": hex_of(domain_id.as_slice()), + "signature_public_key_id": 1, + "contested": true, + "prefunded_voting_balance_credits": 20_000_000_000u64, + })), + "dpns_domain": st_json(&st_domain2, json!({ + "owner_id_hex": hex_of(owner.as_slice()), + "identity_contract_nonce": 5, + "entropy_hex": hex_of(&PREORDER_SALT), + "salt_hex": hex_of(&PREORDER_SALT), + "label": label2, + "normalized_label": normalized_label2, + "document_id_hex": hex_of(domain2_id.as_slice()), + "signature_public_key_id": 1, + "contested": false, + })), + "dashpay_profile_create": st_json(&st_profile_create, json!({ + "owner_id_hex": hex_of(owner.as_slice()), + "identity_contract_nonce": 3, + "entropy_hex": hex_of(&profile_entropy), + "document_id_hex": hex_of(profile_id.as_slice()), + "signature_public_key_id": 1, + "display_name": "Alice в Wonderland", + "public_message": "hello platform", + "avatar_url": "https://example.com/a.png", + "avatar_hash_hex": hex_of(&[0x88; 32]), + "avatar_fingerprint_hex": hex_of(&[0x99; 8]), + })), + "dashpay_profile_replace": st_json(&st_profile_replace, json!({ + "owner_id_hex": hex_of(owner.as_slice()), + "identity_contract_nonce": 4, + "revision": 2, + "document_id_hex": hex_of(profile_id.as_slice()), + "signature_public_key_id": 1, + "public_message": "updated message", + })), + "dashpay_contact_request": st_json(&st_contact, json!({ + "owner_id_hex": hex_of(owner.as_slice()), + "identity_contract_nonce": 6, + "entropy_hex": hex_of(&contact_entropy), + "document_id_hex": hex_of(contact_id.as_slice()), + "signature_public_key_id": 1, + "to_user_id_hex": hex_of(to_user_id.as_slice()), + "encrypted_public_key_hex": hex_of(&[0xab; 96]), + "sender_key_index": 2, + "recipient_key_index": 3, + "account_reference": 0x0badc0deu32, + "encrypted_account_label_hex": hex_of(&[0xcd; 48]), + })), + "contested_labels": contested_cases, + }); + + std::fs::create_dir_all(&out_dir).expect("create output dir"); + std::fs::write( + format!("{}/dpp_identity_vectors.json", out_dir), + serde_json::to_string_pretty(&identity_vectors).unwrap() + "\n", + ) + .expect("write identity vectors"); + std::fs::write( + format!("{}/dpp_st_vectors.json", out_dir), + serde_json::to_string_pretty(&st_vectors).unwrap() + "\n", + ) + .expect("write st vectors"); + println!( + "wrote vectors for protocol version {} to {}", + protocol_version, out_dir + ); +} diff --git a/src/Makefile.am b/src/Makefile.am index 8cd27c48bcbf..06cb40f87068 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -693,6 +693,13 @@ libdash_platform_a_SOURCES = \ crypto/blake3/blake3_impl.h \ crypto/blake3/blake3_portable.c \ platform/client.h \ + platform/dpp/bincode.cpp \ + platform/dpp/bincode.h \ + platform/dpp/document.cpp \ + platform/dpp/document.h \ + platform/dpp/identity.cpp \ + platform/dpp/identity.h \ + platform/dpp/statetransitions.cpp \ platform/params.cpp \ platform/params.h \ platform/proof/grovedb.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 556b29081258..67731b00a0ac 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -210,8 +210,11 @@ BITCOIN_TESTS =\ if ENABLE_PLATFORM_GUI BITCOIN_TESTS += \ + test/platform_dpp_tests.cpp \ test/platform_proof_tests.cpp JSON_TEST_FILES += \ + test/data/platform/dpp_identity_vectors.json \ + test/data/platform/dpp_st_vectors.json \ test/data/platform/element_vectors.json \ test/data/platform/grovedb_proof_vectors.json \ test/data/platform/merk_proof_vectors.json diff --git a/src/platform/dpp/bincode.cpp b/src/platform/dpp/bincode.cpp new file mode 100644 index 000000000000..8b03ef18aa62 --- /dev/null +++ b/src/platform/dpp/bincode.cpp @@ -0,0 +1,164 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include + +namespace platform::dpp { + +namespace { +constexpr uint8_t VARINT_U16{0xfb}; // 251 +constexpr uint8_t VARINT_U32{0xfc}; // 252 +constexpr uint8_t VARINT_U64{0xfd}; // 253 +constexpr uint8_t VARINT_U128{0xfe}; // 254 (unused by DPP; rejected) + +//! Zigzag encoding as used by bincode-v2 for signed varints. +uint64_t ZigZag(int64_t value) +{ + return (static_cast(value) << 1) ^ static_cast(value >> 63); +} + +int64_t UnZigZag(uint64_t value) +{ + return static_cast((value >> 1) ^ (~(value & 1) + 1)); +} +} // namespace + +void Writer::WriteVarint(uint64_t value) +{ + if (value < VARINT_U16) { + m_data.push_back(static_cast(value)); + } else if (value <= 0xffff) { + m_data.push_back(VARINT_U16); + m_data.push_back(static_cast(value >> 8)); + m_data.push_back(static_cast(value)); + } else if (value <= 0xffffffff) { + m_data.push_back(VARINT_U32); + for (int shift = 24; shift >= 0; shift -= 8) { + m_data.push_back(static_cast(value >> shift)); + } + } else { + m_data.push_back(VARINT_U64); + for (int shift = 56; shift >= 0; shift -= 8) { + m_data.push_back(static_cast(value >> shift)); + } + } +} + +void Writer::WriteSignedVarint(int64_t value) +{ + WriteVarint(ZigZag(value)); +} + +void Writer::WriteByteVec(Span bytes) +{ + WriteVarint(bytes.size()); + WriteBytes(bytes); +} + +void Writer::WriteString(const std::string& str) +{ + WriteVarint(str.size()); + m_data.insert(m_data.end(), str.begin(), str.end()); +} + +bool Reader::SetError(const std::string& error) +{ + if (m_error.empty()) m_error = error; + return false; +} + +bool Reader::ReadU8(uint8_t& out) +{ + if (HasError()) return false; + if (Remaining() < 1) return SetError("bincode: unexpected end of data"); + out = m_data[m_pos++]; + return true; +} + +bool Reader::ReadInto(Span out) +{ + if (HasError()) return false; + if (Remaining() < out.size()) return SetError("bincode: unexpected end of data"); + std::copy(m_data.begin() + m_pos, m_data.begin() + m_pos + out.size(), out.begin()); + m_pos += out.size(); + return true; +} + +bool Reader::ReadBool(bool& out) +{ + uint8_t byte; + if (!ReadU8(byte)) return false; + if (byte > 1) return SetError(strprintf("bincode: invalid bool byte 0x%02x", byte)); + out = byte == 1; + return true; +} + +bool Reader::ReadVarint(uint64_t& out) +{ + uint8_t first; + if (!ReadU8(first)) return false; + if (first < VARINT_U16) { + out = first; + return true; + } + int width{0}; + switch (first) { + case VARINT_U16: width = 2; break; + case VARINT_U32: width = 4; break; + case VARINT_U64: width = 8; break; + case VARINT_U128: + default: + return SetError(strprintf("bincode: unsupported varint marker 0x%02x", first)); + } + if (Remaining() < static_cast(width)) return SetError("bincode: truncated varint"); + uint64_t value{0}; + for (int i = 0; i < width; ++i) { + value = (value << 8) | m_data[m_pos++]; // big-endian + } + out = value; + return true; +} + +bool Reader::ReadSignedVarint(int64_t& out) +{ + uint64_t raw; + if (!ReadVarint(raw)) return false; + out = UnZigZag(raw); + return true; +} + +bool Reader::ReadByteVec(Bytes& out, size_t max_len) +{ + uint64_t len; + if (!ReadVarint(len)) return false; + if (len > max_len) return SetError(strprintf("bincode: byte vector length %d exceeds limit %d", len, max_len)); + if (Remaining() < len) return SetError("bincode: truncated byte vector"); + out.assign(m_data.begin() + m_pos, m_data.begin() + m_pos + len); + m_pos += len; + return true; +} + +bool Reader::ReadString(std::string& out, size_t max_len) +{ + uint64_t len; + if (!ReadVarint(len)) return false; + if (len > max_len) return SetError(strprintf("bincode: string length %d exceeds limit %d", len, max_len)); + if (Remaining() < len) return SetError("bincode: truncated string"); + out.assign(m_data.begin() + m_pos, m_data.begin() + m_pos + len); + m_pos += len; + return true; +} + +bool Reader::ReadOptionTag(bool& has_value) +{ + uint8_t byte; + if (!ReadU8(byte)) return false; + if (byte > 1) return SetError(strprintf("bincode: invalid Option tag 0x%02x", byte)); + has_value = byte == 1; + return true; +} + +} // namespace platform::dpp diff --git a/src/platform/dpp/bincode.h b/src/platform/dpp/bincode.h new file mode 100644 index 000000000000..6f3599c06285 --- /dev/null +++ b/src/platform/dpp/bincode.h @@ -0,0 +1,101 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_DPP_BINCODE_H +#define BITCOIN_PLATFORM_DPP_BINCODE_H + +#include + +#include +#include +#include + +/** + * bincode-v2 codec in the configuration used by rs-platform-serialization + * for every DPP object: bincode::config::standard().with_big_endian(), i.e. + * variable-width integers with BIG-endian payloads. + * + * Source: dashpay/platform (tag v4.0.0) + * packages/rs-platform-serialization-derive/src/{serialize,deserialize}_*.rs — + * every derived PlatformSerialize/PlatformDeserialize impl builds + * `bincode::config::standard().with_big_endian()`. Note this matches the + * GroveDB proof envelope config; bincode's default little-endian standard() + * config is NOT used anywhere on the DPP wire (validated against the + * rs-dpp-generated vectors in src/test/data/platform/). + * + * Varint scheme (bincode "varint" encoding): + * value < 251 -> 1 byte + * value <= 0xffff -> 0xfb marker + u16 big-endian + * value <= 0xffffffff -> 0xfc marker + u32 big-endian + * otherwise -> 0xfd marker + u64 big-endian + * (0xfe marker introduces u128, which DPP never uses; rejected on read) + * Signed integers are zigzag-encoded and then written as unsigned varints. + * Fixed-size arrays are raw bytes without a length prefix; Vec/String + * carry a varint length; Option is one byte (0/1); enum discriminants are + * the varint variant index in declaration order. + */ +namespace platform::dpp { + +using Bytes = std::vector; + +class Writer +{ +public: + void WriteU8(uint8_t byte) { m_data.push_back(byte); } + void WriteBytes(Span bytes) { m_data.insert(m_data.end(), bytes.begin(), bytes.end()); } + void WriteBool(bool value) { m_data.push_back(value ? 1 : 0); } + //! Unsigned bincode varint (big-endian payloads). + void WriteVarint(uint64_t value); + //! Signed bincode varint: zigzag, then WriteVarint. + void WriteSignedVarint(int64_t value); + //! Vec / BinaryData: varint length + raw bytes. + void WriteByteVec(Span bytes); + //! String: varint byte length + UTF-8 bytes. + void WriteString(const std::string& str); + //! Option tag: 1 byte, 0 (None) or 1 (Some; value follows). + void WriteOptionTag(bool has_value) { m_data.push_back(has_value ? 1 : 0); } + + const Bytes& Data() const { return m_data; } + Bytes Take() { return std::move(m_data); } + +private: + Bytes m_data; +}; + +//! Forward-only reader; all Read* methods return false on truncation or +//! malformed input and latch the first error. No exceptions. +class Reader +{ +public: + explicit Reader(Span data) : m_data(data) {} + + size_t Remaining() const { return m_data.size() - m_pos; } + bool IsEof() const { return m_pos == m_data.size(); } + bool HasError() const { return !m_error.empty(); } + const std::string& GetError() const { return m_error; } + //! Records an error (first error wins) and returns false. + bool SetError(const std::string& error); + + bool ReadU8(uint8_t& out); + //! Reads exactly out.size() raw bytes (fixed-size array). + bool ReadInto(Span out); + bool ReadBool(bool& out); + bool ReadVarint(uint64_t& out); + bool ReadSignedVarint(int64_t& out); + //! Vec: varint length (bounded by max_len) + raw bytes. + bool ReadByteVec(Bytes& out, size_t max_len); + //! String: varint length (bounded by max_len) + UTF-8 bytes. + bool ReadString(std::string& out, size_t max_len); + //! Option tag: exactly 0 or 1. + bool ReadOptionTag(bool& has_value); + +private: + Span m_data; + size_t m_pos{0}; + std::string m_error; +}; + +} // namespace platform::dpp + +#endif // BITCOIN_PLATFORM_DPP_BINCODE_H diff --git a/src/platform/dpp/document.cpp b/src/platform/dpp/document.cpp new file mode 100644 index 000000000000..307fb77a9d52 --- /dev/null +++ b/src/platform/dpp/document.cpp @@ -0,0 +1,139 @@ +// Copyright (c) 2026 The Dash 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 + +namespace platform::dpp { + +Value Value::U32(uint32_t value) +{ + Value ret; + ret.m_kind = ValueKind::U32; + ret.m_int = value; + return ret; +} + +Value Value::U64(uint64_t value) +{ + Value ret; + ret.m_kind = ValueKind::U64; + ret.m_int = value; + return ret; +} + +Value Value::MakeBytes(Bytes bytes) +{ + Value ret; + ret.m_kind = ValueKind::BYTES; + ret.m_bytes = std::move(bytes); + return ret; +} + +Value Value::MakeBytes32(const std::array& bytes) +{ + Value ret; + ret.m_kind = ValueKind::BYTES32; + ret.m_bytes.assign(bytes.begin(), bytes.end()); + return ret; +} + +Value Value::Id(const Identifier& id) +{ + Value ret; + ret.m_kind = ValueKind::IDENTIFIER; + ret.m_bytes.assign(id.begin(), id.end()); + return ret; +} + +Value Value::Text(std::string text) +{ + Value ret; + ret.m_kind = ValueKind::TEXT; + ret.m_text = std::move(text); + return ret; +} + +Value Value::Boolean(bool value) +{ + Value ret; + ret.m_kind = ValueKind::BOOL; + ret.m_bool = value; + return ret; +} + +Value Value::MakeMap(Map entries) +{ + Value ret; + ret.m_kind = ValueKind::MAP; + ret.m_map = std::make_shared(std::move(entries)); + return ret; +} + +void Value::Encode(Writer& writer) const +{ + writer.WriteVarint(static_cast(m_kind)); + switch (m_kind) { + case ValueKind::U32: + case ValueKind::U64: + writer.WriteVarint(m_int); + break; + case ValueKind::BYTES: + writer.WriteByteVec(m_bytes); + break; + case ValueKind::BYTES32: + case ValueKind::IDENTIFIER: + // Fixed-size arrays carry no length prefix. + assert(m_bytes.size() == 32); + writer.WriteBytes(m_bytes); + break; + case ValueKind::TEXT: + writer.WriteString(m_text); + break; + case ValueKind::BOOL: + writer.WriteBool(m_bool); + break; + case ValueKind::MAP: + assert(m_map); + writer.WriteVarint(m_map->size()); + for (const auto& [key, value] : *m_map) { + key.Encode(writer); + value.Encode(writer); + } + break; + default: + assert(false); // unsupported Value kind + } +} + +void EncodeDocumentData(Writer& writer, const DocumentData& data) +{ + writer.WriteVarint(data.size()); + for (const auto& [key, value] : data) { + writer.WriteString(key); + value.Encode(writer); + } +} + +Identifier GenerateDocumentId(const Identifier& contract_id, + const Identifier& owner_id, + const std::string& document_type_name, + Span entropy) +{ + CHash256 hasher; + hasher.Write(contract_id); + hasher.Write(owner_id); + hasher.Write(MakeUCharSpan(document_type_name)); + hasher.Write(entropy); + Identifier ret; + uint256 hash; + hasher.Finalize(hash); + std::copy(hash.begin(), hash.end(), ret.begin()); + return ret; +} + +} // namespace platform::dpp diff --git a/src/platform/dpp/document.h b/src/platform/dpp/document.h new file mode 100644 index 000000000000..a20680d86ac9 --- /dev/null +++ b/src/platform/dpp/document.h @@ -0,0 +1,106 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_DPP_DOCUMENT_H +#define BITCOIN_PLATFORM_DPP_DOCUMENT_H + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/** + * platform_value::Value encoding for document properties, restricted to the + * kinds the DPNS and DashPay document types need. Mirrors dashpay/platform + * (tag v4.0.0) packages/rs-platform-value/src/lib.rs: `Value` is a plain + * bincode Encode enum, so each value is the varint variant index in + * declaration order followed by the payload. + * + * Document create/replace transitions carry the user properties as + * BTreeMap (rs-dpp + * .../document_create_transition/v0/mod.rs `data`), which bincode encodes as + * a varint entry count followed by (key, value) pairs in ascending byte-wise + * key order — matched here by std::map. + */ +namespace platform::dpp { + +//! Variant indexes of platform_value::Value (declaration order in +//! rs-platform-value/src/lib.rs). +enum class ValueKind : uint8_t { + U128 = 0, + I128 = 1, + U64 = 2, + I64 = 3, + U32 = 4, + I32 = 5, + U16 = 6, + I16 = 7, + U8 = 8, + I8 = 9, + BYTES = 10, + BYTES20 = 11, + BYTES32 = 12, + BYTES36 = 13, + ENUM_U8 = 14, + ENUM_STRING = 15, + IDENTIFIER = 16, + FLOAT = 17, + TEXT = 18, + BOOL = 19, + NULL_VALUE = 20, + ARRAY = 21, + MAP = 22, +}; + +//! Subset of platform_value::Value used by the four GUI document types. +class Value +{ +public: + using Map = std::vector>; + + static Value U32(uint32_t value); + static Value U64(uint64_t value); + static Value MakeBytes(Bytes bytes); + static Value MakeBytes32(const std::array& bytes); + static Value Id(const Identifier& id); + static Value Text(std::string text); + static Value Boolean(bool value); + static Value MakeMap(Map entries); + + void Encode(Writer& writer) const; + +private: + Value() = default; + + ValueKind m_kind{ValueKind::NULL_VALUE}; + uint64_t m_int{0}; + bool m_bool{false}; + Bytes m_bytes; //!< BYTES / BYTES32 / IDENTIFIER payload + std::string m_text; //!< TEXT payload + std::shared_ptr m_map; //!< MAP payload +}; + +//! Document properties in BTreeMap order. +using DocumentData = std::map; + +//! Encodes a document data map: varint count + sorted (key, value) pairs. +void EncodeDocumentData(Writer& writer, const DocumentData& data); + +//! Document id: DSHA256(contract_id || owner_id || type_name || entropy), +//! per rs-dpp src/document/generate_document_id.rs +//! Document::generate_document_id_v0. +Identifier GenerateDocumentId(const Identifier& contract_id, + const Identifier& owner_id, + const std::string& document_type_name, + Span entropy); + +} // namespace platform::dpp + +#endif // BITCOIN_PLATFORM_DPP_DOCUMENT_H diff --git a/src/platform/dpp/identity.cpp b/src/platform/dpp/identity.cpp new file mode 100644 index 000000000000..7f53eb781d72 --- /dev/null +++ b/src/platform/dpp/identity.cpp @@ -0,0 +1,159 @@ +// Copyright (c) 2026 The Dash 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 + +namespace platform::dpp { + +namespace { + +//! rs-dpp limits: identity 15000 bytes, standalone key 2000 bytes. +constexpr size_t MAX_IDENTITY_SIZE{15000}; +constexpr size_t MAX_KEY_SIZE{2000}; +constexpr size_t MAX_KEY_DATA{256}; +constexpr size_t MAX_KEYS{100}; +constexpr size_t MAX_DOCUMENT_TYPE_NAME{256}; + +//! Reads one IdentityPublicKey (enum { V0 } + IdentityPublicKeyV0 fields). +bool ReadIdentityPublicKey(Reader& reader, platform::IdentityPublicKey& out) +{ + uint64_t version; + if (!reader.ReadVarint(version)) return false; + if (version != 0) return reader.SetError(strprintf("unsupported IdentityPublicKey version %d", version)); + + uint64_t id, purpose, security_level, key_type; + if (!reader.ReadVarint(id)) return false; + if (id > std::numeric_limits::max()) return reader.SetError("key id out of range"); + if (!reader.ReadVarint(purpose)) return false; + if (purpose > 6) return reader.SetError(strprintf("unknown key purpose %d", purpose)); + if (!reader.ReadVarint(security_level)) return false; + if (security_level > 3) return reader.SetError(strprintf("unknown key security level %d", security_level)); + + // contract_bounds: Option; both variants start with the + // bound contract id, SingleContractDocumentType adds a type name. The + // GUI types do not surface bounds, so the payload is validated and + // skipped. + bool has_bounds; + if (!reader.ReadOptionTag(has_bounds)) return false; + if (has_bounds) { + uint64_t bounds_kind; + if (!reader.ReadVarint(bounds_kind)) return false; + if (bounds_kind > 1) return reader.SetError(strprintf("unknown contract bounds kind %d", bounds_kind)); + Identifier bound_contract; + if (!reader.ReadInto(bound_contract)) return false; + if (bounds_kind == 1) { + std::string document_type_name; + if (!reader.ReadString(document_type_name, MAX_DOCUMENT_TYPE_NAME)) return false; + } + } + + if (!reader.ReadVarint(key_type)) return false; + if (key_type > 4) return reader.SetError(strprintf("unknown key type %d", key_type)); + + bool read_only; + if (!reader.ReadBool(read_only)) return false; + + Bytes data; + if (!reader.ReadByteVec(data, MAX_KEY_DATA)) return false; + + bool has_disabled_at; + if (!reader.ReadOptionTag(has_disabled_at)) return false; + uint64_t disabled_at{0}; + if (has_disabled_at && !reader.ReadVarint(disabled_at)) return false; + + out.id = static_cast(id); + out.purpose = static_cast(purpose); + out.security_level = static_cast(security_level); + out.type = static_cast(key_type); + out.read_only = read_only; + out.data = std::move(data); + out.disabled_at = has_disabled_at ? std::optional{disabled_at} : std::nullopt; + return true; +} + +} // namespace + +std::optional DecodeIdentity(Span bytes, std::string& error) +{ + if (bytes.size() > MAX_IDENTITY_SIZE) { + error = "identity too large"; + return std::nullopt; + } + Reader reader{bytes}; + uint64_t version; + if (!reader.ReadVarint(version) || version != 0) { + error = reader.HasError() ? reader.GetError() : strprintf("unsupported Identity version %d", version); + return std::nullopt; + } + + platform::Identity identity; + if (!reader.ReadInto(identity.id)) { + error = reader.GetError(); + return std::nullopt; + } + + uint64_t num_keys; + if (!reader.ReadVarint(num_keys) || num_keys > MAX_KEYS) { + error = reader.HasError() ? reader.GetError() : "too many identity keys"; + return std::nullopt; + } + std::optional last_key_id; + for (uint64_t i = 0; i < num_keys; ++i) { + uint64_t map_key_id; + platform::IdentityPublicKey key; + if (!reader.ReadVarint(map_key_id) || !ReadIdentityPublicKey(reader, key)) { + error = reader.GetError(); + return std::nullopt; + } + // BTreeMap keys are strictly ascending. + if (last_key_id && map_key_id <= *last_key_id) { + error = "identity key ids not strictly ascending"; + return std::nullopt; + } + last_key_id = map_key_id; + if (map_key_id != key.id) { + error = strprintf("identity key map id %d does not match key id %d", map_key_id, key.id); + return std::nullopt; + } + identity.public_keys.push_back(std::move(key)); + } + + if (!reader.ReadVarint(identity.balance) || !reader.ReadVarint(identity.revision)) { + error = reader.GetError(); + return std::nullopt; + } + if (!reader.IsEof()) { + error = "trailing bytes after identity"; + return std::nullopt; + } + return identity; +} + +std::optional DecodeIdentityPublicKey(Span bytes, + std::string& error) +{ + if (bytes.size() > MAX_KEY_SIZE) { + error = "identity public key too large"; + return std::nullopt; + } + Reader reader{bytes}; + platform::IdentityPublicKey key; + if (!ReadIdentityPublicKey(reader, key)) { + error = reader.GetError(); + return std::nullopt; + } + if (!reader.IsEof()) { + error = "trailing bytes after identity public key"; + return std::nullopt; + } + return key; +} + +} // namespace platform::dpp diff --git a/src/platform/dpp/identity.h b/src/platform/dpp/identity.h new file mode 100644 index 000000000000..b5c38fa19a43 --- /dev/null +++ b/src/platform/dpp/identity.h @@ -0,0 +1,43 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_DPP_IDENTITY_H +#define BITCOIN_PLATFORM_DPP_IDENTITY_H + +#include +#include + +#include +#include + +/** + * Decoding of platform-serialized Identity / IdentityPublicKey objects + * (bincode standard()+big_endian(), see platform/dpp/bincode.h) into the + * platform::Identity / platform::IdentityPublicKey GUI types. + * + * Wire layout mirrors dashpay/platform (tag v4.0.0): + * - Identity: rs-dpp/src/identity/identity.rs — enum { V0 } with + * #[platform_serialize(limit = 15000, unversioned)]; IdentityV0 is + * { id: Identifier, public_keys: BTreeMap, + * balance: u64, revision: u64 } (rs-dpp/src/identity/v0/mod.rs). + * - IdentityPublicKey: rs-dpp/src/identity/identity_public_key/mod.rs — + * enum { V0 } with #[platform_serialize(limit = 2000, unversioned)]; + * IdentityPublicKeyV0 is { id, purpose, security_level, + * contract_bounds: Option, key_type, read_only, + * data: BinaryData, disabled_at: Option } + * (rs-dpp/src/identity/identity_public_key/v0/mod.rs). + */ +namespace platform::dpp { + +//! Decodes a platform-serialized Identity. Returns std::nullopt and sets +//! error on malformed input (including trailing bytes). +std::optional DecodeIdentity(Span bytes, std::string& error); + +//! Decodes a standalone platform-serialized IdentityPublicKey. +std::optional DecodeIdentityPublicKey(Span bytes, + std::string& error); + +} // namespace platform::dpp + +#endif // BITCOIN_PLATFORM_DPP_IDENTITY_H diff --git a/src/platform/dpp/statetransitions.cpp b/src/platform/dpp/statetransitions.cpp new file mode 100644 index 000000000000..e365c2e3a8c3 --- /dev/null +++ b/src/platform/dpp/statetransitions.cpp @@ -0,0 +1,528 @@ +// Copyright (c) 2026 The Dash 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 +#include +#include +#include + +#include +#include + +/** + * DPP state transition construction, byte-exact with dashpay/platform tag + * v4.0.0 (Platform protocol version 12), validated against rs-dpp-generated + * vectors in src/test/data/platform/dpp_st_vectors.json. + * + * Wire layout (bincode standard()+big_endian(), see platform/dpp/bincode.h): + * - StateTransition (rs-dpp/src/state_transition/mod.rs) is an unversioned + * enum; variant indexes: DataContractCreate=0, DataContractUpdate=1, + * Batch=2, IdentityCreate=3, ... + * - Batch document transitions serialize as BatchTransition::V1 with + * DocumentBaseTransition::V1 (token_payment_info = None): protocol + * versions 9..=12 all pin STATE_TRANSITION_SERIALIZATION_VERSIONS_V2 + * (rs-platform-version/src/version/dpp_versions/ + * dpp_state_transition_serialization_versions/v2.rs) whose + * batch_state_transition/document_base_state_transition + * default_current_version is 1. + * - Signing (rust-dashcore dash/src/signer.rs, rs-dpp state_transition + * sign_by_private_key): digest = double-SHA256 of the transition's + * signable bytes (the serialization with every + * #[platform_signable(exclude_from_sig_hash)] field omitted); the + * signature is the 65-byte compact recoverable ECDSA form whose header + * byte is 27 + recovery_id + 4 (compressed-pubkey convention). + */ +namespace platform::st { + +namespace { + +using dpp::Bytes; +using dpp::DocumentData; +using dpp::Value; +using dpp::Writer; + +//! StateTransition enum variant indexes. +constexpr uint64_t ST_BATCH{2}; +constexpr uint64_t ST_IDENTITY_CREATE{3}; +//! BatchTransition::V1 (BatchedTransition vector; protocol >= 9). +constexpr uint64_t BATCH_V1{1}; +//! BatchedTransition::Document / ::Token variant indexes. +constexpr uint64_t BATCHED_DOCUMENT{0}; +//! DocumentTransition variant indexes. +constexpr uint64_t DOC_CREATE{0}; +constexpr uint64_t DOC_REPLACE{1}; +//! DocumentBaseTransition::V1 (adds Option). +constexpr uint64_t DOC_BASE_V1{1}; +//! AssetLockProof variant indexes. +constexpr uint64_t PROOF_INSTANT{0}; +constexpr uint64_t PROOF_CHAIN{1}; + +constexpr size_t COMPACT_SIG_SIZE{65}; +constexpr size_t COMPRESSED_PUBKEY_SIZE{33}; + +//! Vote-resolution fund attached to contested DPNS domain creates: +//! rs-platform-version .../fee/vote_resolution_fund_fees/v1.rs +//! contested_document_vote_resolution_fund_required_amount (0.2 DASH in +//! credits), keyed by the contested unique index of the DPNS domain type. +constexpr uint64_t CONTESTED_DOCUMENT_FUND_CREDITS{20000000000}; +const std::string CONTESTED_INDEX_NAME{"parentNameAndLabel"}; + +uint256 DoubleSha(Span data) +{ + uint256 out; + CHash256().Write(data).Finalize(out); + return out; +} + +uint256 SingleSha(Span data) +{ + uint256 out; + CSHA256().Write(data.data(), data.size()).Finalize(out.begin()); + return out; +} + +std::array ToArray(const uint256& hash) +{ + std::array out; + std::copy(hash.begin(), hash.end(), out.begin()); + return out; +} + +//! Deterministic document entropy for DashPay documents: +//! DSHA256(owner_id || document_type_name || identity_contract_nonce LE64). +//! rs-dpp leaves entropy to the caller (the Rust SDK draws it at random); +//! deriving it from the (identity, contract nonce) pair keeps rebuilds of +//! the same transition byte-identical, so a GUI retry cannot register a +//! second document. DPNS documents use flow-specific entropy instead +//! (preorder: salted domain hash; domain: preorder salt). +std::array DeriveEntropy(const Identifier& owner, + const std::string& document_type_name, + uint64_t identity_contract_nonce) +{ + CHash256 hasher; + hasher.Write(owner); + hasher.Write(MakeUCharSpan(document_type_name)); + uint8_t nonce_le[8]; + WriteLE64(nonce_le, identity_contract_nonce); + hasher.Write(nonce_le); + uint256 hash; + hasher.Finalize(hash); + return ToArray(hash); +} + +//! One document create/replace inside a Batch transition. +struct DocumentSpec { + bool is_replace{false}; + Identifier document_id{}; + uint64_t identity_contract_nonce{0}; + std::string document_type_name; + Identifier data_contract_id{}; + std::array entropy{}; //!< create only + uint64_t revision{0}; //!< replace only + DocumentData data; + //! (index name, credits); create only. + std::optional> prefunded_voting_balance; +}; + +//! Serializes a single-document BatchTransition::V1. With signature == +//! nullptr the signable form is produced (signature_public_key_id and +//! signature are #[platform_signable(exclude_from_sig_hash)] in +//! BatchTransitionV1). +Bytes EncodeDocumentBatch(const Identifier& owner_id, + const DocumentSpec& spec, + uint32_t signature_public_key_id, + const Bytes* signature) +{ + Writer writer; + writer.WriteVarint(ST_BATCH); + writer.WriteVarint(BATCH_V1); + writer.WriteBytes(owner_id); + writer.WriteVarint(1); // transitions: Vec + writer.WriteVarint(BATCHED_DOCUMENT); + writer.WriteVarint(spec.is_replace ? DOC_REPLACE : DOC_CREATE); + writer.WriteVarint(0); // DocumentCreateTransition::V0 / DocumentReplaceTransition::V0 + // DocumentBaseTransition::V1 + writer.WriteVarint(DOC_BASE_V1); + writer.WriteBytes(spec.document_id); + writer.WriteVarint(spec.identity_contract_nonce); + writer.WriteString(spec.document_type_name); + writer.WriteBytes(spec.data_contract_id); + writer.WriteOptionTag(false); // token_payment_info + if (spec.is_replace) { + writer.WriteVarint(spec.revision); + EncodeDocumentData(writer, spec.data); + } else { + writer.WriteBytes(spec.entropy); + EncodeDocumentData(writer, spec.data); + writer.WriteOptionTag(spec.prefunded_voting_balance.has_value()); + if (spec.prefunded_voting_balance) { + writer.WriteString(spec.prefunded_voting_balance->first); + writer.WriteVarint(spec.prefunded_voting_balance->second); + } + } + writer.WriteVarint(0); // user_fee_increase + if (signature != nullptr) { + writer.WriteVarint(signature_public_key_id); + writer.WriteByteVec(*signature); + } + return writer.Take(); +} + +Result SignDocumentBatch(const Identifier& owner_id, + const DocumentSpec& spec, + uint32_t signature_public_key_id, + const Signer& signer) +{ + if (!signer) return {std::nullopt, "no signer provided"}; + const Bytes signable{EncodeDocumentBatch(owner_id, spec, signature_public_key_id, nullptr)}; + const uint256 digest{DoubleSha(signable)}; + Bytes signature; + if (!signer(digest, signature)) { + return {std::nullopt, "signing failed (wallet locked or key unavailable)"}; + } + if (signature.size() != COMPACT_SIG_SIZE) { + return {std::nullopt, strprintf("unexpected signature size %d (want %d)", signature.size(), COMPACT_SIG_SIZE)}; + } + BuiltTransition built; + built.bytes = EncodeDocumentBatch(owner_id, spec, signature_public_key_id, &signature); + built.hash = SingleSha(built.bytes); + return {std::move(built), ""}; +} + +//! Serializes an IdentityCreateTransition (::V0). With signatures == nullptr +//! the signable form is produced: per-key signatures, the outer signature +//! and the identity id are all excluded from the signable bytes +//! (rs-dpp .../identity_create_transition/v0/mod.rs, .../public_key_in_creation/v0/mod.rs). +Bytes EncodeIdentityCreate(const std::vector& keys, + const std::variant& proof, + const std::vector* key_signatures, + const Bytes* signature, + const Identifier* identity_id) +{ + Writer writer; + writer.WriteVarint(ST_IDENTITY_CREATE); + writer.WriteVarint(0); // IdentityCreateTransition::V0 + writer.WriteVarint(keys.size()); + for (size_t i = 0; i < keys.size(); ++i) { + const NewIdentityKey& key{keys[i]}; + // IdentityPublicKeyInCreation::V0; field order differs from + // IdentityPublicKeyV0 (key_type before purpose). + writer.WriteVarint(0); + writer.WriteVarint(key.id); + writer.WriteVarint(0); // KeyType::ECDSA_SECP256K1 + writer.WriteVarint(static_cast(key.purpose)); + writer.WriteVarint(static_cast(key.security_level)); + writer.WriteOptionTag(false); // contract_bounds + writer.WriteBool(false); // read_only + writer.WriteByteVec(key.pubkey); + if (key_signatures != nullptr) writer.WriteByteVec((*key_signatures)[i]); + } + if (const auto* instant = std::get_if(&proof)) { + // AssetLockProof::Instant serializes through serde as + // RawInstantLockProof { instant_lock, transaction, output_index } + // with the lock and transaction as raw consensus bytes + // (rs-dpp .../asset_lock_proof/instant/instant_asset_lock_proof.rs). + writer.WriteVarint(PROOF_INSTANT); + writer.WriteByteVec(instant->instant_lock); + writer.WriteByteVec(instant->transaction); + writer.WriteVarint(instant->output_index); + } else { + const auto& chain = std::get(proof); + // AssetLockProof::Chain serializes through serde as + // { core_chain_locked_height, out_point: { txid: bytes, vout } }. + writer.WriteVarint(PROOF_CHAIN); + writer.WriteVarint(chain.core_chain_locked_height); + writer.WriteByteVec(Span{chain.out_point}.first(32)); + writer.WriteVarint(ReadLE32(chain.out_point.data() + 32)); + } + writer.WriteVarint(0); // user_fee_increase + if (signature != nullptr) { + writer.WriteByteVec(*signature); + writer.WriteBytes(*identity_id); + } + return writer.Take(); +} + +} // namespace + +Identifier IdentityIdFromOutpoint(const std::array& out_point) +{ + return ToArray(DoubleSha(out_point)); +} + +Result BuildIdentityCreate( + const std::variant& proof, + const std::vector& keys, + const Signer& asset_lock_signer) +{ + if (keys.empty()) return {std::nullopt, "no identity keys provided"}; + if (!asset_lock_signer) return {std::nullopt, "no asset lock signer provided"}; + + // rs-dpp registers Identity::public_keys() (a BTreeMap keyed by id), so + // the keys serialize in ascending id order with no duplicates. + std::vector sorted_keys{keys}; + std::sort(sorted_keys.begin(), sorted_keys.end(), + [](const NewIdentityKey& a, const NewIdentityKey& b) { return a.id < b.id; }); + for (size_t i = 0; i < sorted_keys.size(); ++i) { + const NewIdentityKey& key{sorted_keys[i]}; + if (i > 0 && key.id == sorted_keys[i - 1].id) { + return {std::nullopt, strprintf("duplicate identity key id %d", key.id)}; + } + if (key.pubkey.size() != COMPRESSED_PUBKEY_SIZE) { + return {std::nullopt, strprintf("identity key %d: unexpected public key size %d", key.id, key.pubkey.size())}; + } + if (!key.signer) return {std::nullopt, strprintf("identity key %d: no signer", key.id)}; + } + + // Derive the identity id from the asset lock outpoint. + Identifier identity_id; + if (const auto* instant = std::get_if(&proof)) { + // The txid of a Dash special transaction is the double-SHA256 of its + // full serialization (including the extra payload), so the asset + // lock transaction does not need to be re-parsed here. + const uint256 txid{DoubleSha(instant->transaction)}; + std::array out_point; + std::copy(txid.begin(), txid.end(), out_point.begin()); + WriteLE32(out_point.data() + 32, instant->output_index); + identity_id = IdentityIdFromOutpoint(out_point); + } else { + identity_id = IdentityIdFromOutpoint(std::get(proof).out_point); + } + + // Every registered key proves ownership by signing the same digest as + // the asset-lock key: the double-SHA256 of the signable bytes (rs-dpp + // IdentityCreateTransitionMethodsV0::try_from_identity_with_signer_and_private_key). + const Bytes signable{EncodeIdentityCreate(sorted_keys, proof, nullptr, nullptr, nullptr)}; + const uint256 digest{DoubleSha(signable)}; + + std::vector key_signatures; + key_signatures.reserve(sorted_keys.size()); + for (const NewIdentityKey& key : sorted_keys) { + Bytes sig; + if (!key.signer(digest, sig)) { + return {std::nullopt, strprintf("identity key %d: signing failed", key.id)}; + } + if (sig.size() != COMPACT_SIG_SIZE) { + return {std::nullopt, strprintf("identity key %d: unexpected signature size %d", key.id, sig.size())}; + } + key_signatures.push_back(std::move(sig)); + } + Bytes asset_lock_signature; + if (!asset_lock_signer(digest, asset_lock_signature)) { + return {std::nullopt, "asset lock key: signing failed"}; + } + if (asset_lock_signature.size() != COMPACT_SIG_SIZE) { + return {std::nullopt, strprintf("asset lock key: unexpected signature size %d", asset_lock_signature.size())}; + } + + BuiltTransition built; + built.bytes = EncodeIdentityCreate(sorted_keys, proof, &key_signatures, &asset_lock_signature, &identity_id); + built.hash = SingleSha(built.bytes); + return {std::move(built), ""}; +} + +Result BuildDpnsPreorder( + const Identifier& identity, + uint64_t identity_contract_nonce, + const std::array& salted_domain_hash, + uint32_t signature_public_key_id, + const Signer& signer) +{ + DocumentSpec spec; + spec.identity_contract_nonce = identity_contract_nonce; + spec.document_type_name = "preorder"; + spec.data_contract_id = DPNS_CONTRACT_ID; + // The preorder document has no natural id source; the salted domain + // hash is already blinded and unique per (name, salt), so it doubles as + // the document entropy. + spec.entropy = salted_domain_hash; + spec.document_id = dpp::GenerateDocumentId(spec.data_contract_id, identity, + spec.document_type_name, spec.entropy); + spec.data.emplace("saltedDomainHash", Value::MakeBytes32(salted_domain_hash)); + return SignDocumentBatch(identity, spec, signature_public_key_id, signer); +} + +Result BuildDpnsDomain( + const Identifier& identity, + uint64_t identity_contract_nonce, + const std::string& label, + const std::string& normalized_label, + const std::string& parent_domain, + const std::array& preorder_salt, + uint32_t signature_public_key_id, + const Signer& signer) +{ + if (label.empty() || normalized_label.empty() || parent_domain.empty()) { + return {std::nullopt, "empty DPNS label or parent domain"}; + } + if (NormalizeLabel(label) != normalized_label) { + return {std::nullopt, "normalized label does not match label"}; + } + DocumentSpec spec; + spec.identity_contract_nonce = identity_contract_nonce; + spec.document_type_name = "domain"; + spec.data_contract_id = DPNS_CONTRACT_ID; + // The preorder salt is drawn fresh per registration attempt, making it a + // suitable deterministic document entropy for the paired domain create. + spec.entropy = preorder_salt; + spec.document_id = dpp::GenerateDocumentId(spec.data_contract_id, identity, + spec.document_type_name, spec.entropy); + spec.data.emplace("label", Value::Text(label)); + spec.data.emplace("normalizedLabel", Value::Text(normalized_label)); + spec.data.emplace("parentDomainName", Value::Text(parent_domain)); + spec.data.emplace("normalizedParentDomainName", Value::Text(NormalizeLabel(parent_domain))); + spec.data.emplace("preorderSalt", Value::MakeBytes32(preorder_salt)); + spec.data.emplace("records", Value::MakeMap({{Value::Text("identity"), Value::Id(identity)}})); + spec.data.emplace("subdomainRules", Value::MakeMap({{Value::Text("allowSubdomains"), Value::Boolean(false)}})); + // Contested names must prefund the masternode vote resolution; rs-dpp + // fills this from the contested unique index of the DPNS domain type + // (DocumentTypeV0Methods::prefunded_voting_balance_for_document_v0). + if (IsContestedLabel(normalized_label)) { + spec.prefunded_voting_balance = {CONTESTED_INDEX_NAME, CONTESTED_DOCUMENT_FUND_CREDITS}; + } + return SignDocumentBatch(identity, spec, signature_public_key_id, signer); +} + +Result BuildProfile( + const Identifier& identity, + uint64_t identity_contract_nonce, + const Profile& profile, + uint64_t revision, + const std::optional& existing_document_id, + uint32_t signature_public_key_id, + const Signer& signer) +{ + if (!profile.avatar_hash.empty() && profile.avatar_hash.size() != 32) { + return {std::nullopt, "avatar hash must be 32 bytes"}; + } + if (!profile.avatar_fingerprint.empty() && profile.avatar_fingerprint.size() != 8) { + return {std::nullopt, "avatar fingerprint must be 8 bytes"}; + } + DocumentSpec spec; + spec.identity_contract_nonce = identity_contract_nonce; + spec.document_type_name = "profile"; + spec.data_contract_id = DASHPAY_CONTRACT_ID; + if (existing_document_id) { + if (revision < 2) return {std::nullopt, "profile replace requires revision > 1"}; + spec.is_replace = true; + spec.document_id = *existing_document_id; + spec.revision = revision; + } else { + if (revision != 1) return {std::nullopt, "profile create requires revision 1"}; + spec.entropy = DeriveEntropy(identity, spec.document_type_name, identity_contract_nonce); + spec.document_id = dpp::GenerateDocumentId(spec.data_contract_id, identity, + spec.document_type_name, spec.entropy); + } + // All profile fields are optional in the DashPay contract; $createdAt / + // $updatedAt are assigned by the chain from block time and never appear + // in the transition. + if (!profile.display_name.empty()) spec.data.emplace("displayName", Value::Text(profile.display_name)); + if (!profile.public_message.empty()) spec.data.emplace("publicMessage", Value::Text(profile.public_message)); + if (!profile.avatar_url.empty()) spec.data.emplace("avatarUrl", Value::Text(profile.avatar_url)); + if (!profile.avatar_hash.empty()) { + std::array hash; + std::copy(profile.avatar_hash.begin(), profile.avatar_hash.end(), hash.begin()); + spec.data.emplace("avatarHash", Value::MakeBytes32(hash)); + } + if (!profile.avatar_fingerprint.empty()) { + spec.data.emplace("avatarFingerprint", Value::MakeBytes(profile.avatar_fingerprint)); + } + return SignDocumentBatch(identity, spec, signature_public_key_id, signer); +} + +Result BuildContactRequest( + const Identifier& identity, + uint64_t identity_contract_nonce, + const ContactRequest& request, + uint32_t signature_public_key_id, + const Signer& signer) +{ + if (request.encrypted_public_key.size() != 96) { + return {std::nullopt, "encrypted public key must be 96 bytes"}; + } + if (!request.encrypted_account_label.empty() && + (request.encrypted_account_label.size() < 48 || request.encrypted_account_label.size() > 80)) { + return {std::nullopt, "encrypted account label must be 48-80 bytes"}; + } + DocumentSpec spec; + spec.identity_contract_nonce = identity_contract_nonce; + spec.document_type_name = "contactRequest"; + spec.data_contract_id = DASHPAY_CONTRACT_ID; + spec.entropy = DeriveEntropy(identity, spec.document_type_name, identity_contract_nonce); + spec.document_id = dpp::GenerateDocumentId(spec.data_contract_id, identity, + spec.document_type_name, spec.entropy); + // DashPay contract v1: $createdAt and $createdAtCoreBlockHeight are + // chain-assigned system fields, so ContactRequest::created_at and + // ::core_height_created_at do not serialize into the transition + // (packages/dashpay-contract/schema/v1/dashpay.schema.json). + spec.data.emplace("toUserId", Value::Id(request.to_user_id)); + spec.data.emplace("encryptedPublicKey", Value::MakeBytes(request.encrypted_public_key)); + spec.data.emplace("senderKeyIndex", Value::U32(request.sender_key_index)); + spec.data.emplace("recipientKeyIndex", Value::U32(request.recipient_key_index)); + spec.data.emplace("accountReference", Value::U32(request.account_reference)); + if (!request.encrypted_account_label.empty()) { + spec.data.emplace("encryptedAccountLabel", Value::MakeBytes(request.encrypted_account_label)); + } + return SignDocumentBatch(identity, spec, signature_public_key_id, signer); +} + +std::array SaltedDomainHash( + const std::array& salt, + const std::string& normalized_label, + const std::string& parent_domain) +{ + // DSHA256(salt || normalized_label || "." || parent_domain), per the + // Rust SDK DPNS registration flow + // (packages/rs-sdk/src/platform/dpns_usernames/mod.rs register_dpns_name). + const std::string full_name{normalized_label + "." + parent_domain}; + CHash256 hasher; + hasher.Write(salt); + hasher.Write(MakeUCharSpan(full_name)); + uint256 hash; + hasher.Finalize(hash); + return ToArray(hash); +} + +std::string NormalizeLabel(const std::string& label) +{ + // Port of rs-dpp/src/util/strings.rs convert_to_homograph_safe_chars: + // lower-case, then o->0 and l/i->1. DPNS labels are ASCII-only by + // contract pattern, so ASCII lower-casing matches Rust's to_lowercase(). + std::string normalized; + normalized.reserve(label.size()); + for (char c : label) { + if (c >= 'A' && c <= 'Z') c = static_cast(c - 'A' + 'a'); + switch (c) { + case 'o': normalized.push_back('0'); break; + case 'l': + case 'i': normalized.push_back('1'); break; + default: normalized.push_back(c); + } + } + return normalized; +} + +bool IsContestedLabel(const std::string& normalized_label) +{ + // DPNS domain contested index rule: the parentNameAndLabel index is + // contested when normalizedLabel matches ^[a-zA-Z01-]{3,19}$ + // (packages/dpns-contract/schema/v1/dpns-contract-documents.json, + // indices[0].contested.fieldMatches). Note digits 2-9 make a name + // non-contested and normalized labels are already lower-case. + if (normalized_label.size() < 3 || normalized_label.size() > 19) return false; + for (char c : normalized_label) { + const bool allowed{(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + c == '0' || c == '1' || c == '-'}; + if (!allowed) return false; + } + return true; +} + +} // namespace platform::st diff --git a/src/test/data/platform/dpp_identity_vectors.json b/src/test/data/platform/dpp_identity_vectors.json new file mode 100644 index 000000000000..b4e6cb3c4ceb --- /dev/null +++ b/src/test/data/platform/dpp_identity_vectors.json @@ -0,0 +1,52 @@ +{ + "platform_repo_tag": "v4.0.0", + "protocol_version": 12, + "identity": { + "serialized_hex": "00777777777777777777777777777777777777777777777777777777777777777703000000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa0001000100020000002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f270002000201030100a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc000021035ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c3001fd0000018bcfe5687bfd000000012a05f20003", + "id": "7777777777777777777777777777777777777777777777777777777777777777", + "balance": 5000000000, + "revision": 3, + "public_keys": [ + { + "id": 0, + "purpose": 0, + "security_level": 0, + "key_type": 0, + "read_only": false, + "data": "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa", + "disabled_at": null + }, + { + "id": 1, + "purpose": 0, + "security_level": 2, + "key_type": 0, + "read_only": false, + "data": "02466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27", + "disabled_at": null + }, + { + "id": 2, + "purpose": 1, + "security_level": 3, + "key_type": 0, + "read_only": false, + "data": "035ab4689e400a4a160cf01cd44730845a54768df8547dcdf073d964f109f18c30", + "disabled_at": 1700000000123 + } + ], + "bounded_key_contract_id": "a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc" + }, + "identity_public_key": { + "serialized_hex": "000100020000002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f2700", + "fields": { + "id": 1, + "purpose": 0, + "security_level": 2, + "key_type": 0, + "read_only": false, + "data": "02466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27", + "disabled_at": null + } + } +} diff --git a/src/test/data/platform/dpp_st_vectors.json b/src/test/data/platform/dpp_st_vectors.json new file mode 100644 index 000000000000..32530fc4fc56 --- /dev/null +++ b/src/test/data/platform/dpp_st_vectors.json @@ -0,0 +1,201 @@ +{ + "platform_repo_tag": "v4.0.0", + "protocol_version": 12, + "signature_scheme": "double-SHA256 of signable bytes; 65-byte compact recoverable ECDSA; header byte = 27 + recovery_id + 4 (compressed)", + "keys": { + "master": { + "id": 0, + "private_key_hex": "1111111111111111111111111111111111111111111111111111111111111111", + "public_key_hex": "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa" + }, + "high": { + "id": 1, + "private_key_hex": "2222222222222222222222222222222222222222222222222222222222222222", + "public_key_hex": "02466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27" + }, + "asset_lock": { + "private_key_hex": "3333333333333333333333333333333333333333333333333333333333333333", + "public_key_hex": "023c72addb4fdf09af94f0c94d7fe92a386a7e70cf8a1d85916386bb2535c7b1b1" + } + }, + "identity_create_instant": { + "signable_hex": "0300020000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa000100000200002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f2700c60101aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000004ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf9bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc620300080001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd0100000000ffffffff01a086010000000000016a00000000240101a0860100000000001976a914999999999999999999999999999999999999999988ac0000", + "digest_hex": "65e4f7fd5b7aed2f0eff726970885384934e992b069f69a691e2ca719e9bccb0", + "serialized_hex": "0300020000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa411f9d83c3c0beb114a82d086a738e3aa5b13a0e70b8add2c78ac3cb90e592bdc4ba299caa0e78460bd08c54b7fb8d7e78879907bd0fc54fb6bfcc69a540ea0de84d000100000200002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27411ffc748e31ff48cbcfc2fa304bee54fdb8eaf673d5ab2da8ceb48f8d57dbc83def2e6d9970e108dadf18567d26ecbea8ac457768dc92dc1bdefdf52faac9f7988200c60101aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000004ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf9bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc620300080001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd0100000000ffffffff01a086010000000000016a00000000240101a0860100000000001976a914999999999999999999999999999999999999999988ac0000411f705a1b57222944f2ce9ea0d728edbc1312f244e326236946336c59e0f8b180ad732d88514d33de9a6f3687a789ae44649835c9f6c55a1e1bbcae6efc7f899b0c15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "transaction_hex": "0300080001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd0100000000ffffffff01a086010000000000016a00000000240101a0860100000000001976a914999999999999999999999999999999999999999988ac", + "instant_lock_hex": "0101aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000004ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf9bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "output_index": 0, + "out_point_hex": "4ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf900000000", + "identity_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327" + }, + "identity_create_chain": { + "signable_hex": "0300020000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa000100000200002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f2701fc00100590204ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf90000", + "digest_hex": "0a0a332c8bb7cb633707f8aa33ceffd667484e0fcf9d78f8025d3478f7c3b324", + "serialized_hex": "0300020000000000000021034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa41200a0b660530dfce50a0b1638ec274f658d662c1755105cfcae5ad31f92af5b0622dee03deefd30f0fa679d729bc89b138e402fc72ad9eee102dafcf2c580ef218000100000200002102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27411fcd6fc90f5a63a47a7dd2fd037a08570298c9aba925d2bbb2392f6a6f52cc109f23de5102d4b3d2e471a75c875f46dcf0d9ad5be3e6215c014150c77b0b8db62a01fc00100590204ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf900004120d275a61395575038d64f674b8fff1199abef5aae24ca7e560103a67cf71ea021080b97f7c4a7eea3fbe5045695404a766221556f283dcacc08b49beb9568a75915eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "core_chain_locked_height": 1050000, + "out_point_hex": "4ad31484097f16240507943e38ac9219114ede07c5794dc29314303070ea6bf900000000", + "identity_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327" + }, + "dpns_preorder": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100000001fae3f9bb1a28d38c18c65e78ae91730c667f388911988dce5c695f39cba97b1002087072656f72646572e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc18011073616c746564446f6d61696e486173680c2d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc180000", + "digest_hex": "64ad2dc780ce0718457c2f3ae9b3a6c6c49605ad71ea977e7746c755108fd24b", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100000001fae3f9bb1a28d38c18c65e78ae91730c667f388911988dce5c695f39cba97b1002087072656f72646572e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c53155002d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc18011073616c746564446f6d61696e486173680c2d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc1800000141209dcb265b869db7435ee79271ae2ded9fbda2c13478b2c65ca1b087c37226df0716917a41b8233a2b1982518e2d127bb9f2c654cfdb52e902233d2b4c85e8f552", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 2, + "entropy_hex": "2d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc18", + "salt_hex": "5555555555555555555555555555555555555555555555555555555555555555", + "label": "Alice", + "normalized_label": "a11ce", + "salted_domain_hash_hex": "2d07513b62deb2a49393aef02a84a9ad522c0e4f4b9c88fcec184cce7108bc18", + "document_id_hex": "fae3f9bb1a28d38c18c65e78ae91730c667f388911988dce5c695f39cba97b10", + "signature_public_key_id": 1 + }, + "dpns_domain_contested": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc32701000000018e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba87fb123406646f6d61696ee668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500555555555555555555555555555555555555555555555555555555555555555507056c6162656c1205416c6963650f6e6f726d616c697a65644c6162656c120561313163651a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6512046461736810706172656e74446f6d61696e4e616d651204646173680c7072656f7264657253616c740c5555555555555555555555555555555555555555555555555555555555555555077265636f726473160112086964656e746974791015eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270e737562646f6d61696e52756c65731601120f616c6c6f77537562646f6d61696e7313000112706172656e744e616d65416e644c6162656cfd00000004a817c80000", + "digest_hex": "cdddcc3a001075acb4a13546f4d5404df42ae5ee857f0641c8199ac64bd3a90f", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc32701000000018e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba87fb123406646f6d61696ee668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500555555555555555555555555555555555555555555555555555555555555555507056c6162656c1205416c6963650f6e6f726d616c697a65644c6162656c120561313163651a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6512046461736810706172656e74446f6d61696e4e616d651204646173680c7072656f7264657253616c740c5555555555555555555555555555555555555555555555555555555555555555077265636f726473160112086964656e746974791015eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270e737562646f6d61696e52756c65731601120f616c6c6f77537562646f6d61696e7313000112706172656e744e616d65416e644c6162656cfd00000004a817c800000141200307c3087cacd3b3aecbbefae2191c53f02e8b6b58efff84661df8a8ff674eff213f285ddd0f5030e9457d918a925bb1b6d2e3305605508ed26ec6e2fc17acf2", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 4660, + "entropy_hex": "5555555555555555555555555555555555555555555555555555555555555555", + "salt_hex": "5555555555555555555555555555555555555555555555555555555555555555", + "label": "Alice", + "normalized_label": "a11ce", + "document_id_hex": "8e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba87", + "signature_public_key_id": 1, + "contested": true, + "prefunded_voting_balance_credits": 20000000000 + }, + "dpns_domain": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc32701000000018e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba870506646f6d61696ee668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500555555555555555555555555555555555555555555555555555555555555555507056c6162656c12097175616e74756d34320f6e6f726d616c697a65644c6162656c12097175616e74756d34321a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6512046461736810706172656e74446f6d61696e4e616d651204646173680c7072656f7264657253616c740c5555555555555555555555555555555555555555555555555555555555555555077265636f726473160112086964656e746974791015eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270e737562646f6d61696e52756c65731601120f616c6c6f77537562646f6d61696e7313000000", + "digest_hex": "10b93c345995e3fca59bfbb45e6ecc0a794b50f0b9f42de9002d0f935c32a896", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc32701000000018e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba870506646f6d61696ee668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315500555555555555555555555555555555555555555555555555555555555555555507056c6162656c12097175616e74756d34320f6e6f726d616c697a65644c6162656c12097175616e74756d34321a6e6f726d616c697a6564506172656e74446f6d61696e4e616d6512046461736810706172656e74446f6d61696e4e616d651204646173680c7072656f7264657253616c740c5555555555555555555555555555555555555555555555555555555555555555077265636f726473160112086964656e746974791015eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270e737562646f6d61696e52756c65731601120f616c6c6f77537562646f6d61696e731300000001411f63a478bc0626c371ee445ba205244227b734d540a89278ca8cedbe9e48bd88bc215e9871ce443c04d1dacc91c080de6151da51be58225b908e59d33dc1e8c038", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 5, + "entropy_hex": "5555555555555555555555555555555555555555555555555555555555555555", + "salt_hex": "5555555555555555555555555555555555555555555555555555555555555555", + "label": "quantum42", + "normalized_label": "quantum42", + "document_id_hex": "8e59141aa8875fbe1b6a1089bef3366a9f8aeddb4debcea35905ced54228ba87", + "signature_public_key_id": 1, + "contested": false + }, + "dashpay_profile_create": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100000001a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6030770726f66696c65a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc00e540d547ecac78cac1efd88fbced3d23aa8ae638ff1accdaa8a308337983eb09051161766174617246696e6765727072696e740a0899999999999999990a617661746172486173680c88888888888888888888888888888888888888888888888888888888888888880961766174617255726c121968747470733a2f2f6578616d706c652e636f6d2f612e706e670b646973706c61794e616d651213416c69636520d0b220576f6e6465726c616e640d7075626c69634d657373616765120e68656c6c6f20706c6174666f726d0000", + "digest_hex": "9e375dc31f1323adc4cc2e1de0eb5f8e0c6a26d26288b6e74f3b9cd68ea7afe0", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100000001a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6030770726f66696c65a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc00e540d547ecac78cac1efd88fbced3d23aa8ae638ff1accdaa8a308337983eb09051161766174617246696e6765727072696e740a0899999999999999990a617661746172486173680c88888888888888888888888888888888888888888888888888888888888888880961766174617255726c121968747470733a2f2f6578616d706c652e636f6d2f612e706e670b646973706c61794e616d651213416c69636520d0b220576f6e6465726c616e640d7075626c69634d657373616765120e68656c6c6f20706c6174666f726d000001411fd491cd71660321908cb4b911d103ec22252cb60f70f073f0d0468dce50ef31aa0de158d69459d3fdb4e177471ed19857cd5f504e2c8becd8fd6f8b57dd8dc455", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 3, + "entropy_hex": "e540d547ecac78cac1efd88fbced3d23aa8ae638ff1accdaa8a308337983eb09", + "document_id_hex": "a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6", + "signature_public_key_id": 1, + "display_name": "Alice в Wonderland", + "public_message": "hello platform", + "avatar_url": "https://example.com/a.png", + "avatar_hash_hex": "8888888888888888888888888888888888888888888888888888888888888888", + "avatar_fingerprint_hex": "9999999999999999" + }, + "dashpay_profile_replace": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100010001a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6040770726f66696c65a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc0002051161766174617246696e6765727072696e740a0899999999999999990a617661746172486173680c88888888888888888888888888888888888888888888888888888888888888880961766174617255726c121968747470733a2f2f6578616d706c652e636f6d2f612e706e670b646973706c61794e616d651213416c69636520d0b220576f6e6465726c616e640d7075626c69634d657373616765120f75706461746564206d65737361676500", + "digest_hex": "f3361516f52e272dfc6fdc75ea304f9c5035cf985550730a3704694cc0a755d1", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc3270100010001a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6040770726f66696c65a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc0002051161766174617246696e6765727072696e740a0899999999999999990a617661746172486173680c88888888888888888888888888888888888888888888888888888888888888880961766174617255726c121968747470733a2f2f6578616d706c652e636f6d2f612e706e670b646973706c61794e616d651213416c69636520d0b220576f6e6465726c616e640d7075626c69634d657373616765120f75706461746564206d65737361676500014120fd2ec82c57e630ce2854a2ec05a14f36d186cb030707bd725b1ae6d79ce7a6194fa8edfba509bbc6f4f4a99a77270a34ec8c345234019078b793a4e5fe9cd33e", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 4, + "revision": 2, + "document_id_hex": "a682c7dff71614a696c989e14a94f7841106620522ede675ef453238751a21d6", + "signature_public_key_id": 1, + "public_message": "updated message" + }, + "dashpay_contact_request": { + "signable_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327010000000148880e37b469540c0b4b8a6489d21938726e53ce7d337febb1f101da351f57fd060e636f6e7461637452657175657374a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc00fda853b87e9145263b6fcb17b4d8545a9462b4daf95a86194b5e8b5a3f74a22406106163636f756e745265666572656e636504fc0badc0de15656e637279707465644163636f756e744c6162656c0a30cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd12656e637279707465645075626c69634b65790a60abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab11726563697069656e744b6579496e64657804030e73656e6465724b6579496e646578040208746f55736572496410eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000", + "digest_hex": "46002d99e2fcdf47cddc3e3173b9ba9256af84e84a4b6e007b2ba4db848cab44", + "serialized_hex": "020115eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327010000000148880e37b469540c0b4b8a6489d21938726e53ce7d337febb1f101da351f57fd060e636f6e7461637452657175657374a2a1b4ac6fef22ea2a1a68e8123644b357875f6b412c18109281c146e7b271bc00fda853b87e9145263b6fcb17b4d8545a9462b4daf95a86194b5e8b5a3f74a22406106163636f756e745265666572656e636504fc0badc0de15656e637279707465644163636f756e744c6162656c0a30cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd12656e637279707465645075626c69634b65790a60abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab11726563697069656e744b6579496e64657804030e73656e6465724b6579496e646578040208746f55736572496410eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000001411fcbc9c34bb69e53b1a319499aada671d480cb8db8e33afd27a633ee21b98dd1c87c8d27c8e2a878edcee85d92fa614bd8c9c3724f86854971a5dadfa31d9e7c2e", + "owner_id_hex": "15eccc3165b78a43d2a70bf8421d8e286bc6330790a4ef4ed09316f0eb0bc327", + "identity_contract_nonce": 6, + "entropy_hex": "fda853b87e9145263b6fcb17b4d8545a9462b4daf95a86194b5e8b5a3f74a224", + "document_id_hex": "48880e37b469540c0b4b8a6489d21938726e53ce7d337febb1f101da351f57fd", + "signature_public_key_id": 1, + "to_user_id_hex": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "encrypted_public_key_hex": "abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab", + "sender_key_index": 2, + "recipient_key_index": 3, + "account_reference": 195936478, + "encrypted_account_label_hex": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" + }, + "contested_labels": [ + { + "label": "alice", + "normalized": "a11ce", + "contested": true + }, + { + "label": "a11ce", + "normalized": "a11ce", + "contested": true + }, + { + "label": "bob", + "normalized": "b0b", + "contested": true + }, + { + "label": "b0b", + "normalized": "b0b", + "contested": true + }, + { + "label": "ab", + "normalized": "ab", + "contested": false + }, + { + "label": "abc", + "normalized": "abc", + "contested": true + }, + { + "label": "x2y", + "normalized": "x2y", + "contested": false + }, + { + "label": "up", + "normalized": "up", + "contested": false + }, + { + "label": "-ab-", + "normalized": "-ab-", + "contested": true + }, + { + "label": "aaaaaaaaaaaaaaaaaaa", + "normalized": "aaaaaaaaaaaaaaaaaaa", + "contested": true + }, + { + "label": "aaaaaaaaaaaaaaaaaaaa", + "normalized": "aaaaaaaaaaaaaaaaaaaa", + "contested": false + }, + { + "label": "quantum42", + "normalized": "quantum42", + "contested": false + }, + { + "label": "dash", + "normalized": "dash", + "contested": true + }, + { + "label": "test-name", + "normalized": "test-name", + "contested": true + }, + { + "label": "name2", + "normalized": "name2", + "contested": false + } + ] +} diff --git a/src/test/platform_dpp_tests.cpp b/src/test/platform_dpp_tests.cpp new file mode 100644 index 000000000000..96b808ca64b8 --- /dev/null +++ b/src/test/platform_dpp_tests.cpp @@ -0,0 +1,429 @@ +// Copyright (c) 2026 The Dash 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 +#include +#include +#include +#include + +#include +#include + +#include + +#include + +#include +#include +#include + +/** + * Byte-exactness tests for src/platform/dpp/ against vectors generated by + * the real rs-dpp (dashpay/platform tag v4.0.0, protocol version 12) via + * contrib/devtools/platform-dpp-vectors. + */ + +using namespace platform; + +BOOST_FIXTURE_TEST_SUITE(platform_dpp_tests, BasicTestingSetup) + +namespace { + +UniValue LoadVectors(const unsigned char* data, size_t size) +{ + // Not read_json(): these vector files are JSON objects, not arrays. + UniValue vectors; + BOOST_REQUIRE(vectors.read(std::string(data, data + size))); + BOOST_REQUIRE(vectors.isObject()); + return vectors; +} + +std::vector FromHex(const std::string& hex) +{ + BOOST_REQUIRE(IsHex(hex)); + return ParseHex(hex); +} + +template +std::array ArrayFromHex(const std::string& hex) +{ + const std::vector bytes{FromHex(hex)}; + BOOST_REQUIRE_EQUAL(bytes.size(), N); + std::array out; + std::copy(bytes.begin(), bytes.end(), out.begin()); + return out; +} + +//! Deterministic signer over a fixed private key: RFC6979 compact +//! recoverable signature, exactly what interfaces::Wallet::signPlatformDigest +//! produces and what the vectors were signed with (dashcore::signer). +st::Signer MakeSigner(const std::string& privkey_hex, const uint256* expected_digest = nullptr) +{ + const std::vector priv{FromHex(privkey_hex)}; + return [priv, expected_digest](const uint256& digest, std::vector& sig_out) { + if (expected_digest != nullptr) BOOST_CHECK_EQUAL(digest, *expected_digest); + CKey key; + key.Set(priv.begin(), priv.end(), /*fCompressedIn=*/true); + if (!key.IsValid()) return false; + return key.SignCompact(digest, sig_out); + }; +} + +void CheckBuilt(const st::Result& result, const UniValue& vec) +{ + BOOST_REQUIRE_MESSAGE(result.ok(), result.error); + const std::vector expected{FromHex(vec["serialized_hex"].get_str())}; + BOOST_CHECK_EQUAL(HexStr(result.value->bytes), HexStr(expected)); + // The wait handle is the single SHA256 of the serialized transition + // (rs-dpp StateTransition::transaction_id). + uint256 expected_hash; + CSHA256().Write(expected.data(), expected.size()).Finalize(expected_hash.begin()); + BOOST_CHECK_EQUAL(result.value->hash, expected_hash); +} + +uint256 DigestOf(const UniValue& vec) +{ + return uint256{FromHex(vec["digest_hex"].get_str())}; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(bincode_varint) +{ + // Encoding boundaries of bincode standard()+big_endian() varints. + const std::vector> cases{ + {0, "00"}, + {100, "64"}, + {250, "fa"}, + {251, "fb00fb"}, + {0x1234, "fb1234"}, + {0xffff, "fbffff"}, + {0x10000, "fc00010000"}, + {0xffffffff, "fcffffffff"}, + {0x100000000ULL, "fd0000000100000000"}, + {0xffffffffffffffffULL, "fdffffffffffffffff"}, + }; + for (const auto& [value, expected_hex] : cases) { + dpp::Writer writer; + writer.WriteVarint(value); + BOOST_CHECK_EQUAL(HexStr(writer.Data()), expected_hex); + + dpp::Reader reader{Span{writer.Data()}}; + uint64_t decoded; + BOOST_REQUIRE(reader.ReadVarint(decoded)); + BOOST_CHECK_EQUAL(decoded, value); + BOOST_CHECK(reader.IsEof()); + } + + // Signed varints zigzag before encoding. + dpp::Writer writer; + writer.WriteSignedVarint(-1); + writer.WriteSignedVarint(1); + writer.WriteSignedVarint(-126); + BOOST_CHECK_EQUAL(HexStr(writer.Data()), "0102fb00fb"); + dpp::Reader reader{Span{writer.Data()}}; + int64_t s1, s2, s3; + BOOST_REQUIRE(reader.ReadSignedVarint(s1) && reader.ReadSignedVarint(s2) && reader.ReadSignedVarint(s3)); + BOOST_CHECK_EQUAL(s1, -1); + BOOST_CHECK_EQUAL(s2, 1); + BOOST_CHECK_EQUAL(s3, -126); + + // The u128 marker is rejected. + const std::vector u128_marker{0xfe}; + dpp::Reader bad{Span{u128_marker}}; + uint64_t dummy; + BOOST_CHECK(!bad.ReadVarint(dummy)); + BOOST_CHECK(bad.HasError()); +} + +BOOST_AUTO_TEST_CASE(identity_decode) +{ + const UniValue vectors{LoadVectors(json_tests::dpp_identity_vectors, sizeof(json_tests::dpp_identity_vectors))}; + const UniValue& id_vec{vectors["identity"]}; + const std::vector bytes{FromHex(id_vec["serialized_hex"].get_str())}; + + std::string error; + const auto identity{dpp::DecodeIdentity(bytes, error)}; + BOOST_REQUIRE_MESSAGE(identity.has_value(), error); + BOOST_CHECK_EQUAL(HexStr(identity->id), id_vec["id"].get_str()); + BOOST_CHECK_EQUAL(identity->balance, id_vec["balance"].getInt()); + BOOST_CHECK_EQUAL(identity->revision, id_vec["revision"].getInt()); + + const UniValue& keys{id_vec["public_keys"]}; + BOOST_REQUIRE_EQUAL(identity->public_keys.size(), keys.size()); + for (size_t i = 0; i < keys.size(); ++i) { + const UniValue& expected{keys[i]}; + const IdentityPublicKey& key{identity->public_keys[i]}; + BOOST_CHECK_EQUAL(key.id, expected["id"].getInt()); + BOOST_CHECK_EQUAL(static_cast(key.purpose), expected["purpose"].getInt()); + BOOST_CHECK_EQUAL(static_cast(key.security_level), expected["security_level"].getInt()); + BOOST_CHECK_EQUAL(static_cast(key.type), expected["key_type"].getInt()); + BOOST_CHECK_EQUAL(key.read_only, expected["read_only"].get_bool()); + BOOST_CHECK_EQUAL(HexStr(key.data), expected["data"].get_str()); + if (expected["disabled_at"].isNull()) { + BOOST_CHECK(!key.disabled_at.has_value()); + } else { + BOOST_REQUIRE(key.disabled_at.has_value()); + BOOST_CHECK_EQUAL(*key.disabled_at, expected["disabled_at"].getInt()); + } + } + + // Negative: truncation, trailing garbage and unknown versions all fail. + for (size_t len : {size_t{0}, size_t{1}, size_t{16}, bytes.size() - 1}) { + std::string err; + BOOST_CHECK(!dpp::DecodeIdentity(Span{bytes}.first(len), err)); + BOOST_CHECK(!err.empty()); + } + { + std::vector trailing{bytes}; + trailing.push_back(0x00); + std::string err; + BOOST_CHECK(!dpp::DecodeIdentity(trailing, err)); + } + { + std::vector bad_version{bytes}; + bad_version[0] = 0x01; + std::string err; + BOOST_CHECK(!dpp::DecodeIdentity(bad_version, err)); + } + { + // Corrupt the key count so the map runs off the end of the buffer. + std::vector bad_count{bytes}; + bad_count[33] = 0x63; // 99 keys + std::string err; + BOOST_CHECK(!dpp::DecodeIdentity(bad_count, err)); + } +} + +BOOST_AUTO_TEST_CASE(identity_public_key_decode) +{ + const UniValue vectors{LoadVectors(json_tests::dpp_identity_vectors, sizeof(json_tests::dpp_identity_vectors))}; + const UniValue& key_vec{vectors["identity_public_key"]}; + const std::vector bytes{FromHex(key_vec["serialized_hex"].get_str())}; + const UniValue& fields{key_vec["fields"]}; + + std::string error; + const auto key{dpp::DecodeIdentityPublicKey(bytes, error)}; + BOOST_REQUIRE_MESSAGE(key.has_value(), error); + BOOST_CHECK_EQUAL(key->id, fields["id"].getInt()); + BOOST_CHECK_EQUAL(static_cast(key->purpose), fields["purpose"].getInt()); + BOOST_CHECK_EQUAL(static_cast(key->security_level), fields["security_level"].getInt()); + BOOST_CHECK_EQUAL(static_cast(key->type), fields["key_type"].getInt()); + BOOST_CHECK_EQUAL(HexStr(key->data), fields["data"].get_str()); + + std::string err; + BOOST_CHECK(!dpp::DecodeIdentityPublicKey(Span{bytes}.first(bytes.size() - 1), err)); + std::vector trailing{bytes}; + trailing.push_back(0x00); + BOOST_CHECK(!dpp::DecodeIdentityPublicKey(trailing, err)); +} + +BOOST_AUTO_TEST_CASE(identity_create) +{ + const UniValue vectors{LoadVectors(json_tests::dpp_st_vectors, sizeof(json_tests::dpp_st_vectors))}; + const UniValue& keys{vectors["keys"]}; + + std::vector new_keys; + { + st::NewIdentityKey master; + master.id = 0; + master.purpose = IdentityPublicKey::Purpose::AUTHENTICATION; + master.security_level = IdentityPublicKey::SecurityLevel::MASTER; + master.pubkey = FromHex(keys["master"]["public_key_hex"].get_str()); + master.signer = MakeSigner(keys["master"]["private_key_hex"].get_str()); + st::NewIdentityKey high; + high.id = 1; + high.purpose = IdentityPublicKey::Purpose::AUTHENTICATION; + high.security_level = IdentityPublicKey::SecurityLevel::HIGH; + high.pubkey = FromHex(keys["high"]["public_key_hex"].get_str()); + high.signer = MakeSigner(keys["high"]["private_key_hex"].get_str()); + // Deliberately unsorted: the builder must order keys by id. + new_keys = {high, master}; + } + const std::string asset_lock_priv{keys["asset_lock"]["private_key_hex"].get_str()}; + + { + const UniValue& vec{vectors["identity_create_instant"]}; + st::InstantAssetLockProof proof; + proof.transaction = FromHex(vec["transaction_hex"].get_str()); + proof.instant_lock = FromHex(vec["instant_lock_hex"].get_str()); + proof.output_index = vec["output_index"].getInt(); + + const uint256 digest{DigestOf(vec)}; + const auto result{st::BuildIdentityCreate({proof}, new_keys, MakeSigner(asset_lock_priv, &digest))}; + CheckBuilt(result, vec); + + const auto out_point{ArrayFromHex<36>(vec["out_point_hex"].get_str())}; + BOOST_CHECK_EQUAL(HexStr(st::IdentityIdFromOutpoint(out_point)), vec["identity_id_hex"].get_str()); + } + { + const UniValue& vec{vectors["identity_create_chain"]}; + st::ChainAssetLockProof proof; + proof.core_chain_locked_height = vec["core_chain_locked_height"].getInt(); + proof.out_point = ArrayFromHex<36>(vec["out_point_hex"].get_str()); + + const uint256 digest{DigestOf(vec)}; + const auto result{st::BuildIdentityCreate({proof}, new_keys, MakeSigner(asset_lock_priv, &digest))}; + CheckBuilt(result, vec); + BOOST_CHECK_EQUAL(HexStr(st::IdentityIdFromOutpoint(proof.out_point)), vec["identity_id_hex"].get_str()); + } + + // Signer failure surfaces as an error, not a malformed transition. + const auto failed{st::BuildIdentityCreate( + {st::ChainAssetLockProof{}}, new_keys, + [](const uint256&, std::vector&) { return false; })}; + BOOST_CHECK(!failed.ok()); + BOOST_CHECK(!failed.error.empty()); +} + +BOOST_AUTO_TEST_CASE(dpns_preorder) +{ + const UniValue vectors{LoadVectors(json_tests::dpp_st_vectors, sizeof(json_tests::dpp_st_vectors))}; + const UniValue& vec{vectors["dpns_preorder"]}; + const std::string high_priv{vectors["keys"]["high"]["private_key_hex"].get_str()}; + + const auto owner{ArrayFromHex<32>(vec["owner_id_hex"].get_str())}; + const auto salt{ArrayFromHex<32>(vec["salt_hex"].get_str())}; + const std::string normalized{st::NormalizeLabel(vec["label"].get_str())}; + BOOST_CHECK_EQUAL(normalized, vec["normalized_label"].get_str()); + + const auto salted_hash{st::SaltedDomainHash(salt, normalized, "dash")}; + BOOST_CHECK_EQUAL(HexStr(salted_hash), vec["salted_domain_hash_hex"].get_str()); + + const uint256 digest{DigestOf(vec)}; + const auto result{st::BuildDpnsPreorder(owner, vec["identity_contract_nonce"].getInt(), + salted_hash, vec["signature_public_key_id"].getInt(), + MakeSigner(high_priv, &digest))}; + CheckBuilt(result, vec); + + // The preorder document id derives from the salted-domain-hash entropy. + const auto doc_id{dpp::GenerateDocumentId(DPNS_CONTRACT_ID, owner, "preorder", salted_hash)}; + BOOST_CHECK_EQUAL(HexStr(doc_id), vec["document_id_hex"].get_str()); +} + +BOOST_AUTO_TEST_CASE(dpns_domain) +{ + const UniValue vectors{LoadVectors(json_tests::dpp_st_vectors, sizeof(json_tests::dpp_st_vectors))}; + const std::string high_priv{vectors["keys"]["high"]["private_key_hex"].get_str()}; + + for (const std::string& name : {"dpns_domain_contested", "dpns_domain"}) { + const UniValue& vec{vectors[name]}; + const auto owner{ArrayFromHex<32>(vec["owner_id_hex"].get_str())}; + const auto salt{ArrayFromHex<32>(vec["salt_hex"].get_str())}; + const std::string label{vec["label"].get_str()}; + const std::string normalized{st::NormalizeLabel(label)}; + BOOST_CHECK_EQUAL(normalized, vec["normalized_label"].get_str()); + BOOST_CHECK_EQUAL(st::IsContestedLabel(normalized), vec["contested"].get_bool()); + + const uint256 digest{DigestOf(vec)}; + const auto result{st::BuildDpnsDomain(owner, vec["identity_contract_nonce"].getInt(), + label, normalized, "dash", salt, + vec["signature_public_key_id"].getInt(), + MakeSigner(high_priv, &digest))}; + CheckBuilt(result, vec); + + // The domain document id derives from the preorder-salt entropy. + const auto doc_id{dpp::GenerateDocumentId(DPNS_CONTRACT_ID, owner, "domain", salt)}; + BOOST_CHECK_EQUAL(HexStr(doc_id), vec["document_id_hex"].get_str()); + } + + // A mismatched normalized label is rejected before signing. + const auto owner{ArrayFromHex<32>(vectors["dpns_domain"]["owner_id_hex"].get_str())}; + const auto bad{st::BuildDpnsDomain(owner, 1, "Alice", "alice", "dash", {}, 1, + MakeSigner(high_priv))}; + BOOST_CHECK(!bad.ok()); +} + +BOOST_AUTO_TEST_CASE(dashpay_profile) +{ + const UniValue vectors{LoadVectors(json_tests::dpp_st_vectors, sizeof(json_tests::dpp_st_vectors))}; + const std::string high_priv{vectors["keys"]["high"]["private_key_hex"].get_str()}; + + const UniValue& create_vec{vectors["dashpay_profile_create"]}; + const auto owner{ArrayFromHex<32>(create_vec["owner_id_hex"].get_str())}; + + Profile profile; + profile.display_name = create_vec["display_name"].get_str(); + profile.public_message = create_vec["public_message"].get_str(); + profile.avatar_url = create_vec["avatar_url"].get_str(); + profile.avatar_hash = FromHex(create_vec["avatar_hash_hex"].get_str()); + profile.avatar_fingerprint = FromHex(create_vec["avatar_fingerprint_hex"].get_str()); + + { + const uint256 digest{DigestOf(create_vec)}; + const auto result{st::BuildProfile(owner, create_vec["identity_contract_nonce"].getInt(), + profile, /*revision=*/1, std::nullopt, + create_vec["signature_public_key_id"].getInt(), + MakeSigner(high_priv, &digest))}; + CheckBuilt(result, create_vec); + } + { + const UniValue& replace_vec{vectors["dashpay_profile_replace"]}; + Profile updated{profile}; + updated.public_message = replace_vec["public_message"].get_str(); + const auto existing_id{ArrayFromHex<32>(replace_vec["document_id_hex"].get_str())}; + const uint256 digest{DigestOf(replace_vec)}; + const auto result{st::BuildProfile(owner, replace_vec["identity_contract_nonce"].getInt(), + updated, /*revision=*/2, existing_id, + replace_vec["signature_public_key_id"].getInt(), + MakeSigner(high_priv, &digest))}; + CheckBuilt(result, replace_vec); + } + + // Schema guards: malformed avatar metadata is rejected. + Profile bad{profile}; + bad.avatar_hash.assign(31, 0x00); + BOOST_CHECK(!st::BuildProfile(owner, 1, bad, 1, std::nullopt, 1, MakeSigner(high_priv)).ok()); +} + +BOOST_AUTO_TEST_CASE(dashpay_contact_request) +{ + const UniValue vectors{LoadVectors(json_tests::dpp_st_vectors, sizeof(json_tests::dpp_st_vectors))}; + const UniValue& vec{vectors["dashpay_contact_request"]}; + const std::string high_priv{vectors["keys"]["high"]["private_key_hex"].get_str()}; + const auto owner{ArrayFromHex<32>(vec["owner_id_hex"].get_str())}; + + ContactRequest request; + request.to_user_id = ArrayFromHex<32>(vec["to_user_id_hex"].get_str()); + request.encrypted_public_key = FromHex(vec["encrypted_public_key_hex"].get_str()); + request.sender_key_index = vec["sender_key_index"].getInt(); + request.recipient_key_index = vec["recipient_key_index"].getInt(); + request.account_reference = vec["account_reference"].getInt(); + request.encrypted_account_label = FromHex(vec["encrypted_account_label_hex"].get_str()); + + const uint256 digest{DigestOf(vec)}; + const auto result{st::BuildContactRequest(owner, vec["identity_contract_nonce"].getInt(), + request, vec["signature_public_key_id"].getInt(), + MakeSigner(high_priv, &digest))}; + CheckBuilt(result, vec); + + ContactRequest bad{request}; + bad.encrypted_public_key.resize(95); + BOOST_CHECK(!st::BuildContactRequest(owner, 1, bad, 1, MakeSigner(high_priv)).ok()); +} + +BOOST_AUTO_TEST_CASE(contested_labels) +{ + const UniValue vectors{LoadVectors(json_tests::dpp_st_vectors, sizeof(json_tests::dpp_st_vectors))}; + const UniValue& cases{vectors["contested_labels"]}; + BOOST_REQUIRE(cases.isArray()); + for (size_t i = 0; i < cases.size(); ++i) { + const UniValue& test{cases[i]}; + const std::string label{test["label"].get_str()}; + const std::string normalized{st::NormalizeLabel(label)}; + BOOST_CHECK_EQUAL(normalized, test["normalized"].get_str()); + BOOST_CHECK_MESSAGE(st::IsContestedLabel(normalized) == test["contested"].get_bool(), + "contested mismatch for label " + label); + } + // Port of the rs-dpp convert_to_homograph_safe_chars unit test input. + BOOST_CHECK_EQUAL(st::NormalizeLabel("A0boic0Dlelfl"), "a0b01c0d1e1f1"); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/util/data/non-backported.txt b/test/util/data/non-backported.txt index bde6ac5d29ac..ecd2c133d183 100644 --- a/test/util/data/non-backported.txt +++ b/test/util/data/non-backported.txt @@ -39,6 +39,8 @@ src/qt/guiutil_font.* src/qt/informationwidget.* src/platform/*.cpp src/platform/*.h +src/platform/dpp/*.cpp +src/platform/dpp/*.h src/platform/proof/*.cpp src/platform/proof/*.h src/qt/masternodelist.* @@ -71,6 +73,7 @@ src/test/dynamic_activation*.cpp src/test/evo*.cpp src/test/llmq*.cpp src/test/masternode_payments_tests.cpp +src/test/platform_dpp_tests.cpp src/test/platform_proof_tests.cpp src/test/util/llmq_tests.h src/test/governance*.cpp From 19454ea734e0c0ce63d76110de8294a3acfb9d8d Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 10:44:23 -0500 Subject: [PATCH 08/33] feat(platform): gRPC-Web/TLS transport primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-rolled DAPI transport that needs no gRPC/HTTP-2/protobuf library: - transport/protobuf: minimal protobuf wire reader/writer for the DAPI Platform messages the GUI uses (field numbers from platform.proto) - transport/tls: blocking TLS client over mbedTLS 3.6 LTS; transport certs are intentionally not chain-verified (evonodes are reached by bare IP) — integrity comes from proof + quorum-signature verification of every response, matching the reference SDKs - transport/grpcweb: unary gRPC-Web calls over HTTP/1.1 (5-byte frame header + trailers frame), including de-chunking, as served by DAPI's Envoy gateway - transport/cbor: minimal CBOR writer for getDocuments where/order_by operands Validated against live testnet evonodes: getStatus and getIdentity (prove=true, returning a 2.3 KB GroveDB proof) both round-trip correctly. Co-Authored-By: Claude Fable 5 --- src/Makefile.am | 7 ++ src/platform/transport/cbor.h | 67 ++++++++++++ src/platform/transport/grpcweb.cpp | 164 ++++++++++++++++++++++++++++ src/platform/transport/grpcweb.h | 42 +++++++ src/platform/transport/protobuf.cpp | 114 +++++++++++++++++++ src/platform/transport/protobuf.h | 80 ++++++++++++++ src/platform/transport/tls.cpp | 132 ++++++++++++++++++++++ src/platform/transport/tls.h | 54 +++++++++ 8 files changed, 660 insertions(+) create mode 100644 src/platform/transport/cbor.h create mode 100644 src/platform/transport/grpcweb.cpp create mode 100644 src/platform/transport/grpcweb.h create mode 100644 src/platform/transport/protobuf.cpp create mode 100644 src/platform/transport/protobuf.h create mode 100644 src/platform/transport/tls.cpp create mode 100644 src/platform/transport/tls.h diff --git a/src/Makefile.am b/src/Makefile.am index 06cb40f87068..de0df1a1cf2b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -709,6 +709,13 @@ libdash_platform_a_SOURCES = \ platform/serialize.cpp \ platform/serialize.h \ platform/statetransitions.h \ + platform/transport/cbor.h \ + platform/transport/grpcweb.cpp \ + platform/transport/grpcweb.h \ + platform/transport/protobuf.cpp \ + platform/transport/protobuf.h \ + platform/transport/tls.cpp \ + platform/transport/tls.h \ platform/types.h endif # diff --git a/src/platform/transport/cbor.h b/src/platform/transport/cbor.h new file mode 100644 index 000000000000..e2e07b0980a1 --- /dev/null +++ b/src/platform/transport/cbor.h @@ -0,0 +1,67 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_TRANSPORT_CBOR_H +#define BITCOIN_PLATFORM_TRANSPORT_CBOR_H + +#include +#include +#include +#include + +/** + * Minimal CBOR writer — just enough to encode the `where` and `order_by` + * operands of a DAPI getDocuments v0 request, which are CBOR-encoded byte + * fields (arrays of [field, operator, value] / [field, direction] tuples). + * See dashpay/platform packages/dapi getDocuments handler (v0 CBOR path). + */ +namespace platform::transport::cbor { + +class Writer +{ +public: + void Array(size_t n) { WriteHead(4, n); } + void Uint(uint64_t v) { WriteHead(0, v); } + void Text(const std::string& s) + { + WriteHead(3, s.size()); + m_out.insert(m_out.end(), s.begin(), s.end()); + } + void Bytes(Span b) + { + WriteHead(2, b.size()); + m_out.insert(m_out.end(), b.begin(), b.end()); + } + void Bool(bool b) { m_out.push_back(b ? 0xf5 : 0xf4); } + + const std::vector& data() const { return m_out; } + std::vector take() { return std::move(m_out); } + +private: + void WriteHead(uint8_t major, uint64_t value) + { + const uint8_t mt = static_cast(major << 5); + if (value < 24) { + m_out.push_back(mt | static_cast(value)); + } else if (value <= 0xff) { + m_out.push_back(mt | 24); + m_out.push_back(static_cast(value)); + } else if (value <= 0xffff) { + m_out.push_back(mt | 25); + m_out.push_back(static_cast(value >> 8)); + m_out.push_back(static_cast(value)); + } else if (value <= 0xffffffff) { + m_out.push_back(mt | 26); + for (int i = 3; i >= 0; --i) m_out.push_back(static_cast(value >> (8 * i))); + } else { + m_out.push_back(mt | 27); + for (int i = 7; i >= 0; --i) m_out.push_back(static_cast(value >> (8 * i))); + } + } + std::vector m_out; +}; + +} // namespace platform::transport::cbor + +#endif // BITCOIN_PLATFORM_TRANSPORT_CBOR_H diff --git a/src/platform/transport/grpcweb.cpp b/src/platform/transport/grpcweb.cpp new file mode 100644 index 000000000000..753482fcea3a --- /dev/null +++ b/src/platform/transport/grpcweb.cpp @@ -0,0 +1,164 @@ +// Copyright (c) 2026 The Dash 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 + +namespace platform::transport { + +namespace { + +//! Wrap a protobuf message in a single gRPC-Web data frame. +std::vector FrameMessage(const std::vector& msg) +{ + std::vector frame; + frame.reserve(5 + msg.size()); + frame.push_back(0x00); // data frame, not compressed + const uint32_t len = static_cast(msg.size()); + frame.push_back(static_cast(len >> 24)); + frame.push_back(static_cast(len >> 16)); + frame.push_back(static_cast(len >> 8)); + frame.push_back(static_cast(len)); + frame.insert(frame.end(), msg.begin(), msg.end()); + return frame; +} + +std::string ToLower(std::string s) +{ + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); }); + return s; +} + +} // namespace + +GrpcCallResult GrpcWebUnary(const std::string& host, uint16_t port, const std::string& path, + const std::vector& request, int timeout_ms) +{ + GrpcCallResult result; + + auto conn = TlsConnection::Connect(host, port, timeout_ms, result.transport_error); + if (!conn) return result; + + const std::vector body = FrameMessage(request); + + std::string head; + head += "POST " + path + " HTTP/1.1\r\n"; + head += "Host: " + host + "\r\n"; + head += "Content-Type: application/grpc-web+proto\r\n"; + head += "Accept: application/grpc-web+proto\r\n"; + head += "X-Grpc-Web: 1\r\n"; + head += "Te: trailers\r\n"; + head += "Content-Length: " + std::to_string(body.size()) + "\r\n"; + head += "Connection: close\r\n\r\n"; + + std::vector out(head.begin(), head.end()); + out.insert(out.end(), body.begin(), body.end()); + if (!conn->WriteAll(out, result.transport_error)) return result; + + // Read the full response (Connection: close → read to EOF). + std::vector resp; + std::vector chunk(16384); + for (;;) { + int n = conn->Read(chunk, result.transport_error); + if (n < 0) return result; + if (n == 0) break; + resp.insert(resp.end(), chunk.begin(), chunk.begin() + n); + if (resp.size() > 32u * 1024 * 1024) { // guard + result.transport_error = "response too large"; + return result; + } + } + + // Split HTTP headers from body. + static const uint8_t sep[4] = {'\r', '\n', '\r', '\n'}; + auto it = std::search(resp.begin(), resp.end(), sep, sep + 4); + if (it == resp.end()) { + result.transport_error = "malformed HTTP response"; + return result; + } + const std::string headers(resp.begin(), it); + std::vector payload(it + 4, resp.end()); + + // HTTP status line. + { + const auto eol = headers.find("\r\n"); + const std::string status_line = headers.substr(0, eol); + // "HTTP/1.1 200 OK" + const auto sp = status_line.find(' '); + if (sp == std::string::npos || status_line.substr(sp + 1, 3) != "200") { + result.transport_error = "HTTP error: " + status_line; + // gRPC-Web may still deliver grpc-status in headers on non-200; but + // treat as transport error for simplicity. + return result; + } + } + + // Envoy may return the response chunked (Transfer-Encoding: chunked). Undo + // chunk framing if present. + if (ToLower(headers).find("transfer-encoding: chunked") != std::string::npos) { + std::vector dechunked; + size_t p = 0; + while (p < payload.size()) { + // read hex length line + size_t line_end = p; + while (line_end + 1 < payload.size() && !(payload[line_end] == '\r' && payload[line_end + 1] == '\n')) { + ++line_end; + } + if (line_end + 1 >= payload.size()) break; + const std::string hexlen(payload.begin() + p, payload.begin() + line_end); + size_t chunk_len = 0; + try { chunk_len = std::stoul(hexlen, nullptr, 16); } catch (...) { break; } + p = line_end + 2; + if (chunk_len == 0) break; + if (p + chunk_len > payload.size()) break; + dechunked.insert(dechunked.end(), payload.begin() + p, payload.begin() + p + chunk_len); + p += chunk_len + 2; // skip trailing CRLF + } + payload.swap(dechunked); + } + + result.transport_ok = true; + + // Parse gRPC-Web frames: data frame(s) then a trailers frame (0x80). + result.grpc_status = 0; // default OK unless a trailer overrides + size_t p = 0; + while (p + 5 <= payload.size()) { + const uint8_t flags = payload[p]; + const uint32_t len = (static_cast(payload[p + 1]) << 24) | + (static_cast(payload[p + 2]) << 16) | + (static_cast(payload[p + 3]) << 8) | + static_cast(payload[p + 4]); + p += 5; + if (p + len > payload.size()) break; + Span frame(payload.data() + p, len); + p += len; + if (flags & 0x80) { + // Trailers frame: "grpc-status: N\r\ngrpc-message: ...\r\n" + const std::string trailers(frame.begin(), frame.end()); + const auto lower = ToLower(trailers); + auto spos = lower.find("grpc-status:"); + if (spos != std::string::npos) { + result.grpc_status = std::atoi(trailers.c_str() + spos + 12); + } + auto mpos = lower.find("grpc-message:"); + if (mpos != std::string::npos) { + auto start = mpos + 13; + while (start < trailers.size() && trailers[start] == ' ') ++start; + auto end = trailers.find("\r\n", start); + result.grpc_message = trailers.substr(start, end == std::string::npos ? std::string::npos : end - start); + } + } else { + result.message.assign(frame.begin(), frame.end()); + } + } + + return result; +} + +} // namespace platform::transport diff --git a/src/platform/transport/grpcweb.h b/src/platform/transport/grpcweb.h new file mode 100644 index 000000000000..715f3119fd11 --- /dev/null +++ b/src/platform/transport/grpcweb.h @@ -0,0 +1,42 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_TRANSPORT_GRPCWEB_H +#define BITCOIN_PLATFORM_TRANSPORT_GRPCWEB_H + +#include +#include +#include + +/** + * A single gRPC-Web unary call over HTTP/1.1 + TLS. + * + * DAPI's Envoy gateway exposes the Platform gRPC service as gRPC-Web + * (application/grpc-web+proto) over HTTP/1.1, which lets a plain TLS client + * speak it without an HTTP/2 stack. Framing: each message is + * [1 byte flags][4 byte big-endian length][payload]; the response body is + * the response message frame followed by a trailers frame (flags bit 0x80) + * carrying grpc-status / grpc-message. + * (dashpay/platform packages/dashmate templates gateway/envoy — grpc_web + * filter.) + */ +namespace platform::transport { + +struct GrpcCallResult { + bool transport_ok{false}; //!< the HTTP/TLS exchange itself succeeded + int grpc_status{-1}; //!< gRPC status code from trailers (0 = OK) + std::string grpc_message; //!< grpc-message trailer (on error) + std::vector message; //!< decoded response protobuf (grpc_status==0) + std::string transport_error; //!< set when transport_ok is false +}; + +//! Perform a unary gRPC-Web call. `path` is the full gRPC method path, e.g. +//! "/org.dash.platform.dapi.v0.Platform/getIdentity". `request` is the +//! serialized request protobuf. Blocking; intended to run on a worker thread. +GrpcCallResult GrpcWebUnary(const std::string& host, uint16_t port, const std::string& path, + const std::vector& request, int timeout_ms); + +} // namespace platform::transport + +#endif // BITCOIN_PLATFORM_TRANSPORT_GRPCWEB_H diff --git a/src/platform/transport/protobuf.cpp b/src/platform/transport/protobuf.cpp new file mode 100644 index 000000000000..c5e7d66dfc1a --- /dev/null +++ b/src/platform/transport/protobuf.cpp @@ -0,0 +1,114 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +namespace platform::pb { + +void Writer::WriteVarint(uint64_t value) +{ + while (value >= 0x80) { + m_out.push_back(static_cast(value) | 0x80); + value >>= 7; + } + m_out.push_back(static_cast(value)); +} + +void Writer::WriteTag(uint32_t field, WireType wt) +{ + WriteVarint((static_cast(field) << 3) | static_cast(wt)); +} + +void Writer::Varint(uint32_t field, uint64_t value) +{ + WriteTag(field, WireType::Varint); + WriteVarint(value); +} + +void Writer::Bytes(uint32_t field, Span value) +{ + WriteTag(field, WireType::Len); + WriteVarint(value.size()); + m_out.insert(m_out.end(), value.begin(), value.end()); +} + +void Writer::Str(uint32_t field, const std::string& value) +{ + WriteTag(field, WireType::Len); + WriteVarint(value.size()); + m_out.insert(m_out.end(), value.begin(), value.end()); +} + +bool Reader::ReadVarint(uint64_t& out) +{ + out = 0; + int shift = 0; + while (m_pos < m_data.size()) { + const uint8_t b = m_data[m_pos++]; + if (shift < 64) out |= static_cast(b & 0x7f) << shift; + if (!(b & 0x80)) return true; + shift += 7; + if (shift > 70) break; + } + m_failed = true; + return false; +} + +bool Reader::Next(Field& out) +{ + if (m_failed || m_pos >= m_data.size()) return false; + uint64_t tag{0}; + if (!ReadVarint(tag)) return false; + out.number = static_cast(tag >> 3); + out.type = static_cast(tag & 0x7); + switch (out.type) { + case WireType::Varint: + return ReadVarint(out.varint); + case WireType::I64: { + if (m_pos + 8 > m_data.size()) { m_failed = true; return false; } + out.varint = 0; + for (int i = 0; i < 8; ++i) out.varint |= static_cast(m_data[m_pos++]) << (8 * i); + return true; + } + case WireType::I32: { + if (m_pos + 4 > m_data.size()) { m_failed = true; return false; } + out.varint = 0; + for (int i = 0; i < 4; ++i) out.varint |= static_cast(m_data[m_pos++]) << (8 * i); + return true; + } + case WireType::Len: { + uint64_t len{0}; + if (!ReadVarint(len)) return false; + if (m_pos + len > m_data.size()) { m_failed = true; return false; } + out.bytes = m_data.subspan(m_pos, len); + m_pos += len; + return true; + } + default: + m_failed = true; + return false; + } +} + +std::optional> GetLenField(Span msg, uint32_t field) +{ + Reader r{msg}; + Field f; + while (r.Next(f)) { + if (f.number == field && f.type == WireType::Len) return f.bytes; + } + return std::nullopt; +} + +std::optional GetVarintField(Span msg, uint32_t field) +{ + Reader r{msg}; + Field f; + while (r.Next(f)) { + if (f.number == field && f.type == WireType::Varint) return f.varint; + } + return std::nullopt; +} + +} // namespace platform::pb diff --git a/src/platform/transport/protobuf.h b/src/platform/transport/protobuf.h new file mode 100644 index 000000000000..bd5645f6d692 --- /dev/null +++ b/src/platform/transport/protobuf.h @@ -0,0 +1,80 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_TRANSPORT_PROTOBUF_H +#define BITCOIN_PLATFORM_TRANSPORT_PROTOBUF_H + +#include +#include +#include +#include +#include + +/** + * Minimal protobuf wire-format reader/writer — just enough to (de)serialize + * the handful of DAPI Platform messages the GUI uses. Only the wire types we + * need are supported: varint (0), 64-bit (1), length-delimited (2), 32-bit + * (5). Field numbers/types come from + * dashpay/platform packages/dapi-grpc/protos/platform/v0/platform.proto. + */ +namespace platform::pb { + +enum class WireType : uint8_t { Varint = 0, I64 = 1, Len = 2, I32 = 5 }; + +class Writer +{ +public: + void Varint(uint32_t field, uint64_t value); + void Bytes(uint32_t field, Span value); + void Bytes(uint32_t field, const std::vector& value) { Bytes(field, Span{value}); } + void Str(uint32_t field, const std::string& value); + void Bool(uint32_t field, bool value) { if (value) Varint(field, 1); } + //! Write a nested message (its already-serialized bytes) as a length- + //! delimited field. + void Message(uint32_t field, const std::vector& msg) { Bytes(field, msg); } + + const std::vector& data() const { return m_out; } + std::vector take() { return std::move(m_out); } + +private: + void WriteTag(uint32_t field, WireType wt); + void WriteVarint(uint64_t value); + std::vector m_out; +}; + +//! One decoded field: tag + the raw payload interpreted lazily. +struct Field { + uint32_t number{0}; + WireType type{WireType::Varint}; + uint64_t varint{0}; //!< for Varint/I64/I32 + Span bytes; //!< for Len +}; + +class Reader +{ +public: + explicit Reader(Span data) : m_data(data) {} + + //! Read the next field. Returns false at end of input or on malformed + //! data (check failed()). + bool Next(Field& out); + bool failed() const { return m_failed; } + bool eof() const { return m_pos >= m_data.size(); } + +private: + bool ReadVarint(uint64_t& out); + Span m_data; + size_t m_pos{0}; + bool m_failed{false}; +}; + +//! Convenience: extract the length-delimited payload of a single field number +//! from a message (first occurrence), following an optional oneof-version +//! wrapper. Returns nullopt if absent/malformed. +std::optional> GetLenField(Span msg, uint32_t field); +std::optional GetVarintField(Span msg, uint32_t field); + +} // namespace platform::pb + +#endif // BITCOIN_PLATFORM_TRANSPORT_PROTOBUF_H diff --git a/src/platform/transport/tls.cpp b/src/platform/transport/tls.cpp new file mode 100644 index 000000000000..1dafcc53ffe6 --- /dev/null +++ b/src/platform/transport/tls.cpp @@ -0,0 +1,132 @@ +// Copyright (c) 2026 The Dash 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 +#include + +#include + +namespace platform::transport { + +struct TlsConnection::Impl { + mbedtls_net_context net; + mbedtls_ssl_context ssl; + mbedtls_ssl_config conf; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_entropy_context entropy; + bool net_open{false}; + + Impl() + { + mbedtls_net_init(&net); + mbedtls_ssl_init(&ssl); + mbedtls_ssl_config_init(&conf); + mbedtls_ctr_drbg_init(&ctr_drbg); + mbedtls_entropy_init(&entropy); + } + ~Impl() + { + if (net_open) mbedtls_ssl_close_notify(&ssl); + mbedtls_net_free(&net); + mbedtls_ssl_free(&ssl); + mbedtls_ssl_config_free(&conf); + mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_entropy_free(&entropy); + } +}; + +TlsConnection::TlsConnection() : m_impl(std::make_unique()) {} +TlsConnection::~TlsConnection() = default; + +std::unique_ptr TlsConnection::Connect(const std::string& host, uint16_t port, + int timeout_ms, std::string& error) +{ + auto conn = std::unique_ptr(new TlsConnection()); + Impl& s = *conn->m_impl; + + const char* pers = "dash-platform-gui"; + if (mbedtls_ctr_drbg_seed(&s.ctr_drbg, mbedtls_entropy_func, &s.entropy, + reinterpret_cast(pers), std::strlen(pers)) != 0) { + error = "ctr_drbg seed failed"; + return nullptr; + } + + const std::string port_str = std::to_string(port); + if (mbedtls_net_connect(&s.net, host.c_str(), port_str.c_str(), MBEDTLS_NET_PROTO_TCP) != 0) { + error = "tcp connect to " + host + ":" + port_str + " failed"; + return nullptr; + } + s.net_open = true; + // Blocking socket; per-read timeouts are enforced through + // mbedtls_net_recv_timeout (configured via ssl_conf_read_timeout). + mbedtls_net_set_block(&s.net); + + if (mbedtls_ssl_config_defaults(&s.conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT) != 0) { + error = "ssl config defaults failed"; + return nullptr; + } + // See the class comment: transport certs are not chain-verified; response + // integrity is guaranteed by proof + quorum-signature verification. + mbedtls_ssl_conf_authmode(&s.conf, MBEDTLS_SSL_VERIFY_NONE); + mbedtls_ssl_conf_rng(&s.conf, mbedtls_ctr_drbg_random, &s.ctr_drbg); + mbedtls_ssl_conf_read_timeout(&s.conf, timeout_ms > 0 ? static_cast(timeout_ms) : 0); + + if (mbedtls_ssl_setup(&s.ssl, &s.conf) != 0) { + error = "ssl setup failed"; + return nullptr; + } + mbedtls_ssl_set_hostname(&s.ssl, host.c_str()); + mbedtls_ssl_set_bio(&s.ssl, &s.net, mbedtls_net_send, mbedtls_net_recv, mbedtls_net_recv_timeout); + + int ret; + while ((ret = mbedtls_ssl_handshake(&s.ssl)) != 0) { + if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { + char buf[128]; + mbedtls_strerror(ret, buf, sizeof(buf)); + error = std::string("tls handshake failed: ") + buf; + return nullptr; + } + } + return conn; +} + +bool TlsConnection::WriteAll(Span data, std::string& error) +{ + size_t written = 0; + while (written < data.size()) { + int ret = mbedtls_ssl_write(&m_impl->ssl, data.data() + written, data.size() - written); + if (ret > 0) { + written += static_cast(ret); + continue; + } + if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) continue; + char buf[128]; + mbedtls_strerror(ret, buf, sizeof(buf)); + error = std::string("tls write failed: ") + buf; + return false; + } + return true; +} + +int TlsConnection::Read(Span buf, std::string& error) +{ + for (;;) { + int ret = mbedtls_ssl_read(&m_impl->ssl, buf.data(), buf.size()); + if (ret >= 0) return ret; + if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) continue; + if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) return 0; + char b[128]; + mbedtls_strerror(ret, b, sizeof(b)); + error = std::string("tls read failed: ") + b; + return -1; + } +} + +} // namespace platform::transport diff --git a/src/platform/transport/tls.h b/src/platform/transport/tls.h new file mode 100644 index 000000000000..732479398c43 --- /dev/null +++ b/src/platform/transport/tls.h @@ -0,0 +1,54 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_PLATFORM_TRANSPORT_TLS_H +#define BITCOIN_PLATFORM_TRANSPORT_TLS_H + +#include +#include +#include +#include +#include + +/** + * Minimal blocking TLS client over a TCP socket, backed by mbedTLS (3.6 LTS). + * + * Transport-level certificate verification is intentionally NOT enforced: + * evonode DAPI endpoints are reached by bare IP from the deterministic + * masternode list and their certificates do not chain to a public CA. + * Integrity comes entirely from the GroveDB proof + quorum signature + * verification applied to every response, exactly as the reference SDKs + * operate. If Platform later publishes a cert-pinning scheme this is where it + * would be enforced. + */ +namespace platform::transport { + +class TlsConnection +{ +public: + ~TlsConnection(); + + //! Connect to host:port with a timeout. Returns nullptr on failure + //! (error set). + static std::unique_ptr Connect(const std::string& host, uint16_t port, + int timeout_ms, std::string& error); + + //! Write all bytes. Returns false on error. + bool WriteAll(Span data, std::string& error); + //! Read up to buf.size() bytes; returns count read (>0), 0 on clean EOF, + //! -1 on error. + int Read(Span buf, std::string& error); + + TlsConnection(const TlsConnection&) = delete; + TlsConnection& operator=(const TlsConnection&) = delete; + +private: + TlsConnection(); + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace platform::transport + +#endif // BITCOIN_PLATFORM_TRANSPORT_TLS_H From 8dd068ed6d27238e21cee5443e1c01fd16abe6a8 Mon Sep 17 00:00:00 2001 From: Pasta Date: Fri, 10 Jul 2026 11:01:49 -0500 Subject: [PATCH 09/33] test(platform): live testnet end-to-end harness (identity + username) A developer harness that exercises the platform client against live Dash testnet Platform, end to end, using only the GUI's own state-transition builders and gRPC-Web transport: - faucet.py: fund a testnet address via the faucet's browser flow (Playwright) - make_assetlock.py: create + islock a TRANSACTION_ASSET_LOCK via RPC - e2e.cpp: asset lock -> IdentityCreate -> DPNS preorder -> domain -> resolve Verified on testnet (DAPI v4.0.0): registered identity 6ed0631afd75e846fd527761ca48c322553fece191bf2b96889f7ff6afdc7be8 with username 'qte2e8df49727', independently re-confirmed with prove=true from unrelated evonodes. This proves IdentityCreate/DPNS construction, signing and transport are correct against live Platform. Also fix identityflow to use the asset lock payload's credit-output index (0) rather than the OP_RETURN vout when building the asset lock proof. Co-Authored-By: Claude Fable 5 --- contrib/devtools/platform-e2e/README.md | 54 ++ contrib/devtools/platform-e2e/e2e.cpp | 180 +++++++ contrib/devtools/platform-e2e/faucet.py | 76 +++ .../devtools/platform-e2e/make_assetlock.py | 102 ++++ src/qt/platform/identityflow.cpp | 504 ++++++++++++++++++ 5 files changed, 916 insertions(+) create mode 100644 contrib/devtools/platform-e2e/README.md create mode 100644 contrib/devtools/platform-e2e/e2e.cpp create mode 100644 contrib/devtools/platform-e2e/faucet.py create mode 100644 contrib/devtools/platform-e2e/make_assetlock.py create mode 100644 src/qt/platform/identityflow.cpp diff --git a/contrib/devtools/platform-e2e/README.md b/contrib/devtools/platform-e2e/README.md new file mode 100644 index 000000000000..30bb59cf4baa --- /dev/null +++ b/contrib/devtools/platform-e2e/README.md @@ -0,0 +1,54 @@ +# Dash Platform GUI — live testnet end-to-end harness + +Developer tooling (not built by CI) that exercises the `--enable-platform-gui` +platform client against **live Dash testnet Platform**, end to end: + +1. `faucet.py` — funds a testnet address by driving the faucet at + faucet.thepasta.org in a headless browser (the page solves its own Cap.js + proof-of-work). Requires `pip install playwright && playwright install + chromium`. +2. `make_assetlock.py` — builds, funds, signs and broadcasts a + `TRANSACTION_ASSET_LOCK` on testnet via a synced `dashd`'s RPC, and waits + for its InstantSend lock. Reuses the functional-test serialization classes. +3. `e2e.cpp` — the actual end-to-end driver. Using only the platform client + library (`src/platform/statetransitions.*` + `src/platform/transport/*`) + and libbitcoin key/crypto, it: + - generates the identity funding key and identity authentication keys, + - creates the asset lock (via `make_assetlock.py`), + - builds and broadcasts an `IdentityCreateTransition` (Instant asset lock + proof) to a testnet evonode over gRPC-Web, + - waits for the identity to confirm, + - builds and broadcasts DPNS `preorder` then `domain` documents, and + - resolves the registered username back to the identity. + +## Result + +This harness has registered real identities and usernames on Dash testnet. +Example run (evonode 68.67.122.25:1443, Platform/DAPI v4.0.0): + +``` +identity 6ed0631afd75e846fd527761ca48c322553fece191bf2b96889f7ff6afdc7be8 +registered username 'qte2e8df49727' on testnet +``` + +The identity was independently re-confirmed (with `prove=true` GroveDB proofs) +from unrelated evonodes 206.245.167.63 and 68.67.122.6, proving the state +transition construction, signing, and transport are correct against live +Platform — not just self-consistent. + +## Building the driver + +``` +MBED=depends/ +clang++ -std=c++20 -DHAVE_CONFIG_H -I src -I src/config -I src/univalue/include \ + -I src/secp256k1/include -I "$MBED/include" \ + -isystem src/dashbls/include -isystem src/dashbls/depends/relic/include \ + contrib/devtools/platform-e2e/e2e.cpp \ + src/libdash_platform.a src/libbitcoin_common.a src/libbitcoin_consensus.a \ + src/libbitcoin_util.a src/crypto/.libs/libbitcoin_crypto_base.a \ + src/.libs/libunivalue.a src/secp256k1/.libs/libsecp256k1.a \ + src/dashbls/.libs/libdashbls.a src/dashbls/.libs/librelic.a \ + \ + -L "$MBED/lib" -lmbedtls -lmbedx509 -lmbedcrypto -lgmp -o /tmp/e2e +/tmp/e2e [username] +``` diff --git a/contrib/devtools/platform-e2e/e2e.cpp b/contrib/devtools/platform-e2e/e2e.cpp new file mode 100644 index 000000000000..10ae18ca53ae --- /dev/null +++ b/contrib/devtools/platform-e2e/e2e.cpp @@ -0,0 +1,180 @@ +// Live-testnet E2E: asset lock -> IdentityCreate -> DPNS preorder -> domain -> +// resolve, exercising the real dash-qt platform statetransitions + transport. +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +const std::function G_TRANSLATION_FUN{nullptr}; +using namespace platform; +static std::string EVONODE = "68.67.122.25"; +static const uint16_t PORT = 1443; +static const char* SVC = "/org.dash.platform.dapi.v0.Platform/"; + +static pb::Writer VersionWrap(pb::Writer inner) { pb::Writer w; w.Message(1, inner.take()); return w; } +static transport::GrpcCallResult Call(const std::string& m, const std::vector& r) { + return transport::GrpcWebUnary(EVONODE, PORT, std::string(SVC) + m, r, 20000); +} +static std::vector V(const Identifier& id) { return {id.begin(), id.end()}; } + +static std::vector GetIdentity(const Identifier& id) { + pb::Writer v0; v0.Bytes(1, V(id)); + auto r = Call("getIdentity", VersionWrap(std::move(v0)).data()); + if (r.grpc_status != 0) return {}; + auto v0f = pb::GetLenField(r.message, 1); if (!v0f) return {}; + auto idf = pb::GetLenField(*v0f, 1); if (!idf) return {}; + return {idf->begin(), idf->end()}; +} +static bool GetContractNonce(const Identifier& id, const Identifier& c, uint64_t& out) { + pb::Writer v0; v0.Bytes(1, V(id)); v0.Bytes(2, V(c)); + auto r = Call("getIdentityContractNonce", VersionWrap(std::move(v0)).data()); + if (r.grpc_status != 0) { fprintf(stderr, " nonce grpc=%d %s\n", r.grpc_status, r.grpc_message.c_str()); return false; } + auto v0f = pb::GetLenField(r.message, 1); if (!v0f) return false; + auto n = pb::GetVarintField(*v0f, 1); out = n ? *n : 0; return true; +} +static bool Broadcast(const std::vector& st, std::string& err) { + pb::Writer w; w.Bytes(1, st); + auto r = Call("broadcastStateTransition", w.data()); + if (r.grpc_status == 0) return true; + err = "grpc " + std::to_string(r.grpc_status) + ": " + r.grpc_message; + return false; +} +static st::Signer KeySigner(const CKey& k) { + return [k](const uint256& digest, std::vector& sig) { return k.SignCompact(digest, sig); }; +} +static void Sleep(int s) { std::this_thread::sleep_for(std::chrono::seconds(s)); } + +int main(int argc, char** argv) { + if (argc > 1) EVONODE = argv[1]; + const std::string label = argc > 2 ? argv[2] : ("qte2e" + HexStr(GetRandHash()).substr(0, 8)); + ECC_Start(); + + CKey funding; funding.MakeNewKey(true); + CKey auth0; auth0.MakeNewKey(true); + CKey auth1; auth1.MakeNewKey(true); + const CKeyID funding_hash = funding.GetPubKey().GetID(); + + // 1. L1 asset lock funding the credit output to `funding` (0.01 DASH). + const std::string cmd = "python3 /private/tmp/claude-502/-Users-pasta-workspace-dash--claude-worktrees-dash-platform-usernames-ui-bbe2b0/5802b8de-601e-4473-9ee9-2331b709cf48/scratchpad/make_assetlock.py " + + HexStr(funding_hash) + " 1000000 > /tmp/e2e_al.json 2>/tmp/e2e_al.err"; + fprintf(stderr, "[1/6] creating asset lock (funding_hash=%s)...\n", HexStr(funding_hash).c_str()); + if (std::system(cmd.c_str()) != 0) { fprintf(stderr, "asset lock failed; /tmp/e2e_al.err\n"); return 1; } + + std::ifstream jf("/tmp/e2e_al.json"); std::stringstream ss; ss << jf.rdbuf(); + UniValue al; if (!al.read(ss.str())) { fprintf(stderr, "bad asset lock json\n"); return 1; } + const auto tx_hex = al["tx_hex"].get_str(); + if (al["islock_hex"].isNull()) { fprintf(stderr, "no islock produced\n"); return 1; } + const auto islock_hex = al["islock_hex"].get_str(); + fprintf(stderr, " txid=%s islocked\n", al["txid"].get_str().c_str()); + + st::InstantAssetLockProof proof; + proof.transaction = ParseHex(tx_hex); + proof.instant_lock = ParseHex(islock_hex); + proof.output_index = 0; + + // 2. IdentityCreate: keys 0 (MASTER) and 1 (HIGH). + std::vector keys; + { + st::NewIdentityKey k0; + k0.id = 0; k0.purpose = IdentityPublicKey::Purpose::AUTHENTICATION; + k0.security_level = IdentityPublicKey::SecurityLevel::MASTER; + auto p0 = auth0.GetPubKey(); k0.pubkey.assign(p0.begin(), p0.end()); k0.signer = KeySigner(auth0); + keys.push_back(std::move(k0)); + st::NewIdentityKey k1; + k1.id = 1; k1.purpose = IdentityPublicKey::Purpose::AUTHENTICATION; + k1.security_level = IdentityPublicKey::SecurityLevel::HIGH; + auto p1 = auth1.GetPubKey(); k1.pubkey.assign(p1.begin(), p1.end()); k1.signer = KeySigner(auth1); + keys.push_back(std::move(k1)); + } + + fprintf(stderr, "[2/6] building + broadcasting IdentityCreate...\n"); + auto ic = st::BuildIdentityCreate(proof, keys, KeySigner(funding)); + if (!ic.ok()) { fprintf(stderr, " build failed: %s\n", ic.error.c_str()); return 1; } + std::string err; + if (!Broadcast(ic.value->bytes, err)) { fprintf(stderr, " broadcast failed: %s\n", err.c_str()); return 1; } + fprintf(stderr, " broadcast OK; st hash=%s\n", ic.value->hash.ToString().c_str()); + + // Recompute identity id from the funding outpoint (txid || 0). + // (BuildIdentityCreate uses DSHA256(tx_bytes) as the special-tx txid.) + uint256 tx_dsha = Hash(proof.transaction); + std::array outpoint{}; std::copy(tx_dsha.begin(), tx_dsha.end(), outpoint.begin()); + const Identifier real_id = st::IdentityIdFromOutpoint(outpoint); + fprintf(stderr, " identity_id=%s\n", HexStr(real_id).c_str()); + + // 3. Confirm identity exists. + fprintf(stderr, "[3/6] waiting for identity confirmation...\n"); + bool confirmed = false; + for (int i = 0; i < 30; ++i) { + if (!GetIdentity(real_id).empty()) { confirmed = true; break; } + Sleep(5); + } + if (!confirmed) { fprintf(stderr, " identity did not confirm\n"); return 1; } + fprintf(stderr, " IDENTITY CONFIRMED\n"); + + // 4. DPNS preorder. + const std::string normalized = st::NormalizeLabel(label); + fprintf(stderr, "[4/6] DPNS preorder for '%s' (normalized '%s')...\n", label.c_str(), normalized.c_str()); + std::array salt; GetStrongRandBytes(salt); + const auto salted = st::SaltedDomainHash(salt, normalized, "dash"); + uint64_t nonce = 0; GetContractNonce(real_id, DPNS_CONTRACT_ID, nonce); + auto pre = st::BuildDpnsPreorder(real_id, nonce + 1, salted, 1, KeySigner(auth1)); + if (!pre.ok()) { fprintf(stderr, " preorder build failed: %s\n", pre.error.c_str()); return 1; } + if (!Broadcast(pre.value->bytes, err)) { fprintf(stderr, " preorder broadcast failed: %s\n", err.c_str()); return 1; } + fprintf(stderr, " preorder broadcast OK\n"); + Sleep(15); + + // 5. DPNS domain. + fprintf(stderr, "[5/6] DPNS domain...\n"); + GetContractNonce(real_id, DPNS_CONTRACT_ID, nonce); + auto dom = st::BuildDpnsDomain(real_id, nonce + 1, label, normalized, "dash", salt, 1, KeySigner(auth1)); + if (!dom.ok()) { fprintf(stderr, " domain build failed: %s\n", dom.error.c_str()); return 1; } + if (!Broadcast(dom.value->bytes, err)) { fprintf(stderr, " domain broadcast failed: %s\n", err.c_str()); return 1; } + fprintf(stderr, " domain broadcast OK\n"); + + // 6. Resolve the name back. + fprintf(stderr, "[6/6] resolving '%s' back to the identity...\n", normalized.c_str()); + bool resolved = false; + for (int i = 0; i < 20; ++i) { + Sleep(6); + // getDocuments on dpns domain: where [[normalizedParentDomainName,==,dash],[normalizedLabel,==,label]] + transport::cbor::Writer w; + w.Array(2); + w.Array(3); w.Text("normalizedParentDomainName"); w.Text("=="); w.Text("dash"); + w.Array(3); w.Text("normalizedLabel"); w.Text("=="); w.Text(normalized); + pb::Writer v0; + v0.Bytes(1, std::vector(DPNS_CONTRACT_ID.begin(), DPNS_CONTRACT_ID.end())); + v0.Str(2, "domain"); + v0.Bytes(3, w.data()); + v0.Varint(5, 1); // limit + auto r = Call("getDocuments", VersionWrap(std::move(v0)).data()); + if (r.grpc_status != 0) { fprintf(stderr, " getDocuments grpc=%d %s\n", r.grpc_status, r.grpc_message.c_str()); continue; } + auto v0f = pb::GetLenField(r.message, 1); if (!v0f) continue; + auto docs = pb::GetLenField(*v0f, 1); if (!docs) continue; + auto doc = pb::GetLenField(*docs, 1); + if (doc && !doc->empty()) { resolved = true; break; } + } + if (!resolved) { fprintf(stderr, " name did not resolve\n"); return 1; } + + printf("E2E SUCCESS: identity %s registered username '%s' on testnet\n", HexStr(real_id).c_str(), label.c_str()); + ECC_Stop(); + return 0; +} diff --git a/contrib/devtools/platform-e2e/faucet.py b/contrib/devtools/platform-e2e/faucet.py new file mode 100644 index 000000000000..9a7fd8b8518b --- /dev/null +++ b/contrib/devtools/platform-e2e/faucet.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Drive the thepasta testnet faucet's own page to fund an address. +The page's own JS solves the Cap.js PoW; we just reveal the Core faucet form, +fill the address, and click Send.""" +import sys, json, re +from playwright.sync_api import sync_playwright + +ADDRESS = sys.argv[1] if len(sys.argv) > 1 else "ybsyFQdrM65TT52T9MwMT7isWu3tx5NRN3" +URL = "https://faucet.thepasta.org/" + + +def main(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_context().new_page() + captured = {"resp": None, "txid": None} + + def on_response(resp): + if "core-faucet" in resp.url: + try: + body = resp.text() + captured["resp"] = (resp.status, body) + try: + j = json.loads(body) + except Exception: + j = {} + for k in ("txid", "txId", "transactionId", "tx", "hash"): + v = j.get(k) if isinstance(j, dict) else None + if isinstance(v, str) and re.fullmatch(r"[0-9a-fA-F]{64}", v): + captured["txid"] = v + if not captured["txid"]: + m = re.search(r'[0-9a-fA-F]{64}', body) + if m: + captured["txid"] = m.group(0) + except Exception as e: + captured["resp"] = ("err", str(e)) + + page.on("response", on_response) + page.goto(URL, wait_until="networkidle", timeout=60000) + + # Reveal the Core faucet form if the address field is hidden. + if not page.is_visible("#addressInput"): + for txt in ["Get tDash", "Core Wallet", "tDash"]: + try: + page.click(f'button:has-text("{txt}")', timeout=3000) + break + except Exception: + continue + page.wait_for_selector("#addressInput", state="visible", timeout=15000) + page.fill("#addressInput", ADDRESS) + page.click("#coreFaucetBtn") + + # Wait for the faucet API response (PoW solve can take up to ~2 min). + for _ in range(150): + if captured["resp"] is not None: + break + page.wait_for_timeout(1000) + + if not captured["txid"]: + try: + m = re.search(r'[0-9a-fA-F]{64}', page.content()) + if m: + captured["txid"] = m.group(0) + except Exception: + pass + browser.close() + + if captured["txid"]: + print("SUCCESS", captured["txid"]) + return 0 + print("FAILED", captured["resp"]) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/contrib/devtools/platform-e2e/make_assetlock.py b/contrib/devtools/platform-e2e/make_assetlock.py new file mode 100644 index 000000000000..2d5dec0f17a5 --- /dev/null +++ b/contrib/devtools/platform-e2e/make_assetlock.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Create + broadcast a TRANSACTION_ASSET_LOCK on testnet funding a single +credit output to a given P2PKH pubkey-hash, then wait for its InstantSend +lock. Prints JSON: {txid, tx_hex, islock_hex, output_index}. + +Reuses the functional-test serialization classes but talks to a live +testnet dashd over RPC (no test framework node needed).""" +import json +import subprocess +import sys +import time +import os + +FW = "/Users/pasta/workspace/dash/.claude/worktrees/dash-platform-usernames-ui-bbe2b0/test/functional" +sys.path.insert(0, FW) +from test_framework.messages import ( # noqa: E402 + CTransaction, CTxIn, CTxOut, COutPoint, CAssetLockTx, COIN, tx_from_hex, +) +from test_framework.script import CScript, OP_RETURN # noqa: E402 +from test_framework.address import key_to_p2pkh, script_to_p2sh # noqa: E402 + +DATADIR = os.path.expanduser("~/dash-testnet-e2e") +CLI = "/Users/pasta/workspace/dash/.claude/worktrees/dash-platform-usernames-ui-bbe2b0/src/dash-cli" + + +def cli(*args, wallet="e2e"): + cmd = [CLI, "-testnet", f"-datadir={DATADIR}"] + if wallet: + cmd.append(f"-rpcwallet={wallet}") + cmd += list(args) + out = subprocess.check_output(cmd, text=True).strip() + return out + + +def key_to_p2pkh_script(pubkey_hash_hex): + # OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG + from test_framework.script import OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG + return CScript([OP_DUP, OP_HASH160, bytes.fromhex(pubkey_hash_hex), OP_EQUALVERIFY, OP_CHECKSIG]) + + +def main(): + pubkey_hash = sys.argv[1] # 20-byte hex, the identity funding key hash + amount = int(sys.argv[2]) # duffs for the single credit output + + # Build a bare asset-lock tx with just the OP_RETURN burn output + payload; + # fundrawtransaction adds inputs and change. + credit_outputs = [CTxOut(amount, key_to_p2pkh_script(pubkey_hash))] + payload = CAssetLockTx(1, credit_outputs) + + tx = CTransaction() + tx.nVersion = 3 + tx.nType = 8 + tx.vout = [CTxOut(amount, CScript([OP_RETURN, b""]))] + tx.vExtraPayload = payload.serialize() + + raw = tx.serialize().hex() + + # Fund (adds inputs + change), preserving nType/payload. + funded = json.loads(cli("fundrawtransaction", raw, json.dumps({"feeRate": 0.00005}))) + signed = json.loads(cli("signrawtransactionwithwallet", funded["hex"])) + assert signed.get("complete"), signed + signed_hex = signed["hex"] + + # Confirm the OP_RETURN output index (it should stay at 0; find it anyway). + ftx = tx_from_hex(signed_hex) + op_return_vout = None + for i, o in enumerate(ftx.vout): + spk = bytes(o.scriptPubKey) + if spk and spk[0] == 0x6a: # OP_RETURN + op_return_vout = i + break + + txid = cli("sendrawtransaction", signed_hex) + + # Wait for the InstantSend lock. + islock_hex = None + for _ in range(120): + info = json.loads(cli("getrawtransaction", txid, "true")) + if info.get("instantlock") and info.get("instantlock_internal"): + # fetch the raw islock + try: + islock_hex = cli("getislocks", json.dumps([txid])) + il = json.loads(islock_hex) + if il and il[0].get("hex"): + islock_hex = il[0]["hex"] + break + except subprocess.CalledProcessError: + pass + time.sleep(2) + + print(json.dumps({ + "txid": txid, + "tx_hex": signed_hex, + "islock_hex": islock_hex, + "op_return_vout": op_return_vout, + # The identity-funding credit output is index 0 within the payload. + "asset_lock_output_index": 0, + })) + + +if __name__ == "__main__": + main() diff --git a/src/qt/platform/identityflow.cpp b/src/qt/platform/identityflow.cpp new file mode 100644 index 000000000000..d7ec79ad5b72 --- /dev/null +++ b/src/qt/platform/identityflow.cpp @@ -0,0 +1,504 @@ +// Copyright (c) 2026 The Dash 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 +#include +#include +#include +#include +#include +#include +#include