Skip to content

Commit b0e4a71

Browse files
committed
fuzz: add two fuzz targets for OP_TEMPLATEHASH
We introduce one specialized target focused on exercising the new `GetTemplateHash()` logic introduced for OP_TEMPLATEHASH, and one broader fuzz target which exercises using OP_TEMPLATEHASH on a variety of transactions while asserting invariants.
1 parent 878ea82 commit b0e4a71

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

src/test/fuzz/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ add_executable(fuzz
117117
string.cpp
118118
strprintf.cpp
119119
system.cpp
120+
templatehash.cpp
120121
timeoffsets.cpp
121122
torcontrol.cpp
122123
transaction.cpp

src/test/fuzz/templatehash.cpp

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Copyright (c) 2009-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 <addresstype.h>
6+
#include <consensus/amount.h>
7+
#include <policy/policy.h>
8+
#include <primitives/transaction.h>
9+
#include <script/interpreter.h>
10+
#include <script/script.h>
11+
#include <script/signingprovider.h>
12+
#include <test/fuzz/FuzzedDataProvider.h>
13+
#include <test/fuzz/fuzz.h>
14+
#include <test/fuzz/util.h>
15+
#include <test/util/script.h>
16+
17+
//! Maximum number of inputs and outputs in transactions consumed from fuzzer.
18+
static constexpr int MAX_TX_IN_OUT{10'000};
19+
//! Verification flags to set for script validation.
20+
static constexpr unsigned VERIFY_FLAGS{MANDATORY_SCRIPT_VERIFY_FLAGS | SCRIPT_VERIFY_TEMPLATEHASH};
21+
22+
static bool VerifyTemplateCheck(const CMutableTransaction& tx, unsigned int in_index,
23+
std::vector<CTxOut> spent_outputs, const CScript& spent_spk)
24+
{
25+
constexpr auto mdb{MissingDataBehavior::ASSERT_FAIL};
26+
constexpr CAmount dummy_am{0}; // We never check signatures.
27+
PrecomputedTransactionData precomp;
28+
precomp.Init(tx, std::move(spent_outputs));
29+
const auto checker{GenericTransactionSignatureChecker(&tx, in_index, dummy_am, precomp, mdb)};
30+
return VerifyScript(tx.vin[in_index].scriptSig, spent_spk, &tx.vin[in_index].scriptWitness, VERIFY_FLAGS, checker);
31+
}
32+
33+
/** Target specialized on the new logic introduced for OP_TEMPLATEHASH. */
34+
FUZZ_TARGET(gettemplatehash)
35+
{
36+
FuzzedDataProvider provider(buffer.data(), buffer.size());
37+
38+
// First get the transaction for which to generate the template hash. It's unnecessary to
39+
// get the fuzzer to try to find a valid deserialization, as only the version+locktime is
40+
// used by GetTemplateHash().
41+
CMutableTransaction tx;
42+
tx.version = provider.ConsumeIntegral<uint32_t>();
43+
tx.nLockTime = provider.ConsumeIntegral<uint32_t>();
44+
45+
// Manually set the precomputed values used in the template hash.
46+
PrecomputedTransactionData precomp{tx};
47+
precomp.m_sequences_single_hash = ConsumeUInt256(provider);
48+
precomp.m_outputs_single_hash = ConsumeUInt256(provider);
49+
precomp.m_bip341_taproot_ready = true;
50+
51+
// Sometimes commit to the annex too.
52+
ScriptExecutionData execdata;
53+
execdata.m_annex_present = provider.ConsumeBool();
54+
if (execdata.m_annex_present) {
55+
execdata.m_annex_hash = ConsumeUInt256(provider);
56+
}
57+
execdata.m_annex_init = true;
58+
59+
// Finally, exercise the GetTemplateHash() function.
60+
const auto in_index{provider.ConsumeIntegral<unsigned>()};
61+
const CAmount dummy_am{0};
62+
const auto dummy_mdb{MissingDataBehavior::ASSERT_FAIL};
63+
const auto checker{GenericTransactionSignatureChecker(&tx, in_index, dummy_am, precomp, dummy_mdb)};
64+
(void)checker.GetTemplateHash(execdata);
65+
}
66+
67+
/** Broader target which exercises the commit-to-spending transaction use case of OP_TEMPLATEHASH for
68+
* various fuzzer-provided transactions and asserts invariants. */
69+
FUZZ_TARGET(spendtemplatehash)
70+
{
71+
FuzzedDataProvider provider(buffer.data(), buffer.size());
72+
73+
// Compute the template hash for a fuzzer-provided transaction and input index.
74+
std::vector<uint8_t> annex;
75+
auto tx{ConsumeTransaction(provider, {}, MAX_TX_IN_OUT, MAX_TX_IN_OUT)};
76+
if (tx.vin.empty()) return;
77+
const auto in_index{provider.ConsumeIntegralInRange<unsigned int>(0, tx.vin.size() - 1)};
78+
const auto template_hash{GetTemplateHash(tx, in_index, &annex)};
79+
80+
// Construct the output script committing to this template hash.
81+
const auto leaf_script{CScript() << template_hash << OP_TEMPLATEHASH << OP_EQUAL};
82+
TaprootBuilder builder;
83+
builder.Add(0, leaf_script, TAPROOT_LEAF_TAPSCRIPT);
84+
builder.Finalize(XOnlyPubKey::NUMS_H);
85+
const CScript spent_spk{GetScriptForDestination(builder.GetOutput())};
86+
87+
// Set the witness for this transaction input.
88+
const auto spend_data{builder.GetSpendData()};
89+
const auto control_blocks{spend_data.scripts.begin()->second};
90+
const auto& cb{*control_blocks.begin()};
91+
tx.vin[in_index].scriptWitness.stack.clear();
92+
tx.vin[in_index].scriptWitness.stack.emplace_back(leaf_script.begin(), leaf_script.end());
93+
tx.vin[in_index].scriptWitness.stack.emplace_back(cb.begin(), cb.end());
94+
if (!annex.empty()) {
95+
tx.vin[in_index].scriptWitness.stack.push_back(std::move(annex)); // TODO: a good way to test the target is to comment out this line
96+
}
97+
98+
// Get the vector of spent outputs from the fuzzer. No spent output are taken into account in
99+
// computing the template hash. Therefore the specific values of the other spent outputs can
100+
// be set freely. However the spent output referred to by the input being verified must be
101+
// correctly set to a Taproot scriptpubkey for bip341 subfields to be precomputed.
102+
std::vector<CTxOut> spent_outputs(tx.vin.size());
103+
for (unsigned i{0}; i < spent_outputs.size(); ++i) {
104+
spent_outputs[i].scriptPubKey = i == in_index ? spent_spk : ConsumeScript(provider);
105+
spent_outputs[i].nValue = ConsumeMoney(provider);
106+
}
107+
108+
// Run script validation for this transaction input. It must pass.
109+
tx.vin[in_index].scriptSig.clear(); // witness requires empty scriptSig
110+
Assert(VerifyTemplateCheck(tx, in_index, std::vector<CTxOut>(spent_outputs), spent_spk));
111+
112+
// Malleate a field of the spending transaction and assert whether it invalidates the spend.
113+
CallOneOf(
114+
provider,
115+
// Changing version will invalidate template hash.
116+
[&] {
117+
const auto prev_version{tx.version};
118+
tx.version = provider.ConsumeIntegral<uint32_t>();
119+
const bool version_changed{tx.version != prev_version};
120+
Assert(VerifyTemplateCheck(tx, in_index, std::move(spent_outputs), spent_spk) == !version_changed);
121+
},
122+
// Changing locktime will invalidate template hash.
123+
[&] {
124+
const auto prev_locktime{tx.nLockTime};
125+
tx.nLockTime = provider.ConsumeIntegral<uint32_t>();
126+
const bool locktime_changed{tx.nLockTime != prev_locktime};
127+
Assert(VerifyTemplateCheck(tx, in_index, std::move(spent_outputs), spent_spk) == !locktime_changed);
128+
},
129+
// Changing the sequence of any input will invalidate template hash.
130+
[&] {
131+
const auto i{provider.ConsumeIntegralInRange<size_t>(0, tx.vin.size() - 1)};
132+
const auto prev_sequence{tx.vin[i].nSequence};
133+
tx.vin[i].nSequence = provider.ConsumeIntegral<uint32_t>();
134+
const bool seq_changed{tx.vin[i].nSequence != prev_sequence};
135+
Assert(VerifyTemplateCheck(tx, in_index, std::move(spent_outputs), spent_spk) == !seq_changed);
136+
},
137+
// Changing the prevout of any input will not invalidate template hash.
138+
[&] {
139+
const auto i{provider.ConsumeIntegralInRange<size_t>(0, tx.vin.size() - 1)};
140+
tx.vin[i].prevout.hash = Txid::FromUint256(ConsumeUInt256(provider));
141+
tx.vin[i].prevout.n = provider.ConsumeIntegral<uint32_t>();
142+
Assert(VerifyTemplateCheck(tx, in_index, std::move(spent_outputs), spent_spk));
143+
},
144+
// Changing the scriptSig of an input will only make Script validation fail if it malleates
145+
// that of the input being validated (because Segwit mandates empty scriptSig).
146+
[&] {
147+
const auto i{provider.ConsumeIntegralInRange<size_t>(0, tx.vin.size() - 1)};
148+
tx.vin[i].scriptSig = ConsumeScript(provider);
149+
Assert(VerifyTemplateCheck(tx, in_index, std::move(spent_outputs), spent_spk) == tx.vin[in_index].scriptSig.empty());
150+
},
151+
// Changing the annex of the spending input will invalidate template hash. Changing the
152+
// witness of any other input will not invalidate template hash.
153+
[&] {
154+
const auto i{provider.ConsumeIntegralInRange<size_t>(0, tx.vin.size() - 1)};
155+
if (i == in_index) {
156+
// Don't necessarily create a well-formatted annex.
157+
auto new_annex{ConsumeRandomLengthByteVector(provider)};
158+
const bool expect_failure{annex.empty() || annex != new_annex};
159+
if (annex.empty()) {
160+
tx.vin[i].scriptWitness.stack.emplace_back();
161+
}
162+
tx.vin[i].scriptWitness.stack.back() = std::move(new_annex);
163+
Assert(VerifyTemplateCheck(tx, in_index, std::move(spent_outputs), spent_spk) == !expect_failure);
164+
} else {
165+
tx.vin[i].scriptWitness = ConsumeScriptWitness(provider);
166+
Assert(VerifyTemplateCheck(tx, in_index, std::move(spent_outputs), spent_spk));
167+
}
168+
},
169+
// Changing the value of any output will invalidate template hash.
170+
[&] {
171+
if (tx.vout.empty()) return;
172+
const auto i{provider.ConsumeIntegralInRange<size_t>(0, tx.vout.size() - 1)};
173+
const auto prev_value{tx.vout[i].nValue};
174+
tx.vout[i].nValue = provider.ConsumeIntegral<CAmount>();
175+
const bool value_changed{tx.vout[i].nValue != prev_value};
176+
Assert(VerifyTemplateCheck(tx, in_index, std::move(spent_outputs), spent_spk) == !value_changed);
177+
},
178+
// Changing the scriptPubKey of any output will invalidate template hash.
179+
[&] {
180+
if (tx.vout.empty()) return;
181+
const auto i{provider.ConsumeIntegralInRange<size_t>(0, tx.vout.size() - 1)};
182+
const auto prev_spk{tx.vout[i].scriptPubKey};
183+
tx.vout[i].scriptPubKey = ConsumeScript(provider);
184+
const bool spk_changed{tx.vout[i].scriptPubKey != prev_spk};
185+
Assert(VerifyTemplateCheck(tx, in_index, std::move(spent_outputs), spent_spk) == !spk_changed);
186+
}
187+
// TODO: check that adding/removing inputs/outputs invalidates template hash.
188+
);
189+
}

0 commit comments

Comments
 (0)