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..72ec9ede --- /dev/null +++ b/dip-0033.md @@ -0,0 +1,428 @@ +
+ 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", + "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. 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. +* 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. `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)) +``` + +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) +``` + +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" || 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), 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 + +* **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. *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 + +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 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. + +## 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. 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. + +## 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): + 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) # 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) +``` + +## 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