Reject witness-only UTXO for legacy inputs; Suppress fee for unverified witness UTXOs;normalize legacy inputs to proper utxo
What changed, and why it matters
This update fixes a bug in the COLDCARD hardware wallet where a malicious or malformed Bitcoin transaction file (PSBT) could trick the device into using the wrong amount of money for a non-SegWit (legacy) input. Specifically, if the file only provided a lightweight 'witness UTXO' for a legacy input, the device would trust that amount. An attacker could lie about the amount, making the transaction look like it had a normal fee when it actually sent much more to the miner or another recipient. The fix rejects such files for legacy inputs, prefers the full previous transaction when both types are present, and suppresses fee calculation when a witness-only UTXO cannot be verified as genuinely SegWit.
Treat this as a security fix and include it in the next firmware release. Users should upgrade when available. Wallet software that builds PSBTs for COLDCARD should ensure legacy inputs include the full non-witness UTXO (the previous transaction), not just a witness UTXO. Review any custom PSBT construction or test helpers for similar assumptions.
Security signals we found
Fixes legacy input amount spoofing via witness-only UTXO
Rejects PSBT inputs that provide only PSBT_IN_WITNESS_UTXO for non-SegWit inputs the device must sign
Prefers full non_witness_utxo over witness_utxo when both are present
Suppresses fee calculation when witness UTXO cannot be verified as SegWit
Adds redeem-script-based proof that a P2SH witness UTXO is genuinely SegWit
Changelog explicitly calls the issue a bugfix and credits @Damir
Evidence from the diff
The patch addresses ‘legacy input amount spoofing’ in shared/psbt.py. Previously, psbtInputProxy.get_utxo() preferred PSBT_IN_WITNESS_UTXO over PSBT_IN_NON_WITNESS_UTXO. For legacy (non-SegWit) inputs, the witness_utxo is not a commitment to the full previous transaction, so a malicious PSBT can supply an arbitrary value/script and the device would use it for amount/fee calculation and signing decisions. The change reorders get_utxo() to prefer the full non_witness_utxo when present, adds witness_utxo_is_provably_segwit() to detect P2SH-P2WPKH/P2SH-P2WSH via redeem script, and raises FatalPSBTIssue if a legacy input that the device must sign has only an unverifiable witness_utxo. It also adds unverified_witness_utxo tracking so total input value/fee is not computed from untrusted data, and emits a warning instead.
Changed components
shared/psbt.pytesting/psbt.pytesting/test_sign.pytesting/test_multisig.pytesting/test_teleport.pyInspect captured patch +224 / −65
diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
index 5951180..ba339b6 100644
--- a/releases/Next-ChangeLog.md
+++ b/releases/Next-ChangeLog.md
@@ -9,6 +9,9 @@ This lists the new changes that have not yet been published in a normal release.
- Enhancement: WIF Store export watch-only descriptor
- Enhancement: WIF Store address detection without the need for PSBT_IN_BIP32_DERIVATION (Electrum support)
- Enhancement: Improve USB length validation
+- Bugfix: Fixes legacy input amount spoofing by rejecting witness-utxo-only PSBT inputs when Coldcard is expected to sign a non-segwit input.
+ When both UTXO fields are present the full non_witness_utxo is now preferred for amount/script lookup. Thanks, @Damir
+- Bugfix: Emit warning and do not calculate fee for legacy UTXOs with only witness utxo
- Bugfix: Disable Virtual Disk and NFC before activating HSM
- Bugfix: Custom address default menu position wrong
- Bugfix: Delta Mode Trick PIN was never restored from backup
diff --git a/shared/psbt.py b/shared/psbt.py
index 599637f..d9eb586 100644
--- a/shared/psbt.py
+++ b/shared/psbt.py
@@ -720,49 +720,58 @@ class psbtInputProxy(psbtProxy):
fd = self.fd
old_pos = fd.tell()
- if self.witness_utxo:
- # Going forward? Just what we will witness; no other junk
- # - 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
+ if self.utxo:
+ # skip over all the parts of the txn we don't care about, without
+ # fully parsing it... pull out a single TXO
+ fd.seek(self.utxo[0])
- fd.seek(self.witness_utxo[0])
- utxo = CTxOut()
- utxo.deserialize(fd)
- fd.seek(old_pos)
+ _, marker, flags = unpack("<iBB", fd.read(6))
+ wit_format = (marker == 0 and flags != 0x0)
+ if not wit_format:
+ # rewind back over marker+flags
+ fd.seek(-2, 1)
- return utxo
+ # How many ins? We accept zero here because utxo's inputs might have been
+ # trimmed to save space, and we have test cases like that.
+ num_in = deser_compact_size(fd)
+ _skip_n_objs(fd, num_in, 'CTxIn')
- assert self.utxo, 'no utxo'
+ num_out = deser_compact_size(fd)
+ assert idx < num_out, "not enuf outs"
+ _skip_n_objs(fd, idx, 'CTxOut')
- # skip over all the parts of the txn we don't care about, without
- # fully parsing it... pull out a single TXO
- fd.seek(self.utxo[0])
+ utxo = CTxOut()
+ utxo.deserialize(fd)
- _, marker, flags = unpack("<iBB", fd.read(6))
- wit_format = (marker == 0 and flags != 0x0)
- if not wit_format:
- # rewind back over marker+flags
- fd.seek(-2, 1)
+ # ... followed by more outs, and maybe witness data, but we don't care ...
- # How many ins? We accept zero here because utxo's inputs might have been
- # trimmed to save space, and we have test cases like that.
- num_in = deser_compact_size(fd)
- _skip_n_objs(fd, num_in, 'CTxIn')
+ fd.seek(old_pos)
- num_out = deser_compact_size(fd)
- assert idx < num_out, "not enuf outs"
- _skip_n_objs(fd, idx, 'CTxOut')
+ return utxo
+
+ assert self.witness_utxo, 'no utxo'
+ fd.seek(self.witness_utxo[0])
utxo = CTxOut()
utxo.deserialize(fd)
-
- # ... followed by more outs, and maybe witness data, but we don't care ...
-
fd.seek(old_pos)
return utxo
+ def witness_utxo_is_provably_segwit(self, utxo):
+ af, addr_or_pubkey, addr_is_segwit = utxo.get_address()
+ if addr_is_segwit:
+ return True
+
+ if af != AF_P2SH or not self.redeem_script:
+ return False
+
+ redeem_script = self.get(self.redeem_script)
+ return redeem_script[0] == 0 and \
+ ((len(redeem_script) == 22 and redeem_script[1] == 20) or
+ (len(redeem_script) == 34 and redeem_script[1] == 32)) and \
+ hash160(redeem_script) == addr_or_pubkey
+
def determine_my_signing_key(self, my_idx, utxo, my_xfp, psbt, cosign_xfp=None):
# See what it takes to sign this particular input
# - type of script
@@ -1831,6 +1840,7 @@ class psbtObject(psbtProxy):
# hashes match, and what values are we getting?
# Important: parse incoming UTXO to build total input value
foreign = []
+ unverified_witness_utxo = []
total_in = 0
from_wif_store = []
prevouts = set()
@@ -1860,12 +1870,18 @@ class psbtObject(psbtProxy):
assert utxo.nValue >= 0, "negative input value: i%d" % i
total_in += utxo.nValue
+ if not inp.utxo and not inp.witness_utxo_is_provably_segwit(utxo):
+ unverified_witness_utxo.append(i)
# Look at what kind of input this will be, and therefore what
# type of signing will be required, and which key we need.
# - also validates redeem_script when present
# - also finds appropriate multisig wallet to be used
inp.determine_my_signing_key(i, utxo, self.my_xfp, self, cosign_xfp)
+
+ if inp.required_key and not inp.is_segwit and not inp.utxo:
+ raise FatalPSBTIssue('Legacy input #%d requires non-witness UTXO' % i)
+
# determine_my_signing_key is updating fully_signed for multisig inputs
# based on redeem/witness script
if inp.fully_signed:
@@ -1898,7 +1914,7 @@ class psbtObject(psbtProxy):
# XXX scan witness data provided, and consider those ins signed if not multisig?
- if not foreign:
+ if not foreign and not unverified_witness_utxo:
# no foreign inputs, we can calculate the total input value
self.total_value_in = total_in
assert total_in > 0 or self.por322, "zero value txn"
@@ -1907,9 +1923,15 @@ class psbtObject(psbtProxy):
# OK for multi-party transactions (coinjoin etc.)
assert not self.por322 # cannot have foreign inputs in POR txn
self.total_value_in = None
- self.warnings.append(
- ("Unable to calculate fee", "Some input(s) haven't provided UTXO(s): " + seq_to_str(foreign))
- )
+ if foreign:
+ self.warnings.append(
+ ("Unable to calculate fee", "Some input(s) haven't provided UTXO(s): " + seq_to_str(foreign))
+ )
+ if unverified_witness_utxo:
+ self.warnings.append(
+ ("Unable to calculate fee", "Some input(s) provided unverified witness UTXO(s): " +
+ seq_to_str(unverified_witness_utxo))
+ )
if len(self.presigned_inputs) == self.num_inputs:
# Maybe wrong for multisig cases? Maybe they want to add their
diff --git a/testing/psbt.py b/testing/psbt.py
index ab7912a..c893a7c 100644
--- a/testing/psbt.py
+++ b/testing/psbt.py
@@ -517,6 +517,42 @@ class BasicPSBT:
def as_b64_str(self):
return b64encode(self.as_bytes()).decode()
+ def convert_witness_utxo_to_utxo(self, idx):
+ # Test helper: the original prev txn cannot be reconstructed from a
+ # witness_utxo, so retarget this PSBT input to a synthetic funding txn.
+ inp = self.inputs[idx]
+ assert inp.witness_utxo
+ assert inp.utxo is None
+
+ prev_txo = CTxOut()
+ prev_txo.deserialize(io.BytesIO(inp.witness_utxo))
+
+ if self.is_v2():
+ assert inp.prevout_idx is not None
+ prevout_idx = inp.prevout_idx
+ else:
+ assert self.parsed_txn
+ txin = self.parsed_txn.vin[idx]
+ prevout_idx = txin.prevout.n
+
+ funding = CTransaction()
+ funding.nVersion = 2
+ funding.vin = [CTxIn(COutPoint(0, 0xffffffff), nSequence=0xffffffff)]
+ funding.vout = [CTxOut(0, b'') for _ in range(prevout_idx)]
+ funding.vout.append(prev_txo)
+ funding.calc_sha256()
+
+ inp.utxo = funding.serialize_with_witness()
+ inp.witness_utxo = None
+
+ if self.is_v2():
+ inp.previous_txid = ser_uint256(funding.sha256)
+ else:
+ txin.prevout.hash = funding.sha256
+ self.txn = self.parsed_txn.serialize_with_witness()
+
+ return funding
+
def to_v2(self):
if self.version is None or self.version == 0:
self.version = 2
diff --git a/testing/test_multisig.py b/testing/test_multisig.py
index 14e02a3..94d5890 100644
--- a/testing/test_multisig.py
+++ b/testing/test_multisig.py
@@ -1279,7 +1279,7 @@ def fake_ms_txn(pytestconfig):
# - but has UTXO's to match needs
from struct import pack
- def doit(num_ins, num_outs, M, keys, fee=10000, outvals=None, segwit_in=False,
+ def doit(num_ins, num_outs, M, keys, fee=10000, outvals=None,
outstyles=['p2pkh'], change_outputs=[], incl_xpubs=False, hack_psbt=None,
hack_change_out=False, input_amount=1E8, psbt_v2=None, bip67=True,
violate_script_key_order=False, path_mapper=None, inp_af=AF_P2WSH,
@@ -1329,21 +1329,14 @@ def fake_ms_txn(pytestconfig):
)
# lots of supporting details needed for p2sh inputs
- if inp_af:
- if inp_af == AF_P2WSH:
- psbt.inputs[i].witness_script = script
- elif inp_af == AF_P2SH:
- psbt.inputs[i].redeem_script = script
- else:
- assert inp_af == AF_P2WSH_P2SH
- psbt.inputs[i].witness_script = script
- psbt.inputs[i].redeem_script = b'\0\x20' + sha256(script).digest()
-
+ if inp_af == AF_P2WSH:
+ psbt.inputs[i].witness_script = script
+ elif inp_af == AF_P2SH:
+ psbt.inputs[i].redeem_script = script
else:
- if segwit_in:
- psbt.inputs[i].witness_script = script
- else:
- psbt.inputs[i].redeem_script = script
+ assert inp_af == AF_P2WSH_P2SH
+ psbt.inputs[i].witness_script = script
+ psbt.inputs[i].redeem_script = b'\0\x20' + sha256(script).digest()
for pubkey, xfp_path in details:
psbt.inputs[i].bip32_paths[pubkey] = b''.join(pack('<I', j) for j in xfp_path)
@@ -1359,10 +1352,10 @@ def fake_ms_txn(pytestconfig):
supply.vout.append(CTxOut(int(input_amount), scriptPubKey))
- if not segwit_in:
- psbt.inputs[i].utxo = supply.serialize_with_witness()
- else:
+ if inp_af in [AF_P2WSH, AF_P2WSH_P2SH]:
psbt.inputs[i].witness_utxo = supply.vout[-1].serialize()
+ else:
+ psbt.inputs[i].utxo = supply.serialize_with_witness()
if lock_time and not i:
seq = 0xfffffffd
@@ -1532,9 +1525,9 @@ def test_1of1_multisig_sign(finalize, clear_ms, import_ms_wallet, fake_ms_txn, s
@pytest.mark.bitcoind
@pytest.mark.parametrize('num_ins', [ 15 ])
@pytest.mark.parametrize('M', [ 2, 4, 1])
-@pytest.mark.parametrize('segwit', [True, False])
+@pytest.mark.parametrize('addr_fmt', [AF_P2SH, AF_P2WSH])
@pytest.mark.parametrize('incl_xpubs', [ True, False ])
-def test_ms_sign_myself(M, use_regtest, make_myself_wallet, segwit, num_ins, dev, clear_ms,
+def test_ms_sign_myself(M, use_regtest, make_myself_wallet, addr_fmt, num_ins, dev, clear_ms,
fake_ms_txn, try_sign, incl_xpubs, bitcoind, sim_root_dir):
# IMPORTANT: wont work if you start simulator with --ms flag. Use no args
@@ -1550,9 +1543,9 @@ def test_ms_sign_myself(M, use_regtest, make_myself_wallet, segwit, num_ins, dev
N = len(keys)
assert M<=N
- psbt = fake_ms_txn(num_ins, num_outs, M, keys, segwit_in=segwit, incl_xpubs=incl_xpubs,
+ psbt = fake_ms_txn(num_ins, num_outs, M, keys, incl_xpubs=incl_xpubs,
outstyles=all_out_styles, change_outputs=list(range(1,num_outs)),
- inp_af=AF_P2SH)
+ inp_af=addr_fmt)
with open(f'{sim_root_dir}/debug/myself-before.psbt', 'w') as f:
f.write(b64encode(psbt).decode())
@@ -3114,7 +3107,7 @@ def test_ms_wallet_ordering(clear_ms, import_ms_wallet, try_sign_microsd, fake_m
name = f'ms2'
keys3 = import_ms_wallet(3, 5, name=name, accept=1, do_import=True, addr_fmt="p2wsh")
- psbt = fake_ms_txn(5, 5, 3, keys3, outstyles=all_out_styles, segwit_in=True, incl_xpubs=True)
+ psbt = fake_ms_txn(5, 5, 3, keys3, outstyles=all_out_styles, incl_xpubs=True)
try_sign_microsd(psbt, encoding='base64')
@@ -3135,12 +3128,12 @@ def test_ms_xpub_ordering(descriptor, m_n, clear_ms, make_multisig, import_ms_wa
import_ms_wallet(M, N, keys=opt, name=name, accept=1, do_import=True,
addr_fmt="p2wsh", descriptor=descriptor)
psbt = fake_ms_txn(5, 5, M, opt, outstyles=all_out_styles,
- segwit_in=True, incl_xpubs=True)
+ incl_xpubs=True)
try_sign_microsd(psbt, encoding='base64')
for opt_1 in all_options:
# create PSBT with original keys order
psbt = fake_ms_txn(5, 5, M, opt_1, outstyles=all_out_styles,
- segwit_in=True, incl_xpubs=True)
+ incl_xpubs=True)
try_sign_microsd(psbt, encoding='base64')
@@ -4102,7 +4095,7 @@ def test_change_output_script_type(clear_ms, import_ms_wallet, start_sign, end_s
sign_check(psbt)
psbt = fake_ms_txn(2, 2, M, keys, force_outstyle="p2sh-p2wsh",
- change_outputs=[0,1], inp_af=AF_P2SH, segwit_in=True)
+ change_outputs=[0,1], inp_af=AF_P2SH)
sign_check(psbt)
diff --git a/testing/test_sign.py b/testing/test_sign.py
index 1c0f387..1c63a58 100644
--- a/testing/test_sign.py
+++ b/testing/test_sign.py
@@ -559,6 +559,9 @@ def test_change_case(start_sign, use_regtest, end_sign, check_against_bitcoind,
chg_addr = 'mvBGHpVtTyjmcfSsy6f715nbTGvwgbgbwo'
psbt = open('data/example-change.psbt', 'rb').read()
+ b4 = BasicPSBT().parse(psbt)
+ b4.convert_witness_utxo_to_utxo(0)
+ psbt = b4.as_bytes()
start_sign(psbt)
@@ -568,7 +571,6 @@ def test_change_case(start_sign, use_regtest, end_sign, check_against_bitcoind,
assert split_sory[0] == "Change back:"
assert chg_addr == addr_from_display_format(split_sory[-1])
- b4 = BasicPSBT().parse(psbt)
check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[1,])
signed = end_sign(True)
@@ -610,6 +612,7 @@ def test_change_fraud_path(start_sign, use_regtest, end_sign, case, check_agains
psbt = open('data/example-change.psbt', 'rb').read()
b4 = BasicPSBT().parse(psbt)
+ b4.convert_witness_utxo_to_utxo(0)
(pubkey, path), = b4.outputs[1].bip32_paths.items()
skp = bytearray(b4.outputs[1].bip32_paths[pubkey])
@@ -654,6 +657,7 @@ def test_change_fraud_addr(start_sign, end_sign, use_regtest, check_against_bitc
psbt = open('data/example-change.psbt', 'rb').read()
b4 = BasicPSBT().parse(psbt)
+ b4.convert_witness_utxo_to_utxo(0)
# tweak output addr to garbage
t = CTransaction()
@@ -691,6 +695,7 @@ def test_change_p2sh_p2wpkh(start_sign, end_sign, check_against_bitcoind, use_re
psbt = f.read()
b4 = BasicPSBT().parse(psbt)
+ b4.convert_witness_utxo_to_utxo(0)
t = CTransaction()
t.deserialize(BytesIO(b4.txn))
@@ -840,15 +845,16 @@ def test_sign_multisig_partial_fail(start_sign, end_sign):
def test_sign_wutxo(start_sign, set_seed_words, end_sign, cap_story, sim_exec, sim_execfile,
sim_root_dir):
- # Example from SomberNight: we can sign it, but signature won't be accepted by
- # network because the PSBT lies about the UTXO amount and tries to give away to miners,
- # as overly-large fee.
+ # Example from SomberNight, normalized to use a full UTXO so this test still
+ # reaches the fee display path after legacy witness-only UTXOs are rejected.
set_seed_words('fault lava rice chest uncle exclude power tornado catalog stool'
' swear rival sun aspect oyster deer pepper exchange scrap toward'
' mix second world shaft')
- in_psbt = a2b_hex(open('data/snight-example.psbt', 'rb').read()[:-1])
+ snight = BasicPSBT().parse(a2b_hex(open('data/snight-example.psbt', 'rb').read()[:-1]))
+ snight.convert_witness_utxo_to_utxo(0)
+ in_psbt = snight.as_bytes()
for fin in (False, True):
start_sign(in_psbt, finalize=fin)
@@ -1136,6 +1142,7 @@ def test_change_troublesome(dev, start_sign, cap_story, try_path, expect, sim_ro
psbt = open('data/example-change.psbt', 'rb').read()
b4 = BasicPSBT().parse(psbt)
+ b4.convert_witness_utxo_to_utxo(0)
pubkey = a2b_hex('03c80814536f8e801859fc7c2e5129895b261153f519d4f3418ffb322884a7d7e1')
path = [int(p) if ("'" not in p) else 0x80000000+int(p[:-1])
@@ -1769,6 +1776,104 @@ def test_own_utxo_missing(segwit_in, num_missing, dev, fake_txn, start_sign, cap
assert "Missing own UTXO(s)" in story
press_cancel()
+def _replace_input_utxo_with_witness_utxo(psbt, idx):
+ inp = psbt.inputs[idx]
+ txn = CTransaction()
+ txn.deserialize(BytesIO(inp.utxo))
+ assert len(txn.vout) == 1
+ inp.witness_utxo = txn.vout[0].serialize()
+ inp.utxo = None
+
+def test_nested_segwit_witness_utxo_only_fee_shown(dev, fake_txn, start_sign,
+ cap_story, end_sign):
+ psbt = fake_txn(1, 2, dev.master_xpub, segwit_in=True, wrapped=True)
+ start_sign(psbt)
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "OK TO SEND?"
+ assert "Network fee" in story
+ assert "unverified witness UTXO" not in story
+ end_sign(accept=True)
+
+def test_own_legacy_witness_utxo_only_fails(dev, fake_txn, start_sign, cap_story, press_cancel):
+ def hack(psbt):
+ _replace_input_utxo_with_witness_utxo(psbt, 0)
+
+ psbt = fake_txn(1, 2, dev.master_xpub, segwit_in=False, psbt_hacker=hack)
+ start_sign(psbt)
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "Failure"
+ assert "Legacy input #0 requires non-witness UTXO" in story
+ press_cancel()
+
+def test_foreign_legacy_witness_utxo_only_ok(dev, fake_txn, start_sign, cap_story, end_sign):
+ def hack(psbt):
+ pk = list(psbt.inputs[1].bip32_paths.keys())[0]
+ pp = psbt.inputs[1].bip32_paths[pk]
+ psbt.inputs[1].bip32_paths[pk] = b'what' + pp[4:]
+ _replace_input_utxo_with_witness_utxo(psbt, 1)
+
+ psbt = fake_txn(2, 2, dev.master_xpub, segwit_in=False, psbt_hacker=hack)
+ start_sign(psbt)
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "OK TO SEND?"
+ assert "Limited Signing" in story
+ assert "Unable to calculate fee" in story
+ assert "Some input(s) provided unverified witness UTXO(s): 1" in story
+ assert "Legacy input #1 requires non-witness UTXO" not in story
+ signed = end_sign(accept=True)
+ assert signed != psbt
+
+def test_mismatched_p2sh_witness_program_unverified(dev, fake_txn, start_sign,
+ cap_story, end_sign):
+ def hack(psbt):
+ pk = list(psbt.inputs[1].bip32_paths.keys())[0]
+ pp = psbt.inputs[1].bip32_paths[pk]
+ psbt.inputs[1].bip32_paths[pk] = b'what' + pp[4:]
+
+ inp = psbt.inputs[1]
+ txn = CTransaction()
+ txn.deserialize(BytesIO(inp.utxo))
+ assert len(txn.vout) == 1
+
+ redeem_script = bytes([0, 20]) + os.urandom(32)
+ inp.redeem_script = redeem_script
+ inp.witness_utxo = CTxOut(txn.vout[0].nValue,
+ bytes([0xa9, 0x14]) + hash160(redeem_script) + bytes([0x87])).serialize()
+ inp.utxo = None
+
+ psbt = fake_txn(2, 2, dev.master_xpub, segwit_in=False, psbt_hacker=hack)
+ start_sign(psbt)
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "OK TO SEND?"
+ assert "Limited Signing" in story
+ assert "Unable to calculate fee" in story
+ assert "Some input(s) provided unverified witness UTXO(s): 1" in story
+ assert "Network fee" not in story
+ signed = end_sign(accept=True)
+ assert signed != psbt
+
+def test_presigned_own_legacy_witness_utxo_only_ok(dev, fake_txn, start_sign, cap_story, end_sign):
+ def hack(psbt):
+ pubkey = list(psbt.inputs[1].bip32_paths.keys())[0]
+ psbt.inputs[1].part_sigs[pubkey] = os.urandom(71)
+ _replace_input_utxo_with_witness_utxo(psbt, 1)
+
+ psbt = fake_txn(2, 2, dev.master_xpub, segwit_in=False, psbt_hacker=hack)
+ start_sign(psbt)
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "OK TO SEND?"
+ assert "Partly Signed Already" in story
+ assert "Unable to calculate fee" in story
+ assert "Some input(s) provided unverified witness UTXO(s): 1" in story
+ assert "Legacy input #1 requires non-witness UTXO" not in story
+ signed = end_sign(accept=True)
+ assert signed != psbt
+
@pytest.mark.bitcoind
def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_path, try_sign):
# batch tx created from three different psbts (using joinpsbts)
diff --git a/testing/test_teleport.py b/testing/test_teleport.py
index 385bc15..37be0f4 100644
--- a/testing/test_teleport.py
+++ b/testing/test_teleport.py
@@ -734,7 +734,7 @@ def test_teleport_real_ms(dev, fake_ms_txn):
# match the default paths created by CC in airgapped MS wallet creation.
return str_to_path(deriv)
- psbt = fake_ms_txn(3, 2, M, keys, fee=10000, outvals=None, segwit_in=False,
+ psbt = fake_ms_txn(3, 2, M, keys, fee=10000, outvals=None, inp_af=AF_P2WSH,
outstyles=['p2pkh'], change_outputs=[], incl_xpubs=False,
hack_change_out=False, input_amount=1E8, path_mapper=p2wsh_mapper)
Why this scored 78/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.