key_io_tests.cpp raw
1 // Copyright (c) 2011-2022 The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #include <test/data/key_io_invalid.json.h>
6 #include <test/data/key_io_valid.json.h>
7
8 #include <key.h>
9 #include <bech32.h>
10 #include <key_io.h>
11 #include <script/script.h>
12 #include <test/util/json.h>
13 #include <test/util/setup_common.h>
14 #include <univalue.h>
15 #include <util/chaintype.h>
16 #include <util/strencodings.h>
17
18 #include <boost/test/unit_test.hpp>
19
20 #include <algorithm>
21
22 BOOST_FIXTURE_TEST_SUITE(key_io_tests, BasicTestingSetup)
23
24 // Goal: check that parsed keys match test payload
25 BOOST_AUTO_TEST_CASE(key_io_valid_parse)
26 {
27 UniValue tests = read_json(json_tests::key_io_valid);
28 CKey privkey;
29 CTxDestination destination;
30 SelectParams(ChainType::MAIN);
31
32 for (unsigned int idx = 0; idx < tests.size(); idx++) {
33 const UniValue& test = tests[idx];
34 std::string strTest = test.write();
35 if (test.size() < 3) { // Allow for extra stuff (useful for comments)
36 BOOST_ERROR("Bad test: " << strTest);
37 continue;
38 }
39 std::string exp_base58string = test[0].get_str();
40 const std::vector<std::byte> exp_payload{ParseHex<std::byte>(test[1].get_str())};
41 const UniValue &metadata = test[2].get_obj();
42 bool isPrivkey = metadata.find_value("isPrivkey").get_bool();
43 SelectParams(ChainTypeFromString(metadata.find_value("chain").get_str()).value());
44 bool try_case_flip = metadata.find_value("tryCaseFlip").isNull() ? false : metadata.find_value("tryCaseFlip").get_bool();
45 if (isPrivkey) {
46 bool isCompressed = metadata.find_value("isCompressed").get_bool();
47 // Must be valid private key
48 privkey = DecodeSecret(exp_base58string);
49 BOOST_CHECK_MESSAGE(privkey.IsValid(), "!IsValid:" + strTest);
50 BOOST_CHECK_MESSAGE(privkey.IsCompressed() == isCompressed, "compressed mismatch:" + strTest);
51 BOOST_CHECK_MESSAGE(std::ranges::equal(privkey, exp_payload), "key mismatch:" + strTest);
52
53 // Private key must be invalid public key
54 destination = DecodeDestination(exp_base58string);
55 BOOST_CHECK_MESSAGE(!IsValidDestination(destination), "IsValid privkey as pubkey:" + strTest);
56 } else {
57 // Must be valid public key
58 destination = DecodeDestination(exp_base58string);
59 CScript script = GetScriptForDestination(destination);
60 BOOST_CHECK_MESSAGE(IsValidDestination(destination), "!IsValid:" + strTest);
61 BOOST_CHECK_EQUAL(HexStr(script), HexStr(exp_payload));
62
63 // Try flipped case version
64 for (char& c : exp_base58string) {
65 if (c >= 'a' && c <= 'z') {
66 c = (c - 'a') + 'A';
67 } else if (c >= 'A' && c <= 'Z') {
68 c = (c - 'A') + 'a';
69 }
70 }
71 destination = DecodeDestination(exp_base58string);
72 BOOST_CHECK_MESSAGE(IsValidDestination(destination) == try_case_flip, "!IsValid case flipped:" + strTest);
73 if (IsValidDestination(destination)) {
74 script = GetScriptForDestination(destination);
75 BOOST_CHECK_EQUAL(HexStr(script), HexStr(exp_payload));
76 }
77
78 // Public key must be invalid private key
79 privkey = DecodeSecret(exp_base58string);
80 BOOST_CHECK_MESSAGE(!privkey.IsValid(), "IsValid pubkey as privkey:" + strTest);
81 }
82 }
83 }
84
85 // Goal: check that generated keys match test vectors
86 BOOST_AUTO_TEST_CASE(key_io_valid_gen)
87 {
88 UniValue tests = read_json(json_tests::key_io_valid);
89
90 for (unsigned int idx = 0; idx < tests.size(); idx++) {
91 const UniValue& test = tests[idx];
92 std::string strTest = test.write();
93 if (test.size() < 3) // Allow for extra stuff (useful for comments)
94 {
95 BOOST_ERROR("Bad test: " << strTest);
96 continue;
97 }
98 std::string exp_base58string = test[0].get_str();
99 std::vector<unsigned char> exp_payload = ParseHex(test[1].get_str());
100 const UniValue &metadata = test[2].get_obj();
101 bool isPrivkey = metadata.find_value("isPrivkey").get_bool();
102 SelectParams(ChainTypeFromString(metadata.find_value("chain").get_str()).value());
103 if (isPrivkey) {
104 bool isCompressed = metadata.find_value("isCompressed").get_bool();
105 CKey key;
106 key.Set(exp_payload.begin(), exp_payload.end(), isCompressed);
107 assert(key.IsValid());
108 BOOST_CHECK_MESSAGE(EncodeSecret(key) == exp_base58string, "result mismatch: " + strTest);
109 } else {
110 CTxDestination dest;
111 CScript exp_script(exp_payload.begin(), exp_payload.end());
112 BOOST_CHECK(ExtractDestination(exp_script, dest));
113 std::string address = EncodeDestination(dest);
114
115 BOOST_CHECK_EQUAL(address, exp_base58string);
116 }
117 }
118
119 SelectParams(ChainType::MAIN);
120 }
121
122
123 // Goal: check that base58 parsing code is robust against a variety of corrupted data
124 BOOST_AUTO_TEST_CASE(key_io_invalid)
125 {
126 UniValue tests = read_json(json_tests::key_io_invalid); // Negative testcases
127 CKey privkey;
128 CTxDestination destination;
129
130 for (unsigned int idx = 0; idx < tests.size(); idx++) {
131 const UniValue& test = tests[idx];
132 std::string strTest = test.write();
133 if (test.size() < 1) // Allow for extra stuff (useful for comments)
134 {
135 BOOST_ERROR("Bad test: " << strTest);
136 continue;
137 }
138 std::string exp_base58string = test[0].get_str();
139
140 // must be invalid as public and as private key
141 for (const auto& chain : {ChainType::MAIN, ChainType::TESTNET, ChainType::SIGNET, ChainType::REGTEST}) {
142 SelectParams(chain);
143 destination = DecodeDestination(exp_base58string);
144 BOOST_CHECK_MESSAGE(!IsValidDestination(destination), "IsValid pubkey in mainnet:" + strTest);
145 privkey = DecodeSecret(exp_base58string);
146 BOOST_CHECK_MESSAGE(!privkey.IsValid(), "IsValid privkey in mainnet:" + strTest);
147 }
148 }
149 }
150
151 BOOST_AUTO_TEST_CASE(key_io_lm1_p2spkh)
152 {
153 // lm1 addresses carry the P2SPKH hash with a fixed chain-independent HRP.
154 CKey key = GenerateRandomKey();
155 const XOnlyPubKey xpk(key.GetPubKey());
156 const WitnessV3SpkHash hash(xpk);
157 const std::string addr = EncodeDestination(hash);
158 BOOST_CHECK(addr.starts_with("lm1"));
159 BOOST_CHECK_EQUAL(addr.substr(0, 4), "lm11");
160
161 CTxDestination decoded = DecodeDestination(addr);
162 BOOST_CHECK(IsValidDestination(decoded));
163 BOOST_CHECK(std::holds_alternative<WitnessV3SpkHash>(decoded));
164 BOOST_CHECK_EQUAL(uint256(std::get<WitnessV3SpkHash>(decoded)), uint256(hash));
165 }
166
167 BOOST_AUTO_TEST_CASE(key_io_lm2_stealth)
168 {
169 // lm2 addresses carry view+spend keys (66 bytes, roughly twice a v3).
170 const CKey view = GenerateRandomKey();
171 const CKey spend = GenerateRandomKey();
172 const WitnessV4StealthAddress stealth{view.GetPubKey(), spend.GetPubKey()};
173 const std::string addr = EncodeDestination(stealth);
174 BOOST_CHECK(addr.starts_with("lm2"));
175 BOOST_CHECK_EQUAL(addr.substr(0, 4), "lm21");
176 // Roughly twice the length of a standard witness address.
177 BOOST_CHECK(addr.size() > 100);
178
179 CTxDestination decoded = DecodeDestination(addr);
180 BOOST_CHECK(IsValidDestination(decoded));
181 BOOST_CHECK(std::holds_alternative<WitnessV4StealthAddress>(decoded));
182 const auto& got = std::get<WitnessV4StealthAddress>(decoded);
183 BOOST_CHECK(got.view == view.GetPubKey());
184 BOOST_CHECK(got.spend == spend.GetPubKey());
185
186 // Tampered checksum / wrong key bytes fail.
187 std::string bad = addr;
188 bad[bad.size() - 1] = (bad[bad.size() - 1] == 'q') ? 'p' : 'q';
189 std::string err;
190 CTxDestination bad_dec = DecodeDestination(bad, err);
191 BOOST_CHECK(!IsValidDestination(bad_dec));
192 }
193
194 BOOST_AUTO_TEST_CASE(key_io_lm2_rejects_short_payload)
195 {
196 // A valid bech32m lm2 string with a truncated payload must not decode.
197 std::vector<unsigned char> payload(33, 0x02);
198 std::vector<unsigned char> data;
199 ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, payload.begin(), payload.end());
200 const std::string addr = bech32::Encode(bech32::Encoding::BECH32M, "lm2", data);
201 CTxDestination decoded = DecodeDestination(addr);
202 BOOST_CHECK(!IsValidDestination(decoded));
203 }
204
205 BOOST_AUTO_TEST_SUITE_END()
206