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
13 changes: 9 additions & 4 deletions lib/ssz_ex/decode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -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
15 changes: 8 additions & 7 deletions lib/ssz_ex/encode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ defmodule SszEx.Encode do
{:error,
%Error{
message:
"Invalid binary length while encoding BitVector. \nExpected: #{size}.\nFound: #{bit_vector_size(bit_vector)}."
"Invalid binary length while encoding BitVector. \nExpected: #{size}.\nFound: #{bit_vector_size(bit_vector)}.\n"
}}

defp encode_bitvector(bit_vector, _size),
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
23 changes: 20 additions & 3 deletions lib/ssz_ex/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,30 @@ 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
def format(%Error{message: message, stacktrace: nil}), do: "#{message}"

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

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

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}, module),
do: {:error, Error.add_trace(error, module)}

def add_trace(value, _module), do: value

defimpl String.Chars, for: __MODULE__ do
def to_string(error), do: Error.format(error)
end
Expand Down
10 changes: 9 additions & 1 deletion 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)}\n"
}}

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 @@ -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_trace("#{name}")

@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_trace("#{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_trace("#{name}")

@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
157 changes: 145 additions & 12 deletions test/unit/ssz_ex_test.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Unit.SSZExTest do
alias LambdaEthereumConsensus.Utils.BitVector
alias LambdaEthereumConsensus.Utils.Diff
alias SszEx.Error
alias SszEx.Merkleization
Expand All @@ -17,6 +18,59 @@ defmodule Unit.SSZExTest do
assert {:ok, deserialized} === SszEx.decode(serialized, schema)
end

def build_broken_attester_slashing() do
checkpoint_source = %Types.Checkpoint{
epoch: 3_776_037_760_046_644_755,
root:
<<29, 22, 191, 147, 188, 238, 162, 89, 147, 162, 202, 111, 169, 162, 84, 95, 194, 85, 54,
172, 44, 74, 37, 128, 248, 21, 86, 246, 151, 54, 24, 54>>
}

checkpoint_target = %Types.Checkpoint{
epoch: 2_840_053_453_521_072_037,
root:
<<15, 174, 23, 120, 4, 9, 2, 116, 67, 73, 254, 53, 197, 3, 191, 166, 104, 34, 121, 2, 57,
69, 75, 69, 254, 237, 132, 68, 254, 49, 127, 175>>
}

attestation_data = %Types.AttestationData{
slot: 5_057_010_135_270_197_978,
index: 6_920_931_864_607_509_210,
beacon_block_root:
<<31, 38, 101, 174, 248, 168, 116, 226, 15, 39, 218, 148, 42, 8, 80, 80, 241, 149, 162,
32, 176, 208, 120, 120, 89, 123, 136, 115, 154, 28, 21, 174>>,
source: checkpoint_source,
target: checkpoint_target
}

indexed_attestation_1 = %Types.IndexedAttestation{
attesting_indices: [15_833_676_831_095_072_535, 7_978_643_446_947_046_229],
data: attestation_data,
signature:
<<46, 244, 83, 164, 182, 222, 218, 247, 8, 186, 138, 100, 5, 96, 34, 117, 134, 123, 219,
188, 181, 11, 209, 57, 207, 24, 249, 42, 74, 27, 228, 97, 73, 46, 219, 202, 122, 149,
135, 30, 91, 126, 180, 69, 129, 170, 147, 142, 242, 27, 233, 63, 242, 7, 144, 8, 192,
165, 194, 220, 77, 247, 128, 107, 41, 199, 166, 59, 34, 160, 222, 114, 250, 250, 3, 130,
145, 8, 45, 65, 13, 82, 44, 80, 30, 181, 239, 54, 152, 237, 244, 72, 231, 179, 239, 22>>
}

broken_attestation = %Types.IndexedAttestation{
attesting_indices: List.duplicate(0, 3000),
data: attestation_data,
signature:
<<46, 244, 83, 164, 182, 222, 218, 247, 8, 186, 138, 100, 5, 96, 34, 117, 134, 123, 219,
188, 181, 11, 209, 57, 207, 24, 249, 42, 74, 27, 228, 97, 73, 46, 219, 202, 122, 149,
135, 30, 91, 126, 180, 69, 129, 170, 147, 142, 242, 27, 233, 63, 242, 7, 144, 8, 192,
165, 194, 220, 77, 247, 128, 107, 41, 199, 166, 59, 34, 160, 222, 114, 250, 250, 3, 130,
145, 8, 45, 65, 13, 82, 44, 80, 30, 181, 239, 54, 152, 237, 244, 72, 231, 179, 239, 22>>
}

%Types.AttesterSlashing{
attestation_1: indexed_attestation_1,
attestation_2: broken_attestation
}
end

test "packing a list of uints" do
list_1 = [1, 2, 3, 4, 5]

Expand Down Expand Up @@ -737,24 +791,20 @@ defmodule Unit.SSZExTest do
encoded_checkpoint =
<<0, 0, 0>>

assert SszEx.decode(encoded_checkpoint, Checkpoint) ==
{:error,
%Error{
message:
"Invalid binary length while decoding Elixir.Types.Checkpoint. \nExpected 40. \nFound 3.\n"
}}
{:error, error} = SszEx.decode(encoded_checkpoint, Checkpoint)

assert "#{error}" ==
"Invalid binary length while decoding Elixir.Types.Checkpoint. \nExpected 40. \nFound 3.\nStacktrace: Elixir.Types.Checkpoint"
end

test "decode longer checkpoint" do
encoded_checkpoint =
<<0::size(41 * 8)>>

assert SszEx.decode(encoded_checkpoint, Checkpoint) ==
{:error,
%Error{
message:
"Invalid binary length while decoding Elixir.Types.Checkpoint. \nExpected 40. \nFound 41.\n"
}}
{:error, error} = SszEx.decode(encoded_checkpoint, Checkpoint)

assert "#{error}" ==
"Invalid binary length while decoding Elixir.Types.Checkpoint. \nExpected 40. \nFound 41.\nStacktrace: Elixir.Types.Checkpoint"
end

test "hash tree root of list exceeding max size" do
Expand All @@ -766,4 +816,87 @@ defmodule Unit.SSZExTest do
{:error, result} = SszEx.hash_tree_root(initial_list, {:list, {:int, 8}, 2})
assert error == "#{result}"
end

test "hash_tree_root with invalid logs_bloom" do
execution_payload = %ExecutionPayload{
parent_hash: @default_hash,
fee_recipient: <<0::size(20 * 8)>>,
state_root: @default_root,
receipts_root: @default_root,
logs_bloom: <<0, 0, 0>>,
prev_randao: <<0::size(32 * 8)>>,
block_number: 0,
gas_limit: 0,
gas_used: 0,
timestamp: 0,
extra_data: <<>>,
base_fee_per_gas: 0,
block_hash: @default_hash,
transactions: [],
withdrawals: [],
blob_gas_used: 0,
excess_blob_gas: 0
}

{:error, error} = SszEx.hash_tree_root(execution_payload, ExecutionPayload)

assert "#{error}" ==
"Invalid binary length while merkleizing byte_vector.\nExpected size: 256.\nFound: 3\nStacktrace: logs_bloom"
end

test "stacktrace in hash_tree_root with invalid logs_bloom" do
execution_payload = %ExecutionPayload{
parent_hash: @default_hash,
fee_recipient: <<0::size(20 * 8)>>,
state_root: @default_root,
receipts_root: @default_root,
logs_bloom: <<0, 0, 0>>,
prev_randao: <<0::size(32 * 8)>>,
block_number: 0,
gas_limit: 0,
gas_used: 0,
timestamp: 0,
extra_data: <<>>,
base_fee_per_gas: 0,
block_hash: @default_hash,
transactions: [],
withdrawals: [],
blob_gas_used: 0,
excess_blob_gas: 0
}

{:error, error} = SszEx.hash_tree_root(execution_payload)

assert "#{error}" ==
"Invalid binary length while merkleizing byte_vector.\nExpected size: 256.\nFound: 3\nStacktrace: Elixir.Types.ExecutionPayload.logs_bloom"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we normalize types so that it says ExecutionPayload.logs_bloom instead of Elixir.Types.ExecutionPayload.logs_bloom?

end

test "stacktrace in encode with invalid sync_committee_bits" do
sync_aggregate = %SyncAggregate{
sync_committee_bits: BitVector.new(2),
sync_committee_signature: <<0::size(32 * 8)>>
}

{:error, error} = SszEx.encode(sync_aggregate)

assert "#{error}" ==
"Invalid binary length while encoding BitVector. \nExpected: 512.\nFound: 2.\nStacktrace: Elixir.Types.SyncAggregate.sync_committee_bits"
end

test "stacktrace encode nested container" do
attester_slashing = build_broken_attester_slashing()

{:error, error} = SszEx.encode(attester_slashing)

assert "#{error}" ==
"Invalid binary length while encoding list of {:int, 64}.\nExpected max_size: 2048.\nFound: 3000\nStacktrace: Elixir.Types.AttesterSlashing.attestation_2.attesting_indices"
end

test "stacktrace hash_tree_root nested container" do
attester_slashing = build_broken_attester_slashing()
{:error, error} = SszEx.hash_tree_root(attester_slashing)

assert "Invalid binary length while merkleizing list of {:int, 64}.\nExpected max_size: 2048.\nFound: 3000\nStacktrace: Elixir.Types.AttesterSlashing.attestation_2.attesting_indices" =
"#{error}"
end
end
Loading