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: merklelization of containers #697

Merged
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
eac1b66
Fix spec-test runner command
Godspower-Eze Dec 12, 2023
4013dfe
Merge branch 'lambdaclass:main' into main
Godspower-Eze Dec 13, 2023
838c782
Merge branch 'main' of github.com:Godspower-Eze/lambda_ethereum_conse…
Godspower-Eze Dec 14, 2023
2ea4f10
Merge branch 'main' of github.com:Godspower-Eze/lambda_ethereum_conse…
Godspower-Eze Dec 14, 2023
8b29cf4
Merge branch 'main' of github.com:Godspower-Eze/lambda_ethereum_conse…
Godspower-Eze Jan 12, 2024
65e32dc
Merge branch 'main' of github.com:Godspower-Eze/lambda_ethereum_conse…
Godspower-Eze Jan 14, 2024
c0407e5
Merge branch 'main' of github.com:Godspower-Eze/lambda_ethereum_conse…
Godspower-Eze Jan 16, 2024
3a91893
feat: added boilerplate for merkleization benchmark
Godspower-Eze Jan 17, 2024
0073c8b
Merge branch 'main' into #issue597
Godspower-Eze Jan 19, 2024
74e73b4
update
Godspower-Eze Jan 19, 2024
b324445
update
Godspower-Eze Jan 22, 2024
579ef94
Merge branch 'main' into #issue597
Godspower-Eze Jan 22, 2024
3bae020
update
Godspower-Eze Jan 23, 2024
bed96e9
Merge branch '#issue597' of github.com:Godspower-Eze/lambda_ethereum_…
Godspower-Eze Jan 23, 2024
d6a390d
Merge branch 'main' into #issue597
Godspower-Eze Jan 23, 2024
143cca1
update
Godspower-Eze Jan 24, 2024
bad2c97
Merge branch 'main' into #issue597
Godspower-Eze Jan 24, 2024
77013de
Merge branch '#issue597' of github.com:Godspower-Eze/lambda_ethereum_…
Godspower-Eze Jan 24, 2024
208e62c
update
Godspower-Eze Jan 24, 2024
755b740
update
Godspower-Eze Jan 24, 2024
fe46df3
update
Godspower-Eze Jan 25, 2024
4c91091
update
Godspower-Eze Jan 25, 2024
028c9f2
Merge branch 'main' into #issue597
Godspower-Eze Jan 25, 2024
1190247
feat: added merkleization of containers
Godspower-Eze Jan 28, 2024
c923af7
Merge branch 'main' into merklelization-of-containers
Godspower-Eze Jan 28, 2024
9ed34f6
update
Godspower-Eze Jan 28, 2024
92f061a
update
Godspower-Eze Jan 28, 2024
82ca523
Merge branch 'main' into #issue597
Godspower-Eze Jan 28, 2024
5a5f29d
Merge branch '#issue597' of github.com:Godspower-Eze/lambda_ethereum_…
Godspower-Eze Jan 28, 2024
c3e911c
Merge branch '#issue597' into merklelization-of-containers
Godspower-Eze Jan 28, 2024
45a7e7f
Merge branch 'merklelization-of-containers' of github.com:Godspower-E…
Godspower-Eze Jan 28, 2024
8ded18e
update
Godspower-Eze Jan 28, 2024
5173bb8
Merge branch '#issue597' into merklelization-of-containers
Godspower-Eze Jan 28, 2024
c104a48
update
Godspower-Eze Jan 28, 2024
938c4be
update
Godspower-Eze Jan 28, 2024
3adcf8e
Merge branch '#issue597' into merklelization-of-containers
Godspower-Eze Jan 28, 2024
1ee15b7
update
Godspower-Eze Jan 28, 2024
70219b9
update
Godspower-Eze Jan 29, 2024
eb1a6b2
update
Godspower-Eze Jan 29, 2024
dade9d5
Merge branch '#issue597' into merklelization-of-containers
Godspower-Eze Jan 29, 2024
932a413
Merge branch 'main' into merklelization-of-containers
Godspower-Eze Jan 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions bench/ssz.exs
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,26 @@ Benchee.run(
warmup: 2,
time: 5
)

## Benchmark Merkleization

list = Stream.cycle([65_535]) |> Enum.take(316)
Godspower-Eze marked this conversation as resolved.
Show resolved Hide resolved
schema = {:list, {:int, 16}, 1024}
packed_chunks = SszEx.pack(list, schema)
limit = SszEx.chunk_count(schema)

Benchee.run(
Godspower-Eze marked this conversation as resolved.
Show resolved Hide resolved
%{
"SszEx.merkleize_chunks" => fn {chunks, leaf_count} ->
SszEx.merkleize_chunks(chunks, leaf_count)
end,
"SszEx.merkleize_chunks_with_virtual_padding" => fn {chunks, leaf_count} ->
SszEx.merkleize_chunks_with_virtual_padding(chunks, leaf_count)
end
},
inputs: %{
"args" => {packed_chunks, limit}
},
warmup: 2,
time: 5
)
6 changes: 4 additions & 2 deletions lib/spec/runners/ssz_generic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ defmodule SszGenericTestRunner do
{:container, module},
real_serialized,
real_deserialized,
_hash_tree_root
expected_hash_tree_root
) do
real_struct = struct!(module, real_deserialized)
{:ok, deserialized} = SszEx.decode(real_serialized, module)
assert deserialized == real_struct
{:ok, serialized} = SszEx.encode(real_struct, module)
assert serialized == real_serialized
actual_hash_tree_root = SszEx.hash_tree_root!(real_struct, module)
assert actual_hash_tree_root == expected_hash_tree_root
end

defp assert_ssz(
Expand All @@ -92,7 +94,7 @@ defmodule SszGenericTestRunner do

assert serialized == real_serialized

{:ok, actual_hash_tree_root} = SszEx.hash_tree_root(real_deserialized, schema)
actual_hash_tree_root = SszEx.hash_tree_root!(real_deserialized, schema)

assert actual_hash_tree_root == expected_hash_tree_root
end
Expand Down
171 changes: 146 additions & 25 deletions lib/ssz_ex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule LambdaEthereumConsensus.SszEx do
alias LambdaEthereumConsensus.Utils.BitList
alias LambdaEthereumConsensus.Utils.BitVector
import alias LambdaEthereumConsensus.Utils.BitVector
alias LambdaEthereumConsensus.Utils.ZeroHashes

#################
### Public API
Expand Down Expand Up @@ -73,6 +74,41 @@ defmodule LambdaEthereumConsensus.SszEx do
@spec hash_tree_root!(non_neg_integer, {:int, non_neg_integer}) :: Types.root()
def hash_tree_root!(value, {:int, size}), do: pack(value, {:int, size})

@spec hash_tree_root!(binary, {:bytes, non_neg_integer}) :: Types.root()
def hash_tree_root!(value, {:bytes, size}) do
packed_chunks = pack(value, {:bytes, size})
leaf_count = packed_chunks |> get_chunks_len() |> next_pow_of_two()
root = merkleize_chunks_with_virtual_padding(packed_chunks, leaf_count)
root
end

@spec hash_tree_root!(list(), {:list, any, non_neg_integer}) :: Types.root()
def hash_tree_root!(list, {:list, type, size}) do
{:ok, root} = hash_tree_root(list, {:list, type, size})
root
end

@spec hash_tree_root!(list(), {:vector, any, non_neg_integer}) :: Types.root()
def hash_tree_root!(vector, {:vector, type, size}) do
{:ok, root} = hash_tree_root(vector, {:vector, type, size})
root
end

@spec hash_tree_root!(struct(), atom()) :: Types.root()
def hash_tree_root!(container, module) when is_map(container) do
chunks =
module.schema()
|> Enum.reduce(<<>>, fn {key, schema}, acc_root ->
value = container |> Map.get(key)
root = hash_tree_root!(value, schema)
acc_root <> root
end)

leaf_count = chunks |> get_chunks_len() |> next_pow_of_two()
root = merkleize_chunks_with_virtual_padding(chunks, leaf_count)
root
end

@spec hash_tree_root(list(), {:list, any, non_neg_integer}) ::
{:ok, Types.root()} | {:error, String.t()}
def hash_tree_root(list, {:list, type, size}) do
Expand Down Expand Up @@ -109,7 +145,7 @@ defmodule LambdaEthereumConsensus.SszEx do
if chunks_len > limit do
{:error, "chunk size exceeds limit"}
else
root = merkleize_chunks(chunks, limit) |> mix_in_length(len)
root = merkleize_chunks_with_virtual_padding(chunks, limit) |> mix_in_length(len)
{:ok, root}
end
end
Expand All @@ -118,7 +154,7 @@ defmodule LambdaEthereumConsensus.SszEx do
{:ok, Types.root()} | {:error, String.t()}
def hash_tree_root_vector_basic_type(chunks) do
leaf_count = chunks |> get_chunks_len() |> next_pow_of_two()
root = merkleize_chunks(chunks, leaf_count)
root = merkleize_chunks_with_virtual_padding(chunks, leaf_count)
{:ok, root}
end

Expand Down Expand Up @@ -164,6 +200,38 @@ defmodule LambdaEthereumConsensus.SszEx do
end
end

def merkleize_chunks_with_virtual_padding(chunks, leaf_count) do
chunks_len = chunks |> get_chunks_len()
power = leaf_count |> compute_pow()
height = power + 1

cond do
chunks_len == 0 ->
depth = height - 1
ZeroHashes.get_zero_hash(depth)

chunks_len == 1 and leaf_count == 1 ->
chunks

true ->
power = leaf_count |> compute_pow()
height = power + 1
layers = chunks
last_index = chunks_len - 1

{_, final_layer} =
1..(height - 1)
|> Enum.reverse()
|> Enum.reduce({last_index, layers}, fn i, {acc_last_index, acc_layers} ->
updated_layers = update_layers(i, height, acc_layers, acc_last_index)
{acc_last_index |> div(2), updated_layers}
end)

<<root::binary-size(@bytes_per_chunk), _::binary>> = final_layer
root
end
end

@spec pack(boolean, :bool) :: binary()
def pack(true, :bool), do: <<1::@bits_per_chunk-little>>
def pack(false, :bool), do: @zero_chunk
Expand All @@ -173,6 +241,11 @@ defmodule LambdaEthereumConsensus.SszEx do
<<value::size(size)-little>> |> pack_bytes()
end

@spec pack(binary, {:bytes, non_neg_integer}) :: binary()
def pack(value, {:bytes, _size}) do
value |> pack_bytes()
end

@spec pack(list(), {:list | :vector, any, non_neg_integer}) :: binary() | :error
def pack(list, {type, schema, _}) when type in [:vector, :list] do
if variable_size?(schema) do
Expand All @@ -184,6 +257,18 @@ defmodule LambdaEthereumConsensus.SszEx do
end
end

def chunk_count({:list, type, max_size}) do
size = size_of(type)
(max_size * size + 31) |> div(32)
end

def replace_chunk(chunks, start, new_chunk) do
<<left::binary-size(start), _::size(@bits_per_chunk), right::binary>> =
chunks

<<left::binary, new_chunk::binary, right::binary>>
end

#################
### Private functions
#################
Expand Down Expand Up @@ -592,16 +677,6 @@ defmodule LambdaEthereumConsensus.SszEx do

defp size_of({:int, size}), do: size |> div(@bits_per_byte)

defp chunk_count({:list, {:int, size}, max_size}) do
size = size_of({:int, size})
(max_size * size + 31) |> div(32)
end

defp chunk_count({:list, :bool, max_size}) do
size = size_of(:bool)
(max_size * size + 31) |> div(32)
end

defp pack_basic_type_list(list, schema) do
list
|> Enum.reduce(<<>>, fn x, acc ->
Expand Down Expand Up @@ -635,23 +710,69 @@ defmodule LambdaEthereumConsensus.SszEx do
end
end

defp next_pow_of_two(len) when is_integer(len) and len >= 0 do
if len == 0 do
0
else
n = ((len <<< 1) - 1) |> :math.log2() |> trunc()
2 ** n
end
end
defp next_pow_of_two(0), do: 0

defp replace_chunk(chunks, start, new_chunk) do
<<left::binary-size(start), _::size(@bits_per_chunk), right::binary>> =
chunks

<<left::binary, new_chunk::binary, right::binary>>
defp next_pow_of_two(len) when is_integer(len) and len > 0 do
n = ((len <<< 1) - 1) |> compute_pow()
2 ** n
end

defp get_chunks_len(chunks) do
chunks |> byte_size() |> div(@bytes_per_chunk)
end

defp compute_pow(value) do
:math.log2(value) |> trunc()
end

defp update_layers(i, height, acc_layers, acc_last_index) do
0..(2 ** i - 1)
|> Enum.filter(fn x -> rem(x, 2) == 0 end)
|> Enum.reduce_while(acc_layers, fn j, acc_layers ->
parent_index = j |> div(2)
nodes = get_nodes(parent_index, i, j, height, acc_layers, acc_last_index)
hash_nodes_and_replace(nodes, acc_layers)
end)
end

defp get_nodes(parent_index, _i, j, _height, acc_layers, acc_last_index)
when j < acc_last_index do
start = parent_index * @bytes_per_chunk
stop = (j + 2) * @bytes_per_chunk
focus = acc_layers |> :binary.part(start, stop - start)
focus_len = focus |> byte_size()
children_index = focus_len - 2 * @bytes_per_chunk
<<_::binary-size(children_index), children::binary>> = focus

<<left::binary-size(@bytes_per_chunk), right::binary-size(@bytes_per_chunk)>> =
children

{children_index, left, right}
end

defp get_nodes(parent_index, i, j, height, acc_layers, acc_last_index)
when j == acc_last_index do
start = parent_index * @bytes_per_chunk
stop = (j + 1) * @bytes_per_chunk
focus = acc_layers |> :binary.part(start, stop - start)
focus_len = focus |> byte_size()
children_index = focus_len - @bytes_per_chunk
<<_::binary-size(children_index), left::binary>> = focus
depth = height - i - 1
right = ZeroHashes.get_zero_hash(depth)
{children_index, left, right}
end

defp get_nodes(_, _, _, _, _, _), do: :error

defp hash_nodes_and_replace(nodes, layers) do
case nodes do
:error ->
{:halt, layers}

{index, left, right} ->
hash = hash_nodes(left, right)
{:cont, replace_chunk(layers, index, hash)}
end
end
end
Loading
Loading