From 2e1d0ebcee51791f5aa7f681620cc3f0de6f89ca Mon Sep 17 00:00:00 2001 From: Martin Paulucci Date: Mon, 5 Feb 2024 15:55:49 +0100 Subject: [PATCH 1/4] Make Fork Choice on_block a non blocking call. --- .../beacon/pending_blocks.ex | 68 +++++++++++-------- .../fork_choice/fork_choice.ex | 28 ++------ test/unit/pending_blocks_test.exs | 3 +- 3 files changed, 48 insertions(+), 51 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 8e23547bd..76f914316 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -10,11 +10,13 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.P2P.BlockDownloader + alias LambdaEthereumConsensus.Store.Blocks alias Types.SignedBeaconBlock @type state :: %{ pending_blocks: %{Types.root() => SignedBeaconBlock.t()}, invalid_blocks: %{Types.root() => map()}, + processing_blocks: MapSet.t(Types.root()), blocks_to_download: MapSet.t(Types.root()) } @@ -45,7 +47,14 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do def init(_opts) do schedule_blocks_processing() schedule_blocks_download() - {:ok, %{pending_blocks: %{}, invalid_blocks: %{}, blocks_to_download: MapSet.new()}} + + {:ok, + %{ + pending_blocks: %{}, + invalid_blocks: %{}, + blocks_to_download: MapSet.new(), + processing_blocks: MapSet.new() + }} end @impl true @@ -55,6 +64,23 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do {:noreply, Map.put(state, :pending_blocks, pending_blocks)} end + @impl true + def handle_cast({:block_processed, signed_block, block_root, is_valid?}, state) do + if is_valid? do + state |> Map.update!(:processing_blocks, &MapSet.delete(&1, block_root)) + else + state + |> Map.update!(:processing_blocks, &MapSet.delete(&1, block_root)) + |> Map.update!( + :invalid_blocks, + &Map.put(&1, block_root, signed_block.message |> Map.take([:slot, :parent_root])) + ) + end + |> then(fn state -> + {:noreply, state} + end) + end + @impl true def handle_call({:is_pending_block, block_root}, _from, state) do {:reply, Map.has_key?(state.pending_blocks, block_root), state} @@ -83,29 +109,29 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do &Map.put(&1, block_root, signed_block.message |> Map.take([:slot, :parent_root])) ) + # If parent is processing, block is pending + state.processing_blocks |> MapSet.member?(parent_root) -> + state + # If parent is pending, block is pending state.pending_blocks |> Map.has_key?(parent_root) -> state # If already in fork choice, remove from pending - ForkChoice.has_block?(block_root) -> + Blocks.get_block(block_root) -> state |> Map.update!(:pending_blocks, &Map.delete(&1, block_root)) - # If parent is not in fork choice, download parent - not ForkChoice.has_block?(parent_root) -> + # If parent is not in fork choice, download parentx + !Blocks.get_block(parent_root) -> state |> Map.update!(:blocks_to_download, &MapSet.put(&1, parent_root)) # If all the other conditions are false, add block to fork choice true -> - new_state = send_block_to_forkchoice(state, signed_block, block_root) + ForkChoice.on_block(signed_block, block_root) - # When on checkpoint sync, we might accumulate a couple of hundred blocks in the pending blocks queue. - # This can cause the ForkChoice to timeout on other call requests since it has to process all the - # pending blocks first. - # TODO: find a better way to handle this - Process.sleep(100) - - new_state + state + |> Map.update!(:pending_blocks, &Map.delete(&1, block_root)) + |> Map.update!(:processing_blocks, &MapSet.put(&1, block_root)) end end) |> then(fn state -> @@ -125,7 +151,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do @impl true def handle_info(:download_blocks, state) do - blocks_in_store = state.blocks_to_download |> MapSet.filter(&ForkChoice.has_block?/1) + blocks_in_store = state.blocks_to_download |> MapSet.filter(&Blocks.get_block/1) downloaded_blocks = state.blocks_to_download @@ -159,22 +185,6 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do ### Private Functions ########################## - @spec send_block_to_forkchoice(state(), SignedBeaconBlock.t(), Types.root()) :: state() - defp send_block_to_forkchoice(state, signed_block, block_root) do - case ForkChoice.on_block(signed_block, block_root) do - :ok -> - state |> Map.update!(:pending_blocks, &Map.delete(&1, block_root)) - - :error -> - state - |> Map.update!(:pending_blocks, &Map.delete(&1, block_root)) - |> Map.update!( - :invalid_blocks, - &Map.put(&1, block_root, signed_block.message |> Map.take([:slot, :parent_root])) - ) - end - end - def schedule_blocks_processing do Process.send_after(__MODULE__, :process_blocks, 3000) end diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index 1edd58006..1a4480e3b 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -15,8 +15,6 @@ defmodule LambdaEthereumConsensus.ForkChoice do alias Types.SignedBeaconBlock alias Types.Store - @default_timeout 100_000 - ########################## ### Public API ########################## @@ -27,11 +25,6 @@ defmodule LambdaEthereumConsensus.ForkChoice do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end - @spec has_block?(Types.root()) :: boolean() - def has_block?(block_root) do - GenServer.call(__MODULE__, {:has_block?, block_root}, @default_timeout) - end - @spec on_tick(Types.uint64()) :: :ok def on_tick(time) do GenServer.cast(__MODULE__, {:on_tick, time}) @@ -39,7 +32,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do @spec on_block(Types.SignedBeaconBlock.t(), Types.root()) :: :ok | :error def on_block(signed_block, block_root) do - GenServer.call(__MODULE__, {:on_block, block_root, signed_block}, @default_timeout) + GenServer.cast(__MODULE__, {:on_block, block_root, signed_block, self()}) end @spec on_attestation(Types.Attestation.t()) :: :ok @@ -76,17 +69,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do end @impl GenServer - def handle_call({:get_store_attrs, attrs}, _from, state) do - values = Enum.map(attrs, &Map.fetch!(state, &1)) - {:reply, values, state} - end - - def handle_call({:has_block?, block_root}, _from, state) do - {:reply, Store.has_block?(state, block_root), state} - end - - @impl GenServer - def handle_call({:on_block, block_root, %SignedBeaconBlock{} = signed_block}, _from, store) do + def handle_cast({:on_block, block_root, %SignedBeaconBlock{} = signed_block, from}, store) do slot = signed_block.message.slot result = @@ -100,11 +83,14 @@ defmodule LambdaEthereumConsensus.ForkChoice do Logger.info("[Fork choice] New block added", slot: slot, root: block_root) Task.async(__MODULE__, :recompute_head, [new_store]) - {:reply, :ok, new_store} + + GenServer.cast(from, {:block_processed, signed_block, block_root, true}) + {:noreply, new_store} {:error, reason} -> Logger.error("[Fork choice] Failed to add block: #{reason}", slot: slot) - {:reply, :error, store} + GenServer.cast(from, {:block_processed, signed_block, block_root, false}) + {:noreply, store} end end diff --git a/test/unit/pending_blocks_test.exs b/test/unit/pending_blocks_test.exs index d8145cabd..97e2fbcf1 100644 --- a/test/unit/pending_blocks_test.exs +++ b/test/unit/pending_blocks_test.exs @@ -6,6 +6,7 @@ defmodule Unit.PendingBlocks do alias LambdaEthereumConsensus.Beacon.PendingBlocks alias LambdaEthereumConsensus.ForkChoice + alias LambdaEthereumConsensus.Store.Blocks alias LambdaEthereumConsensus.Store.BlockStore setup do @@ -23,7 +24,7 @@ defmodule Unit.PendingBlocks do signed_block = Fixtures.Block.signed_beacon_block() block_root = Ssz.hash_tree_root!(signed_block.message) - patch(ForkChoice, :has_block?, fn root -> root == signed_block.message.parent_root end) + patch(Blocks, :get_block, fn root -> root == signed_block.message.parent_root end) patch(ForkChoice, :on_block, fn _block, _root -> :ok end) # Don't store the block in the DB, to avoid having to set it up From 6b059cd4e33c2d9a48c151e844d773c29094062c Mon Sep 17 00:00:00 2001 From: Martin Paulucci Date: Mon, 5 Feb 2024 18:10:04 +0100 Subject: [PATCH 2/4] Refactor pending blocks state. --- .../beacon/pending_blocks.ex | 128 +++++++----------- test/unit/pending_blocks_test.exs | 41 ------ 2 files changed, 47 insertions(+), 122 deletions(-) delete mode 100644 test/unit/pending_blocks_test.exs diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 76f914316..db069e2ef 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -13,12 +13,8 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do alias LambdaEthereumConsensus.Store.Blocks alias Types.SignedBeaconBlock - @type state :: %{ - pending_blocks: %{Types.root() => SignedBeaconBlock.t()}, - invalid_blocks: %{Types.root() => map()}, - processing_blocks: MapSet.t(Types.root()), - blocks_to_download: MapSet.t(Types.root()) - } + @type block_status :: :pending | :invalid | :processing | :download | :unknown + @type state :: %{Types.root() => {SignedBeaconBlock.t() | nil, block_status()}} ########################## ### Public API @@ -33,11 +29,6 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do GenServer.cast(__MODULE__, {:add_block, signed_block}) end - @spec is_pending_block(Types.root()) :: boolean() - def is_pending_block(block_root) do - GenServer.call(__MODULE__, {:is_pending_block, block_root}) - end - ########################## ### GenServer Callbacks ########################## @@ -48,44 +39,28 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do schedule_blocks_processing() schedule_blocks_download() - {:ok, - %{ - pending_blocks: %{}, - invalid_blocks: %{}, - blocks_to_download: MapSet.new(), - processing_blocks: MapSet.new() - }} + {:ok, Map.new()} end @impl true def handle_cast({:add_block, %SignedBeaconBlock{message: block} = signed_block}, state) do block_root = Ssz.hash_tree_root!(block) - pending_blocks = Map.put(state.pending_blocks, block_root, signed_block) - {:noreply, Map.put(state, :pending_blocks, pending_blocks)} + {:noreply, Map.put(state, block_root, {signed_block, :pending})} end @impl true def handle_cast({:block_processed, signed_block, block_root, is_valid?}, state) do if is_valid? do - state |> Map.update!(:processing_blocks, &MapSet.delete(&1, block_root)) + state |> Map.delete(block_root) else state - |> Map.update!(:processing_blocks, &MapSet.delete(&1, block_root)) - |> Map.update!( - :invalid_blocks, - &Map.put(&1, block_root, signed_block.message |> Map.take([:slot, :parent_root])) - ) + |> Map.put(block_root, {signed_block, :invalid}) end |> then(fn state -> {:noreply, state} end) end - @impl true - def handle_call({:is_pending_block, block_root}, _from, state) do - {:reply, Map.has_key?(state.pending_blocks, block_root), state} - end - @spec handle_info(any(), state()) :: {:noreply, state()} @doc """ @@ -94,44 +69,39 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do @impl true @spec handle_info(atom(), state()) :: {:noreply, state()} def handle_info(:process_blocks, state) do - state.pending_blocks + state + |> Map.filter(fn {_, {_, s}} -> s == :pending end) + |> Enum.map(fn {root, {block, _}} -> {root, block} end) |> Enum.sort_by(fn {_, signed_block} -> signed_block.message.slot end) |> Enum.reduce(state, fn {block_root, signed_block}, state -> parent_root = signed_block.message.parent_root cond do + # If already processed, remove it + Blocks.get_block(block_root) -> + state |> Map.delete(block_root) + # If parent is invalid, block is invalid - state.invalid_blocks |> Map.has_key?(parent_root) -> - state - |> Map.update!(:pending_blocks, &Map.delete(&1, block_root)) - |> Map.update!( - :invalid_blocks, - &Map.put(&1, block_root, signed_block.message |> Map.take([:slot, :parent_root])) - ) + get_block_status(state, parent_root) == :invalid -> + state |> Map.put(block_root, :invalid) # If parent is processing, block is pending - state.processing_blocks |> MapSet.member?(parent_root) -> + get_block_status(state, parent_root) == :processing -> state # If parent is pending, block is pending - state.pending_blocks |> Map.has_key?(parent_root) -> + get_block_status(state, parent_root) == :pending -> state - # If already in fork choice, remove from pending - Blocks.get_block(block_root) -> - state |> Map.update!(:pending_blocks, &Map.delete(&1, block_root)) - - # If parent is not in fork choice, download parentx + # If parent is not in fork choice, download parent !Blocks.get_block(parent_root) -> - state |> Map.update!(:blocks_to_download, &MapSet.put(&1, parent_root)) + state |> Map.put(parent_root, {nil, :download}) # If all the other conditions are false, add block to fork choice true -> + Logger.info("Adding block to fork choice: ", root: block_root) ForkChoice.on_block(signed_block, block_root) - - state - |> Map.update!(:pending_blocks, &Map.delete(&1, block_root)) - |> Map.update!(:processing_blocks, &MapSet.put(&1, block_root)) + state |> Map.put(block_root, {signed_block, :processing}) end end) |> then(fn state -> @@ -140,51 +110,47 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end) end - @empty_mapset MapSet.new() - - @impl true - def handle_info(:download_blocks, %{blocks_to_download: to_download} = state) - when to_download == @empty_mapset do - schedule_blocks_download() - {:noreply, state} - end - @impl true def handle_info(:download_blocks, state) do - blocks_in_store = state.blocks_to_download |> MapSet.filter(&Blocks.get_block/1) + blocks_to_download = state |> Map.filter(fn {_, {_, s}} -> s == :download end) |> Map.keys() downloaded_blocks = - state.blocks_to_download - |> MapSet.difference(blocks_in_store) - |> Enum.take(16) - |> BlockDownloader.request_blocks_by_root() - |> case do - {:ok, signed_blocks} -> - signed_blocks - - {:error, reason} -> - Logger.debug("Block download failed: '#{reason}'") - [] + if Enum.empty?(blocks_to_download) do + [] + else + blocks_to_download + |> Enum.take(16) + |> BlockDownloader.request_blocks_by_root() + |> case do + {:ok, signed_blocks} -> + signed_blocks + + {:error, reason} -> + Logger.debug("Block download failed: '#{reason}'") + [] + end end - for signed_block <- downloaded_blocks do - add_block(signed_block) - end - - roots_to_remove = + new_state = downloaded_blocks - |> Enum.map(&Ssz.hash_tree_root!(&1.message)) - |> MapSet.new() - |> MapSet.union(blocks_in_store) + |> Enum.reduce(state, fn signed_block, state -> + block_root = Ssz.hash_tree_root!(signed_block.message) + state |> Map.put(block_root, {signed_block, :pending}) + end) schedule_blocks_download() - {:noreply, Map.update!(state, :blocks_to_download, &MapSet.difference(&1, roots_to_remove))} + {:noreply, new_state} end ########################## ### Private Functions ########################## + @spec get_block_status(state(), Types.root()) :: block_status() + defp get_block_status(state, block_root) do + state |> Map.get(block_root, {nil, :unknown}) |> elem(1) + end + def schedule_blocks_processing do Process.send_after(__MODULE__, :process_blocks, 3000) end diff --git a/test/unit/pending_blocks_test.exs b/test/unit/pending_blocks_test.exs deleted file mode 100644 index 97e2fbcf1..000000000 --- a/test/unit/pending_blocks_test.exs +++ /dev/null @@ -1,41 +0,0 @@ -defmodule Unit.PendingBlocks do - @moduledoc false - - use ExUnit.Case - use Patch - - alias LambdaEthereumConsensus.Beacon.PendingBlocks - alias LambdaEthereumConsensus.ForkChoice - alias LambdaEthereumConsensus.Store.Blocks - alias LambdaEthereumConsensus.Store.BlockStore - - setup do - Application.put_env(:lambda_ethereum_consensus, ChainSpec, config: MainnetConfig) - - # Lets trigger the process_blocks manually - patch(PendingBlocks, :schedule_blocks_processing, fn -> :ok end) - patch(PendingBlocks, :schedule_blocks_download, fn -> :ok end) - - start_supervised!({PendingBlocks, []}) - :ok - end - - test "Adds a pending block to fork choice if the parent is there" do - signed_block = Fixtures.Block.signed_beacon_block() - block_root = Ssz.hash_tree_root!(signed_block.message) - - patch(Blocks, :get_block, fn root -> root == signed_block.message.parent_root end) - patch(ForkChoice, :on_block, fn _block, _root -> :ok end) - - # Don't store the block in the DB, to avoid having to set it up - patch(BlockStore, :store_block, fn _block -> :ok end) - - PendingBlocks.add_block(signed_block) - - assert PendingBlocks.is_pending_block(block_root) - send(PendingBlocks, :process_blocks) - - # If the block is not pending anymore, it means it was added to the fork choice - assert not PendingBlocks.is_pending_block(block_root) - end -end From 6c8045620068c5f0f3632e43e3ae00bc517cd752 Mon Sep 17 00:00:00 2001 From: Martin Paulucci Date: Mon, 5 Feb 2024 18:24:38 +0100 Subject: [PATCH 3/4] Dont pass signed block. --- lib/lambda_ethereum_consensus/beacon/pending_blocks.ex | 8 +++++--- lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index db069e2ef..74f4c2f2b 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -42,6 +42,8 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do {:ok, Map.new()} end + @spec handle_cast(any(), state()) :: {:noreply, state()} + @impl true def handle_cast({:add_block, %SignedBeaconBlock{message: block} = signed_block}, state) do block_root = Ssz.hash_tree_root!(block) @@ -49,12 +51,12 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end @impl true - def handle_cast({:block_processed, signed_block, block_root, is_valid?}, state) do + def handle_cast({:block_processed, block_root, is_valid?}, state) do if is_valid? do state |> Map.delete(block_root) else state - |> Map.put(block_root, {signed_block, :invalid}) + |> Map.put(block_root, {nil, :invalid}) end |> then(fn state -> {:noreply, state} @@ -83,7 +85,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do # If parent is invalid, block is invalid get_block_status(state, parent_root) == :invalid -> - state |> Map.put(block_root, :invalid) + state |> Map.put(block_root, {nil, :invalid}) # If parent is processing, block is pending get_block_status(state, parent_root) == :processing -> diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index 1a4480e3b..494e1d009 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -84,12 +84,12 @@ defmodule LambdaEthereumConsensus.ForkChoice do Task.async(__MODULE__, :recompute_head, [new_store]) - GenServer.cast(from, {:block_processed, signed_block, block_root, true}) + GenServer.cast(from, {:block_processed, block_root, true}) {:noreply, new_store} {:error, reason} -> Logger.error("[Fork choice] Failed to add block: #{reason}", slot: slot) - GenServer.cast(from, {:block_processed, signed_block, block_root, false}) + GenServer.cast(from, {:block_processed, block_root, false}) {:noreply, store} end end From 9cb07523fb65d2a46ca7398141022fcfbb6646b3 Mon Sep 17 00:00:00 2001 From: Martin Paulucci Date: Thu, 8 Feb 2024 17:04:53 +0100 Subject: [PATCH 4/4] Changes per CR. --- .../beacon/pending_blocks.ex | 38 ++++++++++--------- .../p2p/block_downloader.ex | 8 +++- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 74f4c2f2b..537c7db73 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -47,7 +47,12 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do @impl true def handle_cast({:add_block, %SignedBeaconBlock{message: block} = signed_block}, state) do block_root = Ssz.hash_tree_root!(block) - {:noreply, Map.put(state, block_root, {signed_block, :pending})} + + if state |> Map.get(block_root) do + {:noreply, state} + else + {:noreply, state |> Map.put(block_root, {signed_block, :pending})} + end end @impl true @@ -77,6 +82,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do |> Enum.sort_by(fn {_, signed_block} -> signed_block.message.slot end) |> Enum.reduce(state, fn {block_root, signed_block}, state -> parent_root = signed_block.message.parent_root + parent_status = get_block_status(state, parent_root) cond do # If already processed, remove it @@ -84,15 +90,15 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do state |> Map.delete(block_root) # If parent is invalid, block is invalid - get_block_status(state, parent_root) == :invalid -> + parent_status == :invalid -> state |> Map.put(block_root, {nil, :invalid}) # If parent is processing, block is pending - get_block_status(state, parent_root) == :processing -> + parent_status == :processing -> state # If parent is pending, block is pending - get_block_status(state, parent_root) == :pending -> + parent_status == :pending -> state # If parent is not in fork choice, download parent @@ -117,20 +123,16 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do blocks_to_download = state |> Map.filter(fn {_, {_, s}} -> s == :download end) |> Map.keys() downloaded_blocks = - if Enum.empty?(blocks_to_download) do - [] - else - blocks_to_download - |> Enum.take(16) - |> BlockDownloader.request_blocks_by_root() - |> case do - {:ok, signed_blocks} -> - signed_blocks - - {:error, reason} -> - Logger.debug("Block download failed: '#{reason}'") - [] - end + blocks_to_download + |> Enum.take(16) + |> BlockDownloader.request_blocks_by_root() + |> case do + {:ok, signed_blocks} -> + signed_blocks + + {:error, reason} -> + Logger.debug("Block download failed: '#{reason}'") + [] end new_state = diff --git a/lib/lambda_ethereum_consensus/p2p/block_downloader.ex b/lib/lambda_ethereum_consensus/p2p/block_downloader.ex index af035471f..5ed2caea0 100644 --- a/lib/lambda_ethereum_consensus/p2p/block_downloader.ex +++ b/lib/lambda_ethereum_consensus/p2p/block_downloader.ex @@ -79,7 +79,13 @@ defmodule LambdaEthereumConsensus.P2P.BlockDownloader do @spec request_blocks_by_root([Types.root()], integer()) :: {:ok, [Types.SignedBeaconBlock.t()]} | {:error, binary()} - def request_blocks_by_root(roots, retries \\ @default_retries) do + def request_blocks_by_root(roots, retries \\ @default_retries) + + def request_blocks_by_root([], _retries) do + {:ok, []} + end + + def request_blocks_by_root(roots, retries) do Logger.debug("Requesting block for roots #{Enum.map_join(roots, ", ", &Base.encode16/1)}") peer_id = get_some_peer()