From 698b93bfb949fe6b6e4b9344fe1c98c9672ae2b2 Mon Sep 17 00:00:00 2001 From: thephez Date: Mon, 8 Dec 2025 17:07:46 -0500 Subject: [PATCH 1/7] feat(dip-18): switch to bech32m encoding with reference implementation Replace Base58Check encoding with bech32m (BIP-350) for Platform addresses. Add Python reference implementation with full DIP-17 test vector validation. --- dip-0018.md | 210 +++++++++++++------- dip-0018/bech32.py | 475 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 620 insertions(+), 65 deletions(-) create mode 100644 dip-0018/bech32.py diff --git a/dip-0018.md b/dip-0018.md index 0c1d2d4a..9826e311 100644 --- a/dip-0018.md +++ b/dip-0018.md @@ -33,72 +33,128 @@ # 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 human-readable part (HRP), data layout, and checksum rules for Platform pay-to-public-key-hash (P2PKH) addresses derived via DIP-0017, and introduces Platform pay-to-script-hash (P2SH) addresses. Distinct HRPs for different networks 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 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-0017. 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 (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 -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**, with the general encoding rules inherited from **BIP-173**. -* 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., `dashp`, `tdashp`) +* `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**, with padding enabled +3. Compute the bech32m checksum using the algorithm defined in **BIP-350**, 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** (`qpzry9x8gf2tvdw0s3jn54khce6mua7l`) +5. Produce the final address string: `hrp + "1" + encoded_data` + +Decoders MUST reverse these steps and MUST verify: + +* Checksum validity (per BIP-350), +* 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 -| Address type | Mainnet version | Expected prefix | Testnet/Devnet/Regtest version | Expected prefix | -| ------------ | --------------- | --------------- | ------------------------------ | ---------------- | -| Platform P2PKH | `0x1e` | `D` | `0x5a` | `d` | -| Platform P2SH | `0x38` | `P` | `0x75` | `p` | +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 | `dashp` | +| Testnet / Devnet / Regtest | `tdashp` | -These prefixes are distinct from Dash Core chain (`X`/`7`/`y`/`8`) and from each other. +Type byte meaning: + +| Address Type | Type byte | +| -------------- | --------- | +| Platform P2PKH | `0x00` | +| Platform P2SH | `0x01` | ## Validation -An address is valid for a network if: +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`. -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. +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 -* **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. +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 @@ -107,36 +163,58 @@ No impact on Core chain addresses. Platform P2PKH/P2SH prefixes are new and cann # Reference Implementation ```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": "dashp", + "testnet": "tdashp", + }.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) - if type == "p2pkh": - version = 0x1e if network == "mainnet" else 0x5a - else if type == "p2sh": - version = 0x38 if network == "mainnet" else 0x75 + # 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 = { + "dashp": "mainnet", + "tdashp": "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 @@ -147,7 +225,7 @@ function decode_platform_address(addr, network): # Privacy Considerations -* Base58Check addresses are unshielded; privacy relies on HD key rotation per [DIP-17](dip-0017.md) and script hygiene. +* Addresses are unshielded; 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 @@ -155,20 +233,22 @@ function decode_platform_address(addr, network): 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). -| 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 | +## P2PKH examples + +| Vector | Payload (HASH160) | Mainnet (`dashp`) | Testnet (`tdashp`) | +| ------ | ------------------------------------------ | ------------------------------------------------ | ------------------------------------------------- | +| 1 | `f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525` | `dashp1qrma5z3ttj75la4m93xcndna9ullamq9y5uywec4` | `tdashp1qrma5z3ttj75la4m93xcndna9ullamq9y5sawjck` | +| 2 | `a5ff0046217fd1c7d238e3e146cc5bfd90832a7e` | `dashp1qzjl7qzxy9lar37j8r37z3kvt07epqe20cn855et` | `tdashp1qzjl7qzxy9lar37j8r37z3kvt07epqe20cl75leg` | +| 3 | `6d92674fd64472a3dfcfc3ebcfed7382bf699d7b` | `dashp1qpkeye606ez89g7lelp7hnldwwpt76va0v5n8t3z` | `tdashp1qpkeye606ez89g7lelp7hnldwwpt76va0vc28q3p` | ## P2SH example -* Script (hex): `76a914000102030405060708090a0b0c0d0e0f101112131488ac` (standard HASH160-to-pubkey script for illustration) -* HASH160(script): `43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636` -* Mainnet P2SH: `Pe8D1pMrEnWsmuj5zCEBhHTcsFE51Asp8k` -* Testnet P2SH: `pBk15SYRYnnKfMENUnYdGw4cG1wcRmSdoh` +Payload: `43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636` + +* Mainnet: `dashp1q9pl5xpu70aka8nacc4kj2htflydspzkxc6v9urk` +* Testnet: `tdashp1q9pl5xpu70aka8nacc4kj2htflydspzkxck49hr4` # Copyright diff --git a/dip-0018/bech32.py b/dip-0018/bech32.py new file mode 100644 index 00000000..59a8b027 --- /dev/null +++ b/dip-0018/bech32.py @@ -0,0 +1,475 @@ +#!/usr/bin/env python3 +""" +DIP-18 Bech32m address encoding for Dash Platform, with full DIP-17 test vector validation. + +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 +import hashlib +import hmac + +from ecdsa import SECP256k1, SigningKey + +# ---- Generic bech32m implementation (BIP-350) ---- + +# 32-character Bech32 alphabet +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +CHARSET_REV = {c: i for i, c in enumerate(CHARSET)} + +# Generator constants for the polymod function +GENERATOR = [0x3b6a57b2, + 0x26508e6d, + 0x1ea119fa, + 0x3d4233dd, + 0x2a1462b3] + +# Bech32m constant from BIP-350 +BECH32M_CONST = 0x2bc830a3 + + +def _polymod(values) -> int: + """Internal: compute Bech32/Bech32m polymod checksum.""" + chk = 1 + for v in values: + top = chk >> 25 + chk = ((chk & 0x1ffffff) << 5) ^ v + for i in range(5): + if (top >> i) & 1: + chk ^= GENERATOR[i] + return chk + + +def _hrp_expand(hrp: str): + """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 _create_checksum_bech32m(hrp: str, data): + """Create a Bech32m checksum for given HRP and data.""" + values = _hrp_expand(hrp) + list(data) + [0, 0, 0, 0, 0, 0] + polymod = _polymod(values) ^ BECH32M_CONST + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def _verify_checksum_bech32m(hrp: str, data) -> bool: + """Verify Bech32m checksum for a given HRP and data+checksum.""" + return _polymod(_hrp_expand(hrp) + list(data)) == BECH32M_CONST + + +def bech32m_encode(hrp: str, data) -> str: + """ + Encode HRP + data (5-bit values) into a Bech32m string. + `data` should NOT contain the checksum yet. + """ + if not hrp or any(ord(c) < 33 or ord(c) > 126 for c in hrp): + raise ValueError("invalid HRP") + + # Enforce lowercase for output + hrp = hrp.lower() + + checksum = _create_checksum_bech32m(hrp, data) + combined = list(data) + checksum + return hrp + "1" + "".join(CHARSET[d] for d in combined) + + +def bech32m_decode(addr: str) -> Tuple[str, list]: + """ + Decode a Bech32m string into (hrp, data_without_checksum). + Raises ValueError if invalid or checksum fails. + """ + if any(ord(c) < 33 or ord(c) > 126 for c in addr): + raise ValueError("invalid characters") + + # Reject mixed case + if addr.lower() != addr and addr.upper() != addr: + raise ValueError("mixed case not allowed") + + addr = addr.lower() + + # Overall length constraints from BIP-173/350 + if len(addr) < 8 or len(addr) > 90: + raise ValueError("invalid length") + + # Separator must be present and not at extremes + pos = addr.rfind("1") + if pos == -1 or pos < 1 or pos + 7 > len(addr): + raise ValueError("invalid separator position") + + hrp = addr[:pos] + data_part = addr[pos + 1:] + + if not hrp: + raise ValueError("empty HRP") + + data = [] + for c in data_part: + if c not in CHARSET_REV: + raise ValueError("invalid character in data part") + data.append(CHARSET_REV[c]) + + if not _verify_checksum_bech32m(hrp, data): + raise ValueError("invalid Bech32m checksum") + + # Strip checksum (last 6 symbols) + return hrp, data[:-6] + + +def convertbits(data, frombits: int, tobits: int, pad: bool = True): + """ + General power-of-2 base conversion. + Used to convert 8-bit bytes <-> 5-bit Bech32 values. + + `data` is an iterable of integers. + """ + 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) != 0: + raise ValueError("convertbits: invalid value") + 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) + else: + if bits >= frombits: + # leftover bits are enough to encode a full symbol → invalid + raise ValueError("convertbits: invalid padding") + if (acc << (tobits - bits)) & maxv: + # leftover bits would encode a non-zero symbol → invalid + raise ValueError("convertbits: non-zero padding") + + return ret + + +# ---- Dash Platform address encoding on top of Bech32m ---- + +Network = Literal["mainnet", "testnet"] +AddrType = Literal["p2pkh", "p2sh"] + +NETWORK_TO_HRP = { + "mainnet": "dashp", + "testnet": "tdashp" +} + +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 bech32m_encode(hrp, data5) + + +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 = bech32m_decode(addr) + + 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 = bytes(convertbits(data_no_checksum, 5, 8, pad=False)) + + 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.""" + 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", + "dashp1qrma5z3ttj75la4m93xcndna9ullamq9y5uywec4", + "tdashp1qrma5z3ttj75la4m93xcndna9ullamq9y5sawjck", + ), + # Vector 2: m/9'/5'/17'/0'/0'/1 + ( + [9 + H, 5 + H, 17 + H, 0 + H, 0 + H, 1], + "eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523", + "02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da", + "a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", + "dashp1qzjl7qzxy9lar37j8r37z3kvt07epqe20cn855et", + "tdashp1qzjl7qzxy9lar37j8r37z3kvt07epqe20cl75leg", + ), + # 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", + "dashp1qpkeye606ez89g7lelp7hnldwwpt76va0v5n8t3z", + "tdashp1qpkeye606ez89g7lelp7hnldwwpt76va0vc28q3p", + ), +] + +# DIP-18 P2SH vector (address encoding only, no derivation path) +P2SH_VECTOR = ( + "43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636", + "dashp1q9pl5xpu70aka8nacc4kj2htflydspzkxc6v9urk", + "tdashp1q9pl5xpu70aka8nacc4kj2htflydspzkxck49hr4", +) + + +# ---- 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) From 4e3d5f699bc6ac89d45d922aeac499ccd8dd1d16 Mon Sep 17 00:00:00 2001 From: thephez Date: Wed, 10 Dec 2025 08:52:11 -0500 Subject: [PATCH 2/7] chore: update authors list for dip-18 --- README.md | 2 +- dip-0018.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 9826e311..c3a14b1c 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

