Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions src/chainparams.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ class CChainParams
/** Return the list of hostnames to look up for DNS seeds */
const std::vector<std::string>& DNSSeeds() const { return vSeeds; }
const std::vector<unsigned char>& 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<uint8_t>& FixedSeeds() const { return vFixedSeeds; }
const CCheckpointData& Checkpoints() const { return checkpointData; }
Expand Down Expand Up @@ -164,6 +166,7 @@ class CChainParams
uint64_t m_assumed_chain_state_size;
std::vector<std::string> vSeeds;
std::vector<unsigned char> base58Prefixes[MAX_BASE58_TYPES];
std::string bech32_platform_hrp;
int nExtCoinType;
std::string strNetworkID;
CBlock genesis;
Expand Down
97 changes: 97 additions & 0 deletions src/key_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <base58.h>
#include <bech32.h>
#include <chainparams.h>
#include <util/strencodings.h>

#include <algorithm>
#include <assert.h>
Expand Down Expand Up @@ -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<uint160>& hash)
{
std::vector<uint8_t> payload;
payload.reserve(DIP18_PAYLOAD_SIZE);
payload.push_back(type_byte);
payload.insert(payload.end(), hash.begin(), hash.end());
std::vector<uint8_t> 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<CNoDestination>(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<uint8_t> 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);
}
31 changes: 31 additions & 0 deletions src/key_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint160>
{
PlatformP2PKHDestination() = default;
explicit PlatformP2PKHDestination(const uint160& hash) : BaseHash(hash) {}
};

struct PlatformP2SHDestination : public BaseHash<uint160>
{
PlatformP2SHDestination() = default;
explicit PlatformP2SHDestination(const uint160& hash) : BaseHash(hash) {}
};

using PlatformDestination = std::variant<CNoDestination, PlatformP2PKHDestination, PlatformP2SHDestination>;

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);
Comment on lines +56 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: EncodePlatformDestination lacks a CChainParams-injecting overload

DecodePlatformDestination is exposed with both a (str, params, error_str) and a global-Params form, but EncodePlatformDestination only exists in the form that reads the global Params() (see src/key_io.cpp:223-226). That asymmetry makes it harder to encode Platform addresses from contexts that already thread a non-global CChainParams (future multi-chain unit tests, RPC helpers, wallet code). Adding a EncodePlatformDestination(const PlatformDestination&, const CChainParams&) overload and forwarding the existing free function to it mirrors the base58 Encode/Decode factoring and lets the encoder be tested without touching global state.

source: ['claude']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/key_io.h`:
- [SUGGESTION] lines 56-60: EncodePlatformDestination lacks a CChainParams-injecting overload
  DecodePlatformDestination is exposed with both a `(str, params, error_str)` and a global-Params form, but EncodePlatformDestination only exists in the form that reads the global `Params()` (see src/key_io.cpp:223-226). That asymmetry makes it harder to encode Platform addresses from contexts that already thread a non-global CChainParams (future multi-chain unit tests, RPC helpers, wallet code). Adding a `EncodePlatformDestination(const PlatformDestination&, const CChainParams&)` overload and forwarding the existing free function to it mirrors the base58 Encode/Decode factoring and lets the encoder be tested without touching global state.


#endif // BITCOIN_KEY_IO_H
99 changes: 99 additions & 0 deletions src/test/key_io_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <test/data/key_io_invalid.json.h>
#include <test/data/key_io_valid.json.h>

#include <bech32.h>
#include <chainparams.h>
#include <key.h>
#include <key_io.h>
Expand Down Expand Up @@ -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<unsigned char> got_hash;
if (s.is_p2sh) {
BOOST_REQUIRE(std::holds_alternative<PlatformP2SHDestination>(dest));
const auto& h = std::get<PlatformP2SHDestination>(dest);
got_hash.assign(h.begin(), h.end());
} else {
BOOST_REQUIRE(std::holds_alternative<PlatformP2PKHDestination>(dest));
const auto& h = std::get<PlatformP2PKHDestination>(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<uint8_t> payload = ParseHex("b0f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525");
std::vector<uint8_t> 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<uint8_t> payload = ParseHex("00f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec0525");
std::vector<uint8_t> 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<uint8_t> payload = ParseHex("b0f7da0a2b5cbd4ff6bb2c4d89b67d2f3ffeec05");
std::vector<uint8_t> 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()
Loading
Loading