Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 71 additions & 27 deletions src/script/interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2802,11 +2802,61 @@ bool SignatureHashSchnorr(uint256& hash_out, ScriptExecutionData& execdata, cons
return true;
}

int SigHashCache::CacheIndex(int32_t hash_type) const noexcept
{
// Note that we do not distinguish between BASE and WITNESS_V0 to determine the cache index,
// because no input can simultaneously use both.
// ELEMENTS: SIGHASH_RANGEPROOF changes the preimage (segwit v0 appends hashRangeproofs; the
// legacy serializer appends each output's rangeproof and surjectionproof), so it must be a
// dimension of the cache key.
return 8 * !!(hash_type & SIGHASH_RANGEPROOF) + // bit 3
3 * !!(hash_type & SIGHASH_ANYONECANPAY) + // bit 2
2 * ((hash_type & 0x1f) == SIGHASH_SINGLE) + // bit 1
1 * ((hash_type & 0x1f) == SIGHASH_NONE); // bit 0
}

bool SigHashCache::Load(int32_t hash_type, const CScript& script_code, HashWriter& writer) const noexcept
{
auto& entry = m_cache_entries[CacheIndex(hash_type)];
if (entry.has_value()) {
if (script_code == entry->first) {
writer = HashWriter(entry->second);
return true;
}
}
return false;
}

void SigHashCache::Store(int32_t hash_type, const CScript& script_code, const HashWriter& writer) noexcept
{
auto& entry = m_cache_entries[CacheIndex(hash_type)];
entry.emplace(script_code, writer);
}

template <class T>
uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn, int32_t nHashType, const CConfidentialValue& amount, SigVersion sigversion, unsigned int flags, const PrecomputedTransactionData* cache)
uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn, int32_t nHashType, const CConfidentialValue& amount, SigVersion sigversion, unsigned int flags, const PrecomputedTransactionData* cache, SigHashCache* sighash_cache)
{
assert(nIn < txTo.vin.size());

if (sigversion != SigVersion::WITNESS_V0) {
// Check for invalid use of SIGHASH_SINGLE
if ((nHashType & 0x1f) == SIGHASH_SINGLE) {
if (nIn >= txTo.vout.size()) {
// nOut out of range
return uint256::ONE;
}
}
}

HashWriter ss{};

// Try to compute using cached SHA256 midstate.
if (sighash_cache && sighash_cache->Load(nHashType, scriptCode, ss)) {
// Add sighash type and hash.
ss << nHashType;
return ss.GetHash();
}

if (sigversion == SigVersion::WITNESS_V0) {
uint256 hashPrevouts;
uint256 hashSequence;
Expand Down Expand Up @@ -2835,24 +2885,23 @@ uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn
hashRangeproofs = cacheready ? cache->hashRangeproofs : GetRangeproofsHash(txTo);
}
} else if ((nHashType & 0x1f) == SIGHASH_SINGLE && nIn < txTo.vout.size()) {
HashWriter ss{};
ss << txTo.vout[nIn];
hashOutputs = ss.GetHash();
HashWriter inner_ss{};
inner_ss << txTo.vout[nIn];
hashOutputs = inner_ss.GetHash();

if (fRangeproof) {
HashWriter ss{};
HashWriter rp_ss{};
if (nIn < txTo.witness.vtxoutwit.size()) {
ss << txTo.witness.vtxoutwit[nIn].vchRangeproof;
ss << txTo.witness.vtxoutwit[nIn].vchSurjectionproof;
rp_ss << txTo.witness.vtxoutwit[nIn].vchRangeproof;
rp_ss << txTo.witness.vtxoutwit[nIn].vchSurjectionproof;
} else {
ss << (unsigned char) 0;
ss << (unsigned char) 0;
rp_ss << (unsigned char) 0;
rp_ss << (unsigned char) 0;
}
hashRangeproofs = ss.GetHash();
hashRangeproofs = rp_ss.GetHash();
}
}

