Skip to content

Commit a27430e

Browse files
committed
Merge bitcoin/bitcoin#32473: Introduce per-txin sighash midstate cache for legacy/p2sh/segwitv0 scripts
8395027 qa: unit test sighash caching (Antoine Poinsot) b221aa8 qa: simple differential fuzzing for sighash with/without caching (Antoine Poinsot) 92af9f7 script: (optimization) introduce sighash midstate caching (Pieter Wuille) 8f3ddb0 script: (refactor) prepare for introducing sighash midstate cache (Pieter Wuille) 9014d40 tests: add sighash caching tests to feature_taproot (Pieter Wuille) Pull request description: This introduces a per-txin cache for sighash midstate computation to the script interpreter for legacy (bare), P2SH, P2WSH, and (as collateral effect, but not actually useful) P2WPKH. This reduces the impact of certain types of quadratic hashing attacks that use standard transactions. It is not known to improve the situation for attacks involving non-standard transaction attacks. The cache works by remembering for each of the 6 sighash modes a `(scriptCode, midstate)` tuple, which gives a midstate `CSHA256` object right before the appending of the sighash type itself (to permit all 256, rather than just the 6 ones that match the modes). The midstate is only reused if the `scriptCode` matches. This works because - within a single input - only the sighash type and the `scriptCode` affect the actual sighash used. The PR implements two different approaches: * The initial commits introduce the caching effect always, for both consensus and relay relation validation. Despite being primarily intended for improving the situation for standard transactions only, I chose this approach as the code paths are already largely common between the two, and this approach I believe involves fewer code changes than a more targetted approach, and furthermore, it should not hurt (it may even help common multisig cases slightly). * The final commit changes the behavior to only using the cache for non-consensus script validation. I'm open to feedback about whether adding this commit is worth it. Functional tests are included that construct contrived cases with many sighash types (standard and non-standard ones) and `OP_CODESEPARATOR`s in all script types (including P2TR, which isn't modified by this PR). ACKs for top commit: achow101: ACK 8395027 dergoegge: Code review ACK 8395027 darosior: re-ACK 8395027 Tree-SHA512: 65ae8635429a4d563b19969bac8128038ac2cbe01d9c9946abd4cac3c0780974d1e8b9aae9bb83f414e5d247a59f4a18fef5b37d93ad59ed41b6f11c3fe05af4
2 parents 34b366f + 8395027 commit a27430e

File tree

5 files changed

+290
-28
lines changed

5 files changed

+290
-28
lines changed

src/script/interpreter.cpp

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,11 +1564,57 @@ bool SignatureHashSchnorr(uint256& hash_out, ScriptExecutionData& execdata, cons
15641564
return true;
15651565
}
15661566

