12 Commits

Author SHA1 Message Date
fffee9f0b5 build(lite-backend): pin the SDXL backend to rustc 1.63 via rust-toolchain.toml
The pinned librustzcash / transitive crates (notably traitobject 0.1.0) rely on
pre-1.70 trait coherence and fail to compile on newer rustc (E0119), so the backend
must build with 1.63. Add a rust-toolchain.toml in the vendored backend so rustup
auto-selects 1.63 when cargo runs there — no more manual RUSTUP_TOOLCHAIN=1.63.0.
The pin is scoped to the backend tree (repo-root cargo keeps the default toolchain).
Also symlink the pin into the prepared build root so --silentdragonxlitelib-dir
builds honor it too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 21:30:16 -05:00
00ffc959e5 fix(build): name macOS DMGs after the app + stop wiping other variants' artifacts
- The macOS DMG filename used a separate hardcoded DMG_BASENAME ("DragonX_Wallet"),
  so the export didn't match the .app/zip (ObsidianDragon). Derive it from
  APP_BASENAME: full-node -> ObsidianDragon-*.dmg, lite -> ObsidianDragonLite-*.dmg.
  The mounted volume + CFBundleName keep the "DragonX Wallet" display branding.
- build_release_mac did `rm -rf "$out"`, wiping all of release/mac on every build, so
  building one variant destroyed the other's artifacts. Scope the cleanup to the
  current variant's files (as the Linux/Windows release paths already do) so the
  full-node and lite releases coexist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 21:24:54 -05:00
b561406c23 fix(build): make macOS release builds work natively
The --mac-release path (full-node and --lite-backend) had never run on a real
Mac and broke on several Linux-only assumptions:

- build.sh version parser used GNU sed \+ (BSD sed reads it literally), so it
  aborted before compiling — switched to POSIX [[:space:]][[:space:]]*.
- build.sh libsodium universal check used GNU grep \| — switched to grep -E.
- libwebp's cpu.cmake applies -mno-sse2 to its reference DSP files; under a
  universal build (-arch arm64;x86_64) that lands on the x86_64 slice, where it
  disables _Float16 and breaks the SDK 26 <math.h>. Added an idempotent
  FetchContent PATCH_COMMAND (cmake/patch-libwebp-simd.cmake) to neutralize it.
- The SDXL lite backend static lib needs Security + CoreFoundation frameworks on
  macOS; added them to the imported dragonx_lite_backend target for APPLE.
- Added a DRAGONX_MAC_ARCHS override plus auto-detect: with --lite-backend the
  app is built for the arch(es) the backend .a actually provides (its pinned
  ring 0.16.11 is x86_64-only), instead of failing to link a universal app.
- build-lite-backend-artifact.sh uses bash 4+ (mapfile); added a re-exec guard
  for macOS's stock bash 3.2 and added bash to setup.sh's macOS core deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:03:33 -05:00
5a0743a17b fix(setup): skip sudo apt when the Windows toolchain is already installed
setup.sh --win ran `sudo apt-get install` (and update-alternatives)
unconditionally, forcing the whole run under sudo even when mingw-w64 was
already present. Running setup as root makes the daemon cross-compile run as
root, leaving root-owned artifacts under external/dragonx that break `make
clean` on a later non-sudo build (stale objects relink -> mingw link failure
recurs). Gate the apt/update-alternatives block behind a presence check so
`./setup.sh --win` runs sudo-free (and the daemon build as the invoking user)
when the toolchain is already installed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 17:59:05 -05:00
a7b0770ad0 feat(scripts): sign-daemon-release release command (package + sign prebuilt binaries)
Adds `sign-daemon-release.sh release <secret.key> <version>`: zips each staged
prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,
macos,win64}.zip, signs each (detached ed25519), and prints the SHA-256 checksum
table for the release body. Platforms with no staged daemon are skipped; warns
if the signing key doesn't match the pubkey pinned in daemon_updater.h. Keeps the
existing keygen/pubkey/sign subcommands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 17:59:05 -05:00
7d8323a622 Merge branch 'chore/rename-xmrig-hac-to-drg-xmrig' into dev
Point the miner build at DragonX/drg-xmrig and rename the prebuilt staging
dir xmrig-hac -> drg-xmrig across setup.sh, build.sh, the legacy Windows
script, .gitignore and README.
2026-07-23 14:56:28 -05:00
320944fd18 chore(setup): build miner from DragonX/drg-xmrig and rename staging dir
setup.sh now clones/builds the miner from git.dragonx.is/DragonX/drg-xmrig
(was dragonx/xmrig-hac), and the prebuilt staging dir is renamed
prebuilt-binaries/xmrig-hac -> prebuilt-binaries/drg-xmrig across build.sh, the
legacy Windows build script, .gitignore and README.