From d6a4bcdf1a4df8a944155674f4ba8e2e5842d8be Mon Sep 17 00:00:00 2001
From: thephez 
Date: Wed, 10 Dec 2025 13:33:32 -0500
Subject: [PATCH 3/7] chore(dip18): update bech2 hrp to (t)dashevo

---
 dip-0018.md        | 26 +++++++++++++-------------
 dip-0018/bech32.py | 20 ++++++++++----------
 2 files changed, 23 insertions(+), 23 deletions(-)

diff --git a/dip-0018.md b/dip-0018.md
index c3a14b1c..92d29d7a 100644
--- a/dip-0018.md
+++ b/dip-0018.md
@@ -60,7 +60,7 @@ Encoding a Dash Platform address uses the bech32m format defined in **BIP-350**,
 
 Given:
 
-* `hrp`: the network human-readable prefix (e.g., `dashp`, `tdashp`)
+* `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
 
@@ -115,8 +115,8 @@ The following values define the canonical human-readable prefixes (HRPs) and typ
 
 | Network | HRP      |
 | ------- | -------- |
-| Mainnet | `dashp`  |
-| Testnet / Devnet / Regtest | `tdashp` |
+| Mainnet | `dashevo`  |
+| Testnet / Devnet / Regtest | `tdashevo` |
 
 Type byte meaning:
 
@@ -172,8 +172,8 @@ function encode_platform_address(hash160, type, network):
     type_byte = 0x00 if type=="p2pkh" else 0x01 if type=="p2sh" else error()
 
     hrp = {
-        "mainnet": "dashp",
-        "testnet": "tdashp",
+        "mainnet": "dashevo",
+        "testnet": "tdashevo",
     }.get(network) or error()
 
     payload = [type_byte] || hash160
@@ -193,8 +193,8 @@ function decode_platform_address(addr):
 
     # Infer network from HRP
     network = {
-        "dashp":  "mainnet",
-        "tdashp": "testnet",
+        "dashevo":  "mainnet",
+        "tdashevo": "testnet",
     }.get(hrp)
 
     if network is null:
@@ -237,18 +237,18 @@ The HASH160 payloads in the following tables are derived from the mnemonic and p
 
 ## P2PKH examples
 
-| Vector | Payload (HASH160) | Mainnet (`dashp`) | Testnet (`tdashp`) |
+| Vector | Payload (HASH160) | Mainnet (`dashevo`) | Testnet (`tdashevo`) |
 | ------ | ------------------------------------------ | ------------------------------------------------ | ------------------------------------------------- |
-| 1      | `f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525` | `dashp1qrma5z3ttj75la4m93xcndna9ullamq9y5uywec4` | `tdashp1qrma5z3ttj75la4m93xcndna9ullamq9y5sawjck` |
-| 2      | `a5ff0046217fd1c7d238e3e146cc5bfd90832a7e` | `dashp1qzjl7qzxy9lar37j8r37z3kvt07epqe20cn855et` | `tdashp1qzjl7qzxy9lar37j8r37z3kvt07epqe20cl75leg` |
-| 3      | `6d92674fd64472a3dfcfc3ebcfed7382bf699d7b` | `dashp1qpkeye606ez89g7lelp7hnldwwpt76va0v5n8t3z` | `tdashp1qpkeye606ez89g7lelp7hnldwwpt76va0vc28q3p` |
+| 1      | `f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525` | `dashevo1qrma5z3ttj75la4m93xcndna9ullamq9y5smxxxm` | `tdashevo1qrma5z3ttj75la4m93xcndna9ullamq9y5aawfeu` |
+| 2      | `a5ff0046217fd1c7d238e3e146cc5bfd90832a7e` | `dashevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20clcut89` | `tdashevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20cj75ycz` |
+| 3      | `6d92674fd64472a3dfcfc3ebcfed7382bf699d7b` | `dashevo1qpkeye606ez89g7lelp7hnldwwpt76va0vcv050v` | `tdashevo1qpkeye606ez89g7lelp7hnldwwpt76va0v428mst` |
 
 ## P2SH example
 
 Payload: `43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636`
 
-* Mainnet: `dashp1q9pl5xpu70aka8nacc4kj2htflydspzkxc6v9urk`
-* Testnet: `tdashp1q9pl5xpu70aka8nacc4kj2htflydspzkxck49hr4`
+* Mainnet: `dashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxckndrac`
+* Testnet: `tdashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxcm49vzl`
 
 # Copyright
 
diff --git a/dip-0018/bech32.py b/dip-0018/bech32.py
index 59a8b027..3bc1655b 100644
--- a/dip-0018/bech32.py
+++ b/dip-0018/bech32.py
@@ -167,8 +167,8 @@ def convertbits(data, frombits: int, tobits: int, pad: bool = True):
 AddrType = Literal["p2pkh", "p2sh"]
 
 NETWORK_TO_HRP = {
-    "mainnet": "dashp",
-    "testnet": "tdashp"
+    "mainnet": "dashevo",
+    "testnet": "tdashevo"
 }
 
 HRP_TO_NETWORK = {v: k for k, v in NETWORK_TO_HRP.items()}
@@ -317,8 +317,8 @@ def format_path(path: list[int]) -> str:
         "6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe",
         "03de102ed1fc43cbdb16af02e294945ffaed8e0595d3072f4c592ae80816e6859e",
         "f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525",
-        "dashp1qrma5z3ttj75la4m93xcndna9ullamq9y5uywec4",
-        "tdashp1qrma5z3ttj75la4m93xcndna9ullamq9y5sawjck",
+        "dashevo1qrma5z3ttj75la4m93xcndna9ullamq9y5smxxxm",
+        "tdashevo1qrma5z3ttj75la4m93xcndna9ullamq9y5aawfeu",
     ),
     # Vector 2: m/9'/5'/17'/0'/0'/1
     (
@@ -326,8 +326,8 @@ def format_path(path: list[int]) -> str:
         "eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523",
         "02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da",
         "a5ff0046217fd1c7d238e3e146cc5bfd90832a7e",
-        "dashp1qzjl7qzxy9lar37j8r37z3kvt07epqe20cn855et",
-        "tdashp1qzjl7qzxy9lar37j8r37z3kvt07epqe20cl75leg",
+        "dashevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20clcut89",
+        "tdashevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20cj75ycz",
     ),
     # Vector 3: m/9'/5'/17'/0'/1'/0 (key_class' = 1')
     (
@@ -335,16 +335,16 @@ def format_path(path: list[int]) -> str:
         "cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db",
         "0317a3ed70c141cffafe00fa8bf458cec119f6fc039a7ba9a6b7303dc65b27bed3",
         "6d92674fd64472a3dfcfc3ebcfed7382bf699d7b",
-        "dashp1qpkeye606ez89g7lelp7hnldwwpt76va0v5n8t3z",
-        "tdashp1qpkeye606ez89g7lelp7hnldwwpt76va0vc28q3p",
+        "dashevo1qpkeye606ez89g7lelp7hnldwwpt76va0vcv050v",
+        "tdashevo1qpkeye606ez89g7lelp7hnldwwpt76va0v428mst",
     ),
 ]
 
 # DIP-18 P2SH vector (address encoding only, no derivation path)
 P2SH_VECTOR = (
     "43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636",
-    "dashp1q9pl5xpu70aka8nacc4kj2htflydspzkxc6v9urk",
-    "tdashp1q9pl5xpu70aka8nacc4kj2htflydspzkxck49hr4",
+    "dashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxckndrac",
+    "tdashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxcm49vzl",
 )
 
 

From d803fc0776b1b22da02f80efa36074ad5e137cf9 Mon Sep 17 00:00:00 2001
From: thephez 
Date: Thu, 11 Dec 2025 11:30:16 -0500
Subject: [PATCH 4/7] docs(dip18): formatting and minor updates

---
 dip-0018.md | 62 +++++++++++++++++++++++++++--------------------------
 1 file changed, 32 insertions(+), 30 deletions(-)

diff --git a/dip-0018.md b/dip-0018.md
index 92d29d7a..47de2044 100644
--- a/dip-0018.md
+++ b/dip-0018.md
@@ -31,32 +31,32 @@
 1. [Test Vectors](#test-vectors)
 1. [Copyright](#copyright)
 
-# Abstract
+## Abstract
 
-This DIP specifies the bech32m address encoding formats for Dash Platform payments. It defines human-readable part (HRP), data layout, and checksum rules for Platform pay-to-public-key-hash (P2PKH) addresses derived via DIP-0017, and introduces Platform pay-to-script-hash (P2SH) addresses. Distinct HRPs for different networks prevent confusion with Dash Core chain addresses and with other bech32-based formats.
+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-0017. 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.
+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:** `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
 
