Skip to content

Commit 843475a

Browse files
Merge #7285: feat: basic support for platform bech32m addresses for dash core and functional tests
535cb71 test: dashify segwit_addr.py and remove bitcoin's specific code (Konstantin Akimov) 89ec176 partial Merge bitcoin#20861: BIP 350: Implement Bech32m and use it for v1+ segwit addresses (Konstantin Akimov) 7c7e722 Merge bitcoin#19253: Tests: tidy up address.py and segwit_addr.py (Konstantin Akimov) 393ca5e partial Merge bitcoin#11167: Full BIP173 (Bech32) support (Konstantin Akimov) c3261a9 test: regression tests for platform addresses (Konstantin Akimov) 51ce91f feat: add utils to parse platform bech32m destinations (Konstantin Akimov) Pull request description: ## What was done? It's a step forward to implement changes in dip-0008 with platform addresses and asset-lock transactions v2, see dashpay/dips#182 This PR adds code to parse and encode platform bech32m addresses in C++ code and in functional tests. Some changes are done as backports: - partial bitcoin#11167 - merged previously, extra fixes from bitcoin#19253 - partial bitcoin#20861 ## How Has This Been Tested? See changes in regression tests, in functional tests. ``` $ test/functional/test_runner.py rpc_help.py Temporary test directory at /tmp/test_runner_∋_🏃_20260422_014702 Running Unit Tests for Test Framework Modules .................... ---------------------------------------------------------------------- Ran 20 tests in 3.427s ... ``` Amount of unit tests is increased from 19 to 20 as expected. ## Breaking Changes N/A This PR doesn't include any breaking changes in RPCs or consensus related changes. It's an utility PR that could be merged with low risk. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ ACKs for top commit: UdjinM6: utACK 535cb71 Tree-SHA512: 38e4a924bb0ea3ac7ff99cefead131a8d1fd806585da86e356e1836c6792c73a1481afcb572e89eb1e4eff9448b234ea4a182f5127f3f4ce9b9b2c1a05a5cf5c
2 parents 9e31f8d + 535cb71 commit 843475a

7 files changed

Lines changed: 395 additions & 0 deletions

File tree

src/chainparams.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ class CMainParams : public CChainParams {
260260
// Dash BIP32 prvkeys start with 'xprv' (Bitcoin defaults)
261261
base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x88, 0xAD, 0xE4};
262262

263+
// DIP-18 Dash Platform address HRP (bech32m)
264+
bech32_platform_hrp = "dash";
265+
263266
// Dash BIP44 coin type is '5'
264267
nExtCoinType = 5;
265268

@@ -452,6 +455,9 @@ class CTestNetParams : public CChainParams {
452455
// Testnet Dash BIP32 prvkeys start with 'tprv' (Bitcoin defaults)
453456
base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94};
454457

458+
// DIP-18 Dash Platform address HRP (bech32m)
459+
bech32_platform_hrp = "tdash";
460+
455461
// Testnet Dash BIP44 coin type is '1' (All coin's testnet default)
456462
nExtCoinType = 1;
457463

@@ -625,6 +631,9 @@ class CDevNetParams : public CChainParams {
625631
// Testnet Dash BIP32 prvkeys start with 'tprv' (Bitcoin defaults)
626632
base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94};
627633

634+
// DIP-18 Dash Platform address HRP (bech32m)
635+
bech32_platform_hrp = "tdash";
636+
628637
// Testnet Dash BIP44 coin type is '1' (All coin's testnet default)
629638
nExtCoinType = 1;
630639

@@ -900,6 +909,9 @@ class CRegTestParams : public CChainParams {
900909
// Regtest Dash BIP32 prvkeys start with 'tprv' (Bitcoin defaults)
901910
base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94};
902911

912+
// DIP-18 Dash Platform address HRP (bech32m)
913+
bech32_platform_hrp = "tdash";
914+
903915
// Regtest Dash BIP44 coin type is '1' (All coin's testnet default)
904916
nExtCoinType = 1;
905917

src/chainparams.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ class CChainParams
124124
/** Return the list of hostnames to look up for DNS seeds */
125125
const std::vector<std::string>& DNSSeeds() const { return vSeeds; }
126126
const std::vector<unsigned char>& Base58Prefix(Base58Type type) const { return base58Prefixes[type]; }
127+
/** DIP-18 Platform address bech32m HRP: "dash" on mainnet, "tdash" on test chains */
128+
const std::string& Bech32PlatformHRP() const { return bech32_platform_hrp; }
127129
int ExtCoinType() const { return nExtCoinType; }
128130
const std::vector<uint8_t>& FixedSeeds() const { return vFixedSeeds; }
129131
const CCheckpointData& Checkpoints() const { return checkpointData; }
@@ -164,6 +166,7 @@ class CChainParams
164166
uint64_t m_assumed_chain_state_size;
165167
std::vector<std::string> vSeeds;
166168
std::vector<unsigned char> base58Prefixes[MAX_BASE58_TYPES];
169+
std::string bech32_platform_hrp;
167170
int nExtCoinType;
168171
std::string strNetworkID;
169172
CBlock genesis;

