mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge pull request #1027 from kdmukai/psbt_parser_docstrings
Clarify psbt_parser.py docstrings and comments; minor cleanup
This commit is contained in:
@@ -32,33 +32,21 @@ class PSBTVerificationError(Exception):
|
|||||||
|
|
||||||
class PSBTOutputOwnershipClaimError(PSBTVerificationError):
|
class PSBTOutputOwnershipClaimError(PSBTVerificationError):
|
||||||
"""
|
"""
|
||||||
An output scope claims this seed's fingerprint on a key the seed does not derive.
|
An output claims this seed's fingerprint on a key the seed does not derive.
|
||||||
|
|
||||||
This is not a psbt that merely fails to be ours. A fingerprint is
|
This is asserting that an output belongs to this seed when it does not. We treat this
|
||||||
coordinator-supplied metadata, so this is a psbt asserting that a key belongs to this
|
deception as an attack.
|
||||||
seed when it does not. On an output that assertion is how a fake change output is
|
|
||||||
dressed up as the user's own, so it should be treated as an attack.
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PSBTInputOwnershipClaimError(PSBTVerificationError):
|
class PSBTInputOwnershipClaimError(PSBTVerificationError):
|
||||||
"""
|
"""
|
||||||
An input scope claims this seed's fingerprint on a key the seed does not derive.
|
An input claims this seed's fingerprint on a key the seed does not derive.
|
||||||
|
|
||||||
The same false claim as PSBTOutputOwnershipClaimError, but on an input the threat
|
The same type of false claim as the output case above, but a forged input claim simply
|
||||||
picture inverts. A forged input claim has no path to losing funds: it cannot produce a
|
renders the psbt unsignable (embit re-derives the real key and will refuse to sign on
|
||||||
signature (embit re-derives the real key and refuses on a mismatch), and it cannot
|
a mismatch) so there's no point in continuing.
|
||||||
alter the amounts, the fee, or how outputs are classified. The likely causes are
|
|
||||||
instead a psbt assembled for a different wallet, a corrupted entry, or a collaborative
|
|
||||||
spend that happens to include a key whose 4-byte fingerprint collides with ours (1 in
|
|
||||||
2^32 chance).
|
|
||||||
|
|
||||||
The psbt still fails, deliberately, following embit's lead: sign_with raises on this
|
|
||||||
same condition and abandons the entire signing pass, so tolerating the entry here
|
|
||||||
would only defer the failure to a worse spot. Changing this behavior, if desired,
|
|
||||||
should happen in embit first. Until then the trade-off is accepted: a
|
|
||||||
collaborative-spend counterparty could grief such a transaction into unsignability.
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -133,9 +121,9 @@ class PSBTParser():
|
|||||||
self.destination_amounts = []
|
self.destination_amounts = []
|
||||||
self.op_return_data: bytes = None
|
self.op_return_data: bytes = None
|
||||||
|
|
||||||
# Indexed alongside psbt.inputs / psbt.outputs. Each entry is the derivation path
|
# Contains one entry per input in psbt.inputs and per output in psbt.outputs. Each
|
||||||
# the seed genuinely owns in each scope or None where it owns nothing. Determined
|
# entry is either the derivation path the seed genuinely owns there, or it is set
|
||||||
# in _verify_claimed_derivation_paths.
|
# to `None`.
|
||||||
self.verified_input_derivation_paths: List[List[int] | None] = []
|
self.verified_input_derivation_paths: List[List[int] | None] = []
|
||||||
self.verified_output_derivation_paths: List[List[int] | None] = []
|
self.verified_output_derivation_paths: List[List[int] | None] = []
|
||||||
|
|
||||||
@@ -176,12 +164,12 @@ class PSBTParser():
|
|||||||
"""
|
"""
|
||||||
Establishes, in order:
|
Establishes, in order:
|
||||||
|
|
||||||
1. _fill_missing_fingerprints: backfills all-zero fingerprints, but only for
|
1. _fill_missing_fingerprints: backfills all-zero fingerprints, but only where
|
||||||
scopes the seed provably derives.
|
the seed provably derives the key.
|
||||||
|
|
||||||
2. _verify_claimed_derivation_paths: each input and output scope that claims to
|
2. _verify_claimed_derivation_paths: each input and output that claims to be
|
||||||
be controlled by the seed is verified. Raises an Input/Output
|
controlled by the seed declares a derivation path that must be verified.
|
||||||
OwnershipClaimError if a claimed scope fails verification.
|
Raises an Input/Output OwnershipClaimError if a claim fails verification.
|
||||||
|
|
||||||
3. _reject_if_seed_cannot_sign: raises PSBTSeedCannotSignError if none of the
|
3. _reject_if_seed_cannot_sign: raises PSBTSeedCannotSignError if none of the
|
||||||
inputs can be signed by the seed. A mismatch rather than an attack, caught
|
inputs can be signed by the seed. A mismatch rather than an attack, caught
|
||||||
@@ -257,6 +245,10 @@ class PSBTParser():
|
|||||||
|
|
||||||
|
|
||||||
def _parse_inputs(self, child_key_derivation_cache: dict):
|
def _parse_inputs(self, child_key_derivation_cache: dict):
|
||||||
|
"""
|
||||||
|
Totals the input amounts and determines the wallet policy. Every input must
|
||||||
|
resolve to the same policy, otherwise a RuntimeError is raised.
|
||||||
|
"""
|
||||||
self.input_amount = 0
|
self.input_amount = 0
|
||||||
self.num_inputs = len(self.psbt.inputs)
|
self.num_inputs = len(self.psbt.inputs)
|
||||||
for inp in self.psbt.inputs:
|
for inp in self.psbt.inputs:
|
||||||
@@ -314,7 +306,8 @@ class PSBTParser():
|
|||||||
elif self.policy["type"] == "p2sh":
|
elif self.policy["type"] == "p2sh":
|
||||||
sc = script.p2sh(out.redeem_script)
|
sc = script.p2sh(out.redeem_script)
|
||||||
|
|
||||||
# single-sig
|
# single-sig: p2pkh, p2sh-p2wpkh, and p2wpkh; taproot handled separately
|
||||||
|
# below.
|
||||||
elif "pkh" in self.policy["type"]:
|
elif "pkh" in self.policy["type"]:
|
||||||
my_pubkey = None
|
my_pubkey = None
|
||||||
|
|
||||||
@@ -431,6 +424,8 @@ class PSBTParser():
|
|||||||
policy = {"type": script_type}
|
policy = {"type": script_type}
|
||||||
|
|
||||||
# expected multisig
|
# expected multisig
|
||||||
|
# TODO: rename this local. It shadows the embit `script` module for the rest of
|
||||||
|
# this function, so script.p2wsh() and the other constructors are unreachable.
|
||||||
script = None
|
script = None
|
||||||
if script_type:
|
if script_type:
|
||||||
if "p2wsh" in script_type and scope.witness_script is not None:
|
if "p2wsh" in script_type and scope.witness_script is not None:
|
||||||
@@ -459,19 +454,19 @@ class PSBTParser():
|
|||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_multisig(sc):
|
def _parse_multisig(multisig_script):
|
||||||
"""Takes a script and extracts m,n and pubkeys from it"""
|
"""Takes a script and extracts m,n and pubkeys from it"""
|
||||||
# OP_m <len:pubkey> ... <len:pubkey> OP_n OP_CHECKMULTISIG
|
# OP_m <len:pubkey> ... <len:pubkey> OP_n OP_CHECKMULTISIG
|
||||||
# check min size
|
# check min size
|
||||||
if len(sc.data) < 37 or sc.data[-1] != 0xAE:
|
if len(multisig_script.data) < 37 or multisig_script.data[-1] != 0xAE:
|
||||||
raise ValueError("Not a multisig script")
|
raise ValueError("Not a multisig script")
|
||||||
m = sc.data[0] - 0x50
|
m = multisig_script.data[0] - 0x50
|
||||||
if m < 1 or m > 16:
|
if m < 1 or m > 16:
|
||||||
raise ValueError("Invalid multisig script")
|
raise ValueError("Invalid multisig script")
|
||||||
n = sc.data[-2] - 0x50
|
n = multisig_script.data[-2] - 0x50
|
||||||
if n < m or n > 16:
|
if n < m or n > 16:
|
||||||
raise ValueError("Invalid multisig script")
|
raise ValueError("Invalid multisig script")
|
||||||
s = BytesIO(sc.data)
|
s = BytesIO(multisig_script.data)
|
||||||
# drop first byte
|
# drop first byte
|
||||||
s.read(1)
|
s.read(1)
|
||||||
# read pubkeys
|
# read pubkeys
|
||||||
@@ -482,7 +477,7 @@ class PSBTParser():
|
|||||||
raise ValueError("Invlid pubkey")
|
raise ValueError("Invlid pubkey")
|
||||||
pubkeys.append(ec.PublicKey.parse(s.read(33)))
|
pubkeys.append(ec.PublicKey.parse(s.read(33)))
|
||||||
# check that nothing left
|
# check that nothing left
|
||||||
if s.read() != sc.data[-2:]:
|
if s.read() != multisig_script.data[-2:]:
|
||||||
raise ValueError("Invalid multisig script")
|
raise ValueError("Invalid multisig script")
|
||||||
return m, n, pubkeys
|
return m, n, pubkeys
|
||||||
|
|
||||||
@@ -670,11 +665,7 @@ class PSBTParser():
|
|||||||
match). Returns the verified derivation path (as a list of ints) or None.
|
match). Returns the verified derivation path (as a list of ints) or None.
|
||||||
|
|
||||||
Every key in the scope that claims this seed's fingerprint is re-derived and
|
Every key in the scope that claims this seed's fingerprint is re-derived and
|
||||||
checked. A claim that does not hold up raises
|
checked. A false claim raises PSBT[Output|Input]OwnershipClaimError.
|
||||||
PSBT[Output|Input]OwnershipClaimError. This includes fingerprint collisions (two
|
|
||||||
different keys with the same 4-byte fingerprint):
|
|
||||||
* On the output side, a collision is considered an attack.
|
|
||||||
* On the input side it is merely disallowed because it is unsignable by embit.
|
|
||||||
|
|
||||||
One edge case:
|
One edge case:
|
||||||
* A multisig could use this seed in more than one cosigner slot, each
|
* A multisig could use this seed in more than one cosigner slot, each
|
||||||
|
|||||||
Reference in New Issue
Block a user