script_assets_test_minimizer.cpp
1 // Copyright (c) 2020-present The Bitcoin Core 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/fuzz/fuzz.h> 6 7 #include <primitives/transaction.h> 8 #include <pubkey.h> 9 #include <script/interpreter.h> 10 #include <serialize.h> 11 #include <streams.h> 12 #include <univalue.h> 13 #include <util/strencodings.h> 14 #include <util/string.h> 15 16 #include <cstdint> 17 #include <string> 18 #include <vector> 19 20 using util::SplitString; 21 22 // This fuzz "test" can be used to minimize test cases for script_assets_test in 23 // src/test/script_tests.cpp. While it written as a fuzz test, and can be used as such, 24 // fuzzing the inputs is unlikely to construct useful test cases. 25 // 26 // Instead, it is primarily intended to be run on a test set that was generated 27 // externally, for example using test/functional/feature_taproot.py's --dumptests mode. 28 // The minimized set can then be concatenated together, surrounded by '[' and ']', 29 // and used as the script_assets_test.json input to the script_assets_test unit test: 30 // 31 // (normal build) 32 // $ mkdir dump 33 // $ for N in $(seq 1 10); do TEST_DUMP_DIR=dump test/functional/feature_taproot.py --dumptests; done 34 // $ ... 35 // 36 // (libFuzzer build) 37 // $ mkdir dump-min 38 // $ FUZZ=script_assets_test_minimizer ./bin/fuzz -merge=1 -use_value_profile=1 dump-min/ dump/ 39 // $ (echo -en '[\n'; cat dump-min/* | head -c -2; echo -en '\n]') >script_assets_test.json 40 41 namespace { 42 43 std::vector<unsigned char> CheckedParseHex(const std::string& str) 44 { 45 if (str.size() && !IsHex(str)) throw std::runtime_error("Non-hex input '" + str + "'"); 46 return ParseHex(str); 47 } 48 49 CScript ScriptFromHex(const std::string& str) 50 { 51 std::vector<unsigned char> data = CheckedParseHex(str); 52 return CScript(data.begin(), data.end()); 53 } 54 55 CMutableTransaction TxFromHex(const std::string& str) 56 { 57 CMutableTransaction tx; 58 try { 59 SpanReader{CheckedParseHex(str)} >> TX_NO_WITNESS(tx); 60 } catch (const std::ios_base::failure&) { 61 throw std::runtime_error("Tx deserialization failure"); 62 } 63 return tx; 64 } 65 66 std::vector<CTxOut> TxOutsFromJSON(const UniValue& univalue) 67 { 68 if (!univalue.isArray()) throw std::runtime_error("Prevouts must be array"); 69 std::vector<CTxOut> prevouts; 70 for (size_t i = 0; i < univalue.size(); ++i) { 71 CTxOut txout; 72 try { 73 SpanReader{CheckedParseHex(univalue[i].get_str())} >> txout; 74 } catch (const std::ios_base::failure&) { 75 throw std::runtime_error("Prevout invalid format"); 76 } 77 prevouts.push_back(std::move(txout)); 78 } 79 return prevouts; 80 } 81 82 CScriptWitness ScriptWitnessFromJSON(const UniValue& univalue) 83 { 84 if (!univalue.isArray()) throw std::runtime_error("Script witness is not array"); 85 CScriptWitness scriptwitness; 86 for (size_t i = 0; i < univalue.size(); ++i) { 87 auto bytes = CheckedParseHex(univalue[i].get_str()); 88 scriptwitness.stack.push_back(std::move(bytes)); 89 } 90 return scriptwitness; 91 } 92 93 const std::map<std::string, script_verify_flag_name> FLAG_NAMES = { 94 {std::string("P2SH"), SCRIPT_VERIFY_P2SH}, 95 {std::string("DERSIG"), SCRIPT_VERIFY_DERSIG}, 96 {std::string("NULLDUMMY"), SCRIPT_VERIFY_NULLDUMMY}, 97 {std::string("CHECKLOCKTIMEVERIFY"), SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY}, 98 {std::string("CHECKSEQUENCEVERIFY"), SCRIPT_VERIFY_CHECKSEQUENCEVERIFY}, 99 {std::string("WITNESS"), SCRIPT_VERIFY_WITNESS}, 100 {std::string("TAPROOT"), SCRIPT_VERIFY_TAPROOT}, 101 }; 102 103 std::vector<script_verify_flags> AllFlags() 104 { 105 std::vector<script_verify_flags> ret; 106 107 for (unsigned int i = 0; i < 128; ++i) { 108 script_verify_flags flag = 0; 109 if (i & 1) flag |= SCRIPT_VERIFY_P2SH; 110 if (i & 2) flag |= SCRIPT_VERIFY_DERSIG; 111 if (i & 4) flag |= SCRIPT_VERIFY_NULLDUMMY; 112 if (i & 8) flag |= SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY; 113 if (i & 16) flag |= SCRIPT_VERIFY_CHECKSEQUENCEVERIFY; 114 if (i & 32) flag |= SCRIPT_VERIFY_WITNESS; 115 if (i & 64) flag |= SCRIPT_VERIFY_TAPROOT; 116 117 // SCRIPT_VERIFY_WITNESS requires SCRIPT_VERIFY_P2SH 118 if (flag & SCRIPT_VERIFY_WITNESS && !(flag & SCRIPT_VERIFY_P2SH)) continue; 119 // SCRIPT_VERIFY_TAPROOT requires SCRIPT_VERIFY_WITNESS 120 if (flag & SCRIPT_VERIFY_TAPROOT && !(flag & SCRIPT_VERIFY_WITNESS)) continue; 121 122 ret.push_back(flag); 123 } 124 125 return ret; 126 } 127 128 const std::vector<script_verify_flags> ALL_FLAGS = AllFlags(); 129 130 script_verify_flags ParseScriptFlags(const std::string& str) 131 { 132 if (str.empty()) return 0; 133 134 script_verify_flags flags = 0; 135 std::vector<std::string> words = SplitString(str, ','); 136 137 for (const std::string& word : words) { 138 auto it = FLAG_NAMES.find(word); 139 if (it == FLAG_NAMES.end()) throw std::runtime_error("Unknown verification flag " + word); 140 flags |= it->second; 141 } 142 143 return flags; 144 } 145 146 void Test(const std::string& str) 147 { 148 UniValue test; 149 if (!test.read(str) || !test.isObject()) throw std::runtime_error("Non-object test input"); 150 151 CMutableTransaction tx = TxFromHex(test["tx"].get_str()); 152 const std::vector<CTxOut> prevouts = TxOutsFromJSON(test["prevouts"]); 153 if (prevouts.size() != tx.vin.size()) throw std::runtime_error("Incorrect number of prevouts"); 154 size_t idx = test["index"].getInt<int64_t>(); 155 if (idx >= tx.vin.size()) throw std::runtime_error("Invalid index"); 156 script_verify_flags test_flags = ParseScriptFlags(test["flags"].get_str()); 157 bool final = test.exists("final") && test["final"].get_bool(); 158 159 if (test.exists("success")) { 160 tx.vin[idx].scriptSig = ScriptFromHex(test["success"]["scriptSig"].get_str()); 161 tx.vin[idx].scriptWitness = ScriptWitnessFromJSON(test["success"]["witness"]); 162 PrecomputedTransactionData txdata; 163 txdata.Init(tx, std::vector<CTxOut>(prevouts)); 164 MutableTransactionSignatureChecker txcheck(&tx, idx, prevouts[idx].nValue, txdata, MissingDataBehavior::ASSERT_FAIL); 165 for (const auto flags : ALL_FLAGS) { 166 // "final": true tests are valid for all flags. Others are only valid with flags that are 167 // a subset of test_flags. 168 if (final || ((flags & test_flags) == flags)) { 169 (void)VerifyScript(tx.vin[idx].scriptSig, prevouts[idx].scriptPubKey, &tx.vin[idx].scriptWitness, flags, txcheck, nullptr); 170 } 171 } 172 } 173 174 if (test.exists("failure")) { 175 tx.vin[idx].scriptSig = ScriptFromHex(test["failure"]["scriptSig"].get_str()); 176 tx.vin[idx].scriptWitness = ScriptWitnessFromJSON(test["failure"]["witness"]); 177 PrecomputedTransactionData txdata; 178 txdata.Init(tx, std::vector<CTxOut>(prevouts)); 179 MutableTransactionSignatureChecker txcheck(&tx, idx, prevouts[idx].nValue, txdata, MissingDataBehavior::ASSERT_FAIL); 180 for (const auto flags : ALL_FLAGS) { 181 // If a test is supposed to fail with test_flags, it should also fail with any superset thereof. 182 if ((flags & test_flags) == test_flags) { 183 (void)VerifyScript(tx.vin[idx].scriptSig, prevouts[idx].scriptPubKey, &tx.vin[idx].scriptWitness, flags, txcheck, nullptr); 184 } 185 } 186 } 187 } 188 189 void test_init() {} 190 191 FUZZ_TARGET(script_assets_test_minimizer, .init = test_init, .hidden = true) 192 { 193 if (buffer.size() < 2 || buffer.back() != '\n' || buffer[buffer.size() - 2] != ',') return; 194 const std::string str((const char*)buffer.data(), buffer.size() - 2); 195 try { 196 Test(str); 197 } catch (const std::runtime_error&) { 198 } 199 } 200 201 } // namespace