Merge pull request #1005 from kdmukai/psbt_parser_trust_vocabulary
What changed, and why it matters
This commit is a code cleanup that renames internal data fields from 'fingerprint' and 'derivation_path' to 'claimed_fingerprints' and 'claimed_derivation_paths'. It does not change how the software verifies Bitcoin transaction data or how it protects users. The rename is meant to make it clearer to programmers that these values come from an external, untrusted source and should not be trusted until independently verified. The actual security logic is unchanged.
No action required. Treat as normal maintenance/refactor. If auditing the PSBT trust model, focus on whether verified_ values are actually produced by re-derivation, which this commit does not address.
Security signals we found
No functional security change; only identifier renaming and documentation
Expanded docstring explicitly distinguishes claimed_ vs verified_ data
No new validation, no removed validation, no algorithm change
Tests updated only to match renamed keys
Evidence from the diff
The patch is a pure refactor in PSBTParser and PSBTViews. It renames dictionary keys in change_data from ‘fingerprint’/’derivation_path’ to ‘claimed_fingerprints’/’claimed_derivation_paths’ and updates all consumers and tests accordingly. The docstring is expanded to document the trust model: coordinator-supplied metadata is a claim, verified data must come from re-derivation. No verification logic is added, removed, or altered; no cryptographic checks are changed. The commit is defensive documentation/vocabulary work, not a security fix.
Changed components
src/seedsigner/models/psbt_parser.pysrc/seedsigner/views/psbt_views.pytests/test_psbt_parser.pyInspect captured patch +62 / −38
### src/seedsigner/models/psbt_parser.py
@@ -21,16 +21,36 @@ class OPCODES:
class PSBTParser():
"""
- Reads a psbt on behalf of one seed and works out everything the signing flow shows
- the user before they approve: the wallet policy (script type, plus m-of-n and the
- cosigners for multisig), the amount coming in, what is being spent, what comes back
- as change, the fee, where the spend is going, and any OP_RETURN payload.
-
- Constructing it with a seed parses immediately. The results are read off the instance
- attributes, with per-change-output detail in change_data.
-
- has_matching_input_fingerprint answers the earlier question of which seed a psbt is
- for and needs no parse.
+ Reads a psbt on behalf of one seed and works out everything the signing flow shows the
+ user before they approve: the wallet policy (script type, plus m-of-n and the
+ cosigners for multisig), the amount coming in, what is being spent, what comes back as
+ change, the fee, where the spend is going, and any OP_RETURN payload.
+
+ Constructing it with a seed parses immediately.
+
+ The parse fully processes the psbt, validates what it can, then stores the organized
+ results in the instance attributes (spend_amount, fee_amount, destination_addresses,
+ etc.). Note that change_data and change_amount cover EVERY output coming back to this
+ seed, including self-transfers to a receive address. The view layer tells the two
+ apart by the branch index in the derivation path.
+
+ A psbt is written by an untrusted coordinator. The metadata it carries about keys
+ (fingerprints, derivation paths, xpubs) is a claim, not a fact. The onus is on us to
+ verify by re-deriving from the signing seed. For multisig, verification depends on the
+ user providing a "known good" descriptor (i.e. can be trusted) from which we can
+ verify the outputs by deriving from the cosigners' xpubs.
+
+ This class makes the difference visible in its own names:
+
+ claimed_... coordinator-supplied metadata (fingerprints, derivation paths, xpubs).
+ Safe to read and display; never safe to make a decision on.
+ verified_... a fact this device proved by re-deriving from the signing seed and
+ matching real key material. Only assigned by code that performed that
+ derivation.
+
+ Invariant: no verified_ value is ever assigned from a claimed_ value without an
+ intervening re-derivation from self.root or from a user-supplied "known good"
+ descriptor.
"""
# Upper bound on how many levels of derivation a single parse will cache. 1000 is
@@ -237,27 +257,27 @@ def _parse_outputs(self, child_key_derivation_cache: dict):
elif is_change:
addr = vout[i].script_pubkey.address(NETWORKS[SettingsConstants.map_network_to_embit(self.network)])
- fingerprints = []
- derivation_paths = []
+ claimed_fingerprints = []
+ claimed_derivation_paths = []
# extract info from non-taproot outputs
if len(self.psbt.outputs[i].bip32_derivations) > 0:
for d, derivation_path in self.psbt.outputs[i].bip32_derivations.items():
- fingerprints.append(hexlify(derivation_path.fingerprint).decode())
- derivation_paths.append(bip32.path_to_str(derivation_path.derivation))
+ claimed_fingerprints.append(hexlify(derivation_path.fingerprint).decode())
+ claimed_derivation_paths.append(bip32.path_to_str(derivation_path.derivation))
# extract info from taproot outputs
if len(self.psbt.outputs[i].taproot_bip32_derivations) > 0:
for d, (leaf_hashes, derivation) in self.psbt.outputs[i].taproot_bip32_derivations.items():
- fingerprints.append(hexlify(derivation.fingerprint).decode())
- derivation_paths.append(bip32.path_to_str(derivation.derivation))
+ claimed_fingerprints.append(hexlify(derivation.fingerprint).decode())
+ claimed_derivation_paths.append(bip32.path_to_str(derivation.derivation))
self.change_data.append({
"output_index": i,
"address": addr,
"amount": vout[i].value,
- "fingerprint": fingerprints,
- "derivation_path": derivation_paths,
+ "claimed_fingerprints": claimed_fingerprints,
+ "claimed_derivation_paths": claimed_derivation_paths,
})
self.change_amount += vout[i].value
@@ -469,8 +489,13 @@ def get_input_fingerprints(psbt: PSBT) -> List[str]:
@staticmethod
def has_matching_input_fingerprint(psbt: PSBT, seed: Seed, network: str = SettingsConstants.MAINNET):
"""
- Extracts the fingerprint from each psbt input utxo. Returns True if any match
- the current seed.
+ Extracts the claimed fingerprint from each psbt input. Returns True if any
+ match the provided seed.
+
+ This is merely a routing hint to help the user select a seed that looks like
+ it should be able to sign the psbt; it verifies nothing. Actual verification
+ only begins once a seed has been selected and passed into a PSBTParser
+ instance.
"""
seed_fingerprint = seed.get_fingerprint(network)
### src/seedsigner/views/psbt_views.py
@@ -116,16 +116,15 @@ def run(self):
{
'address': 'bc1q............',
'amount': 397621401,
- 'fingerprint': ['22bde1a9', '73c5da0a'],
- 'derivation_path': ['m/48h/1h/0h/2h/1/0', 'm/48h/1h/0h/2h/1/0']
+ 'claimed_fingerprints': ['22bde1a9', '73c5da0a'],
+ 'claimed_derivation_paths': ['m/48h/1h/0h/2h/1/0', 'm/48h/1h/0h/2h/1/0']
}, {},
]
"""
num_change_outputs = 0
num_self_transfer_outputs = 0
for change_output in change_data:
- # print(f"""{change_output["derivation_path"][0]}""")
- if change_output["derivation_path"][0].split("/")[-2] == "1":
+ if change_output["claimed_derivation_paths"][0].split("/")[-2] == "1":
num_change_outputs += 1
else:
num_self_transfer_outputs += 1
@@ -325,25 +324,25 @@ def run(self):
{
'address': 'bc1q............',
'amount': 397621401,
- 'fingerprint': ['22bde1a9', '73c5da0a'],
- 'derivation_path': ['m/48h/1h/0h/2h/1/0', 'm/48h/1h/0h/2h/1/0']
+ 'claimed_fingerprints': ['22bde1a9', '73c5da0a'],
+ 'claimed_derivation_paths': ['m/48h/1h/0h/2h/1/0', 'm/48h/1h/0h/2h/1/0']
}
"""
# Single-sig verification is easy. We expect to find a single fingerprint
# and derivation path.
seed_fingerprint = self.controller.psbt_seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
- if seed_fingerprint not in change_data.get("fingerprint"):
+ if seed_fingerprint not in change_data.get("claimed_fingerprints"):
# TODO: Something is wrong with this psbt(?). Reroute to warning?
return Destination(NotYetImplementedView)
- i = change_data.get("fingerprint").index(seed_fingerprint)
- derivation_path = change_data.get("derivation_path")[i]
+ i = change_data.get("claimed_fingerprints").index(seed_fingerprint)
+ claimed_derivation_path = change_data.get("claimed_derivation_paths")[i]
# 'm/84h/1h/0h/1/0' would be a change addr while 'm/84h/1h/0h/0/0' is a self-receive
- is_change_derivation_path = int(derivation_path.split("/")[-2]) == 1
- derivation_path_addr_index = int(derivation_path.split("/")[-1])
+ is_change_derivation_path = int(claimed_derivation_path.split("/")[-2]) == 1
+ derivation_path_addr_index = int(claimed_derivation_path.split("/")[-1])
if is_change_derivation_path:
# TRANSLATOR_NOTE: The amount you're receiving back from the transaction
@@ -384,8 +383,8 @@ def run(self):
script_type = pubkey.script_type()
# extract derivation path to get wallet and change derivation
- change_path = '/'.join(derivation_path.split("/")[-2:])
- wallet_path = '/'.join(derivation_path.split("/")[:-2])
+ change_path = '/'.join(claimed_derivation_path.split("/")[-2:])
+ wallet_path = '/'.join(claimed_derivation_path.split("/")[:-2])
xpub = self.controller.psbt_seed.get_xpub(
wallet_path=wallet_path,
@@ -425,7 +424,7 @@ def run(self):
amount=change_data.get("amount"),
is_multisig=psbt_parser.is_multisig,
fingerprint=seed_fingerprint,
- derivation_path=derivation_path,
+ derivation_path=claimed_derivation_path,
is_change_derivation_path=is_change_derivation_path,
derivation_path_addr_index=derivation_path_addr_index,
is_change_addr_verified=is_change_addr_verified,
### tests/test_psbt_parser.py
@@ -321,8 +321,8 @@ def test_p2tr_change_detection():
'output_index': 0,
'address': 'bcrt1prz4g6saush37epdwhvwpu78td3q7yfz3xxz37axlx7udck6wracq3rwq30',
'amount': 2871443918,
- 'fingerprint': ['394aed14'],
- 'derivation_path': ['m/86h/1h/0h/1/1']}
+ 'claimed_fingerprints': ['394aed14'],
+ 'claimed_derivation_paths': ['m/86h/1h/0h/1/1']}
]
assert pp.spend_amount == 319049328
assert pp.change_amount == 2871443918
@@ -481,8 +481,8 @@ def test_parse_op_return_content():
'output_index': 0,
'address': 'bcrt1qvwkhakqhz7m7kmz6332avatsmdy32m644g86vv',
'amount': 99992296,
- 'fingerprint': ['0fb882ff'],
- 'derivation_path': ["m/84h/1h/0h/0/2"]}
+ 'claimed_fingerprints': ['0fb882ff'],
+ 'claimed_derivation_paths': ["m/84h/1h/0h/0/2"]}
]
assert psbt_parser.spend_amount == 0 # This is a self-spend; no value being spent, other than the tx fee
assert psbt_parser.change_amount == 99992296Why 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.