diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index bd888e4362..9689a5251e 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. + 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; 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() 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