Also aligns setup.sh's section-8 directory list with section 7 (it previously
used a bare "xmrig" token, creating a stray empty prebuilt-binaries/xmrig/),
and updates the xmrig_manager.cpp header comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:55:42 -05:00
6d5e0ac614 Merge contacts table-view footer fix into dev (follow-up to #1) 2026-07-22 23:46:55 -05:00
02554d523d fix(ui): align contacts table-view footer with cards/list views
In table view the table is inset into its glass panel by tVpad and its
outer_size is listH-2*tVpad, so the post-table cursor ended tVpad (~10px) above
where the cards/list views leave it, pulling the "N address saved" footer up.
Land the cursor at the glass-panel bottom (tpMin.y + listH) so the footer lines
up across all three view styles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:45:14 -05:00
21da9e75fc Merge pull request 'Security audit remediations + Overview/Mining UI polish' (#1) from fix/audit-remediations-dev into dev
Reviewed-on: #1
2026-07-22 17:19:29 -05:00
c53b7f771e fix(ui): overview/mining polish + default-pool cleanup
- balance: inset the address-row favorite (star) button by the card's inner
  padding so it mirrors the left margin instead of hugging the card edge.
- mining: remove pool.dragonx.cc from the built-in default pools (pool.dragonx.is
  is now the sole default); update the pool-registry test accordingly.
- mining: middle-truncate saved pool-URL and payout-address dropdown rows (new
  shared material::TruncateToWidth helper) so a full z-address no longer runs
  under the trailing delete (X) button.
- mining: fix the thread-grid cells overflowing the card at >100% display
  scaling — the reserved Mine-button width used a raw clamp that didn't scale;
  scale it by dp so cols is estimated correctly (no-op at 100%).

Full-node build + test suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:01:51 -05:00
bcee4bfe72 fix(security): apply audit remediations to dev (ported + dev-only)
Dev-branch audit (docs/_archive/security-audit-dev-2026-07-22.md) re-found the
master issues (absent on dev) plus new ones in dev-only code. Applied here;
full-node build + test suite green; the subtle fixes were adversarially re-verified.

Ported from the master remediation (adapted to dev's code):
- bootstrap: reject zip-slip / path-traversal archive members (isSafeArchivePath)
  before writing (S2-1). (dev already fail-closes on a missing checksum.)
- xmrig updater: fail closed when a signature is required but no key is pinned (F1-1).
- http_download.httpGetString: 16 MiB hard cap + MAXFILESIZE on the shared
  metadata/price fetch (F1-2 / caps the updater + exchange paths at one site).
- rpc_client: explicit SSL_VERIFYPEER/VERIFYHOST (F4-1) and a 256 MiB response
  cap in WriteCallback (F4-3).
- lite_connection_service: reject remote http:// lite servers, loopback only (L1-1).
  Loopback is matched by a strict dotted-decimal 127.0.0.0/8 check (not a
  startsWith("127.") prefix, which would wrongly accept 127.0.0.1.evil.com), with
  userinfo/fragment stripping.
- lite controller: propagate encrypt/decrypt save() failure instead of reporting
  success (F7-1).
- xmrig_manager: chmod(0600) the pool config before writing secrets (F5-2).
- app: clear the copied secret from the OS clipboard on shutdown (F3b-1).
- export_transactions: neutralize CSV/spreadsheet formula injection (F13-1).
- build pipeline: build-from-source lite backend + remove the self-attested
  CMake signature gate (F15-1); pinned+verified appimagetool (F15-3/4);
  verified Sapling params in setup.sh (F15-6); build.sh exits 0 on success.
  (F14-1 empty-quoted-arg and F8-2 NUL-termination were already fixed on dev.)

Dev-only findings:
- rpc_client.callRaw: scrub the raw buffer + parsed tree (templated scrubJsonSecrets
  for ordered_json) so console dumpprivkey/z_exportkey keys don't linger in freed
  heap (N1-1).
- seed_wallet_creator: wipe the exported mnemonic on the failure path so a discarded
  failed result never carries a live seed (W1-2).
- export_all_keys: write the plaintext key dump 0600 + atomically via
  writeFileAtomically (U1-2).
- chat_database: restrict chat_messages.sqlite and its WAL/SHM sidecars to owner-only (C3-1).

Not done (need a decision, documented in the report):
- Chat header metadata (cid/z/p) rides outside the AEAD (C1/C2) — binding it is a
  wire-protocol change requiring SilentDragonXLite interop review.
- Bootstrap lacks an offline-rooted signature (S2-2 residual) — needs signing infra.
- Console scrollback retains console-typed key-export output in plaintext (N1-1
  residual) — inherent to an echoing console; would need output redaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:08:24 -05:00
32 changed files with 609 additions and 254 deletions

6
.gitignore vendored
View File

@@ -11,8 +11,8 @@ prebuilt-binaries/dragonxd-win/*
!prebuilt-binaries/dragonxd-win/.gitkeep !prebuilt-binaries/dragonxd-win/.gitkeep
prebuilt-binaries/dragonxd-mac/* prebuilt-binaries/dragonxd-mac/*
!prebuilt-binaries/dragonxd-mac/.gitkeep !prebuilt-binaries/dragonxd-mac/.gitkeep
prebuilt-binaries/xmrig-hac/* prebuilt-binaries/drg-xmrig/*
!prebuilt-binaries/xmrig-hac/.gitkeep !prebuilt-binaries/drg-xmrig/.gitkeep
# External sources / toolchains (created by scripts/setup.sh) # External sources / toolchains (created by scripts/setup.sh)
@@ -33,7 +33,7 @@ imgui.ini
*.bak* *.bak*
*.params *.params
asmap.dat asmap.dat
/external/xmrig-hac /external/drg-xmrig
/memory /memory
/todo.md /todo.md
/.github/ /.github/

View File

@@ -53,7 +53,6 @@ set_property(CACHE DRAGONX_LITE_BACKEND_LINK_MODE PROPERTY STRINGS imported)
set(DRAGONX_LITE_BACKEND_ABI "sdxl-c-v1" CACHE STRING "Expected lite backend C ABI version") set(DRAGONX_LITE_BACKEND_ABI "sdxl-c-v1" CACHE STRING "Expected lite backend C ABI version")
set(DRAGONX_LITE_BACKEND_SYMBOLS_FILE "" CACHE FILEPATH "Path to generated lite backend exported-symbol inventory") set(DRAGONX_LITE_BACKEND_SYMBOLS_FILE "" CACHE FILEPATH "Path to generated lite backend exported-symbol inventory")
set(DRAGONX_LITE_BACKEND_MANIFEST "" CACHE FILEPATH "Optional path to generated lite backend artifact manifest") set(DRAGONX_LITE_BACKEND_MANIFEST "" CACHE FILEPATH "Optional path to generated lite backend artifact manifest")
option(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE "Require verified signature metadata in the lite backend artifact manifest" OFF)
set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS
litelib_wallet_exists litelib_wallet_exists
litelib_initialize_new litelib_initialize_new
@@ -126,36 +125,24 @@ if(DRAGONX_ENABLE_LITE_BACKEND)
if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}") if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}") message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}")
endif() endif()
if(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE) # Note (F15-1): the former signature-metadata gate was removed. It trusted a
if(NOT DRAGONX_LITE_BACKEND_MANIFEST) # "verification_status: verified" field that scripts/build-lite-backend-artifact.sh
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires DRAGONX_LITE_BACKEND_MANIFEST") # self-attested with no cryptographic check (the "verified" SHA was just the artifact's
endif() # own SHA). The trust root is now build-from-source: that script builds the backend from
file(READ "${DRAGONX_LITE_BACKEND_MANIFEST}" DRAGONX_LITE_BACKEND_MANIFEST_JSON) # the vendored in-tree source and refuses prebuilt artifacts, so the library linked here
string(JSON DRAGONX_LITE_SIGNATURE_STATUS ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_STATUS_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_status) # is the one built from reviewed source. The required-symbol inventory check above stays.
if(DRAGONX_LITE_SIGNATURE_STATUS_ERROR)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing signature verification status")
endif()
if(NOT DRAGONX_LITE_SIGNATURE_STATUS STREQUAL "verified")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verified signature metadata")
endif()
string(JSON DRAGONX_LITE_SIGNATURE_VERIFIED_SHA ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verified_artifact_sha256)
string(JSON DRAGONX_LITE_ARTIFACT_SHA ERROR_VARIABLE DRAGONX_LITE_ARTIFACT_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" artifact sha256)
if(DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR OR DRAGONX_LITE_ARTIFACT_SHA_ERROR)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing artifact/signature SHA-256 metadata")
endif()
if(NOT DRAGONX_LITE_SIGNATURE_VERIFIED_SHA STREQUAL DRAGONX_LITE_ARTIFACT_SHA)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST signature metadata does not verify the artifact SHA-256")
endif()
string(JSON DRAGONX_LITE_SIGNATURE_PERFORMED ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_performed)
if(DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR OR NOT DRAGONX_LITE_SIGNATURE_PERFORMED)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verification_performed=true")
endif()
endif()
add_library(dragonx_lite_backend UNKNOWN IMPORTED) add_library(dragonx_lite_backend UNKNOWN IMPORTED)
set_target_properties(dragonx_lite_backend PROPERTIES set_target_properties(dragonx_lite_backend PROPERTIES
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}" IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
) )
if(APPLE)
# The Rust backend's TLS stack (security-framework / core-foundation crates)
# references Secure Transport (SSL*) + CoreFoundation symbols. Link the frameworks
# that provide them, or the static lib leaves ~130 symbols undefined at link time.
set_property(TARGET dragonx_lite_backend APPEND PROPERTY
INTERFACE_LINK_LIBRARIES "-framework Security" "-framework CoreFoundation")
endif()
if(DRAGONX_LITE_BACKEND_INCLUDE_DIR) if(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}") if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}") message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
@@ -304,6 +291,15 @@ FetchContent_Declare(
GIT_REPOSITORY https://github.com/webmproject/libwebp.git GIT_REPOSITORY https://github.com/webmproject/libwebp.git
GIT_TAG v1.4.0 GIT_TAG v1.4.0
GIT_SHALLOW TRUE GIT_SHALLOW TRUE
# libwebp's cpu.cmake applies -mno-sse2/-mno-sse4.1 to its scalar reference DSP
# files when it can't probe SSE support. Under a macOS universal build
# (-arch arm64;x86_64) that probe fails, so the flags land on the x86_64 slice,
# where -mno-sse2 disables _Float16 and breaks the SDK's <math.h>. Neutralize
# those disable flags (SSE2 is x86_64 baseline). Portable + idempotent; a no-op
# for single-arch Linux/Windows/x86_64 builds. See cmake/patch-libwebp-simd.cmake.
PATCH_COMMAND ${CMAKE_COMMAND}
-DCPU_CMAKE=<SOURCE_DIR>/cmake/cpu.cmake
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/patch-libwebp-simd.cmake
) )
set(WEBP_LINK_STATIC ON CACHE BOOL "" FORCE) set(WEBP_LINK_STATIC ON CACHE BOOL "" FORCE)
set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "" FORCE) set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "" FORCE)
@@ -1204,5 +1200,5 @@ message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}")
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}") message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}") message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}") message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}")
message(STATUS " Lite signature: ${DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE}") message(STATUS " Lite trust: built-from-source (vendored third_party/silentdragonxlite)")
message(STATUS "") message(STATUS "")

View File

@@ -81,8 +81,8 @@ Download linux and windows binaries of latest releases and place in binary direc
- prebuilt-binaries/dragonxd-win/ - prebuilt-binaries/dragonxd-win/
- prebuilt-binaries/dragonxd-mac/ - prebuilt-binaries/dragonxd-mac/
**xmrig HAC fork** (https://git.dragonx.is/dragonx/xmrig-hac): **DRG-XMRig fork** (https://git.dragonx.is/DragonX/drg-xmrig):
- prebuilt-binaries/xmrig-hac/ - prebuilt-binaries/drg-xmrig/
## Build Steps ## Build Steps

View File

@@ -131,7 +131,7 @@ fi
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses # truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX. # DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
_cml="$SCRIPT_DIR/CMakeLists.txt" _cml="$SCRIPT_DIR/CMakeLists.txt"
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1) _full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]][[:space:]]*\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
_full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1) _full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1) _lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1) _lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
@@ -386,7 +386,7 @@ build_release_linux() {
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/" [[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
fi fi
# Bundle xmrig for mining support # Bundle xmrig for mining support
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig" local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip" [[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
cp -r bin/res "$dist_dir/" 2>/dev/null || true cp -r bin/res "$dist_dir/" 2>/dev/null || true
@@ -419,7 +419,7 @@ build_release_linux() {
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/" [[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
fi fi
# Bundle xmrig for mining support # Bundle xmrig for mining support
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig" local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; } [[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
# Desktop entry # Desktop entry
@@ -478,18 +478,28 @@ APPRUN
done done
[[ -f "$bd/_deps/sdl3-build/libSDL3.so" ]] && cp "$bd/_deps/sdl3-build/libSDL3.so"* "$APPDIR/usr/lib/" 2>/dev/null || true [[ -f "$bd/_deps/sdl3-build/libSDL3.so" ]] && cp "$bd/_deps/sdl3-build/libSDL3.so"* "$APPDIR/usr/lib/" 2>/dev/null || true
# appimagetool # appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
# The old "continuous" tag is a MOVING build fetched over the network and run on the release
# builder; a compromised/MITM'd artifact would execute here. Verify, or refuse to package.
local APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
local APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
local APPIMAGETOOL="" local APPIMAGETOOL=""
if command -v appimagetool &>/dev/null; then if command -v appimagetool &>/dev/null; then
APPIMAGETOOL="appimagetool" APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
elif [[ -f "$bd/appimagetool-x86_64.AppImage" ]]; then
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
else else
info "Downloading appimagetool ..." local at="$bd/appimagetool-x86_64.AppImage"
wget -q -O "$bd/appimagetool-x86_64.AppImage" \ # Re-verify any cached copy too; a stale unverified download must not be trusted.
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" if [[ ! -f "$at" ]] || ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
chmod +x "$bd/appimagetool-x86_64.AppImage" info "Downloading appimagetool 1.9.0 (pinned) ..."
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage" wget -q -O "$at" "$APPIMAGETOOL_URL"
if ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
err "appimagetool SHA-256 verification failed — refusing to use it"
rm -f "$at"
return 1
fi
chmod +x "$at"
fi
APPIMAGETOOL="$at"
fi fi
local ARCH local ARCH
@@ -628,8 +638,8 @@ HDR
info "Lite mode: skipping embedded daemon binaries" info "Lite mode: skipping embedded daemon binaries"
fi fi
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ──────────────── # ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac" local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
# The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat # The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat
# xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise # xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with # the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
@@ -781,7 +791,7 @@ HDR
fi fi
# Bundle xmrig for mining support # Bundle xmrig for mining support
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe" local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig.exe"
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip" [[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
cp -r bin/res "$dist_dir/" 2>/dev/null || true cp -r bin/res "$dist_dir/" 2>/dev/null || true
@@ -891,8 +901,26 @@ build_release_mac() {
fi fi
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)" info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
else else
# Native macOS: build universal binary (arm64 + x86_64) # Native macOS: build universal (arm64 + x86_64) by default. Override with
# DRAGONX_MAC_ARCHS (e.g. "x86_64").
MAC_ARCHS="${DRAGONX_MAC_ARCHS:-arm64;x86_64}"
# When linking the real lite backend, the app can only include architectures
# the backend static library actually provides. Its pinned ring 0.16.11 has no
# Apple-Silicon assembly, so that artifact is x86_64-only — constrain the app
# arch to the backend's (unless the user explicitly forced DRAGONX_MAC_ARCHS),
# otherwise the arm64 slice fails to link.
if $DO_LITE_BACKEND && [[ -z "${DRAGONX_MAC_ARCHS:-}" && -n "${lb_lib:-}" ]] && command -v lipo &>/dev/null; then
local _backend_archs; _backend_archs=$(lipo -archs "$lb_lib" 2>/dev/null | tr ' ' ';')
if [[ -n "$_backend_archs" && "$_backend_archs" != "$MAC_ARCHS" ]]; then
warn "Lite backend provides only [$_backend_archs] — building the app for that instead of universal."
MAC_ARCHS="$_backend_archs"
fi
fi
if [[ "$MAC_ARCHS" == *";"* || "$MAC_ARCHS" == *","* ]]; then
MAC_ARCH="universal" MAC_ARCH="universal"
else
MAC_ARCH="$MAC_ARCHS"
fi
export MACOSX_DEPLOYMENT_TARGET="11.0" export MACOSX_DEPLOYMENT_TARGET="11.0"
fi fi
@@ -980,7 +1008,7 @@ TOOLCHAIN
need_sodium=true need_sodium=true
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
# Rebuild if existing lib is not universal (single-arch won't link) # Rebuild if existing lib is not universal (single-arch won't link)
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -Eq "arm64.*x86_64|x86_64.*arm64"; then
info "Existing libsodium is not universal — rebuilding ..." info "Existing libsodium is not universal — rebuilding ..."
rm -rf "$SCRIPT_DIR/libs/libsodium" rm -rf "$SCRIPT_DIR/libs/libsodium"
need_sodium=true need_sodium=true
@@ -991,13 +1019,13 @@ TOOLCHAIN
"$SCRIPT_DIR/scripts/fetch-libsodium.sh" "$SCRIPT_DIR/scripts/fetch-libsodium.sh"
fi fi
info "Configuring (native universal arm64+x86_64) ..." info "Configuring (native macOS, arch: $MAC_ARCHS) ..."
cmake "$SCRIPT_DIR" \ cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \ -DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \ -DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \ -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \ -DCMAKE_OSX_ARCHITECTURES="$MAC_ARCHS" \
"${CMAKE_LITE_ARGS[@]}" "${CMAKE_LITE_ARGS[@]}"
fi fi
@@ -1027,8 +1055,12 @@ TOOLCHAIN
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)" info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
# ── Create .app bundle ─────────────────────────────────────────────────── # ── Create .app bundle ───────────────────────────────────────────────────
rm -rf "$out"
mkdir -p "$out" mkdir -p "$out"
# Clean only THIS variant's prior artifacts so full-node and lite releases can
# coexist in release/mac/ (Linux/Windows scope their cleanup the same way). The
# "ObsidianDragon-" glob never matches "ObsidianDragonLite-" (and vice versa),
# and the ".app" names are exact.
rm -rf "$out/${APP_BASENAME}.app" "$out/${APP_BASENAME}-"*.app.zip "$out/${APP_BASENAME}-"*.dmg
local APP="$out/${APP_BASENAME}.app" local APP="$out/${APP_BASENAME}.app"
local CONTENTS="$APP/Contents" local CONTENTS="$APP/Contents"
@@ -1078,8 +1110,8 @@ TOOLCHAIN
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling" info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
fi fi
# xmrig binary (from prebuilt-binaries/xmrig-hac/) # xmrig binary (from prebuilt-binaries/drg-xmrig/)
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig" local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
if [[ -f "$XMRIG_MAC" ]]; then if [[ -f "$XMRIG_MAC" ]]; then
cp "$XMRIG_MAC" "$MACOS/xmrig" cp "$XMRIG_MAC" "$MACOS/xmrig"
chmod +x "$MACOS/xmrig" chmod +x "$MACOS/xmrig"
@@ -1228,8 +1260,10 @@ PLIST
fi fi
# ── Create DMG ─────────────────────────────────────────────────────────── # ── Create DMG ───────────────────────────────────────────────────────────
local DMG_BASENAME="DragonX_Wallet" # DMG filename matches the app bundle name (ObsidianDragon / ObsidianDragonLite).
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite" # The mounted volume + CFBundleName keep the "DragonX Wallet" display branding
# (APP_DISPLAY_NAME above).
local DMG_BASENAME="${APP_BASENAME}"
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg" local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
if command -v create-dmg &>/dev/null; then if command -v create-dmg &>/dev/null; then
@@ -1311,3 +1345,9 @@ if $DO_LINUX || $DO_WIN || $DO_MAC; then
[[ -d "$SCRIPT_DIR/release/windows" ]] && echo -e " ${CYAN}windows/${NC} — .exe + .zip" [[ -d "$SCRIPT_DIR/release/windows" ]] && echo -e " ${CYAN}windows/${NC} — .exe + .zip"
[[ -d "$SCRIPT_DIR/release/mac" ]] && echo -e " ${CYAN}mac/${NC} — .app + .dmg" [[ -d "$SCRIPT_DIR/release/mac" ]] && echo -e " ${CYAN}mac/${NC} — .app + .dmg"
fi fi
# Reaching here means the build completed (real failures exit 1 at their point of failure).
# Exit 0 explicitly: the final `[[ -d release/mac ]] && echo` above returns non-zero on a
# non-mac build — and since set -e exempts the left side of an &&, that status would otherwise
# become the script's exit code and make a successful build report failure (e.g. to CI).
exit 0

View File

@@ -0,0 +1,31 @@
# patch-libwebp-simd.cmake — portable, idempotent FetchContent patch for libwebp.
#
# libwebp's cmake/cpu.cmake compiles its scalar *reference* DSP files with the
# SSE-disable flags "-mno-sse4.1;-mno-sse2" whenever it can't positively detect
# SSE support. Under a macOS *universal* build (-arch arm64;x86_64) the per-arch
# SSE flag probe fails (a flag valid for x86_64 is invalid for arm64), so those
# disable flags get applied to the x86_64 slice. clang gates the _Float16 type on
# SSE2 for x86_64, and the macOS 15+/26 SDK's <math.h> declares _Float16 math
# functions unconditionally — so any TU including <math.h> fails to compile with
# "_Float16 is not supported on this target".
#
# SSE2 is part of the x86_64 baseline ABI, so disabling it on the reference files
# is unnecessary on every platform we target. Blanking the SSE entries (indices
# must stay aligned with WEBP_SIMD_FLAGS) fixes the universal build and is a no-op
# for single-arch Linux/Windows/x86_64 builds. Idempotent: re-running is a no-op.
if(NOT DEFINED CPU_CMAKE OR NOT EXISTS "${CPU_CMAKE}")
message(FATAL_ERROR "patch-libwebp-simd: cpu.cmake not found at '${CPU_CMAKE}'")
endif()
file(READ "${CPU_CMAKE}" _contents)
string(REPLACE
"set(SIMD_DISABLE_FLAGS \"-mno-sse4.1;-mno-sse2;;-mno-dspr2;;-mno-msa\")"
"set(SIMD_DISABLE_FLAGS \";;;-mno-dspr2;;-mno-msa\")"
_patched "${_contents}")
if(_patched STREQUAL _contents)
message(STATUS "patch-libwebp-simd: no change (already patched or pattern absent)")
else()
file(WRITE "${CPU_CMAKE}" "${_patched}")
message(STATUS "patch-libwebp-simd: neutralized x86 SSE-disable flags in cpu.cmake")
endif()

View File

View File

@@ -1,5 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# This script uses bash 4+ features (mapfile, safe empty-array expansion under
# `set -u`). macOS ships bash 3.2, so re-exec under a newer bash when one is
# present (Homebrew), and fail with a clear message otherwise.
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
for _newer_bash in /opt/homebrew/bin/bash /usr/local/bin/bash; do
[ -x "$_newer_bash" ] && exec "$_newer_bash" "$0" "$@"
done
echo "ERROR: build-lite-backend-artifact.sh requires bash 4+ (found ${BASH_VERSION:-unknown})." >&2
echo " On macOS: brew install bash" >&2
exit 1
fi
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -67,25 +79,9 @@ Options:
--backend-dir PATH SilentDragonXLite/lib source directory. --backend-dir PATH SilentDragonXLite/lib source directory.
--silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path. --silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path.
--out-dir PATH Output directory for copied artifact and metadata. --out-dir PATH Output directory for copied artifact and metadata.
--artifact PATH Inventory an existing artifact instead of building.
--no-build Do not run cargo; requires --artifact.
--reproducible Add deterministic Rust path remaps for clean builds. --reproducible Add deterministic Rust path remaps for clean builds.
--remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible. --remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible.
--builder NAME Redacted builder/provenance label. Default: local. --builder NAME Redacted builder/provenance label. Default: local.
--signature-required Fail if verified signature metadata is not supplied.
--signature-file PATH Existing sidecar signature file to record.
--signature-format FORMAT Signature format: minisign, gpg, sigstore, external, or other.
--signature-verification-tool T Verification tool and version used by the release builder.
--signature-verification-command C
Verification command already run by the release builder.
--signature-key-fingerprint F Reviewed public-key fingerprint, when applicable.
--signature-certificate-identity ID
Reviewed certificate identity, when applicable.
--signature-certificate-issuer I
Reviewed certificate issuer, when applicable.
--signature-transparency-log-url URL
Transparency log entry, when applicable.
--signature-verified-sha256 SHA Artifact SHA-256 verified by the signature check.
-j, --jobs N Cargo parallel jobs. -j, --jobs N Cargo parallel jobs.
--cargo-arg ARG Extra argument forwarded to cargo build. --cargo-arg ARG Extra argument forwarded to cargo build.
-h, --help Show this help. -h, --help Show this help.
@@ -95,9 +91,13 @@ Outputs:
<out>/<platform>/lite-backend-symbols.txt <out>/<platform>/lite-backend-symbols.txt
<out>/<platform>/lite-backend-artifact-manifest.json <out>/<platform>/lite-backend-artifact-manifest.json
The script captures symbols, checksums, and optional read-only signature The lite backend is always built from the vendored in-tree source
verification metadata only. It does not load the library, resolve function (third_party/silentdragonxlite), which is the trust root. Prebuilt artifacts
pointers, call SDXL, sign, upload, or publish artifacts. and self-attested signature metadata are NOT accepted (F15-1) — the previous
scheme only recorded an unverified "verified" claim. The script captures the
freshly-built artifact's symbols and checksum, and records build provenance.
It does not load the library, resolve function pointers, call SDXL, sign,
upload, or publish artifacts.
EOF EOF
} }
@@ -166,15 +166,8 @@ while [[ $# -gt 0 ]]; do
OUT_DIR="$(absolute_path "$2")" OUT_DIR="$(absolute_path "$2")"
shift 2 shift 2
;; ;;
--artifact) --artifact|--no-build)
[[ $# -ge 2 ]] || die "--artifact requires a value" die "$1 was removed (F15-1): the lite backend must be built from the vendored in-tree source (third_party/silentdragonxlite); prebuilt artifacts are no longer accepted."
ARTIFACT_PATH="$(absolute_path "$2")"
BUILD_ARTIFACT=false
shift 2
;;
--no-build)
BUILD_ARTIFACT=false
shift
;; ;;
--reproducible) --reproducible)
REPRODUCIBLE=true REPRODUCIBLE=true
@@ -191,54 +184,11 @@ while [[ $# -gt 0 ]]; do
BUILDER="$2" BUILDER="$2"
shift 2 shift 2
;; ;;
--signature-required) --signature-required|--signature-file|--signature-path|--signature-format|\
SIGNATURE_REQUIRED=true --signature-verification-tool|--signature-tool|--signature-verification-command|\
shift --signature-key-fingerprint|--signature-certificate-identity|--signature-certificate-issuer|\
;; --signature-transparency-log-url|--signature-verified-sha256)
--signature-file|--signature-path) die "signature-attestation flags were removed (F15-1): they recorded a self-attested \"verified\" claim without running any cryptographic verifier. The lite backend is built from the vendored in-tree source, which is the trust root."
[[ $# -ge 2 ]] || die "$1 requires a value"
SIGNATURE_FILE="$(absolute_path "$2")"
shift 2
;;
--signature-format)
[[ $# -ge 2 ]] || die "--signature-format requires a value"
SIGNATURE_FORMAT="$2"
shift 2
;;
--signature-verification-tool|--signature-tool)
[[ $# -ge 2 ]] || die "$1 requires a value"
SIGNATURE_VERIFICATION_TOOL="$2"
shift 2
;;
--signature-verification-command)
[[ $# -ge 2 ]] || die "--signature-verification-command requires a value"
SIGNATURE_VERIFICATION_COMMAND="$2"
shift 2
;;
--signature-key-fingerprint)
[[ $# -ge 2 ]] || die "--signature-key-fingerprint requires a value"
SIGNATURE_KEY_FINGERPRINT="$2"
shift 2
;;
--signature-certificate-identity)
[[ $# -ge 2 ]] || die "--signature-certificate-identity requires a value"
SIGNATURE_CERTIFICATE_IDENTITY="$2"
shift 2
;;
--signature-certificate-issuer)
[[ $# -ge 2 ]] || die "--signature-certificate-issuer requires a value"
SIGNATURE_CERTIFICATE_ISSUER="$2"
shift 2
;;
--signature-transparency-log-url)
[[ $# -ge 2 ]] || die "--signature-transparency-log-url requires a value"
SIGNATURE_TRANSPARENCY_LOG_URL="$2"
shift 2
;;
--signature-verified-sha256)
[[ $# -ge 2 ]] || die "--signature-verified-sha256 requires a value"
SIGNATURE_VERIFIED_SHA256="$2"
shift 2
;; ;;
-j|--jobs) -j|--jobs)
[[ $# -ge 2 ]] || die "--jobs requires a value" [[ $# -ge 2 ]] || die "--jobs requires a value"
@@ -374,6 +324,9 @@ prepare_backend_source() {
ln -s "$BACKEND_SOURCE_DIR/src" "$prepared_root/src" ln -s "$BACKEND_SOURCE_DIR/src" "$prepared_root/src"
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock" [[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo" [[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
# Honor the pinned Rust toolchain (rust-toolchain.toml) inside the prepared root too,
# so builds using --silentdragonxlitelib-dir still select rustc 1.63.
[[ -f "$BACKEND_SOURCE_DIR/rust-toolchain.toml" ]] && ln -s "$BACKEND_SOURCE_DIR/rust-toolchain.toml" "$prepared_root/rust-toolchain.toml"
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw" [[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
# Vendored crate deps (offline builds): the .cargo/config.toml's vendored-sources directory is # Vendored crate deps (offline builds): the .cargo/config.toml's vendored-sources directory is
# "vendor" relative to the build root, so expose it inside the prepared root too. # "vendor" relative to the build root, so expose it inside the prepared root too.
@@ -766,6 +719,7 @@ MANIFEST_FILE="$PLATFORM_OUT_DIR/lite-backend-artifact-manifest.json"
printf ' },\n' printf ' },\n'
printf ' "provenance": {\n' printf ' "provenance": {\n'
printf ' "owner_ready": true,\n' printf ' "owner_ready": true,\n'
printf ' "built_from_source": true,\n'
printf ' "metadata_provided": true,\n' printf ' "metadata_provided": true,\n'
printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n' printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n'
printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n' printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n'

View File

@@ -24,18 +24,27 @@ if [ ! -f "${BUILD_DIR}/bin/ObsidianDragon" ]; then
exit 1 exit 1
fi fi
# Check for appimagetool # Check for appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
# The old "continuous" tag is a moving, unverified network download that runs on the release
# builder; verify it or refuse to package.
APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
APPIMAGETOOL="" APPIMAGETOOL=""
if command -v appimagetool &> /dev/null; then if command -v appimagetool &> /dev/null; then
APPIMAGETOOL="appimagetool" APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
elif [ -f "${BUILD_DIR}/appimagetool-x86_64.AppImage" ]; then
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
else else
print_status "Downloading appimagetool..." AT="${BUILD_DIR}/appimagetool-x86_64.AppImage"
wget -q -O "${BUILD_DIR}/appimagetool-x86_64.AppImage" \ if [ ! -f "$AT" ] || ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" print_status "Downloading appimagetool 1.9.0 (pinned)..."
chmod +x "${BUILD_DIR}/appimagetool-x86_64.AppImage" wget -q -O "$AT" "$APPIMAGETOOL_URL"
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage" if ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
print_error "appimagetool SHA-256 verification failed — refusing to use it"
rm -f "$AT"
exit 1
fi
chmod +x "$AT"
fi
APPIMAGETOOL="$AT"
fi fi
print_status "Creating AppDir structure..." print_status "Creating AppDir structure..."

View File

@@ -256,8 +256,8 @@ HEADER_START
echo -e "${YELLOW}Note: Daemon binaries not found in prebuilt-binaries/dragonxd-win/ — wallet only${NC}" echo -e "${YELLOW}Note: Daemon binaries not found in prebuilt-binaries/dragonxd-win/ — wallet only${NC}"
fi fi
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ──────────────── # ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac" XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
if [ -f "$XMRIG_DIR/xmrig.exe" ]; then if [ -f "$XMRIG_DIR/xmrig.exe" ]; then
cp -f "$XMRIG_DIR/xmrig.exe" "$EMBED_RES_DIR/xmrig.exe" cp -f "$XMRIG_DIR/xmrig.exe" "$EMBED_RES_DIR/xmrig.exe"
echo " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))" echo " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"

View File

@@ -1,25 +1,31 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Sign dragonx full-node release archives for the wallet's in-app daemon updater (ed25519). # Package the prebuilt dragonx full-node binaries into per-platform release archives and sign them
# for the wallet's in-app daemon updater (ed25519 over the EXACT archive bytes).
# #
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public # The wallet verifies a detached ed25519 signature over the archive bytes against a public key
# key pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is # pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is MANDATORY
# MANDATORY (kDaemonRequireSignature = true): an in-app update is refused unless a valid signature # (kDaemonRequireSignature = true): an in-app update is refused unless a valid "<archive>.sig" is
# is published. For each archive <name>.zip this produces <name>.zip.sig holding the base64 of the # published next to the archive. The wallet also checks each archive's SHA-256 against a markdown
# raw 64-byte ed25519 signature — upload that .sig next to the .zip as a release asset. # checksum table in the release body, so `release` prints that table for you to paste in.
# #
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl needed. OpenSSL's ed25519 is PureEdDSA (RFC 8032), # Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl. OpenSSL's ed25519 is PureEdDSA (RFC 8032), the
# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible # same primitive libsodium's crypto_sign_verify_detached checks, so the signatures are compatible.
# (the same flow the wallet's unit tests verify for the miner updater).
# #
# Usage: # Usage:
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64} # scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin # scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
# scripts/sign-daemon-release.sh sign <secret.key> <file>...# -> <file>.sig per file # scripts/sign-daemon-release.sh sign <secret.key> <file>... # sign existing files -> <file>.sig
# scripts/sign-daemon-release.sh release <secret.key> <version> [--src DIR] [--out DIR]
# # zip prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,macos,
# # win64}.zip, sign each, and print the SHA-256 checksum table. Platforms with no dragonxd
# # binary staged are skipped.
# #
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into # Keep the secret key (.ed25519.key) OFFLINE (mode 600). Paste the base64 public key into
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h. # kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
die() { echo "error: $*" >&2; exit 1; } die() { echo "error: $*" >&2; exit 1; }
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)" command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
@@ -27,6 +33,26 @@ command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key. # ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; } pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
sha256_of() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}';
else shasum -a 256 "$1" | awk '{print $1}'; fi
}
# Detached ed25519 signature over the raw file bytes -> <file>.sig (base64 of the 64-byte sig).
sign_file() {
local key="$1" f="$2" raw
raw="$(mktemp)"
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
openssl base64 -A -in "$raw" > "$f.sig"
printf '\n' >> "$f.sig"
rm -f "$raw"
}
# platform -> (staging dir under prebuilt-binaries, release token, expected daemon binary name)
plat_dir() { case "$1" in linux) echo dragonxd-linux;; mac) echo dragonxd-mac;; win) echo dragonxd-win;; esac; }
plat_token() { case "$1" in linux) echo linux-amd64;; mac) echo macos;; win) echo win64;; esac; }
plat_daemon() { case "$1" in win) echo dragonxd.exe;; *) echo dragonxd;; esac; }
cmd="${1:-}"; shift || true cmd="${1:-}"; shift || true
case "$cmd" in case "$cmd" in
keygen) keygen)
@@ -42,26 +68,90 @@ case "$cmd" in
echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):" echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):"
echo " $pub" echo " $pub"
;; ;;
pubkey) pubkey)
[ $# -ge 1 ] || die "usage: pubkey <secret.key>" [ $# -ge 1 ] || die "usage: pubkey <secret.key>"
pubkey_b64 "$1" pubkey_b64 "$1"
;; ;;
sign) sign)
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..." [ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
key="$1"; shift key="$1"; shift
[ -f "$key" ] || die "no such key: $key" [ -f "$key" ] || die "no such key: $key"
for f in "$@"; do for f in "$@"; do
[ -f "$f" ] || die "no such file: $f" [ -f "$f" ] || die "no such file: $f"
raw="$(mktemp)" sign_file "$key" "$f"
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
openssl base64 -A -in "$raw" > "$f.sig"
printf '\n' >> "$f.sig"
rm -f "$raw"
echo "signed: $f -> $f.sig" echo "signed: $f -> $f.sig"
done done
echo "Upload each .sig as a release asset next to its archive." echo "Upload each .sig as a release asset next to its archive."
;; ;;
release)
[ $# -ge 2 ] || die "usage: release <secret.key> <version> [--src DIR] [--out DIR]"
key="$1"; version="$2"; shift 2
src="$PROJECT_ROOT/prebuilt-binaries"
out="$PROJECT_ROOT/release/daemon"
while [ $# -gt 0 ]; do
case "$1" in
--src) [ $# -ge 2 ] || die "--src needs a value"; src="$2"; shift 2 ;;
--out) [ $# -ge 2 ] || die "--out needs a value"; out="$2"; shift 2 ;;
*) die "unknown option: $1" ;;
esac
done
[ -f "$key" ] || die "no such key: $key"
[ -d "$src" ] || die "no such source dir: $src"
command -v zip >/dev/null 2>&1 || die "zip not found (install 'zip')"
mkdir -p "$out"
# Sanity: warn if this key does not match the public key pinned in the wallet (the wallet would
# then reject every signature made with it — only expected when deliberately rotating the key).
pinned="$(grep -oE '"[A-Za-z0-9+/]{43}="' "$PROJECT_ROOT/src/util/daemon_updater.h" 2>/dev/null | head -1 | tr -d '"')"
mine="$(pubkey_b64 "$key")"
if [ -n "$pinned" ] && [ "$pinned" != "$mine" ]; then
echo "WARNING: this key's public key does not match the one pinned in daemon_updater.h:" >&2
echo " signing key -> $mine" >&2
echo " pinned key -> $pinned" >&2
echo " The wallet will REJECT these signatures unless you are rotating the pinned key." >&2
echo >&2
fi
made=0
table=""
for plat in linux mac win; do
d="$src/$(plat_dir "$plat")"
daemon="$d/$(plat_daemon "$plat")"
if [ ! -f "$daemon" ]; then
echo "skip $plat: no $(plat_daemon "$plat") staged in $d" >&2
continue
fi
archive="dragonx-$version-$(plat_token "$plat").zip"
apath="$out/$archive"
rm -f "$apath"
# Zip the staged files at the archive root (binaries + sapling params + asmap), excluding
# the .gitkeep placeholder. The updater flattens paths via baseName(), so a flat zip is fine.
files=()
while IFS= read -r fn; do files+=("$fn"); done < <(cd "$d" && ls -A | grep -vx '.gitkeep')
[ "${#files[@]}" -gt 0 ] || { echo "skip $plat: nothing to package in $d" >&2; continue; }
( cd "$d" && zip -q -X "$apath" "${files[@]}" )
sign_file "$key" "$apath"
sum="$(sha256_of "$apath")"
table+="| $archive | \`$sum\` |"$'\n'
echo "packaged + signed: $apath (+ .sig) sha256=$sum"
made=$((made + 1))
done
[ "$made" -gt 0 ] || die "no platform had a staged daemon binary under $src/dragonxd-{linux,mac,win}/"
echo
echo "Checksum table (paste into the release body so the wallet can verify SHA-256):"
echo "| Archive | SHA-256 |"
echo "|---|---|"
printf '%s' "$table"
echo
echo "Upload each .zip AND its .zip.sig as release assets. Wallet enforces the ed25519 signature"
echo "(kDaemonRequireSignature=true) and the SHA-256 from the table above."
;;
*) *)
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}" die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>... | release <secret.key> <version> [--src DIR] [--out DIR]}"
;; ;;
esac esac

View File

@@ -133,7 +133,7 @@ pkgs_core_arch="base-devel cmake git pkg-config
libxkbcommon wayland libsodium curl libxkbcommon wayland libsodium curl
autoconf automake libtool wget python xxd" autoconf automake libtool wget python xxd"
pkgs_core_macos="cmake python xxd" pkgs_core_macos="bash cmake python xxd"
# Windows cross-compile (from Linux) # Windows cross-compile (from Linux)
pkgs_win_debian="mingw-w64 zip" pkgs_win_debian="mingw-w64 zip"
@@ -284,6 +284,14 @@ fi
header "Windows Cross-Compile" header "Windows Cross-Compile"
if $SETUP_WIN; then if $SETUP_WIN; then
# Only touch apt / update-alternatives (which need sudo) when the toolchain is missing. If it is
# already installed, skip them so `./setup.sh --win` can run WITHOUT sudo — important because the
# daemon cross-compile that follows should run as the invoking user. Running the whole setup under
# sudo leaves root-owned build artifacts under external/dragonx, which then break `make clean` on
# a later non-sudo build (stale objects get relinked -> the mingw link failure recurs).
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
ok "Windows cross-compile toolchain already present — skipping apt install"
else
win_pkgs="$(get_pkgs win)" win_pkgs="$(get_pkgs win)"
if [[ -n "$win_pkgs" ]]; then if [[ -n "$win_pkgs" ]]; then
install_pkgs "$win_pkgs" "Windows cross-compile" install_pkgs "$win_pkgs" "Windows cross-compile"
@@ -298,6 +306,7 @@ if $SETUP_WIN; then
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true /usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
fi fi
fi fi
fi
# Fetch libsodium for Windows # Fetch libsodium for Windows
if [[ ! -f "$PROJECT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then if [[ ! -f "$PROJECT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then
@@ -391,11 +400,26 @@ elif $SETUP_SAPLING; then
SPEND_URL="https://z.cash/downloads/sapling-spend.params" SPEND_URL="https://z.cash/downloads/sapling-spend.params"
OUTPUT_URL="https://z.cash/downloads/sapling-output.params" OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
# Consensus-critical MPC parameters with fixed, well-known SHA-256 (identical across every
# Zcash-family node; also pinned in scripts/build-lite-backend-artifact.sh). z.cash is
# plain HTTPS with no signature, so verify the digest and refuse a tampered/corrupt file.
SPEND_SHA256="8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"
OUTPUT_SHA256="2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \ if curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" \
ok "Downloaded sapling-spend.params" && echo "${SPEND_SHA256} $PARAMS_DIR/sapling-spend.params" | sha256sum -c --status; then
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \ ok "Downloaded + verified sapling-spend.params"
ok "Downloaded sapling-output.params" else
rm -f "$PARAMS_DIR/sapling-spend.params"
err "sapling-spend.params download or SHA-256 verification failed — not installed"
fi
if curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" \
&& echo "${OUTPUT_SHA256} $PARAMS_DIR/sapling-output.params" | sha256sum -c --status; then
ok "Downloaded + verified sapling-output.params"
else
rm -f "$PARAMS_DIR/sapling-output.params"
err "sapling-output.params download or SHA-256 verification failed — not installed"
fi
fi fi
else else
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)" skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
@@ -684,11 +708,13 @@ if [[ "$STALE_DAEMON" -eq 1 ]]; then
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac" warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
fi fi
# ── 7. xmrig-hac (mining binary) ──────────────────────────────────────────── # ── 7. drg-xmrig (mining binary) ────────────────────────────────────────────
header "xmrig-hac Mining Binary" header "drg-xmrig Mining Binary"
XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac" XMRIG_SRC="$PROJECT_DIR/external/drg-xmrig"
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac" # Output dir bundled by build.sh (Linux zip, AppImage, Windows embed, mac .app)
# and scripts/legacy/build-windows.sh — keep this path in sync with those.
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/drg-xmrig"
# Clean previous prebuilt xmrig binaries so we always rebuild # Clean previous prebuilt xmrig binaries so we always rebuild
# Only clean the binary for the platform(s) we are actually building, # Only clean the binary for the platform(s) we are actually building,
@@ -700,14 +726,14 @@ if ! $CHECK_ONLY; then
fi fi
fi fi
# Helper: clone xmrig-hac if not present # Helper: clone drg-xmrig if not present
clone_xmrig_if_needed() { clone_xmrig_if_needed() {
if [[ ! -d "$XMRIG_SRC" ]]; then if [[ ! -d "$XMRIG_SRC" ]]; then
info "Cloning xmrig-hac..." info "Cloning drg-xmrig..."
git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC" git clone https://git.dragonx.is/DragonX/drg-xmrig.git "$XMRIG_SRC"
else else
ok "xmrig-hac source already present" ok "drg-xmrig source already present"
info "Pulling latest xmrig-hac..." info "Pulling latest drg-xmrig..."
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true) (cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
fi fi
} }
@@ -728,15 +754,15 @@ else
rm -rf "$XMRIG_SRC/build" rm -rf "$XMRIG_SRC/build"
# Build dependencies (libuv, hwloc, openssl) # Build dependencies (libuv, hwloc, openssl)
info "Building xmrig-hac dependencies (libuv, hwloc, openssl)..." info "Building drg-xmrig dependencies (libuv, hwloc, openssl)..."
( (
cd "$XMRIG_SRC/scripts" cd "$XMRIG_SRC/scripts"
sh build_deps.sh sh build_deps.sh
) )
ok "xmrig-hac dependencies built" ok "drg-xmrig dependencies built"
# Build xmrig # Build xmrig
info "Building xmrig-hac (Linux)..." info "Building drg-xmrig (Linux)..."
mkdir -p "$XMRIG_SRC/build" mkdir -p "$XMRIG_SRC/build"
( (
cd "$XMRIG_SRC/build" cd "$XMRIG_SRC/build"
@@ -753,7 +779,7 @@ else
mkdir -p "$XMRIG_PREBUILT" mkdir -p "$XMRIG_PREBUILT"
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX" cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX"
ok "xmrig (Linux) built and installed to prebuilt-binaries/xmrig-hac/" ok "xmrig (Linux) built and installed to prebuilt-binaries/drg-xmrig/"
else else
err "xmrig (Linux) build failed — binary not found" err "xmrig (Linux) build failed — binary not found"
MISSING=$((MISSING + 1)) MISSING=$((MISSING + 1))
@@ -777,7 +803,7 @@ else
# Clean previous Windows build # Clean previous Windows build
rm -rf "$XMRIG_SRC/build-windows" rm -rf "$XMRIG_SRC/build-windows"
info "Building xmrig-hac (Windows cross-compile)..." info "Building drg-xmrig (Windows cross-compile)..."
( (
cd "$XMRIG_SRC/scripts" cd "$XMRIG_SRC/scripts"
bash build_windows.sh bash build_windows.sh
@@ -787,7 +813,7 @@ else
mkdir -p "$XMRIG_PREBUILT" mkdir -p "$XMRIG_PREBUILT"
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN" cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN"
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/xmrig-hac/" ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/drg-xmrig/"
else else
err "xmrig.exe (Windows) build failed — binary not found" err "xmrig.exe (Windows) build failed — binary not found"
MISSING=$((MISSING + 1)) MISSING=$((MISSING + 1))
@@ -797,7 +823,7 @@ fi
# ── 8. Binary directories ─────────────────────────────────────────────────── # ── 8. Binary directories ───────────────────────────────────────────────────
header "Binary Directories" header "Binary Directories"
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do for platform in dragonxd-linux dragonxd-win dragonxd-mac drg-xmrig; do
dir="$PROJECT_DIR/prebuilt-binaries/$platform" dir="$PROJECT_DIR/prebuilt-binaries/$platform"
if [[ -d "$dir" ]]; then if [[ -d "$dir" ]]; then
# Count actual files (not .gitkeep) # Count actual files (not .gitkeep)

View File

@@ -5320,6 +5320,11 @@ void App::renderLoadingOverlay(float contentH)
void App::shutdown() void App::shutdown()
{ {
// Wipe any copied secret from the OS clipboard before we exit — the 45s auto-clear timer
// never fires if the user quits sooner, which would otherwise leave a key/seed resident.
// (ImGui context is still alive here; App::shutdown() runs before ImGui::DestroyContext().)
clearSecretClipboardIfArmed();
// Clean up bootstrap if running // Clean up bootstrap if running
if (bootstrap_) { if (bootstrap_) {
bootstrap_->cancel(); bootstrap_->cancel();
@@ -5522,10 +5527,9 @@ void App::copySecretToClipboard(const std::string& secret)
ui::Notifications::instance().info("Copied — clipboard auto-clears in 45s", 4.0f); ui::Notifications::instance().info("Copied — clipboard auto-clears in 45s", 4.0f);
} }
void App::pumpSecretClipboardClear() void App::clearSecretClipboardIfArmed()
{ {
if (clipboard_clear_deadline_ <= 0.0) return; if (clipboard_secret_hash_ == 0) return;
if (ImGui::GetTime() < clipboard_clear_deadline_) return;
// Only clear if the clipboard STILL holds our secret (the user may have copied something else). // Only clear if the clipboard STILL holds our secret (the user may have copied something else).
if (const char* cb = ImGui::GetClipboardText()) { if (const char* cb = ImGui::GetClipboardText()) {
std::uint64_t h = 1469598103934665603ULL; std::uint64_t h = 1469598103934665603ULL;
@@ -5536,6 +5540,13 @@ void App::pumpSecretClipboardClear()
clipboard_secret_hash_ = 0; clipboard_secret_hash_ = 0;
} }
void App::pumpSecretClipboardClear()
{
if (clipboard_clear_deadline_ <= 0.0) return;
if (ImGui::GetTime() < clipboard_clear_deadline_) return;
clearSecretClipboardIfArmed();
}
void App::maybeFinishTransactionSendProgress() void App::maybeFinishTransactionSendProgress()
{ {
using Job = services::NetworkRefreshService::Job; using Job = services::NetworkRefreshService::Job;

View File

@@ -573,6 +573,9 @@ public:
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear. // plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
void copySecretToClipboard(const std::string& secret); void copySecretToClipboard(const std::string& secret);
void pumpSecretClipboardClear(); void pumpSecretClipboardClear();
// Immediately clear the clipboard if it still holds the armed secret (ignores the 45s timer).
// Called on app shutdown so a copied key/seed does not outlive the process in the OS clipboard.
void clearSecretClipboardIfArmed();
bool isTransactionRefreshInProgress() const { bool isTransactionRefreshInProgress() const {
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions); return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
} }

View File

@@ -228,6 +228,17 @@ bool ChatDatabase::ensureOpen()
exec("PRAGMA journal_mode=WAL"); exec("PRAGMA journal_mode=WAL");
exec("PRAGMA synchronous=NORMAL"); exec("PRAGMA synchronous=NORMAL");
// C3-1: restrict the chat DB and its WAL/SHM sidecars to owner-only. sqlite creates them with
// umask-derived permissions (often world/group-readable); they hold per-row nonces + AEAD
// ciphertext of the user's messages. Best-effort (errors swallowed; a no-op-ish on Windows).
{
std::error_code perr;
const auto ownerOnly = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write;
std::filesystem::permissions(database_path_, ownerOnly, std::filesystem::perm_options::replace, perr);
std::filesystem::permissions(database_path_ + "-wal", ownerOnly, std::filesystem::perm_options::replace, perr);
std::filesystem::permissions(database_path_ + "-shm", ownerOnly, std::filesystem::perm_options::replace, perr);
}
if (!createSchema()) { if (!createSchema()) {
close(); close();
return false; return false;

View File

@@ -136,6 +136,14 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
} }
} }
// W1-2: never hand back a live seed on a failure path. If the mnemonic was exported but a
// later step failed (empty address, or z_getnewaddress threw), the caller discards this
// result without wiping it, which would leave the seed resident. Success keeps it deliberately.
if (!r.ok && !r.seedPhrase.empty()) {
sodium_memzero(&r.seedPhrase[0], r.seedPhrase.size());
r.seedPhrase.clear();
}
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly). // 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).
cli.disconnect(); cli.disconnect();
temp.stop(20000); temp.stop(20000);

View File

@@ -2,7 +2,7 @@
// Copyright 2024-2026 The Hush Developers // Copyright 2024-2026 The Hush Developers
// Released under the GPLv3 // Released under the GPLv3
// //
// xmrig_manager.cpp — Pool mining process management via xmrig-hac. // xmrig_manager.cpp — Pool mining process management via drg-xmrig.
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares. // Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
#include "xmrig_manager.h" #include "xmrig_manager.h"
@@ -208,19 +208,20 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
try { try {
fs::create_directories(fs::path(outPath).parent_path()); fs::create_directories(fs::path(outPath).parent_path());
std::ofstream ofs(outPath); std::ofstream ofs(outPath, std::ios::trunc);
if (!ofs.is_open()) { if (!ofs.is_open()) {
last_error_ = "Cannot write xmrig config: " + outPath; last_error_ = "Cannot write xmrig config: " + outPath;
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str()); DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
return false; return false;
} }
ofs << j.dump(4);
ofs.close();
#ifndef _WIN32 #ifndef _WIN32
// 0600 permissions — only owner can read/write // Restrict to owner (0600) BEFORE writing any secret material (API token, wallet
// address, worker name). The file is still empty here, so the config is never
// world-readable — closing the window between creation and the previous post-write chmod.
chmod(outPath.c_str(), 0600); chmod(outPath.c_str(), 0600);
#endif #endif
ofs << j.dump(4);
ofs.close();
return true; return true;
} catch (const std::exception& e) { } catch (const std::exception& e) {
last_error_ = std::string("Config write error: ") + e.what(); last_error_ = std::string("Config write error: ") + e.what();

View File

@@ -25,9 +25,11 @@ namespace {
// Recursively zero every string value in a JSON tree in place — used to wipe a discarded parse tree // Recursively zero every string value in a JSON tree in place — used to wipe a discarded parse tree
// that held a secret (B7). Operates on the underlying std::string buffers via get_ref. // that held a secret (B7). Operates on the underlying std::string buffers via get_ref.
void scrubJsonSecrets(nlohmann::json& j) { // Templated so it works on both nlohmann::json and nlohmann::ordered_json (callRaw uses the latter).
template <typename J>
void scrubJsonSecrets(J& j) {
if (j.is_string()) { if (j.is_string()) {
auto& s = j.get_ref<std::string&>(); auto& s = j.template get_ref<std::string&>();
if (!s.empty()) sodium_memzero(&s[0], s.size()); if (!s.empty()) sodium_memzero(&s[0], s.size());
} else if (j.is_object() || j.is_array()) { } else if (j.is_object() || j.is_array()) {
for (auto& el : j) scrubJsonSecrets(el); for (auto& el : j) scrubJsonSecrets(el);
@@ -96,6 +98,10 @@ void RPCClient::setTraceSource(std::string source)
// Callback for libcurl to write response data // Callback for libcurl to write response data
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) { static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
size_t totalSize = size * nmemb; size_t totalSize = size * nmemb;
// Bound accumulation so a hostile/compromised daemon cannot OOM the client with an unbounded
// response body. 256 MiB is far above any legitimate JSON-RPC response yet prevents exhaustion.
static constexpr size_t kMaxRpcResponseBytes = 256u * 1024 * 1024;
if (userp->size() + totalSize > kMaxRpcResponseBytes) return 0; // short count aborts the transfer
userp->append((char*)contents, totalSize); userp->append((char*)contents, totalSize);
return totalSize; return totalSize;
} }
@@ -202,6 +208,10 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
// budget for the TCP + TLS handshake over real network latency (1s would spuriously fail). // budget for the TCP + TLS handshake over real network latency (1s would spuriously fail).
const long connectTimeout = Connection::isLocalHost(host) ? 2L : 10L; const long connectTimeout = Connection::isLocalHost(host) ? 2L : 10L;
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, connectTimeout); curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, connectTimeout);
// Enforce TLS certificate + hostname verification explicitly rather than relying on libcurl's
// build defaults. Harmless on the localhost http:// case; essential for a remote https daemon.
curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYHOST, 2L);
// Test connection with getinfo. Use a SHORT timeout for the probe on localhost: a healthy // Test connection with getinfo. Use a SHORT timeout for the probe on localhost: a healthy
// local daemon answers in milliseconds and a warming one returns -28 just as fast, so a long // local daemon answers in milliseconds and a warming one returns -28 just as fast, so a long
@@ -508,14 +518,22 @@ std::string RPCClient::callRaw(const std::string& method, const json& params)
} }
auto& result = oj["result"]; auto& result = oj["result"];
std::string out;
if (result.is_null()) { if (result.is_null()) {
return "null"; out = "null";
} else if (result.is_string()) { } else if (result.is_string()) {
// Return the raw string (not JSON-encoded) — caller wraps as needed // Return the raw string (not JSON-encoded) — caller wraps as needed
return result.get<std::string>(); out = result.get<std::string>();
} else { } else {
return result.dump(4); out = result.dump(4);
} }
// B7: this raw path serves arbitrary console commands including dumpprivkey / z_exportkey,
// whose response carries plaintext key material. Zero the raw buffer and the parsed tree so
// the secret does not linger in freed heap (matching callSecret). The single returned copy is
// the caller's to manage.
scrubJsonSecrets(oj);
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
return out;
} }
void RPCClient::doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err) void RPCClient::doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err)

View File

@@ -57,6 +57,21 @@ inline ImU32 ReadableError() {
return IM_COL32(r, g, b, (e >> IM_COL32_A_SHIFT) & 0xFF); return IM_COL32(r, g, b, (e >> IM_COL32_A_SHIFT) & 0xFF);
} }
// Middle-ellipsis truncation ("front...back", roughly equal halves) so `text` fits within
// maxWidth pixels when drawn with `font` at `fontSize`. Returns `text` unchanged if it already
// fits (or maxWidth is non-positive). Display-only — never mutate the underlying value with this.
inline std::string TruncateToWidth(const std::string& text, ImFont* font, float fontSize, float maxWidth) {
if (text.empty() || !font || maxWidth <= 0.0f) return text;
if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, text.c_str()).x <= maxWidth) return text;
const int n = static_cast<int>(text.size());
for (int f = n / 2; f >= 3; --f) {
const int b = (f - 2 > 3) ? (f - 2) : 3; // keep the two halves roughly equal
std::string t = text.substr(0, f) + "..." + text.substr(n - b);
if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, t.c_str()).x <= maxWidth) return t;
}
return n > 6 ? (text.substr(0, 3) + "..." + text.substr(n - 3)) : text;
}
// Animated "loading" ellipsis: "", ".", "..", "..." cycling on a ~3Hz phase. // Animated "loading" ellipsis: "", ".", "..", "..." cycling on a ~3Hz phase.
inline const char* LoadingDots() { inline const char* LoadingDots() {
int n = ((int)(ImGui::GetTime() * 3.0f)) % 4; int n = ((int)(ImGui::GetTime() * 3.0f)) % 4;

View File

@@ -70,6 +70,7 @@ AddressRowLayout ComputeAddressRowLayout(float rowX,
float spacingSm, float spacingSm,
float spacingXs) float spacingXs)
{ {
(void)spacingXs; // trailing button now insets by rowPadLeft (mirrors the left margin)
AddressRowLayout layout; AddressRowLayout layout;
layout.contentStartX = rowX + rowPadLeft; layout.contentStartX = rowX + rowPadLeft;
layout.contentStartY = rowY + spacingMd; layout.contentStartY = rowY + spacingMd;
@@ -77,7 +78,9 @@ AddressRowLayout ComputeAddressRowLayout(float rowX,
const float buttonY = rowY + (rowHeight - layout.buttonSize) * 0.5f; const float buttonY = rowY + (rowHeight - layout.buttonSize) * 0.5f;
const float rightEdge = rowX + rowWidth; const float rightEdge = rowX + rowWidth;
const float favoriteX = rightEdge - layout.buttonSize - spacingXs; // Inset the trailing (favorite/star) button by the card's inner padding — the same margin the
// left content uses (rowPadLeft) — so it mirrors the left edge instead of hugging the card edge.
const float favoriteX = rightEdge - layout.buttonSize - rowPadLeft;
const float visibilityX = favoriteX - spacingSm - layout.buttonSize; const float visibilityX = favoriteX - spacingSm - layout.buttonSize;
layout.favoriteButton = {favoriteX, buttonY, layout.buttonSize, layout.buttonSize}; layout.favoriteButton = {favoriteX, buttonY, layout.buttonSize, layout.buttonSize};

View File

@@ -1097,6 +1097,10 @@ void RenderContactsTab(App* app)
} }
ImGui::EndTable(); ImGui::EndTable();
} }
// Land the cursor at the glass-panel bottom (tpMin.y + listH) so the count footer lines up
// with the Cards/List views. The table is inset by tVpad and its outer_size is listH-2*tVpad,
// so it would otherwise end tVpad higher and pull the footer up.
ImGui::SetCursorScreenPos(ImVec2(tpMin.x, tpMin.y + listH));
} else { } else {
// ── CARDS (0) / LIST (1) mode — tactile Material items, no grid lines. ── // ── CARDS (0) / LIST (1) mode — tactile Material items, no grid lines. ──
const bool asCard = (viewMode == 0); const bool asCard = (viewMode == 0);

View File

@@ -211,12 +211,11 @@ void ExportAllKeysDialog::render(App* app)
std::string filepath = configDir + "/" + filename; std::string filepath = configDir + "/" + filename;
bool writeOk = false; bool writeOk = false;
if (exported > 0) { if (exported > 0) {
std::ofstream file(filepath); // Write the plaintext private keys 0600 + atomically (never
if (file.is_open()) { // world/group-readable, never a half-written file) — the same restricted
file << keys; // atomic-write idiom the PIN vault uses. A default ofstream would create
file.close(); // the key dump with umask-derived (often 0644) permissions.
writeOk = true; writeOk = util::Platform::writeFileAtomically(filepath, keys, /*restrictPermissions=*/true);
}
} }
if (!keys.empty()) sodium_memzero(&keys[0], keys.size()); // don't leave every key in freed heap if (!keys.empty()) sodium_memzero(&keys[0], keys.size()); // don't leave every key in freed heap

