diff --git a/shared/psbt.py b/shared/psbt.py index da4186d2..3f3245fa 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -613,7 +613,6 @@ class psbtInputProxy(psbtProxy): # - prefer this format, altho does that imply segwit txn must be generated? # - I don't know why we wouldn't always use this # - once we use this partial utxo data, we must create witness data out - self.is_segwit = True fd.seek(self.witness_utxo[0]) utxo = CTxOut() @@ -1698,7 +1697,6 @@ class psbtObject(psbtProxy): rv.update(ser_compact_size(num_inputs)) for in_idx, txi in self.input_iter(): if in_idx == replace_idx: - assert not self.inputs[in_idx].witness_utxo assert not self.inputs[in_idx].is_segwit assert replacement.scriptSig rv.update(replacement.serialize()) diff --git a/testing/test_multisig.py b/testing/test_multisig.py index cf5f9427..2a6f07bd 100644 --- a/testing/test_multisig.py +++ b/testing/test_multisig.py @@ -19,6 +19,7 @@ from helpers import path_to_str, str_to_path, slip132undo from struct import unpack, pack from constants import * from pycoin.key.BIP32Node import BIP32Node +from pycoin.tx import Tx from io import BytesIO from hashlib import sha256 @@ -2088,6 +2089,111 @@ def test_bitcoind_ms_address(change, descriptor, M_N, addr_fmt, clear_ms, goto_h assert bitcoind_addrs[idx].endswith(_end) +@pytest.mark.bitcoind +def test_legacy_multisig_witness_utxo_in_psbt(bitcoind, use_regtest, clear_ms, microsd_wipe, goto_home, need_keypress, + pick_menu_item, cap_story, load_export, microsd_path, cap_menu, try_sign): + use_regtest() + clear_ms() + microsd_wipe() + M,N = 2,2 + cosigner = bitcoind.create_wallet(wallet_name=f"bitcoind--signer-wit-utxo", disable_private_keys=False, blank=False, + passphrase=None, avoid_reuse=False, descriptors=True) + ms = bitcoind.create_wallet( + wallet_name=f"watch_only_legacy_2of2", disable_private_keys=True, + blank=True, passphrase=None, avoid_reuse=False, descriptors=True + ) + goto_home() + pick_menu_item('Settings') + pick_menu_item('Multisig Wallets') + pick_menu_item('Export XPUB') + time.sleep(0.5) + title, story = cap_story() + assert "extended public keys (XPUB) you would need to join a multisig wallet" in story + need_keypress("y") + need_keypress("0") # account + need_keypress("y") + xpub_obj = load_export("sd", label="Multisig XPUB", is_json=True, sig_check=False) + template = xpub_obj["p2sh_desc"] + # get key from bitcoind cosigner + target_desc = "" + bitcoind_descriptors = cosigner.listdescriptors()["descriptors"] + for desc in bitcoind_descriptors: + if desc["desc"].startswith("pkh(") and desc["internal"] is False: + target_desc = desc["desc"] + core_desc, checksum = target_desc.split("#") + # remove pkh(....) + core_key = core_desc[4:-1] + desc = template.replace("M", str(M), 1).replace("...", core_key) + desc_info = ms.getdescriptorinfo(desc) + desc_w_checksum = desc_info["descriptor"] # with checksum + name = f"core{M}of{N}_legacy.txt" + with open(microsd_path(name), "w") as f: + f.write(desc_w_checksum + "\n") + goto_home() + pick_menu_item('Settings') + pick_menu_item('Multisig Wallets') + pick_menu_item('Import from File') + time.sleep(0.3) + _, story = cap_story() + if "Press (1) to import multisig wallet file from SD Card" in story: + # in case Vdisk is enabled + need_keypress("1") + time.sleep(0.5) + need_keypress("y") + pick_menu_item(name) + _, story = cap_story() + assert "Create new multisig wallet?" in story + assert name.split(".")[0] in story + assert f"{M} of {N}" in story + assert f"All {N} co-signers must approve spends" in story + assert "P2SH" in story + assert "Derivation:\n Varies (2)" in story + need_keypress("y") # approve multisig import + goto_home() + pick_menu_item('Settings') + pick_menu_item('Multisig Wallets') + menu = cap_menu() + pick_menu_item(menu[0]) # pick imported descriptor multisig wallet + pick_menu_item("Descriptors") + pick_menu_item("Bitcoin Core") + text = load_export("sd", label="Bitcoin Core multisig setup", is_json=False, sig_check=False) + text = text.replace("importdescriptors ", "").strip() + # remove junk + r1 = text.find("[") + r2 = text.find("]", -1, 0) + text = text[r1: r2] + core_desc_object = json.loads(text) + # import descriptors to watch only wallet + res = ms.importdescriptors(core_desc_object) + for obj in res: + assert obj["success"], obj + # send to address type + addr_type = "legacy" + multi_addr = ms.getnewaddress("", addr_type) + bitcoind.supply_wallet.sendtoaddress(address=multi_addr, amount=49) + bitcoind.supply_wallet.generatetoaddress(101, bitcoind.supply_wallet.getnewaddress()) # mining + dest_addr = ms.getnewaddress("", addr_type) + assert all([addr.startswith("2") for addr in [multi_addr, dest_addr]]) + # create funded PSBT + psbt_resp = ms.walletcreatefundedpsbt( + [], [{dest_addr: 5}], 0, {"fee_rate": 20, "change_type": addr_type, "subtractFeeFromOutputs": [0]} + ) + psbt = psbt_resp.get("psbt") + import base64 + o = BasicPSBT().parse(base64.b64decode(psbt)) + assert len(o.inputs) == 1 + non_witness_utxo = o.inputs[0].utxo + from io import BytesIO + parsed_tx = Tx.Tx.parse(BytesIO(non_witness_utxo)) + witness_utxo = BytesIO() + for oo in parsed_tx.txs_out: + if oo.coin_value == 4900000000: + parsed_tx.txs_out[0].stream(witness_utxo) + o.inputs[0].witness_utxo = witness_utxo.getvalue() + updated = o.as_bytes() + try_sign(updated) + + @pytest.mark.bitcoind @pytest.mark.parametrize("m_n", [(2,2), (3, 5), (15, 15)]) @pytest.mark.parametrize("desc_type", ["p2wsh_desc", "p2sh_p2wsh_desc", "p2sh_desc"])