From 7f8794da0c0d133bb3beb0ad1729b32dae2722cb Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 20 Jan 2026 12:47:19 -0500 Subject: [PATCH 1/2] feat(dip-18): use type bytes 0xb0/0x80 for P2PKH/P2SH Change type bytes to produce distinct address prefixes: - P2PKH: 0xb0 -> addresses start with "k" - P2SH: 0x80 -> addresses start with "s" Co-Authored-By: Claude Opus 4.5 --- dip-0018.md | 30 +++++++++++++++--------------- dip-0018/bech32.py | 20 ++++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/dip-0018.md b/dip-0018.md index cef3494b..8b86981e 100644 --- a/dip-0018.md +++ b/dip-0018.md @@ -61,7 +61,7 @@ Encoding a Dash Platform address uses the bech32m format defined in [BIP-350](ht Given: * `hrp`: the network human-readable prefix (e.g., `evo`, `tevo`) -* `type_byte`: `0x00` for P2PKH or `0x01` for P2SH +* `type_byte`: `0xb0` for P2PKH or `0x80` for P2SH * `hash160`: a 20-byte `HASH160(pubkey or script)` value The address MUST be encoded as follows: @@ -77,7 +77,7 @@ Decoders MUST reverse these steps and MUST verify: * Checksum validity (per [BIP-350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki#appendix-checksum-design--properties)), * HRP correctness for the target network, * Data-part length requirements, -* The type byte is either `0x00` or `0x01`. +* The type byte is either `0xb0` or `0x80`. #### Structure @@ -89,7 +89,7 @@ All Platform addresses are encoded as: * `` is network-specific (see table). * `` contains: - * one type byte (`0x00` P2PKH, `0x01` P2SH), followed by + * one type byte (`0xb0` P2PKH, `0x80` 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). @@ -122,8 +122,8 @@ Type byte meaning: | Address Type | Type byte | | -------------- | --------- | -| Platform P2PKH | `0x00` | -| Platform P2SH | `0x01` | +| Platform P2PKH | `0xb0` | +| Platform P2SH | `0x80` | ### Validation @@ -133,7 +133,7 @@ A Platform address is valid if: 2. HRP matches expected network. 3. bech32m checksum verifies. 4. Payload decodes to exactly 21 bytes. -5. `payload[0]` is `0x00` or `0x01`. +5. `payload[0]` is `0xb0` or `0x80`. Wallets MUST reject Platform addresses when constructing Dash Core chain scripts and SHOULD present a clear warning if a user attempts to mix layers. @@ -144,7 +144,7 @@ Wallets MUST reject Platform addresses when constructing Dash Core chain scripts * 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. +* Hardware wallets MUST validate the HRP to confirm network identity and MUST enforce the type byte (`0xb0` or `0x80`). Devices MUST display a user-facing descriptor: “Dash Platform address” for P2PKH and “Dash Platform script address” for P2SH. ## Rationale @@ -171,7 +171,7 @@ function encode_platform_address(hash160, type, network): if len(hash160) != 20: error("invalid hash160 length") - type_byte = 0x00 if type=="p2pkh" else 0x01 if type=="p2sh" else error() + type_byte = 0xb0 if type=="p2pkh" else 0x80 if type=="p2sh" else error() hrp = { "mainnet": "evo", @@ -209,9 +209,9 @@ function decode_platform_address(addr): type_byte = payload[0] hash160 = payload[1:21] - if type_byte == 0x00: + if type_byte == 0xb0: addr_type = "p2pkh" - else if type_byte == 0x01: + else if type_byte == 0x80: addr_type = "p2sh" else: error("unknown type byte") @@ -243,16 +243,16 @@ The HASH160 payloads in the following tables are derived from the mnemonic and p | Vector | Payload (HASH160) | Mainnet (`evo`) | Testnet (`tevo`) | | ------ | ------------------------------------------ | ------------------------------------------------ | ------------------------------------------------- | -| 1 | `f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525` | `evo1qrma5z3ttj75la4m93xcndna9ullamq9y57vzaqm` | `tevo1qrma5z3ttj75la4m93xcndna9ullamq9y5ch5x7d` | -| 2 | `a5ff0046217fd1c7d238e3e146cc5bfd90832a7e` | `evo1qzjl7qzxy9lar37j8r37z3kvt07epqe20c30csp9` | `tevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20ch5wtln` | -| 3 | `6d92674fd64472a3dfcfc3ebcfed7382bf699d7b` | `evo1qpkeye606ez89g7lelp7hnldwwpt76va0vkmt0fv` | `tevo1qpkeye606ez89g7lelp7hnldwwpt76va0vsqa5h6` | +| 1 | `f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525` | `evo1krma5z3ttj75la4m93xcndna9ullamq9y59dj9x7` | `tevo1krma5z3ttj75la4m93xcndna9ullamq9y5rky7cg` | +| 2 | `a5ff0046217fd1c7d238e3e146cc5bfd90832a7e` | `evo1kzjl7qzxy9lar37j8r37z3kvt07epqe20c2wgg8q` | `tevo1kzjl7qzxy9lar37j8r37z3kvt07epqe20cv47nek` | +| 3 | `6d92674fd64472a3dfcfc3ebcfed7382bf699d7b` | `evo1kpkeye606ez89g7lelp7hnldwwpt76va0vd6mh0f` | `tevo1kpkeye606ez89g7lelp7hnldwwpt76va0vtpdv3l` | ### P2SH example Payload: `43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636` -* Mainnet: `evo1q9pl5xpu70aka8nacc4kj2htflydspzkxccyfcmc` -* Testnet: `tevo1q9pl5xpu70aka8nacc4kj2htflydspzkxc7llr9w` +* Mainnet: `evo1sppl5xpu70aka8nacc4kj2htflydspzkxctaevg5` +* Testnet: `tevo1sppl5xpu70aka8nacc4kj2htflydspzkxcdx0hkz` ## Copyright diff --git a/dip-0018/bech32.py b/dip-0018/bech32.py index 4a411437..6c2d6342 100644 --- a/dip-0018/bech32.py +++ b/dip-0018/bech32.py @@ -155,8 +155,8 @@ def convertbits(data, frombits, tobits, pad=True): HRP_TO_NETWORK = {v: k for k, v in NETWORK_TO_HRP.items()} TYPE_TO_BYTE = { - "p2pkh": 0x00, - "p2sh": 0x01, + "p2pkh": 0xb0, + "p2sh": 0x80, } BYTE_TO_TYPE = {v: k for k, v in TYPE_TO_BYTE.items()} @@ -309,8 +309,8 @@ def format_path(path: list[int]) -> str: "6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe", "03de102ed1fc43cbdb16af02e294945ffaed8e0595d3072f4c592ae80816e6859e", "f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", - "evo1qrma5z3ttj75la4m93xcndna9ullamq9y57vzaqm", - "tevo1qrma5z3ttj75la4m93xcndna9ullamq9y5ch5x7d", + "evo1krma5z3ttj75la4m93xcndna9ullamq9y59dj9x7", + "tevo1krma5z3ttj75la4m93xcndna9ullamq9y5rky7cg", ), # Vector 2: m/9'/5'/17'/0'/0'/1 ( @@ -318,8 +318,8 @@ def format_path(path: list[int]) -> str: "eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523", "02269ff766fcd04184bc314f5385a04498df215ce1e7193cec9a607f69bc8954da", "a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", - "evo1qzjl7qzxy9lar37j8r37z3kvt07epqe20c30csp9", - "tevo1qzjl7qzxy9lar37j8r37z3kvt07epqe20ch5wtln", + "evo1kzjl7qzxy9lar37j8r37z3kvt07epqe20c2wgg8q", + "tevo1kzjl7qzxy9lar37j8r37z3kvt07epqe20cv47nek", ), # Vector 3: m/9'/5'/17'/0'/1'/0 (key_class' = 1') ( @@ -327,16 +327,16 @@ def format_path(path: list[int]) -> str: "cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db", "0317a3ed70c141cffafe00fa8bf458cec119f6fc039a7ba9a6b7303dc65b27bed3", "6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", - "evo1qpkeye606ez89g7lelp7hnldwwpt76va0vkmt0fv", - "tevo1qpkeye606ez89g7lelp7hnldwwpt76va0vsqa5h6", + "evo1kpkeye606ez89g7lelp7hnldwwpt76va0vd6mh0f", + "tevo1kpkeye606ez89g7lelp7hnldwwpt76va0vtpdv3l", ), ] # DIP-18 P2SH vector (address encoding only, no derivation path) P2SH_VECTOR = ( "43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636", - "evo1q9pl5xpu70aka8nacc4kj2htflydspzkxccyfcmc", - "tevo1q9pl5xpu70aka8nacc4kj2htflydspzkxc7llr9w", + "evo1sppl5xpu70aka8nacc4kj2htflydspzkxctaevg5", + "tevo1sppl5xpu70aka8nacc4kj2htflydspzkxcdx0hkz", ) From ad2af0c3a1856c061022b018a745122019df7fec Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 20 Jan 2026 13:38:59 -0500 Subject: [PATCH 2/2] feat(dip-18): document type byte visual identification and reserved ranges - Explain why type byte values were chosen - Add reserved type byte ranges to prevent future collisions - Add type_byte_calc.py utility for finding type bytes that map to desired characters --- dip-0018.md | 36 ++++++++++++++++--- dip-0018/type_byte_calc.py | 73 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 dip-0018/type_byte_calc.py diff --git a/dip-0018.md b/dip-0018.md index 8b86981e..2bfcf261 100644 --- a/dip-0018.md +++ b/dip-0018.md @@ -120,10 +120,28 @@ The following values define the canonical human-readable prefixes (HRPs) and typ Type byte meaning: -| Address Type | Type byte | -| -------------- | --------- | -| Platform P2PKH | `0xb0` | -| Platform P2SH | `0x80` | +| Address Type | Type byte | First data character | +| -------------- | --------- | -------------------- | +| Platform P2PKH | `0xb0` | `k` | +| Platform P2SH | `0x80` | `s` | + +The type bytes `0xb0` and `0x80` were specifically chosen so that the first character after the `1` separator is always `k` for P2PKH and `s` for P2SH. This provides immediate visual identification of address type: + +* P2PKH addresses always match pattern `evo1k...` (mainnet) or `tevo1k...` (testnet) +* P2SH addresses always match pattern `evo1s...` (mainnet) or `tevo1s...` (testnet) + +This mapping is guaranteed because the bech32 `convertbits()` function deterministically converts the first 5 bits of the type byte to the first data character. The high 5 bits of `0xb0` (binary `10110...`) map to index 22 in the bech32 alphabet (`k`), while the high 5 bits of `0x80` (binary `10000...`) map to index 16 (`s`). + +#### Reserved Type Byte Ranges + +To preserve visual identification, future address types MUST NOT reuse the first-character mappings of existing types. The first data character is determined by the high 5 bits of the type byte: + +| High 5 bits | First char | Reserved for | Type byte range | +| ----------- | ---------- | --------------- | --------------- | +| `10110` | `k` | P2PKH | `0xb0`–`0xb7` | +| `10000` | `s` | P2SH | `0x80`–`0x87` | + +Future address types SHOULD select type bytes that produce unique first characters not already in use. The reference script [`dip-0018/type_byte_calc.py`](dip-0018/type_byte_calc.py) can be used to find type bytes that map to a desired character. ### Validation @@ -148,6 +166,8 @@ Wallets MUST reject Platform addresses when constructing Dash Core chain scripts ## Rationale +### Why Bech32m? + Bech32m was chosen over Base58Check because it: * Improves checksum strength @@ -156,6 +176,14 @@ Bech32m was chosen over Base58Check because it: * Clearly separates networks using HRPs * Future-proofs script or address extensions +### Why a Type Byte? + +Platform addresses embed a type byte as the first byte of the payload. This approach was chosen because: + +* Platform addresses distinguish between *address types* (P2PKH vs P2SH) for wallet display purposes, not script interpretation versions for consensus (e.g., P2WPKH/P2WSH vs Taproot in Bitcoin). Both address types use identical HASH160 payloads with the same cryptographic properties. +* A full byte in the payload is straightforward to encode and decode without special handling separate from the hash data. +* Specific type byte values improve human identification of address type by guaranteeing that P2PKH and P2SH addresses have a unique starting characters. + ## 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. diff --git a/dip-0018/type_byte_calc.py b/dip-0018/type_byte_calc.py new file mode 100644 index 00000000..87e4b774 --- /dev/null +++ b/dip-0018/type_byte_calc.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Calculate type bytes that produce specific bech32 characters.""" + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + +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 + +def find_type_byte_for_char(target_char): + """Find type bytes where the first encoded character is target_char.""" + target_index = CHARSET.index(target_char) + print(f"\nLooking for type bytes that produce '{target_char}' (index {target_index})") + print("-" * 60) + + # Use a sample hash (all zeros for simplicity) + sample_hash = [0] * 20 + + results = [] + for type_byte in range(256): + payload = [type_byte] + sample_hash + converted = convertbits(payload, 8, 5) + if converted and CHARSET[converted[0]] == target_char: + results.append(type_byte) + + print(f"Type bytes that produce '{target_char}': {results}") + print(f"Hex values: {[hex(b) for b in results]}") + return results + +def show_encoding_for_type_byte(type_byte): + """Show the full encoding details for a given type byte.""" + sample_hash = [0] * 20 + payload = [type_byte] + sample_hash + converted = convertbits(payload, 8, 5) + + print(f"\nType byte: {type_byte} (0x{type_byte:02x}, binary: {type_byte:08b})") + print(f"First few 5-bit values: {converted[:5]}") + print(f"First few chars: {''.join(CHARSET[v] for v in converted[:5])}") + +# Find type bytes for desired characters +print("=" * 60) +print("Finding type bytes for specific first characters") +print("=" * 60) + +for char in ['q', 'k', 's', 'x', '7']: + find_type_byte_for_char(char) + +print("\n" + "=" * 60) +print("Encoding details for current and proposed type bytes") +print("=" * 60) + +# Check proposed combinations +print("\n--- Checking specific values ---") +for tb in [0x30, 0xE0, 0xA0, 0x80, 48, 224]: + show_encoding_for_type_byte(tb)