View File

@@ -36,21 +36,30 @@ static bool s_exporting = false;
// Helper to escape CSV field // Helper to escape CSV field
static std::string escapeCSV(const std::string& field) static std::string escapeCSV(const std::string& field)
{ {
if (field.find(',') != std::string::npos || // Neutralize spreadsheet formula injection: a field beginning with '=', '+', '-', '@',
field.find('"') != std::string::npos || // tab, or CR is interpreted as a formula by Excel/LibreOffice, letting an attacker-supplied
field.find('\n') != std::string::npos) { // memo/address execute on open. Prefix such fields with a single quote so they render as text.
std::string safe = field;
if (!safe.empty()) {
const char c0 = safe.front();
if (c0 == '=' || c0 == '+' || c0 == '-' || c0 == '@' || c0 == '\t' || c0 == '\r')
safe.insert(safe.begin(), '\'');
}
if (safe.find(',') != std::string::npos ||
safe.find('"') != std::string::npos ||
safe.find('\n') != std::string::npos) {
// Escape quotes and wrap in quotes // Escape quotes and wrap in quotes
std::string escaped; std::string escaped;
escaped.reserve(field.size() + 4); escaped.reserve(safe.size() + 4);
escaped += '"'; escaped += '"';
for (char c : field) { for (char c : safe) {
if (c == '"') escaped += "\"\""; if (c == '"') escaped += "\"\"";
else escaped += c; else escaped += c;
} }
escaped += '"'; escaped += '"';
return escaped; return escaped;
} }
return field; return safe;
} }
void ExportTransactionsDialog::show() void ExportTransactionsDialog::show()

