From ddcecd9f5ea2f252fb93d9cfb9df17c457d4bc74 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 19 Aug 2026 22:21:29 -0500 Subject: [PATCH 1/8] build(depends): add pinned Rust toolchain packages and crate vendoring native_rust installs the pinned prebuilt Rust toolchain as a native package and rust_stdlib provides the precompiled standard library for every supported cross target; contrib/devtools/update-rust-hashes.py maintains both pins together. funcs.mk gains a cargo environment wired to the depends cross toolchain and a per-package crate-vendoring template: any package that declares a vendored archive name and a cargo manifest gets its vendored-crate archive modeled as a real make target, created by cargo vendor when absent and required by the package's preprocess stamp and by make download, so a clean build can never reach the offline cargo build without vendored sources. Preprocessing extracts the archive and generates a rustc linker wrapper that preserves the full configured compiler command (target and sysroot flags, and any env prefix), since -C linker= takes a single executable. --- contrib/devtools/update-rust-hashes.py | 125 ++++++++++++++++++ depends/funcs.mk | 82 ++++++++++++ depends/packages/native_rust.mk | 55 ++++++++ depends/packages/rust_stdlib.mk | 70 ++++++++++ .../native_rust/fix-elf-interpreter.sh | 91 +++++++++++++ 5 files changed, 423 insertions(+) create mode 100755 contrib/devtools/update-rust-hashes.py create mode 100644 depends/packages/native_rust.mk create mode 100644 depends/packages/rust_stdlib.mk create mode 100755 depends/patches/native_rust/fix-elf-interpreter.sh 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/funcs.mk b/depends/funcs.mk index 566f83a9868e..a0caa3665386 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,50 @@ $(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 + +define download_rust_std_target +([ -f "$(SOURCES_PATH)/rust-std-$(rust_stdlib_version)-$(1).tar.gz" ] && \ + 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" "$(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" | $(build_SHA256SUM) -c - && \ +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 +346,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 +376,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_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/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/patches/native_rust/fix-elf-interpreter.sh b/depends/patches/native_rust/fix-elf-interpreter.sh new file mode 100755 index 000000000000..ccf05f8ecc05 --- /dev/null +++ b/depends/patches/native_rust/fix-elf-interpreter.sh @@ -0,0 +1,91 @@ +#!/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 + +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. + case "$(command -v ls)" in + /gnu/store/*) + echo "ERROR: patchelf is required inside the Guix environment but was not found" >&2 + exit 1 + ;; + esac + 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 + 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 From 104da7d3afe2cc53089f9423ddb2276a715384bd Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 19 Aug 2026 22:21:29 -0500 Subject: [PATCH 2/8] build(depends): build Dash Platform CXX bindings behind a PLATFORM_GUI knob PLATFORM_GUI=1 adds mbedtls, native_protobuf, tenderdash_sources and platform_cxx to the package set. platform_cxx builds libdash_platform_cxx.a and its installed headers from a pinned dashpay/platform commit (packages/rs-platform-cxx), offline via the per-package vendored crates. config.site.in exports enable_platform_gui and PLATFORM_CXX_{CFLAGS,LIBS} discovery for the configure flag that arrives with the first C++ consumer. --- depends/Makefile | 33 +++++++++++--- depends/README.md | 2 + depends/config.site.in | 11 +++++ depends/packages/mbedtls.mk | 27 ++++++++++++ depends/packages/native_protobuf.mk | 43 +++++++++++++++++++ depends/packages/packages.mk | 3 ++ depends/packages/platform_cxx.mk | 37 ++++++++++++++++ depends/packages/tenderdash_sources.mk | 36 ++++++++++++++++ .../patches/platform_cxx/cargo-config.toml | 30 +++++++++++++ depends/patches/platform_cxx/rustc-linker.sh | 6 +++ 10 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 depends/packages/mbedtls.mk create mode 100644 depends/packages/native_protobuf.mk create mode 100644 depends/packages/platform_cxx.mk create mode 100644 depends/packages/tenderdash_sources.mk create mode 100644 depends/patches/platform_cxx/cargo-config.toml create mode 100644 depends/patches/platform_cxx/rustc-linker.sh 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/packages/mbedtls.mk b/depends/packages/mbedtls.mk new file mode 100644 index 000000000000..cea4466c167d --- /dev/null +++ b/depends/packages/mbedtls.mk @@ -0,0 +1,27 @@ +package=mbedtls +$(package)_version=3.6.3.1 +$(package)_download_path=https://github.com/Mbed-TLS/mbedtls/releases/download/v$($(package)_version)/ +$(package)_file_name=$(package)-$($(package)_version).tar.bz2 +$(package)_sha256_hash=243ed496d5f88a5b3791021be2800aac821b9a4cc16e7134aa413c58b4c20e0c + +define $(package)_set_vars +$(package)_config_opts := -DENABLE_PROGRAMS=OFF -DENABLE_TESTING=OFF +$(package)_config_opts += -DUSE_SHARED_MBEDTLS_LIBRARY=OFF -DUSE_STATIC_MBEDTLS_LIBRARY=ON +$(package)_config_opts += -DMBEDTLS_FATAL_WARNINGS=OFF -DGEN_FILES=OFF +endef + +define $(package)_config_cmds + $($(package)_cmake) -S . -B . +endef + +define $(package)_build_cmds + $(MAKE) +endef + +define $(package)_stage_cmds + $(MAKE) DESTDIR=$($(package)_staging_dir) install +endef + +define $(package)_postprocess_cmds + rm -rf lib/cmake +endef diff --git a/depends/packages/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/packages.mk b/depends/packages/packages.mk index 7e0bb2633219..38f0d38a8373 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 = mbedtls 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..d8f200c0a2e0 --- /dev/null +++ b/depends/packages/platform_cxx.mk @@ -0,0 +1,37 @@ +# 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=df4fdb68559ef57d50624b7f0841594aef8647e5 +$(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=935b64a4f3acf48840706d573acc4c43ce4ac44272265ea96af45e35c47d829a +$(package)_build_subdir=packages/rs-platform-cxx/standalone +$(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=packages/rs-platform-cxx/standalone/Cargo.toml +$(package)_cargo_lock_path=packages/rs-platform-cxx/standalone/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) +endef + +define $(package)_stage_cmds + CARGO_BUILD_TARGET=$(rust_stdlib_target) \ + CARGO_PROFILE=release \ + CARGO_TARGET_DIR=$($(package)_build_dir)/target \ + bash ../install.sh $($(package)_staging_prefix_dir) +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/platform_cxx/cargo-config.toml b/depends/patches/platform_cxx/cargo-config.toml new file mode 100644 index 000000000000..1f46305c8c3f --- /dev/null +++ b/depends/patches/platform_cxx/cargo-config.toml @@ -0,0 +1,30 @@ +[source.crates-io] +replace-with = "vendored-sources" + +[source.vendored-sources] +directory = "vendored" + +[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/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9"] +git = "https://github.com/dashpay/grovedb" +rev = "a2791bbdca756d6a6113024aec48f09f7a33faa9" +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=173ffac0fdc0c73dda0626cf385bbcfcf2437aeb"] +git = "https://github.com/dashpay/rust-dashcore" +rev = "173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" +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" diff --git a/depends/patches/platform_cxx/rustc-linker.sh b/depends/patches/platform_cxx/rustc-linker.sh new file mode 100644 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 "$@" From fab4d10bb905c7bbf2735735734ebe059baadb21 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 19 Aug 2026 23:49:44 -0500 Subject: [PATCH 3/8] ci: exercise the PLATFORM_GUI depends knob in a linux64_platform_gui lane The new lane builds depends with PLATFORM_GUI=1 (producing and hash-verifying the Platform CXX archive offline from vendored crates), then builds dash-qt against that prefix and runs the unit tests. The cache-sources producer generates and caches the platform-cxx vendored-crates archive, handing it to same-run consumers as an artifact on cache miss. The --enable-platform-gui configure flag is added to this lane's BITCOIN_CONFIG by the Platform client library PR; until then the lane proves the depends knob and prefix link-compatibility. build.yml runs PR validation from the base branch (pull_request_target), so the lane first runs on push CI for this branch and takes effect for PRs after merge. --- .github/workflows/build-depends.yml | 27 ++++++++++++++ .github/workflows/build.yml | 37 +++++++++++++++++++ .github/workflows/cache-depends-sources.yml | 40 +++++++++++++++++++-- ci/dash/matrix.sh | 2 ++ ci/test/00_setup_env_native_platform_gui.sh | 25 +++++++++++++ 5 files changed, 129 insertions(+), 2 deletions(-) create mode 100755 ci/test/00_setup_env_native_platform_gui.sh 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..76af827df3f1 --- /dev/null +++ b/ci/test/00_setup_env_native_platform_gui.sh @@ -0,0 +1,25 @@ +#!/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 mbedtls and the Platform-owned CXX +# binding archive (libdash_platform_cxx.a, built offline from vendored crates) +# are 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++" From 82c62a050e0ba941b91d63256d14140f6da7b5fc Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 31 Aug 2026 11:51:24 +0200 Subject: [PATCH 4/8] build(depends): redownload rust-std archives that fail hash verification The all-target rust-std downloader wrote directly to the final source-cache path and treated any existing file as complete, so a partial file left behind by an interrupted download made every subsequent 'make PLATFORM_GUI=1 download' fail on the same archive until it was removed by hand. Verify an existing archive against the pinned hash and re-fetch it when it does not match, downloading to a temp path and only moving a verified archive into place, matching fetch_file_inner. --- depends/funcs.mk | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/depends/funcs.mk b/depends/funcs.mk index a0caa3665386..2d334ac243fa 100644 --- a/depends/funcs.mk +++ b/depends/funcs.mk @@ -320,12 +320,17 @@ $($(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 -([ -f "$(SOURCES_PATH)/rust-std-$(rust_stdlib_version)-$(1).tar.gz" ] && \ - echo "Already have rust-std-$(rust_stdlib_version)-$(1).tar.gz" || \ +((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" "$(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" | $(build_SHA256SUM) -c - && \ + $(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 From 978a0d2e76009adb5a43b01f490042bb25169dba Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 31 Aug 2026 11:51:24 +0200 Subject: [PATCH 5/8] build(depends): fail native_rust staging in Guix when a runtime library is missing Inside a Guix environment there are no default loader search paths, so a toolchain staged without libgcc_s/libz next to it is nonfunctional and would be cached in that state. Treat a missing required runtime library as a staging failure there, consistent with the existing fatal patchelf check; outside Guix it remains a warning since the system loader can still resolve the libraries. --- .../native_rust/fix-elf-interpreter.sh | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/depends/patches/native_rust/fix-elf-interpreter.sh b/depends/patches/native_rust/fix-elf-interpreter.sh index ccf05f8ecc05..805f9ac06110 100755 --- a/depends/patches/native_rust/fix-elf-interpreter.sh +++ b/depends/patches/native_rust/fix-elf-interpreter.sh @@ -8,15 +8,20 @@ export LC_ALL=C 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. - case "$(command -v ls)" in - /gnu/store/*) - echo "ERROR: patchelf is required inside the Guix environment but was not found" >&2 - exit 1 - ;; - esac + 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 @@ -68,6 +73,14 @@ for libname in libgcc_s.so.1 libz.so.1; do 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 From 092453f58c887c3891b0d5bb8b9d0103b5760ed6 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 8 Sep 2026 14:56:59 -0500 Subject: [PATCH 6/8] build(depends): make rustc-linker.sh executable The linker wrapper carries a shebang, so the lint-files check requires the executable bit; cargo invokes it through the RUSTFLAGS -C linker= path either way. --- depends/patches/platform_cxx/rustc-linker.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 depends/patches/platform_cxx/rustc-linker.sh diff --git a/depends/patches/platform_cxx/rustc-linker.sh b/depends/patches/platform_cxx/rustc-linker.sh old mode 100644 new mode 100755 From bece6652be29646f608e2763eeeee1a4a20e5746 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 8 Sep 2026 14:39:23 -0500 Subject: [PATCH 7/8] build(depends): pin platform_cxx to the SDK-backed dash-platform-cxx crate dashpay/platform#4633 rebuilds the Platform CXX bindings as a thin bridge over dash-sdk: the SDK owns DAPI transport, retries and proof verification, and Core supplies endpoints, quorum keys, its ChainLock height and wallet signatures. The crate is an ordinary workspace member now, so the package vendors from the workspace root (the lockfile made vendorable by dashpay/platform#4631), builds with -p dash-platform-cxx, and installs the header tree the crate's build.rs stages plus the static archive; the nested standalone manifest and install.sh are gone with the old design. mbedtls leaves depends: the SDK carries its own TLS stack (rustls with the system trust store), so Core no longer links a TLS library for Platform. The vendoring config gains the workspace's git sources. Validated on aarch64-apple-darwin: make -C depends PLATFORM_GUI=1 platform_cxx vendors 840 crates (150 MB archive) and builds the crate offline in 3 minutes; the staged prefix carries include/dash/platform/{ffi.h,signer.h}, include/rust/cxx.h and lib/libdash_platform_cxx.a. The knob-off package set is unchanged. --- ci/test/00_setup_env_native_platform_gui.sh | 17 +++--- depends/packages/mbedtls.mk | 27 ---------- depends/packages/packages.mk | 2 +- depends/packages/platform_cxx.mk | 21 ++++---- .../patches/platform_cxx/cargo-config.toml | 52 ++++++++++++++++--- 5 files changed, 67 insertions(+), 52 deletions(-) delete mode 100644 depends/packages/mbedtls.mk diff --git a/ci/test/00_setup_env_native_platform_gui.sh b/ci/test/00_setup_env_native_platform_gui.sh index 76af827df3f1..13fb49be63bc 100755 --- a/ci/test/00_setup_env_native_platform_gui.sh +++ b/ci/test/00_setup_env_native_platform_gui.sh @@ -6,14 +6,15 @@ export LC_ALL=C.UTF-8 -# Builds depends with PLATFORM_GUI=1 so mbedtls and the Platform-owned CXX -# binding archive (libdash_platform_cxx.a, built offline from vendored crates) -# are 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. +# 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" diff --git a/depends/packages/mbedtls.mk b/depends/packages/mbedtls.mk deleted file mode 100644 index cea4466c167d..000000000000 --- a/depends/packages/mbedtls.mk +++ /dev/null @@ -1,27 +0,0 @@ -package=mbedtls -$(package)_version=3.6.3.1 -$(package)_download_path=https://github.com/Mbed-TLS/mbedtls/releases/download/v$($(package)_version)/ -$(package)_file_name=$(package)-$($(package)_version).tar.bz2 -$(package)_sha256_hash=243ed496d5f88a5b3791021be2800aac821b9a4cc16e7134aa413c58b4c20e0c - -define $(package)_set_vars -$(package)_config_opts := -DENABLE_PROGRAMS=OFF -DENABLE_TESTING=OFF -$(package)_config_opts += -DUSE_SHARED_MBEDTLS_LIBRARY=OFF -DUSE_STATIC_MBEDTLS_LIBRARY=ON -$(package)_config_opts += -DMBEDTLS_FATAL_WARNINGS=OFF -DGEN_FILES=OFF -endef - -define $(package)_config_cmds - $($(package)_cmake) -S . -B . -endef - -define $(package)_build_cmds - $(MAKE) -endef - -define $(package)_stage_cmds - $(MAKE) DESTDIR=$($(package)_staging_dir) install -endef - -define $(package)_postprocess_cmds - rm -rf lib/cmake -endef diff --git a/depends/packages/packages.mk b/depends/packages/packages.mk index 38f0d38a8373..a49caa9c755e 100644 --- a/depends/packages/packages.mk +++ b/depends/packages/packages.mk @@ -26,7 +26,7 @@ natpmp_packages=libnatpmp multiprocess_packages = libmultiprocess capnp multiprocess_native_packages = native_libmultiprocess native_capnp -platform_packages = mbedtls rust_stdlib tenderdash_sources platform_cxx +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 index d8f200c0a2e0..e1f5505bf2ae 100644 --- a/depends/packages/platform_cxx.mk +++ b/depends/packages/platform_cxx.mk @@ -3,17 +3,16 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. package=platform_cxx -$(package)_version=df4fdb68559ef57d50624b7f0841594aef8647e5 +$(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=935b64a4f3acf48840706d573acc4c43ce4ac44272265ea96af45e35c47d829a -$(package)_build_subdir=packages/rs-platform-cxx/standalone +$(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=packages/rs-platform-cxx/standalone/Cargo.toml -$(package)_cargo_lock_path=packages/rs-platform-cxx/standalone/Cargo.lock +$(package)_cargo_manifest=Cargo.toml +$(package)_cargo_lock_path=Cargo.lock define $(package)_preprocess_cmds true @@ -26,12 +25,14 @@ define $(package)_build_cmds 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) + $($(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 - CARGO_BUILD_TARGET=$(rust_stdlib_target) \ - CARGO_PROFILE=release \ - CARGO_TARGET_DIR=$($(package)_build_dir)/target \ - bash ../install.sh $($(package)_staging_prefix_dir) + 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/patches/platform_cxx/cargo-config.toml b/depends/patches/platform_cxx/cargo-config.toml index 1f46305c8c3f..aa2657e50c6b 100644 --- a/depends/patches/platform_cxx/cargo-config.toml +++ b/depends/patches/platform_cxx/cargo-config.toml @@ -1,17 +1,39 @@ [source.crates-io] replace-with = "vendored-sources" -[source.vendored-sources] -directory = "vendored" +[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/grovedb?rev=a2791bbdca756d6a6113024aec48f09f7a33faa9"] +[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 = "a2791bbdca756d6a6113024aec48f09f7a33faa9" +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"] @@ -19,12 +41,30 @@ 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=173ffac0fdc0c73dda0626cf385bbcfcf2437aeb"] +[source."git+https://github.com/dashpay/rust-dashcore?rev=93260bf39bac5d9d09e89bfb45e9ea3ff7fdcbcd"] git = "https://github.com/dashpay/rust-dashcore" -rev = "173ffac0fdc0c73dda0626cf385bbcfcf2437aeb" +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" From 6e7e20b192bb9de34f0a4223eb4c511083dfac80 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 8 Sep 2026 14:42:57 -0500 Subject: [PATCH 8/8] feat: Dash Platform client library over the Platform SDK behind --enable-platform-gui The Qt-free client library dash-qt drives for DashPay: per-network parameters and system contract ids, the PlatformClient interface, DPP decoding and state-transition adapters, and the wallet record formats. Where the earlier revision (dashpay/dash#7626) carried its own gRPC-Web/TLS transport, hand-written protobuf and CBOR encoders, per-endpoint retry and freshness tracking, and handed request/response byte pairs to a transport-free verifier, this one is a thin consumer of the Dash Platform SDK through dash-platform-cxx (dashpay/platform#4633). The SDK owns query construction, DAPI transport, retries with address banning, proof verification (GroveDB replay plus the Tenderdash quorum signature against the keys this node pushes from its LLMQ store), protocol-version tracking and the chain-id and ChainLock freshness checks; the node supplies evonode endpoints from its deterministic masternode list, the Platform quorum keys, its best ChainLock height and wallet signatures through a digest callback, so private keys never leave the wallet. The PlatformClient interface the GUI programs against is unchanged apart from gaining an sdk() accessor; the production implementation keeps its single worker thread and callback marshalling and forwards each query to the SDK handle. Absence stays proven, never inferred: an empty result only reaches a callback after the SDK verified a proof of it. The DPP decoders and state-transition builders take the SDK handle so they build under the protocol version the SDK has seen the network run, ratcheted up from the per-network floor in params.cpp. The C++ transport, protobuf, CBOR, retry and freshness code and their unit tests are gone with the design; the DPP byte-exactness suite and the wallet key tests stay, and the fuzz harness keeps the decoder targets (proof verification is fuzzed upstream). Validated on aarch64-apple-darwin against a depends prefix carrying the SDK-backed archive: configure detects the bindings, libdash_platform.a and test_dash build, platform_dpp_tests and platformkeys_tests pass. --- configure.ac | 56 +++ src/Makefile.am | 29 +- src/Makefile.test.include | 19 + src/platform/README.md | 36 ++ src/platform/client.cpp | 427 ++++++++++++++++ src/platform/client.h | 136 ++++++ src/platform/dpp/document.cpp | 98 ++++ src/platform/dpp/document.h | 38 ++ src/platform/dpp/identity.cpp | 68 +++ src/platform/dpp/identity.h | 38 ++ src/platform/dpp/statetransitions.cpp | 340 +++++++++++++ src/platform/params.cpp | 44 ++ src/platform/params.h | 72 +++ src/platform/statetransitions.h | 155 ++++++ src/platform/types.h | 119 +++++ src/platform/walletrecords.cpp | 74 +++ src/platform/walletrecords.h | 88 ++++ .../data/platform/dpp_identity_vectors.json | 52 ++ src/test/data/platform/dpp_st_vectors.json | 233 +++++++++ src/test/fuzz/platform_bridge.cpp | 65 +++ src/test/platform_dpp_tests.cpp | 458 ++++++++++++++++++ src/wallet/test/platformkeys_tests.cpp | 145 ++++++ test/util/data/non-backported.txt | 3 + 23 files changed, 2792 insertions(+), 1 deletion(-) create mode 100644 src/platform/README.md create mode 100644 src/platform/client.cpp create mode 100644 src/platform/client.h create mode 100644 src/platform/dpp/document.cpp create mode 100644 src/platform/dpp/document.h create mode 100644 src/platform/dpp/identity.cpp create mode 100644 src/platform/dpp/identity.h create mode 100644 src/platform/dpp/statetransitions.cpp create mode 100644 src/platform/params.cpp create mode 100644 src/platform/params.h create mode 100644 src/platform/statetransitions.h create mode 100644 src/platform/types.h create mode 100644 src/platform/walletrecords.cpp create mode 100644 src/platform/walletrecords.h create mode 100644 src/test/data/platform/dpp_identity_vectors.json create mode 100644 src/test/data/platform/dpp_st_vectors.json create mode 100644 src/test/fuzz/platform_bridge.cpp create mode 100644 src/test/platform_dpp_tests.cpp 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/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