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)