Skip to content

Commit e10c578

Browse files
authored
fix: eth1data votes error on block propose (#1307)
1 parent 86f4df2 commit e10c578

File tree

3 files changed

+80
-32
lines changed

3 files changed

+80
-32
lines changed

lib/lambda_ethereum_consensus/execution/execution_chain.ex

+56-31
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ defmodule LambdaEthereumConsensus.Execution.ExecutionChain do
138138

139139
if has_majority?(eth1_data_votes, eth1_data) do
140140
case update_deposit_tree(new_state, eth1_data) do
141-
{:ok, new_tree} -> %{state | deposit_tree: new_tree, current_eth1_data: eth1_data}
141+
{:ok, new_tree} -> %{new_state | deposit_tree: new_tree, current_eth1_data: eth1_data}
142142
_ -> new_state
143143
end
144144
else
@@ -193,16 +193,16 @@ defmodule LambdaEthereumConsensus.Execution.ExecutionChain do
193193
%{
194194
eth1_chain: eth1_chain,
195195
eth1_data_votes: seen_votes,
196-
deposit_tree: deposit_tree
196+
deposit_tree: deposit_tree,
197+
current_eth1_data: default
197198
},
198199
slot
199200
) do
200201
period_start = voting_period_start_time(slot)
201-
follow_time = ChainSpec.get("SECONDS_PER_ETH1_BLOCK") * ChainSpec.get("ETH1_FOLLOW_DISTANCE")
202202

203203
blocks_to_consider =
204204
eth1_chain
205-
|> Enum.filter(&candidate_block?(&1.timestamp, period_start, follow_time))
205+
|> Enum.filter(&candidate_block?(&1.timestamp, period_start))
206206
|> Enum.reverse()
207207

208208
# TODO: backfill chain
@@ -217,54 +217,79 @@ defmodule LambdaEthereumConsensus.Execution.ExecutionChain do
217217
# TODO: fetch asynchronously
218218
with {:ok, new_deposits} <-
219219
ExecutionClient.get_deposit_logs(block_number_min..block_number_max) do
220-
get_first_valid_vote(blocks_to_consider, seen_votes, deposit_tree, new_deposits)
220+
get_first_valid_vote(blocks_to_consider, seen_votes, deposit_tree, new_deposits, default)
221221
end
222222
end
223223
end
224224

225-
defp get_first_valid_vote(blocks_to_consider, seen_votes, deposit_tree, new_deposits) do
226-
grouped_deposits = Enum.group_by(new_deposits, &Map.fetch!(&1, :block_number))
227-
228-
{valid_votes, _last_tree} =
229-
blocks_to_consider
230-
|> Enum.reduce({MapSet.new(), deposit_tree}, fn block, {set, tree} ->
231-
new_tree =
232-
case grouped_deposits[block.block_number] do
233-
nil -> tree
234-
deposits -> update_tree_with_deposits(tree, deposits)
235-
end
225+
defp get_first_valid_vote(blocks_to_consider, seen_votes, deposit_tree, new_deposits, default) do
226+
Logger.debug(
227+
"Processing new deposits: #{inspect(new_deposits)} and get first valid vote, with default: #{inspect(default)}"
228+
)
236229

237-
data = %Eth1Data{
238-
deposit_root: DepositTree.get_root(new_tree),
239-
deposit_count: DepositTree.get_deposit_count(new_tree),
240-
block_hash: block.block_hash
241-
}
230+
{valid_votes, last_eth1_data} =
231+
get_valid_votes(blocks_to_consider, deposit_tree, new_deposits, default)
242232

243-
{MapSet.put(set, data), new_tree}
244-
end)
233+
# Default vote on latest eth1 block data in the period range unless eth1 chain is not live
234+
default_vote = last_eth1_data || default
245235

246-
# Tiebreak by smallest distance to period start
236+
# Tiebreak by smallest distance to period start seen_votes is a %{eth1_data -> {count, dist}}
247237
result =
248238
seen_votes
249-
|> Stream.filter(&MapSet.member?(valid_votes, &1))
250-
|> Enum.max(fn {_, count1}, {_, count2} -> count1 >= count2 end, fn -> nil end)
239+
|> Stream.filter(fn {eth1_data, _} -> MapSet.member?(valid_votes, eth1_data) end)
240+
|> Enum.max(
241+
fn {_, {count1, dist1}}, {_, {count2, dist2}} ->
242+
cond do
243+
count1 > count2 -> true
244+
count1 == count2 && dist1 > dist2 -> true
245+
true -> false
246+
end
247+
end,
248+
fn -> nil end
249+
)
251250

