electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit ce5cc135cd432552006c713d1d83d9fb51e1e7e2
parent 53fd6a2df590e2acd1117a05e87e66d65843005d
Author: SomberNight <somber.night@protonmail.com>
Date:   Sat, 29 Sep 2018 19:47:55 +0200

transaction: make get_address_from_output_script safer

closes #4743

Diffstat:
Melectrum/ecc.py | 8++++++++
Melectrum/tests/test_transaction.py | 16++++++++++++++++
Melectrum/transaction.py | 72++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
3 files changed, 74 insertions(+), 22 deletions(-)

diff --git a/electrum/ecc.py b/electrum/ecc.py @@ -296,6 +296,14 @@ class ECPubkey(object): def is_at_infinity(self): return self == point_at_infinity() + @classmethod + def is_pubkey_bytes(cls, b: bytes): + try: + ECPubkey(b) + return True + except: + return False + def msg_magic(message: bytes) -> bytes: from .bitcoin import var_int diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py @@ -175,6 +175,8 @@ class TestTransaction(SequentialTestCase): # the inverse of this test is in test_bitcoin: test_address_to_script addr_from_script = lambda script: transaction.get_address_from_output_script(bfh(script)) ADDR = transaction.TYPE_ADDRESS + PUBKEY = transaction.TYPE_PUBKEY + SCRIPT = transaction.TYPE_SCRIPT # bech32 native segwit # test vectors from BIP-0173 @@ -182,14 +184,28 @@ class TestTransaction(SequentialTestCase): self.assertEqual((ADDR, 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'), addr_from_script('5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6')) self.assertEqual((ADDR, 'bc1sw50qa3jx3s'), addr_from_script('6002751e')) self.assertEqual((ADDR, 'bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'), addr_from_script('5210751e76e8199196d454941c45d1b3a323')) + # almost but not quite + self.assertEqual((SCRIPT, '0013751e76e8199196d454941c45d1b3a323f1433b'), addr_from_script('0013751e76e8199196d454941c45d1b3a323f1433b')) # base58 p2pkh self.assertEqual((ADDR, '14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac')) self.assertEqual((ADDR, '1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv'), addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac')) + # almost but not quite + self.assertEqual((SCRIPT, '76a9130000000000000000000000000000000000000088ac'), addr_from_script('76a9130000000000000000000000000000000000000088ac')) # base58 p2sh self.assertEqual((ADDR, '35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487')) self.assertEqual((ADDR, '3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji'), addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387')) + # almost but not quite + self.assertEqual((SCRIPT, 'a912f47c8954e421031ad04ecd8e7752c947920687'), addr_from_script('a912f47c8954e421031ad04ecd8e7752c947920687')) + + # p2pk + self.assertEqual((PUBKEY, '0289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8b'), addr_from_script('210289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac')) + self.assertEqual((PUBKEY, '045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120'), addr_from_script('41045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120ac')) + # almost but not quite + self.assertEqual((SCRIPT, '200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac'), addr_from_script('200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac')) + self.assertEqual((SCRIPT, '210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'), addr_from_script('210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac')) + ##### diff --git a/electrum/transaction.py b/electrum/transaction.py @@ -27,7 +27,8 @@ # Note: The deserialization code originally comes from ABE. -from typing import Sequence, Union, NamedTuple, Tuple, Optional, Iterable +from typing import (Sequence, Union, NamedTuple, Tuple, Optional, Iterable, + Callable) from .util import print_error, profiler @@ -288,15 +289,39 @@ def script_GetOpName(opcode): return (opcodes.whatis(opcode)).replace("OP_", "") +class OPPushDataGeneric: + def __init__(self, pushlen: Callable=None): + if pushlen is not None: + self.check_data_len = pushlen + + @classmethod + def check_data_len(cls, datalen: int) -> bool: + # Opcodes below OP_PUSHDATA4 all just push data onto stack, and are equivalent. + return opcodes.OP_PUSHDATA4 >= datalen >= 0 + + @classmethod + def is_instance(cls, item): + # accept objects that are instances of this class + # or other classes that are subclasses + return isinstance(item, cls) \ + or (isinstance(item, type) and issubclass(item, cls)) + + +OPPushDataPubkey = OPPushDataGeneric(lambda x: x in (33, 65)) +# note that this does not include x_pubkeys ! + + def match_decoded(decoded, to_match): if decoded is None: return False if len(decoded) != len(to_match): return False for i in range(len(decoded)): - if to_match[i] == opcodes.OP_PUSHDATA4 and decoded[i][0] <= opcodes.OP_PUSHDATA4 and decoded[i][0]>0: - continue # Opcodes below OP_PUSHDATA4 all just push data onto stack, and are equivalent. - if to_match[i] != decoded[i][0]: + to_match_item = to_match[i] + decoded_item = decoded[i] + if OPPushDataGeneric.is_instance(to_match_item) and to_match_item.check_data_len(decoded_item[0]): + continue + if to_match_item != decoded_item[0]: return False return True @@ -319,7 +344,7 @@ def parse_scriptSig(d, _bytes): bh2u(_bytes)) return - match = [ opcodes.OP_PUSHDATA4 ] + match = [OPPushDataGeneric] if match_decoded(decoded, match): item = decoded[0][1] if item[0] == 0: @@ -350,7 +375,7 @@ def parse_scriptSig(d, _bytes): # p2pkh TxIn transactions push a signature # (71-73 bytes) and then their public key # (33 or 65 bytes) onto the stack: - match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ] + match = [OPPushDataGeneric, OPPushDataGeneric] if match_decoded(decoded, match): sig = bh2u(decoded[0][1]) x_pubkey = bh2u(decoded[1][1]) @@ -370,7 +395,7 @@ def parse_scriptSig(d, _bytes): return # p2sh transaction, m of n - match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1) + match = [opcodes.OP_0] + [OPPushDataGeneric] * (len(decoded) - 1) if match_decoded(decoded, match): x_sig = [bh2u(x[1]) for x in decoded[1:-1]] redeem_script_unsanitized = decoded[-1][1] # for partial multisig txn, this has x_pubkeys @@ -393,7 +418,7 @@ def parse_scriptSig(d, _bytes): return # custom partial format for imported addresses - match = [ opcodes.OP_INVALIDOPCODE, opcodes.OP_0, opcodes.OP_PUSHDATA4 ] + match = [opcodes.OP_INVALIDOPCODE, opcodes.OP_0, OPPushDataGeneric] if match_decoded(decoded, match): x_pubkey = bh2u(decoded[2][1]) pubkey, address = xpubkey_to_address(x_pubkey) @@ -421,7 +446,7 @@ def parse_redeemScript_multisig(redeem_script: bytes): raise NotRecognizedRedeemScript() op_m = opcodes.OP_1 + m - 1 op_n = opcodes.OP_1 + n - 1 - match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ] + match_multisig = [op_m] + [OPPushDataGeneric] * n + [op_n, opcodes.OP_CHECKMULTISIG] if not match_decoded(dec2, match_multisig): raise NotRecognizedRedeemScript() x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] @@ -433,33 +458,36 @@ def parse_redeemScript_multisig(redeem_script: bytes): return m, n, x_pubkeys, pubkeys, redeem_script_sanitized -def get_address_from_output_script(_bytes, *, net=None): +def get_address_from_output_script(_bytes: bytes, *, net=None) -> Tuple[int, str]: try: decoded = [x for x in script_GetOp(_bytes)] except MalformedBitcoinScript: decoded = None - # The Genesis Block, self-payments, and pay-by-IP-address payments look like: - # 65 BYTES:... CHECKSIG - match = [ opcodes.OP_PUSHDATA4, opcodes.OP_CHECKSIG ] - if match_decoded(decoded, match): + # p2pk + match = [OPPushDataPubkey, opcodes.OP_CHECKSIG] + if match_decoded(decoded, match) and ecc.ECPubkey.is_pubkey_bytes(decoded[0][1]): return TYPE_PUBKEY, bh2u(decoded[0][1]) - # Pay-by-Bitcoin-address TxOuts look like: - # DUP HASH160 20 BYTES:... EQUALVERIFY CHECKSIG - match = [ opcodes.OP_DUP, opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG ] + # p2pkh + match = [opcodes.OP_DUP, opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG] if match_decoded(decoded, match): return TYPE_ADDRESS, hash160_to_p2pkh(decoded[2][1], net=net) # p2sh - match = [ opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUAL ] + match = [opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUAL] if match_decoded(decoded, match): return TYPE_ADDRESS, hash160_to_p2sh(decoded[1][1], net=net) - # segwit address - possible_witness_versions = [opcodes.OP_0] + list(range(opcodes.OP_1, opcodes.OP_16 + 1)) - for witver, opcode in enumerate(possible_witness_versions): - match = [ opcode, opcodes.OP_PUSHDATA4 ] + # segwit address (version 0) + match = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))] + if match_decoded(decoded, match): + return TYPE_ADDRESS, hash_to_segwit_addr(decoded[1][1], witver=0, net=net) + + # segwit address (version 1-16) + future_witness_versions = list(range(opcodes.OP_1, opcodes.OP_16 + 1)) + for witver, opcode in enumerate(future_witness_versions, start=1): + match = [opcode, OPPushDataGeneric(lambda x: 2 <= x <= 40)] if match_decoded(decoded, match): return TYPE_ADDRESS, hash_to_segwit_addr(decoded[1][1], witver=witver, net=net)