Skip to content

Commit cbf3850

Browse files
committed
Merge bitcoin/bitcoin#27888: Fuzz: a more efficient descriptor parsing target
131314b fuzz: increase coverage of the descriptor targets (Antoine Poinsot) 90a2474 fuzz: add a new, more efficient, descriptor parsing target (Antoine Poinsot) d60229e fuzz: make the parsed descriptor testing into a function (Antoine Poinsot) Pull request description: The current descriptor parsing fuzz target requires valid public or private keys to be provided. This is unnecessary as we are only interested in fuzzing the descriptor parsing logic here (other targets are focused on fuzzing keys serializations). And it's pretty inefficient, especially for formats that need a checksum (`xpub`, `xprv`, WIF). This introduces a new target that mocks the keys as an index in a list of precomputed keys. Keys are represented as 2 hex characters in the descriptor. The key type (private, public, extended, ..) is deterministically based on this one-byte value. Keys are deterministically generated at target initialization. This is much more efficient and also largely reduces the size of the seeds. TL;DR: for instance instead of requiring the fuzzer to generate a `pk(xpub6DdBu7pBoyf7RjnUVhg8y6LFCfca2QAGJ39FcsgXM52Pg7eejUHLBJn4gNMey5dacyt4AjvKzdTQiuLfRdK8rSzyqZPJmNAcYZ9kVVEz4kj)` to parse a valid descriptor, it just needs to generate a `pk(03)`. Note we only mock the keys themselves, not the entire descriptor key expression. As we want to fuzz the real code that parses the rest of the key expression (origin, derivation paths, ..). This is a target i used for reviewing #17190 and #27255, and figured it was worth PR'ing on its own since the added complexity for mocking the keys is minimal and it could help prevent introducing bugs to the descriptor parsing logic much more efficiently. ACKs for top commit: MarcoFalke: re-ACK 131314b 🐓 achow101: ACK 131314b Tree-SHA512: 485a8d6a0f31a3a132df94dc57f97bdd81583d63507510debaac6a41dbbb42fa83c704ff3f2bd0b78c8673c583157c9a3efd79410e5e79511859e1470e629118
2 parents 272c4f3 + 131314b commit cbf3850

File tree

1 file changed

+144
-5
lines changed

1 file changed

+144
-5
lines changed

src/test/fuzz/descriptor_parse.cpp

Lines changed: 144 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,167 @@
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

55
#include <chainparams.h>
6+
#include <key_io.h>
67
#include <pubkey.h>
78
#include <script/descriptor.h>
89
#include <test/fuzz/fuzz.h>
910
#include <util/chaintype.h>
1011

