Skip to content

Commit 181ac77

Browse files
feat: optimization on merkleization of chunks by precomputed hashes (#644)
1 parent e915941 commit 181ac77

File tree

4 files changed

+231
-28
lines changed

4 files changed

+231
-28
lines changed

bench/ssz.exs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,26 @@ Benchee.run(
3434
warmup: 2,
3535
time: 5
3636
)
37+
38+
## Benchmark Merkleization
39+
40+
list = Stream.cycle([65_535]) |> Enum.take(316)
41+
schema = {:list, {:int, 16}, 1024}
42+
packed_chunks = SszEx.pack(list, schema)
43+
limit = SszEx.chunk_count(schema)
44+
45+
Benchee.run(
46+
%{
47+
"SszEx.merkleize_chunks" => fn {chunks, leaf_count} ->
48+
SszEx.merkleize_chunks(chunks, leaf_count)
49+
end,
50+
"SszEx.merkleize_chunks_with_virtual_padding" => fn {chunks, leaf_count} ->
51+
SszEx.merkleize_chunks_with_virtual_padding(chunks, leaf_count)
52+
end
53+
},
54+
inputs: %{
55+
"args" => {packed_chunks, limit}
56+
},
57+
warmup: 2,
58+
time: 5
59+
)

lib/ssz_ex.ex

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule LambdaEthereumConsensus.SszEx do
55
alias LambdaEthereumConsensus.Utils.BitList
66
alias LambdaEthereumConsensus.Utils.BitVector
77
import alias LambdaEthereumConsensus.Utils.BitVector
8+
alias LambdaEthereumConsensus.Utils.ZeroHashes
89

910
#################
1011
### Public API
@@ -15,6 +16,7 @@ defmodule LambdaEthereumConsensus.SszEx do
1516
@bits_per_byte 8
1617
@bits_per_chunk @bytes_per_chunk * @bits_per_byte
1718
@zero_chunk <<0::size(@bits_per_chunk)>>
19+
@zero_hashes ZeroHashes.compute_zero_hashes()
1820

1921
@spec hash(iodata()) :: binary()
2022
def hash(data), do: :crypto.hash(:sha256, data)
@@ -109,7 +111,7 @@ defmodule LambdaEthereumConsensus.SszEx do
109111
if chunks_len > limit do
110112
{:error, "chunk size exceeds limit"}
111113
else
112-
root = merkleize_chunks(chunks, limit) |> mix_in_length(len)
114+
root = merkleize_chunks_with_virtual_padding(chunks, limit) |> mix_in_length(len)
113115
{:ok, root}
114116
end
115117
end
@@ -118,7 +120,7 @@ defmodule LambdaEthereumConsensus.SszEx do
118120
{:ok, Types.root()} | {:error, String.t()}
119121
def hash_tree_root_vector_basic_type(chunks) do
120122
leaf_count = chunks |> get_chunks_len() |> next_pow_of_two()
121-
root = merkleize_chunks(chunks, leaf_count)
123+
root = merkleize_chunks_with_virtual_padding(chunks, leaf_count)
122124
{:ok, root}
123125
end
124126

@@ -164,6 +166,38 @@ defmodule LambdaEthereumConsensus.SszEx do
164166
end
165167
end
166168

169+
def merkleize_chunks_with_virtual_padding(chunks, leaf_count) do
170+
chunks_len = chunks |> get_chunks_len()
171+
power = leaf_count |> compute_pow()
172+
height = power + 1
173+
174+
cond do
175+
chunks_len == 0 ->
176+
depth = height - 1
177+
get_zero_hash(depth)
178+
179+
chunks_len == 1 and leaf_count == 1 ->
180+
chunks
181+
182+
true ->
183+
power = leaf_count |> compute_pow()
184+
height = power + 1
185+
layers = chunks
186+
last_index = chunks_len - 1
187+
188+
{_, final_layer} =
189+
1..(height - 1)
190+
|> Enum.reverse()
191+
|> Enum.reduce({last_index, layers}, fn i, {acc_last_index, acc_layers} ->
192+
updated_layers = update_layers(i, height, acc_layers, acc_last_index)
193+
{acc_last_index |> div(2), updated_layers}
194+
end)
195+
196+
<<root::binary-size(@bytes_per_chunk), _::binary>> = final_layer
197+
root
198+
end
199+
end
200+
167201
@spec pack(boolean, :bool) :: binary()
168202
def pack(true, :bool), do: <<1::@bits_per_chunk-little>>
169203
def pack(false, :bool), do: @zero_chunk
@@ -184,6 +218,11 @@ defmodule LambdaEthereumConsensus.SszEx do
184218
end
185219
end
186220

221+
def chunk_count({:list, type, max_size}) do
222+
size = size_of(type)
223+
(max_size * size + 31) |> div(32)
224+
end
225+
187226
#################
188227
### Private functions
189228
#################
@@ -592,16 +631,6 @@ defmodule LambdaEthereumConsensus.SszEx do
592631

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

595-
defp chunk_count({:list, {:int, size}, max_size}) do
596-
size = size_of({:int, size})
597-
(max_size * size + 31) |> div(32)
598-
end
599-
600-
defp chunk_count({:list, :bool, max_size}) do
601-
size = size_of(:bool)
602-
(max_size * size + 31) |> div(32)
603-
end
604-
605634
defp pack_basic_type_list(list, schema) do
606635
list
607636
|> Enum.reduce(<<>>, fn x, acc ->
@@ -635,12 +664,69 @@ defmodule LambdaEthereumConsensus.SszEx do
635664
end
636665
end
637666

638-
defp next_pow_of_two(len) when is_integer(len) and len >= 0 do
639-
if len == 0 do
640-
0
641-
else
642-
n = ((len <<< 1) - 1) |> :math.log2() |> trunc()
643-
2 ** n
667+
defp next_pow_of_two(0), do: 0
668+
669+
defp next_pow_of_two(len) when is_integer(len) and len > 0 do
670+
n = ((len <<< 1) - 1) |> compute_pow()
671+
2 ** n
672+
end
673+
674+
defp get_chunks_len(chunks) do
675+
chunks |> byte_size() |> div(@bytes_per_chunk)
676+
end
677+
678+
defp compute_pow(value) do
679+
:math.log2(value) |> trunc()
680+
end
681+
682+
defp update_layers(i, height, acc_layers, acc_last_index) do
683+
0..(2 ** i - 1)
684+
|> Enum.filter(fn x -> rem(x, 2) == 0 end)
685+
|> Enum.reduce_while(acc_layers, fn j, acc_layers ->
686+
parent_index = j |> div(2)
687+
nodes = get_nodes(parent_index, i, j, height, acc_layers, acc_last_index)
688+
hash_nodes_and_replace(nodes, acc_layers)
689+
end)
690+
end
691+
692+
defp get_nodes(parent_index, _i, j, _height, acc_layers, acc_last_index)
693+
when j < acc_last_index do
694+
start = parent_index * @bytes_per_chunk
695+
stop = (j + 2) * @bytes_per_chunk
696+
focus = acc_layers |> :binary.part(start, stop - start)
697+
focus_len = focus |> byte_size()
698+
children_index = focus_len - 2 * @bytes_per_chunk
699+
<<_::binary-size(children_index), children::binary>> = focus
700+
701+
<<left::binary-size(@bytes_per_chunk), right::binary-size(@bytes_per_chunk)>> =
702+
children
703+
704+
{children_index, left, right}
705+
end
706+
707+
defp get_nodes(parent_index, i, j, height, acc_layers, acc_last_index)
708+
when j == acc_last_index do
709+
start = parent_index * @bytes_per_chunk
710+
stop = (j + 1) * @bytes_per_chunk
711+
focus = acc_layers |> :binary.part(start, stop - start)
712+
focus_len = focus |> byte_size()
713+
children_index = focus_len - @bytes_per_chunk
714+
<<_::binary-size(children_index), left::binary>> = focus
715+
depth = height - i - 1
716+
right = get_zero_hash(depth)
717+
{children_index, left, right}
718+
end
719+
720+
defp get_nodes(_, _, _, _, _, _), do: :error
721+
722+
defp hash_nodes_and_replace(nodes, layers) do
723+
case nodes do
724+
:error ->
725+
{:halt, layers}
726+
727+
{index, left, right} ->
728+
hash = hash_nodes(left, right)
729+
{:cont, replace_chunk(layers, index, hash)}
644730
end
645731
end
646732

@@ -651,7 +737,9 @@ defmodule LambdaEthereumConsensus.SszEx do
651737
<<left::binary, new_chunk::binary, right::binary>>
652738
end
653739

654-
defp get_chunks_len(chunks) do
655-
chunks |> byte_size() |> div(@bytes_per_chunk)
740+
defp get_zero_hash(depth) do
741+
offset = (depth + 1) * @bytes_per_chunk - @bytes_per_chunk
742+
<<_::binary-size(offset), hash::binary-size(@bytes_per_chunk), _::binary>> = @zero_hashes
743+
hash
656744
end
657745
end

lib/utils/zero_hashes.ex

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
defmodule LambdaEthereumConsensus.Utils.ZeroHashes do
2+
@moduledoc """
3+
Precomputed zero hashes
4+
"""
5+
6+
@bits_per_byte 8
7+
@bytes_per_chunk 32
8+
@bits_per_chunk @bytes_per_chunk * @bits_per_byte
9+
@max_merkle_tree_depth 64
10+
11+
def compute_zero_hashes do
12+
buffer = <<0::size(@bytes_per_chunk * @max_merkle_tree_depth * @bits_per_byte)>>
13+
14+
0..(@max_merkle_tree_depth - 2)
15+
|> Enum.reduce(buffer, fn index, acc_buffer ->
16+
start = index * @bytes_per_chunk
17+
stop = (index + 2) * @bytes_per_chunk
18+
focus = acc_buffer |> :binary.part(start, stop - start)
19+
<<left::binary-size(@bytes_per_chunk), _::binary>> = focus
20+
hash = hash_nodes(left, left)
21+
change_index = (index + 1) * @bytes_per_chunk
22+
replace_chunk(acc_buffer, change_index, hash)
23+
end)
24+
end
25+
26+
defp hash_nodes(left, right), do: :crypto.hash(:sha256, left <> right)
27+
28+
defp replace_chunk(chunks, start, new_chunk) do
29+
<<left::binary-size(start), _::size(@bits_per_chunk), right::binary>> =
30+
chunks
31+
32+
<<left::binary, new_chunk::binary, right::binary>>
33+
end
34+
end

test/unit/ssz_ex_test.exs

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ defmodule Unit.SSZExTest do
7272
end
7373

7474
test "merklelization of chunks" do
75-
## Reference: https://github.com/ralexstokes/ssz-rs/blob/1f94d5dfc70c86dab672e91ac46af04a5f96c342/ssz-rs/src/merkleization/mod.rs#L371
76-
## https://github.com/ralexstokes/ssz-rs/blob/1f94d5dfc70c86dab672e91ac46af04a5f96c342/ssz-rs/src/merkleization/mod.rs#L416
75+
# Reference: https://github.com/ralexstokes/ssz-rs/blob/1f94d5dfc70c86dab672e91ac46af04a5f96c342/ssz-rs/src/merkleization/mod.rs#L371
76+
# https://github.com/ralexstokes/ssz-rs/blob/1f94d5dfc70c86dab672e91ac46af04a5f96c342/ssz-rs/src/merkleization/mod.rs#L416
7777
zero = <<0::256>>
7878

7979
chunks = zero
@@ -136,13 +136,71 @@ defmodule Unit.SSZExTest do
136136
root = chunks |> SszEx.merkleize_chunks(2 ** 10)
137137
expected_value = "2647cb9e26bd83eeb0982814b2ac4d6cc4a65d0d98637f1a73a4c06d3db0e6ce"
138138
assert root |> Base.encode16(case: :lower) == expected_value
139+
end
140+
141+
test "merklelization of chunks with virtual padding" do
142+
zero = <<0::256>>
143+
144+
chunks = zero
145+
root = SszEx.merkleize_chunks_with_virtual_padding(chunks, 1)
146+
expected_value = "0000000000000000000000000000000000000000000000000000000000000000"
147+
assert root |> Base.encode16(case: :lower) == expected_value
148+
149+
chunks = zero <> zero
150+
root = chunks |> SszEx.merkleize_chunks_with_virtual_padding(2)
151+
expected_value = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"
152+
assert root |> Base.encode16(case: :lower) == expected_value
153+
154+
ones = 0..31 |> Enum.reduce(<<>>, fn _, acc -> <<1>> <> acc end)
155+
156+
chunks = ones <> ones
157+
root = chunks |> SszEx.merkleize_chunks_with_virtual_padding(2)
158+
expected_value = "7c8975e1e60a5c8337f28edf8c33c3b180360b7279644a9bc1af3c51e6220bf5"
159+
assert root |> Base.encode16(case: :lower) == expected_value
160+
161+
chunks = zero <> zero <> zero <> zero
162+
root = chunks |> SszEx.merkleize_chunks_with_virtual_padding(4)
163+
expected_value = "db56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"
164+
assert root |> Base.encode16(case: :lower) == expected_value
165+
166+
chunks = zero <> zero <> zero <> zero <> zero <> zero <> zero <> zero
167+
root = chunks |> SszEx.merkleize_chunks_with_virtual_padding(8)
168+
expected_value = "c78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c"
169+
assert root |> Base.encode16(case: :lower) == expected_value
170+
171+
chunks = ones
172+
root = chunks |> SszEx.merkleize_chunks_with_virtual_padding(4)
173+
expected_value = "29797eded0e83376b70f2bf034cc0811ae7f1414653b1d720dfd18f74cf13309"
174+
assert root |> Base.encode16(case: :lower) == expected_value
175+
176+
twos = 0..31 |> Enum.reduce(<<>>, fn _, acc -> <<2>> <> acc end)
177+
178+
chunks = twos
179+
root = chunks |> SszEx.merkleize_chunks_with_virtual_padding(8)
180+
expected_value = "fa4cf775712aa8a2fe5dcb5a517d19b2e9effcf58ff311b9fd8e4a7d308e6d00"
181+
assert root |> Base.encode16(case: :lower) == expected_value
182+
183+
chunks = ones <> ones <> ones
184+
root = chunks |> SszEx.merkleize_chunks_with_virtual_padding(4)
185+
expected_value = "65aa94f2b59e517abd400cab655f42821374e433e41b8fe599f6bb15484adcec"
186+
assert root |> Base.encode16(case: :lower) == expected_value
139187

140-
## TOO HEAVY COMPUTATION!
141-
# chunks = 1..70 |> Enum.reduce(<<>>, fn _, acc -> acc <> ones end)
142-
# leaf_count = 9_223_372_036_854_775_808 # 2 ** 63
143-
# root = chunks |> SszEx.merkleize_chunks(leaf_count)
144-
# expected_value = "9317695d95b5a3b46e976b5a9cbfcfccb600accaddeda9ac867cc9669b862979"
145-
# assert root |> Base.encode16(case: :lower) == expected_value
188+
chunks = ones <> ones <> ones <> ones <> ones
189+
root = chunks |> SszEx.merkleize_chunks_with_virtual_padding(8)
190+
expected_value = "0ae67e34cba4ad2bbfea5dc39e6679b444021522d861fab00f05063c54341289"
191+
assert root |> Base.encode16(case: :lower) == expected_value
192+
193+
chunks = ones <> ones <> ones <> ones <> ones <> ones
194+
root = chunks |> SszEx.merkleize_chunks_with_virtual_padding(8)
195+
expected_value = "0ef7df63c204ef203d76145627b8083c49aa7c55ebdee2967556f55a4f65a238"
196+
assert root |> Base.encode16(case: :lower) == expected_value
197+
198+
## Large Leaf Count
199+
200+
chunks = ones <> ones <> ones <> ones <> ones
201+
root = chunks |> SszEx.merkleize_chunks_with_virtual_padding(2 ** 10)
202+
expected_value = "2647cb9e26bd83eeb0982814b2ac4d6cc4a65d0d98637f1a73a4c06d3db0e6ce"
203+
assert root |> Base.encode16(case: :lower) == expected_value
146204
end
147205

148206
test "hash tree root of list" do

0 commit comments

Comments
 (0)