Skip to content

feat(qt): Dash Platform usernames, profiles, and DashPay contacts in dash-qt#49

Draft
PastaPastaPasta wants to merge 33 commits into
developfrom
claude/dash-platform-usernames-ui-bbe2b0
Draft

feat(qt): Dash Platform usernames, profiles, and DashPay contacts in dash-qt#49
PastaPastaPasta wants to merge 33 commits into
developfrom
claude/dash-platform-usernames-ui-bbe2b0

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Issue being fixed or feature implemented

Dash-QT has long lacked first-class Dash Platform support: usernames (DPNS), DashPay profiles, and contact-to-contact payments — the feature set of the DashPay mobile wallets — without coupling Dash Core to Platform. This PR adds that as a compile-time-optional GUI feature (--enable-platform-gui, default off): dashd and default builds contain zero Platform symbols and are behaviorally unchanged.

What was done?

Pure C++ Platform client library (src/platform/, linked only into dash-qt):

  • gRPC-Web/HTTP1.1/TLS (mbedtls) transport over evonode endpoints taken from the locally synced deterministic masternode list, with ban/backoff and cross-node retry.
  • Hand-rolled protobuf codecs for the ~15 DAPI Platform messages used, bincode-v2 codec, DPP identity/document/data-contract decoding, and state-transition construction/signing (identity create, DPNS preorder/domain, DashPay profile and contactRequest).
  • A GroveDB proof verifier ported 1:1 from grovedb v5.0.0's storage-free verify slice (merk op-stack replay over blake3, layered proofs, absence proofs) plus tenderdash sign-digest reconstruction, checked against the locally synced LLMQ Platform quorum keys — every query uses prove=true; there is no trusted intermediary.
  • Correctness is pinned by Rust-generated fixtures (PastaPastaPasta/dash-platform-test-vectors) reproduced by C++ unit tests (src/test/platform_*_tests.cpp), including negative tests.

Wallet/node seams (additive):

  • DIP-13/14/15 key derivation (256-bit hardened child indices), sign-only interfaces (raw keys never cross into GUI code), asset-lock transaction creation, generic per-wallet platform data records, and DIP-15 friendship receiving-keychain import as a ranged descriptor.
  • The contact's own receiving chain is deliberately never imported — its scriptPubKeys must not be IsMine, or payments to the contact decompose as payments-to-self. Contact destinations are derived statelessly from the stored xpub instead.
  • DescriptorScriptPubKeyMan::IsMine now reports public-only descriptors in signing wallets as watch-only rather than spendable.

GUI (src/qt/platform/):

  • A DashPay tab: username registration wizard (live proof-backed availability checks, contested-name warnings, resumable progress; the flow persists in the wallet DB and survives restarts), dashboard with profile header and proof-verified credit balance, pending-request banner, and a contacts list with accept/pay actions.
  • Contact round-trip: ECDH-encrypted friendship xpub exchange per DashPay contract, with Drive-proof confirmation of both directions.
  • Send-to-username: the Send tab resolves usernames to fresh DIP-15 addresses with inline verification state, a contact picker, and consistent "user (DashPay)" address-book labels so both directions of contact payments are attributed in transaction history.

How Has This Been Tested?

  • Full E2E against live testnet Platform (DAPI v4.0.0, harness: PastaPastaPasta/dash-platform-e2e) using only the GUI's own builders and transport: asset lock → identity create → DPNS preorder → domain → resolve (identity 6ed0631a…be8, username qte2e8df49727), independently re-confirmed with prove=true GroveDB proofs from unrelated evonodes; plus a two-wallet contact request round-trip and send-to-username exercised interactively on testnet.
  • make check including the four new platform_* suites driven by Rust-generated vectors; test_dash-qt passes.
  • Builds clean both with and without --enable-platform-gui; without the flag, dashd/dash-qt contain no platform symbols.
  • Lint: lint-circular-dependencies, lint-whitespace, lint-includes clean; new Dash-specific paths added to non-backported.txt.

Breaking Changes

None. The feature is compile-time optional and off by default; consensus, dashd, and wallet behavior are unchanged when disabled. New wallet interface methods have stub implementations when the flag is off.

Checklist:

  • 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)

🤖 Generated with Claude Code

PastaPastaPasta and others added 17 commits July 10, 2026 01:13
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>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ccbbb2a4-33f4-460d-8568-3a0780ea01a9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/dash-platform-usernames-ui-bbe2b0

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

PastaPastaPasta and others added 12 commits July 10, 2026 19:06
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>
PastaPastaPasta and others added 4 commits July 11, 2026 00:13
…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>
The chunked-transfer decoder required the entire chunk-size line to be
hexadecimal, rejecting valid lines that carry chunk extensions such as
"1a;ext=value" (RFC 9112 7.1.1). Parse only the hex prefix up to any ';'
and ignore the extension.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cppcheck (variableScope, an always-enabled Dash lint category) flagged the
per-event winner Identifier in the contested-vote stored-info decoder. It is
parsed only to advance the reader — the authoritative winner comes from the
poll status field — so move it into the branch that reads it. Unblocks the
lint job the platform-gui CI build depends on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n test

The dpns_domain test case iterated a brace-init-list of string literals with
a 'const std::string&' loop variable, binding the reference to a temporary
std::string constructed from each 'const char*'. This is legal C++ but GCC's
-Wrange-loop-construct flags it (clang does not), and the CI build uses
-Werror. This is the first CI job to actually compile platform_dpp_tests.cpp
under --enable-platform-gui, which is why it was never caught before. Loop
over 'const char*' instead; UniValue::operator[] already takes the implicit
std::string conversion at the call site, so behavior is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant