Skip to content

Commit

Permalink
feat: add stacktrace to SSZ (#1017)
Browse files Browse the repository at this point in the history
Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
  • Loading branch information
avilagaston9 and MegaRedHand authored Apr 24, 2024
1 parent 5a53ee2 commit 7a7b5a9
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 45 deletions.
35 changes: 20 additions & 15 deletions lib/ssz_ex/decode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ defmodule SszEx.Decode do
{:error,
%Error{
message:
"Invalid binary length while decoding uint.\nExpected size: #{size}.\nFound:#{bit_size(binary)}\n"
"Invalid binary length while decoding uint.\nExpected size: #{size}.\nFound:#{bit_size(binary)}"
}}

defp decode_uint(binary, size) do
Expand All @@ -68,7 +68,7 @@ defmodule SszEx.Decode do
{:error,
%Error{
message:
"Invalid binary length while decoding bool.\nExpected size: 1.\nFound:#{byte_size(binary)}\n"
"Invalid binary length while decoding bool.\nExpected size: 1.\nFound:#{byte_size(binary)}"
}}

defp decode_bool("\x01"), do: {:ok, true}
Expand All @@ -86,7 +86,7 @@ defmodule SszEx.Decode do
do:
{:error,
%Error{
message: "Invalid binary value while decoding BitList.\nEmpty binary found.\n"
message: "Invalid binary value while decoding BitList.\nEmpty binary found."
}}

defp decode_bitlist(bit_list, max_size) do
Expand All @@ -98,14 +98,14 @@ defmodule SszEx.Decode do
match?(<<_::binary-size(num_bytes - 1), 0>>, bit_list) ->
{:error,
%Error{
message: "Invalid binary value while decoding BitList.\nMissing sentinel bit.\n"
message: "Invalid binary value while decoding BitList.\nMissing sentinel bit."
}}

len > max_size ->
{:error,
%Error{
message:
"Invalid binary length while decoding BitList. \nExpected max_size: #{max_size}. Found: #{len}.\n"
"Invalid binary length while decoding BitList. \nExpected max_size: #{max_size}. Found: #{len}."
}}

true ->
Expand All @@ -125,7 +125,7 @@ defmodule SszEx.Decode do
_ ->
{:error,
%Error{
message: "Invalid binary length while decoding BitVector. \nExpected size: #{size}.\n"
message: "Invalid binary length while decoding BitVector. \nExpected size: #{size}."
}}
end
end
Expand Down Expand Up @@ -162,7 +162,7 @@ defmodule SszEx.Decode do
{:error,
%Error{
message:
"Invalid binary length while decoding list of #{inspect(inner_type)}.\nExpected max_size: #{max_size}.\nFound: #{byte_length}\n"
"Invalid binary length while decoding list of #{inspect(inner_type)}.\nExpected max_size: #{max_size}.\nFound: #{byte_length}"
}}

defp check_valid_fixed_list_size(_byte_length, _inner_type, _inner_type_size, _max_size),
Expand All @@ -174,7 +174,7 @@ defmodule SszEx.Decode do
{:error,
%Error{
message:
"Invalid binary length while decoding vector of #{inspect(inner_type)}.\nExpected size #{inner_type_size * size} bytes.\nFound: #{byte_length}.\n"
"Invalid binary length while decoding vector of #{inspect(inner_type)}.\nExpected size #{inner_type_size * size} bytes.\nFound: #{byte_length}."
}}

defp check_valid_vector_size(_byte_length, _inner_type, _inner_type_size, _size),
Expand Down Expand Up @@ -310,7 +310,7 @@ defmodule SszEx.Decode do
{:error,
%Error{
message:
"Invalid binary length while decoding #{module}. \nExpected #{expected_length}. \nFound #{size}.\n"
"Invalid binary length while decoding #{module}. \nExpected #{expected_length}. \nFound #{size}."
}}

defp check_fixed_container_size(_module, _expected_length, _size),
Expand All @@ -322,7 +322,7 @@ defmodule SszEx.Decode do
{:error,
%Error{
message:
"First offset does not point to the first variable byte.\nExpected index: #{items_index}.\nOffset: #{offset}. "
"First offset does not point to the first variable byte.\nExpected index: #{items_index}.\nOffset: #{offset}."
}}

defp check_first_offset(_offsets, _items_index, _binary_size),
Expand All @@ -337,14 +337,17 @@ defmodule SszEx.Decode do
:ok ->
size = next_offset - offset
<<chunk::binary-size(size), rest::bitstring>> = rest_bytes
{:cont, {rest, [{key, decode(chunk, schema)} | acc_variable_parts]}}

{:cont,
{rest, [{key, decode(chunk, schema) |> Error.add_trace(key)} | acc_variable_parts]}}

error ->
{:halt, {<<>>, [{key, error} | acc_variable_parts]}}
end

[{_offset, {key, schema}}], {rest_bytes, acc_variable_parts} ->
{:cont, {<<>>, [{key, decode(rest_bytes, schema)} | acc_variable_parts]}}
{:cont,
{<<>>, [{key, decode(rest_bytes, schema) |> Error.add_trace(key)} | acc_variable_parts]}}
end)
|> then(fn {<<>>, variable_parts} ->
flatten_container_results(variable_parts)
Expand All @@ -363,7 +366,9 @@ defmodule SszEx.Decode do
else
ssz_fixed_len = Utils.get_fixed_size(schema)
<<chunk::binary-size(ssz_fixed_len), rest::bitstring>> = binary
{rest, [{key, decode(chunk, schema)} | fixed_parts], offsets, items_index + ssz_fixed_len}

{rest, [{key, decode(chunk, schema) |> Error.add_trace(key)} | fixed_parts], offsets,
items_index + ssz_fixed_len}
end
end)
|> then(fn {_rest_bytes, fixed_parts, offsets, items_index} ->
Expand Down Expand Up @@ -441,7 +446,7 @@ defmodule SszEx.Decode do
{:error,
%Error{
message:
"Invalid binary length while decoding collection. \nInner type size: #{chunk_size} bytes. Binary length: #{byte_size(binary)} bytes.\n"
"Invalid binary length while decoding collection. \nInner type size: #{chunk_size} bytes. Binary length: #{byte_size(binary)} bytes."
}}
| results
]
Expand All @@ -455,7 +460,7 @@ defmodule SszEx.Decode do
case Enum.group_by(results, fn {_, {type, _}} -> type end, fn {key, {_, result}} ->
{key, result}
end) do
%{error: errors} -> {:error, errors}
%{error: [first_error | _rest]} -> {:error, first_error}
summary -> {:ok, Map.get(summary, :ok, [])}
end
end
Expand Down
19 changes: 10 additions & 9 deletions lib/ssz_ex/encode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ defmodule SszEx.Encode do
{:error,
%Error{
message:
"Invalid binary length while encoding list of #{inspect(inner_type)}.\nExpected max_size: #{max_size}.\nFound: #{size}\n"
"Invalid binary length while encoding list of #{inspect(inner_type)}.\nExpected max_size: #{max_size}.\nFound: #{size}"
}}
else
list
Expand All @@ -71,7 +71,7 @@ defmodule SszEx.Encode do
{:error,
%Error{
message:
"Invalid binary length while encoding BitList.\nExpected max_size: #{max_size}. Found: #{len}.\n"
"Invalid binary length while encoding BitList.\nExpected max_size: #{max_size}. Found: #{len}."
}}
else
{:ok, BitList.to_bytes(bit_list)}
Expand All @@ -96,7 +96,7 @@ defmodule SszEx.Encode do
{:error,
%Error{
message:
"Invalid binary length while encoding list of #{inspect(inner_type)}.\nExpected max_size: #{max_size}.\nFound: #{size}\n"
"Invalid binary length while encoding list of #{inspect(inner_type)}.\nExpected max_size: #{max_size}.\nFound: #{size}"
}}
else
fixed_lengths = @bytes_per_length_offset * length(list)
Expand Down Expand Up @@ -147,8 +147,7 @@ defmodule SszEx.Encode do
Enum.reduce(variable_parts, 0, fn part, acc -> byte_size(part) + acc end),
:ok <- check_length(fixed_length, variable_length),
{:ok, fixed_parts} <-
replace_offsets(fixed_size_values, offsets)
|> encode_schemas() do
replace_offsets(fixed_size_values, offsets) |> encode_schemas() do
(fixed_parts ++ variable_parts)
|> Enum.join()
|> then(&{:ok, &1})
Expand All @@ -163,23 +162,25 @@ defmodule SszEx.Encode do

