diff --git a/README.md b/README.md index 83f8a054..4c701011 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Number | Layer | Title | Owner | Type | Status [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 | 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-0018.md b/dip-0018.md index 0c1d2d4a..361fd604 100644 --- a/dip-0018.md +++ b/dip-0018.md @@ -1,7 +1,7 @@
DIP: 0018 Title: Dash Platform Payment Address Encodings - Author(s): Samuel Westrich + Author(s): Samuel Westrich, thephez Special-Thanks: Dash Platform Team Comments-Summary: No comments yet. Status: Draft @@ -31,145 +31,229 @@ 1. [Test Vectors](#test-vectors) 1. [Copyright](#copyright) -# Abstract +## Abstract -This DIP specifies the address encoding formats for Dash Platform payments. It defines Base58Check parameters 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 mainnet and testnet prefixes prevent confusion with Dash Core chain addresses. +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 +## 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 prefixes and checksum rules is required. Script-hash addresses are also needed for multisig and other script-based Platform payments. +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 +## 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 +## Specification -## Address Types +### Address Types -* **Platform P2PKH (D-address):** HASH160 of a compressed secp256k1 public key derived per [DIP-17](dip-0017.md). -* **Platform P2SH:** HASH160 of a Platform script (e.g., multisig or policy script) for receiving Platform payments to scripts. +* **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 -Base58Check is used for all Platform address types. +#### Encoding Algorithm Summary -Payloads: +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). -* P2PKH payload: `HASH160(pubkey)` where `pubkey` is compressed secp256k1 (33 bytes). -* P2SH payload: `HASH160(script)` where `script` is the raw byte serialization of the Platform script being paid to. +Given: -Encoding algorithm (for either address type): +* `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 -1. Prepend the network-specific version byte for the address type. -2. Compute checksum = first 4 bytes of `SHA256(SHA256(version || payload))`. -3. Concatenate `version || payload || checksum`. -4. Base58Check-encode the 25-byte result. +The address MUST be encoded as follows: -Decoding reverses these steps and verifies checksum and version. +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` -## Network Parameters +Decoders MUST reverse these steps and MUST verify: -| Address type | Mainnet version | Expected prefix | Testnet/Devnet/Regtest version | Expected prefix | -| ------------ | --------------- | --------------- | ------------------------------ | ---------------- | -| Platform P2PKH | `0x1e` | `D` | `0x5a` | `d` | -| Platform P2SH | `0x38` | `P` | `0x75` | `p` | +* 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`. -These prefixes are distinct from Dash Core chain (`X`/`7`/`y`/`8`) and from each other. +#### Structure -## Validation +All Platform addresses are encoded as: -An address is valid for a network if: +```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. Base58 decoding yields 25 bytes. -2. The checksum matches the first 4 bytes of `SHA256(SHA256(version || payload))`. -3. The version byte equals the network’s P2PKH or P2SH Platform prefix. -4. Wallets MUST reject Platform addresses when constructing Dash Core chain scripts and SHOULD present a clear warning if a user attempts to mix layers. +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`. -## Wallet and Hardware Wallet Behavior +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. -* Hardware wallets MUST whitelist the version bytes above and display “Dash Platform address” or “Dash Platform script address” as appropriate. +* 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 -# Rationale +Bech32m was chosen over Base58Check because it: -* **Base58Check:** Aligns with existing Dash address UX and reduces user confusion compared to introducing a new encoding. -* **Distinct prefixes:** `D/d` for P2PKH and `P/p` for P2SH avoid collisions with Core chain addresses and with each other. -* **Script support:** P2SH enables multisig and policy scripts on Platform without overloading the P2PKH prefix. +* Improves checksum strength +* Is QR efficient +* Avoids ambiguous characters +* Clearly separates networks using HRPs +* Future-proofs script or address extensions -# Backwards Compatibility +## 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 +## 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(payload, type, network): - # payload: 20-byte HASH160 +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) - if type == "p2pkh": - version = 0x1e if network == "mainnet" else 0x5a - else if type == "p2sh": - version = 0x38 if network == "mainnet" else 0x75 + # 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") - - data = version || payload - checksum = SHA256(SHA256(data))[0:4] - return Base58Encode(data || checksum) - -function decode_platform_address(addr, network): - raw = Base58Decode(addr) - assert len(raw) == 25 - version = raw[0] - payload = raw[1:21] - checksum = raw[21:25] - expect = SHA256(SHA256(raw[0:21]))[0:4] - assert checksum == expect - - valid_p2pkh = (version == 0x1e and network == "mainnet") or (version == 0x5a and network != "mainnet") - valid_p2sh = (version == 0x38 and network == "mainnet") or (version == 0x75 and network != "mainnet") - assert valid_p2pkh or valid_p2sh - - return payload, ("p2pkh" if valid_p2pkh else "p2sh") + error("unknown type byte") + + return network, addr_type, hash160 ``` -# Security Considerations +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 Considerations -* Base58Check addresses are unshielded; privacy relies on HD key rotation per [DIP-17](dip-0017.md) and script hygiene. +* 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 +## Test Vectors Mnemonic (shared with [DIP-17](dip-0017.md)): `abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about` Passphrase: `""` -## P2PKH examples (payloads from DIP-17) +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` | -| Vector | Path (mainnet / testnet) | account' | key_class' | index | HASH160(pubkey) | Mainnet P2PKH | Testnet P2PKH | -| ------ | ----------------------- | -------- | ---------- | ----- | --------------- | ------------- | -------------- | -| 1 | m/9'/5'/17'/0'/0'/0 / m/9'/1'/17'/0'/0'/0 | 0' | 0' | 0 | f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525 | DTjceJiEqrNkCsSizK65fojEANTKoQMtsR | dSqV2orinasFpYAMGQTLy6uYpW9Dnge563 | -| 2 | m/9'/5'/17'/0'/0'/1 / m/9'/1'/17'/0'/0'/1 | 0' | 0' | 1 | a5ff0046217fd1c7d238e3e146cc5bfd90832a7e | DLGoWhHfAyFcJafRgt2fFN7fxgLqNrfCXm | dZoepEc46ivSfm3VYr8mJeA4hZXYytgkKZ | -| 3 | m/9'/5'/17'/0'/1'/0 / m/9'/1'/17'/0'/1'/0 | 0' | 1' | 0 | 6d92674fd64472a3dfcfc3ebcfed7382bf699d7b | DF8TaTy7YrLdGYqq6cwapSUqdM3qJxLQbo | dFQSkGujaeDNwWTQDDVfbrurQ9ChXXYDov | +### P2SH example -## P2SH example +Payload: `43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636` -* Script (hex): `76a914000102030405060708090a0b0c0d0e0f101112131488ac` (standard HASH160-to-pubkey script for illustration) -* HASH160(script): `43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636` -* Mainnet P2SH: `Pe8D1pMrEnWsmuj5zCEBhHTcsFE51Asp8k` -* Testnet P2SH: `pBk15SYRYnnKfMENUnYdGw4cG1wcRmSdoh` +* Mainnet: `dashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxckndrac` +* Testnet: `tdashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxcm49vzl` -# Copyright +## 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)