From d3ed31917805d4a549ff721fd03178613ecd932d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 3 Aug 2026 08:19:16 +0700 Subject: [PATCH 1/5] Add DIP 0033: DashPay Payment Addresses and Payment Notifications Two-tier payment addressing for DashPay across the Core chain and Platform payment address (DIP-17/18) rails: * Public tier: optional corePaymentAddress / platformPaymentAddress fields on the profile document (static, deliberately linkable). * Notified tier: PAYMENT_SCAN / PAYMENT_SPEND identity key purposes, DIP-9 feature 33' derivation, stealth one-time address derivation shared by both rails, and encrypted payment notification documents with sender recovery and an anonymous-sender variant. Reserves the shielded-pool tier and a silent-payments-style tweak index as future extensions. Includes derivation test vectors. Co-Authored-By: Claude Fable 5 --- README.md | 3 +- dip-0033.md | 420 ++++++++++++++++++++++++++++++++++++++++++++++ project-words.txt | 9 +- 3 files changed, 430 insertions(+), 2 deletions(-) create mode 100644 dip-0033.md 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..42ae7aa6 --- /dev/null +++ b/dip-0033.md @@ -0,0 +1,420 @@ +
+  DIP: 0033
+  Title: DashPay Payment Addresses and Payment Notifications
+  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: 9, 11, 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. [Payment Detection Keys](#payment-detection-keys) + 1. [HD Derivation](#hd-derivation) + 1. [One-Time Address Derivation](#one-time-address-derivation) + 1. [Payment Notifications](#payment-notifications) + 1. [Client Behavior](#client-behavior) +1. [Rationale](#rationale) +1. [Backwards Compatibility](#backwards-compatibility) +1. [Security Considerations](#security-considerations) +1. [Privacy Considerations](#privacy-considerations) +1. [Future Extensions](#future-extensions) +1. [Reference Implementation](#reference-implementation) +1. [Test Vectors](#test-vectors) +1. [DIP-9 Registry Update](#dip-9-registry-update) +1. [Copyright](#copyright) + +## Abstract + +This DIP extends DashPay with payment addresses and private payment detection across two payment rails: the Dash Core chain and Dash Platform payment addresses ([DIP-17](dip-0017.md)/[DIP-18](dip-0018.md)). It defines two tiers: + +* A **public tier**: optional payment address fields on the DashPay profile document, allowing anyone to pay a profile with zero interaction. Payments to these addresses are publicly linkable to the profile by design. +* A **notified tier**: the payer derives a fresh one-time (stealth) address from two detection keys published in the recipient's identity key set, pays it, and publishes an encrypted **payment notification** document that only the recipient can detect by trial decryption. Third parties cannot link the payment to either party; the recipient learns the payer's identity (unless the payer explicitly chooses the anonymous variant). + +The same secp256k1 detection key pair serves both rails. A new [DIP-9](dip-0009.md) feature index `33'` is registered for the detection keys, and two new Platform identity key purposes (`PAYMENT_SCAN`, `PAYMENT_SPEND`) are introduced. A shielded-pool tier is reserved for a future extension. + +## 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, and no way for the recipient to detect such a payment without prohibitive scanning costs on light clients. + +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 viable machinery: static profile addresses for the public case, and stealth one-time addresses plus Platform-published encrypted notifications for the private case. Platform's replicated document storage turns the classical stealth-address detection problem (scan every transaction) into a cheap, deterministic, proof-friendly lookup that works on light clients. + +## Prior Work + +* [DIP-0009: Feature Derivation Paths](dip-0009.md) +* [DIP-0011: Identities](dip-0011.md) +* [DIP-0013: Identities in Hierarchical Deterministic Wallets](dip-0013.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 Rationale) +* [BIP-0352: Silent Payments](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki) (rejected approach; see Rationale) +* Stealth addresses as originally described by Peter Todd and used by CryptoNote-family protocols + +## Terminology + +* **Rail**: a settlement system for value. This DIP covers the **Core rail** (Dash Core chain transactions) and the **Platform rail** (Platform payment addresses per DIP-17/18, moved by address funds transfers and credited by unshielding from the Platform shielded pool). +* **Public tier**: payment targets published in the DashPay profile document; payments to them are publicly linkable to the profile. +* **Notified tier**: payments to one-time addresses announced by encrypted payment notifications; unlinkable by third parties. +* **Detection keys**: the recipient's published scan and spend public keys (`PAYMENT_SCAN`, `PAYMENT_SPEND` identity keys). +* **One-time address**: a payer-derived address spendable only by the recipient, appearing on-chain with no link to the recipient's published keys. +* **Payment notification**: an encrypted document published on Platform announcing a payment to its recipient. + +## Specification + +### Overview + +A recipient publishes up to two kinds of payment information: + +1. **Addresses** in the profile document (public tier): a Core chain address and/or a Platform payment address. +2. **Detection keys** in the identity key set (notified tier): a scan key and a spend key, one pair covering both rails. + +A payer paying the notified tier performs, in order: derive a one-time address ([One-Time Address Derivation](#one-time-address-derivation)); publish a payment notification encrypted to the recipient's scan key ([Payment Notifications](#payment-notifications)); await Platform finality of the notification; broadcast the settlement transaction. The recipient's client discovers payments by trial-decrypting the notification stream, then verifies settlement on the relevant rail. + +Identity-to-identity credit transfers are out of scope: they are inherently public, identity-linked operations. Value privacy for identity balances is obtained by moving value through Platform payment addresses and 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": "string", + "pattern": "^[1-9A-HJ-NP-Za-km-z]{26,34}$", + "minLength": 26, + "maxLength": 34, + "position": 5, + "description": "Base58Check P2PKH/P2SH Core chain address 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` MUST be a valid Base58Check Core chain address for the network the contract is deployed on. Clients MUST validate the checksum and network byte before paying. +* `platformPaymentAddress` MUST use the [DIP-18](dip-0018.md) storage serialization (variant byte `0x00` for P2PKH or `0x01` for P2SH, followed by the 20-byte HASH160). Clients MUST display it in the [DIP-18](dip-0018.md) bech32m encoding. +* 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. A payer MAY additionally publish a payment notification (rail codes 1 or 3, below) to attribute the payment and attach a memo. + +### Payment Detection Keys + +Two new Platform identity key purposes are defined, extending the purpose registry of [DIP-11](dip-0011.md) identities: + +| Purpose | Value | Meaning | +| ------- | ----- | ------- | +| `PAYMENT_SCAN` | `7` | Detection key: ECDH target for one-time address detection and for payment notification decryption | +| `PAYMENT_SPEND` | `8` | Spend base key for one-time addresses | + +Normative requirements: + +* Key type MUST be `ECDSA_SECP256K1`, with the full 33-byte compressed public key stored as the key data. +* Keys of these purposes MUST NOT be usable to sign state transitions or documents (they are non-signing, like encryption/decryption purpose keys). +* Keys of these purposes MUST NOT carry contract bounds. The capability they express is protocol-wide, not application-scoped. +* An identity MUST have **at most one active key of each of these purposes**. Registration of a second active key of the same purpose is invalid. Rotation is performed by disabling the existing key and adding a replacement in the same identity update. +* An identity that publishes one of the pair SHOULD publish both; the notified tier requires both. + +Placing these keys in the identity key set (rather than in a document) means changes require the identity's master-key authorization, gives first-class on-chain revocation via key disabling, and makes any identity payable through the notified tier even if it has never created a DashPay profile. + +### HD Derivation + +The detection keys are derived under [DIP-9](dip-0009.md) feature index `33'`: + +```text +m / 9' / coin_type' / 33' / account' / key_class' / index' +``` + +* `coin_type'` MUST be `5'` on mainnet and `1'` on testnet/devnet/regtest, consistent with [DIP-17](dip-0017.md). +* `account'` MUST be hardened; `0'` is the default account. +* `key_class'` MUST be hardened and takes the following values: + +| key_class' | Key | +| ---------- | --- | +| `0'` | Scan key (`b_scan`, published as the `PAYMENT_SCAN` identity key) | +| `1'` | Spend key (`b_spend`, published as the `PAYMENT_SPEND` identity key) | +| `2'` | Notification out-key (`k_out`, sender-side recovery key; never published) | + +* `index'` MUST be hardened; `0'` is the default. Incrementing `index'` is the rotation mechanism: when a wallet rotates its detection keys it derives the next index and replaces the identity keys accordingly. On restore, wallets recover the active index by matching derived public keys against the identity's registered keys, trying indices in ascending order. +* All three key classes are hardened siblings: disclosure of the scan private key (see [Client Behavior](#client-behavior) on watch services) reveals nothing about the spend or out keys. + +### One-Time Address Derivation + +All points are secp256k1, serialized in 33-byte compressed SEC1 form (`ser(·)`). `H(·)` is SHA-256. `order` is the secp256k1 group order. Let `B_scan` and `B_spend` be the recipient's active `PAYMENT_SCAN` and `PAYMENT_SPEND` keys. + +Rail domain-separation bytes: + +| Rail | `rail` byte | +| ---- | ----------- | +| Core chain | `0x00` | +| Platform payment addresses | `0x02` | + +**Payer**: + +1. Generate a fresh ephemeral scalar `r` (see [Payment Notifications](#payment-notifications) for the deterministic derivation from the notification seed) and compute `R = r·G`. +2. Compute the shared point `S = r·B_scan`. +3. For each output `n` (`n = 0, 1, ...` for multiple outputs to the same recipient within one payment): + + ```text + t_n = int(H("DashPay/Stealth/v1" || ser(S) || ser(R) || rail || LE32(n))) mod order + ``` + + If `t_n = 0`, or if step 4 yields the point at infinity, the payer MUST restart with a fresh `r`. +4. Compute the one-time public key `P_n = B_spend + t_n·G`. +5. Form the output: + * Core rail: a standard P2PKH output paying `HASH160(ser(P_n))`. + * Platform rail: the P2PKH platform address over `HASH160(ser(P_n))`, encoded per [DIP-18](dip-0018.md), used as an output of an address funds transfer or as the credited address of an unshielding operation. + +An ephemeral scalar MUST NOT be reused across payments. The `rail` byte makes Core and Platform one-time addresses distinct even under (faulty) ephemeral reuse. One-time outputs MUST NOT be script-hash outputs; stealth derivation is defined for P2PKH only. + +**Recipient** (given `R` from a payment notification): + +1. Compute `S = b_scan·R` (equal to `r·B_scan` by Diffie–Hellman). +2. Recompute `t_n` and `P_n` and check that the referenced output pays `HASH160(ser(P_n))`: + * Core rail: fetch the transaction named by the notification's `txid` and verify the output script (light clients verify with a merkle proof). + * Platform rail: derive the platform address and fetch its balance and nonce with a state proof. + +**Spending**: the one-time private key is `p_n = (b_spend + t_n) mod order`. On the Core rail this signs a standard P2PKH input. On the Platform rail it produces the address witness signature for an address funds transfer or shield input. In both cases `P_n` becomes public at spend time, as is normal for hash-of-key outputs. + +Detection capability (deriving `t_n` and recognizing outputs) requires only `b_scan` and the public `B_spend`; spend capability requires `b_spend`. This separation is what allows watch-only delegation. + +### Payment Notifications + +#### Document type + +The DashPay data contract gains a `paymentNotification` document type declared **private**: entries are stored as uniform-size opaque records in an append-only store with no public indices and no owner-visible recipient. Recipients discover entries by paginated range reads over insertion positions and trial decryption. The storage element, its proofs, and the contract-level declaration vocabulary are specified by Dash Platform (private document stores); this DIP specifies the entry content. + +Entries are immutable and never deleted. This is load-bearing: the notification store is the durable record from which stealth outputs are recovered after a seed restore. + +#### Entry format + +Each entry is exactly **586 bytes**: + +| Field | Size | Description | +| ----- | ---- | ----------- | +| `cm` | 32 | Commitment binding the recipient key and plaintext (below) | +| `epk` | 33 | Payer's ephemeral public key for notification encryption (distinct from `R`) | +| `body_ct` | 416 | AEAD ciphertext of the 400-byte body plaintext | +| `out_ct` | 105 | Sender-recovery ciphertext (below) | + +Body plaintext, fixed 400-byte layout (all multi-byte integers little-endian; unused trailing bytes zero): + +| Field | Offset | Size | Description | +| ----- | ------ | ---- | ----------- | +| `version` | 0 | 1 | Payload version; this DIP defines version `1` | +| `rail` | 1 | 1 | Rail code (below) | +| `senderIdentityId` | 2 | 32 | Payer identity; all-zeros for the anonymous variant | +| `senderKeyIndex` | 34 | 4 | Identity key id that produced `sig`; zero when anonymous | +| `recipientIdentityId` | 38 | 32 | Intended recipient; binds the notification against cross-recipient replay | +| `railBody` | 70 | 81 | Rail-specific body (below), zero-padded to 81 bytes | +| `memoLen` | 151 | 1 | Memo byte length, `0..=180` | +| `memo` | 152 | 180 | UTF-8 memo, zero-padded | +| `sig` | 332 | 65 | Recoverable ECDSA signature (below); all-zeros when anonymous | +| reserved | 397 | 3 | MUST be zero | + +Rail codes and rail bodies: + +| Code | Rail | Rail body layout | +| ---- | ---- | ---------------- | +| `0` | Core, one-time address | `txid[32] \|\| vout[4] \|\| R[33] \|\| amount[8] \|\| coreHeightHint[4]` — amount in duffs | +| `1` | Core, public profile address | `txid[32] \|\| vout[4] \|\| amount[8]` — attribution/memo for a payment to `corePaymentAddress` | +| `2` | Platform, one-time address | `R[33] \|\| outputCounter[4] \|\| amount[8] \|\| fundingKind[1]` — amount in credits; `fundingKind`: `0` = address funds transfer, `1` = unshielding. No transaction locator is needed: the derived address is the locator | +| `3` | Platform, public profile address | `platformAddress[21] \|\| amount[8] \|\| stHash[32]` — amount in credits; `stHash` identifies the crediting state transition | +| `4` | Reserved | Shielded pool tier (future extension) | + +The signature covers: + +```text +sig = ECDSA_sign_recoverable(senderKey, + H("DashPay/PayNotif/v1/sig" || rail || recipientIdentityId || railBody || memoLen || memo)) +``` + +Recipients MUST verify `sig` against an active authentication-capable key of `senderIdentityId` (fetched with proof) before attributing the payment. Notifications whose signature does not verify MUST be treated as anonymous at best and SHOULD be discarded. + +#### Encryption + +Let `ScanPub` be the recipient's active `PAYMENT_SCAN` key. + +```text +eskSeed = 16 random bytes # the payment's root secret +esk = int(H("DashPay/PayNotif/v1/esk" || eskSeed)) mod order +r = int(H("DashPay/Stealth/v1/r" || eskSeed)) mod order # stealth ephemeral (rails 0 and 2) +epk = esk·G +shared = ser(esk·ScanPub) +key = H("DashPay/PayNotif/v1/key" || shared || ser(epk) || ser(ScanPub)) +cm = H("DashPay/PayNotif/v1/cm" || ser(ScanPub) || body_plaintext) +body_ct = ChaCha20-Poly1305(key, nonce = 12 zero bytes, aad = cm, body_plaintext) +``` + +The recipient trial-decrypts each entry with `key' = H(... || ser(b_scan·epk) || ser(epk) || ser(ScanPub))`, and on AEAD success MUST recompute `cm` from the plaintext and discard the entry on mismatch. Deriving `esk` and `r` from separate domain-separated hashes of one seed keeps notification-decryption capability and address-derivation material independent while letting the sender recover both from a single stored secret. (The commitment formula is provisional pending the Platform private document store specification; if that specification defines a store-level commitment, it governs.) + +#### Sender recovery (out-ciphertext) + +Mirroring the outgoing-viewing-key pattern of note encryption, every notification carries a 105-byte `out_ct` that lets the **payer** recover the payment from seed alone: + +```text +ovk = H("DashPay/PayNotif/v1/ovk" || k_out) # k_out: key_class 2' private key +ock = H("DashPay/PayNotif/v1/ock" || ovk || ser(epk)) +out_pt = recipientIdentityId[32] || rail[1] || ref[32] || amount[8] || eskSeed[16] # 89 bytes +out_ct = ChaCha20-Poly1305(ock, nonce = 12 zero bytes, aad = cm, out_pt) # 105 bytes +``` + +`ref` is `txid` (rails 0/1), zero (rail 2 — the address is re-derivable from `eskSeed`), or `stHash` (rail 3). A restoring sender scans the store trial-decrypting `out_ct` with `ock` candidates derived from each entry's `epk`, recovering whom they paid, how much, and — via `eskSeed` — the ephemeral keys. + +#### Anonymous variant + +A payer MAY withhold attribution by setting `senderIdentityId`, `senderKeyIndex`, and `sig` to all zeros. Clients MUST render such payments as anonymous. This variant is only meaningful with pool funding (below); an identity-funded document names its creator in the envelope regardless of payload contents. + +#### Funding modes + +* **Identity-funded** (default): the notification document is created by an ordinary, fee-paying document create signed by the payer identity. Third parties learn that the payer created *a* payment notification at some time — not its recipient, amount, or rail. +* **Pool-funded**: the document creation fee is paid from the Platform shielded pool with no owning identity, following the pool-funded private document mechanism. Third parties learn nothing about the payer. Combined with rail 2, `fundingKind = 1` (unshielding), and the anonymous variant, this yields a payment with no payer identity anywhere in the system while remaining recoverable by the recipient. + +The entry bytes are identical in both modes; attribution always comes from the inner signature, never the envelope. + +#### Publication ordering + +For one-time address payments (rails 0 and 2), the payer MUST publish the payment notification and observe its inclusion in a finalized Platform block **before** broadcasting the settlement transaction. One-time outputs are unrecoverable from the recipient's seed alone — the notification is the recovery record, not optional metadata. A notification that is never followed by settlement is harmless (recipients verify settlement and ignore dangling notifications); settlement without a notification is unrecoverable except by manual intervention. + +For public-address payments (rails 1 and 3) the notification is attribution only and MAY be published at any time relative to settlement. + +Clients SHOULD introduce a randomized delay between notification finality and settlement broadcast to weaken timing correlation by global observers. + +### Client Behavior + +* **Scanning**: clients periodically fetch new store entries by position-range reads with proofs, maintaining a persistent cursor per store, and trial-decrypt each entry once per registered scan key. Wallets managing multiple identities or accounts perform one trial per (identity, entry). +* **Verification order** on decrypt success: recompute `cm`; verify `recipientIdentityId` is the scanning identity; verify `sig` (or mark anonymous); verify settlement on the rail; only then surface the payment. +* **Deduplication**: duplicate notifications for the same settlement (same `txid`/`vout`, same derived platform address, or same `stHash`) are valid; clients MUST deduplicate after decryption. +* **Public-address monitoring**: clients watching a profile's `corePaymentAddress` use normal Core-chain means (e.g. compact block filters); clients watching a `platformPaymentAddress` poll its balance and nonce with state proofs. +* **Spend watching**: a detected Core one-time output is added to the client's filter set to observe its spend; a detected Platform one-time address is watched by nonce. +* **Restore from seed**: derive all keys (including past `index'` rotations); replay the notification store from position zero (recovering received payments via `b_scan` and sent payments via `ock`); then verify settlements — Platform addresses by direct state query, Core outputs by fetching the referenced transactions, bounded by `coreHeightHint` where a rescan is needed. +* **Watch-only delegation**: the scan private key MAY be given to a semi-trusted watch service, which can then detect notifications and derive (but not spend) one-time addresses. The spend key and out-key MUST NOT be delegated for watch purposes. + +## 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. +* **Detection keys as identity keys rather than document fields.** Identity key changes require master-key authorization, whereas profile documents update under lower-security keys — and the detection keys gate every future notified payment, making them the highest-value target in this specification. Registry placement also yields on-chain revocation (key disabling), makes every identity payable without a DashPay profile, and keeps the keys inside future identity-key infrastructure. Contract bounds are deliberately absent: this is protocol-wide payment capability, not an application binding. +* **Notification documents rather than BIP-352-style scanning.** Silent-payments detection requires, per transaction, the input public keys — data that is absent from compact block filters (which commit only output and prevout scripts) and absent from Platform address-transfer state. Worse, the output script being sought is unknowable before performing the per-transaction ECDH, so filters cannot shortlist candidates: a scanning recipient must download and process every transaction forever, which is untenable for light clients. A Platform-published notification inverts the problem — the recipient learns the exact transaction (or derives the exact address) and verifies it directly with proofs. +* **Notification documents rather than BIP-47-style notification transactions.** An on-chain notification transaction is itself public: observers see that someone new is paying the recipient, and the notification inputs link payer to recipient unless specially prepared coins are burned. It also spends Core chain fees and block space on what is pure messaging, which Platform documents handle with encryption, uniform-size records, and provable retrieval. +* **One shared detection key pair across rails.** Both rails are secp256k1/HASH160 systems; the `rail` byte in the tweak hash keeps their one-time address spaces disjoint. Separate per-rail key pairs would double registry footprint and trial-decryption cost for no privacy gain. +* **Distinct `epk` and `R`.** Notification decryption capability and address derivation material stay independent, so a watch service holding `b_scan` can be reasoned about cleanly; the cost is 33 bytes per entry. +* **Fixed-size entries.** Uniform records prevent size-based traffic analysis of the notification store and enable simple position arithmetic for paginated reads. + +## Backwards Compatibility + +* The profile fields are optional additions at fresh positions; existing profiles remain valid and older clients ignore the new fields. +* The new key purposes and the `paymentNotification` document type activate with a Platform protocol version upgrade; earlier protocol versions reject them, and non-upgraded clients simply never scan the store. +* No changes are made to DIP-15 contact-request flows; established contacts continue using per-contact derived chains. The notified tier is complementary and MAY also be used between established contacts. +* Core chain consensus is unchanged; one-time outputs are ordinary P2PKH outputs. + +## Security Considerations + +* **Detection key integrity.** The scan/spend keys can only be changed via identity updates under master-key authorization. Wallets SHOULD additionally alert users when a contact's detection keys or profile payment addresses change, and pin previously seen values. +* **Profile address hijacking.** Profile documents update under lower-security keys; a compromised profile key can redirect *public-tier* payments (bounded, per-payment loss). Clients SHOULD surface address changes prominently. The notified tier is unaffected by profile compromise. +* **Spoofed attribution.** Anyone can publish a notification claiming any `senderIdentityId`; attribution is only as good as the inner signature, which recipients MUST verify against the sender identity's registered keys. `recipientIdentityId` inside the signed payload prevents replaying a valid notification to a different recipient. +* **Dangling and mismatched notifications.** Recipients MUST verify settlement before crediting anything; notifications are hints, not money. +* **Ephemeral reuse.** Reusing `eskSeed` (hence `r`) across payments links those payments to each other at the payer's fault; implementations MUST source `eskSeed` from a cryptographically secure generator per payment. +* **One-time key exposure at spend.** As with all hash-of-key outputs, `P_n` becomes public when spent. This has no effect on unrelated payments: each `t_n` is bound to a distinct `(S, R, rail, n)`. + +## 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 the notified tier hides.** Third parties observing both chains and the notification store see: an opaque fixed-size store entry (created by the payer identity, or by nobody when pool-funded), and a settlement paying a never-before-seen address. Neither names the recipient; amounts are visible on-chain but not attributable to the recipient's published identity. +* **Payer-side linkage on the Core rail.** The payer's transaction inputs remain subject to ordinary chain analysis; this DIP hides the *recipient*, not the payer's coin history. Payers wanting input-side privacy should fund stealth payments from CoinJoin-mixed coins. +* **Payer-side linkage on the Platform rail.** Address funds transfers expose their input addresses. Paying by unshielding from the Platform shielded pool (`fundingKind = 1`) severs payer-side linkage entirely; combined with pool funding of the notification and the anonymous variant, no payer-identifying information exists anywhere. Non-normative hygiene recommendations: fund from balances shielded well in advance, avoid amount-identical shield-then-unshield sequences, and prefer round or denomination-friendly amounts. +* **Notification timing.** An identity-funded notification reveals that the payer was active at a point in time. The mandated notification-before-settlement ordering plus a randomized settlement delay weakens correlation between the two events. +* **Trial-decryption metadata.** Recipients fetch store ranges wholesale, so read patterns do not reveal which entries decrypted successfully. + +## Future Extensions + +* **Shielded pool tier.** A follow-up specification will extend the profile with a shielded-pool payment address, extend DIP-15 contact requests with per-contact diversified shielded addresses, and register shielded memo kinds for payment association (rail code `4` and memo kinds `2`/`3` are reserved for it). Shielded payments natively provide recipient unlinkability and trial-decryption detection, so no stealth machinery applies there. +* **Silent-payments-style Core scanning.** The published detection key pair is deliberately shaped like a BIP-352 key pair. A future extension MAY define a notification-less Core payment mode in which network infrastructure serves per-transaction input-key tweaks (a "tweak index") that light clients scan with `b_scan`. Such a mode changes detection economics only; addresses derived under it MUST remain compatible with the derivation of this DIP. This paragraph is a reservation, not a commitment. + +## Reference Implementation + +The following pseudo-code is normative for paying a recipient over the notified tier: + +```text +function pay_notified(rail, recipient, amount, memo, network): + # recipient: identity id; fetch identity with proof + keys = fetch_identity_keys(recipient) + B_scan = keys.active(PAYMENT_SCAN) # 33-byte compressed + B_spend = keys.active(PAYMENT_SPEND) + + eskSeed = random_bytes(16) + esk = int(H("DashPay/PayNotif/v1/esk" || eskSeed)) mod order + r = int(H("DashPay/Stealth/v1/r" || eskSeed)) mod order + + S = r * B_scan + t0 = int(H("DashPay/Stealth/v1" || ser(S) || ser(r*G) || rail_byte(rail) || LE32(0))) mod order + P0 = B_spend + t0 * G + dest = p2pkh(HASH160(ser(P0)), rail, network) # Core P2PKH or DIP-18 platform address + + tx = build_unsigned_settlement(rail, dest, amount) + body = build_body(rail, sender_identity, recipient, locator(tx), amount, memo) + sig = sign_recoverable(sender_auth_key, sig_digest(body)) + entry = encrypt_entry(B_scan, eskSeed, body_with(sig)) # cm || epk || body_ct || out_ct + + publish_and_await_finality(entry) # MUST precede settlement broadcast + broadcast(tx) +``` + +## Test Vectors + +The vectors below exercise the one-time address derivation with fixed test-only scalars. All hex is lowercase. Scalars are 32-byte big-endian; points are compressed SEC1. (BIP-32 path vectors for feature `33'` will be added once tooling lands; the derivation below is independent of the HD paths.) + +Inputs: + +```text +b_scan = 61aafd85dbca17133515038343b05ed2019ca465d1ba93dcbffbf9534d2f436c +B_scan = 0218ee61cb2070d8c63456b2acd9cb0243e4a4ad0791c272e79c0eb8de88284448 +b_spend = 8f54f1cfa054739a1d4a87847bdf6a024caf78ff4b2c91aeb75cb843280957ff +B_spend = 0397b4690ee29e51d7da3186a683a563394981832f8860e405f8827bfd5dfbd7f6 +r = 37ca08877b0beea1b1e7655649274689e89944edae537ecad42764e59ac31e3e +R = 029d683d939d3bb5527ccc96d7ecd4e77274a530b4496fed79f08db99c8fe6e44e +S = 031218547890897c32cbc802902b8b8257cbcbd3cee2f9c393b7e4a74cb635b36e +``` + +| Rail | n | `t_n` | `P_n` | `p_n` | `HASH160(P_n)` | +| ---- | - | ----- | ----- | ----- | -------------- | +| Core (`0x00`) | 0 | `2fe3ccd4f560aadda6cedb4f4b9e0927ecbdf49c4864ba0bfbdb174a80e94cdf` | `029459124e206a8113bdb4c862794788380bae4b1d8bac57e561a6c88eb4eb96f1` | `bf38bea495b51e77c41962d3c77d732a396d6d9b93914bbab337cf8da8f2a4de` | `f7a5c7a50c7c8ef61223d139ae791a9360fe2262` | +| Core (`0x00`) | 1 | `e0867aad0724465861061aa9d89bf7cb71056bad6888b119ca5b4579f3663cf8` | `03356bc4f9ef4256c306872ab072bdef1e317396f2ae04730c6f252e9a02a2c039` | `6fdb6c7ca778b9f27e50a22e547b61cf030607c6046ca28cc1e59f304b3953b6` | `c4566e2150b8fa2f2854aef250962d4117afbb9f` | +| Platform (`0x02`) | 0 | `5bba1d655f05b3b9b91561f818c3d8091863148ed28f129b10e959e8a0847756` | `03b45786021b88c3b0e8730a7164630e75eea49b13fe1fd304c97075103f6fa40e` | `eb0f0f34ff5a2753d65fe97c94a3420b65128d8e1dbba449c846122bc88dcf55` | `7ca4b1b60c05c4fcd606d7c9821ee13eb70652bd` | + +Verification identities: `S = b_scan·R = r·B_scan`; `P_n = B_spend + t_n·G = p_n·G`. + +## DIP-9 Registry Update + +Reserve [DIP-9](dip-0009.md) feature index `33'` for "DashPay Payment Detection Keys": + +| Feature Index | Feature | DIP | Note | +| ------------- | ------- | --- | ---- | +| `33'` | DashPay Payment Detection Keys | DIP-33 | Sub-path: `33'/account'/key_class'/index'` — key_class `0'` scan, `1'` spend, `2'` notification out-key (default account' = `0'`, index' = `0'`) | + +## 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 From 0fdee781cccfeb75aa5be9d0b9513e9960ab3023 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 3 Aug 2026 08:41:25 +0700 Subject: [PATCH 2/5] Address review: normative scalar encoding, sig binding, amount binding, rotation scanning * Define int_be / ser_scalar and zero-scalar regeneration normatively * Bind sig preimage to senderIdentityId + senderKeyIndex; verify against key state at notification creation (disabled-after counts as valid) * Settled value is authoritative; signed amount must match for verified attribution; rail 2 gains an optional stHash settlement locator * Anonymous variant must be pool-funded; clarify inner vs envelope attribution * Scan-key rotation: retain disabled keys, grace-period dual scanning * Document store admission economics and deliberate non-partitioning * Guard pay_notified pseudo-code to notified rails only * Couple the DIP-11 purpose registry update to the same activation rule Co-Authored-By: Claude Fable 5 --- dip-0033.md | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/dip-0033.md b/dip-0033.md index 42ae7aa6..bb164bb4 100644 --- a/dip-0033.md +++ b/dip-0033.md @@ -164,7 +164,7 @@ m / 9' / coin_type' / 33' / account' / key_class' / index' ### One-Time Address Derivation -All points are secp256k1, serialized in 33-byte compressed SEC1 form (`ser(·)`). `H(·)` is SHA-256. `order` is the secp256k1 group order. Let `B_scan` and `B_spend` be the recipient's active `PAYMENT_SCAN` and `PAYMENT_SPEND` keys. +All points are secp256k1, serialized in 33-byte compressed SEC1 form (`ser(·)`). `H(·)` is SHA-256. `order` is the secp256k1 group order. `int_be(·)` interprets a 32-byte digest as a big-endian unsigned integer; `ser_scalar(·)` encodes a scalar as 32 big-endian bytes. Every scalar derived as `int_be(H(...)) mod order` in this DIP MUST be rejected (and its generating seed regenerated) if it equals zero. Let `B_scan` and `B_spend` be the recipient's active `PAYMENT_SCAN` and `PAYMENT_SPEND` keys. Rail domain-separation bytes: @@ -180,7 +180,7 @@ Rail domain-separation bytes: 3. For each output `n` (`n = 0, 1, ...` for multiple outputs to the same recipient within one payment): ```text - t_n = int(H("DashPay/Stealth/v1" || ser(S) || ser(R) || rail || LE32(n))) mod order + t_n = int_be(H("DashPay/Stealth/v1" || ser(S) || ser(R) || rail || LE32(n))) mod order ``` If `t_n = 0`, or if step 4 yields the point at infinity, the payer MUST restart with a fresh `r`. @@ -197,6 +197,7 @@ An ephemeral scalar MUST NOT be reused across payments. The `rail` byte makes Co 2. Recompute `t_n` and `P_n` and check that the referenced output pays `HASH160(ser(P_n))`: * Core rail: fetch the transaction named by the notification's `txid` and verify the output script (light clients verify with a merkle proof). * Platform rail: derive the platform address and fetch its balance and nonce with a state proof. +3. Determine the settled value from the chain, not the notification: the Core output value, or the proven platform address balance (a one-time platform address receives exactly one payment by construction, so its first credited balance is the payment). The settled value is authoritative for crediting. The notification's signed `amount` field MUST equal the settled value for the payment to be displayed as *verified attributed*; on mismatch, clients MUST credit the settled value but treat the attribution and memo as unverified. **Spending**: the one-time private key is `p_n = (b_spend + t_n) mod order`. On the Core rail this signs a standard P2PKH input. On the Platform rail it produces the address witness signature for an address funds transfer or shield input. In both cases `P_n` becomes public at spend time, as is normal for hash-of-key outputs. @@ -210,6 +211,8 @@ The DashPay data contract gains a `paymentNotification` document type declared * Entries are immutable and never deleted. This is load-bearing: the notification store is the durable record from which stealth outputs are recovered after a seed restore. +Admission to the store is economically bounded: every entry pays a document creation fee (identity-funded or pool-funded — there is no free write path), so append-only growth is priced, not free. The store is deliberately **not** partitioned by recipient — any recipient-scoped read pattern would reveal recipient activity to the serving node, defeating the purpose. Client scan work therefore grows with global store throughput: one trial decryption per entry per scan key. This is the same economics as shielded note scanning, and it is mitigated by fixed-size entries, chunk-aligned range reads with proofs, and scan-key delegation to a watch service ([Client Behavior](#client-behavior)). Any additional platform-side quotas on the store are outside the scope of this DIP. + #### Entry format Each entry is exactly **586 bytes**: @@ -242,7 +245,7 @@ Rail codes and rail bodies: | ---- | ---- | ---------------- | | `0` | Core, one-time address | `txid[32] \|\| vout[4] \|\| R[33] \|\| amount[8] \|\| coreHeightHint[4]` — amount in duffs | | `1` | Core, public profile address | `txid[32] \|\| vout[4] \|\| amount[8]` — attribution/memo for a payment to `corePaymentAddress` | -| `2` | Platform, one-time address | `R[33] \|\| outputCounter[4] \|\| amount[8] \|\| fundingKind[1]` — amount in credits; `fundingKind`: `0` = address funds transfer, `1` = unshielding. No transaction locator is needed: the derived address is the locator | +| `2` | Platform, one-time address | `R[33] \|\| outputCounter[4] \|\| amount[8] \|\| fundingKind[1] \|\| stHash[32]` — amount in credits; `fundingKind`: `0` = address funds transfer, `1` = unshielding. The derived address is the primary locator; `stHash` (all-zeros when absent) SHOULD name the crediting state transition to bind the exact settlement | | `3` | Platform, public profile address | `platformAddress[21] \|\| amount[8] \|\| stHash[32]` — amount in credits; `stHash` identifies the crediting state transition | | `4` | Reserved | Shielded pool tier (future extension) | @@ -250,10 +253,13 @@ The signature covers: ```text sig = ECDSA_sign_recoverable(senderKey, - H("DashPay/PayNotif/v1/sig" || rail || recipientIdentityId || railBody || memoLen || memo)) + H("DashPay/PayNotif/v1/sig" || senderIdentityId || LE32(senderKeyIndex) || + rail || recipientIdentityId || railBody || memoLen || memo)) ``` -Recipients MUST verify `sig` against an active authentication-capable key of `senderIdentityId` (fetched with proof) before attributing the payment. Notifications whose signature does not verify MUST be treated as anonymous at best and SHOULD be discarded. +Including `senderIdentityId` and `senderKeyIndex` in the preimage binds the signature to the claimed identity: a signature cannot be re-attributed to another identity that happens to have registered the same public key. + +Recipients MUST verify `sig` against the key at `senderKeyIndex` of `senderIdentityId` (fetched with proof). The key MUST be authentication-capable and MUST have been valid when the notification was created: either currently active, or disabled with a disabling time later than the entry's creation time (identity keys are disabled, never removed, so current identity state suffices for this check — sender key rotation does not retroactively invalidate attribution). Notifications whose signature does not verify MUST be treated as anonymous at best and SHOULD be discarded. #### Encryption @@ -261,8 +267,9 @@ Let `ScanPub` be the recipient's active `PAYMENT_SCAN` key. ```text eskSeed = 16 random bytes # the payment's root secret -esk = int(H("DashPay/PayNotif/v1/esk" || eskSeed)) mod order -r = int(H("DashPay/Stealth/v1/r" || eskSeed)) mod order # stealth ephemeral (rails 0 and 2) +esk = int_be(H("DashPay/PayNotif/v1/esk" || eskSeed)) mod order +r = int_be(H("DashPay/Stealth/v1/r" || eskSeed)) mod order # stealth ephemeral (rails 0 and 2) + # if esk = 0 or r = 0: discard eskSeed and draw a fresh one epk = esk·G shared = ser(esk·ScanPub) key = H("DashPay/PayNotif/v1/key" || shared || ser(epk) || ser(ScanPub)) @@ -277,24 +284,24 @@ The recipient trial-decrypts each entry with `key' = H(... || ser(b_scan·epk) | Mirroring the outgoing-viewing-key pattern of note encryption, every notification carries a 105-byte `out_ct` that lets the **payer** recover the payment from seed alone: ```text -ovk = H("DashPay/PayNotif/v1/ovk" || k_out) # k_out: key_class 2' private key +ovk = H("DashPay/PayNotif/v1/ovk" || ser_scalar(k_out)) # k_out: key_class 2' private key ock = H("DashPay/PayNotif/v1/ock" || ovk || ser(epk)) out_pt = recipientIdentityId[32] || rail[1] || ref[32] || amount[8] || eskSeed[16] # 89 bytes out_ct = ChaCha20-Poly1305(ock, nonce = 12 zero bytes, aad = cm, out_pt) # 105 bytes ``` -`ref` is `txid` (rails 0/1), zero (rail 2 — the address is re-derivable from `eskSeed`), or `stHash` (rail 3). A restoring sender scans the store trial-decrypting `out_ct` with `ock` candidates derived from each entry's `epk`, recovering whom they paid, how much, and — via `eskSeed` — the ephemeral keys. +`ref` is `txid` (rails 0/1), the rail body's `stHash` (rail 2, zero when absent — the address is re-derivable from `eskSeed`), or `stHash` (rail 3). A restoring sender scans the store trial-decrypting `out_ct` with `ock` candidates derived from each entry's `epk`, recovering whom they paid, how much, and — via `eskSeed` — the ephemeral keys. #### Anonymous variant -A payer MAY withhold attribution by setting `senderIdentityId`, `senderKeyIndex`, and `sig` to all zeros. Clients MUST render such payments as anonymous. This variant is only meaningful with pool funding (below); an identity-funded document names its creator in the envelope regardless of payload contents. +A payer MAY withhold attribution by setting `senderIdentityId`, `senderKeyIndex`, and `sig` to all zeros. Clients MUST render such payments as anonymous. Anonymity here is a property of the inner payload only; the storage envelope is separate ([Funding modes](#funding-modes)). Because an identity-funded document names its creator in the envelope regardless of payload contents, the anonymous variant MUST be pool-funded — clients MUST NOT create identity-funded notifications with an anonymous payload. #### Funding modes * **Identity-funded** (default): the notification document is created by an ordinary, fee-paying document create signed by the payer identity. Third parties learn that the payer created *a* payment notification at some time — not its recipient, amount, or rail. * **Pool-funded**: the document creation fee is paid from the Platform shielded pool with no owning identity, following the pool-funded private document mechanism. Third parties learn nothing about the payer. Combined with rail 2, `fundingKind = 1` (unshielding), and the anonymous variant, this yields a payment with no payer identity anywhere in the system while remaining recoverable by the recipient. -The entry bytes are identical in both modes; attribution always comes from the inner signature, never the envelope. +The entry bytes are identical in both modes. *Verified* attribution comes only from the inner signature — recipients never treat the envelope as proof of who paid — but the creator of an identity-funded entry is nonetheless public metadata visible to everyone, which is precisely why the anonymous variant requires pool funding. #### Publication ordering @@ -306,7 +313,8 @@ Clients SHOULD introduce a randomized delay between notification finality and se ### Client Behavior -* **Scanning**: clients periodically fetch new store entries by position-range reads with proofs, maintaining a persistent cursor per store, and trial-decrypt each entry once per registered scan key. Wallets managing multiple identities or accounts perform one trial per (identity, entry). +* **Scanning**: clients periodically fetch new store entries by position-range reads with proofs, maintaining a persistent cursor per store, and trial-decrypt each entry once per scan key. Wallets managing multiple identities or accounts perform one trial per (identity, entry). +* **Scan-key rotation**: payers may act on a stale view of the recipient's identity, so rotation must not orphan in-flight notifications. Clients MUST retain disabled detection key pairs (they are seed-derived at earlier `index'` values), MUST continue trial-decrypting new entries with the most recently disabled scan key in addition to the active one for a client-defined grace period after rotation, and MUST include all historical scan keys when replaying the store from position zero on restore. Payers SHOULD refresh the recipient's identity shortly before paying. * **Verification order** on decrypt success: recompute `cm`; verify `recipientIdentityId` is the scanning identity; verify `sig` (or mark anonymous); verify settlement on the rail; only then surface the payment. * **Deduplication**: duplicate notifications for the same settlement (same `txid`/`vout`, same derived platform address, or same `stHash`) are valid; clients MUST deduplicate after decryption. * **Public-address monitoring**: clients watching a profile's `corePaymentAddress` use normal Core-chain means (e.g. compact block filters); clients watching a `platformPaymentAddress` poll its balance and nonce with state proofs. @@ -327,7 +335,7 @@ Clients SHOULD introduce a randomized delay between notification finality and se ## Backwards Compatibility * The profile fields are optional additions at fresh positions; existing profiles remain valid and older clients ignore the new fields. -* The new key purposes and the `paymentNotification` document type activate with a Platform protocol version upgrade; earlier protocol versions reject them, and non-upgraded clients simply never scan the store. +* The new key purposes and the `paymentNotification` document type activate with a Platform protocol version upgrade; earlier protocol versions reject them, and non-upgraded clients simply never scan the store. This includes extending the [DIP-11](dip-0011.md) identity public key purpose registry (and its schema/validators) with values `7` and `8` — the registry update and this DIP activate under the same protocol version rule. * No changes are made to DIP-15 contact-request flows; established contacts continue using per-contact derived chains. The notified tier is complementary and MAY also be used between established contacts. * Core chain consensus is unchanged; one-time outputs are ordinary P2PKH outputs. @@ -360,17 +368,18 @@ The following pseudo-code is normative for paying a recipient over the notified ```text function pay_notified(rail, recipient, amount, memo, network): + require rail in {0, 2} # public rails (1, 3) pay the profile address directly # recipient: identity id; fetch identity with proof keys = fetch_identity_keys(recipient) B_scan = keys.active(PAYMENT_SCAN) # 33-byte compressed B_spend = keys.active(PAYMENT_SPEND) - eskSeed = random_bytes(16) - esk = int(H("DashPay/PayNotif/v1/esk" || eskSeed)) mod order - r = int(H("DashPay/Stealth/v1/r" || eskSeed)) mod order + eskSeed = random_bytes(16) # redraw if any derived scalar is zero + esk = int_be(H("DashPay/PayNotif/v1/esk" || eskSeed)) mod order + r = int_be(H("DashPay/Stealth/v1/r" || eskSeed)) mod order S = r * B_scan - t0 = int(H("DashPay/Stealth/v1" || ser(S) || ser(r*G) || rail_byte(rail) || LE32(0))) mod order + t0 = int_be(H("DashPay/Stealth/v1" || ser(S) || ser(r*G) || rail_byte(rail) || LE32(0))) mod order P0 = B_spend + t0 * G dest = p2pkh(HASH160(ser(P0)), rail, network) # Core P2PKH or DIP-18 platform address From 1b04d3b98eb8c4bf0c3cc5d8024e09f3a6d03565 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 3 Aug 2026 15:16:56 +0700 Subject: [PATCH 3/5] Drop the Base58 pattern from corePaymentAddress The character-class pattern could not prove the checksum or network byte, so clients had to fully validate the address regardless. Enforcing it on chain only bought a partial check while making every optional-field document generator produce schema-invalid profiles. Consensus now constrains the length only; full validation is explicitly a client responsibility. Co-Authored-By: Claude Fable 5 --- dip-0033.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dip-0033.md b/dip-0033.md index bb164bb4..72ec9ede 100644 --- a/dip-0033.md +++ b/dip-0033.md @@ -98,7 +98,6 @@ The DashPay data contract's `profile` document type gains two OPTIONAL fields. F ```json "corePaymentAddress": { "type": "string", - "pattern": "^[1-9A-HJ-NP-Za-km-z]{26,34}$", "minLength": 26, "maxLength": 34, "position": 5, @@ -116,7 +115,7 @@ The DashPay data contract's `profile` document type gains two OPTIONAL fields. F Normative requirements: -* `corePaymentAddress` MUST be a valid Base58Check Core chain address for the network the contract is deployed on. Clients MUST validate the checksum and network byte before paying. +* `corePaymentAddress` MUST be a valid Base58Check Core chain address for the network the contract is deployed on. Consensus constrains only the string length: a Base58 character-class pattern would still not prove the checksum or network byte, so validation of the address proper is a client responsibility and is not duplicated on chain. Clients MUST validate the checksum and network byte before paying, and MUST treat an undecodable value as no address rather than as an error in the profile. * `platformPaymentAddress` MUST use the [DIP-18](dip-0018.md) storage serialization (variant byte `0x00` for P2PKH or `0x01` for P2SH, followed by the 20-byte HASH160). Clients MUST display it in the [DIP-18](dip-0018.md) bech32m encoding. * 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. From 18e0756bfeeab16432ffa4f197941f24c6b482f4 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 12 Aug 2026 18:21:09 +0700 Subject: [PATCH 4/5] Store corePaymentAddress as 21-byte storage form, symmetric with platformPaymentAddress The Base58Check string form was rejected: consensus could not prove the checksum or network byte anyway, the byte form is exact-size-validated and smaller, and omitting the network byte makes a network mismatch unrepresentable (the network follows the Platform chain the document lives on). --- dip-0033.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dip-0033.md b/dip-0033.md index 72ec9ede..30b83fb9 100644 --- a/dip-0033.md +++ b/dip-0033.md @@ -97,11 +97,12 @@ The DashPay data contract's `profile` document type gains two OPTIONAL fields. F ```json "corePaymentAddress": { - "type": "string", - "minLength": 26, - "maxLength": 34, + "type": "array", + "byteArray": true, + "minItems": 21, + "maxItems": 21, "position": 5, - "description": "Base58Check P2PKH/P2SH Core chain address for public payments" + "description": "Core chain address in storage form (type byte 0x00 P2PKH / 0x01 P2SH followed by 20-byte HASH160) for public payments" }, "platformPaymentAddress": { "type": "array", @@ -115,7 +116,7 @@ The DashPay data contract's `profile` document type gains two OPTIONAL fields. F Normative requirements: -* `corePaymentAddress` MUST be a valid Base58Check Core chain address for the network the contract is deployed on. Consensus constrains only the string length: a Base58 character-class pattern would still not prove the checksum or network byte, so validation of the address proper is a client responsibility and is not duplicated on chain. Clients MUST validate the checksum and network byte before paying, and MUST treat an undecodable value as no address rather than as an error in the profile. +* `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 validate the variant byte, MUST render the address in Base58Check with the version byte of the network they are on, and MUST treat a value with an unknown variant byte as no address rather than as an error in the profile. Consensus constrains only the byte length; 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 (variant byte `0x00` for P2PKH or `0x01` for P2SH, followed by the 20-byte HASH160). Clients MUST display it in the [DIP-18](dip-0018.md) bech32m encoding. * 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. From fec74e33da6b5043da33645a33b5bb41bba83dc4 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 13 Aug 2026 01:11:56 +0700 Subject: [PATCH 5/5] Rescope DIP-33 to payment addresses: public tier + shielded tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notified stealth-address tier is deferred and recorded in an informational appendix. Any payment-time Platform artifact (the notification that stealth discovery depended on) is timing-correlatable with its settlement transaction — fatally for identity-funded notifications, and still marking settlements as private payments when pool-funded. The correlation-free alternative (a BIP-352-style tweak index) is real infrastructure, and the shielded pool already exceeds the tier's goals: it hides amounts, needs no discovery artifact, and recovers from seed plus chain alone. The new shielded tier standardizes the profile shieldedAddress field, per-contact diversified addresses in contact requests, and the PaymentRef memo kind. Detection keys, stealth derivation, notification format, and vectors remain in project history for a future revival paired with a tweak index; the DIP-9 feature index 33' reservation is withdrawn. --- dip-0033.md | 396 ++++++++++++++-------------------------------------- 1 file changed, 102 insertions(+), 294 deletions(-) diff --git a/dip-0033.md b/dip-0033.md index 30b83fb9..d75cf7d9 100644 --- a/dip-0033.md +++ b/dip-0033.md @@ -1,6 +1,6 @@
   DIP: 0033
-  Title: DashPay Payment Addresses and Payment Notifications
+  Title: DashPay Payment Addresses
   Author(s): Samuel Westrich
   Special-Thanks: Dash Platform Team
   Comments-Summary: No comments yet.
@@ -8,7 +8,7 @@
   Type: Standard
   Created: 2026-08-03
   License: MIT License
-  Requires: 9, 11, 15, 17, 18
+  Requires: 15, 17, 18
   Replaces: -
   Superseded-By: -
 
@@ -22,74 +22,70 @@ 1. [Specification](#specification) 1. [Overview](#overview) 1. [Profile Payment Address Fields](#profile-payment-address-fields) - 1. [Payment Detection Keys](#payment-detection-keys) - 1. [HD Derivation](#hd-derivation) - 1. [One-Time Address Derivation](#one-time-address-derivation) - 1. [Payment Notifications](#payment-notifications) + 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. [Reference Implementation](#reference-implementation) -1. [Test Vectors](#test-vectors) -1. [DIP-9 Registry Update](#dip-9-registry-update) 1. [Copyright](#copyright) ## Abstract -This DIP extends DashPay with payment addresses and private payment detection across two payment rails: the Dash Core chain and Dash Platform payment addresses ([DIP-17](dip-0017.md)/[DIP-18](dip-0018.md)). It defines two tiers: +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, allowing anyone to pay a profile with zero interaction. Payments to these addresses are publicly linkable to the profile by design. -* A **notified tier**: the payer derives a fresh one-time (stealth) address from two detection keys published in the recipient's identity key set, pays it, and publishes an encrypted **payment notification** document that only the recipient can detect by trial decryption. Third parties cannot link the payment to either party; the recipient learns the payer's identity (unless the payer explicitly chooses the anonymous variant). +* 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. -The same secp256k1 detection key pair serves both rails. A new [DIP-9](dip-0009.md) feature index `33'` is registered for the detection keys, and two new Platform identity key purposes (`PAYMENT_SCAN`, `PAYMENT_SPEND`) are introduced. A shielded-pool tier is reserved for a future extension. +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, and no way for the recipient to detect such a payment without prohibitive scanning costs on light clients. +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 viable machinery: static profile addresses for the public case, and stealth one-time addresses plus Platform-published encrypted notifications for the private case. Platform's replicated document storage turns the classical stealth-address detection problem (scan every transaction) into a cheap, deterministic, proof-friendly lookup that works on light clients. +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-0009: Feature Derivation Paths](dip-0009.md) * [DIP-0011: Identities](dip-0011.md) -* [DIP-0013: Identities in Hierarchical Deterministic Wallets](dip-0013.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 Rationale) -* [BIP-0352: Silent Payments](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki) (rejected approach; see Rationale) -* Stealth addresses as originally described by Peter Todd and used by CryptoNote-family protocols +* [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 covers the **Core rail** (Dash Core chain transactions) and the **Platform rail** (Platform payment addresses per DIP-17/18, moved by address funds transfers and credited by unshielding from the Platform shielded pool). -* **Public tier**: payment targets published in the DashPay profile document; payments to them are publicly linkable to the profile. -* **Notified tier**: payments to one-time addresses announced by encrypted payment notifications; unlinkable by third parties. -* **Detection keys**: the recipient's published scan and spend public keys (`PAYMENT_SCAN`, `PAYMENT_SPEND` identity keys). -* **One-time address**: a payer-derived address spendable only by the recipient, appearing on-chain with no link to the recipient's published keys. -* **Payment notification**: an encrypted document published on Platform announcing a payment to its recipient. +* **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 two kinds of payment information: +A recipient publishes up to three payment targets: -1. **Addresses** in the profile document (public tier): a Core chain address and/or a Platform payment address. -2. **Detection keys** in the identity key set (notified tier): a scan key and a spend key, one pair covering both rails. +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). -A payer paying the notified tier performs, in order: derive a one-time address ([One-Time Address Derivation](#one-time-address-derivation)); publish a payment notification encrypted to the recipient's scan key ([Payment Notifications](#payment-notifications)); await Platform finality of the notification; broadcast the settlement transaction. The recipient's client discovers payments by trial-decrypting the notification stream, then verifies settlement on the relevant rail. +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 balances is obtained by moving value through Platform payment addresses and the shielded pool. +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 @@ -116,313 +112,125 @@ The DashPay data contract's `profile` document type gains two OPTIONAL fields. F 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 validate the variant byte, MUST render the address in Base58Check with the version byte of the network they are on, and MUST treat a value with an unknown variant byte as no address rather than as an error in the profile. Consensus constrains only the byte length; 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 (variant byte `0x00` for P2PKH or `0x01` for P2SH, followed by the 20-byte HASH160). Clients MUST display it in the [DIP-18](dip-0018.md) bech32m encoding. +* `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. A payer MAY additionally publish a payment notification (rail codes 1 or 3, below) to attribute the payment and attach a memo. +* Payments to these addresses carry no sender attribution by themselves. -### Payment Detection Keys +### Profile Shielded Address -Two new Platform identity key purposes are defined, extending the purpose registry of [DIP-11](dip-0011.md) identities: +The `profile` document type gains one further OPTIONAL field: -| Purpose | Value | Meaning | -| ------- | ----- | ------- | -| `PAYMENT_SCAN` | `7` | Detection key: ECDH target for one-time address detection and for payment notification decryption | -| `PAYMENT_SPEND` | `8` | Spend base key for one-time addresses | - -Normative requirements: - -* Key type MUST be `ECDSA_SECP256K1`, with the full 33-byte compressed public key stored as the key data. -* Keys of these purposes MUST NOT be usable to sign state transitions or documents (they are non-signing, like encryption/decryption purpose keys). -* Keys of these purposes MUST NOT carry contract bounds. The capability they express is protocol-wide, not application-scoped. -* An identity MUST have **at most one active key of each of these purposes**. Registration of a second active key of the same purpose is invalid. Rotation is performed by disabling the existing key and adding a replacement in the same identity update. -* An identity that publishes one of the pair SHOULD publish both; the notified tier requires both. - -Placing these keys in the identity key set (rather than in a document) means changes require the identity's master-key authorization, gives first-class on-chain revocation via key disabling, and makes any identity payable through the notified tier even if it has never created a DashPay profile. - -### HD Derivation - -The detection keys are derived under [DIP-9](dip-0009.md) feature index `33'`: - -```text -m / 9' / coin_type' / 33' / account' / key_class' / index' -``` - -* `coin_type'` MUST be `5'` on mainnet and `1'` on testnet/devnet/regtest, consistent with [DIP-17](dip-0017.md). -* `account'` MUST be hardened; `0'` is the default account. -* `key_class'` MUST be hardened and takes the following values: - -| key_class' | Key | -| ---------- | --- | -| `0'` | Scan key (`b_scan`, published as the `PAYMENT_SCAN` identity key) | -| `1'` | Spend key (`b_spend`, published as the `PAYMENT_SPEND` identity key) | -| `2'` | Notification out-key (`k_out`, sender-side recovery key; never published) | - -* `index'` MUST be hardened; `0'` is the default. Incrementing `index'` is the rotation mechanism: when a wallet rotates its detection keys it derives the next index and replaces the identity keys accordingly. On restore, wallets recover the active index by matching derived public keys against the identity's registered keys, trying indices in ascending order. -* All three key classes are hardened siblings: disclosure of the scan private key (see [Client Behavior](#client-behavior) on watch services) reveals nothing about the spend or out keys. - -### One-Time Address Derivation - -All points are secp256k1, serialized in 33-byte compressed SEC1 form (`ser(·)`). `H(·)` is SHA-256. `order` is the secp256k1 group order. `int_be(·)` interprets a 32-byte digest as a big-endian unsigned integer; `ser_scalar(·)` encodes a scalar as 32 big-endian bytes. Every scalar derived as `int_be(H(...)) mod order` in this DIP MUST be rejected (and its generating seed regenerated) if it equals zero. Let `B_scan` and `B_spend` be the recipient's active `PAYMENT_SCAN` and `PAYMENT_SPEND` keys. - -Rail domain-separation bytes: - -| Rail | `rail` byte | -| ---- | ----------- | -| Core chain | `0x00` | -| Platform payment addresses | `0x02` | - -**Payer**: - -1. Generate a fresh ephemeral scalar `r` (see [Payment Notifications](#payment-notifications) for the deterministic derivation from the notification seed) and compute `R = r·G`. -2. Compute the shared point `S = r·B_scan`. -3. For each output `n` (`n = 0, 1, ...` for multiple outputs to the same recipient within one payment): - - ```text - t_n = int_be(H("DashPay/Stealth/v1" || ser(S) || ser(R) || rail || LE32(n))) mod order - ``` - - If `t_n = 0`, or if step 4 yields the point at infinity, the payer MUST restart with a fresh `r`. -4. Compute the one-time public key `P_n = B_spend + t_n·G`. -5. Form the output: - * Core rail: a standard P2PKH output paying `HASH160(ser(P_n))`. - * Platform rail: the P2PKH platform address over `HASH160(ser(P_n))`, encoded per [DIP-18](dip-0018.md), used as an output of an address funds transfer or as the credited address of an unshielding operation. - -An ephemeral scalar MUST NOT be reused across payments. The `rail` byte makes Core and Platform one-time addresses distinct even under (faulty) ephemeral reuse. One-time outputs MUST NOT be script-hash outputs; stealth derivation is defined for P2PKH only. - -**Recipient** (given `R` from a payment notification): - -1. Compute `S = b_scan·R` (equal to `r·B_scan` by Diffie–Hellman). -2. Recompute `t_n` and `P_n` and check that the referenced output pays `HASH160(ser(P_n))`: - * Core rail: fetch the transaction named by the notification's `txid` and verify the output script (light clients verify with a merkle proof). - * Platform rail: derive the platform address and fetch its balance and nonce with a state proof. -3. Determine the settled value from the chain, not the notification: the Core output value, or the proven platform address balance (a one-time platform address receives exactly one payment by construction, so its first credited balance is the payment). The settled value is authoritative for crediting. The notification's signed `amount` field MUST equal the settled value for the payment to be displayed as *verified attributed*; on mismatch, clients MUST credit the settled value but treat the attribution and memo as unverified. - -**Spending**: the one-time private key is `p_n = (b_spend + t_n) mod order`. On the Core rail this signs a standard P2PKH input. On the Platform rail it produces the address witness signature for an address funds transfer or shield input. In both cases `P_n` becomes public at spend time, as is normal for hash-of-key outputs. - -Detection capability (deriving `t_n` and recognizing outputs) requires only `b_scan` and the public `B_spend`; spend capability requires `b_spend`. This separation is what allows watch-only delegation. - -### Payment Notifications - -#### Document type - -The DashPay data contract gains a `paymentNotification` document type declared **private**: entries are stored as uniform-size opaque records in an append-only store with no public indices and no owner-visible recipient. Recipients discover entries by paginated range reads over insertion positions and trial decryption. The storage element, its proofs, and the contract-level declaration vocabulary are specified by Dash Platform (private document stores); this DIP specifies the entry content. - -Entries are immutable and never deleted. This is load-bearing: the notification store is the durable record from which stealth outputs are recovered after a seed restore. - -Admission to the store is economically bounded: every entry pays a document creation fee (identity-funded or pool-funded — there is no free write path), so append-only growth is priced, not free. The store is deliberately **not** partitioned by recipient — any recipient-scoped read pattern would reveal recipient activity to the serving node, defeating the purpose. Client scan work therefore grows with global store throughput: one trial decryption per entry per scan key. This is the same economics as shielded note scanning, and it is mitigated by fixed-size entries, chunk-aligned range reads with proofs, and scan-key delegation to a watch service ([Client Behavior](#client-behavior)). Any additional platform-side quotas on the store are outside the scope of this DIP. - -#### Entry format - -Each entry is exactly **586 bytes**: - -| Field | Size | Description | -| ----- | ---- | ----------- | -| `cm` | 32 | Commitment binding the recipient key and plaintext (below) | -| `epk` | 33 | Payer's ephemeral public key for notification encryption (distinct from `R`) | -| `body_ct` | 416 | AEAD ciphertext of the 400-byte body plaintext | -| `out_ct` | 105 | Sender-recovery ciphertext (below) | - -Body plaintext, fixed 400-byte layout (all multi-byte integers little-endian; unused trailing bytes zero): - -| Field | Offset | Size | Description | -| ----- | ------ | ---- | ----------- | -| `version` | 0 | 1 | Payload version; this DIP defines version `1` | -| `rail` | 1 | 1 | Rail code (below) | -| `senderIdentityId` | 2 | 32 | Payer identity; all-zeros for the anonymous variant | -| `senderKeyIndex` | 34 | 4 | Identity key id that produced `sig`; zero when anonymous | -| `recipientIdentityId` | 38 | 32 | Intended recipient; binds the notification against cross-recipient replay | -| `railBody` | 70 | 81 | Rail-specific body (below), zero-padded to 81 bytes | -| `memoLen` | 151 | 1 | Memo byte length, `0..=180` | -| `memo` | 152 | 180 | UTF-8 memo, zero-padded | -| `sig` | 332 | 65 | Recoverable ECDSA signature (below); all-zeros when anonymous | -| reserved | 397 | 3 | MUST be zero | - -Rail codes and rail bodies: - -| Code | Rail | Rail body layout | -| ---- | ---- | ---------------- | -| `0` | Core, one-time address | `txid[32] \|\| vout[4] \|\| R[33] \|\| amount[8] \|\| coreHeightHint[4]` — amount in duffs | -| `1` | Core, public profile address | `txid[32] \|\| vout[4] \|\| amount[8]` — attribution/memo for a payment to `corePaymentAddress` | -| `2` | Platform, one-time address | `R[33] \|\| outputCounter[4] \|\| amount[8] \|\| fundingKind[1] \|\| stHash[32]` — amount in credits; `fundingKind`: `0` = address funds transfer, `1` = unshielding. The derived address is the primary locator; `stHash` (all-zeros when absent) SHOULD name the crediting state transition to bind the exact settlement | -| `3` | Platform, public profile address | `platformAddress[21] \|\| amount[8] \|\| stHash[32]` — amount in credits; `stHash` identifies the crediting state transition | -| `4` | Reserved | Shielded pool tier (future extension) | - -The signature covers: - -```text -sig = ECDSA_sign_recoverable(senderKey, - H("DashPay/PayNotif/v1/sig" || senderIdentityId || LE32(senderKeyIndex) || - rail || recipientIdentityId || railBody || memoLen || memo)) +```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" +} ``` -Including `senderIdentityId` and `senderKeyIndex` in the preimage binds the signature to the claimed identity: a signature cannot be re-attributed to another identity that happens to have registered the same public key. - -Recipients MUST verify `sig` against the key at `senderKeyIndex` of `senderIdentityId` (fetched with proof). The key MUST be authentication-capable and MUST have been valid when the notification was created: either currently active, or disabled with a disabling time later than the entry's creation time (identity keys are disabled, never removed, so current identity state suffices for this check — sender key rotation does not retroactively invalidate attribution). Notifications whose signature does not verify MUST be treated as anonymous at best and SHOULD be discarded. - -#### Encryption - -Let `ScanPub` be the recipient's active `PAYMENT_SCAN` key. - -```text -eskSeed = 16 random bytes # the payment's root secret -esk = int_be(H("DashPay/PayNotif/v1/esk" || eskSeed)) mod order -r = int_be(H("DashPay/Stealth/v1/r" || eskSeed)) mod order # stealth ephemeral (rails 0 and 2) - # if esk = 0 or r = 0: discard eskSeed and draw a fresh one -epk = esk·G -shared = ser(esk·ScanPub) -key = H("DashPay/PayNotif/v1/key" || shared || ser(epk) || ser(ScanPub)) -cm = H("DashPay/PayNotif/v1/cm" || ser(ScanPub) || body_plaintext) -body_ct = ChaCha20-Poly1305(key, nonce = 12 zero bytes, aad = cm, body_plaintext) -``` +Normative requirements: -The recipient trial-decrypts each entry with `key' = H(... || ser(b_scan·epk) || ser(epk) || ser(ScanPub))`, and on AEAD success MUST recompute `cm` from the plaintext and discard the entry on mismatch. Deriving `esk` and `r` from separate domain-separated hashes of one seed keeps notification-decryption capability and address-derivation material independent while letting the sender recover both from a single stored secret. (The commitment formula is provisional pending the Platform private document store specification; if that specification defines a store-level commitment, it governs.) +* 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. -#### Sender recovery (out-ciphertext) +### Per-Contact Shielded Addresses -Mirroring the outgoing-viewing-key pattern of note encryption, every notification carries a 105-byte `out_ct` that lets the **payer** recover the payment from seed alone: +The `contactRequest` document type gains one OPTIONAL field. Contact requests are immutable; the field is set at creation: -```text -ovk = H("DashPay/PayNotif/v1/ovk" || ser_scalar(k_out)) # k_out: key_class 2' private key -ock = H("DashPay/PayNotif/v1/ock" || ovk || ser(epk)) -out_pt = recipientIdentityId[32] || rail[1] || ref[32] || amount[8] || eskSeed[16] # 89 bytes -out_ct = ChaCha20-Poly1305(ock, nonce = 12 zero bytes, aad = cm, out_pt) # 105 bytes +```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" +} ``` -`ref` is `txid` (rails 0/1), the rail body's `stHash` (rail 2, zero when absent — the address is re-derivable from `eskSeed`), or `stHash` (rail 3). A restoring sender scans the store trial-decrypting `out_ct` with `ock` candidates derived from each entry's `epk`, recovering whom they paid, how much, and — via `eskSeed` — the ephemeral keys. - -#### Anonymous variant - -A payer MAY withhold attribution by setting `senderIdentityId`, `senderKeyIndex`, and `sig` to all zeros. Clients MUST render such payments as anonymous. Anonymity here is a property of the inner payload only; the storage envelope is separate ([Funding modes](#funding-modes)). Because an identity-funded document names its creator in the envelope regardless of payload contents, the anonymous variant MUST be pool-funded — clients MUST NOT create identity-funded notifications with an anonymous payload. - -#### Funding modes +Normative requirements: -* **Identity-funded** (default): the notification document is created by an ordinary, fee-paying document create signed by the payer identity. Third parties learn that the payer created *a* payment notification at some time — not its recipient, amount, or rail. -* **Pool-funded**: the document creation fee is paid from the Platform shielded pool with no owning identity, following the pool-funded private document mechanism. Third parties learn nothing about the payer. Combined with rail 2, `fundingKind = 1` (unshielding), and the anonymous variant, this yields a payment with no payer identity anywhere in the system while remaining recoverable by the recipient. +* 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`. -The entry bytes are identical in both modes. *Verified* attribution comes only from the inner signature — recipients never treat the envelope as proof of who paid — but the creator of an identity-funded entry is nonetheless public metadata visible to everyone, which is precisely why the anonymous variant requires pool funding. +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. -#### Publication ordering +### Shielded Memo Kinds -For one-time address payments (rails 0 and 2), the payer MUST publish the payment notification and observe its inclusion in a finalized Platform block **before** broadcasting the settlement transaction. One-time outputs are unrecoverable from the recipient's seed alone — the notification is the recovery record, not optional metadata. A notification that is never followed by settlement is harmless (recipients verify settlement and ignore dangling notifications); settlement without a notification is unrecoverable except by manual intervention. +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: -For public-address payments (rails 1 and 3) the notification is attribution only and MAY be published at any time relative to settlement. +| 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 | -Clients SHOULD introduce a randomized delay between notification finality and settlement broadcast to weaken timing correlation by global observers. +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 -* **Scanning**: clients periodically fetch new store entries by position-range reads with proofs, maintaining a persistent cursor per store, and trial-decrypt each entry once per scan key. Wallets managing multiple identities or accounts perform one trial per (identity, entry). -* **Scan-key rotation**: payers may act on a stale view of the recipient's identity, so rotation must not orphan in-flight notifications. Clients MUST retain disabled detection key pairs (they are seed-derived at earlier `index'` values), MUST continue trial-decrypting new entries with the most recently disabled scan key in addition to the active one for a client-defined grace period after rotation, and MUST include all historical scan keys when replaying the store from position zero on restore. Payers SHOULD refresh the recipient's identity shortly before paying. -* **Verification order** on decrypt success: recompute `cm`; verify `recipientIdentityId` is the scanning identity; verify `sig` (or mark anonymous); verify settlement on the rail; only then surface the payment. -* **Deduplication**: duplicate notifications for the same settlement (same `txid`/`vout`, same derived platform address, or same `stHash`) are valid; clients MUST deduplicate after decryption. -* **Public-address monitoring**: clients watching a profile's `corePaymentAddress` use normal Core-chain means (e.g. compact block filters); clients watching a `platformPaymentAddress` poll its balance and nonce with state proofs. -* **Spend watching**: a detected Core one-time output is added to the client's filter set to observe its spend; a detected Platform one-time address is watched by nonce. -* **Restore from seed**: derive all keys (including past `index'` rotations); replay the notification store from position zero (recovering received payments via `b_scan` and sent payments via `ock`); then verify settlements — Platform addresses by direct state query, Core outputs by fetching the referenced transactions, bounded by `coreHeightHint` where a rescan is needed. -* **Watch-only delegation**: the scan private key MAY be given to a semi-trusted watch service, which can then detect notifications and derive (but not spend) one-time addresses. The spend key and out-key MUST NOT be delegated for watch purposes. +* **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. -* **Detection keys as identity keys rather than document fields.** Identity key changes require master-key authorization, whereas profile documents update under lower-security keys — and the detection keys gate every future notified payment, making them the highest-value target in this specification. Registry placement also yields on-chain revocation (key disabling), makes every identity payable without a DashPay profile, and keeps the keys inside future identity-key infrastructure. Contract bounds are deliberately absent: this is protocol-wide payment capability, not an application binding. -* **Notification documents rather than BIP-352-style scanning.** Silent-payments detection requires, per transaction, the input public keys — data that is absent from compact block filters (which commit only output and prevout scripts) and absent from Platform address-transfer state. Worse, the output script being sought is unknowable before performing the per-transaction ECDH, so filters cannot shortlist candidates: a scanning recipient must download and process every transaction forever, which is untenable for light clients. A Platform-published notification inverts the problem — the recipient learns the exact transaction (or derives the exact address) and verifies it directly with proofs. -* **Notification documents rather than BIP-47-style notification transactions.** An on-chain notification transaction is itself public: observers see that someone new is paying the recipient, and the notification inputs link payer to recipient unless specially prepared coins are burned. It also spends Core chain fees and block space on what is pure messaging, which Platform documents handle with encryption, uniform-size records, and provable retrieval. -* **One shared detection key pair across rails.** Both rails are secp256k1/HASH160 systems; the `rail` byte in the tweak hash keeps their one-time address spaces disjoint. Separate per-rail key pairs would double registry footprint and trial-decryption cost for no privacy gain. -* **Distinct `epk` and `R`.** Notification decryption capability and address derivation material stay independent, so a watch service holding `b_scan` can be reasoned about cleanly; the cost is 33 bytes per entry. -* **Fixed-size entries.** Uniform records prevent size-based traffic analysis of the notification store and enable simple position arithmetic for paginated reads. +* **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 profile fields are optional additions at fresh positions; existing profiles remain valid and older clients ignore the new fields. -* The new key purposes and the `paymentNotification` document type activate with a Platform protocol version upgrade; earlier protocol versions reject them, and non-upgraded clients simply never scan the store. This includes extending the [DIP-11](dip-0011.md) identity public key purpose registry (and its schema/validators) with values `7` and `8` — the registry update and this DIP activate under the same protocol version rule. -* No changes are made to DIP-15 contact-request flows; established contacts continue using per-contact derived chains. The notified tier is complementary and MAY also be used between established contacts. -* Core chain consensus is unchanged; one-time outputs are ordinary P2PKH outputs. +* 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 -* **Detection key integrity.** The scan/spend keys can only be changed via identity updates under master-key authorization. Wallets SHOULD additionally alert users when a contact's detection keys or profile payment addresses change, and pin previously seen values. -* **Profile address hijacking.** Profile documents update under lower-security keys; a compromised profile key can redirect *public-tier* payments (bounded, per-payment loss). Clients SHOULD surface address changes prominently. The notified tier is unaffected by profile compromise. -* **Spoofed attribution.** Anyone can publish a notification claiming any `senderIdentityId`; attribution is only as good as the inner signature, which recipients MUST verify against the sender identity's registered keys. `recipientIdentityId` inside the signed payload prevents replaying a valid notification to a different recipient. -* **Dangling and mismatched notifications.** Recipients MUST verify settlement before crediting anything; notifications are hints, not money. -* **Ephemeral reuse.** Reusing `eskSeed` (hence `r`) across payments links those payments to each other at the payer's fault; implementations MUST source `eskSeed` from a cryptographically secure generator per payment. -* **One-time key exposure at spend.** As with all hash-of-key outputs, `P_n` becomes public when spent. This has no effect on unrelated payments: each `t_n` is bound to a distinct `(S, R, rail, n)`. +* **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 the notified tier hides.** Third parties observing both chains and the notification store see: an opaque fixed-size store entry (created by the payer identity, or by nobody when pool-funded), and a settlement paying a never-before-seen address. Neither names the recipient; amounts are visible on-chain but not attributable to the recipient's published identity. -* **Payer-side linkage on the Core rail.** The payer's transaction inputs remain subject to ordinary chain analysis; this DIP hides the *recipient*, not the payer's coin history. Payers wanting input-side privacy should fund stealth payments from CoinJoin-mixed coins. -* **Payer-side linkage on the Platform rail.** Address funds transfers expose their input addresses. Paying by unshielding from the Platform shielded pool (`fundingKind = 1`) severs payer-side linkage entirely; combined with pool funding of the notification and the anonymous variant, no payer-identifying information exists anywhere. Non-normative hygiene recommendations: fund from balances shielded well in advance, avoid amount-identical shield-then-unshield sequences, and prefer round or denomination-friendly amounts. -* **Notification timing.** An identity-funded notification reveals that the payer was active at a point in time. The mandated notification-before-settlement ordering plus a randomized settlement delay weakens correlation between the two events. -* **Trial-decryption metadata.** Recipients fetch store ranges wholesale, so read patterns do not reveal which entries decrypted successfully. - -## Future Extensions +* **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. -* **Shielded pool tier.** A follow-up specification will extend the profile with a shielded-pool payment address, extend DIP-15 contact requests with per-contact diversified shielded addresses, and register shielded memo kinds for payment association (rail code `4` and memo kinds `2`/`3` are reserved for it). Shielded payments natively provide recipient unlinkability and trial-decryption detection, so no stealth machinery applies there. -* **Silent-payments-style Core scanning.** The published detection key pair is deliberately shaped like a BIP-352 key pair. A future extension MAY define a notification-less Core payment mode in which network infrastructure serves per-transaction input-key tweaks (a "tweak index") that light clients scan with `b_scan`. Such a mode changes detection economics only; addresses derived under it MUST remain compatible with the derivation of this DIP. This paragraph is a reservation, not a commitment. +## Considered and Deferred: A Notified Stealth-Address Tier -## Reference Implementation +*This section is informational. It records a fully designed alternative and why it was deferred, so the reasoning survives the decision.* -The following pseudo-code is normative for paying a recipient over the notified tier: +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. -```text -function pay_notified(rail, recipient, amount, memo, network): - require rail in {0, 2} # public rails (1, 3) pay the profile address directly - # recipient: identity id; fetch identity with proof - keys = fetch_identity_keys(recipient) - B_scan = keys.active(PAYMENT_SCAN) # 33-byte compressed - B_spend = keys.active(PAYMENT_SPEND) +It was deferred for three reasons: - eskSeed = random_bytes(16) # redraw if any derived scalar is zero - esk = int_be(H("DashPay/PayNotif/v1/esk" || eskSeed)) mod order - r = int_be(H("DashPay/Stealth/v1/r" || eskSeed)) mod order - - S = r * B_scan - t0 = int_be(H("DashPay/Stealth/v1" || ser(S) || ser(r*G) || rail_byte(rail) || LE32(0))) mod order - P0 = B_spend + t0 * G - dest = p2pkh(HASH160(ser(P0)), rail, network) # Core P2PKH or DIP-18 platform address - - tx = build_unsigned_settlement(rail, dest, amount) - body = build_body(rail, sender_identity, recipient, locator(tx), amount, memo) - sig = sign_recoverable(sender_auth_key, sig_digest(body)) - entry = encrypt_entry(B_scan, eskSeed, body_with(sig)) # cm || epk || body_ct || out_ct - - publish_and_await_finality(entry) # MUST precede settlement broadcast - broadcast(tx) -``` +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. -## Test Vectors +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. -The vectors below exercise the one-time address derivation with fixed test-only scalars. All hex is lowercase. Scalars are 32-byte big-endian; points are compressed SEC1. (BIP-32 path vectors for feature `33'` will be added once tooling lands; the derivation below is independent of the HD paths.) - -Inputs: - -```text -b_scan = 61aafd85dbca17133515038343b05ed2019ca465d1ba93dcbffbf9534d2f436c -B_scan = 0218ee61cb2070d8c63456b2acd9cb0243e4a4ad0791c272e79c0eb8de88284448 -b_spend = 8f54f1cfa054739a1d4a87847bdf6a024caf78ff4b2c91aeb75cb843280957ff -B_spend = 0397b4690ee29e51d7da3186a683a563394981832f8860e405f8827bfd5dfbd7f6 -r = 37ca08877b0beea1b1e7655649274689e89944edae537ecad42764e59ac31e3e -R = 029d683d939d3bb5527ccc96d7ecd4e77274a530b4496fed79f08db99c8fe6e44e -S = 031218547890897c32cbc802902b8b8257cbcbd3cee2f9c393b7e4a74cb635b36e -``` - -| Rail | n | `t_n` | `P_n` | `p_n` | `HASH160(P_n)` | -| ---- | - | ----- | ----- | ----- | -------------- | -| Core (`0x00`) | 0 | `2fe3ccd4f560aadda6cedb4f4b9e0927ecbdf49c4864ba0bfbdb174a80e94cdf` | `029459124e206a8113bdb4c862794788380bae4b1d8bac57e561a6c88eb4eb96f1` | `bf38bea495b51e77c41962d3c77d732a396d6d9b93914bbab337cf8da8f2a4de` | `f7a5c7a50c7c8ef61223d139ae791a9360fe2262` | -| Core (`0x00`) | 1 | `e0867aad0724465861061aa9d89bf7cb71056bad6888b119ca5b4579f3663cf8` | `03356bc4f9ef4256c306872ab072bdef1e317396f2ae04730c6f252e9a02a2c039` | `6fdb6c7ca778b9f27e50a22e547b61cf030607c6046ca28cc1e59f304b3953b6` | `c4566e2150b8fa2f2854aef250962d4117afbb9f` | -| Platform (`0x02`) | 0 | `5bba1d655f05b3b9b91561f818c3d8091863148ed28f129b10e959e8a0847756` | `03b45786021b88c3b0e8730a7164630e75eea49b13fe1fd304c97075103f6fa40e` | `eb0f0f34ff5a2753d65fe97c94a3420b65128d8e1dbba449c846122bc88dcf55` | `7ca4b1b60c05c4fcd606d7c9821ee13eb70652bd` | - -Verification identities: `S = b_scan·R = r·B_scan`; `P_n = B_spend + t_n·G = p_n·G`. - -## DIP-9 Registry Update - -Reserve [DIP-9](dip-0009.md) feature index `33'` for "DashPay Payment Detection Keys": +## Future Extensions -| Feature Index | Feature | DIP | Note | -| ------------- | ------- | --- | ---- | -| `33'` | DashPay Payment Detection Keys | DIP-33 | Sub-path: `33'/account'/key_class'/index'` — key_class `0'` scan, `1'` spend, `2'` notification out-key (default account' = `0'`, index' = `0'`) | +* **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