1567+
int SigHashCache::CacheIndex(int32_t hash_type) const noexcept
1568+
{
1569+
// Note that we do not distinguish between BASE and WITNESS_V0 to determine the cache index,
1570+
// because no input can simultaneously use both.
1571+
return 3 * !!(hash_type & SIGHASH_ANYONECANPAY) +
1572+
2 * ((hash_type & 0x1f) == SIGHASH_SINGLE) +
1573+
1 * ((hash_type & 0x1f) == SIGHASH_NONE);
1574+
}
1575+
1576+
bool SigHashCache::Load(int32_t hash_type, const CScript& script_code, HashWriter& writer) const noexcept
1577+
{
1578+
auto& entry = m_cache_entries[CacheIndex(hash_type)];
1579+
if (entry.has_value()) {
1580+
if (script_code == entry->first) {
1581+
writer = HashWriter(entry->second);
1582+
return true;
1583+
}
1584+
}
1585+
return false;
1586+
}
1587+
1588+
void SigHashCache::Store(int32_t hash_type, const CScript& script_code, const HashWriter& writer) noexcept
1589+
{
1590+
auto& entry = m_cache_entries[CacheIndex(hash_type)];
1591+
entry.emplace(script_code, writer);
1592+
}
1593+
15671594
template <class T>
1568-
uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn, int32_t nHashType, const CAmount& amount, SigVersion sigversion, const PrecomputedTransactionData* cache)
1595+
uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn, int32_t nHashType, const CAmount& amount, SigVersion sigversion, const PrecomputedTransactionData* cache, SigHashCache* sighash_cache)
15691596
{
15701597
assert(nIn < txTo.vin.size());
15711598

1599+
if (sigversion != SigVersion::WITNESS_V0) {
1600+
// Check for invalid use of SIGHASH_SINGLE
1601+
if ((nHashType & 0x1f) == SIGHASH_SINGLE) {
1602+
if (nIn >= txTo.vout.size()) {
1603+
// nOut out of range
1604+
return uint256::ONE;
1605+
}
1606+
}
1607+
}
1608+
1609+
HashWriter ss{};
1610+
1611+
// Try to compute using cached SHA256 midstate.
1612+
if (sighash_cache && sighash_cache->Load(nHashType, scriptCode, ss)) {
1613+
// Add sighash type and hash.
1614+
ss << nHashType;
1615+
return ss.GetHash();
1616+
}
1617+
15721618
if (sigversion == SigVersion::WITNESS_V0) {
15731619
uint256 hashPrevouts;
15741620
uint256 hashSequence;
@@ -1583,16 +1629,14 @@ uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn
15831629
hashSequence = cacheready ? cache->hashSequence : SHA256Uint256(GetSequencesSHA256(txTo));
15841630
}
15851631

1586-
15871632
if ((nHashType & 0x1f) != SIGHASH_SINGLE && (nHashType & 0x1f) != SIGHASH_NONE) {
15881633
hashOutputs = cacheready ? cache->hashOutputs : SHA256Uint256(GetOutputsSHA256(txTo));
15891634
} else if ((nHashType & 0x1f) == SIGHASH_SINGLE && nIn < txTo.vout.size()) {
1590-
HashWriter ss{};
1591-
ss << txTo.vout[nIn];
1592-
hashOutputs = ss.GetHash();
1635+
HashWriter inner_ss{};
1636+
inner_ss << txTo.vout[nIn];
1637+
hashOutputs = inner_ss.GetHash();
15931638
}
15941639

1595-
HashWriter ss{};
15961640
// Version
15971641
ss << txTo.version;
15981642
// Input prevouts/nSequence (none/all, depending on flags)
@@ -1609,26 +1653,21 @@ uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn
16091653
ss << hashOutputs;
16101654
// Locktime
16111655
ss << txTo.nLockTime;
1612-
// Sighash type
1613-
ss << nHashType;
1656+
} else {
1657+
// Wrapper to serialize only the necessary parts of the transaction being signed
1658+
CTransactionSignatureSerializer<T> txTmp(txTo, scriptCode, nIn, nHashType);
16141659

1615-
return ss.GetHash();
1660+
// Serialize
1661+
ss << txTmp;
16161662
}
16171663

1618-
// Check for invalid use of SIGHASH_SINGLE
1619-
if ((nHashType & 0x1f) == SIGHASH_SINGLE) {
1620-
if (nIn >= txTo.vout.size()) {
1621-
// nOut out of range
1622-
return uint256::ONE;
1623-
}
1664+
// If a cache object was provided, store the midstate there.
1665+
if (sighash_cache != nullptr) {
1666+
sighash_cache->Store(nHashType, scriptCode, ss);
16241667
}
16251668

1626-
// Wrapper to serialize only the necessary parts of the transaction being signed
1627-
CTransactionSignatureSerializer<T> txTmp(txTo, scriptCode, nIn, nHashType);
1628-
1629-
// Serialize and hash
1630-
HashWriter ss{};
1631-
ss << txTmp << nHashType;
1669+
// Add sighash type and hash.
1670+
ss << nHashType;
16321671
return ss.GetHash();
16331672
}
16341673

