From 903be1e538ae16beff79ba15081a84362c3dc5c7 Mon Sep 17 00:00:00 2001 From: Tom Trevethan Date: Thu, 13 Aug 2026 14:33:45 +0100 Subject: [PATCH 1/3] ELEMENTS: backport per-txin sighash midstate cache with SIGHASH_RANGEPROOF keying Backport of bitcoin/bitcoin#32473 as integrated on elements-23.x (eb64d2486f54), reapplied against master's tree. The SIGHASH_RANGEPROOF (0x40) bit changes the sighash preimage: segwit v0 appends hashRangeproofs, and the legacy serializer appends each output's rangeproof and surjectionproof. It is therefore part of the cache key, and the table has 16 entries rather than upstream's 6. Dropping that term is a consensus split with deployed elements-23.x in both directions. Upstream added SigHashCache as a pure addition, so a merge of this file will auto-merge without prompting for the Elements adaptation. Hence the explicit markers. --- src/script/interpreter.cpp | 98 +++++++++++++++++++++++++++----------- src/script/interpreter.h | 25 +++++++++- 2 files changed, 95 insertions(+), 28 deletions(-) diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index bd888e4362..3980800cce 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -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. Removing this term is a consensus split with elements-23.x. + 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 -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; @@ -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) @@ -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 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 txTmp(txTo, scriptCode, nIn, nHashType, flags); - - // Serialize and hash - HashWriter ss{}; - ss << txTmp << nHashType; + // Add sighash type and hash. + ss << nHashType; return ss.GetHash(); } @@ -2937,7 +2981,7 @@ bool GenericTransactionSignatureChecker::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; diff --git a/src/script/interpreter.h b/src/script/interpreter.h index b1a510bd62..562f72df2b 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -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 + // merging upstream changes to this file. + std::optional> 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 -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 { @@ -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& vchSig, const CPubKey& vchPubKey, const uint256& sighash) const; From 1484750d83adfd3cd52667013cc77f277821fa7f Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 25 Apr 2025 16:13:25 -0400 Subject: [PATCH 2/3] tests: add sighash caching tests to feature_taproot Github-Pull: #32473 Rebased-From: 9014d4016ad9351cb59b587541895e55f5d589cc (cherry picked from commit 5a0506eea03e423121dd2112c2ba5fb4320022e3) (cherry picked from commit b4780f0d87b173f6ca4a14104e127aa8dfe75208) --- test/functional/feature_taproot.py | 98 ++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index 57156c86f8..5d4cc65d64 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -76,6 +76,7 @@ OP_PUSHDATA1, OP_RETURN, OP_SWAP, + OP_TUCK, OP_VERIFY, SIGHASH_DEFAULT, SIGHASH_ALL, @@ -182,9 +183,9 @@ def get(ctx, name): ctx[name] = expr return expr.value -def getter(name): +def getter(name, **kwargs): """Return a callable that evaluates name in its passed context.""" - return lambda ctx: get(ctx, name) + return lambda ctx: get({**ctx, **kwargs}, name) def override(expr, **kwargs): """Return a callable that evaluates expr in a modified context.""" @@ -229,6 +230,20 @@ def default_controlblock(ctx): return bytes([get(ctx, "leafversion") + get(ctx, "negflag")]) + get(ctx, "pubkey_internal") + get(ctx, "merklebranch") #ELEMENTS: taphash depends on genesis hash +def default_scriptcode_suffix(ctx): + """Default expression for "scriptcode_suffix", the actually used portion of the scriptcode.""" + scriptcode = get(ctx, "scriptcode") + codesepnum = get(ctx, "codesepnum") + if codesepnum == -1: + return scriptcode + codeseps = 0 + for (opcode, data, sop_idx) in scriptcode.raw_iter(): + if opcode == OP_CODESEPARATOR: + if codeseps == codesepnum: + return CScript(scriptcode[sop_idx+1:]) + codeseps += 1 + assert False + def default_sigmsg(ctx): """Default expression for "sigmsg": depending on mode, compute BIP341, BIP143, or legacy sigmsg.""" tx = get(ctx, "tx") @@ -249,12 +264,12 @@ def default_sigmsg(ctx): return TaprootSignatureMsg(tx, utxos, hashtype, genesis_hash, idx, scriptpath=False, annex=annex) elif mode == "witv0": # BIP143 signature hash - scriptcode = get(ctx, "scriptcode") + scriptcode = get(ctx, "scriptcode_suffix") utxos = get(ctx, "utxos") return SegwitV0SignatureMsg(scriptcode, tx, idx, hashtype, utxos[idx].nValue, enable_sighash_rangeproof=False) else: # Pre-segwit signature hash - scriptcode = get(ctx, "scriptcode") + scriptcode = get(ctx, "scriptcode_suffix") return LegacySignatureMsg(scriptcode, tx, idx, hashtype, enable_sighash_rangeproof=False)[0] def default_sighash(ctx): @@ -314,7 +329,12 @@ def default_hashtype_actual(ctx): def default_bytes_hashtype(ctx): """Default expression for "bytes_hashtype": bytes([hashtype_actual]) if not 0, b"" otherwise.""" - return bytes([x for x in [get(ctx, "hashtype_actual")] if x != 0]) + mode = get(ctx, "mode") + hashtype_actual = get(ctx, "hashtype_actual") + if mode != "taproot" or hashtype_actual != 0: + return bytes([hashtype_actual]) + else: + return bytes() def default_sign(ctx): """Default expression for "sign": concatenation of signature and bytes_hashtype.""" @@ -394,6 +414,8 @@ def default_scriptsig(ctx): "key_tweaked": default_key_tweaked, # The tweak to use (None for script path spends, the actual tweak for key path spends). "tweak": default_tweak, + # The part of the scriptcode after the last executed OP_CODESEPARATOR. + "scriptcode_suffix": default_scriptcode_suffix, # The sigmsg value (preimage of sighash) "sigmsg": default_sigmsg, # The sighash value (32 bytes) @@ -428,6 +450,8 @@ def default_scriptsig(ctx): "annex": None, # The codeseparator position (only when mode=="taproot"). "codeseppos": -1, + # Which OP_CODESEPARATOR is the last executed one in the script (in legacy/P2SH/P2WSH). + "codesepnum": -1, # The redeemscript to add to the scriptSig (if P2SH; None implies not P2SH). "script_p2sh": None, # The script to add to the witness in (if P2WSH; None implies P2WPKH) @@ -1240,6 +1264,70 @@ def predict_sigops_ratio(n, dummy_size): standard = hashtype in VALID_SIGHASHES_ECDSA and (p2sh or witv0) add_spender(spenders, "compat/nocsa", hashtype=hashtype, p2sh=p2sh, witv0=witv0, standard=standard, script=CScript([OP_IF, OP_11, pubkey1, OP_CHECKSIGADD, OP_12, OP_EQUAL, OP_ELSE, pubkey1, OP_CHECKSIG, OP_ENDIF]), key=eckey1, sigops_weight=4-3*witv0, inputs=[getter("sign"), b''], failure={"inputs": [getter("sign"), b'\x01']}, **ERR_UNDECODABLE) + # == sighash caching tests == + + # Sighash caching in legacy. + for p2sh in [False, True]: + for witv0 in [False, True]: + eckey1, pubkey1 = generate_keypair(compressed=compressed) + for _ in range(10): + # Construct a script with 20 checksig operations (10 sighash types, each 2 times), + # randomly ordered and interleaved with 4 OP_CODESEPARATORS. + ops = [1, 2, 3, 0x21, 0x42, 0x63, 0x81, 0x83, 0xe1, 0xc2, -1, -1] * 2 + # Make sure no OP_CODESEPARATOR appears last. + while True: + random.shuffle(ops) + if ops[-1] != -1: + break + script = [pubkey1] + inputs = [] + codeseps = -1 + for pos, op in enumerate(ops): + if op == -1: + codeseps += 1 + script.append(OP_CODESEPARATOR) + elif pos + 1 != len(ops): + script += [OP_TUCK, OP_CHECKSIGVERIFY] + inputs.append(getter("sign", codesepnum=codeseps, hashtype=op)) + else: + script += [OP_CHECKSIG] + inputs.append(getter("sign", codesepnum=codeseps, hashtype=op)) + inputs.reverse() + script = CScript(script) + add_spender(spenders, "sighashcache/legacy", p2sh=p2sh, witv0=witv0, standard=False, script=script, inputs=inputs, key=eckey1, sigops_weight=12*8*(4-3*witv0), no_fail=True) + + # Sighash caching in tapscript. + for _ in range(10): + # Construct a script with 700 checksig operations (7 sighash types, each 100 times), + # randomly ordered and interleaved with 100 OP_CODESEPARATORS. + ops = [0, 1, 2, 3, 0x81, 0x82, 0x83, -1] * 100 + # Make sure no OP_CODESEPARATOR appears last. + while True: + random.shuffle(ops) + if ops[-1] != -1: + break + script = [pubs[1]] + inputs = [] + opcount = 1 + codeseppos = -1 + for pos, op in enumerate(ops): + if op == -1: + codeseppos = opcount + opcount += 1 + script.append(OP_CODESEPARATOR) + elif pos + 1 != len(ops): + opcount += 2 + script += [OP_TUCK, OP_CHECKSIGVERIFY] + inputs.append(getter("sign", codeseppos=codeseppos, hashtype=op)) + else: + opcount += 1 + script += [OP_CHECKSIG] + inputs.append(getter("sign", codeseppos=codeseppos, hashtype=op)) + inputs.reverse() + script = CScript(script) + tap = taproot_construct(pubs[0], [("leaf", script)]) + add_spender(spenders, "sighashcache/taproot", tap=tap, leaf="leaf", inputs=inputs, standard=True, key=secs[1], no_fail=True) + return spenders From ec89c80014b1d4f6d8472824f94209ef22df95c4 Mon Sep 17 00:00:00 2001 From: Tom Trevethan Date: Fri, 14 Aug 2026 12:24:21 +0100 Subject: [PATCH 3/3] Add test that sighash midstate cache treats the SIGHASH_RANGEPROOF bit as part of its key --- src/script/interpreter.cpp | 2 +- test/functional/feature_sighash_rangeproof.py | 131 +++++++++++++++++- 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 3980800cce..9689a5251e 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -2808,7 +2808,7 @@ int SigHashCache::CacheIndex(int32_t hash_type) const noexcept // 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. Removing this term is a consensus split with elements-23.x. + // 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 diff --git a/test/functional/feature_sighash_rangeproof.py b/test/functional/feature_sighash_rangeproof.py index 88fb226e1a..4f3090b798 100755 --- a/test/functional/feature_sighash_rangeproof.py +++ b/test/functional/feature_sighash_rangeproof.py @@ -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, ) @@ -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. @@ -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()