From 2561f66a0102743f12b628ed02d3ee2bfb9dc01c Mon Sep 17 00:00:00 2001 From: Alisina Bahadori Date: Fri, 27 Oct 2023 11:39:23 -0400 Subject: [PATCH] Implement packed encoding Closes #139 --- lib/abi.ex | 28 +++++- lib/abi/type_encoder.ex | 175 +++++++++++++++++++++------------ test/abi/type_encoder_test.exs | 61 ++++++++++++ 3 files changed, 201 insertions(+), 63 deletions(-) diff --git a/lib/abi.ex b/lib/abi.ex index ed209ad..515c9fe 100644 --- a/lib/abi.ex +++ b/lib/abi.ex @@ -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 """ diff --git a/lib/abi/type_encoder.ex b/lib/abi/type_encoder.ex index b90a914..00f06d5 100644 --- a/lib/abi/type_encoder.ex +++ b/lib/abi/type_encoder.ex @@ -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) @@ -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]} @@ -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]} @@ -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() @@ -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) @@ -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 -> <> + :packed -> <> + end end - # encoding with integer-signed-256 we already get the right padding - defp encode_int(data), do: <> - 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 @@ -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)>> diff --git a/test/abi/type_encoder_test.exs b/test/abi/type_encoder_test.exs index a5bf652..3b660b3 100644 --- a/test/abi/type_encoder_test.exs +++ b/test/abi/type_encoder_test.exs @@ -408,6 +408,67 @@ defmodule ABI.TypeEncoderTest do assert ABI.TypeDecoder.decode(expected_result, selector) == params end + + test "encodes bytes without padding in packed mode" do + assert <<0, 1, 2>> == ABI.TypeEncoder.encode([<<0, 1, 2>>], [:bytes], :input, :packed) + assert "Hello" == ABI.TypeEncoder.encode(["Hello"], [:string], :input, :packed) + end + + test "encodes types smaller than 256 bits without padding in packed mode" do + for size <- 8..256//8 do + value = Enum.random(1..255) + + assert <> == + ABI.TypeEncoder.encode([value], [{:uint, size}], :input, :packed) + + value = Enum.random(-128..127) + + assert <> == + ABI.TypeEncoder.encode([value], [{:int, size}], :input, :packed) + end + end + + test "encodes bool as one byte in packed mode" do + assert <<1>> == ABI.TypeEncoder.encode([true], [:bool], :input, :packed) + assert <<0>> == ABI.TypeEncoder.encode([false], [:bool], :input, :packed) + end + + test "encodes arrays with padding in packed mode" do + assert ABI.TypeEncoder.encode([[1, 2, 3]], [{:array, {:uint, 8}}]) == + "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003" + |> Base.decode16!(case: :lower) + end + + test "encodes address in 20 bytes in packed mode" do + encoded = + ABI.TypeEncoder.encode( + [ + <<192, 42, 170, 57, 178, 35, 254, 141, 10, 14, 92, 79, 39, 234, 217, 8, 60, 117, 108, + 194>> + ], + [:address], + :input, + :packed + ) + + assert byte_size(encoded) == 20 + end + + test "raises with tuple in packed mode" do + data_to_encode = [{128, 64}] + + selector = %FunctionSelector{ + function: "baz", + types: [ + {:tuple, [{:int, 8}, {:int, 8}]} + ], + returns: :bool + } + + assert_raise RuntimeError, fn -> + ABI.TypeEncoder.encode(data_to_encode, selector, :input, :packed) + end + end end test "example 1 from web3-eth-abi js" do