Skip to content

Commit ae6bb6e

Browse files
committed
Merge bitcoin/bitcoin#26418: Fix signing of multi_a and rawtr scripts with wallets that only have corresponding keys
0de30ed tests: Test Taproot PSBT signing with keys in other descriptor (Andrew Chow) 6efcdf6 tests: Use new wallets for each test in wallet_taproot.py (Andrew Chow) 8781a1b psbt: Include output pubkey in additional pubkeys to sign (Andrew Chow) 323890d sign: Fill in taproot pubkey info for all script path sigs (Andrew Chow) Pull request description: A user reported on [stackexchange](https://bitcoin.stackexchange.com/q/115742/48884) that they were unable to sign for a `multi_a` script using a wallet that only had the corresponding keys (i.e. it did not have the `multi_a()` descriptor). This PR fixes this issue. Additionally, `wallet_taproot.py` is modified to test for this scenario by having another wallet in `do_test_psbt` which contains descriptors that only have the keys involved in the descriptor being tested. `wallet_taproot.py` was also modified to create new wallets for each test case rather than sharing wallets throughout as the sharing could result in the signing wallet having the keys in a different descriptor and accidentally result in failing to detect a test failure. The changes to the test also revealed a similar issue with `rawtr()` descriptors, which has also been fixed by checking if a descriptor can produce a `SigningProvider` for the Taproot output pubkey. ACKs for top commit: instagibbs: crACK bitcoin/bitcoin@0de30ed darosior: ACK 0de30ed Tree-SHA512: 12e131dd8afd93da7b1288c9054de2415a228d4477b97102da3ee4e82ce9de20b186260c3085a4b7b067bd8b74400751dcadf153f113db83abc59e7466e69f14
2 parents e42ba13 + 0de30ed commit ae6bb6e

File tree

3 files changed

+127
-82
lines changed

3 files changed

+127
-82
lines changed

src/script/sign.cpp

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,16 @@ static bool CreateSig(const BaseSignatureCreator& creator, SignatureData& sigdat
146146

147147
static bool CreateTaprootScriptSig(const BaseSignatureCreator& creator, SignatureData& sigdata, const SigningProvider& provider, std::vector<unsigned char>& sig_out, const XOnlyPubKey& pubkey, const uint256& leaf_hash, SigVersion sigversion)
148148
{
149+
KeyOriginInfo info;
150+
if (provider.GetKeyOriginByXOnly(pubkey, info)) {
151+
auto it = sigdata.taproot_misc_pubkeys.find(pubkey);
152+
if (it == sigdata.taproot_misc_pubkeys.end()) {
153+
sigdata.taproot_misc_pubkeys.emplace(pubkey, std::make_pair(std::set<uint256>({leaf_hash}), info));
154+
} else {
155+
it->second.first.insert(leaf_hash);
156+
}
157+
}
158+
149159
auto lookup_key = std::make_pair(pubkey, leaf_hash);
150160
auto it = sigdata.taproot_script_sigs.find(lookup_key);
151161
if (it != sigdata.taproot_script_sigs.end()) {
@@ -170,17 +180,6 @@ static bool SignTaprootScript(const SigningProvider& provider, const BaseSignatu
170180
// <xonly pubkey> OP_CHECKSIG
171181
if (script.size() == 34 && script[33] == OP_CHECKSIG && script[0] == 0x20) {
172182
XOnlyPubKey pubkey{Span{script}.subspan(1, 32)};
173-
174-
KeyOriginInfo info;
175-
if (provider.GetKeyOriginByXOnly(pubkey, info)) {
176-
auto it = sigdata.taproot_misc_pubkeys.find(pubkey);
177-
if (it == sigdata.taproot_misc_pubkeys.end()) {
178-
sigdata.taproot_misc_pubkeys.emplace(pubkey, std::make_pair(std::set<uint256>({leaf_hash}), info));
179-
} else {
180-
it->second.first.insert(leaf_hash);
181-
}
182-
}
183-
184183
std::vector<unsigned char> sig;
185184
if (CreateTaprootScriptSig(creator, sigdata, provider, sig, pubkey, leaf_hash, sigversion)) {
186185
result = Vector(std::move(sig));

src/wallet/scriptpubkeyman.cpp

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2502,25 +2502,38 @@ TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction&
25022502
keys->Merge(std::move(*script_keys));
25032503
} else {
25042504
// Maybe there are pubkeys listed that we can sign for
2505-
script_keys = std::make_unique<FlatSigningProvider>();
2506-
for (const auto& pk_pair : input.hd_keypaths) {
2507-
const CPubKey& pubkey = pk_pair.first;
2508-
std::unique_ptr<FlatSigningProvider> pk_keys = GetSigningProvider(pubkey);
2509-
if (pk_keys) {
2510-
keys->Merge(std::move(*pk_keys));
2511-
}
2505+
std::vector<CPubKey> pubkeys;
2506+
2507+
// ECDSA Pubkeys
2508+
for (const auto& [pk, _] : input.hd_keypaths) {
2509+
pubkeys.push_back(pk);
2510+
}
2511+
2512+
// Taproot output pubkey
2513+
std::vector<std::vector<unsigned char>> sols;
2514+
if (Solver(script, sols) == TxoutType::WITNESS_V1_TAPROOT) {
2515+
sols[0].insert(sols[0].begin(), 0x02);
2516+
pubkeys.emplace_back(sols[0]);
2517+
sols[0][0] = 0x03;
2518+
pubkeys.emplace_back(sols[0]);
25122519
}
2520+
2521+
// Taproot pubkeys
25132522
for (const auto& pk_pair : input.m_tap_bip32_paths) {
25142523
const XOnlyPubKey& pubkey = pk_pair.first;
25152524
for (unsigned char prefix : {0x02, 0x03}) {
25162525
unsigned char b[33] = {prefix};
25172526
std::copy(pubkey.begin(), pubkey.end(), b + 1);
25182527
CPubKey fullpubkey;
25192528
fullpubkey.Set(b, b + 33);
2520-
std::unique_ptr<FlatSigningProvider> pk_keys = GetSigningProvider(fullpubkey);
2521-
if (pk_keys) {
2522-
keys->Merge(std::move(*pk_keys));
2523-
}
2529+
pubkeys.push_back(fullpubkey);
2530+
}
2531+
}
2532+
2533+
for (const auto& pubkey : pubkeys) {
2534+
std::unique_ptr<FlatSigningProvider> pk_keys = GetSigningProvider(pubkey);
2535+
if (pk_keys) {
2536+
keys->Merge(std::move(*pk_keys));
25242537
}
25252538
}
25262539
}

test/functional/wallet_taproot.py

Lines changed: 93 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""Test generation and spending of P2TR addresses."""
66

77
import random
8+
import uuid
89

910
from decimal import Decimal
1011
from test_framework.address import output_key_to_p2tr
@@ -226,104 +227,162 @@ def make_addr(treefn, keys, i):
226227

227228
def do_test_addr(self, comment, pattern, privmap, treefn, keys):
228229
self.log.info("Testing %s address derivation" % comment)
230+
231+
# Create wallets
232+
wallet_uuid = uuid.uuid4().hex
233+
self.nodes[0].createwallet(wallet_name=f"privs_tr_enabled_{wallet_uuid}", descriptors=True, blank=True)
234+
self.nodes[0].createwallet(wallet_name=f"pubs_tr_enabled_{wallet_uuid}", descriptors=True, blank=True, disable_private_keys=True)
235+
self.nodes[0].createwallet(wallet_name=f"addr_gen_{wallet_uuid}", descriptors=True, disable_private_keys=True, blank=True)
236+
privs_tr_enabled = self.nodes[0].get_wallet_rpc(f"privs_tr_enabled_{wallet_uuid}")
237+
pubs_tr_enabled = self.nodes[0].get_wallet_rpc(f"pubs_tr_enabled_{wallet_uuid}")
238+
addr_gen = self.nodes[0].get_wallet_rpc(f"addr_gen_{wallet_uuid}")
239+
229240
desc = self.make_desc(pattern, privmap, keys, False)
230241
desc_pub = self.make_desc(pattern, privmap, keys, True)
231242
assert_equal(self.nodes[0].getdescriptorinfo(desc)['descriptor'], desc_pub)
232-
result = self.addr_gen.importdescriptors([{"desc": desc_pub, "active": True, "timestamp": "now"}])
243+
result = addr_gen.importdescriptors([{"desc": desc_pub, "active": True, "timestamp": "now"}])
233244
assert(result[0]['success'])
245+
address_type = "bech32m" if "tr" in pattern else "bech32"
234246
for i in range(4):
235-
addr_g = self.addr_gen.getnewaddress(address_type='bech32m')
247+
addr_g = addr_gen.getnewaddress(address_type=address_type)
236248
if treefn is not None:
237249
addr_r = self.make_addr(treefn, keys, i)
238250
assert_equal(addr_g, addr_r)
239-
desc_a = self.addr_gen.getaddressinfo(addr_g)['desc']
251+
desc_a = addr_gen.getaddressinfo(addr_g)['desc']
240252
if desc.startswith("tr("):
241253
assert desc_a.startswith("tr(")
242254
rederive = self.nodes[1].deriveaddresses(desc_a)
243255
assert_equal(len(rederive), 1)
244256
assert_equal(rederive[0], addr_g)
245257

246258
# tr descriptors can be imported
247-
result = self.privs_tr_enabled.importdescriptors([{"desc": desc, "timestamp": "now"}])
259+
result = privs_tr_enabled.importdescriptors([{"desc": desc, "timestamp": "now"}])
248260
assert(result[0]["success"])
249-
result = self.pubs_tr_enabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}])
261+
result = pubs_tr_enabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}])
250262
assert(result[0]["success"])
251263

264+
# Cleanup
265+
privs_tr_enabled.unloadwallet()
266+
pubs_tr_enabled.unloadwallet()
267+
addr_gen.unloadwallet()
268+
252269
def do_test_sendtoaddress(self, comment, pattern, privmap, treefn, keys_pay, keys_change):
253270
self.log.info("Testing %s through sendtoaddress" % comment)
271+
272+
# Create wallets
273+
wallet_uuid = uuid.uuid4().hex
274+
self.nodes[0].createwallet(wallet_name=f"rpc_online_{wallet_uuid}", descriptors=True, blank=True)
275+
rpc_online = self.nodes[0].get_wallet_rpc(f"rpc_online_{wallet_uuid}")
276+
254277
desc_pay = self.make_desc(pattern, privmap, keys_pay)
255278
desc_change = self.make_desc(pattern, privmap, keys_change)
256279
desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True)
257280
desc_change_pub = self.make_desc(pattern, privmap, keys_change, True)
258281
assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub)
259282
assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub)
260-
result = self.rpc_online.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])
283+
result = rpc_online.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])
261284
assert(result[0]['success'])
262-
result = self.rpc_online.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])
285+
result = rpc_online.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])
263286
assert(result[0]['success'])
287+
address_type = "bech32m" if "tr" in pattern else "bech32"
264288
for i in range(4):
265-
addr_g = self.rpc_online.getnewaddress(address_type='bech32m')
289+
addr_g = rpc_online.getnewaddress(address_type=address_type)
266290
if treefn is not None:
267291
addr_r = self.make_addr(treefn, keys_pay, i)
268292
assert_equal(addr_g, addr_r)
269293
boring_balance = int(self.boring.getbalance() * 100000000)
270294
to_amnt = random.randrange(1000000, boring_balance)
271295
self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True)
272296
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
273-
test_balance = int(self.rpc_online.getbalance() * 100000000)
297+
test_balance = int(rpc_online.getbalance() * 100000000)
274298
ret_amnt = random.randrange(100000, test_balance)
275299
# Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.
276-
res = self.rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=Decimal(ret_amnt) / 100000000, subtractfeefromamount=True, fee_rate=200)
300+
res = rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=Decimal(ret_amnt) / 100000000, subtractfeefromamount=True, fee_rate=200)
277301
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
278-
assert(self.rpc_online.gettransaction(res)["confirmations"] > 0)
302+
assert(rpc_online.gettransaction(res)["confirmations"] > 0)
303+
304+
# Cleanup
305+
txid = rpc_online.sendall(recipients=[self.boring.getnewaddress()])["txid"]
306+
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
307+
assert(rpc_online.gettransaction(txid)["confirmations"] > 0)
308+
rpc_online.unloadwallet()
279309

280310
def do_test_psbt(self, comment, pattern, privmap, treefn, keys_pay, keys_change):
281311
self.log.info("Testing %s through PSBT" % comment)
312+
313+
# Create wallets
314+
wallet_uuid = uuid.uuid4().hex
315+
self.nodes[0].createwallet(wallet_name=f"psbt_online_{wallet_uuid}", descriptors=True, disable_private_keys=True, blank=True)
316+
self.nodes[1].createwallet(wallet_name=f"psbt_offline_{wallet_uuid}", descriptors=True, blank=True)
317+
self.nodes[1].createwallet(f"key_only_wallet_{wallet_uuid}", descriptors=True, blank=True)
318+
psbt_online = self.nodes[0].get_wallet_rpc(f"psbt_online_{wallet_uuid}")
319+
psbt_offline = self.nodes[1].get_wallet_rpc(f"psbt_offline_{wallet_uuid}")
320+
key_only_wallet = self.nodes[1].get_wallet_rpc(f"key_only_wallet_{wallet_uuid}")
321+
282322
desc_pay = self.make_desc(pattern, privmap, keys_pay, False)
283323
desc_change = self.make_desc(pattern, privmap, keys_change, False)
284324
desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True)
285325
desc_change_pub = self.make_desc(pattern, privmap, keys_change, True)
286326
assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub)
287327
assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub)
288-
result = self.psbt_online.importdescriptors([{"desc": desc_pay_pub, "active": True, "timestamp": "now"}])
328+
result = psbt_online.importdescriptors([{"desc": desc_pay_pub, "active": True, "timestamp": "now"}])
289329
assert(result[0]['success'])
290-
result = self.psbt_online.importdescriptors([{"desc": desc_change_pub, "active": True, "timestamp": "now", "internal": True}])
330+
result = psbt_online.importdescriptors([{"desc": desc_change_pub, "active": True, "timestamp": "now", "internal": True}])
291331
assert(result[0]['success'])
292-
result = self.psbt_offline.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])
332+
result = psbt_offline.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])
293333
assert(result[0]['success'])
294-
result = self.psbt_offline.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])
334+
result = psbt_offline.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])
295335
assert(result[0]['success'])
336+
for key in keys_pay + keys_change:
337+
result = key_only_wallet.importdescriptors([{"desc": descsum_create(f"wpkh({key['xprv']}/*)"), "timestamp":"now"}])
338+
assert(result[0]["success"])
339+
address_type = "bech32m" if "tr" in pattern else "bech32"
296340
for i in range(4):
297-
addr_g = self.psbt_online.getnewaddress(address_type='bech32m')
341+
addr_g = psbt_online.getnewaddress(address_type=address_type)
298342
if treefn is not None:
299343
addr_r = self.make_addr(treefn, keys_pay, i)
300344
assert_equal(addr_g, addr_r)
301345
boring_balance = int(self.boring.getbalance() * 100000000)
302346
to_amnt = random.randrange(1000000, boring_balance)
303347
self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True)
304348
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
305-
test_balance = int(self.psbt_online.getbalance() * 100000000)
349+
test_balance = int(psbt_online.getbalance() * 100000000)
306350
ret_amnt = random.randrange(100000, test_balance)
307351
# Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.
308-
psbt = self.psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0], "fee_rate": 200, "change_type": "bech32m"})['psbt']
309-
res = self.psbt_offline.walletprocesspsbt(psbt=psbt, finalize=False)
310-
311-
decoded = self.psbt_offline.decodepsbt(res["psbt"])
312-
if pattern.startswith("tr("):
313-
for psbtin in decoded["inputs"]:
314-
assert "non_witness_utxo" not in psbtin
315-
assert "witness_utxo" in psbtin
316-
assert "taproot_internal_key" in psbtin
317-
assert "taproot_bip32_derivs" in psbtin
318-
assert "taproot_key_path_sig" in psbtin or "taproot_script_path_sigs" in psbtin
319-
if "taproot_script_path_sigs" in psbtin:
320-
assert "taproot_merkle_root" in psbtin
321-
assert "taproot_scripts" in psbtin
322-
323-
rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']
352+
psbt = psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0], "fee_rate": 200, "change_type": address_type})['psbt']
353+
res = psbt_offline.walletprocesspsbt(psbt=psbt, finalize=False)
354+
for wallet in [psbt_offline, key_only_wallet]:
355+
res = wallet.walletprocesspsbt(psbt=psbt, finalize=False)
356+
357+
decoded = wallet.decodepsbt(res["psbt"])
358+
if pattern.startswith("tr("):
359+
for psbtin in decoded["inputs"]:
360+
assert "non_witness_utxo" not in psbtin
361+
assert "witness_utxo" in psbtin
362+
assert "taproot_internal_key" in psbtin
363+
assert "taproot_bip32_derivs" in psbtin
364+
assert "taproot_key_path_sig" in psbtin or "taproot_script_path_sigs" in psbtin
365+
if "taproot_script_path_sigs" in psbtin:
366+
assert "taproot_merkle_root" in psbtin
367+
assert "taproot_scripts" in psbtin
368+
369+
rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']
370+
res = self.nodes[0].testmempoolaccept([rawtx])
371+
assert res[0]["allowed"]
372+
324373
txid = self.nodes[0].sendrawtransaction(rawtx)
325374
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
326-
assert(self.psbt_online.gettransaction(txid)['confirmations'] > 0)
375+
assert(psbt_online.gettransaction(txid)['confirmations'] > 0)
376+
377+
# Cleanup
378+
psbt = psbt_online.sendall(recipients=[self.boring.getnewaddress()], options={"psbt": True})["psbt"]
379+
res = psbt_offline.walletprocesspsbt(psbt=psbt, finalize=False)
380+
rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']
381+
txid = self.nodes[0].sendrawtransaction(rawtx)
382+
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
383+
assert(psbt_online.gettransaction(txid)['confirmations'] > 0)
384+
psbt_online.unloadwallet()
385+
psbt_offline.unloadwallet()
327386

328387
def do_test(self, comment, pattern, privmap, treefn):
329388
nkeys = len(privmap)
@@ -333,21 +392,8 @@ def do_test(self, comment, pattern, privmap, treefn):
333392
self.do_test_psbt(comment, pattern, privmap, treefn, keys[2*nkeys:3*nkeys], keys[3*nkeys:4*nkeys])
334393

335394
def run_test(self):
336-
self.log.info("Creating wallets...")
337-
self.nodes[0].createwallet(wallet_name="privs_tr_enabled", descriptors=True, blank=True)
338-
self.privs_tr_enabled = self.nodes[0].get_wallet_rpc("privs_tr_enabled")
339-
self.nodes[0].createwallet(wallet_name="pubs_tr_enabled", descriptors=True, blank=True, disable_private_keys=True)
340-
self.pubs_tr_enabled = self.nodes[0].get_wallet_rpc("pubs_tr_enabled")
341395
self.nodes[0].createwallet(wallet_name="boring")
342-
self.nodes[0].createwallet(wallet_name="addr_gen", descriptors=True, disable_private_keys=True, blank=True)
343-
self.nodes[0].createwallet(wallet_name="rpc_online", descriptors=True, blank=True)
344-
self.nodes[0].createwallet(wallet_name="psbt_online", descriptors=True, disable_private_keys=True, blank=True)
345-
self.nodes[1].createwallet(wallet_name="psbt_offline", descriptors=True, blank=True)
346396
self.boring = self.nodes[0].get_wallet_rpc("boring")
347-
self.addr_gen = self.nodes[0].get_wallet_rpc("addr_gen")
348-
self.rpc_online = self.nodes[0].get_wallet_rpc("rpc_online")
349-
self.psbt_online = self.nodes[0].get_wallet_rpc("psbt_online")
350-
self.psbt_offline = self.nodes[1].get_wallet_rpc("psbt_offline")
351397

352398
self.log.info("Mining blocks...")
353399
gen_addr = self.boring.getnewaddress()
@@ -457,18 +503,5 @@ def run_test(self):
457503
lambda k1: key(k1)
458504
)
459505

460-
self.log.info("Sending everything back...")
461-
462-
txid = self.rpc_online.sendall(recipients=[self.boring.getnewaddress()])["txid"]
463-
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
464-
assert(self.rpc_online.gettransaction(txid)["confirmations"] > 0)
465-
466-
psbt = self.psbt_online.sendall(recipients=[self.boring.getnewaddress()], options={"psbt": True})["psbt"]
467-
res = self.psbt_offline.walletprocesspsbt(psbt=psbt, finalize=False)
468-
rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']
469-
txid = self.nodes[0].sendrawtransaction(rawtx)
470-
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
471-
assert(self.psbt_online.gettransaction(txid)['confirmations'] > 0)
472-
473506
if __name__ == '__main__':
474507
WalletTaprootTest().main()

0 commit comments

Comments
 (0)