diff --git a/.github/workflows/build-depends.yml b/.github/workflows/build-depends.yml index e79c5fa4248e..4d941b9fc666 100644 --- a/.github/workflows/build-depends.yml +++ b/.github/workflows/build-depends.yml @@ -15,6 +15,11 @@ on: description: "Short hash of the CI base image manifest for cache busting" required: true type: string + rust-vendor-artifact: + description: "Artifact holding freshly generated Rust vendor archives" + required: false + type: string + default: "" runs-on: description: "Runner label to use (e.g., ubuntu-24.04 or ubuntu-24.04-arm)" required: true @@ -144,6 +149,28 @@ jobs: key: depends-sources-${{ hashFiles('depends/packages/*') }} restore-keys: depends-sources- + - name: Restore Rust vendor sources + id: rust-vendor-cache + if: inputs.build-target == 'linux64_platform_gui' + uses: actions/cache/restore@v5 + with: + path: depends/sources/platform-cxx-*-vendored.tar.gz + key: depends-rust-vendor-sources-${{ hashFiles('depends/Makefile', 'depends/funcs.mk', 'depends/packages/native_rust.mk', 'depends/packages/platform_cxx.mk', 'depends/patches/platform_cxx/cargo-config.toml') }} + + - name: Download Rust vendor sources + if: inputs.build-target == 'linux64_platform_gui' && steps.rust-vendor-cache.outputs.cache-hit != 'true' && inputs.rust-vendor-artifact != '' + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.rust-vendor-artifact }} + path: depends/sources + + - name: Check Rust vendor sources are present + if: inputs.build-target == 'linux64_platform_gui' && steps.rust-vendor-cache.outputs.cache-hit != 'true' && inputs.rust-vendor-artifact == '' + run: | + echo "::error::Rust vendor source cache missed and no same-run artifact was provided" + exit 1 + shell: bash + - name: Restore SDKs cache id: sdk-cache uses: actions/cache/restore@v5 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce8191dc99ea..74de8337be81 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -165,6 +165,18 @@ jobs: base-image-digest: ${{ needs.check-skip.outputs.base-image-digest }} runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + depends-linux64_platform_gui: + name: x86_64-pc-linux-gnu_platform_gui + uses: ./.github/workflows/build-depends.yml + needs: [check-skip, container, cache-sources] + if: ${{ vars.SKIP_LINUX64_PLATFORM_GUI == '' }} + with: + build-target: linux64_platform_gui + container-path: ${{ needs.container.outputs.path }} + base-image-digest: ${{ needs.check-skip.outputs.base-image-digest }} + rust-vendor-artifact: ${{ needs.cache-sources.outputs.rust-vendor-artifact }} + runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + depends-mac: name: x86_64-apple-darwin uses: ./.github/workflows/build-depends.yml @@ -277,6 +289,20 @@ jobs: depends-artifact: ${{ needs.depends-linux64_nowallet.outputs.built-artifact }} runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + src-linux64_platform_gui: + name: linux64_platform_gui-build + uses: ./.github/workflows/build-src.yml + needs: [check-skip, container, depends-linux64_platform_gui] + if: ${{ vars.SKIP_LINUX64_PLATFORM_GUI == '' }} + with: + build-target: linux64_platform_gui + container-path: ${{ needs.container.outputs.path }} + depends-key: ${{ needs.depends-linux64_platform_gui.outputs.key }} + depends-host: ${{ needs.depends-linux64_platform_gui.outputs.host }} + depends-dep-opts: ${{ needs.depends-linux64_platform_gui.outputs.dep-opts }} + depends-artifact: ${{ needs.depends-linux64_platform_gui.outputs.built-artifact }} + runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + src-linux64_sqlite: name: linux64_sqlite-build uses: ./.github/workflows/build-src.yml @@ -372,6 +398,17 @@ jobs: container-path: ${{ needs.container-slim.outputs.path }} runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + test-linux64_platform_gui: + name: linux64_platform_gui-test + uses: ./.github/workflows/test-src.yml + needs: [check-skip, container-slim, src-linux64_platform_gui, lint] + if: ${{ vars.SKIP_LINUX64_PLATFORM_GUI == '' }} + with: + bundle-key: ${{ needs.src-linux64_platform_gui.outputs.key }} + build-target: linux64_platform_gui + container-path: ${{ needs.container-slim.outputs.path }} + runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + test-linux64_sqlite: name: linux64_sqlite-test uses: ./.github/workflows/test-src.yml diff --git a/.github/workflows/cache-depends-sources.yml b/.github/workflows/cache-depends-sources.yml index 122851d8cd72..5e163b94c776 100644 --- a/.github/workflows/cache-depends-sources.yml +++ b/.github/workflows/cache-depends-sources.yml @@ -8,6 +8,10 @@ on: required: false type: string default: ubuntu-24.04-arm + outputs: + rust-vendor-artifact: + description: "Artifact holding freshly generated Rust vendor archives" + value: ${{ jobs.cache-sources.outputs.rust-vendor-artifact }} schedule: # Run daily at 6 AM UTC on the default branch to keep cache warm - cron: '0 6 * * *' @@ -18,6 +22,8 @@ jobs: # Intentionally keep scheduled cache warming on GitHub-hosted ARM runners. # Blacksmith caches are expected to persist long enough without a warmup cron. runs-on: ${{ inputs.runs-on || 'ubuntu-24.04-arm' }} + outputs: + rust-vendor-artifact: ${{ steps.vendor-artifact.outputs.name }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -35,6 +41,36 @@ jobs: restore-keys: depends-sources- lookup-only: true + - name: Cache Rust vendor sources + id: rust-vendor-cache + uses: actions/cache@v5 + with: + path: depends/sources/platform-cxx-*-vendored.tar.gz + key: depends-rust-vendor-sources-${{ hashFiles('depends/Makefile', 'depends/funcs.mk', 'depends/packages/native_rust.mk', 'depends/packages/platform_cxx.mk', 'depends/patches/platform_cxx/cargo-config.toml') }} + - name: Download sources - if: steps.cache-check.outputs.cache-hit != 'true' - run: make -C depends download + if: | + steps.cache-check.outputs.cache-hit != 'true' || + steps.rust-vendor-cache.outputs.cache-hit != 'true' + run: | + make -C depends PLATFORM_GUI=1 download + make -C depends PLATFORM_GUI=1 vendor-platform_cxx-crates + # The cache producer normally runs on ARM, while the platform_gui + # consumer runs on x86_64. Native Rust is selected from the build + # architecture, so fetch the x86_64 compiler archive explicitly too. + make -C depends BUILD=x86_64-pc-linux-gnu PLATFORM_GUI=1 download-one + + - name: Select Rust vendor artifact + id: vendor-artifact + if: github.event_name != 'schedule' && steps.rust-vendor-cache.outputs.cache-hit != 'true' + run: echo "name=depends-rust-vendor-sources-${{ github.run_id }}" >> "$GITHUB_OUTPUT" + + - name: Upload Rust vendor sources + if: steps.vendor-artifact.outputs.name != '' + uses: actions/upload-artifact@v6 + with: + name: ${{ steps.vendor-artifact.outputs.name }} + path: depends/sources/platform-cxx-*-vendored.tar.gz + compression-level: 0 + retention-days: 1 + overwrite: true diff --git a/ci/dash/matrix.sh b/ci/dash/matrix.sh index e54e279422c4..e76379120b7c 100755 --- a/ci/dash/matrix.sh +++ b/ci/dash/matrix.sh @@ -28,6 +28,8 @@ elif [ "$BUILD_TARGET" = "linux64_multiprocess" ]; then source ./ci/test/00_setup_env_native_multiprocess.sh elif [ "$BUILD_TARGET" = "linux64_nowallet" ]; then source ./ci/test/00_setup_env_native_nowallet_libbitcoinkernel.sh +elif [ "$BUILD_TARGET" = "linux64_platform_gui" ]; then + source ./ci/test/00_setup_env_native_platform_gui.sh elif [ "$BUILD_TARGET" = "linux64_sqlite" ]; then source ./ci/test/00_setup_env_native_sqlite.sh elif [ "$BUILD_TARGET" = "linux64_tsan" ]; then diff --git a/ci/test/00_setup_env_native_platform_gui.sh b/ci/test/00_setup_env_native_platform_gui.sh new file mode 100755 index 000000000000..13fb49be63bc --- /dev/null +++ b/ci/test/00_setup_env_native_platform_gui.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# +# 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. + +export LC_ALL=C.UTF-8 + +# Builds depends with PLATFORM_GUI=1 so the Platform-owned CXX binding +# archive (libdash_platform_cxx.a, dash-sdk built offline from vendored +# crates) is produced, hash-verified and installed into the depends prefix, +# then builds dash-qt against that prefix. The --enable-platform-gui configure +# flag and the platform_* unit-test suites arrive with the Platform client +# library and are added to BITCOIN_CONFIG there; until then this lane proves +# the depends knob end to end and that the enriched prefix stays +# link-compatible. Functional tests are skipped: there is no dashd-only +# surface to drive. +export CONTAINER_NAME=ci_native_platform_gui +export HOST=x86_64-pc-linux-gnu +export PACKAGES="python3-zmq qtbase5-dev qttools5-dev-tools libdbus-1-dev libharfbuzz-dev" +export DEP_OPTS="PLATFORM_GUI=1" +export RUN_UNIT_TESTS="true" +export RUN_UNIT_TESTS_SEQUENTIAL="false" +export RUN_FUNCTIONAL_TESTS="false" +export GOAL="install" +export BITCOIN_CONFIG="--with-gui=qt5 --enable-zmq --with-libs=no --enable-reduce-exports LDFLAGS=-static-libstdc++" diff --git a/configure.ac b/configure.ac index f157e099cbd9..107b7b685786 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([PLATFORM_CXX_CFLAGS], [C++ compiler flags for the Dash Platform CXX bindings]) +AC_ARG_VAR([PLATFORM_CXX_LIBS], [Linker flags for the Dash Platform CXX bindings]) + dnl Enable different -fsanitize options AC_ARG_WITH([sanitizers], [AS_HELP_STRING([--with-sanitizers], @@ -883,6 +893,7 @@ case $host in export PKG_CONFIG_PATH="$($BREW --prefix qt@5 2>/dev/null)/lib/pkgconfig:$PKG_CONFIG_PATH" fi + gmp_prefix=$($BREW --prefix gmp 2>/dev/null) if test "$gmp_prefix" != ""; then if test "$suppress_external_warnings" != "no"; then @@ -1938,6 +1949,50 @@ 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. The Platform-owned +dnl CXX archive (dash-sdk, networking included) is linked into dash-qt and the +dnl test binaries only; its TLS stack reads the system trust store, which is +dnl Security.framework on macOS and the crypt32/bcrypt/secur32 APIs on Windows. +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 -z "$PLATFORM_CXX_LIBS"; then + PLATFORM_CXX_LIBS="-ldash_platform_cxx" + fi + PLATFORM_CXX_LIBS="$PLATFORM_CXX_LIBS -lpthread -lm" + case $host in + *darwin*) PLATFORM_CXX_LIBS="$PLATFORM_CXX_LIBS -framework Security -framework CoreFoundation" ;; + *linux*) PLATFORM_CXX_LIBS="$PLATFORM_CXX_LIBS -ldl" ;; + *mingw*) PLATFORM_CXX_LIBS="$PLATFORM_CXX_LIBS -lws2_32 -lbcrypt -lcrypt32 -lsecur32 -lncrypt -luserenv -lntdll" ;; + esac + TEMP_CPPFLAGS="$CPPFLAGS" + TEMP_LIBS="$LIBS" + CPPFLAGS="$CPPFLAGS $PLATFORM_CXX_CFLAGS" + LIBS="$PLATFORM_CXX_LIBS $LIBS" + AC_LANG_PUSH([C++]) + AC_MSG_CHECKING([for Dash Platform CXX bindings]) + AC_LINK_IFELSE([AC_LANG_PROGRAM([[ + #include + ]], [[ + rust::Box client{platform_ffi::new_platform_client()}; + client->shutdown(); + ]])], [AC_MSG_RESULT([yes])], [ + AC_MSG_RESULT([no]) + AC_MSG_ERROR([Dash Platform CXX bindings not found (required by --enable-platform-gui)]) + ]) + AC_LANG_POP + CPPFLAGS="$TEMP_CPPFLAGS" + LIBS="$TEMP_LIBS" + 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(PLATFORM_CXX_CFLAGS) +AC_SUBST(PLATFORM_CXX_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"]) @@ -2135,6 +2190,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 " werror = $enable_werror" echo echo " target os = $host_os" diff --git a/contrib/devtools/update-rust-hashes.py b/contrib/devtools/update-rust-hashes.py new file mode 100755 index 000000000000..41afe9c4f039 --- /dev/null +++ b/contrib/devtools/update-rust-hashes.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2021-2022 The Zcash developers +# 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. + +import hashlib +import re +import sys +import urllib.request +from pathlib import Path + +# Rust standard libraries provisioned in rust_stdlib.mk. Confined to the +# hosts we validate (the Guix release set); see rust_stdlib.mk. +CROSS_TARGETS = [ + # Linux + "aarch64-unknown-linux-musl", + "riscv64gc-unknown-linux-musl", + "x86_64-unknown-linux-musl", + # Windows + "x86_64-pc-windows-gnu", + # macOS + "aarch64-apple-darwin", + "x86_64-apple-darwin", +] + +# Native compilers provisioned in native_rust.mk (build hosts for depends) +NATIVE_TARGETS = [ + # Linux + ("aarch64-unknown-linux-gnu", "aarch64_linux"), + ("x86_64-unknown-linux-gnu", "x86_64_linux"), + # macOS + ("aarch64-apple-darwin", "aarch64_darwin"), + ("x86_64-apple-darwin", "x86_64_darwin"), +] + + +def get_rust_version(makefile_path: Path) -> str: + content = makefile_path.read_text() + match = re.search(r"\$\(package\)_version:=(.+)", content) + if not match: + raise RuntimeError("Could not find Rust version in makefile") + return match.group(1).strip() + + +def compute_sha256(url: str) -> str: + hasher = hashlib.sha256() + with urllib.request.urlopen(url) as response: + while chunk := response.read(8192): + hasher.update(chunk) + return hasher.hexdigest() + + +def update_hash_in_file(makefile_path: Path, pattern: str, new_hash: str) -> None: + content = makefile_path.read_text() + regex = re.compile(rf"^(\$\(package\)_{pattern}:=).*$", re.MULTILINE) + if not regex.search(content): + raise RuntimeError(f"Could not find pattern {pattern} in makefile") + new_content = regex.sub(rf"\g<1>{new_hash}", content) + makefile_path.write_text(new_content) + + +def update_version_in_file(path: Path, pattern: str, version: str) -> None: + content = path.read_text() + regex = re.compile(pattern, re.MULTILINE) + new_content, replacements = regex.subn( + lambda match: f"{match.group(1)}{version}{match.group(2) if match.lastindex == 2 else ''}", content + ) + if replacements != 1: + raise RuntimeError(f"Expected one version pin in {path}, found {replacements}") + path.write_text(new_content) + + +def compute_rust_hash(rust_version: str, rust_target: str) -> str: + url = f"https://static.rust-lang.org/dist/rust-{rust_version}-{rust_target}.tar.gz" + return compute_sha256(url) + + +def compute_stdlib_hash(rust_version: str, rust_target: str) -> str: + url = f"https://static.rust-lang.org/dist/rust-std-{rust_version}-{rust_target}.tar.gz" + return compute_sha256(url) + + +def main() -> int: + script_dir = Path(__file__).resolve().parent + native_rust_path = script_dir / "../../depends/packages/native_rust.mk" + native_rust_path = native_rust_path.resolve() + rust_stdlib_path = script_dir / "../../depends/packages/rust_stdlib.mk" + rust_stdlib_path = rust_stdlib_path.resolve() + for path in (native_rust_path, rust_stdlib_path): + if not path.exists(): + print(f"Error: {path} not found", file=sys.stderr) + return 1 + + rust_version = get_rust_version(native_rust_path) + + print(f"Rust version: {rust_version}\n") + print("Downloading native compiler hashes:") + + native_hashes = {} + for rust_target, makefile_id in NATIVE_TARGETS: + native_hashes[makefile_id] = compute_rust_hash(rust_version, rust_target) + print(f" Downloaded sha256_hash_{makefile_id}") + + print("\nDownloading stdlib hashes:") + stdlib_hashes = {} + for rust_target in CROSS_TARGETS: + stdlib_hashes[rust_target] = compute_stdlib_hash(rust_version, rust_target) + print(f" Downloaded sha256_hash_{rust_target}") + + for makefile_id, hash_value in native_hashes.items(): + update_hash_in_file(native_rust_path, f"sha256_hash_{makefile_id}", hash_value) + for rust_target, hash_value in stdlib_hashes.items(): + update_hash_in_file(rust_stdlib_path, f"sha256_hash_{rust_target}", hash_value) + + update_version_in_file(rust_stdlib_path, r"^(\$\(package\)_version:=).*$", rust_version) + print("\nSynchronized rust_stdlib.mk to the native_rust.mk version") + + print("\nDone!") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/depends/Makefile b/depends/Makefile index c8510f4cc010..bae0ec6932a4 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,14 @@ packages += $(multiprocess_packages) native_packages += $(multiprocess_native_packages) endif +ifeq ($(platform_packages_),) +packages += $(platform_packages) +native_packages += $(platform_native_packages) +rust_download_targets = download-rust-std +endif + +cargo_packages = $(filter platform_cxx,$(packages)) + 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 @@ -202,9 +212,13 @@ $(host_prefix)/.stamp_$(final_build_id): $(native_packages) $(packages) mkdir -p $(@D) echo copying packages: $^ echo to: $(@D) - cd $(@D); $(foreach package,$^, $(build_TAR) xf $($(package)_cached); ) + cd $(@D); $(foreach package,$^, $(build_TAR) --no-same-owner -xf $($(package)_cached); ) touch $@ +# Vendors crate dependencies for cargo-built packages +vendor-dep-crates: $(foreach package,$(cargo_packages),vendor-$(package)-crates) +.PHONY: vendor-dep-crates + # $PATH is not preserved between ./configure and make by convention. Its # modification and overriding at ./configure time is (as I understand it) # supposed to be captured by the AC_{PROG_{,OBJ}CXX,PATH_{PROG,TOOL}} macros, @@ -257,6 +271,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)|' \ @@ -296,18 +311,22 @@ clean: install: check-packages $(host_prefix)/share/config.site -download-one: check-sources $(all_sources) +download-one: check-sources $(all_sources) $(foreach package,$(cargo_packages),$($(package)_vendored_archive)) download-osx: - @$(MAKE) -s HOST=x86_64-apple-darwin download-one + @$(MAKE) -s PLATFORM_GUI=$(PLATFORM_GUI) HOST=x86_64-apple-darwin download-one download-linux: - @$(MAKE) -s HOST=x86_64-unknown-linux-gnu download-one + @$(MAKE) -s PLATFORM_GUI=$(PLATFORM_GUI) HOST=x86_64-unknown-linux-gnu download-one download-win: - @$(MAKE) -s HOST=x86_64-w64-mingw32 download-one -download: download-osx download-linux download-win + @$(MAKE) -s PLATFORM_GUI=$(PLATFORM_GUI) HOST=x86_64-w64-mingw32 download-one +download-rust-std: + @mkdir -p $(SOURCES_PATH) + @mkdir -p $(SOURCES_PATH)/download-stamps + @$(foreach target,$(rust_stdlib_targets),$(call download_rust_std_target,$(target)) && ) true +download: download-osx download-linux download-win $(rust_download_targets) $(foreach package,$(all_packages),$(eval $(call ext_add_stages,$(package)))) -.PHONY: install cached clean clean-all download-one download-osx download-linux download-win download check-packages check-sources +.PHONY: install cached clean clean-all download-one download-osx download-linux download-win download download-rust-std check-packages check-sources .PHONY: FORCE $(V).SILENT: diff --git a/depends/README.md b/depends/README.md index f504627729f0..f4e5f744b5bc 100644 --- a/depends/README.md +++ b/depends/README.md @@ -92,6 +92,8 @@ The following can be set when running make: `make FOO=bar` build script logic) are searched for among the host system packages using `pkg-config`. It allows building with packages of other (newer) versions - `MULTIPROCESS`: build libmultiprocess (experimental, requires cmake) +- `PLATFORM_GUI`: Download/build/cache the Rust toolchain, target standard library and + the Dash Platform CXX bindings needed for `--enable-platform-gui` - `DEBUG`: Disable some optimizations and enable more runtime checking - `HOST_ID_SALT`: Optional salt to use when generating host package ids - `BUILD_ID_SALT`: Optional salt to use when generating build package ids diff --git a/depends/config.site.in b/depends/config.site.in index 398a09b74c63..ef4af20ff1cd 100644 --- a/depends/config.site.in +++ b/depends/config.site.in @@ -50,6 +50,17 @@ 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 "$PLATFORM_CXX_CFLAGS" && test -f "${depends_prefix}/include/dash/platform/ffi.h"; then + PLATFORM_CXX_CFLAGS="-I${depends_prefix}/include" +fi +if test -z "$PLATFORM_CXX_LIBS" && test -f "${depends_prefix}/lib/libdash_platform_cxx.a"; then + PLATFORM_CXX_LIBS="${depends_prefix}/lib/libdash_platform_cxx.a" +fi + if test -z "$with_miniupnpc" && test -n "@no_upnp@"; then with_miniupnpc=no fi diff --git a/depends/funcs.mk b/depends/funcs.mk index 566f83a9868e..2d334ac243fa 100644 --- a/depends/funcs.mk +++ b/depends/funcs.mk @@ -197,6 +197,22 @@ $(1)_cmake += -DCMAKE_C_COMPILER_TARGET=$(host) $(1)_cmake += -DCMAKE_CXX_COMPILER_TARGET=$(host) endif endif + +$(1)_cargo=env CC="$$($(1)_cc)" \ + CXX="$$($(1)_cxx)" \ + AR="$$($(1)_ar)" \ + CFLAGS="$$($(1)_cppflags) $$($(1)_cflags)" \ + CXXFLAGS="$$($(1)_cppflags) $$($(1)_cxxflags)" \ + LDFLAGS="$$($(1)_ldflags)" \ + RUSTFLAGS="-C linker=$$($(1)_extract_dir)/rustc-linker.sh" \ + LD_LIBRARY_PATH="$$($($(1)_type)_prefix)/lib" +ifeq ($(host_os),darwin) +$(1)_cargo += MACOSX_DEPLOYMENT_TARGET="$(OSX_MIN_VERSION)" +ifneq ($(host),$(build)) +$(1)_cargo += SDKROOT="$(OSX_SDK)" +endif +endif +$(1)_cargo += cargo endef define int_add_cmds @@ -269,6 +285,55 @@ $(foreach stage,$(stages), .PHONY: $(1)_$(stage)) endef +# Template for vendoring a package's Rust crate dependencies +# Packages opt-in by defining $(package)_vendored_file_name and $(package)_cargo_manifest +# +# The archive is a real file target and a hard prerequisite of the +# package's preprocess stamp, so a clean build (or `make download`, +# which lists it as a prerequisite) vendors the crates before the +# offline cargo build can run; its absence is never silently skipped. +# vendor-$(1)-crates remains as a phony alias; delete the archive from +# SOURCES_PATH to force a re-vendor. +define int_vendor_crates +ifneq ($($(1)_vendored_file_name),) +$(1)_vendored_archive = $(SOURCES_PATH)/$($(1)_vendored_file_name) + +$$($(1)_vendored_archive): $(native_rust_cached) $($(1)_fetched) + @rm -rf $(WORK_PATH)/vendor-$(1) + @mkdir -p $(WORK_PATH)/vendor-$(1) + @$(build_TAR) --no-same-owner -xf $(native_rust_cached) -C $(WORK_PATH)/vendor-$(1) + @echo "Vendoring $(1) crates..." + @mkdir -p $(WORK_PATH)/vendor-$(1)/src + @cd $(WORK_PATH)/vendor-$(1)/src && $(build_TAR) --no-same-owner --strip-components=1 -xf $(SOURCES_PATH)/$($(1)_file_name) + @if test -f $(PATCHES_PATH)/$(1)/Cargo.lock; then \ + cp $(PATCHES_PATH)/$(1)/Cargo.lock $(WORK_PATH)/vendor-$(1)/src/$($(1)_cargo_lock_path); \ + fi + @$(WORK_PATH)/vendor-$(1)/native/bin/cargo vendor --locked --manifest-path $(WORK_PATH)/vendor-$(1)/src/$($(1)_cargo_manifest) $(WORK_PATH)/vendor-$(1)/src/vendored + @cd $(WORK_PATH)/vendor-$(1)/src; find vendored | sort | $(build_TAR) --no-recursion -czf $$($(1)_vendored_archive) -T - + @rm -rf $(WORK_PATH)/vendor-$(1) + @echo "Created $$($(1)_vendored_archive)" + +vendor-$(1)-crates: $$($(1)_vendored_archive) +.PHONY: vendor-$(1)-crates + +$($(1)_preprocessed): $$($(1)_vendored_archive) +endif +endef + +# A cached archive is trusted only if it matches the pinned hash; anything +# else (including a partial file from an interrupted download) is re-fetched. +# Like fetch_file_inner, download to a temp path and only move a verified +# archive into the source cache so a bad download can never wedge it. +define download_rust_std_target +((echo "$(rust_stdlib_sha256_hash_$(1)) $(SOURCES_PATH)/rust-std-$(rust_stdlib_version)-$(1).tar.gz" | $(build_SHA256SUM) -c - >/dev/null 2>&1 && \ + echo "Already have rust-std-$(rust_stdlib_version)-$(1).tar.gz") || \ + (echo "Downloading rust-std-$(rust_stdlib_version)-$(1).tar.gz..." && \ + $(build_DOWNLOAD) "$(SOURCES_PATH)/rust-std-$(rust_stdlib_version)-$(1).tar.gz.temp" "$(rust_stdlib_download_path)/rust-std-$(rust_stdlib_version)-$(1).tar.gz" && \ + echo "$(rust_stdlib_sha256_hash_$(1)) $(SOURCES_PATH)/rust-std-$(rust_stdlib_version)-$(1).tar.gz.temp" | $(build_SHA256SUM) -c - && \ + mv "$(SOURCES_PATH)/rust-std-$(rust_stdlib_version)-$(1).tar.gz.temp" "$(SOURCES_PATH)/rust-std-$(rust_stdlib_version)-$(1).tar.gz")) && \ +echo "$(rust_stdlib_sha256_hash_$(1)) rust-std-$(rust_stdlib_version)-$(1).tar.gz" > "$(SOURCES_PATH)/download-stamps/.stamp_fetched-rust_stdlib-$(rust_stdlib_version)-$(rust_stdlib_sha256_hash_$(1)).hash" +endef + # These functions create the build targets for each package. They must be # broken down into small steps so that each part is done for all packages # before moving on to the next step. Otherwise, a package's info @@ -286,6 +351,25 @@ $(foreach package,$(all_packages),$(eval $(call int_vars,$(package)))) $(foreach native_package,$(native_packages),$(eval include packages/$(native_package).mk)) $(foreach package,$(packages),$(eval include packages/$(package).mk)) +# Extend preprocess_cmds for cargo packages: extract the vendored +# crates (the archive is a hard prerequisite of the preprocess stamp, +# wired up in int_vendor_crates, so it is always present here) and +# install the rustc linker wrapper from the package's patches. rustc's +# `-C linker=` takes a single executable, but the configured compiler +# is a full command line (target/sysroot flags; under Guix an +# `env -u ...` prefix), so the wrapper execs $$CC from cargo's +# environment instead of the command's first word. +define int_cargo_preprocess_ext +$(1)_preprocess_cmds += && \ + echo "Extracting vendored crates for $(1)..." && \ + $(build_TAR) --no-same-owner -xf $(SOURCES_PATH)/$($(1)_vendored_file_name) && \ + mkdir -p .cargo && \ + cp $(PATCHES_PATH)/$(1)/cargo-config.toml .cargo/config.toml && \ + cp $(PATCHES_PATH)/$(1)/rustc-linker.sh rustc-linker.sh && \ + chmod +x rustc-linker.sh +endef +$(foreach cargo_package,$(cargo_packages),$(eval $(call int_cargo_preprocess_ext,$(cargo_package)))) + #compute a hash of all files that comprise this package's build recipe $(foreach package,$(all_packages),$(eval $(call int_get_build_recipe_hash,$(package)))) @@ -297,3 +381,6 @@ $(foreach package,$(all_packages),$(eval $(call int_config_attach_build_config,$ #create build targets $(foreach package,$(all_packages),$(eval $(call int_add_cmds,$(package)))) + +#create vendor targets for cargo packages +$(foreach cargo_package,$(cargo_packages),$(eval $(call int_vendor_crates,$(cargo_package)))) diff --git a/depends/packages/native_protobuf.mk b/depends/packages/native_protobuf.mk new file mode 100644 index 000000000000..52c3745ee279 --- /dev/null +++ b/depends/packages/native_protobuf.mk @@ -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. + +package=native_protobuf +$(package)_version=32.0 +$(package)_download_path=https://github.com/protocolbuffers/protobuf/releases/download/v$($(package)_version) + +# Linux (ARMv8) +$(package)_file_name_aarch64_linux=protoc-$($(package)_version)-linux-aarch_64.zip +$(package)_sha256_hash_aarch64_linux=56af3fc2e43a0230802e6fadb621d890ba506c5c17a1ae1070f685fe79ba12d0 + +# Linux (x86_64) +$(package)_file_name_x86_64_linux=protoc-$($(package)_version)-linux-x86_64.zip +$(package)_sha256_hash_x86_64_linux=7ca037bfe5e5cabd4255ccd21dd265f79eb82d3c010117994f5dc81d2140ee88 + +# macOS (ARMv8) +$(package)_file_name_aarch64_darwin=protoc-$($(package)_version)-osx-aarch_64.zip +$(package)_sha256_hash_aarch64_darwin=09a2c729cc821215cc0d4c564b761760961fe338c52f24b302fd7e18e7b675d1 + +# macOS (x86_64) +$(package)_file_name_x86_64_darwin=protoc-$($(package)_version)-osx-x86_64.zip +$(package)_sha256_hash_x86_64_darwin=63eeba15ddc12ab11b0a8bce81fb2d46cc69022c3e6ad21fecde90d52139bff6 + +$(package)_file_name=$($(package)_file_name_$(build_arch)_$(build_os)) +$(package)_sha256_hash=$($(package)_sha256_hash_$(build_arch)_$(build_os)) + +ifeq ($($(package)_file_name),) +$(error native_protobuf has no prebuilt protoc $($(package)_version) for $(build_arch)-$(build_os)) +endif + +define $(package)_extract_cmds + echo "$($(package)_sha256_hash) $($(package)_source)" > .$($(package)_file_name).hash && \ + $(build_SHA256SUM) -c .$($(package)_file_name).hash && \ + python3 -m zipfile -e $($(package)_source) . +endef + +define $(package)_stage_cmds + mkdir -p $($(package)_staging_prefix_dir)/bin $($(package)_staging_prefix_dir)/include && \ + cp bin/protoc $($(package)_staging_prefix_dir)/bin/ && \ + chmod 0755 $($(package)_staging_prefix_dir)/bin/protoc && \ + cp -R include/google $($(package)_staging_prefix_dir)/include/ +endef diff --git a/depends/packages/native_rust.mk b/depends/packages/native_rust.mk new file mode 100644 index 000000000000..b474a0971ae3 --- /dev/null +++ b/depends/packages/native_rust.mk @@ -0,0 +1,55 @@ +# Copyright (c) 2016-2025 The Zcash developers +# 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. + +# To update the Rust compiler, change the version below and then run the script +# ./contrib/devtools/update-rust-hashes.py + +package:=native_rust +$(package)_version:=1.92.0 +$(package)_download_path:=https://static.rust-lang.org/dist +$(package)_patches:=fix-elf-interpreter.sh + +# Linux (ARMv8) +$(package)_file_name_aarch64_linux:=rust-$($(package)_version)-aarch64-unknown-linux-gnu.tar.gz +$(package)_sha256_hash_aarch64_linux:=c812028423c3d7dd7ba99f66101e9e1aa3f66eab44a1285f41c363825d49dca4 + +# Linux (x86_64) +$(package)_file_name_x86_64_linux:=rust-$($(package)_version)-x86_64-unknown-linux-gnu.tar.gz +$(package)_sha256_hash_x86_64_linux:=6e5efd6c25953b2732d4e6b1842512536650c68cf72a8b99a0fc566012dd6ca5 + +# macOS (ARMv8) +$(package)_file_name_aarch64_darwin:=rust-$($(package)_version)-aarch64-apple-darwin.tar.gz +$(package)_sha256_hash_aarch64_darwin:=235a6cca2dd4881130a9ae61ad1149bbf28bba184dd4621700f0c98c97457716 + +# macOS (x86_64) +$(package)_file_name_x86_64_darwin:=rust-$($(package)_version)-x86_64-apple-darwin.tar.gz +$(package)_sha256_hash_x86_64_darwin:=fc6868991e61e9262272effbb8956b23428430f5f4300c1b48eaae3969f8af2a + +$(package)_file_name=$($(package)_file_name_$(build_arch)_$(build_os)) +$(package)_sha256_hash=$($(package)_sha256_hash_$(build_arch)_$(build_os)) + +define $(package)_set_vars +$(package)_stage_opts=--disable-ldconfig +$(package)_stage_build_opts=--without=rust-docs-json-preview,rust-docs +endef + +define $(package)_fetch_cmds +$(call fetch_file,$(package),$($(package)_download_path),$($(package)_file_name),$($(package)_file_name),$($(package)_sha256_hash)) +endef + +define $(package)_stage_cmds + mkdir -p $($(package)_staging_dir)/$(host_prefix)/native/bin && \ + mkdir -p $($(package)_staging_dir)/$(host_prefix)/native/lib/rustlib && \ + cp cargo/bin/cargo $($(package)_staging_dir)/$(host_prefix)/native/bin/ && \ + cp rustc/bin/rustc $($(package)_staging_dir)/$(host_prefix)/native/bin/ && \ + cp rustc/bin/rustdoc $($(package)_staging_dir)/$(host_prefix)/native/bin/ && \ + cp -r rustc/lib/* $($(package)_staging_dir)/$(host_prefix)/native/lib/ && \ + cp -r rust-std-*/lib/rustlib/* $($(package)_staging_dir)/$(host_prefix)/native/lib/rustlib/ && \ + bash $($(package)_patch_dir)/fix-elf-interpreter.sh \ + $($(package)_staging_dir)/$(host_prefix)/native/lib \ + $($(package)_staging_dir)/$(host_prefix)/native/bin/cargo \ + $($(package)_staging_dir)/$(host_prefix)/native/bin/rustc \ + $($(package)_staging_dir)/$(host_prefix)/native/bin/rustdoc +endef diff --git a/depends/packages/packages.mk b/depends/packages/packages.mk index 7e0bb2633219..a49caa9c755e 100644 --- a/depends/packages/packages.mk +++ b/depends/packages/packages.mk @@ -26,4 +26,7 @@ natpmp_packages=libnatpmp multiprocess_packages = libmultiprocess capnp multiprocess_native_packages = native_libmultiprocess native_capnp +platform_packages = rust_stdlib tenderdash_sources platform_cxx +platform_native_packages = native_protobuf native_rust + usdt_linux_packages=systemtap diff --git a/depends/packages/platform_cxx.mk b/depends/packages/platform_cxx.mk new file mode 100644 index 000000000000..e1f5505bf2ae --- /dev/null +++ b/depends/packages/platform_cxx.mk @@ -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. + +package=platform_cxx +$(package)_version=b283a5f71644b592f86cd6a1ddf16834ac0edd43 +$(package)_download_path=https://github.com/dashpay/platform/archive +$(package)_download_file=$($(package)_version).tar.gz +$(package)_file_name=platform-$($(package)_version).tar.gz +$(package)_sha256_hash=86fe5a3f8cfa96e0d92509b6987ac9c8ba99efdc70ba37929c4fb68f7c5250b8 +$(package)_dependencies=native_rust rust_stdlib native_protobuf tenderdash_sources +$(package)_patches=cargo-config.toml rustc-linker.sh +$(package)_vendored_file_name=platform-cxx-$($(package)_version)-vendored.tar.gz +$(package)_cargo_manifest=Cargo.toml +$(package)_cargo_lock_path=Cargo.lock + +define $(package)_preprocess_cmds + true +endef + +define $(package)_build_cmds + mkdir -p target && \ + cp $(host_prefix)/tenderdash-sources/tenderdash-*.zip target/ && \ + CARGO_BUILD_TARGET=$(rust_stdlib_target) \ + CARGO_TARGET_DIR=$($(package)_build_dir)/target \ + PROTOC=$(build_prefix)/bin/protoc \ + PROTOC_INCLUDE=$(build_prefix)/include \ + $($(package)_cargo) build --locked --offline --release --target $(rust_stdlib_target) -p dash-platform-cxx +endef + +# The crate's build.rs stages the generated bridge header, the cxx runtime +# header and signer.h under target//release/include; that tree plus +# the static archive is the whole installed interface. +define $(package)_stage_cmds + mkdir -p $($(package)_staging_prefix_dir)/include $($(package)_staging_prefix_dir)/lib && \ + cp -R target/$(rust_stdlib_target)/release/include/. $($(package)_staging_prefix_dir)/include/ && \ + cp target/$(rust_stdlib_target)/release/libdash_platform_cxx.a $($(package)_staging_prefix_dir)/lib/ +endef diff --git a/depends/packages/rust_stdlib.mk b/depends/packages/rust_stdlib.mk new file mode 100644 index 000000000000..99395326ebad --- /dev/null +++ b/depends/packages/rust_stdlib.mk @@ -0,0 +1,70 @@ +# Copyright (c) 2016-2025 The Zcash developers +# 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. + +# This version is synchronized from native_rust.mk by update-rust-hashes.py. + +package:=rust_stdlib +$(package)_version:=1.92.0 +$(package)_download_path:=https://static.rust-lang.org/dist +$(package)_dependencies:=native_rust + +# Rust support is deliberately confined to the hosts we actually validate +# (the Guix release set plus native development hosts). RUST=1 on any other +# host fails explicitly below rather than fetching a stdlib we never test. + +# Linux (ARMv8) +$(package)_targets += aarch64-unknown-linux-musl +$(package)_target_aarch64-unknown-linux-gnu:=aarch64-unknown-linux-musl +$(package)_sha256_hash_aarch64-unknown-linux-musl:=715fbcfd8712c723947a020d0371c8a1a21f7531f2b696aeaed50ac23ba675c9 + +# Linux (RISCV64GC) +$(package)_targets += riscv64gc-unknown-linux-musl +$(package)_target_riscv64-unknown-linux-gnu:=riscv64gc-unknown-linux-musl +$(package)_target_riscv64gc-unknown-linux-gnu:=riscv64gc-unknown-linux-musl +$(package)_sha256_hash_riscv64gc-unknown-linux-musl:=34f5722ff2a0940bcd7ff6603a7748d2b963de72f6f713579c39c74ead06a7a0 + +# Linux (x86_64) +$(package)_targets += x86_64-unknown-linux-musl +$(package)_target_x86_64-unknown-linux-gnu:=x86_64-unknown-linux-musl +$(package)_sha256_hash_x86_64-unknown-linux-musl:=8bfd9a42c8295949d556587201acdb35d2bfb8b7ce55223845f337aa5614f9a3 + +# macOS (ARMv8) +$(package)_targets += aarch64-apple-darwin +$(package)_target_aarch64-apple-darwin:=aarch64-apple-darwin +$(package)_target_arm64-apple-darwin:=aarch64-apple-darwin +$(package)_sha256_hash_aarch64-apple-darwin:=b1f55aac4bc982ea67b68b262b711263005e470d31cab5d09d534bc1866d455a + +# macOS (x86_64) +$(package)_targets += x86_64-apple-darwin +$(package)_target_x86_64-apple-darwin:=x86_64-apple-darwin +$(package)_sha256_hash_x86_64-apple-darwin:=1e5a8fee4e038ea2d35d82a680e2b9bf44ffccb3746aaf9dbdc56cb14152dcb8 + +# Windows (x86_64) +$(package)_targets += x86_64-pc-windows-gnu +$(package)_target_x86_64-w64-mingw32:=x86_64-pc-windows-gnu +$(package)_sha256_hash_x86_64-pc-windows-gnu:=6256f3497e3b14b6650511e84fdfb51fc632db1908ae5a173dffcdc96c80b7ce + +$(package)_target:=$(or \ + $($(package)_target_$(canonical_host)),\ + $($(package)_target_$(subst -pc-,-unknown-,$(canonical_host))),\ + $($(package)_target_$(subst -unknown-,-pc-,$(canonical_host))),\ + $($(package)_target_$(subst -linux-,-unknown-linux-,$(canonical_host))),\ + $(if $(findstring -apple-darwin,$(canonical_host)),$(host_arch)-apple-darwin)) + +ifeq ($($(package)_target),) +$(error Unsupported Rust standard library target: $(canonical_host)) +endif + +$(package)_file_name=rust-std-$($(package)_version)-$($(package)_target).tar.gz +$(package)_sha256_hash=$($(package)_sha256_hash_$($(package)_target)) + +define $(package)_fetch_cmds + $(call fetch_file,$(package),$($(package)_download_path),$($(package)_file_name),$($(package)_file_name),$($(package)_sha256_hash)) +endef + +define $(package)_stage_cmds + mkdir -p $($(package)_staging_dir)/$(host_prefix)/native/lib/rustlib && \ + cp -r rust-std-$($(package)_target)/lib/rustlib/$($(package)_target) $($(package)_staging_dir)/$(host_prefix)/native/lib/rustlib/ +endef diff --git a/depends/packages/tenderdash_sources.mk b/depends/packages/tenderdash_sources.mk new file mode 100644 index 000000000000..057abbf6048b --- /dev/null +++ b/depends/packages/tenderdash_sources.mk @@ -0,0 +1,36 @@ +# 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. + +# Source-only package: the tenderdash source archive that tenderdash-proto's +# build.rs (rs-tenderdash-abci, a dependency of the Platform CXX package) +# compiles its protobuf definitions from. +# Online builds download this zip themselves at cargo build time; depends +# builds must not touch the network, so the sha256-pinned archive is staged +# verbatim into the prefix and platform_cxx copies it into the Cargo target +# directory, where build.rs treats it as a pre-populated download +# cache (cache file name: tenderdash-$(TENDERDASH_COMMITISH).zip). +# +# The version must match TENDERDASH_COMMITISH default of the pinned +# tenderdash-proto crate (rs-tenderdash-abci proto/build.rs). + +package=tenderdash_sources +$(package)_version=1.5.1 +$(package)_download_path=https://github.com/dashpay/tenderdash/archive +$(package)_download_file=v$($(package)_version).zip +$(package)_file_name=tenderdash-v$($(package)_version).zip +$(package)_sha256_hash=7a8844899a4635a6c2f55057e0c0f7cec357907d0cfeb2900e035760cf187f9a + +# Keep the archive as-is: the consumer (tenderdash-proto build.rs) unzips it +# from its own cache directory, so extraction here would only be discarded. +define $(package)_extract_cmds + mkdir -p $($(package)_extract_dir) && \ + echo "$($(package)_sha256_hash) $($(package)_source)" > $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + $(build_SHA256SUM) -c $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + cp $($(package)_source) $($(package)_file_name) +endef + +define $(package)_stage_cmds + mkdir -p $($(package)_staging_dir)/$(host_prefix)/tenderdash-sources && \ + cp $($(package)_file_name) $($(package)_staging_dir)/$(host_prefix)/tenderdash-sources/ +endef diff --git a/depends/patches/native_rust/fix-elf-interpreter.sh b/depends/patches/native_rust/fix-elf-interpreter.sh new file mode 100755 index 000000000000..805f9ac06110 --- /dev/null +++ b/depends/patches/native_rust/fix-elf-interpreter.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +export LC_ALL=C + +# 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. + +LIBDIR="$1" +shift + +in_guix_env() { + case "$(command -v ls)" in + /gnu/store/*) return 0 ;; + esac + return 1 +} + +if ! command -v patchelf >/dev/null 2>&1; then + # Inside a Guix environment the prebuilt binaries cannot run without + # having their interpreter patched, so a missing patchelf is fatal there. + if in_guix_env; then + echo "ERROR: patchelf is required inside the Guix environment but was not found" >&2 + exit 1 + fi + echo "patchelf not found, skipping ELF fix" + exit 0 +fi + +# Get the interpreter from a known working binary (ls) +LS_PATH=$(command -v ls) +GUIX_INTERP=$(patchelf --print-interpreter "$LS_PATH" 2>/dev/null) + +if [ -z "$GUIX_INTERP" ]; then + echo "Could not detect interpreter, skipping" + exit 0 +fi + +echo "Detected interpreter: $GUIX_INTERP" + +# Find and copy runtime libraries the prebuilt binaries need into our lib +# directory so the $ORIGIN-based RPATH can resolve them. +for libname in libgcc_s.so.1 libz.so.1; do + LIB_SRC="" + + # Method 1: Use gcc to find it + if command -v gcc >/dev/null 2>&1; then + CANDIDATE=$(gcc -print-file-name="$libname" 2>/dev/null) + if [ -f "$CANDIDATE" ]; then + LIB_SRC="$CANDIDATE" + else + GCC_PATH=$(command -v gcc) + GCC_PREFIX=$(dirname "$(dirname "$GCC_PATH")") + if [ -f "$GCC_PREFIX/lib/$libname" ]; then + LIB_SRC="$GCC_PREFIX/lib/$libname" + fi + fi + fi + + # Method 2: Search LIBRARY_PATH + if [ -z "$LIB_SRC" ] && [ -n "$LIBRARY_PATH" ]; then + IFS=':' read -ra LIB_PATHS <<< "$LIBRARY_PATH" + for libpath in "${LIB_PATHS[@]}"; do + if [ -f "$libpath/$libname" ]; then + LIB_SRC="$libpath/$libname" + break + fi + done + fi + + if [ -n "$LIB_SRC" ]; then + # Resolve symlinks and copy the actual file + LIB_REAL=$(readlink -f "$LIB_SRC") + echo "Copying $libname from: $LIB_REAL" + cp "$LIB_REAL" "$LIBDIR/$libname" + else + # Outside Guix the loader can still resolve these from the default + # system search paths; inside Guix there are none, so a toolchain + # missing one of these libraries is nonfunctional and must not be + # staged and cached. + if in_guix_env; then + echo "ERROR: $libname is required inside the Guix environment but was not found" >&2 + exit 1 + fi + echo "WARNING: Could not find $libname to copy" + fi +done + +# RPATH just needs $ORIGIN/../lib - everything is self-contained +GUIX_RPATH="\$ORIGIN/../lib" +echo "Using RPATH: $GUIX_RPATH" + +for binary in "$@"; do + if [ -f "$binary" ]; then + echo "Patching: $binary" + patchelf --set-interpreter "$GUIX_INTERP" "$binary" + patchelf --set-rpath "$GUIX_RPATH" "$binary" + fi +done + +if [ -n "$1" ]; then + echo "Verifying first binary:" + patchelf --print-interpreter "$1" + patchelf --print-rpath "$1" +fi diff --git a/depends/patches/platform_cxx/cargo-config.toml b/depends/patches/platform_cxx/cargo-config.toml new file mode 100644 index 000000000000..aa2657e50c6b --- /dev/null +++ b/depends/patches/platform_cxx/cargo-config.toml @@ -0,0 +1,70 @@ +[source.crates-io] +replace-with = "vendored-sources" + +[source."git+https://github.com/QuantumExplorer/rust-rocksdb.git?rev=52772eea7bcd214d1d07d80aa538b1d24e5015b7"] +git = "https://github.com/QuantumExplorer/rust-rocksdb.git" +rev = "52772eea7bcd214d1d07d80aa538b1d24e5015b7" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/agora-blsful?rev=0c34a7a488a0bd1c9a9a2196e793b303ad35c900"] +git = "https://github.com/dashpay/agora-blsful" +rev = "0c34a7a488a0bd1c9a9a2196e793b303ad35c900" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/bls-signatures?rev=0842b17583888e8f46c252a4ee84cdfd58e0546f"] +git = "https://github.com/dashpay/bls-signatures" +rev = "0842b17583888e8f46c252a4ee84cdfd58e0546f" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/grovedb?rev=6fc7e1e82de27a56c3428c0d5dd15b5f6a0ea23e"] +git = "https://github.com/dashpay/grovedb" +rev = "6fc7e1e82de27a56c3428c0d5dd15b5f6a0ea23e" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/jsonschema-rs?branch=configure_regexp"] +git = "https://github.com/dashpay/jsonschema-rs" +branch = "configure_regexp" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/orchard.git?tag=dashified-0.14.1"] +git = "https://github.com/dashpay/orchard.git" +tag = "dashified-0.14.1" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/rs-bip37-bloom-filter?branch=develop"] +git = "https://github.com/dashpay/rs-bip37-bloom-filter" +branch = "develop" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.5.1"] +git = "https://github.com/dashpay/rs-tenderdash-abci" +tag = "v1.5.1" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/rust-dashcore?rev=93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd"] +git = "https://github.com/dashpay/rust-dashcore" +rev = "93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/serde-wasm-bindgen?branch=fix/uint8array-to-bytes"] +git = "https://github.com/dashpay/serde-wasm-bindgen" +branch = "fix/uint8array-to-bytes" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/vsss-rs?branch=main"] +git = "https://github.com/dashpay/vsss-rs" +branch = "main" +replace-with = "vendored-sources" + +[source."git+https://github.com/dashpay/zcash_note_encryption?rev=9f7e93d"] +git = "https://github.com/dashpay/zcash_note_encryption" +rev = "9f7e93d" +replace-with = "vendored-sources" + +[source."git+https://github.com/gvz/zmq.rs?rev=b0787de310befaedd1f762e3b9bc711612d8137f"] +git = "https://github.com/gvz/zmq.rs" +rev = "b0787de310befaedd1f762e3b9bc711612d8137f" +replace-with = "vendored-sources" + +[source.vendored-sources] +directory = "vendored" diff --git a/depends/patches/platform_cxx/rustc-linker.sh b/depends/patches/platform_cxx/rustc-linker.sh new file mode 100755 index 000000000000..a2f99d543930 --- /dev/null +++ b/depends/patches/platform_cxx/rustc-linker.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# rustc's `-C linker=` takes a single executable, but the configured +# compiler is a full command line (target/sysroot flags; under Guix an +# `env -u ...` prefix). CC carries that command in cargo's environment; +# word-splitting it here preserves every part of it. +exec $CC "$@" diff --git a/src/Makefile.am b/src/Makefile.am index c5b08a2e17f7..f46cefad93cb 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -62,6 +62,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 @@ -127,7 +130,8 @@ EXTRA_LIBRARIES += \ $(LIBBITCOIN_IPC) \ $(LIBBITCOIN_WALLET) \ $(LIBBITCOIN_WALLET_TOOL) \ - $(LIBBITCOIN_ZMQ) + $(LIBBITCOIN_ZMQ) \ + $(LIBDASH_PLATFORM) if BUILD_BITCOIND bin_PROGRAMS += dashd @@ -698,6 +702,29 @@ 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) $(PLATFORM_CXX_CFLAGS) +libdash_platform_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) +libdash_platform_a_CFLAGS = $(AM_CFLAGS) $(PIE_FLAGS) +libdash_platform_a_SOURCES = \ + platform/client.cpp \ + platform/client.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/statetransitions.h \ + platform/types.h \ + platform/walletrecords.cpp \ + platform/walletrecords.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.test.include b/src/Makefile.test.include index a34ea4b120a3..775ec7968b93 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -62,6 +62,10 @@ FUZZ_SUITE_LD_COMMON = \ $(GMP_LIBS) \ $(BACKTRACE_LIBS) +if ENABLE_PLATFORM_GUI +FUZZ_SUITE_LD_COMMON += $(LIBDASH_PLATFORM) $(PLATFORM_CXX_LIBS) +endif + if USE_UPNP FUZZ_SUITE_LD_COMMON += $(MINIUPNPC_LIBS) endif @@ -221,6 +225,14 @@ BITCOIN_TESTS =\ test/versionbits_tests.cpp \ test/xoroshiro128plusplus_tests.cpp +if ENABLE_PLATFORM_GUI +BITCOIN_TESTS += \ + test/platform_dpp_tests.cpp +JSON_TEST_FILES += \ + test/data/platform/dpp_identity_vectors.json \ + test/data/platform/dpp_st_vectors.json +endif + if ENABLE_WALLET BITCOIN_TESTS += \ wallet/test/bip39_tests.cpp \ @@ -274,6 +286,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) $(PLATFORM_CXX_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) @@ -400,6 +415,10 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/utxo_total_supply.cpp \ test/fuzz/validation_load_mempool.cpp \ test/fuzz/versionbits.cpp + +if ENABLE_PLATFORM_GUI +test_fuzz_fuzz_SOURCES += test/fuzz/platform_bridge.cpp +endif endif # ENABLE_FUZZ_BINARY nodist_test_test_dash_SOURCES = $(GENERATED_TEST_FILES) diff --git a/src/platform/README.md b/src/platform/README.md new file mode 100644 index 000000000000..d833c41cd2a8 --- /dev/null +++ b/src/platform/README.md @@ -0,0 +1,36 @@ +# 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.*`); +- the `PlatformClient` interface the GUI drives (`client.h`) and its + production implementation (`client.cpp`): a worker thread over the Dash + Platform SDK's C++ bindings (`dash-platform-cxx`, namespace `platform_ffi`, + built from dashpay/platform through depends). The SDK owns query + construction, DAPI transport (TLS to the evonodes), retries, proof + verification (GroveDB replay plus the Tenderdash quorum signature against + the quorum keys this node pushes from its LLMQ store), protocol-version + tracking and the chain-id / ChainLock freshness checks. This node supplies + the evonode endpoints from its deterministic masternode list, the Platform + quorum keys, its best ChainLock height and wallet signatures; +- thin adapters (`dpp/`) over the same bindings for DPP object decoding and + state-transition construction, signed through a digest callback so private + keys never leave the wallet; +- the wallet record formats the GUI persists (`walletrecords.*`). + +## 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` and the standard library. + It must not depend on Qt. + +Upstream references are pinned in code comments (dashpay/platform). The +protocol version transitions are built under is the one the SDK has seen the +network run, ratcheted upward from the per-network floor in `params.cpp`. diff --git a/src/platform/client.cpp b/src/platform/client.cpp new file mode 100644 index 000000000000..bc4a12364d8a --- /dev/null +++ b/src/platform/client.cpp @@ -0,0 +1,427 @@ +// 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 + +namespace platform { + +namespace { + +rust::Slice ToSlice(const Identifier& id) +{ + return {id.data(), id.size()}; +} + +bool CopyId(const rust::Vec& bytes, Identifier& out) +{ + if (bytes.size() != out.size()) return false; + std::copy(bytes.begin(), bytes.end(), out.begin()); + return true; +} + +//! The signature-authenticated metadata of a verified response; the SDK +//! applied its freshness policy and the chain-id / ChainLock checks before +//! handing it back. +ResponseMetadata MetaFromFfi(const platform_ffi::FfiMeta& meta) +{ + ResponseMetadata out; + out.height = meta.height; + out.core_chain_locked_height = meta.core_chain_locked_height; + out.time_ms = meta.time_ms; + out.protocol_version = meta.protocol_version; + out.chain_id = std::string{meta.chain_id}; + return out; +} + +IdentityPublicKey KeyFromFfi(const platform_ffi::FfiIdentityKey& key) +{ + IdentityPublicKey out; + out.id = key.id; + out.purpose = static_cast(key.purpose); + out.security_level = static_cast(key.security_level); + out.type = static_cast(key.key_type); + out.read_only = key.read_only; + out.data.assign(key.data.begin(), key.data.end()); + out.disabled_at = key.has_disabled_at ? std::optional{key.disabled_at} : std::nullopt; + return out; +} + +bool IdentityFromFfi(const platform_ffi::FfiIdentity& in, Identity& out) +{ + if (!CopyId(in.id, out.id)) return false; + out.balance = in.balance; + out.revision = in.revision; + out.public_keys.clear(); + out.public_keys.reserve(in.keys.size()); + for (const platform_ffi::FfiIdentityKey& key : in.keys) out.public_keys.push_back(KeyFromFfi(key)); + return true; +} + +bool NameFromFfi(const platform_ffi::FfiDpnsName& in, DpnsName& out) +{ + out.label = std::string{in.label}; + out.normalized_label = std::string{in.normalized_label}; + out.parent_domain = std::string{in.parent_domain}; + return CopyId(in.identity, out.identity) && CopyId(in.document_id, out.document_id); +} + +bool ProfileFromFfi(const platform_ffi::FfiProfile& in, Profile& out) +{ + if (!CopyId(in.document_id, out.document_id) || !CopyId(in.owner_id, out.owner_id)) return false; + out.display_name = std::string{in.display_name}; + out.public_message = std::string{in.public_message}; + out.avatar_url = std::string{in.avatar_url}; + out.avatar_hash.assign(in.avatar_hash.begin(), in.avatar_hash.end()); + out.avatar_fingerprint.assign(in.avatar_fingerprint.begin(), in.avatar_fingerprint.end()); + out.created_at = in.created_at; + out.updated_at = in.updated_at; + out.revision = in.revision; + return true; +} + +bool ContactRequestFromFfi(const platform_ffi::FfiContactRequest& in, ContactRequest& out) +{ + if (!CopyId(in.owner_id, out.owner_id) || !CopyId(in.to_user_id, out.to_user_id) || + !CopyId(in.document_id, out.document_id)) { + return false; + } + out.encrypted_public_key.assign(in.encrypted_public_key.begin(), in.encrypted_public_key.end()); + out.sender_key_index = in.sender_key_index; + out.recipient_key_index = in.recipient_key_index; + out.account_reference = in.account_reference; + out.encrypted_account_label.assign(in.encrypted_account_label.begin(), in.encrypted_account_label.end()); + out.core_height_created_at = in.core_height_created_at; + out.created_at = in.created_at; + return true; +} + +//! Runs one SDK call on the worker thread and delivers its outcome. A +//! rust::Error (transport failure, failed verification, stale metadata, +//! contained panic) becomes the result's error string. +template +void Deliver(const PlatformClient::Callback& cb, const Fn& fn) +{ + Result out; + try { + fn(out); + } catch (const std::exception& e) { + // rust::Error from the bridge, but also cxx marshalling throws such + // as rust::String rejecting invalid UTF-8. + out.value.reset(); + out.error = e.what(); + } + cb(std::move(out)); +} + +class SdkClient final : public PlatformClient +{ +public: + SdkClient(Params params, uint8_t platform_llmq_type) + : m_params(std::move(params)), m_llmq_type(platform_llmq_type), m_sdk(platform_ffi::new_platform_client()) + { + try { + m_sdk->set_context(m_params.network_id, m_llmq_type, m_params.tenderdash_chain_id, + m_params.protocol_version_floor, /*platform_activation_height=*/0); + } catch (const std::exception& e) { + LogPrintf("Platform client: unable to set SDK context: %s\n", e.what()); + } + m_worker = std::thread([this] { Run(); }); + } + ~SdkClient() override { shutdown(); } + + const platform_ffi::PlatformClient& sdk() const override { return *m_sdk; } + + void shutdown() override + { + if (m_stop.exchange(true)) return; + { + std::lock_guard lk(m_mtx); + m_queue.clear(); + } + m_cv.notify_all(); + // Interrupts the SDK call the worker may be blocked in, then joins. + m_sdk->shutdown(); + if (m_worker.joinable()) m_worker.join(); + } + + void updateEndpoints(std::vector endpoints) override + { + rust::Vec uris; + uris.reserve(endpoints.size()); + for (const Endpoint& endpoint : endpoints) { + uris.push_back("https://" + endpoint.service.ToStringAddrPort()); + } + try { + if (uris.empty()) { + LogPrint(BCLog::QT, "Platform client: no evonode endpoints yet\n"); + return; + } + m_sdk->set_endpoints(std::move(uris)); + } catch (const std::exception& e) { + LogPrintf("Platform client: unable to update endpoints: %s\n", e.what()); + } + } + + void updateQuorumKeys(uint8_t llmq_type, std::vector keys) override + { + if (llmq_type != m_llmq_type) { + LogPrintf("Platform client: ignoring quorum keys of LLMQ type %d (Platform type is %d)\n", + llmq_type, m_llmq_type); + return; + } + // Hand the keys over in the byte order DAPI proofs carry the quorum + // hash (display order, the reverse of uint256's internal order; the + // representation boundary QuorumKey::matchesProofHash documents). + rust::Vec ffi_keys; + ffi_keys.reserve(keys.size()); + for (const QuorumKey& key : keys) { + platform_ffi::FfiQuorumKey ffi_key; + ffi_key.quorum_hash.reserve(32); + for (size_t i = 0; i < 32; ++i) ffi_key.quorum_hash.push_back(key.quorum_hash.begin()[31 - i]); + ffi_key.pubkey.reserve(key.pubkey.size()); + for (const uint8_t byte : key.pubkey) ffi_key.pubkey.push_back(byte); + ffi_keys.push_back(std::move(ffi_key)); + } + try { + m_sdk->update_quorum_keys(std::move(ffi_keys)); + } catch (const std::exception& e) { + LogPrintf("Platform client: unable to update quorum keys: %s\n", e.what()); + } + } + + void updateCoreChainLockedHeight(int32_t height) override + { + if (height > 0) m_sdk->set_core_chain_locked_height(static_cast(height)); + } + + // Queries: each enqueues a task on the worker thread. + void resolveName(const std::string& normalized_label, Callback> cb) override + { + Enqueue([=, this] { + Deliver(cb, [&](Result>& out) { + const auto res{m_sdk->resolve_name(normalized_label)}; + out.metadata = MetaFromFfi(res.meta); + if (!res.present) { + out.value = std::optional{}; // proven absent + return; + } + DpnsName name; + if (!NameFromFfi(res.name, name)) throw std::runtime_error("bridge returned a malformed DPNS name"); + out.value = std::move(name); + }); + }); + } + void searchNames(const std::string& prefix, uint32_t limit, Callback> cb) override + { + Enqueue([=, this] { + Deliver(cb, [&](Result>& out) { + NamesResult(m_sdk->search_names(prefix, limit), out); + }); + }); + } + void namesOfIdentity(const Identifier& identity, Callback> cb) override + { + Enqueue([=, this] { + Deliver(cb, [&](Result>& out) { + NamesResult(m_sdk->names_of_identity(ToSlice(identity)), out); + }); + }); + } + void getIdentity(const Identifier& id, Callback> cb) override + { + Enqueue([=, this] { + Deliver(cb, [&](Result>& out) { + IdentityResult(m_sdk->get_identity(ToSlice(id)), out); + }); + }); + } + void getIdentityByPublicKeyHash(const std::array& h, Callback> cb) override + { + Enqueue([=, this] { + Deliver(cb, [&](Result>& out) { + IdentityResult(m_sdk->get_identity_by_pubkey_hash(rust::Slice{h.data(), h.size()}), out); + }); + }); + } + void getIdentityNonce(const Identifier& id, Callback cb) override + { + Enqueue([=, this] { + Deliver(cb, [&](Result& out) { NonceResult(m_sdk->get_identity_nonce(ToSlice(id)), out); }); + }); + } + void getIdentityContractNonce(const Identifier& id, const Identifier& contract, Callback cb) override + { + Enqueue([=, this] { + Deliver(cb, [&](Result& out) { + NonceResult(m_sdk->get_identity_contract_nonce(ToSlice(id), ToSlice(contract)), out); + }); + }); + } + void getProfile(const Identifier& owner_id, Callback> cb) override + { + Enqueue([=, this] { + Deliver(cb, [&](Result>& out) { + const auto res{m_sdk->get_profile(ToSlice(owner_id))}; + out.metadata = MetaFromFfi(res.meta); + if (!res.present) { + out.value = std::optional{}; // proven absent + return; + } + Profile profile; + if (!ProfileFromFfi(res.profile, profile)) throw std::runtime_error("bridge returned a malformed profile"); + out.value = std::move(profile); + }); + }); + } + void getContactRequests(const Identifier& identity, bool to_me, uint64_t /*since_ms*/, + Callback> cb) override + { + Enqueue([=, this] { + Deliver(cb, [&](Result>& out) { + const auto res{m_sdk->get_contact_requests(ToSlice(identity), to_me)}; + out.metadata = MetaFromFfi(res.meta); + std::vector requests; + requests.reserve(res.requests.size()); + for (const platform_ffi::FfiContactRequest& ffi : res.requests) { + ContactRequest request; + if (!ContactRequestFromFfi(ffi, request)) throw std::runtime_error("bridge returned a malformed contact request"); + requests.push_back(std::move(request)); + } + out.value = std::move(requests); + }); + }); + } + void getContestedNameState(const std::string& normalized_label, Callback cb) override + { + Enqueue([=, this] { + Deliver(cb, [&](Result& out) { + const auto res{m_sdk->get_contested_vote_state(normalized_label)}; + out.metadata = MetaFromFfi(res.meta); + ContestedNameState state; + state.normalized_label = normalized_label; + for (const platform_ffi::FfiContender& contender : res.contenders) { + Identifier id; + if (!CopyId(contender.identity, id)) throw std::runtime_error("bridge returned a malformed contender"); + state.contenders.emplace_back(id, contender.has_votes ? contender.votes : 0); + } + state.abstain_votes = res.has_abstain ? res.abstain_votes : 0; + state.lock_votes = res.has_lock ? res.lock_votes : 0; + if (!res.contest_found) { + state.status = ContestedNameState::Status::UNKNOWN; + } else if (!res.finished) { + state.status = ContestedNameState::Status::CONTEST_IN_PROGRESS; + } else if (res.locked) { + state.status = ContestedNameState::Status::LOCKED; + state.ends_at = res.finished_at_time_ms; + } else { + state.status = ContestedNameState::Status::WON; + if (res.has_winner) { + Identifier winner; + if (!CopyId(res.winner, winner)) throw std::runtime_error("bridge returned a malformed winner"); + state.winner = winner; + } + state.ends_at = res.finished_at_time_ms; + } + out.value = std::move(state); + }); + }); + } + void broadcastStateTransition(const std::vector& st, Callback cb) override + { + Enqueue([=, this] { + Deliver(cb, [&](Result& out) { + const auto res{m_sdk->broadcast_state_transition(rust::Slice{st.data(), st.size()})}; + BroadcastResult br; + br.accepted = res.accepted; + br.error = std::string{res.error}; + br.error_code = res.error_code; + out.value = std::move(br); + }); + }); + } + +private: + void NamesResult(const platform_ffi::FfiVerifiedDpnsNames& res, Result>& out) + { + out.metadata = MetaFromFfi(res.meta); + std::vector names; + names.reserve(res.names.size()); + for (const platform_ffi::FfiDpnsName& ffi : res.names) { + DpnsName name; + if (!NameFromFfi(ffi, name)) throw std::runtime_error("bridge returned a malformed DPNS name"); + names.push_back(std::move(name)); + } + out.value = std::move(names); + } + void IdentityResult(const platform_ffi::FfiVerifiedIdentity& res, Result>& out) + { + out.metadata = MetaFromFfi(res.meta); + if (!res.present) { + out.value = std::optional{}; // proven absent + return; + } + Identity identity; + if (!IdentityFromFfi(res.identity, identity)) throw std::runtime_error("bridge returned a malformed identity"); + out.value = std::move(identity); + } + void NonceResult(const platform_ffi::FfiVerifiedU64& res, Result& out) + { + out.metadata = MetaFromFfi(res.meta); + out.value = res.present ? res.value : 0; // proven absent = never used + } + + // ---- worker plumbing ---- + void Enqueue(std::function task) + { + { + std::lock_guard lk(m_mtx); + if (m_stop) return; + m_queue.push_back(std::move(task)); + } + m_cv.notify_one(); + } + void Run() + { + for (;;) { + std::function task; + { + std::unique_lock lk(m_mtx); + m_cv.wait(lk, [this] { return m_stop || !m_queue.empty(); }); + if (m_stop) return; + task = std::move(m_queue.front()); + m_queue.pop_front(); + } + task(); + } + } + + Params m_params; + uint8_t m_llmq_type; + rust::Box m_sdk; + std::thread m_worker; + std::mutex m_mtx; + std::condition_variable m_cv; + std::deque> m_queue; + std::atomic_bool m_stop{false}; +}; + +} // namespace + +std::unique_ptr MakeSdkPlatformClient(const Params& params, uint8_t platform_llmq_type) +{ + return std::make_unique(params, platform_llmq_type); +} + +} // namespace platform diff --git a/src/platform/client.h b/src/platform/client.h new file mode 100644 index 000000000000..5111ffd65d19 --- /dev/null +++ b/src/platform/client.h @@ -0,0 +1,136 @@ +// 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 + +namespace platform_ffi { +class PlatformClient; +} // namespace platform_ffi + +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}; + + //! DAPI encodes Proof.quorum_hash in display (big-endian) byte order, + //! while uint256::begin() exposes Dash Core's internal little-endian + //! representation. Keep that representation boundary explicit here so a + //! valid locally synced quorum can be selected without weakening proof + //! verification. + bool matchesProofHash(const std::array& proof_hash) const + { + for (size_t i = 0; i < proof_hash.size(); ++i) { + if (proof_hash[i] != quorum_hash.begin()[proof_hash.size() - 1 - i]) return false; + } + return true; + } +}; + +//! An evonode DAPI endpoint, fed from the deterministic masternode list. +struct Endpoint { + CService service; //!< platform HTTPS (gRPC gateway) addr:port + uint256 pro_tx_hash{}; + + friend bool operator==(const Endpoint& lhs, const Endpoint& rhs) + { + return lhs.service == rhs.service && lhs.pro_tx_hash == rhs.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. +class PlatformClient +{ +public: + template + using Callback = std::function)>; + + virtual ~PlatformClient() = default; + + //! Resolve an exact normalized label under the "dash" parent domain. An + //! empty optional in the value means the name is 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. + 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; + //! The node's best locally verified core ChainLock height. Used as a + //! coarse staleness floor so an on-path attacker cannot replay a much + //! older (but validly signed) platform state. 0 means "unknown" and + //! disables the floor. + virtual void updateCoreChainLockedHeight(int32_t height) = 0; + + //! Stop all I/O and drop pending callbacks (must be called before the + //! consumer is destroyed). + virtual void shutdown() = 0; + + //! The underlying Platform SDK handle, for the DPP decoders and + //! state-transition builders that need the network's protocol version. + virtual const platform_ffi::PlatformClient& sdk() const = 0; +}; + +//! Create the production client: the Dash Platform SDK (dash-platform-cxx) +//! with its own DAPI transport, fed endpoints, quorum keys and ChainLock +//! heights from this node. `platform_llmq_type` is the LLMQ type Platform +//! quorums use on this network (Consensus::Params::llmqTypePlatform). +std::unique_ptr MakeSdkPlatformClient(const Params& params, uint8_t platform_llmq_type); + +} // namespace platform + +#endif // BITCOIN_PLATFORM_CLIENT_H diff --git a/src/platform/dpp/document.cpp b/src/platform/dpp/document.cpp new file mode 100644 index 000000000000..1d9726767139 --- /dev/null +++ b/src/platform/dpp/document.cpp @@ -0,0 +1,98 @@ +// 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 { + +namespace { + +rust::Slice ToSlice(Span data) +{ + return {data.data(), data.size()}; +} + +bool CopyId(const rust::Vec& bytes, Identifier& out) +{ + if (bytes.size() != out.size()) return false; + std::copy(bytes.begin(), bytes.end(), out.begin()); + return true; +} + +} // namespace + +bool DecodeDpnsDomain(const platform_ffi::PlatformClient& sdk, Span doc, DpnsName& out) +{ + try { + const platform_ffi::FfiDpnsName decoded{sdk.decode_dpns_domain(ToSlice(doc))}; + DpnsName name; + name.label = std::string{decoded.label}; + name.normalized_label = std::string{decoded.normalized_label}; + name.parent_domain = std::string{decoded.parent_domain}; + if (!CopyId(decoded.identity, name.identity) || !CopyId(decoded.document_id, name.document_id)) { + return false; + } + out = std::move(name); + return true; + } catch (const rust::Error&) { + return false; + } +} + +bool DecodeDashPayProfile(const platform_ffi::PlatformClient& sdk, Span doc, Profile& out) +{ + try { + const platform_ffi::FfiProfile decoded{sdk.decode_dashpay_profile(ToSlice(doc))}; + Profile profile; + if (!CopyId(decoded.document_id, profile.document_id) || !CopyId(decoded.owner_id, profile.owner_id)) { + return false; + } + profile.display_name = std::string{decoded.display_name}; + profile.public_message = std::string{decoded.public_message}; + profile.avatar_url = std::string{decoded.avatar_url}; + profile.avatar_hash.assign(decoded.avatar_hash.begin(), decoded.avatar_hash.end()); + profile.avatar_fingerprint.assign(decoded.avatar_fingerprint.begin(), + decoded.avatar_fingerprint.end()); + profile.created_at = decoded.created_at; + profile.updated_at = decoded.updated_at; + profile.revision = decoded.revision; + out = std::move(profile); + return true; + } catch (const rust::Error&) { + return false; + } +} + +bool DecodeDashPayContactRequest(const platform_ffi::PlatformClient& sdk, Span doc, + ContactRequest& out) +{ + try { + const platform_ffi::FfiContactRequest decoded{sdk.decode_contact_request(ToSlice(doc))}; + ContactRequest contact; + if (!CopyId(decoded.owner_id, contact.owner_id) || + !CopyId(decoded.to_user_id, contact.to_user_id) || + !CopyId(decoded.document_id, contact.document_id)) { + return false; + } + contact.encrypted_public_key.assign(decoded.encrypted_public_key.begin(), + decoded.encrypted_public_key.end()); + contact.sender_key_index = decoded.sender_key_index; + contact.recipient_key_index = decoded.recipient_key_index; + contact.account_reference = decoded.account_reference; + contact.encrypted_account_label.assign(decoded.encrypted_account_label.begin(), + decoded.encrypted_account_label.end()); + contact.core_height_created_at = decoded.core_height_created_at; + contact.created_at = decoded.created_at; + out = std::move(contact); + return true; + } catch (const rust::Error&) { + return false; + } +} + +} // namespace platform::dpp diff --git a/src/platform/dpp/document.h b/src/platform/dpp/document.h new file mode 100644 index 000000000000..ec5ce1264d1f --- /dev/null +++ b/src/platform/dpp/document.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_PLATFORM_DPP_DOCUMENT_H +#define BITCOIN_PLATFORM_DPP_DOCUMENT_H + +#include +#include + +#include + +namespace platform_ffi { +struct PlatformClient; +} // namespace platform_ffi + +/** + * Decoding of stored (platform-serialized) DPNS and DashPay documents into + * the GUI types. Thin adapters over the Platform SDK bindings, which + * deserialize with the real rs-dpp against the pinned system data contracts + * under the protocol version the SDK has seen the network run. + */ +namespace platform::dpp { + +//! Decode the GUI-relevant fields of a DPNS `domain` document. Returns false +//! on malformed input. +bool DecodeDpnsDomain(const platform_ffi::PlatformClient& sdk, Span doc, DpnsName& out); + +//! Decode the GUI-relevant fields of a DashPay `profile` document. +bool DecodeDashPayProfile(const platform_ffi::PlatformClient& sdk, Span doc, Profile& out); + +//! Decode a DashPay `contactRequest` document. +bool DecodeDashPayContactRequest(const platform_ffi::PlatformClient& sdk, Span doc, + ContactRequest& out); + +} // 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..37de92fbd7a0 --- /dev/null +++ b/src/platform/dpp/identity.cpp @@ -0,0 +1,68 @@ +// 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 { + +namespace { + +platform::IdentityPublicKey KeyFromFfi(const platform_ffi::FfiIdentityKey& key) +{ + platform::IdentityPublicKey out; + out.id = key.id; + out.purpose = static_cast(key.purpose); + out.security_level = static_cast(key.security_level); + out.type = static_cast(key.key_type); + out.read_only = key.read_only; + out.data.assign(key.data.begin(), key.data.end()); + out.disabled_at = key.has_disabled_at ? std::optional{key.disabled_at} : std::nullopt; + return out; +} + +} // namespace + +std::optional DecodeIdentity(const platform_ffi::PlatformClient& sdk, + Span bytes, std::string& error) +{ + try { + const platform_ffi::FfiIdentity decoded{ + sdk.decode_identity(rust::Slice{bytes.data(), bytes.size()})}; + if (decoded.id.size() != std::tuple_size_v) { + error = "bridge returned a malformed identity id"; + return std::nullopt; + } + platform::Identity identity; + std::copy(decoded.id.begin(), decoded.id.end(), identity.id.begin()); + identity.balance = decoded.balance; + identity.revision = decoded.revision; + identity.public_keys.reserve(decoded.keys.size()); + for (const platform_ffi::FfiIdentityKey& key : decoded.keys) { + identity.public_keys.push_back(KeyFromFfi(key)); + } + return identity; + } catch (const rust::Error& e) { + error = e.what(); + return std::nullopt; + } +} + +std::optional DecodeIdentityPublicKey(const platform_ffi::PlatformClient& sdk, + Span bytes, + std::string& error) +{ + try { + return KeyFromFfi(sdk.decode_identity_public_key( + rust::Slice{bytes.data(), bytes.size()})); + } catch (const rust::Error& e) { + error = e.what(); + return std::nullopt; + } +} + +} // namespace platform::dpp diff --git a/src/platform/dpp/identity.h b/src/platform/dpp/identity.h new file mode 100644 index 000000000000..b0aadd1adbbf --- /dev/null +++ b/src/platform/dpp/identity.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_PLATFORM_DPP_IDENTITY_H +#define BITCOIN_PLATFORM_DPP_IDENTITY_H + +#include +#include + +#include +#include + +namespace platform_ffi { +struct PlatformClient; +} // namespace platform_ffi + +/** + * Decoding of platform-serialized Identity / IdentityPublicKey objects into + * the platform::Identity / platform::IdentityPublicKey GUI types. Thin + * adapters over the Platform SDK bindings, which deserialize with the real + * rs-dpp. + */ +namespace platform::dpp { + +//! Decodes a platform-serialized Identity. Returns std::nullopt and sets +//! error on malformed input. +std::optional DecodeIdentity(const platform_ffi::PlatformClient& sdk, + Span bytes, std::string& error); + +//! Decodes a standalone platform-serialized IdentityPublicKey. +std::optional DecodeIdentityPublicKey(const platform_ffi::PlatformClient& sdk, + 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..d19e26a2ea45 --- /dev/null +++ b/src/platform/dpp/statetransitions.cpp @@ -0,0 +1,340 @@ +// 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 + +/** + * DPP state transition construction and signing, delegated to the + * Platform-owned CXX bindings, which build and serialize with the real + * rs-dpp. Signing crosses the FFI as a digest + * callback (platform_ffi::WalletSigner) so private keys never leave the + * wallet: Rust hands back the key id being signed plus the double-SHA256 + * digest of the transition's signable bytes and expects a 65-byte compact + * recoverable ECDSA signature. + * + * The DPNS helpers (label normalization, salted domain hashes, the contested + * rule) and the deterministic document entropy remain implemented here; the + * builders validate/reproduce them against rs-dpp on the Rust side. + */ +namespace platform::st { + +namespace { + +uint256 DoubleSha(Span data) +{ + uint256 out; + CHash256().Write(data).Finalize(out); + 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); +} + +rust::Slice ToSlice(Span data) +{ + return {data.data(), data.size()}; +} + +//! Wraps a single st::Signer as the bridge's keyed signer; the key id is +//! already pinned on the C++ side (signature_public_key_id), so it is +//! ignored here. +platform_ffi::WalletSigner SingleKeySigner(const Signer& signer) +{ + return platform_ffi::WalletSigner{ + [&signer](uint32_t /*key_id*/, const std::array& digest, + std::vector& sig_out) { + return signer(uint256{digest}, sig_out); + }}; +} + +//! The identity key the GUI signs document transitions with: its HIGH-level +//! ECDSA authentication key. Only the metadata reaches the bridge (rs-dpp +//! checks purpose and security level and records the key id in the +//! transition); the public key itself stays in the wallet with its private +//! counterpart. +platform_ffi::FfiIdentityKey HighAuthKey(uint32_t signature_public_key_id) +{ + platform_ffi::FfiIdentityKey key{}; + key.id = signature_public_key_id; + key.purpose = static_cast(IdentityPublicKey::Purpose::AUTHENTICATION); + key.security_level = static_cast(IdentityPublicKey::SecurityLevel::HIGH); + key.key_type = static_cast(IdentityPublicKey::Type::ECDSA_SECP256K1); + key.read_only = false; + key.has_disabled_at = false; + key.disabled_at = 0; + return key; +} + +Result FromFfi(const platform_ffi::FfiBuiltTransition& built) +{ + BuiltTransition out; + out.bytes.assign(built.bytes.begin(), built.bytes.end()); + if (built.hash.size() != out.hash.size()) { + return {std::nullopt, "bridge returned a malformed transition hash"}; + } + std::copy(built.hash.begin(), built.hash.end(), out.hash.begin()); + return {std::move(out), ""}; +} + +} // namespace + +Identifier IdentityIdFromOutpoint(const std::array& out_point) +{ + return ToArray(DoubleSha(out_point)); +} + +Result BuildIdentityCreate( + const platform_ffi::PlatformClient& sdk, + 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"}; + + rust::Vec ffi_keys; + ffi_keys.reserve(keys.size()); + for (const NewIdentityKey& key : keys) { + if (!key.signer) return {std::nullopt, strprintf("identity key %d: no signer", key.id)}; + platform_ffi::FfiNewIdentityKey ffi_key{}; + ffi_key.id = key.id; + ffi_key.purpose = static_cast(key.purpose); + ffi_key.security_level = static_cast(key.security_level); + ffi_key.pubkey.reserve(key.pubkey.size()); + for (const uint8_t byte : key.pubkey) ffi_key.pubkey.push_back(byte); + ffi_keys.push_back(std::move(ffi_key)); + } + + // Route each per-key signature request to the matching registered key's + // signer; the sentinel ASSET_LOCK_KEY_ID selects the one-time asset-lock + // key. Every key signs the same signable-bytes digest. + const platform_ffi::WalletSigner signer{ + [&keys, &asset_lock_signer](uint32_t key_id, const std::array& digest, + std::vector& sig_out) { + const uint256 digest256{digest}; + if (key_id == platform_ffi::WalletSigner::ASSET_LOCK_KEY_ID) { + return asset_lock_signer(digest256, sig_out); + } + for (const NewIdentityKey& key : keys) { + if (key.id == key_id) return key.signer(digest256, sig_out); + } + return false; + }}; + + const bool is_instant{std::holds_alternative(proof)}; + Span transaction, instant_lock; + uint32_t output_index{0}, core_chain_locked_height{0}; + Span out_point; + if (is_instant) { + const auto& instant{std::get(proof)}; + transaction = instant.transaction; + instant_lock = instant.instant_lock; + output_index = instant.output_index; + } else { + const auto& chain{std::get(proof)}; + core_chain_locked_height = chain.core_chain_locked_height; + out_point = chain.out_point; + } + + try { + return FromFfi(sdk.st_build_identity_create( + is_instant, ToSlice(transaction), ToSlice(instant_lock), output_index, + core_chain_locked_height, ToSlice(out_point), std::move(ffi_keys), signer)); + } catch (const rust::Error& e) { + return {std::nullopt, e.what()}; + } +} + +Result BuildDpnsPreorder( + const platform_ffi::PlatformClient& sdk, + const Identifier& identity, + uint64_t identity_contract_nonce, + const std::string& label, + const std::array& preorder_salt, + uint32_t signature_public_key_id, + const Signer& signer) +{ + if (!signer) return {std::nullopt, "no signer provided"}; + const platform_ffi::WalletSigner ffi_signer{SingleKeySigner(signer)}; + try { + // The preorder document has no natural id source; the bridge derives + // the salted domain hash — already blinded and unique per + // (name, salt) — and doubles it as the document entropy. + return FromFfi(sdk.st_build_dpns_preorder( + ToSlice(identity), identity_contract_nonce, label, ToSlice(preorder_salt), + signature_public_key_id, HighAuthKey(signature_public_key_id), ffi_signer)); + } catch (const rust::Error& e) { + return {std::nullopt, e.what()}; + } +} + +Result BuildDpnsDomain( + const platform_ffi::PlatformClient& sdk, + 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 (!signer) return {std::nullopt, "no signer provided"}; + const platform_ffi::WalletSigner ffi_signer{SingleKeySigner(signer)}; + try { + // The preorder salt is drawn fresh per registration attempt; the + // bridge doubles it as the deterministic document entropy for the + // paired domain create. + return FromFfi(sdk.st_build_dpns_domain( + ToSlice(identity), identity_contract_nonce, label, normalized_label, parent_domain, + ToSlice(preorder_salt), signature_public_key_id, + HighAuthKey(signature_public_key_id), ffi_signer)); + } catch (const rust::Error& e) { + return {std::nullopt, e.what()}; + } +} + +Result BuildProfile( + const platform_ffi::PlatformClient& sdk, + 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 (!signer) return {std::nullopt, "no signer provided"}; + const platform_ffi::WalletSigner ffi_signer{SingleKeySigner(signer)}; + const std::array entropy{DeriveEntropy(identity, "profile", identity_contract_nonce)}; + const Identifier existing{existing_document_id.value_or(Identifier{})}; + try { + return FromFfi(sdk.st_build_profile( + ToSlice(identity), identity_contract_nonce, profile.display_name, + profile.public_message, profile.avatar_url, ToSlice(profile.avatar_hash), + ToSlice(profile.avatar_fingerprint), revision, existing_document_id.has_value(), + ToSlice(existing), ToSlice(entropy), signature_public_key_id, + HighAuthKey(signature_public_key_id), ffi_signer)); + } catch (const rust::Error& e) { + return {std::nullopt, e.what()}; + } +} + +Result BuildContactRequest( + const platform_ffi::PlatformClient& sdk, + const Identifier& identity, + uint64_t identity_contract_nonce, + const ContactRequest& request, + uint32_t signature_public_key_id, + const Signer& signer) +{ + if (!signer) return {std::nullopt, "no signer provided"}; + const platform_ffi::WalletSigner ffi_signer{SingleKeySigner(signer)}; + const std::array entropy{ + DeriveEntropy(identity, "contactRequest", identity_contract_nonce)}; + try { + // $createdAt and $createdAtCoreBlockHeight are chain-assigned system + // fields, so ContactRequest::created_at and ::core_height_created_at + // do not enter the transition. + return FromFfi(sdk.st_build_contact_request( + ToSlice(identity), identity_contract_nonce, ToSlice(request.to_user_id), + ToSlice(request.encrypted_public_key), request.sender_key_index, + request.recipient_key_index, request.account_reference, + ToSlice(request.encrypted_account_label), ToSlice(entropy), signature_public_key_id, + HighAuthKey(signature_public_key_id), ffi_signer)); + } catch (const rust::Error& e) { + return {std::nullopt, e.what()}; + } +} + +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/platform/params.cpp b/src/platform/params.cpp new file mode 100644 index 000000000000..48c10bff8fae --- /dev/null +++ b/src/platform/params.cpp @@ -0,0 +1,44 @@ +// 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, + .protocol_version_floor = 11, + .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, + .contested_identity_funding_amount = 25000000, + }; + } + if (network_id == CBaseChainParams::TESTNET) { + return Params{ + .network_id = network_id, + .protocol_version_floor = 12, + .tenderdash_chain_id = "dash-testnet-51", + .default_identity_funding_amount = 1000000, + .contested_identity_funding_amount = 25000000, + }; + } + // 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..b0ece1165acf --- /dev/null +++ b/src/platform/params.h @@ -0,0 +1,72 @@ +// 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; + //! Lowest Platform protocol version this network can be running; the + //! SDK ratchets upward from it as verified responses report newer + //! versions (rs-sdk min_protocol_version). + uint32_t protocol_version_floor{0}; + //! 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{0}; + //! Funding used for contested names. Includes the 0.2 DASH protocol vote + //! reserve plus headroom for identity/domain transition fees. + int64_t contested_identity_funding_amount{0}; + + //! 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/platform/statetransitions.h b/src/platform/statetransitions.h new file mode 100644 index 000000000000..8a1a64474316 --- /dev/null +++ b/src/platform/statetransitions.h @@ -0,0 +1,155 @@ +// 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. + * + * Transitions are built under the protocol version the SDK handle has seen + * the network run, so their wire format is what the network accepts. + * + * 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_ffi { +struct PlatformClient; +} // namespace platform_ffi + +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 platform_ffi::PlatformClient& sdk, + 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. The salted domain hash +//! (DSHA256(salt || normalized_label || ".dash")) is derived on the Rust +//! side by the upstream DPNS document builder. +Result BuildDpnsPreorder( + const platform_ffi::PlatformClient& sdk, + const Identifier& identity, + uint64_t identity_contract_nonce, + const std::string& label, + const std::array& preorder_salt, + 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 platform_ffi::PlatformClient& sdk, + 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 platform_ffi::PlatformClient& sdk, + 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 platform_ffi::PlatformClient& sdk, + 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/platform/types.h b/src/platform/types.h new file mode 100644 index 000000000000..7fc9a39cd836 --- /dev/null +++ b/src/platform/types.h @@ -0,0 +1,119 @@ +// 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 { + +using Bytes = std::vector; + +//! 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 document_id{}; + 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. Status is +//! contest-global: WON means the contest finished with `winner` awarded the +//! name (callers compare against their own identity); UNKNOWN means no +//! contest exists for the label (proven absent). +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 winner; //!< set when status == WON + std::optional ends_at; //!< ms since epoch (finish time once decided) +}; + +//! 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. All fields are +//! covered by the Tenderdash quorum signature (they enter the StateId / +//! CanonicalVote sign bytes), so they are trustworthy once verification +//! succeeded. +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}; + std::string chain_id; //!< tenderdash chain id +}; + +} // namespace platform + +#endif // BITCOIN_PLATFORM_TYPES_H diff --git a/src/platform/walletrecords.cpp b/src/platform/walletrecords.cpp new file mode 100644 index 000000000000..02295b99101c --- /dev/null +++ b/src/platform/walletrecords.cpp @@ -0,0 +1,74 @@ +// 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 { + +std::vector SerializeIdentityRecord(const IdentityRecord& r) +{ + CDataStream s(SER_DISK, CLIENT_VERSION); + s << r.version << static_cast(r.state) << r.funding_txid << r.funding_vout + << r.funding_key_index << r.funding_amount + << std::vector(r.identity_id.begin(), r.identity_id.end()) + << r.label << r.normalized_label + << std::vector(r.preorder_salt.begin(), r.preorder_salt.end()) + << r.contested << r.last_error << r.started_at; + const auto span = MakeUCharSpan(s); + return {span.begin(), span.end()}; +} + +bool DeserializeIdentityRecord(const std::vector& data, IdentityRecord& r) +{ + try { + CDataStream s(data, SER_DISK, CLIENT_VERSION); + uint8_t state{0}; + std::vector identity_id, salt; + s >> r.version; + if (r.version == 0 || r.version > IdentityRecord::CURRENT_VERSION) return false; + s >> state >> r.funding_txid >> r.funding_vout >> r.funding_key_index >> r.funding_amount >> + identity_id >> r.label >> r.normalized_label >> salt >> r.contested >> r.last_error >> + r.started_at; + if (identity_id.size() != r.identity_id.size() || salt.size() != r.preorder_salt.size()) return false; + std::copy(identity_id.begin(), identity_id.end(), r.identity_id.begin()); + std::copy(salt.begin(), salt.end(), r.preorder_salt.begin()); + r.state = static_cast(state); + return true; + } catch (const std::exception&) { + return false; + } +} + +std::vector EncodePaymentCursor(uint32_t next_index) +{ + return {static_cast(next_index), static_cast(next_index >> 8), + static_cast(next_index >> 16), static_cast(next_index >> 24)}; +} + +uint32_t DecodePaymentCursor(const std::vector& data) +{ + if (data.size() != 4) return 0; + return uint32_t{data[0]} | (uint32_t{data[1]} << 8) | (uint32_t{data[2]} << 16) | + (uint32_t{data[3]} << 24); +} + +uint32_t ComputePaymentCursor(uint32_t window, + const std::function(uint32_t)>& derive, + const std::set& wallet_output_scripts) +{ + uint32_t cursor{0}; + for (uint32_t index = 0; index < window; ++index) { + const auto script{derive(index)}; + if (!script) break; + if (wallet_output_scripts.count(*script)) cursor = index + 1; + } + return cursor; +} + +} // namespace platform diff --git a/src/platform/walletrecords.h b/src/platform/walletrecords.h new file mode 100644 index 000000000000..f7d67d34ed28 --- /dev/null +++ b/src/platform/walletrecords.h @@ -0,0 +1,88 @@ +// 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_WALLETRECORDS_H +#define BITCOIN_PLATFORM_WALLETRECORDS_H + +#include +#include +#include