Skip to content

Commit e5823ec

Browse files
backport: guard missing-output SINGLE signing (bitcoin#35984)
Backport Bitcoin Core 8df006f. Use Dash legacy sighash representation and preserve its signing interfaces. Port the supported legacy regression with both wallet modes, ANYONECANPAY and ALL controls, and raw-signing coverage. The ancillary PSBT-v2 UTXO-selection test hunks have no Dash counterparts; Taproot regression coverage awaits Dash support.
1 parent f35a24c commit e5823ec

2 files changed

Lines changed: 39 additions & 6 deletions

File tree

‎src/script/sign.cpp‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ bool MutableTransactionSignatureCreator::CreateSig(const SigningProvider& provid
3737
if (!provider.GetKey(address, key))
3838
return false;
3939

40+
// If an input is signed with SIGHASH_SINGLE but there is no output at the same index, the
41+
// signature commits to no output at all. Which means such a signature stays valid if the
42+
// output is swapped, which is a footgun. So don't produce it.
43+
if ((nHashType & 0x1f) == SIGHASH_SINGLE && nIn >= m_txto.vout.size()) return false;
44+
4045
uint256 hash = SignatureHash(scriptCode, m_txto, nIn, nHashType, amount, sigversion, m_txdata);
4146
if (!key.Sign(hash, vchSig))
4247
return false;
@@ -400,8 +405,6 @@ bool IsSolvable(const SigningProvider& provider, const CScript& script)
400405

401406
bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, int nHashType, std::map<int, bilingual_str>& input_errors)
402407
{
403-
bool fHashSingle = ((nHashType & ~SIGHASH_ANYONECANPAY) == SIGHASH_SINGLE);
404-
405408
// Use CTransaction for the constant parts of the
406409
// transaction to avoid rehashing.
407410
const CTransaction txConst(mtx);
@@ -437,10 +440,7 @@ bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore,
437440
const CAmount& amount = coin->second.out.nValue;
438441

439442
SignatureData sigdata = DataFromTransaction(mtx, i, coin->second.out);
440-
// Only sign SIGHASH_SINGLE if there's a corresponding output:
441-
if (!fHashSingle || (i < mtx.vout.size())) {
442-
ProduceSignature(*keystore, MutableTransactionSignatureCreator(mtx, i, amount, &txdata, nHashType), prevPubKey, sigdata);
443-
}
443+
ProduceSignature(*keystore, MutableTransactionSignatureCreator(mtx, i, amount, &txdata, nHashType), prevPubKey, sigdata);
444444

445445
UpdateInput(txin, sigdata);
446446

‎test/functional/rpc_psbt.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,38 @@ def test_input_confs_control(self):
108108

109109
wallet.unloadwallet()
110110

111+
def test_sighash_single(self):
112+
self.log.info("Test that SIGHASH_SINGLE won't sign an input with no matching output")
113+
node = self.nodes[0]
114+
node.createwallet("sighash_single", descriptors=self.options.descriptors)
115+
wallet = node.get_wallet_rpc("sighash_single")
116+
def_wallet = node.get_wallet_rpc(self.default_wallet_name)
117+
118+
addrs = [wallet.getnewaddress() for _ in range(2)]
119+
for addr in addrs:
120+
def_wallet.sendtoaddress(addr, 1)
121+
self.generatetoaddress(node, 1, def_wallet.getnewaddress())
122+
node.syncwithvalidationinterfacequeue()
123+
ins = [{"txid": u["txid"], "vout": u["vout"]} for u in wallet.listunspent(addresses=addrs)]
124+
assert_equal(len(ins), 2)
125+
126+
raw = node.createrawtransaction(ins, [{wallet.getnewaddress(): 1.9999}])
127+
for sighash in ["SINGLE", "SINGLE|ANYONECANPAY", "ALL"]:
128+
signed = wallet.walletprocesspsbt(node.converttopsbt(raw), True, sighash)["psbt"]
129+
state = wallet.analyzepsbt(signed)["inputs"]
130+
# Input 0 has a matching output and can always sign and finalize.
131+
assert state[0]["is_final"]
132+
# Input 1 has no matching output, so only ALL can sign it.
133+
assert_equal(state[1]["is_final"], sighash == "ALL")
134+
135+
signed_raw = wallet.signrawtransactionwithwallet(raw, [], sighash)
136+
assert_equal(signed_raw["complete"], sighash == "ALL")
137+
tx = node.decoderawtransaction(signed_raw["hex"])
138+
assert tx["vin"][0]["scriptSig"]["hex"]
139+
assert_equal(bool(tx["vin"][1]["scriptSig"]["hex"]), sighash == "ALL")
140+
141+
wallet.unloadwallet()
142+
111143
def run_test(self):
112144
# Create and fund a raw tx for sending 10 DASH
113145
psbtx1 = self.nodes[0].walletcreatefundedpsbt([], {self.nodes[2].getnewaddress():10})['psbt']
@@ -441,6 +473,7 @@ def run_test(self):
441473
assert_equal(extracted, extractor['result'])
442474

443475
self.test_input_confs_control()
476+
self.test_sighash_single()
444477

445478
# Test that psbts with p2pkh outputs are created properly
446479
p2pkh = self.nodes[0].getnewaddress()

0 commit comments

Comments
 (0)