src/key_io.cpp

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <base58.h>
88
#include <bech32.h>
99
#include <chainparams.h>
10+
#include <util/strencodings.h>
1011

1112
#include <algorithm>
1213
#include <assert.h>
@@ -176,3 +177,99 @@ bool IsValidDestinationString(const std::string& str)
176177
{
177178
return IsValidDestinationString(str, Params());
178179
}
180+
181+
namespace {
182+
constexpr uint8_t DIP18_TYPE_BYTE_P2PKH = 0xb0;
183+
constexpr uint8_t DIP18_TYPE_BYTE_P2SH = 0x80;
184+
constexpr size_t DIP18_PAYLOAD_SIZE = 21; // 1 type byte + 20-byte HASH160
185+
186+
std::string EncodePlatformBech32m(const CChainParams& params, uint8_t type_byte, const BaseHash<uint160>& hash)
187+
{
188+
std::vector<uint8_t> payload;
189+
payload.reserve(DIP18_PAYLOAD_SIZE);
190+
payload.push_back(type_byte);
191+
payload.insert(payload.end(), hash.begin(), hash.end());
192+
std::vector<uint8_t> values;
193+
values.reserve(((DIP18_PAYLOAD_SIZE * 8) + 4) / 5);
194+
ConvertBits<8, 5, true>([&](uint8_t v) { values.push_back(v); }, payload.begin(), payload.end());
195+
return bech32::Encode(bech32::Encoding::BECH32M, params.Bech32PlatformHRP(), values);
196+
}
197+
198+
class PlatformDestinationEncoder
199+
{
200+
private:
201+
const CChainParams& m_params;
202+
203+
public:
204+
explicit PlatformDestinationEncoder(const CChainParams& params) : m_params(params) {}
205+
206+
std::string operator()(const PlatformP2PKHDestination& id) const
207+
{
208+
return EncodePlatformBech32m(m_params, DIP18_TYPE_BYTE_P2PKH, id);
209+
}
210+
std::string operator()(const PlatformP2SHDestination& id) const
211+
{
212+
return EncodePlatformBech32m(m_params, DIP18_TYPE_BYTE_P2SH, id);
213+
}
214+
std::string operator()(const CNoDestination&) const { return {}; }
215+
};
216+
} // namespace
217+
218+
bool IsValidPlatformDestination(const PlatformDestination& dest)
219+
{
220+
return !std::holds_alternative<CNoDestination>(dest);
221+
}
222+
223+
std::string EncodePlatformDestination(const PlatformDestination& dest)
224+
{
225+
return std::visit(PlatformDestinationEncoder(Params()), dest);
226+
}
227+
228+
PlatformDestination DecodePlatformDestination(const std::string& str, const CChainParams& params, std::string& error_str)
229+
{
230+
error_str.clear();
231+
const bech32::DecodeResult dec = bech32::Decode(str);
232+
if (dec.encoding == bech32::Encoding::INVALID) {
233+
error_str = "Invalid bech32m encoding";
234+
return CNoDestination();
235+
}
236+
if (dec.encoding != bech32::Encoding::BECH32M) {
237+
error_str = "DIP-18 Platform addresses require bech32m checksum";
238+
return CNoDestination();
239+
}
240+
if (dec.hrp != params.Bech32PlatformHRP()) {
241+
error_str = "Invalid Platform HRP for the selected network";
242+
return CNoDestination();
243+
}
244+
std::vector<uint8_t> payload;
245+
payload.reserve((dec.data.size() * 5) / 8);
246+
if (!ConvertBits<5, 8, false>([&](uint8_t b) { payload.push_back(b); }, dec.data.begin(), dec.data.end())) {
247+
error_str = "Invalid Platform address payload encoding";
248+
return CNoDestination();
249+
}
250+
if (payload.size() != DIP18_PAYLOAD_SIZE) {
251+
error_str = "Invalid Platform address payload length";
252+
return CNoDestination();
253+
}
254+
uint160 hash;
255+
std::copy(payload.begin() + 1, payload.end(), hash.begin());
256+
switch (payload[0]) {
257+
case DIP18_TYPE_BYTE_P2PKH:
258+
return PlatformP2PKHDestination(hash);
259+
case DIP18_TYPE_BYTE_P2SH:
260+
return PlatformP2SHDestination(hash);
261+
}
262+
error_str = "Unknown DIP-18 type byte";
263+
return CNoDestination();
264+
}
265+
266+
PlatformDestination DecodePlatformDestination(const std::string& str, std::string& error_str)
267+
{
268+
return DecodePlatformDestination(str, Params(), error_str);
269+
}
270+
271+
PlatformDestination DecodePlatformDestination(const std::string& str)
272+
{
273+
std::string error_str;
274+
return DecodePlatformDestination(str, error_str);
275+
}

