Skip to content

Commit 0e2a5e4

Browse files
committed
tests: dumping and minimizing of script assets data
This adds a --dumptests flag to the feature_taproot.py test, to dump all its generated test cases to files, in a format compatible with the script_assets_test unit test. A fuzzer for said format is added as well, whose primary purpose is coverage-based minimization of those dumps.
1 parent 4567ba0 commit 0e2a5e4

File tree

4 files changed

+258
-1
lines changed

4 files changed

+258
-1
lines changed

src/Makefile.test.include

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ FUZZ_TARGETS = \
129129
test/fuzz/script_deserialize \
130130
test/fuzz/script_flags \
131131
test/fuzz/script_interpreter \
132+
test/fuzz/script_assets_test_minimizer \
132133
test/fuzz/script_ops \
133134
test/fuzz/script_sigcache \
134135
test/fuzz/script_sign \
@@ -1082,6 +1083,12 @@ test_fuzz_script_interpreter_LDADD = $(FUZZ_SUITE_LD_COMMON)
10821083
test_fuzz_script_interpreter_LDFLAGS = $(FUZZ_SUITE_LDFLAGS_COMMON)
10831084
test_fuzz_script_interpreter_SOURCES = test/fuzz/script_interpreter.cpp
10841085

1086+
test_fuzz_script_assets_test_minimizer_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES)
1087+
test_fuzz_script_assets_test_minimizer_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
1088+
test_fuzz_script_assets_test_minimizer_LDADD = $(FUZZ_SUITE_LD_COMMON)
1089+
test_fuzz_script_assets_test_minimizer_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS)
1090+
test_fuzz_script_assets_test_minimizer_SOURCES = test/fuzz/script_assets_test_minimizer.cpp
1091+
10851092
test_fuzz_script_ops_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES)
10861093
test_fuzz_script_ops_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
10871094
test_fuzz_script_ops_LDADD = $(FUZZ_SUITE_LD_COMMON)
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright (c) 2020 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+
15+
#include <boost/algorithm/string.hpp>
16+
#include <cstdint>
17+
#include <string>
18+
#include <vector>
19+
20+
// This fuzz "test" can be used to minimize test cases for script_assets_test in
21+
// src/test/script_tests.cpp. While it written as a fuzz test, and can be used as such,
22+
// fuzzing the inputs is unlikely to construct useful test cases.
23+
//
24+
// Instead, it is primarily intended to be run on a test set that was generated
25+
// externally, for example using test/functional/feature_taproot.py's --dumptests mode.
26+
// The minimized set can then be concatenated together, surrounded by '[' and ']',
27+
// and used as the script_assets_test.json input to the script_assets_test unit test:
28+
//
29+
// (normal build)
30+
// $ mkdir dump
31+
// $ for N in $(seq 1 10); do TEST_DUMP_DIR=dump test/functional/feature_taproot --dumptests; done
32+
// $ ...
33+
//
34+
// (fuzz test build)
35+
// $ mkdir dump-min
36+
// $ ./src/test/fuzz/script_assets_test_minimizer -merge=1 dump-min/ dump/
37+
// $ (echo -en '[\n'; cat dump-min/* | head -c -2; echo -en '\n]') >script_assets_test.json
38+
39+
namespace {
40+
41+
std::vector<unsigned char> CheckedParseHex(const std::string& str)
42+
{
43+
if (str.size() && !IsHex(str)) throw std::runtime_error("Non-hex input '" + str + "'");
44+
return ParseHex(str);
45+
}
46+
47+
CScript ScriptFromHex(const std::string& str)
48+
{
49+
std::vector<unsigned char> data = CheckedParseHex(str);
50+
return CScript(data.begin(), data.end());
51+
}
52+
53+
CMutableTransaction TxFromHex(const std::string& str)
54+
{
55+
CMutableTransaction tx;
56+
try {
57+
VectorReader(SER_DISK, SERIALIZE_TRANSACTION_NO_WITNESS, CheckedParseHex(str), 0) >> tx;
58+
} catch (const std::ios_base::failure&) {
59+
throw std::runtime_error("Tx deserialization failure");
60+
}
61+
return tx;
62+
}
63+
64+
std::vector<CTxOut> TxOutsFromJSON(const UniValue& univalue)
65+
{
66+
if (!univalue.isArray()) throw std::runtime_error("Prevouts must be array");
67+
std::vector<CTxOut> prevouts;
68+
for (size_t i = 0; i < univalue.size(); ++i) {
69+
CTxOut txout;
70+
try {
71+
VectorReader(SER_DISK, 0, CheckedParseHex(univalue[i].get_str()), 0) >> txout;
72+
} catch (const std::ios_base::failure&) {
73+
throw std::runtime_error("Prevout invalid format");
74+
}
75+
prevouts.push_back(std::move(txout));
76+
}
77+
return prevouts;
78+
}
79+
80+
CScriptWitness ScriptWitnessFromJSON(const UniValue& univalue)
81+
{
82+
if (!univalue.isArray()) throw std::runtime_error("Script witness is not array");
83+
CScriptWitness scriptwitness;
84+
for (size_t i = 0; i < univalue.size(); ++i) {
85+
auto bytes = CheckedParseHex(univalue[i].get_str());
86+
scriptwitness.stack.push_back(std::move(bytes));
87+
}
88+
return scriptwitness;
89+
}
90+
91+
const std::map<std::string, unsigned int> FLAG_NAMES = {
92+
{std::string("P2SH"), (unsigned int)SCRIPT_VERIFY_P2SH},
93+
{std::string("DERSIG"), (unsigned int)SCRIPT_VERIFY_DERSIG},
94+
{std::string("NULLDUMMY"), (unsigned int)SCRIPT_VERIFY_NULLDUMMY},
95+
{std::string("CHECKLOCKTIMEVERIFY"), (unsigned int)SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY},
96+
{std::string("CHECKSEQUENCEVERIFY"), (unsigned int)SCRIPT_VERIFY_CHECKSEQUENCEVERIFY},
97+
{std::string("WITNESS"), (unsigned int)SCRIPT_VERIFY_WITNESS},
98+
{std::string("TAPROOT"), (unsigned int)SCRIPT_VERIFY_TAPROOT},
99+
};
100+
101+
std::vector<unsigned int> AllFlags()
102+
{
103+
std::vector<unsigned int> ret;
104+
105+
for (unsigned int i = 0; i < 128; ++i) {
106+
unsigned int flag = 0;
107+
if (i & 1) flag |= SCRIPT_VERIFY_P2SH;
108+
if (i & 2) flag |= SCRIPT_VERIFY_DERSIG;
109+
if (i & 4) flag |= SCRIPT_VERIFY_NULLDUMMY;
110+
if (i & 8) flag |= SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY;
111+
if (i & 16) flag |= SCRIPT_VERIFY_CHECKSEQUENCEVERIFY;
112+
if (i & 32) flag |= SCRIPT_VERIFY_WITNESS;
113+
if (i & 64) flag |= SCRIPT_VERIFY_TAPROOT;
114+
115+
// SCRIPT_VERIFY_WITNESS requires SCRIPT_VERIFY_P2SH
116+
if (flag & SCRIPT_VERIFY_WITNESS && !(flag & SCRIPT_VERIFY_P2SH)) continue;
117+
// SCRIPT_VERIFY_TAPROOT requires SCRIPT_VERIFY_WITNESS
118+
if (flag & SCRIPT_VERIFY_TAPROOT && !(flag & SCRIPT_VERIFY_WITNESS)) continue;
119+
120+
ret.push_back(flag);
121+
}
122+
123+
return ret;
124+
}
125+
126+
const std::vector<unsigned int> ALL_FLAGS = AllFlags();
127+
128+
unsigned int ParseScriptFlags(const std::string& str)
129+
{
130+
if (str.empty()) return 0;
131+
132+
unsigned int flags = 0;
133+
std::vector<std::string> words;
134+
boost::algorithm::split(words, str, boost::algorithm::is_any_of(","));
135+
136+
for (const std::string& word : words)
137+
{
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"].get_int64();
155+
if (idx >= tx.vin.size()) throw std::runtime_error("Invalid index");
156+
unsigned int 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);
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);
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+
ECCVerifyHandle handle;
190+
191+
}
192+
193+
void test_one_input(const std::vector<uint8_t>& buffer)
194+
{
195+
if (buffer.size() < 2 || buffer.back() != '\n' || buffer[buffer.size() - 2] != ',') return;
196+
const std::string str((const char*)buffer.data(), buffer.size() - 2);
197+
try {
198+
Test(str);
199+
} catch (const std::runtime_error&) {}
200+
}

