From e1d9a17dff4f089002cd3b5b39e27d9df5c4314b Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 24 Sep 2026 21:03:45 -0500 Subject: [PATCH 01/13] build(depends): add a prebuilt Rust toolchain and per-host standard library native_rust stages the prebuilt Rust 1.98.1 compiler and Cargo (the toolchain dashpay/platform pins) for the four supported build hosts, patchelf'd with fix-elf-interpreter.sh when run inside a Guix environment. rust_stdlib stages the standard library for the host, for every default Guix host. Linux hosts use the glibc (-unknown-linux-gnu) standard library, the one Rust supports for linking into a glibc program. Its libc imports are unversioned and bind to the glibc the program is linked against; every symbol it requires unconditionally is in glibc 2.31 on all five Linux architectures. contrib/devtools/update-rust-hashes.py refreshes the pins and requires every download to match the .sha256 file static.rust-lang.org publishes; --check compares the pins with those files. Nothing uses the packages yet. Co-Authored-By: Claude Opus 5.5 (1M context) --- contrib/devtools/README.md | 10 ++ contrib/devtools/update-rust-hashes.py | 113 ++++++++++++++++++ depends/packages/native_rust.mk | 52 ++++++++ depends/packages/rust_stdlib.mk | 65 ++++++++++ .../native_rust/fix-elf-interpreter.sh | 103 ++++++++++++++++ 5 files changed, 343 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/README.md b/contrib/devtools/README.md index c7dbad85e8be..ded48a615855 100644 --- a/contrib/devtools/README.md +++ b/contrib/devtools/README.md @@ -193,3 +193,13 @@ Example usage: cd .../src ../contrib/devtools/circular-dependencies.py {*,*/*,*/*/*}.{h,cpp} + +update-rust-hashes.py +===================== + +Refreshes the sha256 pins of the prebuilt Rust toolchain +(`depends/packages/native_rust.mk`) and of the per-host standard libraries +(`depends/packages/rust_stdlib.mk`) after the version in `native_rust.mk` was +changed; each downloaded archive must match the `.sha256` file published next +to it. `--check` compares the pins with the published `.sha256` files instead +of rewriting them. diff --git a/contrib/devtools/update-rust-hashes.py b/contrib/devtools/update-rust-hashes.py new file mode 100755 index 000000000000..5017729bf51d --- /dev/null +++ b/contrib/devtools/update-rust-hashes.py @@ -0,0 +1,113 @@ +#!/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. +''' +Refresh the sha256 pins of the prebuilt Rust toolchain (native_rust.mk) and +the per-host standard libraries (rust_stdlib.mk) for the version set in +native_rust.mk, or verify them with --check. + +Rewriting downloads every archive from static.rust-lang.org, hashes it locally +and requires the result to match the .sha256 file published next to it. +--check only compares the pins with the published .sha256 files. +''' +import argparse +import hashlib +import http.client +import re +import sys +import urllib.request +from pathlib import Path + +DIST_URL = "https://static.rust-lang.org/dist" +TIMEOUT = 60 # seconds without data before a request fails +PACKAGES_DIR = Path(__file__).resolve().parents[2] / "depends" / "packages" +NATIVE_RUST_MK = PACKAGES_DIR / "native_rust.mk" +RUST_STDLIB_MK = PACKAGES_DIR / "rust_stdlib.mk" + + +def pins(content: str, kind: str) -> dict: + """Return {id: value} for every `$(package)__:=` line.""" + return dict(re.findall(rf"^\$\(package\)_{kind}_(\w+):=(\S+)$", content, re.MULTILINE)) + + +def published_sha256(url: str) -> str: + """Return the hash in the ` ` line of `.sha256`.""" + with urllib.request.urlopen(f"{url}.sha256", timeout=TIMEOUT) as response: + return response.read().decode().split()[0] + + +def sha256_of(url: str, attempts: int = 3) -> str: + for _ in range(attempts): + hasher = hashlib.sha256() + received = 0 + try: + with urllib.request.urlopen(url, timeout=TIMEOUT) as response: + length = response.headers["Content-Length"] + while chunk := response.read(1 << 20): + hasher.update(chunk) + received += len(chunk) + except (http.client.IncompleteRead, OSError) as error: + print(f"warning: {url}: {error}", file=sys.stderr) + continue + if length is None or received == int(length): + return hasher.hexdigest() + print(f"warning: {url}: download ended after {received} of {length} bytes", file=sys.stderr) + sys.exit(f"error: {url}: no complete download in {attempts} attempts") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--check", action="store_true", help="verify the pinned hashes instead of rewriting them") + args = parser.parse_args() + + native = NATIVE_RUST_MK.read_text(encoding="utf-8") + stdlib = RUST_STDLIB_MK.read_text(encoding="utf-8") + version_pin = re.search(r"^\$\(package\)_version:=(\S+)$", native, re.MULTILINE) + if version_pin is None: + sys.exit(f"error: no version pin in {NATIVE_RUST_MK}") + version = version_pin.group(1) + + archives = [] # (makefile, pin id, archive name) + build_targets = pins(native, "build_target") + for build_id, file_name in pins(native, "file_name").items(): + file_name = file_name.replace("$($(package)_version)", version) + if build_id not in build_targets: + sys.exit(f"error: {NATIVE_RUST_MK} has no build_target_{build_id} pin") + file_name = file_name.replace(f"$($(package)_build_target_{build_id})", build_targets[build_id]) + archives.append((NATIVE_RUST_MK, build_id, file_name)) + for host_id, target in pins(stdlib, "target").items(): + archives.append((RUST_STDLIB_MK, host_id, f"rust-std-{version}-{target}.tar.gz")) + + contents = {NATIVE_RUST_MK: native, RUST_STDLIB_MK: stdlib} + stale = [] + for makefile, pin_id, archive in archives: + pinned = pins(contents[makefile], "sha256_hash").get(pin_id) + if pinned is None: + sys.exit(f"error: {makefile} has no sha256_hash_{pin_id} pin") + url = f"{DIST_URL}/{archive}" + actual = published_sha256(url) + if not args.check: + downloaded = sha256_of(url) + if downloaded != actual: + sys.exit(f"error: {archive} hashes to {downloaded}, but its published .sha256 says {actual}") + print(f"{actual} {archive}") + if actual != pinned: + stale.append(archive) + contents[makefile] = re.sub(rf"^(\$\(package\)_sha256_hash_{pin_id}:=)\S*$", + rf"\g<1>{actual}", contents[makefile], flags=re.MULTILINE) + + if args.check: + for archive in stale: + print(f"error: pinned hash for {archive} does not match", file=sys.stderr) + return 1 if stale else 0 + + for makefile, content in contents.items(): + makefile.write_text(content, encoding="utf-8") + print(f"Updated {len(stale)} of {len(archives)} pins for Rust {version}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/depends/packages/native_rust.mk b/depends/packages/native_rust.mk new file mode 100644 index 000000000000..c5a6bf275224 --- /dev/null +++ b/depends/packages/native_rust.mk @@ -0,0 +1,52 @@ +# 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.98.1 +$(package)_download_path:=https://static.rust-lang.org/dist +$(package)_patches:=fix-elf-interpreter.sh + +# Linux (ARMv8) +$(package)_build_target_aarch64_linux:=aarch64-unknown-linux-gnu +$(package)_file_name_aarch64_linux:=rust-$($(package)_version)-$($(package)_build_target_aarch64_linux).tar.gz +$(package)_sha256_hash_aarch64_linux:=f00ba576645cef658e1deed96fab8f707958e9d58808b16343448b5d1c4f7407 + +# Linux (x86_64) +$(package)_build_target_x86_64_linux:=x86_64-unknown-linux-gnu +$(package)_file_name_x86_64_linux:=rust-$($(package)_version)-$($(package)_build_target_x86_64_linux).tar.gz +$(package)_sha256_hash_x86_64_linux:=24ba1338a2d35c5a3247936546429e163fa674d726102af18bdf624582c57aea + +# macOS (ARMv8) +$(package)_build_target_aarch64_darwin:=aarch64-apple-darwin +$(package)_file_name_aarch64_darwin:=rust-$($(package)_version)-$($(package)_build_target_aarch64_darwin).tar.gz +$(package)_sha256_hash_aarch64_darwin:=cfc171d8120d401b10a1028c52646dd8e00e3e66852f949061ce087845f55afd + +# macOS (x86_64) +$(package)_build_target_x86_64_darwin:=x86_64-apple-darwin +$(package)_file_name_x86_64_darwin:=rust-$($(package)_version)-$($(package)_build_target_x86_64_darwin).tar.gz +$(package)_sha256_hash_x86_64_darwin:=443a1165abbac41c9143b83ff837c0fb1d8c03d2f8fb1da27427bc9fc646aad3 + +# The Rust target triple of the build machine. +$(package)_build_target:=$($(package)_build_target_$(build_arch)_$(build_os)) +$(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_rust has no prebuilt Rust $($(package)_version) for $(build_arch)-$(build_os)) +endif + +define $(package)_stage_cmds + mkdir -p $($(package)_staging_prefix_dir)/bin $($(package)_staging_prefix_dir)/lib/rustlib && \ + cp cargo/bin/cargo rustc/bin/rustc $($(package)_staging_prefix_dir)/bin/ && \ + cp -R rustc/lib/. $($(package)_staging_prefix_dir)/lib/ && \ + cp -R rust-std-*/lib/rustlib/. $($(package)_staging_prefix_dir)/lib/rustlib/ && \ + bash $($(package)_patch_dir)/fix-elf-interpreter.sh \ + $($(package)_staging_prefix_dir)/lib \ + $($(package)_staging_prefix_dir)/bin/cargo \ + $($(package)_staging_prefix_dir)/bin/rustc +endef diff --git a/depends/packages/rust_stdlib.mk b/depends/packages/rust_stdlib.mk new file mode 100644 index 000000000000..e6600acac2be --- /dev/null +++ b/depends/packages/rust_stdlib.mk @@ -0,0 +1,65 @@ +# 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. + +# Precompiled Rust standard library for the host, installed next to the +# native_rust compiler. The version follows native_rust.mk; update both with +# ./contrib/devtools/update-rust-hashes.py + +package:=rust_stdlib +$(package)_version:=$(native_rust_version) +$(package)_download_path:=$(native_rust_download_path) + +# Every host in contrib/guix/guix-build's default HOSTS has an entry. Linux +# hosts use the glibc (-unknown-linux-gnu) standard library, the one Rust +# supports for linking into a glibc program. Its libc imports are unversioned +# in the archive and bind at link time to the glibc the program is linked +# against; every symbol it requires unconditionally exists in glibc 2.31 on +# all five Linux architectures (the rest are weak and looked up at run time). + +# Linux (x86_64) +$(package)_target_x86_64_linux:=x86_64-unknown-linux-gnu +$(package)_sha256_hash_x86_64_linux:=eddab0358cbd12aeb897716aab00d1db7b59696e85b9ac4982e72259a9a976b1 + +# Linux (ARMv8) +$(package)_target_aarch64_linux:=aarch64-unknown-linux-gnu +$(package)_sha256_hash_aarch64_linux:=779407b14507542581216d89eb9f3fbb232abbf3abcc15c365cb32fa0614e409 + +# Linux (RISC-V 64) +$(package)_target_riscv64_linux:=riscv64gc-unknown-linux-gnu +$(package)_sha256_hash_riscv64_linux:=bea4eac8f0b752aec63389d626d96280424da68b033c2d515bc4af204f07bf44 + +# Linux (ARMv7, hard float) +$(package)_target_arm_linux:=armv7-unknown-linux-gnueabihf +$(package)_sha256_hash_arm_linux:=6f15060d308793d1687a5092c80f2fbebc808c73096980b67f9de71d4f54f92c + +# Linux (POWER, big endian) +$(package)_target_powerpc64_linux:=powerpc64-unknown-linux-gnu +$(package)_sha256_hash_powerpc64_linux:=2d6268b4dddc385c24ae77b2e1fe16161781b92958108cbb2a6f9fab21689823 + +# Windows (x86_64) +$(package)_target_x86_64_mingw32:=x86_64-pc-windows-gnu +$(package)_sha256_hash_x86_64_mingw32:=0cda26447df0749bc84044be8c8083ac4dc87bf137c11cc31ec9673a6e2e0344 + +# macOS (x86_64) +$(package)_target_x86_64_darwin:=x86_64-apple-darwin +$(package)_sha256_hash_x86_64_darwin:=af7ffb3b408aa2f6a6940fc83ea6dc9c3e919d18f1b04f1a581b7896441e8b78 + +# macOS (ARMv8) +$(package)_target_aarch64_darwin:=aarch64-apple-darwin +$(package)_sha256_hash_aarch64_darwin:=840484e8f9c2a8ed024b706262a1257bb07d9617670a1fc90020536282950690 + +$(package)_target:=$($(package)_target_$(host_arch)_$(host_os)) +$(package)_sha256_hash:=$($(package)_sha256_hash_$(host_arch)_$(host_os)) + +ifeq ($($(package)_target),) +$(error rust_stdlib has no Rust standard library for $(host)) +endif + +$(package)_file_name:=rust-std-$($(package)_version)-$($(package)_target).tar.gz + +define $(package)_stage_cmds + mkdir -p $($(package)_staging_dir)$(build_prefix)/lib/rustlib && \ + cp -R rust-std-$($(package)_target)/lib/rustlib/$($(package)_target) $($(package)_staging_dir)$(build_prefix)/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..dbedd5151198 --- /dev/null +++ b/depends/patches/native_rust/fix-elf-interpreter.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +export LC_ALL=C +set -euo pipefail + +# 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. + +# Usage: fix-elf-interpreter.sh ... +# +# The prebuilt Rust binaries expect a conventional /lib64/ld-linux-* loader +# and system library directories. Inside a Guix environment neither exists, so +# the binaries get the environment's loader, an $ORIGIN-relative RPATH, and +# copies of the runtime libraries they need. Anywhere else they run as they +# are, and this script does nothing. + +LIBDIR="$1" +shift + +case "$(readlink -f "$(command -v ls)")" in + /gnu/store/*) ;; + *) exit 0 ;; +esac + +if ! command -v patchelf >/dev/null 2>&1; then + echo "ERROR: patchelf is required inside the Guix environment but was not found" >&2 + exit 1 +fi + +# Get the interpreter from a known working binary (ls) +LS_PATH=$(command -v ls) +GUIX_INTERP=$(patchelf --print-interpreter "$LS_PATH") + +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 || true) + 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 + + # Method 3: the Guix profile. contrib/guix/libexec/build.sh narrows + # LIBRARY_PATH to the gcc-toolchain outputs, so libraries provisioned by + # contrib/guix/manifest.scm (zlib) are only reachable through the + # profile union that guix shell exposes as GUIX_ENVIRONMENT. + if [ -z "$LIB_SRC" ] && [ -n "${GUIX_ENVIRONMENT:-}" ] && [ -f "$GUIX_ENVIRONMENT/lib/$libname" ]; then + LIB_SRC="$GUIX_ENVIRONMENT/lib/$libname" + fi + + if [ -z "$LIB_SRC" ]; then + # There are no default library search paths inside Guix, so a + # toolchain missing one of these libraries is nonfunctional and must + # not be staged and cached. + echo "ERROR: $libname is required inside the Guix environment but was not found" >&2 + exit 1 + fi + # Resolve symlinks and copy the actual file + LIB_REAL=$(readlink -f "$LIB_SRC") + echo "Copying $libname from: $LIB_REAL" + cp "$LIB_REAL" "$LIBDIR/$libname" +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 [ $# -gt 0 ]; then + echo "Verifying first binary:" + patchelf --print-interpreter "$1" + patchelf --print-rpath "$1" +fi From 919db861b6690692a45efbd2f657dda23d7b4363 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 24 Sep 2026 21:03:58 -0500 Subject: [PATCH 02/13] build(depends): build the Dash Platform CXX bindings behind a PLATFORM_GUI knob PLATFORM_GUI=1 adds native_rust, rust_stdlib, prebuilt protoc 32.0 (native_protobuf) and platform_cxx, which builds packages/rs-platform-cxx of dashpay/platform and installs its static library and cxx headers. The knob follows MULTIPROCESS: default package sets are unchanged, and config.site enables --enable-platform-gui. Combining it with NO_QT or NO_WALLET is an error, since the bindings are for the GUI wallet only. platform_cxx is built with cargo build --frozen --offline from two sha256-pinned archives, the Platform source tarball at the pinned commit and a crate bundle; depends never vendors crates. Both archives must be on the depends sources mirror before this is merged. contrib/devtools/platform-bundle.sh produces the bundle reproducibly from a commit: workspace trimmed to the crate, Cargo.lock pruned to it, cargo vendor --locked --versioned-dirs, crates outside the build closure reduced to their manifests, the Tenderdash source archive for the tag the lock pins together with TENDERDASH_COMMITISH set to that tag, and tar and gzip with fixed metadata. It prints the pins for platform_cxx.mk. Only the bundle's Cargo configuration is used: Cargo runs from / with --config, its home is private, and variables that would change the build (wrappers, CARGO_BUILD_*, CARGO_PROFILE_*, CARGO_TARGET_*, per-target compiler overrides, TENDERDASH_*) are unset. The release profile is pinned to Platform's (Cargo's default, panic=unwind). The depends host compiler links the crate and compiles its C and C++, the build compiler links build scripts and proc macros, and the build directory is remapped out of the objects. The build refuses a dependency graph that reaches the trusted context provider, an HTTP client or OpenSSL. Co-Authored-By: Claude Opus 5.5 (1M context) --- contrib/devtools/README.md | 16 ++ contrib/devtools/platform-bundle.sh | 204 +++++++++++++++++++ depends/Makefile | 11 + depends/README.md | 3 + depends/config.site.in | 4 + depends/packages/native_protobuf.mk | 43 ++++ depends/packages/packages.mk | 3 + depends/packages/platform_cxx.mk | 172 ++++++++++++++++ depends/patches/platform_cxx/build-linker.sh | 8 + depends/patches/platform_cxx/rustc-linker.sh | 10 + doc/dependencies.md | 7 + 11 files changed, 481 insertions(+) create mode 100755 contrib/devtools/platform-bundle.sh create mode 100644 depends/packages/native_protobuf.mk create mode 100644 depends/packages/platform_cxx.mk create mode 100755 depends/patches/platform_cxx/build-linker.sh create mode 100755 depends/patches/platform_cxx/rustc-linker.sh diff --git a/contrib/devtools/README.md b/contrib/devtools/README.md index ded48a615855..501a915e28e9 100644 --- a/contrib/devtools/README.md +++ b/contrib/devtools/README.md @@ -203,3 +203,19 @@ Refreshes the sha256 pins of the prebuilt Rust toolchain changed; each downloaded archive must match the `.sha256` file published next to it. `--check` compares the pins with the published `.sha256` files instead of rewriting them. + +platform-bundle.sh +================== + +Produces the crate bundle that `depends/packages/platform_cxx.mk` builds the +Dash Platform CXX bindings from, for a given `dashpay/platform` commit, and +prints the hashes to pin in `platform_cxx.mk`. The bundle includes the +Tenderdash source archive for the tag the commit's `Cargo.lock` pins, so the +two cannot disagree. Requires the Cargo version pinned in `native_rust.mk`, +GNU tar, GNU gzip (set `TAR` and `GZIP_PROG` if they are installed under other +names) and a `TMPDIR` with no `.cargo/config.toml` in any parent directory. +The output is reproducible: rerunning it for the same commit yields the same +bundle hash on any machine. The Platform tarball and the bundle are then +uploaded to the depends sources mirror. + + contrib/devtools/platform-bundle.sh diff --git a/contrib/devtools/platform-bundle.sh b/contrib/devtools/platform-bundle.sh new file mode 100755 index 000000000000..8b45ee15cb49 --- /dev/null +++ b/contrib/devtools/platform-bundle.sh @@ -0,0 +1,204 @@ +#!/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 +set -euo pipefail + +# Produces the crate bundle that depends/packages/platform_cxx.mk builds +# dash-platform-cxx from, for a given dashpay/platform commit. +# +# The bundle holds a Cargo workspace trimmed to packages/rs-platform-cxx, the +# commit's Cargo.lock pruned to that workspace (entries are only removed, never +# changed), every locked crate vendored with `cargo vendor --locked +# --versioned-dirs`, the Tenderdash source archive tenderdash-proto's build +# script generates its protobuf code from (tenderdash/tenderdash-.zip, for +# the rs-tenderdash-abci tag the lock pins), and a .cargo/config.toml that +# replaces all crate sources with the vendored directory and sets +# TENDERDASH_COMMITISH to that same tag. Vendored crates outside the build +# closure of dash-platform-cxx (dev-dependencies and crates only other +# workspace members use) are reduced to their manifest and empty target files: +# Cargo needs them to resolve the lock, never to build. The archive is written +# with a fixed file order, owner, mode and mtime, so the same commit yields the +# same sha256 on every machine that uses the pinned Rust version, GNU tar and +# GNU gzip (and as long as GitHub serves the same Tenderdash zip). +# +# This is the only step that downloads crates. The maintainer bumping the +# pin runs it, updates the hashes in platform_cxx.mk from its output, and +# uploads the Platform tarball and the bundle to the depends sources mirror; +# reviewers rerun it to reproduce the hash. + +usage() { + echo "Usage: $0 " >&2 + echo >&2 + echo "Writes platform-.tar.gz and platform-cxx-crates-.tar.gz to" >&2 + echo "SOURCES_PATH (default: depends/sources), and keeps the downloaded" >&2 + echo "tenderdash-.zip there." >&2 + exit 1 +} + +[ $# -eq 1 ] || usage +COMMIT="$1" +[[ "$COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "error: expected a full 40-character commit hash" >&2; exit 1; } + +TOPDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SOURCES_PATH="${SOURCES_PATH:-$TOPDIR/depends/sources}" +mkdir -p "$SOURCES_PATH" +# The script changes directory below; a relative path must not follow it. +SOURCES_PATH="$(cd "$SOURCES_PATH" && pwd)" +# shellcheck disable=SC2016 +RUST_VERSION="$(sed -n 's/^\$(package)_version:=//p' "$TOPDIR/depends/packages/native_rust.mk")" +CARGO="${CARGO:-cargo}" +TAR="${TAR:-tar}" +GZIP_PROG="${GZIP_PROG:-gzip}" + +# cargo vendor output (normalized manifests) depends on the Cargo version. +export RUSTUP_TOOLCHAIN="${RUSTUP_TOOLCHAIN:-$RUST_VERSION}" +case "$("$CARGO" --version)" in + "cargo $RUST_VERSION "*) ;; + *) echo "error: $CARGO is not Cargo $RUST_VERSION (the version pinned in native_rust.mk)" >&2; exit 1 ;; +esac +case "$("$TAR" --version)" in + *"GNU tar"*) ;; + *) echo "error: $TAR is not GNU tar; set TAR" >&2; exit 1 ;; +esac +case "$("$GZIP_PROG" --version)" in + *"Free Software Foundation"*) ;; + *) echo "error: $GZIP_PROG is not GNU gzip; set GZIP_PROG" >&2; exit 1 ;; +esac + +WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}/platform-bundle.XXXXXX")" +trap 'rm -rf "$WORKDIR"' EXIT +# Cargo looks for configuration above the physical directory it runs in. +WORKDIR="$(cd "$WORKDIR" && pwd -P)" +# A private Cargo home keeps the cached registries and the Cargo home's +# config.toml out of the bundle. Cargo also reads .cargo/config.toml from every +# parent of the directory it runs in, so the work directory must have none +# (TMPDIR under a home directory with ~/.cargo/config.toml fails here). +export CARGO_HOME="$WORKDIR/cargo-home" +dir="$WORKDIR" +while :; do + dir="$(dirname "$dir")" + for config in "$dir/.cargo/config" "$dir/.cargo/config.toml"; do + if [ -e "$config" ]; then + echo "error: $config would configure Cargo; set TMPDIR to a directory outside its tree" >&2 + exit 1 + fi + done + [ "$dir" != / ] || break +done + +# Downloads $1 to SOURCES_PATH/$2, unless a copy there passes the integrity +# test $3 (a partial or corrupt one is downloaded again). +fetch() { + local url="$1" file="$2" test="$3" + if [ -f "$SOURCES_PATH/$file" ] && $test "$SOURCES_PATH/$file" 2> /dev/null; then + return + fi + rm -f "$SOURCES_PATH/$file" + curl --location --fail --silent --show-error --retry 3 -o "$SOURCES_PATH/$file.temp" "$url" + $test "$SOURCES_PATH/$file.temp" + mv "$SOURCES_PATH/$file.temp" "$SOURCES_PATH/$file" +} + +gzip_test() { + "$GZIP_PROG" -t "$1" +} + +zip_test() { + python3 -c 'import sys, zipfile; sys.exit(zipfile.ZipFile(sys.argv[1]).testzip() is not None)' "$1" +} + +sha256() { + if command -v sha256sum >/dev/null; then + sha256sum "$1" | cut -d' ' -f1 + else + shasum -a 256 "$1" | cut -d' ' -f1 + fi +} + +# Prints "name version source" for every [[package]] of a Cargo.lock. +lock_entries() { + awk '/^\[\[package\]\]$/ { if (name) print name, version, source; name = version = source = "" } + /^name = / { name = $3 } + /^version = / { version = $3 } + /^source = / { source = $3 } + END { if (name) print name, version, source }' "$1" | sort +} + +PLATFORM_ARCHIVE="platform-$COMMIT.tar.gz" +fetch "https://github.com/dashpay/platform/archive/$COMMIT.tar.gz" "$PLATFORM_ARCHIVE" gzip_test + +SRC="$WORKDIR/src" +mkdir -p "$SRC" +"$TAR" --strip-components=1 -xzf "$SOURCES_PATH/$PLATFORM_ARCHIVE" -C "$SRC" +cd "$SRC" + +# Trim the workspace to the one crate depends builds and prune the lock to it. +cp Cargo.lock "$WORKDIR/Cargo.lock.pinned" +awk '/^members = \[/ { print "members = [\"packages/rs-platform-cxx\"]"; skip = !/\]/; next } + skip && /^\]/ { skip = 0; next } + !skip' Cargo.toml > "$WORKDIR/Cargo.toml" +mv "$WORKDIR/Cargo.toml" Cargo.toml +"$CARGO" metadata --format-version 1 > /dev/null +if lock_entries Cargo.lock | comm -13 <(lock_entries "$WORKDIR/Cargo.lock.pinned") - | grep .; then + echo "error: trimming the workspace changed the locked packages above" >&2 + exit 1 +fi + +TENDERDASH_TAG="$(awk '/^name = "tenderdash-proto"$/ { found = 1 } + found && /^source = / { print; exit }' Cargo.lock | + sed -n 's|.*/rs-tenderdash-abci?tag=\(v[^#]*\)#.*|\1|p')" +[ -n "$TENDERDASH_TAG" ] || { echo "error: no tagged tenderdash-proto entry in Cargo.lock" >&2; exit 1; } +TENDERDASH_ARCHIVE="tenderdash-$TENDERDASH_TAG.zip" +fetch "https://github.com/dashpay/tenderdash/archive/$TENDERDASH_TAG.zip" "$TENDERDASH_ARCHIVE" zip_test +mkdir tenderdash +cp "$SOURCES_PATH/$TENDERDASH_ARCHIVE" tenderdash/ + +rm -rf .cargo +mkdir .cargo +{ + echo "# Generated by contrib/devtools/platform-bundle.sh for dashpay/platform@$COMMIT" + "$CARGO" vendor --locked --versioned-dirs vendor + echo + echo "[env]" + echo "TENDERDASH_COMMITISH = \"$TENDERDASH_TAG\"" +} > "$WORKDIR/config.toml" +grep -qx 'directory = "vendor"' "$WORKDIR/config.toml" || { echo "error: cargo vendor printed no source replacement" >&2; exit 1; } +mv "$WORKDIR/config.toml" .cargo/config.toml +# From here on every crate must come from vendor/, never from the Cargo home. +rm -rf "$CARGO_HOME" + +closure() { + "$CARGO" tree --frozen -p dash-platform-cxx -e normal,build --target all --prefix none --format '{p}' | + awk '{ sub(/^v/, "", $2); print $1 "-" $2 }' | sort -u +} +closure > "$WORKDIR/closure" +for dir in vendor/*/; do + dir="${dir%/}" + grep -qxF "${dir#vendor/}" "$WORKDIR/closure" && continue + checksum="$(sed -En 's/.*"package":(null|"[0-9a-f]*")}$/\1/p' "$dir/.cargo-checksum.json")" + find "$dir" ! -type d ! -name '*.rs' ! -path "$dir/Cargo.toml" -delete + find "$dir" -type f -name '*.rs' -print0 | while IFS= read -r -d '' file; do : > "$file"; done + find "$dir" -type d -empty -delete + printf '{"files":{},"package":%s}' "$checksum" > "$dir/.cargo-checksum.json" +done +closure | cmp -s - "$WORKDIR/closure" || { echo "error: the build closure changed after pruning" >&2; exit 1; } + +BUNDLE="platform-cxx-crates-$COMMIT.tar.gz" +"$TAR" --create --format=gnu --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner \ + --mode='u+rw,go+r-w,a+X' Cargo.toml Cargo.lock .cargo/config.toml tenderdash vendor | + "$GZIP_PROG" -9n > "$SOURCES_PATH/$BUNDLE.temp" +mv "$SOURCES_PATH/$BUNDLE.temp" "$SOURCES_PATH/$BUNDLE" + +cat < .$($(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..160fce36ef80 100644 --- a/depends/packages/packages.mk +++ b/depends/packages/packages.mk @@ -26,4 +26,7 @@ natpmp_packages=libnatpmp multiprocess_packages = libmultiprocess capnp multiprocess_native_packages = native_libmultiprocess native_capnp +platform_packages = rust_stdlib platform_cxx +platform_native_packages = native_rust native_protobuf + usdt_linux_packages=systemtap diff --git a/depends/packages/platform_cxx.mk b/depends/packages/platform_cxx.mk new file mode 100644 index 000000000000..9cd76d8d77c9 --- /dev/null +++ b/depends/packages/platform_cxx.mk @@ -0,0 +1,172 @@ +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# Dash Platform CXX bindings (packages/rs-platform-cxx of dashpay/platform), +# built from two sha256-pinned archives: the Platform source tarball at the +# pinned commit, and the crate bundle that contrib/devtools/platform-bundle.sh +# produces for that commit (trimmed workspace, pruned Cargo.lock, vendored +# crates, the Tenderdash sources tenderdash-proto generates its protobuf code +# from, Cargo configuration). Cargo runs with --frozen and never touches the +# network. +# +# Both archives must be on the depends sources mirror (FALLBACK_DOWNLOAD_PATH) +# before this is merged. The bundle has no upstream URL, and GitHub does not +# promise stable bytes for the archive endpoint the tarball comes from. +# Whoever bumps the pin runs platform-bundle.sh, which prints the lines to +# update here, and uploads both. Until then, a bundle the script wrote to +# SOURCES_PATH is used as is; its hash is checked on extraction either way. + +package=platform_cxx +$(package)_version=02b1749cb6aefd75a6fd6a15cbd666fd7a58dfa8 +$(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=a5e4e7db2d2a11c8becb7bcd1fb206c956e75e52cfcb5d869b0a59b443c98e13 +$(package)_crates_file_name=platform-cxx-crates-$($(package)_version).tar.gz +$(package)_crates_sha256_hash=54586810d30debbe1406305c5bd6552439248346b9875bb5c558ca05a573f721 +$(package)_extra_sources=$($(package)_crates_file_name) +$(package)_dependencies=native_rust rust_stdlib native_protobuf +$(package)_patches=rustc-linker.sh build-linker.sh + +define $(package)_fetch_cmds +$(call fetch_file,$(package),$($(package)_download_path),$($(package)_download_file),$($(package)_file_name),$($(package)_sha256_hash)) && \ +( test -f $($(package)_source_dir)/$($(package)_crates_file_name) || \ + $(call fetch_file_inner,$(package),$(FALLBACK_DOWNLOAD_PATH),$($(package)_crates_file_name),$($(package)_crates_file_name),$($(package)_crates_sha256_hash)) ) +endef + +# Only packages/ is needed from the Platform tarball; the bundle supplies the +# workspace manifest, lock and Cargo configuration. +define $(package)_extract_cmds + echo "$($(package)_sha256_hash) $($(package)_source)" > .$($(package)_file_name).hash && \ + echo "$($(package)_crates_sha256_hash) $($(package)_source_dir)/$($(package)_crates_file_name)" >> .$($(package)_file_name).hash && \ + $(build_SHA256SUM) -c .$($(package)_file_name).hash && \ + $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source) platform-$($(package)_version)/packages && \ + $(build_TAR) --no-same-owner -xf $($(package)_source_dir)/$($(package)_crates_file_name) +endef + +$(package)_rust_target=$(rust_stdlib_target) +$(package)_cc_target=$(subst -,_,$($(package)_rust_target)) +$(package)_build_linker_var:=CARGO_TARGET_$(shell echo $(native_rust_build_target) | tr a-z- A-Z_)_LINKER + +# Only the bundle's .cargo/config.toml configures Cargo: +# - Cargo reads .cargo/config.toml in the directory it runs in and in every +# parent, whatever --manifest-path says, so it runs from / and is given the +# bundle's file with --config. Configuration above the depends tree, such as +# ~/.cargo/config.toml when the tree is under a home directory, is therefore +# never read, and the build works wherever the tree is; preprocess fails if +# / itself has one. +# - The Cargo home is private and empty. +# - Environment variables that would change the build are removed: +# RUSTC_WORKSPACE_WRAPPER, RUSTC_BOOTSTRAP, CARGO_ENCODED_RUSTFLAGS, +# __CARGO_DEFAULT_LIB_METADATA, and all CARGO_BUILD_*, CARGO_PROFILE_*, +# CARGO_TARGET_* and CARGO_UNSTABLE_* ones, and the _ forms +# (such as CC_x86_64-unknown-linux-gnu) that cc-rs prefers over the +# _ ones set below; TENDERDASH_DIR and +# TENDERDASH_COMMITISH, which the bundle's configuration sets to the tag of +# the Tenderdash archive it carries. +# - The release profile is pinned to Platform's, which is Cargo's default: +# opt-level 3, no debug info, no LTO, 16 codegen units, no incremental +# compilation, and unwinding panics (the crate refuses panic=abort). Nothing +# Cargo links here is installed (build scripts, the cdylib dash-sdk also +# declares), so nothing is stripped; on macOS that would need rust-objcopy +# and an LLVM library native_rust does not stage. +$(package)_unset_env:=RUSTC_WORKSPACE_WRAPPER RUSTC_BOOTSTRAP CARGO_ENCODED_RUSTFLAGS __CARGO_DEFAULT_LIB_METADATA +$(package)_unset_env+=TENDERDASH_DIR TENDERDASH_COMMITISH +$(package)_unset_env+=$(filter CARGO_BUILD_% CARGO_PROFILE_% CARGO_TARGET_% CARGO_UNSTABLE_%,$(.VARIABLES)) +$(package)_unset_env+=$(filter %_$(rust_stdlib_target) %_$(native_rust_build_target),$(.VARIABLES)) +$(package)_profile_env:=CARGO_PROFILE_RELEASE_OPT_LEVEL=3 CARGO_PROFILE_RELEASE_DEBUG=false +$(package)_profile_env+=CARGO_PROFILE_RELEASE_LTO=false CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 +$(package)_profile_env+=CARGO_PROFILE_RELEASE_PANIC=unwind CARGO_PROFILE_RELEASE_INCREMENTAL=false +$(package)_profile_env+=CARGO_PROFILE_RELEASE_STRIP=false CARGO_INCREMENTAL=0 + +# RUSTFLAGS takes the place of Platform's .cargo/config.toml, which the bundle +# does not carry. Of what that file sets, --cfg tokio_unstable only enables +# tokio APIs (runtime metrics, task hooks, io-uring) that nothing in +# dash-platform-cxx's dependency graph uses, target-feature=-crt-static only +# changes musl targets (glibc targets link the C runtime dynamically anyway), +# and target-cpu=x86-64 and -lstdc++ are the defaults or only matter for +# executables; none of them is set here. +# +# For the host, the depends compiler links (through rustc-linker.sh), the C and +# C++ sources in the crate closure (ring, secp256k1, the cxx bridge) are +# compiled with the depends compiler and flags, and the build directory is +# remapped out of both the Rust and the C objects. Build scripts and proc +# macros run on the build machine; build-linker.sh links them with the depends +# build compiler, which also compiles any C they need (CC_ and +# HOST_CC; when the build and host triples are the same, the host's +# CC_ wins and the host and build compilers are the same machine's). +# +# For windows-gnu, rustc creates the import libraries of raw-dylib imports +# (windows-link, which the Rust standard library and windows-sys use) with +# the mingw-w64 dlltool, so the Guix manifest must provide it. +define $(package)_set_vars +$(package)_cargo_env = CARGO_HOME=$$($(package)_build_dir)/.cargo-home +$(package)_cargo_env += CARGO_TARGET_DIR=$$($(package)_build_dir)/target +$(package)_cargo_env += $($(package)_profile_env) +$(package)_cargo_env += $($(package)_build_linker_var)=$$($(package)_build_dir)/build-linker.sh +$(package)_cargo = cd / && env $$(addprefix -u ,$$($(package)_unset_env)) $$($(package)_cargo_env) \ + cargo --config $$($(package)_build_dir)/.cargo/config.toml +$(package)_rustflags = -C linker=$$($(package)_build_dir)/rustc-linker.sh --remap-path-prefix=$(BASEDIR)=/build +$(package)_rustflags_mingw32 = -C dlltool=$(host_toolchain)dlltool +$(package)_build_env += RUSTC="$(build_prefix)/bin/rustc" RUSTC_WRAPPER= +$(package)_build_env += RUSTFLAGS="$$($(package)_rustflags) $$($(package)_rustflags_$(host_os))" +$(package)_build_env += DEPENDS_CC="$$($(package)_cc)" DEPENDS_LDFLAGS="$$($(package)_ldflags)" +$(package)_build_env += DEPENDS_BUILD_CC="$(build_CC)" +$(package)_build_env += CC_$($(package)_cc_target)="$$($(package)_cc)" CXX_$($(package)_cc_target)="$$($(package)_cxx)" +$(package)_build_env += AR_$($(package)_cc_target)="$$($(package)_ar)" +$(package)_build_env += CFLAGS_$($(package)_cc_target)="$$($(package)_cppflags) $$($(package)_cflags) -ffile-prefix-map=$(BASEDIR)=/build" +$(package)_build_env += CXXFLAGS_$($(package)_cc_target)="$$($(package)_cppflags) $$($(package)_cxxflags) -ffile-prefix-map=$(BASEDIR)=/build" +$(package)_build_env += HOST_CC="$(build_CC)" HOST_CXX="$(build_CXX)" +ifneq ($(native_rust_build_target),$($(package)_rust_target)) +$(package)_build_env += CC_$(subst -,_,$(native_rust_build_target))="$(build_CC)" CXX_$(subst -,_,$(native_rust_build_target))="$(build_CXX)" +endif +$(package)_build_env += PROTOC="$(build_prefix)/bin/protoc" PROTOC_INCLUDE="$(build_prefix)/include" +ifeq ($(host_os),darwin) +$(package)_build_env += MACOSX_DEPLOYMENT_TARGET=$(OSX_MIN_VERSION) +ifneq ($(build_os),darwin) +$(package)_build_env += SDKROOT="$(OSX_SDK)" +endif +endif +endef + +# tenderdash-proto's build script extracts its sources from +# tenderdash-$$TENDERDASH_COMMITISH.zip in its cache directory +# (CARGO_TARGET_DIR) and would download the archive if it were missing; the +# bundle's configuration sets the tag of the archive the bundle carries. +define $(package)_preprocess_cmds + for config in /.cargo/config /.cargo/config.toml; do \ + if test -e $$$$config; then echo "$$$$config would configure Cargo; remove it" >&2; exit 1; fi; \ + done && \ + cp $($(package)_patch_dir)/rustc-linker.sh $($(package)_patch_dir)/build-linker.sh . && \ + chmod +x rustc-linker.sh build-linker.sh && \ + tag=$$$$(sed -n 's/^TENDERDASH_COMMITISH = "\(.*\)"$$$$/\1/p' .cargo/config.toml) && \ + if ! test -f "tenderdash/tenderdash-$$$$tag.zip"; then \ + echo "the crate bundle has no Tenderdash archive for TENDERDASH_COMMITISH \"$$$$tag\"" >&2; exit 1; \ + fi && \ + mkdir -p target && \ + cp "tenderdash/tenderdash-$$$$tag.zip" target/ +endef + +# The build fails if the dependency graph (normal and build dependencies, both +# of which Cargo compiles) reaches a trusted third-party context provider, an +# HTTP client or OpenSSL: every trust input comes from Core. The graph goes to +# a file first, so that a failing cargo tree fails the build. +define $(package)_build_cmds + ( $($(package)_cargo) tree --frozen --manifest-path $($(package)_build_dir)/Cargo.toml \ + -p dash-platform-cxx -e normal,build --target $($(package)_rust_target) ) > cargo-tree.txt && \ + if grep -E 'rs-sdk-trusted-context-provider|reqwest|openssl-sys' cargo-tree.txt; then \ + echo "dash-platform-cxx depends on a forbidden crate" >&2; exit 1; \ + fi && \ + ( $($(package)_cargo) build --frozen --offline --release --manifest-path $($(package)_build_dir)/Cargo.toml \ + -p dash-platform-cxx --target $($(package)_rust_target) ) +endef + +# The crate's build script stages the generated bridge header (ffi.h), the cxx +# runtime header and signer.h under target//release/include; that tree +# and the static archive are the installed interface. +define $(package)_stage_cmds + mkdir -p $($(package)_staging_prefix_dir)/include $($(package)_staging_prefix_dir)/lib && \ + cp -R target/$($(package)_rust_target)/release/include/. $($(package)_staging_prefix_dir)/include/ && \ + cp target/$($(package)_rust_target)/release/libdash_platform_cxx.a $($(package)_staging_prefix_dir)/lib/ +endef diff --git a/depends/patches/platform_cxx/build-linker.sh b/depends/patches/platform_cxx/build-linker.sh new file mode 100755 index 000000000000..da7d04b6a9cc --- /dev/null +++ b/depends/patches/platform_cxx/build-linker.sh @@ -0,0 +1,8 @@ +#!/bin/sh +export LC_ALL=C +set -f +# Links build scripts and proc macros, which run on the build machine, with the +# depends build compiler. rustc's `-C linker=` takes a single executable, but +# DEPENDS_BUILD_CC is a command line (on macOS with an -isysroot flag). +# shellcheck disable=SC2086 +exec $DEPENDS_BUILD_CC "$@" diff --git a/depends/patches/platform_cxx/rustc-linker.sh b/depends/patches/platform_cxx/rustc-linker.sh new file mode 100755 index 000000000000..db2063792520 --- /dev/null +++ b/depends/patches/platform_cxx/rustc-linker.sh @@ -0,0 +1,10 @@ +#!/bin/sh +export LC_ALL=C +set -f +# rustc's `-C linker=` takes a single executable, but the depends compiler is +# a command line (target and sysroot flags, under Guix an `env -u ...` prefix). +# platform_cxx.mk passes that command in DEPENDS_CC and the link flags in +# DEPENDS_LDFLAGS; word splitting them here keeps every part, and set -f keeps +# a flag with a glob character from being expanded. +# shellcheck disable=SC2086 +exec $DEPENDS_CC $DEPENDS_LDFLAGS "$@" diff --git a/doc/dependencies.md b/doc/dependencies.md index a54e60efbe03..b480aad1e5f7 100644 --- a/doc/dependencies.md +++ b/doc/dependencies.md @@ -36,6 +36,13 @@ You can find installation instructions in the `build-*.md` file for your platfor | [qrencode](../depends/packages/qrencode.mk) | [link](https://fukuchi.org/works/qrencode/) | [4.1.1](https://github.com/bitcoin/bitcoin/pull/27312) | | No | | [Qt](../depends/packages/qt.mk) | [link](https://download.qt.io/official_releases/qt/) | [5.15.18](https://github.com/dashpay/dash/pull/6949) | [5.11.3](https://github.com/bitcoin/bitcoin/pull/24132) | No | +### Dash Platform GUI (`--enable-platform-gui`, depends `PLATFORM_GUI=1` only) +| Dependency | Releases | Version used | Minimum required | Runtime | +| --- | --- | --- | --- | --- | +| [Rust](../depends/packages/native_rust.mk) (compiler and standard library) | [link](https://forge.rust-lang.org/infra/other-installation-methods.html#standalone-installers) | 1.98.1 | 1.98.1 | No | +| [protoc](../depends/packages/native_protobuf.mk) (build tool) | [link](https://github.com/protocolbuffers/protobuf/releases) | 32.0 | 25.0 | No | +| [Dash Platform CXX bindings](../depends/packages/platform_cxx.mk) | [link](https://github.com/dashpay/platform) | commit [02b1749...](https://github.com/dashpay/platform/tree/02b1749cb6aefd75a6fd6a15cbd666fd7a58dfa8) | | No | + ### Networking | Dependency | Releases | Version used | Minimum required | Runtime | | --- | --- | --- | --- | --- | From e2596e5020446a730d414f3691c852150f605c3b Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 24 Sep 2026 10:39:48 -0500 Subject: [PATCH 03/13] build: add --enable-platform-gui The option (default no) requires the GUI and the wallet, and checks that a program using the Dash Platform CXX bindings links: it includes dash/platform/ffi.h and creates and shuts down a platform_ffi::PlatformClient. PLATFORM_CXX_LIBS names the bindings library and defaults to -ldash_platform_cxx from the depends prefix; the system libraries rustc reports for the archive (less the C++ runtime) are always appended to it. The option defines ENABLE_PLATFORM_GUI and the automake conditional of the same name, under which PLATFORM_CXX_LIBS is added to the link of dash-qt, test_dash and test_dash-qt only; dashd and the other binaries never link it, and nothing references the bindings yet. Co-Authored-By: Claude Opus 5.5 (1M context) --- configure.ac | 57 +++++++++++++++++++++++++++++++++++++ src/Makefile.qt.include | 3 ++ src/Makefile.qttest.include | 3 ++ src/Makefile.test.include | 3 ++ 4 files changed, 66 insertions(+) diff --git a/configure.ac b/configure.ac index edfb87a1db61..e979eaa4c748 100644 --- a/configure.ac +++ b/configure.ac @@ -301,6 +301,14 @@ if test "$enable_miner" = "yes"; then AC_DEFINE(ENABLE_MINER, 1, [Define this symbol if in-wallet miner should be enabled]) fi +AC_ARG_ENABLE([platform-gui], + [AS_HELP_STRING([--enable-platform-gui], + [enable Dash Platform (usernames, DashPay contacts) in the GUI; requires the GUI, the wallet and the Dash Platform CXX bindings built by depends with PLATFORM_GUI=1 (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 library (default: -ldash_platform_cxx); the system libraries it needs are always appended]) + dnl Enable different -fsanitize options AC_ARG_WITH([sanitizers], [AS_HELP_STRING([--with-sanitizers], @@ -1936,6 +1944,54 @@ 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 The Dash Platform CXX bindings are a Rust static library linked into dash-qt +dnl (and its tests) only. PLATFORM_CXX_LIBS names the library (default +dnl -ldash_platform_cxx, from the depends prefix); the system libraries rustc +dnl reports for the archive (--print native-static-libs), less the C++ runtime, +dnl are appended to it. +if test "$enable_platform_gui" = "yes"; then + if test "$bitcoin_enable_qt" != "yes"; then + AC_MSG_ERROR([--enable-platform-gui requires the GUI]) + fi + if test "$enable_wallet" != "yes"; then + AC_MSG_ERROR([--enable-platform-gui requires the wallet]) + fi + if test -z "$PLATFORM_CXX_LIBS"; then + PLATFORM_CXX_LIBS="-ldash_platform_cxx" + fi + case $host in + *darwin*) + PLATFORM_CXX_LIBS="$PLATFORM_CXX_LIBS -framework Security -framework CoreFoundation" + ;; + *mingw*) + PLATFORM_CXX_LIBS="$PLATFORM_CXX_LIBS -lbcrypt -ladvapi32 -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp" + ;; + *) + PLATFORM_CXX_LIBS="$PLATFORM_CXX_LIBS -lpthread -ldl -lm" + ;; + 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 the Dash Platform CXX bindings]) + AC_LINK_IFELSE([AC_LANG_PROGRAM([[ + #include + ]], [[ + rust::Box client{platform_ffi::new_platform_client(platform_ffi::Config{})}; + client->shutdown(); + ]])], + [AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no]) + AC_MSG_ERROR([--enable-platform-gui requires the Dash Platform CXX bindings (build depends with PLATFORM_GUI=1)])]) + AC_LANG_POP([C++]) + 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"]) + AM_CONDITIONAL([TARGET_DARWIN], [test "$TARGET_OS" = "darwin"]) AM_CONDITIONAL([BUILD_DARWIN], [test "$BUILD_OS" = "darwin"]) AM_CONDITIONAL([TARGET_LINUX], [test "$TARGET_OS" = "linux"]) @@ -2132,6 +2188,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.qt.include b/src/Makefile.qt.include index ca191acfef7b..53eec4a79bde 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -487,6 +487,9 @@ endif if ENABLE_ZMQ bitcoin_qt_ldadd += $(LIBBITCOIN_ZMQ) $(ZMQ_LIBS) endif +if ENABLE_PLATFORM_GUI +bitcoin_qt_ldadd += $(PLATFORM_CXX_LIBS) +endif bitcoin_qt_ldadd += $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON) $(LIBBITCOIN_UTIL) $(LIBBITCOIN_CONSENSUS) $(LIBBITCOIN_CRYPTO) $(LIBDASHBLS) $(LIBUNIVALUE) $(LIBLEVELDB) $(LIBMEMENV) \ $(BACKTRACE_LIBS) $(QT_LIBS) $(QT_DBUS_LIBS) $(QR_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(LIBSECP256K1) \ $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) diff --git a/src/Makefile.qttest.include b/src/Makefile.qttest.include index d4e84bf80057..6388242acc34 100644 --- a/src/Makefile.qttest.include +++ b/src/Makefile.qttest.include @@ -84,6 +84,9 @@ endif if ENABLE_ZMQ qt_test_test_dash_qt_LDADD += $(LIBBITCOIN_ZMQ) $(ZMQ_LIBS) endif +if ENABLE_PLATFORM_GUI +qt_test_test_dash_qt_LDADD += $(PLATFORM_CXX_LIBS) +endif qt_test_test_dash_qt_LDADD += $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON) $(LIBBITCOIN_UTIL) $(LIBBITCOIN_CONSENSUS) $(LIBBITCOIN_CRYPTO) $(LIBDASHBLS) $(LIBUNIVALUE) $(LIBLEVELDB) \ $(LIBMEMENV) $(BACKTRACE_LIBS) $(QT_LIBS) $(QT_DBUS_LIBS) $(QT_TEST_LIBS) \ $(QR_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(LIBSECP256K1) \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 90a0ed75ed64..28474503589a 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -276,6 +276,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 += $(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) From 053e0749a4c6c33dc56c0a16309f784da7b97c3c Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 24 Sep 2026 10:39:48 -0500 Subject: [PATCH 04/13] guix: allow the Windows DLLs the Dash Platform CXX bindings import A dash-qt built with --enable-platform-gui for Windows imports: - CRYPT32, ncrypt and Secur32: the rustls platform verifier reads the system trust store through schannel; - ntdll: the Rust standard library and mio; - bcryptprimitives (ProcessPrng) and api-ms-win-core-synch-l1-2-0 (WaitOnAddress): raw-dylib imports of the Rust standard library. Only dash-qt with the option imports them; the list is shared by every binary, so check-no-rust.py keeps the others free of Rust instead. windows-sys names its DLLs in lowercase, so the check now compares DLL names case-insensitively, as Windows does. Co-Authored-By: Claude Opus 5.5 (1M context) --- contrib/guix/symbol-check.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/contrib/guix/symbol-check.py b/contrib/guix/symbol-check.py index bca9029b24d8..440b2714ec91 100755 --- a/contrib/guix/symbol-check.py +++ b/contrib/guix/symbol-check.py @@ -171,6 +171,16 @@ 'VERSION.dll', # version checking 'WINMM.dll', # WinMM audio API 'WTSAPI32.dll', # Remote Desktop +# dash-qt only, and only with --enable-platform-gui (the Dash Platform CXX +# bindings); dash-qt without it, dashd and the tools never import these. +# The list is shared by every binary, so this is not enforced here: CI runs +# contrib/devtools/check-no-rust.py on the other binaries instead. +'api-ms-win-core-synch-l1-2-0.dll', # dash-qt: WaitOnAddress (Rust standard library) +'bcryptprimitives.dll', # dash-qt: ProcessPrng (Rust standard library) +'CRYPT32.dll', # dash-qt: system trust store (rustls-native-certs via schannel) +'ncrypt.dll', # dash-qt: CNG key storage (schannel) +'ntdll.dll', # dash-qt: NT native API (Rust standard library, mio) +'Secur32.dll', # dash-qt: SSPI (schannel) } def check_version(max_versions, version, arch) -> bool: @@ -251,8 +261,10 @@ def check_MACHO_lld(binary) -> bool: def check_PE_libraries(binary) -> bool: ok: bool = True + # DLL names are case-insensitive; Rust's windows-sys imports lowercase ones. + allowed = {lib.lower() for lib in PE_ALLOWED_LIBRARIES} for dylib in binary.libraries: - if dylib not in PE_ALLOWED_LIBRARIES: + if dylib.lower() not in allowed: print(f'{dylib} is not in ALLOWED_LIBRARIES!') ok = False return ok From 967c7db9d5ff4131e5b3b51da6b15d394503b0e8 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 24 Sep 2026 10:39:48 -0500 Subject: [PATCH 05/13] ci: build linux64_sqlite against depends with PLATFORM_GUI=1 A new linux64_platform_gui depends target builds depends with PLATFORM_GUI=1, and linux64_sqlite builds against it instead of the linux64 depends (as linux64_tsan builds against linux64_multiprocess's), so dash-qt and the unit tests of that job are built with --enable-platform-gui (enabled through config.site) without adding a separate build and test job. contrib/devtools/check-no-rust.py then fails the linux64_sqlite build if dashd, dash-cli, dash-tx, dash-wallet or the fuzz binary contain cxx bridge, Rust runtime or Rust standard library symbols, or no symbols at all: the bindings are for dash-qt only. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/build.yml | 24 +++++--- ci/dash/build_src.sh | 4 ++ ci/dash/matrix.sh | 2 + ci/test/00_setup_env_native_platform_gui.sh | 12 ++++ ci/test/00_setup_env_native_sqlite.sh | 1 + contrib/devtools/README.md | 8 +++ contrib/devtools/check-no-rust.py | 63 +++++++++++++++++++++ 7 files changed, 107 insertions(+), 7 deletions(-) create mode 100755 ci/test/00_setup_env_native_platform_gui.sh create mode 100755 contrib/devtools/check-no-rust.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a8332aa110e9..f0a81b1a82e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,8 +138,7 @@ jobs: if: | vars.SKIP_LINUX64 == '' || vars.SKIP_LINUX64_ASAN == '' || - vars.SKIP_LINUX64_FUZZ == '' || - vars.SKIP_LINUX64_SQLITE == '' + vars.SKIP_LINUX64_FUZZ == '' with: build-target: linux64 container-path: ${{ needs.container.outputs.path }} @@ -170,6 +169,17 @@ 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_SQLITE == '' }} + with: + build-target: linux64_platform_gui + container-path: ${{ needs.container.outputs.path }} + base-image-digest: ${{ needs.check-skip.outputs.base-image-digest }} + runs-on: ${{ needs.check-skip.outputs['runner-amd64'] }} + depends-mac: name: x86_64-apple-darwin uses: ./.github/workflows/build-depends.yml @@ -285,15 +295,15 @@ jobs: src-linux64_sqlite: name: linux64_sqlite-build uses: ./.github/workflows/build-src.yml - needs: [check-skip, container, depends-linux64] + needs: [check-skip, container, depends-linux64_platform_gui] if: ${{ vars.SKIP_LINUX64_SQLITE == '' }} with: build-target: linux64_sqlite container-path: ${{ needs.container.outputs.path }} - depends-key: ${{ needs.depends-linux64.outputs.key }} - depends-host: ${{ needs.depends-linux64.outputs.host }} - depends-dep-opts: ${{ needs.depends-linux64.outputs.dep-opts }} - depends-artifact: ${{ needs.depends-linux64.outputs.built-artifact }} + 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_tsan: diff --git a/ci/dash/build_src.sh b/ci/dash/build_src.sh index 2184966c28eb..0e3b750f3dfd 100755 --- a/ci/dash/build_src.sh +++ b/ci/dash/build_src.sh @@ -54,6 +54,10 @@ if [ "${RUN_STDLIB_PATH_CHECK}" = "true" ]; then make -C src --jobs=1 check-stdlib-paths fi +if [ "${RUN_CHECK_NO_RUST}" = "true" ]; then + "${BASE_ROOT_DIR}/contrib/devtools/check-no-rust.py" src/dashd src/dash-cli src/dash-tx src/dash-wallet src/test/fuzz/fuzz +fi + if [ -n "$USE_VALGRIND" ]; then echo "valgrind in USE!" "${BASE_ROOT_DIR}/ci/test/wrap-valgrind.sh" diff --git a/ci/dash/matrix.sh b/ci/dash/matrix.sh index 26ea6d1fc10f..7fdf2b20b849 100755 --- a/ci/dash/matrix.sh +++ b/ci/dash/matrix.sh @@ -30,6 +30,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..57178ceaef8b --- /dev/null +++ b/ci/test/00_setup_env_native_platform_gui.sh @@ -0,0 +1,12 @@ +#!/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 + +# Depends with the Dash Platform CXX bindings; linux64_sqlite builds against it. +export CONTAINER_NAME=ci_native_platform_gui +export HOST=x86_64-pc-linux-gnu +export DEP_OPTS="PLATFORM_GUI=1" diff --git a/ci/test/00_setup_env_native_sqlite.sh b/ci/test/00_setup_env_native_sqlite.sh index 8af6d3fd9ae9..32f42e300a7e 100755 --- a/ci/test/00_setup_env_native_sqlite.sh +++ b/ci/test/00_setup_env_native_sqlite.sh @@ -11,3 +11,4 @@ export PACKAGES="python3-zmq qtbase5-dev qttools5-dev-tools libdbus-1-dev libhar export DEP_OPTS="NO_BDB=1 NO_UPNP=1 DEBUG=1" export GOAL="install" export BITCOIN_CONFIG="--enable-zmq --enable-reduce-exports --with-sqlite --without-bdb CC=gcc-11 CXX=g++-11" +export RUN_CHECK_NO_RUST="true" diff --git a/contrib/devtools/README.md b/contrib/devtools/README.md index 501a915e28e9..a2c7479f9b73 100644 --- a/contrib/devtools/README.md +++ b/contrib/devtools/README.md @@ -219,3 +219,11 @@ bundle hash on any machine. The Platform tarball and the bundle are then uploaded to the depends sources mirror. contrib/devtools/platform-bundle.sh + +check-no-rust.py +================ + +Fails if any of the given executables contain Rust code. Run by the +`linux64_sqlite` CI job, which builds against depends with `PLATFORM_GUI=1`, +on `dashd`, the command-line tools and the fuzz binary, which must never link +the Dash Platform CXX bindings. diff --git a/contrib/devtools/check-no-rust.py b/contrib/devtools/check-no-rust.py new file mode 100755 index 000000000000..9a04c27de743 --- /dev/null +++ b/contrib/devtools/check-no-rust.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# 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. +''' +Check that executables do not link Rust code. + +The Dash Platform CXX bindings (--enable-platform-gui) are for dash-qt only; +dashd, the command-line tools and the fuzz binary must not contain any of it. +A binary fails if its symbol table has cxx bridge, Rust runtime or Rust +standard library symbols, or has no symbols at all (a stripped binary cannot +be checked). + +Example usage: + + contrib/devtools/check-no-rust.py src/dashd src/dash-cli src/dash-tx src/dash-wallet src/test/fuzz/fuzz +''' +import re +import subprocess +import sys + +from utils import determine_wellknown_cmd + +# cxx bridge symbols carry "cxxbridge1$". Rust code is either legacy-mangled +# under a crate namespace (rust, std, core, alloc) or v0-mangled ("_R" +# followed by a path tag and a crate root "Cs_"), which C and +# C++ symbols never match. The allocator shim, the panic handler and the +# unwinding personality are plain C names every Rust program links. +RUST_SYMBOL = re.compile( + r'cxxbridge1\$' + r'|_ZN(4rust|3std|4core|5alloc)[0-9]' + r'|\b_?_R[A-Za-z0-9_]*?Cs[A-Za-z0-9]*_[0-9]' + r'|\b_?(__rust_alloc|rust_begin_unwind|rust_eh_personality)' +) + + +def rust_symbols(nm, path): + result = subprocess.run(nm + [path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False) + if result.returncode != 0: + sys.exit(f'{path}: {result.stderr.strip()}') + lines = result.stdout.splitlines() + if not lines: + sys.exit(f'{path}: no symbols, so it cannot be checked (stripped?)') + return [line for line in lines if RUST_SYMBOL.search(line)] + + +def main(): + if len(sys.argv) < 2: + sys.exit(__doc__) + nm = determine_wellknown_cmd('NM', 'nm') + failed = False + for path in sys.argv[1:]: + found = rust_symbols(nm, path) + if found: + failed = True + print(f'{path}: links Rust code, for example:') + for line in found[:5]: + print(f' {line}') + sys.exit(1 if failed else 0) + + +if __name__ == '__main__': + main() From 992c66db95cc3acb6c5eb3f48bab55e68d7e4c68 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 23 Sep 2026 08:25:36 -0500 Subject: [PATCH 06/13] feat(wallet): DIP-15 compact xpub fingerprint and accountReference MAC seams FriendshipXpub carries the BIP32 parent fingerprint of the friendship leaf, so CompactXpubBytes() yields the 69-byte DIP-15 compact form (parentFingerprint || chainCode || pubKey) that contactRequest encryptedPublicKey and the accountReference MAC are computed over. The fingerprint is that of the key one step above the final 256-bit derivation, as rust-dashcore's key-wallet reports it. interfaces::Wallet::platformAccountReferenceMac computes HMAC-SHA256 keyed by the derived ENCRYPTION private key over the compact xpub, matching rs-platform-encryption's calculate_account_reference; only the 32-byte MAC leaves the wallet and the ASK28 masking stays with the caller. It is purpose-specific rather than a generic keyed-hash oracle. Both it and platformECDHSecret refuse key index 0, the identity MASTER key, which DIP-15 never uses for either operation. Tests: ECDH known-answer vector ported from rs-platform-encryption, parent fingerprint, compact xpub, accountReference MAC and DIP-15 payment-address vectors generated with key-wallet e4208c90786a and rs-platform-encryption from the DIP-14 test seed, and MASTER-key refusals. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/interfaces/wallet.h | 8 ++ src/wallet/interfaces.cpp | 5 + src/wallet/platformkeys.cpp | 27 +++++- src/wallet/platformkeys.h | 15 ++- src/wallet/platformtypes.h | 16 ++++ src/wallet/test/platformkeys_tests.cpp | 127 ++++++++++++++++++++++++- src/wallet/wallet.cpp | 24 ++++- src/wallet/wallet.h | 1 + 8 files changed, 214 insertions(+), 9 deletions(-) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index e42bfe09d29d..0aa1734120c6 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -155,9 +155,17 @@ class Wallet //! ECDH shared secret between the identity authentication key //! and a counterparty public key, using the libsecp256k1 ECDH KDF. + //! Refuses key_index 0 (the identity MASTER key). virtual wallet::PlatformKeyResult platformECDHSecret(const wallet::IdentityAuthKey& key, const CPubKey& counterparty) = 0; + //! DIP-15 accountReference MAC: HMAC-SHA256 keyed by the identity + //! authentication (ENCRYPTION) private key over the compact xpub sent in + //! a contact request. Refuses key_index 0. The caller masks the account + //! index with the result; the key never leaves the wallet. + virtual wallet::PlatformKeyResult platformAccountReferenceMac(const wallet::IdentityAuthKey& key, + const wallet::CompactXpub& compact_xpub) = 0; + //! Ensure our private DIP-15 receiving chain is imported as a ranged //! descriptor and return the corresponding public chain. Derivation, //! descriptor update and result publication are one wallet-locked action. diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 83ddf3a38cb4..3fdf2d3cc4a6 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -263,6 +263,11 @@ class WalletImpl : public Wallet { return m_wallet->PlatformECDHSecret(key, counterparty); } + wallet::PlatformKeyResult platformAccountReferenceMac(const wallet::IdentityAuthKey& key, + const wallet::CompactXpub& compact_xpub) override + { + return m_wallet->PlatformAccountReferenceMac(key, compact_xpub); + } wallet::PlatformKeyResult ensureFriendshipReceivingKeychain( const wallet::FriendshipKeychainRequest& request) override { diff --git a/src/wallet/platformkeys.cpp b/src/wallet/platformkeys.cpp index aa188969c7ed..842ce53d02e1 100644 --- a/src/wallet/platformkeys.cpp +++ b/src/wallet/platformkeys.cpp @@ -4,6 +4,7 @@ #include +#include #include #include @@ -88,8 +89,15 @@ bool DeriveExtKey(const CExtKey& parent, const Path& path, ExtKey256& out) CKey key{parent.key}; ChainCode chaincode{parent.chaincode}; + std::array parent_fingerprint{}; + + for (size_t i{0}; i < path.size(); ++i) { + const auto& element{path[i]}; + if (i + 1 == path.size()) { + const CKeyID parent_id{key.GetPubKey().GetID()}; + std::copy_n(parent_id.begin(), parent_fingerprint.size(), parent_fingerprint.begin()); + } - for (const auto& element : path) { CKey child_key; ChainCode child_cc; bool ok{false}; @@ -107,6 +115,7 @@ bool DeriveExtKey(const CExtKey& parent, const Path& path, ExtKey256& out) out.key = key; out.chaincode = chaincode; + out.parent_fingerprint = parent_fingerprint; return true; } @@ -144,10 +153,26 @@ bool ComputeECDHSecret(const CKey& key, const CPubKey& counterparty, SecureVecto return true; } +bool ComputeAccountReferenceMac(const CKey& key, const CompactXpub& compact_xpub, uint256& mac_out) +{ + if (!key.IsValid()) return false; + CHMAC_SHA256{key.begin(), key.size()}.Write(compact_xpub.data(), compact_xpub.size()).Finalize(mac_out.begin()); + return true; +} + } // namespace wallet::platformkeys namespace wallet { +bool CompactXpubBytes(const FriendshipXpub& xpub, CompactXpub& compact_out) +{ + if (!xpub.pubkey.IsCompressed()) return false; + auto it{std::copy(xpub.parent_fingerprint.begin(), xpub.parent_fingerprint.end(), compact_out.begin())}; + it = std::copy(xpub.chaincode.begin(), xpub.chaincode.end(), it); + std::copy(xpub.pubkey.begin(), xpub.pubkey.end(), it); + return true; +} + bool DeriveFriendshipPaymentDestination(const FriendshipXpub& xpub, uint32_t index, CTxDestination& destination_out) { platformkeys::ExtPubKey256 child; diff --git a/src/wallet/platformkeys.h b/src/wallet/platformkeys.h index aeeeebc59fff..3b57717221a0 100644 --- a/src/wallet/platformkeys.h +++ b/src/wallet/platformkeys.h @@ -60,11 +60,15 @@ inline constexpr uint32_t AUTH_KEY_TYPE_ECDSA{0}; inline constexpr uint32_t AUTH_KEY_TYPE_BLS{1}; //! An extended key produced by walking a (possibly 256-bit) derivation path. -//! Unlike CExtKey this does not carry BIP32 serialization metadata; DIP-14 -//! extended-key serialization tracks the path separately. +//! Unlike CExtKey this does not carry BIP32 serialization metadata beyond the +//! parent fingerprint; DIP-14 extended-key serialization tracks the path +//! separately. struct ExtKey256 { CKey key; ChainCode chaincode; + //! BIP32 fingerprint of the key the last path step was derived from + //! (Hash160 prefix of its public key); the DIP-15 compact xpub carries it. + std::array parent_fingerprint{}; ExtKey256() = default; }; @@ -100,6 +104,13 @@ Path FriendshipPath(uint32_t coin_type, uint32_t account, Span us //! encryption. Returns a 32-byte secret. [[nodiscard]] bool ComputeECDHSecret(const CKey& key, const CPubKey& counterparty, SecureVector& secret_out); +//! DIP-15 accountReference MAC: HMAC-SHA256 keyed by the sender's ENCRYPTION +//! private key over the compact xpub sent in the contact request. Matches +//! rs-platform-encryption's calculate_account_reference, which then masks +//! the account index with the low bits of this digest; the masking itself +//! is not done here. +[[nodiscard]] bool ComputeAccountReferenceMac(const CKey& key, const CompactXpub& compact_xpub, uint256& mac_out); + } // namespace wallet::platformkeys #endif // BITCOIN_WALLET_PLATFORMKEYS_H diff --git a/src/wallet/platformtypes.h b/src/wallet/platformtypes.h index 2c9e9913be00..880d281b284a 100644 --- a/src/wallet/platformtypes.h +++ b/src/wallet/platformtypes.h @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include @@ -63,8 +65,22 @@ struct FriendshipKeychainRequest { struct FriendshipXpub { CPubKey pubkey; ChainCode chaincode; + //! BIP32 fingerprint (Hash160 prefix) of the key one level up, i.e. of + //! m/9'/coin'/15'/account'/ for the receiving chain. + std::array parent_fingerprint{}; }; +//! DIP-15 compact extended public key: parentFingerprint(4) || chainCode(32) +//! || pubKey(33). This is the contactRequest encryptedPublicKey plaintext and +//! the accountReference MAC input; unlike a BIP32/DIP-14 serialization it +//! carries no version, depth or child number. +inline constexpr size_t COMPACT_XPUB_SIZE{4 + 32 + 33}; +using CompactXpub = std::array; + +//! Serialize a friendship xpub in the compact form. Fails for a xpub whose +//! key is not compressed, as externally supplied contact xpubs may be. +[[nodiscard]] bool CompactXpubBytes(const FriendshipXpub& xpub, CompactXpub& compact_out); + //! Derive one contact payment destination from a DIP-15 friendship xpub. //! This is public-only key math and does not require a wallet instance. bool DeriveFriendshipPaymentDestination(const FriendshipXpub& xpub, uint32_t index, CTxDestination& destination_out); diff --git a/src/wallet/test/platformkeys_tests.cpp b/src/wallet/test/platformkeys_tests.cpp index aa02aeb771a4..c2bdef589db2 100644 --- a/src/wallet/test/platformkeys_tests.cpp +++ b/src/wallet/test/platformkeys_tests.cpp @@ -3,6 +3,8 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include +#include #include #include #include @@ -10,6 +12,7 @@ #include