mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-09-14 04:45:08 +00:00
Merge pull request #1005 from kdmukai/psbt_parser_trust_vocabulary
[refactor] PSBT parsing: rename `change_data`'s unverified fields to `claimed_*`
This commit is contained in:
@@ -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.
|
||||
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.
|
||||
Constructing it with a seed parses immediately.
|
||||
|
||||
has_matching_input_fingerprint answers the earlier question of which seed a psbt is
|
||||
for and needs no parse.
|
||||
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 @@ class PSBTParser():
|
||||
|
||||
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 @@ class PSBTParser():
|
||||
@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)
|
||||
|
||||
|
||||
@@ -116,16 +116,15 @@ class PSBTOverviewView(View):
|
||||
{
|
||||
'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,8 +324,8 @@ class PSBTChangeDetailsView(View):
|
||||
{
|
||||
'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']
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -334,16 +333,16 @@ class PSBTChangeDetailsView(View):
|
||||
# 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 @@ class PSBTChangeDetailsView(View):
|
||||
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 @@ class PSBTChangeDetailsView(View):
|
||||
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,
|
||||
|
||||
@@ -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 == 99992296
|
||||
|
||||
Reference in New Issue
Block a user