From 51ce91f7ab651da451517bc991d2984e6d95d6a7 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Fri, 17 Apr 2026 03:05:48 +0700 Subject: [PATCH 1/6] feat: add utils to parse platform bech32m destinations --- src/chainparams.cpp | 12 ++++++ src/chainparams.h | 3 ++ src/key_io.cpp | 97 +++++++++++++++++++++++++++++++++++++++++++++ src/key_io.h | 31 +++++++++++++++ 4 files changed, 143 insertions(+) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 377b6b5af5b7..2d7910e34672 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -260,6 +260,9 @@ class CMainParams : public CChainParams { // Dash BIP32 prvkeys start with 'xprv' (Bitcoin defaults) base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x88, 0xAD, 0xE4}; + // DIP-18 Dash Platform address HRP (bech32m) + bech32_platform_hrp = "dash"; + // Dash BIP44 coin type is '5' nExtCoinType = 5; @@ -452,6 +455,9 @@ class CTestNetParams : public CChainParams { // Testnet Dash BIP32 prvkeys start with 'tprv' (Bitcoin defaults) base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; + // DIP-18 Dash Platform address HRP (bech32m) + bech32_platform_hrp = "tdash"; + // Testnet Dash BIP44 coin type is '1' (All coin's testnet default) nExtCoinType = 1; @@ -625,6 +631,9 @@ class CDevNetParams : public CChainParams { // Testnet Dash BIP32 prvkeys start with 'tprv' (Bitcoin defaults) base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; + // DIP-18 Dash Platform address HRP (bech32m) + bech32_platform_hrp = "tdash"; + // Testnet Dash BIP44 coin type is '1' (All coin's testnet default) nExtCoinType = 1; @@ -900,6 +909,9 @@ class CRegTestParams : public CChainParams { // Regtest Dash BIP32 prvkeys start with 'tprv' (Bitcoin defaults) base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; + // DIP-18 Dash Platform address HRP (bech32m) + bech32_platform_hrp = "tdash"; + // Regtest Dash BIP44 coin type is '1' (All coin's testnet default) nExtCoinType = 1; diff --git a/src/chainparams.h b/src/chainparams.h index 7b5528b9b550..058570124306 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -124,6 +124,8 @@ class CChainParams /** Return the list of hostnames to look up for DNS seeds */ const std::vector& DNSSeeds() const { return vSeeds; } const std::vector& Base58Prefix(Base58Type type) const { return base58Prefixes[type]; } + /** DIP-18 Platform address bech32m HRP: "dash" on mainnet, "tdash" on test chains */ + const std::string& Bech32PlatformHRP() const { return bech32_platform_hrp; } int ExtCoinType() const { return nExtCoinType; } const std::vector& FixedSeeds() const { return vFixedSeeds; } const CCheckpointData& Checkpoints() const { return checkpointData; } @@ -164,6 +166,7 @@ class CChainParams uint64_t m_assumed_chain_state_size; std::vector vSeeds; std::vector base58Prefixes[MAX_BASE58_TYPES]; + std::string bech32_platform_hrp; int nExtCoinType; std::string strNetworkID; CBlock genesis; diff --git a/src/key_io.cpp b/src/key_io.cpp index 0136857973fa..b90c08bb13dd 100644 --- a/src/key_io.cpp +++ b/src/key_io.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -176,3 +177,99 @@ bool IsValidDestinationString(const std::string& str) { return IsValidDestinationString(str, Params()); } + +namespace { +constexpr uint8_t DIP18_TYPE_BYTE_P2PKH = 0xb0; +constexpr uint8_t DIP18_TYPE_BYTE_P2SH = 0x80; +constexpr size_t DIP18_PAYLOAD_SIZE = 21; // 1 type byte + 20-byte HASH160 + +std::string EncodePlatformBech32m(const CChainParams& params, uint8_t type_byte, const BaseHash& hash) +{ + std::vector payload; + payload.reserve(DIP18_PAYLOAD_SIZE); + payload.push_back(type_byte); + payload.insert(payload.end(), hash.begin(), hash.end()); + std::vector values; + values.reserve(((DIP18_PAYLOAD_SIZE * 8) + 4) / 5); + ConvertBits<8, 5, true>([&](uint8_t v) { values.push_back(v); }, payload.begin(), payload.end()); + return bech32::Encode(bech32::Encoding::BECH32M, params.Bech32PlatformHRP(), values); +} + +class PlatformDestinationEncoder +{ +private: + const CChainParams& m_params; + +public: + explicit PlatformDestinationEncoder(const CChainParams& params) : m_params(params) {} + + std::string operator()(const PlatformP2PKHDestination& id) const + { + return EncodePlatformBech32m(m_params, DIP18_TYPE_BYTE_P2PKH, id); + } + std::string operator()(const PlatformP2SHDestination& id) const + { + return EncodePlatformBech32m(m_params, DIP18_TYPE_BYTE_P2SH, id); + } + std::string operator()(const CNoDestination&) const { return {}; } +}; +} // namespace + +bool IsValidPlatformDestination(const PlatformDestination& dest) +{ + return !std::holds_alternative(dest); +} + +std::string EncodePlatformDestination(const PlatformDestination& dest) +{ + return std::visit(PlatformDestinationEncoder(Params()), dest); +} + +PlatformDestination DecodePlatformDestination(const std::string& str, const CChainParams& params, std::string& error_str) +{ + error_str.clear(); + const bech32::DecodeResult dec = bech32::Decode(str); + if (dec.encoding == bech32::Encoding::INVALID) { + error_str = "Invalid bech32m encoding"; + return CNoDestination(); + } + if (dec.encoding != bech32::Encoding::BECH32M) { + error_str = "DIP-18 Platform addresses require bech32m checksum"; + return CNoDestination(); + } + if (dec.hrp != params.Bech32PlatformHRP()) { + error_str = "Invalid Platform HRP for the selected network"; + return CNoDestination(); + } + std::vector payload; + payload.reserve((dec.data.size() * 5) / 8); + if (!ConvertBits<5, 8, false>([&](uint8_t b) { payload.push_back(b); }, dec.data.begin(), dec.data.end())) { + error_str = "Invalid Platform address payload encoding"; + return CNoDestination(); + } + if (payload.size() != DIP18_PAYLOAD_SIZE) { + error_str = "Invalid Platform address payload length"; + return CNoDestination(); + } + uint160 hash; + std::copy(payload.begin() + 1, payload.end(), hash.begin()); + switch (payload[0]) { + case DIP18_TYPE_BYTE_P2PKH: + return PlatformP2PKHDestination(hash); + case DIP18_TYPE_BYTE_P2SH: + return PlatformP2SHDestination(hash); + } + error_str = "Unknown DIP-18 type byte"; + return CNoDestination(); +} + +PlatformDestination DecodePlatformDestination(const std::string& str, std::string& error_str) +{ + return DecodePlatformDestination(str, Params(), error_str); +} + +PlatformDestination DecodePlatformDestination(const std::string& str) +{ + std::string error_str; + return DecodePlatformDestination(str, error_str); +} diff --git a/src/key_io.h b/src/key_io.h index 605a087433f3..11bbe5cd6fb1 100644 --- a/src/key_io.h +++ b/src/key_io.h @@ -28,4 +28,35 @@ CTxDestination DecodeDestination(const std::string& str, std::string& error_msg) bool IsValidDestinationString(const std::string& str); bool IsValidDestinationString(const std::string& str, const CChainParams& params); +/** + * DIP-18 Dash Platform addresses (bech32m). + * + * Platform addresses decode to a 20-byte HASH160 prefixed by a type byte: + * 0xb0 -> Platform P2PKH (addresses of the form dash1k... / tdash1k...) + * 0x80 -> Platform P2SH (addresses of the form dash1s... / tdash1s...) + * + * Unlike base58 Dash addresses, Platform destinations have no on-chain + * scriptPubKey: they are only valid as credit output recipients of an + * asset-lock special transaction (see DIP-27 and src/evo/assetlocktx.h). + */ +struct PlatformP2PKHDestination : public BaseHash +{ + PlatformP2PKHDestination() = default; + explicit PlatformP2PKHDestination(const uint160& hash) : BaseHash(hash) {} +}; + +struct PlatformP2SHDestination : public BaseHash +{ + PlatformP2SHDestination() = default; + explicit PlatformP2SHDestination(const uint160& hash) : BaseHash(hash) {} +}; + +using PlatformDestination = std::variant; + +bool IsValidPlatformDestination(const PlatformDestination& dest); +std::string EncodePlatformDestination(const PlatformDestination& dest); +PlatformDestination DecodePlatformDestination(const std::string& str); +PlatformDestination DecodePlatformDestination(const std::string& str, std::string& error_str); +PlatformDestination DecodePlatformDestination(const std::string& str, const CChainParams& params, std::string& error_str); + #endif // BITCOIN_KEY_IO_H From c3261a9ae7ce7daa76bc8a6c1d9522bd46791244 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Fri, 17 Apr 2026 03:24:50 +0700 Subject: [PATCH 2/6] test: regression tests for platform addresses --- src/test/key_io_tests.cpp | 99 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/test/key_io_tests.cpp b/src/test/key_io_tests.cpp index 827c6cc71e18..55a0b22f55fc 100644 --- a/src/test/key_io_tests.cpp +++ b/src/test/key_io_tests.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -146,4 +147,102 @@ BOOST_AUTO_TEST_CASE(key_io_invalid) } } +// DIP-18: Dash Platform bech32m address encoding. +BOOST_AUTO_TEST_CASE(dip18_platform_roundtrip) +{ + struct Sample { + std::string hash_hex; + std::string address; + std::string chain; + bool is_p2sh; + }; + // Samples from DIP-0018 (Test Vectors section). + const Sample samples[] = { + {"f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", "dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs", CBaseChainParams::MAIN, false}, + {"a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", "dash1kzjl7qzxy9lar37j8r37z3kvt07epqe20ckxfezw", CBaseChainParams::MAIN, false}, + {"6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", "dash1kpkeye606ez89g7lelp7hnldwwpt76va0v3j6x28", CBaseChainParams::MAIN, false}, + {"f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", "tdash1krma5z3ttj75la4m93xcndna9ullamq9y5fzq2j7", CBaseChainParams::TESTNET, false}, + {"a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", "tdash1kzjl7qzxy9lar37j8r37z3kvt07epqe20cxp68nq", CBaseChainParams::TESTNET, false}, + {"6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", "tdash1kpkeye606ez89g7lelp7hnldwwpt76va0vp4fcmf", CBaseChainParams::TESTNET, false}, + {"43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636", "dash1sppl5xpu70aka8nacc4kj2htflydspzkxch4cad6", CBaseChainParams::MAIN, true}, + {"43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636", "tdash1sppl5xpu70aka8nacc4kj2htflydspzkxc8jtru5", CBaseChainParams::TESTNET, true}, + }; + for (const auto& s : samples) { + SelectParams(s.chain); + std::string err; + PlatformDestination dest = DecodePlatformDestination(s.address, err); + BOOST_REQUIRE_MESSAGE(IsValidPlatformDestination(dest), + std::string{"decode failed: "} + s.address + " err=" + err); + std::vector got_hash; + if (s.is_p2sh) { + BOOST_REQUIRE(std::holds_alternative(dest)); + const auto& h = std::get(dest); + got_hash.assign(h.begin(), h.end()); + } else { + BOOST_REQUIRE(std::holds_alternative(dest)); + const auto& h = std::get(dest); + got_hash.assign(h.begin(), h.end()); + } + BOOST_CHECK_EQUAL(HexStr(got_hash), std::string(s.hash_hex)); + BOOST_CHECK_EQUAL(EncodePlatformDestination(dest), std::string(s.address)); + } + SelectParams(CBaseChainParams::MAIN); +} + +BOOST_AUTO_TEST_CASE(dip18_platform_invalid) +{ + SelectParams(CBaseChainParams::MAIN); + std::string err; + + // Wrong HRP for the selected network (testnet string on mainnet). + BOOST_CHECK(!IsValidPlatformDestination( + DecodePlatformDestination("tdash1krma5z3ttj75la4m93xcndna9ullamq9y5fzq2j7", err))); + + // Mixed case is forbidden by BIP-173. + BOOST_CHECK(!IsValidPlatformDestination( + DecodePlatformDestination("Dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs", err))); + + // Bech32 (BIP-173) checksum MUST be rejected; only bech32m is valid for DIP-18. + // Re-encode the same 21-byte payload with the BIP-173 generator and verify rejection. + { + std::vector payload = ParseHex("b0f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525"); + std::vector values; + ConvertBits<8, 5, true>([&](uint8_t b) { values.push_back(b); }, payload.begin(), payload.end()); + const std::string bech32_str = bech32::Encode(bech32::Encoding::BECH32, "dash", values); + BOOST_REQUIRE(!bech32_str.empty()); + BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination(bech32_str, err))); + } + + // Unknown DIP-18 type byte (0x00) must be rejected. + { + std::vector payload = ParseHex("00f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525"); + std::vector values; + ConvertBits<8, 5, true>([&](uint8_t b) { values.push_back(b); }, payload.begin(), payload.end()); + const std::string bad = bech32::Encode(bech32::Encoding::BECH32M, "dash", values); + BOOST_REQUIRE(!bad.empty()); + BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination(bad, err))); + } + + // Wrong payload length (19-byte hash) must be rejected. + { + std::vector payload = ParseHex("b0f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec05"); + std::vector values; + ConvertBits<8, 5, true>([&](uint8_t b) { values.push_back(b); }, payload.begin(), payload.end()); + const std::string bad = bech32::Encode(bech32::Encoding::BECH32M, "dash", values); + BOOST_REQUIRE(!bad.empty()); + BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination(bad, err))); + } + + // Empty / garbage inputs. + BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination("", err))); + BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination("not-an-address", err))); + + // Mainnet address on testnet must fail. + SelectParams(CBaseChainParams::TESTNET); + BOOST_CHECK(!IsValidPlatformDestination( + DecodePlatformDestination("dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs", err))); + + SelectParams(CBaseChainParams::MAIN); +} + BOOST_AUTO_TEST_SUITE_END() From 393ca5ee223a3f4cb5a41e17aa490581c7d0f513 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Fri, 29 Sep 2017 10:11:01 +0200 Subject: [PATCH 3/6] partial Merge #11167: Full BIP173 (Bech32) support 8213838 [Qt] tolerate BIP173/bech32 addresses during input validation (Jonas Schnelli) 06eaca6 [RPC] Wallet: test importing of native witness scripts (NicolasDorier) fd0041a Use BIP173 addresses in segwit.py test (Pieter Wuille) e278f12 Support BIP173 in addwitnessaddress (Pieter Wuille) c091b99 Implement BIP173 addresses and tests (Pieter Wuille) bd355b8 Add regtest testing to base58_tests (Pieter Wuille) 6565c55 Convert base58_tests from type/payload to scriptPubKey comparison (Pieter Wuille) 8fd2267 Import Bech32 C++ reference code & tests (Pieter Wuille) 1e46ebd Implement {Encode,Decode}Destination without CBitcoinAddress (Pieter Wuille) Pull request description: Builds on top of #11117. This adds support for: * Creating BIP173 addresses for testing (through `addwitnessaddress`, though by default it still produces P2SH versions) * Sending to BIP173 addresses (including non-v0 ones) * Analysing BIP173 addresses (through `validateaddress`) It includes a reformatted version of the [C++ Bech32 reference code](https://github.com/sipa/bech32/tree/master/ref/c%2B%2B) and an independent implementation of the address encoding/decoding logic (integrated with CTxDestination). All BIP173 test vectors are included. Not included (and intended for other PRs): * Full wallet support for SegWit (which would include automatically adding witness scripts to the wallet during automatic keypool topup, SegWit change outputs, ...) [see #11403] * Splitting base58.cpp and tests/base58_tests.cpp up into base58-specific code, and "address encoding"-code [see #11372] * Error locating in UI for BIP173 addresses. Tree-SHA512: 238031185fd07f3ac873c586043970cc2db91bf7735c3c168cb33a3db39a7bda81d4891b649685bb17ef90dc63af0328e7705d8cd3e8dafd6c4d3c08fb230341 Co-authored-by: Wladimir J. van der Laan --- test/functional/test_framework/segwit_addr.py | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 test/functional/test_framework/segwit_addr.py diff --git a/test/functional/test_framework/segwit_addr.py b/test/functional/test_framework/segwit_addr.py new file mode 100644 index 000000000000..02368e938fbb --- /dev/null +++ b/test/functional/test_framework/segwit_addr.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017 Pieter Wuille +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Reference implementation for Bech32 and segwit addresses.""" + + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + +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.""" + return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 + + +def bech32_create_checksum(hrp, data): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data): + """Compute a Bech32 string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + + +def bech32_decode(bech): + """Validate a Bech32 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) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + if not bech32_verify_checksum(hrp, data): + return (None, None) + return (hrp, data[:-6]) + + +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 decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) + if decode(hrp, ret) == (None, None): + return None + return ret From 7c7e7225e221655cd3b1c74ac949bf9215857ab7 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 1 Oct 2020 09:43:00 +0200 Subject: [PATCH 4/6] Merge #19253: Tests: tidy up address.py and segwit_addr.py BACKPORT NOTE: missing changes for segwit_address.py and enabling this unit test 825fcae484f31182041dfacbf820e818d759b130 [tests] Replace bytes literals with hex literals (John Newbery) 64eca45100536579a3849631e59d4277bbc25be1 [tests] Fix pep8 style violations in address.py (John Newbery) b230f8b3f3adcb1e2ae299094f9ae0a8bc7cc3d0 [tests] Correct docstring for address.py (John Newbery) ea70e6a2ca0e183ef40cdb9b3b86f39e94366015 [tests] Tidy up imports in address.py (John Newbery) 7f639df0b8a15aaeccedab00b634925f568c2c9a [tests] Remove unused optional verify_checksum parameter (John Newbery) 011e784f74411bd5d5dbccfd3af39e0937fd8933 [tests] Rename segwit encode and decode functions (John Newbery) e4557133f595f357df5e16ae4f2f19c579631396 [tests] Move bech32 unit tests to test framework (John Newbery) Pull request description: Lots of small fixes: - moving unit tests to test_framework implementation files - renaming functions to be clearer - removing multiple imports - removing unreadable byte literals from the code - fixing pep8 violations - correcting out-of-date docstring ACKs for top commit: jonatack: re-ACK 825fcae484f31182041dfacbf820e818d759b130 per `git range-diff a0a422c 7edcdcd 825fcae` and verified `wallet_address_types.py` and `wallet_basic.py --descriptors` (the failure on one travis job) are green locally. MarcoFalke: ACK 825fcae484f31182041dfacbf820e818d759b130 fanquake: ACK 825fcae484f31182041dfacbf820e818d759b130 - looks ok to me. Tree-SHA512: aea509c27c1bcb94bef11205b6a79836c39c62249672815efc9822f411bc2e2336ceb3d72b3b861c3f4054a08e16edb28c6edd3aa5eff72eec1d60ea6ca82dc4 Co-authored-by: MarcoFalke --- test/functional/test_framework/segwit_addr.py | 22 +++++++++++++++---- test/functional/test_runner.py | 1 + 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/test/functional/test_framework/segwit_addr.py b/test/functional/test_framework/segwit_addr.py index 02368e938fbb..00c0d8a91932 100644 --- a/test/functional/test_framework/segwit_addr.py +++ b/test/functional/test_framework/segwit_addr.py @@ -3,7 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Reference implementation for Bech32 and segwit addresses.""" - +import unittest CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" @@ -84,7 +84,7 @@ def convertbits(data, frombits, tobits, pad=True): return ret -def decode(hrp, addr): +def decode_segwit_address(hrp, addr): """Decode a segwit address.""" hrpgot, data = bech32_decode(addr) if hrpgot != hrp: @@ -99,9 +99,23 @@ def decode(hrp, addr): return (data[0], decoded) -def encode(hrp, witver, witprog): +def encode_segwit_address(hrp, witver, witprog): """Encode a segwit address.""" ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) - if decode(hrp, ret) == (None, None): + if decode_segwit_address(hrp, ret) == (None, None): return None return ret + +class TestFrameworkScript(unittest.TestCase): + def test_segwit_encode_decode(self): + def test_python_bech32(addr): + hrp = addr[:4] + self.assertEqual(hrp, "bcrt") + (witver, witprog) = decode_segwit_address(hrp, addr) + self.assertEqual(encode_segwit_address(hrp, witver, witprog), addr) + + # P2WPKH + test_python_bech32('bcrt1qthmht0k2qnh3wy7336z05lu2km7emzfpm3wg46') + # P2WSH + test_python_bech32('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj') + test_python_bech32('bcrt1qft5p2uhsdcdc3l2ua4ap5qqfg4pjaqlp250x7us7a8qqhrxrxfsqseac85') diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 90c8fbe8e49e..ff7b7998e932 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -83,6 +83,7 @@ "crypto.poly1305", "crypto.ripemd160", "script", + "segwit_addr", ] EXTENDED_SCRIPTS = [ From 89ec1769e0f08b16a2f8a18c1591aa29ad80620d Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 18 Mar 2021 20:21:00 +0100 Subject: [PATCH 5/6] partial Merge #20861: BIP 350: Implement Bech32m and use it for v1+ segwit addresses BACKPORT NOTE: only test/functional/test_framework/segwit_addr.py changes 03346022d611871f2cc185440b19d928b9264d9d naming nits (Fabian Jahr) 2e7c80fb5be82ad4a3f737cab65b31f70a772a23 Add signet support to gen_key_io_test_vectors.py (Pieter Wuille) fe5e495c31de47b0ec732b943db11fe345d874af Use Bech32m encoding for v1+ segwit addresses (Pieter Wuille) 25b1c6e13ddf1626210d5e3d37298d1f3a78a94f Add Bech32m test vectors (Pieter Wuille) da2bb6976dadeec682d163c258c9afecc87d6428 Implement Bech32m encoding/decoding (Pieter Wuille) Pull request description: This implements [BIP 350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki): * For segwit v1+ addresses, a new checksum algorithm called Bech32m is used. * Segwit v0 address keep using Bech32 as specified in [BIP 173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki). ACKs for top commit: Sjors: utACK 0334602 jnewbery: utACK 03346022d6 achow101: ACK 0334602 fjahr: re-ACK 0334602 benthecarman: ACK 03346022d611871f2cc185440b19d928b9264d9d Tree-SHA512: 4424cfd44869d813d6152fb3ed867b204036736bc2344a039b93700b6f36a43e9110478f138eb81c97c77ab27ecb776dada5ba632cb5a3a9d244924d2540a557 Co-authored-by: Wladimir J. van der Laan --- test/functional/test_framework/segwit_addr.py | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/test/functional/test_framework/segwit_addr.py b/test/functional/test_framework/segwit_addr.py index 00c0d8a91932..861ca2b949bd 100644 --- a/test/functional/test_framework/segwit_addr.py +++ b/test/functional/test_framework/segwit_addr.py @@ -2,10 +2,18 @@ # Copyright (c) 2017 Pieter Wuille # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Reference implementation for Bech32 and segwit addresses.""" +"""Reference implementation for Bech32/Bech32m and segwit addresses.""" import unittest +from enum import Enum CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +BECH32_CONST = 1 +BECH32M_CONST = 0x2bc830a3 + +class Encoding(Enum): + """Enumeration type to list the various supported encodings.""" + BECH32 = 1 + BECH32M = 2 def bech32_polymod(values): @@ -27,38 +35,45 @@ def bech32_hrp_expand(hrp): def bech32_verify_checksum(hrp, data): """Verify a checksum given HRP and converted data characters.""" - return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 - + check = bech32_polymod(bech32_hrp_expand(hrp) + data) + if check == BECH32_CONST: + return Encoding.BECH32 + elif check == BECH32M_CONST: + return Encoding.BECH32M + else: + return None -def bech32_create_checksum(hrp, data): +def bech32_create_checksum(encoding, hrp, data): """Compute the checksum values given HRP and data.""" values = bech32_hrp_expand(hrp) + data - polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 + const = BECH32M_CONST if encoding == Encoding.BECH32M else BECH32_CONST + 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): - """Compute a Bech32 string given HRP and data values.""" - combined = data + bech32_create_checksum(hrp, data) +def bech32_encode(encoding, hrp, data): + """Compute a Bech32 or Bech32m string given HRP and data values.""" + combined = data + bech32_create_checksum(encoding, hrp, data) return hrp + '1' + ''.join([CHARSET[d] for d in combined]) def bech32_decode(bech): - """Validate a Bech32 string, and determine HRP and data.""" + """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) + 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) + return (None, None, None) if not all(x in CHARSET for x in bech[pos+1:]): - return (None, None) + return (None, None, None) hrp = bech[:pos] data = [CHARSET.find(x) for x in bech[pos+1:]] - if not bech32_verify_checksum(hrp, data): - return (None, None) - return (hrp, data[:-6]) + encoding = bech32_verify_checksum(hrp, data) + if encoding is None: + return (None, None, None) + return (encoding, hrp, data[:-6]) def convertbits(data, frombits, tobits, pad=True): @@ -86,7 +101,7 @@ def convertbits(data, frombits, tobits, pad=True): def decode_segwit_address(hrp, addr): """Decode a segwit address.""" - hrpgot, data = bech32_decode(addr) + encoding, hrpgot, data = bech32_decode(addr) if hrpgot != hrp: return (None, None) decoded = convertbits(data[1:], 5, 8, False) @@ -96,12 +111,15 @@ def decode_segwit_address(hrp, addr): return (None, None) if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: return (None, None) + if (data[0] == 0 and encoding != Encoding.BECH32) or (data[0] != 0 and encoding != Encoding.BECH32M): + return (None, None) return (data[0], decoded) def encode_segwit_address(hrp, witver, witprog): """Encode a segwit address.""" - ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) + encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M + ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5)) if decode_segwit_address(hrp, ret) == (None, None): return None return ret @@ -119,3 +137,5 @@ def test_python_bech32(addr): # P2WSH test_python_bech32('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj') test_python_bech32('bcrt1qft5p2uhsdcdc3l2ua4ap5qqfg4pjaqlp250x7us7a8qqhrxrxfsqseac85') + # P2TR + test_python_bech32('bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6') From 535cb71a1ddd7df8bf39b840d3256abea22ebcf0 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 21 Apr 2026 20:37:05 +0700 Subject: [PATCH 6/6] test: dashify segwit_addr.py and remove bitcoin's specific code --- test/functional/test_framework/segwit_addr.py | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/test/functional/test_framework/segwit_addr.py b/test/functional/test_framework/segwit_addr.py index 861ca2b949bd..16d5f5719431 100644 --- a/test/functional/test_framework/segwit_addr.py +++ b/test/functional/test_framework/segwit_addr.py @@ -99,43 +99,54 @@ def convertbits(data, frombits, tobits, pad=True): return ret -def decode_segwit_address(hrp, addr): - """Decode a segwit address.""" +DIP18_TYPE_P2PKH = 0xb0 +DIP18_TYPE_P2SH = 0x80 + + +def encode_platform_p2pkh(hrp, keyhash): + """Encode a 20-byte keyhash as a DIP-18 Platform P2PKH bech32m address.""" + assert len(keyhash) == 20 + payload = [DIP18_TYPE_P2PKH] + list(keyhash) + data = convertbits(payload, 8, 5) + return bech32_encode(Encoding.BECH32M, hrp, data) + + +def encode_platform_p2sh(hrp, scripthash): + """Encode a 20-byte scripthash as a DIP-18 Platform P2SH bech32m address.""" + assert len(scripthash) == 20 + payload = [DIP18_TYPE_P2SH] + list(scripthash) + data = convertbits(payload, 8, 5) + return bech32_encode(Encoding.BECH32M, hrp, data) + + +def decode_platform_address(hrp, addr): + """Decode a DIP-18 bech32m platform address. Returns (type_byte, hash_bytes) or (None, None).""" encoding, hrpgot, data = bech32_decode(addr) - if hrpgot != hrp: + if encoding != Encoding.BECH32M or hrpgot != hrp: return (None, None) - decoded = convertbits(data[1:], 5, 8, False) - if decoded is None or len(decoded) < 2 or len(decoded) > 40: + payload = convertbits(data, 5, 8, pad=False) + if payload is None or len(payload) != 21: return (None, None) - if data[0] > 16: + type_byte = payload[0] + if type_byte not in (DIP18_TYPE_P2PKH, DIP18_TYPE_P2SH): return (None, None) - if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: - return (None, None) - if (data[0] == 0 and encoding != Encoding.BECH32) or (data[0] != 0 and encoding != Encoding.BECH32M): - return (None, None) - return (data[0], decoded) - + return (type_byte, bytes(payload[1:])) -def encode_segwit_address(hrp, witver, witprog): - """Encode a segwit address.""" - encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M - ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5)) - if decode_segwit_address(hrp, ret) == (None, None): - return None - return ret class TestFrameworkScript(unittest.TestCase): - def test_segwit_encode_decode(self): - def test_python_bech32(addr): - hrp = addr[:4] - self.assertEqual(hrp, "bcrt") - (witver, witprog) = decode_segwit_address(hrp, addr) - self.assertEqual(encode_segwit_address(hrp, witver, witprog), addr) - - # P2WPKH - test_python_bech32('bcrt1qthmht0k2qnh3wy7336z05lu2km7emzfpm3wg46') - # P2WSH - test_python_bech32('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj') - test_python_bech32('bcrt1qft5p2uhsdcdc3l2ua4ap5qqfg4pjaqlp250x7us7a8qqhrxrxfsqseac85') - # P2TR - test_python_bech32('bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6') + def test_platform_encode_decode(self): + def test_platform_roundtrip(hrp, addr, expected_type): + typ, payload = decode_platform_address(hrp, addr) + self.assertIsNotNone(typ) + self.assertEqual(typ, expected_type) + if expected_type == DIP18_TYPE_P2PKH: + self.assertEqual(encode_platform_p2pkh(hrp, payload), addr) + else: + self.assertEqual(encode_platform_p2sh(hrp, payload), addr) + + # DIP-18 P2PKH + test_platform_roundtrip('dash', 'dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs', DIP18_TYPE_P2PKH) + test_platform_roundtrip('tdash', 'tdash1krma5z3ttj75la4m93xcndna9ullamq9y5fzq2j7', DIP18_TYPE_P2PKH) + # DIP-18 P2SH + test_platform_roundtrip('dash', 'dash1sppl5xpu70aka8nacc4kj2htflydspzkxch4cad6', DIP18_TYPE_P2SH) + test_platform_roundtrip('tdash', 'tdash1sppl5xpu70aka8nacc4kj2htflydspzkxc8jtru5', DIP18_TYPE_P2SH)