Skip to content

Commit

Permalink
Implement packed encoding
Browse files Browse the repository at this point in the history
Closes #139
  • Loading branch information
alisinabh committed Oct 27, 2023
1 parent 3d5dc7e commit 2561f66
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 63 deletions.
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
175 changes: 113 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,106 +67,134 @@ 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
# TODO: Should consider packed?
data_bytes_size = byte_size(result)

{[{:dynamic, data_bytes_size} | static_acc], [result | dynamic_acc]}
Expand All @@ -169,22 +203,27 @@ 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(type = {:tuple, _types}, tuple_parameters, static_acc, dynamic_acc)
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, mode)
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, mode)
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, mode)
when is_list(list_parameters) do
result = do_encode(list_parameters, types)
result = do_encode(list_parameters, types, mode)

if FunctionSelector.is_dynamic?(type) do
# TODO: Should consider packed?
data_bytes_size = byte_size(result)

{[{:dynamic, data_bytes_size} | static_acc], [result | dynamic_acc]}
Expand All @@ -193,8 +232,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 +255,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 +265,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 +291,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

0 comments on commit 2561f66

Please sign in to comment.