12+
//! Types are raw (un)compressed pubkeys, raw xonly pubkeys, raw privkeys (WIF), xpubs, xprvs.
13+
static constexpr uint8_t KEY_TYPES_COUNT{6};
14+
//! How many keys we'll generate in total.
15+
static constexpr size_t TOTAL_KEYS_GENERATED{std::numeric_limits<uint8_t>::max() + 1};
16+
17+
/**
18+
* Converts a mocked descriptor string to a valid one. Every key in a mocked descriptor key is
19+
* represented by 2 hex characters preceded by the '%' character. We parse the two hex characters
20+
* as an index in a list of pre-generated keys. This list contains keys of the various types
21+
* accepted in descriptor keys expressions.
22+
*/
23+
class MockedDescriptorConverter {
24+
//! 256 keys of various types.
25+
std::array<std::string, TOTAL_KEYS_GENERATED> keys_str;
26+
27+
public:
28+
// We derive the type of key to generate from the 1-byte id parsed from hex.
29+
bool IdIsCompPubKey(uint8_t idx) const { return idx % KEY_TYPES_COUNT == 0; }
30+
bool IdIsUnCompPubKey(uint8_t idx) const { return idx % KEY_TYPES_COUNT == 1; }
31+
bool IdIsXOnlyPubKey(uint8_t idx) const { return idx % KEY_TYPES_COUNT == 2; }
32+
bool IdIsConstPrivKey(uint8_t idx) const { return idx % KEY_TYPES_COUNT == 3; }
33+
bool IdIsXpub(uint8_t idx) const { return idx % KEY_TYPES_COUNT == 4; }
34+
bool IdIsXprv(uint8_t idx) const { return idx % KEY_TYPES_COUNT == 5; }
35+
36+
//! When initializing the target, populate the list of keys.
37+
void Init() {
38+
// The data to use as a private key or a seed for an xprv.
39+
std::array<std::byte, 32> key_data{std::byte{1}};
40+
// Generate keys of all kinds and store them in the keys array.
41+
for (size_t i{0}; i < TOTAL_KEYS_GENERATED; i++) {
42+
key_data[31] = std::byte(i);
43+
44+
// If this is a "raw" key, generate a normal privkey. Otherwise generate
45+
// an extended one.
46+
if (IdIsCompPubKey(i) || IdIsUnCompPubKey(i) || IdIsXOnlyPubKey(i) || IdIsConstPrivKey(i)) {
47+
CKey privkey;
48+
privkey.Set(UCharCast(key_data.begin()), UCharCast(key_data.end()), !IdIsUnCompPubKey(i));
49+
if (IdIsCompPubKey(i) || IdIsUnCompPubKey(i)) {
50+
CPubKey pubkey{privkey.GetPubKey()};
51+
keys_str[i] = HexStr(pubkey);
52+
} else if (IdIsXOnlyPubKey(i)) {
53+
const XOnlyPubKey pubkey{privkey.GetPubKey()};
54+
keys_str[i] = HexStr(pubkey);
55+
} else {
56+
keys_str[i] = EncodeSecret(privkey);
57+
}
58+
} else {
59+
CExtKey ext_privkey;
60+
ext_privkey.SetSeed(key_data);
61+
if (IdIsXprv(i)) {
62+
keys_str[i] = EncodeExtKey(ext_privkey);
63+
} else {
64+
const CExtPubKey ext_pubkey{ext_privkey.Neuter()};
65+
keys_str[i] = EncodeExtPubKey(ext_pubkey);
66+
}
67+
}
68+
}
69+
}
70+
71+
//! Parse an id in the keys vectors from a 2-characters hex string.
72+
std::optional<uint8_t> IdxFromHex(std::string_view hex_characters) const {
73+
if (hex_characters.size() != 2) return {};
74+
auto idx = ParseHex(hex_characters);
75+
if (idx.size() != 1) return {};
76+
return idx[0];
77+
}
78+
79+
//! Get an actual descriptor string from a descriptor string whose keys were mocked.
80+
std::optional<std::string> GetDescriptor(std::string_view mocked_desc) const {
81+
// The smallest fragment would be "pk(%00)"
82+
if (mocked_desc.size() < 7) return {};
83+
84+
// The actual descriptor string to be returned.
85+
std::string desc;
86+
desc.reserve(mocked_desc.size());
87+
88+
// Replace all occurences of '%' followed by two hex characters with the corresponding key.
89+
for (size_t i = 0; i < mocked_desc.size();) {
90+
if (mocked_desc[i] == '%') {
91+
if (i + 3 >= mocked_desc.size()) return {};
92+
if (const auto idx = IdxFromHex(mocked_desc.substr(i + 1, 2))) {
93+
desc += keys_str[*idx];
94+
i += 3;
95+
} else {
96+
return {};
97+
}
98+
} else {
99+
desc += mocked_desc[i++];
100+
}
101+
}
102+
103+
return desc;
104+
}
105+
};
106+
107+
//! The converter of mocked descriptors, needs to be initialized when the target is.
108+
MockedDescriptorConverter MOCKED_DESC_CONVERTER;
109+
110+
/** Test a successfully parsed descriptor. */
111+
static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_provider, std::string& dummy)
112+
{
113+
// Trivial helpers.
114+
(void)desc.IsRange();
115+
(void)desc.IsSolvable();
116+
(void)desc.IsSingleType();
117+
(void)desc.GetOutputType();
118+
119+
// Serialization to string representation.
120+
(void)desc.ToString();
121+
(void)desc.ToPrivateString(sig_provider, dummy);
122+
(void)desc.ToNormalizedString(sig_provider, dummy);
123+
124+
// Serialization to Script.
125+
DescriptorCache cache;
126+
std::vector<CScript> out_scripts;
127+
(void)desc.Expand(0, sig_provider, out_scripts, sig_provider, &cache);
128+
(void)desc.ExpandPrivate(0, sig_provider, sig_provider);
129+
(void)desc.ExpandFromCache(0, cache, out_scripts, sig_provider);
130+
131+
// If we could serialize to script we must be able to infer using the same provider.
132+
if (!out_scripts.empty()) {
133+
assert(InferDescriptor(out_scripts.back(), sig_provider));
134+
}
135+
}
136+
11137
void initialize_descriptor_parse()
12138
{
13139
ECC_Start();
14140
SelectParams(ChainType::MAIN);
15141
}
16142

143+
void initialize_mocked_descriptor_parse()
144+
{
145+
initialize_descriptor_parse();
146+
MOCKED_DESC_CONVERTER.Init();
147+
}
148+
149+
FUZZ_TARGET(mocked_descriptor_parse, .init = initialize_mocked_descriptor_parse)
150+
{
151+
const std::string mocked_descriptor{buffer.begin(), buffer.end()};
152+
if (const auto descriptor = MOCKED_DESC_CONVERTER.GetDescriptor(mocked_descriptor)) {
153+
FlatSigningProvider signing_provider;
154+
std::string error;
155+
const auto desc = Parse(*descriptor, signing_provider, error);
156+
if (desc) TestDescriptor(*desc, signing_provider, error);
157+
}
158+
}
159+
17160
FUZZ_TARGET(descriptor_parse, .init = initialize_descriptor_parse)
18161
{
19162
const std::string descriptor(buffer.begin(), buffer.end());
20163
FlatSigningProvider signing_provider;
21164
std::string error;
22165
for (const bool require_checksum : {true, false}) {
23166
const auto desc = Parse(descriptor, signing_provider, error, require_checksum);
24-
if (desc) {
25-
(void)desc->ToString();
26-
(void)desc->IsRange();
27-
(void)desc->IsSolvable();
28-
}
167+
if (desc) TestDescriptor(*desc, signing_provider, error);
29168
}
30169
}

0 commit comments

Comments
 (0)