View File

@@ -57,7 +57,10 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
// --- Compute thread grid layout based on controls card width --- // --- Compute thread grid layout based on controls card width ---
// Estimate controlsW first to compute cols correctly // Estimate controlsW first to compute cols correctly
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size, miningBtnMaxW) - miningBtnGap; // The Mine button is square (= card height, which scales with DPI), so the width we
// reserve for it here must scale too — a RAW clamp under-reserves at >100% scaling, which
// over-estimates the grid width and lets the thread cells overflow the card (e.g. at 150%).
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size * dp, miningBtnMaxW) - miningBtnGap;
float innerW = estControlsW - pad * 2; float innerW = estControlsW - pad * 2;
float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f)); float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f));
float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size); float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size);

View File

@@ -308,11 +308,14 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10)); pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
if (rowHov && !inXZone) if (rowHov && !inXZone)
pdl->AddRectFilled(rowMin, rowMax, StateHover()); pdl->AddRectFilled(rowMin, rowMax, StateHover());
// Item text with internal padding // Item text with internal padding, middle-truncated so a long saved URL
// can't run under the trailing X (delete) button.
float textY = rowMin.y + (rowH - rowFontSz) * 0.5f; float textY = rowMin.y + (rowH - rowFontSz) * 0.5f;
float maxTextW = popupInnerW - xZoneW - textPadX * 2.0f;
std::string urlDisp = material::TruncateToWidth(url, rowFont, rowFontSz, maxTextW);
pdl->AddText(rowFont, rowFontSz, pdl->AddText(rowFont, rowFontSz,
ImVec2(rowMin.x + textPadX, textY), ImVec2(rowMin.x + textPadX, textY),
isCurrent ? Primary() : OnSurface(), url.c_str()); isCurrent ? Primary() : OnSurface(), urlDisp.c_str());
// X button — flush with right edge, icon centered // X button — flush with right edge, icon centered
{ {
ImVec2 xMin(rowMax.x - xZoneW, rowMin.y); ImVec2 xMin(rowMax.x - xZoneW, rowMin.y);
@@ -467,11 +470,14 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10)); pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
if (rowHov && !inXZone) if (rowHov && !inXZone)
pdl->AddRectFilled(rowMin, rowMax, StateHover()); pdl->AddRectFilled(rowMin, rowMax, StateHover());
// Full address text with internal padding // Address text with internal padding, middle-truncated so a full z-address
// (~78 chars) can't run under the trailing X (delete) button.
float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f; float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f;
float wMaxTextW = wPopupInnerW - wXZoneW - wTextPadX * 2.0f;
std::string addrDisp = material::TruncateToWidth(addr, wRowFont, wRowFontSz, wMaxTextW);
pdl->AddText(wRowFont, wRowFontSz, pdl->AddText(wRowFont, wRowFontSz,
ImVec2(rowMin.x + wTextPadX, textY), ImVec2(rowMin.x + wTextPadX, textY),
isCurrent ? Primary() : OnSurface(), addr.c_str()); isCurrent ? Primary() : OnSurface(), addrDisp.c_str());
// Tooltip for long addresses // Tooltip for long addresses
if (rowHov && !inXZone) if (rowHov && !inXZone)
material::Tooltip("%s", addr.c_str()); material::Tooltip("%s", addr.c_str());

