Skip to content

Commit 90a2474

Browse files
committed
fuzz: add a new, more efficient, descriptor parsing target
This new target focuses on fuzzing the actual descriptor parsing logic by not requiring the fuzzer to produce valid keys (nor a valid checksum for that matter). This should make it much more efficient to find bugs we could introduce moving forward. Using a character as a marker (here '%') to be able to search and replace in the string without having to mock the actual descriptor parsing logic was an insight from Pieter Wuille.
1 parent d60229e commit 90a2474

File tree

1 file changed

+116
-0
lines changed

1 file changed

+116
-0
lines changed

src/test/fuzz/descriptor_parse.cpp

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,110 @@
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+
11110
/** Test a successfully parsed descriptor. */
12111
static void TestDescriptor(const Descriptor& desc)
13112
{
@@ -22,6 +121,23 @@ void initialize_descriptor_parse()
22121
SelectParams(ChainType::MAIN);
23122
}
24123

124+
void initialize_mocked_descriptor_parse()
125+
{
126+
initialize_descriptor_parse();
127+
MOCKED_DESC_CONVERTER.Init();
128+
}
129+
130+
FUZZ_TARGET(mocked_descriptor_parse, .init = initialize_mocked_descriptor_parse)
131+
{
132+
const std::string mocked_descriptor{buffer.begin(), buffer.end()};
133+
if (const auto descriptor = MOCKED_DESC_CONVERTER.GetDescriptor(mocked_descriptor)) {
134+
FlatSigningProvider signing_provider;
135+
std::string error;
136+
const auto desc = Parse(*descriptor, signing_provider, error);
137+
if (desc) TestDescriptor(*desc);
138+
}
139+
}
140+
25141
FUZZ_TARGET(descriptor_parse, .init = initialize_descriptor_parse)
26142
{
27143
const std::string descriptor(buffer.begin(), buffer.end());

0 commit comments

Comments
 (0)