diff --git a/src/cashaddrenc.cpp b/src/cashaddrenc.cpp index 2e09358c94..7a8898cdff 100644 --- a/src/cashaddrenc.cpp +++ b/src/cashaddrenc.cpp @@ -1,156 +1,183 @@ // Copyright (c) 2017 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "cashaddrenc.h" #include "cashaddr.h" #include "chainparams.h" #include "pubkey.h" #include "script/script.h" #include "utilstrencodings.h" #include #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. -const size_t CASHADDR_GROUPED_SIZE = 34; /* 5 bit representation */ -const size_t CASHADDR_BYTES = 21; /* 8 bit representation */ - namespace { // Convert the data part to a 5 bit representation. template -std::vector PackAddrData(const T &id, uint8_t type, - size_t expectedSize) { - std::vector data = {uint8_t(type << 3)}; - data.insert(data.end(), id.begin(), id.end()); +std::vector PackAddrData(const T &id, uint8_t type) { + uint8_t version_byte(type << 3); + size_t size = id.size(); + uint8_t encoded_size = 0; + switch (size * 8) { + case 160: + encoded_size = 0; + break; + case 192: + encoded_size = 1; + break; + case 224: + encoded_size = 2; + break; + case 256: + encoded_size = 3; + break; + case 320: + encoded_size = 4; + break; + case 384: + encoded_size = 5; + break; + case 448: + encoded_size = 6; + break; + case 512: + encoded_size = 7; + break; + default: + throw std::runtime_error( + "Error packing cashaddr: invalid address length"); + } + version_byte |= encoded_size; + std::vector data = {version_byte}; + data.insert(data.end(), std::begin(id), std::end(id)); std::vector converted; - converted.reserve(expectedSize); - ConvertBits<8, 5, true>(converted, begin(data), end(data)); - - if (converted.size() != expectedSize) { - throw std::runtime_error("Error packing cashaddr"); - } + // Reserve the number of bytes required for a 5-bit packed version of a + // hash, with version byte. Add half a byte(4) so integer math provides + // the next multiple-of-5 that would fit all the data. + converted.reserve(((size + 1) * 8 + 4) / 5); + ConvertBits<8, 5, true>(converted, std::begin(data), std::end(data)); return converted; } // 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); + std::vector data = PackAddrData(id, PUBKEY_TYPE); return cashaddr::Encode(params.CashAddrPrefix(), data); } std::string operator()(const CScriptID &id) const { - std::vector data = - PackAddrData(id, SCRIPT_TYPE, CASHADDR_GROUPED_SIZE); + std::vector data = PackAddrData(id, SCRIPT_TYPE); return cashaddr::Encode(params.CashAddrPrefix(), data); } std::string operator()(const CNoDestination &) const { return ""; } private: const CChainParams ¶ms; }; } // anon ns std::string EncodeCashAddr(const CTxDestination &dst, const CChainParams ¶ms) { return boost::apply_visitor(CashAddrEncoder(params), dst); } 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(addr); if (cashaddr.first != params.CashAddrPrefix()) { return {}; } if (cashaddr.second.empty()) { 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; - data.reserve(CASHADDR_BYTES); + data.reserve(cashaddr.second.size() * 5 / 8); 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 {}; } - uint8_t type = (version >> 3) & 0x1f; + auto type = CashAddrType((version >> 3) & 0x1f); uint32_t hash_size = 20 + 4 * (version & 0x03); if (version & 0x04) { hash_size *= 2; } // Check that we decoded the exact number of bytes we expected. if (data.size() != hash_size + 1) { return {}; } // Pop the version. data.erase(data.begin()); return {type, std::move(data)}; } CTxDestination DecodeCashAddrDestination(const CashAddrContent &content) { if (content.hash.size() != 20) { // Only 20 bytes hash are supported now. 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{}; } } + +// PackCashAddrContent allows for testing PackAddrData in unittests due to +// template definitions. +std::vector PackCashAddrContent(const CashAddrContent &content) { + return PackAddrData(content.hash, content.type); +} diff --git a/src/cashaddrenc.h b/src/cashaddrenc.h index 24eb3ae54f..561bb28957 100644 --- a/src/cashaddrenc.h +++ b/src/cashaddrenc.h @@ -1,27 +1,30 @@ // Copyright (c) 2017 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_CASHADDRENC_H #define BITCOIN_CASHADDRENC_H #include "script/standard.h" #include #include class CChainParams; +enum CashAddrType : uint8_t { PUBKEY_TYPE = 0, SCRIPT_TYPE = 1 }; + std::string EncodeCashAddr(const CTxDestination &, const CChainParams &); struct CashAddrContent { - uint8_t type; + CashAddrType type; std::vector hash; }; CTxDestination DecodeCashAddr(const std::string &addr, const CChainParams ¶ms); CashAddrContent DecodeCashAddrContent(const std::string &addr, const CChainParams ¶ms); CTxDestination DecodeCashAddrDestination(const CashAddrContent &content); +std::vector PackCashAddrContent(const CashAddrContent &content); #endif diff --git a/src/test/cashaddrenc_tests.cpp b/src/test/cashaddrenc_tests.cpp index 3c4b906d74..3774a0d33e 100644 --- a/src/test/cashaddrenc_tests.cpp +++ b/src/test/cashaddrenc_tests.cpp @@ -1,253 +1,297 @@ // Copyright (c) 2017 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "cashaddr.h" #include "cashaddrenc.h" #include "chainparams.h" #include "random.h" #include "test/test_bitcoin.h" #include "uint256.h" #include namespace { std::vector GetNetworks() { return {CBaseChainParams::MAIN, CBaseChainParams::TESTNET, CBaseChainParams::REGTEST}; } uint160 insecure_GetRandUInt160(FastRandomContext &rand) { uint160 n; for (uint8_t *c = n.begin(); c != n.end(); ++c) { *c = static_cast(rand.rand32()); } return n; } +std::vector insecure_GetRandomByteArray(FastRandomContext &rand, + size_t n) { + std::vector out; + out.reserve(n); + + for (size_t i = 0; i < n; i++) { + out.push_back(uint8_t(rand.randbits(8))); + } + return out; +} + class DstTypeChecker : public boost::static_visitor { public: void operator()(const CKeyID &id) { isKey = true; } void operator()(const CScriptID &id) { isScript = true; } void operator()(const CNoDestination &) {} static bool IsScriptDst(const CTxDestination &d) { DstTypeChecker checker; boost::apply_visitor(checker, d); return checker.isScript; } static bool IsKeyDst(const CTxDestination &d) { DstTypeChecker checker; boost::apply_visitor(checker, d); return checker.isKey; } private: DstTypeChecker() : isKey(false), isScript(false) {} bool isKey; bool isScript; }; +// Map all possible size bits in the version to the expected size of the +// hash in bytes. +const std::array, 8> valid_sizes = { + {{0, 20}, {1, 24}, {2, 28}, {3, 32}, {4, 40}, {5, 48}, {6, 56}, {7, 64}}}; + } // anon ns BOOST_FIXTURE_TEST_SUITE(cashaddrenc_tests, BasicTestingSetup) +BOOST_AUTO_TEST_CASE(encode_decode_all_sizes) { + FastRandomContext rand(true); + const CChainParams ¶ms = Params(CBaseChainParams::MAIN); + + for (auto ps : valid_sizes) { + std::vector data = + insecure_GetRandomByteArray(rand, ps.second); + CashAddrContent content = {PUBKEY_TYPE, data}; + std::vector packed_data = PackCashAddrContent(content); + + // Check that the packed size is correct + BOOST_CHECK_EQUAL(packed_data[1] >> 2, ps.first); + std::string address = + cashaddr::Encode(params.CashAddrPrefix(), packed_data); + + // Check that the address decodes properly + CashAddrContent decoded = DecodeCashAddrContent(address, params); + BOOST_CHECK_EQUAL_COLLECTIONS( + std::begin(content.hash), std::end(content.hash), + std::begin(decoded.hash), std::end(decoded.hash)); + } +} + +BOOST_AUTO_TEST_CASE(check_packaddr_throws) { + FastRandomContext rand(true); + + for (auto ps : valid_sizes) { + std::vector data = + insecure_GetRandomByteArray(rand, ps.second - 1); + CashAddrContent content = {PUBKEY_TYPE, data}; + BOOST_CHECK_THROW(PackCashAddrContent(content), std::runtime_error); + } +} + BOOST_AUTO_TEST_CASE(encode_decode) { std::vector toTest = {CNoDestination{}, CKeyID(uint160S("badf00d")), CScriptID(uint160S("f00dbad"))}; for (auto dst : toTest) { for (auto net : GetNetworks()) { std::string encoded = EncodeCashAddr(dst, Params(net)); CTxDestination decoded = DecodeCashAddr(encoded, Params(net)); BOOST_CHECK(dst == decoded); } } } // Check that an encoded cash address is not valid on another network. BOOST_AUTO_TEST_CASE(invalid_on_wrong_network) { const CTxDestination dst = CKeyID(uint160S("c0ffee")); const CTxDestination invalidDst = CNoDestination{}; for (auto net : GetNetworks()) { for (auto otherNet : GetNetworks()) { if (net == otherNet) continue; std::string encoded = EncodeCashAddr(dst, Params(net)); CTxDestination decoded = DecodeCashAddr(encoded, Params(otherNet)); BOOST_CHECK(decoded != dst); BOOST_CHECK(decoded == invalidDst); } } } BOOST_AUTO_TEST_CASE(random_dst) { FastRandomContext rand(true); const size_t NUM_TESTS = 5000; const CChainParams ¶ms = Params(CBaseChainParams::MAIN); for (size_t i = 0; i < NUM_TESTS; ++i) { uint160 hash = insecure_GetRandUInt160(rand); const CTxDestination dst_key = CKeyID(hash); const CTxDestination dst_scr = CScriptID(hash); const std::string encoded_key = EncodeCashAddr(dst_key, params); const CTxDestination decoded_key = DecodeCashAddr(encoded_key, params); const std::string encoded_scr = EncodeCashAddr(dst_scr, params); const CTxDestination decoded_scr = DecodeCashAddr(encoded_scr, params); std::string err("cashaddr failed for hash: "); err += hash.ToString(); BOOST_CHECK_MESSAGE(dst_key == decoded_key, err); BOOST_CHECK_MESSAGE(dst_scr == decoded_scr, err); BOOST_CHECK_MESSAGE(DstTypeChecker::IsKeyDst(decoded_key), err); BOOST_CHECK_MESSAGE(DstTypeChecker::IsScriptDst(decoded_scr), err); } } /** * 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) { data.push_back(1); } - BOOST_CHECK_EQUAL(data.size(), 34); + BOOST_CHECK_EQUAL(data.size(), 34UL); 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); + BOOST_CHECK_EQUAL(content.hash.size(), 20UL); // 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); + BOOST_CHECK_EQUAL(content.hash.size(), 0UL); } } /** * We ensure size is extracted and checked properly. */ BOOST_AUTO_TEST_CASE(check_size) { const CTxDestination nodst = CNoDestination{}; const CChainParams params = Params(CBaseChainParams::MAIN); - // 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) { + for (auto ps : valid_sizes) { // Number of bytes required for a 5-bit packed version of a hash, with // version byte. Add half a byte(4) so integer math provides the next // multiple-of-5 that would fit all the data. size_t expectedSize = (8 * (1 + ps.second) + 4) / 5; data.resize(expectedSize); std::fill(begin(data), end(data), 0); // After conversion from 8 bit packing to 5 bit packing, the size will // be in the second 5-bit group, shifted left twice. 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); + BOOST_CHECK_EQUAL(content.hash.size(), 0UL); 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_CHECK_EQUAL(content.hash.size(), 0UL); } } BOOST_AUTO_TEST_CASE(test_addresses) { const CChainParams params = Params(CBaseChainParams::MAIN); std::vector> hash{ {118, 160, 64, 83, 189, 160, 168, 139, 218, 81, 119, 184, 106, 21, 195, 178, 159, 85, 152, 115}, {203, 72, 18, 50, 41, 156, 213, 116, 49, 81, 172, 75, 45, 99, 174, 25, 142, 123, 176, 169}, {1, 31, 40, 228, 115, 201, 95, 64, 19, 215, 213, 62, 197, 251, 195, 180, 45, 248, 237, 16}}; std::vector pubkey = { "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", "bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy", "bitcoincash:qqq3728yw0y47sqn6l2na30mcw6zm78dzqre909m2r"}; std::vector script = { "bitcoincash:ppm2qsznhks23z7629mms6s4cwef74vcwvn0h829pq", "bitcoincash:pr95sy3j9xwd2ap32xkykttr4cvcu7as4yc93ky28e", "bitcoincash:pqq3728yw0y47sqn6l2na30mcw6zm78dzq5ucqzc37"}; for (size_t i = 0; i < hash.size(); ++i) { const CTxDestination dstKey = CKeyID(uint160(hash[i])); BOOST_CHECK_EQUAL(pubkey[i], EncodeCashAddr(dstKey, params)); const CTxDestination dstScript = CScriptID(uint160(hash[i])); BOOST_CHECK_EQUAL(script[i], EncodeCashAddr(dstScript, params)); } } BOOST_AUTO_TEST_SUITE_END()