From 529eaa1f3a45f6922acb72309f92e9c85de1b832 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 18 Aug 2026 15:20:26 +0200 Subject: [PATCH 1/9] test: ruff format for feature_sighash_rangeproof.py --- test/functional/feature_sighash_rangeproof.py | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/test/functional/feature_sighash_rangeproof.py b/test/functional/feature_sighash_rangeproof.py index 88fb226e1a..a2f20824f5 100755 --- a/test/functional/feature_sighash_rangeproof.py +++ b/test/functional/feature_sighash_rangeproof.py @@ -8,36 +8,35 @@ """ import struct -from test_framework.test_framework import BitcoinTestFramework + +from test_framework import util from test_framework.address import base58_to_byte +from test_framework.blocktools import add_witness_commitment +from test_framework.key import ECKey +from test_framework.messages import ( + CBlock, + from_hex, + tx_from_hex, +) from test_framework.script import ( - hash160, - LegacySignatureHash, - SegwitV0SignatureHash, - SIGHASH_ALL, - SIGHASH_RANGEPROOF, - CScript, - CScriptOp, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, + SIGHASH_ALL, + SIGHASH_RANGEPROOF, + CScript, + CScriptOp, + LegacySignatureHash, + SegwitV0SignatureHash, + hash160, ) -from test_framework.key import ECKey - -from test_framework.messages import ( - CBlock, - tx_from_hex, - from_hex, -) - -from test_framework import util +from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, ) -from test_framework.blocktools import add_witness_commitment def get_p2pkh_script(pubkeyhash): """Get the script associated with a P2PKH.""" @@ -112,7 +111,7 @@ def prepare_tx_signed_with_sighash(self, address_type, sighash_rangeproof_aware, # Prepare the keypair we need to re-sign the tx. wif = self.nodes[1].dumpprivkey(addr) - (b, v) = base58_to_byte(wif) + (b, _v) = base58_to_byte(wif) privkey = ECKey() privkey.set(b[0:32], len(b) == 33) pubkey = privkey.get_pubkey() @@ -235,12 +234,12 @@ def run_test(self): # - the tx is not accepted in the mempool and # - the tx is accepted if manually mined in a block for address_type in ADDRESS_TYPES: - self.log.info("Pre-activation for {} address".format(address_type)) + self.log.info(f"Pre-activation for {address_type} address") tx = self.prepare_tx_signed_with_sighash(address_type, False, False) self.assert_tx_standard(tx, False) self.assert_tx_valid(tx, True) - self.log.info("Pre-activation for {} address (with issuance)".format(address_type)) + self.log.info(f"Pre-activation for {address_type} address (with issuance)") tx = self.prepare_tx_signed_with_sighash(address_type, False, True) self.assert_tx_standard(tx, False) self.assert_tx_valid(tx, True) @@ -257,25 +256,29 @@ def run_test(self): # Test that the use of SIGHASH_RANGEPROOF is legal and standard # after activation. for address_type in ADDRESS_TYPES: - self.log.info("Post-activation for {} address".format(address_type)) + self.log.info(f"Post-activation for {address_type} address") tx = self.prepare_tx_signed_with_sighash(address_type, True, False) self.assert_tx_standard(tx, True) self.assert_tx_valid(tx, True) - self.log.info("Post-activation for {} address (with issuance)".format(address_type)) + self.log.info(f"Post-activation for {address_type} address (with issuance)") tx = self.prepare_tx_signed_with_sighash(address_type, True, True) self.assert_tx_standard(tx, True) self.assert_tx_valid(tx, True) + # Post-activation, the wallet default must set SIGHASH_RANGEPROOF. + self.log.info(f"Post-activation default sighash for {address_type} address") + self.assert_default_sign_commits_rangeproof(address_type, expect_rangeproof=True) + # Ensure that if we then use the old sighash algorithm that doesn't hash # the rangeproofs, the signature is no longer valid. for address_type in ADDRESS_TYPES: - self.log.info("Post-activation invalid sighash for {} address".format(address_type)) + self.log.info(f"Post-activation invalid sighash for {address_type} address") tx = self.prepare_tx_signed_with_sighash(address_type, False, False) self.assert_tx_standard(tx, False) self.assert_tx_valid(tx, False) - self.log.info("Post-activation invalid sighash for {} address (with issuance)".format(address_type)) + self.log.info(f"Post-activation invalid sighash for {address_type} address (with issuance)") tx = self.prepare_tx_signed_with_sighash(address_type, False, True) self.assert_tx_standard(tx, False) self.assert_tx_valid(tx, False) From 6a531c262670f421af23fcfb95a8b090135553df Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 18 Aug 2026 15:32:43 +0200 Subject: [PATCH 2/9] script: add SIGHASH_ALL_WITH_RANGEPROOF and DefaultSighashType; strip rangeproof bit for Taproot signing Add a named default constant SIGHASH_ALL_WITH_RANGEPROOF (SIGHASH_ALL | SIGHASH_RANGEPROOF) and a DefaultSighashType() helper that selects the default pre-Taproot sighash based on whether SIGHASH_RANGEPROOF is active for the target chain. Strip the 0x40 (SIGHASH_RANGEPROOF) bit when computing Schnorr signatures, since the BIP341-style sighash always commits to rangeproofs and rejects the bit. This keeps SIGHASH_ALL_WITH_RANGEPROOF a valid universal default for both pre-Taproot and Taproot signing. --- src/script/interpreter.h | 9 +++++++++ src/script/sign.cpp | 18 ++++++++++++++++-- src/script/sign.h | 14 ++++++++++++++ src/test/sighash_tests.cpp | 18 ++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/script/interpreter.h b/src/script/interpreter.h index b1a510bd62..1189f0838e 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -39,6 +39,15 @@ enum // ELEMENTS: // A flag that means the rangeproofs should be included in the sighash. SIGHASH_RANGEPROOF = 0x40, + + // ELEMENTS: + // The default sighash used by wallets/tools when signing pre-Taproot + // (BASE/WITNESS_V0) inputs on chains where SIGHASH_RANGEPROOF is active. + // This commits to the output rangeproofs, closing the pre-Taproot + // rangeproof (witness) malleability gap. Note this must only be used once + // dynafed (which enables SCRIPT_SIGHASH_RANGEPROOF) is active for the target + // chain; otherwise the resulting signatures are non-standard and invalid. + SIGHASH_ALL_WITH_RANGEPROOF = SIGHASH_ALL | SIGHASH_RANGEPROOF, }; /** Script verification flags. diff --git a/src/script/sign.cpp b/src/script/sign.cpp index cea4ee1732..9db8ce7abb 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -38,6 +38,15 @@ MutableTransactionSignatureCreator::MutableTransactionSignatureCreator(const CMu { } +int DefaultSighashType(bool sighash_rangeproof_active) +{ + // When SIGHASH_RANGEPROOF is active for the chain, default to committing to + // rangeproofs for pre-Taproot inputs. The 0x40 bit is stripped for Taproot + // signing (see CreateSchnorrSig), so this is a safe universal default. + // Otherwise fall back to SIGHASH_DEFAULT (== SIGHASH_ALL for pre-Taproot). + return sighash_rangeproof_active ? SIGHASH_ALL_WITH_RANGEPROOF : SIGHASH_DEFAULT; +} + bool MutableTransactionSignatureCreator::CreateSig(const SigningProvider& provider, std::vector& vchSig, const CKeyID& address, const CScript& scriptCode, SigVersion sigversion, unsigned int flags) const { assert(sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0); @@ -85,12 +94,17 @@ bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& execdata.m_tapleaf_hash_init = true; execdata.m_tapleaf_hash = *leaf_hash; } + // ELEMENTS: SIGHASH_RANGEPROOF is a pre-Taproot-only flag; the BIP341-style + // sighash always commits to rangeproofs and rejects the 0x40 bit. Strip it so + // that a universal default of SIGHASH_ALL_WITH_RANGEPROOF still produces valid + // Taproot signatures. + const int taproot_hashtype = nHashType & ~SIGHASH_RANGEPROOF; uint256 hash; - if (!SignatureHashSchnorr(hash, execdata, m_txto, nIn, nHashType, sigversion, *m_txdata, MissingDataBehavior::FAIL)) return false; + if (!SignatureHashSchnorr(hash, execdata, m_txto, nIn, taproot_hashtype, sigversion, *m_txdata, MissingDataBehavior::FAIL)) return false; sig.resize(64); // Use uint256{} as aux_rnd for now. if (!key.SignSchnorr(hash, sig, merkle_root, {})) return false; - if (nHashType) sig.push_back(nHashType); + if (taproot_hashtype) sig.push_back(taproot_hashtype); return true; } diff --git a/src/script/sign.h b/src/script/sign.h index 5820080f08..f98a89a7f9 100644 --- a/src/script/sign.h +++ b/src/script/sign.h @@ -107,4 +107,18 @@ bool IsSegWitOutput(const SigningProvider& provider, const CScript& script); /** Sign the CMutableTransaction */ bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* provider, const std::map& coins, int sighash, const uint256& hash_genesis_block, std::map& input_errors); +/** + * ELEMENTS: Return the default sighash type to use when the caller did not + * specify one. When SIGHASH_RANGEPROOF is active for the target chain, the + * default commits to output rangeproofs (SIGHASH_ALL_WITH_RANGEPROOF for + * pre-Taproot inputs); otherwise the historical default (SIGHASH_DEFAULT, which + * is equivalent to SIGHASH_ALL for pre-Taproot) is used so that signatures stay + * standard and valid on chains where dynafed is not active. + * + * Note: for Taproot inputs the sighash byte's rangeproof bit is ignored (the + * BIP341-style sighash always commits to rangeproofs), so this default is only + * meaningful for BASE/WITNESS_V0 signing. + */ +int DefaultSighashType(bool sighash_rangeproof_active); + #endif // BITCOIN_SCRIPT_SIGN_H diff --git a/src/test/sighash_tests.cpp b/src/test/sighash_tests.cpp index 5db1987c7f..90760b93cb 100644 --- a/src/test/sighash_tests.cpp +++ b/src/test/sighash_tests.cpp @@ -8,6 +8,7 @@ #include #include