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:
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)