script_assets_test_minimizer.cpp raw

   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
 202