-### Encoding Algorithm Summary
+#### Encoding Algorithm Summary
 
-Encoding a Dash Platform address uses the bech32m format defined in **BIP-350**, with the general encoding rules inherited from **BIP-173**.
+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:
 
@@ -67,19 +67,19 @@ Given:
 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**, with padding enabled
-3. Compute the bech32m checksum using the algorithm defined in **BIP-350**, 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** (`qpzry9x8gf2tvdw0s3jn54khce6mua7l`)
+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),
+* 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
+#### Structure
 
 All Platform addresses are encoded as:
 
@@ -94,14 +94,14 @@ All Platform addresses are encoded as:
 
 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
+#### 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
+#### 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):
 
@@ -109,7 +109,7 @@ The bech32m encoding used in this specification requires a fixed, 32-character b
 qpzry9x8gf2tvdw0s3jn54khce6mua7l
 ```
 
-## Network Parameters
+### 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.
 
@@ -125,7 +125,7 @@ Type byte meaning:
 | Platform P2PKH | `0x00`    |
 | Platform P2SH  | `0x01`    |
 
-## Validation
+### Validation
 
 A Platform address is valid if:
 
@@ -137,7 +137,7 @@ A Platform address is valid if:
 
 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
+### 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.
@@ -146,7 +146,7 @@ Wallets MUST reject Platform addresses when constructing Dash Core chain scripts
 * 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:
 
@@ -156,11 +156,13 @@ Bech32m was chosen over Base58Check because it:
 * 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: This reference code 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):
@@ -217,25 +219,25 @@ function decode_platform_address(addr):
     return network, addr_type, hash160
 ```
 
