diff --git a/mmgen/altcoins/eth/contract.py b/mmgen/altcoins/eth/contract.py index b6cbfd4b..9168e6e0 100755 --- a/mmgen/altcoins/eth/contract.py +++ b/mmgen/altcoins/eth/contract.py @@ -21,7 +21,7 @@ altcoins.eth.contract: Ethereum contract and token classes for the MMGen suite """ from decimal import Decimal -import rlp +from . import rlp from mmgen.globalvars import g from mmgen.common import * @@ -110,7 +110,7 @@ class Token(MMGenObject): # ERC20 def txsign(self,tx_in,key,from_addr,chain_id=None): try: from ethereum.transactions import Transaction - except: from mmgen.altcoins.eth.pyethereum.transactions import Transaction + except: from .pyethereum.transactions import Transaction if chain_id is None: chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in g.rpch.caps] diff --git a/mmgen/altcoins/eth/pyethereum/LICENSE b/mmgen/altcoins/eth/pyethereum/LICENSE new file mode 100644 index 00000000..8401aaff --- /dev/null +++ b/mmgen/altcoins/eth/pyethereum/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Vitalik Buterin, Heiko Hees + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/mmgen/altcoins/eth/pyethereum/transactions.py b/mmgen/altcoins/eth/pyethereum/transactions.py index 9c412a56..29d44d46 100644 --- a/mmgen/altcoins/eth/pyethereum/transactions.py +++ b/mmgen/altcoins/eth/pyethereum/transactions.py @@ -1,12 +1,13 @@ # # Adapted from: # https://github.com/ethereum/pyethereum/blob/master/ethereum/transactions.py # -import rlp -from rlp.sedes import big_endian_int,binary -from mmgen.altcoins.eth.pyethereum.utils import ( - str_to_bytes,encode_hex,ascii_chr,big_endian_to_int,TT256,mk_contract_address, +from .. import rlp +from ..rlp.sedes import big_endian_int,binary +from .utils import ( + str_to_bytes,encode_hex,ascii_chr,big_endian_to_int, + TT256,mk_contract_address, ecsign,ecrecover_to_pub,normalize_key ) -import mmgen.altcoins.eth.pyethereum.utils as utils +from . import utils class InvalidTransaction(Exception): pass class opcodes(object): diff --git a/mmgen/altcoins/eth/pyethereum/utils.py b/mmgen/altcoins/eth/pyethereum/utils.py index 628849c3..7262f997 100644 --- a/mmgen/altcoins/eth/pyethereum/utils.py +++ b/mmgen/altcoins/eth/pyethereum/utils.py @@ -2,12 +2,9 @@ # Adapted from: https://github.com/ethereum/pyethereum/blob/master/ethereum/utils.py # -from py_ecc.secp256k1 import privtopub, ecdsa_raw_sign, ecdsa_raw_recover -import rlp -from rlp.sedes import big_endian_int, BigEndianInt, Binary -from eth_utils import encode_hex as encode_hex_0x -from eth_utils import decode_hex, int_to_big_endian, big_endian_to_int -from rlp.utils import ALL_BYTES +from py_ecc.secp256k1 import privtopub,ecdsa_raw_sign,ecdsa_raw_recover +from .. import rlp +from ..rlp.sedes import Binary from mmgen.globalvars import g try: @@ -19,6 +16,60 @@ except: def sha3_256(x): return keccak_256(x).digest() +import struct +ALL_BYTES = tuple( struct.pack('B', i) for i in range(256) ) + +# from eth_utils: + +# Type ignored for `codecs.decode()` due to lack of mypy support for 'hex' encoding +# https://github.com/python/typeshed/issues/300 +from typing import AnyStr,Any,Callable,TypeVar +import codecs +import functools + +T = TypeVar("T") +TVal = TypeVar("TVal") +TKey = TypeVar("TKey") + +def apply_to_return_value(callback: Callable[..., T]) -> Callable[..., Callable[..., T]]: + + def outer(fn): + # We would need to type annotate *args and **kwargs but doing so segfaults + # the PyPy builds. We ignore instead. + @functools.wraps(fn) + def inner(*args, **kwargs) -> T: # type: ignore + return callback(fn(*args, **kwargs)) + + return inner + + return outer + +to_list = apply_to_return_value(list) +to_set = apply_to_return_value(set) +to_dict = apply_to_return_value(dict) +to_tuple = apply_to_return_value(tuple) +to_list = apply_to_return_value(list) + +def encode_hex_0x(value: AnyStr) -> str: + if not is_string(value): + raise TypeError("Value must be an instance of str or unicode") + binary_hex = codecs.encode(value, "hex") # type: ignore + return '0x' + binary_hex.decode("ascii") + +def decode_hex(value: str) -> bytes: + if not isinstance(value,str): + raise TypeError("Value must be an instance of str") + return codecs.decode(remove_0x_prefix(value), "hex") # type: ignore + +def is_bytes(value: Any) -> bool: + return isinstance(value, (bytes,bytearray)) + +def int_to_big_endian(value: int) -> bytes: + return value.to_bytes((value.bit_length() + 7) // 8 or 1, "big") + +def big_endian_to_int(value: bytes) -> int: + return int.from_bytes(value, "big") +# end from eth_utils class Memoize: def __init__(self, fn): @@ -30,9 +81,6 @@ class Memoize: return self.memo[args] TT256 = 2 ** 256 -TT256M1 = 2 ** 256 - 1 -TT255 = 2 ** 255 -SECP256K1P = 2**256 - 4294968273 def is_numeric(x): return isinstance(x, int) @@ -46,24 +94,11 @@ def to_string(value): if isinstance(value, int): return bytes(str(value), 'utf-8') -def int_to_bytes(value): - if isinstance(value, bytes): - return value - return int_to_big_endian(value) - -def to_string_for_regexp(value): - return str(to_string(value), 'utf-8') unicode = str -def bytearray_to_bytestr(value): - return bytes(value) - def encode_int32(v): return v.to_bytes(32, byteorder='big') -def bytes_to_int(value): - return int.from_bytes(value, byteorder='big') - def str_to_bytes(value): if isinstance(value, bytearray): value = bytes(value) @@ -102,39 +137,6 @@ def mk_metropolis_contract_address(sender, initcode): return sha3(normalize_address(sender) + initcode)[12:] -def safe_ord(value): - if isinstance(value, int): - return value - else: - return ord(value) - -# decorator - - -def flatten(li): - o = [] - for l in li: - o.extend(l) - return o - - -def bytearray_to_int(arr): - o = 0 - for a in arr: - o = (o << 8) + a - return o - - -def int_to_32bytearray(i): - o = [0] * 32 - for x in range(32): - o[31 - x] = i & 0xff - i >>= 8 - return o - -# sha3_count = [0] - - def sha3(seed): return sha3_256(to_string(seed)) @@ -183,36 +185,6 @@ def normalize_key(key): return o -def zpad(x, l): - """ Left zero pad value `x` at least to length `l`. - - >>> zpad('', 1) - '\x00' - >>> zpad('\xca\xfe', 4) - '\x00\x00\xca\xfe' - >>> zpad('\xff', 1) - '\xff' - >>> zpad('\xca\xfe', 2) - '\xca\xfe' - """ - return b'\x00' * max(0, l - len(x)) + x - - -def rzpad(value, total_length): - """ Right zero pad value `x` at least to length `l`. - - >>> zpad('', 1) - '\x00' - >>> zpad('\xca\xfe', 4) - '\xca\xfe\x00\x00' - >>> zpad('\xff', 1) - '\xff' - >>> zpad('\xca\xfe', 2) - '\xca\xfe' - """ - return value + b'\x00' * max(0, total_length - len(value)) - - def int_to_addr(x): o = [b''] * 20 for i in range(20): @@ -221,153 +193,10 @@ def int_to_addr(x): return b''.join(o) -def coerce_addr_to_bin(x): - if is_numeric(x): - return encode_hex(zpad(big_endian_int.serialize(x), 20)) - elif len(x) == 40 or len(x) == 0: - return decode_hex(x) - else: - return zpad(x, 20)[-20:] - - -def coerce_addr_to_hex(x): - if is_numeric(x): - return encode_hex(zpad(big_endian_int.serialize(x), 20)) - elif len(x) == 40 or len(x) == 0: - return x - else: - return encode_hex(zpad(x, 20)[-20:]) - - -def coerce_to_int(x): - if is_numeric(x): - return x - elif len(x) == 40: - return big_endian_to_int(decode_hex(x)) - else: - return big_endian_to_int(x) - - -def coerce_to_bytes(x): - if is_numeric(x): - return big_endian_int.serialize(x) - elif len(x) == 40: - return decode_hex(x) - else: - return x - - -def parse_int_or_hex(s): - if is_numeric(s): - return s - elif s[:2] in (b'0x', '0x'): - s = to_string(s) - tail = (b'0' if len(s) % 2 else b'') + s[2:] - return big_endian_to_int(decode_hex(tail)) - else: - return int(s) - - -def ceil32(x): - return x if x % 32 == 0 else x + 32 - (x % 32) - - -def to_signed(i): - return i if i < TT255 else i - TT256 - - -def sha3rlp(x): - return sha3(rlp.encode(x)) - - -# Format encoders/decoders for bin, addr, int - - -def decode_bin(v): - """decodes a bytearray from serialization""" - if not is_string(v): - raise Exception("Value must be binary, not RLP array") - return v - - -def decode_addr(v): - """decodes an address from serialization""" - if len(v) not in [0, 20]: - raise Exception("Serialized addresses must be empty or 20 bytes long!") - return encode_hex(v) - - -def decode_int(v): - """decodes and integer from serialization""" - if len(v) > 0 and (v[0] == b'\x00' or v[0] == 0): - raise Exception("No leading zero bytes allowed for integers") - return big_endian_to_int(v) - - -def decode_int256(v): - return big_endian_to_int(v) - - -def encode_bin(v): - """encodes a bytearray into serialization""" - return v - - -def encode_root(v): - """encodes a trie root into serialization""" - return v - - -def encode_int(v): - """encodes an integer into serialization""" - if not is_numeric(v) or v < 0 or v >= TT256: - raise Exception("Integer invalid or out of range: %r" % v) - return int_to_big_endian(v) - - -def encode_int256(v): - return zpad(int_to_big_endian(v), 256) - - -def scan_bin(v): - if v[:2] in ('0x', b'0x'): - return decode_hex(v[2:]) - else: - return decode_hex(v) - - -def scan_int(v): - if v[:2] in ('0x', b'0x'): - return big_endian_to_int(decode_hex(v[2:])) - else: - return int(v) - - -def int_to_hex(x): - o = encode_hex(encode_int(x)) - return '0x' + (o[1:] if (len(o) > 0 and o[0] == b'0') else o) - - -def remove_0x_head(s): +def remove_0x_prefix(s): return s[2:] if s[:2] in (b'0x', '0x') else s -def parse_as_bin(s): - return decode_hex(s[2:] if s[:2] == '0x' else s) - - -def parse_as_int(s): - return s if is_numeric(s) else int( - '0' + s[2:], 16) if s[:2] == '0x' else int(s) - - -def dump_state(trie): - res = '' - for k, v in list(trie.to_dict().items()): - res += '%r:%r\n' % (encode_hex(k), encode_hex(v)) - return res - - class Denoms(): def __init__(self): @@ -385,13 +214,4 @@ class Denoms(): self.ether = 10 ** 18 self.turing = 2 ** 256 - 1 - -denoms = Denoms() - - address = Binary.fixed_length(20, allow_empty=True) -int20 = BigEndianInt(20) -int32 = BigEndianInt(32) -int256 = BigEndianInt(256) -hash32 = Binary.fixed_length(32) -trie_root = Binary.fixed_length(32, allow_empty=True) diff --git a/mmgen/altcoins/eth/rlp/LICENSE b/mmgen/altcoins/eth/rlp/LICENSE new file mode 100644 index 00000000..35dce3b0 --- /dev/null +++ b/mmgen/altcoins/eth/rlp/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jnnk, Vitalik Buterin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/mmgen/altcoins/eth/rlp/__init__.py b/mmgen/altcoins/eth/rlp/__init__.py new file mode 100644 index 00000000..e043125f --- /dev/null +++ b/mmgen/altcoins/eth/rlp/__init__.py @@ -0,0 +1,14 @@ +from . import sedes # noqa: F401 +from .codec import ( # noqa: F401 + encode, + decode, + infer_sedes, +) +from .exceptions import ( # noqa: F401 + RLPException, + EncodingError, + DecodingError, + SerializationError, + DeserializationError, +) +from .sedes import Serializable # noqa: F401 diff --git a/mmgen/altcoins/eth/rlp/atomic.py b/mmgen/altcoins/eth/rlp/atomic.py new file mode 100644 index 00000000..1898b08d --- /dev/null +++ b/mmgen/altcoins/eth/rlp/atomic.py @@ -0,0 +1,10 @@ +import abc + + +class Atomic(metaclass=abc.ABCMeta): + """ABC for objects that can be RLP encoded as is.""" + pass + + +Atomic.register(bytes) +Atomic.register(bytearray) diff --git a/mmgen/altcoins/eth/rlp/codec.py b/mmgen/altcoins/eth/rlp/codec.py new file mode 100644 index 00000000..61346896 --- /dev/null +++ b/mmgen/altcoins/eth/rlp/codec.py @@ -0,0 +1,277 @@ +import collections + +from ..pyethereum.utils import big_endian_to_int,int_to_big_endian,is_bytes,ALL_BYTES + +from .atomic import Atomic +from .exceptions import EncodingError, DecodingError +from .sedes.binary import Binary as BinaryClass +from .sedes import big_endian_int, binary, boolean, text +from .sedes.lists import List, is_sedes, is_sequence +from .sedes.serializable import Serializable + + +def encode(obj, sedes=None, infer_serializer=True, cache=True): + """Encode a Python object in RLP format. + + By default, the object is serialized in a suitable way first (using + :func:`rlp.infer_sedes`) and then encoded. Serialization can be explicitly + suppressed by setting `infer_serializer` to ``False`` and not passing an + alternative as `sedes`. + + If `obj` has an attribute :attr:`_cached_rlp` (as, notably, + :class:`rlp.Serializable`) and its value is not `None`, this value is + returned bypassing serialization and encoding, unless `sedes` is given (as + the cache is assumed to refer to the standard serialization which can be + replaced by specifying `sedes`). + + If `obj` is a :class:`rlp.Serializable` and `cache` is true, the result of + the encoding will be stored in :attr:`_cached_rlp` if it is empty. + + :param sedes: an object implementing a function ``serialize(obj)`` which will be used to + serialize ``obj`` before encoding, or ``None`` to use the infered one (if any) + :param infer_serializer: if ``True`` an appropriate serializer will be selected using + :func:`rlp.infer_sedes` to serialize `obj` before encoding + :param cache: cache the return value in `obj._cached_rlp` if possible + (default `True`) + :returns: the RLP encoded item + :raises: :exc:`rlp.EncodingError` in the rather unlikely case that the item is too big to + encode (will not happen) + :raises: :exc:`rlp.SerializationError` if the serialization fails + """ + if isinstance(obj, Serializable): + cached_rlp = obj._cached_rlp + if sedes is None and cached_rlp: + return cached_rlp + else: + really_cache = ( + cache and + sedes is None + ) + else: + really_cache = False + + if sedes: + item = sedes.serialize(obj) + elif infer_serializer: + item = infer_sedes(obj).serialize(obj) + else: + item = obj + + result = encode_raw(item) + if really_cache: + obj._cached_rlp = result + return result + + +def encode_raw(item): + """RLP encode (a nested sequence of) :class:`Atomic`s.""" + if isinstance(item, Atomic): + if len(item) == 1 and item[0] < 128: + return item + payload = item + prefix_offset = 128 # string + elif not isinstance(item, str) and isinstance(item, collections.Sequence): + payload = b''.join(encode_raw(x) for x in item) + prefix_offset = 192 # list + else: + msg = 'Cannot encode object of type {0}'.format(type(item).__name__) + raise EncodingError(msg, item) + + try: + prefix = length_prefix(len(payload), prefix_offset) + except ValueError: + raise EncodingError('Item too big to encode', item) + + return prefix + payload + + +LONG_LENGTH = 256**8 + + +def length_prefix(length, offset): + """Construct the prefix to lists or strings denoting their length. + + :param length: the length of the item in bytes + :param offset: ``0x80`` when encoding raw bytes, ``0xc0`` when encoding a + list + """ + if length < 56: + return ALL_BYTES[offset + length] + elif length < LONG_LENGTH: + length_string = int_to_big_endian(length) + return ALL_BYTES[offset + 56 - 1 + len(length_string)] + length_string + else: + raise ValueError('Length greater than 256**8') + + +SHORT_STRING = 128 + 56 + + +def consume_length_prefix(rlp, start): + """Read a length prefix from an RLP string. + + :param rlp: the rlp byte string to read from + :param start: the position at which to start reading + :returns: a tuple ``(prefix, type, length, end)``, where ``type`` is either ``str`` + or ``list`` depending on the type of the following payload, + ``length`` is the length of the payload in bytes, and ``end`` is + the position of the first payload byte in the rlp string + """ + b0 = rlp[start] + if b0 < 128: # single byte + return (b'', bytes, 1, start) + elif b0 < SHORT_STRING: # short string + if b0 - 128 == 1 and rlp[start + 1] < 128: + raise DecodingError('Encoded as short string although single byte was possible', rlp) + return (rlp[start:start + 1], bytes, b0 - 128, start + 1) + elif b0 < 192: # long string + ll = b0 - 183 # - (128 + 56 - 1) + if rlp[start + 1:start + 2] == b'\x00': + raise DecodingError('Length starts with zero bytes', rlp) + len_prefix = rlp[start + 1:start + 1 + ll] + l = big_endian_to_int(len_prefix) # noqa: E741 + if l < 56: + raise DecodingError('Long string prefix used for short string', rlp) + return (rlp[start:start + 1] + len_prefix, bytes, l, start + 1 + ll) + elif b0 < 192 + 56: # short list + return (rlp[start:start + 1], list, b0 - 192, start + 1) + else: # long list + ll = b0 - 192 - 56 + 1 + if rlp[start + 1:start + 2] == b'\x00': + raise DecodingError('Length starts with zero bytes', rlp) + len_prefix = rlp[start + 1:start + 1 + ll] + l = big_endian_to_int(len_prefix) # noqa: E741 + if l < 56: + raise DecodingError('Long list prefix used for short list', rlp) + return (rlp[start:start + 1] + len_prefix, list, l, start + 1 + ll) + + +def consume_payload(rlp, prefix, start, type_, length): + """Read the payload of an item from an RLP string. + + :param rlp: the rlp string to read from + :param type_: the type of the payload (``bytes`` or ``list``) + :param start: the position at which to start reading + :param length: the length of the payload in bytes + :returns: a tuple ``(item, per_item_rlp, end)``, where ``item`` is + the read item, per_item_rlp is a list containing the RLP + encoding of each item and ``end`` is the position of the + first unprocessed byte + """ + if type_ is bytes: + item = rlp[start: start + length] + return (item, [prefix + item], start + length) + elif type_ is list: + items = [] + per_item_rlp = [] + list_rlp = prefix + next_item_start = start + end = next_item_start + length + while next_item_start < end: + p, t, l, s = consume_length_prefix(rlp, next_item_start) + item, item_rlp, next_item_start = consume_payload(rlp, p, s, t, l) + per_item_rlp.append(item_rlp) + # When the item returned above is a single element, item_rlp will also contain a + # single element, but when it's a list, the first element will be the RLP of the + # whole List, which is what we want here. + list_rlp += item_rlp[0] + items.append(item) + per_item_rlp.insert(0, list_rlp) + if next_item_start > end: + raise DecodingError('List length prefix announced a too small ' + 'length', rlp) + return (items, per_item_rlp, next_item_start) + else: + raise TypeError('Type must be either list or bytes') + + +def consume_item(rlp, start): + """Read an item from an RLP string. + + :param rlp: the rlp string to read from + :param start: the position at which to start reading + :returns: a tuple ``(item, per_item_rlp, end)``, where ``item`` is + the read item, per_item_rlp is a list containing the RLP + encoding of each item and ``end`` is the position of the + first unprocessed byte + """ + p, t, l, s = consume_length_prefix(rlp, start) + return consume_payload(rlp, p, s, t, l) + + +def decode(rlp, sedes=None, strict=True, recursive_cache=False, **kwargs): + """Decode an RLP encoded object. + + If the deserialized result `obj` has an attribute :attr:`_cached_rlp` (e.g. if `sedes` is a + subclass of :class:`rlp.Serializable`) it will be set to `rlp`, which will improve performance + on subsequent :func:`rlp.encode` calls. Bear in mind however that `obj` needs to make sure that + this value is updated whenever one of its fields changes or prevent such changes entirely + (:class:`rlp.sedes.Serializable` does the latter). + + :param sedes: an object implementing a function ``deserialize(code)`` which will be applied + after decoding, or ``None`` if no deserialization should be performed + :param \*\*kwargs: additional keyword arguments that will be passed to the deserializer + :param strict: if false inputs that are longer than necessary don't cause an exception + :returns: the decoded and maybe deserialized Python object + :raises: :exc:`rlp.DecodingError` if the input string does not end after the root item and + `strict` is true + :raises: :exc:`rlp.DeserializationError` if the deserialization fails + """ + if not is_bytes(rlp): + raise DecodingError('Can only decode RLP bytes, got type %s' % type(rlp).__name__, rlp) + try: + item, per_item_rlp, end = consume_item(rlp, 0) + except IndexError: + raise DecodingError('RLP string too short', rlp) + if end != len(rlp) and strict: + msg = 'RLP string ends with {} superfluous bytes'.format(len(rlp) - end) + raise DecodingError(msg, rlp) + if sedes: + obj = sedes.deserialize(item, **kwargs) + if is_sequence(obj) or hasattr(obj, '_cached_rlp'): + _apply_rlp_cache(obj, per_item_rlp, recursive_cache) + return obj + else: + return item + + +def _apply_rlp_cache(obj, split_rlp, recursive): + item_rlp = split_rlp.pop(0) + if isinstance(obj, (int, bool, str, bytes, bytearray)): + return + elif hasattr(obj, '_cached_rlp'): + obj._cached_rlp = item_rlp + if not recursive: + return + for sub in obj: + if isinstance(sub, (int, bool, str, bytes, bytearray)): + split_rlp.pop(0) + else: + sub_rlp = split_rlp.pop(0) + _apply_rlp_cache(sub, sub_rlp, recursive) + + +def infer_sedes(obj): + """Try to find a sedes objects suitable for a given Python object. + + The sedes objects considered are `obj`'s class, `big_endian_int` and + `binary`. If `obj` is a sequence, a :class:`rlp.sedes.List` will be + constructed recursively. + + :param obj: the python object for which to find a sedes object + :raises: :exc:`TypeError` if no appropriate sedes could be found + """ + if is_sedes(obj.__class__): + return obj.__class__ + elif not isinstance(obj, bool) and isinstance(obj, int) and obj >= 0: + return big_endian_int + elif BinaryClass.is_valid_type(obj): + return binary + elif not isinstance(obj, str) and isinstance(obj, collections.Sequence): + return List(map(infer_sedes, obj)) + elif isinstance(obj, bool): + return boolean + elif isinstance(obj, str): + return text + msg = 'Did not find sedes handling type {}'.format(type(obj).__name__) + raise TypeError(msg) diff --git a/mmgen/altcoins/eth/rlp/exceptions.py b/mmgen/altcoins/eth/rlp/exceptions.py new file mode 100644 index 00000000..eaf5c31f --- /dev/null +++ b/mmgen/altcoins/eth/rlp/exceptions.py @@ -0,0 +1,144 @@ +class RLPException(Exception): + """Base class for exceptions raised by this package.""" + pass + + +class EncodingError(RLPException): + """Exception raised if encoding fails. + + :ivar obj: the object that could not be encoded + """ + + def __init__(self, message, obj): + super(EncodingError, self).__init__(message) + self.obj = obj + + +class DecodingError(RLPException): + """Exception raised if decoding fails. + + :ivar rlp: the RLP string that could not be decoded + """ + + def __init__(self, message, rlp): + super(DecodingError, self).__init__(message) + self.rlp = rlp + + +class SerializationError(RLPException): + """Exception raised if serialization fails. + + :ivar obj: the object that could not be serialized + """ + + def __init__(self, message, obj): + super(SerializationError, self).__init__(message) + self.obj = obj + + +class ListSerializationError(SerializationError): + """Exception raised if serialization by a :class:`sedes.List` fails. + + :ivar element_exception: the exception that occurred during the serialization of one of the + elements, or `None` if the error is unrelated to a specific element + :ivar index: the index in the list that produced the error or `None` if the error is unrelated + to a specific element + """ + + def __init__(self, message=None, obj=None, element_exception=None, index=None): + if message is None: + assert index is not None + assert element_exception is not None + message = ('Serialization failed because of element at index {} ' + '("{}")'.format(index, str(element_exception))) + super(ListSerializationError, self).__init__(message, obj) + self.index = index + self.element_exception = element_exception + + +class ObjectSerializationError(SerializationError): + """Exception raised if serialization of a :class:`sedes.Serializable` object fails. + + :ivar sedes: the :class:`sedes.Serializable` that failed + :ivar list_exception: exception raised by the underlying list sedes, or `None` if no such + exception has been raised + :ivar field: name of the field of the object that produced the error, or `None` if no field + responsible for the error + """ + + def __init__(self, message=None, obj=None, sedes=None, list_exception=None): + if message is None: + assert list_exception is not None + if list_exception.element_exception is None: + field = None + message = ('Serialization failed because of underlying list ' + '("{}")'.format(str(list_exception))) + else: + assert sedes is not None + field = sedes._meta.field_names[list_exception.index] + message = ('Serialization failed because of field {} ' + '("{}")'.format(field, str(list_exception.element_exception))) + else: + field = None + super(ObjectSerializationError, self).__init__(message, obj) + self.field = field + self.list_exception = list_exception + + +class DeserializationError(RLPException): + """Exception raised if deserialization fails. + + :ivar serial: the decoded RLP string that could not be deserialized + """ + + def __init__(self, message, serial): + super(DeserializationError, self).__init__(message) + self.serial = serial + + +class ListDeserializationError(DeserializationError): + """Exception raised if deserialization by a :class:`sedes.List` fails. + + :ivar element_exception: the exception that occurred during the deserialization of one of the + elements, or `None` if the error is unrelated to a specific element + :ivar index: the index in the list that produced the error or `None` if the error is unrelated + to a specific element + """ + + def __init__(self, message=None, serial=None, element_exception=None, index=None): + if not message: + assert index is not None + assert element_exception is not None + message = ('Deserialization failed because of element at index {} ' + '("{}")'.format(index, str(element_exception))) + super(ListDeserializationError, self).__init__(message, serial) + self.index = index + self.element_exception = element_exception + + +class ObjectDeserializationError(DeserializationError): + """Exception raised if deserialization by a :class:`sedes.Serializable` fails. + + :ivar sedes: the :class:`sedes.Serializable` that failed + :ivar list_exception: exception raised by the underlying list sedes, or `None` if no such + exception has been raised + :ivar field: name of the field of the object that produced the error, or `None` if no field + responsible for the error + """ + + def __init__(self, message=None, serial=None, sedes=None, list_exception=None): + if not message: + assert list_exception is not None + if list_exception.element_exception is None: + field = None + message = ('Deserialization failed because of underlying list ' + '("{}")'.format(str(list_exception))) + else: + assert sedes is not None + field = sedes._meta.field_names[list_exception.index] + message = ('Deserialization failed because of field {} ' + '("{}")'.format(field, str(list_exception.element_exception))) + super(ObjectDeserializationError, self).__init__(message, serial) + self.sedes = sedes + self.list_exception = list_exception + self.field = field diff --git a/mmgen/altcoins/eth/rlp/sedes/__init__.py b/mmgen/altcoins/eth/rlp/sedes/__init__.py new file mode 100644 index 00000000..dfae6575 --- /dev/null +++ b/mmgen/altcoins/eth/rlp/sedes/__init__.py @@ -0,0 +1,7 @@ +from . import raw # noqa: F401 +from .binary import Binary, binary # noqa: F401 +from .boolean import Boolean, boolean # noqa: F401 +from .big_endian_int import BigEndianInt, big_endian_int # noqa: F401 +from .lists import List # noqa: F401 +from .text import Text, text # noqa: #401 +from .serializable import Serializable # noqa: F401 diff --git a/mmgen/altcoins/eth/rlp/sedes/big_endian_int.py b/mmgen/altcoins/eth/rlp/sedes/big_endian_int.py new file mode 100644 index 00000000..0afadd04 --- /dev/null +++ b/mmgen/altcoins/eth/rlp/sedes/big_endian_int.py @@ -0,0 +1,46 @@ +from ...pyethereum.utils import int_to_big_endian,big_endian_to_int +from ..exceptions import DeserializationError,SerializationError + + +class BigEndianInt(object): + """A sedes for big endian integers. + + :param l: the size of the serialized representation in bytes or `None` to + use the shortest possible one + """ + + def __init__(self, l=None): + self.l = l + + def serialize(self, obj): + if isinstance(obj, bool) or not isinstance(obj, int): + raise SerializationError('Can only serialize integers', obj) + if self.l is not None and obj >= 256**self.l: + raise SerializationError('Integer too large (does not fit in {} ' + 'bytes)'.format(self.l), obj) + if obj < 0: + raise SerializationError('Cannot serialize negative integers', obj) + + if obj == 0: + s = b'' + else: + s = int_to_big_endian(obj) + + if self.l is not None: + return b'\x00' * max(0, self.l - len(s)) + s + else: + return s + + def deserialize(self, serial): + if self.l is not None and len(serial) != self.l: + raise DeserializationError('Invalid serialization (wrong size)', + serial) + if self.l is None and len(serial) > 0 and serial[0:1] == b'\x00': + raise DeserializationError('Invalid serialization (not minimal ' + 'length)', serial) + + serial = serial or b'\x00' + return big_endian_to_int(serial) + + +big_endian_int = BigEndianInt() diff --git a/mmgen/altcoins/eth/rlp/sedes/binary.py b/mmgen/altcoins/eth/rlp/sedes/binary.py new file mode 100644 index 00000000..7b6df453 --- /dev/null +++ b/mmgen/altcoins/eth/rlp/sedes/binary.py @@ -0,0 +1,55 @@ +from ..exceptions import SerializationError,DeserializationError +from ..atomic import Atomic + + +class Binary(object): + """A sedes object for binary data of certain length. + + :param min_length: the minimal length in bytes or `None` for no lower limit + :param max_length: the maximal length in bytes or `None` for no upper limit + :param allow_empty: if true, empty strings are considered valid even if + a minimum length is required otherwise + """ + + def __init__(self, min_length=None, max_length=None, allow_empty=False): + self.min_length = min_length or 0 + if max_length is None: + self.max_length = float('inf') + else: + self.max_length = max_length + self.allow_empty = allow_empty + + @classmethod + def fixed_length(cls, l, allow_empty=False): + """Create a sedes for binary data with exactly `l` bytes.""" + return cls(l, l, allow_empty=allow_empty) + + @classmethod + def is_valid_type(cls, obj): + return isinstance(obj, (bytes, bytearray)) + + def is_valid_length(self, l): + return any((self.min_length <= l <= self.max_length, + self.allow_empty and l == 0)) + + def serialize(self, obj): + if not Binary.is_valid_type(obj): + raise SerializationError('Object is not a serializable ({})'.format(type(obj)), obj) + + if not self.is_valid_length(len(obj)): + raise SerializationError('Object has invalid length', obj) + + return obj + + def deserialize(self, serial): + if not isinstance(serial, Atomic): + m = 'Objects of type {} cannot be deserialized' + raise DeserializationError(m.format(type(serial).__name__), serial) + + if self.is_valid_length(len(serial)): + return serial + else: + raise DeserializationError('{} has invalid length'.format(type(serial)), serial) + + +binary = Binary() diff --git a/mmgen/altcoins/eth/rlp/sedes/boolean.py b/mmgen/altcoins/eth/rlp/sedes/boolean.py new file mode 100644 index 00000000..2fef17a7 --- /dev/null +++ b/mmgen/altcoins/eth/rlp/sedes/boolean.py @@ -0,0 +1,30 @@ +from ..exceptions import DeserializationError,SerializationError + + +class Boolean: + """A sedes for booleans + """ + def serialize(self, obj): + if not isinstance(obj, bool): + raise SerializationError('Can only serialize integers', obj) + + if obj is False: + return b'' + elif obj is True: + return b'\x01' + else: + raise Exception("Invariant: no other options for boolean values") + + def deserialize(self, serial): + if serial == b'': + return False + elif serial == b'\x01': + return True + else: + raise DeserializationError( + 'Invalid serialized boolean. Must be either 0x01 or 0x00', + serial + ) + + +boolean = Boolean() diff --git a/mmgen/altcoins/eth/rlp/sedes/lists.py b/mmgen/altcoins/eth/rlp/sedes/lists.py new file mode 100644 index 00000000..5aae7aa1 --- /dev/null +++ b/mmgen/altcoins/eth/rlp/sedes/lists.py @@ -0,0 +1,92 @@ +""" +Module for sedes objects that use lists as serialization format. +""" +from collections import Sequence + +from ...pyethereum.utils import to_list,to_tuple + +from ..exceptions import ( + SerializationError, + ListSerializationError, + DeserializationError, + ListDeserializationError, +) + +from .binary import ( + Binary as BinaryClass, +) + + +def is_sedes(obj): + """Check if `obj` is a sedes object. + + A sedes object is characterized by having the methods `serialize(obj)` and + `deserialize(serial)`. + """ + return hasattr(obj, 'serialize') and hasattr(obj, 'deserialize') + + +def is_sequence(obj): + """Check if `obj` is a sequence, but not a string or bytes.""" + return isinstance(obj, Sequence) and not ( + isinstance(obj, str) or BinaryClass.is_valid_type(obj)) + + +class List(list): + + """A sedes for lists, implemented as a list of other sedes objects. + + :param strict: If true (de)serializing lists that have a length not + matching the sedes length will result in an error. If false + (de)serialization will stop as soon as either one of the + lists runs out of elements. + """ + + def __init__(self, elements=None, strict=True): + super(List, self).__init__() + self.strict = strict + + if elements: + for e in elements: + if is_sedes(e): + self.append(e) + elif isinstance(e, Sequence): + self.append(List(e)) + else: + raise TypeError( + 'Instances of List must only contain sedes objects or ' + 'nested sequences thereof.' + ) + + @to_list + def serialize(self, obj): + if not is_sequence(obj): + raise ListSerializationError('Can only serialize sequences', obj) + if self.strict and len(self) != len(obj): + raise ListSerializationError( + 'Serializing list length (%d) does not match sedes (%d)' % ( + len(obj), len(self)), + obj) + + for index, (element, sedes) in enumerate(zip(obj, self)): + try: + yield sedes.serialize(element) + except SerializationError as e: + raise ListSerializationError(obj=obj, element_exception=e, index=index) + + @to_tuple + def deserialize(self, serial): + if not is_sequence(serial): + raise ListDeserializationError('Can only deserialize sequences', serial) + + if self.strict and len(serial) != len(self): + raise ListDeserializationError( + 'Deserializing list length (%d) does not match sedes (%d)' % ( + len(serial), len(self)), + serial) + + for idx, (sedes, element) in enumerate(zip(self, serial)): + try: + yield sedes.deserialize(element) + except DeserializationError as e: + raise ListDeserializationError(serial=serial, element_exception=e, index=idx) diff --git a/mmgen/altcoins/eth/rlp/sedes/raw.py b/mmgen/altcoins/eth/rlp/sedes/raw.py new file mode 100644 index 00000000..4f241e85 --- /dev/null +++ b/mmgen/altcoins/eth/rlp/sedes/raw.py @@ -0,0 +1,29 @@ +""" +A sedes that does nothing. Thus, everything that can be directly encoded by RLP +is serializable. This sedes can be used as a placeholder when deserializing +larger structures. +""" +from collections import Sequence + +from ..exceptions import SerializationError +from ..atomic import Atomic + + +def serializable(obj): + if isinstance(obj, Atomic): + return True + elif not isinstance(obj, str) and isinstance(obj, Sequence): + return all(map(serializable, obj)) + else: + return False + + +def serialize(obj): + if not serializable(obj): + raise SerializationError('Can only serialize nested lists of strings', + obj) + return obj + + +def deserialize(serial): + return serial diff --git a/mmgen/altcoins/eth/rlp/sedes/serializable.py b/mmgen/altcoins/eth/rlp/sedes/serializable.py new file mode 100644 index 00000000..ce141523 --- /dev/null +++ b/mmgen/altcoins/eth/rlp/sedes/serializable.py @@ -0,0 +1,484 @@ +import abc +import collections +import copy +import enum +import re + +from ...pyethereum.utils import to_dict,to_set,to_tuple + +from ..exceptions import ( + ListSerializationError, + ObjectSerializationError, + ListDeserializationError, + ObjectDeserializationError, +) + +from .lists import ( + List, +) + + +class MetaBase: + fields = None + field_names = None + field_attrs = None + sedes = None + + +def _get_duplicates(values): + counts = collections.Counter(values) + return tuple( + item + for item, num in counts.items() + if num > 1 + ) + + +def validate_args_and_kwargs(args, kwargs, arg_names, allow_missing=False): + duplicate_arg_names = _get_duplicates(arg_names) + if duplicate_arg_names: + raise TypeError("Duplicate argument names: {0}".format(sorted(duplicate_arg_names))) + + needed_kwargs = arg_names[len(args):] + used_kwargs = set(arg_names[:len(args)]) + + duplicate_kwargs = used_kwargs.intersection(kwargs.keys()) + if duplicate_kwargs: + raise TypeError("Duplicate kwargs: {0}".format(sorted(duplicate_kwargs))) + + unknown_kwargs = set(kwargs.keys()).difference(arg_names) + if unknown_kwargs: + raise TypeError("Unknown kwargs: {0}".format(sorted(unknown_kwargs))) + + missing_kwargs = set(needed_kwargs).difference(kwargs.keys()) + if not allow_missing and missing_kwargs: + raise TypeError("Missing kwargs: {0}".format(sorted(missing_kwargs))) + + +@to_tuple +def merge_kwargs_to_args(args, kwargs, arg_names, allow_missing=False): + validate_args_and_kwargs(args, kwargs, arg_names, allow_missing=allow_missing) + + needed_kwargs = arg_names[len(args):] + + yield from args + for arg_name in needed_kwargs: + yield kwargs[arg_name] + + +@to_dict +def merge_args_to_kwargs(args, kwargs, arg_names, allow_missing=False): + validate_args_and_kwargs(args, kwargs, arg_names, allow_missing=allow_missing) + + yield from kwargs.items() + for value, name in zip(args, arg_names): + yield name, value + + +def _eq(left, right): + """ + Equality comparison that allows for equality between tuple and list types + with equivalent elements. + """ + if isinstance(left, (tuple, list)) and isinstance(right, (tuple, list)): + return len(left) == len(right) and all(_eq(*pair) for pair in zip(left, right)) + else: + return left == right + + +class ChangesetState(enum.Enum): + INITIALIZED = 'INITIALIZED' + OPEN = 'OPEN' + CLOSED = 'CLOSED' + + +class ChangesetField: + field = None + + def __init__(self, field): + self.field = field + + def __get__(self, instance, type=None): + if instance is None: + return self + elif instance.__state__ is not ChangesetState.OPEN: + raise AttributeError("Changeset is not active. Attribute access not allowed") + else: + try: + return instance.__diff__[self.field] + except KeyError: + return getattr(instance.__original__, self.field) + + def __set__(self, instance, value): + if instance.__state__ is not ChangesetState.OPEN: + raise AttributeError("Changeset is not active. Attribute access not allowed") + instance.__diff__[self.field] = value + + +class BaseChangeset: + # reference to the original Serializable instance. + __original__ = None + # the state of this fieldset. Initialized -> Open -> Closed + __state__ = None + # the field changes that have been made in this change + __diff__ = None + + def __init__(self, obj, changes=None): + self.__original__ = obj + self.__state__ = ChangesetState.INITIALIZED + self.__diff__ = changes or {} + + def commit(self): + obj = self.build_rlp() + self.close() + return obj + + def build_rlp(self): + if self.__state__ == ChangesetState.OPEN: + field_kwargs = { + name: self.__diff__.get(name, self.__original__[name]) + for name + in self.__original__._meta.field_names + } + return type(self.__original__)(**field_kwargs) + else: + raise ValueError("Cannot open Changeset which is not in the OPEN state") + + def open(self): + if self.__state__ == ChangesetState.INITIALIZED: + self.__state__ = ChangesetState.OPEN + else: + raise ValueError("Cannot open Changeset which is not in the INITIALIZED state") + + def close(self): + if self.__state__ == ChangesetState.OPEN: + self.__state__ = ChangesetState.CLOSED + else: + raise ValueError("Cannot open Changeset which is not in the INITIALIZED state") + + def __enter__(self): + if self.__state__ == ChangesetState.INITIALIZED: + self.open() + return self + else: + raise ValueError("Cannot open Changeset which is not in the INITIALIZED state") + + def __exit__(self, exc_type, exc_value, traceback): + if self.__state__ == ChangesetState.OPEN: + self.close() + + +def Changeset(obj, changes): + namespace = { + name: ChangesetField(name) + for name + in obj._meta.field_names + } + cls = type( + "{0}Changeset".format(obj.__class__.__name__), + (BaseChangeset,), + namespace, + ) + return cls(obj, changes) + + +class BaseSerializable(collections.Sequence): + def __init__(self, *args, **kwargs): + if kwargs: + field_values = merge_kwargs_to_args(args, kwargs, self._meta.field_names) + else: + field_values = args + + if len(field_values) != len(self._meta.field_names): + raise TypeError( + 'Argument count mismatch. expected {0} - got {1} - missing {2}'.format( + len(self._meta.field_names), + len(field_values), + ','.join(self._meta.field_names[len(field_values):]), + ) + ) + + for value, attr in zip(field_values, self._meta.field_attrs): + setattr(self, attr, make_immutable(value)) + + _cached_rlp = None + + def as_dict(self): + return dict( + (field, value) + for field, value + in zip(self._meta.field_names, self) + ) + + def __iter__(self): + for attr in self._meta.field_attrs: + yield getattr(self, attr) + + def __getitem__(self, idx): + if isinstance(idx, int): + attr = self._meta.field_attrs[idx] + return getattr(self, attr) + elif isinstance(idx, slice): + field_slice = self._meta.field_attrs[idx] + return tuple(getattr(self, field) for field in field_slice) + elif isinstance(idx, str): + return getattr(self, idx) + else: + raise IndexError("Unsupported type for __getitem__: {0}".format(type(idx))) + + def __len__(self): + return len(self._meta.fields) + + def __eq__(self, other): + return isinstance(other, Serializable) and hash(self) == hash(other) + + def __getstate__(self): + state = self.__dict__.copy() + # The hash() builtin is not stable across processes + # (https://docs.python.org/3/reference/datamodel.html#object.__hash__), so we do this here + # to ensure pickled instances don't carry the cached hash() as that may cause issues like + # https://github.com/ethereum/py-evm/issues/1318 + state['_hash_cache'] = None + return state + + _hash_cache = None + + def __hash__(self): + if self._hash_cache is None: + self._hash_cache = hash(tuple(self)) + + return self._hash_cache + + @classmethod + def serialize(cls, obj): + try: + return cls._meta.sedes.serialize(obj) + except ListSerializationError as e: + raise ObjectSerializationError(obj=obj, sedes=cls, list_exception=e) + + @classmethod + def deserialize(cls, serial, **extra_kwargs): + try: + values = cls._meta.sedes.deserialize(serial) + except ListDeserializationError as e: + raise ObjectDeserializationError(serial=serial, sedes=cls, list_exception=e) + + args_as_kwargs = merge_args_to_kwargs(values, {}, cls._meta.field_names) + return cls(**args_as_kwargs, **extra_kwargs) + + def copy(self, *args, **kwargs): + missing_overrides = set( + self._meta.field_names + ).difference( + kwargs.keys() + ).difference( + self._meta.field_names[:len(args)] + ) + unchanged_kwargs = { + key: copy.deepcopy(value) + for key, value + in self.as_dict().items() + if key in missing_overrides + } + combined_kwargs = dict(**unchanged_kwargs, **kwargs) + all_kwargs = merge_args_to_kwargs(args, combined_kwargs, self._meta.field_names) + return type(self)(**all_kwargs) + + def __copy__(self): + return self.copy() + + def __deepcopy__(self, *args): + return self.copy() + + _in_mutable_context = False + + def build_changeset(self, *args, **kwargs): + args_as_kwargs = merge_args_to_kwargs( + args, + kwargs, + self._meta.field_names, + allow_missing=True, + ) + return Changeset(self, changes=args_as_kwargs) + + +def make_immutable(value): + if isinstance(value, list): + return tuple(make_immutable(item) for item in value) + else: + return value + + +@to_tuple +def _mk_field_attrs(field_names, extra_namespace): + namespace = set(field_names).union(extra_namespace) + for field in field_names: + while True: + field = '_' + field + if field not in namespace: + namespace.add(field) + yield field + break + + +def _mk_field_property(field, attr): + def field_fn_getter(self): + return getattr(self, attr) + + def field_fn_setter(self, value): + if not self._in_mutable_context: + raise AttributeError("can't set attribute") + setattr(self, attr, value) + + return property(field_fn_getter, field_fn_setter) + + +IDENTIFIER_REGEX = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE) + + +def _is_valid_identifier(value): + # Source: https://stackoverflow.com/questions/5474008/regular-expression-to-confirm-whether-a-string-is-a-valid-identifier-in-python # noqa: E501 + if not isinstance(value, str): + return False + return bool(IDENTIFIER_REGEX.match(value)) + + +@to_set +def _get_class_namespace(cls): + if hasattr(cls, '__dict__'): + yield from cls.__dict__.keys() + if hasattr(cls, '__slots__'): + yield from cls.__slots__ + + +class SerializableBase(abc.ABCMeta): + def __new__(cls, name, bases, attrs): + super_new = super(SerializableBase, cls).__new__ + + serializable_bases = tuple(b for b in bases if isinstance(b, SerializableBase)) + has_multiple_serializable_parents = len(serializable_bases) > 1 + is_serializable_subclass = any(serializable_bases) + declares_fields = 'fields' in attrs + + if not is_serializable_subclass: + # If this is the original creation of the `Serializable` class, + # just create the class. + return super_new(cls, name, bases, attrs) + elif not declares_fields: + if has_multiple_serializable_parents: + raise TypeError( + "Cannot create subclass from multiple parent `Serializable` " + "classes without explicit `fields` declaration." + ) + else: + # This is just a vanilla subclass of a `Serializable` parent class. + parent_serializable = serializable_bases[0] + + if hasattr(parent_serializable, '_meta'): + fields = parent_serializable._meta.fields + else: + # This is a subclass of `Serializable` which has no + # `fields`, likely intended for further subclassing. + fields = () + else: + # ensure that the `fields` property is a tuple of tuples to ensure + # immutability. + fields = tuple(tuple(field) for field in attrs.pop('fields')) + + # split the fields into names and sedes + if fields: + field_names, sedes = zip(*fields) + else: + field_names, sedes = (), () + + # check that field names are unique + duplicate_field_names = _get_duplicates(field_names) + if duplicate_field_names: + raise TypeError( + "The following fields are duplicated in the `fields` " + "declaration: " + "{0}".format(",".join(sorted(duplicate_field_names))) + ) + + # check that field names are valid identifiers + invalid_field_names = { + field_name + for field_name + in field_names + if not _is_valid_identifier(field_name) + } + if invalid_field_names: + raise TypeError( + "The following field names are not valid python identifiers: {0}".format( + ",".join("`{0}`".format(item) for item in sorted(invalid_field_names)) + ) + ) + + # extract all of the fields from parent `Serializable` classes. + parent_field_names = { + field_name + for base in serializable_bases if hasattr(base, '_meta') + for field_name in base._meta.field_names + } + + # check that all fields from parent serializable classes are + # represented on this class. + missing_fields = parent_field_names.difference(field_names) + if missing_fields: + raise TypeError( + "Subclasses of `Serializable` **must** contain a full superset " + "of the fields defined in their parent classes. The following " + "fields are missing: " + "{0}".format(",".join(sorted(missing_fields))) + ) + + # the actual field values are stored in separate *private* attributes. + # This computes attribute names that don't conflict with other + # attributes already present on the class. + reserved_namespace = set(attrs.keys()).union( + attr + for base in bases + for parent_cls in base.__mro__ + for attr in _get_class_namespace(parent_cls) + ) + field_attrs = _mk_field_attrs(field_names, reserved_namespace) + + # construct the Meta object to store field information for the class + meta_namespace = { + 'fields': fields, + 'field_attrs': field_attrs, + 'field_names': field_names, + 'sedes': List(sedes), + } + + meta_base = attrs.pop('_meta', MetaBase) + meta = type( + 'Meta', + (meta_base,), + meta_namespace, + ) + attrs['_meta'] = meta + + # construct `property` attributes for read only access to the fields. + field_props = tuple( + (field, _mk_field_property(field, attr)) + for field, attr + in zip(meta.field_names, meta.field_attrs) + ) + + return super_new( + cls, + name, + bases, + dict( + field_props + + tuple(attrs.items()) + ), + ) + + +class Serializable(BaseSerializable, metaclass=SerializableBase): + """ + The base class for serializable objects. + """ + pass diff --git a/mmgen/altcoins/eth/rlp/sedes/text.py b/mmgen/altcoins/eth/rlp/sedes/text.py new file mode 100644 index 00000000..159c81f7 --- /dev/null +++ b/mmgen/altcoins/eth/rlp/sedes/text.py @@ -0,0 +1,64 @@ +from ..exceptions import SerializationError,DeserializationError +from ..atomic import Atomic + + +class Text: + """A sedes object for encoded text data of certain length. + + :param min_length: the minimal length in encoded characters or `None` for no lower limit + :param max_length: the maximal length in encoded characters or `None` for no upper limit + :param allow_empty: if true, empty strings are considered valid even if + a minimum length is required otherwise + """ + + def __init__(self, min_length=None, max_length=None, allow_empty=False, encoding='utf8'): + self.min_length = min_length or 0 + if max_length is None: + self.max_length = float('inf') + else: + self.max_length = max_length + self.allow_empty = allow_empty + self.encoding = encoding + + @classmethod + def fixed_length(cls, l, allow_empty=False): + """Create a sedes for text data with exactly `l` encoded characters.""" + return cls(l, l, allow_empty=allow_empty) + + @classmethod + def is_valid_type(cls, obj): + return isinstance(obj, str) + + def is_valid_length(self, l): + return any(( + self.min_length <= l <= self.max_length, + self.allow_empty and l == 0 + )) + + def serialize(self, obj): + if not self.is_valid_type(obj): + raise SerializationError('Object is not a serializable ({})'.format(type(obj)), obj) + + if not self.is_valid_length(len(obj)): + raise SerializationError('Object has invalid length', obj) + + return obj.encode(self.encoding) + + def deserialize(self, serial): + if not isinstance(serial, Atomic): + m = 'Objects of type {} cannot be deserialized' + raise DeserializationError(m.format(type(serial).__name__), serial) + + try: + text_value = serial.decode(self.encoding) + except UnicodeDecodeError as err: + raise DeserializationError(str(err), serial) + + if self.is_valid_length(len(text_value)): + return text_value + else: + raise DeserializationError('{} has invalid length'.format(type(serial)), serial) + + +text = Text() + diff --git a/mmgen/altcoins/eth/tw.py b/mmgen/altcoins/eth/tw.py index bfd7649c..90dce27b 100755 --- a/mmgen/altcoins/eth/tw.py +++ b/mmgen/altcoins/eth/tw.py @@ -25,7 +25,7 @@ from mmgen.common import * from mmgen.obj import ETHAmt,TwMMGenID,TwComment,TwLabel from mmgen.tw import TrackingWallet,TwAddrList,TwUnspentOutputs from mmgen.addr import AddrData -from mmgen.altcoins.eth.contract import Token +from .contract import Token class EthereumTrackingWallet(TrackingWallet): diff --git a/mmgen/altcoins/eth/tx.py b/mmgen/altcoins/eth/tx.py index 1a302e52..fcca7436 100755 --- a/mmgen/altcoins/eth/tx.py +++ b/mmgen/altcoins/eth/tx.py @@ -92,9 +92,9 @@ class EthereumMMGenTX(MMGenTX): if self.check_sigs(): try: from ethereum.transactions import Transaction - except: from mmgen.altcoins.eth.pyethereum.transactions import Transaction + except: from .pyethereum.transactions import Transaction - import rlp + from . import rlp etx = rlp.decode(bytes.fromhex(self.hex),Transaction) d = etx.to_dict() # ==> hex values have '0x' prefix, 0 is '0x' for k in ('sender','to','data'): @@ -288,12 +288,12 @@ class EthereumMMGenTX(MMGenTX): 'data': bytes.fromhex(d['data'])} try: from ethereum.transactions import Transaction - except: from mmgen.altcoins.eth.pyethereum.transactions import Transaction + except: from .pyethereum.transactions import Transaction etx = Transaction(**d_in).sign(wif,d['chainId']) assert etx.sender.hex() == d['from'],( 'Sender address recovered from signature does not match true sender') - import rlp + from . import rlp self.hex = rlp.encode(etx).hex() self.coin_txid = CoinTxID(etx.hash.hex()) if d['data']: @@ -410,7 +410,7 @@ class EthereumTokenMMGenTX(EthereumMMGenTX): if self.outputs[0].is_chg: send_acct_tbal = '0' else: - from mmgen.altcoins.eth.contract import Token + from .contract import Token send_acct_tbal = Token(g.token).balance(self.inputs[0].addr) - self.outputs[0].amt return m.format(ETHAmt(change_amt).hl(),g.coin,ETHAmt(send_acct_tbal).hl(),g.dcoin) @@ -431,7 +431,7 @@ class EthereumTokenMMGenTX(EthereumMMGenTX): def make_txobj(self): super(EthereumTokenMMGenTX,self).make_txobj() - from mmgen.altcoins.eth.contract import Token + from .contract import Token t = Token(g.token) o = t.txcreate( self.inputs[0].addr, self.outputs[0].addr, @@ -444,7 +444,7 @@ class EthereumTokenMMGenTX(EthereumMMGenTX): def check_txfile_hex_data(self): d = super(EthereumTokenMMGenTX,self).check_txfile_hex_data() o = self.txobj - from mmgen.altcoins.eth.contract import Token + from .contract import Token if self.check_sigs(): # online, from rlp rpc_init() o['token_addr'] = TokenAddr(o['to']) @@ -462,7 +462,7 @@ class EthereumTokenMMGenTX(EthereumMMGenTX): r=super(EthereumTokenMMGenTX,self).format_view_body(*args,**kwargs)) def do_sign(self,d,wif,tx_num_str): - from mmgen.altcoins.eth.contract import Token + from .contract import Token d = self.txobj t = Token(d['token_addr'],decimals=d['decimals']) tx_in = t.txcreate(d['from'],d['to'],d['amt'],self.start_gas,d['gasPrice'],nonce=d['nonce']) diff --git a/scripts/test-release.sh b/scripts/test-release.sh index bf68f664..9a0b4d5a 100755 --- a/scripts/test-release.sh +++ b/scripts/test-release.sh @@ -205,13 +205,13 @@ t_alts=( "$gentest_py --coin=ltc 2:ext $rounds" "$gentest_py --coin=ltc --type=compressed 2:ext $rounds" # "$gentest_py --coin=ltc --type=segwit 2:ext $rounds" # pycoin generates old-style LTC Segwit addrs - "$gentest_py --coin=etc 2:ext $rounds" - "$gentest_py --coin=eth 2:ext $rounds" +# "$gentest_py --coin=etc 2:ext $rounds" # no pythereum +# "$gentest_py --coin=eth 2:ext $rounds" "$gentest_py --coin=zec 2:ext $rounds" "$gentest_py --coin=zec --type=zcash_z 2:ext $rounds_mid" "$gentest_py --all 2:pycoin $rounds" - "$gentest_py --all 2:pyethereum $rounds" +# "$gentest_py --all 2:pyethereum $rounds" "$gentest_py --all 2:keyconv $rounds_mid" "$gentest_py --all 2:zcash_mini $rounds_mid") if [ "$MINGW" ]; then diff --git a/setup.py b/setup.py index 20671423..c05c6c14 100755 --- a/setup.py +++ b/setup.py @@ -152,10 +152,26 @@ setup( 'mmgen.altcoins.eth.obj', 'mmgen.altcoins.eth.tx', 'mmgen.altcoins.eth.tw', + + 'mmgen/altcoins/eth/pyethereum/LICENSE', 'mmgen.altcoins.eth.pyethereum.__init__', 'mmgen.altcoins.eth.pyethereum.transactions', 'mmgen.altcoins.eth.pyethereum.utils', + 'mmgen/altcoins/eth/rlp/LICENSE', + 'mmgen/altcoins/eth/rlp/__init__', + 'mmgen/altcoins/eth/rlp/atomic', + 'mmgen/altcoins/eth/rlp/codec', + 'mmgen/altcoins/eth/rlp/exceptions', + 'mmgen/altcoins/eth/rlp/sedes/__init__', + 'mmgen/altcoins/eth/rlp/sedes/big_endian_int', + 'mmgen/altcoins/eth/rlp/sedes/binary', + 'mmgen/altcoins/eth/rlp/sedes/boolean', + 'mmgen/altcoins/eth/rlp/sedes/lists', + 'mmgen/altcoins/eth/rlp/sedes/raw', + 'mmgen/altcoins/eth/rlp/sedes/serializable', + 'mmgen/altcoins/eth/rlp/sedes/text', + 'mmgen.main', 'mmgen.main_addrgen', 'mmgen.main_addrimport', diff --git a/test/test_py_d/ts_ethdev.py b/test/test_py_d/ts_ethdev.py index 5a003c92..d02afc09 100755 --- a/test/test_py_d/ts_ethdev.py +++ b/test/test_py_d/ts_ethdev.py @@ -373,7 +373,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): args = ['98831F3A:E:1,123.456'] return self.txcreate(args=args,menu=menu,acct='1',non_mmgen_inputs=1) - def txsign1(self): return self.txsign() + def txsign1(self): return self.txsign(add_args=['--use-internal-keccak-module']) def txsign1_ni(self): return self.txsign(ni=True) def txsend1(self): return self.txsend() def bal1(self): return self.bal(n='1')