src/key_io.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,35 @@ CTxDestination DecodeDestination(const std::string& str, std::string& error_msg)
2828
bool IsValidDestinationString(const std::string& str);
2929
bool IsValidDestinationString(const std::string& str, const CChainParams& params);
3030

31+
/**
32+
* DIP-18 Dash Platform addresses (bech32m).
33+
*
34+
* Platform addresses decode to a 20-byte HASH160 prefixed by a type byte:
35+
* 0xb0 -> Platform P2PKH (addresses of the form dash1k... / tdash1k...)
36+
* 0x80 -> Platform P2SH (addresses of the form dash1s... / tdash1s...)
37+
*
38+
* Unlike base58 Dash addresses, Platform destinations have no on-chain
39+
* scriptPubKey: they are only valid as credit output recipients of an
40+
* asset-lock special transaction (see DIP-27 and src/evo/assetlocktx.h).
41+
*/
42+
struct PlatformP2PKHDestination : public BaseHash<uint160>
43+
{
44+
PlatformP2PKHDestination() = default;
45+
explicit PlatformP2PKHDestination(const uint160& hash) : BaseHash(hash) {}
46+
};
47+
48+
struct PlatformP2SHDestination : public BaseHash<uint160>
49+
{
50+
PlatformP2SHDestination() = default;
51+
explicit PlatformP2SHDestination(const uint160& hash) : BaseHash(hash) {}
52+
};
53+
54+
using PlatformDestination = std::variant<CNoDestination, PlatformP2PKHDestination, PlatformP2SHDestination>;
55+
56+
bool IsValidPlatformDestination(const PlatformDestination& dest);
57+
std::string EncodePlatformDestination(const PlatformDestination& dest);
58+
PlatformDestination DecodePlatformDestination(const std::string& str);
59+
PlatformDestination DecodePlatformDestination(const std::string& str, std::string& error_str);
60+
PlatformDestination DecodePlatformDestination(const std::string& str, const CChainParams& params, std::string& error_str);
61+
3162
#endif // BITCOIN_KEY_IO_H

src/test/key_io_tests.cpp

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <test/data/key_io_invalid.json.h>
66
#include <test/data/key_io_valid.json.h>
77

8+
#include <bech32.h>
89
#include <chainparams.h>
910
#include <key.h>
1011
#include <key_io.h>
@@ -146,4 +147,102 @@ BOOST_AUTO_TEST_CASE(key_io_invalid)
146147
}
147148
}
148149

