Browse Source

Eliminate dependencies on all ethereum packages except py_ecc

The rationale for this patch is similar to that of commit a7126ed:

- Many packages were imported for the sake of just a few trivial conversion
  functions.  These functions have been copied into the local pyethereum.utils.

- rlp has been locally copied and its import statements modified to import
  the locally-copied functions.  Unneeded classes and functions have been
  removed.

- As a result, dependencies on the following external packages have been
  eliminated:

  + rlp
  + eth-hash
  + eth-utils
  + eth-typing
  + toolz
  + cytoolz
  + setuptools
MMGen 6 years ago
parent
commit
66d0f76

+ 2 - 2
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]

+ 21 - 0
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.

+ 6 - 5
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):

+ 58 - 238
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)

+ 21 - 0
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.

+ 14 - 0
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

+ 10 - 0
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)

+ 277 - 0
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)

+ 144 - 0
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

+ 7 - 0
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

+ 46 - 0
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()

+ 55 - 0
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()

+ 30 - 0
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()

+ 92 - 0
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)

+ 29 - 0
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

+ 484 - 0
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

+ 64 - 0
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()
+

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

+ 8 - 8
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'])

+ 3 - 3
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

+ 16 - 0
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',

+ 1 - 1
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')