View File

@@ -31,6 +31,31 @@ static bool endsWith(const std::string& s, const std::string& suffix) {
return s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; return s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
} }
// Reject archive member names that would escape the extraction root (zip-slip).
// The snapshot legitimately carries sub-paths (blocks/, chainstate/), so — unlike the
// updaters, which flatten to baseName() — we keep the relative path but refuse any entry
// that is absolute, drive/UNC-rooted, or contains a ".." component.
static bool isSafeArchivePath(const std::string& name) {
if (name.empty()) return false;
std::string n = name;
std::replace(n.begin(), n.end(), '\\', '/'); // normalize Windows separators
if (n.front() == '/') return false; // absolute POSIX path
const char c0 = n[0];
if (n.size() >= 2 && n[1] == ':' &&
((c0 >= 'A' && c0 <= 'Z') || (c0 >= 'a' && c0 <= 'z')))
return false; // Windows drive letter (C:...)
size_t start = 0;
while (start <= n.size()) {
const size_t slash = n.find('/', start);
const std::string comp =
n.substr(start, slash == std::string::npos ? std::string::npos : slash - start);
if (comp == "..") return false; // path traversal component
if (slash == std::string::npos) break;
start = slash + 1;
}
return true;
}
static size_t writeFileCallback(void* contents, size_t size, size_t nmemb, void* userp) { static size_t writeFileCallback(void* contents, size_t size, size_t nmemb, void* userp) {
size_t total = size * nmemb; size_t total = size * nmemb;
FILE* fp = static_cast<FILE*>(userp); FILE* fp = static_cast<FILE*>(userp);
@@ -383,6 +408,16 @@ bool Bootstrap::extract(const std::string& zipPath, const std::string& dataDir)
std::string filename = stat.m_filename; std::string filename = stat.m_filename;
// *** SECURITY: reject zip-slip / path-traversal entries before building any path ***
// A legitimate snapshot from the project host never contains these; an entry that does
// indicates a malicious/corrupt archive, so abort rather than silently skip.
if (!isSafeArchivePath(filename)) {
DEBUG_LOGF("[Bootstrap] Unsafe archive path rejected: %s\n", filename.c_str());
setProgress(State::Failed, "Refusing to extract unsafe archive entry: " + filename);
mz_zip_reader_end(&zip);
return false;
}
// *** CRITICAL: Skip wallet.dat *** // *** CRITICAL: Skip wallet.dat ***
if (filename == "wallet.dat" || endsWith(filename, "/wallet.dat")) { if (filename == "wallet.dat" || endsWith(filename, "/wallet.dat")) {
DEBUG_LOGF("[Bootstrap] Skipping wallet.dat (protected)\n"); DEBUG_LOGF("[Bootstrap] Skipping wallet.dat (protected)\n");

View File

@@ -19,10 +19,19 @@ namespace util {
namespace { namespace {
// Cap for in-memory text/JSON responses (release metadata, price/candle data). Far above any
// legitimate body, but bounds memory if a hostile/MITM'd server streams an unbounded response.
constexpr std::size_t kMaxMetadataBytes = 16u * 1024 * 1024;
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp) size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
{ {
static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb); auto* s = static_cast<std::string*>(userp);
return size * nmemb; const size_t n = size * nmemb;
// Hard cap: a chunked response omits Content-Length, so CURLOPT_MAXFILESIZE cannot catch it —
// aborting here (short count) makes curl fail the transfer instead of exhausting memory.
if (s->size() + n > kMaxMetadataBytes) return 0;
s->append(static_cast<char*>(contents), n);
return n;
} }
size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp) size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp)
@@ -52,6 +61,7 @@ std::string httpGetString(const std::string& url, const char* logTag)
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0"); curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, static_cast<curl_off_t>(kMaxMetadataBytes)); // reject oversized metadata
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
const CURLcode res = curl_easy_perform(curl); const CURLcode res = curl_easy_perform(curl);

View File

@@ -30,14 +30,6 @@ const std::vector<KnownPool>& knownPools()
"https://pool.dragonx.is/api/stats", PoolStatsSchema::DragonXIs, "https://pool.dragonx.is/api/stats", PoolStatsSchema::DragonXIs,
/*miningcorePoolId=*/"", /*feePercent=*/0.0, /*official=*/true, /*miningcorePoolId=*/"", /*feePercent=*/0.0, /*official=*/true,
}, },
KnownPool{
// The mining (stratum) host is us.dragonx.cc — pool.dragonx.cc is the
// Cloudflare-proxied web/API host and does NOT accept stratum on :3333.
// Stats still come from pool.dragonx.cc/api/pools (proxied HTTP is fine).
"dragonx-cc-pplns", "pool.dragonx.cc", "us.dragonx.cc:3333", "rx/dragonx",
"https://pool.dragonx.cc/api/pools", PoolStatsSchema::Miningcore,
/*miningcorePoolId=*/"dragonx-pplns", /*feePercent=*/3.0, /*official=*/true,
},
}; };
return pools; return pools;
} }