150+
// DIP-18: Dash Platform bech32m address encoding.
151+
BOOST_AUTO_TEST_CASE(dip18_platform_roundtrip)
152+
{
153+
struct Sample {
154+
std::string hash_hex;
155+
std::string address;
156+
std::string chain;
157+
bool is_p2sh;
158+
};
159+
// Samples from DIP-0018 (Test Vectors section).
160+
const Sample samples[] = {
161+
{"f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", "dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs", CBaseChainParams::MAIN, false},
162+
{"a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", "dash1kzjl7qzxy9lar37j8r37z3kvt07epqe20ckxfezw", CBaseChainParams::MAIN, false},
163+
{"6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", "dash1kpkeye606ez89g7lelp7hnldwwpt76va0v3j6x28", CBaseChainParams::MAIN, false},
164+
{"f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525", "tdash1krma5z3ttj75la4m93xcndna9ullamq9y5fzq2j7", CBaseChainParams::TESTNET, false},
165+
{"a5ff0046217fd1c7d238e3e146cc5bfd90832a7e", "tdash1kzjl7qzxy9lar37j8r37z3kvt07epqe20cxp68nq", CBaseChainParams::TESTNET, false},
166+
{"6d92674fd64472a3dfcfc3ebcfed7382bf699d7b", "tdash1kpkeye606ez89g7lelp7hnldwwpt76va0vp4fcmf", CBaseChainParams::TESTNET, false},
167+
{"43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636", "dash1sppl5xpu70aka8nacc4kj2htflydspzkxch4cad6", CBaseChainParams::MAIN, true},
168+
{"43fa183cf3fb6e9e7dc62b692aeb4fc8d8045636", "tdash1sppl5xpu70aka8nacc4kj2htflydspzkxc8jtru5", CBaseChainParams::TESTNET, true},
169+
};
170+
for (const auto& s : samples) {
171+
SelectParams(s.chain);
172+
std::string err;
173+
PlatformDestination dest = DecodePlatformDestination(s.address, err);
174+
BOOST_REQUIRE_MESSAGE(IsValidPlatformDestination(dest),
175+
std::string{"decode failed: "} + s.address + " err=" + err);
176+
std::vector<unsigned char> got_hash;
177+
if (s.is_p2sh) {
178+
BOOST_REQUIRE(std::holds_alternative<PlatformP2SHDestination>(dest));
179+
const auto& h = std::get<PlatformP2SHDestination>(dest);
180+
got_hash.assign(h.begin(), h.end());
181+
} else {
182+
BOOST_REQUIRE(std::holds_alternative<PlatformP2PKHDestination>(dest));
183+
const auto& h = std::get<PlatformP2PKHDestination>(dest);
184+
got_hash.assign(h.begin(), h.end());
185+
}
186+
BOOST_CHECK_EQUAL(HexStr(got_hash), std::string(s.hash_hex));
187+
BOOST_CHECK_EQUAL(EncodePlatformDestination(dest), std::string(s.address));
188+
}
189+
SelectParams(CBaseChainParams::MAIN);
190+
}
191+
192+
BOOST_AUTO_TEST_CASE(dip18_platform_invalid)
193+
{
194+
SelectParams(CBaseChainParams::MAIN);
195+
std::string err;
196+
197+
// Wrong HRP for the selected network (testnet string on mainnet).
198+
BOOST_CHECK(!IsValidPlatformDestination(
199+
DecodePlatformDestination("tdash1krma5z3ttj75la4m93xcndna9ullamq9y5fzq2j7", err)));
200+
201+
// Mixed case is forbidden by BIP-173.
202+
BOOST_CHECK(!IsValidPlatformDestination(
203+
DecodePlatformDestination("Dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs", err)));
204+
205+
// Bech32 (BIP-173) checksum MUST be rejected; only bech32m is valid for DIP-18.
206+
// Re-encode the same 21-byte payload with the BIP-173 generator and verify rejection.
207+
{
208+
std::vector<uint8_t> payload = ParseHex("b0f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525");
209+
std::vector<uint8_t> values;
210+
ConvertBits<8, 5, true>([&](uint8_t b) { values.push_back(b); }, payload.begin(), payload.end());
211+
const std::string bech32_str = bech32::Encode(bech32::Encoding::BECH32, "dash", values);
212+
BOOST_REQUIRE(!bech32_str.empty());
213+
BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination(bech32_str, err)));
214+
}
215+
216+
// Unknown DIP-18 type byte (0x00) must be rejected.
217+
{
218+
std::vector<uint8_t> payload = ParseHex("00f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525");
219+
std::vector<uint8_t> values;
220+
ConvertBits<8, 5, true>([&](uint8_t b) { values.push_back(b); }, payload.begin(), payload.end());
221+
const std::string bad = bech32::Encode(bech32::Encoding::BECH32M, "dash", values);
222+
BOOST_REQUIRE(!bad.empty());
223+
BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination(bad, err)));
224+
}
225+
226+
// Wrong payload length (19-byte hash) must be rejected.
227+
{
228+
std::vector<uint8_t> payload = ParseHex("b0f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec05");
229+
std::vector<uint8_t> values;
230+
ConvertBits<8, 5, true>([&](uint8_t b) { values.push_back(b); }, payload.begin(), payload.end());
231+
const std::string bad = bech32::Encode(bech32::Encoding::BECH32M, "dash", values);
232+
BOOST_REQUIRE(!bad.empty());
233+
BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination(bad, err)));
234+
}
235+
236+
// Empty / garbage inputs.
237+
BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination("", err)));
238+
BOOST_CHECK(!IsValidPlatformDestination(DecodePlatformDestination("not-an-address", err)));
239+
240+
// Mainnet address on testnet must fail.
241+
SelectParams(CBaseChainParams::TESTNET);
242+
BOOST_CHECK(!IsValidPlatformDestination(
243+
DecodePlatformDestination("dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs", err)));
244+
245+
SelectParams(CBaseChainParams::MAIN);
246+
}
247+
149248
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)