Skip to content

Commit

Permalink
Merge pull request #89 from davesque/packed-mode
Browse files Browse the repository at this point in the history
Add support for non-standard packed mode
  • Loading branch information
davesque committed Aug 10, 2018
2 parents 2edf081 + aa7cd7a commit d67e89c
Show file tree
Hide file tree
Showing 12 changed files with 481 additions and 101 deletions.
28 changes: 28 additions & 0 deletions docs/encoding.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,31 @@ for a given ABI type using :any:`encode_single`:

>>> is_encodable('(int,bool)', (0, 0))
False

Non-Standard Packed Mode Encoding
---------------------------------

.. warning::

Non-standard packed mode encoding is an experimental feature in the eth-abi
library. Use at your own risk and please report any problems at
https://github.com/ethereum/eth-abi/issues.

In certain cases, the Solidity programming language uses a non-standard packed
encoding. You can encode values in this format like so:

.. doctest::

>>> from eth_abi.packed import encode_single_packed, encode_abi_packed

>>> encode_single_packed('uint32', 12345)
b'\x00\x0009'

>>> encode_single_packed('(int8[],uint32)', ([1, 2, 3, 4], 12345))
b'\x01\x02\x03\x04\x00\x0009'

>>> encode_abi_packed(['int8[]', 'uint32'], ([1, 2, 3, 4], 12345))
b'\x01\x02\x03\x04\x00\x0009'

More information about this encoding format is available at
https://solidity.readthedocs.io/en/develop/abi-spec.html#non-standard-packed-mode.
108 changes: 108 additions & 0 deletions eth_abi/encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ def from_type_str(cls, abi_type, registry):
return cls()


class PackedBooleanEncoder(BooleanEncoder):
data_byte_size = 1


class NumberEncoder(Fixed32ByteSizeEncoder):
is_big_endian = True
bounds_fn = None
Expand Down Expand Up @@ -277,6 +281,15 @@ def from_type_str(cls, abi_type, registry):
encode_uint_256 = UnsignedIntegerEncoder(value_bit_size=256, data_byte_size=32)


class PackedUnsignedIntegerEncoder(UnsignedIntegerEncoder):
@parse_type_str('uint')
def from_type_str(cls, abi_type, registry):
return cls(
value_bit_size=abi_type.sub,
data_byte_size=abi_type.sub // 8,
)


class SignedIntegerEncoder(NumberEncoder):
bounds_fn = staticmethod(compute_signed_integer_bounds)
type_check_fn = staticmethod(is_integer)
Expand All @@ -300,6 +313,15 @@ def from_type_str(cls, abi_type, registry):
return cls(value_bit_size=abi_type.sub)


class PackedSignedIntegerEncoder(SignedIntegerEncoder):
@parse_type_str('int')
def from_type_str(cls, abi_type, registry):
return cls(
value_bit_size=abi_type.sub,
data_byte_size=abi_type.sub // 8,
)


class BaseFixedEncoder(NumberEncoder):
frac_places = None

Expand Down Expand Up @@ -362,6 +384,18 @@ def from_type_str(cls, abi_type, registry):
)


class PackedUnsignedFixedEncoder(UnsignedFixedEncoder):
@parse_type_str('ufixed')
def from_type_str(cls, abi_type, registry):
value_bit_size, frac_places = abi_type.sub

return cls(
value_bit_size=value_bit_size,
data_byte_size=value_bit_size // 8,
frac_places=frac_places,
)


class SignedFixedEncoder(BaseFixedEncoder):
def bounds_fn(self, value_bit_size):
return compute_signed_fixed_bounds(self.value_bit_size, self.frac_places)
Expand Down Expand Up @@ -396,6 +430,18 @@ def from_type_str(cls, abi_type, registry):
)


class PackedSignedFixedEncoder(SignedFixedEncoder):
@parse_type_str('fixed')
def from_type_str(cls, abi_type, registry):
value_bit_size, frac_places = abi_type.sub

return cls(
value_bit_size=value_bit_size,
data_byte_size=value_bit_size // 8,
frac_places=frac_places,
)


class AddressEncoder(Fixed32ByteSizeEncoder):
value_bit_size = 20 * 8
encode_fn = staticmethod(to_canonical_address)
Expand All @@ -422,6 +468,10 @@ def from_type_str(cls, abi_type, registry):
return cls()


class PackedAddressEncoder(AddressEncoder):
data_byte_size = 20


class BytesEncoder(Fixed32ByteSizeEncoder):
is_big_endian = False

Expand Down Expand Up @@ -450,6 +500,15 @@ def from_type_str(cls, abi_type, registry):
return cls(value_bit_size=abi_type.sub * 8)


class PackedBytesEncoder(BytesEncoder):
@parse_type_str('bytes')
def from_type_str(cls, abi_type, registry):
return cls(
value_bit_size=abi_type.sub * 8,
data_byte_size=abi_type.sub,
)


class ByteStringEncoder(BaseEncoder):
is_dynamic = True

Expand Down Expand Up @@ -482,6 +541,15 @@ def from_type_str(cls, abi_type, registry):
return cls()


class PackedByteStringEncoder(ByteStringEncoder):
is_dynamic = False

@classmethod
def encode(cls, value):
cls.validate_value(value)
return value