@@ -1661,7 +1700,7 @@ bool GenericTransactionSignatureChecker<T>::CheckECDSASignature(const std::vecto
16611700
// Witness sighashes need the amount.
16621701
if (sigversion == SigVersion::WITNESS_V0 && amount < 0) return HandleMissingData(m_mdb);
16631702

1664-
uint256 sighash = SignatureHash(scriptCode, *txTo, nIn, nHashType, amount, sigversion, this->txdata);
1703+
uint256 sighash = SignatureHash(scriptCode, *txTo, nIn, nHashType, amount, sigversion, this->txdata, &m_sighash_cache);
16651704

16661705
if (!VerifyECDSASignature(vchSig, pubkey, sighash))
16671706
return false;

src/script/interpreter.h

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,27 @@ extern const HashWriter HASHER_TAPSIGHASH; //!< Hasher with tag "TapSighash" pre
239239
extern const HashWriter HASHER_TAPLEAF; //!< Hasher with tag "TapLeaf" pre-fed to it.
240240
extern const HashWriter HASHER_TAPBRANCH; //!< Hasher with tag "TapBranch" pre-fed to it.
241241

242+
/** Data structure to cache SHA256 midstates for the ECDSA sighash calculations
243+
* (bare, P2SH, P2WPKH, P2WSH). */
244+
class SigHashCache
245+
{
246+
/** For each sighash mode (ALL, SINGLE, NONE, ALL|ANYONE, SINGLE|ANYONE, NONE|ANYONE),
247+
* optionally store a scriptCode which the hash is for, plus a midstate for the SHA256
248+
* computation just before adding the hash_type itself. */
249+
std::optional<std::pair<CScript, HashWriter>> m_cache_entries[6];
250+
251+
/** Given a hash_type, find which of the 6 cache entries is to be used. */
252+
int CacheIndex(int32_t hash_type) const noexcept;
253+
254+
public:
255+
/** Load into writer the SHA256 midstate if found in this cache. */
256+
[[nodiscard]] bool Load(int32_t hash_type, const CScript& script_code, HashWriter& writer) const noexcept;
257+
/** Store into this cache object the provided SHA256 midstate. */
258+
void Store(int32_t hash_type, const CScript& script_code, const HashWriter& writer) noexcept;
259+
};
260+
242261
template <class T>
243-
uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn, int32_t nHashType, const CAmount& amount, SigVersion sigversion, const PrecomputedTransactionData* cache = nullptr);
262+
uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn, int32_t nHashType, const CAmount& amount, SigVersion sigversion, const PrecomputedTransactionData* cache = nullptr, SigHashCache* sighash_cache = nullptr);
244263