if Utils.variable_size?(schema) do
{[:offset | acc_fixed_size_values], @bytes_per_length_offset + acc_fixed_length,
[{value, schema} | acc_variable_values]}
[{value, key, schema} | acc_variable_values]}
else
{[{value, schema} | acc_fixed_size_values],
{[{value, key, schema} | acc_fixed_size_values],
acc_fixed_length + Utils.get_fixed_size(schema), acc_variable_values}
end
end)
end

defp encode_schemas(tuple_values) do
Enum.map(tuple_values, fn {value, schema} -> encode(value, schema) end)
Enum.map(tuple_values, fn {value, key, schema} ->
encode(value, schema) |> Error.add_trace(key)
end)
|> Utils.flatten_results()
end

defp calculate_offsets(variable_parts, fixed_length) do
{offsets, _} =
Enum.reduce(variable_parts, {[], fixed_length}, fn element, {res, acc} ->
{[{acc, {:int, 32}} | res], byte_size(element) + acc}
{[{acc, :offset, {:int, 32}} | res], byte_size(element) + acc}
end)

offsets
Expand Down
47 changes: 43 additions & 4 deletions lib/ssz_ex/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,52 @@ defmodule SszEx.Error do
Error messages for SszEx domain.
"""
alias SszEx.Error
defstruct [:message]
@type t :: %__MODULE__{message: String.t()}
defstruct [:message, stacktrace: []]
@type t :: %__MODULE__{message: String.t(), stacktrace: list()}

def format(%Error{message: message}) do
"#{message}"
def format(%Error{message: message, stacktrace: []}), do: "#{message}\n"

def format(%Error{message: message, stacktrace: stacktrace}) do
formatted_stacktrace = stacktrace |> Enum.join(".")
"#{message}\nStacktrace: #{formatted_stacktrace}"
end

def add_container(%Error{message: message, stacktrace: stacktrace}, value)
when is_struct(value) do
new_trace =
value.__struct__ |> Module.split() |> List.last()

%Error{message: message, stacktrace: [new_trace | stacktrace]}
end

def add_container(%Error{} = error, :bool), do: error

def add_container(%Error{message: message, stacktrace: stacktrace}, value)
when is_atom(value) do
new_trace =
value |> Module.split() |> List.last()

%Error{message: message, stacktrace: [new_trace | stacktrace]}
end

def add_container(%Error{message: message, stacktrace: stacktrace}, new_trace) do
%Error{message: message, stacktrace: [new_trace | stacktrace]}
end

def add_container({:error, %Error{} = error}, new_trace),
do: {:error, Error.add_container(error, new_trace)}

def add_container(value, _module), do: value

def add_trace(%Error{message: message, stacktrace: stacktrace}, new_trace) do
%Error{message: message, stacktrace: [new_trace | stacktrace]}
end

def add_trace({:error, %Error{} = error}, new_trace),
do: {:error, Error.add_trace(error, new_trace)}

def add_trace(value, _module), do: value

defimpl String.Chars, for: __MODULE__ do
def to_string(error), do: Error.format(error)
end
Expand Down
14 changes: 11 additions & 3 deletions lib/ssz_ex/merkleization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ defmodule SszEx.Merkleization do

@spec hash_tree_root(binary, {:byte_vector, non_neg_integer}) ::
{:ok, Types.root()}
def hash_tree_root(value, {:byte_vector, size}) when byte_size(value) != size,
do:
{:error,
%Error{
message:
"Invalid binary length while merkleizing byte_vector.\nExpected size: #{size}.\nFound: #{byte_size(value)}"
}}

def hash_tree_root(value, {:byte_vector, _size}) do
packed_chunks = pack_bytes(value)
leaf_count = packed_chunks |> get_chunks_len() |> next_pow_of_two()
Expand Down Expand Up @@ -73,7 +81,7 @@ defmodule SszEx.Merkleization do
{:error,
%Error{
message:
"Invalid binary length while merkleizing list of #{inspect(type)}.\nExpected max_size: #{max_size}.\nFound: #{len}\n"
"Invalid binary length while merkleizing list of #{inspect(type)}.\nExpected max_size: #{max_size}.\nFound: #{len}"
}}

Utils.basic_type?(type) ->
Expand All @@ -91,7 +99,7 @@ defmodule SszEx.Merkleization do
{:error,
%Error{
message:
"Invalid binary length while merkleizing vector of #{inspect(inner_type)}.\nExpected size: #{size}.\nFound: #{length(vector)}\n"
"Invalid binary length while merkleizing vector of #{inspect(inner_type)}.\nExpected size: #{size}.\nFound: #{length(vector)}"
}}

def hash_tree_root(vector, {:vector, type, _size} = schema) do
Expand All @@ -118,7 +126,7 @@ defmodule SszEx.Merkleization do

case hash_tree_root(value, schema) do
{:ok, root} -> {:cont, {:ok, acc_root <> root}}
{:error, %Error{}} = error -> {:halt, error}
{:error, %Error{} = error} -> {:halt, {:error, Error.add_trace(error, key)}}
end
end)

Expand Down
11 changes: 10 additions & 1 deletion lib/ssz_ex/ssz_ex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,17 @@ defmodule SszEx do

@spec encode(struct()) ::
{:ok, binary()} | {:error, Error.t()}
def encode(%name{} = value), do: encode(value, name)
def encode(%name{} = value), do: encode(value, name) |> Error.add_container(value)

@spec encode(any(), schema()) ::
{:ok, binary()} | {:error, Error.t()}
def encode(value, schema), do: Encode.encode(value, schema)

@spec decode(binary(), schema()) ::
{:ok, any()} | {:error, Error.t()}
def decode(value, module) when is_atom(module),
do: Decode.decode(value, module) |> Error.add_container(module)

@spec decode(binary(), schema()) ::
{:ok, any()} | {:error, Error.t()}
def decode(value, schema), do: Decode.decode(value, schema)
Expand All @@ -78,6 +83,10 @@ defmodule SszEx do
@spec hash_tree_root!(any, any) :: Types.root()
def hash_tree_root!(value, schema), do: Merkleization.hash_tree_root!(value, schema)

@spec hash_tree_root(struct()) :: {:ok, Types.root()} | {:error, Error.t()}
def hash_tree_root(%name{} = value),
do: hash_tree_root(value, name) |> Error.add_container(value)

@spec hash_tree_root(any, any) :: {:ok, Types.root()} | {:error, Error.t()}
def hash_tree_root(value, schema), do: Merkleization.hash_tree_root(value, schema)

Expand Down
2 changes: 1 addition & 1 deletion lib/ssz_ex/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ defmodule SszEx.Utils do

def flatten_results_by(results, fun) do
case Enum.group_by(results, fn {type, _} -> type end, fn {_, result} -> result end) do
%{error: errors} -> {:error, errors}
%{error: [first_error | _rest]} -> {:error, first_error}
summary -> {:ok, fun.(Map.get(summary, :ok, []))}
end
end
Expand Down
Loading

0 comments on commit 7a7b5a9

Please sign in to comment.