HashWriter ss{};
// Version
ss << txTo.version;
// Input prevouts/nSequence (none/all, depending on flags)
Expand Down Expand Up @@ -2885,26 +2934,21 @@ uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn
}
// Locktime
ss << txTo.nLockTime;
// Sighash type
ss << nHashType;
} else {
// Wrapper to serialize only the necessary parts of the transaction being signed
CTransactionSignatureSerializer<T> txTmp(txTo, scriptCode, nIn, nHashType, flags);

return ss.GetHash();
// Serialize
ss << txTmp;
}

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

// Wrapper to serialize only the necessary parts of the transaction being signed
CTransactionSignatureSerializer<T> txTmp(txTo, scriptCode, nIn, nHashType, flags);

// Serialize and hash
HashWriter ss{};
ss << txTmp << nHashType;
// Add sighash type and hash.
ss << nHashType;
return ss.GetHash();
}

Expand Down Expand Up @@ -2937,7 +2981,7 @@ bool GenericTransactionSignatureChecker<T>::CheckECDSASignature(const std::vecto
// Witness sighashes need the amount.
if (sigversion == SigVersion::WITNESS_V0 && amount.IsNull()) return HandleMissingData(m_mdb);

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

if (!VerifyECDSASignature(vchSig, pubkey, sighash))
return false;
Expand Down
25 changes: 24 additions & 1 deletion src/script/interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,30 @@ extern const HashWriter HASHER_TAPSIGHASH_ELEMENTS; //!< Hasher with tag "TapSig
extern const HashWriter HASHER_TAPLEAF_ELEMENTS; //!< Hasher with tag "TapLeaf" pre-fed to it.
extern const HashWriter HASHER_TAPBRANCH_ELEMENTS; //!< Hasher with tag "TapBranch" pre-fed to it.

/** Data structure to cache SHA256 midstates for the ECDSA sighash calculations
* (bare, P2SH, P2WPKH, P2WSH). */
class SigHashCache
{
/** For each sighash mode (ALL, SINGLE, NONE, ALL|ANYONE, SINGLE|ANYONE, NONE|ANYONE, ALL|RANGEPROOF, SINGLE|RANGEPROOF, NONE|RANGEPROOF, ALL|ANYONE|RANGEPROOF, SINGLE|ANYONE|RANGEPROOF, NONE|ANYONE|RANGEPROOF),
* optionally store a scriptCode which the hash is for, plus a midstate for the SHA256
* computation just before adding the hash_type itself. */
// ELEMENTS: the SIGHASH_RANGEPROOF (0x40) bit changes the sighash preimage, so it is part of
// the cache key and the table has 16 entries rather than upstream's 6. Do not drop this when

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 903be1e:

Fine to leave as-is, but the old table had 6 elements and we're adding 8 to it, so 14 is the correct number.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right. I will leave as is though, as it is (again incorrectly) 16 in 23.x.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it 12?

3 (sighash types) x 2 (anyonecanpay) x 2 (rangeproof)

@tomt1664 tomt1664 Aug 14, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The index is 8 x rp + 3 x acp + 2 x single + 1 x none. The low part 3 x acp + 2 x single + 1 x none spans 0–5, and the rangeproof term adds 8, so the reachable indices are:

0 1 2 3 4 5 (0x40 clear)
8 9 10 11 12 13 (0x40 set)

Twelve reachable values, but a maximum of 13 — so it must be minimum 14. 6 and 7 are unreachable.

// merging upstream changes to this file.
std::optional<std::pair<CScript, HashWriter>> m_cache_entries[16];

/** Given a hash_type, find which of the cache entries is to be used. */
int CacheIndex(int32_t hash_type) const noexcept;

public:
/** Load into writer the SHA256 midstate if found in this cache. */
[[nodiscard]] bool Load(int32_t hash_type, const CScript& script_code, HashWriter& writer) const noexcept;
/** Store into this cache object the provided SHA256 midstate. */
void Store(int32_t hash_type, const CScript& script_code, const HashWriter& writer) noexcept;
};

template <class T>
uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn, int32_t nHashType, const CConfidentialValue& amount, SigVersion sigversion, unsigned int flags, const PrecomputedTransactionData* cache = nullptr);
uint256 SignatureHash(const CScript& scriptCode, const T& txTo, unsigned int nIn, int32_t nHashType, const CConfidentialValue& amount, SigVersion sigversion, unsigned int flags, const PrecomputedTransactionData* cache = nullptr, SigHashCache* sighash_cache = nullptr);