245264
class BaseSignatureChecker
246265
{
@@ -289,6 +308,7 @@ class GenericTransactionSignatureChecker : public BaseSignatureChecker
289308
unsigned int nIn;
290309
const CAmount amount;
291310
const PrecomputedTransactionData* txdata;
311+
mutable SigHashCache m_sighash_cache;
292312

293313
protected:
294314
virtual bool VerifyECDSASignature(const std::vector<unsigned char>& vchSig, const CPubKey& vchPubKey, const uint256& sighash) const;

src/test/fuzz/script_interpreter.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <test/fuzz/FuzzedDataProvider.h>
88
#include <test/fuzz/fuzz.h>
99
#include <test/fuzz/util.h>
10+
#include <util/check.h>
1011

1112
#include <cstdint>
1213
#include <optional>
@@ -45,3 +46,27 @@ FUZZ_TARGET(script_interpreter)
4546
(void)CastToBool(ConsumeRandomLengthByteVector(fuzzed_data_provider));
4647
}
4748
}
49+
50+
/** Differential fuzzing for SignatureHash with and without cache. */
51+
FUZZ_TARGET(sighash_cache)
52+
{
53+
FuzzedDataProvider provider(buffer.data(), buffer.size());
54+
55+
// Get inputs to the sighash function that won't change across types.
56+
const auto scriptcode{ConsumeScript(provider)};
57+
const auto tx{ConsumeTransaction(provider, std::nullopt)};
58+
if (tx.vin.empty()) return;
59+
const auto in_index{provider.ConsumeIntegralInRange<uint32_t>(0, tx.vin.size() - 1)};
60+
const auto amount{ConsumeMoney(provider)};
61+
const auto sigversion{(SigVersion)provider.ConsumeIntegralInRange(0, 1)};
62+
63+
// Check the sighash function will give the same result for 100 fuzzer-generated hash types whether or not a cache is
64+
// provided. The cache is conserved across types to exercise cache hits.
65+
SigHashCache sighash_cache{};
66+
for (int i{0}; i < 100; ++i) {
67+
const auto hash_type{((i & 2) == 0) ? provider.ConsumeIntegral<int8_t>() : provider.ConsumeIntegral<int32_t>()};
68+
const auto nocache_res{SignatureHash(scriptcode, tx, in_index, hash_type, amount, sigversion)};
69+
const auto cache_res{SignatureHash(scriptcode, tx, in_index, hash_type, amount, sigversion, nullptr, &sighash_cache)};
70+
Assert(nocache_res == cache_res);
71+
}
72+
}

