Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Packed encoding #154

Merged
merged 2 commits into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion lib/abi.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,33 @@ defmodule ABI do
end

def encode(%FunctionSelector{} = function_selector, data, data_type) do
TypeEncoder.encode(data, function_selector, data_type)
TypeEncoder.encode(data, function_selector, data_type, :standard)
end

@doc """
Encodes the given data into the given types in packed encoding mode.

Note that packed encoding mode is ambiguous and cannot be decoded (there are no decode_packed functions).
Also, tuples (structs) and nester arrays are not supported.

More info https://docs.soliditylang.org/en/latest/abi-spec.html#non-standard-packed-mode

## Examples

iex> ABI.encode_packed([{:uint, 16}], [0x12])
...> |> Base.encode16(case: :lower)
"0012"

iex> ABI.encode_packed([:string, {:uint, 16}], ["Elixir ABI", 0x12])
...> |> Base.encode16(case: :lower)
"456c69786972204142490012"

iex> ABI.encode_packed([{:int, 16}, {:bytes, 1}, {:uint, 16}, :string], [-1, <<0x42>>, 0x03, "Hello, world!"])
...> |> Base.encode16(case: :lower)
"ffff42000348656c6c6f2c20776f726c6421"
"""
def encode_packed(types, data) when is_list(types) do
TypeEncoder.encode(data, types, :input, :packed)
end

@doc """
Expand Down
179 changes: 117 additions & 62 deletions lib/abi/type_encoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,39 @@ defmodule ABI.TypeEncoder do

@doc """
Encodes the given data based on the function selector.

