|
| 1 | +# dash-platform-cxx |
| 2 | + |
| 3 | +Dash Platform for C++ embedders, as a thin [`cxx`](https://cxx.rs) shell over |
| 4 | +`dash-sdk`. The embedder supplies what only it knows: the evonode endpoints |
| 5 | +from its masternode list, the Platform quorum keys from its LLMQ store, its |
| 6 | +best ChainLock height, and signatures from its wallet. `dash-sdk` supplies |
| 7 | +everything else: query construction, transport, retries with address |
| 8 | +banning, proof verification, the signed-time window, protocol-version |
| 9 | +tracking, contact-request minting. Dash Core's platform GUI is the first |
| 10 | +consumer. |
| 11 | + |
| 12 | +## Trust model |
| 13 | + |
| 14 | +Every response byte comes from an untrusted node; nothing in the closure |
| 15 | +fetches from a trusted third-party service (no `rs-sdk-trusted-context-provider`, |
| 16 | +no HTTP client, no default seed list; CI asserts it). Trust rests on inputs |
| 17 | +the embedder pushes: |
| 18 | + |
| 19 | +- **Quorum keys** (`set_quorum_keys`, the full active set, hashes in the |
| 20 | + embedder's internal byte order). The provider (`src/provider.rs`) hands the |
| 21 | + verifier a key only if the proof names the network's Platform LLMQ type and |
| 22 | + a pushed hash; anything else fails signature verification. |
| 23 | +- **ChainLock anchor** (`set_chainlock_height`, monotonic). Until the first |
| 24 | + push no proved read is dispatched (`Unavailable`). A proof whose signed |
| 25 | + core-chain-locked height trails the anchor by more than 288 blocks is |
| 26 | + refused as stale; there is no ceiling, since a node one ChainLock ahead of |
| 27 | + the embedder is honest. |
| 28 | +- **Chain id** (`Config.tenderdash_chain_id`). After the SDK verified the |
| 29 | + quorum signature and its signed-time window, the shell (`src/ops.rs`) |
| 30 | + compares the signed `chain_id` (`ChainIdMismatch`), then applies its own |
| 31 | + monotonic Platform-height watermark (tolerance 3 blocks, `Rejected`), then |
| 32 | + records the verified protocol version the builders use, then flags a |
| 33 | + `protocol_version` above what this build knows |
| 34 | + (`UnsupportedProtocolVersion`, the value is still returned). The SDK's own |
| 35 | + height watermark is off and the shell keys its watermark and its verified |
| 36 | + version after the chain-id check, so a validly signed proof from another |
| 37 | + chain moves nothing the shell decides on (the SDK's internal version |
| 38 | + ratchet, which runs inside verification, may move upward on it; nothing |
| 39 | + reads that for building). |
| 40 | + |
| 41 | +Absence is proven, never inferred: `ProvenAbsent` comes only after the SDK |
| 42 | +verified a proof of it, and carries the same verified metadata as a value |
| 43 | +(also under an unsupported protocol version, which `meta` then shows). |
| 44 | +Every failure is classified by error variant into `Status.kind`, never by |
| 45 | +message text: a node that definitively refuses a request (a gRPC code the |
| 46 | +SDK would not retry, including a response above the 4 MiB decoding bound) |
| 47 | +is `Rejected`; no answer at all is `Unavailable`. Every bridge entry point runs under `catch_unwind`, so a panic |
| 48 | +on hostile input is a `rust::Error` or an `Internal` status, never an |
| 49 | +abort; the crate refuses to build with `panic = "abort"`. |
| 50 | + |
| 51 | +## API |
| 52 | + |
| 53 | +Namespace `platform_ffi`; the full declaration is `src/lib.rs`, the |
| 54 | +generated header `dash/platform/ffi.h`. |
| 55 | + |
| 56 | +- **Lifecycle**: `new_platform_client(Config{network, tenderdash_chain_id, |
| 57 | + platform_llmq_type})`, `set_endpoints(&[String])` (`https://` only), |
| 58 | + `set_quorum_keys`, `set_chainlock_height`, `shutdown()` (aborts the |
| 59 | + in-flight request, stops the runtime; idempotent, also run on drop). |
| 60 | + The SDK keeps a pooled connection per evonode it has talked to, and only |
| 61 | + dropping the SDK closes them: an empty endpoint set drops it (no socket |
| 62 | + stays open while the embedder has disabled networking), a set that |
| 63 | + removes endpoints rebuilds it over the updated list (retained entries |
| 64 | + keep their ban state), a set that only adds updates it in place. The |
| 65 | + quorum keys, the ChainLock anchor, the height watermark and the verified |
| 66 | + protocol version belong to the client and survive every rebuild. |
| 67 | +- **Proved reads**, one SDK request each, returning `Verified*{status, meta, |
| 68 | + value | items + page}`: `get_identity`, `get_identity_by_pubkey_hash`, |
| 69 | + `get_identity_contract_nonce` (masked to the 40-bit value; `ProvenAbsent` |
| 70 | + = the identity has not used the contract, i.e. 0), `resolve_name`, |
| 71 | + `search_names` (prefix of 1 to 63 normalized characters, limit clamped to |
| 72 | + 1..=100), `names_of_identity`, `get_profile`, `get_contact_requests` (to |
| 73 | + or from an identity, after a time, oldest first), |
| 74 | + `get_contested_vote_state`. Paged reads return one page of up to their |
| 75 | + limit; `page.has_more` is set on a page that fills it (which may still be |
| 76 | + the last one) and `page.next_start_after` is the cursor for the next call |
| 77 | + (all zero = from the start). Queries use this build's latest compiled-in |
| 78 | + contracts, which decode every older document; the SDK's tracked version |
| 79 | + only matters for building. At protocol version 13 Drive answers a |
| 80 | + continuation of `names_of_identity` with an empty page, so only its first |
| 81 | + 100 names are reachable there and a continuation is `Unavailable` rather |
| 82 | + than a last page. |
| 83 | +- **Broadcast**: `broadcast(&[u8]) -> BroadcastResult{status}`, advisory and |
| 84 | + typed: `Ok`, `AlreadyExists`, `Consensus` with `consensus_code`, `Rejected` |
| 85 | + (a definitive non-consensus refusal), `Unavailable` (no answer). Every |
| 86 | + write is confirmed by a proved re-query. |
| 87 | +- **Builders**, no network, returning `Built{bytes, hash, object_id}`: |
| 88 | + `build_identity_create`, `build_dpns_preorder`, `build_dpns_domain` (the |
| 89 | + caller supplies and persists the salt), `build_profile` (create, or |
| 90 | + replace the `Profile` a `get_profile` returned at its revision + 1: a |
| 91 | + replace carries the whole document, so the avatar and payment-address |
| 92 | + fields another wallet set are carried over unedited), |
| 93 | + `build_contact_request` (minted by `Sdk::create_contact_request` with the |
| 94 | + embedder's ECDH secret). A create carries the document id |
| 95 | + `dash-sdk`'s `put_to_platform` would derive at the network's version |
| 96 | + (from the entropy alone up to protocol version 13, also from the identity |
| 97 | + contract nonce from 14), and a property the network's contract does not |
| 98 | + have yet (DashPay's payment addresses before 14) is refused before |
| 99 | + anything is signed. Transition rules change across protocol |
| 100 | + versions, so every builder (and `contested_vote_fund_credits`) needs the |
| 101 | + version a verified read has shown the network to run: before the first |
| 102 | + read the shell accepted they fail with a `rust::Error`, except on a |
| 103 | + devnet, whose floor is the latest version (regtest's is not: one read |
| 104 | + first). Once a read has shown a version this build does not know, they |
| 105 | + fail too, until the embedder is updated. The version only moves up, on |
| 106 | + each accepted read; the nonce read right before a build refreshes it. |
| 107 | +- **Pure helpers**: `normalize_label`, `is_valid_username`, |
| 108 | + `is_contested_username`, `credits_per_duff`, `system_contract_id`, and the |
| 109 | + DIP-15 pieces that need only 32-byte inputs: `dip15_decrypt_xpub`, |
| 110 | + `dip15_account_reference_from_mac`, |
| 111 | + `dip15_unmask_account_reference_from_mac`, `dip15_select_recipient_key`, |
| 112 | + `dip15_receive_keys_acceptable`. The infallible ones answer `false`, `0` |
| 113 | + or empty on a (contained) panic, which reads as a refusal. |
| 114 | + |
| 115 | +`Status.kind` is one of `Ok, ProvenAbsent, AlreadyExists, Consensus, |
| 116 | +Unavailable, Rejected, ChainIdMismatch, UnsupportedProtocolVersion, |
| 117 | +Internal`; a `Verified*` value is meaningful only under `Ok` and |
| 118 | +`UnsupportedProtocolVersion`. |
| 119 | + |
| 120 | +## Threading and signing |
| 121 | + |
| 122 | +- Reads and the broadcast block the calling thread until the SDK request |
| 123 | + completes or `shutdown` aborts it; the embedder serializes them on its own |
| 124 | + worker. One tokio runtime (`src/runtime.rs`, two worker threads with |
| 125 | + 16 MiB stacks for GroveDB proof replay) is owned per client. No single |
| 126 | + call issues more than one SDK request. |
| 127 | +- Builders run to completion on the calling thread: dpp's async builders are |
| 128 | + driven by a local executor, no runtime is entered, and the `WalletSigner` |
| 129 | + is only ever invoked on the thread that called the builder (tested). The |
| 130 | + embedder's `WalletSigner` (`include/dash/platform/signer.h`) must |
| 131 | + nonetheless be callable from any thread (take the wallet's own lock, no |
| 132 | + thread-local state); that contract, stated in the header, is what its |
| 133 | + `Send + Sync` impls rest on. |
| 134 | +- `SignForKey(key_id, signable)` receives the full signable preimage of the |
| 135 | + transition, so the wallet hashes it (double SHA256) itself and can check |
| 136 | + what it signs; the first byte is the `StateTransition` variant index |
| 137 | + (`test_data/state_transition_first_byte.json`: 2 = batch, 3 = identity |
| 138 | + create). It answers with a 65-byte compact recoverable ECDSA signature, |
| 139 | + exactly `dashcore::signer::sign` over the raw key. |
| 140 | + `SignAssetLockSighash(sighash)` is the one digest path: the asset lock's |
| 141 | + outpoint key signs the 32-byte double SHA256 the builder computed; the |
| 142 | + public key is recovered from the signature (compressed-key header, |
| 143 | + 31 + recovery id; anything else is refused), so it is never exported. |
| 144 | + Private keys never cross the FFI; the shell never derives keys. |
| 145 | + |
| 146 | +## Building and testing |
| 147 | + |
| 148 | +An ordinary workspace member: `cargo build -p dash-platform-cxx --release`. |
| 149 | +`build.rs` stages `dash/platform/ffi.h`, `dash/platform/signer.h` and |
| 150 | +`rust/cxx.h` under `target/<profile>/include/`; install that tree and |
| 151 | +`libdash_platform_cxx.a`, link with `-lpthread -lm` (`-ldl` on Linux, |
| 152 | +`-framework Security -framework CoreFoundation` on macOS for the rustls |
| 153 | +trust store). `scripts/cxx-smoke.sh` compiles and runs `tests/cxx_smoke.cc` |
| 154 | +against exactly that interface. |
| 155 | + |
| 156 | +`cargo test -p dash-platform-cxx` runs the unit tests, `tests/replay.rs` |
| 157 | +(a real Drive state proved and BLS-signed by a test quorum, replayed through |
| 158 | +`dash-sdk`'s mock transport so the SDK runs the full GroveDB replay and |
| 159 | +signature check: the freshness matrix, absence, paging, broadcast |
| 160 | +classification) and `tests/builders.rs` (every builder byte for byte against |
| 161 | +dpp's in-process private-key constructions, the document ids and data Drive |
| 162 | +validates, signer refusals, the no-reactor invariant, and |
| 163 | +`test_data/state_transition_first_byte.json` against the transitions it |
| 164 | +builds; `UPDATE_TEST_VECTORS=1` rewrites that file). Both suites run every |
| 165 | +test at protocol version 13 (what testnet and mainnet run) and at this |
| 166 | +build's latest, the fixture state generated at each. |
0 commit comments