What changed, and why it matters
This commit only updates the COLDCARD firmware's internal test suite to remain compatible with Bitcoin Core version 32. It adds helper methods to read and set locktime in a way that works for both old PSBT version 0/1 and new PSBT version 2, and changes test calls to Bitcoin Core so they request PSBT version 0 when the newer Core would otherwise default to version 2. There is no change to the actual firmware code that runs on the device, and no security vulnerability is fixed or introduced.
No security action needed. This is a test-infrastructure compatibility update. Reviewers may verify that CI passes against both old and new Bitcoin Core versions.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff is confined to testing/psbt.py and three test files. testing/psbt.py gains get_locktime()/set_locktime() abstractions on BasicPSBT so tests can inspect/modify nLockTime regardless of whether the PSBT is v0/v1 (parsed_txn.nLockTime) or v2 (fallback_locktime). test_sign.py wraps walletcreatefundedpsbt calls to pass explicit psbt_version=0 on Core v32, falling back to the old call signature on older Core. test_ccc.py and test_sssp.py replace direct parsed_txn.nLockTime access with the new helpers. No firmware runtime code is modified.
Changed components
testing/psbt.pytesting/test_ccc.pytesting/test_sign.pytesting/test_sssp.pyInspect captured patch +58 / −20
### testing/psbt.py
@@ -375,6 +375,19 @@ def __eq__(a, b):
def is_v2(self):
return (self.version == 2) or (not self.txn)
+ def get_locktime(self):
+ if self.is_v2():
+ return self.fallback_locktime or 0
+
+ return self.parsed_txn.nLockTime
+
+ def set_locktime(self, locktime):
+ if self.is_v2():
+ self.fallback_locktime = locktime
+ else:
+ self.parsed_txn.nLockTime = locktime
+ self.txn = self.parsed_txn.serialize_with_witness()
+
def parse(self, raw):
if isinstance(raw, str):
raw = raw.encode('ascii')
### testing/test_ccc.py
@@ -819,7 +819,7 @@ def test_ccc_velocity(velocity_mi, setup_ccc, ccc_ms_setup, bitcoind, settings_s
init_block_height) # nLockTime set to current block height
psbt = psbt_resp.get("psbt")
po = BasicPSBT().parse(base64.b64decode(psbt))
- assert po.parsed_txn.nLockTime == init_block_height
+ assert po.get_locktime() == init_block_height
policy_sign(bitcoind_wo, psbt) # success as this is first tx that sets block height from 0
assert settings_get("ccc")["pol"]["block_h"] == init_block_height
@@ -832,7 +832,7 @@ def test_ccc_velocity(velocity_mi, setup_ccc, ccc_ms_setup, bitcoind, settings_s
block_height)
psbt = psbt_resp.get("psbt")
po = BasicPSBT().parse(base64.b64decode(psbt))
- assert po.parsed_txn.nLockTime == block_height
+ assert po.get_locktime() == block_height
policy_sign(bitcoind_wo, psbt, violation="velocity")
assert settings_get("ccc")["pol"]["block_h"] == init_block_height # still initial block height as above failed
@@ -844,7 +844,7 @@ def test_ccc_velocity(velocity_mi, setup_ccc, ccc_ms_setup, bitcoind, settings_s
block_height)
psbt = psbt_resp.get("psbt")
po = BasicPSBT().parse(base64.b64decode(psbt))
- assert po.parsed_txn.nLockTime == block_height
+ assert po.get_locktime() == block_height
policy_sign(bitcoind_wo, psbt) # success
assert settings_get("ccc")["pol"]["block_h"] == block_height # updated block height
@@ -922,7 +922,7 @@ def test_ccc_warnings(setup_ccc, ccc_ms_setup, bitcoind, settings_set, policy_si
init_block_height, {"fee_rate":39000})
psbt = psbt_resp.get("psbt")
po = BasicPSBT().parse(base64.b64decode(psbt))
- assert po.parsed_txn.nLockTime == init_block_height
+ assert po.get_locktime() == init_block_height
policy_sign(bitcoind_wo, psbt, violation="has warnings", num_warn=2, warn_list=["Big Fee"])
# invalidate nLockTime with use of nSequence max values
@@ -940,9 +940,8 @@ def test_ccc_warnings(setup_ccc, ccc_ms_setup, bitcoind, settings_set, policy_si
psbt_resp = bitcoind_wo.walletcreatefundedpsbt(ins, [{whitelist[0]: 0.06},{whitelist[1]: 0.01},{whitelist[2]: 0.03}],
0, {"fee_rate":2, "replaceable": False}) # locktime needs to be zero, otherwise exception from core (contradicting parameters)
po = BasicPSBT().parse(base64.b64decode(psbt_resp.get("psbt")))
- assert po.parsed_txn.nLockTime == 0
- po.parsed_txn.nLockTime = init_block_height # add locktime
- po.txn = po.parsed_txn.serialize_with_witness()
+ assert po.get_locktime() == 0
+ po.set_locktime(init_block_height) # add locktime
policy_sign(bitcoind_wo, po.as_b64_str(), violation="has warnings", num_warn=2, warn_list=["Bad Locktime"])
# exotic sighash warning
### testing/test_sign.py
@@ -775,7 +775,19 @@ def test_wrong_p2sh_p2wpkh(bitcoind, start_sign, end_sign, bitcoind_d_sim_watch,
utxos = sim.listunspent()
assert len(utxos) == 1
conso_addr = sim.getnewaddress("", "legacy")
- psbt_resp = sim.walletcreatefundedpsbt([], [{conso_addr: 1}], 0, {"fee_rate": 2, "change_type": "bech32"})
+ try:
+ # new in Core v32 - default PSBT version is now 2
+ # old core version fails with "too many arguemnts"
+ # does not know the last one "psbt_version=0"
+ psbt_resp = sim.walletcreatefundedpsbt([], [{conso_addr: 1}], 0,
+ {"fee_rate": 2, "change_type": "bech32"},
+ True, # BIP-32 paths
+ 2, # txn version
+ 0) # PSBT version
+ except:
+ psbt_resp = sim.walletcreatefundedpsbt([], [{conso_addr: 1}], 0,
+ {"fee_rate": 2, "change_type": "bech32"})
+
psbt = psbt_resp.get("psbt")
b4 = BasicPSBT().parse(base64.b64decode(psbt))
t = CTransaction()
@@ -1900,8 +1912,16 @@ def test_bitcoind_missing_foreign_utxo(bitcoind, bitcoind_d_sim_watch, microsd_p
psbt_list = []
for w in (alice, bob, cc):
assert w.listunspent()
- psbt = w.walletcreatefundedpsbt([], [{dest_address: 1.0}], 0, {"fee_rate": 20})["psbt"]
- psbt_list.append(psbt)
+ try:
+ # TODO when joinpsbt is fixed to operate on PSBT v2 - remove
+ res = w.walletcreatefundedpsbt([], [{dest_address: 1.0}], 0, {"fee_rate": 20},
+ True, # BIP-32 paths
+ 2, # txn version
+ 0) # PSBT version
+ except:
+ res = w.walletcreatefundedpsbt([], [{dest_address: 1.0}], 0, {"fee_rate": 20})
+
+ psbt_list.append(res["psbt"])
# join PSBTs to one
the_psbt = bitcoind.supply_wallet.joinpsbts(psbt_list)
@@ -3580,8 +3600,15 @@ def test_finalize_with_foreign_inputs(bitcoind, bitcoind_d_sim_watch, start_sign
psbt_list = []
for w in (alice, bob, cc):
assert w.listunspent()
- psbt = w.walletcreatefundedpsbt([], [{dest_address: 1.0}], 0, {"fee_rate": 20})["psbt"]
- psbt_list.append(psbt)
+ try:
+ # TODO when joinpsbt is fixed to operate on PSBT v2 - remove
+ res = w.walletcreatefundedpsbt([], [{dest_address: 1.0}], 0, {"fee_rate": 20},
+ True, # BIP-32 paths
+ 2, # txn version
+ 0) # PSBT version
+ except:
+ res = w.walletcreatefundedpsbt([], [{dest_address: 1.0}], 0, {"fee_rate": 20})
+ psbt_list.append(res["psbt"])
# join PSBTs to one
the_psbt = bitcoind.supply_wallet.joinpsbts(psbt_list)
### testing/test_sssp.py
@@ -380,7 +380,7 @@ def test_velocity(velocity_mi, setup_sssp, bitcoind, settings_set, pick_menu_ite
init_block_height) # nLockTime set to current block height
psbt = psbt_resp.get("psbt")
po = BasicPSBT().parse(base64.b64decode(psbt))
- assert po.parsed_txn.nLockTime == init_block_height
+ assert po.get_locktime() == init_block_height
policy_sign(wo, psbt) # success as this is first tx that sets block height from 0
assert settings_get("sssp")["pol"]["block_h"] == init_block_height
@@ -393,7 +393,7 @@ def test_velocity(velocity_mi, setup_sssp, bitcoind, settings_set, pick_menu_ite
block_height)
psbt = psbt_resp.get("psbt")
po = BasicPSBT().parse(base64.b64decode(psbt))
- assert po.parsed_txn.nLockTime == block_height
+ assert po.get_locktime() == block_height
policy_sign(wo, psbt, violation="velocity")
assert settings_get("sssp")["pol"]["block_h"] == init_block_height # still initial block height as above failed
@@ -405,7 +405,7 @@ def test_velocity(velocity_mi, setup_sssp, bitcoind, settings_set, pick_menu_ite
block_height)
psbt = psbt_resp.get("psbt")
po = BasicPSBT().parse(base64.b64decode(psbt))
- assert po.parsed_txn.nLockTime == block_height
+ assert po.get_locktime() == block_height
policy_sign(wo, psbt) # success
assert settings_get("sssp")["pol"]["block_h"] == block_height # updated block height
@@ -470,7 +470,7 @@ def test_warnings(setup_sssp, bitcoind, settings_set, policy_sign, pick_menu_ite
init_block_height, {"fee_rate":48000})
psbt = psbt_resp.get("psbt")
po = BasicPSBT().parse(base64.b64decode(psbt))
- assert po.parsed_txn.nLockTime == init_block_height
+ assert po.get_locktime() == init_block_height
policy_sign(wo, psbt, violation="has warnings")
# invalidate nLockTime with use of nSequence max values
@@ -488,9 +488,8 @@ def test_warnings(setup_sssp, bitcoind, settings_set, policy_sign, pick_menu_ite
psbt_resp = wo.walletcreatefundedpsbt(ins, [{whitelist[0]: 0.06},{whitelist[1]: 0.01},{whitelist[2]: 0.03}],
0, {"fee_rate":2, "replaceable": False}) # locktime needs to be zero, otherwise exception from core (contradicting parameters)
po = BasicPSBT().parse(base64.b64decode(psbt_resp.get("psbt")))
- assert po.parsed_txn.nLockTime == 0
- po.parsed_txn.nLockTime = init_block_height # add locktime
- po.txn = po.parsed_txn.serialize_with_witness()
+ assert po.get_locktime() == 0
+ po.set_locktime(init_block_height) # add locktime
# num_warn=2, warn_list=["Bad Locktime"]
policy_sign(wo, po.as_b64_str(), violation="has warnings")
@@ -608,7 +607,7 @@ def test_deltamode_signature(active_policy, setup_sssp, bitcoind, settings_set,
psbt = psbt_resp.get("psbt")
po = BasicPSBT().parse(base64.b64decode(psbt))
- assert po.parsed_txn.nLockTime == init_block_height
+ assert po.get_locktime() == init_block_height
start_sign(base64.b64decode(psbt), finalize=True)
signed = end_sign(accept=True, finalize=True)Why this scored 15/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.