View File

@@ -314,6 +314,12 @@ void XmrigUpdater::installResolved(const std::string& targetDir, const XmrigRele
return; return;
} }
} }
} else if (kXmrigRequireSignature) {
// Signatures are required but no key is pinned in this build: fail closed rather than
// silently downgrading to checksum-only (the checksum is same-origin as the archive).
fs::remove(zipPath, ec);
setProgress(State::Failed, "No signing key is pinned in this build — refusing to install.");
return;
} }
} }

View File

@@ -81,10 +81,56 @@ LiteConnectionSettings defaultLiteConnectionSettings()
return settings; return settings;
} }
// Strict dotted-decimal check for the 127.0.0.0/8 loopback block: exactly four numeric octets
// (each 0-255) with the first equal to 127. This must NOT be a prefix match — "127.0.0.1.evil.com"
// and "127.evil.com" are attacker-controlled DNS names that a startsWith("127.") test would wrongly
// treat as loopback, reopening the plaintext-downgrade hole.
static bool isNumericIpv4Loopback(const std::string& h)
{
int parts = 0;
size_t start = 0;
while (true) {
const size_t dot = h.find('.', start);
const std::string seg = h.substr(start, dot == std::string::npos ? std::string::npos : dot - start);
if (seg.empty() || seg.size() > 3) return false;
int v = 0;
for (char c : seg) { if (c < '0' || c > '9') return false; v = v * 10 + (c - '0'); }
if (v > 255) return false;
if (parts == 0 && v != 127) return false; // 127.0.0.0/8 only
++parts;
if (dot == std::string::npos) break;
start = dot + 1;
}
return parts == 4;
}
static bool isLoopbackLiteHostSpec(const std::string& hostPort)
{
std::string h = hostPort;
const size_t term = h.find_first_of("/?#"); // strip path/query/fragment
if (term != std::string::npos) h = h.substr(0, term);
const size_t at = h.rfind('@'); // strip userinfo (user:pass@host)
if (at != std::string::npos) h = h.substr(at + 1);
if (!h.empty() && h.front() == '[') { // [::1]:port (bracketed IPv6)
const size_t close = h.find(']');
h = (close == std::string::npos) ? h : h.substr(1, close - 1);
} else { // host or host:port
const size_t colon = h.find(':');
if (colon != std::string::npos) h = h.substr(0, colon);
}
return h == "localhost" || h == "::1" || isNumericIpv4Loopback(h);
}
bool isLiteServerUrlUsable(const std::string& serverUrl) bool isLiteServerUrlUsable(const std::string& serverUrl)
{ {
const std::string normalized = liteTrimCopy(serverUrl); const std::string normalized = liteTrimCopy(serverUrl);
return startsWith(normalized, "https://") || startsWith(normalized, "http://"); if (startsWith(normalized, "https://")) return true;
// SECURITY: plaintext http:// is a TLS downgrade for lightwalletd traffic (view keys,
// transactions, addresses). Permit it only for loopback (a local dev lightwalletd);
// reject remote plaintext servers instead of silently accepting the downgrade.
if (startsWith(normalized, "http://"))
return isLoopbackLiteHostSpec(normalized.substr(sizeof("http://") - 1));
return false;
} }
bool isOfficialLiteServer(const std::string& serverUrl) bool isOfficialLiteServer(const std::string& serverUrl)

