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

feat: add stacktrace to SSZ #1017

Merged
merged 12 commits into from
Apr 24, 2024
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
Loading