Skip to content

Commit d846b69

Browse files
feat: merklelization of list of composite types (#703)
1 parent 301476b commit d846b69

File tree

3 files changed

+230
-81
lines changed

3 files changed

+230
-81
lines changed

lib/ssz_ex.ex

Lines changed: 99 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -80,78 +80,97 @@ defmodule LambdaEthereumConsensus.SszEx do
8080
else: decode_fixed_container(binary, module)
8181
end
8282

83-
@spec hash_tree_root!(boolean, atom) :: Types.root()
84-
def hash_tree_root!(value, :bool), do: pack(value, :bool)
83+
@spec hash_tree_root!(any, any) :: Types.root()
84+
def hash_tree_root!(value, schema) do
85+
{:ok, root} = hash_tree_root(value, schema)
86+
root
87+
end
8588

86-
@spec hash_tree_root!(non_neg_integer, {:int, non_neg_integer}) :: Types.root()
87-
def hash_tree_root!(value, {:int, size}), do: pack(value, {:int, size})
89+
@spec hash_tree_root(boolean, atom) :: Types.root()
90+
def hash_tree_root(value, :bool), do: {:ok, pack(value, :bool)}
8891

89-
@spec hash_tree_root!(binary, {:bytes, non_neg_integer}) :: Types.root()
90-
def hash_tree_root!(value, {:bytes, size}) do
92+
@spec hash_tree_root(non_neg_integer, {:int, non_neg_integer}) :: Types.root()
93+
def hash_tree_root(value, {:int, size}), do: {:ok, pack(value, {:int, size})}
94+
95+
@spec hash_tree_root(binary, {:bytes, non_neg_integer}) :: Types.root()
96+
def hash_tree_root(value, {:bytes, size}) do
9197
packed_chunks = pack(value, {:bytes, size})
9298
leaf_count = packed_chunks |> get_chunks_len() |> next_pow_of_two()
9399
root = merkleize_chunks_with_virtual_padding(packed_chunks, leaf_count)
94-
root
95-
end
96-
97-
@spec hash_tree_root!(list(), {:list, any, non_neg_integer}) :: Types.root()
98-
def hash_tree_root!(list, {:list, type, size}) do
99-
{:ok, root} = hash_tree_root(list, {:list, type, size})
100-
root
101-
end
102-
103-
@spec hash_tree_root!(list(), {:vector, any, non_neg_integer}) :: Types.root()
104-
def hash_tree_root!(vector, {:vector, type, size}) do
105-
{:ok, root} = hash_tree_root(vector, {:vector, type, size})
106-
root
100+
{:ok, root}
107101
end
108102

109-
@spec hash_tree_root!(struct(), atom()) :: Types.root()
110-
def hash_tree_root!(container, module) when is_map(container) do
111-
chunks =
103+
@spec hash_tree_root(struct(), atom()) :: Types.root()
104+
def hash_tree_root(container, module) when is_map(container) do
105+
value =
112106
module.schema()
113-
|> Enum.reduce(<<>>, fn {key, schema}, acc_root ->
107+
|> Enum.reduce_while({:ok, <<>>}, fn {key, schema}, {_, acc_root} ->
114108
value = container |> Map.get(key)
115-
root = hash_tree_root!(value, schema)
116-
acc_root <> root
109+
110+
case hash_tree_root(value, schema) do
111+
{:ok, root} -> {:cont, {:ok, acc_root <> root}}
112+
{:error, reason} -> {:halt, {:error, reason}}
113+
end
117114
end)
118115

119-
leaf_count = chunks |> get_chunks_len() |> next_pow_of_two()
120-
root = merkleize_chunks_with_virtual_padding(chunks, leaf_count)
121-
root
116+
case value do
117+
{:ok, chunks} ->
118+
leaf_count = chunks |> get_chunks_len() |> next_pow_of_two()
119+
root = chunks |> merkleize_chunks_with_virtual_padding(leaf_count)
120+
{:ok, root}
121+
122+
{:error, reason} ->
123+
{:error, reason}
124+
end
122125
end
123126

124127
@spec hash_tree_root(list(), {:list, any, non_neg_integer}) ::
125128
{:ok, Types.root()} | {:error, String.t()}
126-
def hash_tree_root(list, {:list, type, size}) do
127-
if variable_size?(type) do
128-
# TODO
129-
# hash_tree_root_list_complex_type(list, {:list, type, size}, limit)
130-
{:error, "Not implemented"}
131-
else
132-
packed_chunks = pack(list, {:list, type, size})
133-
limit = chunk_count({:list, type, size})
134-
len = length(list)
135-
hash_tree_root_list_basic_type(packed_chunks, limit, len)
129+
def hash_tree_root(list, {:list, type, _size} = schema) do
130+
limit = chunk_count(schema)
131+
len = length(list)
132+
133+
value =
134+
if basic_type?(type) do
135+
pack(list, schema)
136+
else
137+
list_hash_tree_root(list, type)
138+
end
139+
140+
case value do
141+
{:ok, chunks} -> chunks |> hash_tree_root_list(limit, len)
142+
{:error, reason} -> {:error, reason}
143+
chunks -> chunks |> hash_tree_root_list(limit, len)
136144
end
137145
end
138146

139147
@spec hash_tree_root(list(), {:vector, any, non_neg_integer}) ::
140148
{:ok, Types.root()} | {:error, String.t()}
141-
def hash_tree_root(vector, {:vector, type, size}) do
142-
if variable_size?(type) do
143-
# TODO
144-
# hash_tree_root_vector_complex_type(vector, {:vector, type, size}, limit)
145-
{:error, "Not implemented"}
146-
else
147-
packed_chunks = pack(vector, {:list, type, size})
148-
hash_tree_root_vector_basic_type(packed_chunks)
149+
def hash_tree_root(vector, {:vector, _type, size}) when length(vector) != size,
150+
do: {:error, "invalid size"}
151+
152+
def hash_tree_root(vector, {:vector, type, _size} = schema) do
153+
value =
154+
if basic_type?(type) do
155+
pack(vector, schema)
156+
else
157+
list_hash_tree_root(vector, type)
158+
end
159+
160+
case value do
161+
{:ok, chunks} -> chunks |> hash_tree_root_vector()
162+
{:error, reason} -> {:error, reason}
163+
chunks -> chunks |> hash_tree_root_vector()
149164
end
150165
end
151166

152-
@spec hash_tree_root_list_basic_type(binary(), non_neg_integer, non_neg_integer) ::
153-
{:ok, Types.root()} | {:error, String.t()}
154-
def hash_tree_root_list_basic_type(chunks, limit, len) do
167+
def hash_tree_root_vector(chunks) do
168+
leaf_count = chunks |> get_chunks_len() |> next_pow_of_two()
169+
root = merkleize_chunks_with_virtual_padding(chunks, leaf_count)
170+
{:ok, root}
171+
end
172+
173+
def hash_tree_root_list(chunks, limit, len) do
155174
chunks_len = chunks |> get_chunks_len()
156175

157176
if chunks_len > limit do
@@ -162,14 +181,6 @@ defmodule LambdaEthereumConsensus.SszEx do
162181
end
163182
end
164183

165-
@spec hash_tree_root_vector_basic_type(binary()) ::
166-
{:ok, Types.root()} | {:error, String.t()}
167-
def hash_tree_root_vector_basic_type(chunks) do
168-
leaf_count = chunks |> get_chunks_len() |> next_pow_of_two()
169-
root = merkleize_chunks_with_virtual_padding(chunks, leaf_count)
170-
{:ok, root}
171-
end
172-
173184
@spec mix_in_length(Types.root(), non_neg_integer) :: Types.root()
174185
def mix_in_length(root, len) do
175186
{:ok, serialized_len} = encode_int(len, @bits_per_chunk)
@@ -260,18 +271,21 @@ defmodule LambdaEthereumConsensus.SszEx do
260271

261272
@spec pack(list(), {:list | :vector, any, non_neg_integer}) :: binary() | :error
262273
def pack(list, {type, schema, _}) when type in [:vector, :list] do
263-
if variable_size?(schema) do
264-
# TODO
265-
# pack_complex_type_list(list)
266-
:error
267-
else
268-
pack_basic_type_list(list, schema)
269-
end
274+
list
275+
|> Enum.reduce(<<>>, fn x, acc ->
276+
{:ok, encoded} = encode(x, schema)
277+
acc <> encoded
278+
end)
279+
|> pack_bytes()
270280
end
271281

272282
def chunk_count({:list, type, max_size}) do
273-
size = size_of(type)
274-
(max_size * size + 31) |> div(32)
283+
if basic_type?(type) do
284+
size = size_of(type)
285+
(max_size * size + 31) |> div(32)
286+
else
287+
max_size
288+
end
275289
end
276290

277291
#################
@@ -779,19 +793,19 @@ defmodule LambdaEthereumConsensus.SszEx do
779793
|> Enum.any?()
780794
end
781795

796+
defp basic_type?({:int, _}), do: true
797+
defp basic_type?(:bool), do: true
798+
defp basic_type?({:bytes, _}), do: true
799+
defp basic_type?({:list, _, _}), do: false
800+
defp basic_type?({:vector, _, _}), do: false
801+
defp basic_type?({:bitlist, _}), do: false
802+
defp basic_type?({:bitvector, _}), do: false
803+
defp basic_type?(module) when is_atom(module), do: false
804+
782805
defp size_of(:bool), do: @bytes_per_boolean
783806

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

786-
defp pack_basic_type_list(list, schema) do
787-
list
788-
|> Enum.reduce(<<>>, fn x, acc ->
789-
{:ok, encoded} = encode(x, schema)
790-
acc <> encoded
791-
end)
792-
|> pack_bytes()
793-
end
794-
795809
defp pack_bytes(value) when is_binary(value) do
796810
incomplete_chunk_len = value |> bit_size() |> rem(@bits_per_chunk)
797811

@@ -894,4 +908,14 @@ defmodule LambdaEthereumConsensus.SszEx do
894908
<<_::binary-size(offset), hash::binary-size(@bytes_per_chunk), _::binary>> = @zero_hashes
895909
hash
896910
end
911+
912+
def list_hash_tree_root(list, inner_schema) do
913+
list
914+
|> Enum.reduce_while({:ok, <<>>}, fn value, {_, acc_roots} ->
915+
case hash_tree_root(value, inner_schema) do
916+
{:ok, root} -> {:cont, {:ok, acc_roots <> root}}
917+
{:error, reason} -> {:halt, {:error, reason}}
918+
end
919+
end)
920+
end
897921
end

test/spec/runners/ssz_generic.ex

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,32 @@ defmodule SszGenericTestRunner do
6767
assert_ssz_invalid(schema, real_serialized)
6868
end
6969

70-
defp assert_ssz_valid(schema, real_serialized, real_deserialized, _expected_hash_tree_root) do
70+
defp assert_ssz_valid(schema, real_serialized, real_deserialized, expected_hash_tree_root) do
7171
{:ok, deserialized} = SszEx.decode(real_serialized, schema)
7272
assert deserialized == real_deserialized
7373

7474
{:ok, serialized} = SszEx.encode(real_deserialized, schema)
7575

7676
assert serialized == real_serialized
7777

78-
# TODO enable when merklelization is ready for all schemas
79-
# actual_hash_tree_root = SszEx.hash_tree_root!(real_deserialized, schema)
80-
#
81-
# assert actual_hash_tree_root == expected_hash_tree_root
78+
## TODO: To be removed when hash_tree_root for bitlist and bitvector is implemented
79+
case schema do
80+
{:bitlist, _} ->
81+
## TODO
82+
nil
83+
84+
{:bitvector, _} ->
85+
## TODO
86+
nil
87+
88+
module when is_atom(module) ->
89+
## TODO
90+
nil
91+
92+
_ ->
93+
actual_hash_tree_root = SszEx.hash_tree_root!(real_deserialized, schema)
94+
assert actual_hash_tree_root == expected_hash_tree_root
95+
end
8296
end
8397

8498
defp assert_ssz_invalid(schema, real_serialized) do

0 commit comments

Comments
 (0)