src/test/sighash_tests.cpp

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,94 @@ BOOST_AUTO_TEST_CASE(sighash_from_data)
207207
BOOST_CHECK_MESSAGE(sh.GetHex() == sigHashHex, strTest);
208208
}
209209
}
210+
211+
BOOST_AUTO_TEST_CASE(sighash_caching)
212+
{
213+
// Get a script, transaction and parameters as inputs to the sighash function.
214+
CScript scriptcode;
215+
RandomScript(scriptcode);
216+
CScript diff_scriptcode{scriptcode};
217+
diff_scriptcode << OP_1;
218+
CMutableTransaction tx;
219+
RandomTransaction(tx, /*fSingle=*/false);
220+
const auto in_index{static_cast<uint32_t>(m_rng.randrange(tx.vin.size()))};
221+
const auto amount{m_rng.rand<CAmount>()};
222+
223+
// Exercise the sighash function under both legacy and segwit v0.
224+
for (const auto sigversion: {SigVersion::BASE, SigVersion::WITNESS_V0}) {
225+
// For each, run it against all the 6 standard hash types and a few additional random ones.
226+
std::vector<int32_t> hash_types{{SIGHASH_ALL, SIGHASH_SINGLE, SIGHASH_NONE, SIGHASH_ALL | SIGHASH_ANYONECANPAY,
227+
SIGHASH_SINGLE | SIGHASH_ANYONECANPAY, SIGHASH_NONE | SIGHASH_ANYONECANPAY,
228+
SIGHASH_ANYONECANPAY, 0, std::numeric_limits<int32_t>::max()}};
229+
for (int i{0}; i < 10; ++i) {
230+
hash_types.push_back(i % 2 == 0 ? m_rng.rand<int8_t>() : m_rng.rand<int32_t>());
231+
}
232+
233+
// Reuse the same cache across script types. This must not cause any issue as the cached value for one hash type must never
234+
// be confused for another (instantiating the cache within the loop instead would prevent testing this).
235+
SigHashCache cache;
236+
for (const auto hash_type: hash_types) {
237+
const bool expect_one{sigversion == SigVersion::BASE && ((hash_type & 0x1f) == SIGHASH_SINGLE) && in_index >= tx.vout.size()};
238+
239+
// The result of computing the sighash should be the same with or without cache.
240+
const auto sighash_with_cache{SignatureHash(scriptcode, tx, in_index, hash_type, amount, sigversion, nullptr, &cache)};
241+
const auto sighash_no_cache{SignatureHash(scriptcode, tx, in_index, hash_type, amount, sigversion, nullptr, nullptr)};
242+
BOOST_CHECK_EQUAL(sighash_with_cache, sighash_no_cache);
243+
244+
// Calling the cached version again should return the same value again.
245+
BOOST_CHECK_EQUAL(sighash_with_cache, SignatureHash(scriptcode, tx, in_index, hash_type, amount, sigversion, nullptr, &cache));
246+
247+
// While here we might as well also check that the result for legacy is the same as for the old SignatureHash() function.
248+
if (sigversion == SigVersion::BASE) {
249+
BOOST_CHECK_EQUAL(sighash_with_cache, SignatureHashOld(scriptcode, CTransaction(tx), in_index, hash_type));
250+
}
251+
252+
// Calling with a different scriptcode (for instance in case a CODESEP is encountered) will not return the cache value but
253+
// overwrite it. The sighash will always be different except in case of legacy SIGHASH_SINGLE bug.
254+
const auto sighash_with_cache2{SignatureHash(diff_scriptcode, tx, in_index, hash_type, amount, sigversion, nullptr, &cache)};
255+
const auto sighash_no_cache2{SignatureHash(diff_scriptcode, tx, in_index, hash_type, amount, sigversion, nullptr, nullptr)};
256+
BOOST_CHECK_EQUAL(sighash_with_cache2, sighash_no_cache2);
257+
if (!expect_one) {
258+
BOOST_CHECK_NE(sighash_with_cache, sighash_with_cache2);
259+
} else {
260+
BOOST_CHECK_EQUAL(sighash_with_cache, sighash_with_cache2);
261+
BOOST_CHECK_EQUAL(sighash_with_cache, uint256::ONE);
262+
}
263+
264+
// Calling the cached version again should return the same value again.
265+
BOOST_CHECK_EQUAL(sighash_with_cache2, SignatureHash(diff_scriptcode, tx, in_index, hash_type, amount, sigversion, nullptr, &cache));
266+
267+
// And if we store a different value for this scriptcode and hash type it will return that instead.
268+
{
269+
HashWriter h{};
270+
h << 42;
271+
cache.Store(hash_type, scriptcode, h);
272+
const auto stored_hash{h.GetHash()};
273+
BOOST_CHECK(cache.Load(hash_type, scriptcode, h));
274+
const auto loaded_hash{h.GetHash()};
275+
BOOST_CHECK_EQUAL(stored_hash, loaded_hash);
276+
}
277+
278+
// And using this mutated cache with the sighash function will return the new value (except in the legacy SIGHASH_SINGLE bug
279+
// case in which it'll return 1).
280+
if (!expect_one) {
281+
BOOST_CHECK_NE(SignatureHash(scriptcode, tx, in_index, hash_type, amount, sigversion, nullptr, &cache), sighash_with_cache);
282+
HashWriter h{};
283+
BOOST_CHECK(cache.Load(hash_type, scriptcode, h));
284+
h << hash_type;
285+
const auto new_hash{h.GetHash()};
286+
BOOST_CHECK_EQUAL(SignatureHash(scriptcode, tx, in_index, hash_type, amount, sigversion, nullptr, &cache), new_hash);
287+
} else {
288+
BOOST_CHECK_EQUAL(SignatureHash(scriptcode, tx, in_index, hash_type, amount, sigversion, nullptr, &cache), uint256::ONE);
289+
}
290+
291+
// Wipe the cache and restore the correct cached value for this scriptcode and hash_type before starting the next iteration.
292+
HashWriter dummy{};
293+
cache.Store(hash_type, diff_scriptcode, dummy);
294+
(void)SignatureHash(scriptcode, tx, in_index, hash_type, amount, sigversion, nullptr, &cache);
295+
BOOST_CHECK(cache.Load(hash_type, scriptcode, dummy) || expect_one);
296+
}
297+
}
298+
}
299+
210300
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)