Skip to content

Commit cd5e10e

Browse files
darosiorajtowns
authored andcommitted
qa: add a fuzz target ensuring the sighash behaviour for non APO keys was conserved
This fuzz targets copied the SignatureHashSchnorr function for Bitcoin Core 23.0 and checks the output of the APO-ready SignatureHashSchnorr from this branch against it. This is to make sure the behaviour of the function was not changed for non ANYPREVOUT keys, which would make some previously valid signatures invalid and, even worse, some previously invalid signatures valid.
1 parent 63b2f8a commit cd5e10e

File tree

3 files changed

+192
-0
lines changed

3 files changed

+192
-0
lines changed

src/Makefile.test.include

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ test_fuzz_fuzz_SOURCES = \
289289
$(FUZZ_WALLET_SRC) \
290290
test/fuzz/addition_overflow.cpp \
291291
test/fuzz/addrman.cpp \
292+
test/fuzz/anyprevout.cpp \
292293
test/fuzz/asmap.cpp \
293294
test/fuzz/asmap_direct.cpp \
294295
test/fuzz/autofile.cpp \

src/script/interpreter.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1671,9 +1671,11 @@ bool SignatureHashSchnorr(uint256& hash_out, ScriptExecutionData& execdata, cons
16711671
ss << cache.m_spent_outputs[in_pos];
16721672
ss << tx_to.vin[in_pos].nSequence;
16731673
} else if (input_type == SIGHASH_ANYPREVOUT) {
1674+
assert(keyversion == KeyVersion::ANYPREVOUT);
16741675
ss << cache.m_spent_outputs[in_pos];
16751676
ss << tx_to.vin[in_pos].nSequence;
16761677
} else if (input_type == SIGHASH_ANYPREVOUTANYSCRIPT) {
1678+
assert(keyversion == KeyVersion::ANYPREVOUT);
16771679
ss << tx_to.vin[in_pos].nSequence;
16781680
} else {
16791681
ss << in_pos;
@@ -1698,6 +1700,8 @@ bool SignatureHashSchnorr(uint256& hash_out, ScriptExecutionData& execdata, cons
16981700
assert(execdata.m_tapleaf_hash_init);
16991701
if (input_type != SIGHASH_ANYPREVOUTANYSCRIPT) {
17001702
ss << execdata.m_tapleaf_hash;
1703+
} else {
1704+
assert(keyversion == KeyVersion::ANYPREVOUT);
17011705
}
17021706
ss << uint8_t(keyversion);
17031707
assert(execdata.m_codeseparator_pos_init);

src/test/fuzz/anyprevout.cpp

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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 <hash.h>
6+
#include <primitives/transaction.h>
7+
#include <script/interpreter.h>
8+
#include <test/fuzz/FuzzedDataProvider.h>
9+
#include <test/fuzz/fuzz.h>
10+
#include <test/fuzz/util.h>
11+
12+
#include <cstdint>
13+
#include <optional>
14+
#include <string>
15+
#include <vector>
16+
17+
/**
18+
* SignatureHashSchnorr from Bitcoin Core 23.0.
19+
*
20+
* Only change is using an assert directly instead of HandleMissingData.
21+
*/
22+
template<typename T>
23+
bool OldSignatureHashSchnorr(uint256& hash_out, ScriptExecutionData& execdata, const T& tx_to, uint32_t in_pos, uint8_t hash_type, SigVersion sigversion, const PrecomputedTransactionData& cache, MissingDataBehavior mdb)
24+
{
25+
uint8_t ext_flag, key_version;
26+
switch (sigversion) {
27+
case SigVersion::TAPROOT:
28+
ext_flag = 0;
29+
// key_version is not used and left uninitialized.
30+
break;
31+
case SigVersion::TAPSCRIPT:
32+
ext_flag = 1;
33+
// key_version must be 0 for now, representing the current version of
34+
// 32-byte public keys in the tapscript signature opcode execution.
35+
// An upgradable public key version (with a size not 32-byte) may
36+
// request a different key_version with a new sigversion.
37+
key_version = 0;
38+
break;
39+
default:
40+
assert(false);
41+
}
42+
assert(in_pos < tx_to.vin.size());
43+
assert(cache.m_bip341_taproot_ready && cache.m_spent_outputs_ready);
44+
45+
HashWriter ss = HASHER_TAPSIGHASH;
46+
47+
// Epoch
48+
static constexpr uint8_t EPOCH = 0;
49+
ss << EPOCH;
50+
51+
// Hash type
52+
const uint8_t output_type = (hash_type == SIGHASH_DEFAULT) ? SIGHASH_ALL : (hash_type & SIGHASH_OUTPUT_MASK); // Default (no sighash byte) is equivalent to SIGHASH_ALL
53+
const uint8_t input_type = hash_type & SIGHASH_INPUT_MASK;
54+
if (!(hash_type <= 0x03 || (hash_type >= 0x81 && hash_type <= 0x83))) return false;
55+
ss << hash_type;
56+
57+
// Transaction level data
58+
ss << tx_to.version;
59+
ss << tx_to.nLockTime;
60+
if (input_type != SIGHASH_ANYONECANPAY) {
61+
ss << cache.m_prevouts_single_hash;
62+
ss << cache.m_spent_amounts_single_hash;
63+
ss << cache.m_spent_scripts_single_hash;
64+
ss << cache.m_sequences_single_hash;
65+
}
66+
if (output_type == SIGHASH_ALL) {
67+
ss << cache.m_outputs_single_hash;
68+
}
69+
70+
// Data about the input/prevout being spent
71+
assert(execdata.m_annex_init);
72+
const bool have_annex = execdata.m_annex_present;
73+
const uint8_t spend_type = (ext_flag << 1) + (have_annex ? 1 : 0); // The low bit indicates whether an annex is present.
74+
ss << spend_type;
75+
if (input_type == SIGHASH_ANYONECANPAY) {
76+
ss << tx_to.vin[in_pos].prevout;
77+
ss << cache.m_spent_outputs[in_pos];
78+
ss << tx_to.vin[in_pos].nSequence;
79+
} else {
80+
ss << in_pos;
81+
}
82+
if (have_annex) {
83+
ss << execdata.m_annex_hash;
84+
}
85+
86+
// Data about the output (if only one).
87+
if (output_type == SIGHASH_SINGLE) {
88+
if (in_pos >= tx_to.vout.size()) return false;
89+
if (!execdata.m_output_hash) {
90+
HashWriter sha_single_output{};
91+
sha_single_output << tx_to.vout[in_pos];
92+
execdata.m_output_hash = sha_single_output.GetSHA256();
93+
}
94+
ss << execdata.m_output_hash.value();
95+
}
96+
97+
// Additional data for BIP 342 signatures
98+
if (sigversion == SigVersion::TAPSCRIPT) {
99+
assert(execdata.m_tapleaf_hash_init);
100+
ss << execdata.m_tapleaf_hash;
101+
ss << key_version;
102+
assert(execdata.m_codeseparator_pos_init);
103+
ss << execdata.m_codeseparator_pos;
104+
}
105+
106+
hash_out = ss.GetSHA256();
107+
return true;
108+
}
109+
110+
/** Test APO's SignatureHashSchnorr against Bitcoin 23.0's SignatureHashSchnorr.
111+
*
112+
* This makes sure the behaviour for non-APO keys was conserved with the introduction of the APO logic.
113+
*/
114+
FUZZ_TARGET(anyprevout_old_new_sighash)
115+
{
116+
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
117+
{
118+
// Get a transaction and pick the input we'll generate the sighash for from the fuzzer output.
119+
std::optional<CMutableTransaction> mtx = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS);
120+
if (!mtx) return;
121+
const uint32_t in_pos = fuzzed_data_provider.ConsumeIntegral<uint32_t>();
122+
if (in_pos >= mtx->vin.size()) return;
123+
124+
// Make sure at least one of the transaction's inputs has a non-empty witness, as cache.Init()
125+
// below needs it. Then take a script code (we don't need it to be valid), a signature version
126+
// (which must be anything but APO), and a hash type to use from the fuzzer's output.
127+
if (mtx->vin[0].scriptWitness.IsNull()) mtx->vin[0].scriptWitness.stack.push_back({0});
128+
const CTransaction tx_to{*mtx};
129+
const CScript script_code = ConsumeScript(fuzzed_data_provider);
130+
const SigVersion sig_version{fuzzed_data_provider.PickValueInArray({SigVersion::TAPROOT, SigVersion::TAPSCRIPT})};
131+
const uint8_t hash_type{fuzzed_data_provider.ConsumeIntegral<uint8_t>()};
132+
133+
// Populate the transaction cache with the transaction and dummy spent outputs. The scriptPubkKey
134+
// must match the Taproot template for Init() to populate the right fields.
135+
PrecomputedTransactionData cache;
136+
std::vector<CTxOut> spent_outputs;
137+
spent_outputs.reserve(tx_to.vin.size());
138+
for (unsigned i = 0; i < tx_to.vin.size(); ++i) {
139+
CScript script_pubkey;
140+
script_pubkey << OP_1 << fuzzed_data_provider.ConsumeBytes<uint8_t>(WITNESS_V1_TAPROOT_SIZE);
141+
if (script_pubkey.size() != 2 + WITNESS_V1_TAPROOT_SIZE) return;
142+
const CAmount value{fuzzed_data_provider.ConsumeIntegral<int64_t>()};
143+
spent_outputs.emplace_back(value, std::move(script_pubkey));
144+
}
145+
cache.Init(tx_to, std::move(spent_outputs));
146+
assert(cache.m_spent_outputs_ready);
147+
assert(cache.m_bip341_taproot_ready);
148+
149+
// Now populate the Script execution data. Annex needs always be populated and tapleaf hash
150+
// is only need for Script-path spends.
151+
ScriptExecutionData exec_data;
152+
exec_data.m_annex_present = false;
153+
if (fuzzed_data_provider.ConsumeBool()) {
154+
if (const auto annex_hash = ConsumeDeserializable<uint256>(fuzzed_data_provider)) {
155+
exec_data.m_annex_hash = *annex_hash;
156+
exec_data.m_annex_present = true;
157+
}
158+
}
159+
exec_data.m_annex_init = true;
160+
if (sig_version == SigVersion::TAPSCRIPT) {
161+
if (const auto tapleaf_hash = ConsumeDeserializable<uint256>(fuzzed_data_provider)) {
162+
exec_data.m_tapleaf_hash = *tapleaf_hash;
163+
exec_data.m_tapleaf_hash_init = true;
164+
} else {
165+
return;
166+
}
167+
exec_data.m_codeseparator_pos = fuzzed_data_provider.ConsumeIntegral<uint32_t>();
168+
exec_data.m_codeseparator_pos_init = true;
169+
}
170+
171+
// Assert the existing behaviour was conserved with the introduction of the APO logic for
172+
// the fully pseudo-random hashtype.
173+
uint256 old_sighash, new_sighash;
174+
const auto new_res = SignatureHashSchnorr(new_sighash, exec_data, tx_to, in_pos, hash_type, sig_version,
175+
KeyVersion::TAPROOT, cache, MissingDataBehavior::ASSERT_FAIL);
176+
const auto old_res = OldSignatureHashSchnorr(old_sighash, exec_data, tx_to, in_pos, hash_type, sig_version,
177+
cache, MissingDataBehavior::ASSERT_FAIL);
178+
assert(new_res == old_res);
179+
assert(new_sighash == old_sighash);
180+
const auto new_res_h = SignatureHashSchnorr(new_sighash, exec_data, tx_to, in_pos, hash_type, sig_version,
181+
KeyVersion::TAPROOT, cache, MissingDataBehavior::ASSERT_FAIL);
182+
const auto old_res_h = OldSignatureHashSchnorr(old_sighash, exec_data, tx_to, in_pos, hash_type, sig_version,
183+
cache, MissingDataBehavior::ASSERT_FAIL);
184+
assert(new_res_h == old_res_h);
185+
assert(new_sighash == old_sighash);
186+
}
187+
}

0 commit comments

Comments
 (0)