-# Security Considerations
+## 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
 
-* 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: `""`
 
 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
+### P2PKH examples
 
 | Vector | Payload (HASH160) | Mainnet (`dashevo`) | Testnet (`tdashevo`) |
 | ------ | ------------------------------------------ | ------------------------------------------------ | ------------------------------------------------- |
@@ -243,13 +245,13 @@ The HASH160 payloads in the following tables are derived from the mnemonic and p
 | 2      | `a5ff0046217fd1c7d238e3e146cc5bfd90832a7e` | `dashevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20clcut89` | `tdashevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20cj75ycz` |
 | 3      | `6d92674fd64472a3dfcfc3ebcfed7382bf699d7b` | `dashevo1qpkeye606ez89g7lelp7hnldwwpt76va0vcv050v` | `tdashevo1qpkeye606ez89g7lelp7hnldwwpt76va0v428mst` |
 
-## P2SH example
+### P2SH example
 
 Payload: `43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636`
 
 * Mainnet: `dashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxckndrac`
 * Testnet: `tdashevo1q9pl5xpu70aka8nacc4kj2htflydspzkxcm49vzl`
 
-# Copyright
+## Copyright
 
 Copyright (c) 2025 Dash Core Group, Inc. [Licensed under the MIT License](https://opensource.org/licenses/MIT)

From 96f032a32fe3d76990529a214a1ca354f199b006 Mon Sep 17 00:00:00 2001
From: thephez 
Date: Thu, 11 Dec 2025 12:59:28 -0500
Subject: [PATCH 5/7] fix(dip18): add BIP-39 NFKD normalization to
 mnemonic_to_seed

Normalize mnemonic and passphrase with NFKD before UTF-8 encoding per BIP-39 spec.
---
 dip-0018/bech32.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/dip-0018/bech32.py b/dip-0018/bech32.py
index 3bc1655b..1467aec8 100644
--- a/dip-0018/bech32.py
+++ b/dip-0018/bech32.py
@@ -18,6 +18,7 @@
 from hashlib import sha256, pbkdf2_hmac
 import hashlib
 import hmac
+import unicodedata
 
 from ecdsa import SECP256k1, SigningKey
 
@@ -249,6 +250,8 @@ def decode_platform_address(addr: str) -> Tuple[Network, AddrType, bytes]:
 
 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"),

From 44570a5e690ba8151d232faaa9a6f72015f503c7 Mon Sep 17 00:00:00 2001
From: thephez 
Date: Thu, 11 Dec 2025 13:47:35 -0500
Subject: [PATCH 6/7] refactor(dip18): use verbatim BIP-350 reference
 implementation

Replace custom Bech32m implementation with exact code from sipa's reference at github.com/sipa/bech32. Adds MIT license header and proper attribution.
---
 dip-0018/bech32.py | 205 +++++++++++++++++++++------------------------
 1 file changed, 97 insertions(+), 108 deletions(-)

diff --git a/dip-0018/bech32.py b/dip-0018/bech32.py
index 1467aec8..6b6477fa 100644
--- a/dip-0018/bech32.py
+++ b/dip-0018/bech32.py
@@ -1,7 +1,30 @@
 #!/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)
@@ -16,152 +39,109 @@
 
 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
 
-# ---- Generic bech32m implementation (BIP-350) ----
+# ---- 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
 
-# 32-character Bech32 alphabet
-CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
-CHARSET_REV = {c: i for i, c in enumerate(CHARSET)}
 
-# Generator constants for the polymod function
-GENERATOR = [0x3b6a57b2,
-             0x26508e6d,
-             0x1ea119fa,
-             0x3d4233dd,
-             0x2a1462b3]
+class Encoding(Enum):
+    """Enumeration type to list the various supported encodings."""
+    BECH32 = 1
+    BECH32M = 2
 
-# Bech32m constant from BIP-350
+CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
 BECH32M_CONST = 0x2bc830a3
 
-
-def _polymod(values) -> int:
-    """Internal: compute Bech32/Bech32m polymod checksum."""
+def bech32_polymod(values):
+    """Internal function that computes the Bech32 checksum."""
+    generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
     chk = 1
-    for v in values:
+    for value in values:
         top = chk >> 25
-        chk = ((chk & 0x1ffffff) << 5) ^ v
+        chk = (chk & 0x1ffffff) << 5 ^ value
         for i in range(5):
-            if (top >> i) & 1:
-                chk ^= GENERATOR[i]
+            chk ^= generator[i] if ((top >> i) & 1) else 0
     return chk
 
 
-def _hrp_expand(hrp: str):
+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 _create_checksum_bech32m(hrp: str, data):
-    """Create a Bech32m checksum for given HRP and data."""
-    values = _hrp_expand(hrp) + list(data) + [0, 0, 0, 0, 0, 0]
-    polymod = _polymod(values) ^ BECH32M_CONST
+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 _verify_checksum_bech32m(hrp: str, data) -> bool:
-    """Verify Bech32m checksum for a given HRP and data+checksum."""
-    return _polymod(_hrp_expand(hrp) + list(data)) == BECH32M_CONST
-
-
-def bech32m_encode(hrp: str, data) -> str:
-    """
-    Encode HRP + data (5-bit values) into a Bech32m string.
-    `data` should NOT contain the checksum yet.
-    """
-    if not hrp or any(ord(c) < 33 or ord(c) > 126 for c in hrp):
-        raise ValueError("invalid HRP")
-
-    # Enforce lowercase for output
-    hrp = hrp.lower()
-
-    checksum = _create_checksum_bech32m(hrp, data)
-    combined = list(data) + checksum
-    return hrp + "1" + "".join(CHARSET[d] for d in combined)
-
-
-def bech32m_decode(addr: str) -> Tuple[str, list]:
-    """
-    Decode a Bech32m string into (hrp, data_without_checksum).
-    Raises ValueError if invalid or checksum fails.
-    """
-    if any(ord(c) < 33 or ord(c) > 126 for c in addr):
-        raise ValueError("invalid characters")
-
-    # Reject mixed case
-    if addr.lower() != addr and addr.upper() != addr:
-        raise ValueError("mixed case not allowed")
-
-    addr = addr.lower()
-
-    # Overall length constraints from BIP-173/350
-    if len(addr) < 8 or len(addr) > 90:
-        raise ValueError("invalid length")
-
-    # Separator must be present and not at extremes
-    pos = addr.rfind("1")
-    if pos == -1 or pos < 1 or pos + 7 > len(addr):
-        raise ValueError("invalid separator position")
-
-    hrp = addr[:pos]
-    data_part = addr[pos + 1:]
-
-    if not hrp:
-        raise ValueError("empty HRP")
-
-    data = []
-    for c in data_part:
-        if c not in CHARSET_REV:
-            raise ValueError("invalid character in data part")
-        data.append(CHARSET_REV[c])
-
-    if not _verify_checksum_bech32m(hrp, data):
-        raise ValueError("invalid Bech32m checksum")
-
-    # Strip checksum (last 6 symbols)
-    return hrp, data[:-6]
-
-
-def convertbits(data, frombits: int, tobits: int, pad: bool = True):
-    """
-    General power-of-2 base conversion.
-    Used to convert 8-bit bytes <-> 5-bit Bech32 values.
-
-    `data` is an iterable of integers.
-    """
+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) != 0:
-            raise ValueError("convertbits: invalid value")
+        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)
-    else:
-        if bits >= frombits:
-            # leftover bits are enough to encode a full symbol → invalid
-            raise ValueError("convertbits: invalid padding")
-        if (acc << (tobits - bits)) & maxv:
-            # leftover bits would encode a non-zero symbol → invalid
-            raise ValueError("convertbits: non-zero padding")
-
+    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"]
@@ -208,7 +188,7 @@ def encode_platform_address(hash160: bytes,
     payload_bytes = bytes([type_byte]) + hash160  # 21 bytes
     data5 = convertbits(payload_bytes, 8, 5, pad=True)
 
-    return bech32m_encode(hrp, data5)
+    return bech32_encode(hrp, data5, Encoding.BECH32M)
 
 
 def decode_platform_address(addr: str) -> Tuple[Network, AddrType, bytes]:
@@ -218,7 +198,13 @@ def decode_platform_address(addr: str) -> Tuple[Network, AddrType, bytes]:
     :param addr: Bech32m-encoded Dash Platform address
     :return: (network, "p2pkh"/"p2sh", 20-byte hash160)
     """
-    hrp, data_no_checksum = bech32m_decode(addr)
+    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")
@@ -226,7 +212,10 @@ def decode_platform_address(addr: str) -> Tuple[Network, AddrType, bytes]:
     network = HRP_TO_NETWORK[hrp]
 
     # Convert back to 8-bit payload, without padding
-    payload_bytes = bytes(convertbits(data_no_checksum, 5, 8, pad=False))
+    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)")

From 1a3e5ec87275fd672cdda4bfd1a1628d94865221 Mon Sep 17 00:00:00 2001
From: thephez 
Date: Thu, 11 Dec 2025 15:08:25 -0500
Subject: [PATCH 7/7] docs(dip18): reference Python implementation in spec

---
 dip-0018.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/dip-0018.md b/dip-0018.md
index 47de2044..361fd604 100644
--- a/dip-0018.md
+++ b/dip-0018.md
@@ -162,7 +162,7 @@ No impact on Core chain addresses. Platform P2PKH/P2SH prefixes are new and cann
 
 ## Reference Implementation
 
-Note: This reference code covers the encoding and decoding parts of DIP-18 only, not the wallet-UI or signing device behaviors.
+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):
@@ -219,6 +219,8 @@ function decode_platform_address(addr):
     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.