class TextStringEncoder(BaseEncoder):
is_dynamic = True

Expand Down Expand Up @@ -516,6 +584,15 @@ def from_type_str(cls, abi_type, registry):
return cls()


class PackedTextStringEncoder(TextStringEncoder):
is_dynamic = False

@classmethod
def encode(cls, value):
cls.validate_value(value)
return codecs.encode(value, 'utf8')


class BaseArrayEncoder(BaseEncoder):
item_encoder = None

Expand Down Expand Up @@ -563,6 +640,37 @@ def from_type_str(cls, abi_type, registry):
return DynamicArrayEncoder(item_encoder=item_encoder)


class PackedArrayEncoder(BaseArrayEncoder):
array_size = None

def validate_value(self, value):
super().validate_value(value)

if self.array_size is not None and len(value) != self.array_size:
raise ValueOutOfBounds(
"Expected value with length {0}. Provided value has {1} "
"elements".format(self.array_size, len(value))
)

def encode(self, value):
encoded_elements = self.encode_elements(value)

return encoded_elements

@parse_type_str(with_arrlist=True)
def from_type_str(cls, abi_type, registry):
item_encoder = registry.get_encoder(str(abi_type.item_type))

array_spec = abi_type.arrlist[-1]
if len(array_spec) == 1:
return cls(
array_size=array_spec[0],
item_encoder=item_encoder,
)
else:
return cls(item_encoder=item_encoder)


class SizedArrayEncoder(BaseArrayEncoder):
array_size = None

Expand Down
70 changes: 70 additions & 0 deletions eth_abi/packed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from typing import (
Any,
Iterable,
)
import warnings

from eth_typing.abi import (
TypeStr,
)

from eth_abi.encoding import (
TupleEncoder,
)
from eth_abi.registry import (
registry_packed,
)
from eth_abi.utils.parsing import (
collapse_type,
)

warnings.warn(
"Packed mode encoding is an experimental feature. Please report any "
"problems at https://github.com/ethereum/eth-abi/issues."
)


def encode_single_packed(typ: TypeStr, arg: Any) -> bytes:
"""
Encodes the python value ``arg`` as a binary value of the ABI type ``typ``
in non-standard packed mode.
:param typ: The string representation of the ABI type that will be used for
encoding e.g. ``'uint256'``, ``'bytes[]'``, ``'(int,int)'``, etc.
:param arg: The python value to be encoded.
:returns: The non-standard packed mode binary representation of the python
value ``arg`` as a value of the ABI type ``typ``.
"""
if isinstance(typ, str):
type_str = typ
else:
type_str = collapse_type(*typ)

encoder = registry_packed.get_encoder(type_str)

return encoder(arg)


def encode_abi_packed(types: Iterable[TypeStr], args: Iterable[Any]) -> bytes:
"""
Encodes the python values in ``args`` as a sequence of binary values of the
ABI types in ``types`` via the head-tail mechanism. Binary values are
encoded in non-standard packed mode.
:param types: An iterable of string representations of the ABI types that
will be used for encoding e.g. ``('uint256', 'bytes[]', '(int,int)')``
:param args: An iterable of python values to be encoded.
:returns: The head-tail encoded non-standard packed mode binary
representation of the python values in ``args`` as values of the ABI types
in ``types``.
"""
encoders = [
registry_packed.get_encoder(type_str)
for type_str in types
]

encoder = TupleEncoder(encoders=encoders)

return encoder(args)
63 changes: 63 additions & 0 deletions eth_abi/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,66 @@ def get_decoder(self, type_str):
encoding.TupleEncoder, decoding.TupleDecoder,
label='is_base_tuple',
)

registry_packed = ABIRegistry()

registry_packed.register_encoder(
BaseEquals('uint'),
encoding.PackedUnsignedIntegerEncoder,
label='uint',
)
registry_packed.register_encoder(
BaseEquals('int'),
encoding.PackedSignedIntegerEncoder,
label='int',
)
registry_packed.register_encoder(
BaseEquals('address'),
encoding.PackedAddressEncoder,
label='address',
)
registry_packed.register_encoder(
BaseEquals('bool'),
encoding.PackedBooleanEncoder,
label='bool',
)
registry_packed.register_encoder(
BaseEquals('ufixed'),
encoding.PackedUnsignedFixedEncoder,
label='ufixed',
)
registry_packed.register_encoder(
BaseEquals('fixed'),
encoding.PackedSignedFixedEncoder,
label='fixed',
)
registry_packed.register_encoder(
BaseEquals('bytes', with_sub=True),
encoding.PackedBytesEncoder,
label='bytes<M>',
)
registry_packed.register_encoder(
BaseEquals('bytes', with_sub=False),
encoding.PackedByteStringEncoder,
label='bytes',
)
registry_packed.register_encoder(
BaseEquals('function'),
encoding.PackedBytesEncoder,
label='function',
)
registry_packed.register_encoder(
BaseEquals('string'),
encoding.PackedTextStringEncoder,
label='string',
)
registry_packed.register_encoder(
has_arrlist,
encoding.PackedArrayEncoder,
label='has_arrlist',
)
registry_packed.register_encoder(
is_base_tuple,
encoding.TupleEncoder,
label='is_base_tuple',
)

0 comments on commit d67e89c

Please sign in to comment.