From 680447d067fa808b40fcbb32301972ed9e566876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 3 Jun 2024 18:27:50 +0200 Subject: [PATCH 01/37] remove pending blocks state. It compiles, but get_blocks_with_status still needs to be written --- .../beacon/pending_blocks.ex | 192 +++++++++--------- .../fork_choice/fork_choice.ex | 7 +- .../store/block_db.ex | 75 ++++--- lib/lambda_ethereum_consensus/store/blocks.ex | 12 ++ 4 files changed, 155 insertions(+), 131 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 488b84806..812039409 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -20,7 +20,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do @type block_info :: {SignedBeaconBlock.t(), :pending | :download_blobs} | {nil, :invalid | :processing | :download} - @type state :: %{Types.root() => block_info()} + @type state :: nil ########################## ### Public API @@ -35,6 +35,16 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do GenServer.cast(__MODULE__, {:add_block, signed_block}) end + @spec notify_block_transitioned(BlockInfo.t()) :: :ok + def notify_block_transitioned(block_info) do + GenServer.cast(__MODULE__, {:block_transitioned, block_info}) + end + + @spec notify_block_transition_failed(BlockInfo.t()) :: :ok + def notify_block_transition_failed(block_info) do + GenServer.cast(__MODULE__, {:block_transition_failed, block_info}) + end + ########################## ### GenServer Callbacks ########################## @@ -46,40 +56,44 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do schedule_blocks_download() schedule_blobs_download() - {:ok, Map.new()} + {:ok, nil} 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) + def handle_cast({:add_block, %SignedBeaconBlock{} = signed_block}, _state) do + block_info = BlockInfo.from_block(signed_block) cond do # If already processing or processed, ignore it - Map.has_key?(state, block_root) or Blocks.has_block?(block_root) -> - state + Blocks.has_block?(block_info.root) -> + :ok - blocks_to_missing_blobs([{block_root, signed_block}]) |> Enum.empty?() -> - state |> Map.put(block_root, {signed_block, :pending}) + Enum.empty?(missing_blobs(block_info)) -> + Blocks.store_block_info(block_info) true -> - state |> Map.put(block_root, {signed_block, :download_blobs}) + block_info + |> BlockInfo.change_status(:download_blobs) + |> Blocks.store_block_info() end - |> then(&{:noreply, &1}) + + {:noreply, nil} end @impl true - def handle_cast({:block_processed, block_root, true}, state) do + def handle_cast({:block_transitioned, block_info}, _state) do # Block is valid. We immediately check if we can process another block. - new_state = state |> Map.delete(block_root) |> process_blocks() - {:noreply, new_state} + Blocks.change_status(block_info, :transitioned) + process_blocks() + {:noreply, nil} end @impl true - def handle_cast({:block_processed, block_root, false}, state) do - # Block is invalid - {:noreply, state |> Map.put(block_root, {nil, :invalid})} + def handle_cast({:block_transition_failed, block_info}, _state) do + Blocks.change_status(block_info, :invalid) + {:noreply, nil} end @doc """ @@ -87,48 +101,44 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do """ @impl true @spec handle_info(atom(), state()) :: {:noreply, state()} - def handle_info(:process_blocks, state) do + def handle_info(:process_blocks, _state) do schedule_blocks_processing() - {:noreply, process_blocks(state)} + {:noreply, process_blocks()} end @impl true - def handle_info(:download_blocks, state) do - blocks_to_download = state |> Map.filter(fn {_, {_, s}} -> s == :download end) |> Map.keys() - - downloaded_blocks = - 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 = - downloaded_blocks - |> Enum.reduce(state, fn signed_block, state -> - block_root = Ssz.hash_tree_root!(signed_block.message) - state |> Map.put(block_root, {signed_block, :download_blobs}) - end) + def handle_info(:download_blocks, _state) do + Blocks.get_blocks_with_status(:download) + |> Enum.take(16) + |> Enum.map(& &1.root) + |> BlockDownloader.request_blocks_by_root() + |> case do + {:ok, signed_blocks} -> + signed_blocks + + {:error, reason} -> + Logger.debug("Block download failed: '#{reason}'") + [] + end + |> Enum.each(fn signed_block -> + signed_block + |> BlockInfo.from_block() + |> BlockInfo.change_status(:download_blobs) + |> Blocks.store_block_info() + end) schedule_blocks_download() - {:noreply, new_state} + {:noreply, nil} end @impl true - def handle_info(:download_blobs, state) do + def handle_info(:download_blobs, _state) do blocks_with_blobs = - Stream.filter(state, fn {_, {_, s}} -> s == :download_blobs end) - |> Enum.sort_by(fn {_, {signed_block, _}} -> signed_block.message.slot end) - |> Stream.map(fn {root, {block, _}} -> {root, block} end) + Blocks.get_blocks_with_status(:download_blobs) + |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) |> Enum.take(16) - blobs_to_download = blocks_to_missing_blobs(blocks_with_blobs) + blobs_to_download = Enum.flat_map(blocks_with_blobs, &missing_blobs/1) downloaded_blobs = blobs_to_download @@ -144,68 +154,52 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do Enum.each(downloaded_blobs, &BlobDb.store_blob/1) - new_state = - if length(downloaded_blobs) == length(blobs_to_download) do - blocks_with_blobs - |> Enum.reduce(state, fn {block_root, signed_block}, state -> - state |> Map.put(block_root, {signed_block, :pending}) - end) - else - state - end + # TODO: is it not possible that blobs were downloaded for one and not for another? + if length(downloaded_blobs) == length(blobs_to_download) do + Enum.each(blocks_with_blobs, fn block_info -> Blocks.change_status(block_info, :pending) end) + end schedule_blobs_download() - {:noreply, new_state} + {:noreply, nil} end ########################## ### Private Functions ########################## - defp process_blocks(state) do - state - |> Enum.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 -> - block_info = BlockInfo.from_block(signed_block, block_root, :pending) - - parent_root = signed_block.message.parent_root - parent_status = get_block_status(state, parent_root) - - cond do - # If parent is invalid, block is invalid - parent_status == :invalid -> - state |> Map.put(block_root, {nil, :invalid}) - - # If parent isn't processed, block is pending - parent_status in [:processing, :pending, :download, :download_blobs] -> - state - - # If parent is not in fork choice, download parent - not Blocks.has_block?(parent_root) -> - state |> Map.put(parent_root, {nil, :download}) - - # If all the other conditions are false, add block to fork choice - true -> - ForkChoice.on_block(block_info) - state |> Map.put(block_root, {signed_block, :processing}) - end - end) + defp process_blocks() do + Blocks.get_blocks_with_status(:pending) + |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) + |> Enum.each(&process_block/1) end - @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) + defp process_block(block_info) do + parent_root = block_info.signed_block.message.parent_root + parent = Blocks.get_block_info(parent_root) + + cond do + is_nil(parent) -> + # TODO: add parent root to download list instead. + %BlockInfo{root: parent_root, status: :download, signed_block: nil} + |> Blocks.store_block_info() + + # If parent is invalid, block is invalid + parent.status == :invalid -> + Blocks.change_status(block_info, :invalid) + + # If all the other conditions are false, add block to fork choice + parent.status == :transitioned -> + Blocks.change_status(block_info, :processing) + ForkChoice.on_block(block_info) + end end - defp blocks_to_missing_blobs(blocks) do - Enum.flat_map(blocks, fn {block_root, - %{message: %{body: %{blob_kzg_commitments: commitments}}}} -> - Stream.with_index(commitments) - |> Enum.filter(&blob_needs_download?(&1, block_root)) - |> Enum.map(&%Types.BlobIdentifier{block_root: block_root, index: elem(&1, 1)}) - end) + @spec missing_blobs(BlockInfo.t()) :: [Types.BlobIdentifier.t()] + defp missing_blobs(%BlockInfo{root: root, signed_block: signed_block}) do + signed_block.message.body.blob_kzg_commitments + |> Stream.with_index() + |> Enum.filter(&blob_needs_download?(&1, root)) + |> Enum.map(&%Types.BlobIdentifier{block_root: root, index: elem(&1, 1)}) end defp blob_needs_download?({commitment, index}, block_root) do @@ -218,15 +212,15 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end end - def schedule_blocks_processing() do + defp schedule_blocks_processing() do Process.send_after(__MODULE__, :process_blocks, 500) end - def schedule_blobs_download() do + defp schedule_blobs_download() do Process.send_after(__MODULE__, :download_blobs, 500) end - def schedule_blocks_download() do + defp schedule_blocks_download() do Process.send_after(__MODULE__, :download_blocks, 1000) end end diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index e0018a3ac..cea0b752f 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -7,6 +7,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do require Logger alias LambdaEthereumConsensus.Beacon.BeaconChain + alias LambdaEthereumConsensus.Beacon.PendingBlocks alias LambdaEthereumConsensus.Execution.ExecutionChain alias LambdaEthereumConsensus.ForkChoice.Handlers alias LambdaEthereumConsensus.ForkChoice.Head @@ -70,7 +71,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do end @impl GenServer - def handle_cast({:on_block, %BlockInfo{} = block_info, from}, store) do + def handle_cast({:on_block, %BlockInfo{} = block_info}, store) do slot = block_info.signed_block.message.slot block_root = block_info.root @@ -94,12 +95,12 @@ defmodule LambdaEthereumConsensus.ForkChoice do prune_old_states(last_finalized_checkpoint.epoch, new_finalized_checkpoint.epoch) - GenServer.cast(from, {:block_processed, block_root, true}) + PendingBlocks.notify_block_transitioned(block_info) {:noreply, new_store} {:error, reason} -> Logger.error("[Fork choice] Failed to add block: #{reason}", slot: slot, root: block_root) - GenServer.cast(from, {:block_processed, block_root, false}) + PendingBlocks.notify_block_transition_failed(block_info) {:noreply, store} end end diff --git a/lib/lambda_ethereum_consensus/store/block_db.ex b/lib/lambda_ethereum_consensus/store/block_db.ex index d1e6bce91..755feccdb 100644 --- a/lib/lambda_ethereum_consensus/store/block_db.ex +++ b/lib/lambda_ethereum_consensus/store/block_db.ex @@ -26,11 +26,22 @@ defmodule LambdaEthereumConsensus.Store.BlockDb do @type t :: %__MODULE__{ root: Types.root(), - signed_block: Types.SignedBeaconBlock.t(), + signed_block: Types.SignedBeaconBlock.t() | nil, status: block_status() } defstruct [:root, :signed_block, :status] + defguard is_status(atom) + when atom in [ + :pending, + :invalid, + :processing, + :download, + :download_blobs, + :unknown, + :transitioned + ] + @spec from_block(SignedBeaconBlock.t(), block_status()) :: t() def from_block(signed_block, status \\ :pending) do {:ok, root} = Ssz.hash_tree_root(signed_block.message) @@ -41,39 +52,55 @@ defmodule LambdaEthereumConsensus.Store.BlockDb do def from_block(signed_block, root, status) do %__MODULE__{root: root, signed_block: signed_block, status: status} end - end - defguard is_status(atom) - when atom in [ - :pending, - :invalid, - :processing, - :download, - :download_blobs, - :unknown, - :transitioned - ] + def change_status(%__MODULE__{} = block_info, new_status) when is_status(new_status) do + %__MODULE__{block_info | status: new_status} + end - def store_block_info(%BlockInfo{} = block_info) do - {:ok, encoded_signed_block} = Ssz.to_ssz(block_info.signed_block) + def encode(%__MODULE__{} = block_info) do + with {:ok, encoded_signed_block} <- Ssz.to_ssz(block_info.signed_block) do + :erlang.term_to_binary({encoded_signed_block, block_info.status}) + end + end + + def decode(block_root, data) do + with {:ok, {encoded_signed_block, status}} <- validate_term(:erlang.binary_to_term(data)), + {:ok, signed_block} <- Ssz.from_ssz(encoded_signed_block, SignedBeaconBlock) do + {:ok, %BlockInfo{root: block_root, signed_block: signed_block, status: status}} + end + end + + # Validates a term that came out of the first decoding step for a stored block info tuple. + defp validate_term({encoded_signed_block, status}) + when is_binary(encoded_signed_block) and is_status(status) do + {:ok, {encoded_signed_block, status}} + end + defp validate_term(other) do + {:error, "Block decoding failed, decoded term is not the expected tuple: #{other}"} + end + end + + def store_block_info(%BlockInfo{} = block_info) do + # TODO handle encoding errors properly. + {:ok, encoded} = BlockInfo.encode(block_info) key = block_key(block_info.root) - Db.put(key, :erlang.term_to_binary({encoded_signed_block, block_info.status})) + Db.put(key, encoded) # WARN: this overrides any previous mapping for the same slot # TODO: this should apply fork-choice if not applied elsewhere # TODO: handle cases where slot is empty slothash_key = block_root_by_slot_key(block_info.signed_block.message.slot) Db.put(slothash_key, block_info.root) + + # Here we will also add a status list. end @spec get_block_info(Types.root()) :: {:ok, BlockInfo.t()} | {:error, String.t()} | :not_found def get_block_info(block_root) do - with {:ok, data} <- Db.get(block_key(block_root)), - {:ok, {encoded_signed_block, status}} <- :erlang.binary_to_term(data) |> validate_term(), - {:ok, signed_block} <- Ssz.from_ssz(encoded_signed_block, SignedBeaconBlock) do - {:ok, %BlockInfo{root: block_root, signed_block: signed_block, status: status}} + with {:ok, data} <- Db.get(block_key(block_root)) do + BlockInfo.decode(block_root, data) end end @@ -115,16 +142,6 @@ defmodule LambdaEthereumConsensus.Store.BlockDb do Logger.info("[BlockDb] Pruning finished. #{Enum.count(slots_to_remove)} blocks removed.") end - # Validates a term that came out of the first decoding step for a stored block info tuple. - defp validate_term({encoded_signed_block, status}) - when is_binary(encoded_signed_block) and is_status(status) do - {:ok, {encoded_signed_block, status}} - end - - defp validate_term(other) do - {:error, "Block decoding failed, decoded term is not the expected tuple: #{other}"} - end - @spec remove_block_by_slot(non_neg_integer()) :: :ok | :not_found defp remove_block_by_slot(slot) do slothash_key = block_root_by_slot_key(slot) diff --git a/lib/lambda_ethereum_consensus/store/blocks.ex b/lib/lambda_ethereum_consensus/store/blocks.ex index f728c8e7c..5d69da73f 100644 --- a/lib/lambda_ethereum_consensus/store/blocks.ex +++ b/lib/lambda_ethereum_consensus/store/blocks.ex @@ -66,6 +66,18 @@ defmodule LambdaEthereumConsensus.Store.Blocks do end end + @spec change_status(BlockInfo.t(), BlockInfo.status()) :: :ok + def change_status(block_info, status) do + block_info + |> BlockInfo.change_status(status) + |> store_block_info() + end + + def get_blocks_with_status(_status) do + # TODO + [] + end + ########################## ### Private Functions ########################## From e34441e0e1d9bc7ededcd65aa1d42daee51174de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 3 Jun 2024 18:37:14 +0200 Subject: [PATCH 02/37] fix dialyzer --- lib/lambda_ethereum_consensus/store/block_db.ex | 6 +++++- lib/lambda_ethereum_consensus/store/blocks.ex | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/lambda_ethereum_consensus/store/block_db.ex b/lib/lambda_ethereum_consensus/store/block_db.ex index 755feccdb..43818660e 100644 --- a/lib/lambda_ethereum_consensus/store/block_db.ex +++ b/lib/lambda_ethereum_consensus/store/block_db.ex @@ -53,16 +53,19 @@ defmodule LambdaEthereumConsensus.Store.BlockDb do %__MODULE__{root: root, signed_block: signed_block, status: status} end + @spec change_status(t(), block_status()) :: t() def change_status(%__MODULE__{} = block_info, new_status) when is_status(new_status) do %__MODULE__{block_info | status: new_status} end + @spec encode(t()) :: {:ok, binary()} | {:error, binary()} def encode(%__MODULE__{} = block_info) do with {:ok, encoded_signed_block} <- Ssz.to_ssz(block_info.signed_block) do - :erlang.term_to_binary({encoded_signed_block, block_info.status}) + {:ok, :erlang.term_to_binary({encoded_signed_block, block_info.status})} end end + @spec decode(Types.root(), binary()) :: {:error, binary()} | {:ok, t()} def decode(block_root, data) do with {:ok, {encoded_signed_block, status}} <- validate_term(:erlang.binary_to_term(data)), {:ok, signed_block} <- Ssz.from_ssz(encoded_signed_block, SignedBeaconBlock) do @@ -81,6 +84,7 @@ defmodule LambdaEthereumConsensus.Store.BlockDb do end end + @spec store_block_info(BlockInfo.t()) :: :ok def store_block_info(%BlockInfo{} = block_info) do # TODO handle encoding errors properly. {:ok, encoded} = BlockInfo.encode(block_info) diff --git a/lib/lambda_ethereum_consensus/store/blocks.ex b/lib/lambda_ethereum_consensus/store/blocks.ex index 5d69da73f..d353de655 100644 --- a/lib/lambda_ethereum_consensus/store/blocks.ex +++ b/lib/lambda_ethereum_consensus/store/blocks.ex @@ -66,13 +66,14 @@ defmodule LambdaEthereumConsensus.Store.Blocks do end end - @spec change_status(BlockInfo.t(), BlockInfo.status()) :: :ok + @spec change_status(BlockInfo.t(), BlockInfo.block_status()) :: :ok def change_status(block_info, status) do block_info |> BlockInfo.change_status(status) |> store_block_info() end + @spec get_blocks_with_status(BlockInfo.block_status()) :: [BlockInfo.t()] def get_blocks_with_status(_status) do # TODO [] From 4e2a163c53f29d0f9d233d2dee3cebebb9536497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 3 Jun 2024 18:40:06 +0200 Subject: [PATCH 03/37] readd underscore --- lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index f66c6ea03..4562d6218 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -72,7 +72,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do end @impl GenServer - def handle_cast({:on_block, %BlockInfo{} = block_info, from}, _store) do + def handle_cast({:on_block, %BlockInfo{} = block_info, _from}, _store) do store = fetch_store!() slot = block_info.signed_block.message.slot block_root = block_info.root From 6f094b327b626c2d626836856344c9eb4139d869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 3 Jun 2024 19:57:14 +0200 Subject: [PATCH 04/37] add status lists --- .../store/block_db.ex | 39 +++++++++++++++++++ lib/lambda_ethereum_consensus/store/blocks.ex | 32 +++++++++++++-- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/lib/lambda_ethereum_consensus/store/block_db.ex b/lib/lambda_ethereum_consensus/store/block_db.ex index 43818660e..7ed8e817f 100644 --- a/lib/lambda_ethereum_consensus/store/block_db.ex +++ b/lib/lambda_ethereum_consensus/store/block_db.ex @@ -9,6 +9,7 @@ defmodule LambdaEthereumConsensus.Store.BlockDb do @block_prefix "blockHash" @blockslot_prefix "blockSlot" + @block_status_prefix "blockStatus" defmodule BlockInfo do @moduledoc """ @@ -129,6 +130,42 @@ defmodule LambdaEthereumConsensus.Store.BlockDb do end end + @spec remove_root_from_status(Types.root(), BlockInfo.block_status()) :: :ok + def remove_root_from_status(root, status) do + get_roots_with_status(status) + |> MapSet.delete(root) + |> store_roots_with_status(status) + end + + @spec add_root_to_status(Types.root(), BlockInfo.block_status()) :: :ok + def add_root_to_status(root, status) do + get_roots_with_status(status) + |> MapSet.put(root) + |> store_roots_with_status(status) + end + + def change_root_status(root, from_status, to_status) do + remove_root_from_status(root, from_status) + add_root_to_status(root, to_status) + + # TODO: if we need to perform some level of db recovery, we probably should consider the + # blocks db as the source of truth and reconstruct the status ones. Either that or + # perform an ACID-like transaction. + end + + @spec store_roots_with_status(MapSet.t(Types.root()), BlockInfo.block_status()) :: :ok + defp store_roots_with_status(block_roots, status) do + Db.put(block_status_key(status), :erlang.term_to_binary(block_roots)) + end + + @spec get_roots_with_status(BlockInfo.block_status()) :: MapSet.t(Types.root()) + def get_roots_with_status(status) do + case Db.get(block_status_key(status)) do + {:ok, binary} -> :erlang.binary_to_term(binary) + :not_found -> [] + end + end + @spec prune_blocks_older_than(non_neg_integer()) :: :ok | {:error, String.t()} | :not_found def prune_blocks_older_than(slot) do Logger.info("[BlockDb] Pruning started.", slot: slot) @@ -184,4 +221,6 @@ defmodule LambdaEthereumConsensus.Store.BlockDb do defp block_key(root), do: Utils.get_key(@block_prefix, root) defp block_root_by_slot_key(slot), do: Utils.get_key(@blockslot_prefix, slot) + + defp block_status_key(status), do: Utils.get_key(@block_status_prefix, Atom.to_string(status)) end diff --git a/lib/lambda_ethereum_consensus/store/blocks.ex b/lib/lambda_ethereum_consensus/store/blocks.ex index d353de655..e1ba7d8cd 100644 --- a/lib/lambda_ethereum_consensus/store/blocks.ex +++ b/lib/lambda_ethereum_consensus/store/blocks.ex @@ -32,6 +32,7 @@ defmodule LambdaEthereumConsensus.Store.Blocks do } end + # TODO: make private. @spec store_block_info(BlockInfo.t()) :: :ok def store_block_info(block_info) do LRUCache.put(@table, block_info.root, block_info) @@ -66,17 +67,40 @@ defmodule LambdaEthereumConsensus.Store.Blocks do end end + @spec new_block_info(BlockInfo.t()) :: :ok + def new_block_info(block_info) do + store_block_info(block_info) + BlockDb.add_root_to_status(block_info.root, block_info.status) + end + @spec change_status(BlockInfo.t(), BlockInfo.block_status()) :: :ok def change_status(block_info, status) do + old_status = block_info.status + block_info |> BlockInfo.change_status(status) |> store_block_info() + + BlockDb.change_root_status(block_info.root, old_status, status) end - @spec get_blocks_with_status(BlockInfo.block_status()) :: [BlockInfo.t()] - def get_blocks_with_status(_status) do - # TODO - [] + @spec get_blocks_with_status(BlockInfo.block_status()) :: + {:ok, [BlockInfo.t()]} | {:error, binary()} + def get_blocks_with_status(status) do + BlockDb.get_roots_with_status(status) + |> Enum.reduce_while([], fn root, acc -> + case get_block_info(root) do + nil -> {:halt, root} + block_info -> {:cont, [block_info | acc]} + end + end) + |> case do + block_info when is_list(block_info) -> + {:ok, Enum.reverse(block_info)} + + root -> + {:error, "Error getting blocks with status #{status}. Block with root #{root} not found."} + end end ########################## From 7d261c7865647ed2defc999e84124ed96c3145c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 4 Jun 2024 14:38:38 +0200 Subject: [PATCH 05/37] only use blocks wrappers --- lib/lambda_ethereum_consensus/beacon/pending_blocks.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 812039409..2298e1b12 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -71,12 +71,12 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do :ok Enum.empty?(missing_blobs(block_info)) -> - Blocks.store_block_info(block_info) + Blocks.new_block_info(block_info) true -> block_info |> BlockInfo.change_status(:download_blobs) - |> Blocks.store_block_info() + |> Blocks.new_block_info() end {:noreply, nil} @@ -124,7 +124,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do signed_block |> BlockInfo.from_block() |> BlockInfo.change_status(:download_blobs) - |> Blocks.store_block_info() + |> Blocks.new_block_info() end) schedule_blocks_download() @@ -181,7 +181,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do is_nil(parent) -> # TODO: add parent root to download list instead. %BlockInfo{root: parent_root, status: :download, signed_block: nil} - |> Blocks.store_block_info() + |> Blocks.new_block_info() # If parent is invalid, block is invalid parent.status == :invalid -> From 71100ab624a54dae9cbbddd8c40d21fc3267f72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 4 Jun 2024 16:11:24 +0200 Subject: [PATCH 06/37] Add tests and fix the cache to enable block updates --- .../store/block_db.ex | 2 +- .../store/lru_cache.ex | 2 +- test/unit/blocks.exs | 44 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 test/unit/blocks.exs diff --git a/lib/lambda_ethereum_consensus/store/block_db.ex b/lib/lambda_ethereum_consensus/store/block_db.ex index 7ed8e817f..0b826bacc 100644 --- a/lib/lambda_ethereum_consensus/store/block_db.ex +++ b/lib/lambda_ethereum_consensus/store/block_db.ex @@ -162,7 +162,7 @@ defmodule LambdaEthereumConsensus.Store.BlockDb do def get_roots_with_status(status) do case Db.get(block_status_key(status)) do {:ok, binary} -> :erlang.binary_to_term(binary) - :not_found -> [] + :not_found -> MapSet.new([]) end end diff --git a/lib/lambda_ethereum_consensus/store/lru_cache.ex b/lib/lambda_ethereum_consensus/store/lru_cache.ex index e82011e98..3538c48be 100644 --- a/lib/lambda_ethereum_consensus/store/lru_cache.ex +++ b/lib/lambda_ethereum_consensus/store/lru_cache.ex @@ -106,7 +106,7 @@ defmodule LambdaEthereumConsensus.Store.LRUCache do end defp cache_value(table, key, value) do - :ets.insert_new(table, {key, value, nil}) + :ets.insert(table, {key, value, nil}) GenServer.cast(table, {:touch_entry, key}) value end diff --git a/test/unit/blocks.exs b/test/unit/blocks.exs new file mode 100644 index 000000000..575d5bd26 --- /dev/null +++ b/test/unit/blocks.exs @@ -0,0 +1,44 @@ +defmodule BlocksTest do + use ExUnit.Case + alias Fixtures.Block + alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo + alias LambdaEthereumConsensus.Store.Blocks + alias Types.BeaconBlock + + setup %{tmp_dir: tmp_dir} do + start_link_supervised!({LambdaEthereumConsensus.Store.Db, dir: tmp_dir}) + start_link_supervised!(LambdaEthereumConsensus.Store.Blocks) + :ok + end + + @tag :tmp_dir + test "Block info construction correctly calculates the root." do + block_info = BlockInfo.from_block(Block.signed_beacon_block()) + + assert {:ok, block_info.root} == + Ssz.hash_tree_root(block_info.signed_block.message, BeaconBlock) + end + + @tag :tmp_dir + test "Basic block saving and loading" do + block_info = BlockInfo.from_block(Block.signed_beacon_block()) + assert Blocks.get_block(block_info.root) == nil + Blocks.new_block_info(block_info) + assert block_info == Blocks.get_block_info(block_info.root) + end + + @tag :tmp_dir + test "Status is updated correctly when changing the status" do + assert {:ok, []} == Blocks.get_blocks_with_status(:pending) + block_info = BlockInfo.from_block(Block.signed_beacon_block()) + + Blocks.new_block_info(block_info) + assert {:ok, [block_info]} == Blocks.get_blocks_with_status(:pending) + + Blocks.change_status(block_info, :invalid) + expected_block = BlockInfo.change_status(block_info, :invalid) + assert {:ok, []} == Blocks.get_blocks_with_status(:pending) + assert Blocks.get_block_info(block_info.root) == expected_block + assert {:ok, [expected_block]} == Blocks.get_blocks_with_status(:invalid) + end +end From a15f343686e7900dd361233abcbf9576203b6b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 4 Jun 2024 16:32:06 +0200 Subject: [PATCH 07/37] simplify pending blocks code for add_block --- .../beacon/pending_blocks.ex | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 2298e1b12..053570eb3 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -65,18 +65,14 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do def handle_cast({:add_block, %SignedBeaconBlock{} = signed_block}, _state) do block_info = BlockInfo.from_block(signed_block) - cond do - # If already processing or processed, ignore it - Blocks.has_block?(block_info.root) -> - :ok - - Enum.empty?(missing_blobs(block_info)) -> - Blocks.new_block_info(block_info) - - true -> + # If already processing or processed, ignore it + if not Blocks.has_block?(block_info.root) do + if Enum.empty?(missing_blobs(block_info)) do block_info - |> BlockInfo.change_status(:download_blobs) - |> Blocks.new_block_info() + else + block_info |> BlockInfo.change_status(:download_blobs) + end + |> Blocks.new_block_info() end {:noreply, nil} From 5f486ea78cb8be7ffe2be0d6446c4637406c9578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 4 Jun 2024 17:19:40 +0200 Subject: [PATCH 08/37] fix fork choice call --- .../beacon/pending_blocks.ex | 49 ++++++------------- 1 file changed, 16 insertions(+), 33 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 469911eee..5915a9991 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -86,7 +86,8 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do @spec handle_info(atom(), state()) :: {:noreply, state()} def handle_info(:process_blocks, _state) do schedule_blocks_processing() - {:noreply, process_blocks()} + process_blocks() + {:noreply, nil} end @impl true @@ -172,38 +173,20 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do # If all the other conditions are false, add block to fork choice parent.status == :transitioned -> - Blocks.change_status(block_info, :processing) - ForkChoice.on_block(block_info) - end - end - - defp process_block(state, block_info) do - case ForkChoice.on_block(block_info) do - :ok -> - state |> Map.delete(block_info.root) - - {:error, reason} -> - Logger.error("[PendingBlocks] Saving block as invalid #{reason}", - slot: block_info.signed_block.message.slot, - root: block_info.root - ) - - state |> Map.put(block_info.root, {nil, :invalid}) - end - end - - defp process_block(state, block_info) do - case ForkChoice.on_block(block_info) do - :ok -> - state |> Map.delete(block_info.root) - - {:error, reason} -> - Logger.error("[PendingBlocks] Saving block as invalid #{reason}", - slot: block_info.signed_block.message.slot, - root: block_info.root - ) - - state |> Map.put(block_info.root, {nil, :invalid}) + case ForkChoice.on_block(block_info) do + :ok -> + Blocks.change_status(block_info, :transitioned) + # Block is valid. We immediately check if we can process another block. + process_blocks() + + {:error, reason} -> + Logger.error("[PendingBlocks] Saving block as invalid #{reason}", + slot: block_info.signed_block.message.slot, + root: block_info.root + ) + + Blocks.change_status(block_info, :invalid) + end end end From a98d2893d4bcd207f430a1957d680b0811649439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 4 Jun 2024 17:28:07 +0200 Subject: [PATCH 09/37] remove unused alias --- lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index f637a7f6f..3d492779a 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -6,7 +6,6 @@ defmodule LambdaEthereumConsensus.ForkChoice do require Logger alias LambdaEthereumConsensus.Beacon.BeaconChain - alias LambdaEthereumConsensus.Beacon.PendingBlocks alias LambdaEthereumConsensus.Execution.ExecutionChain alias LambdaEthereumConsensus.ForkChoice.Handlers alias LambdaEthereumConsensus.ForkChoice.Head From e6afeeca8ee2dc475d695b66ef43de2c515ba0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 4 Jun 2024 17:59:19 +0200 Subject: [PATCH 10/37] add cases for the get_blocks_with_status calls --- .../beacon/pending_blocks.ex | 103 +++++++++++------- 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 5915a9991..a6e94d60b 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -92,24 +92,30 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do @impl true def handle_info(:download_blocks, _state) do - Blocks.get_blocks_with_status(:download) - |> Enum.take(16) - |> Enum.map(& &1.root) - |> BlockDownloader.request_blocks_by_root() - |> case do - {:ok, signed_blocks} -> - signed_blocks + case Blocks.get_blocks_with_status(:download) do + {:ok, blocks_to_download} -> + blocks_to_download + |> Enum.take(16) + |> Enum.map(& &1.root) + |> BlockDownloader.request_blocks_by_root() + |> case do + {:ok, signed_blocks} -> + signed_blocks + + {:error, reason} -> + Logger.debug("Block download failed: '#{reason}'") + [] + end + |> Enum.each(fn signed_block -> + signed_block + |> BlockInfo.from_block() + |> BlockInfo.change_status(:download_blobs) + |> Blocks.new_block_info() + end) {:error, reason} -> - Logger.debug("Block download failed: '#{reason}'") - [] + Logger.error("[PendingBlocks] Failed to get blocks to download. Reason: #{reason}") end - |> Enum.each(fn signed_block -> - signed_block - |> BlockInfo.from_block() - |> BlockInfo.change_status(:download_blobs) - |> Blocks.new_block_info() - end) schedule_blocks_download() {:noreply, nil} @@ -117,30 +123,35 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do @impl true def handle_info(:download_blobs, _state) do - blocks_with_blobs = - Blocks.get_blocks_with_status(:download_blobs) - |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) - |> Enum.take(16) - - blobs_to_download = Enum.flat_map(blocks_with_blobs, &missing_blobs/1) - - downloaded_blobs = - blobs_to_download - |> BlobDownloader.request_blobs_by_root() - |> case do - {:ok, blobs} -> - blobs - - {:error, reason} -> - Logger.debug("Blob download failed: '#{reason}'") - [] - end - - Enum.each(downloaded_blobs, &BlobDb.store_blob/1) - - # TODO: is it not possible that blobs were downloaded for one and not for another? - if length(downloaded_blobs) == length(blobs_to_download) do - Enum.each(blocks_with_blobs, fn block_info -> Blocks.change_status(block_info, :pending) end) + case Blocks.get_blocks_with_status(:download_blobs) do + {:ok, blocks_with_missing_blobs} -> + blocks_with_blobs = + blocks_with_missing_blobs + |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) + |> Enum.take(16) + + blobs_to_download = Enum.flat_map(blocks_with_blobs, &missing_blobs/1) + + downloaded_blobs = + blobs_to_download + |> BlobDownloader.request_blobs_by_root() + |> case do + {:ok, blobs} -> + blobs + + {:error, reason} -> + Logger.debug("Blob download failed: '#{reason}'") + [] + end + + Enum.each(downloaded_blobs, &BlobDb.store_blob/1) + + # TODO: is it not possible that blobs were downloaded for one and not for another? + if length(downloaded_blobs) == length(blobs_to_download) do + Enum.each(blocks_with_blobs, fn block_info -> + Blocks.change_status(block_info, :pending) + end) + end end schedule_blobs_download() @@ -152,9 +163,17 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do ########################## defp process_blocks() do - Blocks.get_blocks_with_status(:pending) - |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) - |> Enum.each(&process_block/1) + case Blocks.get_blocks_with_status(:pending) do + {:ok, blocks} -> + blocks + |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) + |> Enum.each(&process_block/1) + + {:error, reason} -> + Logger.error( + "[Pending Blocks] Failed to get pending blocks to process. Reason: #{reason}" + ) + end end defp process_block(block_info) do From 02de936d80e4163e987078d8110fd760dac75f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 4 Jun 2024 20:02:36 +0200 Subject: [PATCH 11/37] take blockinfo out. Make the put a call in LRU cache. Fix nil/download cases --- bench/block_processing.exs | 2 +- bench/multiple_blocks_processing.exs | 2 +- .../controllers/v2/beacon_controller.ex | 2 +- .../beacon/pending_blocks.ex | 20 ++--- .../fork_choice/fork_choice.ex | 2 +- .../fork_choice/handlers.ex | 2 +- .../p2p/incoming_requests/handler.ex | 2 +- .../store/block_db.ex | 83 ++----------------- lib/lambda_ethereum_consensus/store/blocks.ex | 8 +- .../store/lru_cache.ex | 15 ++-- lib/types/store.ex | 2 +- .../integration/fork_choice/handlers_test.exs | 2 +- test/spec/runners/fork_choice.ex | 2 +- test/unit/beacon_api/beacon_api_v1_test.exs | 2 +- test/unit/beacon_api/beacon_api_v2_test.exs | 2 +- test/unit/blocks.exs | 10 ++- 16 files changed, 51 insertions(+), 107 deletions(-) diff --git a/bench/block_processing.exs b/bench/block_processing.exs index f702f83e9..383175f5a 100644 --- a/bench/block_processing.exs +++ b/bench/block_processing.exs @@ -3,9 +3,9 @@ alias LambdaEthereumConsensus.ForkChoice.Handlers alias LambdaEthereumConsensus.StateTransition.Cache alias LambdaEthereumConsensus.Store alias LambdaEthereumConsensus.Store.BlockDb -alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.StateDb alias Types.BeaconState +alias Types.BlockInfo alias Types.SignedBeaconBlock Logger.configure(level: :warning) diff --git a/bench/multiple_blocks_processing.exs b/bench/multiple_blocks_processing.exs index b9979006c..10ea26569 100644 --- a/bench/multiple_blocks_processing.exs +++ b/bench/multiple_blocks_processing.exs @@ -3,9 +3,9 @@ alias LambdaEthereumConsensus.ForkChoice.Handlers alias LambdaEthereumConsensus.StateTransition.Cache alias LambdaEthereumConsensus.Store alias LambdaEthereumConsensus.Store.BlockDb -alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.StateDb alias Types.BeaconState +alias Types.BlockInfo alias Types.SignedBeaconBlock Logger.configure(level: :warning) diff --git a/lib/beacon_api/controllers/v2/beacon_controller.ex b/lib/beacon_api/controllers/v2/beacon_controller.ex index cb1f41369..cb922e3c6 100644 --- a/lib/beacon_api/controllers/v2/beacon_controller.ex +++ b/lib/beacon_api/controllers/v2/beacon_controller.ex @@ -5,9 +5,9 @@ defmodule BeaconApi.V2.BeaconController do alias BeaconApi.ErrorController alias BeaconApi.Utils alias LambdaEthereumConsensus.Store.BlockDb - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.Blocks alias Types + alias Types.BlockInfo plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index a6e94d60b..d08be0939 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -12,8 +12,8 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do alias LambdaEthereumConsensus.P2P.BlobDownloader alias LambdaEthereumConsensus.P2P.BlockDownloader alias LambdaEthereumConsensus.Store.BlobDb - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.Blocks + alias Types.BlockInfo alias Types.SignedBeaconBlock @type block_status :: :pending | :invalid | :download | :download_blobs | :unknown @@ -178,20 +178,15 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do defp process_block(block_info) do parent_root = block_info.signed_block.message.parent_root - parent = Blocks.get_block_info(parent_root) - cond do - is_nil(parent) -> - # TODO: add parent root to download list instead. - %BlockInfo{root: parent_root, status: :download, signed_block: nil} - |> Blocks.new_block_info() + case Blocks.get_block_info(parent_root) do + nil -> + Blocks.add_block_to_download(parent_root) - # If parent is invalid, block is invalid - parent.status == :invalid -> + %BlockInfo{status: :invalid} -> Blocks.change_status(block_info, :invalid) - # If all the other conditions are false, add block to fork choice - parent.status == :transitioned -> + %BlockInfo{status: :transitioned} -> case ForkChoice.on_block(block_info) do :ok -> Blocks.change_status(block_info, :transitioned) @@ -206,6 +201,9 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do Blocks.change_status(block_info, :invalid) end + + _other -> + :ok end end diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index 3d492779a..7f27c171e 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -11,12 +11,12 @@ defmodule LambdaEthereumConsensus.ForkChoice do alias LambdaEthereumConsensus.ForkChoice.Head alias LambdaEthereumConsensus.P2P.Gossip.OperationsCollector alias LambdaEthereumConsensus.Store.BlockDb - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.Blocks alias LambdaEthereumConsensus.Store.StateDb alias LambdaEthereumConsensus.Store.StoreDb alias LambdaEthereumConsensus.Validator.ValidatorManager alias Types.Attestation + alias Types.BlockInfo alias Types.Store ########################## diff --git a/lib/lambda_ethereum_consensus/fork_choice/handlers.ex b/lib/lambda_ethereum_consensus/fork_choice/handlers.ex index b002c97ab..aa692721d 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/handlers.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/handlers.ex @@ -11,9 +11,9 @@ defmodule LambdaEthereumConsensus.ForkChoice.Handlers do alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.StateTransition.Predicates alias LambdaEthereumConsensus.Store.BlobDb - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.Blocks alias LambdaEthereumConsensus.Store.BlockStates + alias Types.BlockInfo alias Types.Attestation alias Types.AttestationData diff --git a/lib/lambda_ethereum_consensus/p2p/incoming_requests/handler.ex b/lib/lambda_ethereum_consensus/p2p/incoming_requests/handler.ex index 1e176e475..0dd6133d9 100644 --- a/lib/lambda_ethereum_consensus/p2p/incoming_requests/handler.ex +++ b/lib/lambda_ethereum_consensus/p2p/incoming_requests/handler.ex @@ -124,7 +124,7 @@ defmodule LambdaEthereumConsensus.P2P.IncomingRequests.Handler do defp map_block_result({:ok, block}), do: map_block_result(block) defp map_block_result({:error, _}), do: {:error, {2, "Server Error"}} - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo + alias Types.BlockInfo defp map_block_result(%BlockInfo{} = block_info), do: diff --git a/lib/lambda_ethereum_consensus/store/block_db.ex b/lib/lambda_ethereum_consensus/store/block_db.ex index 1181a6727..da170cf5d 100644 --- a/lib/lambda_ethereum_consensus/store/block_db.ex +++ b/lib/lambda_ethereum_consensus/store/block_db.ex @@ -5,85 +5,12 @@ defmodule LambdaEthereumConsensus.Store.BlockDb do require Logger alias LambdaEthereumConsensus.Store.Db alias LambdaEthereumConsensus.Store.Utils - alias Types.SignedBeaconBlock + alias Types.BlockInfo @block_prefix "blockHash" @blockslot_prefix "blockSlot" @block_status_prefix "blockStatus" - defmodule BlockInfo do - @moduledoc """ - Signed beacon block accompanied with its root and its processing status. - Maps to what's saved on the blocks db. - """ - @type block_status :: - :pending - | :invalid - | :download - | :download_blobs - | :unknown - | :transitioned - - @type t :: %__MODULE__{ - root: Types.root(), - signed_block: Types.SignedBeaconBlock.t() | nil, - status: block_status() - } - defstruct [:root, :signed_block, :status] - - defguard is_status(atom) - when atom in [ - :pending, - :invalid, - :processing, - :download, - :download_blobs, - :unknown, - :transitioned - ] - - @spec from_block(SignedBeaconBlock.t(), block_status()) :: t() - def from_block(signed_block, status \\ :pending) do - {:ok, root} = Ssz.hash_tree_root(signed_block.message) - from_block(signed_block, root, status) - end - - @spec from_block(SignedBeaconBlock.t(), Types.root(), block_status()) :: t() - def from_block(signed_block, root, status) do - %__MODULE__{root: root, signed_block: signed_block, status: status} - end - - @spec change_status(t(), block_status()) :: t() - def change_status(%__MODULE__{} = block_info, new_status) when is_status(new_status) do - %__MODULE__{block_info | status: new_status} - end - - @spec encode(t()) :: {:ok, binary()} | {:error, binary()} - def encode(%__MODULE__{} = block_info) do - with {:ok, encoded_signed_block} <- Ssz.to_ssz(block_info.signed_block) do - {:ok, :erlang.term_to_binary({encoded_signed_block, block_info.status})} - end - end - - @spec decode(Types.root(), binary()) :: {:error, binary()} | {:ok, t()} - def decode(block_root, data) do - with {:ok, {encoded_signed_block, status}} <- validate_term(:erlang.binary_to_term(data)), - {:ok, signed_block} <- Ssz.from_ssz(encoded_signed_block, SignedBeaconBlock) do - {:ok, %BlockInfo{root: block_root, signed_block: signed_block, status: status}} - end - end - - # Validates a term that came out of the first decoding step for a stored block info tuple. - defp validate_term({encoded_signed_block, status}) - when is_binary(encoded_signed_block) and is_status(status) do - {:ok, {encoded_signed_block, status}} - end - - defp validate_term(other) do - {:error, "Block decoding failed, decoded term is not the expected tuple: #{other}"} - end - end - @spec store_block_info(BlockInfo.t()) :: :ok def store_block_info(%BlockInfo{} = block_info) do # TODO handle encoding errors properly. @@ -94,10 +21,10 @@ defmodule LambdaEthereumConsensus.Store.BlockDb do # WARN: this overrides any previous mapping for the same slot # TODO: this should apply fork-choice if not applied elsewhere # TODO: handle cases where slot is empty - slothash_key = block_root_by_slot_key(block_info.signed_block.message.slot) - Db.put(slothash_key, block_info.root) - - # Here we will also add a status list. + if not is_nil(block_info.signed_block) do + slothash_key = block_root_by_slot_key(block_info.signed_block.message.slot) + Db.put(slothash_key, block_info.root) + end end @spec get_block_info(Types.root()) :: diff --git a/lib/lambda_ethereum_consensus/store/blocks.ex b/lib/lambda_ethereum_consensus/store/blocks.ex index e1ba7d8cd..cc19bddcf 100644 --- a/lib/lambda_ethereum_consensus/store/blocks.ex +++ b/lib/lambda_ethereum_consensus/store/blocks.ex @@ -3,9 +3,9 @@ defmodule LambdaEthereumConsensus.Store.Blocks do Interface to `Store.blocks`. """ alias LambdaEthereumConsensus.Store.BlockDb - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.LRUCache alias Types.BeaconBlock + alias Types.BlockInfo @table :blocks_by_hash @max_entries 512 @@ -32,6 +32,12 @@ defmodule LambdaEthereumConsensus.Store.Blocks do } end + @spec add_block_to_download(Types.root()) :: :ok + def add_block_to_download(root) do + %BlockInfo{root: root, status: :download, signed_block: nil} + |> new_block_info() + end + # TODO: make private. @spec store_block_info(BlockInfo.t()) :: :ok def store_block_info(block_info) do diff --git a/lib/lambda_ethereum_consensus/store/lru_cache.ex b/lib/lambda_ethereum_consensus/store/lru_cache.ex index 3538c48be..fd7ae2184 100644 --- a/lib/lambda_ethereum_consensus/store/lru_cache.ex +++ b/lib/lambda_ethereum_consensus/store/lru_cache.ex @@ -34,7 +34,7 @@ defmodule LambdaEthereumConsensus.Store.LRUCache do @spec put(atom(), key(), value()) :: :ok def put(table, key, value) do cache_value(table, key, value) - GenServer.cast(table, {:put, key, value}) + GenServer.call(table, {:put, key, value}) end @spec get(atom(), key(), (key() -> value() | nil)) :: value() | nil @@ -71,15 +71,15 @@ defmodule LambdaEthereumConsensus.Store.LRUCache do end @impl GenServer - def handle_cast({:put, key, value}, %{store_func: store} = state) do + def handle_call({:put, key, value}, _from, %{store_func: store} = state) do store.(key, value) - handle_cast({:touch_entry, key}, state) + touch_entry(key, state) + {:reply, :ok, state} end @impl GenServer def handle_cast({:touch_entry, key}, state) do - update_ttl(state[:data_table], state[:ttl_table], key) - prune_cache(state) + touch_entry(key, state) {:noreply, state} end @@ -87,6 +87,11 @@ defmodule LambdaEthereumConsensus.Store.LRUCache do ### Private Functions ########################## + defp touch_entry(key, state) do + update_ttl(state[:data_table], state[:ttl_table], key) + prune_cache(state) + end + defp lookup(table, key, fetch_func) do case :ets.lookup_element(table, key, 2, nil) do nil -> diff --git a/lib/types/store.ex b/lib/types/store.ex index 55bc1b9b7..a5f2205d6 100644 --- a/lib/types/store.ex +++ b/lib/types/store.ex @@ -6,11 +6,11 @@ defmodule Types.Store do alias LambdaEthereumConsensus.ForkChoice.Simple.Tree alias LambdaEthereumConsensus.StateTransition.Accessors alias LambdaEthereumConsensus.StateTransition.Misc - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.Blocks alias LambdaEthereumConsensus.Store.BlockStates alias Types.BeaconBlock alias Types.BeaconState + alias Types.BlockInfo alias Types.Checkpoint alias Types.SignedBeaconBlock diff --git a/test/integration/fork_choice/handlers_test.exs b/test/integration/fork_choice/handlers_test.exs index 88654a312..c6c96285d 100644 --- a/test/integration/fork_choice/handlers_test.exs +++ b/test/integration/fork_choice/handlers_test.exs @@ -4,10 +4,10 @@ defmodule Integration.ForkChoice.HandlersTest do alias LambdaEthereumConsensus.ForkChoice.Handlers alias LambdaEthereumConsensus.StateTransition.Cache alias LambdaEthereumConsensus.Store.BlockDb - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.Blocks alias LambdaEthereumConsensus.Store.Db alias LambdaEthereumConsensus.Store.StateDb + alias Types.BlockInfo setup_all do start_supervised!(Db) diff --git a/test/spec/runners/fork_choice.ex b/test/spec/runners/fork_choice.ex index d34e29222..2d6983629 100644 --- a/test/spec/runners/fork_choice.ex +++ b/test/spec/runners/fork_choice.ex @@ -9,10 +9,10 @@ defmodule ForkChoiceTestRunner do alias LambdaEthereumConsensus.ForkChoice.Handlers alias LambdaEthereumConsensus.ForkChoice.Head alias LambdaEthereumConsensus.Store.BlobDb - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.Blocks alias Types.BeaconBlock alias Types.BeaconState + alias Types.BlockInfo alias Types.SignedBeaconBlock alias Types.Store diff --git a/test/unit/beacon_api/beacon_api_v1_test.exs b/test/unit/beacon_api/beacon_api_v1_test.exs index 95fbf1da2..c0144a19a 100644 --- a/test/unit/beacon_api/beacon_api_v1_test.exs +++ b/test/unit/beacon_api/beacon_api_v1_test.exs @@ -7,8 +7,8 @@ defmodule Unit.BeaconApiTest.V1 do alias BeaconApi.Utils alias LambdaEthereumConsensus.Beacon.BeaconChain alias LambdaEthereumConsensus.Store.BlockDb - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.Db + alias Types.BlockInfo @moduletag :beacon_api_case @moduletag :tmp_dir diff --git a/test/unit/beacon_api/beacon_api_v2_test.exs b/test/unit/beacon_api/beacon_api_v2_test.exs index 84a24f9e4..2b59570dc 100644 --- a/test/unit/beacon_api/beacon_api_v2_test.exs +++ b/test/unit/beacon_api/beacon_api_v2_test.exs @@ -5,9 +5,9 @@ defmodule Unit.BeaconApiTest.V2 do alias BeaconApi.Router alias LambdaEthereumConsensus.Store.BlockDb - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.Blocks alias LambdaEthereumConsensus.Store.Db + alias Types.BlockInfo @moduletag :beacon_api_case @moduletag :tmp_dir diff --git a/test/unit/blocks.exs b/test/unit/blocks.exs index 575d5bd26..895732c65 100644 --- a/test/unit/blocks.exs +++ b/test/unit/blocks.exs @@ -1,9 +1,9 @@ defmodule BlocksTest do use ExUnit.Case alias Fixtures.Block - alias LambdaEthereumConsensus.Store.BlockDb.BlockInfo alias LambdaEthereumConsensus.Store.Blocks alias Types.BeaconBlock + alias Types.BlockInfo setup %{tmp_dir: tmp_dir} do start_link_supervised!({LambdaEthereumConsensus.Store.Db, dir: tmp_dir}) @@ -41,4 +41,12 @@ defmodule BlocksTest do assert Blocks.get_block_info(block_info.root) == expected_block assert {:ok, [expected_block]} == Blocks.get_blocks_with_status(:invalid) end + + @tag :tmp_dir + test "A nil block can be saved" do + Blocks.add_block_to_download("some_root") + {:ok, [block]} = Blocks.get_blocks_with_status(:download) + assert block == %BlockInfo{status: :download, root: "some_root", signed_block: nil} + assert Blocks.get_block_info("some_root").signed_block == nil + end end From 862d4d84364db9277010f0ff2c1dbb0ade4e336a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 4 Jun 2024 20:03:18 +0200 Subject: [PATCH 12/37] add block info --- lib/types/block_info.ex | 85 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 lib/types/block_info.ex diff --git a/lib/types/block_info.ex b/lib/types/block_info.ex new file mode 100644 index 000000000..20b136ef9 --- /dev/null +++ b/lib/types/block_info.ex @@ -0,0 +1,85 @@ +defmodule Types.BlockInfo do + @moduledoc """ + Signed beacon block accompanied with its root and its processing status. + Maps to what's saved on the blocks db. + """ + + alias Types.SignedBeaconBlock + + @type block_status :: + :pending + | :invalid + | :download + | :download_blobs + | :unknown + | :transitioned + + @type t :: %__MODULE__{ + root: Types.root(), + signed_block: Types.SignedBeaconBlock.t() | nil, + status: block_status() + } + defstruct [:root, :signed_block, :status] + + defguard is_status(atom) + when atom in [ + :pending, + :invalid, + :processing, + :download, + :download_blobs, + :unknown, + :transitioned + ] + + @spec from_block(SignedBeaconBlock.t(), block_status()) :: t() + def from_block(signed_block, status \\ :pending) do + {:ok, root} = Ssz.hash_tree_root(signed_block.message) + from_block(signed_block, root, status) + end + + @spec from_block(SignedBeaconBlock.t(), Types.root(), block_status()) :: t() + def from_block(signed_block, root, status) do + %__MODULE__{root: root, signed_block: signed_block, status: status} + end + + @spec change_status(t(), block_status()) :: t() + def change_status(%__MODULE__{} = block_info, new_status) when is_status(new_status) do + %__MODULE__{block_info | status: new_status} + end + + @spec encode(t()) :: {:ok, binary()} | {:error, binary()} + def encode(%__MODULE__{} = block_info) do + with {:ok, encoded_signed_block} <- encode_signed_block(block_info.signed_block) do + {:ok, :erlang.term_to_binary({encoded_signed_block, block_info.status})} + end + end + + @spec decode(Types.root(), binary()) :: {:error, binary()} | {:ok, t()} + def decode(block_root, data) do + with {:ok, {encoded_signed_block, status}} <- validate_term(:erlang.binary_to_term(data)), + {:ok, signed_block} <- decode_signed_block(encoded_signed_block) do + {:ok, %__MODULE__{root: block_root, signed_block: signed_block, status: status}} + end + end + + defp encode_signed_block(nil), do: {:ok, nil} + defp encode_signed_block(%SignedBeaconBlock{} = block), do: Ssz.to_ssz(block) + + defp decode_signed_block(nil), do: {:ok, nil} + + defp decode_signed_block(data) when is_binary(data) do + Ssz.from_ssz(data, SignedBeaconBlock) + end + + # Validates a term that came out of the first decoding step for a stored block info tuple. + defp validate_term({encoded_signed_block, status}) + when (is_binary(encoded_signed_block) or is_nil(encoded_signed_block)) and + is_status(status) do + {:ok, {encoded_signed_block, status}} + end + + defp validate_term(other) do + {:error, "Block decoding failed, decoded term is not the expected tuple: #{other}"} + end +end From 3d808ef53902b8103035eaf1fbbc5429c1482fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 4 Jun 2024 20:05:39 +0200 Subject: [PATCH 13/37] fix lint --- lib/lambda_ethereum_consensus/beacon/pending_blocks.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index d08be0939..d98b9e853 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -148,9 +148,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do # TODO: is it not possible that blobs were downloaded for one and not for another? if length(downloaded_blobs) == length(blobs_to_download) do - Enum.each(blocks_with_blobs, fn block_info -> - Blocks.change_status(block_info, :pending) - end) + Enum.each(blocks_with_blobs, &Blocks.change_status(&1, :pending)) end end From 6dc0b04d8751a127b659859bff09a81be0061bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Wed, 5 Jun 2024 20:54:01 +0200 Subject: [PATCH 14/37] change handle_info to handle_casts --- .../p2p/gossip/operations_collector.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex b/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex index 56e020124..f5b9b8cb5 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex @@ -142,7 +142,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.OperationsCollector do end @impl true - def handle_info( + def handle_cast( {:gossipsub, {<<_::binary-size(15)>> <> "voluntary_exit" <> _, _msg_id, message}}, state ) do @@ -154,7 +154,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.OperationsCollector do end @impl true - def handle_info( + def handle_cast( {:gossipsub, {<<_::binary-size(15)>> <> "proposer_slashing" <> _, _msg_id, message}}, state ) do @@ -166,7 +166,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.OperationsCollector do end @impl true - def handle_info( + def handle_cast( {:gossipsub, {<<_::binary-size(15)>> <> "attester_slashing" <> _, _msg_id, message}}, state ) do @@ -178,7 +178,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.OperationsCollector do end @impl true - def handle_info( + def handle_cast( {:gossipsub, {<<_::binary-size(15)>> <> "bls_to_execution_change" <> _, _msg_id, message}}, state From 6eefe0744a0858600cfe48271e66d4918ec86f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Thu, 6 Jun 2024 14:13:34 +0200 Subject: [PATCH 15/37] Make LRU cache writes to be calls --- .../beacon/pending_blocks.ex | 6 +++ .../store/lru_cache.ex | 52 ++++++++++--------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index d98b9e853..b6f1fa5e6 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -94,6 +94,8 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do def handle_info(:download_blocks, _state) do case Blocks.get_blocks_with_status(:download) do {:ok, blocks_to_download} -> + IO.inspect("Blocks to download: #{length(blocks_to_download)}") + blocks_to_download |> Enum.take(16) |> Enum.map(& &1.root) @@ -125,6 +127,8 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do def handle_info(:download_blobs, _state) do case Blocks.get_blocks_with_status(:download_blobs) do {:ok, blocks_with_missing_blobs} -> + IO.inspect("Blocks with blobs to download: #{length(blocks_with_missing_blobs)}") + blocks_with_blobs = blocks_with_missing_blobs |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) @@ -163,6 +167,8 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do defp process_blocks() do case Blocks.get_blocks_with_status(:pending) do {:ok, blocks} -> + IO.inspect("Blocks pending to send to fork choice: #{length(blocks)}") + blocks |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) |> Enum.each(&process_block/1) diff --git a/lib/lambda_ethereum_consensus/store/lru_cache.ex b/lib/lambda_ethereum_consensus/store/lru_cache.ex index fd7ae2184..c3672ec74 100644 --- a/lib/lambda_ethereum_consensus/store/lru_cache.ex +++ b/lib/lambda_ethereum_consensus/store/lru_cache.ex @@ -33,12 +33,29 @@ defmodule LambdaEthereumConsensus.Store.LRUCache do @spec put(atom(), key(), value()) :: :ok def put(table, key, value) do - cache_value(table, key, value) GenServer.call(table, {:put, key, value}) + :ok end @spec get(atom(), key(), (key() -> value() | nil)) :: value() | nil - def get(table, key, fetch_func), do: lookup(table, key, fetch_func) + def get(table, key, fetch_func) do + case :ets.lookup_element(table, key, 2, nil) do + nil -> + # Cache miss. + case fetch_func.(key) do + nil -> + nil + + value -> + GenServer.call(table, {:cache_value, key, value}) + value + end + + v -> + :ok = GenServer.cast(table, {:touch_entry, key}) + v + end + end ########################## ### GenServer Callbacks @@ -73,10 +90,16 @@ defmodule LambdaEthereumConsensus.Store.LRUCache do @impl GenServer def handle_call({:put, key, value}, _from, %{store_func: store} = state) do store.(key, value) + cache_value(state, key, value) touch_entry(key, state) {:reply, :ok, state} end + def handle_call({:cache_value, key, value}, _from, state) do + cache_value(state, key, value) + {:reply, :ok, value} + end + @impl GenServer def handle_cast({:touch_entry, key}, state) do touch_entry(key, state) @@ -92,28 +115,9 @@ defmodule LambdaEthereumConsensus.Store.LRUCache do prune_cache(state) end - defp lookup(table, key, fetch_func) do - case :ets.lookup_element(table, key, 2, nil) do - nil -> - cache_miss(table, key, fetch_func) - - v -> - :ok = GenServer.cast(table, {:touch_entry, key}) - v - end - end - - defp cache_miss(table, key, fetch_func) do - case fetch_func.(key) do - nil -> nil - value -> cache_value(table, key, value) - end - end - - defp cache_value(table, key, value) do - :ets.insert(table, {key, value, nil}) - GenServer.cast(table, {:touch_entry, key}) - value + defp cache_value(state, key, value) do + :ets.insert(state.data_table, {key, value, nil}) + touch_entry(key, state) end defp update_ttl(data_table, ttl_table, key) do From 4c5498a2b523ca28d9af8254a36392cc3071d559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Thu, 6 Jun 2024 14:16:10 +0200 Subject: [PATCH 16/37] Remove io inspects --- lib/lambda_ethereum_consensus/beacon/pending_blocks.ex | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index b6f1fa5e6..d98b9e853 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -94,8 +94,6 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do def handle_info(:download_blocks, _state) do case Blocks.get_blocks_with_status(:download) do {:ok, blocks_to_download} -> - IO.inspect("Blocks to download: #{length(blocks_to_download)}") - blocks_to_download |> Enum.take(16) |> Enum.map(& &1.root) @@ -127,8 +125,6 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do def handle_info(:download_blobs, _state) do case Blocks.get_blocks_with_status(:download_blobs) do {:ok, blocks_with_missing_blobs} -> - IO.inspect("Blocks with blobs to download: #{length(blocks_with_missing_blobs)}") - blocks_with_blobs = blocks_with_missing_blobs |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) @@ -167,8 +163,6 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do defp process_blocks() do case Blocks.get_blocks_with_status(:pending) do {:ok, blocks} -> - IO.inspect("Blocks pending to send to fork choice: #{length(blocks)}") - blocks |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) |> Enum.each(&process_block/1) From 41618b9de3198b18fe744c326c634e28ba7e0470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Thu, 6 Jun 2024 14:58:41 +0200 Subject: [PATCH 17/37] adding some logs to debug ci --- lib/lambda_ethereum_consensus/store/lru_cache.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/lambda_ethereum_consensus/store/lru_cache.ex b/lib/lambda_ethereum_consensus/store/lru_cache.ex index c3672ec74..2d4251f92 100644 --- a/lib/lambda_ethereum_consensus/store/lru_cache.ex +++ b/lib/lambda_ethereum_consensus/store/lru_cache.ex @@ -4,6 +4,7 @@ defmodule LambdaEthereumConsensus.Store.LRUCache do and `LambdaEthereumConsensus.Store.BlockStates`. """ use GenServer + require Logger @default_max_entries 512 @default_batch_prune_size 32 @@ -33,6 +34,7 @@ defmodule LambdaEthereumConsensus.Store.LRUCache do @spec put(atom(), key(), value()) :: :ok def put(table, key, value) do + Logger.notice("DEBUG: before call") GenServer.call(table, {:put, key, value}) :ok end @@ -89,8 +91,14 @@ defmodule LambdaEthereumConsensus.Store.LRUCache do @impl GenServer def handle_call({:put, key, value}, _from, %{store_func: store} = state) do + Logger.notice("DEBUG: before store key value") + store.(key, value) + Logger.notice("DEBUG: before caching") + cache_value(state, key, value) + Logger.notice("DEBUG: before touch entry") + touch_entry(key, state) {:reply, :ok, state} end From efcd70deca0390c76f5870ba9d87d3b505702308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 11 Jun 2024 15:56:27 +0200 Subject: [PATCH 18/37] Everything compiling --- docs/architecture.md | 32 +++++- .../beacon/beacon_chain.ex | 4 +- .../beacon/pending_blocks.ex | 106 +++--------------- .../p2p/blob_downloader.ex | 4 +- .../p2p/block_downloader.ex | 4 +- lib/libp2p_port.ex | 56 +++++++-- .../internal/proto_helpers/proto_helpers.go | 19 ++++ .../libp2p_port/internal/reqresp/reqresp.go | 2 +- proto/libp2p.proto | 8 ++ 9 files changed, 127 insertions(+), 108 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 8f708556b..04713185d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -264,7 +264,37 @@ Asynchronously, a new task is started to recompute the new head, as this takes a ## Request-Response -**TO DO**: document how ports work for this. +Request-response is an on-demand protocol where a node asks for information directly to a peer and expects a response. This may be to request metadata that corresponds to that peer for discovery purposes, or to request information from the past that will not appear on when listening to gossip (useful for checkpoint sync). + +It's implemented in the following way: + +```mermaid +sequenceDiagram + +participant req as Requesting Process +participant p2p as Libp2pPort +participant gomain as go libp2p main +participant goreq as request goroutine + +req ->> req: send_request(peer_id, protocol_id, message) +req ->> p2p: send_protobuf(from: self()) +activate p2p +p2p ->> gomain: %Command{} +deactivate p2p +req ->>+ req: receive_response() + +gomain ->> gomain: handle_command() +gomain ->>+ goreq: go sendAsyncRequest() +goreq ->>- p2p: SendNotification(%Result{from, response, err}) + +p2p ->>p2p: handle_notification(%Result{from: from}) +p2p ->> req: {:response, result} +deactivate req +``` + +Explained, a process that wants to request something from Libp2pPort sends a request with its own pid, which is then included in the Command payload. The request is handled asynchronously in the go side, and eventually, the pid is included in the response, and sent back to LibP2PPort, who now knows to which process it needs to be dispatched. + +The specific kind of command (a request) is specified, but there's nothing identifying this is a response vs any other kind of result, or the specific kind of response (e.g. a block download vs a blob download). Currently the only way this is handled differentially is because the pid is waiting for a specific kind of response and for nothing else at a time. ### Checkpoint sync diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_chain.ex b/lib/lambda_ethereum_consensus/beacon/beacon_chain.ex index 8b65bf40e..4430466f4 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_chain.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_chain.ex @@ -3,7 +3,7 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconChain do use GenServer - alias LambdaEthereumConsensus.Beacon.PendingBlocks + alias LambdaEthereumConsensus.Libp2pPort alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.Validator.ValidatorManager alias Types.BeaconState @@ -172,7 +172,7 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconChain do new_state = %BeaconChainState{state | time: time} if time >= state.genesis_time do - PendingBlocks.on_tick(time) + Libp2pPort.on_tick(time) # TODO: reduce time between ticks to account for gnosis' 5s slot time. old_logical_time = compute_logical_time(state) new_logical_time = compute_logical_time(new_state) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index d98b9e853..19e2195e5 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -4,13 +4,10 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do The main purpose of this module is making sure that a blocks parent is already in the fork choice. If it's not, it will request it to the block downloader. """ - - use GenServer require Logger alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.P2P.BlobDownloader - alias LambdaEthereumConsensus.P2P.BlockDownloader alias LambdaEthereumConsensus.Store.BlobDb alias LambdaEthereumConsensus.Store.Blocks alias Types.BlockInfo @@ -22,106 +19,44 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do | {nil, :invalid | :download} @type state :: nil - ########################## - ### Public API - ########################## + @doc """ + If the block is not present, it will be stored as pending. - def start_link(opts) do - GenServer.start_link(__MODULE__, opts, name: __MODULE__) - end + In case it's ready to be processed + (the parent is present and already transitioned), then the block's state transition will be + calculated, resulting in a new saved block. + If the new state enables older blocks that were pending to be processed, they will be processed + immediately. + """ @spec add_block(SignedBeaconBlock.t()) :: :ok def add_block(signed_block) do - GenServer.cast(__MODULE__, {:add_block, signed_block}) - end - - @spec on_tick(Types.uint64()) :: :ok - def on_tick(time) do - GenServer.cast(__MODULE__, {:on_tick, time}) - end - - ########################## - ### GenServer Callbacks - ########################## - - @impl true - @spec init(any) :: {:ok, state()} - def init(_opts) do - schedule_blocks_processing() - schedule_blocks_download() - schedule_blobs_download() - - {:ok, nil} - end - - @spec handle_cast(any(), state()) :: {:noreply, state()} - - @impl true - def handle_cast({:add_block, %SignedBeaconBlock{} = signed_block}, _state) do block_info = BlockInfo.from_block(signed_block) - # If already processing or processed, ignore it - if not Blocks.has_block?(block_info.root) do + # If the block is new or was to be downloaded, we store it. + loaded_block = Blocks.get_block_info(block_info.root) + + if is_nil(loaded_block) or loaded_block.status == :download do if Enum.empty?(missing_blobs(block_info)) do block_info else block_info |> BlockInfo.change_status(:download_blobs) end |> Blocks.new_block_info() - end - {:noreply, nil} - end - - @impl true - def handle_cast({:on_tick, time}, state) do - ForkChoice.on_tick(time) - {:noreply, state} + process_block(block_info) + end end @doc """ Iterates through the pending blocks and adds them to the fork choice if their parent is already in the fork choice. """ - @impl true @spec handle_info(atom(), state()) :: {:noreply, state()} def handle_info(:process_blocks, _state) do - schedule_blocks_processing() process_blocks() {:noreply, nil} end - @impl true - def handle_info(:download_blocks, _state) do - case Blocks.get_blocks_with_status(:download) do - {:ok, blocks_to_download} -> - blocks_to_download - |> Enum.take(16) - |> Enum.map(& &1.root) - |> BlockDownloader.request_blocks_by_root() - |> case do - {:ok, signed_blocks} -> - signed_blocks - - {:error, reason} -> - Logger.debug("Block download failed: '#{reason}'") - [] - end - |> Enum.each(fn signed_block -> - signed_block - |> BlockInfo.from_block() - |> BlockInfo.change_status(:download_blobs) - |> Blocks.new_block_info() - end) - - {:error, reason} -> - Logger.error("[PendingBlocks] Failed to get blocks to download. Reason: #{reason}") - end - - schedule_blocks_download() - {:noreply, nil} - end - - @impl true def handle_info(:download_blobs, _state) do case Blocks.get_blocks_with_status(:download_blobs) do {:ok, blocks_with_missing_blobs} -> @@ -152,7 +87,6 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end end - schedule_blobs_download() {:noreply, nil} end @@ -222,16 +156,4 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do true end end - - defp schedule_blocks_processing() do - Process.send_after(__MODULE__, :process_blocks, 500) - end - - defp schedule_blobs_download() do - Process.send_after(__MODULE__, :download_blobs, 500) - end - - defp schedule_blocks_download() do - Process.send_after(__MODULE__, :download_blocks, 1000) - end end diff --git a/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex b/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex index 86d8a610e..1dffea0aa 100644 --- a/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex +++ b/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex @@ -35,7 +35,7 @@ defmodule LambdaEthereumConsensus.P2P.BlobDownloader do |> ReqResp.encode_request() with {:ok, response} <- - Libp2pPort.send_request(peer_id, @blobs_by_range_protocol_id, request), + Libp2pPort.send_async_request(peer_id, @blobs_by_range_protocol_id, request), {:ok, blobs} <- ReqResp.decode_response(response, BlobSidecar), :ok <- verify_batch(blobs, slot, count) do {:ok, blobs} @@ -74,7 +74,7 @@ defmodule LambdaEthereumConsensus.P2P.BlobDownloader do request = ReqResp.encode_request({identifiers, TypeAliases.blob_sidecars_by_root_request()}) with {:ok, response} <- - Libp2pPort.send_request(peer_id, @blobs_by_root_protocol_id, request), + Libp2pPort.send_async_request(peer_id, @blobs_by_root_protocol_id, request), {:ok, blobs} <- ReqResp.decode_response(response, BlobSidecar) do {:ok, blobs} else diff --git a/lib/lambda_ethereum_consensus/p2p/block_downloader.ex b/lib/lambda_ethereum_consensus/p2p/block_downloader.ex index fcb044e64..2654515a9 100644 --- a/lib/lambda_ethereum_consensus/p2p/block_downloader.ex +++ b/lib/lambda_ethereum_consensus/p2p/block_downloader.ex @@ -34,7 +34,7 @@ defmodule LambdaEthereumConsensus.P2P.BlockDownloader do |> ReqResp.encode_request() with {:ok, response} <- - Libp2pPort.send_request(peer_id, @blocks_by_range_protocol_id, request), + Libp2pPort.send_async_request(peer_id, @blocks_by_range_protocol_id, request), {:ok, blocks} <- ReqResp.decode_response(response, SignedBeaconBlock), :ok <- verify_batch(blocks, slot, count) do tags = %{result: "success", type: "by_slot", reason: "success"} @@ -78,7 +78,7 @@ defmodule LambdaEthereumConsensus.P2P.BlockDownloader do request = ReqResp.encode_request({roots, TypeAliases.beacon_blocks_by_root_request()}) with {:ok, response} <- - Libp2pPort.send_request(peer_id, @blocks_by_root_protocol_id, request), + Libp2pPort.send_async_request(peer_id, @blocks_by_root_protocol_id, request), {:ok, blocks} <- ReqResp.decode_response(response, SignedBeaconBlock) do tags = %{result: "success", type: "by_root", reason: "success"} :telemetry.execute([:network, :request], %{blocks: length(roots)}, tags) diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index bccce5dfa..c93adb594 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -16,6 +16,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do alias LambdaEthereumConsensus.Utils.BitVector alias Types.EnrForkId + alias LambdaEthereumConsensus.ForkChoice alias Libp2pProto.AddPeer alias Libp2pProto.Command alias Libp2pProto.Enr @@ -28,6 +29,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do alias Libp2pProto.Notification alias Libp2pProto.Publish alias Libp2pProto.Request + alias Libp2pProto.Response alias Libp2pProto.Result alias Libp2pProto.ResultMessage alias Libp2pProto.SendRequest @@ -57,6 +59,15 @@ defmodule LambdaEthereumConsensus.Libp2pPort do | {:new_peer_handler, pid()} | {:join_init_topics, boolean()} + @type node_identity() :: %{ + peer_id: binary(), + # Pretty-printed version of the peer ID + pretty_peer_id: String.t(), + enr: String.t(), + p2p_addresses: [String.t()], + discovery_addresses: [String.t()] + } + ###################### ### API ###################### @@ -82,14 +93,10 @@ defmodule LambdaEthereumConsensus.Libp2pPort do GenServer.start_link(__MODULE__, args, opts) end - @type node_identity() :: %{ - peer_id: binary(), - # Pretty-printed version of the peer ID - pretty_peer_id: String.t(), - enr: String.t(), - p2p_addresses: [String.t()], - discovery_addresses: [String.t()] - } + @spec on_tick(Types.uint64()) :: :ok + def on_tick(time) do + GenServer.cast(__MODULE__, {:on_tick, time}) + end @doc """ Retrieves identity info from the underlying LibP2P node. @@ -140,6 +147,15 @@ defmodule LambdaEthereumConsensus.Libp2pPort do call_command(pid, {:send_request, c}) end + @doc """ + Sends a request to a peer. The response will be processed by the Libp2p process. + """ + def send_async_request(pid \\ __MODULE__, peer_id, protocol_id, message) do + :telemetry.execute([:port, :message], %{}, %{function: "send_request", direction: "elixir->"}) + c = %SendRequest{id: peer_id, protocol_id: protocol_id, message: message} + cast_command(pid, {:send_request, c}) + end + @doc """ Returns the next request received by the server for registered handlers on the current process. If there are no requests, it waits for one. @@ -325,6 +341,14 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:noreply, %{state | new_peer_handler: new_peer_handler}} end + @impl GenServer + def handle_cast({:on_tick, time}, state) do + # TODO: we probably want to remove this from here, but we keep it here to have this serialized + # with respect to the other fork choice store modifications. + ForkChoice.on_tick(time) + {:noreply, state} + end + @impl GenServer def handle_info({_port, {:data, data}}, state) do %Notification{n: {_, payload}} = Notification.decode(data) @@ -378,6 +402,17 @@ defmodule LambdaEthereumConsensus.Libp2pPort do send(handler, {:new_peer, peer_id}) end + defp handle_notification(%Response{} = response, _state) do + :telemetry.execute([:port, :message], %{}, %{function: "response", direction: "->elixir"}) + success = if response.success, do: :ok, else: :error + + if response.from != "" do + send(:erlang.binary_to_term(response.from), {:response, {success, response.message}}) + else + handle_async_response(response.protocol_id, success, response.message) + end + end + defp handle_notification(%Result{from: "", result: result}, _state) do :telemetry.execute([:port, :message], %{}, %{function: "result", direction: "->elixir"}) # TODO: amount of failures would be a useful metric @@ -517,4 +552,9 @@ defmodule LambdaEthereumConsensus.Libp2pPort do } |> encode_enr(attnets, syncnets) end + + @spec handle_async_response(binary(), :ok | :error, binary()) :: :ok + defp handle_async_response(protocol_id, _success?, _message) do + Logger.warning("Received unhandled response from protocol #{protocol_id}.") + end end diff --git a/native/libp2p_port/internal/proto_helpers/proto_helpers.go b/native/libp2p_port/internal/proto_helpers/proto_helpers.go index b65bea3d3..003be2097 100644 --- a/native/libp2p_port/internal/proto_helpers/proto_helpers.go +++ b/native/libp2p_port/internal/proto_helpers/proto_helpers.go @@ -144,6 +144,25 @@ func ResultNotification(from []byte, result []byte, err error) *proto_defs.Notif return &proto_defs.Notification{N: &proto_defs.Notification_Result{Result: responseNotification}} } +func ResponseNotification(from []byte, result []byte, err error, protocolId string) *proto_defs.Notification { + var message []byte + var success bool + + if err != nil { + success = false + message = []byte(err.Error()) + } else { + success = true + if result != nil { + message = result + } else { + message = []byte{} + } + } + response := &proto_defs.Response{From: from, Message: message, Success: success, ProtocolId: []byte(protocolId)} + return &proto_defs.Notification{N: &proto_defs.Notification_Response{Response: response}} +} + func NodeIdentityNotification(from []byte, nodeIdentity *proto_defs.NodeIdentity) *proto_defs.Notification { responseNotification := &proto_defs.Result{From: from, Result: &proto_defs.Result_NodeIdentity{NodeIdentity: nodeIdentity}} return &proto_defs.Notification{N: &proto_defs.Notification_Result{Result: responseNotification}} diff --git a/native/libp2p_port/internal/reqresp/reqresp.go b/native/libp2p_port/internal/reqresp/reqresp.go index 5d496de75..a7013752f 100644 --- a/native/libp2p_port/internal/reqresp/reqresp.go +++ b/native/libp2p_port/internal/reqresp/reqresp.go @@ -95,7 +95,7 @@ func (l *Listener) SendRequest(from, peerId []byte, protocolId string, message [ func sendAsyncRequest(h host.Host, p port.Port, from []byte, peerId peer.ID, protocolId protocol.ID, message []byte) { response, err := sendRequest(h, peerId, protocolId, message) - result := proto_helpers.ResultNotification([]byte(from), response, err) + result := proto_helpers.ResponseNotification([]byte(from), response, err, string(protocolId)) p.SendNotification(result) } diff --git a/proto/libp2p.proto b/proto/libp2p.proto index d3699c80e..0a074d54c 100644 --- a/proto/libp2p.proto +++ b/proto/libp2p.proto @@ -181,6 +181,13 @@ message Result { } } +message Response { + bool success = 1; + bytes from = 2; + bytes protocol_id = 3; + bytes message = 4; +} + message Tracer { oneof t { Join joined = 1; @@ -204,5 +211,6 @@ message Notification { NewPeer new_peer = 3; Result result = 4; Tracer tracer = 5; + Response response = 6; } } From d9b300dd0eb787af0fbcfb1bf2dc6d9a9f7730cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Fri, 14 Jun 2024 16:05:37 +0200 Subject: [PATCH 19/37] progress for friday --- .../beacon/pending_blocks.ex | 61 ++++--- lib/lambda_ethereum_consensus/metrics.ex | 58 +++++++ .../p2p/blob_downloader.ex | 51 +++--- .../p2p/block_downloader.ex | 78 ++++++--- lib/lambda_ethereum_consensus/p2p/requests.ex | 48 ++++++ lib/libp2p_port.ex | 149 +++++++----------- mix.exs | 3 +- mix.lock | 1 + .../internal/proto_helpers/proto_helpers.go | 12 +- .../libp2p_port/internal/reqresp/reqresp.go | 2 +- proto/libp2p.proto | 11 +- test/unit/p2p/requests_test.exs | 37 +++++ 12 files changed, 329 insertions(+), 182 deletions(-) create mode 100644 lib/lambda_ethereum_consensus/metrics.ex create mode 100644 lib/lambda_ethereum_consensus/p2p/requests.ex create mode 100644 test/unit/p2p/requests_test.exs diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 19e2195e5..9fa4bf8d7 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -28,6 +28,8 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do If the new state enables older blocks that were pending to be processed, they will be processed immediately. + + If blobs are missing, they will be requested. """ @spec add_block(SignedBeaconBlock.t()) :: :ok def add_block(signed_block) do @@ -37,9 +39,12 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do loaded_block = Blocks.get_block_info(block_info.root) if is_nil(loaded_block) or loaded_block.status == :download do - if Enum.empty?(missing_blobs(block_info)) do + missing_blobs = missing_blobs(block_info) + + if Enum.empty?(missing_blobs) do block_info else + BlobDownloader.request_blobs_by_root(missing_blobs) block_info |> BlockInfo.change_status(:download_blobs) end |> Blocks.new_block_info() @@ -48,6 +53,27 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end end + @doc """ + To be used when a series of blobs are downloaded. Stores each blob. + If there are blocks that can be processed, does so immediately. + """ + def add_blobs(blobs) do + Enum.map(blobs, fn blob -> + BlobDb.store_blob(blob) + Ssz.hash_tree_root!(blob.signed_block_header.message) + end) + |> Enum.uniq() + |> Enum.each(fn root -> + with %BlockInfo{} = block_info <- Blocks.get_block_info(root) do + if Enum.empty?(missing_blobs(block_info)) do + Blocks.change_status(block_info, :pending) + end + + process_block(block_info) + end + end) + end + @doc """ Iterates through the pending blocks and adds them to the fork choice if their parent is already in the fork choice. """ @@ -57,39 +83,6 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do {:noreply, nil} end - def handle_info(:download_blobs, _state) do - case Blocks.get_blocks_with_status(:download_blobs) do - {:ok, blocks_with_missing_blobs} -> - blocks_with_blobs = - blocks_with_missing_blobs - |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) - |> Enum.take(16) - - blobs_to_download = Enum.flat_map(blocks_with_blobs, &missing_blobs/1) - - downloaded_blobs = - blobs_to_download - |> BlobDownloader.request_blobs_by_root() - |> case do - {:ok, blobs} -> - blobs - - {:error, reason} -> - Logger.debug("Blob download failed: '#{reason}'") - [] - end - - Enum.each(downloaded_blobs, &BlobDb.store_blob/1) - - # TODO: is it not possible that blobs were downloaded for one and not for another? - if length(downloaded_blobs) == length(blobs_to_download) do - Enum.each(blocks_with_blobs, &Blocks.change_status(&1, :pending)) - end - end - - {:noreply, nil} - end - ########################## ### Private Functions ########################## diff --git a/lib/lambda_ethereum_consensus/metrics.ex b/lib/lambda_ethereum_consensus/metrics.ex new file mode 100644 index 000000000..e6a92d137 --- /dev/null +++ b/lib/lambda_ethereum_consensus/metrics.ex @@ -0,0 +1,58 @@ +defmodule LambdaEthereumConsensus.Metrics do + def tracer({:joined, %{topic: topic}}, _state) do + :telemetry.execute([:network, :pubsub_topic_active], %{active: 1}, %{ + topic: get_topic_name(topic) + }) + end + + def tracer({:left, %{topic: topic}}, _state) do + :telemetry.execute([:network, :pubsub_topic_active], %{active: -1}, %{ + topic: get_topic_name(topic) + }) + end + + def tracer({:grafted, %{topic: topic}}) do + :telemetry.execute([:network, :pubsub_topics_graft], %{}, %{topic: get_topic_name(topic)}) + end + + def tracer({:pruned, %{topic: topic}}) do + :telemetry.execute([:network, :pubsub_topics_prune], %{}, %{topic: get_topic_name(topic)}) + end + + def tracer({:deliver_message, %{topic: topic}}) do + :telemetry.execute([:network, :pubsub_topics_deliver_message], %{}, %{ + topic: get_topic_name(topic) + }) + end + + def tracer({:duplicate_message, %{topic: topic}}) do + :telemetry.execute([:network, :pubsub_topics_duplicate_message], %{}, %{ + topic: get_topic_name(topic) + }) + end + + def tracer({:reject_message, %{topic: topic}}) do + :telemetry.execute([:network, :pubsub_topics_reject_message], %{}, %{ + topic: get_topic_name(topic) + }) + end + + def tracer({:un_deliverable_message, %{topic: topic}}) do + :telemetry.execute([:network, :pubsub_topics_un_deliverable_message], %{}, %{ + topic: get_topic_name(topic) + }) + end + + def tracer({:validate_message, %{topic: topic}}) do + :telemetry.execute([:network, :pubsub_topics_validate_message], %{}, %{ + topic: get_topic_name(topic) + }) + end + + def get_topic_name(topic) do + case topic |> String.split("/") |> Enum.fetch(3) do + {:ok, name} -> name + :error -> topic + end + end +end diff --git a/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex b/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex index 1dffea0aa..70848b581 100644 --- a/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex +++ b/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex @@ -12,18 +12,20 @@ defmodule LambdaEthereumConsensus.P2P.BlobDownloader do @blobs_by_range_protocol_id "/eth2/beacon_chain/req/blob_sidecars_by_range/1/ssz_snappy" @blobs_by_root_protocol_id "/eth2/beacon_chain/req/blob_sidecars_by_root/1/ssz_snappy" + @type on_blobs :: ({:ok, [BlobSidecar.t()]} | {:error, any()} -> :ok) + # Requests to peers might fail for various reasons, # for example they might not support the protocol or might not reply # so we want to try again with a different peer @default_retries 5 - @spec request_blobs_by_range(Types.slot(), non_neg_integer(), non_neg_integer()) :: - {:ok, [BlobSidecar.t()]} | {:error, any()} - def request_blobs_by_range(slot, count, retries \\ @default_retries) + @spec request_blobs_by_range(Types.slot(), non_neg_integer(), on_blobs(), non_neg_integer()) :: + :ok + def request_blobs_by_range(slot, count, on_blobs, retries \\ @default_retries) - def request_blobs_by_range(_slot, 0, _retries), do: {:ok, []} + def request_blobs_by_range(_slot, 0, _on_blobs, _retries), do: {:ok, []} - def request_blobs_by_range(slot, count, retries) do + def request_blobs_by_range(slot, count, on_blobs, retries) do Logger.debug("Requesting blobs", slot: slot) # TODO: handle no-peers asynchronously? @@ -34,11 +36,16 @@ defmodule LambdaEthereumConsensus.P2P.BlobDownloader do %Types.BeaconBlocksByRangeRequest{start_slot: slot, count: count} |> ReqResp.encode_request() - with {:ok, response} <- - Libp2pPort.send_async_request(peer_id, @blobs_by_range_protocol_id, request), - {:ok, blobs} <- ReqResp.decode_response(response, BlobSidecar), + Libp2pPort.send_async_request(peer_id, @blobs_by_range_protocol_id, request, fn response -> + handle_blobs_by_range_response(response, peer_id, count, slot, retries, on_blobs) + end) + end + + defp handle_blobs_by_range_response(response, peer_id, count, slot, retries, on_blobs) do + with {:ok, response_message} <- response, + {:ok, blobs} <- ReqResp.decode_response(response_message, BlobSidecar), :ok <- verify_batch(blobs, slot, count) do - {:ok, blobs} + on_blobs.({:ok, blobs}) else {:error, reason} -> P2P.Peerbook.penalize_peer(peer_id) @@ -47,7 +54,7 @@ defmodule LambdaEthereumConsensus.P2P.BlobDownloader do Logger.debug("Retrying request for #{count} blobs", slot: slot) request_blobs_by_range(slot, count, retries - 1) else - {:error, reason} + on_blobs.({:error, reason}) end end end @@ -60,23 +67,27 @@ defmodule LambdaEthereumConsensus.P2P.BlobDownloader do end end - @spec request_blobs_by_root([Types.BlobIdentifier.t()], non_neg_integer()) :: - {:ok, [BlobSidecar.t()]} | {:error, binary()} - def request_blobs_by_root(identifiers, retries \\ @default_retries) + @spec request_blobs_by_root([Types.BlobIdentifier.t()], on_blobs(), non_neg_integer()) :: :ok + def request_blobs_by_root(identifiers, on_blobs, retries \\ @default_retries) - def request_blobs_by_root([], _retries), do: {:ok, []} + def request_blobs_by_root([], on_blobs, _retries), do: {:ok, []} - def request_blobs_by_root(identifiers, retries) do + def request_blobs_by_root(identifiers, on_blobs, retries) do Logger.debug("Requesting #{length(identifiers)} blobs.") peer_id = get_some_peer() request = ReqResp.encode_request({identifiers, TypeAliases.blob_sidecars_by_root_request()}) - with {:ok, response} <- - Libp2pPort.send_async_request(peer_id, @blobs_by_root_protocol_id, request), - {:ok, blobs} <- ReqResp.decode_response(response, BlobSidecar) do - {:ok, blobs} + Libp2pPort.send_async_request(peer_id, @blobs_by_root_protocol_id, request, fn response -> + handle_blobs_by_root(response, peer_id, identifiers, retries, on_blobs) + end) + end + + def handle_blobs_by_root(response, peer_id, identifiers, retries, on_blobs) do + with {:ok, response_message} <- response, + {:ok, blobs} <- ReqResp.decode_response(response_message, BlobSidecar) do + on_blobs.({:ok, blobs}) else {:error, reason} -> P2P.Peerbook.penalize_peer(peer_id) @@ -85,7 +96,7 @@ defmodule LambdaEthereumConsensus.P2P.BlobDownloader do Logger.debug("Retrying request for blobs.") request_blobs_by_root(identifiers, retries - 1) else - {:error, reason} + on_blobs.({:error, reason}) end end end diff --git a/lib/lambda_ethereum_consensus/p2p/block_downloader.ex b/lib/lambda_ethereum_consensus/p2p/block_downloader.ex index 2654515a9..7677fb769 100644 --- a/lib/lambda_ethereum_consensus/p2p/block_downloader.ex +++ b/lib/lambda_ethereum_consensus/p2p/block_downloader.ex @@ -17,13 +17,17 @@ defmodule LambdaEthereumConsensus.P2P.BlockDownloader do # so we want to try again with a different peer @default_retries 5 - @spec request_blocks_by_range(Types.slot(), non_neg_integer(), non_neg_integer()) :: - {:ok, [SignedBeaconBlock.t()]} | {:error, any()} - def request_blocks_by_range(slot, count, retries \\ @default_retries) + @spec request_blocks_by_range( + Types.slot(), + non_neg_integer(), + ({:ok, [SignedBeaconBlock.t()]} | {:error, any()} -> term()), + non_neg_integer() + ) :: :ok + def request_blocks_by_range(slot, count, on_blocks, retries \\ @default_retries) - def request_blocks_by_range(_slot, 0, _retries), do: {:ok, []} + def request_blocks_by_range(_slot, 0, _on_blocks, _retries), do: {:ok, []} - def request_blocks_by_range(slot, count, retries) do + def request_blocks_by_range(slot, count, on_blocks, retries) do Logger.debug("Requesting block", slot: slot) # TODO: handle no-peers asynchronously? @@ -33,13 +37,18 @@ defmodule LambdaEthereumConsensus.P2P.BlockDownloader do %Types.BeaconBlocksByRangeRequest{start_slot: slot, count: count} |> ReqResp.encode_request() - with {:ok, response} <- - Libp2pPort.send_async_request(peer_id, @blocks_by_range_protocol_id, request), - {:ok, blocks} <- ReqResp.decode_response(response, SignedBeaconBlock), + Libp2pPort.send_async_request(peer_id, @blocks_by_range_protocol_id, request, fn response -> + handle_blocks_by_range_response(response, slot, count, retries, peer_id, on_blocks) + end) + end + + defp handle_blocks_by_range_response(response, slot, count, retries, peer_id, on_blocks) do + with {:ok, response_message} <- response, + {:ok, blocks} <- ReqResp.decode_response(response_message, SignedBeaconBlock), :ok <- verify_batch(blocks, slot, count) do tags = %{result: "success", type: "by_slot", reason: "success"} :telemetry.execute([:network, :request], %{blocks: count}, tags) - {:ok, blocks} + on_blocks.({:ok, blocks}) else {:error, reason} -> tags = %{type: "by_slot", reason: parse_reason(reason)} @@ -48,41 +57,58 @@ defmodule LambdaEthereumConsensus.P2P.BlockDownloader do if retries > 0 do :telemetry.execute([:network, :request], %{blocks: 0}, Map.put(tags, :result, "retry")) Logger.debug("Retrying request for #{count} blocks", slot: slot) - request_blocks_by_range(slot, count, retries - 1) + request_blocks_by_range(slot, count, on_blocks, retries - 1) else :telemetry.execute([:network, :request], %{blocks: 0}, Map.put(tags, :result, "error")) + on_blocks.({:error, reason}) {:error, reason} end end end - @spec request_block_by_root(Types.root(), integer()) :: - {:ok, SignedBeaconBlock.t()} | {:error, binary()} - def request_block_by_root(root, retries \\ @default_retries) do - with {:ok, [block]} <- request_blocks_by_root([root], retries) do - {:ok, block} - end + @spec request_block_by_root( + Types.root(), + ({:ok, SignedBeaconBlock.t()} | {:error, binary()} -> :ok), + integer() + ) :: :ok + def request_block_by_root(root, on_block, retries \\ @default_retries) do + request_blocks_by_root( + [root], + fn + {:ok, [block]} -> on_block.({:ok, block}) + other -> on_block.(other) + end, + retries + ) end - @spec request_blocks_by_root([Types.root()], integer()) :: - {:ok, [SignedBeaconBlock.t()]} | {:error, binary()} - def request_blocks_by_root(roots, retries \\ @default_retries) + @spec request_blocks_by_root( + [Types.root()], + ({:ok, [SignedBeaconBlock.t()]} | {:error, binary()} -> :ok), + integer() + ) :: :ok + def request_blocks_by_root(roots, on_blocks, retries \\ @default_retries) - def request_blocks_by_root([], _retries), do: {:ok, []} + def request_blocks_by_root([], _on_blocks, _retries), do: {:ok, []} - def request_blocks_by_root(roots, retries) do + def request_blocks_by_root(roots, on_blocks, retries) do Logger.debug("Requesting block for roots #{Enum.map_join(roots, ", ", &Base.encode16/1)}") peer_id = get_some_peer() request = ReqResp.encode_request({roots, TypeAliases.beacon_blocks_by_root_request()}) - with {:ok, response} <- - Libp2pPort.send_async_request(peer_id, @blocks_by_root_protocol_id, request), - {:ok, blocks} <- ReqResp.decode_response(response, SignedBeaconBlock) do + Libp2pPort.send_async_request(peer_id, @blocks_by_root_protocol_id, request, fn response -> + handle_blocks_by_root_response(response, roots, on_blocks, peer_id, retries) + end) + end + + defp handle_blocks_by_root_response(response, roots, on_blocks, peer_id, retries) do + with {:ok, response_message} <- response, + {:ok, blocks} <- ReqResp.decode_response(response_message, SignedBeaconBlock) do tags = %{result: "success", type: "by_root", reason: "success"} :telemetry.execute([:network, :request], %{blocks: length(roots)}, tags) - {:ok, blocks} + on_blocks.({:ok, blocks}) else {:error, reason} -> tags = %{type: "by_root", reason: parse_reason(reason)} @@ -95,7 +121,7 @@ defmodule LambdaEthereumConsensus.P2P.BlockDownloader do request_blocks_by_root(roots, retries - 1) else :telemetry.execute([:network, :request], %{blocks: 0}, Map.put(tags, :result, "error")) - {:error, reason} + on_blocks.({:error, reason}) end end end diff --git a/lib/lambda_ethereum_consensus/p2p/requests.ex b/lib/lambda_ethereum_consensus/p2p/requests.ex new file mode 100644 index 000000000..4e3138eb9 --- /dev/null +++ b/lib/lambda_ethereum_consensus/p2p/requests.ex @@ -0,0 +1,48 @@ +defmodule LambdaEthereumConsensus.P2p.Requests do + @moduledoc """ + Uses uuids to identify requests and their handlers. Saves the handler in the struct until a + response is available and then handles appropriately. + """ + @type id :: binary + @type handler :: (term() -> term()) + @type requests :: %{id => handler} + + @doc """ + Creates a requests object that will hold response handlers. + """ + @spec new() :: requests() + def new(), do: %{} + + @doc """ + Adds a handler for a request. + + Returns a tuple {requests, request_id}, where: + - Requests is the modified requests object with the added handler. + - The id for the handler for that request. This will be used later when calling handle_response/3. + """ + @spec add_response_handler(requests(), handler()) :: {requests(), id()} + def add_response_handler(requests, handler) do + id = UUID.uuid4() + {Map.put(requests, id, handler), id} + end + + @doc """ + Handles a request using handler_id. The handler will be popped from the + requests object. + + Returns a {status, requests} tuple where: + - status is :ok if it was handled or :unhandled if the id didn't correspond to a saved handler. + - requests is the modified requests object with the handler removed. + """ + @spec handle_response(requests(), term(), id()) :: {:ok | :unhandled, requests()} + def handle_response(requests, response, handler_id) do + case Map.fetch(requests, handler_id) do + {:ok, handler} -> + handler.(response) + {:ok, Map.delete(requests, handler_id)} + + :error -> + {:unhandled, requests} + end + end +end diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index c93adb594..2fc4d43c8 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -10,8 +10,10 @@ defmodule LambdaEthereumConsensus.Libp2pPort do use GenServer alias LambdaEthereumConsensus.Beacon.BeaconChain + alias LambdaEthereumConsensus.Metrics alias LambdaEthereumConsensus.P2P.Gossip.BeaconBlock alias LambdaEthereumConsensus.P2P.Gossip.BlobSideCar + alias LambdaEthereumConsensus.P2p.Requests alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.Utils.BitVector alias Types.EnrForkId @@ -143,17 +145,23 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:ok, binary()} | {:error, String.t()} def send_request(pid \\ __MODULE__, peer_id, protocol_id, message) do :telemetry.execute([:port, :message], %{}, %{function: "send_request", direction: "elixir->"}) - c = %SendRequest{id: peer_id, protocol_id: protocol_id, message: message} - call_command(pid, {:send_request, c}) + from = self() + + GenServer.cast( + pid, + {:send_request, peer_id, protocol_id, message, + fn response -> send(from, {:response, response}) end} + ) + + receive_response() end @doc """ Sends a request to a peer. The response will be processed by the Libp2p process. """ - def send_async_request(pid \\ __MODULE__, peer_id, protocol_id, message) do + def send_async_request(pid \\ __MODULE__, peer_id, protocol_id, message, handler) do :telemetry.execute([:port, :message], %{}, %{function: "send_request", direction: "elixir->"}) - c = %SendRequest{id: peer_id, protocol_id: protocol_id, message: message} - cast_command(pid, {:send_request, c}) + GenServer.cast(pid, {:send_request, peer_id, protocol_id, message, handler}) end @doc """ @@ -349,12 +357,32 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:noreply, state} end + def handle_cast( + {:send_request, peer_id, protocol_id, message, handler}, + %{ + requests: requests, + port: port + } = state + ) do + {new_requests, handler_id} = Requests.add_response_handler(requests, handler) + + command = %Command{ + c: %SendRequest{ + id: peer_id, + protocol_id: protocol_id, + message: message, + request_id: handler_id + } + } + + send_data(port, Command.encode(command)) + {:noreply, state |> Map.put(:requests, new_requests)} + end + @impl GenServer def handle_info({_port, {:data, data}}, state) do %Notification{n: {_, payload}} = Notification.decode(data) - handle_notification(payload, state) - - {:noreply, state} + {:noreply, handle_notification(payload, state)} end @impl GenServer @@ -372,13 +400,15 @@ defmodule LambdaEthereumConsensus.Libp2pPort do ### PRIVATE FUNCTIONS ###################### - defp handle_notification(%GossipSub{} = gs, %{subscriptors: subscriptors}) do + defp handle_notification(%GossipSub{} = gs, %{subscriptors: subscriptors} = state) do :telemetry.execute([:port, :message], %{}, %{function: "gossipsub", direction: "->elixir"}) case Map.fetch(subscriptors, gs.topic) do {:ok, module} -> module.handle_gossip_message(gs.topic, gs.msg_id, gs.message) :error -> Logger.error("[Gossip] Received gossip from unknown topic: #{gs.topic}.") end + + state end defp handle_notification( @@ -388,103 +418,53 @@ defmodule LambdaEthereumConsensus.Libp2pPort do request_id: request_id, message: message }, - _state + state ) do :telemetry.execute([:port, :message], %{}, %{function: "request", direction: "->elixir"}) handler_pid = :erlang.binary_to_term(handler) send(handler_pid, {:request, {protocol_id, request_id, message}}) + state end - defp handle_notification(%NewPeer{peer_id: _peer_id}, %{new_peer_handler: nil}), do: :ok + defp handle_notification(%NewPeer{peer_id: _peer_id}, %{new_peer_handler: nil} = state), + do: state defp handle_notification(%NewPeer{peer_id: peer_id}, %{new_peer_handler: handler}) do :telemetry.execute([:port, :message], %{}, %{function: "new peer", direction: "->elixir"}) send(handler, {:new_peer, peer_id}) end - defp handle_notification(%Response{} = response, _state) do + defp handle_notification(%Response{} = response, %{requests: requests} = state) do :telemetry.execute([:port, :message], %{}, %{function: "response", direction: "->elixir"}) success = if response.success, do: :ok, else: :error - if response.from != "" do - send(:erlang.binary_to_term(response.from), {:response, {success, response.message}}) - else - handle_async_response(response.protocol_id, success, response.message) + {result, new_requests} = + Requests.handle_response(requests, {success, response.message}, response.id) + + if result == :unhandled do + Logger.error("Unhandled response with id: #{response.id}. Message: #{response.message}") end + + state |> Map.put(:requests, new_requests) end - defp handle_notification(%Result{from: "", result: result}, _state) do + defp handle_notification(%Result{from: "", result: result}, state) do :telemetry.execute([:port, :message], %{}, %{function: "result", direction: "->elixir"}) # TODO: amount of failures would be a useful metric _success_txt = if match?({:ok, _}, result), do: "success", else: "failed" + state end - defp handle_notification(%Result{from: from, result: result}, _state) do + defp handle_notification(%Result{from: from, result: result}, state) do :telemetry.execute([:port, :message], %{}, %{function: "result", direction: "->elixir"}) pid = :erlang.binary_to_term(from) send(pid, {:response, result}) + state end - defp handle_notification(%Tracer{t: {:add_peer, %{}}}, _state) do - :telemetry.execute([:network, :pubsub_peers], %{}, %{ - result: "add" - }) - end - - defp handle_notification(%Tracer{t: {:remove_peer, %{}}}, _state) do - :telemetry.execute([:network, :pubsub_peers], %{}, %{ - result: "remove" - }) - end - - defp handle_notification(%Tracer{t: {:joined, %{topic: topic}}}, _state) do - :telemetry.execute([:network, :pubsub_topic_active], %{active: 1}, %{ - topic: get_topic_name(topic) - }) - end - - defp handle_notification(%Tracer{t: {:left, %{topic: topic}}}, _state) do - :telemetry.execute([:network, :pubsub_topic_active], %{active: -1}, %{ - topic: get_topic_name(topic) - }) - end - - defp handle_notification(%Tracer{t: {:grafted, %{topic: topic}}}, _state) do - :telemetry.execute([:network, :pubsub_topics_graft], %{}, %{topic: get_topic_name(topic)}) - end - - defp handle_notification(%Tracer{t: {:pruned, %{topic: topic}}}, _state) do - :telemetry.execute([:network, :pubsub_topics_prune], %{}, %{topic: get_topic_name(topic)}) - end - - defp handle_notification(%Tracer{t: {:deliver_message, %{topic: topic}}}, _state) do - :telemetry.execute([:network, :pubsub_topics_deliver_message], %{}, %{ - topic: get_topic_name(topic) - }) - end - - defp handle_notification(%Tracer{t: {:duplicate_message, %{topic: topic}}}, _state) do - :telemetry.execute([:network, :pubsub_topics_duplicate_message], %{}, %{ - topic: get_topic_name(topic) - }) - end - - defp handle_notification(%Tracer{t: {:reject_message, %{topic: topic}}}, _state) do - :telemetry.execute([:network, :pubsub_topics_reject_message], %{}, %{ - topic: get_topic_name(topic) - }) - end - - defp handle_notification(%Tracer{t: {:un_deliverable_message, %{topic: topic}}}, _state) do - :telemetry.execute([:network, :pubsub_topics_un_deliverable_message], %{}, %{ - topic: get_topic_name(topic) - }) - end - - defp handle_notification(%Tracer{t: {:validate_message, %{topic: topic}}}, _state) do - :telemetry.execute([:network, :pubsub_topics_validate_message], %{}, %{ - topic: get_topic_name(topic) - }) + defp handle_notification(%Tracer{t: notification}, state) do + Metrics.tracer(notification) + state end defp parse_args(args) do @@ -516,13 +496,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:response, {:node_identity, identity}} -> identity {:response, {res, %ResultMessage{message: []}}} -> res {:response, {res, %ResultMessage{message: message}}} -> [res | message] |> List.to_tuple() - end - end - - defp get_topic_name(topic) do - case topic |> String.split("/") |> Enum.fetch(3) do - {:ok, name} -> name - :error -> topic + {:response, {res, %Response{} = response}} -> {res, response} end end @@ -552,9 +526,4 @@ defmodule LambdaEthereumConsensus.Libp2pPort do } |> encode_enr(attnets, syncnets) end - - @spec handle_async_response(binary(), :ok | :error, binary()) :: :ok - defp handle_async_response(protocol_id, _success?, _message) do - Logger.warning("Received unhandled response from protocol #{protocol_id}.") - end end diff --git a/mix.exs b/mix.exs index 554f45fb8..adfc7e9a2 100644 --- a/mix.exs +++ b/mix.exs @@ -69,7 +69,8 @@ defmodule LambdaEthereumConsensus.MixProject do {:prometheus_process_collector, git: "https://github.com/lambdaclass/prometheus_process_collector", branch: "update-makefile-to-support-otp-26", - override: true} + override: true}, + {:uuid, "~> 1.1"} ] end diff --git a/mix.lock b/mix.lock index 3157ed063..719ac0b6f 100644 --- a/mix.lock +++ b/mix.lock @@ -76,6 +76,7 @@ "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, diff --git a/native/libp2p_port/internal/proto_helpers/proto_helpers.go b/native/libp2p_port/internal/proto_helpers/proto_helpers.go index 003be2097..aeaf99222 100644 --- a/native/libp2p_port/internal/proto_helpers/proto_helpers.go +++ b/native/libp2p_port/internal/proto_helpers/proto_helpers.go @@ -144,22 +144,22 @@ func ResultNotification(from []byte, result []byte, err error) *proto_defs.Notif return &proto_defs.Notification{N: &proto_defs.Notification_Result{Result: responseNotification}} } -func ResponseNotification(from []byte, result []byte, err error, protocolId string) *proto_defs.Notification { - var message []byte +func ResponseNotification(request_id []byte, result []byte, err error, protocolId string, requestMessage []byte) *proto_defs.Notification { + var responseMessage []byte var success bool if err != nil { success = false - message = []byte(err.Error()) + responseMessage = []byte(err.Error()) } else { success = true if result != nil { - message = result + responseMessage = result } else { - message = []byte{} + responseMessage = []byte{} } } - response := &proto_defs.Response{From: from, Message: message, Success: success, ProtocolId: []byte(protocolId)} + response := &proto_defs.Response{Id: request_id, Success: success, Message: responseMessage} return &proto_defs.Notification{N: &proto_defs.Notification_Response{Response: response}} } diff --git a/native/libp2p_port/internal/reqresp/reqresp.go b/native/libp2p_port/internal/reqresp/reqresp.go index a7013752f..99c9eec58 100644 --- a/native/libp2p_port/internal/reqresp/reqresp.go +++ b/native/libp2p_port/internal/reqresp/reqresp.go @@ -95,7 +95,7 @@ func (l *Listener) SendRequest(from, peerId []byte, protocolId string, message [ func sendAsyncRequest(h host.Host, p port.Port, from []byte, peerId peer.ID, protocolId protocol.ID, message []byte) { response, err := sendRequest(h, peerId, protocolId, message) - result := proto_helpers.ResponseNotification([]byte(from), response, err, string(protocolId)) + result := proto_helpers.ResponseNotification([]byte(from), response, err, string(protocolId), message) p.SendNotification(result) } diff --git a/proto/libp2p.proto b/proto/libp2p.proto index 0a074d54c..3ec64d25d 100644 --- a/proto/libp2p.proto +++ b/proto/libp2p.proto @@ -80,10 +80,14 @@ message AddPeer { int64 ttl = 3; } +// Outgoing request to be sent from this node to a different one. message SendRequest { + // Peer id bytes id = 1; string protocol_id = 2; bytes message = 3; + // internal identifier for our request + bytes request_id = 4; } message SendResponse { @@ -182,10 +186,9 @@ message Result { } message Response { - bool success = 1; - bytes from = 2; - bytes protocol_id = 3; - bytes message = 4; + bytes id = 1; + bool success = 2; + bytes message = 3; } message Tracer { diff --git a/test/unit/p2p/requests_test.exs b/test/unit/p2p/requests_test.exs new file mode 100644 index 000000000..9e7274add --- /dev/null +++ b/test/unit/p2p/requests_test.exs @@ -0,0 +1,37 @@ +defmodule Unit.P2p.RequestsTest do + use ExUnit.Case + + alias LambdaEthereumConsensus.P2p.Requests + + test "An empty requests object shouldn't handle a request" do + requests = Requests.new() + + assert {:unhandled, requests} == + Requests.handle_response(requests, "some response", "fake id") + end + + test "A requests object should handler a request only once" do + requests = Requests.new() + pid = self() + + {requests_2, handler_id} = + Requests.add_response_handler( + requests, + fn response -> send(pid, response) end + ) + + {:ok, requests_3} = Requests.handle_response(requests_2, "some response", handler_id) + + response = + receive do + response -> response + end + + assert response == "some response" + + assert requests_3 == requests + + assert {:unhandled, requests_3} == + Requests.handle_response(requests, "some response", handler_id) + end +end From fd1ed80094dbfaf3d814af3e6285c51a66c57b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 17 Jun 2024 12:14:35 +0200 Subject: [PATCH 20/37] compile without warnings --- .../beacon/pending_blocks.ex | 8 +++++++- .../p2p/blob_downloader.ex | 19 ++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 9fa4bf8d7..79a9b1d9f 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -44,7 +44,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do if Enum.empty?(missing_blobs) do block_info else - BlobDownloader.request_blobs_by_root(missing_blobs) + BlobDownloader.request_blobs_by_root(missing_blobs, &process_blobs/1) block_info |> BlockInfo.change_status(:download_blobs) end |> Blocks.new_block_info() @@ -132,6 +132,12 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end end + defp process_blobs({:ok, blobs}), do: add_blobs(blobs) + + defp process_blobs({:error, reason}) do + Logger.error("Error downloading blobs: #{inspect(reason)}") + end + @spec missing_blobs(BlockInfo.t()) :: [Types.BlobIdentifier.t()] defp missing_blobs(%BlockInfo{root: root, signed_block: signed_block}) do signed_block.message.body.blob_kzg_commitments diff --git a/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex b/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex index 70848b581..0e0291495 100644 --- a/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex +++ b/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex @@ -13,6 +13,7 @@ defmodule LambdaEthereumConsensus.P2P.BlobDownloader do @blobs_by_root_protocol_id "/eth2/beacon_chain/req/blob_sidecars_by_root/1/ssz_snappy" @type on_blobs :: ({:ok, [BlobSidecar.t()]} | {:error, any()} -> :ok) + @type on_blob :: ({:ok, BlobSidecar.t()} | {:error, any()} -> :ok) # Requests to peers might fail for various reasons, # for example they might not support the protocol or might not reply @@ -59,18 +60,22 @@ defmodule LambdaEthereumConsensus.P2P.BlobDownloader do end end - @spec request_blob_by_root(Types.BlobIdentifier.t(), non_neg_integer()) :: - {:ok, BlobSidecar.t()} | {:error, binary()} - def request_blob_by_root(identifier, retries \\ @default_retries) do - with {:ok, [blob]} <- request_blobs_by_root([identifier], retries) do - {:ok, blob} - end + @spec request_blob_by_root(Types.BlobIdentifier.t(), on_blob(), non_neg_integer()) :: :ok + def request_blob_by_root(identifier, on_blob, retries \\ @default_retries) do + request_blobs_by_root( + [identifier], + fn + {:ok, [blob]} -> on_blob.({:ok, blob}) + other -> on_blob.(other) + end, + retries + ) end @spec request_blobs_by_root([Types.BlobIdentifier.t()], on_blobs(), non_neg_integer()) :: :ok def request_blobs_by_root(identifiers, on_blobs, retries \\ @default_retries) - def request_blobs_by_root([], on_blobs, _retries), do: {:ok, []} + def request_blobs_by_root([], _on_blobs, _retries), do: {:ok, []} def request_blobs_by_root(identifiers, on_blobs, retries) do Logger.debug("Requesting #{length(identifiers)} blobs.") From 5ac39eee86446f479aeb7f402902356327afcd93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 17 Jun 2024 13:29:00 +0200 Subject: [PATCH 21/37] add sync block download. fix lint dialyzer and sync_blocks. --- .../beacon/sync_blocks.ex | 2 +- lib/lambda_ethereum_consensus/metrics.ex | 4 ++ .../p2p/block_downloader.ex | 39 +++++++++++++++---- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex index 6f8418f26..5e87ea2a6 100644 --- a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex @@ -93,7 +93,7 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do @spec fetch_blocks_by_slot(Types.slot(), non_neg_integer()) :: {:ok, [SignedBeaconBlock.t()]} | {:error, String.t()} def fetch_blocks_by_slot(from, count) do - case BlockDownloader.request_blocks_by_range(from, count, 0) do + case BlockDownloader.request_blocks_by_range_sync(from, count, 0) do {:ok, blocks} -> {:ok, blocks} diff --git a/lib/lambda_ethereum_consensus/metrics.ex b/lib/lambda_ethereum_consensus/metrics.ex index e6a92d137..d2894d9ec 100644 --- a/lib/lambda_ethereum_consensus/metrics.ex +++ b/lib/lambda_ethereum_consensus/metrics.ex @@ -1,4 +1,8 @@ defmodule LambdaEthereumConsensus.Metrics do + @moduledoc """ + Basic telemetry metric generation to be used across the node. + """ + def tracer({:joined, %{topic: topic}}, _state) do :telemetry.execute([:network, :pubsub_topic_active], %{active: 1}, %{ topic: get_topic_name(topic) diff --git a/lib/lambda_ethereum_consensus/p2p/block_downloader.ex b/lib/lambda_ethereum_consensus/p2p/block_downloader.ex index 7677fb769..de021a330 100644 --- a/lib/lambda_ethereum_consensus/p2p/block_downloader.ex +++ b/lib/lambda_ethereum_consensus/p2p/block_downloader.ex @@ -17,15 +17,40 @@ defmodule LambdaEthereumConsensus.P2P.BlockDownloader do # so we want to try again with a different peer @default_retries 5 - @spec request_blocks_by_range( - Types.slot(), - non_neg_integer(), - ({:ok, [SignedBeaconBlock.t()]} | {:error, any()} -> term()), - non_neg_integer() - ) :: :ok + @type download_result :: {:ok, [SignedBeaconBlock.t()]} | {:error, any()} + @type on_blocks :: (download_result() -> term()) + + @doc """ + Requests a series of blocks in batch, and synchronously (the caller will block waiting for the + result). As this is a synchronous function, the caller process must be different than Libp2pPort. + + Arguments: + - slot: the slot that marks the start of the requested range. + - count: the amount of blocks that will be requested for download. + - retries (optional): if the download fails the request will retry, using a different random + peer. This argument determines the amount of times that will happen before returning an error. + """ + @spec request_blocks_by_range_sync(Types.slot(), non_neg_integer(), non_neg_integer()) :: + download_result() + def request_blocks_by_range_sync(slot, count, retries \\ @default_retries) + + def request_blocks_by_range_sync(_slot, 0, _retries), do: {:ok, []} + + def request_blocks_by_range_sync(slot, count, retries) do + pid = self() + request_blocks_by_range(slot, count, fn result -> send(pid, result) end, retries) + + receive do + result -> result + end + end + + @spec request_blocks_by_range(Types.slot(), non_neg_integer(), on_blocks(), non_neg_integer()) :: + :ok + @spec request_blocks_by_range(Types.slot(), non_neg_integer(), on_blocks()) :: :ok def request_blocks_by_range(slot, count, on_blocks, retries \\ @default_retries) - def request_blocks_by_range(_slot, 0, _on_blocks, _retries), do: {:ok, []} + def request_blocks_by_range(_slot, 0, _on_blocks, _retries), do: :ok def request_blocks_by_range(slot, count, on_blocks, retries) do Logger.debug("Requesting block", slot: slot) From 7d657558b4613bb7896be9b90c6dbd909fe5f747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 17 Jun 2024 18:42:24 +0200 Subject: [PATCH 22/37] fix tracer call, state return, send_request command, add some flatmaps --- .../beacon/beacon_node.ex | 1 - .../beacon/sync_blocks.ex | 18 +++++++++---- lib/lambda_ethereum_consensus/metrics.ex | 12 +++++++-- lib/libp2p_port.ex | 25 ++++++++++++------- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex index f1c5287a7..334f369c3 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex @@ -50,7 +50,6 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do {LambdaEthereumConsensus.Libp2pPort, libp2p_args}, LambdaEthereumConsensus.P2P.Peerbook, LambdaEthereumConsensus.P2P.IncomingRequests, - LambdaEthereumConsensus.Beacon.PendingBlocks, LambdaEthereumConsensus.Beacon.SyncBlocks, LambdaEthereumConsensus.P2P.Gossip.Attestation, LambdaEthereumConsensus.P2P.Gossip.OperationsCollector, diff --git a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex index 5e87ea2a6..0e8713fd3 100644 --- a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex @@ -64,15 +64,23 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do end) results - |> Enum.filter(fn result -> match?({:ok, _}, result) end) - |> Enum.map(fn {:ok, blocks} -> blocks end) - |> List.flatten() + |> Enum.flat_map(fn + {:ok, blocks} -> blocks + _other -> [] + end) + |> tap(fn blocks -> Logger.notice("Downloaded #{length(blocks)} blocks successfully.") end) |> Enum.each(&PendingBlocks.add_block/1) remaining_chunks = Enum.zip(chunks, results) - |> Enum.filter(fn {_chunk, result} -> match?({:error, _}, result) end) - |> Enum.map(fn {chunk, _} -> chunk end) + |> Enum.flat_map(fn + {chunk, {:error, reason}} -> + Logger.error(inspect(reason)) + [chunk] + + _other -> + [] + end) if Enum.empty?(chunks) do Logger.info("[Optimistic Sync] Sync completed") diff --git a/lib/lambda_ethereum_consensus/metrics.ex b/lib/lambda_ethereum_consensus/metrics.ex index d2894d9ec..f7bd6b03c 100644 --- a/lib/lambda_ethereum_consensus/metrics.ex +++ b/lib/lambda_ethereum_consensus/metrics.ex @@ -3,13 +3,21 @@ defmodule LambdaEthereumConsensus.Metrics do Basic telemetry metric generation to be used across the node. """ - def tracer({:joined, %{topic: topic}}, _state) do + def tracer({:add_peer, %{}}) do + :telemetry.execute([:network, :pubsub_peers], %{}, %{result: "add"}) + end + + def tracer({:remove_peer, %{}}) do + :telemetry.execute([:network, :pubsub_peers], %{}, %{result: "remove"}) + end + + def tracer({:joined, %{topic: topic}}) do :telemetry.execute([:network, :pubsub_topic_active], %{active: 1}, %{ topic: get_topic_name(topic) }) end - def tracer({:left, %{topic: topic}}, _state) do + def tracer({:left, %{topic: topic}}) do :telemetry.execute([:network, :pubsub_topic_active], %{active: -1}, %{ topic: get_topic_name(topic) }) diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 2fc4d43c8..97577b9ee 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -329,7 +329,13 @@ defmodule LambdaEthereumConsensus.Libp2pPort do if join_init_topics, do: join_init_topics(port) - {:ok, %{port: port, new_peer_handler: new_peer_handler, subscriptors: %{}}} + {:ok, + %{ + port: port, + new_peer_handler: new_peer_handler, + subscriptors: %{}, + requests: Requests.new() + }} end @impl GenServer @@ -366,15 +372,15 @@ defmodule LambdaEthereumConsensus.Libp2pPort do ) do {new_requests, handler_id} = Requests.add_response_handler(requests, handler) - command = %Command{ - c: %SendRequest{ - id: peer_id, - protocol_id: protocol_id, - message: message, - request_id: handler_id - } + send_request = %SendRequest{ + id: peer_id, + protocol_id: protocol_id, + message: message, + request_id: handler_id } + command = %Command{c: {:send_request, send_request}} + send_data(port, Command.encode(command)) {:noreply, state |> Map.put(:requests, new_requests)} end @@ -429,9 +435,10 @@ defmodule LambdaEthereumConsensus.Libp2pPort do defp handle_notification(%NewPeer{peer_id: _peer_id}, %{new_peer_handler: nil} = state), do: state - defp handle_notification(%NewPeer{peer_id: peer_id}, %{new_peer_handler: handler}) do + defp handle_notification(%NewPeer{peer_id: peer_id}, %{new_peer_handler: handler} = state) do :telemetry.execute([:port, :message], %{}, %{function: "new peer", direction: "->elixir"}) send(handler, {:new_peer, peer_id}) + state end defp handle_notification(%Response{} = response, %{requests: requests} = state) do From a49e5da492a4b1ddb64945508e17038cc643774c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 17 Jun 2024 20:11:08 +0200 Subject: [PATCH 23/37] add request id in go response --- lib/lambda_ethereum_consensus/beacon/sync_blocks.ex | 11 +++++++++-- .../internal/proto_helpers/proto_helpers.go | 4 ++-- native/libp2p_port/internal/reqresp/reqresp.go | 8 ++++---- native/libp2p_port/main.go | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex index 0e8713fd3..21c800ebd 100644 --- a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex @@ -68,14 +68,17 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do {:ok, blocks} -> blocks _other -> [] end) - |> tap(fn blocks -> Logger.notice("Downloaded #{length(blocks)} blocks successfully.") end) + |> tap(fn blocks -> Logger.info("Downloaded #{length(blocks)} blocks successfully.") end) |> Enum.each(&PendingBlocks.add_block/1) remaining_chunks = Enum.zip(chunks, results) |> Enum.flat_map(fn {chunk, {:error, reason}} -> - Logger.error(inspect(reason)) + Logger.error( + "Failed downloading the chunk #{inspect(chunk)}. Reason: #{inspect(reason)}" + ) + [chunk] _other -> @@ -101,8 +104,12 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do @spec fetch_blocks_by_slot(Types.slot(), non_neg_integer()) :: {:ok, [SignedBeaconBlock.t()]} | {:error, String.t()} def fetch_blocks_by_slot(from, count) do + Logger.info("Fetching #{count} blocks from #{from}") + case BlockDownloader.request_blocks_by_range_sync(from, count, 0) do {:ok, blocks} -> + Logger.info("Fetched #{count} blocks from #{from}") + {:ok, blocks} {:error, error} -> diff --git a/native/libp2p_port/internal/proto_helpers/proto_helpers.go b/native/libp2p_port/internal/proto_helpers/proto_helpers.go index aeaf99222..fb5876d5f 100644 --- a/native/libp2p_port/internal/proto_helpers/proto_helpers.go +++ b/native/libp2p_port/internal/proto_helpers/proto_helpers.go @@ -144,7 +144,7 @@ func ResultNotification(from []byte, result []byte, err error) *proto_defs.Notif return &proto_defs.Notification{N: &proto_defs.Notification_Result{Result: responseNotification}} } -func ResponseNotification(request_id []byte, result []byte, err error, protocolId string, requestMessage []byte) *proto_defs.Notification { +func ResponseNotification(requestId []byte, result []byte, err error, protocolId string, requestMessage []byte) *proto_defs.Notification { var responseMessage []byte var success bool @@ -159,7 +159,7 @@ func ResponseNotification(request_id []byte, result []byte, err error, protocolI responseMessage = []byte{} } } - response := &proto_defs.Response{Id: request_id, Success: success, Message: responseMessage} + response := &proto_defs.Response{Id: requestId, Success: success, Message: responseMessage} return &proto_defs.Notification{N: &proto_defs.Notification_Response{Response: response}} } diff --git a/native/libp2p_port/internal/reqresp/reqresp.go b/native/libp2p_port/internal/reqresp/reqresp.go index 99c9eec58..1ddd5fad5 100644 --- a/native/libp2p_port/internal/reqresp/reqresp.go +++ b/native/libp2p_port/internal/reqresp/reqresp.go @@ -89,13 +89,13 @@ func (l *Listener) AddPeerWithAddrInfo(addrInfo peer.AddrInfo, ttl int64) { l.port.SendNotification(¬ification) } -func (l *Listener) SendRequest(from, peerId []byte, protocolId string, message []byte) { - go sendAsyncRequest(l.hostHandle, *l.port, from, peer.ID(peerId), protocol.ID(protocolId), message) +func (l *Listener) SendRequest(peerId []byte, protocolId string, message []byte, requestId []byte) { + go sendAsyncRequest(l.hostHandle, *l.port, peer.ID(peerId), protocol.ID(protocolId), message, requestId) } -func sendAsyncRequest(h host.Host, p port.Port, from []byte, peerId peer.ID, protocolId protocol.ID, message []byte) { +func sendAsyncRequest(h host.Host, p port.Port, peerId peer.ID, protocolId protocol.ID, message []byte, requestId []byte) { response, err := sendRequest(h, peerId, protocolId, message) - result := proto_helpers.ResponseNotification([]byte(from), response, err, string(protocolId), message) + result := proto_helpers.ResponseNotification(requestId, response, err, string(protocolId), message) p.SendNotification(result) } diff --git a/native/libp2p_port/main.go b/native/libp2p_port/main.go index 91b9ef617..b9f263d7f 100644 --- a/native/libp2p_port/main.go +++ b/native/libp2p_port/main.go @@ -20,7 +20,7 @@ func handleCommand(command *proto_defs.Command, listener *reqresp.Listener, subs case *proto_defs.Command_AddPeer: listener.AddPeer(c.AddPeer.Id, c.AddPeer.Addrs, c.AddPeer.Ttl) case *proto_defs.Command_SendRequest: - listener.SendRequest(command.From, c.SendRequest.Id, c.SendRequest.ProtocolId, c.SendRequest.Message) + listener.SendRequest(c.SendRequest.Id, c.SendRequest.ProtocolId, c.SendRequest.Message, c.SendRequest.RequestId) return nil // No response case *proto_defs.Command_SendResponse: listener.SendResponse(c.SendResponse.RequestId, c.SendResponse.Message) From f71bd4668da8a30764f037bc0088f54255d3aa64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 25 Jun 2024 17:56:27 +0200 Subject: [PATCH 24/37] fix download blob recursive calls --- lib/lambda_ethereum_consensus/p2p/blob_downloader.ex | 4 ++-- lib/lambda_ethereum_consensus/p2p/block_downloader.ex | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex b/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex index 0e0291495..6888ba9aa 100644 --- a/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex +++ b/lib/lambda_ethereum_consensus/p2p/blob_downloader.ex @@ -53,7 +53,7 @@ defmodule LambdaEthereumConsensus.P2P.BlobDownloader do if retries > 0 do Logger.debug("Retrying request for #{count} blobs", slot: slot) - request_blobs_by_range(slot, count, retries - 1) + request_blobs_by_range(slot, count, on_blobs, retries - 1) else on_blobs.({:error, reason}) end @@ -99,7 +99,7 @@ defmodule LambdaEthereumConsensus.P2P.BlobDownloader do if retries > 0 do Logger.debug("Retrying request for blobs.") - request_blobs_by_root(identifiers, retries - 1) + request_blobs_by_root(identifiers, on_blobs, retries - 1) else on_blobs.({:error, reason}) end diff --git a/lib/lambda_ethereum_consensus/p2p/block_downloader.ex b/lib/lambda_ethereum_consensus/p2p/block_downloader.ex index de021a330..702f7a84b 100644 --- a/lib/lambda_ethereum_consensus/p2p/block_downloader.ex +++ b/lib/lambda_ethereum_consensus/p2p/block_downloader.ex @@ -143,7 +143,7 @@ defmodule LambdaEthereumConsensus.P2P.BlockDownloader do :telemetry.execute([:network, :request], %{blocks: 0}, Map.put(tags, :result, "retry")) pretty_roots = Enum.map_join(roots, ", ", &Base.encode16/1) Logger.debug("Retrying request for blocks with roots #{pretty_roots}") - request_blocks_by_root(roots, retries - 1) + request_blocks_by_root(roots, on_blocks, retries - 1) else :telemetry.execute([:network, :request], %{blocks: 0}, Map.put(tags, :result, "error")) on_blocks.({:error, reason}) From 8c37cf5edc93a5c09680ac12ad2c62bd4eecf873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 25 Jun 2024 19:50:13 +0200 Subject: [PATCH 25/37] fix sync requests --- lib/libp2p_port.ex | 2 +- test/unit/libp2p_port_test.exs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 01b177005..c33710a4d 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -483,7 +483,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:response, {:node_identity, identity}} -> identity {:response, {res, %ResultMessage{message: []}}} -> res {:response, {res, %ResultMessage{message: message}}} -> [res | message] |> List.to_tuple() - {:response, {res, %Response{} = response}} -> {res, response} + {:response, {res, response}} -> {res, response} end end diff --git a/test/unit/libp2p_port_test.exs b/test/unit/libp2p_port_test.exs index c4ceb2a8c..1bb20a505 100644 --- a/test/unit/libp2p_port_test.exs +++ b/test/unit/libp2p_port_test.exs @@ -52,8 +52,6 @@ defmodule Unit.Libp2pPortTest do # (recver) Read the "ping" message assert {^protocol_id, id, "ping"} = Libp2pPort.handle_request() :ok = Libp2pPort.send_response(:recver, id, "pong") - - send(pid, :message_received) end) # (sender) Wait for handler to be set @@ -64,7 +62,6 @@ defmodule Unit.Libp2pPortTest do # (sender) Send "ping" to recver and receive "pong" assert {:ok, "pong"} = Libp2pPort.send_request(:sender, id, protocol_id, "ping") - assert_receive :message_received, 1000 end # TODO: flaky test, fix From e06036f08f124013b3f121d3c94d3fd3b3ed7e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Thu, 27 Jun 2024 12:38:21 +0200 Subject: [PATCH 26/37] improve logs --- .../beacon/sync_blocks.ex | 35 ++++++++++++------- .../p2p/gossip/beacon_block.ex | 21 +++++++---- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex index 0fbfb6aa6..74fa8e6b3 100644 --- a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex @@ -31,19 +31,24 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do # If we're around genesis, we consider ourselves synced if last_slot > 0 do - Enum.chunk_every(initial_slot..last_slot, @blocks_per_chunk) - |> Enum.map(fn chunk -> - first_slot = List.first(chunk) - last_slot = List.last(chunk) - count = last_slot - first_slot + 1 - %{from: first_slot, count: count} - end) - |> perform_sync() + perform_sync(initial_slot, last_slot) else start_subscriptions() end end + @spec perform_sync(integer(), integer()) :: :ok + def perform_sync(initial_slot, last_slot) do + Enum.chunk_every(initial_slot..last_slot, @blocks_per_chunk) + |> Enum.map(fn chunk -> + first_slot = List.first(chunk) + last_slot = List.last(chunk) + count = last_slot - first_slot + 1 + %{from: first_slot, count: count} + end) + |> perform_sync() + end + @spec perform_sync([chunk()]) :: :ok def perform_sync(chunks) do remaining = chunks |> Stream.map(fn %{count: c} -> c end) |> Enum.sum() @@ -68,16 +73,20 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do {:ok, blocks} -> blocks _other -> [] end) - |> tap(fn blocks -> Logger.info("Downloaded #{length(blocks)} blocks successfully.") end) + |> tap(fn blocks -> + Logger.info("[Optimistic Sync] Downloaded #{length(blocks)} blocks successfully.") + end) |> Enum.each(&PendingBlocks.add_block/1) remaining_chunks = Enum.zip(chunks, results) |> Enum.flat_map(fn {chunk, {:error, reason}} -> - Logger.error( - "Failed downloading the chunk #{inspect(chunk)}. Reason: #{inspect(reason)}" - ) + if not String.contains?(inspect(reason), "failed to dial") do + Logger.debug( + "[Optimistic Sync] Failed downloading the chunk #{inspect(chunk)}. Reason: #{inspect(reason)}" + ) + end [chunk] @@ -104,7 +113,7 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do @spec fetch_blocks_by_slot(Types.slot(), non_neg_integer()) :: {:ok, [SignedBeaconBlock.t()]} | {:error, String.t()} def fetch_blocks_by_slot(from, count) do - Logger.info("Fetching #{count} blocks from #{from}") + Logger.info("[Optimistic Sync] Fetching #{count} blocks starting from slot #{from}.") case BlockDownloader.request_blocks_by_range_sync(from, count, 0) do {:ok, blocks} -> diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex b/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex index 94f598de0..7ea2f008e 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex @@ -22,16 +22,16 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.BeaconBlock do with {:ok, uncompressed} <- :snappyer.decompress(message), {:ok, signed_block} <- Ssz.from_ssz(uncompressed, SignedBeaconBlock), :ok <- validate(signed_block, slot) do - Logger.info("[Gossip] Block received", slot: signed_block.message.slot) + Logger.info("[Gossip] Block received, block.slot: #{signed_block.message.slot}.") Libp2pPort.validate_message(msg_id, :accept) PendingBlocks.add_block(signed_block) else {:ignore, reason} -> - Logger.warning("[Gossip] Block ignored, reason: #{inspect(reason)}", slot: slot) + Logger.warning("[Gossip] Block ignored, reason: #{inspect(reason)}.") Libp2pPort.validate_message(msg_id, :ignore) {:error, reason} -> - Logger.warning("[Gossip] Block rejected, reason: #{inspect(reason)}", slot: slot) + Logger.warning("[Gossip] Block rejected, reason: #{inspect(reason)}.") Libp2pPort.validate_message(msg_id, :reject) end @@ -63,11 +63,20 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.BeaconBlock do @spec validate(SignedBeaconBlock.t(), Types.slot()) :: :ok | {:error, any} defp validate(%SignedBeaconBlock{message: block}, current_slot) do + min_slot = current_slot - ChainSpec.get("SLOTS_PER_EPOCH") + cond do # TODO incorporate MAXIMUM_GOSSIP_CLOCK_DISPARITY into future block calculations - block.slot <= current_slot - ChainSpec.get("SLOTS_PER_EPOCH") -> {:ignore, :block_too_old} - block.slot > current_slot -> {:ignore, :block_from_future} - true -> :ok + block.slot <= min_slot -> + {:ignore, + "Block too old: block.slot=#{block.slot}. Current slot: #{current_slot}. Minimum expected slot: #{min_slot}"} + + block.slot > current_slot -> + {:ignore, + "Block is from the future: block.slot=#{block.slot}. Current slot: #{current_slot}."} + + true -> + :ok end end end From fb789d4c31f44e063f2f484ad2d1f73a56fa2e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 1 Jul 2024 19:11:26 +0200 Subject: [PATCH 27/37] blocks move through libp2p port --- .../beacon/pending_blocks.ex | 4 ++++ lib/lambda_ethereum_consensus/beacon/sync_blocks.ex | 8 ++------ lib/lambda_ethereum_consensus/metrics.ex | 10 ++++++++++ lib/lambda_ethereum_consensus/store/blocks.ex | 11 +++++++++++ lib/lambda_ethereum_consensus/telemetry.ex | 3 ++- lib/libp2p_port.ex | 8 ++++++++ mix.exs | 2 +- 7 files changed, 38 insertions(+), 8 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 79a9b1d9f..2388e2a33 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -92,6 +92,10 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do {:ok, blocks} -> blocks |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) + |> tap(fn sorted_blocks -> + Enum.frequencies_by(sorted_blocks, fn b -> b.status end) + |> IO.inspect(label: "statuses") + end) |> Enum.each(&process_block/1) {:error, reason} -> diff --git a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex index 74fa8e6b3..6c80b8266 100644 --- a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex @@ -8,7 +8,7 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do require Logger alias LambdaEthereumConsensus.Beacon.BeaconChain - alias LambdaEthereumConsensus.Beacon.PendingBlocks + alias LambdaEthereumConsensus.Libp2pPort alias LambdaEthereumConsensus.P2P.BlockDownloader alias LambdaEthereumConsensus.P2P.Gossip alias LambdaEthereumConsensus.StateTransition.Misc @@ -76,7 +76,7 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do |> tap(fn blocks -> Logger.info("[Optimistic Sync] Downloaded #{length(blocks)} blocks successfully.") end) - |> Enum.each(&PendingBlocks.add_block/1) + |> Enum.each(&Libp2pPort.add_block/1) remaining_chunks = Enum.zip(chunks, results) @@ -113,12 +113,8 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do @spec fetch_blocks_by_slot(Types.slot(), non_neg_integer()) :: {:ok, [SignedBeaconBlock.t()]} | {:error, String.t()} def fetch_blocks_by_slot(from, count) do - Logger.info("[Optimistic Sync] Fetching #{count} blocks starting from slot #{from}.") - case BlockDownloader.request_blocks_by_range_sync(from, count, 0) do {:ok, blocks} -> - Logger.info("Fetched #{count} blocks from #{from}") - {:ok, blocks} {:error, error} -> diff --git a/lib/lambda_ethereum_consensus/metrics.ex b/lib/lambda_ethereum_consensus/metrics.ex index f7bd6b03c..6d7748959 100644 --- a/lib/lambda_ethereum_consensus/metrics.ex +++ b/lib/lambda_ethereum_consensus/metrics.ex @@ -67,4 +67,14 @@ defmodule LambdaEthereumConsensus.Metrics do :error -> topic end end + + def block_status(root, status) do + hex_root = root |> Base.encode16() + + :telemetry.execute([:blocks, :status], %{}, %{ + mainstat: status, + id: hex_root, + title: hex_root + }) + end end diff --git a/lib/lambda_ethereum_consensus/store/blocks.ex b/lib/lambda_ethereum_consensus/store/blocks.ex index cc19bddcf..d6d555a03 100644 --- a/lib/lambda_ethereum_consensus/store/blocks.ex +++ b/lib/lambda_ethereum_consensus/store/blocks.ex @@ -2,8 +2,10 @@ defmodule LambdaEthereumConsensus.Store.Blocks do @moduledoc """ Interface to `Store.blocks`. """ + alias LambdaEthereumConsensus.Metrics alias LambdaEthereumConsensus.Store.BlockDb alias LambdaEthereumConsensus.Store.LRUCache + alias LambdaEthereumConsensus.Utils alias Types.BeaconBlock alias Types.BlockInfo @@ -81,10 +83,19 @@ defmodule LambdaEthereumConsensus.Store.Blocks do @spec change_status(BlockInfo.t(), BlockInfo.block_status()) :: :ok def change_status(block_info, status) do + Metrics.block_status(block_info.root, status) + + IO.puts( + "Block: #{Utils.format_shorten_binary(block_info.root)}. Changing status from #{block_info.status} to #{status}" + ) + old_status = block_info.status block_info |> BlockInfo.change_status(status) + |> tap(fn bi -> + IO.puts("Status for #{Utils.format_shorten_binary(bi.root)}: #{bi.status}") + end) |> store_block_info() BlockDb.change_root_status(block_info.root, old_status, status) diff --git a/lib/lambda_ethereum_consensus/telemetry.ex b/lib/lambda_ethereum_consensus/telemetry.ex index 2a9b0b8d7..33b01e8a5 100644 --- a/lib/lambda_ethereum_consensus/telemetry.ex +++ b/lib/lambda_ethereum_consensus/telemetry.ex @@ -139,7 +139,8 @@ defmodule LambdaEthereumConsensus.Telemetry do ), last_value("fork_choice.recompute_head.exception.duration", unit: {:native, :millisecond} - ) + ), + counter("blocks.status.count", tags: [:title, :mainstat, :id]) ] end diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index c33710a4d..72b13b2e9 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -10,6 +10,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do use GenServer alias LambdaEthereumConsensus.Beacon.BeaconChain + alias LambdaEthereumConsensus.Beacon.PendingBlocks alias LambdaEthereumConsensus.Metrics alias LambdaEthereumConsensus.P2P.Gossip.BeaconBlock alias LambdaEthereumConsensus.P2P.Gossip.BlobSideCar @@ -299,6 +300,8 @@ defmodule LambdaEthereumConsensus.Libp2pPort do OperationsCollector.init() end + def add_block(pid \\ __MODULE__, block), do: GenServer.cast(pid, {:add_block, block}) + ######################## ### GenServer Callbacks ######################## @@ -368,6 +371,11 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:noreply, state |> Map.put(:requests, new_requests)} end + def handle_cast({:add_block, block}, state) do + PendingBlocks.add_block(block) + {:noreply, state} + end + @impl GenServer def handle_info({_port, {:data, data}}, state) do %Notification{n: {_, payload}} = Notification.decode(data) diff --git a/mix.exs b/mix.exs index adfc7e9a2..c671b70b6 100644 --- a/mix.exs +++ b/mix.exs @@ -22,7 +22,7 @@ defmodule LambdaEthereumConsensus.MixProject do # Run "mix help compile.app" to learn about applications. def application() do [ - extra_applications: [:logger, :observer, :prometheus_ex], + extra_applications: [:logger, :observer, :prometheus_ex, :wx, :runtime_tools], mod: {LambdaEthereumConsensus.Application, []} ] end From bd29ee69c071543440a8825c914b8b97ba4fa5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Mon, 1 Jul 2024 19:59:45 +0200 Subject: [PATCH 28/37] remove recursion from process_blocks, leave a single call in add_block if necessary --- lib/lambda_ethereum_consensus/beacon/pending_blocks.ex | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 2388e2a33..5e1db6476 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -49,7 +49,9 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end |> Blocks.new_block_info() - process_block(block_info) + if process_block(block_info) in [:transitioned, :invalid] do + process_blocks() + end end end @@ -111,16 +113,17 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do case Blocks.get_block_info(parent_root) do nil -> Blocks.add_block_to_download(parent_root) + :download_pending %BlockInfo{status: :invalid} -> Blocks.change_status(block_info, :invalid) + :invalid %BlockInfo{status: :transitioned} -> case ForkChoice.on_block(block_info) do :ok -> Blocks.change_status(block_info, :transitioned) - # Block is valid. We immediately check if we can process another block. - process_blocks() + :transitioned {:error, reason} -> Logger.error("[PendingBlocks] Saving block as invalid #{reason}", @@ -129,6 +132,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do ) Blocks.change_status(block_info, :invalid) + :invalid end _other -> From 71c3a89382fbd089f8cd09025542105c332687a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 2 Jul 2024 12:43:54 +0200 Subject: [PATCH 29/37] fix lint --- lib/lambda_ethereum_consensus/beacon/pending_blocks.ex | 4 ---- lib/lambda_ethereum_consensus/beacon/sync_blocks.ex | 2 +- lib/libp2p_port.ex | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index cd51604f2..7f863e8f0 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -94,10 +94,6 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do {:ok, blocks} -> blocks |> Enum.sort_by(fn %BlockInfo{} = block_info -> block_info.signed_block.message.slot end) - |> tap(fn sorted_blocks -> - Enum.frequencies_by(sorted_blocks, fn b -> b.status end) - |> IO.inspect(label: "statuses") - end) |> Enum.each(&process_block/1) {:error, reason} -> diff --git a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex index 75d93d246..750a7aea4 100644 --- a/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/sync_blocks.ex @@ -7,8 +7,8 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do require Logger - alias LambdaEthereumConsensus.Libp2pPort alias LambdaEthereumConsensus.ForkChoice + alias LambdaEthereumConsensus.Libp2pPort alias LambdaEthereumConsensus.P2P.BlockDownloader alias LambdaEthereumConsensus.P2P.Gossip alias LambdaEthereumConsensus.StateTransition.Misc diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 4253436d7..1308fe5b6 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -9,8 +9,8 @@ defmodule LambdaEthereumConsensus.Libp2pPort do use GenServer - alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.Beacon.PendingBlocks + alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.Metrics alias LambdaEthereumConsensus.P2P.Gossip.BeaconBlock alias LambdaEthereumConsensus.P2P.Gossip.BlobSideCar From 12ff58ebc5a5e2a6d2238ad9323db5b98f5b9543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 2 Jul 2024 12:47:25 +0200 Subject: [PATCH 30/37] fix alias in clock --- lib/lambda_ethereum_consensus/beacon/clock.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/clock.ex b/lib/lambda_ethereum_consensus/beacon/clock.ex index cde0a4ab6..45d8f10e5 100644 --- a/lib/lambda_ethereum_consensus/beacon/clock.ex +++ b/lib/lambda_ethereum_consensus/beacon/clock.ex @@ -3,7 +3,7 @@ defmodule LambdaEthereumConsensus.Beacon.Clock do use GenServer - alias LambdaEthereumConsensus.Beacon.PendingBlocks + alias LambdaEthereumConsensus.Libp2pPort alias LambdaEthereumConsensus.Validator.ValidatorManager require Logger @@ -50,7 +50,7 @@ defmodule LambdaEthereumConsensus.Beacon.Clock do new_state = %{state | time: time} if time >= state.genesis_time do - LibP2pPort.on_tick(time) + Libp2pPort.on_tick(time) # TODO: reduce time between ticks to account for gnosis' 5s slot time. old_logical_time = compute_logical_time(state) new_logical_time = compute_logical_time(new_state) From af692839a79c30eebf096ed268d11e8b716906cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 2 Jul 2024 13:10:28 +0200 Subject: [PATCH 31/37] skip pending blocks until we re-add downloading --- test/unit/pending_blocks.exs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/unit/pending_blocks.exs b/test/unit/pending_blocks.exs index b2874e489..ff2747571 100644 --- a/test/unit/pending_blocks.exs +++ b/test/unit/pending_blocks.exs @@ -12,7 +12,6 @@ defmodule PendingBlocksTest do setup %{tmp_dir: tmp_dir} do start_link_supervised!({LambdaEthereumConsensus.Store.Db, dir: tmp_dir}) start_link_supervised!(LambdaEthereumConsensus.Store.Blocks) - start_link_supervised!(LambdaEthereumConsensus.Beacon.PendingBlocks) :ok end @@ -21,21 +20,24 @@ defmodule PendingBlocksTest do end @tag :tmp_dir + @tag :skip test "Download blocks" do block_info = new_block_info() + root = block_info.root + # This now needs to send stuff to libP2P instead of returning. patch(BlockDownloader, :request_blocks_by_root, fn - [block_info.root] -> {:ok, [block_info.signed_block]} + [^root] -> {:ok, [block_info.signed_block]} _else -> {:error, nil} end) - Blocks.add_block_to_download(block_info.root) + Blocks.add_block_to_download(root) assert Blocks.get_blocks_with_status(:download) == {:ok, [ %BlockInfo{ - root: block_info.root, + root: root, status: :download, signed_block: nil } From 17bfef230bc51640e64cd8c47c34681c2c279392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 2 Jul 2024 13:11:50 +0200 Subject: [PATCH 32/37] remove beacon chain again --- .../beacon/beacon_chain.ex | 254 ------------------ 1 file changed, 254 deletions(-) delete mode 100644 lib/lambda_ethereum_consensus/beacon/beacon_chain.ex diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_chain.ex b/lib/lambda_ethereum_consensus/beacon/beacon_chain.ex deleted file mode 100644 index 4430466f4..000000000 --- a/lib/lambda_ethereum_consensus/beacon/beacon_chain.ex +++ /dev/null @@ -1,254 +0,0 @@ -defmodule LambdaEthereumConsensus.Beacon.BeaconChain do - @moduledoc false - - use GenServer - - alias LambdaEthereumConsensus.Libp2pPort - alias LambdaEthereumConsensus.StateTransition.Misc - alias LambdaEthereumConsensus.Validator.ValidatorManager - alias Types.BeaconState - alias Types.Checkpoint - - require Logger - - defmodule BeaconChainState do - @moduledoc false - - defstruct [ - :genesis_time, - :genesis_validators_root, - :time, - :cached_fork_choice, - :synced - ] - - @type fork_choice_data :: %{ - head_root: Types.root(), - head_slot: Types.slot(), - justified: Types.Checkpoint.t(), - finalized: Types.Checkpoint.t() - } - - @type t :: %__MODULE__{ - genesis_time: Types.uint64(), - genesis_validators_root: Types.bytes32(), - time: Types.uint64(), - cached_fork_choice: fork_choice_data(), - synced: boolean() - } - end - - @spec start_link({BeaconState.t(), Types.uint64()}) :: :ignore | {:error, any} | {:ok, pid} - def start_link(opts) do - GenServer.start_link(__MODULE__, opts, name: __MODULE__) - end - - @spec get_current_slot() :: Types.slot() - def get_current_slot(), do: GenServer.call(__MODULE__, :get_current_slot) - - @spec get_genesis_time() :: Types.uint64() - def get_genesis_time(), do: GenServer.call(__MODULE__, :get_genesis_time) - - @spec update_fork_choice_cache(Types.root(), Types.slot(), Checkpoint.t(), Checkpoint.t()) :: - :ok - def update_fork_choice_cache(head_root, head_slot, justified, finalized) do - GenServer.cast( - __MODULE__, - {:update_fork_choice_cache, head_root, head_slot, justified, finalized} - ) - end - - @spec get_finalized_checkpoint() :: Types.Checkpoint.t() - def get_finalized_checkpoint() do - %{finalized: finalized} = GenServer.call(__MODULE__, :get_fork_choice_cache) - finalized - end - - @spec get_justified_checkpoint() :: Types.Checkpoint.t() - def get_justified_checkpoint() do - %{justified: justified} = GenServer.call(__MODULE__, :get_fork_choice_cache) - justified - end - - @spec get_current_epoch() :: integer() - def get_current_epoch() do - Misc.compute_epoch_at_slot(get_current_slot()) - end - - @spec get_fork_digest() :: Types.fork_digest() - def get_fork_digest() do - GenServer.call(__MODULE__, :get_fork_digest) - end - - @spec get_fork_digest_for_slot(Types.slot()) :: binary() - def get_fork_digest_for_slot(slot) do - compute_fork_digest(slot, ChainSpec.get_genesis_validators_root()) - end - - @spec get_fork_version() :: Types.version() - def get_fork_version(), do: GenServer.call(__MODULE__, :get_fork_version) - - @spec get_current_status_message() :: Types.StatusMessage.t() - def get_current_status_message(), do: GenServer.call(__MODULE__, :get_current_status_message) - - ########################## - ### GenServer Callbacks - ########################## - - @impl GenServer - @spec init({Types.uint64(), Types.root(), BeaconChainState.fork_choice_data(), Types.uint64()}) :: - {:ok, BeaconChainState.t()} | {:stop, any} - def init({genesis_time, genesis_validators_root, fork_choice_data, time}) do - schedule_next_tick() - - {:ok, - %BeaconChainState{ - genesis_time: genesis_time, - genesis_validators_root: genesis_validators_root, - time: time, - synced: false, - cached_fork_choice: fork_choice_data - }} - end - - @impl true - def handle_call(:get_current_slot, _from, state) do - {:reply, compute_current_slot(state), state} - end - - @impl true - def handle_call(:get_genesis_time, _from, state) do - {:reply, state.genesis_time, state} - end - - @impl true - def handle_call(:get_fork_choice_cache, _, %{cached_fork_choice: cached} = state) do - {:reply, cached, state} - end - - @impl true - def handle_call(:get_fork_digest, _from, state) do - fork_digest = - compute_current_slot(state) |> compute_fork_digest(state.genesis_validators_root) - - {:reply, fork_digest, state} - end - - @impl true - def handle_call(:get_fork_version, _from, state) do - fork_version = - compute_current_slot(state) - |> Misc.compute_epoch_at_slot() - |> ChainSpec.get_fork_version_for_epoch() - - {:reply, fork_version, state} - end - - @impl true - @spec handle_call(:get_current_status_message, any, BeaconChainState.t()) :: - {:reply, Types.StatusMessage.t(), BeaconChainState.t()} - def handle_call(:get_current_status_message, _from, state) do - %{ - head_root: head_root, - head_slot: head_slot, - finalized: %{root: finalized_root, epoch: finalized_epoch} - } = state.cached_fork_choice - - status_message = %Types.StatusMessage{ - fork_digest: compute_fork_digest(head_slot, state.genesis_validators_root), - finalized_root: finalized_root, - finalized_epoch: finalized_epoch, - head_root: head_root, - head_slot: head_slot - } - - {:reply, status_message, state} - end - - @impl true - def handle_info(:on_tick, state) do - schedule_next_tick() - time = :os.system_time(:second) - new_state = %BeaconChainState{state | time: time} - - if time >= state.genesis_time do - Libp2pPort.on_tick(time) - # TODO: reduce time between ticks to account for gnosis' 5s slot time. - old_logical_time = compute_logical_time(state) - new_logical_time = compute_logical_time(new_state) - - if old_logical_time != new_logical_time do - notify_subscribers(new_logical_time) - end - end - - {:noreply, new_state} - end - - @impl true - def handle_cast({:update_fork_choice_cache, head_root, head_slot, justified, finalized}, state) do - new_cache = %{ - head_root: head_root, - head_slot: head_slot, - justified: justified, - finalized: finalized - } - - new_state = Map.put(state, :cached_fork_choice, new_cache) - - # TODO: make this check dynamic - if compute_current_slot(state) <= head_slot do - {:noreply, %{new_state | synced: true}} - else - {:noreply, new_state} - end - end - - def schedule_next_tick() do - # For millisecond precision - time_to_next_tick = 1000 - rem(:os.system_time(:millisecond), 1000) - Process.send_after(__MODULE__, :on_tick, time_to_next_tick) - end - - defp compute_current_slot(state) do - div(state.time - state.genesis_time, ChainSpec.get("SECONDS_PER_SLOT")) - end - - defp compute_fork_digest(slot, genesis_validators_root) do - Misc.compute_epoch_at_slot(slot) - |> ChainSpec.get_fork_version_for_epoch() - |> Misc.compute_fork_digest(genesis_validators_root) - end - - @type slot_third :: :first_third | :second_third | :last_third - @type logical_time :: {Types.slot(), slot_third()} - - @spec compute_logical_time(BeaconChainState.t()) :: logical_time() - defp compute_logical_time(state) do - elapsed_time = state.time - state.genesis_time - - slot_thirds = div(elapsed_time * 3, ChainSpec.get("SECONDS_PER_SLOT")) - slot = div(slot_thirds, 3) - - slot_third = - case rem(slot_thirds, 3) do - 0 -> :first_third - 1 -> :second_third - 2 -> :last_third - end - - {slot, slot_third} - end - - defp notify_subscribers(logical_time) do - log_new_slot(logical_time) - ValidatorManager.notify_tick(logical_time) - end - - defp log_new_slot({slot, :first_third}) do - :telemetry.execute([:sync, :store], %{slot: slot}) - Logger.info("[BeaconChain] Slot transition", slot: slot) - end - - defp log_new_slot(_), do: :ok -end From 6eb8e61eb0f9504a05962de84fae69783dabc9c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 2 Jul 2024 18:44:56 +0200 Subject: [PATCH 33/37] only call process_blocks when needed. Add logs. --- .../beacon/pending_blocks.ex | 26 ++++++++++++------- lib/lambda_ethereum_consensus/store/blocks.ex | 22 +++++++--------- lib/types/block_info.ex | 7 +++++ native/libp2p_port/internal/utils/utils.go | 11 ++++++++ 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 7f863e8f0..9633590bb 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -42,15 +42,17 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do missing_blobs = missing_blobs(block_info) if Enum.empty?(missing_blobs) do - block_info + Blocks.new_block_info(block_info) + + if process_block(block_info) in [:transitioned, :invalid] do + process_blocks() + end else BlobDownloader.request_blobs_by_root(missing_blobs, &process_blobs/1) - block_info |> BlockInfo.change_status(:download_blobs) - end - |> Blocks.new_block_info() - if process_block(block_info) in [:transitioned, :invalid] do - process_blocks() + block_info + |> BlockInfo.change_status(:download_blobs) + |> Blocks.new_block_info() end end end @@ -68,10 +70,12 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do |> Enum.each(fn root -> with %BlockInfo{} = block_info <- Blocks.get_block_info(root) do if Enum.empty?(missing_blobs(block_info)) do - Blocks.change_status(block_info, :pending) - end + new_block_info = Blocks.change_status(block_info, :pending) - process_block(block_info) + if process_block(new_block_info) in [:transitioned, :invalid] do + process_blocks() + end + end end end) end @@ -104,6 +108,10 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end defp process_block(block_info) do + if block_info.status != :pending do + Logger.error("Called process block for a block that's not ready: #{block_info}") + end + parent_root = block_info.signed_block.message.parent_root case Blocks.get_block_info(parent_root) do diff --git a/lib/lambda_ethereum_consensus/store/blocks.ex b/lib/lambda_ethereum_consensus/store/blocks.ex index d6d555a03..7bf3871ee 100644 --- a/lib/lambda_ethereum_consensus/store/blocks.ex +++ b/lib/lambda_ethereum_consensus/store/blocks.ex @@ -81,24 +81,22 @@ defmodule LambdaEthereumConsensus.Store.Blocks do BlockDb.add_root_to_status(block_info.root, block_info.status) end - @spec change_status(BlockInfo.t(), BlockInfo.block_status()) :: :ok + @doc """ + Changes the status of a block in the db. Returns the block with the modified status. + """ + @spec change_status(BlockInfo.t(), BlockInfo.block_status()) :: BlockInfo.t() def change_status(block_info, status) do Metrics.block_status(block_info.root, status) - IO.puts( - "Block: #{Utils.format_shorten_binary(block_info.root)}. Changing status from #{block_info.status} to #{status}" - ) + IO.puts("Changing status for #{block_info} to #{status}") - old_status = block_info.status - - block_info - |> BlockInfo.change_status(status) - |> tap(fn bi -> - IO.puts("Status for #{Utils.format_shorten_binary(bi.root)}: #{bi.status}") - end) - |> store_block_info() + new_block_info = BlockInfo.change_status(block_info, status) + store_block_info(new_block_info) + old_status = block_info.status BlockDb.change_root_status(block_info.root, old_status, status) + + new_block_info end @spec get_blocks_with_status(BlockInfo.block_status()) :: diff --git a/lib/types/block_info.ex b/lib/types/block_info.ex index 7c8870273..6c5abe3c2 100644 --- a/lib/types/block_info.ex +++ b/lib/types/block_info.ex @@ -6,6 +6,7 @@ defmodule Types.BlockInfo do signed_block field may be nil if it's queued for download. """ + alias LambdaEthereumConsensus.Utils alias Types.SignedBeaconBlock @type block_status :: @@ -34,6 +35,12 @@ defmodule Types.BlockInfo do :transitioned ] + defimpl String.Chars, for: __MODULE__ do + def to_string(block_info) do + "Slot: #{block_info.signed_block.message.slot}. Root: #{Utils.format_shorten_binary(block_info.root)}. Status: #{inspect(block_info.status)}" + end + end + @spec from_block(SignedBeaconBlock.t(), block_status()) :: t() def from_block(signed_block, status \\ :pending) do {:ok, root} = Ssz.hash_tree_root(signed_block.message) diff --git a/native/libp2p_port/internal/utils/utils.go b/native/libp2p_port/internal/utils/utils.go index 839858803..358fc75df 100644 --- a/native/libp2p_port/internal/utils/utils.go +++ b/native/libp2p_port/internal/utils/utils.go @@ -6,6 +6,7 @@ import ( "encoding/binary" "errors" "math/big" + "os" "github.com/btcsuite/btcd/btcec/v2" gcrypto "github.com/ethereum/go-ethereum/crypto" @@ -93,3 +94,13 @@ func MsgID(msg *pb.Message) string { digest = h.Sum(digest) return string(digest[:20]) } + +// Log function that writes to a single file. Only to be used for testing. +func Log(msg string) { + f, err := os.OpenFile("logs/go_log.txt", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666) + PanicIfError(err) + defer f.Close() + + _, err = f.WriteString(msg) + PanicIfError(err) +} From 30b9fe3b2d99f17b6dbf7d35e4c8381f5bd45dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 2 Jul 2024 18:52:46 +0200 Subject: [PATCH 34/37] fix linter --- .../beacon/pending_blocks.ex | 26 ++++++++++++------- lib/lambda_ethereum_consensus/store/blocks.ex | 1 - 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 9633590bb..0b2b5e1d0 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -43,10 +43,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do if Enum.empty?(missing_blobs) do Blocks.new_block_info(block_info) - - if process_block(block_info) in [:transitioned, :invalid] do - process_blocks() - end + process_block_and_check_children(block_info) else BlobDownloader.request_blobs_by_root(missing_blobs, &process_blobs/1) @@ -70,11 +67,9 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do |> Enum.each(fn root -> with %BlockInfo{} = block_info <- Blocks.get_block_info(root) do if Enum.empty?(missing_blobs(block_info)) do - new_block_info = Blocks.change_status(block_info, :pending) - - if process_block(new_block_info) in [:transitioned, :invalid] do - process_blocks() - end + block_info + |> Blocks.change_status(:pending) + |> process_block_and_check_children() end end end) @@ -107,6 +102,18 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end end + @doc """ + Processes a single block. If it was transitioned or declared invalid, then process_blocks + is called to check if there's any children that can now be processed. This function + is only to be called when a new block is saved as pending, not when processing blocks + in batch, to avoid unneeded recursion. + """ + defp process_block_and_check_children(block_info) do + if process_block(block_info) in [:transitioned, :invalid] do + process_blocks() + end + end + defp process_block(block_info) do if block_info.status != :pending do Logger.error("Called process block for a block that's not ready: #{block_info}") @@ -145,6 +152,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end # TODO: add block downloading either before or after processing. It's async so it's the same. + # TODO: add a new missing blobs call if some blobs are still missing for a block. defp process_blobs({:ok, blobs}), do: add_blobs(blobs) diff --git a/lib/lambda_ethereum_consensus/store/blocks.ex b/lib/lambda_ethereum_consensus/store/blocks.ex index 7bf3871ee..e15174b48 100644 --- a/lib/lambda_ethereum_consensus/store/blocks.ex +++ b/lib/lambda_ethereum_consensus/store/blocks.ex @@ -5,7 +5,6 @@ defmodule LambdaEthereumConsensus.Store.Blocks do alias LambdaEthereumConsensus.Metrics alias LambdaEthereumConsensus.Store.BlockDb alias LambdaEthereumConsensus.Store.LRUCache - alias LambdaEthereumConsensus.Utils alias Types.BeaconBlock alias Types.BlockInfo From 006fd93ccd4253ee6ca81556c1801bb274ca2804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 2 Jul 2024 19:11:59 +0200 Subject: [PATCH 35/37] Added more retries for blob downloads. Moved the blobs handler to private. --- .../beacon/pending_blocks.ex | 67 ++++++++----------- 1 file changed, 27 insertions(+), 40 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex index 0b2b5e1d0..abcc351d2 100644 --- a/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex +++ b/lib/lambda_ethereum_consensus/beacon/pending_blocks.ex @@ -45,7 +45,7 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do Blocks.new_block_info(block_info) process_block_and_check_children(block_info) else - BlobDownloader.request_blobs_by_root(missing_blobs, &process_blobs/1) + BlobDownloader.request_blobs_by_root(missing_blobs, &process_blobs/1, 30) block_info |> BlockInfo.change_status(:download_blobs) @@ -54,36 +54,6 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end end - @doc """ - To be used when a series of blobs are downloaded. Stores each blob. - If there are blocks that can be processed, does so immediately. - """ - def add_blobs(blobs) do - Enum.map(blobs, fn blob -> - BlobDb.store_blob(blob) - Ssz.hash_tree_root!(blob.signed_block_header.message) - end) - |> Enum.uniq() - |> Enum.each(fn root -> - with %BlockInfo{} = block_info <- Blocks.get_block_info(root) do - if Enum.empty?(missing_blobs(block_info)) do - block_info - |> Blocks.change_status(:pending) - |> process_block_and_check_children() - end - end - end) - end - - @doc """ - Iterates through the pending blocks and adds them to the fork choice if their parent is already in the fork choice. - """ - @spec handle_info(atom(), state()) :: {:noreply, state()} - def handle_info(:process_blocks, _state) do - process_blocks() - {:noreply, nil} - end - ########################## ### Private Functions ########################## @@ -102,12 +72,10 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end end - @doc """ - Processes a single block. If it was transitioned or declared invalid, then process_blocks - is called to check if there's any children that can now be processed. This function - is only to be called when a new block is saved as pending, not when processing blocks - in batch, to avoid unneeded recursion. - """ + # Processes a block. If it was transitioned or declared invalid, then process_blocks + # is called to check if there's any children that can now be processed. This function + # is only to be called when a new block is saved as pending, not when processing blocks + # in batch, to avoid unneeded recursion. defp process_block_and_check_children(block_info) do if process_block(block_info) in [:transitioned, :invalid] do process_blocks() @@ -151,13 +119,32 @@ defmodule LambdaEthereumConsensus.Beacon.PendingBlocks do end end - # TODO: add block downloading either before or after processing. It's async so it's the same. - # TODO: add a new missing blobs call if some blobs are still missing for a block. - defp process_blobs({:ok, blobs}), do: add_blobs(blobs) defp process_blobs({:error, reason}) do Logger.error("Error downloading blobs: #{inspect(reason)}") + + # We might want to declare a block invalid here. + end + + # To be used when a series of blobs are downloaded. Stores each blob. + # If there are blocks that can be processed, does so immediately. + defp add_blobs(blobs) do + Enum.map(blobs, fn blob -> + BlobDb.store_blob(blob) + Ssz.hash_tree_root!(blob.signed_block_header.message) + end) + |> Enum.uniq() + |> Enum.each(fn root -> + with %BlockInfo{} = block_info <- Blocks.get_block_info(root) do + # TODO: add a new missing blobs call if some blobs are still missing for a block. + if Enum.empty?(missing_blobs(block_info)) do + block_info + |> Blocks.change_status(:pending) + |> process_block_and_check_children() + end + end + end) end @spec missing_blobs(BlockInfo.t()) :: [Types.BlobIdentifier.t()] From fb61a14d938af6d13a81e5883a29c7a553a2668c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Tue, 2 Jul 2024 19:17:37 +0200 Subject: [PATCH 36/37] remove IO.puts --- lib/lambda_ethereum_consensus/store/blocks.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/lambda_ethereum_consensus/store/blocks.ex b/lib/lambda_ethereum_consensus/store/blocks.ex index e15174b48..a4f404682 100644 --- a/lib/lambda_ethereum_consensus/store/blocks.ex +++ b/lib/lambda_ethereum_consensus/store/blocks.ex @@ -87,8 +87,6 @@ defmodule LambdaEthereumConsensus.Store.Blocks do def change_status(block_info, status) do Metrics.block_status(block_info.root, status) - IO.puts("Changing status for #{block_info} to #{status}") - new_block_info = BlockInfo.change_status(block_info, status) store_block_info(new_block_info) From 305b1abb51f9e2c86c1ef2b119901b1d42466c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Thu, 4 Jul 2024 16:05:07 +0200 Subject: [PATCH 37/37] Update test/unit/p2p/requests_test.exs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Avila Gastón <72628438+avilagaston9@users.noreply.github.com> --- test/unit/p2p/requests_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/p2p/requests_test.exs b/test/unit/p2p/requests_test.exs index 9e7274add..a7b6cb56c 100644 --- a/test/unit/p2p/requests_test.exs +++ b/test/unit/p2p/requests_test.exs @@ -10,7 +10,7 @@ defmodule Unit.P2p.RequestsTest do Requests.handle_response(requests, "some response", "fake id") end - test "A requests object should handler a request only once" do + test "A requests object should handle a request only once" do requests = Requests.new() pid = self()