diff --git a/README.md b/README.md index 3f565747..4c701011 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ Number | Layer | Title | Owner | Type | Status [14](dip-0014.md) | Applications | Extended Key Derivation using 256-Bit Unsigned Integers | Samuel Westrich | Informational | Proposed [15](dip-0015.md) | Applications | DashPay | Samuel Westrich, Eric Britten | Standard | Proposed [16](dip-0016.md) | Applications | Headers First Synchronization on Simple Payment Verification Wallets | Samuel Westrich | Informational | Proposed +[17](dip-0017.md) | Consensus | Dash Platform Payment Addresses and HD Derivation | Samuel Westrich | Standard | Proposed +[18](dip-0018.md) | Consensus | Dash Platform Payment Address Encodings | Samuel Westrich, thephez | Standard | Proposed [20](dip-0020.md) | Consensus | Dash Opcode Updates | Mart Mangus | Standard | Final [21](dip-0021.md) | Consensus | LLMQ DKG Data Sharing | dustinface | Standard | Final [22](dip-0022.md) | Consensus | Making InstantSend Deterministic using Quorum Cycles | Samuel Westrich, UdjinM6 | Standard | Final diff --git a/dip-0009/assignments.md b/dip-0009/assignments.md index d0f6adaf..7e7e137a 100644 --- a/dip-0009/assignments.md +++ b/dip-0009/assignments.md @@ -9,6 +9,7 @@ Here is a table of current feature paths and any associated DIP. Future DIPs may | `5'` | Identity Keys | [DIP 0013: Identities in Hierarchical Deterministic wallets](../dip-0013.md) | The related keys are located in the following sub-paths:
`0'/key type'/identity index'/key index'/*` - Identity Authentication ([details](../dip-0013.md#identity-authentication-keys))
`1'/*` - Identity Registration Funding ([details](../dip-0013.md#identity-registration-funding-keys))
`2'/*` - Identity Topup Funding ([details](../dip-0013.md#identity-top-up-funding-keys))
`3'/*` - Identity Invitation Funding ([details](../dip-0013.md#identity-invitatation-funding-keys))

For example, the first Identity Registration Funding key for Dash would be at `m/9'/5'/5'/1'/0` | | `15'` | DashPay - Incoming Funds | [DIP 0015: DashPay](../dip-0015.md#dashpay-incoming-funds-derivation-path) | The related keys are located in the following sub-paths: `/0'/account'/*`

For example, incoming funds for the first identity would be at `m/9'/5'/15'/0'/*` | | `16'` | DashPay - Auto Accept Proof | [DIP 0015: DashPay](../dip-0015.md#auto-accept-proof-autoacceptproof) | The related keys are located in the following sub-paths: `16'/expiration timestamp'`

For example, the key for a proof expiring at a Unix epoch time of `1605927033` would be at `m/9'/5'/16'/1605927033'` | +| `17'` | Platform Payment Addresses | [DIP 0017: Dash Platform Payment Addresses and HD Derivation](../dip-0017.md) | The related keys are located in the following sub-paths: `17'/key_class'/index` (default key_class' = `0'`) | Note: all DIP 0009 paths are of the format: `m / 9' / coin_type' / feature' / *` diff --git a/dip-0017.md b/dip-0017.md new file mode 100644 index 00000000..899b6e10 --- /dev/null +++ b/dip-0017.md @@ -0,0 +1,185 @@ +
+  DIP: 0017
+  Title: Dash Platform Payment Addresses and HD Derivation
+  Author(s): Samuel Westrich
+  Special-Thanks: Dash Platform Team
+  Comments-Summary: No comments yet.
+  Status: Draft
+  Type: Standard
+  Created: 2025-11-28
+  License: MIT License
+  Replaces: -
+  Superseded-By: -
+
+ +## Table of Contents + +1. [Abstract](#abstract) +1. [Motivation](#motivation) +1. [Prior Work](#prior-work) +1. [Specification](#specification) + 1. [Overview](#overview) + 1. [Derivation Path Definition](#derivation-path-definition) + 1. [Wallet and Hardware Wallet Behavior](#wallet-and-hardware-wallet-behavior) +1. [Rationale](#rationale) +1. [Backwards Compatibility](#backwards-compatibility) +1. [Reference Implementation](#reference-implementation) +1. [Security Considerations](#security-considerations) +1. [Privacy Considerations](#privacy-considerations) +1. [Test Vectors](#test-vectors) +1. [DIP-9 Registry Update](#dip-9-registry-update) +1. [Copyright](#copyright) + +## Abstract + +This DIP defines Dash Platform payment addresses and their hierarchical deterministic (HD) derivation under [DIP-9](dip-0009.md). It uses Dash coin type 5' on mainnet and coin type 1' on test networks, and introduces a new [DIP-9](dip-0009.md) feature index for Platform payments. The specification standardizes derivation paths (including account separation) and wallet/hardware wallet guidance for Platform payment keys. Address encoding (version bytes and formats) and script-hash address formats are defined in [DIP-18](dip-0018.md). + +## Motivation + +While Dash Platform identities can hold balances, identity-to-identity payments alone cannot satisfy the requirements for value transfer on Platform. Identities are designed to be persistent, publicly linkable entities with DPNS names and associated documents. Transferring value directly between identities permanently links those identities on-chain, which is undesirable for common payment scenarios and prevents future improvements to privacy and fungibility. + +Identity-based transfers also make onboarding unnecessarily complex. Creating a new identity requires funding an asset-lock transaction on the Core chain before the identity can be registered. With a Platform-level payment mechanism, an existing user can send value directly to an address controlled by a new user, allowing that user to create or top up an identity entirely within Platform. + +Additionally, many use cases require value that is not tied to a persistent identity—such as one-time payment targets, merchant invoices, or exchange integrations. Platform payment addresses provide this flexibility in a manner analogous to Core-chain addresses. + +For these reasons, DIP-17 introduces an address subsystem that complements identity balances and enables direct value transfer, simplified onboarding, improved privacy, and broader application support within Dash Platform. Wallets require an unambiguous address type, network-specific encodings, and deterministic derivation paths that coexist with BIP-44 Core chain funds and existing [DIP-9](dip-0009.md) features (masternodes, identities, DashPay). This DIP defines a single standard so wallets, hardware wallets, and services can implement Platform payments. + +## Prior Work + +* [DIP-0009: Feature Derivation Paths](https://github.com/dashpay/dips/blob/master/dip-0009.md) +* [DIP-0013: Identities in Hierarchical Deterministic Wallets](https://github.com/dashpay/dips/blob/master/dip-0013.md) +* [DIP-0015: DashPay](https://github.com/dashpay/dips/blob/master/dip-0015.md) +* [BIP-0032: Hierarchical Deterministic Wallets](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) +* [BIP-0044: Multi-Account Hierarchy for Deterministic Wallets](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) + +## Specification + +### Overview + +Platform payment keys identify recipients of value on Dash Platform. This value includes Dash Credits, which represent DASH held on Platform, as well as other Platform-based payments. These keys are independent of Dash Platform identities and do not belong to an identity’s key set. Instead, they function as standalone payment keys that hold value outside of any identity. They use a single secp256k1 key pair and are not script-based. They are not valid Dash Core chain addresses and MUST NOT be used for Core chain transactions. Their encoding format is specified in [DIP-18](dip-0018.md). + +### Derivation Path Definition + +The Platform payment feature is assigned [DIP-9](dip-0009.md) feature index `17'`. The canonical derivation path is: + +```text +m / 9' / coin_type' / 17' / account' / key_class' / index +``` + +Normative requirements: + +* `purpose'` MUST be `9'`. +* `coin_type'` MUST be `5'` on mainnet (Dash SLIP-44 coin type) and MUST be `1'` on testnet/devnet/regtest (SLIP-44 test coin type), consistent with BIP-44 conventions. +* `feature'` MUST be `17'` (Platform payment feature). +* `account'` MUST be hardened. `0'` is the default account. Additional accounts MAY be used following BIP-44-style multi-account semantics. +* `key_class'` MUST be hardened. The default class for Platform payment receive keys is `0'`. Additional hardened classes MAY be defined by future DIPs; `1'` is reserved for wallet-internal or change-like segregation if a wallet chooses to implement it. Wallets MUST ignore unknown key_class values rather than rejecting the entire account. +* `index` MUST be non-hardened (`0 ≤ index ≤ 2³¹−1`). +* No BIP-44 change level is used; privacy is obtained by incrementing `index` and optionally segregating with `key_class'`. + +Default account paths: + +| Network(s) | Default account path | +|-|-| +| Mainnet | `m/9'/5'/17'/0'/0'/index` | +| Testnet / Devnet / Regtest | `m/9'/1'/17'/0'/0'/index` (coin type `1'` for test networks) | + +Wallets MAY derive and expose an extended public key at `m/9'/5'/17'/account'/key_class'` (mainnet) or `m/9'/1'/17'/account'/key_class'` (test networks) for watch-only or monitoring. They MUST NOT expose hardened parent levels. + +Accounts follow BIP-44 semantics: `account'` partitions user-controlled sets of Platform payment keys, enabling multiple profiles or organizational separations while preserving hardened isolation between accounts. + +### Wallet and Hardware Wallet Behavior + +* Wallets MUST derive Platform payment keys only from `m/9'/5'/17'/account'/key_class'/index` (mainnet) or `m/9'/1'/17'/account'/key_class'/index` (test networks). Address encoding of the resulting public keys is specified in [DIP-18](dip-0018.md). +* Wallets SHOULD clearly separate Platform chain balances from Core chain balances in UI and storage. +* Wallets SHOULD rotate addresses by incrementing `index` to avoid reuse; a default gap limit of 20 is RECOMMENDED for discovery. +* Wallets MAY support watch-only by exporting the xpub at `m/9'/5'/17'/account'/key_class'` (mainnet) or `m/9'/1'/17'/account'/key_class'` (test networks). +* Wallets MAY present multiple accounts following BIP-44 semantics (distinct `account'` values), and SHOULD clearly label the active account in UI. +* Hardware wallets MUST whitelist the above derivation path and display a label such as “Dash Platform address” when showing or signing. +* Hardware wallets MUST apply the address encodings defined in [DIP-18](dip-0018.md) and MUST NOT reinterpret these as Core chain P2PKH/P2SH. +* If a wallet does not implement Platform, it simply never derives the `17'` feature path. + +## Rationale + +* **Coin type 5' on mainnet, 1' on test networks:** Mainnet keeps Dash SLIP-44 coin type 5', while testnet/devnet/regtest follow SLIP-44 convention with coin type 1'. This avoids new registry allocations and keeps all Dash features under the established namespaces. +* **DIP-9 vs BIP-44:** DIP-9’s feature level cleanly separates Platform addresses from Core chain funds and from identities/masternodes without overloading BIP-44’s change level or accounts. +* **Feature index 17':** The next available [DIP-9](dip-0009.md) feature after 16' (DashPay) is reserved for Platform payments, avoiding collisions with existing features. +* **Hardened upper levels:** `9'/coin_type'/17'/account'/key_class'` isolate Platform keys from other features and from each other. An xpub leak below `key_class'` cannot compromise hardened parents. +* **Non-hardened leaf index:** Enables watch-only, auditing, and future multisig/shared-custody schemes that rely on unhardened derivation of child public keys. Fully hardened leaves were rejected to preserve these capabilities. +* **Accounts retained:** A hardened `account'` level maintains BIP-44-style multi-account semantics while still isolating Platform keys under the [DIP-9](dip-0009.md) feature branch. +* **No BIP-44 change level:** Platform addresses are not UTXO change outputs; a linear `index` (optionally partitioned by `key_class'`) keeps the model simple for hardware wallets and avoids misuse of the 0/1 change bit. + +## Backwards Compatibility + +* Classic Dash addresses (`X...`, `7...`, `y...`, `8...`) are unaffected. Nodes do not accept Platform addresses in Core chain scripts. +* DIP-[3](dip-0003.md)/[8](dip-0008.md) masternode derivations and [DIP-13](dip-0013.md) identity derivations remain unchanged. +* Existing seeds stay valid; wallets can add Platform support without migration. +* Wallets unaware of Platform will not derive `m/9'/5'/17'/...` and therefore will not interfere with Platform balances. + +# Reference Implementation + +The following pseudo-code is normative for deriving a Platform payment address: + +```text +function platform_payment_key(seed, account, key_class, index, network): + # seed: BIP-39/BIP-32 seed bytes + # account: hardened int (default 0) + # key_class: hardened int (default 0) + # index: non-hardened child number + # network: "mainnet" or "testnet" + + coin_type = 5' if network == "mainnet" else 1' + path = [9' , coin_type , 17' , account , key_class' , index] + + master_priv, master_chain = bip32_master(seed) # HMAC-SHA512("Bitcoin seed", seed) + child_priv, child_chain = bip32_derive(master_priv, master_chain, path) + + pubkey = secp256k1_compress(secp256k1_point(child_priv)) + payload = RIPEMD160(SHA256(pubkey)) + + return { + "private_key": child_priv, + "public_key": pubkey, + "hash160": payload + } +``` + +Encoding and decoding of these payloads into addresses is specified in [DIP-18](dip-0018.md). + +## Security Considerations + +* Derivation uses hardened separation at `feature'` and `key_class'`; compromise of a Platform xpub does not expose other features or other key classes. +* Leakage of the xpub at `m/9'/coin_type'/17'/account'/key_class'` allows derivation of all Platform public keys for that key class but does not leak private keys or other features. +* Wallets MUST reject attempts to use Platform addresses in Core chain transactions to prevent misdirected funds. +* Hardware wallets MUST show the full derivation path and “Dash Platform address” to reduce key-path confusion attacks. +* The checksum and distinct prefixes mitigate accidental prefix confusion with `X`/`7`/`y`/`8` addresses. + +## Privacy Considerations + +* Wallets SHOULD avoid address reuse by incrementing `index` and MAY use separate `key_class'` values to segregate user profiles or accounts. +* Wallets SHOULD avoid correlating Platform `index` progression with Core chain BIP-44 indices to reduce cross-layer linkability. +* Sharing xpubs at `m/9'/coin_type'/17'/account'/key_class'` enables watch-only but also enables address graph reconstruction for that class; applications should only share when necessary. + +## Test Vectors + +Mnemonic (test-only): `abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about` +Passphrase: `""` (empty) + +All hex strings are lowercase, big-endian. Address encodings for the HASH160 values are specified in [DIP-18](dip-0018.md). + +| Vector | Path (mainnet / testnet) | account' | key_class' | index | Private Key (hex) | Compressed Pubkey (hex) | HASH160(pubkey) | +| ------ | ----------------------- | -------- | ---------- | ----- | ----------------- | ----------------------- | --------------- | +| 1 | m/9'/5'/17'/0'/0'/0 (mainnet) / m/9'/1'/17'/0'/0'/0 (test) | 0' | 0' | 0 | 6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe | 03de102ed1fc43cbdb16af02e294945ffaed8e0595d3072f4c592ae80816e6859e | f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525 | +| 2 | m/9'/5'/17'/0'/0'/1 (mainnet) / m/9'/1'/17'/0'/0'/1 (test) | 0' | 0' | 1 | eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523 | 02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da | a5ff0046217fd1c7d238e3e146cc5bfd90832a7e | +| 3 (non-default class) | m/9'/5'/17'/0'/1'/0 (mainnet) / m/9'/1'/17'/0'/1'/0 (test) | 0' | 1' | 0 | cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db | 0317a3ed70c141cffafe00fa8bf458cec119f6fc039a7ba9a6b7303dc65b27bed3 | 6d92674fd64472a3dfcfc3ebcfed7382bf699d7b | + +## DIP-9 Registry Update + +Reserve [DIP-9](dip-0009.md) feature index `17'` for “Platform Payment Addresses”: + +| Feature Index | Feature | DIP | Note | +| ------------- | ------- | --- | ---- | +| `17'` | Platform Payment Addresses | DIP-17 | Sub-path: `17'/account'/key_class'/index` (default account' = `0'`, key_class' = `0'`) | + +## Copyright + +Copyright (c) 2025 Dash Core Group, Inc. [Licensed under the MIT License](https://opensource.org/licenses/MIT) diff --git a/dip-0018.md b/dip-0018.md new file mode 100644 index 00000000..361fd604 --- /dev/null +++ b/dip-0018.md @@ -0,0 +1,259 @@ +
+  DIP: 0018
+  Title: Dash Platform Payment Address Encodings
+  Author(s): Samuel Westrich, thephez
+  Special-Thanks: Dash Platform Team
+  Comments-Summary: No comments yet.
+  Status: Draft
+  Type: Standard
+  Created: 2025-11-28
+  License: MIT License
+  Replaces: -
+  Superseded-By: -
+
+ +## Table of Contents + +1. [Abstract](#abstract) +1. [Motivation](#motivation) +1. [Prior Work](#prior-work) +1. [Specification](#specification) + 1. [Address Types](#address-types) + 1. [Encoding](#encoding) + 1. [Network Parameters](#network-parameters) + 1. [Validation](#validation) + 1. [Wallet and Hardware Wallet Behavior](#wallet-and-hardware-wallet-behavior) +1. [Rationale](#rationale) +1. [Backwards Compatibility](#backwards-compatibility) +1. [Reference Implementation](#reference-implementation) +1. [Security Considerations](#security-considerations) +1. [Privacy Considerations](#privacy-considerations) +1. [Test Vectors](#test-vectors) +1. [Copyright](#copyright) + +## Abstract + +This DIP specifies the bech32m address encoding formats for Dash Platform payments. It defines the human-readable part (HRP), data layout, and checksum rules for Platform pay-to-public-key-hash (P2PKH) addresses derived via [DIP-17](dip-0017.md), and introduces Platform pay-to-script-hash (P2SH) addresses. Distinct HRPs for Platform prevent confusion with Dash Core chain addresses and with other bech32-based formats. + +## Motivation + +Platform payment keys are derived under [DIP-17](dip-0017.md). To interoperate between wallets, hardware wallets, and services, a standard encoding with explicit network separation is required. This DIP adopts bech32m, which provides strong error detection, produces compact QR codes, and excludes ambiguous characters. + +## Prior Work + +* [DIP-0017: Dash Platform Payment Addresses and HD Derivation](https://github.com/dashpay/dips/blob/master/dip-0017.md) +* [BIP-0173: bech32 format](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) +* [BIP-0350: bech32m format for modern checksums](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki) + +## Specification + +### Address Types + +* **Platform P2PKH:** `HASH160(pubkey)` where `pubkey` is a compressed secp256k1 public key derived per [DIP-17](dip-0017.md). +* **Platform P2SH:** `HASH160(script)` where `script` is the raw byte serialization of a Platform script (e.g., multisig or policy script) being paid to. + +### Encoding + +#### Encoding Algorithm Summary + +Encoding a Dash Platform address uses the bech32m format defined in [BIP-350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki), with the general encoding rules inherited from [BIP-173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki). + +Given: + +* `hrp`: the network human-readable prefix (e.g., `dashevo`, `tdashevo`) +* `type_byte`: `0x00` for P2PKH or `0x01` for P2SH +* `hash160`: a 20-byte `HASH160(pubkey or script)` value + +The address MUST be encoded as follows: + +1. Form the 21-byte payload: `payload = type_byte || hash160` +2. Convert the payload from 8-bit bytes to 5-bit groups using the standard `convertbits()` procedure described in [BIP-173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki), with padding enabled +3. Compute the bech32m checksum using the algorithm defined in [BIP-350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki#appendix-checksum-design--properties), including HRP expansion and polymod evaluation using the bech32 generator constants +4. Append the checksum to the data and map the resulting 5-bit values to characters using the bech32 alphabet defined in [BIP-173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) (`qpzry9x8gf2tvdw0s3jn54khce6mua7l`) +5. Produce the final address string: `hrp + "1" + encoded_data` + +Decoders MUST reverse these steps and MUST verify: + +* Checksum validity (per [BIP-350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki#appendix-checksum-design--properties)), +* HRP correctness for the target network, +* Data-part length requirements, +* The type byte is either `0x00` or `0x01`. + +#### Structure + +All Platform addresses are encoded as: + +```text + "1" +``` + +* `` is network-specific (see table). +* `` contains: + * one type byte (`0x00` P2PKH, `0x01` P2SH), followed by + * 20-byte HASH160 payload encoded as 5-bit groups via bech32 rules. + +The checksum MUST be calculated using the [bech32m algorithm as defined in BIP-350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki#bech32m). + +#### Rules + +* MUST be lowercase when encoded and displayed. +* Decoders MUST reject mixed-case input. +* Uppercase MAY be accepted but MUST normalize to lowercase before storage/display. +* Attempting to validate or decode Dash Platform addresses using legacy bech32 rules (BIP-173 checksum constant) MUST fail. + +#### Character set + +The bech32m encoding used in this specification requires a fixed, 32-character base-32 alphabet. The data portion of all encoded addresses MUST use the same character set as Bitcoin and [Bitcoin Cash](https://reference.cash/protocol/blockchain/encoding/cashaddr): + +```text +qpzry9x8gf2tvdw0s3jn54khce6mua7l +``` + +### Network Parameters + +The following values define the canonical human-readable prefixes (HRPs) and type-byte assignments for Dash Platform addresses. These values are fixed and MUST be used exactly as specified. + +| Network | HRP | +| ------- | -------- | +| Mainnet | `dashevo` | +| Testnet / Devnet / Regtest | `tdashevo` | + +Type byte meaning: + +| Address Type | Type byte | +| -------------- | --------- | +| Platform P2PKH | `0x00` | +| Platform P2SH | `0x01` | + +### Validation + +A Platform address is valid if: + +1. It is lowercase or uppercase (but not mixed). +2. HRP matches expected network. +3. bech32m checksum verifies. +4. Payload decodes to exactly 21 bytes. +5. `payload[0]` is `0x00` or `0x01`. + +Wallets MUST reject Platform addresses when constructing Dash Core chain scripts and SHOULD present a clear warning if a user attempts to mix layers. + +### Wallet and Hardware Wallet Behavior + +* Wallets MUST use the P2PKH encoding above for public keys derived per [DIP-17](dip-0017.md). +* Wallets MUST use the P2SH encoding for Platform scripts intended to receive Platform funds. +* Wallets MUST treat HRP as the network selector. +* Software wallets SHOULD label Platform balances separately from Core chain balances and SHOULD avoid auto-pasting Platform addresses into Core chain contexts. +* Wallets SHOULD derive payloads via [DIP-17](dip-0017.md) and then encode using these rules; no alternative prefixes are allowed. +* Hardware wallets MUST validate the HRP to confirm network identity and MUST enforce the type byte (`0x00` or `0x01`). Devices MUST display a user-facing descriptor: “Dash Platform address” for P2PKH and “Dash Platform script address” for P2SH. + +## Rationale + +Bech32m was chosen over Base58Check because it: + +* Improves checksum strength +* Is QR efficient +* Avoids ambiguous characters +* Clearly separates networks using HRPs +* Future-proofs script or address extensions + +## Backwards Compatibility + +No impact on Core chain addresses. Platform P2PKH/P2SH prefixes are new and cannot be misinterpreted as existing Dash formats. Seeds and derivation ([DIP-17](dip-0017.md)) are unchanged. + +## Reference Implementation + +Note: The following pseudocode covers the encoding and decoding parts of DIP-18 only, not the wallet-UI or signing device behaviors. + +```text +function encode_platform_address(hash160, type, network): + # type: "p2pkh" or "p2sh" + # network: "mainnet" or "testnet" + if len(hash160) != 20: + error("invalid hash160 length") + + type_byte = 0x00 if type=="p2pkh" else 0x01 if type=="p2sh" else error() + + hrp = { + "mainnet": "dashevo", + "testnet": "tdashevo", + }.get(network) or error() + + payload = [type_byte] || hash160 + data = convertbits(payload, 8, 5, pad=true) + return bech32m_encode(hrp, data) + +function decode_platform_address(addr): + if mixed_case(addr): error("mixed case not allowed") + + addr = to_lowercase(addr) + + # bech32m_decode MUST: + # - verify bech32m checksum (BIP-350) + # - validate character set + # - return (hrp, data_without_checksum) + hrp, data = bech32m_decode(addr) + + # Infer network from HRP + network = { + "dashevo": "mainnet", + "tdashevo": "testnet", + }.get(hrp) + + if network is null: + error("unknown hrp / network") + + payload = convertbits(data, 5, 8, pad=false) + if len(payload) != 21: + error("invalid payload length") + + type_byte = payload[0] + hash160 = payload[1:21] + + if type_byte == 0x00: + addr_type = "p2pkh" + else if type_byte == 0x01: + addr_type = "p2sh" + else: + error("unknown type byte") + + return network, addr_type, hash160 +``` + +A Python implementation is available at [`dip-0018/bech32.py`](dip-0018/bech32.py). It uses the [BIP-350 reference code](https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py) by Pieter Wuille and validates against the [provided test vectors](#test-vectors). + +## Security Considerations + +* Checksums detect mistyped addresses; distinct prefixes reduce layer-mixing mistakes. +* Hardware wallet whitelisting of prefixes mitigates key-path confusion. +* P2SH scripts must be fully validated by wallets before signing or displaying to prevent malicious script substitution. + +## Privacy Considerations + +* Privacy relies on HD key rotation per [DIP-17](dip-0017.md) and script hygiene. +* P2SH script hashes reveal neither full script nor participant keys but can still be correlated if reused; wallets SHOULD discourage P2SH reuse. + +## Test Vectors + +Mnemonic (shared with [DIP-17](dip-0017.md)): `abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about` +Passphrase: `""` + +The HASH160 payloads in the following tables are derived from the mnemonic and paths specified in [DIP-17](dip-0017.md). Implementations MAY use those derivation vectors to perform end-to-end tests (mnemonic → key → pubkey → HASH160 → address). + +### P2PKH examples + +| Vector | Payload (HASH160) | Mainnet (`dashevo`) | Testnet (`tdashevo`) | +| ------ | ------------------------------------------ | ------------------------------------------------ | ------------------------------------------------- | +| 1 | `f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525` | `dashevo1qrma5z3ttj75la4m93xcndna9ullamq9y5smxxxm` | `tdashevo1qrma5z3ttj75la4m93xcndna9ullamq9y5aawfeu` | +| 2 | `a5ff0046217fd1c7d238e3e146cc5bfd90832a7e` | `dashevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20clcut89` | `tdashevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20cj75ycz` | +| 3 | `6d92674fd64472a3dfcfc3ebcfed7382bf699d7b` | `dashevo1qpkeye606ez89g7lelp7hnldwwpt76va0vcv050v` | `tdashevo1qpkeye606ez89g7lelp7hnldwwpt76va0v428mst` | + +### P2SH example + +Payload: `43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636` + +* Mainnet: `dashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxckndrac` +* Testnet: `tdashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxcm49vzl` + +## Copyright + +Copyright (c) 2025 Dash Core Group, Inc. [Licensed under the MIT License](https://opensource.org/licenses/MIT) diff --git a/dip-0018/bech32.py b/dip-0018/bech32.py new file mode 100644 index 00000000..6b6477fa --- /dev/null +++ b/dip-0018/bech32.py @@ -0,0 +1,467 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017, 2020 Pieter Wuille +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +DIP-18 Bech32m address encoding for Dash Platform, with full DIP-17 test vector validation. + +Bech32/Bech32m functions are copied verbatim from BIP-350 reference implementation: +https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py + +This module provides: +- Bech32m encoding/decoding (BIP-350) +- Dash Platform address encoding/decoding (DIP-18) +- BIP-39/BIP-32 derivation for validating DIP-17 test vectors + +Dependencies: + pip install ecdsa + +Usage: + python bech32.py +""" + +from typing import Tuple, Literal +from hashlib import sha256, pbkdf2_hmac +from enum import Enum +import hashlib +import hmac +import unicodedata + +from ecdsa import SECP256k1, SigningKey + +# ---- Bech32/Bech32m reference implementation (BIP-350) ---- +# The following code up to "End of BIP-350 reference" is copied verbatim from: +# https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py + + +class Encoding(Enum): + """Enumeration type to list the various supported encodings.""" + BECH32 = 1 + BECH32M = 2 + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +BECH32M_CONST = 0x2bc830a3 + +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for value in values: + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ value + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def bech32_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data characters.""" + const = bech32_polymod(bech32_hrp_expand(hrp) + data) + if const == 1: + return Encoding.BECH32 + if const == BECH32M_CONST: + return Encoding.BECH32M + return None + +def bech32_create_checksum(hrp, data, spec): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + const = BECH32M_CONST if spec == Encoding.BECH32M else 1 + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data, spec): + """Compute a Bech32 string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data, spec) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + +def bech32_decode(bech): + """Validate a Bech32/Bech32m string, and determine HRP and data.""" + if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or + (bech.lower() != bech and bech.upper() != bech)): + return (None, None, None) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + spec = bech32_verify_checksum(hrp, data) + if spec is None: + return (None, None, None) + return (hrp, data[:-6], spec) + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +# ---- End of BIP-350 reference ---- + + +# ---- Dash Platform address encoding on top of Bech32m ---- + +Network = Literal["mainnet", "testnet"] +AddrType = Literal["p2pkh", "p2sh"] + +NETWORK_TO_HRP = { + "mainnet": "dashevo", + "testnet": "tdashevo" +} + +HRP_TO_NETWORK = {v: k for k, v in NETWORK_TO_HRP.items()} + +TYPE_TO_BYTE = { + "p2pkh": 0x00, + "p2sh": 0x01, +} + +BYTE_TO_TYPE = {v: k for k, v in TYPE_TO_BYTE.items()} + + +def encode_platform_address(hash160: bytes, + addr_type: AddrType, + network: Network) -> str: + """ + Encode a Dash Platform address as Bech32m. + + :param hash160: 20-byte HASH160(pubkey or script) + :param addr_type: "p2pkh" or "p2sh" + :param network: "mainnet", "testnet" + :return: Bech32m-encoded address string + """ + if len(hash160) != 20: + raise ValueError("hash160 must be 20 bytes") + + if addr_type not in TYPE_TO_BYTE: + raise ValueError("unknown addr_type") + + if network not in NETWORK_TO_HRP: + raise ValueError("unknown network") + + hrp = NETWORK_TO_HRP[network] + type_byte = TYPE_TO_BYTE[addr_type] + + payload_bytes = bytes([type_byte]) + hash160 # 21 bytes + data5 = convertbits(payload_bytes, 8, 5, pad=True) + + return bech32_encode(hrp, data5, Encoding.BECH32M) + + +def decode_platform_address(addr: str) -> Tuple[Network, AddrType, bytes]: + """ + Decode a Dash Platform Bech32m address into (network, addr_type, hash160). + + :param addr: Bech32m-encoded Dash Platform address + :return: (network, "p2pkh"/"p2sh", 20-byte hash160) + """ + hrp, data_no_checksum, spec = bech32_decode(addr) + + if hrp is None: + raise ValueError("invalid Bech32m address") + + if spec != Encoding.BECH32M: + raise ValueError("address is not Bech32m encoded") + + if hrp not in HRP_TO_NETWORK: + raise ValueError(f"unknown HRP '{hrp}' for Dash Platform") + + network = HRP_TO_NETWORK[hrp] + + # Convert back to 8-bit payload, without padding + payload_bytes = convertbits(data_no_checksum, 5, 8, pad=False) + if payload_bytes is None: + raise ValueError("invalid payload encoding") + payload_bytes = bytes(payload_bytes) + + if len(payload_bytes) != 21: + raise ValueError("invalid payload length (expected 21 bytes)") + + type_byte = payload_bytes[0] + hash160 = payload_bytes[1:] + + if type_byte not in BYTE_TO_TYPE: + raise ValueError(f"unknown type byte {type_byte:#x}") + + addr_type = BYTE_TO_TYPE[type_byte] + + return network, addr_type, hash160 + + +# ---- BIP-39 / BIP-32 derivation for test vector validation ---- + +# Hardened flag for BIP-32 derivation +H = 0x80000000 + + +def mnemonic_to_seed(mnemonic: str, passphrase: str = "") -> bytes: + """BIP-39: Convert mnemonic to 64-byte seed using PBKDF2-HMAC-SHA512.""" + mnemonic = unicodedata.normalize('NFKD', mnemonic) + passphrase = unicodedata.normalize('NFKD', passphrase) + return pbkdf2_hmac( + "sha512", + mnemonic.encode("utf-8"), + ("mnemonic" + passphrase).encode("utf-8"), + 2048 + ) + + +def bip32_master(seed: bytes) -> tuple[bytes, bytes]: + """BIP-32: Derive master private key and chain code from seed.""" + I = hmac.new(b"Bitcoin seed", seed, "sha512").digest() + return I[:32], I[32:] # private_key, chain_code + + +def bip32_derive_child(priv_key: bytes, chain_code: bytes, index: int) -> tuple[bytes, bytes]: + """BIP-32: Derive child key at given index (hardened if index >= 0x80000000).""" + if index >= 0x80000000: # hardened + data = b'\x00' + priv_key + index.to_bytes(4, 'big') + else: # normal + sk = SigningKey.from_string(priv_key, curve=SECP256k1) + pubkey = sk.get_verifying_key().to_string("compressed") + data = pubkey + index.to_bytes(4, 'big') + + I = hmac.new(chain_code, data, "sha512").digest() + child_key_int = (int.from_bytes(I[:32], 'big') + int.from_bytes(priv_key, 'big')) % SECP256k1.order + return child_key_int.to_bytes(32, 'big'), I[32:] + + +def derive_path(seed: bytes, path: list[int]) -> bytes: + """Derive private key at full BIP-32 path.""" + priv, chain = bip32_master(seed) + for idx in path: + priv, chain = bip32_derive_child(priv, chain, idx) + return priv + + +def hash160(data: bytes) -> bytes: + """HASH160 = RIPEMD160(SHA256(data)).""" + return hashlib.new('ripemd160', sha256(data).digest()).digest() + + +def priv_to_compressed_pub(priv_key: bytes) -> bytes: + """Convert private key to compressed public key (33 bytes).""" + sk = SigningKey.from_string(priv_key, curve=SECP256k1) + return sk.get_verifying_key().to_string("compressed") + + +def format_path(path: list[int]) -> str: + """Format derivation path for display.""" + parts = ["m"] + for idx in path: + if idx >= H: + parts.append(f"{idx - H}'") + else: + parts.append(str(idx)) + return "/".join(parts) + + +# ---- Test vectors ---- + +# DIP-17 P2PKH vectors: (path, priv_hex, pub_hex, hash160_hex, mainnet_addr, testnet_addr) +DIP17_VECTORS = [ + # Vector 1: m/9'/5'/17'/0'/0'/0 + ( + [9 + H, 5 + H, 17 + H, 0 + H, 0 + H, 0], + "6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe", + "03de102ed1fc43cbdb16af02e294945ffaed8e0595d3072f4c592ae80816e6859e", + "f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", + "dashevo1qrma5z3ttj75la4m93xcndna9ullamq9y5smxxxm", + "tdashevo1qrma5z3ttj75la4m93xcndna9ullamq9y5aawfeu", + ), + # Vector 2: m/9'/5'/17'/0'/0'/1 + ( + [9 + H, 5 + H, 17 + H, 0 + H, 0 + H, 1], + "eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523", + "02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da", + "a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", + "dashevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20clcut89", + "tdashevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20cj75ycz", + ), + # Vector 3: m/9'/5'/17'/0'/1'/0 (key_class' = 1') + ( + [9 + H, 5 + H, 17 + H, 0 + H, 1 + H, 0], + "cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db", + "0317a3ed70c141cffafe00fa8bf458cec119f6fc039a7ba9a6b7303dc65b27bed3", + "6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", + "dashevo1qpkeye606ez89g7lelp7hnldwwpt76va0vcv050v", + "tdashevo1qpkeye606ez89g7lelp7hnldwwpt76va0v428mst", + ), +] + +# DIP-18 P2SH vector (address encoding only, no derivation path) +P2SH_VECTOR = ( + "43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636", + "dashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxckndrac", + "tdashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxcm49vzl", +) + + +# ---- Self-test ---- + +if __name__ == "__main__": + print("=" * 70) + print("DIP-17 / DIP-18 Test Vector Validation") + print("=" * 70) + + # BIP-39 mnemonic (test-only) + mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + passphrase = "" + + print(f"\nMnemonic: {mnemonic}") + print(f"Passphrase: {'(empty)' if not passphrase else passphrase}") + + # Step 1: Mnemonic to seed + seed = mnemonic_to_seed(mnemonic, passphrase) + print(f"Seed: {seed.hex()}") + + all_passed = True + + # Step 2: Validate DIP-17 derivation vectors + print("\n" + "-" * 70) + print("DIP-17 Derivation Validation") + print("-" * 70) + + for i, (path, expected_priv, expected_pub, expected_hash, _, _) in enumerate(DIP17_VECTORS, 1): + print(f"\nVector {i}: {format_path(path)}") + + # Derive private key + priv = derive_path(seed, path) + priv_hex = priv.hex() + + # Derive public key + pub = priv_to_compressed_pub(priv) + pub_hex = pub.hex() + + # Compute HASH160 + h160 = hash160(pub) + h160_hex = h160.hex() + + # Validate + priv_ok = priv_hex == expected_priv + pub_ok = pub_hex == expected_pub + hash_ok = h160_hex == expected_hash + + print(f" Private Key: {priv_hex}") + print(f" Expected: {expected_priv} {'✓' if priv_ok else '✗ MISMATCH'}") + + print(f" Public Key: {pub_hex}") + print(f" Expected: {expected_pub} {'✓' if pub_ok else '✗ MISMATCH'}") + + print(f" HASH160: {h160_hex}") + print(f" Expected: {expected_hash} {'✓' if hash_ok else '✗ MISMATCH'}") + + if not (priv_ok and pub_ok and hash_ok): + all_passed = False + + # Step 3: Validate DIP-18 P2PKH address encoding + print("\n" + "-" * 70) + print("DIP-18 P2PKH Address Encoding Validation") + print("-" * 70) + + for i, (_, _, _, expected_hash, expected_main, expected_test) in enumerate(DIP17_VECTORS, 1): + h160 = bytes.fromhex(expected_hash) + + mainnet_addr = encode_platform_address(h160, "p2pkh", "mainnet") + testnet_addr = encode_platform_address(h160, "p2pkh", "testnet") + + main_ok = mainnet_addr == expected_main + test_ok = testnet_addr == expected_test + + print(f"\nVector {i} (HASH160: {expected_hash[:16]}...):") + print(f" Mainnet: {mainnet_addr}") + print(f" Expected: {expected_main} {'✓' if main_ok else '✗ MISMATCH'}") + print(f" Testnet: {testnet_addr}") + print(f" Expected: {expected_test} {'✓' if test_ok else '✗ MISMATCH'}") + + # Verify round-trip decoding + dec_net_m, dec_type_m, dec_hash_m = decode_platform_address(mainnet_addr) + dec_net_t, dec_type_t, dec_hash_t = decode_platform_address(testnet_addr) + + assert dec_net_m == "mainnet" and dec_type_m == "p2pkh" and dec_hash_m.hex() == expected_hash + assert dec_net_t == "testnet" and dec_type_t == "p2pkh" and dec_hash_t.hex() == expected_hash + + if not (main_ok and test_ok): + all_passed = False + + # Step 4: Validate DIP-18 P2SH address encoding + print("\n" + "-" * 70) + print("DIP-18 P2SH Address Encoding Validation") + print("-" * 70) + + p2sh_hash, p2sh_main_expected, p2sh_test_expected = P2SH_VECTOR + h160 = bytes.fromhex(p2sh_hash) + + mainnet_addr = encode_platform_address(h160, "p2sh", "mainnet") + testnet_addr = encode_platform_address(h160, "p2sh", "testnet") + + main_ok = mainnet_addr == p2sh_main_expected + test_ok = testnet_addr == p2sh_test_expected + + print(f"\nP2SH (HASH160: {p2sh_hash[:16]}...):") + print(f" Mainnet: {mainnet_addr}") + print(f" Expected: {p2sh_main_expected} {'✓' if main_ok else '✗ MISMATCH'}") + print(f" Testnet: {testnet_addr}") + print(f" Expected: {p2sh_test_expected} {'✓' if test_ok else '✗ MISMATCH'}") + + # Verify round-trip decoding + dec_net_m, dec_type_m, dec_hash_m = decode_platform_address(mainnet_addr) + dec_net_t, dec_type_t, dec_hash_t = decode_platform_address(testnet_addr) + + assert dec_net_m == "mainnet" and dec_type_m == "p2sh" and dec_hash_m.hex() == p2sh_hash + assert dec_net_t == "testnet" and dec_type_t == "p2sh" and dec_hash_t.hex() == p2sh_hash + + if not (main_ok and test_ok): + all_passed = False + + # Summary + print("\n" + "=" * 70) + if all_passed: + print("✓ All test vectors validated successfully!") + else: + print("✗ Some test vectors failed validation!") + exit(1) + print("=" * 70)