From b4c84ba21e407c1740209f56d835d04ae280797e Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 28 Feb 2022 15:10:25 -0500 Subject: [PATCH 1/5] Simplify miniscript_random initialization --- bitcoin/test/fuzz/miniscript_random.cpp | 69 ++++++++++++------------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/bitcoin/test/fuzz/miniscript_random.cpp b/bitcoin/test/fuzz/miniscript_random.cpp index f6115d4..b229aa0 100644 --- a/bitcoin/test/fuzz/miniscript_random.cpp +++ b/bitcoin/test/fuzz/miniscript_random.cpp @@ -67,12 +67,11 @@ struct TestData { if (i & 1) hash160_preimages[hash] = std::vector(keydata, keydata + 32); } } -}; +} TEST_DATA; //! Context to parse a Miniscript node to and from Script or text representation. struct ParserContext { typedef CPubKey Key; - TestData *test_data; bool ToString(const Key& key, std::string& ret) const { ret = HexStr(key); return true; } @@ -101,12 +100,12 @@ struct ParserContext { assert(last - first == 20); CKeyID keyid; std::copy(first, last, keyid.begin()); - const auto it = test_data->dummy_keys_map.find(keyid); - if (it == test_data->dummy_keys_map.end()) return false; + const auto it = TEST_DATA.dummy_keys_map.find(keyid); + if (it == TEST_DATA.dummy_keys_map.end()) return false; key = it->second; return true; } -}; +} PARSER_CTX; //! Context to produce a satisfaction for a Miniscript node using the pre-computed data. struct SatisfierContext: ParserContext { @@ -117,8 +116,8 @@ struct SatisfierContext: ParserContext { // Signature challenges fulfilled with a dummy signature, if it was one of our dummy keys. miniscript::Availability Sign(const CPubKey& key, std::vector& sig) const { - const auto it = test_data->dummy_sigs.find(key); - if (it == test_data->dummy_sigs.end()) return miniscript::Availability::NO; + const auto it = TEST_DATA.dummy_sigs.find(key); + if (it == TEST_DATA.dummy_sigs.end()) return miniscript::Availability::NO; if (it->second.second) { // Key is "available" sig = it->second.first; @@ -138,18 +137,18 @@ struct SatisfierContext: ParserContext { return miniscript::Availability::YES; } miniscript::Availability SatSHA256(const std::vector& hash, std::vector& preimage) const { - return LookupHash(hash, preimage, test_data->sha256_preimages); + return LookupHash(hash, preimage, TEST_DATA.sha256_preimages); } miniscript::Availability SatRIPEMD160(const std::vector& hash, std::vector& preimage) const { - return LookupHash(hash, preimage, test_data->ripemd160_preimages); + return LookupHash(hash, preimage, TEST_DATA.ripemd160_preimages); } miniscript::Availability SatHASH256(const std::vector& hash, std::vector& preimage) const { - return LookupHash(hash, preimage, test_data->hash256_preimages); + return LookupHash(hash, preimage, TEST_DATA.hash256_preimages); } miniscript::Availability SatHASH160(const std::vector& hash, std::vector& preimage) const { - return LookupHash(hash, preimage, test_data->hash160_preimages); + return LookupHash(hash, preimage, TEST_DATA.hash160_preimages); } -}; +} SATISFIER_CTX; //! Context to check a satisfaction against the pre-computed data. struct CheckerContext: BaseSignatureChecker { @@ -160,19 +159,14 @@ struct CheckerContext: BaseSignatureChecker { const CScript& scriptCode, SigVersion sigversion) const override { const CPubKey key{vchPubKey}; - const auto it = test_data->dummy_sigs.find(key); - if (it == test_data->dummy_sigs.end()) return false; + const auto it = TEST_DATA.dummy_sigs.find(key); + if (it == TEST_DATA.dummy_sigs.end()) return false; return it->second.first == sig; } bool CheckLockTime(const CScriptNum& nLockTime) const override { return nLockTime.GetInt64() & 1; } bool CheckSequence(const CScriptNum& nSequence) const override { return nSequence.GetInt64() & 1; } -}; +} CHECKER_CTX; -// The various contexts -TestData TEST_DATA; -ParserContext PARSER_CTX; -SatisfierContext SATISFIER_CTX; -CheckerContext CHECKER_CTX; // A dummy scriptsig to pass to VerifyScript (we always use Segwit v0). const CScript DUMMY_SCRIPTSIG; @@ -323,7 +317,7 @@ struct SmartInfo using recipe = std::pair>; std::map> table; - SmartInfo() + void Init() { /* Construct a set of interesting type requirements to reason with (sections of BKVWzondu). */ std::vector types; @@ -550,7 +544,7 @@ struct SmartInfo ); } } -}; +} SMARTINFO; /** * Consume a Miniscript node from the fuzzer's output. @@ -563,11 +557,9 @@ struct SmartInfo * everything). */ std::optional ConsumeNodeSmart(FuzzedDataProvider& provider, Type type_needed) { - /** Precompute table once, but only when this function is invoked (it can take ~seconds). */ - static const SmartInfo g_smartinfo; /** Table entry for the requested type. */ - auto recipes_it = g_smartinfo.table.find(type_needed); - assert(recipes_it != g_smartinfo.table.end()); + auto recipes_it = SMARTINFO.table.find(type_needed); + assert(recipes_it != SMARTINFO.table.end()); /** Pick one recipe from the available ones for that type. */ const auto& [frag, subt] = PickValue(provider, recipes_it->second); @@ -695,15 +687,6 @@ NodeRef GenNode(F ConsumeNode, Type root_type = ""_mst, bool strict_valid = fals return std::move(stack[0]); } -//! Pre-compute the test data and point the various contexts to it. -void initialize_miniscript_random() { - ECC_Start(); - TEST_DATA.Init(); - PARSER_CTX.test_data = &TEST_DATA; - SATISFIER_CTX.test_data = &TEST_DATA; - CHECKER_CTX.test_data = &TEST_DATA; -} - /** Perform various applicable tests on a miniscript Node. */ void TestNode(const NodeRef& node, FuzzedDataProvider& provider) { @@ -839,10 +822,22 @@ void TestNode(const NodeRef& node, FuzzedDataProvider& provider) assert(mal_success == satisfiable); } +void FuzzInit() +{ + ECC_Start(); + TEST_DATA.Init(); +} + +void FuzzInitSmart() +{ + FuzzInit(); + SMARTINFO.Init(); +} + } // namespace /** Fuzz target that runs TestNode on nodes generated using ConsumeNodeStable. */ -FUZZ_TARGET_INIT(miniscript_random_stable, initialize_miniscript_random) +FUZZ_TARGET_INIT(miniscript_random_stable, FuzzInit) { FuzzedDataProvider provider(buffer.data(), buffer.size()); TestNode(GenNode([&](Type) { @@ -851,7 +846,7 @@ FUZZ_TARGET_INIT(miniscript_random_stable, initialize_miniscript_random) } /** Fuzz target that runs TestNode on nodes generated using ConsumeNodeSmart. */ -FUZZ_TARGET_INIT(miniscript_random_smart, initialize_miniscript_random) +FUZZ_TARGET_INIT(miniscript_random_smart, FuzzInitSmart) { /** The set of types we aim to construct nodes for. Together they cover all. */ static constexpr std::array BASE_TYPES{"B"_mst, "V"_mst, "K"_mst, "W"_mst}; From 2cf7c86f964f2d1d523be3ce5808f7168805051c Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 28 Feb 2022 15:27:59 -0500 Subject: [PATCH 2/5] Switch pubkey encoding to 2 hex characters (their idx) --- bitcoin/test/fuzz/miniscript_random.cpp | 28 +++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/bitcoin/test/fuzz/miniscript_random.cpp b/bitcoin/test/fuzz/miniscript_random.cpp index b229aa0..94f5e4a 100644 --- a/bitcoin/test/fuzz/miniscript_random.cpp +++ b/bitcoin/test/fuzz/miniscript_random.cpp @@ -20,6 +20,7 @@ struct TestData { // Precomputed public keys, and a dummy signature for each of them. std::vector dummy_keys; + std::map dummy_key_idx_map; std::map dummy_keys_map; std::map, bool>> dummy_sigs; @@ -43,7 +44,9 @@ struct TestData { const Key pubkey = privkey.GetPubKey(); dummy_keys.push_back(pubkey); + dummy_key_idx_map.emplace(pubkey, i); dummy_keys_map.insert({pubkey.GetID(), pubkey}); + std::vector sig; privkey.Sign(uint256S(""), sig); sig.push_back(1); // SIGHASH_ALL @@ -73,20 +76,33 @@ struct TestData { struct ParserContext { typedef CPubKey Key; - bool ToString(const Key& key, std::string& ret) const { ret = HexStr(key); return true; } + bool ToString(const Key& key, std::string& ret) const + { + auto it = TEST_DATA.dummy_key_idx_map.find(key); + if (it == TEST_DATA.dummy_key_idx_map.end()) return false; + uint8_t idx = it->second; + ret = HexStr(Span{&idx, 1}); + return true; + } - const std::vector ToPKBytes(const Key& key) const { return {key.begin(), key.end()}; } + const std::vector ToPKBytes(const Key& key) const + { + return {key.begin(), key.end()}; + } - const std::vector ToPKHBytes(const Key& key) const { + const std::vector ToPKHBytes(const Key& key) const + { const auto h = Hash160(key); return {h.begin(), h.end()}; } template bool FromString(I first, I last, Key& key) const { - const auto bytes = ParseHex(std::string(first, last)); - key.Set(bytes.begin(), bytes.end()); - return key.IsValid(); + if (last - first != 2) return false; + auto idx = ParseHex(std::string(first, last)); + if (idx.size() != 1) return false; + key = TEST_DATA.dummy_keys[idx[0]]; + return true; } template From 6f497145f80412db76aaf026b9deb8945501ddb0 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 28 Feb 2022 16:31:40 -0500 Subject: [PATCH 3/5] Add miniscript_string fuzz test --- bitcoin/test/fuzz/miniscript_random.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bitcoin/test/fuzz/miniscript_random.cpp b/bitcoin/test/fuzz/miniscript_random.cpp index 94f5e4a..e46370e 100644 --- a/bitcoin/test/fuzz/miniscript_random.cpp +++ b/bitcoin/test/fuzz/miniscript_random.cpp @@ -872,3 +872,18 @@ FUZZ_TARGET_INIT(miniscript_random_smart, FuzzInitSmart) return ConsumeNodeSmart(provider, needed_type); }, PickValue(provider, BASE_TYPES), true), provider); } + +/* Fuzz tests that test parsing from a string, and roundtripping via string. */ +FUZZ_TARGET_INIT(miniscript_string, FuzzInit) +{ + FuzzedDataProvider provider(buffer.data(), buffer.size()); + auto str = provider.ConsumeRemainingBytesAsString(); + auto parsed = miniscript::FromString(str, PARSER_CTX); + if (!parsed) return; + + std::string str2; + assert(parsed->ToString(PARSER_CTX, str2)); + auto parsed2 = miniscript::FromString(str2, PARSER_CTX); + assert(parsed2); + assert(*parsed == *parsed2); +} From f990c4ba1a9508dad273a50273c8876bbd3f7bbc Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 28 Feb 2022 18:24:07 -0500 Subject: [PATCH 4/5] Add miniscript_script fuzz test --- bitcoin/test/fuzz/miniscript_random.cpp | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/bitcoin/test/fuzz/miniscript_random.cpp b/bitcoin/test/fuzz/miniscript_random.cpp index e46370e..040a80a 100644 --- a/bitcoin/test/fuzz/miniscript_random.cpp +++ b/bitcoin/test/fuzz/miniscript_random.cpp @@ -123,6 +123,43 @@ struct ParserContext { } } PARSER_CTX; +//! Context that implements naive conversion from/to script only, for roundtrip testing. +struct ScriptParserContext { + struct Key { + bool is_hash; + std::vector data; + }; + + const std::vector& ToPKBytes(const Key& key) const + { + assert(!key.is_hash); + return key.data; + } + + const std::vector ToPKHBytes(const Key& key) const + { + if (key.is_hash) return key.data; + const auto h = Hash160(key.data); + return {h.begin(), h.end()}; + } + + template + bool FromPKBytes(I first, I last, Key& key) const + { + key.data.assign(first, last); + key.is_hash = false; + return true; + } + + template + bool FromPKHBytes(I first, I last, Key& key) const + { + key.data.assign(first, last); + key.is_hash = true; + return true; + } +} SCRIPT_PARSER_CONTEXT; + //! Context to produce a satisfaction for a Miniscript node using the pre-computed data. struct SatisfierContext: ParserContext { // Timelock challenges satisfaction. Make the value (deterministically) vary to explore different @@ -887,3 +924,16 @@ FUZZ_TARGET_INIT(miniscript_string, FuzzInit) assert(parsed2); assert(*parsed == *parsed2); } + +/* Fuzz tests that test parsing from a script, and roundtripping via script. */ +FUZZ_TARGET(miniscript_script) +{ + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + const std::optional script = ConsumeDeserializable(fuzzed_data_provider); + if (!script) return; + + const auto ms = miniscript::FromScript(*script, SCRIPT_PARSER_CONTEXT); + if (!ms) return; + + assert(ms->ToScript(SCRIPT_PARSER_CONTEXT) == *script); +} From c65113d0791da170139ab4dc2cb66394c94ddcf0 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 28 Feb 2022 18:25:24 -0500 Subject: [PATCH 5/5] Rename miniscript_random fuzz test -> miniscript --- bitcoin/test/fuzz/{miniscript_random.cpp => miniscript.cpp} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename bitcoin/test/fuzz/{miniscript_random.cpp => miniscript.cpp} (99%) diff --git a/bitcoin/test/fuzz/miniscript_random.cpp b/bitcoin/test/fuzz/miniscript.cpp similarity index 99% rename from bitcoin/test/fuzz/miniscript_random.cpp rename to bitcoin/test/fuzz/miniscript.cpp index 040a80a..c776eb2 100644 --- a/bitcoin/test/fuzz/miniscript_random.cpp +++ b/bitcoin/test/fuzz/miniscript.cpp @@ -890,7 +890,7 @@ void FuzzInitSmart() } // namespace /** Fuzz target that runs TestNode on nodes generated using ConsumeNodeStable. */ -FUZZ_TARGET_INIT(miniscript_random_stable, FuzzInit) +FUZZ_TARGET_INIT(miniscript_stable, FuzzInit) { FuzzedDataProvider provider(buffer.data(), buffer.size()); TestNode(GenNode([&](Type) { @@ -899,7 +899,7 @@ FUZZ_TARGET_INIT(miniscript_random_stable, FuzzInit) } /** Fuzz target that runs TestNode on nodes generated using ConsumeNodeSmart. */ -FUZZ_TARGET_INIT(miniscript_random_smart, FuzzInitSmart) +FUZZ_TARGET_INIT(miniscript_smart, FuzzInitSmart) { /** The set of types we aim to construct nodes for. Together they cover all. */ static constexpr std::array BASE_TYPES{"B"_mst, "V"_mst, "K"_mst, "W"_mst};