## Parameters
- data: The data to encode
- selector_or_types: Either a FunctionSelector struct or a list of types to encode the data with
- data_type: Determines which types to use from a FunctionSelector struct. Can be `:input` or `:output`.
- mode: Encoding mode. Can be `:standard` or `:packed`.
"""

def encode(data, selector_or_types, data_type \\ :input)
def encode(data, selector_or_types, data_type \\ :input, mode \\ :standard)

def encode(data, %FunctionSelector{function: nil, types: types}, :input) do
do_encode(data, types)
def encode(data, %FunctionSelector{function: nil, types: types}, :input, mode) do
do_encode(data, types, mode)
end

def encode(data, %FunctionSelector{types: types} = function_selector, :input) do
encode_method_id(function_selector) <> do_encode(data, types)
def encode(data, %FunctionSelector{types: types} = function_selector, :input, mode) do
encode_method_id(function_selector) <> do_encode(data, types, mode)
end

def encode(data, %FunctionSelector{returns: types}, :output) do
do_encode(data, types)
def encode(data, %FunctionSelector{returns: types}, :output, mode) do
do_encode(data, types, mode)
end

def encode(data, types, _) when is_list(types) do
do_encode(data, types)
def encode(data, types, _, mode) when is_list(types) do
do_encode(data, types, mode)
end

def encode_raw(data, types) when is_list(types) do
do_encode(data, types)
def encode_raw(data, types, mode) when is_list(types) do
do_encode(data, types, mode)
end

defp do_encode(params, types, static_acc \\ [], dynamic_acc \\ [])
defp do_encode(params, types, static_acc \\ [], dynamic_acc \\ [], mode)

defp do_encode([], [], reversed_static_acc, reversed_dynamic_acc) do
defp do_encode([], [], reversed_static_acc, reversed_dynamic_acc, :standard) do
static_acc = Enum.reverse(reversed_static_acc)
dynamic_acc = Enum.reverse(reversed_dynamic_acc)

Expand All @@ -61,104 +67,131 @@ defmodule ABI.TypeEncoder do
{complete_static_part, _} =
Enum.reduce(dynamic_indexes, {static_acc, static_part_size}, fn {index, byte_size},
{acc, size_acc} ->
new_static_acc = List.replace_at(acc, index, encode_uint(size_acc, 256))
new_prefix_size = byte_size + size_acc
new_static_acc = List.replace_at(acc, index, encode_uint(size_acc, 256, :standard))

{new_static_acc, new_prefix_size}
{new_static_acc, byte_size + size_acc}
end)

Enum.join(complete_static_part ++ dynamic_acc)
end

defp do_encode([], [], static_acc, dynamic_acc, :packed) do
{values_acc, []} =
Enum.reduce(static_acc, {[], dynamic_acc}, fn
{:dynamic, _}, {values_acc, [value | dynamic_acc]} ->
{[value | values_acc], dynamic_acc}

value, {values_acc, dynamic_acc} ->
{[value | values_acc], dynamic_acc}
end)

Enum.join(values_acc)
end

defp do_encode(
[current_parameter | remaining_parameters],
[current_type | remaining_types],
static_acc,
dynamic_acc
dynamic_acc,
mode
) do
{new_static_acc, new_dynamic_acc} =
do_encode_type(current_type, current_parameter, static_acc, dynamic_acc)
do_encode_type(current_type, current_parameter, static_acc, dynamic_acc, mode)

do_encode(remaining_parameters, remaining_types, new_static_acc, new_dynamic_acc)
do_encode(remaining_parameters, remaining_types, new_static_acc, new_dynamic_acc, mode)
end

defp do_encode_type(:bool, parameter, static_part, dynamic_part) do
defp do_encode_type(:bool, parameter, static_part, dynamic_part, mode) do
value =
case parameter do
true -> encode_uint(1, 8)
false -> encode_uint(0, 8)
true -> encode_uint(1, 8, mode)
false -> encode_uint(0, 8, mode)
_ -> raise "Invalid data for bool: #{inspect(parameter)}"
end

{[value | static_part], dynamic_part}
end

defp do_encode_type({:uint, size}, parameter, static_part, dynamic_part) do
value = encode_uint(parameter, size)
defp do_encode_type({:uint, size}, parameter, static_part, dynamic_part, mode) do
value = encode_uint(parameter, size, mode)

{[value | static_part], dynamic_part}
end

defp do_encode_type({:int, size}, parameter, static_part, dynamic_part) do
value = encode_int(parameter, size)
defp do_encode_type({:int, size}, parameter, static_part, dynamic_part, mode) do
value = encode_int(parameter, size, mode)

{[value | static_part], dynamic_part}
end

defp do_encode_type(:string, parameter, static_part, dynamic_part) do
do_encode_type(:bytes, parameter, static_part, dynamic_part)
defp do_encode_type(:string, parameter, static_part, dynamic_part, mode) do
do_encode_type(:bytes, parameter, static_part, dynamic_part, mode)
end

defp do_encode_type(:bytes, parameter, static_part, dynamic_part) do
defp do_encode_type(:bytes, parameter, static_part, dynamic_part, mode) do
binary_param = maybe_encode_unsigned(parameter)
value = encode_uint(byte_size(binary_param), 256) <> encode_bytes(binary_param)

value =
case mode do
:standard ->
encode_uint(byte_size(binary_param), 256, mode) <> encode_bytes(binary_param, mode)

:packed ->
encode_bytes(binary_param, mode)
end

dynamic_part_byte_size = byte_size(value)

{[{:dynamic, dynamic_part_byte_size} | static_part], [value | dynamic_part]}
end

defp do_encode_type({:bytes, size}, parameter, static_part, dynamic_part)
defp do_encode_type({:bytes, size}, parameter, static_part, dynamic_part, mode)
when is_binary(parameter) and byte_size(parameter) <= size do
value = encode_bytes(parameter)
value = encode_bytes(parameter, mode)

{[value | static_part], dynamic_part}
end

defp do_encode_type({:bytes, size}, data, _, _) when is_binary(data) do
defp do_encode_type({:bytes, size}, data, _, _, _) when is_binary(data) do
raise "size mismatch for bytes#{size}: #{inspect(data)}"
end

defp do_encode_type({:bytes, size}, data, static_part, dynamic_part) when is_integer(data) do
defp do_encode_type({:bytes, size}, data, static_part, dynamic_part, mode)
when is_integer(data) do
binary_param = maybe_encode_unsigned(data)

do_encode_type({:bytes, size}, binary_param, static_part, dynamic_part)
do_encode_type({:bytes, size}, binary_param, static_part, dynamic_part, mode)
end

defp do_encode_type({:bytes, size}, data, _, _) do
defp do_encode_type({:bytes, size}, data, _, _, _) do
raise "wrong datatype for bytes#{size}: #{inspect(data)}"
end

defp do_encode_type({:array, type}, data, static_acc, dynamic_acc) do
defp do_encode_type({:array, type}, data, static_acc, dynamic_acc, mode) do
param_count = Enum.count(data)

encoded_size = encode_uint(param_count, 256)

types = List.duplicate(type, param_count)

result = do_encode(data, types)
result = do_encode(data, types, mode)

dynamic_acc_with_size = [encoded_size | dynamic_acc]
{dynamic_acc_with_size, data_bytes_size} =
case mode do
:standard ->
encoded_size = encode_uint(param_count, 256, mode)
# length is included and also length size is added
{[encoded_size | dynamic_acc], byte_size(result) + 32}

# number of elements count + data size
data_bytes_size = byte_size(result) + 32
:packed ->
# ignoring length of array
{dynamic_acc, byte_size(result)}
end

{[{:dynamic, data_bytes_size} | static_acc], [result | dynamic_acc_with_size]}
end

defp do_encode_type({:array, type, size}, data, static_acc, dynamic_acc) do
defp do_encode_type({:array, type, size}, data, static_acc, dynamic_acc, mode) do
types = List.duplicate(type, size)
result = do_encode(data, types)
result = do_encode(data, types, mode)

if FunctionSelector.is_dynamic?(type) do
data_bytes_size = byte_size(result)
Expand All @@ -169,20 +202,30 @@ defmodule ABI.TypeEncoder do
end
end

defp do_encode_type(:address, data, static_acc, dynamic_acc) do
do_encode_type({:uint, 160}, data, static_acc, dynamic_acc)
defp do_encode_type(:address, data, static_acc, dynamic_acc, mode) do
do_encode_type({:uint, 160}, data, static_acc, dynamic_acc, mode)
end

defp do_encode_type({:tuple, _types}, _, _, _, :packed) do
raise RuntimeError, "Structs (tuples) are not supported in packed mode encoding"
end

defp do_encode_type(type = {:tuple, _types}, tuple_parameters, static_acc, dynamic_acc)
defp do_encode_type(
type = {:tuple, _types},
tuple_parameters,
static_acc,
dynamic_acc,
:standard
)
when is_tuple(tuple_parameters) do
list_parameters = Tuple.to_list(tuple_parameters)

do_encode_type(type, list_parameters, static_acc, dynamic_acc)
do_encode_type(type, list_parameters, static_acc, dynamic_acc, :standard)
end

defp do_encode_type(type = {:tuple, types}, list_parameters, static_acc, dynamic_acc)
defp do_encode_type(type = {:tuple, types}, list_parameters, static_acc, dynamic_acc, :standard)
when is_list(list_parameters) do
result = do_encode(list_parameters, types)
result = do_encode(list_parameters, types, :standard)

if FunctionSelector.is_dynamic?(type) do
data_bytes_size = byte_size(result)
Expand All @@ -193,8 +236,8 @@ defmodule ABI.TypeEncoder do
end
end

defp encode_bytes(bytes) do
pad(bytes, byte_size(bytes), :right)
defp encode_bytes(bytes, mode) do
pad(bytes, byte_size(bytes), :right, mode)
end

@spec encode_method_id(FunctionSelector.t()) :: binary()
Expand All @@ -216,7 +259,7 @@ defmodule ABI.TypeEncoder do

# Note, we'll accept a binary or an integer here, so long as the
# binary is not longer than our allowed data size
defp encode_uint(data, size_in_bits) when rem(size_in_bits, 8) == 0 do
defp encode_uint(data, size_in_bits, mode) when rem(size_in_bits, 8) == 0 do
size_in_bytes = (size_in_bits / 8) |> round
bin = maybe_encode_unsigned(data)

Expand All @@ -226,22 +269,22 @@ defmodule ABI.TypeEncoder do
"Data overflow encoding uint, data `#{data}` cannot fit in #{size_in_bytes * 8} bits"
)

bin |> pad(size_in_bytes, :left)
bin |> pad(size_in_bytes, :left, mode)
end

defp encode_int(data, size_in_bits) when rem(size_in_bits, 8) == 0 do
defp encode_int(data, size_in_bits, mode) when rem(size_in_bits, 8) == 0 do
if signed_overflow?(data, size_in_bits) do
raise("Data overflow encoding int, data `#{data}` cannot fit in #{size_in_bits} bits")
end

encode_int(data)
case mode do
:standard -> <<data::signed-256>>
:packed -> <<data::signed-size(size_in_bits)>>
end
end

# encoding with integer-signed-256 we already get the right padding
defp encode_int(data), do: <<data::signed-256>>

defp signed_overflow?(n, max_bits) do
n < :math.pow(2, max_bits - 1) * -1 + 1 || n > :math.pow(2, max_bits - 1) - 1
n < 2 ** (max_bits - 1) * -1 + 1 || n > 2 ** (max_bits - 1) - 1
end

def mod(x, n) do
Expand All @@ -252,7 +295,19 @@ defmodule ABI.TypeEncoder do
else: remainder
end

defp pad(bin, size_in_bytes, direction) do
defp pad(bin, size_in_bytes, _direction, :packed) when byte_size(bin) == size_in_bytes, do: bin

defp pad(bin, size_in_bytes, direction, :packed) when byte_size(bin) < size_in_bytes do
padding_size_bits = (size_in_bytes - byte_size(bin)) * 8
padding = <<0::size(padding_size_bits)>>

case direction do
:left -> padding <> bin
:right -> bin <> padding
end
end

defp pad(bin, size_in_bytes, direction, :standard) do
total_size = size_in_bytes + mod(32 - size_in_bytes, 32)
padding_size_bits = (total_size - byte_size(bin)) * 8
padding = <<0::size(padding_size_bits)>>
Expand Down
Loading
Loading