ethereum / eth-abi

Ethereum ABI utilities for python
MIT License
247 stars 269 forks source link

Is there a way to match calldata/memory encoding conventions? #185

Open UsmannK opened 2 years ago

UsmannK commented 2 years ago

In solidity calldata has special conventions for encoding. See: https://docs.soliditylang.org/en/v0.8.14/types.html#arrays

Variables of type bytes and string are special arrays. The bytes type is similar to bytes1[], but it is packed tightly in calldata and memory. string is equal to bytes but does not allow length or index access.

This results in a scheme where some things are packed tightly and others aren't.

Is there a way to use this library to easily match the semantics of abi.encodeWithSignature?

Specifically I am looking for a way to do the following:

>>> import eth_abi
>>> eth_abi.__version__
'3.0.1'
>>> eth_abi.encode(['uint256', 'bytes'], [0xDEADBEEF, b'']).hex()
'00000000000000000000000000000000000000000000000000000000deadbeef000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'

Here the empty bytes are getting encoded as 0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

but I'd like it to be 0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000

essentially, there should be 32 fewer bytes

icodezjb commented 1 year ago

I had the same problem and found this info: Don't add an empty data slot to ABI-encoded empty strings from storage

And I re-registered the bytes type encoder:

import eth_abi
from eth_abi.encoding import encode_uint_256
from eth_abi.registry import BaseEquals, encoding
from eth_abi.utils.padding import zpad_right

class NewByteStringEncoder(encoding.ByteStringEncoder):
    is_dynamic = True

    @classmethod
    def encode(cls, value):
        cls.validate_value(value)
        value_length = len(value)

        encoded_size = encode_uint_256(value_length)

        if value_length == 0:
            return encoded_size

        ceil32 = value_length if value_length % 32 == 0 else value_length + 32 - (value_length % 32)
        padded_value = zpad_right(value, ceil32)

        return encoded_size + padded_value

eth_abi.abi.registry.unregister_encoder(
    BaseEquals("bytes", with_sub=False),
)

eth_abi.abi.registry.register_encoder(
    BaseEquals("bytes", with_sub=False),
    NewByteStringEncoder
)