diff --git a/README.md b/README.md index 3e8d1cc4..8627af3c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ DIP stands for Dash Improvement Proposal. Similar to Bitcoin's [BIPs](https://github.com/bitcoin/bips/), a DIP is a design document providing information to the Dash community, or describing a new feature for Dash or its processes or environment. The DIP should provide a concise technical specification of the feature and a rationale for the feature. -Because Dash is forked from the Bitcoin codebase, many of the BIPs can be applied to Dash as well (a list of the BIPs updated to include Dash-specific details can be found [here](https://github.com/dashevo/bips)). The purpose of the DIPs is not to duplicate those which exist as BIPs, but to introduce protocol upgrades or feature specifications which are unique to Dash. +Because Dash is forked from the Bitcoin codebase, many of the BIPs can be applied to Dash as well (a list of the BIPs updated to include Dash-specific details can be found in the [dashevo/bips repository](https://github.com/dashevo/bips)). The purpose of the DIPs is not to duplicate those which exist as BIPs, but to introduce protocol upgrades or feature specifications which are unique to Dash. ## Contributions @@ -47,6 +47,7 @@ Number | Layer | Title | Owner | Type | Status [29](dip-0029.md) | Consensus | Randomness Beacon For LLMQ Selection | Virgile Bartolo | Standard | Proposed [30](dip-0030.md) | Consensus | Replay Attack Prevention and State Transition Nonces | Samuel Westrich | Standard | Proposed [31](dip-0031.md) | Consensus | Platform Proof of Service | Ivan Shumkov, Pasta | Standard | Proposed +[33](dip-0033.md) | Applications | DashPay Payment Addresses and Payment Notifications | Samuel Westrich | Standard | Proposed ## License diff --git a/dip-0033.md b/dip-0033.md new file mode 100644 index 00000000..d75cf7d9 --- /dev/null +++ b/dip-0033.md @@ -0,0 +1,237 @@ +
+ DIP: 0033 + Title: DashPay Payment Addresses + Author(s): Samuel Westrich + Special-Thanks: Dash Platform Team + Comments-Summary: No comments yet. + Status: Proposed + Type: Standard + Created: 2026-08-03 + License: MIT License + Requires: 15, 17, 18 + Replaces: - + Superseded-By: - ++ +## Table of Contents + +1. [Abstract](#abstract) +1. [Motivation](#motivation) +1. [Prior Work](#prior-work) +1. [Terminology](#terminology) +1. [Specification](#specification) + 1. [Overview](#overview) + 1. [Profile Payment Address Fields](#profile-payment-address-fields) + 1. [Profile Shielded Address](#profile-shielded-address) + 1. [Per-Contact Shielded Addresses](#per-contact-shielded-addresses) + 1. [Shielded Memo Kinds](#shielded-memo-kinds) + 1. [Client Behavior](#client-behavior) +1. [Rationale](#rationale) +1. [Backwards Compatibility](#backwards-compatibility) +1. [Security Considerations](#security-considerations) +1. [Privacy Considerations](#privacy-considerations) +1. [Considered and Deferred: A Notified Stealth-Address Tier](#considered-and-deferred-a-notified-stealth-address-tier) +1. [Future Extensions](#future-extensions) +1. [Copyright](#copyright) + +## Abstract + +This DIP extends DashPay with payment addresses across the payment rails of the Dash network: the Dash Core chain, Dash Platform payment addresses ([DIP-17](dip-0017.md)/[DIP-18](dip-0018.md)), and the Dash Platform shielded pool. It defines two tiers: + +* A **public tier**: optional payment address fields on the DashPay profile document — a Core chain address and a Platform payment address — allowing anyone to pay a profile with zero interaction. Payments to these addresses are publicly linkable to the profile by design. +* A **shielded tier**: a published shielded-pool payment address on the profile for private zero-interaction payments, and per-contact diversified shielded addresses exchanged through [DIP-15](dip-0015.md) contact requests for private payments with built-in attribution. Shielded payments natively hide the payer, the recipient, and the amount; publishing a shielded address reveals only that the profile accepts shielded payments. + +A third design — payer-derived stealth addresses on the transparent rails, announced by encrypted Platform notifications — was fully specified, reviewed, and deliberately deferred; the [appendix](#considered-and-deferred-a-notified-stealth-address-tier) records the design and the reasons, chiefly that any payment-time Platform artifact is timing-correlatable with its settlement transaction. + +## Motivation + +[DIP-15](dip-0015.md) establishes private payment channels between Dash identities, but only after a contact relationship is established: each side derives a per-contact address chain from encrypted extended public keys exchanged in contact requests. Two gaps remain: + +1. **Zero-interaction payments.** There is no standard way to pay a profile you have no relationship with — no "tips jar", no pay-by-username without a prior contact handshake. +2. **Private payments without a relationship.** Establishing a DashPay contact publishes a public contact edge. There is no way to pay someone privately — unlinkable by third parties — without first creating that public relationship. + +Additionally, [DIP-17](dip-0017.md) introduced Platform payment addresses as identity-independent value holders, and payments to them face the same two gaps. + +This DIP fills both gaps with the minimum machinery: static profile addresses for the public case, and the Platform shielded pool for the private case. The pool already provides recipient unlinkability, hidden amounts, and light-client detection by trial decryption of note ciphertexts; this DIP only standardizes how DashPay publishes and exchanges shielded payment addresses and how payments are attributed. + +## Prior Work + +* [DIP-0011: Identities](dip-0011.md) +* [DIP-0015: DashPay](dip-0015.md) +* [DIP-0017: Dash Platform Payment Addresses and HD Derivation](dip-0017.md) +* [DIP-0018: Dash Platform Payment Address Encodings](dip-0018.md) +* [BIP-0047: Reusable Payment Codes for Hierarchical Deterministic Wallets](https://github.com/bitcoin/bips/blob/master/bip-0047.mediawiki) (rejected approach; see appendix) +* [BIP-0352: Silent Payments](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki) (rejected approach; see appendix) +* Zcash Orchard diversified payment addresses (the unlinkability mechanism the shielded tier builds on) +* Stealth addresses as originally described by Peter Todd and used by CryptoNote-family protocols (deferred approach; see appendix) + +## Terminology + +* **Rail**: a settlement system for value. This DIP touches the **Core rail** (Dash Core chain transactions), the **Platform rail** (Platform payment addresses per DIP-17/18), and the **shielded rail** (the Dash Platform shielded pool). +* **Public tier**: payment targets published in the DashPay profile document whose payments are publicly linkable to the profile. +* **Shielded tier**: payment targets in the shielded pool; payments to them are unlinkable by third parties and hide their amounts. +* **Diversified address**: one of many unlinkable shielded payment addresses derivable from a single full viewing key; addresses at different diversifier indices cannot be linked to each other without the viewing key. +* **IVK / FVK**: the pool's incoming viewing key (detects and decrypts incoming notes) and full viewing key. + +## Specification + +### Overview + +A recipient publishes up to three payment targets: + +1. A **Core chain address** and/or a **Platform payment address** in the profile document (public tier). +2. A **shielded payment address** in the profile document (shielded tier, zero-interaction). +3. **Per-contact diversified shielded addresses**, one per contact, delivered inside DIP-15 contact requests (shielded tier, attributed). + +Payments to the public tier are ordinary transparent payments, linkable to the profile by design. Payments to the shielded tier are ordinary shielded-pool payments: the recipient detects them by the pool's native note trial decryption, and no additional discovery mechanism, publication, or ordering rule is required — the encrypted note carried by the payment itself is the notification. + +Identity-to-identity credit transfers are out of scope: they are inherently public, identity-linked operations. Value privacy for identity-held value is obtained by moving it through the shielded pool. + +### Profile Payment Address Fields + +The DashPay data contract's `profile` document type gains two OPTIONAL fields. Field positions continue after the existing `displayName` (position 4); existing fields, indices, and required properties are unchanged. + +```json +"corePaymentAddress": { + "type": "array", + "byteArray": true, + "minItems": 21, + "maxItems": 21, + "position": 5, + "description": "Core chain address in storage form (type byte 0x00 P2PKH / 0x01 P2SH followed by 20-byte HASH160) for public payments" +}, +"platformPaymentAddress": { + "type": "array", + "byteArray": true, + "minItems": 21, + "maxItems": 21, + "position": 6, + "description": "DIP-18 platform address in storage form (type byte 0x00 P2PKH / 0x01 P2SH followed by 20-byte HASH160) for public payments" +} +``` + +Normative requirements: + +* `corePaymentAddress` uses the same storage serialization as `platformPaymentAddress`: a variant byte (`0x00` for P2PKH, `0x01` for P2SH) followed by the 20-byte HASH160 (RIPEMD160 of SHA256) of the public key or redeem script. No network byte is stored: the network is determined by the Platform chain the document lives on, which makes a network mismatch unrepresentable. Clients MUST render the address in Base58Check with the version byte of the network they are on. A Base58Check string was rejected as the storage form because consensus could not prove its checksum or network byte anyway, while the byte form is smaller, exact-size-validated, and symmetric with the platform field. +* `platformPaymentAddress` MUST use the [DIP-18](dip-0018.md) storage serialization. Clients MUST display it in the [DIP-18](dip-0018.md) bech32m encoding. +* **Consensus enforces the type byte**: profile creation and replacement MUST reject either field whose leading byte is not `0x00` or `0x01` (implemented as a data trigger, since the schema vocabulary cannot constrain individual bytes of a byte array). Together with the exact-length constraint, an accepted value is always fully decodable as the advertised address type. +* Both fields are single static addresses, intentionally reusable and publicly linkable to the profile. Publishing an extended public key or any other derivation source is NOT part of this specification: since anything published in the profile is world-readable, a published chain is exactly as linkable as a single address while adding gap-limit scanning and index-coordination costs. +* Owners MAY rotate either address at any time by updating the profile document. Clients SHOULD continue monitoring previously published addresses they have handed out. +* Payments to these addresses carry no sender attribution by themselves. + +### Profile Shielded Address + +The `profile` document type gains one further OPTIONAL field: + +```json +"shieldedAddress": { + "type": "array", + "byteArray": true, + "minItems": 43, + "maxItems": 43, + "position": 7, + "description": "Shielded pool payment address in raw form (11-byte diversifier followed by 32-byte diversified transmission key) for private payments" +} +``` + +Normative requirements: + +* The value is the raw 43-byte shielded payment address (11-byte diversifier `d` followed by the 32-byte diversified transmission key `pk_d`), exactly as used by shielded pool outputs. Clients MUST validate that the bytes decode to a valid shielded payment address before paying and MUST treat an undecodable value as no address rather than as an error in the profile. +* Wallets MUST reserve a dedicated diversifier index for the published profile address (index `0` RECOMMENDED) and MUST NOT hand the profile address out as a per-contact address, so that attribution by diversifier ([Client Behavior](#client-behavior)) stays meaningful. +* Owners MAY rotate the published address at any time by updating the profile document; because shielded addresses never appear on chain, rotation is hygiene rather than necessity, and old addresses remain spendable-to and detected by the same viewing key. + +### Per-Contact Shielded Addresses + +The `contactRequest` document type gains one OPTIONAL field. Contact requests are immutable; the field is set at creation: + +```json +"encryptedShieldedAddress": { + "type": "array", + "byteArray": true, + "minItems": 64, + "maxItems": 80, + "position": 7, + "description": "Sender's diversified shielded payment address (43 bytes), encrypted to the contact with the DIP-15 shared key" +} +``` + +Normative requirements: + +* The plaintext is the sender's 43-byte raw shielded payment address at a **fresh diversifier index unique to this contact**. +* Encryption MUST use the same ECDH shared secret derivation as the contact request's `encryptedPublicKey` field ([DIP-15](dip-0015.md)), with AES-256-CBC and a random 16-byte IV prepended to the ciphertext (16 + 48 = 64 bytes). Values longer than 64 bytes are reserved for future encryption modes; clients MUST ignore an `encryptedShieldedAddress` they cannot decrypt or decode. +* The recipient of the contact request uses this address for all shielded payments to the sender, in preference to the sender's public profile `shieldedAddress`. + +Per-contact diversified addresses provide attribution for free: because the wallet records which contact each diversifier index was minted for, an incoming note to that address identifies the paying contact without any memo — while remaining unlinkable, on chain and to third parties, to the profile address and to every other contact's address. + +### Shielded Memo Kinds + +Shielded pool note memos are 36 bytes: a little-endian `u32` kind tag followed by a 32-byte payload. Kinds `0` (empty) and `1` (UTF-8 text) are already established; unknown kinds round-trip untouched. This DIP registers: + +| Kind | Name | Payload | Semantics | +| ---- | ---- | ------- | --------- | +| `2` | `PaymentRef` | Sender identity id (32 bytes) | "This payment is from identity X." **Unauthenticated**: anyone can write any identity id. Clients MUST render the sender as *claimed* unless corroborated — for example, the note arrived on that contact's per-contact diversified address. | +| `3` | reserved | — | Reserved for a future document-reference kind | + +A 36-byte memo cannot carry a signature (65 bytes); authenticated attribution is out of scope for memos. Between contacts, the per-contact diversified address provides attribution that a third party cannot forge without knowledge of the address; a `PaymentRef` arriving on a per-contact address and naming that contact is consistent attribution, and clients MAY display it as such. + +### Client Behavior + +* **Detection** is the pool's native mechanism: clients trial-decrypt note ciphertexts with their IVK. One IVK covers *all* diversified addresses of the wallet — the profile address and every per-contact address — so publishing or minting more addresses adds no scanning cost. +* **Attribution**: a note on a per-contact diversified address is attributed to that contact (subject to the weak-authentication caveat in [Security Considerations](#security-considerations)); a note on the profile address is anonymous unless it carries a corroborated `PaymentRef` memo; senders wanting to attach context use a text memo (kind `1`) or `PaymentRef` (kind `2`). +* **Exiting to transparent funds**: a recipient who wants Core-chain or Platform-address funds unshields to themselves at a time and amount of their own choosing. Non-normative hygiene: unshield to fresh addresses, avoid amount-identical shield-then-unshield sequences close in time, and prefer round or denomination-friendly amounts. +* **Watch-only delegation**: the pool's viewing keys provide detection-without-spend delegation; nothing in this DIP adds a delegation mechanism or requires one. +* **Restore from seed**: the pool rescan recovers all received notes (the viewing key detects every diversified address, including per-contact ones, without knowing their indices in advance). Per-contact attribution mappings are recovered by re-reading the wallet's own sent contact requests and re-deriving each `encryptedShieldedAddress` plaintext with the DIP-15 shared key. Sender-side history is recovered via the pool's outgoing viewing key, which the pool's note encryption already provides. +* **Paying a profile**: given a profile, clients SHOULD offer the shielded address as the private payment option and the transparent addresses as the public ones, and say plainly which is which. + +## Rationale + +* **Static profile addresses, not extended public keys.** A profile is world-readable; a published xpub lets everyone derive and link the full address chain, so it is exactly as linkable as one static address while adding receiver-side gap-limit scanning across light-client filters, payer-side index races, and larger documents. Rotation-by-profile-update captures all the realizable benefit. +* **The byte storage form with a consensus-checked type byte.** The storage form carries no checksum, so unlike a Base58Check string the type-byte-plus-length check is *complete* validation: everything consensus accepts is decodable. (A Base58 pattern was tried and rejected: it could not prove the checksum, and it made every randomly generated test document invalid.) +* **The shielded pool as the private tier, not stealth addresses.** The deferred stealth design ([appendix](#considered-and-deferred-a-notified-stealth-address-tier)) hid the recipient of transparent payments but required a discovery channel. Every payment-time discovery artifact on Platform is timing-correlatable with its settlement transaction, which at minimum marks the settlement as a private payment and at worst links the payer identity to it. The pool needs no discovery artifact at all — the payment carries its own encrypted notification — and additionally hides the amount, which no transparent-rail scheme can. Concentrating private payments in the pool also concentrates the anonymity set in one mechanism instead of splitting it across three. +* **Per-contact diversified addresses rather than memos for attribution.** Diversifier attribution costs nothing at scan time, works with empty memos, and cannot be spoofed by a third party who has not been given the address. Memo-based attribution (`PaymentRef`) is deliberately secondary and explicitly unauthenticated. +* **A reserved diversifier index for the profile address.** Keeping the published address disjoint from per-contact addresses preserves the meaning of diversifier attribution; the choice of index `0` is convention, invisible on chain. + +## Backwards Compatibility + +* The transparent profile fields (positions 5 and 6) are optional additions at fresh positions; existing profiles remain valid and older clients ignore the new fields. They activate with a Platform protocol version upgrade that reloads the DashPay contract. +* The `shieldedAddress` profile field and the `contactRequest.encryptedShieldedAddress` field activate with a subsequent contract version under the same mechanism. +* Memo kinds are a client-side registry over the pool's existing extensible memo format; unknown kinds already round-trip, so no protocol change is involved. +* No changes are made to DIP-15 contact-request flows; established contacts continue using per-contact derived chains for transparent payments. The shielded tier is complementary. +* Core chain consensus is unchanged. + +## Security Considerations + +* **Profile address hijacking.** Profile documents update under lower-security keys than identity keys; a compromised profile key can redirect *public-tier* payments and the published shielded address. Clients SHOULD alert users when a contact's payment fields change and pin previously seen values. Per-contact shielded addresses ride the contact request (immutable, created by the contact's identity) and are unaffected by profile compromise. +* **Weak authentication of per-contact addresses.** Anyone who *learns* a per-contact diversified address can pay it; diversifier attribution therefore means "very probably this contact", not proof. Clients requiring strong attribution need signed context at the application layer. +* **Spoofed `PaymentRef` memos.** The memo payload is written by the payer with no authentication; clients MUST NOT render it as a verified sender without corroboration. +* **Payer-provable payments.** A payer to any published shielded address can later prove they made the payment (they know the note plaintext). This is inherent to any receiving address and usually desirable (receipts). + +## Privacy Considerations + +* **Public tier is public.** Payments to profile addresses are linkable to the profile by anyone, permanently; on the Platform rail the address's balance and history are directly queryable state. Wallets SHOULD say so in UI when a user pays or publishes a tips address. +* **What publishing a shielded address leaks**: only that the profile accepts shielded payments. Payments to it produce note ciphertexts that reveal nothing, and the address itself never appears on chain. Diversified-address unlinkability means the published address cannot be linked to any per-contact address derived from the same viewing key. +* **Payer-side linkage on entry.** Shielding transparent funds is a visible event. Payers who shield and immediately pay amount-identical values create a correlation between their shielding and the recipient's later unshielding; the hygiene guidance in [Client Behavior](#client-behavior) applies to both sides. +* **The anonymity set is pool activity.** The privacy of the shielded tier is proportional to overall pool usage. This is a deliberate concentration: one well-used private mechanism outperforms several thinly used ones. + +## Considered and Deferred: A Notified Stealth-Address Tier + +*This section is informational. It records a fully designed alternative and why it was deferred, so the reasoning survives the decision.* + +Earlier drafts of this DIP specified a third tier for the transparent rails: the recipient published two secp256k1 **detection keys** (scan and spend) as new identity key purposes (`PAYMENT_SCAN = 7`, `PAYMENT_SPEND = 8`, derivation under a reserved DIP-9 feature index `33'`); the payer derived a one-time (stealth) address `P = B_spend + H(ECDH(r, B_scan) || …)·G`, paid it on either transparent rail, and published an encrypted, fixed-size **payment notification** document on Platform that only the recipient could detect by trial decryption. The notification carried the settlement locator, the stealth ephemeral key, an authenticated sender identity, and a memo; an out-ciphertext mirroring the pool's outgoing-viewing-key pattern gave the payer seed-only recovery. The design was implemented through review, including consensus validation of the new key purposes. + +It was deferred for three reasons: + +1. **Timing correlation defeats the goal.** The notification had to reach Platform finality before settlement broadcast (it was the recipient's only recovery record for outputs underivable from their seed). A global observer correlating notification-store writes with settlement transactions therefore links the two events: with identity-funded notifications this links the payer identity to the settlement — fatal; with pool-funded notifications it still marks the settlement as a stealth payment, collapsing its anonymity set from "all P2PKH transactions" to "stealth payments in this interval". Randomized delays help only at volumes the mechanism would not have at launch. +2. **The correlation-free alternative is real infrastructure.** Detection without any payment-time artifact means BIP-352-style scanning, whose per-transaction input-key data is absent from compact block filters; light clients would need network infrastructure serving per-transaction ECDH tweaks (a "tweak index"). Composable with existing GCS filters, and deliberately kept open — the detection keys were shaped exactly like BIP-352 key pairs — but a substantial commitment relative to demand. +3. **The pool already exceeds it.** Shielded payments hide the amount as well as the parties, need no discovery artifact (the payment is its own encrypted notification, immune to timing analysis by construction), recover from seed plus chain alone, and were shipping regardless. A recipient who wants transparent-rail funds unshields to themselves, achieving what the stealth tier offered with hygiene under their own control. + +Deferral is cheap to reverse: the key purposes, derivation paths, stealth math, notification format, and test vectors exist in the project history (implementation reviewed and parked), and nothing shipped by this DIP occupies or constrains them. A future DIP reviving the tier SHOULD pair it with a tweak index rather than payment-time notifications, and SHOULD reuse the BIP-352-shaped key material unchanged. The DIP-9 feature index `33'` reservation is withdrawn until then. + +## Future Extensions + +* **Notification-less stealth tier.** As recorded in the appendix: a revival would combine the parked detection-key and stealth-derivation design with a network-served per-transaction tweak index, making transparent-rail private payments detectable by scanning with no payment-time artifact. This paragraph is a reservation, not a commitment. +* **Document-reference memo kind.** Memo kind `3` is reserved for pointing a shielded payment at a larger encrypted context document, should an application need more than 32 bytes of payment context. + +## Copyright + +Copyright (c) 2026 Dash Core Group, Inc. [Licensed under the MIT License](https://opensource.org/licenses/MIT) diff --git a/project-words.txt b/project-words.txt index 85f03cf8..0c64f8c4 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1,5 +1,6 @@ # Acceptable ASIC +bech Bitfield bitfield bitsets @@ -63,6 +64,7 @@ Merkle mnhfsignal MNLISTDIFF Namecoin +Notif pBFT platformban plwdtx @@ -103,9 +105,14 @@ topup topups tprv tpub +unlinkability +unlinkable unretrievable +unshield +unshielding UTXO varint +vout vvec xprv xpub @@ -162,4 +169,4 @@ Udjin Udjinm Virgile Westrich -Wray \ No newline at end of file +Wray