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 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() diff --git a/test/functional/test_framework/segwit_addr.py b/test/functional/test_framework/segwit_addr.py new file mode 100644 index 000000000000..16d5f5719431 --- /dev/null +++ b/test/functional/test_framework/segwit_addr.py @@ -0,0 +1,152 @@ +#!/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/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): + """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.""" + 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(encoding, hrp, data): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + 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(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/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:]] + 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): + """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 + + +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 encoding != Encoding.BECH32M or hrpgot != hrp: + return (None, None) + payload = convertbits(data, 5, 8, pad=False) + if payload is None or len(payload) != 21: + return (None, None) + type_byte = payload[0] + if type_byte not in (DIP18_TYPE_P2PKH, DIP18_TYPE_P2SH): + return (None, None) + return (type_byte, bytes(payload[1:])) + + +class TestFrameworkScript(unittest.TestCase): + 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) 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 = [