feat(qt): back Dash Platform GUI internals with real grovedb/drive/dpp crates - #67
feat(qt): back Dash Platform GUI internals with real grovedb/drive/dpp crates#67PastaPastaPasta wants to merge 96 commits into
Conversation
Introduce the build and UI skeleton for Dash Platform (usernames / DashPay) support in dash-qt, keeping dashd and consensus code fully independent of Platform: - new configure flag --enable-platform-gui (default off), gating everything - depends: new optional mbedtls package (PLATFORM_GUI=1), for the future DAPI TLS client; config.site auto-enables the flag - vendor BLAKE3 1.8.5 (portable C only) under src/crypto/blake3/, used exclusively for GroveDB merk proof verification in the GUI client library - new src/platform/ Qt-free client library (libdash_platform.a, linked into dash-qt and test binaries only) seeded with per-network parameters and the well-known DPNS/DashPay system contract ids - new DashPay tab (placeholder page) wired through BitcoinGUI/WalletFrame/ WalletView following the Masternodes tab pattern, with an OptionsModel ShowPlatformTab toggle and options dialog checkbox; the tab is only offered on networks where Platform is deployed - lint/non-backported list updates for the new Dash-specific paths Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Dash Platform HD key derivation to the wallet, exposed to the GUI through new interfaces::Wallet methods that never hand out raw private keys (sign/pubkey/ECDH only, precedent: signSpecialTxPayload): - DIP-14 (256-bit child index) derivation: CKey::Derive256 / CPubKey::Derive256 + DIP14Hash, with BIP32 compatibility mode for sub-2^32 indexes; verified against all four official DIP-14 test vectors - wallet/platformkeys: DIP-13 identity authentication and funding paths (m/9'/coin'/5'/...) and DIP-15 friendship paths (m/9'/coin'/15'/account'/idA/idB, ids non-hardened per dashj), path walking from the wallet BIP39 seed (legacy CHDChain and descriptor wallets), and ECDH secrets using the libsecp256k1 KDF (matches dashj KeyCrypterECDH used for DashPay contact request encryption) - enable the vendored secp256k1 ECDH module when --enable-platform-gui - interfaces::Wallet: getPlatformPubKey, signPlatformDigest, platformECDHSecret, getFriendshipXpub (stubbed when the feature is compiled out); seed access follows the getMnemonic precedent and respects wallet encryption/locking Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wallet: interfaces::Wallet::createAssetLockTransaction builds a signed TRANSACTION_ASSET_LOCK special transaction (single OP_RETURN burn output carrying the credit amount, CAssetLockPayload with one P2PKH credit output) funded via CreateTransaction and signed as a whole, ready for commitTransaction. This is the L1 leg of Platform identity funding. Node: expose locally synced data the GUI Platform client needs for verification and asset lock proofs: - LLMQ::getPlatformQuorums(llmq_type): active quorum hashes + BLS public keys (basic scheme) so Platform state-root quorum signatures can be verified against the node's own quorum list (no external trust source) - LLMQ::getInstantSendLock(txid): serialized islock for building InstantAssetLockProofs - MnEntry::getPlatformHTTPSAddrs(): evonode DAPI gateway endpoints from the extended masternode address list Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Qt-free contract between the DAPI transport implementation and the GUI service layer: async PlatformClient (every query issued with prove=true and verified before callbacks fire; endpoints from the deterministic MN list, quorum keys injected from the node) and the plain structs the GUI consumes (Identity, DpnsName, Profile, ContactRequest, contested name state, broadcast results). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…builder contract - Wallet DB: new DBKeys::PLATFORM_DATA generic key/value records (opaque to the wallet, travel with backups) with CWallet load/write/prefix-scan and interfaces::Wallet::writePlatformData/getPlatformData. The Platform GUI persists its identity/username/contact flow state here so multi-step flows survive restarts and are recoverable from backups. - src/platform/statetransitions.h: contract for DPP state transition construction (identity create from asset lock proofs, DPNS preorder/domain, DashPay profile/contactRequest), with signing delegated to wallet callbacks so keys never leave the wallet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure-C++ verification of DAPI GroveDB proofs, mirroring the storage-free
verify slice of dashpay/grovedb v5.0.0 and validated byte-for-byte against
vectors generated from the real Rust grovedb:
- serialize.{h,cpp}: bincode-v2 big-endian varint reader (+zigzag), LEB128
helpers, fixed-width BE readers, exception-free Result-style cursor
- proof/merk.{h,cpp}: blake3 node hashing (value/kv/kv_digest/node/combine,
sum variant), the full op-stack decoder and replay machine, and
query-completeness + absence-proof verification (a node cannot silently
omit matching results)
- proof/grovedb.{h,cpp}: Element decode (Item/Reference/Tree/SumItem/
SumTree + reference-path resolution), GroveDBProof V0/V1 envelope decode,
and recursive layered verification binding each subtree to its parent via
combine_hash(H(element), child_root)
- contrib/devtools/platform-test-vectors: Rust generator (developer tool,
not built by CI) emitting deterministic element/merk/grovedb vectors
- tests: 8 cases incl. ~2500 single-byte-flip forgery attempts, wrong-root,
truncation, and query-completeness attacks — all must fail verification
Also drop an accidentally committed generated moc file and gitignore it.
Notable finding: V0 tree-result proofs do not bind the tree element bytes;
the GUI must require V1 envelopes when a queried result is itself a tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure-C++ encode/decode of Dash Platform Protocol objects and the state
transitions the GUI broadcasts, pinned to dashpay/platform v4.0.0
(protocol version 12) and validated byte-for-byte against vectors from the
real Rust rs-dpp:
- dpp/bincode: bincode-v2 big-endian varint reader/writer (zigzag signed,
versioned-enum discriminants) matching rs-platform-serialization
- dpp/document: platform Value / document property encoding and the
document id derivation used by the four GUI document types
- dpp/identity: Identity / IdentityPublicKey decode into GUI types
- dpp/statetransitions: IdentityCreate (instant + chain asset lock proofs),
DPNS preorder/domain, DashPay profile create+replace and contactRequest;
signing delegated to wallet callbacks (double-SHA256 of signable bytes,
65-byte compact recoverable ECDSA, header 27+recid+4); plus salted domain
hash, homograph-safe label normalization, and the DPNS v1 contested-name
rule (normalizedLabel ^[a-zA-Z01-]{3,19}$)
- contrib/devtools/platform-dpp-vectors: Rust vector generator (dev tool)
- tests: byte-exact signable bytes/digests/serialized STs, document ids,
salted hashes, identity ids, identity decode + tamper negatives (9 cases)
Key correction over initial notes: DPP bincode is big-endian (same config
as the grovedb envelope), and batch transitions serialize as
BatchTransition::V1 / DocumentBaseTransition::V1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hand-rolled DAPI transport that needs no gRPC/HTTP-2/protobuf library: - transport/protobuf: minimal protobuf wire reader/writer for the DAPI Platform messages the GUI uses (field numbers from platform.proto) - transport/tls: blocking TLS client over mbedTLS 3.6 LTS; transport certs are intentionally not chain-verified (evonodes are reached by bare IP) — integrity comes from proof + quorum-signature verification of every response, matching the reference SDKs - transport/grpcweb: unary gRPC-Web calls over HTTP/1.1 (5-byte frame header + trailers frame), including de-chunking, as served by DAPI's Envoy gateway - transport/cbor: minimal CBOR writer for getDocuments where/order_by operands Validated against live testnet evonodes: getStatus and getIdentity (prove=true, returning a 2.3 KB GroveDB proof) both round-trip correctly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A developer harness that exercises the platform client against live Dash testnet Platform, end to end, using only the GUI's own state-transition builders and gRPC-Web transport: - faucet.py: fund a testnet address via the faucet's browser flow (Playwright) - make_assetlock.py: create + islock a TRANSACTION_ASSET_LOCK via RPC - e2e.cpp: asset lock -> IdentityCreate -> DPNS preorder -> domain -> resolve Verified on testnet (DAPI v4.0.0): registered identity 6ed0631afd75e846fd527761ca48c322553fece191bf2b96889f7ff6afdc7be8 with username 'qte2e8df49727', independently re-confirmed with prove=true from unrelated evonodes. This proves IdentityCreate/DPNS construction, signing and transport are correct against live Platform. Also fix identityflow to use the asset lock payload's credit-output index (0) rather than the OP_RETURN vout when building the asset lock proof. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Targeted decoders for the platform-serialized documents the GUI reads back: DPNS domain (label, normalizedLabel, records.identity), DashPay profile (displayName, avatarUrl), and contactRequest (toUserId, encryptedPublicKey). Extract the GUI-relevant fields over the known v1 property order; a full contract-schema-driven document deserializer is a documented follow-on. The DPNS decoder is validated against a real testnet domain document (extracts the correct resolving identity). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Complete the GUI-facing platform stack and wire it into dash-qt: - platform/drive: per-query verifiers (identity balance/revision/nonce/keys, full identity, id-by-public-key-hash) mirroring rs-drive verify functions, plus the Tenderdash quorum threshold-signature check binding a verified GroveDB root to a signed Platform block (BLS basic scheme via dashbls). Validated against vectors from the real Rust rs-drive + rs-tenderdash-abci. - platform/transport/client: the async PlatformClient — worker thread, evonode endpoint pool from the masternode list, quorum-key lookup wired to the node, and per-query verify+decode (identity reads are proof-verified; documents decoded via the DPP layer). - qt/platform: PlatformService (per-wallet orchestrator, marshals client callbacks to the GUI thread, feeds node context), resumable IdentityFlow and ContactFlow state machines persisted in wallet-DB records, the CreateUsername wizard, the DashPay dashboard page, and contact-request ECDH/AES crypto. Verified: dash-qt links the whole feature (553 platform symbols); dashd and the other binaries contain ZERO platform symbols — Core stays fully independent of Platform. All four platform test suites pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ness getIdentityKeys takes a KeyRequestType sub-message at field 2 (AllKeys) and prove at field 5, not prove at field 2 like the balance queries. With this fix the client's full-identity read verifies end to end. Add contrib/devtools/platform-e2e/readverify.cpp: proves the verified READ path against live testnet — transport fetches getIdentityBalance/ BalanceAndRevision/Keys with prove=true, the GroveDB proofs verify to one root, and the LLMQ_25_67 quorum BLS threshold signature binds that root to a signed testnet Platform block. Confirmed on the E2E-registered identity (balance 833498000 credits, 2 keys), completing the read direction to match the already-proven write direction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round out the DashPay GUI: - ContactsModel/ContactsPage: table of established contacts plus incoming and outgoing contact requests, refreshed from the DashPay contactRequest documents, embedded in the dashboard - UsernameSearchDialog: search-as-you-type over DPNS names to find and add contacts - ProfileDialog: view/edit the wallet's DashPay profile (display name, public message, avatar URL) and broadcast a profile state transition - PlatformService gains refreshContacts() and updateProfile() dash-qt builds with the complete feature; all four platform test suites pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-wallet PlatformService owns its IdentityFlow/ContactFlow state machines and the flows call back into the service — the same intentional cycle shape as qt/*tablemodel <-> qt/walletmodel. Add both to the expected circular-dependency list and remove a duplicated include. Verified the feature is optional: a full build WITHOUT --enable-platform-gui produces dashd and dash-qt with zero platform symbols. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t document decode Fixes found while running the live-testnet contact round-trip: - Identity nonce and contract-nonce queries now request prove=true and verify the Drive proof instead of trusting the scalar value path. - Drive proofs may be signed by an older Platform quorum that is still consensus-valid; export the full keepOldKeys retained-quorum window from the node instead of only the signing-active set, and match Proof.quorum_hash in its DAPI (display) byte order explicitly. - namesOfIdentity is now implemented (was stubbed to an empty result). - Stored-document decoding follows rs-dpp DocumentV0 serialize_v0/v1/v2 exactly (bincode varints, timestamp bitmap, presence markers for optional properties) instead of the previous approximate reader. - Add contested_identity_funding_amount (0.2 DASH vote reserve plus fee headroom) to the per-network parameters. - Extend the Rust vector generators and C++ unit tests to cover the new query and decode paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…estinations Adds the wallet seams DashPay contact payments are built on: - importFriendshipKeychains derives the local DIP-15 receiving chain (m/9'/coin'/15'/account'/sha256(myId)'/sha256(theirId)') and imports it as a ranged private pkh descriptor. The contact's own receiving chain is deliberately NOT imported: if its scriptPubKeys were IsMine, every payment to the contact would decompose as a payment-to-self showing only the fee. Contact payment addresses are instead derived statelessly from their xpub via getFriendshipPaymentDestination, with the per-contact cursor kept in wallet platform-data records. - DescriptorScriptPubKeyMan::IsMine reports ISMINE_WATCH_ONLY rather than ISMINE_SPENDABLE for public-only descriptors mixed into a signing wallet (descriptor watch-only and external-signer wallets keep their existing semantics), with unit test coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the contact round-trip in the GUI and reworks the DashPay
surfaces around how a novice actually uses them:
Send tab
- The recipient field accepts DashPay usernames: a permissive entry
validator (DPNS labels are not Base58-safe), debounced proof-backed
resolution, and a trailing status icon inside the field (pending /
verified / failed with tooltip) so the form never reflows. Failed
lookups reuse the field's invalid-input treatment.
- An "@" tool button opens a picker over established contacts; the
DashPay dashboard and contacts list can also prefill the Send tab.
- Sends and receives share one address-book label ("user (DashPay)"):
the service labels the resolved destination and the first receiving
addresses, and the Label field mirrors the address book instead of
inventing its own value, so both directions match in history.
Contact flow
- Accepting a request resolves the sender's identity key, decrypts the
ECDH-encrypted friendship xpub, imports the receiving keychain,
reciprocates, and confirms via a proven re-query.
- Outgoing requests are confirmed against Drive before being reported
as sent.
DashPay tab
- Dashboard header with generated avatar, username, profile line, and
proof-verified credit balance; quick actions (send to contact, find
people, edit profile); a banner for pending incoming requests.
- Registration re-entry: the wizard reopens on its live progress page
mid-flow, and a failed registration resets and restarts from name
entry ("Try again").
- Contacts list with plain-language colored statuses, tooltips,
contextual accept/pay actions, double-click to pay, and empty-state
guidance. Username search annotates results that are yourself,
already contacts, or already invited.
- Profile loading distinguishes verified absence ("No profile yet")
from lookup failure (auto-retry) instead of showing a permanent
loading state.
- A new AmountTemporarilyUnavailable send error distinguishes
"waiting on pending funds" from a plain insufficient balance, and
UnlockContext gained a move constructor for the async accept path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Rust vector-generator crates (platform-dpp-vectors, platform-drive-vectors, platform-test-vectors) are development tools that are never built by Dash Core's build system or CI; keeping them in contrib/ pulled Rust tooling into the core tree for no benefit. They now live at github.com/PastaPastaPasta/dash-platform-test-vectors. The generated fixtures remain committed under src/test/data/platform/ and the unit-test references point at the new home. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Like the vector generators, contrib/devtools/platform-e2e is developer tooling never built by Dash Core's build system or CI. It now lives at github.com/PastaPastaPasta/dash-platform-e2e, which documents how to build its drivers against a dash checkout configured with --enable-platform-gui. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The merk and grovedb proof verifiers carried two verification branches no vector executed: right-to-left traversal (the mirrored bound-proving and terminate logic in ExecuteProof) and conditional subquery branches (RecursiveQueryItems / HasSubqueryOrMatchingInPathOnKey). Regenerate the fixtures with the extended platform-test-vectors crate (all pre-existing vectors reproduce byte-identically from the pinned grovedb v5.0.0 tag): - eight left_to_right=false merk vectors (present/absent keys, ranges, limits, sum trees) plus an RTL grove query through a layered proof; - three conditional-subquery grove vectors (conditional only, conditional with default branch, conditional with a subquery_path) whose per-byte corruption sweeps now exercise those code paths; - a merk_query_completeness_rtl case proving abridged data is still detected when traversing right-to-left; - ParseGroveQuery support for the conditional_subquery_branches fixture field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getContestedNameState was a stub returning an empty state, so an active masternode vote (or a locked name) was indistinguishable from an available name. Implement getContestedResourceVoteState end to end: - drive::VerifyContestedVoteState mirrors rs-drive's ContestedDocumentVotePollDriveQuery::construct_path_query (VoteTally with locked/abstaining tallies) and verify_vote_poll_vote_state_proof_v0 over the votes tree [[112], [0x63], [0x70], contract, doc type, [1], index values...], including the bincode decoder for the awarded/locked ContestedDocumentVotePollStoredInfo item; - the gRPC-Web client issues the request with prove=true, verifies the GroveDB proof and binds the root to a locally known platform quorum key before any result is surfaced; - drive_query_vectors gains active/finished/absent contest fixtures built from the same synthetic Drive layout, with the stored info serialized by the real rs-dpp (the whole fixture set regenerates because the votes subtree changes the synthetic root hash; quorum_sig_vectors re-sign the new balance root); - platform_drive_tests decodes all three fixtures and rejects corrupted proofs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Use the proof-verified vote state where the GUI previously only inferred contest status from domain-document absence: - name availability for contested labels now also requires a proven-absent contest, so a name mid-vote (or locked) no longer shows as available; - IdentityFlow::checkContestedOutcome fails the registration with a clear message when masternodes lock the name or award it to another identity, instead of polling forever; - the dashboard's contested banner shows live tallies (own votes, best rival, abstain, lock) via PlatformService::checkContestedNameState. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reader::Next computed m_pos + len for a length-delimited field where len is an attacker-controlled protobuf varint up to 2^64-1. On a malicious response this wraps and passes the bound check, then subspan() reads out of bounds — reachable by any on-path attacker since the gRPC-Web transport does not authenticate the TLS peer. Compare against the remaining byte count instead (m_data.size() - m_pos never underflows because m_pos <= size()), and use the same non-overflowing form for the fixed 32/64-bit reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IdentityFlow::save() ignored the wallet-DB write result, and start() broadcast the asset-lock funding transaction regardless. A DB write failure between deriving the funding key and committing the transaction left a funded identity flow with no resumable record, orphaning the asset lock from the GUI. save() now returns the write result and start() aborts before commitTransaction() when the pre-broadcast persist fails, leaving the wallet untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lopes Three related hardenings to the network verification path (TLS is unauthenticated by design, so these are reachable by any on-path attacker, not just a malicious evonode): - Root consistency: DoGetIdentity verified balance, revision, and keys as three separate DAPI proofs but never checked their GroveDB roots matched, so a peer could assemble one identity from independently valid states at different heights. Require the three roots to be equal (the invariant VerifyFullIdentity already implements); a transient block boundary makes the query retryable. - Freshness: signed proof metadata had no anchor to local chain state, so a stale-but-validly-signed response could be replayed. Add BindAndCheckFresh, which after the quorum-signature binding enforces (a) session-monotonic platform height and (b) a coarse core-ChainLock floor fed from Node::LLMQ::getBestChainLock via the new updateCoreChainLockedHeight seam. - V1 envelopes: the lenient V0 GroveDBProof format does not bind the serialized element bytes of a non-empty tree returned without a subquery, letting a downgraded proof forge those bytes under the signed root. Current Platform only emits V1, so the Drive query chokepoint (RunQuery) now rejects V0. The V0 decoder is retained for the proof-level unit tests, which assert the reported envelope version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sendcoinsentry.h and sendcoinsdialog.h gate members on ENABLE_PLATFORM_GUI without including config/bitcoin-config.h, so MOC (which does not otherwise see the define) omitted on_contactsButton_clicked from the meta-object. The "@" contacts button's auto-connected slot never fired, and the class layout differed between the MOC translation unit and normal compilation. Add the config include, matching bitcoingui.h / walletview.h / walletframe.h. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make test/lint/all-lint.py pass on the platform feature: - lint-locale-dependence: replace std::tolower (ASCII fold), std::atoi (LocaleIndependentAtoi), std::to_string (ToString), and std::stoul (std::from_chars, base 16) in the gRPC-Web/TLS transport with locale-independent equivalents. - lint-qt-translation: move leading whitespace outside tr() in the username search results. - cppcheck: avoid the implementation-defined signed shift in the bincode zigzag encoder (equivalent sign-extension mask) and rename a shadowing local. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No CI configuration exercised the optional Platform GUI, so the ~16k-line feature and its gated C++ test suites were never built or run. Add a dedicated flag-on native target (linux64_platform_gui) that builds depends with PLATFORM_GUI=1 (pulling in mbedtls), configures with --enable-platform-gui --with-gui=qt5, and runs the platform_* unit tests (functional tests are off — the feature has no dashd-only surface). Wired as depends/src/test jobs in build.yml, gated by SKIP_LINUX64_PLATFORM_GUI like the other targets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GroveDBProof V1 requirement takes effect at Platform protocol version 12 (drive v7 -> grovedb GROVE_V3), not protocol 4 as the comment said. No behavior change; both testnet and mainnet run well past protocol 12 so the gate is a no-op for real proofs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…point Two availability regressions from the earlier freshness/root-consistency hardening, both caused by round-robin endpoint selection interacting with a global check: - DoGetIdentity issued its balance/revision/keys sub-proofs to three different round-robin endpoints, then required all roots to match. Honest nodes a block apart produced different roots and the whole query failed; retrying just picked another three nodes. It now pins a single endpoint per attempt (transport::RetryAcrossEndpoints + CallOn) and retries the whole logical operation against another endpoint on failure, bounded by MAX_OP_ATTEMPTS. - The freshness watermark was a single session-global maximum platform height, so a lagging honest node was rejected for being one block behind the fastest node seen. It is now per-endpoint (transport::FreshnessTracker keyed by proTxHash/address): a node may not roll its own height backwards (the replay we want to catch), while honest cross-node lag is tolerated. Single-call operations thread the answering endpoint's key through. Freshness comments are corrected to describe a best-effort staleness bound (per-endpoint monotonic height + a ~288-block core-ChainLock floor, zero on cold start), not the previously-overstated "airtight" anti-rollback. Adds platform_client_tests covering the per-endpoint watermark (lag tolerated, same-node rollback rejected, core floor) and the retry driver (round-robin rotation, single-endpoint pinning per attempt, bounds) — the availability paths the proof vectors cannot reach. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
We're using `depends` to provide us with the Rust compiler and `cxxbridge`, we can drop it from the CI container and save some space.
The conflict occurs due a difference in glibc version between the Guix environment and the targets compiled, this is only a problem for Linux targets so the issue doesn't extend to Windows
…ging Merging per-crate staticlibs with 'ar -x' into a flat directory silently drops object files once member basenames collide, which happens as soon as two crates share dependencies (measured: 373 of 374 members collide between two crates built from one workspace). Replace the merge with an umbrella staticlib crate, rust/dashrust, that depends on each component as an rlib: cargo deduplicates shared dependencies, applies LTO across the whole graph, and emits a single archive. Component crates keep their own cxx bridges and codegen; per-crate Makefile includes now contribute only cxxbridge artifacts. Optional components become cargo features on the umbrella crate, plumbed from configure via RUST_CRATE_FEATURES. Per-crate dist-hook recipes are replaced by an aggregate dist-hook so additional crates do not collide on automake's one-recipe-per-Makefile rule.
The dashpay/platform v4.1.0 workspace declares rust-version 1.92; rustc 1.85.1 refuses nine of its crates by name. Hashes regenerated with contrib/devtools/update-rust-hashes.py. cxx/cxxbridge stay at 1.0.192, which compiles unchanged under 1.92.
The offline cargo configuration was generated into the source tree, which pollutes srcdir and races when multiple out-of-tree builds for different hosts share one checkout. Cargo discovers .cargo/config.toml by walking up from its invocation directory, so a config under the build tree's root works for both in-tree and out-of-tree builds.
cargo vendor handles git dependencies, but consuming the vendor directory offline requires per-git-source replacement stanzas in .cargo/config.toml, which the build system previously did not generate (only crates.io was redirected). Derive the stanzas deterministically from Cargo.lock with contrib/devtools/cargo-vendor-git-sources.sh — output verified byte-identical to what cargo vendor itself prints — and append them when generating the offline config. Git dependencies are unavoidable for upcoming components: crates.io copies of the dashpay/platform crates are stale or name-squatted.
…i-bbe2b0' into platform-gui-rust # Conflicts: # .gitignore # configure.ac # src/qt/sendcoinsdialog.h # test/lint/lint-circular-dependencies.py # test/lint/lint-include-guards.py # test/lint/lint-locale-dependence.py # test/lint/lint-whitespace.py # test/util/data/non-backported.txt
Rust components exist to serve the Platform GUI, so tie the whole Rust track to that flag: the rust/ subdir, the cxxbridge convenience library, and the umbrella staticlib are built and linked only into dash-qt and the test binaries when ENABLE_PLATFORM_GUI is on, via the new ENABLE_RUST conditional and the 'platform' cargo feature. dashd, dash-cli, dash-tx, dash-wallet, dash-util, bench and libdashconsensus no longer link any Rust object, configure requires cargo/rustc/cxxbridge only when the flag is enabled, and a no-flag build never invokes cargo. The chirp smoke call is dropped from init.cpp accordingly (dashd is Rust-free by design now).
New rust/platform crate exposing, through a cxx bridge, the pieces of the Dash Platform client that src/platform/ hand-implements in C++: drive proof verification for every DAPI query the GUI makes (identity balance, revision, nonce, contract nonce, keys, identity-by-public-key-hash, DPNS and DashPay document queries, contested vote state), DPP identity/document decoding, and state-transition construction and signing (DPNS preorder/domain, profile create/replace, contact request, identity create with instant and chain asset-lock proofs). Pinned to dashpay/platform v4.1.0 and grovedb v5.0.1, matching drive's verify feature: no storage engine and no async runtime. Wallet keys never cross the bridge: builders call back into a C++ platform_ffi::WalletSigner (src/platform/ffi/signer.h) with the key id and the 32-byte double-SHA256 digest of the signable bytes, receiving a 65-byte compact recoverable ECDSA signature, the exact contract of PR 49's platform::st::Signer / interfaces::Wallet::signPlatformDigest. Acceptance is pinned by PR 49's Rust-generated fixtures replayed through the crate: all 15 drive query vectors verify with matching root hashes and results, decoders are field-exact, and all 7 state-transition fixtures in dpp_st_vectors.json serialize byte-identically to the C++ implementation. cxx/cxxbridge is bumped 1.0.192 -> 1.0.198 across the manifests and the native_cxxbridge depends package (hash + vendored lockfile regenerated) so the crate pin, workspace lock, and depends-built CLI agree exactly.
src/platform/'s hand-rolled internals now delegate to the dash-platform-ffi
crate: drive/queries.cpp, dpp/identity.cpp, dpp/document.cpp and
dpp/statetransitions.cpp become thin adapters over the platform_ffi cxx
bridge, converting arguments, catching rust::Error into the existing
Result-with-error-string style, and routing wallet signing callbacks through
platform_ffi::WalletSigner (per-key dispatch with the ASSET_LOCK_KEY_ID
sentinel for identity creation). Public headers are unchanged: the GUI,
transport and quorum-signature code compile untouched, and the surviving
platform_drive/dpp/client test suites now drive the original JSON vectors
through the C++-to-Rust round trip (31 cases, all passing).
Superseded C++ removed (net -5,808 lines):
- src/platform/proof/{merk,grovedb}.{h,cpp} - the layered merk/GroveDB proof
verifier, replaced by rs-drive's verify slice; the Hash256 alias the drive
layer still uses moved to src/platform/drive/hash256.h
- src/crypto/blake3/ - vendored portable-only blake3; grovedb's own blake3
now runs inside the Rust staticlib, and the -DBLAKE3_* defines left
libdash_platform_a_CPPFLAGS with it
- src/platform/dpp/bincode.{h,cpp} - bincode-v2 reader/writer; rs-dpp
serializes on the Rust side now
- platform_value machinery in dpp/document.{h,cpp} (Value, ValueKind,
EncodeDocumentData, GenerateDocumentId, DecodeDocumentHeader) - rs-dpp
decodes documents against the pinned system contracts
- BytesReader and the bincode/LEB128 read helpers in serialize.{h,cpp};
only WriteLEB128 survives for the Tenderdash StateId sign bytes
- src/test/platform_proof_tests.cpp and the merk/grovedb/element vector
files it consumed - that coverage lives in rust/platform/tests, which
replays the same drive/dpp/identity vectors through the real crates
The workspace panic profile flips abort -> unwind: verify_*/decode_* parse
attacker-controlled bytes from remote DAPI nodes deep inside grovedb/drive,
and with unwind cxx converts any residual panic into a catchable C++
exception at the bridge instead of aborting dash-qt. dashd links no Rust
either way.
rust/Makefile.am's all-local built the umbrella crate unconditionally, so a default build still invoked cargo even though nothing links the result and configure only requires the Rust toolchain under --enable-platform-gui. A cargo-less machine could not build dashd. Gate all-local and cargo-build on ENABLE_PLATFORM_GUI; clean targets stay unconditional.
Follow-ups from review of the Rust bridge swap: - WalletSigner::SignDigestForKey swallows exceptions from the wallet signer callback and reports signing refusal; it is invoked from Rust frames and a C++ exception must not unwind through them (unsupported by cxx). - Bridged() in drive/queries.cpp catches std::exception rather than only rust::Error, so cxx marshalling throws (e.g. rust::String rejecting invalid UTF-8 in contested-index values) surface as error strings too. - Drop element/grovedb_proof/merk_proof vector JSONs orphaned by the C++ verifier removal; their coverage lives in rust/platform's crate tests.
|
Important Review skippedToo many files! This PR contains 192 files, which is 92 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. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (192)
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 |
…dder-core Repoint dpp/drive/platform-version to dashpay/platform rev db332fe054 and add the new transport-free upstream layer (dapi-grpc types-only, dash-context-provider, dash-platform-queries, drive-proof-verifier) as dependencies. grovedb now resolves through drive's own pin instead of a direct tag. tenderdash-proto (via dapi-grpc/drive-proof-verifier) downloads the tenderdash sources from GitHub at build time, which breaks offline depends builds. Add a source-only depends package staging the sha256-pinned tenderdash v1.5.3 zip, exposed to configure as TENDERDASH_SOURCES and copied into the cargo target directory before the build so build.rs finds it in its download cache. Online dev builds (--enable-online-rust) keep downloading it themselves.
New rust/platform/src/provider.rs implements dash_context_provider::ContextProvider from node-local state: quorum BLS keys pushed across the bridge from the synced LLMQ store, the pinned DPNS/DashPay system contracts, and the network/activation-height context. verify.rs gains a FromProof-driven layer taking the exact protobuf (request, response) byte pair the transport exchanged; drive-proof-verifier reconstructs the query from the request, replays the GroveDB proof, and checks the Tenderdash BLS quorum signature internally, returning the signature-authenticated ResponseMetadata fields the C++ freshness tracker consumes. The old proof-only verification surface stays temporarily so the C++ side keeps building; it is removed together with the C++ seam rework. tests/from_proof.rs replays the fixture corpus end to end (grovedb + quorum signature) through the new seam, pins the clean decode failure for the placeholder document payloads, and covers tampered signature/proof/metadata and unknown-quorum rejection.
…pure builders st.rs now assembles the DPNS preorder/domain documents through dash-platform-queries' build_dpns_preorder_and_domain_documents and the DashPay contactRequest through build_contact_request_document, instead of hand-assembled property maps. The shared create path runs the upstream put-document guards (ensure_entropy_matches_document_id, prepare_document_for_transition) before signing. The preorder seam now takes (label, preorder salt) instead of the precomputed salted domain hash: the upstream builder derives the hash itself, and the bridge keeps using it as the document entropy, so the built transitions stay byte-identical to the pinned fixtures (rust signing tests and platform_dpp_tests both still check exact bytes). The domain seam drops its redundant entropy parameter (the preorder salt doubled as entropy on both sides already).
The C++ drive layer no longer reimplements the Tenderdash quorum-signature binding: transport/client.cpp threads the exact protobuf request it sent and the full response it received into the drive/queries.cpp adapters, and the Rust bridge (drive-proof-verifier FromProof) reconstructs the query, replays the GroveDB proof and checks the BLS quorum threshold signature against the keys pushed via updateQuorumKeys. The signature-authenticated ResponseMetadata (height, core-chain-locked height, time, chain id) comes back for the chain-id check and the per-endpoint freshness tracker; root hashes no longer cross the boundary. drive/quorumsig.* and drive/verify.* (the C++ ports of the signature preimage assembly) are deleted along with drive/hash256.h; the digest-intermediate vectors they were tested against are covered upstream by the rs-drive-proof-verifier corpus. getIdentity now issues a single proved request (Drive::verify_full_identity_by_identity_id) instead of three root-matched sub-queries, and getIdentityByPublicKeyHash resolves the full identity from one proof instead of chaining a second lookup. platform_drive_tests now drives the (request bytes, response bytes) seam: it synthesizes the DAPI protobufs around the fixture grovedb proofs and the fixture quorum signature, verifies the identity-nonce and contested-vote families end to end, pins the clean decode failure for the placeholder document payloads, and covers tampered signature/proof/metadata and unknown-quorum rejection. The old proof-only bridge surface and its rust drive_vectors tests are removed; tests/from_proof.rs covers the same pipeline on the Rust side.
Review follow-ups on the FromProof rework: the two flagship identity paths (verify_get_identity, verify_get_identity_by_pubkey_hash) had no coverage. The fixture corpus cannot positively test them - its pubkey-hash vectors prove only the unique-hash-to-id mapping while the verifier requires the full identity subtree, and no full-identity vector exists - so pin them negatively for now: an id-mapping-only proof and a wrong-shape proof must fail cleanly, never verify and never panic. Generating a full-identity fixture is flagged as vector-regeneration follow-up work. Also: deduplicate decode_identity through the ffi_identity helper and fix the stale endpoint_retry.h rationale that still described getIdentity as a multi-proof operation.
Issue being fixed or feature implemented
Parallel track to #49 (DashPay usernames/profiles/contacts in dash-qt). PR 49 is a pure-C++ implementation: it hand-ports grovedb v5.0.0's proof verifier, hand-rolls the bincode/DPP codecs, and vendors blake3 — ~6,300 lines of security-critical C++ that must be kept in lockstep with upstream Platform by hand.
This track keeps PR 49's GUI, wallet seams, and transport but backs the crypto/codec core with the real Rust crates from dashpay/platform (grovedb, drive, dpp), built via the Rust build infrastructure of dashpay#7109 — which also gives 7109 the concrete in-tree feature its review asked for. Rust and the platform crates are built and linked only when
--enable-platform-guiis set: a default build invokes zero cargo, requires no Rust toolchain, and dashd/dash-cli/dash-tx contain no Rust in any configuration.What was done?
Rust build infrastructure (7109 refresh + fixes, platform-agnostic):
kitty/rust_build(20 commits) onto develop; bumped the pinned toolchain 1.85.1 → 1.92 (platform's MSRV) and cxx/cxxbridge 1.0.192 → 1.0.198, hashes regenerated via the provided update scripts.ar -xstaticlib merge (object-name collisions at 2+ crates) with a single umbrella workspace crate (rust/dashrust) that links component crates as rlibs; optional components are cargo features driven by configure flags.cargo vendor's git source-replacement stanzas are generated into the offline config (contrib/devtools/cargo-vendor-git-sources.sh), so the dashpay/platform + grovedb git pins build--locked --offlinefrom the depends tarball. Needed because crates.io versions of dpp/drive/grovedb are stale or name-squatted..cargostate from srcdir to builddir; parameterized the per-crate dist hooks.rust/platform(dash-platform-ffi), pinned to platform v4.1.0 / grovedb v5.0.1:drivebuilds with only itsverifyfeature: no RocksDB, no async runtime, no tokio anywhere in the lockfile.WalletSignerwith a key id and the 32-byte double-SHA256 digest of the signable bytes, matching PR 49'splatform::st::Signer/interfaces::Wallet::signPlatformDigestcontract exactly (asset-lock key routed via au32::MAXsentinel).unwind, not 7109'sabort:verify_*/decode_*parse attacker-controlled bytes from remote DAPI nodes deep inside grovedb/drive, and with unwind cxx converts any residual panic into a catchable C++ exception at the bridge instead of aborting dash-qt.The swap (+498/−6,306):
src/platform/drive/queries.cppandsrc/platform/dpp/{identity,document,statetransitions}.cppare now thin adapters over the bridge; public headers are signature-identical, sosrc/qt/platform/, the transport, and the quorum-signature check compile untouched.src/platform/proof/), vendored blake3 (src/crypto/blake3/), the bincode-v2 codec, and theplatform_valuemachinery. Quorum BLS verification over the proof root stays in C++ (dashbls + locally synced LLMQ keys); Rust never sees quorum keys.doc/design/platform-rust-scope.md.How Has This Been Tested?
rust/platform/tests/, 32 cases).platform_drive_tests,platform_dpp_tests,platform_client_tests) drive the original JSON vectors through the full C++→Rust round trip: 31/31 cases, 497 assertions.makewith and without--enable-platform-gui; the no-flag build performs zero cargo invocations and dashd links no Rust symbols.make -C depends vendor-all-cratesproduces a 34 MB offline tarball from which the flag-on build resolves--locked --offline.std::exceptionhandling at the bridge boundary) are included.verify_*/decode_*with malformed-proof corpora.Breaking Changes
None. The feature is compile-time optional and off by default; default builds are behaviorally unchanged and gain no Rust toolchain requirement.
Checklist:
🤖 Generated with Claude Code
Update (2026-08-07): reworked onto the upstream refactor (dashpay/platform#4335)
Per maintainer direction ("split rs-sdk; refactor instead of duplicating"), the drift-prone Rust logic this branch previously carried in-tree has moved upstream as dashpay/platform#4335 (
feat/transport-free-embedder-core, 8 commits), and this branch now pins that revision. Five new commits here:rust/platformno longer hand-buildsDriveDocumentQueryshapes: verification is request-driven through upstreamFromProof/verify_documents_response, with the wire request decoded by the same shared code the server runs.ContextProvider(keys still served from the locally synced LLMQ store through the existingupdateQuorumKeyspush path; trust anchor unchanged). C++drive/quorumsig.*anddrive/verify.*are deleted.tenderdash-proto's build-time proto download is satisfied offline via a new hash-pinned depends source exposed asTENDERDASH_DIR.Known follow-ups: a full-identity proof fixture (the corpus can currently only pin
getIdentity/getIdentityByPublicKeyHashnegatively — clean rejection, no panic), the testnet E2E rerun, and a Guix determinism run.