[Policy] Support raw transaction versions == 3
What changed, and why it matters
This commit updates the COLDCARD Bitcoin hardware wallet firmware to accept and sign transactions that use version 3 of the Bitcoin transaction format. Previously, the wallet only allowed versions 1 and 2. The change is framed as a new feature, not a security fix. It adds parsing and test support for v3 transactions, including a special case for 'ephemeral anchor' outputs that have a zero value and are expected to trigger a warning.
Review whether Bitcoin transaction version 3 introduces new consensus or policy rules (e.g., relative time locks, ephemeral anchors, or package relay behavior) that require additional validation beyond the version-number check. Confirm that existing fee, output-value, and locktime checks remain correct for v3 transactions before release.
Security signals we found
Transaction version whitelist expanded from {1,2} to {1,2,3}
New test covers v3 PSBT parsing and signing paths
Ephemeral anchor test vector triggers expected 'Non-standard zero value output(s)' warning
No additional input/output validation logic added alongside version expansion
ChangeLog labels this as a 'New Feature', not a security fix
Evidence from the diff
The patch modifies shared/psbt.py to accept txn_version values of 1, 2, or 3 instead of only 1 and 2. It updates test helpers in testing/conftest.py and testing/psbt.py to allow version 3, and adds two new tests in testing/test_sign.py: one that constructs synthetic v1/v2 PSBTs with nVersion=3, and one that signs real v3 PSBT test vectors including an ephemeral anchor output. The change is additive and does not alter existing validation logic for versions 1 and 2.
Changed components
shared/psbt.pytesting/conftest.pytesting/psbt.pytesting/test_sign.pyInspect captured patch +87 / −3
diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md
index 5bcf6d9..d8b3dc8 100644
--- a/releases/Next-ChangeLog.md
+++ b/releases/Next-ChangeLog.md
@@ -6,6 +6,7 @@ This lists the new changes that have not yet been published in a normal release.
- New Feature: Export [BIP-380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki) extended key expression.
Navigate to `Advanced/Tools -> Export Wallet -> Key Expression`
+- New Feature: Support for v3 transactions
# Mk4 Specific Changes
diff --git a/shared/psbt.py b/shared/psbt.py
index 76c5acc..1ffe9a6 100644
--- a/shared/psbt.py
+++ b/shared/psbt.py
@@ -1138,7 +1138,7 @@ class psbtObject(psbtProxy):
self.txn_version, marker, flags = unpack("<iBB", fd.read(6))
self.had_witness = (marker == 0 and flags != 0x0)
- assert self.txn_version in {1,2}, "bad txn version"
+ assert self.txn_version in {1,2,3}, "bad txn version"
if not self.had_witness:
# rewind back over marker+flags
diff --git a/testing/conftest.py b/testing/conftest.py
index 1bd5d76..651b062 100644
--- a/testing/conftest.py
+++ b/testing/conftest.py
@@ -1477,7 +1477,7 @@ def end_sign(dev, need_keypress, press_cancel):
assert res[0:4] != b'psbt', 'still a PSBT, but asked for finalize'
t = CTransaction()
t.deserialize(io.BytesIO(res))
- assert t.nVersion in [1, 2]
+ assert t.nVersion in [1, 2, 3]
# TODO: pull out signatures from signed txn
diff --git a/testing/psbt.py b/testing/psbt.py
index ca556d7..dbcf509 100644
--- a/testing/psbt.py
+++ b/testing/psbt.py
@@ -432,7 +432,7 @@ class BasicPSBT:
assert self.txn, 'v0: missing reqd section - PSBT_GLOBAL_UNSIGNED_TX'
elif self.version == 2:
# tx version needs to be at least 2 because locktimes
- assert self.txn_version == 2, 'v2: missing reqd section - PSBT_GLOBAL_TX_VERSION'
+ assert self.txn_version in {2, 3}, 'v2: missing reqd section - PSBT_GLOBAL_TX_VERSION'
assert self.input_count is not None, 'v2: missing reqd section - PSBT_GLOBAL_INPUT_COUNT'
assert self.output_count is not None, 'v2: missing reqd section - PSBT_GLOBAL_OUTPUT_COUNT'
diff --git a/testing/test_sign.py b/testing/test_sign.py
index 0e581a2..e337cec 100644
--- a/testing/test_sign.py
+++ b/testing/test_sign.py
@@ -3411,4 +3411,87 @@ def test_finalize_with_foreign_inputs(bitcoind, bitcoind_d_sim_watch, start_sign
assert title == "PSBT Signed"
assert "Finalized transaction (ready for broadcast)" in story
+
+@pytest.mark.parametrize("psbt_v2", [False, True])
+def test_txn_v3(psbt_v2, fake_txn, start_sign, end_sign, cap_story):
+
+ psbt = fake_txn(2, 2, segwit_in=True, change_outputs=[0],
+ outstyles=ADDR_STYLES_SINGLE, psbt_v2=psbt_v2)
+
+ po = BasicPSBT().parse(psbt)
+ if psbt_v2:
+ po.txn_version = 3
+ else:
+ txo = CTransaction()
+ txo.deserialize(BytesIO(po.txn))
+ txo.nVersion = 3
+ po.txn = txo.serialize()
+
+ start_sign(po.as_bytes())
+ title, story = cap_story()
+ assert title == "OK TO SEND?"
+ assert "Consolidating" not in story
+ assert "Change back" in story
+ assert "to script" not in story
+
+ signed = end_sign(accept=True, finalize=False)
+ assert signed
+
+ po_signed = BasicPSBT().parse(signed)
+ if psbt_v2:
+ assert po_signed.version == 2
+ assert po_signed.txn is None
+ assert po_signed.txn_version == 3
+ else:
+ assert po_signed.txn
+ assert po_signed.version == 0
+ assert po_signed.txn_version is None
+ txo0 = CTransaction()
+ txo0.deserialize(BytesIO(po_signed.txn))
+ assert txo0.nVersion == 3
+
+
+@pytest.mark.parametrize("finalize", [False, True])
+def test_txn_v3_eph_anchor(finalize, set_seed_words, start_sign, end_sign, cap_story):
+ # https://github.com/portlandhodl/jade-docker-web-server/blob/main/example_tools/test_vectors.py
+ TEST_PSBT_V3 = "cHNidP8BAFIDAAAAAdqopVcNAsr10z7dkzqG6J+3/vd10vriIDDe4O7u8GwfAQAAAAD9////AYOtdgAAAAAAFgAUlOv8+jtC6QYYNWgtA5ekaOGSfHH6WAQATwEENYfPA2elg8CAAAAA+dpuPB69NWUcEs/VfnCtrdxTyQG8xphTtTK6VDbgWHICWD2jKQxipNxf2bJk+0aHxEjn0N6nZD7MFrR/4o5QKZIQgLjDfFQAAIABAACAAAAAgAABAHECAAAAAU4xPHhslj2Nik1d9YVWuCSWLCjsI7ZTbACg4uOEtiJTAAAAAAD9////AqCKBgAAAAAAFgAUIzpZyckZ7FOvLecQynCBNbFwTf3xrXYAAAAAABYAFDpfSBYMJlSC2MY0FF4NUiMpxeZI8VgEAAEBH/GtdgAAAAAAFgAUOl9IFgwmVILYxjQUXg1SIynF5kgBAwQBAAAAIgYDz3o9vHG3Vkh9MjjDMaWr+SiQQZDs8AgzFTBs+xUnQkEYgLjDfFQAAIABAACAAAAAgAAAAAAAAAAAACICApGbj0iZaA6IAmws63XtQbV6hfJAxMKojI6QwWSIRflDGIC4w3xUAACAAQAAgAAAAIAAAAAAAQAAAAA="
+ TEST_PSBT_V3_EPHEMERAL_ANCHOR = "cHNidP8BAHEDAAAAAdqopVcNAsr10z7dkzqG6J+3/vd10vriIDDe4O7u8GwfAQAAAAD9////AoOtdgAAAAAAFgAUlOv8+jtC6QYYNWgtA5ekaOGSfHEAAAAAAAAAABYAFJTr/Po7QukGGDVoLQOXpGjhknxx+lgEAE8BBDWHzwNnpYPAgAAAAPnabjwevTVlHBLP1X5wra3cU8kBvMaYU7UyulQ24FhyAlg9oykMYqTcX9myZPtGh8RI59Dep2Q+zBa0f+KOUCmSEIC4w3xUAACAAQAAgAAAAIAAAQBxAgAAAAFOMTx4bJY9jYpNXfWFVrgkliwo7CO2U2wAoOLjhLYiUwAAAAAA/f///wKgigYAAAAAABYAFCM6WcnJGexTry3nEMpwgTWxcE398a12AAAAAAAWABQ6X0gWDCZUgtjGNBReDVIjKcXmSPFYBAABAR/xrXYAAAAAABYAFDpfSBYMJlSC2MY0FF4NUiMpxeZIAQMEAQAAACIGA896Pbxxt1ZIfTI4wzGlq/kokEGQ7PAIMxUwbPsVJ0JBGIC4w3xUAACAAQAAgAAAAIAAAAAAAAAAAAAiAgKRm49ImWgOiAJsLOt17UG1eoXyQMTCqIyOkMFkiEX5QxiAuMN8VAAAgAEAAIAAAACAAAAAAAEAAAAAIgICkZuPSJloDogCbCzrde1BtXqF8kDEwqiMjpDBZIhF+UMYgLjDfFQAAIABAACAAAAAgAAAAAABAAAAAA=="
+ TEST_MNEMONIC = "tobacco tobacco tobacco tobacco tobacco tobacco tobacco tobacco tobacco tobacco tobacco tobacco"
+ set_seed_words(TEST_MNEMONIC)
+
+ v3 = base64.b64decode(TEST_PSBT_V3)
+ po = BasicPSBT().parse(v3)
+ assert po.version == 0
+ start_sign(v3, finalize=finalize)
+ title, story = cap_story()
+ assert title == "OK TO SEND?"
+ assert "warning" not in story
+ res = end_sign(finalize=finalize)
+ if not finalize:
+ po_signed = BasicPSBT().parse(res)
+ assert po_signed.txn_version is None
+ res = po_signed.txn
+
+ txo = CTransaction()
+ txo.deserialize(BytesIO(res))
+ assert txo.nVersion == 3
+
+ v3_eph_anchor = base64.b64decode(TEST_PSBT_V3_EPHEMERAL_ANCHOR)
+ po = BasicPSBT().parse(v3)
+ assert po.version == 0
+ start_sign(v3_eph_anchor, finalize=finalize)
+ title, story = cap_story()
+ assert title == "OK TO SEND?"
+ assert "1 warning below" in story
+ assert "Non-standard zero value output(s)" in story
+ res = end_sign(finalize=finalize)
+ if not finalize:
+ po_signed = BasicPSBT().parse(res)
+ assert po_signed.txn_version is None
+ res = po_signed.txn
+
+ txo = CTransaction()
+ txo.deserialize(BytesIO(res))
+ assert txo.nVersion == 3
+
# EOF
Why this scored 26/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.