diff --git a/src/cashaddrenc.h b/src/cashaddrenc.h --- a/src/cashaddrenc.h +++ b/src/cashaddrenc.h @@ -5,13 +5,23 @@ #define BITCOIN_CASHADDRENC_H #include "script/standard.h" + #include +#include class CChainParams; +std::string EncodeCashAddr(const CTxDestination &, const CChainParams &); + +struct CashAddrContent { + uint8_t type; + std::vector hash; +}; + CTxDestination DecodeCashAddr(const std::string &addr, const CChainParams ¶ms); - -std::string EncodeCashAddr(const CTxDestination &, const CChainParams &); +CashAddrContent DecodeCashAddrContent(const std::string &addr, + const CChainParams ¶ms); +CTxDestination DecodeCashAddrDestination(const CashAddrContent &content); #endif diff --git a/src/cashaddrenc.cpp b/src/cashaddrenc.cpp --- a/src/cashaddrenc.cpp +++ b/src/cashaddrenc.cpp @@ -7,12 +7,13 @@ #include "pubkey.h" #include "script/script.h" #include "utilstrencodings.h" -#include + #include -#include -const uint8_t CASHADDR_VERSION_PUBKEY = 0; -const uint8_t CASHADDR_VERISON_SCRIPT = 8; +#include + +const uint8_t PUBKEY_TYPE = 0; +const uint8_t SCRIPT_TYPE = 1; // Size of data-part in a pubkey/script cash address. // Consists of: 8 bits version + 160 bits hash. @@ -21,57 +22,46 @@ namespace { -// Implements encoding of CTxDestination using cashaddr. -class CashAddrEncoder : public boost::static_visitor { -public: - CashAddrEncoder(const CChainParams &p); - - std::string operator()(const CKeyID &id) const; - std::string operator()(const CScriptID &id) const; - std::string operator()(const CNoDestination &) const; - -private: - const CChainParams ¶ms; -}; - // Convert the data part to a 5 bit representation. template -std::vector PackAddrData(const T &id, uint8_t version, +std::vector PackAddrData(const T &id, uint8_t type, size_t expectedSize) { - std::vector data = {version}; + std::vector data = {uint8_t(type << 3)}; data.insert(data.end(), id.begin(), id.end()); - const std::string errstr = "Error packing cashaddr"; - std::vector converted; - if (!ConvertBits<8, 5, true>(converted, begin(data), end(data))) { - throw std::runtime_error(errstr); - } + converted.reserve(expectedSize); + ConvertBits<8, 5, true>(converted, begin(data), end(data)); if (converted.size() != expectedSize) { - throw std::runtime_error(errstr); + throw std::runtime_error("Error packing cashaddr"); } return converted; } -CashAddrEncoder::CashAddrEncoder(const CChainParams &p) : params(p) {} +// Implements encoding of CTxDestination using cashaddr. +class CashAddrEncoder : public boost::static_visitor { +public: + CashAddrEncoder(const CChainParams &p) : params(p) {} + + std::string operator()(const CKeyID &id) const { + std::vector data = + PackAddrData(id, PUBKEY_TYPE, CASHADDR_GROUPED_SIZE); + return cashaddr::Encode(params.CashAddrPrefix(), data); + } -std::string CashAddrEncoder::operator()(const CKeyID &id) const { - std::vector data = - PackAddrData(id, CASHADDR_VERSION_PUBKEY, CASHADDR_GROUPED_SIZE); - return cashaddr::Encode(params.CashAddrPrefix(), data); -} + std::string operator()(const CScriptID &id) const { + std::vector data = + PackAddrData(id, SCRIPT_TYPE, CASHADDR_GROUPED_SIZE); + return cashaddr::Encode(params.CashAddrPrefix(), data); + } -std::string CashAddrEncoder::operator()(const CScriptID &id) const { - std::vector data = - PackAddrData(id, CASHADDR_VERISON_SCRIPT, CASHADDR_GROUPED_SIZE); - return cashaddr::Encode(params.CashAddrPrefix(), data); -} + std::string operator()(const CNoDestination &) const { return ""; } -std::string CashAddrEncoder::operator()(const CNoDestination &) const { - return ""; -} +private: + const CChainParams ¶ms; +}; } // anon ns @@ -80,49 +70,87 @@ return boost::apply_visitor(CashAddrEncoder(params), dst); } -CTxDestination DecodeCashAddr(const std::string &addrstr, +CTxDestination DecodeCashAddr(const std::string &addr, const CChainParams ¶ms) { + CashAddrContent content = DecodeCashAddrContent(addr, params); + if (content.hash.size() == 0) { + return CNoDestination{}; + } + + return DecodeCashAddrDestination(content); +} + +CashAddrContent DecodeCashAddrContent(const std::string &addr, + const CChainParams ¶ms) { std::pair> cashaddr = - cashaddr::Decode(addrstr); + cashaddr::Decode(addr); if (cashaddr.first != params.CashAddrPrefix()) { - return CNoDestination{}; + return {}; } if (cashaddr.second.empty()) { - return CNoDestination{}; + return {}; + } + + // Check that the padding is zero. + size_t extrabits = cashaddr.second.size() * 5 % 8; + if (extrabits >= 5) { + // We have more padding than allowed. + return {}; + } + + uint8_t last = cashaddr.second.back(); + uint8_t mask = (1 << extrabits) - 1; + if (last & mask) { + // We have non zero bits as padding. + return {}; } std::vector data; - if (!ConvertBits<5, 8, true>(data, begin(cashaddr.second), - end(cashaddr.second))) { - return CNoDestination{}; + data.reserve(CASHADDR_BYTES); + ConvertBits<5, 8, false>(data, begin(cashaddr.second), + end(cashaddr.second)); + + // Decode type and size from the version. + uint8_t version = data[0]; + if (version & 0x80) { + // First bit is reserved. + return {}; } - // Both encoding and decoding add padding, so it's double padded. - // Truncate the double padding. - if (data.back() != 0) { - // Not padded, should be. - return CNoDestination{}; + uint8_t type = (version >> 3) & 0x1f; + uint32_t hash_size = 20 + 4 * (version & 0x03); + if (version & 0x04) { + hash_size *= 2; } - data.pop_back(); // Check that we decoded the exact number of bytes we expected. - if (data.size() != CASHADDR_BYTES) { - return CNoDestination{}; + if (data.size() != hash_size + 1) { + return {}; } - uint160 hash; - std::copy(begin(data) + 1, end(data), hash.begin()); + // Pop the version. + data.erase(data.begin()); - uint8_t version = data.at(0); - if (version == CASHADDR_VERSION_PUBKEY) { - return CKeyID(hash); - } - if (version == CASHADDR_VERISON_SCRIPT) { - return CScriptID(hash); + return {type, std::move(data)}; +} + +CTxDestination DecodeCashAddrDestination(const CashAddrContent &content) { + if (content.hash.size() != 20) { + // Only 20 bytes hash are supported now. + return CNoDestination{}; } - // unknown version - return CNoDestination{}; + uint160 hash; + std::copy(begin(content.hash), end(content.hash), hash.begin()); + + switch (content.type) { + case PUBKEY_TYPE: + return CKeyID(hash); + case SCRIPT_TYPE: + return CScriptID(hash); + default: + return CNoDestination{}; + } } diff --git a/src/test/cashaddrenc_tests.cpp b/src/test/cashaddrenc_tests.cpp --- a/src/test/cashaddrenc_tests.cpp +++ b/src/test/cashaddrenc_tests.cpp @@ -8,6 +8,7 @@ #include "random.h" #include "test/test_bitcoin.h" #include "uint256.h" + #include namespace { @@ -113,28 +114,106 @@ } } -// Test that a invalid, specially crafted cashaddr stays invalid when truncated. -BOOST_AUTO_TEST_CASE(invalid_when_truncated) { - - // Cashaddr payload is 34 5-bit nibbles. The last one is padded. When - // converting back to bytes, there is additional padding. - // - // This extra padding truncated. But we should make sure that what we - // truncate is padding and not data. - // - // This test creates a invalid address that leaves data in the byte that - // should be padding. +/** + * Cashaddr payload made of 5-bit nibbles. The last one is padded. When + * converting back to bytes, this extra padding is truncated. In order to ensure + * cashaddr are cannonicals, we check that the data we truncate is zeroed. + */ +BOOST_AUTO_TEST_CASE(check_padding) { uint8_t version = 0; std::vector data = {version}; - for (size_t i = 0; i < 33; ++i) + for (size_t i = 0; i < 33; ++i) { data.push_back(1); - assert(data.size() == 34); + } + + BOOST_CHECK_EQUAL(data.size(), 34); + + const CTxDestination nodst = CNoDestination{}; + const CChainParams params = Params(CBaseChainParams::MAIN); + + for (uint8_t i = 0; i < 32; i++) { + data[data.size() - 1] = i; + std::string fake = cashaddr::Encode(params.CashAddrPrefix(), data); + CTxDestination dst = DecodeCashAddr(fake, params); + + // We have 168 bits of payload encoded as 170 bits in 5 bits nimbles. As + // a result, we must have 2 zeros. + if (i & 0x03) { + BOOST_CHECK(dst == nodst); + } else { + BOOST_CHECK(dst != nodst); + } + } +} + +/** + * We ensure type is extracted properly from the version. + */ +BOOST_AUTO_TEST_CASE(check_type) { + std::vector data; + data.resize(34); + + const CChainParams params = Params(CBaseChainParams::MAIN); + for (uint8_t v = 0; v < 16; v++) { + std::fill(begin(data), end(data), 0); + data[0] = v; + auto content = DecodeCashAddrContent( + cashaddr::Encode(params.CashAddrPrefix(), data), params); + BOOST_CHECK_EQUAL(content.type, v); + BOOST_CHECK_EQUAL(content.hash.size(), 20); + + // Check that using the reserved bit result in a failure. + data[0] |= 0x10; + content = DecodeCashAddrContent( + cashaddr::Encode(params.CashAddrPrefix(), data), params); + BOOST_CHECK_EQUAL(content.type, 0); + BOOST_CHECK_EQUAL(content.hash.size(), 0); + } +} + +/** + * We ensure size is extracted and checked properly. + */ +BOOST_AUTO_TEST_CASE(check_size) { + const CTxDestination nodst = CNoDestination{}; const CChainParams params = Params(CBaseChainParams::MAIN); - std::string fake = cashaddr::Encode(params.CashAddrPrefix(), data); - CTxDestination nodst = CNoDestination{}; - BOOST_CHECK(nodst == DecodeCashAddr(fake, params)); + // Mapp all possible size bits in the version to the expected size of the + // hash in bytes. + std::vector> sizes = { + {0, 20}, {1, 24}, {2, 28}, {3, 32}, {4, 40}, {5, 48}, {6, 56}, {7, 64}, + }; + + std::vector data; + + for (auto ps : sizes) { + size_t expectedSize = (12 + ps.second * 8) / 5; + data.resize(expectedSize); + std::fill(begin(data), end(data), 0); + data[1] = ps.first << 2; + + auto content = DecodeCashAddrContent( + cashaddr::Encode(params.CashAddrPrefix(), data), params); + + BOOST_CHECK_EQUAL(content.type, 0); + BOOST_CHECK_EQUAL(content.hash.size(), ps.second); + + data.push_back(0); + content = DecodeCashAddrContent( + cashaddr::Encode(params.CashAddrPrefix(), data), params); + + BOOST_CHECK_EQUAL(content.type, 0); + BOOST_CHECK_EQUAL(content.hash.size(), 0); + + data.pop_back(); + data.pop_back(); + content = DecodeCashAddrContent( + cashaddr::Encode(params.CashAddrPrefix(), data), params); + + BOOST_CHECK_EQUAL(content.type, 0); + BOOST_CHECK_EQUAL(content.hash.size(), 0); + } } BOOST_AUTO_TEST_CASE(test_addresses) {