View File

@@ -1066,7 +1066,16 @@ LiteEncryptionResult LiteWalletController::encryptWallet(std::string passphrase)
} }
out = parseEncryptionOpResponse(bridge_->execute("encrypt", passphrase)); out = parseEncryptionOpResponse(bridge_->execute("encrypt", passphrase));
secureWipeLiteSecret(passphrase); secureWipeLiteSecret(passphrase);
if (out.ok) bridge_->execute("save", ""); // persist the now-encrypted wallet if (out.ok) {
// Persist the now-encrypted wallet. If the save fails, do NOT report success — the
// on-disk wallet would still be unencrypted, contradicting what the user was told.
const auto saved = bridge_->execute("save", "");
if (!saved.ok) {
out.ok = false;
out.error = "wallet encrypted in memory but saving to disk failed" +
(saved.error.empty() ? std::string() : (": " + saved.error));
}
}
return out; return out;
} }
@@ -1084,7 +1093,16 @@ LiteEncryptionResult LiteWalletController::decryptWallet(std::string passphrase)
} }
out = parseEncryptionOpResponse(bridge_->execute("decrypt", passphrase)); out = parseEncryptionOpResponse(bridge_->execute("decrypt", passphrase));
secureWipeLiteSecret(passphrase); secureWipeLiteSecret(passphrase);
if (out.ok) bridge_->execute("save", ""); // persist the now-unencrypted wallet if (out.ok) {
// Persist the now-unencrypted wallet. If the save fails, do NOT report success — the
// on-disk wallet would still be encrypted, contradicting what the user was told.
const auto saved = bridge_->execute("save", "");
if (!saved.ok) {
out.ok = false;
out.error = "wallet decrypted in memory but saving to disk failed" +
(saved.error.empty() ? std::string() : (": " + saved.error));
}
}
return out; return out;
} }