src/test/script_tests.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,6 +1715,9 @@ static void AssetTest(const UniValue& test)
17151715

17161716
BOOST_AUTO_TEST_CASE(script_assets_test)
17171717
{
1718+
// See src/test/fuzz/script_assets_test_minimizer.cpp for information on how to generate
1719+
// the script_assets_test.json file used by this test.
1720+
17181721
const char* dir = std::getenv("DIR_UNIT_TEST_DATA");
17191722
BOOST_WARN_MESSAGE(dir != nullptr, "Variable DIR_UNIT_TEST_DATA unset, skipping script_assets_test");
17201723
if (dir == nullptr) return;

test/functional/feature_taproot.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,11 @@
8282
hash160,
8383
sha256,
8484
)
85-
from collections import namedtuple
85+
from collections import OrderedDict, namedtuple
8686
from io import BytesIO
87+
import json
88+
import hashlib
89+
import os
8790
import random
8891

8992
# === Framework for building spending transactions. ===
@@ -1142,10 +1145,52 @@ def spenders_taproot_inactive():
11421145

11431146
return spenders
11441147

1148+
# Consensus validation flags to use in dumps for tests with "legacy/" or "inactive/" prefix.
1149+
LEGACY_FLAGS = "P2SH,DERSIG,CHECKLOCKTIMEVERIFY,CHECKSEQUENCEVERIFY,WITNESS,NULLDUMMY"
1150+
# Consensus validation flags to use in dumps for all other tests.
1151+
TAPROOT_FLAGS = "P2SH,DERSIG,CHECKLOCKTIMEVERIFY,CHECKSEQUENCEVERIFY,WITNESS,NULLDUMMY,TAPROOT"
1152+
1153+
def dump_json_test(tx, input_utxos, idx, success, failure):
1154+
spender = input_utxos[idx].spender
1155+
# Determine flags to dump
1156+
flags = LEGACY_FLAGS if spender.comment.startswith("legacy/") or spender.comment.startswith("inactive/") else TAPROOT_FLAGS
1157+
1158+
fields = [
1159+
("tx", tx.serialize().hex()),
1160+
("prevouts", [x.output.serialize().hex() for x in input_utxos]),
1161+
("index", idx),
1162+
("flags", flags),
1163+
("comment", spender.comment)
1164+
]
1165+
1166+
# The "final" field indicates that a spend should be always valid, even with more validation flags enabled
1167+
# than the listed ones. Use standardness as a proxy for this (which gives a conservative underestimate).
1168+
if spender.is_standard:
1169+
fields.append(("final", True))
1170+
1171+
def dump_witness(wit):
1172+
return OrderedDict([("scriptSig", wit[0].hex()), ("witness", [x.hex() for x in wit[1]])])
1173+
if success is not None:
1174+
fields.append(("success", dump_witness(success)))
1175+
if failure is not None:
1176+
fields.append(("failure", dump_witness(failure)))
1177+
1178+
# Write the dump to $TEST_DUMP_DIR/x/xyz... where x,y,z,... are the SHA1 sum of the dump (which makes the
1179+
# file naming scheme compatible with fuzzing infrastructure).
1180+
dump = json.dumps(OrderedDict(fields)) + ",\n"
1181+
sha1 = hashlib.sha1(dump.encode("utf-8")).hexdigest()
1182+
dirname = os.environ.get("TEST_DUMP_DIR", ".") + ("/%s" % sha1[0])
1183+
os.makedirs(dirname, exist_ok=True)
1184+
with open(dirname + ("/%s" % sha1), 'w', encoding="utf8") as f:
1185+
f.write(dump)
1186+
11451187
# Data type to keep track of UTXOs, where they were created, and how to spend them.
11461188
UTXOData = namedtuple('UTXOData', 'outpoint,output,spender')
11471189

11481190
class TaprootTest(BitcoinTestFramework):
1191+
def add_options(self, parser):
1192+
parser.add_argument("--dumptests", dest="dump_tests", default=False, action="store_true",
1193+
help="Dump generated test cases to directory set by TEST_DUMP_DIR environment variable")
11491194

11501195
def skip_test_if_missing_module(self):
11511196
self.skip_if_no_wallet()
@@ -1356,6 +1401,8 @@ def test_spenders(self, node, spenders, input_counts):
13561401
if not input_utxos[i].spender.no_fail:
13571402
fail = fn(tx, i, [utxo.output for utxo in input_utxos], False)
13581403
input_data.append((fail, success))
1404+
if self.options.dump_tests:
1405+
dump_json_test(tx, input_utxos, i, success, fail)
13591406

13601407
# Sign each input incorrectly once on each complete signing pass, except the very last.
13611408
for fail_input in list(range(len(input_utxos))) + [None]:

0 commit comments

Comments
 (0)