252251
case result do
253-
# Use the first vote if there is a tie
254-
nil -> {:ok, List.last(valid_votes)}
252+
nil -> {:ok, default_vote}
255253
{eth1_data, _} -> {:ok, eth1_data}
256254
end
257255
end
258256

257+
defp get_valid_votes(blocks_to_consider, deposit_tree, new_deposits, default) do
258+
grouped_deposits = Enum.group_by(new_deposits, &Map.fetch!(&1, :block_number))
259+
260+
blocks_to_consider
261+
|> Enum.reduce({MapSet.new(), deposit_tree, nil}, fn block, {set, tree, last_eth1_data} ->
262+
new_tree =
263+
case grouped_deposits[block.block_number] do
264+
nil -> tree
265+
deposits -> update_tree_with_deposits(tree, deposits)
266+
end
267+
268+
data = get_eth1_data(block, new_tree)
269+
270+
if data.deposit_count >= default.deposit_count,
271+
do: {MapSet.put(set, data), new_tree, data},
272+
else: {set, new_tree, last_eth1_data}
273+
end)
274+
end
275+
276+
defp get_eth1_data(block, tree) do
277+
%Eth1Data{
278+
deposit_root: DepositTree.get_root(tree),
279+
deposit_count: DepositTree.get_deposit_count(tree),
280+
block_hash: block.block_hash
281+
}
282+
end
283+
259284
defp update_tree_with_deposits(tree, []), do: tree
260285

261286
defp update_tree_with_deposits(tree, [deposit | rest]) do
262287
DepositTree.push_leaf(tree, deposit.data) |> update_tree_with_deposits(rest)
263288
end
264289

265-
defp candidate_block?(timestamp, period_start, follow_time) do
266-
# follow_time = SECONDS_PER_ETH1_BLOCK * ETH1_FOLLOW_DISTANCE
267-
timestamp in (period_start - follow_time * 2)..(period_start - follow_time)
290+
defp candidate_block?(timestamp, period_start) do
291+
follow_time = ChainSpec.get("SECONDS_PER_ETH1_BLOCK") * ChainSpec.get("ETH1_FOLLOW_DISTANCE")
292+
timestamp + follow_time <= period_start and timestamp + follow_time * 2 >= period_start
268293
end
269294

270295
defp voting_period_start_time(slot) do

lib/types/deposit_tree.ex

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ defmodule Types.DepositTree do
130130
{:node, {create_node(leaves_left, depth - 1), create_node(leaves_right, depth - 1)}}
131131
end
132132

133+
defp finalize_tree({:zero, depth}, 0 = _deposit_count, _), do: {:zero, depth}
133134
defp finalize_tree({:finalized, _} = node, _, _), do: node
134135
defp finalize_tree({:leaf, {hash, _}}, _, _), do: {:finalized, {hash, 1}}
135136

test/unit/deposit_tree_test.exs

+23-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,17 @@ defmodule Unit.DepositTreeTest do
1111

1212
doctest DepositTree
1313

14-
# Testcases taken from EIP-4881
14+
# Testcases taken from EIP-4881 + empty case
15+
@snapshot_empty %DepositTreeSnapshot{
16+
finalized: [],
17+
deposit_root:
18+
Base.decode16!("D70A234731285C6804C2A4F56711DDB8C82C99740F207854891028AF34E27E5E"),
19+
deposit_count: 0,
20+
execution_block_hash:
21+
Base.decode16!("C0B2CBA66FA21E555461E6B699E0F280A5C4A9CD7AE724D79F711E57460FFB2B"),
22+
execution_block_height: 0
23+
}
24+
1525
@snapshot_1 %DepositTreeSnapshot{
1626
finalized: [
1727
Base.decode16!("7AF7DA533B0DC64B690CB0604F5A81E40ED83796DD14037EA3A55383B8F0976A")
@@ -108,4 +118,16 @@ defmodule Unit.DepositTreeTest do
108118

109119
assert DepositTree.get_snapshot(tree) == @snapshot_2
110120
end
121+
122+
test "finalizing an empty tree is equal to itself" do
123+
eth1_data = %Eth1Data{
124+
deposit_root: @snapshot_empty.deposit_root,
125+
deposit_count: @snapshot_empty.deposit_count,
126+
block_hash: @snapshot_empty.execution_block_hash
127+
}
128+
129+
tree = DepositTree.from_snapshot(@snapshot_empty) |> DepositTree.finalize(eth1_data, 0)
130+
131+
assert tree == DepositTree.from_snapshot(@snapshot_empty)
132+
end
111133
end

0 commit comments

Comments
 (0)