feat(qt): back Dash Platform GUI internals with real grovedb/drive/dpp crates - #67
Open
PastaPastaPasta wants to merge 334 commits into
Open
feat(qt): back Dash Platform GUI internals with real grovedb/drive/dpp crates#67PastaPastaPasta wants to merge 334 commits into
PastaPastaPasta wants to merge 334 commits into
Conversation
|
Important Review skippedToo many files! This PR contains 331 files, which is 231 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (331)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Aug 7, 2026
PastaPastaPasta
force-pushed
the
platform-gui-rust
branch
2 times, most recently
from
August 10, 2026 19:34
bd04fed to
d7ad8ad
Compare
This was referenced Aug 11, 2026
DKG intake previously deserialized a copy of each accepted payload (repeating BLS point decompression on the shared network thread) for structural validation, and the DKG worker then deserialized the retained bytes again. Replace the typed intake pass with a framing-only wire walk that validates CompactSize counts, dynamic bitsets (via the same ReadFixedBitSet the typed path uses), quorum-parameter bounds, truncation, and trailing bytes without decoding any BLS object. The worker is now the sole typed deserialization point, immediately followed by the same parameter-derived structural checks. The pre-existing per-peer pending-message quota is rekeyed from NodeId to the MNAuth-verified proTxHash and made cumulative for the round, so a sender can no longer reset its retention budget by reconnecting or by waiting for the worker to drain the queue. Own messages are enqueued under this node's own proTxHash and share the same quota path. Sender identities are pinned to the deterministic masternode list by MNAuth, so worst-case retention is bounded by (hostile MN count) x quota. Duplicate hashes are rejected before charging the quota, and quota-dropped messages are not marked seen so another peer with budget can re-deliver them. The llmqType/quorumHash prefix is peeked via SpanReader instead of read+Rewind, and short payloads are scored instead of throwing out of ProcessMessage. Leftover raw queues are discarded at round start without BLS work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Intake framing validation and worker typed deserialization are two hand-maintained parsers over one wire format. The safety-critical direction is that framing must never reject a payload the worker would accept, otherwise honest DKG messages are silently dropped before retention and quorum formation degrades. Assert that direction over fuzzer-provided payloads for every configured LLMQ and both BLS schemes, plus a constructed well-formed message per input so serializer/framing drift is caught even from an empty corpus. The converse is intentionally not asserted: framing accepts undecodable BLS encodings so the worker can score the sender. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unit tests pin the CDKGPendingMessages semantics: the per-proTx quota survives reconnects and is not refunded by drains, duplicates are rejected before charging, quotas are independent across proTxes, and own messages are charged under this node's own proTxHash. Functional tests cover trailing-byte rejection at intake, deferral of BLS decoding to the DKG worker (scored there, not at intake), quota persistence across reconnects under fresh NodeIds, and late-message retention cleared at round start without BLS work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adding UINT64_MAX to a CRangesSet stores the half-open range with a wrapped end of 0. Contains() then reports the value absent, so a duplicate Add() reaches the set-insert assert instead of returning false, and the asset-unlock duplicate-index check (evo/assetlocktx.cpp) would crash in GetCreditPool rather than cleanly rejecting the transaction with bad-assetunlock-duplicated-index. Size() computed the width of such a range through an unsigned wrap that -fsanitize=integer reports. Treat end == 0 as extends-through-UINT64_MAX in Contains() and Size(), and pin the boundary behavior (membership, sizing, duplicate detection, removal, re-add) in test_CRanges. The new checks fail eight ways on the previous implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… in CRangesSet 498a1ae fix(util): handle the UINT64_MAX-containing range in CRangesSet (pasta) Pull request description: ## Issue being fixed or feature implemented `CRangesSet` stores half-open `[begin, end)` ranges of `uint64_t`. Adding `UINT64_MAX` stores `end = UINT64_MAX + 1`, which wraps to `0`, and the rest of the class does not understand that representation: - `Contains(UINT64_MAX)` returns false for a set that holds it (`prev->end > value` is `0 > UINT64_MAX`), so a duplicate `Add()` walks past the duplicate guard and trips `assert(ret.second)` on the underlying `std::set` insert. Via the asset-unlock duplicate-index check (`src/evo/assetlocktx.cpp:204`), a platform-quorum-signed withdrawal with index `2^64 - 1` would crash the node in `GetCreditPool()` instead of being rejected with `bad-assetunlock-duplicated-index`. Platform assigns withdrawal indexes sequentially, so nothing produces that index today — this is a latent boundary bug, not an exploitable path — but consensus code should reject strange quorum-signed data cleanly, never abort on it. - `Size()` computes the width of such a range through an unsigned wrap. The wrapped arithmetic happens to produce the right number, but it is exactly the class of intentional-looking overflow that `-fsanitize=integer` flags, and it costs nothing to state the intent explicitly. This was found while building the bounded snapshot codec for AssumeUTXO M4 (dashpay#7579). Per review feedback there (knst), the fix is split out so a small consensus-adjacent change can be reviewed on its own; nothing in it depends on the snapshot work. It is a prerequisite of the M4 series only in the sense that the snapshot codec serializes `CRangesSet` and wants the boundary semantics to be trustworthy. ## What was done? - `Contains()`: treat `end == 0` as "extends through `UINT64_MAX`". - `Size()`: compute the width of the wrapped range explicitly instead of relying on modular subtraction. - Extended the existing `test_CRanges` unit test with boundary coverage: membership, sizing, duplicate detection, removal, and re-add at and around `UINT64_MAX`, plus the single-element `{UINT64_MAX}` set. The new checks fail eight ways against the previous implementation. ## How Has This Been Tested? `./src/test/test_dash --run_test=util_tests/test_CRanges` — green with the fix; verified the new assertions fail (8 failures) with the fix reverted. Full unit suite run locally on the branch. ## Breaking Changes None. The serialized encoding of `CRangesSet` is unchanged; only in-memory queries over the wrapped-end representation change, and no currently reachable chain state produces that representation. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ ACKs for top commit: knst: utACK 498a1ae Tree-SHA512: ce1ed5d5069f5073330b8fac34417b9496f7944bfe8e712fd6e665d971c301bed95fa5378e295a7bee660ece4deb215d1f21831cda9436fc5d8313834e573d57
GCC 14 at -O2 (the nowallet CI job) rejects the std::optional<bool> constructor parameter with -Werror=maybe-uninitialized when the guard is default-constructed: the inliner loses track of the has_value() check guarding the payload read. Replace the optional parameter with a default constructor and an explicit bool constructor; both existing value-passing call sites already pass a plain bool, so no caller changes.
…LSLegacyScheme 0d092f5 fix: avoid maybe-uninitialized warning in ScopedBLSLegacyScheme (pasta) Pull request description: ## Issue being fixed or feature implemented `develop` is currently red: the `linux64_nowallet` build fails with ``` validation.cpp:167:34: error: 'enter' may be used uninitialized [-Werror=maybe-uninitialized] ``` GCC 14 at `-O2` (the nowallet job's compiler) loses track of the `has_value()` check guarding the `std::optional<bool>` payload read in `ScopedBLSLegacyScheme`'s constructor when the guard is default-constructed from `ConnectTip`. Introduced by 311efbc; every PR based on the current tip inherits the failure. ## What was done? Replaced the `std::optional<bool>` constructor parameter with a default constructor and an explicit `bool` constructor. Both existing value-passing call sites already pass a plain `bool` (`!DeploymentActiveAt(...)`), so no caller changes; the optional deref the warning pointed at no longer exists. No behavior change. The asan `test_dash-qt` QThread leak that was briefly bundled here has been split into its own PR per review feedback, so this PR is only the build fix. ## How Has This Been Tested? - Local build (clang, macOS) clean; `test_dash --run_test=evo_dip3_activation_tests` and `bls_tests` pass. - The failing configuration is GCC 14 `-Werror` in the `linux64_nowallet` CI job — this PR's CI run is the authoritative check. ## Breaking Changes None. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ Top commit has no ACKs. Tree-SHA512: 6dd8859d435680a78974e33269c735afc9db1e9645bf3c25f0a9f429471bccda69444d11296ad3e1b095ac16fe5cbcdc3bab84c1ef5345c187677421215ca51b
…ze=integer The boundary tests merged in dashpay#7587 exercise Add(UINT64_MAX), whose value + 1 half-open end intentionally wraps to 0. The linux64_asan job runs with -fsanitize=integer, which reports the wrap as unsigned integer overflow at util/ranges_set.cpp:27 and fails make check on develop. Spell the successor as an explicit branch (WrappedSuccessor) at the three arithmetic sites so the wrap is stated intent instead of overflow. No behavior change: the guarded value compares and inserts exactly as the wrapped arithmetic did. Verified with a --with-sanitizers=undefined,integer build: util_tests/test_CRanges reproduces the CI failure unfixed and passes fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
18 tasks
The Rust stdlib/target provisioning covered Android, FreeBSD, ARMv7, i686, PowerPC and more, inherited from the Zcash-derived tooling and later ABI-correctness fixes, even though only x86-64 Linux is CI-covered and Guix Rust builds are disabled. Presence in the build system implies a support commitment we cannot honor, and untested mappings can bitrot or emit broken binaries. Trim CROSS_TARGETS, rust_stdlib.mk, native_rust.mk and RS_SET_TRIPLE to the hosts we actually validate: the narrowed Guix release set (x86_64/aarch64/riscv64 Linux, x86_64 Windows, both macOS) plus native development hosts. Any other host now fails --enable-rust explicitly instead of fetching a standard library we never test. Android triples are rejected explicitly since they would otherwise match the generic Linux arms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RUST_MACOS_DEPLOYMENT_TARGET fell back to the host OS version (sw_vers) whenever OSX_MIN_VERSION was not set as a shell variable. The depends config.site passes the deployment target to C/C++ inside CXXFLAGS (-mmacos-version-min=14.0) without exporting OSX_MIN_VERSION, so C++ objects were built for macOS 14.0 while libdashrust.a claimed the host version (e.g. minos 26.5), producing binaries whose Rust code assumes a newer macOS than the binary advertises. Derive the Rust deployment target from the C++ compiler's effective target instead: honor OSX_MIN_VERSION and MACOSX_DEPLOYMENT_TARGET when set, otherwise probe __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ through $CXX $CXXFLAGS so version-min flags embedded in CXXFLAGS are respected. Verified: configure now reports 14.0 under the depends config.site and the built archive carries minos 14.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dist-chirp copied the generated bridge sources before the version stamp, so in the extracted tarball the stamp was newer than the sources and the strictly-newer staleness check always failed, forcing cxxbridge regeneration and defeating the pre-generated dist sources. Copy the stamp first so the shipped sources compare newer than it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 13, 2026
Merged
PastaPastaPasta
force-pushed
the
platform-gui-rust
branch
from
August 13, 2026 02:40
662d017 to
558b54f
Compare
…esSet under -fsanitize=integer 8b200cd fix: make the wrapped successor explicit in CRangesSet under -fsanitize=integer (pasta) Pull request description: ## Issue being fixed or feature implemented **develop's `linux64_asan` job is red since dashpay#7587 merged.** The boundary tests merged there exercise `Add(UINT64_MAX)`, whose half-open end `value + 1` intentionally wraps to `0` — the representation the rest of dashpay#7587 teaches the class to understand. The asan job builds with `-fsanitize=integer`, which reports the intentional wrap as `unsigned integer overflow: 18446744073709551615 + 1` at `util/ranges_set.cpp:27` and fails `make check`. My verification of dashpay#7587 ran the full unit suite but not under sanitizers, which is exactly the gap this slipped through; apologies for the breakage. ## What was done? Spelled the successor as an explicit branch — a file-local `WrappedSuccessor(value)` (`value == UINT64_MAX ? 0 : value + 1`) — at the three arithmetic sites in `Add()`/`Remove()`. This states the wrap as intent instead of overflow, which is preferable to a sanitizer suppression here: unlike the quorum-snapshot skip-list encoding (suppressed by symbol in eacd9e0 because its wraparound is consensus wire format), this is a private in-memory representation that can simply be written unambiguously. No behavior change: for every `value != UINT64_MAX` the expression is `value + 1` as before, and for `UINT64_MAX` it produces the same `0` the wrap produced. ## How Has This Been Tested? Built with `--with-sanitizers=undefined,integer` (the failing job's relevant checks): `util_tests/test_CRanges` reproduces the exact CI failure without the fix and passes with it, using the repo's ubsan suppressions file. Full unit suite green on a regular `--enable-werror` build, rebased on current develop (the `linux64_nowallet` failure visible on this branch's earlier CI was the pre-existing `ScopedBLSLegacyScheme` gcc-14 warning, fixed independently by dashpay#7586). ## Breaking Changes None. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ Top commit has no ACKs. Tree-SHA512: c1810c61e8f7a9dc98023c11c86f88740c4cb8941d8e8f4152e21a8ddf8acf63711004156de04f0f88d2d3ccedd10f2c30caa98e2de5ae4a161a3368569a8e02
Pure BIP32/DIP-14 key-path math for the wallet's Platform key provider: DIP-9 feature-purpose paths, DIP-13 identity authentication/funding paths, DIP-15 friendship keychain paths with 256-bit non-hardened identity components, private and public (watch-only) derivation, the libsecp256k1 ECDH KDF used for DashPay contact request encryption, and a keyed seed fingerprint for pinning multi-seed wallets to one platform seed. The secp256k1 subtree is now built with the ECDH module enabled, which ComputeECDHSecret requires. Tests pin the DIP-14 test vectors (dashpay/dips dip-0014.md) through the path walker, public/private derivation consistency, ECDH symmetry and the seed fingerprint.
Adds an opaque string-keyed key/value store to the wallet database (DBKeys::PLATFORM_DATA) with write/erase, prefix queries, and a load path into CWallet::m_platform_data, exposed through interfaces::Wallet. Records persist in the wallet database and travel with backups; the wallet itself never interprets them. Tests cover write/prefix-query/erase and the ReadKeyValue load path.
…provider seams Exposes a platform key provider through interfaces::Wallet: DIP-13 identity authentication/funding pubkeys and compact signatures, ECDH secrets for DashPay contact requests, DIP-15 friendship xpubs, and a stateless contact payment-destination derivation from a stored xpub. importFriendshipKeychains imports only the wallet's OWN receiving chain as a ranged private descriptor. The contact's receiving chain is deliberately never imported: its scriptPubKeys must not be IsMine, or payments to the contact would decompose as payments-to-self. Contact payment destinations are derived statelessly from the contact's xpub instead. GetPlatformSeed picks the backing BIP39 seed deterministically for multi-seed descriptor wallets: a pinned platform/seed-id record wins, otherwise the candidate from the lowest spk_man ID; legacy wallets use their HD chain seed. Tests cover own-chain spendability (ISMINE_SPENDABLE and AvailableCoins), the contact chain staying ISMINE_NO, deterministic seed selection with the seed-id override, and seed-only-restore rederivation of auth keys, friendship xpubs, ECDH secrets and imported funds, including import idempotency.
Contact xpubs reaching DerivePubKey() are externally supplied, but CPubKey::Derive()/Derive256() assert a valid compressed parent, so an empty or uncompressed key aborted assertion-enabled builds instead of returning the documented failure. Reject non-compressed parents up front; invalid curve points are still rejected by pubkey parsing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the lock-in-based tolerance from the previous two commits. IsPromotionDemotionImminent treated the whole LOCKED_IN period as skew, but V24's nWindowSize is 4032 blocks -- roughly a week -- so what was described as a tip-skew tolerance was really a week-long amnesty for malformed DSTXes and unbalanced final transactions. Participants and masternodes in a live mixing session are within a block of each other in practice, so tolerate exactly that, consistently on both paths: a DSTX from a masternode one block ahead keeps the small PREMATURE penalty rather than the full structural one, and a final transaction that is unbalanced because we are one block short of activation is still signed. A node further behind than that takes the full penalty and risks the collateral -- at that point it has bigger problems than mixing. IsPromotionDemotionActive keeps its fNextBlock parameter, which both paths now use.
GetQueueItemAndTry() marks a queue tried before handing it out, and it only ever hands out a queue once. JoinExistingQueue() then dropped queues whose denomination didn't match the promotion/demotion target after they had already been marked, so a single rebalance pass consumed every pending announcement: the following demotion attempt, the remaining denomination pairs and the standard JoinExistingQueue() call all saw an empty list and fell back to StartNewQueue(). Move the denomination filter into GetQueueItemAndTry() so a queue it skips is left untried and fTried keeps meaning "we actually tried this queue".
CheckPool() dropped a session whose entries are all in but leave a side of the session denomination uncovered without notifying anyone, unlike every other path that abandons a session. Participants kept their inputs locked and their collateral committed until their own timeout expired. Relay ERR_SESSION first, which makes the clients return their keys, unlock their coins and reset immediately. It has to precede SetNull(), which clears the entries the relay iterates.
Post-V24 IsValidStructure() only capped the input and output counts independently, putting no relation between them. Since the denominated-output check passes vacuously on an empty vout, a DSTX with 200 inputs and no outputs passed a gate that previously required equal counts, and was then stored and relayed. IsValidInOuts() does not run on DSTXes received from the network, so nothing else caught it. Add the relations a transaction composable from valid entries must satisfy, neither of which needs UTXO access. Neither side may exceed PROMOTION_RATIO times the other, since an all-promotion transaction carries at most PROMOTION_RATIO inputs per output and an all-demotion one the mirror. And with p promotions, d demotions and standard entries contributing equally to both sides, inputs - outputs is exactly (PROMOTION_RATIO - 1) * (p - d), so the difference between the sides is a multiple of that step. Balanced transactions above the pre-V24 count cap stay valid. A rebalance session caps every entry at PROMOTION_RATIO coins per side, standard ones included, so one promotion, one demotion and eighteen standard entries at that size come to 191 inputs and 191 outputs. Peers below COINJOIN_REBALANCE_VERSION cannot accept that, which is why CanAnnounceDstxTo withholds it from them rather than IsValidStructure rejecting it. Existing cases that stood in for a promotion shape by dropping an output relied on the looser rule: 3 inputs and 2 outputs is not composable from any session, so it is now rejected regardless of where the tip sits relative to activation. p2p_dstx.py builds a real promotion shape instead - one promotion plus two standard entries, 12 inputs and 3 outputs - which keeps all three of its unbalanced cases meaningful, including the post-activation one that expects the transaction to be structurally valid and reach the masternode lookup. output_limit_postfork asserted 30 inputs and 200 outputs would be valid post-V24, whose difference of 170 is not a multiple of PROMOTION_RATIO - 1; it now uses the input count of an all-demotion session at the output cap, and scales that shape past the cap for the rejection it means to test.
6 tasks
CheckPool() sampled the session a piece at a time: GetMixSideCounts() and GetEntriesCount() each took and released cs_coinjoin separately, and vecSessionCollaterals.size() was read with no lock at all. Since CheckPool() runs on the scheduler thread while the message-handling thread appends entries, the side counts could be read before the last entry arrived and the entry count after it. The result describes no state the session was ever in: all entries accounted for, but a side of the session denomination apparently uncovered. CheckPool() then took that as the unreachable case and reset a session that was covered and about to finalize. The unlocked collateral read could also race SetNull() clearing the vector. Take session id, state, entry count, collateral count and side counts under a single lock, and decide from that snapshot. The actions themselves run without the lock, so the two that mutate the session -- finalizing and resetting -- revalidate that the snapshot still describes the live session before acting, since it may have timed out and been replaced in the meantime.
…dependency linter Division of core_write to submodules in evo/, llmq/, governance/ split out evo, llmq, governance code and potentially is useful to reduce binaries size ; for instance the binary dash-tx links only libbitcoin_common + consensus/util so main core_write should not have governance/ code. This commit is fixing linter's bug and adds suppressions for existing circular dependencies.
The include was left behind when CSimplifiedMNListDiff::ToJson() moved out to evo/core_write.cpp; nothing in smldiff.cpp uses core_io anymore. Now that the linter maps evo/core_write.cpp into the core_io module, the stale include manifests as the circular dependency core_io -> evo/smldiff -> core_io.
The GetJsonHelp()/GetRpcResult() definitions lived in the {evo,llmq,
governance}/core_write.cpp files, which form the core_io module and are built
into libbitcoin_common for the sake of their ToJson() halves (dash-tx prints
special-tx payloads via TxToUniv()). The help halves forced rpc/util.h into
core_io's dependency closure, creating four circular dependencies through
core_io -> rpc/util -> node/transaction, and shipped the help tables in
libbitcoin_common although every consumer (rpc/blockchain, rpc/coinjoin,
rpc/evo, rpc/governance, rpc/masternode, rpc/quorums, rpc/rawtransaction) is
in libbitcoin_node.
Move RPCRESULT_MAP, GetRpcResult() and all GetJsonHelp() definitions verbatim
into a new rpc/json_help.{h,cpp} in libbitcoin_node, and move the
GetRpcResult() declaration from core_io.h to the new header. The core_write
files keep only their ToJson() definitions. The help is RPC documentation
shared by several command files, so it gets its own translation unit rather
than being spliced into the generic machinery of rpc/util.cpp, which is part
of libbitcoin_common and would keep the tables in the common library.
The four coinjoin suppressions through core_io -> rpc/util are gone; the
longer pre-existing coinjoin/client -> coinjoin/util -> wallet/wallet cycle
they had been shadowing is visible to the linter again and returns to the
suppression list.
Unlike the evo and llmq JSON writers, nothing that links only libbitcoin_common prints governance objects: dash-tx's TxToUniv() knows no governance payload, and all callers of these ToJson() definitions sit in libbitcoin_node. Keeping them in a libbitcoin_common file bought nothing and forced governance/governance.h into the core_io module, creating the circular dependency core_io -> governance/governance -> governance/superblock -> core_io. Move CGovernanceManager::ToJson() to governance/governance.cpp, CGovernanceObject::GetInnerJson()/GetVotesJson() to governance/object.cpp and Governance::Object::ToJson() to governance/common.cpp, and delete the file along with its linter module mapping and the now-cleared suppression.
Also drop coinjoin/client.cpp's unused include of the header and correct the stale note claiming CDeterministicMN::ToJson() lived in evo/deterministicmns.cpp: it lived in rpc/evo_util.cpp and now lives in rpc/evo.cpp.
Re-land of pre-rebase 1df2394751, dropped during the rebase over PR 7600. The function looks misplaced next to its siblings in evo/core_write.cpp, so record why it lives here: dash-tx never prints a masternode entry, the g_txindex lookup ties it to libbitcoin_node, and hosting it in evo/deterministicmns.cpp would create four new circular dependencies.
GetNetInfoWithLegacyFields() and GetPlatformPort() are not RPC utilities: they are JSON writers over netInfo and the legacy platform port fields, consumed by the ToJson() definitions in evo/core_write.cpp plus two report builders in rpc/masternode.cpp and rpc/quorums.cpp. After the previous commit hollowed rpc/evo_util down to just these two templates, keeping a separate rpc/ header for them was pure overhead. Define them in evo/core_write.cpp next to their main users, declare them in core_io.h (the module's header, like the other core_write writers), and add explicit CDeterministicMNState instantiations for the two out-of-module consumers. rpc/evo_util.h is deleted; the rpc/evo_util module is gone entirely. No linter suppression changes: the module took part in no cycle.
The split between modules to avoid conflicts for backports which changes qt/guiutil
Motivation to exist providertx_util are:
Owner payout list helpers that operate purely on the serialized representation. They live in
libbitcoin_common (rather than libbitcoin_node alongside the rest of providertx.cpp) so that
common-layer consumers like the bloom filter and the JSON writers can use them without pulling
in node-only dependencies.
This commit removes providertx's dependency on dmnstate
…ords and DIP-15 friendship keychain seams b2a3c40 fix(wallet): derive Platform keys from descriptor root (pasta) 939ee20 refactor(wallet): avoid serializing friendship xprv (pasta) 81ba952 refactor(wallet): confine Platform derivation to key managers (pasta) 449da14 wallet: make the Platform seed provider descriptor-wallet-only (pasta) 6a92a1c fix(wallet): validate mnemonics before deriving the platform seed (pasta) d58facb fix(wallet): keep damaged Platform cache records noncritical on load (pasta) d5e41f4 refactor(wallet): drop the unused friendship import label parameter (pasta) b1da3d4 fix(wallet): treat corrupt platform data records as wallet corruption (pasta) 3f954b4 fix(wallet): reject unknown platform key types before derivation (pasta) 80a46ec fix(wallet): skip empty-mnemonic descriptors in platform seed selection (pasta) b78f3cf chore(wallet): annotate m_platform_data locking, add missing includes (pasta) 47c0751 test(wallet): pin friendship derivation against rust-dashcore key-wallet (pasta) 3a430b5 fix(wallet): require a full unlock before serving the legacy platform seed (pasta) 31637d7 fix(wallet): fail platform seed selection on a malformed seed pin (pasta) 005459f fix(wallet): drop platform data from memory only after the database erase succeeds (pasta) 2cd5bcd fix(wallet): preserve friendship descriptor state on re-import (pasta) d583a42 fix(wallet): fail platform seed selection when the pinned seed is unavailable (pasta) a67dbc2 fix(wallet): validate parent pubkey before DIP-14 public derivation (pasta) 01c8a8a feat(wallet): add DIP-15 friendship keychain import and platform key provider seams (pasta) c6fe429 feat(wallet): add generic per-wallet Platform data records (pasta) 3b8fc64 feat(wallet): add Platform (DIP-9/13/14/15) key derivation helpers (pasta) Pull request description: ## Issue being fixed or feature implemented Part of the Dash Platform GUI PR train tracked in dashpay#7512 (the tracking issue's body still describes an older architecture; the current reference implementation is #67). This PR extracts the **wallet-layer Platform seams**: pure C++ wallet code with **no Rust/FFI dependency**, so it can be reviewed and merged in parallel with the build-system PR dashpay#7580. Builds on the DIP-14 `Derive256` primitives merged in dashpay#7511. ## What was done? Three seams: **1. Platform key derivation helpers (`src/wallet/platformkeys.{h,cpp}`)** Pure BIP32/DIP-14 path math, independent of Platform documents/contracts/network: - DIP-9 feature-purpose paths; DIP-13 identity authentication and funding paths; DIP-15 friendship keychain paths whose two 256-bit identity components are deliberately non-hardened (enabling watch-only xpub derivation). - Private and public (neutered) derivation walkers over mixed 31-bit/256-bit paths. Private derivation starts from a BIP32 extended private key; a mnemonic is only one possible way to create that key and is not required by these primitives. - ECDH shared secrets via the libsecp256k1 ECDH KDF (SHA256 of the compressed shared point), matching dashj's `Secp256k1ECDHAgreement` used for DashPay contact request encryption. The secp256k1 subtree is now configured with `--enable-module-ecdh` (previously disabled). - Descriptor helpers expose a descriptor's depth-zero root xpub and recover the matching root xprv from its signing provider without reconstructing a BIP39 seed. **2. Generic per-wallet Platform data records (walletdb)** A string-keyed, opaque key/value store in the wallet database (`DBKeys::PLATFORM_DATA`): `WalletBatch::{Write,Erase}PlatformData`, `CWallet::{Load,Write,Get}PlatformData` (prefix queries), the `ReadKeyValue` load path, and `interfaces::Wallet::{write,get}PlatformData`. Records persist in the wallet database and travel with backups. **These records are opaque to the wallet by design.** The wallet stores and returns bytes; interpretation lives entirely with the Platform client layers. They are not consulted for Platform key-source selection or key derivation. **3. DIP-15 friendship keychain import + Platform key provider (`interfaces::Wallet`)** - `getPlatformPubKey` / `signPlatformDigest` / `platformECDHSecret`: DIP-13 identity authentication and funding keys derived on demand inside the active descriptor key manager; root private key material never crosses the interface. - `ensureFriendshipReceivingKeychain`: derives the wallet's **own** DIP-15 receiving chain, imports it idempotently as a ranged private descriptor, and returns its public chain in one wallet-locked operation. The stored descriptor uses the xpub plus its private key rather than serializing the friendship xprv. - `DeriveFriendshipPaymentDestination`: derives contact payment destinations statelessly from a contact's stored friendship xpub, without touching any wallet keypool. - Platform derivation is descriptor-wallet-only. A compatible active key manager must expose a genuine depth-zero descriptor root and hold its matching private key. When multiple active managers support Platform derivation, their complete root xpubs (including chain code) must agree. Root-xprv-only descriptor imports are supported; child xprvs, watch-only wallets, external signers, and legacy wallets are rejected. **Design invariant (please review against it):** the contact's own receiving chain is deliberately **never** imported. If its scriptPubKeys became `IsMine`, payments to the contact would decompose as payments-to-self and the contact's outputs would be counted as our own coins. Payment destinations for a contact are instead derived statelessly from their xpub. `friendship_contact_chain_is_not_ours` pins this (`ISMINE_NO` for both the reversed-id chain and a genuinely foreign contact xpub). ### Adaptations relative to the reference branch - **De-gated:** on the reference branch this code sat behind `--enable-platform-gui`. That flag does not exist on `develop`, so the extracted code compiles and is tested **unconditionally** (like dashpay#7511). The `ENABLE_PLATFORM_GUI` ifdefs and their `#else` stubs were removed, and the secp256k1 ECDH module is enabled unconditionally in `configure.ac`. - **Trimmed out of scope** (arrive with later PRs in the train): the asset-lock creation seam (`createAssetLockTransaction`), `startRescanFromHeight`, `wallet/rpc/platform.cpp`, and everything Qt/GUI or Rust/FFI. - The DIP-14 test vectors appear here again on top of dashpay#7511's `dip14_tests`: that suite pins the raw `CKey::Derive256` primitives, while `platformkeys_tests` pins the same vectors through the new `Path`/`DeriveExtKey` walker (mixed 31-bit/256-bit paths). The duplication is deliberate. - New files are listed in `test/util/data/non-backported.txt` so Dash-specific lint (cppcheck, clang-format-diff) covers them. ## How Has This Been Tested? Built with autotools on macOS (aarch64, depends prefix) from a clean tree. New/extended unit tests, all passing: - `platformkeys_tests` (22 cases): DIP-14 vectors 1-4 from dashpay/dips dip-0014.md through the path walker; public/private derivation consistency including hardened-step rejection; ECDH symmetry; descriptor root-source agreement; root-xprv-only derivation without a mnemonic; child-xprv rejection; chain-code-sensitive source identity; invalid mnemonic metadata not redirecting derivation; own friendship chain `ISMINE_SPENDABLE` with coins visible to `AvailableCoins`; contact chain `ISMINE_NO`; recovery rederivation of authentication keys, friendship xpubs/destinations, ECDH secrets and compact signatures; import-after-restore making pre-loss payments spendable; import idempotency without script-pub-key-manager duplication. - `descriptor_tests`: extraction of a unique root extended public key and recovery of its matching root extended private key from the signing provider, including ambiguity and mismatch rejection. - `walletdb_tests`: Platform data record write/prefix-query/erase and the `ReadKeyValue` load path. Also run locally: `dip14_tests` (sanity anchor for dashpay#7511 interplay) plus `wallet_tests`, `scriptpubkeyman_tests`, `ismine_tests`, `spend_tests`, `availablecoins_tests`, `coinselector_tests`, `descriptor_tests` — all green. `test/lint/all-lint.py` passes. ## Breaking Changes None. New wallet records are additive and ignored-by-absence; no existing serialization changes. Enabling the secp256k1 ECDH module only adds symbols to the static subtree library. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ Top commit has no ACKs. Tree-SHA512: 7194093d963190e7a70c16eceba6d01a2664d688b4fe7bcddb3ada4dc08a234fc9d20d35791f6996a2fa4a700d82d26b0f123138a2325ccdd8d461405a067b77
d1fca89 fix(coinjoin): decide the pool state from one consistent snapshot (pasta) adb2765 fix(coinjoin): bound the sides of a post-V24 DSTX against each other (UdjinM6) 26d0a98 fix(coinjoin): tell participants when an uncovered session is reset (UdjinM6) 123858d fix(coinjoin): don't burn queue announcements a rebalance pass skips (UdjinM6) f232f02 fix(coinjoin): keep the V24 tip-skew tolerance at one block (pasta) e6687d9 fix(coinjoin): tolerate arbitrary tip skew when validating a final transaction (pasta) 3646c95 fix(coinjoin): tolerate DSTX tip skew from V24 lock-in, and address review feedback (pasta) 0dc132f docs: describe rebalance session gating and DSTX downgrade accurately (pasta) 5748316 refactor(coinjoin): use static_cast in new promotion/demotion code (pasta) e78b965 test(coinjoin): cover session finalization race and gap-threshold boundary (pasta) 6af461c fix(coinjoin): scale the rebalance gap threshold with the denoms goal (pasta) 428eb97 fix(coinjoin): recognize unbalanced mixing transactions as denominated (pasta) 032bc99 fix(coinjoin): widen the V24 DSTX skew tolerance and downgrade withheld DSTXes (pasta) 1ff74e9 fix(coinjoin): latch rebalance capability on admission, not on creator version (pasta) f7d2bbb fix(coinjoin): bind entry admission and charging to the validated session (pasta) 50c62c9 fix(coinjoin): annotate m_fRebalanceSession as GUARDED_BY(cs_coinjoin) (pasta) 9727337 fix(coinjoin): keep relaying islocks when withholding a DSTX from legacy peers (pasta) 88d4e6b fix(coinjoin): guard against null prevtx in GetRealOutpointCoinJoinRounds (pasta) 9e83d6a fix(coinjoin): don't announce oversized balanced DSTXes to legacy peers (pasta) 9cae6f4 feat(coinjoin): reset mixing rounds on promotion/demotion outputs (pasta) efff25c fix(coinjoin): only announce unbalanced DSTXes to peers that support them (pasta) 363d305 fix(coinjoin): tolerate one-block tip skew only at the V24 boundary (pasta) 9c29052 feat(coinjoin): gate rebalance sessions and require session-denom cover (pasta) 33702b4 docs: add release notes for pr 7052 (Pasta) 3d6d49a test: cover CoinJoin promotion/demotion validation and decision logic (Pasta) 49d7d5f feat: coinjoin promotion / demotion (Pasta) Pull request description: ## Summary This PR adds CoinJoin denomination promotion and demotion so participants can convert between adjacent standard denominations inside a mixing session instead of being limited to strict 1:1 denomination mixes. The new behavior is gated by V24 activation. Pre-V24 behavior remains unchanged. ## What was done? - added promotion support for 10 smaller-denomination inputs to 1 next-larger-denomination output - added demotion support for 1 larger-denomination input to 10 next-smaller-denomination outputs - updated CoinJoin client, server, wallet, and validation flow to build, accept, and verify promotion/demotion entries - updated DSTX structural validation so post-V24 sessions can accept valid unbalanced promo/demo transactions while preserving pre-V24 1:1 rules; input and output counts are capped independently post-V24 - enforced a side-coverage invariant before a session may complete: each side of the session denomination must be occupied by nobody or by at least two participants, since coins are only concealed by other coins of the same size on the same side. A lone promoter or demoter on a side would have the only coins of that size there and be trivially identifiable on-chain. Note that this permits rebalancers to cover each other (e.g. two promoters and no standard mixers); there is deliberately no separate standard-mixer minimum. Sessions whose received entries can no longer cover both sides reset immediately instead of stalling until timeout - rebalance inputs are locked when selected and released on every failure path (queue-join failure, connect failure, entry-preparation failure, session reset) - pre-V24, unbalanced DSVIN entries keep flowing to `AddEntry` → `IsValidInOuts` so their collateral is consumed as before (anti-spam behavior preserved) - conversions only spend fully-mixed coins (both directions; demotion's ready-to-mix fallback removed) and their outputs start mixing over at 0 rounds: the conversion's public 10:1 shape clusters one participant's coins even inside a mixing transaction, so a converted coin is not treated as mixed — it re-enters mixing at its new denomination and disperses normally, while the histories of the fully-mixed coins that fed it remain protected. Implemented in `GetRealOutpointCoinJoinRounds` (0 rounds for an output whose own inputs in the same tx are at a different denomination); the rule is inert pre-V24 and needs no activation gating - extracted the final-transaction aggregate composition check into `CoinJoin::ValidateFinalTxComposition()` and covered it directly in unit tests - expanded unit coverage around structure validation, expiry logic, promotion/demotion entry validation, final-tx composition, and standard-entry privacy checks ### Protocol-version gating of rebalance sessions - bumped `PROTOCOL_VERSION` to 70241 and added `COINJOIN_REBALANCE_VERSION`; the `dsa` message gained a flags field declaring promotion/demotion intent, serialized only between peers that both negotiated ≥ 70241, so the wire format toward older peers is byte-identical and DSQ messages are untouched - each mixing session's rebalance capability is fixed at creation from the creator's negotiated protocol version; older clients are rejected from rebalance-capable sessions at `dsa` time with `ERR_VERSION` (a message ID old releases already understand, delivered before any collateral is committed) — this prevents pre-70241 wallets from ever facing an unbalanced final transaction they cannot validate, which they would refuse to sign at the risk of losing their collateral to `ChargeFees`. Old clients keep mixing in sessions created by old peers, which new clients still join for standard mixing - the masternode records the direction each participant declares in its `dsa`, and `IsSessionReady` holds the session in queue until the declared shapes cover both sides of the session denomination; a session that never attracts the missing counterparty times out fee-free in queue state. Because admission relies on the declarations, every entry must match its participant's declared direction exactly — a deviating entry (e.g. declaring a promotion, then submitting a standard entry, which could strip a side of its declared cover and force a fee-free reset for everyone) has its collateral consumed - clients refuse to sign a post-V24 final transaction without sufficient foreign cover at the session denomination on whichever side they occupy, preventing a malicious masternode from finalizing a pool that would publicly link a promotion participant's 10 fully-mixed inputs to a single output - final-tx validation on the client tolerates a masternode whose tip is one block ahead at the V24 activation boundary (it also accepts V24 activating in the block following the local tip), so tip skew at the boundary cannot cost an honest client its collateral; per-entry validation on the masternode keeps using the strict tip state - unbalanced (promotion/demotion) DSTXes are only announced to peers at protocol ≥ 70241: pre-70241 software treats them as structurally invalid, drops them and penalizes the relayer by 10 per DSTX, which would gradually get honest relayers discouraged by old peers — and the zero-fee transaction can't enter old mempools anyway. Older peers see the transaction on block inclusion instead. Balanced DSTXes keep relaying to everyone, so standard mixes retain their zero-fee propagation ## History The branch is rebased onto current `develop` (dashpay#7507, which it previously depended on, has since merged, so its commit is no longer carried here). Commits: feature, tests, release notes, and protocol-version gating from earlier review rounds, plus follow-ups from review: declared-direction enforcement with collateral consumption, activation-boundary tip-skew tolerance, release-note clarifications, version-gated announcement of unbalanced DSTXes, promotion input selection aligned with the standard selector (spendable-only, shuffled, at most one coin per parent transaction — so a demotion's 10 sibling outputs are never promoted together as an identifiable group), `nFlags` in `CCoinJoinAccept` equality, and the rounds reset for conversion outputs described above. ## How Has This Been Tested? - `./src/test/test_dash --run_test=coinjoin_inouts_tests` (including coverage for version-gated `dsa` serialization, mix-shape classification, and the side-coverage invariant), `--run_test=coinjoin_tests`, and `--run_test=net_tests` pass - new `coinjoin_rebalance_rounds_reset_tests` in the wallet suite exercises `GetRealOutpointCoinJoinRounds` directly: standard 1:1 mixing advances rounds, promotion/demotion-shaped transactions reset their outputs to 0, and a promoted coin advances normally when re-mixed - `dashd` and `test_dash` build cleanly - lint: whitespace, logs, format strings, circular dependencies, python pass - `P2P_VERSION` in the functional-test framework is bumped in lockstep with `PROTOCOL_VERSION`, so functional tests can exercise the new gating; an old client is now cleanly emulatable with a 70240-advertising `P2PInterface` Post-V24 activation behavior (including the server-side admission/entry-enforcement paths) still needs functional coverage because EHF activation paths cannot be fully exercised in these unit tests alone. ## Breaking Changes None. The feature is activation-gated and preserves existing pre-V24 behavior. The protocol version bump to 70241 is backward compatible: older peers keep the previous `dsa` wire format and are only excluded from sessions that could contain entries they cannot validate. Top commit has no ACKs. Tree-SHA512: bf0cedcd19e9ea7ffd3e4e8e371d5124c6f5d3f5565ddeaf515aa57caf8237bee74cef408b8e1b48659f9980418ea86cd1c271cd77836c3d6a35b17bf32724a2
…elated fixes 6f9a7ad fix: add one more module merge for circular linter: evo/providertx_util (Konstantin Akimov) ef7ba7b fix: add one more splitted module: qt/guiutil + qqt/guiutil_font (Konstantin Akimov) 7c1f653 refactor: absorb netinfo legacy-field helpers into the core_io module (Konstantin Akimov) 8e5805e docs: explain CDeterministicMN::ToJson() placement in rpc/evo.cpp (Konstantin Akimov) 520a669 refactor: dissolve rpc/evo_util.cpp into rpc/evo.cpp (Konstantin Akimov) bbeeeb3 refactor: dissolve governance/core_write.cpp into class home files (Konstantin Akimov) 99464cf refactor: move RPC help definitions out of core_io module into rpc/ (Konstantin Akimov) 6c67fc3 refactor: drop unused core_io.h include from evo/smldiff (Konstantin Akimov) f4e6aa2 test: merge multiple core_write instances to one module for circular dependency linter (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented Division of core_write to submodules in evo/, llmq/, governance/ split out evo, llmq, governance code and potentially is useful to reduce binaries size ; for instance the binary dash-tx links only libbitcoin_common + consensus/util so main core_write should not have governance/ code. Though, special rules for core_read and core_write has not been applied for sub-modules evo/core_write, llmq/core_write and governance/core_write. ## What was done? This PR is fixing processing of core_write module. It causes several new circular dependencies to appear which have been resolved by this PR: - drop unused core_io.h include from evo/smldiff - move RPC help definitions out of core_io module into rpc/json_help -- core_io has nothing to do with rpc - dissolve governance/core_write.cpp into class home files - dissolve rpc/evo_util.cpp into rpc/evo.cpp -- there's only 1 method left after 7600 - refactor: absorb netinfo legacy-field helpers into the core_io module ## How Has This Been Tested? Run `test/lint-circular-dependencies.py` ## Breaking Changes N/A ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ ACKs for top commit: PastaPastaPasta: utACK 6f9a7ad Tree-SHA512: a20ccc05139acaf5a72d30355de2fc351a932d2291260b7ebecaeff8b9728e78c9daf7d167a1e59fb6e1d07b8f3162ddd0781ad45131ea8cb637f5573683fec0
Develop merged the reworked wallet seams (dash#7581): Platform key access is now the typed PlatformKeyResult/PlatformKeyRequest API, the getFriendshipXpub + importFriendshipKeychains pair became the atomic ensureFriendshipReceivingKeychain, contact payment addresses derive via the free DeriveFriendshipPaymentDestination, and platformseed.{h,cpp} plus the seed-id pin are gone (derivation now starts at the descriptor root key).
Adapt the composite-only code to that surface: migrate the qt/platform callers off PlatformKeyType/out-param calls, fold the friendship keychain derive+import pair into one ensureFriendshipReceivingKeychain call, and rebuild payment cursors with the public-math helper. Drop the legacy_wallet_has_no_platform_seed unit test: its subject (GetPlatformSeed) no longer exists and develop's legacy_wallet_has_no_platform_keys covers the descriptor-only policy at the surviving surface.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
The composite reference implementation of Dash Platform usernames, profiles, and DashPay contacts in dash-qt, assembled from the current train foundations (tracked in dashpay#7512). It proves the whole path end-to-end — Platform crates, Core build, wallet seams, GUI — while the individual train PRs are reviewed and merged. It is an integration branch, not a merge candidate.
Branch structure (bottom-up)
feat/optional-rust-components) — opt-in Rust + cxxbridge build foundation, validated-host target set, and Guix-baseline coverage.develop.dashpay/wallet-seams) — DIP-13/14/15 key provider, opaque Platform data records, fail-closed Platform-seed pinning, and friendship-keychain import with preserved descriptor state. Includes the cross-implementation derivation vector against rust-dashcore's key-wallet for mobile parity.Platform ownership and Core integration
--enable-platform-guino longer builds an in-treerust/platformcrate. Core'sdependssystem pins Platform dashpay#4416 commitdf4fdb68559ef57d50624b7f0841594aef8647e5, vendors its standalone Cargo dependency graph, builds it offline, and installs this interface into the target prefix:include/dash/platform/ffi.hinclude/dash/platform/signer.hinclude/dash/platform/src/lib.rs.hinclude/rust/cxx.hlib/libdash_platform_cxx.aCore includes and links only that installed interface. Its duplicated Platform Rust crate, generated bridge build, and signer header have been removed. The Platform archive is linked only into dash-qt and the Platform-gated test/fuzz binaries; dashd, dash-cli, dash-tx, wallet libraries, and default builds remain free of Platform code.
Core's independent chirp smoke component remains available through
--enable-rust.--enable-rustand--enable-platform-guiare intentionally mutually exclusive because separately built Rust static archives each contain Rust and CXX runtime symbols; linking both into one binary is non-portable and produced duplicate runtime definitions.Platform dashpay#4416 is currently stacked on dashpay#4389 and assumes Platform dashpay#4388/dashpay#4389 merge. After that merge, dashpay#4416 should be rebased/retargeted to
v4.2-dev; if its commit changes, this branch's depends pin and source hash must be refreshed together.Validation
PLATFORM_GUI=1dependency build succeeds from the pinned Platform source and vendored archive.--enable-rust --enable-online-rust --disable-platform-guifull build passes.platform_drive_tests(5),platform_dpp_tests(9), andplatformkeys_tests(23) pass.Known follow-ups