class BaseSignatureChecker
{
Expand Down Expand Up @@ -374,6 +396,7 @@ class GenericTransactionSignatureChecker : public BaseSignatureChecker
unsigned int nIn;
const CConfidentialValue amount;
const PrecomputedTransactionData* txdata;
mutable SigHashCache m_sighash_cache;

protected:
virtual bool VerifyECDSASignature(const std::vector<unsigned char>& vchSig, const CPubKey& vchPubKey, const uint256& sighash) const;
Expand Down
131 changes: 130 additions & 1 deletion test/functional/feature_sighash_rangeproof.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,47 @@

"""
Test the post-dynafed elements-only SIGHASH_RANGEPROOF sighash flag.

Also tests that the per-input ECDSA sighash midstate cache (SigHashCache= in
src/script/interpreter.cpp) treats the SIGHASH_RANGEPROOF (0x40) bit as part
of its key.
"""

import struct
from decimal import Decimal
from test_framework.test_framework import BitcoinTestFramework
from test_framework.address import base58_to_byte
from test_framework.address import (
base58_to_byte,
script_to_p2sh,
script_to_p2wsh,
)
from test_framework.script import (
hash160,
LegacySignatureHash,
LegacySignatureMsg,
SegwitV0SignatureHash,
SegwitV0SignatureMsg,
SIGHASH_ALL,
SIGHASH_RANGEPROOF,
CScript,
CScriptOp,
OP_0,
OP_2,
OP_CHECKMULTISIG,
OP_CHECKSIG,
OP_DUP,
OP_EQUAL,
OP_EQUALVERIFY,
OP_HASH160,
)
from test_framework.key import ECKey

from test_framework.messages import (
COIN,
CBlock,
CTxInWitness,
hash256,
sha256,
tx_from_hex,
from_hex,
)
Expand Down Expand Up @@ -179,6 +198,102 @@ def prepare_tx_signed_with_sighash(self, address_type, sighash_rangeproof_aware,

return signed_tx

def prepare_mixed_sighash_multisig_tx(self, address_type, use_polluted_midstate):
# Spend of a 2-of-2 CHECKMULTISIG output where the two signatures
# are made over the same scriptCode with sighash bytes that differ only in
# the SIGHASH_RANGEPROOF bit: the first uses ALL|RANGEPROOF and the second ALL.
# Both checks happen while evaluating a single input, so they share a `SigHashCache`.
#
# If `use_polluted_midstate` is
# false, each signiture is made over its own (correct) sighash.
# true, the second signature is made over the hash a node whose cache
# ignores the SIGHASH_RANGEPROOF bit would compute: the 0x41 preimage with the
# sighash type replaced by 0x01.

key_a = ECKey()
key_a.generate()
key_b = ECKey()
key_b.generate()
pubkey_a = key_a.get_pubkey().get_bytes()
pubkey_b = key_b.get_pubkey().get_bytes()

# scriptCode is the same for both CHECKSIG evaluations
script = CScript([OP_2, pubkey_a, pubkey_b, OP_2, CScriptOp(OP_CHECKMULTISIG)])
if address_type == "p2wsh":
address = script_to_p2wsh(script)
script_pubkey = CScript([OP_0, sha256(script)])
elif address_type == "p2sh":
address = script_to_p2sh(script)
script_pubkey = CScript([CScriptOp(OP_HASH160), hash160(script), CScriptOp(OP_EQUAL)])
else:
assert False

# Fund the multisig
funding_txid = self.nodes[0].sendtoaddress(address, 1.0)
self.generate(self.nodes[0], 1)
self.sync_all()
funding_tx = tx_from_hex(self.nodes[0].getrawtransaction(funding_txid))
vout = None
for i, out in enumerate(funding_tx.vout):
if out.scriptPubKey == bytes(script_pubkey):
vout = i
break
assert vout is not None, "could not find the multisig output"
utxo_value = funding_tx.vout[vout].nValue
amount = Decimal(utxo_value.getAmount()) / COIN

# Spend it to two blinded outputs, so the transaction has rangeproofs.
unsigned_hex = self.nodes[0].createrawtransaction(
[{"txid": funding_txid, "vout": vout}],
[
{self.nodes[2].getnewaddress(): amount / 4},
{self.nodes[2].getnewaddress(): amount / 2},
{"fee": amount - amount / 4 - amount / 2},
]
)
zero_blinder = "00" * 32
asset = self.nodes[0].getsidechaininfo()["pegged_asset"]
blinded_hex = self.nodes[0].rawblindrawtransaction(
unsigned_hex, [zero_blinder], [amount], [asset], [zero_blinder]
)
tx = tx_from_hex(blinded_hex)
assert any(len(wit.vchRangeproof) > 0 for wit in tx.wit.vtxoutwit), "the outputs were not blinded"

hashtype_rp = SIGHASH_ALL | SIGHASH_RANGEPROOF
hashtype_plain = SIGHASH_ALL

# Build the preimages
if address_type == "p2wsh":
msg_rp = SegwitV0SignatureMsg(script, tx, 0, hashtype_rp, utxo_value)
msg_plain = SegwitV0SignatureMsg(script, tx, 0, hashtype_plain, utxo_value)
else:
(msg_rp, err) = LegacySignatureMsg(script, tx, 0, hashtype_rp)
assert err is None, err
(msg_plain, err) = LegacySignatureMsg(script, tx, 0, hashtype_plain)
assert err is None, err

# CHECKMULTISIG signature_b is checked first and populates the cache entry; signature_a is the check that reads it back. The polluted
# signature must therefore be signature_a: a 0x40-blind cache serves it signature_b's
# midstate, computed without the rangeproof commitment, and then appends 0x41.
polluted_msg = msg_plain[:-4] + hashtype_rp.to_bytes(4, "little")
assert polluted_msg != msg_rp, "the 0x40 bit did not change the preimage"

signature_a = key_a.sign_ecdsa(
hash256(polluted_msg if use_polluted_midstate else msg_rp)
) + bytes([hashtype_rp])
signature_b = key_b.sign_ecdsa(hash256(msg_plain)) + bytes([hashtype_plain])

# CHECKMULTISIG checks signature_a against pubkey_a first, so signature_a
# populates the cache entry and signature_b is the check that reads it back.
if address_type == "p2wsh":
if len(tx.wit.vtxinwit) != len(tx.vin):
tx.wit.vtxinwit = [CTxInWitness() for _ in tx.vin]
tx.wit.vtxinwit[0].scriptWitness.stack = [b'', signature_a, signature_b, bytes(script)]
else:
tx.vin[0].scriptSig = CScript([OP_0, signature_a, signature_b, bytes(script)])
tx.rehash()
return tx

def assert_tx_standard(self, tx, assert_standard=True):
# Test the standardness of the tx by submitting it to the mempool.

Expand Down Expand Up @@ -280,5 +395,19 @@ def run_test(self):
self.assert_tx_standard(tx, False)
self.assert_tx_valid(tx, False)

# Two ECDSA checks in one input, same scriptCode, sighash bytes differing
# only in SIGHASH_RANGEPROOF. The sighash midstate cache must not
# serve one check's midstate to the other.
for multisig_type in ["p2wsh", "p2sh"]:
self.log.info("Mixed SIGHASH_RANGEPROOF 2-of-2 for {}".format(multisig_type))
tx = self.prepare_mixed_sighash_multisig_tx(multisig_type, False)
self.assert_tx_standard(tx, True)
self.assert_tx_valid(tx, True)

self.log.info("Polluted sighash midstate for {}".format(multisig_type))
tx = self.prepare_mixed_sighash_multisig_tx(multisig_type, True)
self.assert_tx_standard(tx, False)
self.assert_tx_valid(tx, False)

if __name__ == '__main__':
SighashRangeproofTest(__file__).main()
Loading
Loading