View File

@@ -3434,9 +3434,11 @@ void testBalanceAddressListModel()
EXPECT_NEAR(layout.contentStartX, 22.0, 0.0001); EXPECT_NEAR(layout.contentStartX, 22.0, 0.0001);
EXPECT_NEAR(layout.contentStartY, 26.0, 0.0001); EXPECT_NEAR(layout.contentStartY, 26.0, 0.0001);
EXPECT_NEAR(layout.buttonSize, 38.0, 0.0001); EXPECT_NEAR(layout.buttonSize, 38.0, 0.0001);
EXPECT_NEAR(layout.favoriteButton.x, 270.0, 0.0001); // Trailing (favorite) button is inset by rowPadLeft (12) so it mirrors the left margin
EXPECT_NEAR(layout.visibilityButton.x, 228.0, 0.0001); // instead of hugging the card edge: 310 - 38 - 12 = 260.
EXPECT_NEAR(layout.contentRight, 224.0, 0.0001); EXPECT_NEAR(layout.favoriteButton.x, 260.0, 0.0001);
EXPECT_NEAR(layout.visibilityButton.x, 218.0, 0.0001);
EXPECT_NEAR(layout.contentRight, 214.0, 0.0001);
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(2.0, 3.5), std::string("$7.00")); EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(2.0, 3.5), std::string("$7.00"));
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.001, 2.0), std::string("$0.002000")); EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.001, 2.0), std::string("$0.002000"));
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.0, 2.0), std::string("")); EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.0, 2.0), std::string(""));
@@ -5774,23 +5776,20 @@ void testXmrigLiveInstall()
void testPoolRegistryLookup() void testPoolRegistryLookup()
{ {
using namespace dragonx::util; using namespace dragonx::util;
EXPECT_EQ(knownPools().size(), static_cast<std::size_t>(2)); // pool.dragonx.cc was removed from the built-in defaults; pool.dragonx.is is the sole entry.
EXPECT_EQ(knownPools().size(), static_cast<std::size_t>(1));
// The algo follows the pool. Note pool.dragonx.cc's stratum host is us.dragonx.cc // The algo follows the pool: pool.dragonx.is resolves to rx/hush regardless of the caller default.
// (the .cc domain is only the Cloudflare-proxied web/API host).
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc:3333", "rx/hush"), std::string("rx/dragonx"));
EXPECT_EQ(resolvePoolAlgo("pool.dragonx.is:3433", "rx/dragonx"), std::string("rx/hush")); EXPECT_EQ(resolvePoolAlgo("pool.dragonx.is:3433", "rx/dragonx"), std::string("rx/hush"));
// Bare host (no port) still matches; scheme + path are tolerated. // The former us.dragonx.cc (pool.dragonx.cc) host is now unknown -> caller's fallback algo.
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc", "rx/hush"), std::string("rx/dragonx")); EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc:3333", "rx/hush"), std::string("rx/hush"));
EXPECT_EQ(resolvePoolAlgo("stratum+tcp://us.dragonx.cc:3333/x", "rx/hush"),
std::string("rx/dragonx"));
// Unknown host -> fallback algo.
EXPECT_EQ(resolvePoolAlgo("my.pool.example:1234", "rx/hush"), std::string("rx/hush")); EXPECT_EQ(resolvePoolAlgo("my.pool.example:1234", "rx/hush"), std::string("rx/hush"));
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:3333") != nullptr); EXPECT_TRUE(findKnownPoolByUrl("pool.dragonx.is:3433") != nullptr);
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:3333") == nullptr); // removed built-in default
EXPECT_TRUE(findKnownPoolByUrl("unknown.host:1") == nullptr); EXPECT_TRUE(findKnownPoolByUrl("unknown.host:1") == nullptr);
// A mismatched explicit port must NOT match a known pool. // A mismatched explicit port must NOT match a known pool.
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:9999") == nullptr); EXPECT_TRUE(findKnownPoolByUrl("pool.dragonx.is:9999") == nullptr);
} }
// Schema-aware pool hashrate parsing (the two pools speak different APIs). // Schema-aware pool hashrate parsing (the two pools speak different APIs).

View File

@@ -0,0 +1,12 @@
# Rust toolchain pin for the vendored SilentDragonXLite (SDXL) backend.
#
# The pinned librustzcash + transitive crates (notably traitobject 0.1.0) rely on
# pre-1.70 trait-coherence rules and fail to compile on newer rustc with
# error[E0119]: conflicting implementations of trait `Trait` for type `(dyn Send + Sync)`
# so the backend must be built with 1.63 (the toolchain scripts/build-lite-backend-artifact.sh
# and CLAUDE.md target). rustup auto-selects this when cargo runs in this tree, so no
# RUSTUP_TOOLCHAIN / `cargo +1.63.0` is needed.
#
# Install it once with: rustup toolchain install 1.63.0
[toolchain]
channel = "1.63.0"