From b6c2bed9d31c231062fe97e5fa33b296b54acc2f Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 23 Jul 2024 12:46:23 -0300 Subject: [PATCH 01/42] This are the minimum changes to reproduce the invalid signature error --- lib/lambda_ethereum_consensus/beacon/clock.ex | 8 -------- lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex | 2 +- lib/lambda_ethereum_consensus/validator/validator.ex | 2 +- .../validator/validator_manager.ex | 8 +++++++- lib/libp2p_port.ex | 4 ++-- network_params.yaml | 4 ++-- 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/clock.ex b/lib/lambda_ethereum_consensus/beacon/clock.ex index 45d8f10e5..b766f4fee 100644 --- a/lib/lambda_ethereum_consensus/beacon/clock.ex +++ b/lib/lambda_ethereum_consensus/beacon/clock.ex @@ -18,9 +18,6 @@ defmodule LambdaEthereumConsensus.Beacon.Clock do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end - @spec get_current_time() :: Types.uint64() - def get_current_time(), do: GenServer.call(__MODULE__, :get_current_time) - ########################## ### GenServer Callbacks ########################## @@ -38,11 +35,6 @@ defmodule LambdaEthereumConsensus.Beacon.Clock do }} end - @impl true - def handle_call(:get_current_time, _from, %{time: time} = state) do - {:reply, time, state} - end - @impl true def handle_info(:on_tick, state) do schedule_next_tick() diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index c0869f1f3..713d399b2 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -119,7 +119,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do @spec get_current_chain_slot() :: Types.slot() def get_current_chain_slot() do - time = Clock.get_current_time() + time = :os.system_time(:second) genesis_time = StoreDb.fetch_genesis_time!() compute_current_slot(time, genesis_time) end diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index df0486cb4..66220c262 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -255,7 +255,7 @@ defmodule LambdaEthereumConsensus.Validator do log_debug(validator.index, "publishing attestation", log_md) Gossip.Attestation.publish(subnet_id, attestation) - |> log_debug_result(validator.index, "published attestation", log_md) + |> log_info_result(validator.index, "published attestation", log_md) if current_duty.should_aggregate? do log_debug(validator.index, "collecting for future aggregation", log_md) diff --git a/lib/lambda_ethereum_consensus/validator/validator_manager.ex b/lib/lambda_ethereum_consensus/validator/validator_manager.ex index 099a18a75..94bb02d3b 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_manager.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_manager.ex @@ -59,7 +59,7 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do # TODO: The use of a Genserver and cast is still needed to avoid locking at the clock level. # This is a temporary solution and will be taken off in a future PR. - defp notify_validators(msg), do: GenServer.cast(__MODULE__, {:notify_all, msg}) + defp notify_validators(msg), do: GenServer.call(__MODULE__, {:notify_all, msg}) def handle_cast({:notify_all, msg}, validators) do validators = notify_all(validators, msg) @@ -67,6 +67,12 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do {:noreply, validators} end + def handle_call({:notify_all, msg}, _from, validators) do + validators = notify_all(validators, msg) + + {:reply, :ok, validators} + end + defp notify_all(validators, msg) do start_time = System.monotonic_time(:millisecond) diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index d3dc898f4..0b2eeec35 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -226,7 +226,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do direction: "elixir->" }) - call_command(pid, {:publish, %Publish{topic: topic_name, message: message}}) + cast_command(pid, {:publish, %Publish{topic: topic_name, message: message}}) end @doc """ @@ -243,7 +243,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do GenServer.cast(pid, {:new_subscriber, topic_name, module}) - call_command(pid, {:subscribe, %SubscribeToTopic{name: topic_name}}) + cast_command(pid, {:subscribe, %SubscribeToTopic{name: topic_name}}) end @doc """ diff --git a/network_params.yaml b/network_params.yaml index e995d03fa..f685ef8c6 100644 --- a/network_params.yaml +++ b/network_params.yaml @@ -2,11 +2,11 @@ participants: - el_type: geth cl_type: lighthouse count: 2 - validator_count: 32 + validator_count: 27 - el_type: geth cl_type: lambda cl_image: lambda_ethereum_consensus:latest use_separate_vc: false count: 1 - validator_count: 32 + validator_count: 10 cl_max_mem: 4096 \ No newline at end of file From c85151eac197cd483bc90f6986de180ceed9423b Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 23 Jul 2024 14:22:25 -0300 Subject: [PATCH 02/42] Moved notify_tick to libp2p to be sure the issues is just with new blocks and not with libp2p calling the ValidatorManager --- lib/lambda_ethereum_consensus/beacon/clock.ex | 7 ++----- .../fork_choice/fork_choice.ex | 8 +++++--- .../validator/validator_manager.ex | 6 +++++- lib/libp2p_port.ex | 10 +++++++++- network_params.yaml | 2 +- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/clock.ex b/lib/lambda_ethereum_consensus/beacon/clock.ex index b766f4fee..331daee0f 100644 --- a/lib/lambda_ethereum_consensus/beacon/clock.ex +++ b/lib/lambda_ethereum_consensus/beacon/clock.ex @@ -42,15 +42,12 @@ defmodule LambdaEthereumConsensus.Beacon.Clock do new_state = %{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 - log_new_slot(new_logical_time) - ValidatorManager.notify_tick(new_logical_time) - end + Libp2pPort.on_tick({time, new_logical_time, new_logical_time != old_logical_time}) end {:noreply, new_state} diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index 713d399b2..08dbd1e1e 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -5,7 +5,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do require Logger - alias LambdaEthereumConsensus.Beacon.Clock + # alias LambdaEthereumConsensus.Beacon.Clock alias LambdaEthereumConsensus.Execution.ExecutionChain alias LambdaEthereumConsensus.ForkChoice.Handlers alias LambdaEthereumConsensus.ForkChoice.Head @@ -232,7 +232,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do %{slot: slot, body: body} = head_block OperationsCollector.notify_new_block(head_block) - ValidatorManager.notify_new_block(slot, head_root) + ExecutionChain.notify_new_block(slot, body.eth1_data, body.execution_payload) update_fork_choice_data( @@ -242,7 +242,9 @@ defmodule LambdaEthereumConsensus.ForkChoice do store.finalized_checkpoint ) - Logger.debug("[Fork choice] Updated fork choice cache", slot: slot) + ValidatorManager.notify_new_block(slot, head_root) + + Logger.info("[Fork choice] Updated fork choice cache", slot: slot) :ok end diff --git a/lib/lambda_ethereum_consensus/validator/validator_manager.ex b/lib/lambda_ethereum_consensus/validator/validator_manager.ex index 94bb02d3b..4916c0e66 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_manager.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_manager.ex @@ -49,11 +49,15 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do @spec notify_new_block(Types.slot(), Types.root()) :: :ok def notify_new_block(slot, head_root) do - notify_validators({:new_block, slot, head_root}) + # Making this alone a cast solves the issue + GenServer.cast(__MODULE__, {:notify_all, {:new_block, slot, head_root}}) + # notify_validators({:new_block, slot, head_root}) end @spec notify_tick(Clock.logical_time()) :: :ok def notify_tick(logical_time) do + # Making this a cast alone doesn't solve the issue + # GenServer.cast(__MODULE__, {:notify_all, {:on_tick, logical_time}}) notify_validators({:on_tick, logical_time}) end diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 0b2eeec35..c01bdc0b3 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -9,6 +9,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do use GenServer + alias LambdaEthereumConsensus.Validator.ValidatorManager alias LambdaEthereumConsensus.Beacon.PendingBlocks alias LambdaEthereumConsensus.Beacon.SyncBlocks alias LambdaEthereumConsensus.ForkChoice @@ -392,10 +393,17 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end @impl GenServer - def handle_cast({:on_tick, time}, state) do + def handle_cast({:on_tick, {time, slot_data, changed_slot_data}}, 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) + + # For testing that calling it from the libp2p works, and its just a matter of the notify new block, + # not the clock being the one who calls the notify tick. + if changed_slot_data do + ValidatorManager.notify_tick(slot_data) + end + {:noreply, state} end diff --git a/network_params.yaml b/network_params.yaml index f685ef8c6..fc641b906 100644 --- a/network_params.yaml +++ b/network_params.yaml @@ -8,5 +8,5 @@ participants: cl_image: lambda_ethereum_consensus:latest use_separate_vc: false count: 1 - validator_count: 10 + validator_count: 20 cl_max_mem: 4096 \ No newline at end of file From 11c9b724a35465bef1b28ae29f83d7b8192da823 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 23 Jul 2024 18:53:19 -0300 Subject: [PATCH 03/42] Just deactivated attestation publish to check the node running without issues --- .../validator/validator.ex | 33 ++++++++----------- .../validator/validator_manager.ex | 6 ++-- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 66220c262..79877e43e 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -7,6 +7,7 @@ defmodule LambdaEthereumConsensus.Validator do defstruct [ :slot, :root, + :epoch, :duties, :validator, :payload_builder @@ -41,6 +42,7 @@ defmodule LambdaEthereumConsensus.Validator do # just at the begining of every epoch, and then just update them as needed. @type state :: %__MODULE__{ slot: Types.slot(), + epoch: Types.epoch(), root: Types.root(), duties: Duties.duties(), validator: validator(), @@ -51,6 +53,7 @@ defmodule LambdaEthereumConsensus.Validator do def new({head_slot, head_root, {pubkey, privkey}}) do state = %__MODULE__{ slot: head_slot, + epoch: Misc.compute_epoch_at_slot(head_slot), root: head_root, duties: Duties.empty_duties(), validator: %{ @@ -152,17 +155,8 @@ defmodule LambdaEthereumConsensus.Validator do defp update_state(%{slot: slot, root: root} = state, slot, root), do: state - defp update_state(%{slot: slot, root: _other_root} = state, slot, head_root) do - # TODO: this log is appearing for every block - # Logger.warning("[Validator] Block came late", slot: slot, root: head_root) - - # TODO: rollback stale data instead of the whole cache - epoch = Misc.compute_epoch_at_slot(slot + 1) - recompute_duties(state, 0, epoch, slot, head_root) - end - - defp update_state(%{slot: last_slot} = state, slot, head_root) do - last_epoch = Misc.compute_epoch_at_slot(last_slot + 1) + # Epoch as part of the state now avoids recomputing the duties at every block + defp update_state(%{epoch: last_epoch} = state, slot, head_root) do epoch = Misc.compute_epoch_at_slot(slot + 1) if last_epoch == epoch do @@ -187,7 +181,7 @@ defmodule LambdaEthereumConsensus.Validator do move_subnets(state.duties, new_duties) Duties.log_duties(new_duties, state.validator.index) - %{state | slot: slot, root: head_root, duties: new_duties} + %{state | slot: slot, root: head_root, duties: new_duties, epoch: epoch} end @spec fetch_target_state(Types.epoch(), Types.root()) :: Types.BeaconState.t() @@ -254,8 +248,9 @@ defmodule LambdaEthereumConsensus.Validator do log_md = [slot: attestation.data.slot, attestation: attestation, subnet_id: subnet_id] log_debug(validator.index, "publishing attestation", log_md) - Gossip.Attestation.publish(subnet_id, attestation) - |> log_info_result(validator.index, "published attestation", log_md) + # FIXME: Uncommenting this line generates the invalid signature errors upon proposals + # Gossip.Attestation.publish(subnet_id, attestation) + # |> log_info_result(validator.index, "published attestation", log_md) if current_duty.should_aggregate? do log_debug(validator.index, "collecting for future aggregation", log_md) @@ -400,16 +395,16 @@ defmodule LambdaEthereumConsensus.Validator do defp start_payload_builder(%{validator: validator} = state, proposed_slot, head_root) do # TODO: handle reorgs and late blocks - log_debug(validator.index, "starting building payload", slot: proposed_slot) + log_debug(validator.index, "starting building payload for slot #{proposed_slot}") case BlockBuilder.start_building_payload(proposed_slot, head_root) do {:ok, payload_id} -> - log_debug(validator.index, "payload built", slot: proposed_slot) + log_info(validator.index, "payload built for slot #{proposed_slot}") %{state | payload_builder: {proposed_slot, head_root, payload_id}} {:error, reason} -> - log_error(validator.index, "start building payload", reason, slot: proposed_slot) + log_error(validator.index, "start building payload for slot #{proposed_slot}", reason) %{state | payload_builder: nil} end @@ -517,10 +512,10 @@ defmodule LambdaEthereumConsensus.Validator do defp log_result({:error, reason}, _level, index, message, metadata), do: log_error(index, message, reason, metadata) - defp log_info(index, message, metadata), + defp log_info(index, message, metadata \\ []), do: Logger.info("[Validator] #{index} #{message}", metadata) - defp log_debug(index, message, metadata), + defp log_debug(index, message, metadata \\ []), do: Logger.debug("[Validator] #{index} #{message}", metadata) defp log_error(index, message, reason, metadata \\ []), diff --git a/lib/lambda_ethereum_consensus/validator/validator_manager.ex b/lib/lambda_ethereum_consensus/validator/validator_manager.ex index 4916c0e66..602a72ca9 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_manager.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_manager.ex @@ -49,9 +49,9 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do @spec notify_new_block(Types.slot(), Types.root()) :: :ok def notify_new_block(slot, head_root) do - # Making this alone a cast solves the issue - GenServer.cast(__MODULE__, {:notify_all, {:new_block, slot, head_root}}) - # notify_validators({:new_block, slot, head_root}) + # Making this alone a cast sometimes solves the issue for a while + # GenServer.cast(__MODULE__, {:notify_all, {:new_block, slot, head_root}}) + notify_validators({:new_block, slot, head_root}) end @spec notify_tick(Clock.logical_time()) :: :ok From 3c5961fcad5b8d90b52003d24bd463635cc4ef3d Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 26 Jul 2024 14:11:21 -0300 Subject: [PATCH 04/42] Finally fixing the issue --- lib/lambda_ethereum_consensus/beacon/clock.ex | 1 - lib/lambda_ethereum_consensus/validator/duties.ex | 6 +++--- .../validator/validator.ex | 15 +++++++++------ .../validator/validator_manager.ex | 4 +++- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/clock.ex b/lib/lambda_ethereum_consensus/beacon/clock.ex index 331daee0f..d65d7f9bb 100644 --- a/lib/lambda_ethereum_consensus/beacon/clock.ex +++ b/lib/lambda_ethereum_consensus/beacon/clock.ex @@ -4,7 +4,6 @@ defmodule LambdaEthereumConsensus.Beacon.Clock do use GenServer alias LambdaEthereumConsensus.Libp2pPort - alias LambdaEthereumConsensus.Validator.ValidatorManager require Logger diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 6d5145972..d264ddc5c 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -69,9 +69,9 @@ defmodule LambdaEthereumConsensus.Validator.Duties do attester_duties # Drop the first element, which is the previous epoch's duty |> Stream.drop(1) - |> Enum.each(fn %{index_in_committee: i, committee_index: ci, slot: slot} -> - Logger.debug( - "[Validator] #{validator_index} has to attest in committee #{ci} of slot #{slot} with index #{i}" + |> Enum.each(fn %{index_in_committee: i, committee_index: ci, slot: slot, should_aggregate?: sa} -> + Logger.info( + "[Validator] #{validator_index} has to attest in committee #{ci} of slot #{slot} with index #{i}, and should_aggregate?: #{sa}" ) end) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 79877e43e..736c9525f 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -246,11 +246,11 @@ defmodule LambdaEthereumConsensus.Validator do attestation = produce_attestation(current_duty, state.root, state.validator.privkey) log_md = [slot: attestation.data.slot, attestation: attestation, subnet_id: subnet_id] - log_debug(validator.index, "publishing attestation", log_md) + debug_log_msg = "publishing attestation on committee index: #{current_duty.committee_index} | as #{current_duty.index_in_committee}/#{current_duty.committee_length - 1} and pubkey: #{LambdaEthereumConsensus.Utils.format_shorten_binary(validator.pubkey)}" + log_debug(validator.index, debug_log_msg, log_md) - # FIXME: Uncommenting this line generates the invalid signature errors upon proposals - # Gossip.Attestation.publish(subnet_id, attestation) - # |> log_info_result(validator.index, "published attestation", log_md) + Gossip.Attestation.publish(subnet_id, attestation) + |> log_info_result(validator.index, "published attestation", log_md) if current_duty.should_aggregate? do log_debug(validator.index, "collecting for future aggregation", log_md) @@ -295,12 +295,15 @@ defmodule LambdaEthereumConsensus.Validator do end defp aggregate_attestations(attestations) do + # TODO: We need to check why we are producing duplicate attestations, this was generating invalid signatures + unique_attestations = attestations |> Enum.uniq() + aggregation_bits = - attestations + unique_attestations |> Stream.map(&Map.fetch!(&1, :aggregation_bits)) |> Enum.reduce(&BitField.bitwise_or/2) - {:ok, signature} = attestations |> Enum.map(&Map.fetch!(&1, :signature)) |> Bls.aggregate() + {:ok, signature} = unique_attestations |> Enum.map(&Map.fetch!(&1, :signature)) |> Bls.aggregate() %{List.first(attestations) | aggregation_bits: aggregation_bits, signature: signature} end diff --git a/lib/lambda_ethereum_consensus/validator/validator_manager.ex b/lib/lambda_ethereum_consensus/validator/validator_manager.ex index 602a72ca9..6232f48a8 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_manager.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_manager.ex @@ -63,7 +63,7 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do # TODO: The use of a Genserver and cast is still needed to avoid locking at the clock level. # This is a temporary solution and will be taken off in a future PR. - defp notify_validators(msg), do: GenServer.call(__MODULE__, {:notify_all, msg}) + defp notify_validators(msg), do: GenServer.call(__MODULE__, {:notify_all, msg}, 20_000) def handle_cast({:notify_all, msg}, validators) do validators = notify_all(validators, msg) @@ -80,6 +80,8 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do defp notify_all(validators, msg) do start_time = System.monotonic_time(:millisecond) + Logger.info("[Validator Manager] Notifying all Validators with message: #{inspect(msg)}") + updated_validators = Enum.map(validators, ¬ify_validator(&1, msg)) end_time = System.monotonic_time(:millisecond) From b8623ee728260815bb8153dbd3b9067ca47d11b5 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Mon, 29 Jul 2024 20:10:28 -0300 Subject: [PATCH 05/42] Make the clock a ticker --- .../beacon/beacon_node.ex | 9 ++- lib/lambda_ethereum_consensus/beacon/clock.ex | 77 +++++-------------- lib/libp2p_port.ex | 50 ++++++++++-- 3 files changed, 70 insertions(+), 66 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex index d6c4c6cd7..f76ec93cb 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex @@ -24,12 +24,12 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do Cache.initialize_cache() - libp2p_args = get_libp2p_args() - time = :os.system_time(:second) ForkChoice.init_store(store, time) + libp2p_args = get_libp2p_args(store.genesis_time) + validator_manager = get_validator_manager( deposit_tree_snapshot, @@ -39,8 +39,8 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do children = [ - {LambdaEthereumConsensus.Beacon.Clock, {store.genesis_time, time}}, {LambdaEthereumConsensus.Libp2pPort, libp2p_args}, + {LambdaEthereumConsensus.Beacon.Ticker, [LambdaEthereumConsensus.Libp2pPort]}, {Task.Supervisor, name: PruneStatesSupervisor}, {Task.Supervisor, name: PruneBlocksSupervisor}, {Task.Supervisor, name: PruneBlobsSupervisor} @@ -63,7 +63,7 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do ] end - defp get_libp2p_args() do + defp get_libp2p_args(genesis_time) do config = Application.fetch_env!(:lambda_ethereum_consensus, :libp2p) port = Keyword.fetch!(config, :port) bootnodes = Keyword.fetch!(config, :bootnodes) @@ -75,6 +75,7 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do end [ + genesis_time: genesis_time, listen_addr: listen_addr, enable_discovery: true, discovery_addr: "0.0.0.0:#{port}", diff --git a/lib/lambda_ethereum_consensus/beacon/clock.ex b/lib/lambda_ethereum_consensus/beacon/clock.ex index d65d7f9bb..e615dc0ac 100644 --- a/lib/lambda_ethereum_consensus/beacon/clock.ex +++ b/lib/lambda_ethereum_consensus/beacon/clock.ex @@ -1,55 +1,47 @@ -defmodule LambdaEthereumConsensus.Beacon.Clock do +defmodule LambdaEthereumConsensus.Beacon.Ticker do @moduledoc false use GenServer - alias LambdaEthereumConsensus.Libp2pPort - require Logger - @type state :: %{ - genesis_time: Types.uint64(), - time: Types.uint64() - } - - @spec start_link({Types.uint64(), Types.uint64()}) :: :ignore | {:error, any} | {:ok, pid} + @spec start_link([atom()]) :: :ignore | {:error, any} | {:ok, pid} def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end + @spec register_to_tick(atom() | [atom()]) :: :ok + def register_to_tick(to_tick) when is_atom(to_tick), do: register_to_tick([to_tick]) + def register_to_tick(to_tick) when is_list(to_tick) do + GenServer.cast(__MODULE__, {:register_to_tick, to_tick}) + end + ########################## ### GenServer Callbacks ########################## - @impl GenServer - @spec init({Types.uint64(), Types.uint64()}) :: - {:ok, state()} | {:stop, any} - def init({genesis_time, time}) do + @impl true + @spec init([atom()]) :: {:ok, [atom()]} | {:stop, any} + def init(to_tick) when is_list(to_tick) do schedule_next_tick() - {:ok, - %{ - genesis_time: genesis_time, - time: time - }} + {:ok, to_tick} end @impl true - def handle_info(:on_tick, state) do + def handle_cast({:register_to_tick, to_tick_additions}, to_tick) do + new_to_tick = Enum.uniq(to_tick ++ to_tick_additions) + {:noreply, new_to_tick} + end + + @impl true + def handle_info(:on_tick, to_tick) do schedule_next_tick() time = :os.system_time(:second) - new_state = %{state | time: time} - - if time >= state.genesis_time do - - # 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) - Libp2pPort.on_tick({time, new_logical_time, new_logical_time != old_logical_time}) - end + Enum.each(to_tick, & &1.on_tick(time)) - {:noreply, new_state} + {:noreply, to_tick} end def schedule_next_tick() do @@ -57,31 +49,4 @@ defmodule LambdaEthereumConsensus.Beacon.Clock do time_to_next_tick = 1000 - rem(:os.system_time(:millisecond), 1000) Process.send_after(__MODULE__, :on_tick, time_to_next_tick) end - - @type slot_third :: :first_third | :second_third | :last_third - @type logical_time :: {Types.slot(), slot_third()} - - @spec compute_logical_time(state()) :: 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 log_new_slot({slot, :first_third}) do - :telemetry.execute([:sync, :store], %{slot: slot}) - Logger.info("[Clock] Slot transition", slot: slot) - end - - defp log_new_slot(_), do: :ok end diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index c01bdc0b3..ea6a1f57c 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -62,13 +62,16 @@ defmodule LambdaEthereumConsensus.Libp2pPort do ] @type init_arg :: - {:listen_addr, [String.t()]} + {:genesis_time, Types.uint64()} + | {:listen_addr, [String.t()]} | {:enable_discovery, boolean()} | {:discovery_addr, String.t()} | {:bootnodes, [String.t()]} | {:join_init_topics, boolean()} | {:enable_request_handlers, boolean()} + @type slot_data() :: {Types.uint64(), :first_third | :second_third | :last_third} + @type node_identity() :: %{ peer_id: binary(), # Pretty-printed version of the peer ID @@ -350,6 +353,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def init(args) do + {genesis_time, args} = Keyword.pop!(args, :genesis_time) {join_init_topics, args} = Keyword.pop(args, :join_init_topics, false) {enable_request_handlers, args} = Keyword.pop(args, :enable_request_handlers, false) @@ -374,6 +378,8 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:ok, %{ + genesis_time: genesis_time, + slot_data: {0, :first_third}, port: port, subscribers: %{}, requests: Requests.new(), @@ -393,18 +399,22 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end @impl GenServer - def handle_cast({:on_tick, {time, slot_data, changed_slot_data}}, state) do + def handle_cast({:on_tick, time}, %{genesis_time: genesis_time} = state) when time <= genesis_time, do: {:noreply, state} + def handle_cast({:on_tick, time}, %{genesis_time: genesis_time, slot_data: slot_data} = 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) - # For testing that calling it from the libp2p works, and its just a matter of the notify new block, - # not the clock being the one who calls the notify tick. - if changed_slot_data do + new_slot_data = compute_slot(genesis_time, time) + + if slot_data != new_slot_data do ValidatorManager.notify_tick(slot_data) end - {:noreply, state} + log_new_slot(new_slot_data) + + {:noreply, %{state | slot_data: new_slot_data}} end def handle_cast( @@ -682,4 +692,32 @@ defmodule LambdaEthereumConsensus.Libp2pPort do add_subscriber(state, topic, module) end) end + + @spec compute_slot(Types.uint64(), Types.uint64()) :: slot_data() + defp compute_slot(genesis_time, time) do + # TODO: This was copied as it is from the Clock to convert it into just a Ticker, + # slot calculations are spread across modules, we should probably centralize them. + elapsed_time = time - 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 log_new_slot({slot, :first_third}) do + # TODO: as with the previous function, this was copied from the Clock module. + # is use :sync, :store as the slot event, probably something to look into. + :telemetry.execute([:sync, :store], %{slot: slot}) + Logger.info("[Libp2p] Slot transition", slot: slot) + end + + defp log_new_slot(_), do: :ok end From 99850819b622b72bb0722c217fc80a2b600fb1e5 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 30 Jul 2024 12:03:13 -0300 Subject: [PATCH 06/42] Remove unneded diffs --- lib/lambda_ethereum_consensus/beacon/beacon_node.ex | 9 ++++----- lib/libp2p_port.ex | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex index f76ec93cb..5b7543cc5 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex @@ -24,12 +24,12 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do Cache.initialize_cache() + libp2p_args = get_libp2p_args() + time = :os.system_time(:second) ForkChoice.init_store(store, time) - libp2p_args = get_libp2p_args(store.genesis_time) - validator_manager = get_validator_manager( deposit_tree_snapshot, @@ -39,8 +39,8 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do children = [ - {LambdaEthereumConsensus.Libp2pPort, libp2p_args}, {LambdaEthereumConsensus.Beacon.Ticker, [LambdaEthereumConsensus.Libp2pPort]}, + {LambdaEthereumConsensus.Libp2pPort, [{:genesis_time, store.genesis_time} | libp2p_args]}, {Task.Supervisor, name: PruneStatesSupervisor}, {Task.Supervisor, name: PruneBlocksSupervisor}, {Task.Supervisor, name: PruneBlobsSupervisor} @@ -63,7 +63,7 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do ] end - defp get_libp2p_args(genesis_time) do + defp get_libp2p_args() do config = Application.fetch_env!(:lambda_ethereum_consensus, :libp2p) port = Keyword.fetch!(config, :port) bootnodes = Keyword.fetch!(config, :bootnodes) @@ -75,7 +75,6 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do end [ - genesis_time: genesis_time, listen_addr: listen_addr, enable_discovery: true, discovery_addr: "0.0.0.0:#{port}", diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index ea6a1f57c..189fd7ffd 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -379,7 +379,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:ok, %{ genesis_time: genesis_time, - slot_data: {0, :first_third}, + slot_data: nil, port: port, subscribers: %{}, requests: Requests.new(), @@ -399,7 +399,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end @impl GenServer - def handle_cast({:on_tick, time}, %{genesis_time: genesis_time} = state) when time <= genesis_time, do: {:noreply, state} + def handle_cast({:on_tick, time}, %{genesis_time: genesis_time} = state) when time < genesis_time, do: {:noreply, state} def handle_cast({:on_tick, time}, %{genesis_time: genesis_time, slot_data: slot_data} = 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. From cb99b049da9be5c8ad4f6ff4bb3e2c45ddcd629c Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 30 Jul 2024 15:07:07 -0300 Subject: [PATCH 07/42] Format and genesis_time addition to libp2p starts on tests --- lib/lambda_ethereum_consensus/beacon/clock.ex | 7 +++++++ lib/lambda_ethereum_consensus/validator/duties.ex | 7 ++++++- lib/lambda_ethereum_consensus/validator/validator.ex | 8 ++++++-- .../validator/validator_manager.ex | 4 ++-- test/unit/beacon_api/beacon_api_v1_test.exs | 2 +- test/unit/libp2p_port_test.exs | 2 +- 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/clock.ex b/lib/lambda_ethereum_consensus/beacon/clock.ex index e615dc0ac..3dfb2f400 100644 --- a/lib/lambda_ethereum_consensus/beacon/clock.ex +++ b/lib/lambda_ethereum_consensus/beacon/clock.ex @@ -12,6 +12,7 @@ defmodule LambdaEthereumConsensus.Beacon.Ticker do @spec register_to_tick(atom() | [atom()]) :: :ok def register_to_tick(to_tick) when is_atom(to_tick), do: register_to_tick([to_tick]) + def register_to_tick(to_tick) when is_list(to_tick) do GenServer.cast(__MODULE__, {:register_to_tick, to_tick}) end @@ -39,6 +40,12 @@ defmodule LambdaEthereumConsensus.Beacon.Ticker do schedule_next_tick() time = :os.system_time(:second) + # TODO: This assumes that on_tick/1 is implemented for all modules in to_tick + # this could be later a behaviour but I'm not sure we want to maintain this ticker + # in the long run. It's important that on_tick/1 is a very fast operation and + # that any intensive computation is done asynchronously (e.g calling a cast). + # We could also consider using a tasks to run on_tick/1 in parallel, but for + # now this is enough to avoid complexity. Enum.each(to_tick, & &1.on_tick(time)) {:noreply, to_tick} diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index d264ddc5c..ff9b70ff9 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -69,7 +69,12 @@ defmodule LambdaEthereumConsensus.Validator.Duties do attester_duties # Drop the first element, which is the previous epoch's duty |> Stream.drop(1) - |> Enum.each(fn %{index_in_committee: i, committee_index: ci, slot: slot, should_aggregate?: sa} -> + |> Enum.each(fn %{ + index_in_committee: i, + committee_index: ci, + slot: slot, + should_aggregate?: sa + } -> Logger.info( "[Validator] #{validator_index} has to attest in committee #{ci} of slot #{slot} with index #{i}, and should_aggregate?: #{sa}" ) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 736c9525f..3379c4639 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -246,7 +246,10 @@ defmodule LambdaEthereumConsensus.Validator do attestation = produce_attestation(current_duty, state.root, state.validator.privkey) log_md = [slot: attestation.data.slot, attestation: attestation, subnet_id: subnet_id] - debug_log_msg = "publishing attestation on committee index: #{current_duty.committee_index} | as #{current_duty.index_in_committee}/#{current_duty.committee_length - 1} and pubkey: #{LambdaEthereumConsensus.Utils.format_shorten_binary(validator.pubkey)}" + + debug_log_msg = + "publishing attestation on committee index: #{current_duty.committee_index} | as #{current_duty.index_in_committee}/#{current_duty.committee_length - 1} and pubkey: #{LambdaEthereumConsensus.Utils.format_shorten_binary(validator.pubkey)}" + log_debug(validator.index, debug_log_msg, log_md) Gossip.Attestation.publish(subnet_id, attestation) @@ -303,7 +306,8 @@ defmodule LambdaEthereumConsensus.Validator do |> Stream.map(&Map.fetch!(&1, :aggregation_bits)) |> Enum.reduce(&BitField.bitwise_or/2) - {:ok, signature} = unique_attestations |> Enum.map(&Map.fetch!(&1, :signature)) |> Bls.aggregate() + {:ok, signature} = + unique_attestations |> Enum.map(&Map.fetch!(&1, :signature)) |> Bls.aggregate() %{List.first(attestations) | aggregation_bits: aggregation_bits, signature: signature} end diff --git a/lib/lambda_ethereum_consensus/validator/validator_manager.ex b/lib/lambda_ethereum_consensus/validator/validator_manager.ex index 6232f48a8..c6cd5e6ca 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_manager.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_manager.ex @@ -47,8 +47,8 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do {:ok, validators} end - @spec notify_new_block(Types.slot(), Types.root()) :: :ok - def notify_new_block(slot, head_root) do + @spec notify_new_block({Types.slot(), Types.root()}) :: :ok + def notify_new_block({slot, head_root}) do # Making this alone a cast sometimes solves the issue for a while # GenServer.cast(__MODULE__, {:notify_all, {:new_block, slot, head_root}}) notify_validators({:new_block, slot, head_root}) diff --git a/test/unit/beacon_api/beacon_api_v1_test.exs b/test/unit/beacon_api/beacon_api_v1_test.exs index 0157fdac8..91a4ec522 100644 --- a/test/unit/beacon_api/beacon_api_v1_test.exs +++ b/test/unit/beacon_api/beacon_api_v1_test.exs @@ -159,7 +159,7 @@ defmodule Unit.BeaconApiTest.V1 do alias LambdaEthereumConsensus.P2P.Metadata patch(ForkChoice, :get_fork_version, fn -> ChainSpec.get("DENEB_FORK_VERSION") end) - start_link_supervised!(Libp2pPort) + start_link_supervised!({Libp2pPort, genesis_time: 42}) Metadata.init() identity = Libp2pPort.get_node_identity() metadata = Metadata.get_metadata() diff --git a/test/unit/libp2p_port_test.exs b/test/unit/libp2p_port_test.exs index 7bf9e0d12..a3554339b 100644 --- a/test/unit/libp2p_port_test.exs +++ b/test/unit/libp2p_port_test.exs @@ -17,7 +17,7 @@ defmodule Unit.Libp2pPortTest do end defp start_port(name \\ Libp2pPort, init_args \\ []) do - start_link_supervised!({Libp2pPort, [opts: [name: name]] ++ init_args}, id: name) + start_link_supervised!({Libp2pPort, [opts: [name: name], genesis_time: 42] ++ init_args}, id: name) end @tag :tmp_dir From e3d732741c205f25af46e66a4558763410779f54 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 30 Jul 2024 17:05:33 -0300 Subject: [PATCH 08/42] ValidatorsManager Genserver removal --- config/runtime.exs | 2 +- .../beacon/beacon_node.ex | 27 +++---- .../fork_choice/fork_choice.ex | 6 +- .../validator/validator_manager.ex | 80 +++---------------- lib/libp2p_port.ex | 73 ++++++++++++++--- test/unit/libp2p_port_test.exs | 4 +- 6 files changed, 89 insertions(+), 103 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index 76331048b..896b509ae 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -171,7 +171,7 @@ if keystore_pass_dir != nil and not File.dir?(keystore_pass_dir) do System.halt(2) end -config :lambda_ethereum_consensus, LambdaEthereumConsensus.Validator.ValidatorManager, +config :lambda_ethereum_consensus, LambdaEthereumConsensus.Validator.Setup, keystore_dir: keystore_dir, keystore_pass_dir: keystore_pass_dir diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex index 5b7543cc5..742ee9eb4 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex @@ -8,7 +8,7 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.StateTransition.Cache alias LambdaEthereumConsensus.Store.BlockStates - alias LambdaEthereumConsensus.Validator.ValidatorManager + alias LambdaEthereumConsensus.Validator alias Types.BeaconState def start_link(opts) do @@ -24,43 +24,36 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do Cache.initialize_cache() - libp2p_args = get_libp2p_args() - time = :os.system_time(:second) ForkChoice.init_store(store, time) - validator_manager = - get_validator_manager( - deposit_tree_snapshot, - store.head_slot, - store.head_root - ) + init_execution_chain(deposit_tree_snapshot, store.head_root) + + validators = Validator.Setup.init(store.head_slot, store.head_root) + + libp2p_args = [genesis_time: store.genesis_time, validators: validators] ++ get_libp2p_args() children = [ {LambdaEthereumConsensus.Beacon.Ticker, [LambdaEthereumConsensus.Libp2pPort]}, - {LambdaEthereumConsensus.Libp2pPort, [{:genesis_time, store.genesis_time} | libp2p_args]}, + {LambdaEthereumConsensus.Libp2pPort, libp2p_args}, {Task.Supervisor, name: PruneStatesSupervisor}, {Task.Supervisor, name: PruneBlocksSupervisor}, {Task.Supervisor, name: PruneBlobsSupervisor} - ] ++ validator_manager + ] Supervisor.init(children, strategy: :one_for_all) end - defp get_validator_manager(nil, _, _) do + defp init_execution_chain(nil, _) do Logger.warning("Deposit data not found. Validator will be disabled.") [] end - defp get_validator_manager(snapshot, slot, head_root) do + defp init_execution_chain(snapshot, head_root) do %BeaconState{eth1_data_votes: votes} = BlockStates.get_state_info!(head_root).beacon_state LambdaEthereumConsensus.Execution.ExecutionChain.init(snapshot, votes) - # TODO: move checkpoint sync outside and move this to application.ex - [ - {ValidatorManager, {slot, head_root}} - ] end defp get_libp2p_args() do diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index e9bad9250..de7031554 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -4,11 +4,10 @@ defmodule LambdaEthereumConsensus.ForkChoice do """ require Logger - - # alias LambdaEthereumConsensus.Beacon.Clock alias LambdaEthereumConsensus.Execution.ExecutionChain alias LambdaEthereumConsensus.ForkChoice.Handlers alias LambdaEthereumConsensus.ForkChoice.Head + alias LambdaEthereumConsensus.Libp2pPort alias LambdaEthereumConsensus.Metrics alias LambdaEthereumConsensus.P2P.Gossip.OperationsCollector alias LambdaEthereumConsensus.StateTransition.Misc @@ -18,7 +17,6 @@ defmodule LambdaEthereumConsensus.ForkChoice do alias LambdaEthereumConsensus.Store.CheckpointStates alias LambdaEthereumConsensus.Store.StateDb alias LambdaEthereumConsensus.Store.StoreDb - alias LambdaEthereumConsensus.Validator.ValidatorManager alias Types.Attestation alias Types.BlockInfo alias Types.Checkpoint @@ -271,7 +269,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do store.finalized_checkpoint ) - ValidatorManager.notify_new_block(slot, head_root) + Libp2pPort.notify_new_block({slot, head_root}) Logger.info("[Fork choice] Updated fork choice cache", slot: slot) diff --git a/lib/lambda_ethereum_consensus/validator/validator_manager.ex b/lib/lambda_ethereum_consensus/validator/validator_manager.ex index c6cd5e6ca..084fb16fa 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_manager.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_manager.ex @@ -1,21 +1,13 @@ -defmodule LambdaEthereumConsensus.Validator.ValidatorManager do +defmodule LambdaEthereumConsensus.Validator.Setup do @moduledoc """ - Module that manage the validators state + Module that setups the initial validators state """ - use GenServer require Logger - alias LambdaEthereumConsensus.Beacon.Clock alias LambdaEthereumConsensus.Validator - @spec start_link({Types.slot(), Types.root()}) :: :ignore | {:error, any} | {:ok, pid} - def start_link({slot, head_root}) do - GenServer.start_link(__MODULE__, {slot, head_root}, name: __MODULE__) - end - - @spec init({Types.slot(), Types.root()}) :: - {:ok, %{Bls.pubkey() => Validator.state()}} | {:stop, any} - def init({slot, head_root}) do + @spec init(Types.slot(), Types.root()) :: %{Bls.pubkey() => Validator.state()} + def init(slot, head_root) do config = Application.get_env(:lambda_ethereum_consensus, __MODULE__, []) keystore_dir = Keyword.get(config, :keystore_dir) keystore_pass_dir = Keyword.get(config, :keystore_pass_dir) @@ -26,10 +18,10 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) when is_nil(keystore_dir) or is_nil(keystore_pass_dir) do Logger.warning( - "[Validator Manager] No keystore_dir or keystore_pass_dir provided. Validator will not start." + "[Validator] No keystore_dir or keystore_pass_dir provided. Validator will not start." ) - {:ok, []} + %{} end defp setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) do @@ -42,63 +34,11 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do end) |> Map.new() - Logger.info("[Validator Manager] Initialized #{Enum.count(validators)} validators") - - {:ok, validators} - end - - @spec notify_new_block({Types.slot(), Types.root()}) :: :ok - def notify_new_block({slot, head_root}) do - # Making this alone a cast sometimes solves the issue for a while - # GenServer.cast(__MODULE__, {:notify_all, {:new_block, slot, head_root}}) - notify_validators({:new_block, slot, head_root}) - end - - @spec notify_tick(Clock.logical_time()) :: :ok - def notify_tick(logical_time) do - # Making this a cast alone doesn't solve the issue - # GenServer.cast(__MODULE__, {:notify_all, {:on_tick, logical_time}}) - notify_validators({:on_tick, logical_time}) - end - - # TODO: The use of a Genserver and cast is still needed to avoid locking at the clock level. - # This is a temporary solution and will be taken off in a future PR. - defp notify_validators(msg), do: GenServer.call(__MODULE__, {:notify_all, msg}, 20_000) + Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") - def handle_cast({:notify_all, msg}, validators) do - validators = notify_all(validators, msg) - - {:noreply, validators} + validators end - def handle_call({:notify_all, msg}, _from, validators) do - validators = notify_all(validators, msg) - - {:reply, :ok, validators} - end - - defp notify_all(validators, msg) do - start_time = System.monotonic_time(:millisecond) - - Logger.info("[Validator Manager] Notifying all Validators with message: #{inspect(msg)}") - - updated_validators = Enum.map(validators, ¬ify_validator(&1, msg)) - - end_time = System.monotonic_time(:millisecond) - - Logger.debug( - "[Validator Manager] #{inspect(msg)} notified to all Validators after #{end_time - start_time} ms" - ) - - updated_validators - end - - defp notify_validator({pubkey, validator}, {:on_tick, logical_time}), - do: {pubkey, Validator.handle_tick(logical_time, validator)} - - defp notify_validator({pubkey, validator}, {:new_block, slot, head_root}), - do: {pubkey, Validator.handle_new_block(slot, head_root, validator)} - @doc """ Get validator keys from the keystore directory. This function expects two files for each validator: @@ -119,7 +59,7 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do {keystore_file, keystore_pass_file} else - Logger.warning("[Validator Manager] Skipping file: #{filename}. Not a keystore file.") + Logger.warning("[Validator] Skipping file: #{filename}. Not a keystore file.") nil end end) @@ -131,7 +71,7 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do rescue error -> Logger.error( - "[Validator Manager] Failed to decode keystore file: #{keystore_file}. Pass file: #{keystore_pass_file} Error: #{inspect(error)}" + "[Validator] Failed to decode keystore file: #{keystore_file}. Pass file: #{keystore_pass_file} Error: #{inspect(error)}" ) nil diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index af97c3d50..c325be54c 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -9,7 +9,6 @@ defmodule LambdaEthereumConsensus.Libp2pPort do use GenServer - alias LambdaEthereumConsensus.Validator.ValidatorManager alias LambdaEthereumConsensus.Beacon.PendingBlocks alias LambdaEthereumConsensus.Beacon.SyncBlocks alias LambdaEthereumConsensus.ForkChoice @@ -22,6 +21,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do alias LambdaEthereumConsensus.P2p.Requests alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.Utils.BitVector + alias LambdaEthereumConsensus.Validator alias Libp2pProto.AddPeer alias Libp2pProto.Command alias Libp2pProto.Enr @@ -63,6 +63,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @type init_arg :: {:genesis_time, Types.uint64()} + | {:validators, %{}} | {:listen_addr, [String.t()]} | {:enable_discovery, boolean()} | {:discovery_addr, String.t()} @@ -113,6 +114,14 @@ defmodule LambdaEthereumConsensus.Libp2pPort do GenServer.cast(__MODULE__, {:on_tick, time}) end + @spec notify_new_block({Types.slot(), Types.root()}) :: :ok + def notify_new_block(data) do + # TODO: This is quick workarround to notify the libp2p port about new blocks from within + # the ForkChoice.recompute_head/1 without moving the validators to the store this + # allows to deferr that move until we simplify the state and remove duplicates. + send(self(), {:new_block, data}) + end + @doc """ Retrieves identity info from the underlying LibP2P node. """ @@ -354,6 +363,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def init(args) do {genesis_time, args} = Keyword.pop!(args, :genesis_time) + {validators, args} = Keyword.pop(args, :validators, %{}) {join_init_topics, args} = Keyword.pop(args, :join_init_topics, false) {enable_request_handlers, args} = Keyword.pop(args, :enable_request_handlers, false) @@ -379,6 +389,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:ok, %{ genesis_time: genesis_time, + validators: validators, slot_data: nil, port: port, subscribers: %{}, @@ -408,7 +419,10 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end @impl GenServer - def handle_cast({:on_tick, time}, %{genesis_time: genesis_time} = state) when time < genesis_time, do: {:noreply, state} + def handle_cast({:on_tick, time}, %{genesis_time: genesis_time} = state) + when time < genesis_time, + do: {:noreply, state} + def handle_cast({:on_tick, time}, %{genesis_time: genesis_time, slot_data: slot_data} = 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. @@ -417,13 +431,11 @@ defmodule LambdaEthereumConsensus.Libp2pPort do new_slot_data = compute_slot(genesis_time, time) - if slot_data != new_slot_data do - ValidatorManager.notify_tick(slot_data) - end + updated_state = maybe_tick_validators(slot_data != new_slot_data, new_slot_data, state) - log_new_slot(new_slot_data) + log_new_slot(slot_data, new_slot_data) - {:noreply, %{state | slot_data: new_slot_data}} + {:noreply, updated_state} end def handle_cast( @@ -486,6 +498,13 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:noreply, new_state} end + @impl GenServer + def handle_info({:new_block, data}, %{validators: validators} = state) do + updated_validators = notify_validators(validators, {:new_block, data}) + + {:noreply, %{state | validators: updated_validators}} + end + @impl GenServer def handle_info({_port, {:data, data}}, state) do %Notification{n: {_, payload}} = Notification.decode(data) @@ -707,6 +726,38 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end) end + # Validator related functions + + defp maybe_tick_validators(false = _slot_data_changed, _slot_data, state), do: state + + defp maybe_tick_validators(true, slot_data, %{validators: validators} = state) do + updated_validators = notify_validators(validators, {:on_tick, slot_data}) + + %{state | slot_data: slot_data, validators: updated_validators} + end + + defp notify_validators(validators, msg) do + start_time = System.monotonic_time(:millisecond) + + Logger.info("[Libp2p] Notifying all Validators with message: #{inspect(msg)}") + + updated_validators = Enum.map(validators, ¬ify_validator(&1, msg)) + + end_time = System.monotonic_time(:millisecond) + + Logger.debug( + "[Validator Manager] #{inspect(msg)} notified to all Validators after #{end_time - start_time} ms" + ) + + updated_validators + end + + defp notify_validator({pubkey, validator}, {:on_tick, slot_data}), + do: {pubkey, Validator.handle_tick(slot_data, validator)} + + defp notify_validator({pubkey, validator}, {:new_block, {slot, head_root}}), + do: {pubkey, Validator.handle_new_block(slot, head_root, validator)} + @spec compute_slot(Types.uint64(), Types.uint64()) :: slot_data() defp compute_slot(genesis_time, time) do # TODO: This was copied as it is from the Clock to convert it into just a Ticker, @@ -726,12 +777,14 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {slot, slot_third} end - defp log_new_slot({slot, :first_third}) do + defp log_new_slot({slot, _third}, {slot, _third}), do: :ok + + defp log_new_slot({_prev_slot, _thrid}, {slot, :first_third}) do # TODO: as with the previous function, this was copied from the Clock module. - # is use :sync, :store as the slot event, probably something to look into. + # It use :sync, :store as the slot event, probably something to look into. :telemetry.execute([:sync, :store], %{slot: slot}) Logger.info("[Libp2p] Slot transition", slot: slot) end - defp log_new_slot(_), do: :ok + defp log_new_slot(_, _), do: :ok end diff --git a/test/unit/libp2p_port_test.exs b/test/unit/libp2p_port_test.exs index a3554339b..82fb5bc52 100644 --- a/test/unit/libp2p_port_test.exs +++ b/test/unit/libp2p_port_test.exs @@ -17,7 +17,9 @@ defmodule Unit.Libp2pPortTest do end defp start_port(name \\ Libp2pPort, init_args \\ []) do - start_link_supervised!({Libp2pPort, [opts: [name: name], genesis_time: 42] ++ init_args}, id: name) + start_link_supervised!({Libp2pPort, [opts: [name: name], genesis_time: 42] ++ init_args}, + id: name + ) end @tag :tmp_dir From 3802005622f064d3805a978f4fa3e45d8c425312 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 30 Jul 2024 17:14:06 -0300 Subject: [PATCH 09/42] renamed validator manager and clock to Validator.Setup and Ticker --- lib/lambda_ethereum_consensus/{beacon/clock.ex => ticker.ex} | 2 +- .../validator/{validator_manager.ex => setup.ex} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/lambda_ethereum_consensus/{beacon/clock.ex => ticker.ex} (97%) rename lib/lambda_ethereum_consensus/validator/{validator_manager.ex => setup.ex} (100%) diff --git a/lib/lambda_ethereum_consensus/beacon/clock.ex b/lib/lambda_ethereum_consensus/ticker.ex similarity index 97% rename from lib/lambda_ethereum_consensus/beacon/clock.ex rename to lib/lambda_ethereum_consensus/ticker.ex index 3dfb2f400..17f6eb7d7 100644 --- a/lib/lambda_ethereum_consensus/beacon/clock.ex +++ b/lib/lambda_ethereum_consensus/ticker.ex @@ -1,4 +1,4 @@ -defmodule LambdaEthereumConsensus.Beacon.Ticker do +defmodule LambdaEthereumConsensus.Ticker do @moduledoc false use GenServer diff --git a/lib/lambda_ethereum_consensus/validator/validator_manager.ex b/lib/lambda_ethereum_consensus/validator/setup.ex similarity index 100% rename from lib/lambda_ethereum_consensus/validator/validator_manager.ex rename to lib/lambda_ethereum_consensus/validator/setup.ex From 2de70bff7d15cfc2e336494ca810b96e1ab7ea53 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 30 Jul 2024 17:45:34 -0300 Subject: [PATCH 10/42] Small fixe after renaming the ticker --- lib/lambda_ethereum_consensus/beacon/beacon_node.ex | 2 +- lib/lambda_ethereum_consensus/validator/validator.ex | 3 +-- lib/libp2p_port.ex | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex index 742ee9eb4..19f96a212 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex @@ -36,7 +36,7 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do children = [ - {LambdaEthereumConsensus.Beacon.Ticker, [LambdaEthereumConsensus.Libp2pPort]}, + {LambdaEthereumConsensus.Ticker, [LambdaEthereumConsensus.Libp2pPort]}, {LambdaEthereumConsensus.Libp2pPort, libp2p_args}, {Task.Supervisor, name: PruneStatesSupervisor}, {Task.Supervisor, name: PruneBlocksSupervisor}, diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 3379c4639..16c7ba760 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -13,7 +13,6 @@ defmodule LambdaEthereumConsensus.Validator do :payload_builder ] - alias LambdaEthereumConsensus.Beacon.Clock alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.Libp2pPort alias LambdaEthereumConsensus.P2P.Gossip @@ -116,7 +115,7 @@ defmodule LambdaEthereumConsensus.Validator do |> maybe_build_payload(slot + 1) end - @spec handle_tick(Clock.logical_time(), state) :: state + @spec handle_tick({Types.slot(), atom()}, state) :: state def handle_tick(_logical_time, %{validator: %{index: nil}} = state) do log_error("-1", "setup validator", "index not present for handle tick") state diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index c325be54c..33acd2df3 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -777,7 +777,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {slot, slot_third} end - defp log_new_slot({slot, _third}, {slot, _third}), do: :ok + defp log_new_slot({slot, _third}, {slot, _another_third}), do: :ok defp log_new_slot({_prev_slot, _thrid}, {slot, :first_third}) do # TODO: as with the previous function, this was copied from the Clock module. From ff423015de63a757ecf9b18436450afd94e0c752 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 30 Jul 2024 21:05:25 -0300 Subject: [PATCH 11/42] Simplified the Ticker and added dialyzer to the 'make lint' task --- Makefile | 1 + lib/lambda_ethereum_consensus/ticker.ex | 19 ++++--------------- .../validator/validator.ex | 3 --- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index a5c9ee815..4fd177628 100644 --- a/Makefile +++ b/Makefile @@ -264,6 +264,7 @@ lint: mix recode --no-autocorrect mix format --check-formatted mix credo --strict + mix dialyzer --no-check #✅ fmt: @ Format all code (Go, rust and elixir). fmt: diff --git a/lib/lambda_ethereum_consensus/ticker.ex b/lib/lambda_ethereum_consensus/ticker.ex index 17f6eb7d7..4efb5a89c 100644 --- a/lib/lambda_ethereum_consensus/ticker.ex +++ b/lib/lambda_ethereum_consensus/ticker.ex @@ -3,6 +3,8 @@ defmodule LambdaEthereumConsensus.Ticker do use GenServer + @tick_time 1000 + require Logger @spec start_link([atom()]) :: :ignore | {:error, any} | {:ok, pid} @@ -10,13 +12,6 @@ defmodule LambdaEthereumConsensus.Ticker do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end - @spec register_to_tick(atom() | [atom()]) :: :ok - def register_to_tick(to_tick) when is_atom(to_tick), do: register_to_tick([to_tick]) - - def register_to_tick(to_tick) when is_list(to_tick) do - GenServer.cast(__MODULE__, {:register_to_tick, to_tick}) - end - ########################## ### GenServer Callbacks ########################## @@ -25,19 +20,13 @@ defmodule LambdaEthereumConsensus.Ticker do @spec init([atom()]) :: {:ok, [atom()]} | {:stop, any} def init(to_tick) when is_list(to_tick) do schedule_next_tick() - {:ok, to_tick} end - @impl true - def handle_cast({:register_to_tick, to_tick_additions}, to_tick) do - new_to_tick = Enum.uniq(to_tick ++ to_tick_additions) - {:noreply, new_to_tick} - end - @impl true def handle_info(:on_tick, to_tick) do schedule_next_tick() + # If @tick_time becomes less than 1000, we should use :millisecond instead. time = :os.system_time(:second) # TODO: This assumes that on_tick/1 is implemented for all modules in to_tick @@ -53,7 +42,7 @@ defmodule LambdaEthereumConsensus.Ticker do def schedule_next_tick() do # For millisecond precision - time_to_next_tick = 1000 - rem(:os.system_time(:millisecond), 1000) + time_to_next_tick = @tick_time - rem(:os.system_time(:millisecond), @tick_time) Process.send_after(__MODULE__, :on_tick, time_to_next_tick) end end diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 16c7ba760..01a4c460c 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -515,9 +515,6 @@ defmodule LambdaEthereumConsensus.Validator do defp log_result(:ok, :info, index, message, metadata), do: log_info(index, message, metadata) defp log_result(:ok, :debug, index, message, metadata), do: log_debug(index, message, metadata) - defp log_result({:error, reason}, _level, index, message, metadata), - do: log_error(index, message, reason, metadata) - defp log_info(index, message, metadata \\ []), do: Logger.info("[Validator] #{index} #{message}", metadata) From 46efa3a1a575616cbda8cb2f4a2e4d1e56641e89 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 30 Jul 2024 21:11:17 -0300 Subject: [PATCH 12/42] Fixed some dialyzer issues removing unused functions --- .../fork_choice/fork_choice.ex | 3 +-- .../p2p/gossip/beacon_block.ex | 25 ++++++++++--------- .../p2p/gossip/blob_sidecar.ex | 25 ++++++++++--------- .../p2p/gossip/operations_collector.ex | 23 +++++++++-------- 4 files changed, 39 insertions(+), 37 deletions(-) diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index de7031554..e753ab649 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -259,7 +259,6 @@ defmodule LambdaEthereumConsensus.ForkChoice do %{slot: slot, body: body} = head_block OperationsCollector.notify_new_block(head_block) - ExecutionChain.notify_new_block(slot, body.eth1_data, body.execution_payload) update_fork_choice_data( @@ -271,7 +270,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do Libp2pPort.notify_new_block({slot, head_root}) - Logger.info("[Fork choice] Updated fork choice cache", slot: slot) + Logger.debug("[Fork choice] Updated fork choice cache", slot: slot) :ok end diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex b/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex index b09efccf7..03036f1e5 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex @@ -38,19 +38,20 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.BeaconBlock do :ok end - @spec subscribe_to_topic() :: :ok | :error - def subscribe_to_topic() do - topic() - |> Libp2pPort.subscribe_to_topic(__MODULE__) - |> case do - :ok -> - :ok + # # TODO: Is anyone using this function? + # @spec subscribe_to_topic() :: :ok | :error + # def subscribe_to_topic() do + # topic() + # |> Libp2pPort.subscribe_to_topic(__MODULE__) + # |> case do + # :ok -> + # :ok - {:error, reason} -> - Logger.error("[Gossip] Subscription failed: '#{reason}'") - :error - end - end + # {:error, reason} -> + # Logger.error("[Gossip] Subscription failed: '#{reason}'") + # :error + # end + # end def topic() do fork_context = ForkChoice.get_fork_digest() |> Base.encode16(case: :lower) diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/blob_sidecar.ex b/lib/lambda_ethereum_consensus/p2p/gossip/blob_sidecar.ex index 439169bd6..648ef745f 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/blob_sidecar.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/blob_sidecar.ex @@ -26,19 +26,20 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.BlobSideCar do end end - @spec subscribe_to_topics() :: :ok | {:error, String.t()} - def subscribe_to_topics() do - Enum.each(topics(), fn topic -> - case Libp2pPort.subscribe_to_topic(topic, __MODULE__) do - :ok -> - :ok + # TODO: Is anyone using this function? + # @spec subscribe_to_topics() :: :ok | {:error, String.t()} + # def subscribe_to_topics() do + # Enum.each(topics(), fn topic -> + # case Libp2pPort.subscribe_to_topic(topic, __MODULE__) do + # :ok -> + # :ok - {:error, reason} -> - Logger.error("[Gossip] Subscription failed: '#{reason}'") - {:error, reason} - end - end) - end + # {:error, reason} -> + # Logger.error("[Gossip] Subscription failed: '#{reason}'") + # {:error, reason} + # end + # end) + # end def topics() do # TODO: this doesn't take into account fork digest changes diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex b/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex index 46d0578e8..97269c1eb 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex @@ -40,17 +40,18 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.OperationsCollector do "bls_to_execution_change" ] - def subscribe_to_topics() do - Enum.reduce_while(topics(), :ok, fn topic, _acc -> - case Libp2pPort.subscribe_to_topic(topic, __MODULE__) do - :ok -> - {:cont, :ok} - - {:error, reason} -> - {:halt, {:error, "[OperationsCollector] Subscription failed: '#{reason}'"}} - end - end) - end + # TODO: Is anyone using this function? + # def subscribe_to_topics() do + # Enum.reduce_while(topics(), :ok, fn topic, _acc -> + # case Libp2pPort.subscribe_to_topic(topic, __MODULE__) do + # :ok -> + # {:cont, :ok} + + # {:error, reason} -> + # {:halt, {:error, "[OperationsCollector] Subscription failed: '#{reason}'"}} + # end + # end) + # end @spec get_bls_to_execution_changes(non_neg_integer()) :: list(SignedBLSToExecutionChange.t()) def get_bls_to_execution_changes(count) do From 55c5d90fb5d30bfefc04620a693fbf1baa5e9a11 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 30 Jul 2024 21:19:05 -0300 Subject: [PATCH 13/42] Simplify a diff --- .../fork_choice/fork_choice.ex | 3 +-- lib/libp2p_port.ex | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index e753ab649..2e078c0ce 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -259,6 +259,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do %{slot: slot, body: body} = head_block OperationsCollector.notify_new_block(head_block) + Libp2pPort.notify_new_block(slot, head_root) ExecutionChain.notify_new_block(slot, body.eth1_data, body.execution_payload) update_fork_choice_data( @@ -268,8 +269,6 @@ defmodule LambdaEthereumConsensus.ForkChoice do store.finalized_checkpoint ) - Libp2pPort.notify_new_block({slot, head_root}) - Logger.debug("[Fork choice] Updated fork choice cache", slot: slot) :ok diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 33acd2df3..556ac134c 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -114,12 +114,12 @@ defmodule LambdaEthereumConsensus.Libp2pPort do GenServer.cast(__MODULE__, {:on_tick, time}) end - @spec notify_new_block({Types.slot(), Types.root()}) :: :ok - def notify_new_block(data) do + @spec notify_new_block(Types.slot(), Types.root()) :: :ok + def notify_new_block(slot, head_root) do # TODO: This is quick workarround to notify the libp2p port about new blocks from within # the ForkChoice.recompute_head/1 without moving the validators to the store this # allows to deferr that move until we simplify the state and remove duplicates. - send(self(), {:new_block, data}) + send(self(), {:new_block, slot, head_root}) end @doc """ @@ -499,8 +499,8 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end @impl GenServer - def handle_info({:new_block, data}, %{validators: validators} = state) do - updated_validators = notify_validators(validators, {:new_block, data}) + def handle_info({:new_block, slot, head_root}, %{validators: validators} = state) do + updated_validators = notify_validators(validators, {:new_block, slot, head_root}) {:noreply, %{state | validators: updated_validators}} end @@ -755,7 +755,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do defp notify_validator({pubkey, validator}, {:on_tick, slot_data}), do: {pubkey, Validator.handle_tick(slot_data, validator)} - defp notify_validator({pubkey, validator}, {:new_block, {slot, head_root}}), + defp notify_validator({pubkey, validator}, {:new_block, slot, head_root}), do: {pubkey, Validator.handle_new_block(slot, head_root, validator)} @spec compute_slot(Types.uint64(), Types.uint64()) :: slot_data() From 9eed36594be537c777ac80ca247604e073cde210 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 31 Jul 2024 17:20:39 -0300 Subject: [PATCH 14/42] Added an async subscribe to topic to avoid issues in test --- .../p2p/gossip/attestation.ex | 2 +- .../p2p/gossip/beacon_block.ex | 25 +++++++++---------- .../p2p/gossip/blob_sidecar.ex | 25 +++++++++---------- .../p2p/gossip/operations_collector.ex | 23 ++++++++--------- lib/libp2p_port.ex | 17 +++++++++++++ 5 files changed, 53 insertions(+), 39 deletions(-) diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex b/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex index fab0b691b..11ea44353 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex @@ -66,7 +66,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.Attestation do def collect(subnet_id, attestation) do join(subnet_id) SubnetInfo.new_subnet_with_attestation(subnet_id, attestation) - Libp2pPort.subscribe_to_topic(topic(subnet_id), __MODULE__) + Libp2pPort.async_subscribe_to_topic(topic(subnet_id), __MODULE__) end @spec stop_collecting(non_neg_integer()) :: diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex b/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex index 03036f1e5..b09efccf7 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex @@ -38,20 +38,19 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.BeaconBlock do :ok end - # # TODO: Is anyone using this function? - # @spec subscribe_to_topic() :: :ok | :error - # def subscribe_to_topic() do - # topic() - # |> Libp2pPort.subscribe_to_topic(__MODULE__) - # |> case do - # :ok -> - # :ok + @spec subscribe_to_topic() :: :ok | :error + def subscribe_to_topic() do + topic() + |> Libp2pPort.subscribe_to_topic(__MODULE__) + |> case do + :ok -> + :ok - # {:error, reason} -> - # Logger.error("[Gossip] Subscription failed: '#{reason}'") - # :error - # end - # end + {:error, reason} -> + Logger.error("[Gossip] Subscription failed: '#{reason}'") + :error + end + end def topic() do fork_context = ForkChoice.get_fork_digest() |> Base.encode16(case: :lower) diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/blob_sidecar.ex b/lib/lambda_ethereum_consensus/p2p/gossip/blob_sidecar.ex index 648ef745f..439169bd6 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/blob_sidecar.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/blob_sidecar.ex @@ -26,20 +26,19 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.BlobSideCar do end end - # TODO: Is anyone using this function? - # @spec subscribe_to_topics() :: :ok | {:error, String.t()} - # def subscribe_to_topics() do - # Enum.each(topics(), fn topic -> - # case Libp2pPort.subscribe_to_topic(topic, __MODULE__) do - # :ok -> - # :ok + @spec subscribe_to_topics() :: :ok | {:error, String.t()} + def subscribe_to_topics() do + Enum.each(topics(), fn topic -> + case Libp2pPort.subscribe_to_topic(topic, __MODULE__) do + :ok -> + :ok - # {:error, reason} -> - # Logger.error("[Gossip] Subscription failed: '#{reason}'") - # {:error, reason} - # end - # end) - # end + {:error, reason} -> + Logger.error("[Gossip] Subscription failed: '#{reason}'") + {:error, reason} + end + end) + end def topics() do # TODO: this doesn't take into account fork digest changes diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex b/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex index 97269c1eb..46d0578e8 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/operations_collector.ex @@ -40,18 +40,17 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.OperationsCollector do "bls_to_execution_change" ] - # TODO: Is anyone using this function? - # def subscribe_to_topics() do - # Enum.reduce_while(topics(), :ok, fn topic, _acc -> - # case Libp2pPort.subscribe_to_topic(topic, __MODULE__) do - # :ok -> - # {:cont, :ok} - - # {:error, reason} -> - # {:halt, {:error, "[OperationsCollector] Subscription failed: '#{reason}'"}} - # end - # end) - # end + def subscribe_to_topics() do + Enum.reduce_while(topics(), :ok, fn topic, _acc -> + case Libp2pPort.subscribe_to_topic(topic, __MODULE__) do + :ok -> + {:cont, :ok} + + {:error, reason} -> + {:halt, {:error, "[OperationsCollector] Subscription failed: '#{reason}'"}} + end + end) + end @spec get_bls_to_execution_changes(non_neg_integer()) :: list(SignedBLSToExecutionChange.t()) def get_bls_to_execution_changes(count) do diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 556ac134c..8757cb261 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -256,6 +256,23 @@ defmodule LambdaEthereumConsensus.Libp2pPort do GenServer.cast(pid, {:new_subscriber, topic_name, module}) + call_command(pid, {:subscribe, %SubscribeToTopic{name: topic_name}}) + end + + @doc """ + Subscribes to the given topic async, not waiting for a response at the subscribe. + After this, messages published to the topicwill be received by `self()`. + """ + @spec async_subscribe_to_topic(GenServer.server(), String.t(), module()) :: + :ok | {:error, String.t()} + def async_subscribe_to_topic(pid \\ __MODULE__, topic_name, module) do + :telemetry.execute([:port, :message], %{}, %{ + function: "async_subscribe_to_topic", + direction: "elixir->" + }) + + GenServer.cast(pid, {:new_subscriber, topic_name, module}) + cast_command(pid, {:subscribe, %SubscribeToTopic{name: topic_name}}) end From 5721ccf61ca3b1488b8a6f94d2479ab4242a611e Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 31 Jul 2024 17:27:03 -0300 Subject: [PATCH 15/42] Remove unneded diffs --- lib/libp2p_port.ex | 5 +---- network_params.yaml | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 8757cb261..2b68442c7 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -743,8 +743,6 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end) end - # Validator related functions - defp maybe_tick_validators(false = _slot_data_changed, _slot_data, state), do: state defp maybe_tick_validators(true, slot_data, %{validators: validators} = state) do @@ -797,8 +795,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do defp log_new_slot({slot, _third}, {slot, _another_third}), do: :ok defp log_new_slot({_prev_slot, _thrid}, {slot, :first_third}) do - # TODO: as with the previous function, this was copied from the Clock module. - # It use :sync, :store as the slot event, probably something to look into. + # TODO: It used :sync, :store as the slot event in the old Clock, double-check. :telemetry.execute([:sync, :store], %{slot: slot}) Logger.info("[Libp2p] Slot transition", slot: slot) end diff --git a/network_params.yaml b/network_params.yaml index fc641b906..e995d03fa 100644 --- a/network_params.yaml +++ b/network_params.yaml @@ -2,11 +2,11 @@ participants: - el_type: geth cl_type: lighthouse count: 2 - validator_count: 27 + validator_count: 32 - el_type: geth cl_type: lambda cl_image: lambda_ethereum_consensus:latest use_separate_vc: false count: 1 - validator_count: 20 + validator_count: 32 cl_max_mem: 4096 \ No newline at end of file From b1e9a3ed587c9fb9e43f93bef183258c85bdd348 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 1 Aug 2024 11:56:01 -0300 Subject: [PATCH 16/42] Ticker removal --- .../beacon/beacon_node.ex | 1 - lib/lambda_ethereum_consensus/ticker.ex | 48 ------------- lib/libp2p_port.ex | 71 +++++++++++-------- 3 files changed, 40 insertions(+), 80 deletions(-) delete mode 100644 lib/lambda_ethereum_consensus/ticker.ex diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex index 19f96a212..f939f597b 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex @@ -36,7 +36,6 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do children = [ - {LambdaEthereumConsensus.Ticker, [LambdaEthereumConsensus.Libp2pPort]}, {LambdaEthereumConsensus.Libp2pPort, libp2p_args}, {Task.Supervisor, name: PruneStatesSupervisor}, {Task.Supervisor, name: PruneBlocksSupervisor}, diff --git a/lib/lambda_ethereum_consensus/ticker.ex b/lib/lambda_ethereum_consensus/ticker.ex deleted file mode 100644 index 4efb5a89c..000000000 --- a/lib/lambda_ethereum_consensus/ticker.ex +++ /dev/null @@ -1,48 +0,0 @@ -defmodule LambdaEthereumConsensus.Ticker do - @moduledoc false - - use GenServer - - @tick_time 1000 - - require Logger - - @spec start_link([atom()]) :: :ignore | {:error, any} | {:ok, pid} - def start_link(opts) do - GenServer.start_link(__MODULE__, opts, name: __MODULE__) - end - - ########################## - ### GenServer Callbacks - ########################## - - @impl true - @spec init([atom()]) :: {:ok, [atom()]} | {:stop, any} - def init(to_tick) when is_list(to_tick) do - schedule_next_tick() - {:ok, to_tick} - end - - @impl true - def handle_info(:on_tick, to_tick) do - schedule_next_tick() - # If @tick_time becomes less than 1000, we should use :millisecond instead. - time = :os.system_time(:second) - - # TODO: This assumes that on_tick/1 is implemented for all modules in to_tick - # this could be later a behaviour but I'm not sure we want to maintain this ticker - # in the long run. It's important that on_tick/1 is a very fast operation and - # that any intensive computation is done asynchronously (e.g calling a cast). - # We could also consider using a tasks to run on_tick/1 in parallel, but for - # now this is enough to avoid complexity. - Enum.each(to_tick, & &1.on_tick(time)) - - {:noreply, to_tick} - end - - def schedule_next_tick() do - # For millisecond precision - time_to_next_tick = @tick_time - rem(:os.system_time(:millisecond), @tick_time) - Process.send_after(__MODULE__, :on_tick, time_to_next_tick) - end -end diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 2b68442c7..5cadc5d7d 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -9,6 +9,8 @@ defmodule LambdaEthereumConsensus.Libp2pPort do use GenServer + @tick_time 1000 + alias LambdaEthereumConsensus.Beacon.PendingBlocks alias LambdaEthereumConsensus.Beacon.SyncBlocks alias LambdaEthereumConsensus.ForkChoice @@ -109,11 +111,6 @@ defmodule LambdaEthereumConsensus.Libp2pPort do GenServer.start_link(__MODULE__, args, opts) end - @spec on_tick(Types.uint64()) :: :ok - def on_tick(time) do - GenServer.cast(__MODULE__, {:on_tick, time}) - end - @spec notify_new_block(Types.slot(), Types.root()) :: :ok def notify_new_block(slot, head_root) do # TODO: This is quick workarround to notify the libp2p port about new blocks from within @@ -403,6 +400,8 @@ defmodule LambdaEthereumConsensus.Libp2pPort do "[Optimistic Sync] Waiting #{@sync_delay_millis / 1000} seconds to discover some peers before requesting blocks." ) + schedule_next_tick() + {:ok, %{ genesis_time: genesis_time, @@ -435,26 +434,6 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:noreply, state} end - @impl GenServer - def handle_cast({:on_tick, time}, %{genesis_time: genesis_time} = state) - when time < genesis_time, - do: {:noreply, state} - - def handle_cast({:on_tick, time}, %{genesis_time: genesis_time, slot_data: slot_data} = 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) - - new_slot_data = compute_slot(genesis_time, time) - - updated_state = maybe_tick_validators(slot_data != new_slot_data, new_slot_data, state) - - log_new_slot(slot_data, new_slot_data) - - {:noreply, updated_state} - end - def handle_cast( {:send_request, peer_id, protocol_id, message, handler}, %{ @@ -505,6 +484,14 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:noreply, state} end + @impl GenServer + def handle_info(:on_tick, %{genesis_time: genesis_time} = state) do + schedule_next_tick() + time = :os.system_time(:second) + + {:noreply, on_tick(time, state)} + end + @impl GenServer def handle_info(:sync_blocks, state) do blocks_to_download = SyncBlocks.run() @@ -743,6 +730,23 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end) end + defp on_tick(time, %{genesis_time: genesis_time} = state) when time < genesis_time, do: state + + defp on_tick(time, %{genesis_time: genesis_time, slot_data: slot_data} = state) do + # TODO: we probably want to remove this (ForkChoice.on_tick) from here, but we keep it + # here to have this serialized with respect to the other fork choice store modifications. + + ForkChoice.on_tick(time) + + new_slot_data = compute_slot(genesis_time, time) + + updated_state = maybe_tick_validators(slot_data != new_slot_data, new_slot_data, state) + + maybe_log_new_slot(slot_data, new_slot_data) + + updated_state + end + defp maybe_tick_validators(false = _slot_data_changed, _slot_data, state), do: state defp maybe_tick_validators(true, slot_data, %{validators: validators} = state) do @@ -773,10 +777,15 @@ defmodule LambdaEthereumConsensus.Libp2pPort do defp notify_validator({pubkey, validator}, {:new_block, slot, head_root}), do: {pubkey, Validator.handle_new_block(slot, head_root, validator)} - @spec compute_slot(Types.uint64(), Types.uint64()) :: slot_data() + defp schedule_next_tick() do + # For millisecond precision + time_to_next_tick = @tick_time - rem(:os.system_time(:millisecond), @tick_time) + Process.send_after(__MODULE__, :on_tick, time_to_next_tick) + end + defp compute_slot(genesis_time, time) do - # TODO: This was copied as it is from the Clock to convert it into just a Ticker, - # slot calculations are spread across modules, we should probably centralize them. + # TODO: This was copied as it is from the Clock, slot calculations are spread + # across modules, we should probably centralize them. elapsed_time = time - genesis_time slot_thirds = div(elapsed_time * 3, ChainSpec.get("SECONDS_PER_SLOT")) @@ -792,13 +801,13 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {slot, slot_third} end - defp log_new_slot({slot, _third}, {slot, _another_third}), do: :ok + defp maybe_log_new_slot({slot, _third}, {slot, _another_third}), do: :ok - defp log_new_slot({_prev_slot, _thrid}, {slot, :first_third}) do + defp maybe_log_new_slot({_prev_slot, _thrid}, {slot, :first_third}) do # TODO: It used :sync, :store as the slot event in the old Clock, double-check. :telemetry.execute([:sync, :store], %{slot: slot}) Logger.info("[Libp2p] Slot transition", slot: slot) end - defp log_new_slot(_, _), do: :ok + defp maybe_log_new_slot(_, _), do: :ok end From b9423e1fc276d3424fe37f69ab1a5b96c00dc8aa Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 1 Aug 2024 12:12:07 -0300 Subject: [PATCH 17/42] Small fix regarding an info message that should be debug and a unused var --- lib/libp2p_port.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 5cadc5d7d..7fdff43c2 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -485,7 +485,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end @impl GenServer - def handle_info(:on_tick, %{genesis_time: genesis_time} = state) do + def handle_info(:on_tick, state) do schedule_next_tick() time = :os.system_time(:second) @@ -758,7 +758,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do defp notify_validators(validators, msg) do start_time = System.monotonic_time(:millisecond) - Logger.info("[Libp2p] Notifying all Validators with message: #{inspect(msg)}") + Logger.debug("[Libp2p] Notifying all Validators with message: #{inspect(msg)}") updated_validators = Enum.map(validators, ¬ify_validator(&1, msg)) From 2055e785b88b62fc34bb2fc70e2646a2447bcc49 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 2 Aug 2024 15:48:53 -0300 Subject: [PATCH 18/42] Small cleanup of the Validator.Setup --- .../validator/setup.ex | 71 ++++++++++--------- lib/libp2p_port.ex | 8 +-- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/setup.ex b/lib/lambda_ethereum_consensus/validator/setup.ex index 084fb16fa..d14699a1a 100644 --- a/lib/lambda_ethereum_consensus/validator/setup.ex +++ b/lib/lambda_ethereum_consensus/validator/setup.ex @@ -18,21 +18,16 @@ defmodule LambdaEthereumConsensus.Validator.Setup do defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) when is_nil(keystore_dir) or is_nil(keystore_pass_dir) do Logger.warning( - "[Validator] No keystore_dir or keystore_pass_dir provided. Validator will not start." + "[Validator] No keystore_dir or keystore_pass_dir provided. Validators won't start." ) - %{} + [] end defp setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) do validator_keys = decode_validator_keys(keystore_dir, keystore_pass_dir) - validators = - validator_keys - |> Enum.map(fn {pubkey, privkey} -> - {pubkey, Validator.new({slot, head_root, {pubkey, privkey}})} - end) - |> Map.new() + validators = Enum.map(validator_keys, &Validator.new({slot, head_root, &1})) Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") @@ -49,34 +44,46 @@ defmodule LambdaEthereumConsensus.Validator.Setup do list({Bls.pubkey(), Bls.privkey()}) def decode_validator_keys(keystore_dir, keystore_pass_dir) when is_binary(keystore_dir) and is_binary(keystore_pass_dir) do - File.ls!(keystore_dir) - |> Enum.map(fn filename -> - if String.ends_with?(filename, ".json") do - base_name = String.trim_trailing(filename, ".json") + keystore_dir + |> File.ls!() + |> map_rejecting_nils(&paths_from_filename(keystore_dir, keystore_pass_dir, &1, Path.extname(&1))) + |> map_rejecting_nils(&decode_key/1) + end - keystore_file = Path.join(keystore_dir, "#{base_name}.json") - keystore_pass_file = Path.join(keystore_pass_dir, "#{base_name}.txt") + defp decode_key({keystore_file, keystore_pass_file}) do + # TODO: remove `try` and handle errors properly + try do + Keystore.decode_from_files!(keystore_file, keystore_pass_file) + rescue + error -> + Logger.error( + "[Validator] Failed to decode keystore file: #{keystore_file}. Pass file: #{keystore_pass_file} Error: #{inspect(error)}" + ) - {keystore_file, keystore_pass_file} - else - Logger.warning("[Validator] Skipping file: #{filename}. Not a keystore file.") nil + end + end + + defp paths_from_filename(keystore_dir, keystore_pass_dir, filename, ".json") do + basename = Path.basename(filename, ".json") + + keystore_file = Path.join(keystore_dir, "#{basename}.json") + keystore_pass_file = Path.join(keystore_pass_dir, "#{basename}.txt") + + {keystore_file, keystore_pass_file} + end + + defp paths_from_filename(_keystore_dir, _keystore_pass_dir, basename, _ext) do + Logger.warning("[Validator] Skipping file: #{basename}. Not a json keystore file.") + nil + end + + defp map_rejecting_nils(enumerable, fun) do + Enum.reduce(enumerable, [], fn elem, acc -> + case fun.(elem) do + nil -> acc + result -> [result | acc] end end) - |> Enum.reject(&is_nil/1) - |> Enum.map(fn {keystore_file, keystore_pass_file} -> - # TODO: remove `try` and handle errors properly - try do - Keystore.decode_from_files!(keystore_file, keystore_pass_file) - rescue - error -> - Logger.error( - "[Validator] Failed to decode keystore file: #{keystore_file}. Pass file: #{keystore_pass_file} Error: #{inspect(error)}" - ) - - nil - end - end) - |> Enum.reject(&is_nil/1) end end diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 7fdff43c2..42deaee94 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -771,11 +771,11 @@ defmodule LambdaEthereumConsensus.Libp2pPort do updated_validators end - defp notify_validator({pubkey, validator}, {:on_tick, slot_data}), - do: {pubkey, Validator.handle_tick(slot_data, validator)} + defp notify_validator(validator, {:on_tick, slot_data}), + do: Validator.handle_tick(slot_data, validator) - defp notify_validator({pubkey, validator}, {:new_block, slot, head_root}), - do: {pubkey, Validator.handle_new_block(slot, head_root, validator)} + defp notify_validator(validator, {:new_block, slot, head_root}), + do: Validator.handle_new_block(slot, head_root, validator) defp schedule_next_tick() do # For millisecond precision From 9a3b4c3fb8f89f120af6a610f682ec5e24a7c3a2 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 7 Aug 2024 15:08:58 -0300 Subject: [PATCH 19/42] Created the new ValidatorPool and moved everything to work with it instead of Setup --- config/runtime.exs | 2 +- .../beacon/beacon_node.ex | 4 +- .../validator/setup.ex | 114 ------------- .../validator/validator_pool.ex | 150 ++++++++++++++++++ lib/libp2p_port.ex | 6 +- test/unit/libp2p_port_test.exs | 3 +- 6 files changed, 158 insertions(+), 121 deletions(-) delete mode 100644 lib/lambda_ethereum_consensus/validator/setup.ex create mode 100644 lib/lambda_ethereum_consensus/validator/validator_pool.ex diff --git a/config/runtime.exs b/config/runtime.exs index e12304012..c73d4f6ef 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -171,7 +171,7 @@ if keystore_pass_dir != nil and not File.dir?(keystore_pass_dir) do System.halt(2) end -config :lambda_ethereum_consensus, LambdaEthereumConsensus.Validator.Setup, +config :lambda_ethereum_consensus, LambdaEthereumConsensus.ValidatorPool, keystore_dir: keystore_dir, keystore_pass_dir: keystore_pass_dir diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex index f939f597b..0f7119137 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex @@ -8,7 +8,7 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.StateTransition.Cache alias LambdaEthereumConsensus.Store.BlockStates - alias LambdaEthereumConsensus.Validator + alias LambdaEthereumConsensus.ValidatorPool alias Types.BeaconState def start_link(opts) do @@ -30,7 +30,7 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do init_execution_chain(deposit_tree_snapshot, store.head_root) - validators = Validator.Setup.init(store.head_slot, store.head_root) + validators = ValidatorPool.init(store.head_slot, store.head_root) libp2p_args = [genesis_time: store.genesis_time, validators: validators] ++ get_libp2p_args() diff --git a/lib/lambda_ethereum_consensus/validator/setup.ex b/lib/lambda_ethereum_consensus/validator/setup.ex deleted file mode 100644 index 4dc578c0c..000000000 --- a/lib/lambda_ethereum_consensus/validator/setup.ex +++ /dev/null @@ -1,114 +0,0 @@ -defmodule LambdaEthereumConsensus.Validator.Setup do - @moduledoc """ - Module that setups the initial validators state - """ - - require Logger - alias LambdaEthereumConsensus.Validator - - @spec init(Types.slot(), Types.root()) :: %{Bls.pubkey() => Validator.state()} - def init(slot, head_root) do - config = Application.get_env(:lambda_ethereum_consensus, __MODULE__, []) - keystore_dir = Keyword.get(config, :keystore_dir) - keystore_pass_dir = Keyword.get(config, :keystore_pass_dir) - - setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) - end - - defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) - when is_nil(keystore_dir) or is_nil(keystore_pass_dir) do - Logger.warning( - "[Validator] No keystore_dir or keystore_pass_dir provided. Validator will not start." - ) - - [] - end - - defp setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) do - validator_keys = decode_validator_keys(keystore_dir, keystore_pass_dir) - - validators = Enum.map(validator_keys, &Validator.new({slot, head_root, &1})) - - Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") - - validators - end - - @doc """ - Get validator keys from the keystore directory. - This function expects two files for each validator: - - /.json - - /.txt - """ - @spec decode_validator_keys(binary(), binary()) :: - list({Bls.pubkey(), Bls.privkey()}) - def decode_validator_keys(keystore_dir, keystore_pass_dir) - when is_binary(keystore_dir) and is_binary(keystore_pass_dir) do - keystore_dir - |> File.ls!() - |> map_rejecting_nils( - &paths_from_filename(keystore_dir, keystore_pass_dir, &1, Path.extname(&1)) - ) - |> map_rejecting_nils(&decode_key/1) - end - - defp decode_key({keystore_file, keystore_pass_file}) do - # TODO: remove `try` and handle errors properly - try do - Keystore.decode_from_files!(keystore_file, keystore_pass_file) - rescue - error -> - Logger.error( - "[Validator] Failed to decode keystore file: #{keystore_file}. Pass file: #{keystore_pass_file} Error: #{inspect(error)}" - ) - - nil - end - end - - defp paths_from_filename(keystore_dir, keystore_pass_dir, filename, ".json") do - basename = Path.basename(filename, ".json") - - keystore_file = Path.join(keystore_dir, "#{basename}.json") - keystore_pass_file = Path.join(keystore_pass_dir, "#{basename}.txt") - - {keystore_file, keystore_pass_file} - end - - defp paths_from_filename(_keystore_dir, _keystore_pass_dir, basename, _ext) do - Logger.warning("[Validator] Skipping file: #{basename}. Not a json keystore file.") - nil - end - - @spec notify_validators([Validator.state()], tuple()) :: [Validator.state()] - def notify_validators(validators, msg) do - start_time = System.monotonic_time(:millisecond) - - Logger.debug("[Validator] Notifying all Validators with message: #{inspect(msg)}") - - updated_validators = Enum.map(validators, ¬ify_validator(&1, msg)) - - end_time = System.monotonic_time(:millisecond) - - Logger.debug( - "[Validator] #{inspect(msg)} notified to all Validators after #{end_time - start_time} ms" - ) - - updated_validators - end - - defp notify_validator(validator, {:on_tick, slot_data}), - do: Validator.handle_tick(slot_data, validator) - - defp notify_validator(validator, {:new_head, slot, head_root}), - do: Validator.handle_new_head(slot, head_root, validator) - - defp map_rejecting_nils(enumerable, fun) do - Enum.reduce(enumerable, [], fn elem, acc -> - case fun.(elem) do - nil -> acc - result -> [result | acc] - end - end) - end -end diff --git a/lib/lambda_ethereum_consensus/validator/validator_pool.ex b/lib/lambda_ethereum_consensus/validator/validator_pool.ex new file mode 100644 index 000000000..e68ee8c6e --- /dev/null +++ b/lib/lambda_ethereum_consensus/validator/validator_pool.ex @@ -0,0 +1,150 @@ +defmodule LambdaEthereumConsensus.ValidatorPool do + @moduledoc """ + Module that holds the pool of validators and their states, + it also manages the validator's duties as bitmaps to + simplify the delegation of work. + """ + + defstruct epoch: nil, slot: nil, head_root: nil, validators: %{uninitialized: []} + + require Logger + + alias LambdaEthereumConsensus.StateTransition.Misc + alias LambdaEthereumConsensus.Validator + + @type validators :: %{atom() => list(Validator.state())} + @type t :: %__MODULE__{ + epoch: Types.epoch(), + slot: Types.slot(), + head_root: Types.root(), + validators: validators() + } + + @doc """ + Initiate the pool of validators, given the slot and head root. + """ + @spec init(Types.slot(), Types.root()) :: t() + def init(slot, head_root) do + config = Application.get_env(:lambda_ethereum_consensus, __MODULE__, []) + keystore_dir = Keyword.get(config, :keystore_dir) + keystore_pass_dir = Keyword.get(config, :keystore_pass_dir) + + setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) + end + + defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) + when is_nil(keystore_dir) or is_nil(keystore_pass_dir) do + Logger.warning( + "[Validator] No keystore_dir or keystore_pass_dir provided. Validators won't start." + ) + + %__MODULE__{} + end + + defp setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) do + validator_keys = decode_validator_keys(keystore_dir, keystore_pass_dir) + + validators = Enum.map(validator_keys, &Validator.new({slot, head_root, &1})) + + Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") + + %__MODULE__{ + epoch: Misc.compute_epoch_at_slot(slot), + slot: slot, + head_root: head_root, + validators: %{uninitialized: validators} + } + end + + @doc """ + Notify all validators of a new head. + """ + @spec notify_head(t(), Types.slot(), Types.root()) :: t() + def notify_head(%{validators: %{uninitialized: validators}} = pool, slot, head_root) do + uninitialized_validators = + maybe_debug_notify( + fn -> + Enum.map(validators, &Validator.handle_new_head(slot, head_root, &1)) + end, + {:new_head, slot, head_root} + ) + + %{pool | validators: %{uninitialized: uninitialized_validators}} + end + + @doc """ + Notify all validators of a new block. + """ + @spec notify_tick(t(), tuple()) :: t() + def notify_tick(%{validators: %{uninitialized: validators}} = pool, slot_data) do + uninitialized_validators = + maybe_debug_notify( + fn -> + Enum.map(validators, &Validator.handle_tick(slot_data, &1)) + end, + {:on_tick, slot_data} + ) + + %{pool | validators: %{uninitialized: uninitialized_validators}} + end + + defp maybe_debug_notify(fun, data) do + if Application.get_env(:logger, :level) == :debug do + Logger.debug("[Validator] Notifying all Validators with message: #{inspect(data)}") + + start_time = System.monotonic_time(:millisecond) + result = fun.() + end_time = System.monotonic_time(:millisecond) + + Logger.debug( + "[Validator] #{inspect(data)} notified to all Validators after #{end_time - start_time} ms" + ) + + result + else + fun.() + end + end + + @doc """ + Get validator keys from the keystore directory. + This function expects two files for each validator: + - /.json + - /.txt + """ + @spec decode_validator_keys(binary(), binary()) :: + list({Bls.pubkey(), Bls.privkey()}) + def decode_validator_keys(keystore_dir, keystore_pass_dir) + when is_binary(keystore_dir) and is_binary(keystore_pass_dir) do + keystore_dir + |> File.ls!() + |> Enum.flat_map(&paths_from_filename(keystore_dir, keystore_pass_dir, &1, Path.extname(&1))) + |> Enum.flat_map(&decode_key/1) + end + + defp paths_from_filename(keystore_dir, keystore_pass_dir, filename, ".json") do + basename = Path.basename(filename, ".json") + + keystore_file = Path.join(keystore_dir, "#{basename}.json") + keystore_pass_file = Path.join(keystore_pass_dir, "#{basename}.txt") + + [{keystore_file, keystore_pass_file}] + end + + defp paths_from_filename(_keystore_dir, _keystore_pass_dir, basename, _ext) do + Logger.warning("[Validator] Skipping file: #{basename}. Not a json keystore file.") + [] + end + + defp decode_key({keystore_file, keystore_pass_file}) do + # TODO: remove `try` and handle errors properly + [Keystore.decode_from_files!(keystore_file, keystore_pass_file)] + rescue + error -> + Logger.error( + "[Validator] Failed to decode keystore file: #{keystore_file}. Pass file: #{keystore_pass_file} Error: #{inspect(error)}" + ) + + [] + end +end diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 47f86eaa8..e5fefbb72 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -23,7 +23,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do alias LambdaEthereumConsensus.P2p.Requests alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.Utils.BitVector - alias LambdaEthereumConsensus.Validator + alias LambdaEthereumConsensus.ValidatorPool alias Libp2pProto.AddPeer alias Libp2pProto.Command alias Libp2pProto.Enr @@ -507,7 +507,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def handle_info({:new_head, slot, head_root}, %{validators: validators} = state) do updated_validators = - Validator.Setup.notify_validators(validators, {:new_head, slot, head_root}) + ValidatorPool.notify_head(validators, slot, head_root) {:noreply, %{state | validators: updated_validators}} end @@ -748,7 +748,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do state else updated_validators = - Validator.Setup.notify_validators(state.validators, {:on_tick, new_slot_data}) + ValidatorPool.notify_tick(state.validators, new_slot_data) %{state | slot_data: new_slot_data, validators: updated_validators} end diff --git a/test/unit/libp2p_port_test.exs b/test/unit/libp2p_port_test.exs index d50a1a68a..4c3bc9da7 100644 --- a/test/unit/libp2p_port_test.exs +++ b/test/unit/libp2p_port_test.exs @@ -17,7 +17,8 @@ defmodule Unit.Libp2pPortTest do end defp start_port(name \\ Libp2pPort, init_args \\ []) do - start_link_supervised!({Libp2pPort, [opts: [name: name], genesis_time: :os.system_time(:second)] ++ init_args}, + start_link_supervised!( + {Libp2pPort, [opts: [name: name], genesis_time: :os.system_time(:second)] ++ init_args}, id: name ) end From 6046c52fcef6ae02f182191581e7dddaed8abd61 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 7 Aug 2024 20:06:54 -0300 Subject: [PATCH 20/42] ValidatorSet rename + started moving duties to all validators --- config/runtime.exs | 2 +- lib/lambda_ethereum_consensus/beacon/beacon_node.ex | 4 ++-- lib/lambda_ethereum_consensus/validator/duties.ex | 12 ++++++++++++ .../validator/validator_pool.ex | 6 +++--- lib/libp2p_port.ex | 6 +++--- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index c73d4f6ef..ea37483f9 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -171,7 +171,7 @@ if keystore_pass_dir != nil and not File.dir?(keystore_pass_dir) do System.halt(2) end -config :lambda_ethereum_consensus, LambdaEthereumConsensus.ValidatorPool, +config :lambda_ethereum_consensus, LambdaEthereumConsensus.ValidatorSet, keystore_dir: keystore_dir, keystore_pass_dir: keystore_pass_dir diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex index 0f7119137..9316be2bb 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex @@ -8,7 +8,7 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.StateTransition.Cache alias LambdaEthereumConsensus.Store.BlockStates - alias LambdaEthereumConsensus.ValidatorPool + alias LambdaEthereumConsensus.ValidatorSet alias Types.BeaconState def start_link(opts) do @@ -30,7 +30,7 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do init_execution_chain(deposit_tree_snapshot, store.head_root) - validators = ValidatorPool.init(store.head_slot, store.head_root) + validators = ValidatorSet.init(store.head_slot, store.head_root) libp2p_args = [genesis_time: store.genesis_time, validators: validators] ++ get_libp2p_args() diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index ff9b70ff9..70f0be16a 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -40,6 +40,18 @@ defmodule LambdaEthereumConsensus.Validator.Duties do } end + @spec compute_proposers_for_epoch(beacon_state :: BeaconState.t(), epoch :: Types.epoch()) :: + %{Types.slot() => non_neg_integer()} + def compute_proposers_for_epoch(beacon_state, epoch) do + start_slot = Misc.compute_start_slot_at_epoch(epoch) + + start_slot..(start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - 1) + |> Enum.map(fn slot -> + {slot, Accessors.get_beacon_proposer_index(beacon_state, slot)} + end) + |> Map.new() + end + @spec get_current_attester_duty(duties :: duties(), current_slot :: Types.slot()) :: attester_duty() def get_current_attester_duty(%{attester: attester_duties}, current_slot) do diff --git a/lib/lambda_ethereum_consensus/validator/validator_pool.ex b/lib/lambda_ethereum_consensus/validator/validator_pool.ex index e68ee8c6e..3d2b5ad46 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_pool.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_pool.ex @@ -1,6 +1,6 @@ -defmodule LambdaEthereumConsensus.ValidatorPool do +defmodule LambdaEthereumConsensus.ValidatorSet do @moduledoc """ - Module that holds the pool of validators and their states, + Module that holds the set of validators and their states, it also manages the validator's duties as bitmaps to simplify the delegation of work. """ @@ -73,7 +73,7 @@ defmodule LambdaEthereumConsensus.ValidatorPool do end @doc """ - Notify all validators of a new block. + Notify all validators of a new tick. """ @spec notify_tick(t(), tuple()) :: t() def notify_tick(%{validators: %{uninitialized: validators}} = pool, slot_data) do diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index e5fefbb72..b80b0c46a 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -23,7 +23,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do alias LambdaEthereumConsensus.P2p.Requests alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.Utils.BitVector - alias LambdaEthereumConsensus.ValidatorPool + alias LambdaEthereumConsensus.ValidatorSet alias Libp2pProto.AddPeer alias Libp2pProto.Command alias Libp2pProto.Enr @@ -507,7 +507,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def handle_info({:new_head, slot, head_root}, %{validators: validators} = state) do updated_validators = - ValidatorPool.notify_head(validators, slot, head_root) + ValidatorSet.notify_head(validators, slot, head_root) {:noreply, %{state | validators: updated_validators}} end @@ -748,7 +748,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do state else updated_validators = - ValidatorPool.notify_tick(state.validators, new_slot_data) + ValidatorSet.notify_tick(state.validators, new_slot_data) %{state | slot_data: new_slot_data, validators: updated_validators} end From cb0b5807480e25b668c6ff52e5f140b149be3eeb Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 8 Aug 2024 14:09:25 -0300 Subject: [PATCH 21/42] Calculate proposers for the whole epoch --- .../validator/duties.ex | 12 ++++-- .../validator/validator.ex | 15 +++---- .../validator/validator_pool.ex | 39 ++++++++++++++----- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 70f0be16a..98281afa9 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -40,14 +40,18 @@ defmodule LambdaEthereumConsensus.Validator.Duties do } end - @spec compute_proposers_for_epoch(beacon_state :: BeaconState.t(), epoch :: Types.epoch()) :: + @spec compute_proposers_for_epoch(beacon_state :: BeaconState.t(), epoch :: Types.epoch(), %{}) :: %{Types.slot() => non_neg_integer()} - def compute_proposers_for_epoch(beacon_state, epoch) do + def compute_proposers_for_epoch(beacon_state, epoch, validators) do start_slot = Misc.compute_start_slot_at_epoch(epoch) start_slot..(start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - 1) - |> Enum.map(fn slot -> - {slot, Accessors.get_beacon_proposer_index(beacon_state, slot)} + |> Enum.flat_map(fn slot -> + validator_index = Accessors.get_beacon_proposer_index(beacon_state, slot) + + if Map.has_key?(validators, validator_index), + do: [{slot, Accessors.get_beacon_proposer_index(beacon_state, slot)}], + else: [] end) |> Map.new() end diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index fa43825a0..3d8505cf9 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -48,11 +48,11 @@ defmodule LambdaEthereumConsensus.Validator do payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil } - @spec new({Types.slot(), Types.root(), {Bls.pubkey(), Bls.privkey()}}) :: state - def new({head_slot, head_root, {pubkey, privkey}}) do + @spec new({Bls.pubkey(), Bls.privkey()}, Types.epoch(), Types.slot(), Types.root(), Types.BeaconState.t()) :: state + def new({pubkey, privkey}, epoch, head_slot, head_root, beacon) do state = %__MODULE__{ slot: head_slot, - epoch: Misc.compute_epoch_at_slot(head_slot), + epoch: epoch, root: head_root, duties: Duties.empty_duties(), validator: %{ @@ -63,7 +63,7 @@ defmodule LambdaEthereumConsensus.Validator do payload_builder: nil } - case try_setup_validator(state, head_slot, head_root) do + case try_setup_validator(state, epoch, head_slot, head_root, beacon) do nil -> # TODO: Previously this was handled by the validator continously trying to setup itself, # but now that they are processed syncronously, we should handle this case different. @@ -76,11 +76,8 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec try_setup_validator(state, Types.slot(), Types.root()) :: state | nil - defp try_setup_validator(state, slot, root) do - epoch = Misc.compute_epoch_at_slot(slot) - beacon = fetch_target_state(epoch, root) - + @spec try_setup_validator(state, Types.epoch(), Types.slot(), Types.root(), Types.BeaconState.t()) :: state | nil + defp try_setup_validator(state, epoch, slot, root, beacon) do case fetch_validator_index(beacon, state.validator) do nil -> nil diff --git a/lib/lambda_ethereum_consensus/validator/validator_pool.ex b/lib/lambda_ethereum_consensus/validator/validator_pool.ex index 3d2b5ad46..9f8373ddf 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_pool.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_pool.ex @@ -10,13 +10,15 @@ defmodule LambdaEthereumConsensus.ValidatorSet do require Logger alias LambdaEthereumConsensus.StateTransition.Misc + alias LambdaEthereumConsensus.Store.CheckpointStates alias LambdaEthereumConsensus.Validator + alias LambdaEthereumConsensus.Validator.Duties - @type validators :: %{atom() => list(Validator.state())} + @type validators :: %{atom() => %{} | []} @type t :: %__MODULE__{ - epoch: Types.epoch(), - slot: Types.slot(), - head_root: Types.root(), + epoch: Types.epoch() | nil, + slot: Types.slot() | nil, + head_root: Types.root() | nil, validators: validators() } @@ -44,18 +46,33 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) do validator_keys = decode_validator_keys(keystore_dir, keystore_pass_dir) - validators = Enum.map(validator_keys, &Validator.new({slot, head_root, &1})) + epoch = Misc.compute_epoch_at_slot(slot) + beacon = fetch_target_state!(epoch, head_root) + + validators = Map.new(validator_keys, fn validator_key -> + validator = Validator.new(validator_key, epoch, slot, head_root, beacon) + {validator.validator.index, validator} + end) Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") + proposers = Duties.compute_proposers_for_epoch(beacon, epoch, validators) + %__MODULE__{ - epoch: Misc.compute_epoch_at_slot(slot), + epoch: epoch, slot: slot, head_root: head_root, - validators: %{uninitialized: validators} + validators: %{ + proposers: proposers, + uninitialized: validators} } end + defp fetch_target_state!(epoch, head_root) do + {:ok, state} = CheckpointStates.compute_target_checkpoint_state(epoch, head_root) + state + end + @doc """ Notify all validators of a new head. """ @@ -64,7 +81,9 @@ defmodule LambdaEthereumConsensus.ValidatorSet do uninitialized_validators = maybe_debug_notify( fn -> - Enum.map(validators, &Validator.handle_new_head(slot, head_root, &1)) + Map.new(validators, fn {k, v} -> + {k, Validator.handle_new_head(slot, head_root, v)} + end) end, {:new_head, slot, head_root} ) @@ -80,7 +99,9 @@ defmodule LambdaEthereumConsensus.ValidatorSet do uninitialized_validators = maybe_debug_notify( fn -> - Enum.map(validators, &Validator.handle_tick(slot_data, &1)) + Map.new(validators, fn {k, v} -> + {k, Validator.handle_tick(slot_data, v)} + end) end, {:on_tick, slot_data} ) From 69150f2706dd9ebe45d8408d0761efb198c70846 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 8 Aug 2024 20:57:01 -0300 Subject: [PATCH 22/42] Initial attesters_for_epoch implementation working --- .../validator/duties.ex | 97 ++++++++++++++++--- .../validator/validator.ex | 16 ++- .../{validator_pool.ex => validator_set.ex} | 19 ++-- 3 files changed, 113 insertions(+), 19 deletions(-) rename lib/lambda_ethereum_consensus/validator/{validator_pool.ex => validator_set.ex} (90%) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 98281afa9..2e6d012fb 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -17,6 +17,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do signing_domain: Types.domain(), subnet_id: Types.uint64(), slot: Types.slot(), + validator_index: Types.validator_index(), committee_index: Types.uint64(), committee_length: Types.uint64(), index_in_committee: Types.uint64() @@ -40,20 +41,92 @@ defmodule LambdaEthereumConsensus.Validator.Duties do } end - @spec compute_proposers_for_epoch(beacon_state :: BeaconState.t(), epoch :: Types.epoch(), %{}) :: + @spec compute_proposers_for_epoch(BeaconState.t(), Types.epoch(), %{}) :: %{Types.slot() => non_neg_integer()} - def compute_proposers_for_epoch(beacon_state, epoch, validators) do - start_slot = Misc.compute_start_slot_at_epoch(epoch) + def compute_proposers_for_epoch(%BeaconState{} = state, epoch, validators) do + with {:ok, epoch} <- check_valid_epoch(state, epoch), + {start_slot, end_slot} <- boundary_slots(epoch) do + start_slot..end_slot + |> Enum.flat_map(fn slot -> + {:ok, proposer_index} = Accessors.get_beacon_proposer_index(state, slot) - start_slot..(start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - 1) - |> Enum.flat_map(fn slot -> - validator_index = Accessors.get_beacon_proposer_index(beacon_state, slot) + if Map.has_key?(validators, proposer_index), + do: [{slot, proposer_index}], + else: [] + end) + |> Map.new() + end + end - if Map.has_key?(validators, validator_index), - do: [{slot, Accessors.get_beacon_proposer_index(beacon_state, slot)}], - else: [] - end) - |> Map.new() + @spec compute_attesters_for_epoch(BeaconState.t(), Types.epoch(), %{}) :: any() + def compute_attesters_for_epoch(%BeaconState{} = state, epoch, validators) do + with {:ok, epoch} <- check_valid_epoch(state, epoch), + {start_slot, end_slot} <- boundary_slots(epoch) do + + committee_count_per_slot = Accessors.get_committee_count_per_slot(state, epoch) + + start_slot..end_slot + |> Enum.flat_map(fn slot -> + 0..(committee_count_per_slot - 1) + |> Enum.flat_map(&compute_attester_dutys(state, epoch, slot, validators, &1)) + end) + |> then(&{:ok, Map.new(&1)}) + end + end + + @spec compute_attester_dutys( + state :: BeaconState.t(), + epoch :: Types.epoch(), + slot :: Types.slot(), + committee_index :: Types.uint64(), + validator_index :: [{Types.validator_index(), Validator.validator()}] + ) :: attester_duty() | nil + defp compute_attester_dutys(state, epoch, slot, validators, committee_index) do + with {:ok, committee} <- Accessors.get_beacon_committee(state, slot, committee_index) do + committee + |> Stream.with_index() + |> Stream.flat_map(fn {validator_index, index_in_committee} -> + case Map.get(validators, validator_index) do + nil -> [] + validator -> + [%{ + slot: slot, + validator_index: validator_index, + index_in_committee: index_in_committee, + committee_length: length(committee), + committee_index: committee_index, + attested?: false + } + |> update_with_aggregation_duty(state, validator.validator.privkey) + |> update_with_subnet_id(state, epoch)] + end + end) + |> Enum.into([]) + |> case do + [] -> [] + duties -> [{slot, duties}] + end + else + {:error, _} -> + nil + end + end + + defp check_valid_epoch(state, epoch) do + next_epoch = Accessors.get_current_epoch(state) + 1 + + if epoch > next_epoch do + {:error, "epoch must be <= next_epoch"} + else + {:ok, epoch} + end + end + + defp boundary_slots(epoch) do + start_slot = Misc.compute_start_slot_at_epoch(epoch) + end_slot = start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - 1 + + {start_slot, end_slot} end @spec get_current_attester_duty(duties :: duties(), current_slot :: Types.slot()) :: @@ -106,6 +179,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do epoch :: Types.epoch(), validator_index :: Types.validator_index() ) :: proposer_duties() + # TODO: Remove, already moved to an epoch-based approach def compute_proposer_duties(beacon_state, epoch, validator_index) do start_slot = Misc.compute_start_slot_at_epoch(epoch) @@ -197,6 +271,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end @doc """ + TODO: This is not the case anymore? Return the committee assignment in the ``epoch`` for ``validator_index``. ``assignment`` returned is a tuple of the following form: * ``assignment[0]`` is the index of the validator in the committee diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 3d8505cf9..399a9867c 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -48,7 +48,13 @@ defmodule LambdaEthereumConsensus.Validator do payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil } - @spec new({Bls.pubkey(), Bls.privkey()}, Types.epoch(), Types.slot(), Types.root(), Types.BeaconState.t()) :: state + @spec new( + {Bls.pubkey(), Bls.privkey()}, + Types.epoch(), + Types.slot(), + Types.root(), + Types.BeaconState.t() + ) :: state def new({pubkey, privkey}, epoch, head_slot, head_root, beacon) do state = %__MODULE__{ slot: head_slot, @@ -76,7 +82,13 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec try_setup_validator(state, Types.epoch(), Types.slot(), Types.root(), Types.BeaconState.t()) :: state | nil + @spec try_setup_validator( + state, + Types.epoch(), + Types.slot(), + Types.root(), + Types.BeaconState.t() + ) :: state | nil defp try_setup_validator(state, epoch, slot, root, beacon) do case fetch_validator_index(beacon, state.validator) do nil -> diff --git a/lib/lambda_ethereum_consensus/validator/validator_pool.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex similarity index 90% rename from lib/lambda_ethereum_consensus/validator/validator_pool.ex rename to lib/lambda_ethereum_consensus/validator/validator_set.ex index 9f8373ddf..3892cf1f2 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_pool.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -49,14 +49,19 @@ defmodule LambdaEthereumConsensus.ValidatorSet do epoch = Misc.compute_epoch_at_slot(slot) beacon = fetch_target_state!(epoch, head_root) - validators = Map.new(validator_keys, fn validator_key -> - validator = Validator.new(validator_key, epoch, slot, head_root, beacon) - {validator.validator.index, validator} - end) + validators = + Map.new(validator_keys, fn validator_key -> + validator = Validator.new(validator_key, epoch, slot, head_root, beacon) + {validator.validator.index, validator} + end) Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") proposers = Duties.compute_proposers_for_epoch(beacon, epoch, validators) + attesters = Duties.compute_attesters_for_epoch(beacon, epoch, validators) + + Logger.info("[Validator] Proposers: #{inspect(proposers, pretty: true)}") + Logger.info("[Validator] Attesters: #{inspect(attesters, pretty: true)}") %__MODULE__{ epoch: epoch, @@ -64,7 +69,9 @@ defmodule LambdaEthereumConsensus.ValidatorSet do head_root: head_root, validators: %{ proposers: proposers, - uninitialized: validators} + attesters: attesters, + uninitialized: validators + } } end @@ -99,7 +106,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do uninitialized_validators = maybe_debug_notify( fn -> - Map.new(validators, fn {k, v} -> + Map.new(validators, fn {k, v} -> {k, Validator.handle_tick(slot_data, v)} end) end, From a6042130ad76cc02aabf2a1325d9e3db38915b90 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 9 Aug 2024 19:38:39 -0300 Subject: [PATCH 23/42] Notify Head now working from the ValidatorSet instead of Validators --- .../validator/duties.ex | 10 +-- .../validator/validator.ex | 7 +- .../validator/validator_set.ex | 81 ++++++++++++++----- 3 files changed, 67 insertions(+), 31 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 2e6d012fb..074facc59 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -41,11 +41,11 @@ defmodule LambdaEthereumConsensus.Validator.Duties do } end - @spec compute_proposers_for_epoch(BeaconState.t(), Types.epoch(), %{}) :: - %{Types.slot() => non_neg_integer()} + @spec compute_proposers_for_epoch(BeaconState.t(), Types.epoch(), %{}) :: any() def compute_proposers_for_epoch(%BeaconState{} = state, epoch, validators) do with {:ok, epoch} <- check_valid_epoch(state, epoch), {start_slot, end_slot} <- boundary_slots(epoch) do + start_slot..end_slot |> Enum.flat_map(fn slot -> {:ok, proposer_index} = Accessors.get_beacon_proposer_index(state, slot) @@ -54,7 +54,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do do: [{slot, proposer_index}], else: [] end) - |> Map.new() + |> then(&{:ok, Map.new(&1)}) end end @@ -80,7 +80,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do slot :: Types.slot(), committee_index :: Types.uint64(), validator_index :: [{Types.validator_index(), Validator.validator()}] - ) :: attester_duty() | nil + ) :: [attester_duty()] defp compute_attester_dutys(state, epoch, slot, validators, committee_index) do with {:ok, committee} <- Accessors.get_beacon_committee(state, slot, committee_index) do committee @@ -108,7 +108,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end else {:error, _} -> - nil + [] end end diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 399a9867c..20900ba00 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -247,7 +247,7 @@ defmodule LambdaEthereumConsensus.Validator do end @spec attest(state, Duties.attester_duty()) :: :ok - defp attest(%{validator: validator} = state, current_duty) do + def attest(%{validator: validator} = state, current_duty) do subnet_id = current_duty.subnet_id log_debug(validator.index, "attesting", slot: current_duty.slot, subnet_id: subnet_id) @@ -405,10 +405,9 @@ defmodule LambdaEthereumConsensus.Validator do end @spec start_payload_builder(state, Types.slot(), Types.root()) :: state + def start_payload_builder(%{payload_builder: {slot, root, _}} = state, slot, root), do: state - defp start_payload_builder(%{payload_builder: {slot, root, _}} = state, slot, root), do: state - - defp start_payload_builder(%{validator: validator} = state, proposed_slot, head_root) do + def start_payload_builder(%{validator: validator} = state, proposed_slot, head_root) do # TODO: handle reorgs and late blocks log_debug(validator.index, "starting building payload for slot #{proposed_slot}") diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 3892cf1f2..56c49540b 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -5,7 +5,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do simplify the delegation of work. """ - defstruct epoch: nil, slot: nil, head_root: nil, validators: %{uninitialized: []} + defstruct epoch: nil, slot: nil, head_root: nil, duties: %{}, validators: [] require Logger @@ -19,11 +19,12 @@ defmodule LambdaEthereumConsensus.ValidatorSet do epoch: Types.epoch() | nil, slot: Types.slot() | nil, head_root: Types.root() | nil, + duties: %{Types.epoch() => %{proposers: Duties.proposers(), attesters: Duties.attesters()}}, validators: validators() } @doc """ - Initiate the pool of validators, given the slot and head root. + Initiate the set of validators, given the slot and head root. """ @spec init(Types.slot(), Types.root()) :: t() def init(slot, head_root) do @@ -57,8 +58,8 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") - proposers = Duties.compute_proposers_for_epoch(beacon, epoch, validators) - attesters = Duties.compute_attesters_for_epoch(beacon, epoch, validators) + {:ok, proposers} = Duties.compute_proposers_for_epoch(beacon, epoch, validators) + {:ok, attesters} = Duties.compute_attesters_for_epoch(beacon, epoch, validators) Logger.info("[Validator] Proposers: #{inspect(proposers, pretty: true)}") Logger.info("[Validator] Attesters: #{inspect(attesters, pretty: true)}") @@ -67,11 +68,11 @@ defmodule LambdaEthereumConsensus.ValidatorSet do epoch: epoch, slot: slot, head_root: head_root, - validators: %{ + duties: %{epoch => %{ proposers: proposers, - attesters: attesters, - uninitialized: validators - } + attesters: attesters + }}, + validators: validators } end @@ -84,26 +85,62 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Notify all validators of a new head. """ @spec notify_head(t(), Types.slot(), Types.root()) :: t() - def notify_head(%{validators: %{uninitialized: validators}} = pool, slot, head_root) do - uninitialized_validators = - maybe_debug_notify( - fn -> - Map.new(validators, fn {k, v} -> - {k, Validator.handle_new_head(slot, head_root, v)} - end) - end, - {:new_head, slot, head_root} - ) + def notify_head(%{validators: validators, epoch: epoch} = set, slot, head_root) do + set + |> attest(epoch, slot) + |> build_next_payload(epoch, slot, head_root) + |> update_state(slot, head_root) + end - %{pool | validators: %{uninitialized: uninitialized_validators}} + defp update_state(set, slot, head_root) do + %{set | slot: slot, head_root: head_root} end + defp attest(set, epoch, slot) do + updated_duties = + set + |> current_attesters(epoch, slot) + |> Enum.map(fn {validator, duty} -> + Validator.attest(validator, duty) + + # Duty.attested(duty) + %{duty | attested?: true} + end) + + %{set | duties: put_in(set.duties, [set.epoch, :attesters, slot], updated_duties)} + end + + defp build_next_payload(set, epoch, slot, head_root) do + set + |> proposer(epoch, slot + 1) + |> case do + nil -> set + validator_index -> + validator = Map.get(set.validators, validator_index) + updated_validator = Validator.start_payload_builder(validator, slot + 1, head_root) + + %{set | validators: Map.put(set.validators, updated_validator.validator.index, updated_validator)} + end + end + + defp current_attesters(set, epoch, slot) do + attesters(set, epoch, slot) + |> Enum.flat_map(fn + %{attested?: false} = duty -> [{Map.get(set.validators, duty.validator_index), duty}] + _ -> [] + end) + end + + defp proposer(set, epoch, slot), do: get_in(set.duties, [epoch, :proposers, slot]) + defp attesters(set, epoch, slot), do: get_in(set.duties, [epoch, :attesters, slot]) || [] + + @doc """ Notify all validators of a new tick. """ @spec notify_tick(t(), tuple()) :: t() - def notify_tick(%{validators: %{uninitialized: validators}} = pool, slot_data) do - uninitialized_validators = + def notify_tick(%{validators: validators} = set, slot_data) do + validators = maybe_debug_notify( fn -> Map.new(validators, fn {k, v} -> @@ -113,7 +150,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do {:on_tick, slot_data} ) - %{pool | validators: %{uninitialized: uninitialized_validators}} + %{set | validators: validators} end defp maybe_debug_notify(fun, data) do From cfc5efe150b4c04db244dfe4397efe6f204537b2 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Mon, 12 Aug 2024 23:56:23 -0300 Subject: [PATCH 24/42] Hybrid version working with new_head handled at the ValidatorSet --- .../validator/validator.ex | 34 ++++++------ .../validator/validator_set.ex | 54 +++++++++++++------ 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 20900ba00..53c7a8418 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -121,35 +121,35 @@ defmodule LambdaEthereumConsensus.Validator do state |> update_state(slot, head_root) |> maybe_attest(slot) - |> maybe_build_payload(slot + 1) + |> maybe_build_payload(slot + 1, head_root) end - @spec handle_tick({Types.slot(), atom()}, state) :: state - def handle_tick(_logical_time, %{validator: %{index: nil}} = state) do + @spec handle_tick({Types.slot(), atom()}, state, Types.root()) :: state + def handle_tick(_logical_time, %{validator: %{index: nil}} = state, _root) do log_error("-1", "setup validator", "index not present for handle tick") state end - def handle_tick({slot, :first_third}, state) do + def handle_tick({slot, :first_third}, state, root) do log_debug(state.validator.index, "started first third", slot: slot) # Here we may: # 1. propose our blocks # 2. (TODO) start collecting attestations for aggregation - maybe_propose(state, slot) - |> update_state(slot, state.root) + maybe_propose(state, slot, root) + |> update_state(slot, root) end - def handle_tick({slot, :second_third}, state) do + def handle_tick({slot, :second_third}, state, root) do log_debug(state.validator.index, "started second third", slot: slot) # Here we may: # 1. send our attestation for an empty slot # 2. start building a payload state |> maybe_attest(slot) - |> maybe_build_payload(slot + 1) + |> maybe_build_payload(slot + 1, root) end - def handle_tick({slot, :last_third}, state) do + def handle_tick({slot, :last_third}, state, _root) do log_debug(state.validator.index, "started last third", slot: slot) # Here we may publish our attestation aggregate maybe_publish_aggregate(state, slot) @@ -395,8 +395,8 @@ defmodule LambdaEthereumConsensus.Validator do defp proposer?(%{duties: %{proposer: slots}}, slot), do: Enum.member?(slots, slot) - @spec maybe_build_payload(state, Types.slot()) :: state - defp maybe_build_payload(%{root: head_root} = state, proposed_slot) do + @spec maybe_build_payload(state, Types.slot(), Types.root()) :: state + defp maybe_build_payload(state, proposed_slot, head_root) do if proposer?(state, proposed_slot) do start_payload_builder(state, proposed_slot, head_root) else @@ -409,7 +409,7 @@ defmodule LambdaEthereumConsensus.Validator do def start_payload_builder(%{validator: validator} = state, proposed_slot, head_root) do # TODO: handle reorgs and late blocks - log_debug(validator.index, "starting building payload for slot #{proposed_slot}") + log_debug(validator.index, "starting building payload for slot #{proposed_slot}", root: head_root) case BlockBuilder.start_building_payload(proposed_slot, head_root) do {:ok, payload_id} -> @@ -424,9 +424,9 @@ defmodule LambdaEthereumConsensus.Validator do end end - defp maybe_propose(state, slot) do + defp maybe_propose(state, slot, root) do if proposer?(state, slot) do - propose(state, slot) + propose(state, slot, root) else state end @@ -434,11 +434,11 @@ defmodule LambdaEthereumConsensus.Validator do defp propose( %{ - root: head_root, validator: validator, payload_builder: {proposed_slot, head_root, payload_id} } = state, - proposed_slot + proposed_slot, + head_root ) do log_debug(validator.index, "building block", slot: proposed_slot) @@ -467,7 +467,7 @@ defmodule LambdaEthereumConsensus.Validator do end # TODO: at least in kurtosis there are blocks that are proposed without a payload apparently, must investigate. - defp propose(%{payload_builder: nil} = state, _proposed_slot) do + defp propose(%{payload_builder: nil} = state, _proposed_slot, _head_root) do log_error(state.validator.index, "propose block", "lack of execution payload") state end diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 56c49540b..05c4f693b 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -58,24 +58,27 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") - {:ok, proposers} = Duties.compute_proposers_for_epoch(beacon, epoch, validators) - {:ok, attesters} = Duties.compute_attesters_for_epoch(beacon, epoch, validators) - - Logger.info("[Validator] Proposers: #{inspect(proposers, pretty: true)}") - Logger.info("[Validator] Attesters: #{inspect(attesters, pretty: true)}") + duties = compute_duties_for_epoch!(beacon, epoch, validators) %__MODULE__{ epoch: epoch, slot: slot, head_root: head_root, - duties: %{epoch => %{ - proposers: proposers, - attesters: attesters - }}, + duties: %{epoch => duties}, validators: validators } end + defp compute_duties_for_epoch!(beacon, epoch, validators) do + {:ok, proposers} = Duties.compute_proposers_for_epoch(beacon, epoch, validators) + {:ok, attesters} = Duties.compute_attesters_for_epoch(beacon, epoch, validators) + + Logger.info("[Validator] Proposer duties for epoch #{epoch} are: #{inspect(proposers, pretty: true)}") + Logger.info("[Validator] Attester duties for epoch #{epoch} are: #{inspect(attesters, pretty: true)}") + + %{proposers: proposers, attesters: attesters} + end + defp fetch_target_state!(epoch, head_root) do {:ok, state} = CheckpointStates.compute_target_checkpoint_state(epoch, head_root) state @@ -86,14 +89,31 @@ defmodule LambdaEthereumConsensus.ValidatorSet do """ @spec notify_head(t(), Types.slot(), Types.root()) :: t() def notify_head(%{validators: validators, epoch: epoch} = set, slot, head_root) do + # TODO: Just for testing purposes, remove it later + Logger.info("[Validator] Notifying all Validators with new_head", root: head_root, slot: slot) + set + |> update_state(slot, head_root) |> attest(epoch, slot) |> build_next_payload(epoch, slot, head_root) - |> update_state(slot, head_root) end defp update_state(set, slot, head_root) do - %{set | slot: slot, head_root: head_root} + if new_epoch?(set, slot + 1) do + epoch = Misc.compute_epoch_at_slot(slot + 1) + beacon = fetch_target_state!(epoch, head_root) + + duties = compute_duties_for_epoch!(beacon, epoch, set.validators) + + %{set | epoch: epoch, slot: slot, head_root: head_root, duties: Map.put(set.duties, epoch, duties)} + else + %{set | slot: slot, head_root: head_root} + end + end + + defp new_epoch?(set, slot) do + epoch = Misc.compute_epoch_at_slot(slot) + epoch > set.epoch end defp attest(set, epoch, slot) do @@ -119,7 +139,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do validator = Map.get(set.validators, validator_index) updated_validator = Validator.start_payload_builder(validator, slot + 1, head_root) - %{set | validators: Map.put(set.validators, updated_validator.validator.index, updated_validator)} + %{set | validators: Map.put(set.validators, updated_validator.validator.index, %{updated_validator | root: head_root})} end end @@ -139,12 +159,12 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Notify all validators of a new tick. """ @spec notify_tick(t(), tuple()) :: t() - def notify_tick(%{validators: validators} = set, slot_data) do + def notify_tick(%{validators: validators, head_root: head_root} = set, slot_data) do validators = maybe_debug_notify( fn -> Map.new(validators, fn {k, v} -> - {k, Validator.handle_tick(slot_data, v)} + {k, Validator.handle_tick(slot_data, v, head_root)} end) end, {:on_tick, slot_data} @@ -154,14 +174,14 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end defp maybe_debug_notify(fun, data) do - if Application.get_env(:logger, :level) == :debug do - Logger.debug("[Validator] Notifying all Validators with message: #{inspect(data)}") + if Application.get_env(:logger, :level) == :info do # :debug do + Logger.info("[Validator] Notifying all Validators with message: #{inspect(data)}") start_time = System.monotonic_time(:millisecond) result = fun.() end_time = System.monotonic_time(:millisecond) - Logger.debug( + Logger.info( "[Validator] #{inspect(data)} notified to all Validators after #{end_time - start_time} ms" ) From 616f8924a6b4fdeb4dfe2bdb16c079fcf2e23e89 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 13 Aug 2024 12:28:01 -0300 Subject: [PATCH 25/42] Removed slot and root from Validator and started cleaning up the ValidatorSet --- .iex.exs | 0 .../validator/validator.ex | 28 ++-- .../validator/validator_set.ex | 157 ++++++++++-------- 3 files changed, 96 insertions(+), 89 deletions(-) create mode 100644 .iex.exs diff --git a/.iex.exs b/.iex.exs new file mode 100644 index 000000000..e69de29bb diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 53c7a8418..7ed1d0b28 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -5,8 +5,6 @@ defmodule LambdaEthereumConsensus.Validator do require Logger defstruct [ - :slot, - :root, :epoch, :duties, :validator, @@ -40,9 +38,7 @@ defmodule LambdaEthereumConsensus.Validator do # TODO: Slot and Root are redundant, we should also have the duties separated and calculated # just at the begining of every epoch, and then just update them as needed. @type state :: %__MODULE__{ - slot: Types.slot(), epoch: Types.epoch(), - root: Types.root(), duties: Duties.duties(), validator: validator(), payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil @@ -57,9 +53,7 @@ defmodule LambdaEthereumConsensus.Validator do ) :: state def new({pubkey, privkey}, epoch, head_slot, head_root, beacon) do state = %__MODULE__{ - slot: head_slot, epoch: epoch, - root: head_root, duties: Duties.empty_duties(), validator: %{ pubkey: pubkey, @@ -120,7 +114,7 @@ defmodule LambdaEthereumConsensus.Validator do # TODO: this doesn't take into account reorgs state |> update_state(slot, head_root) - |> maybe_attest(slot) + |> maybe_attest(slot, head_root) |> maybe_build_payload(slot + 1, head_root) end @@ -145,7 +139,7 @@ defmodule LambdaEthereumConsensus.Validator do # 1. send our attestation for an empty slot # 2. start building a payload state - |> maybe_attest(slot) + |> maybe_attest(slot, root) |> maybe_build_payload(slot + 1, root) end @@ -168,7 +162,7 @@ defmodule LambdaEthereumConsensus.Validator do epoch = Misc.compute_epoch_at_slot(slot + 1) if last_epoch == epoch do - %{state | slot: slot, root: head_root} + state else recompute_duties(state, last_epoch, epoch, slot, head_root) end @@ -189,7 +183,7 @@ defmodule LambdaEthereumConsensus.Validator do move_subnets(state.duties, new_duties) Duties.log_duties(new_duties, state.validator.index) - %{state | slot: slot, root: head_root, duties: new_duties, epoch: epoch} + %{state | duties: new_duties, epoch: epoch} end @spec fetch_target_state(Types.epoch(), Types.root()) :: Types.BeaconState.t() @@ -230,11 +224,11 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec maybe_attest(state, Types.slot()) :: state - defp maybe_attest(state, slot) do + @spec maybe_attest(state, Types.slot(), Types.root()) :: state + defp maybe_attest(state, slot, head_root) do case Duties.get_current_attester_duty(state.duties, slot) do %{attested?: false} = duty -> - attest(state, duty) + attest(state, duty, head_root) new_duties = Duties.replace_attester_duty(state.duties, duty, %{duty | attested?: true}) @@ -246,12 +240,12 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec attest(state, Duties.attester_duty()) :: :ok - def attest(%{validator: validator} = state, current_duty) do + @spec attest(state, Duties.attester_duty(), Types.root()) :: :ok + def attest(%{validator: validator} = state, current_duty, head_root) do subnet_id = current_duty.subnet_id log_debug(validator.index, "attesting", slot: current_duty.slot, subnet_id: subnet_id) - attestation = produce_attestation(current_duty, state.root, state.validator.privkey) + attestation = produce_attestation(current_duty, head_root, state.validator.privkey) log_md = [slot: attestation.data.slot, attestation: attestation, subnet_id: subnet_id] @@ -472,7 +466,7 @@ defmodule LambdaEthereumConsensus.Validator do state end - defp propose(state, proposed_slot) do + defp propose(state, proposed_slot, _head_root) do Logger.error( "[Validator] Skipping block proposal for slot #{proposed_slot} due to missing validator data" ) diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 05c4f693b..de00ae507 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -5,7 +5,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do simplify the delegation of work. """ - defstruct epoch: nil, slot: nil, head_root: nil, duties: %{}, validators: [] + defstruct head_root: nil, duties: %{}, validators: [] require Logger @@ -16,8 +16,6 @@ defmodule LambdaEthereumConsensus.ValidatorSet do @type validators :: %{atom() => %{} | []} @type t :: %__MODULE__{ - epoch: Types.epoch() | nil, - slot: Types.slot() | nil, head_root: Types.root() | nil, duties: %{Types.epoch() => %{proposers: Duties.proposers(), attesters: Duties.attesters()}}, validators: validators() @@ -35,7 +33,43 @@ defmodule LambdaEthereumConsensus.ValidatorSet do setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) end - defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) + @doc """ + Notify all validators of a new head. + """ + @spec notify_head(t(), Types.slot(), Types.root()) :: t() + def notify_head(%{validators: validators} = set, slot, head_root) do + # TODO: Just for testing purposes, remove it later + Logger.info("[Validator] Notifying all Validators with new_head", root: head_root, slot: slot) + epoch = Misc.compute_epoch_at_slot(slot) + + set + |> update_state(epoch, slot, head_root) + |> attest(epoch, slot, head_root) + |> build_next_payload(epoch, slot, head_root) + end + + @doc """ + Notify all validators of a new tick. + """ + @spec notify_tick(t(), tuple()) :: t() + def notify_tick(%{validators: validators, head_root: head_root} = set, slot_data) do + validators = + maybe_debug_notify( + fn -> + Map.new(validators, fn {k, v} -> + {k, Validator.handle_tick(slot_data, v, head_root)} + end) + end, + {:on_tick, slot_data} + ) + + %{set | validators: validators} + end + + ############################## + # Setup + + defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) when is_nil(keystore_dir) or is_nil(keystore_pass_dir) do Logger.warning( "[Validator] No keystore_dir or keystore_pass_dir provided. Validators won't start." @@ -46,9 +80,10 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) do validator_keys = decode_validator_keys(keystore_dir, keystore_pass_dir) - epoch = Misc.compute_epoch_at_slot(slot) - beacon = fetch_target_state!(epoch, head_root) + + # This will be removed later when refactoring Validator new + beacon = fetch_target_beaconstate!(epoch, head_root) validators = Map.new(validator_keys, fn validator_key -> @@ -58,76 +93,70 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") - duties = compute_duties_for_epoch!(beacon, epoch, validators) - - %__MODULE__{ - epoch: epoch, - slot: slot, - head_root: head_root, - duties: %{epoch => duties}, - validators: validators - } + %__MODULE__{validators: validators} + |> update_state(epoch, slot, head_root) end - defp compute_duties_for_epoch!(beacon, epoch, validators) do - {:ok, proposers} = Duties.compute_proposers_for_epoch(beacon, epoch, validators) - {:ok, attesters} = Duties.compute_attesters_for_epoch(beacon, epoch, validators) - - Logger.info("[Validator] Proposer duties for epoch #{epoch} are: #{inspect(proposers, pretty: true)}") - Logger.info("[Validator] Attester duties for epoch #{epoch} are: #{inspect(attesters, pretty: true)}") + ############################## + # State update - %{proposers: proposers, attesters: attesters} + defp update_state(set, epoch, slot, head_root) do + set + |> update_head(head_root) + |> compute_duties(epoch, slot, head_root) end - defp fetch_target_state!(epoch, head_root) do - {:ok, state} = CheckpointStates.compute_target_checkpoint_state(epoch, head_root) - state - end + defp update_head(%{head_root: head_root} = set, head_root), do: set + defp update_head(set, head_root), do: %{set | head_root: head_root} - @doc """ - Notify all validators of a new head. - """ - @spec notify_head(t(), Types.slot(), Types.root()) :: t() - def notify_head(%{validators: validators, epoch: epoch} = set, slot, head_root) do - # TODO: Just for testing purposes, remove it later - Logger.info("[Validator] Notifying all Validators with new_head", root: head_root, slot: slot) + defp compute_duties(set, epoch, _slot, _head_root) + when not is_nil(:erlang.map_get(epoch, set.duties)), do: set - set - |> update_state(slot, head_root) - |> attest(epoch, slot) - |> build_next_payload(epoch, slot, head_root) + defp compute_duties(set, epoch, slot, head_root) do + epoch + |> fetch_target_beaconstate!(head_root) + |> compute_duties_for_epoch!(epoch, set.validators) + |> merge_duties_and_prune(epoch, set) + end + + defp fetch_target_beaconstate!(epoch, head_root) do + {:ok, beaconstate} = CheckpointStates.compute_target_checkpoint_state(epoch, head_root) + beaconstate end - defp update_state(set, slot, head_root) do - if new_epoch?(set, slot + 1) do - epoch = Misc.compute_epoch_at_slot(slot + 1) - beacon = fetch_target_state!(epoch, head_root) + defp compute_duties_for_epoch!(beacon, epoch, validators) do + {:ok, proposers} = Duties.compute_proposers_for_epoch(beacon, epoch, validators) + {:ok, attesters} = Duties.compute_attesters_for_epoch(beacon, epoch, validators) - duties = compute_duties_for_epoch!(beacon, epoch, set.validators) + Logger.info("[Validator] Proposer duties for epoch #{epoch} are: #{inspect(proposers, pretty: true)}") + Logger.info("[Validator] Attester duties for epoch #{epoch} are: #{inspect(attesters, pretty: true)}") - %{set | epoch: epoch, slot: slot, head_root: head_root, duties: Map.put(set.duties, epoch, duties)} - else - %{set | slot: slot, head_root: head_root} - end + %{epoch => %{proposers: proposers, attesters: attesters}} end - defp new_epoch?(set, slot) do - epoch = Misc.compute_epoch_at_slot(slot) - epoch > set.epoch + defp merge_duties_and_prune(new_duties, epoch, set) do + set.duties + # Remove duties from epoch - 2 or older + |> Map.reject(fn {old_epoch, _} -> old_epoch < epoch - 2 end) + |> Map.merge(new_duties) + |> then(fn current_duties -> %{set | duties: current_duties} end) end - defp attest(set, epoch, slot) do + ############################## + # Attestation and proposal + + defp attest(set, epoch, slot, root) do updated_duties = set |> current_attesters(epoch, slot) |> Enum.map(fn {validator, duty} -> - Validator.attest(validator, duty) + Validator.attest(validator, duty, root) # Duty.attested(duty) %{duty | attested?: true} end) - %{set | duties: put_in(set.duties, [set.epoch, :attesters, slot], updated_duties)} + %{set | duties: put_in(set.duties, [epoch, :attesters, slot], updated_duties)} end defp build_next_payload(set, epoch, slot, head_root) do @@ -139,10 +168,13 @@ defmodule LambdaEthereumConsensus.ValidatorSet do validator = Map.get(set.validators, validator_index) updated_validator = Validator.start_payload_builder(validator, slot + 1, head_root) - %{set | validators: Map.put(set.validators, updated_validator.validator.index, %{updated_validator | root: head_root})} + %{set | validators: Map.put(set.validators, updated_validator.validator.index, updated_validator)} end end + ############################## + # Helpers + defp current_attesters(set, epoch, slot) do attesters(set, epoch, slot) |> Enum.flat_map(fn @@ -154,25 +186,6 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp proposer(set, epoch, slot), do: get_in(set.duties, [epoch, :proposers, slot]) defp attesters(set, epoch, slot), do: get_in(set.duties, [epoch, :attesters, slot]) || [] - - @doc """ - Notify all validators of a new tick. - """ - @spec notify_tick(t(), tuple()) :: t() - def notify_tick(%{validators: validators, head_root: head_root} = set, slot_data) do - validators = - maybe_debug_notify( - fn -> - Map.new(validators, fn {k, v} -> - {k, Validator.handle_tick(slot_data, v, head_root)} - end) - end, - {:on_tick, slot_data} - ) - - %{set | validators: validators} - end - defp maybe_debug_notify(fun, data) do if Application.get_env(:logger, :level) == :info do # :debug do Logger.info("[Validator] Notifying all Validators with message: #{inspect(data)}") From cde101c6ea19c41bfa2e085fcba1a141dd89bc1b Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 13 Aug 2024 16:36:11 -0300 Subject: [PATCH 26/42] Fixed how keystore functions handle the validator set --- .../beacon/beacon_node.ex | 4 +- .../validator/validator.ex | 14 +++++ lib/libp2p_port.ex | 58 ++++++++----------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex index 9316be2bb..bf9b3f83e 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex @@ -30,9 +30,9 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do init_execution_chain(deposit_tree_snapshot, store.head_root) - validators = ValidatorSet.init(store.head_slot, store.head_root) + validator_set = ValidatorSet.init(store.head_slot, store.head_root) - libp2p_args = [genesis_time: store.genesis_time, validators: validators] ++ get_libp2p_args() + libp2p_args = [genesis_time: store.genesis_time, validator_set: validator_set] ++ get_libp2p_args() children = [ diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 66cd402d2..daaa72681 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -40,6 +40,20 @@ defmodule LambdaEthereumConsensus.Validator do payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil } + @spec new( + Keystore.t(), + Types.epoch(), + Types.slot(), + Types.root(), + Types.BeaconState.t() + ) :: t() + def new(keystore, head_slot, head_root) do + epoch = Misc.compute_epoch_at_slot(head_slot) + beacon = fetch_target_state(epoch, head_root) |> go_to_slot(head_slot) + + new(keystore, epoch, head_slot, head_root, beacon) + end + @spec new( Keystore.t(), Types.epoch(), diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index b6b8a7219..1317116f4 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -65,7 +65,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @type init_arg :: {:genesis_time, Types.uint64()} - | {:validators, %{}} + | {:validator_set, ValidatorSet.t()} | {:listen_addr, [String.t()]} | {:enable_discovery, boolean()} | {:discovery_addr, String.t()} @@ -388,7 +388,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def init(args) do {genesis_time, args} = Keyword.pop!(args, :genesis_time) - {validators, args} = Keyword.pop(args, :validators, %{}) + {validator_set, args} = Keyword.pop(args, :validator_set, %{}) {join_init_topics, args} = Keyword.pop(args, :join_init_topics, false) {enable_request_handlers, args} = Keyword.pop(args, :enable_request_handlers, false) @@ -416,7 +416,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:ok, %{ genesis_time: genesis_time, - validators: validators, + validator_set: validator_set, slot_data: nil, port: port, subscribers: %{}, @@ -514,11 +514,11 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end @impl GenServer - def handle_info({:new_head, slot, head_root}, %{validators: validators} = state) do - updated_validators = - ValidatorSet.notify_head(validators, slot, head_root) + def handle_info({:new_head, slot, head_root}, %{validator_set: validator_set} = state) do + updated_validator_set = + ValidatorSet.notify_head(validator_set, slot, head_root) - {:noreply, %{state | validators: updated_validators}} + {:noreply, %{state | validator_set: updated_validator_set}} end @impl GenServer @@ -540,18 +540,20 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end @impl GenServer - def handle_call(:get_keystores, _from, %{validators: validators} = state), - do: {:reply, Enum.map(validators, fn {_pubkey, validator} -> validator.keystore end), state} + def handle_call(:get_keystores, _from, %{validator_set: validator_set} = state), + do: {:reply, Enum.map(validator_set.validators, fn {_index, validator} -> validator.keystore end), state} @impl GenServer - def handle_call({:delete_validator, pubkey}, _from, %{validators: validators} = state) do - case Map.fetch(validators, pubkey) do - {:ok, validator} -> - Logger.warning("[Libp2pPort] Deleting validator with index #{inspect(validator.index)}.") - - {:reply, :ok, %{state | validators: Map.delete(validators, pubkey)}} - - :error -> + def handle_call({:delete_validator, pubkey}, _from, %{validator_set: validator_set} = state) do + validator_set.validators + |> Enum.find(fn {_index, validator} -> validator.keystore.pubkey == pubkey end) + |> case do + {index, validator} -> + Logger.warning("[Libp2pPort] Deleting validator with index #{inspect(index)}.") + updated_validators = Map.delete(validator_set.validators, index) + {:reply, :ok, Map.put(state.validator_set, :validators, updated_validators)} + + _ -> {:error, "Pubkey #{inspect(pubkey)} not found."} end end @@ -560,26 +562,16 @@ defmodule LambdaEthereumConsensus.Libp2pPort do def handle_call( {:add_validator, %Keystore{pubkey: pubkey} = keystore}, _from, - %{validators: validators} = state + %{validator_set: %{head_root: head_root} = validator_set, slot_data: {slot, _third}} = state ) do # TODO (#1263): handle 0 validators - first_validator = validators |> Map.values() |> List.first() - validator = Validator.new({first_validator.slot, first_validator.root, keystore}) + validator = Validator.new(keystore, slot, head_root) Logger.warning( "[Libp2pPort] Adding validator with index #{inspect(validator.index)}. head_slot: #{inspect(validator.slot)}." ) - {:reply, :ok, - %{ - state - | validators: - Map.put( - validators, - pubkey, - validator - ) - }} + {:reply, :ok, put_in(state.validator_set, [:validators, validator.index], validator)} end ###################### @@ -799,10 +791,10 @@ defmodule LambdaEthereumConsensus.Libp2pPort do if slot_data == new_slot_data do state else - updated_validators = - ValidatorSet.notify_tick(state.validators, new_slot_data) + updated_validator_set = + ValidatorSet.notify_tick(state.validator_set, new_slot_data) - %{state | slot_data: new_slot_data, validators: updated_validators} + %{state | slot_data: new_slot_data, validator_set: updated_validator_set} end maybe_log_new_slot(slot_data, new_slot_data) From 654b5ea59801e47f84764bd741d6623082a8cf5f Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 13 Aug 2024 17:12:15 -0300 Subject: [PATCH 27/42] Formatted and fixed all lint and dialyzer issues --- .../beacon/beacon_node.ex | 3 +- .../validator/duties.ex | 35 ++++++++------- .../validator/validator.ex | 15 ++++--- .../validator/validator_set.ex | 44 ++++++++++++------- lib/libp2p_port.ex | 15 ++++--- 5 files changed, 68 insertions(+), 44 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex index bf9b3f83e..ee151b145 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex @@ -32,7 +32,8 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do validator_set = ValidatorSet.init(store.head_slot, store.head_root) - libp2p_args = [genesis_time: store.genesis_time, validator_set: validator_set] ++ get_libp2p_args() + libp2p_args = + [genesis_time: store.genesis_time, validator_set: validator_set] ++ get_libp2p_args() children = [ diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 77b7f233a..5032528a3 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -4,6 +4,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do """ alias LambdaEthereumConsensus.StateTransition.Accessors alias LambdaEthereumConsensus.StateTransition.Misc + alias LambdaEthereumConsensus.Validator alias LambdaEthereumConsensus.Validator.Utils alias Types.BeaconState @@ -44,7 +45,6 @@ defmodule LambdaEthereumConsensus.Validator.Duties do def compute_proposers_for_epoch(%BeaconState{} = state, epoch, validators) do with {:ok, epoch} <- check_valid_epoch(state, epoch), {start_slot, end_slot} <- boundary_slots(epoch) do - start_slot..end_slot |> Enum.flat_map(fn slot -> {:ok, proposer_index} = Accessors.get_beacon_proposer_index(state, slot) @@ -61,7 +61,6 @@ defmodule LambdaEthereumConsensus.Validator.Duties do def compute_attesters_for_epoch(%BeaconState{} = state, epoch, validators) do with {:ok, epoch} <- check_valid_epoch(state, epoch), {start_slot, end_slot} <- boundary_slots(epoch) do - committee_count_per_slot = Accessors.get_committee_count_per_slot(state, epoch) start_slot..end_slot @@ -77,27 +76,31 @@ defmodule LambdaEthereumConsensus.Validator.Duties do state :: BeaconState.t(), epoch :: Types.epoch(), slot :: Types.slot(), - committee_index :: Types.uint64(), - validator_index :: [{Types.validator_index(), Validator.validator()}] - ) :: [attester_duty()] + validators :: %{Types.validator_index() => Validator.t()}, + committee_index :: Types.uint64() + ) :: [{Types.slot(), attester_duty()}] defp compute_attester_dutys(state, epoch, slot, validators, committee_index) do with {:ok, committee} <- Accessors.get_beacon_committee(state, slot, committee_index) do committee |> Stream.with_index() |> Stream.flat_map(fn {validator_index, index_in_committee} -> case Map.get(validators, validator_index) do - nil -> [] + nil -> + [] + validator -> - [%{ - slot: slot, - validator_index: validator_index, - index_in_committee: index_in_committee, - committee_length: length(committee), - committee_index: committee_index, - attested?: false - } - |> update_with_aggregation_duty(state, validator.keystore.privkey) - |> update_with_subnet_id(state, epoch)] + [ + %{ + slot: slot, + validator_index: validator_index, + index_in_committee: index_in_committee, + committee_length: length(committee), + committee_index: committee_index, + attested?: false + } + |> update_with_aggregation_duty(state, validator.keystore.privkey) + |> update_with_subnet_id(state, epoch) + ] end end) |> Enum.into([]) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index daaa72681..f974e6139 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -42,10 +42,8 @@ defmodule LambdaEthereumConsensus.Validator do @spec new( Keystore.t(), - Types.epoch(), Types.slot(), - Types.root(), - Types.BeaconState.t() + Types.root() ) :: t() def new(keystore, head_slot, head_root) do epoch = Misc.compute_epoch_at_slot(head_slot) @@ -184,9 +182,10 @@ defmodule LambdaEthereumConsensus.Validator do end @spec recompute_duties(t(), Types.epoch(), Types.epoch(), Types.slot(), Types.root()) :: t() - defp recompute_duties(state, last_epoch, epoch, slot, head_root) do + defp recompute_duties(state, last_epoch, epoch, _slot, head_root) do start_slot = Misc.compute_start_slot_at_epoch(epoch) - # Why is this needed? something here seems wrong, why would i need to move to a different slot if + + # TODO: Why is this needed? something here seems wrong, why would i need to move to a different slot if # I'm calculating this at a new epoch? need to check it # target_root = if slot == start_slot, do: head_root, else: last_root @@ -259,7 +258,7 @@ defmodule LambdaEthereumConsensus.Validator do end @spec attest(t(), Duties.attester_duty(), Types.root()) :: :ok - def attest(%{index: validator_index, keystore: keystore} = state, current_duty, head_root) do + def attest(%{index: validator_index, keystore: keystore}, current_duty, head_root) do subnet_id = current_duty.subnet_id log_debug(validator_index, "attesting", slot: current_duty.slot, subnet_id: subnet_id) @@ -421,7 +420,9 @@ defmodule LambdaEthereumConsensus.Validator do def start_payload_builder(%{index: validator_index} = state, proposed_slot, head_root) do # TODO: handle reorgs and late blocks - log_debug(validator_index, "starting building payload for slot #{proposed_slot}", root: head_root) + log_debug(validator_index, "starting building payload for slot #{proposed_slot}", + root: head_root + ) case BlockBuilder.start_building_payload(proposed_slot, head_root) do {:ok, payload_id} -> diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 87cb8e36d..8930fa273 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -17,7 +17,12 @@ defmodule LambdaEthereumConsensus.ValidatorSet do @type validators :: %{atom() => %{} | []} @type t :: %__MODULE__{ head_root: Types.root() | nil, - duties: %{Types.epoch() => %{proposers: Duties.proposers(), attesters: Duties.attesters()}}, + duties: %{ + Types.epoch() => %{ + proposers: Duties.proposer_duties(), + attesters: Duties.attester_duties() + } + }, validators: validators() } @@ -37,13 +42,13 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Notify all validators of a new head. """ @spec notify_head(t(), Types.slot(), Types.root()) :: t() - def notify_head(%{validators: validators} = set, slot, head_root) do + def notify_head(set, slot, head_root) do # TODO: Just for testing purposes, remove it later Logger.info("[Validator] Notifying all Validators with new_head", root: head_root, slot: slot) epoch = Misc.compute_epoch_at_slot(slot) set - |> update_state(epoch, slot, head_root) + |> update_state(epoch, head_root) |> attest(epoch, slot, head_root) |> build_next_payload(epoch, slot, head_root) end @@ -69,7 +74,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do ############################## # Setup - defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) + defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) when is_nil(keystore_dir) or is_nil(keystore_pass_dir) do Logger.warning( "[Validator] No keystore_dir or keystore_pass_dir provided. Validators won't start." @@ -94,25 +99,26 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") %__MODULE__{validators: validators} - |> update_state(epoch, slot, head_root) + |> update_state(epoch, head_root) end ############################## # State update - defp update_state(set, epoch, slot, head_root) do + defp update_state(set, epoch, head_root) do set |> update_head(head_root) - |> compute_duties(epoch, slot, head_root) + |> compute_duties(epoch, head_root) end defp update_head(%{head_root: head_root} = set, head_root), do: set defp update_head(set, head_root), do: %{set | head_root: head_root} - defp compute_duties(set, epoch, _slot, _head_root) - when not is_nil(:erlang.map_get(epoch, set.duties)), do: set + defp compute_duties(set, epoch, _head_root) + when not is_nil(:erlang.map_get(epoch, set.duties)), + do: set - defp compute_duties(set, epoch, slot, head_root) do + defp compute_duties(set, epoch, head_root) do epoch |> fetch_target_beaconstate!(head_root) |> compute_duties_for_epoch!(epoch, set.validators) @@ -128,8 +134,13 @@ defmodule LambdaEthereumConsensus.ValidatorSet do {:ok, proposers} = Duties.compute_proposers_for_epoch(beacon, epoch, validators) {:ok, attesters} = Duties.compute_attesters_for_epoch(beacon, epoch, validators) - Logger.info("[Validator] Proposer duties for epoch #{epoch} are: #{inspect(proposers, pretty: true)}") - Logger.info("[Validator] Attester duties for epoch #{epoch} are: #{inspect(attesters, pretty: true)}") + Logger.info( + "[Validator] Proposer duties for epoch #{epoch} are: #{inspect(proposers, pretty: true)}" + ) + + Logger.info( + "[Validator] Attester duties for epoch #{epoch} are: #{inspect(attesters, pretty: true)}" + ) %{epoch => %{proposers: proposers, attesters: attesters}} end @@ -163,7 +174,9 @@ defmodule LambdaEthereumConsensus.ValidatorSet do set |> proposer(epoch, slot + 1) |> case do - nil -> set + nil -> + set + validator_index -> validator = Map.get(set.validators, validator_index) updated_validator = Validator.start_payload_builder(validator, slot + 1, head_root) @@ -187,7 +200,8 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp attesters(set, epoch, slot), do: get_in(set.duties, [epoch, :attesters, slot]) || [] defp maybe_debug_notify(fun, data) do - if Application.get_env(:logger, :level) == :info do # :debug do + # :debug do + if Application.get_env(:logger, :level) == :info do Logger.info("[Validator] Notifying all Validators with message: #{inspect(data)}") start_time = System.monotonic_time(:millisecond) @@ -211,7 +225,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do - /.txt """ @spec decode_validator_keystores(binary(), binary()) :: - list(Keystore.t()) + list(Keystore.t()) def decode_validator_keystores(keystore_dir, keystore_pass_dir) when is_binary(keystore_dir) and is_binary(keystore_pass_dir) do keystore_dir diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 1317116f4..2d1414447 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -23,6 +23,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do alias LambdaEthereumConsensus.P2p.Requests alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.Utils.BitVector + alias LambdaEthereumConsensus.Validator alias LambdaEthereumConsensus.ValidatorSet alias Libp2pProto.AddPeer alias Libp2pProto.Command @@ -541,14 +542,17 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def handle_call(:get_keystores, _from, %{validator_set: validator_set} = state), - do: {:reply, Enum.map(validator_set.validators, fn {_index, validator} -> validator.keystore end), state} + do: + {:reply, + Enum.map(validator_set.validators, fn {_index, validator} -> validator.keystore end), + state} @impl GenServer def handle_call({:delete_validator, pubkey}, _from, %{validator_set: validator_set} = state) do validator_set.validators |> Enum.find(fn {_index, validator} -> validator.keystore.pubkey == pubkey end) |> case do - {index, validator} -> + {index, _validator} -> Logger.warning("[Libp2pPort] Deleting validator with index #{inspect(index)}.") updated_validators = Map.delete(validator_set.validators, index) {:reply, :ok, Map.put(state.validator_set, :validators, updated_validators)} @@ -560,15 +564,16 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def handle_call( - {:add_validator, %Keystore{pubkey: pubkey} = keystore}, + {:add_validator, keystore}, _from, - %{validator_set: %{head_root: head_root} = validator_set, slot_data: {slot, _third}} = state + %{validator_set: %{head_root: head_root}, slot_data: {slot, _third}} = + state ) do # TODO (#1263): handle 0 validators validator = Validator.new(keystore, slot, head_root) Logger.warning( - "[Libp2pPort] Adding validator with index #{inspect(validator.index)}. head_slot: #{inspect(validator.slot)}." + "[Libp2pPort] Adding validator with index #{inspect(validator.index)}. head_slot: #{inspect(slot)}." ) {:reply, :ok, put_in(state.validator_set, [:validators, validator.index], validator)} From 37a593dea26faab5294a1068cf97117ba805453b Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 13 Aug 2024 17:40:30 -0300 Subject: [PATCH 28/42] Fix some warnings from compile and credo --- .../validator/duties.ex | 63 ++++++++++--------- .../validator/validator.ex | 2 +- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 5032528a3..19472c529 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -80,40 +80,45 @@ defmodule LambdaEthereumConsensus.Validator.Duties do committee_index :: Types.uint64() ) :: [{Types.slot(), attester_duty()}] defp compute_attester_dutys(state, epoch, slot, validators, committee_index) do - with {:ok, committee} <- Accessors.get_beacon_committee(state, slot, committee_index) do - committee - |> Stream.with_index() - |> Stream.flat_map(fn {validator_index, index_in_committee} -> - case Map.get(validators, validator_index) do - nil -> - [] - - validator -> - [ - %{ - slot: slot, - validator_index: validator_index, - index_in_committee: index_in_committee, - committee_length: length(committee), - committee_index: committee_index, - attested?: false - } - |> update_with_aggregation_duty(state, validator.keystore.privkey) - |> update_with_subnet_id(state, epoch) - ] - end - end) - |> Enum.into([]) - |> case do - [] -> [] - duties -> [{slot, duties}] - end - else + case Accessors.get_beacon_committee(state, slot, committee_index) do + {:ok, committee} -> + compute_cometee_duties(state, epoch, slot, committee, committee_index, validators) + {:error, _} -> [] end end + defp compute_cometee_duties(state, epoch, slot, committee, committee_index, validators) do + committee + |> Stream.with_index() + |> Stream.flat_map(fn {validator_index, index_in_committee} -> + case Map.get(validators, validator_index) do + nil -> + [] + + validator -> + [ + %{ + slot: slot, + validator_index: validator_index, + index_in_committee: index_in_committee, + committee_length: length(committee), + committee_index: committee_index, + attested?: false + } + |> update_with_aggregation_duty(state, validator.keystore.privkey) + |> update_with_subnet_id(state, epoch) + ] + end + end) + |> Enum.into([]) + |> case do + [] -> [] + duties -> [{slot, duties}] + end + end + defp check_valid_epoch(state, epoch) do next_epoch = Accessors.get_current_epoch(state) + 1 diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index f974e6139..2728701c5 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -540,7 +540,7 @@ defmodule LambdaEthereumConsensus.Validator do defp log_info(index, message, metadata \\ []), do: Logger.info("[Validator] #{index} #{message}", metadata) - defp log_debug(index, message, metadata \\ []), + defp log_debug(index, message, metadata), do: Logger.debug("[Validator] #{index} #{message}", metadata) defp log_error(index, message, reason, metadata \\ []), From c99729f6983e0a027d926c74f9f872ef67de6fb5 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 14 Aug 2024 11:57:45 -0300 Subject: [PATCH 29/42] handle_tick completely moved to ValidatorSet --- .../validator/validator.ex | 44 ++---- .../validator/validator_set.ex | 129 ++++++++++++++---- 2 files changed, 111 insertions(+), 62 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 2728701c5..7c5875429 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -111,26 +111,6 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec handle_new_head(Types.slot(), Types.root(), t()) :: t() - def handle_new_head(slot, head_root, %{index: nil} = state) do - log_error("-1", "setup validator", "index not present handle block", - slot: slot, - root: head_root - ) - - state - end - - def handle_new_head(slot, head_root, state) do - log_debug(state.index, "recieved new head", slot: slot, root: head_root) - - # TODO: this doesn't take into account reorgs - state - |> update_state(slot, head_root) - |> maybe_attest(slot, head_root) - |> maybe_build_payload(slot + 1, head_root) - end - @spec handle_tick({Types.slot(), atom()}, t(), Types.root()) :: t() def handle_tick(_logical_time, %{index: nil} = state, _root) do log_error("-1", "setup validator", "index not present for handle tick") @@ -298,7 +278,7 @@ defmodule LambdaEthereumConsensus.Validator do end end - defp publish_aggregate(duty, validator_index, keystore) do + def publish_aggregate(duty, validator_index, keystore) do case Gossip.Attestation.stop_collecting(duty.subnet_id) do {:ok, attestations} -> log_md = [slot: duty.slot, attestations: attestations] @@ -445,15 +425,15 @@ defmodule LambdaEthereumConsensus.Validator do end end - defp propose( - %{ - index: validator_index, - payload_builder: {proposed_slot, head_root, payload_id}, - keystore: keystore - } = state, - proposed_slot, - head_root - ) do + def propose( + %{ + index: validator_index, + payload_builder: {proposed_slot, head_root, payload_id}, + keystore: keystore + } = state, + proposed_slot, + head_root + ) do log_debug(validator_index, "building block", slot: proposed_slot) build_result = @@ -481,12 +461,12 @@ defmodule LambdaEthereumConsensus.Validator do end # TODO: at least in kurtosis there are blocks that are proposed without a payload apparently, must investigate. - defp propose(%{payload_builder: nil} = state, _proposed_slot, _head_root) do + def propose(%{payload_builder: nil} = state, _proposed_slot, _head_root) do log_error(state.index, "propose block", "lack of execution payload") state end - defp propose(state, proposed_slot, _head_root) do + def propose(state, proposed_slot, _head_root) do Logger.error( "[Validator] Skipping block proposal for slot #{proposed_slot} due to missing validator data" ) diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 8930fa273..accb3d8fd 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -57,20 +57,54 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Notify all validators of a new tick. """ @spec notify_tick(t(), tuple()) :: t() - def notify_tick(%{validators: validators, head_root: head_root} = set, slot_data) do - validators = - maybe_debug_notify( - fn -> - Map.new(validators, fn {k, v} -> - {k, Validator.handle_tick(slot_data, v, head_root)} - end) - end, - {:on_tick, slot_data} - ) + def notify_tick(%{head_root: head_root} = set, {slot, third} = slot_data) do + # TODO: Just for testing purposes, remove it later + Logger.info("[Validator] Notifying all Validators with notify_tick: #{inspect(third)}", + root: head_root, + slot: slot + ) + + epoch = Misc.compute_epoch_at_slot(slot) + + process_tick(set, epoch, slot_data) + end + + @spec process_tick(t(), Types.epoch(), tuple()) :: t() + def process_tick(%{head_root: head_root} = set, epoch, {slot, :first_third}) do + set + |> update_state(epoch, head_root) + |> propose(epoch, slot, head_root) + end - %{set | validators: validators} + @spec process_tick(t(), Types.epoch(), tuple()) :: t() + def process_tick(%{head_root: head_root} = set, epoch, {slot, :second_third}) do + set + |> update_state(epoch, head_root) + |> attest(epoch, slot, head_root) + |> build_next_payload(epoch, slot, head_root) + end + + @spec process_tick(t(), Types.epoch(), tuple()) :: t() + def process_tick(%{head_root: head_root} = set, epoch, {slot, :last_third}) do + set + |> update_state(epoch, head_root) + |> publish_aggregate(epoch, slot, head_root) end + # def process_tick(%{validators: validators, head_root: head_root} = set, _epoch, slot_data) do + # validators = + # maybe_debug_notify( + # fn -> + # Map.new(validators, fn {k, v} -> + # {k, Validator.handle_tick(slot_data, v, head_root)} + # end) + # end, + # {:on_tick, slot_data} + # ) + + # %{set | validators: validators} + # end + ############################## # Setup @@ -170,7 +204,21 @@ defmodule LambdaEthereumConsensus.ValidatorSet do %{set | duties: put_in(set.duties, [epoch, :attesters, slot], updated_duties)} end - defp build_next_payload(set, epoch, slot, head_root) do + defp publish_aggregate(set, epoch, slot, head_root) do + updated_duties = + set + |> current_aggregators(epoch, slot) + |> Enum.map(fn {validator, duty} -> + Validator.publish_aggregate(duty, validator.index, validator.keystore) + + # Duty.aggregated(duty) + %{duty | should_aggregate?: false} + end) + + %{set | duties: put_in(set.duties, [epoch, :attesters, slot], updated_duties)} + end + + defp build_next_payload(%{validators: validators} = set, epoch, slot, head_root) do set |> proposer(epoch, slot + 1) |> case do @@ -178,10 +226,23 @@ defmodule LambdaEthereumConsensus.ValidatorSet do set validator_index -> - validator = Map.get(set.validators, validator_index) - updated_validator = Validator.start_payload_builder(validator, slot + 1, head_root) + validators + |> Map.update!(validator_index, &Validator.start_payload_builder(&1, slot + 1, head_root)) + |> then(&%{set | validators: &1}) + end + end + + defp propose(%{validators: validators} = set, epoch, slot, head_root) do + set + |> proposer(epoch, slot) + |> case do + nil -> + set - %{set | validators: Map.put(set.validators, updated_validator.index, updated_validator)} + validator_index -> + validators + |> Map.update!(validator_index, &Validator.propose(&1, slot, head_root)) + |> then(&%{set | validators: &1}) end end @@ -196,27 +257,35 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end) end + defp current_aggregators(set, epoch, slot) do + attesters(set, epoch, slot) + |> Enum.flat_map(fn + %{should_aggregate?: true} = duty -> [{Map.get(set.validators, duty.validator_index), duty}] + _ -> [] + end) + end + defp proposer(set, epoch, slot), do: get_in(set.duties, [epoch, :proposers, slot]) defp attesters(set, epoch, slot), do: get_in(set.duties, [epoch, :attesters, slot]) || [] - defp maybe_debug_notify(fun, data) do - # :debug do - if Application.get_env(:logger, :level) == :info do - Logger.info("[Validator] Notifying all Validators with message: #{inspect(data)}") + # defp maybe_debug_notify(fun, data) do + # # :debug do + # if Application.get_env(:logger, :level) == :info do + # Logger.info("[Validator] Notifying all Validators with message: #{inspect(data)}") - start_time = System.monotonic_time(:millisecond) - result = fun.() - end_time = System.monotonic_time(:millisecond) + # start_time = System.monotonic_time(:millisecond) + # result = fun.() + # end_time = System.monotonic_time(:millisecond) - Logger.info( - "[Validator] #{inspect(data)} notified to all Validators after #{end_time - start_time} ms" - ) + # Logger.info( + # "[Validator] #{inspect(data)} notified to all Validators after #{end_time - start_time} ms" + # ) - result - else - fun.() - end - end + # result + # else + # fun.() + # end + # end @doc """ Get validator keystores from the keystore directory. From 5331a6323f3bed52396c2cc9f2be8c5c3a54307a Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 14 Aug 2024 17:11:30 -0300 Subject: [PATCH 30/42] Epoch 0 completely working from ValidatorSet --- .../validator/validator.ex | 174 +++++------------- .../validator/validator_set.ex | 85 +++------ 2 files changed, 76 insertions(+), 183 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 7c5875429..e4da48747 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -111,77 +111,46 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec handle_tick({Types.slot(), atom()}, t(), Types.root()) :: t() - def handle_tick(_logical_time, %{index: nil} = state, _root) do - log_error("-1", "setup validator", "index not present for handle tick") - state - end - - def handle_tick({slot, :first_third}, state, root) do - log_debug(state.index, "started first third", slot: slot) - # Here we may: - # 1. propose our blocks - # 2. (TODO) start collecting attestations for aggregation - maybe_propose(state, slot, root) - |> update_state(slot, root) - end - - def handle_tick({slot, :second_third}, state, root) do - log_debug(state.index, "started second third", slot: slot) - # Here we may: - # 1. send our attestation for an empty slot - # 2. start building a payload - state - |> maybe_attest(slot, root) - |> maybe_build_payload(slot + 1, root) - end - - def handle_tick({slot, :last_third}, state, _root) do - log_debug(state.index, "started last third", slot: slot) - # Here we may publish our attestation aggregate - maybe_publish_aggregate(state, slot) - end - ########################## ### Private Functions ########################## - @spec update_state(t(), Types.slot(), Types.root()) :: t() + # @spec update_state(t(), Types.slot(), Types.root()) :: t() - defp update_state(%{slot: slot, root: root} = state, slot, root), do: state + # defp update_state(%{slot: slot, root: root} = state, slot, root), do: state - # Epoch as part of the state now avoids recomputing the duties at every block - defp update_state(%{epoch: last_epoch} = state, slot, head_root) do - epoch = Misc.compute_epoch_at_slot(slot + 1) + # # Epoch as part of the state now avoids recomputing the duties at every block + # defp update_state(%{epoch: last_epoch} = state, slot, head_root) do + # epoch = Misc.compute_epoch_at_slot(slot + 1) - if last_epoch == epoch do - state - else - recompute_duties(state, last_epoch, epoch, slot, head_root) - end - end + # if last_epoch == epoch do + # state + # else + # recompute_duties(state, last_epoch, epoch, slot, head_root) + # end + # end - @spec recompute_duties(t(), Types.epoch(), Types.epoch(), Types.slot(), Types.root()) :: t() - defp recompute_duties(state, last_epoch, epoch, _slot, head_root) do - start_slot = Misc.compute_start_slot_at_epoch(epoch) + # @spec recompute_duties(t(), Types.epoch(), Types.epoch(), Types.slot(), Types.root()) :: t() + # defp recompute_duties(state, last_epoch, epoch, _slot, head_root) do + # start_slot = Misc.compute_start_slot_at_epoch(epoch) - # TODO: Why is this needed? something here seems wrong, why would i need to move to a different slot if - # I'm calculating this at a new epoch? need to check it - # target_root = if slot == start_slot, do: head_root, else: last_root + # # TODO: Why is this needed? something here seems wrong, why would i need to move to a different slot if + # # I'm calculating this at a new epoch? need to check it + # # target_root = if slot == start_slot, do: head_root, else: last_root - # Process the start of the new epoch - # new_beacon = fetch_target_state(epoch, target_root) |> go_to_slot(start_slot) - new_beacon = fetch_target_state(epoch, head_root) |> go_to_slot(start_slot) + # # Process the start of the new epoch + # # new_beacon = fetch_target_state(epoch, target_root) |> go_to_slot(start_slot) + # new_beacon = fetch_target_state(epoch, head_root) |> go_to_slot(start_slot) - new_duties = - Duties.shift_duties(state.duties, epoch, last_epoch) - |> Duties.maybe_update_duties(new_beacon, epoch, state.index, state.keystore.privkey) + # new_duties = + # Duties.shift_duties(state.duties, epoch, last_epoch) + # |> Duties.maybe_update_duties(new_beacon, epoch, state.index, state.keystore.privkey) - move_subnets(state.duties, new_duties) - Duties.log_duties(new_duties, state.index) + # move_subnets(state.duties, new_duties) + # Duties.log_duties(new_duties, state.index) - %{state | duties: new_duties, epoch: epoch} - end + # %{state | duties: new_duties, epoch: epoch} + # end @spec fetch_target_state(Types.epoch(), Types.root()) :: Types.BeaconState.t() defp fetch_target_state(epoch, root) do @@ -189,23 +158,23 @@ defmodule LambdaEthereumConsensus.Validator do state end + defp join_subnets_for_duties(%{attester: duties}) do + duties |> get_subnet_ids() |> join() + end + defp get_subnet_ids(duties), do: duties |> Stream.reject(&(&1 == :not_computed)) |> Enum.map(& &1.subnet_id) - defp move_subnets(%{attester: old_duties}, %{attester: new_duties}) do - old_subnets = old_duties |> get_subnet_ids() |> MapSet.new() - new_subnets = new_duties |> get_subnet_ids() |> MapSet.new() + # defp move_subnets(%{attester: old_duties}, %{attester: new_duties}) do + # old_subnets = old_duties |> get_subnet_ids() |> MapSet.new() + # new_subnets = new_duties |> get_subnet_ids() |> MapSet.new() - # leave old subnets (except for recurring ones) - MapSet.difference(old_subnets, new_subnets) |> leave() + # # leave old subnets (except for recurring ones) + # MapSet.difference(old_subnets, new_subnets) |> leave() - # join new subnets (except for recurring ones) - MapSet.difference(new_subnets, old_subnets) |> join() - end - - defp join_subnets_for_duties(%{attester: duties}) do - duties |> get_subnet_ids() |> join() - end + # # join new subnets (except for recurring ones) + # MapSet.difference(new_subnets, old_subnets) |> join() + # end defp join(subnets) do if not Enum.empty?(subnets) do @@ -214,28 +183,12 @@ defmodule LambdaEthereumConsensus.Validator do end end - defp leave(subnets) do - if not Enum.empty?(subnets) do - Logger.debug("Leaving subnets: #{Enum.join(subnets, ", ")}") - Enum.each(subnets, &Gossip.Attestation.leave/1) - end - end - - @spec maybe_attest(t(), Types.slot(), Types.root()) :: t() - defp maybe_attest(state, slot, head_root) do - case Duties.get_current_attester_duty(state.duties, slot) do - %{attested?: false} = duty -> - attest(state, duty, head_root) - - new_duties = - Duties.replace_attester_duty(state.duties, duty, %{duty | attested?: true}) - - %{state | duties: new_duties} - - _ -> - state - end - end + # defp leave(subnets) do + # if not Enum.empty?(subnets) do + # Logger.debug("Leaving subnets: #{Enum.join(subnets, ", ")}") + # Enum.each(subnets, &Gossip.Attestation.leave/1) + # end + # end @spec attest(t(), Duties.attester_duty(), Types.root()) :: :ok def attest(%{index: validator_index, keystore: keystore}, current_duty, head_root) do @@ -262,22 +215,7 @@ defmodule LambdaEthereumConsensus.Validator do end end - # We publish our aggregate on the next slot, and when we're an aggregator - defp maybe_publish_aggregate(%{index: validator_index, keystore: keystore} = state, slot) do - case Duties.get_current_attester_duty(state.duties, slot) do - %{should_aggregate?: true} = duty -> - publish_aggregate(duty, validator_index, keystore) - - new_duties = - Duties.replace_attester_duty(state.duties, duty, %{duty | should_aggregate?: false}) - - %{state | duties: new_duties} - - _ -> - state - end - end - + @spec publish_aggregate(Duties.attester_duty(), non_neg_integer(), Keystore.t()) :: :ok def publish_aggregate(duty, validator_index, keystore) do case Gossip.Attestation.stop_collecting(duty.subnet_id) do {:ok, attestations} -> @@ -384,17 +322,6 @@ defmodule LambdaEthereumConsensus.Validator do Enum.find_index(beacon.validators, &(&1.pubkey == pubkey)) end - defp proposer?(%{duties: %{proposer: slots}}, slot), do: Enum.member?(slots, slot) - - @spec maybe_build_payload(t(), Types.slot(), Types.root()) :: t() - defp maybe_build_payload(state, proposed_slot, head_root) do - if proposer?(state, proposed_slot) do - start_payload_builder(state, proposed_slot, head_root) - else - state - end - end - @spec start_payload_builder(t(), Types.slot(), Types.root()) :: t() def start_payload_builder(%{payload_builder: {slot, root, _}} = state, slot, root), do: state @@ -417,14 +344,7 @@ defmodule LambdaEthereumConsensus.Validator do end end - defp maybe_propose(state, slot, root) do - if proposer?(state, slot) do - propose(state, slot, root) - else - state - end - end - + @spec propose(t(), Types.slot(), Types.root()) :: t() def propose( %{ index: validator_index, diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index accb3d8fd..efab246ec 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -88,23 +88,9 @@ defmodule LambdaEthereumConsensus.ValidatorSet do def process_tick(%{head_root: head_root} = set, epoch, {slot, :last_third}) do set |> update_state(epoch, head_root) - |> publish_aggregate(epoch, slot, head_root) + |> publish_aggregate(epoch, slot) end - # def process_tick(%{validators: validators, head_root: head_root} = set, _epoch, slot_data) do - # validators = - # maybe_debug_notify( - # fn -> - # Map.new(validators, fn {k, v} -> - # {k, Validator.handle_tick(slot_data, v, head_root)} - # end) - # end, - # {:on_tick, slot_data} - # ) - - # %{set | validators: validators} - # end - ############################## # Setup @@ -190,32 +176,36 @@ defmodule LambdaEthereumConsensus.ValidatorSet do ############################## # Attestation and proposal - defp attest(set, epoch, slot, root) do - updated_duties = - set - |> current_attesters(epoch, slot) - |> Enum.map(fn {validator, duty} -> - Validator.attest(validator, duty, root) + defp attest(set, epoch, slot, head_root) do + case current_attesters(set, epoch, slot) do + [] -> + set - # Duty.attested(duty) - %{duty | attested?: true} - end) + attesters -> + Enum.map(attesters, fn {validator, duty} -> + Validator.attest(validator, duty, head_root) - %{set | duties: put_in(set.duties, [epoch, :attesters, slot], updated_duties)} + # Duty.attested(duty) + %{duty | attested?: true} + end) + |> then(&%{set | duties: put_in(set.duties, [epoch, :attesters, slot], &1)}) + end end - defp publish_aggregate(set, epoch, slot, head_root) do - updated_duties = - set - |> current_aggregators(epoch, slot) - |> Enum.map(fn {validator, duty} -> - Validator.publish_aggregate(duty, validator.index, validator.keystore) + defp publish_aggregate(set, epoch, slot) do + case current_aggregators(set, epoch, slot) do + [] -> + set - # Duty.aggregated(duty) - %{duty | should_aggregate?: false} - end) + aggregators -> + Enum.map(aggregators, fn {validator, duty} -> + Validator.publish_aggregate(duty, validator.index, validator.keystore) - %{set | duties: put_in(set.duties, [epoch, :attesters, slot], updated_duties)} + # Duty.aggregated(duty) + %{duty | should_aggregate?: false} + end) + |> then(&%{set | duties: put_in(set.duties, [epoch, :attesters, slot], &1)}) + end end defp build_next_payload(%{validators: validators} = set, epoch, slot, head_root) do @@ -250,7 +240,8 @@ defmodule LambdaEthereumConsensus.ValidatorSet do # Helpers defp current_attesters(set, epoch, slot) do - attesters(set, epoch, slot) + set + |> attesters(epoch, slot) |> Enum.flat_map(fn %{attested?: false} = duty -> [{Map.get(set.validators, duty.validator_index), duty}] _ -> [] @@ -258,7 +249,8 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end defp current_aggregators(set, epoch, slot) do - attesters(set, epoch, slot) + set + |> attesters(epoch, slot) |> Enum.flat_map(fn %{should_aggregate?: true} = duty -> [{Map.get(set.validators, duty.validator_index), duty}] _ -> [] @@ -268,25 +260,6 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp proposer(set, epoch, slot), do: get_in(set.duties, [epoch, :proposers, slot]) defp attesters(set, epoch, slot), do: get_in(set.duties, [epoch, :attesters, slot]) || [] - # defp maybe_debug_notify(fun, data) do - # # :debug do - # if Application.get_env(:logger, :level) == :info do - # Logger.info("[Validator] Notifying all Validators with message: #{inspect(data)}") - - # start_time = System.monotonic_time(:millisecond) - # result = fun.() - # end_time = System.monotonic_time(:millisecond) - - # Logger.info( - # "[Validator] #{inspect(data)} notified to all Validators after #{end_time - start_time} ms" - # ) - - # result - # else - # fun.() - # end - # end - @doc """ Get validator keystores from the keystore directory. This function expects two files for each validator: From c030f1bef695c96cd3a3e311ebc79b61141043bb Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 15 Aug 2024 11:10:15 -0300 Subject: [PATCH 31/42] ValidatorSet and Validator cleanup --- .../validator/validator.ex | 170 ++++-------------- .../validator/validator_set.ex | 48 ++--- 2 files changed, 48 insertions(+), 170 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index e4da48747..fb88caee7 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -5,8 +5,6 @@ defmodule LambdaEthereumConsensus.Validator do require Logger defstruct [ - :epoch, - :duties, :index, :keystore, :payload_builder @@ -33,162 +31,64 @@ defmodule LambdaEthereumConsensus.Validator do # TODO: Slot and Root are redundant, we should also have the duties separated and calculated # just at the begining of every epoch, and then just update them as needed. @type t :: %__MODULE__{ - epoch: Types.epoch(), - duties: Duties.duties(), index: non_neg_integer() | nil, keystore: Keystore.t(), payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil } - @spec new( - Keystore.t(), - Types.slot(), - Types.root() - ) :: t() + @spec new(Keystore.t(), Types.slot(), Types.root()) :: t() def new(keystore, head_slot, head_root) do epoch = Misc.compute_epoch_at_slot(head_slot) - beacon = fetch_target_state(epoch, head_root) |> go_to_slot(head_slot) + beacon = fetch_target_state_and_go_to_slot(epoch, head_slot, head_root) - new(keystore, epoch, head_slot, head_root, beacon) + new(keystore, beacon) end - @spec new( - Keystore.t(), - Types.epoch(), - Types.slot(), - Types.root(), - Types.BeaconState.t() - ) :: t() - def new(keystore, epoch, head_slot, head_root, beacon) do + @spec new(Keystore.t(), Types.BeaconState.t()) :: t() + def new(keystore, beacon) do state = %__MODULE__{ - epoch: epoch, - duties: Duties.empty_duties(), index: nil, keystore: keystore, payload_builder: nil } - case try_setup_validator(state, epoch, head_slot, head_root, beacon) do - nil -> - # TODO: Previously this was handled by the validator continously trying to setup itself, - # but now that they are processed syncronously, we should handle this case different. - # Right now it's just omitted and logged. - Logger.error("[Validator] Public key not found in the validator set") - state - - new_state -> - new_state - end - end - - @spec try_setup_validator( - t(), - Types.epoch(), - Types.slot(), - Types.root(), - Types.BeaconState.t() - ) :: t() | nil - defp try_setup_validator(state, epoch, slot, root, beacon) do case fetch_validator_index(beacon, state.keystore.pubkey) do nil -> - nil + Logger.warning( + "[Validator] Public key #{state.keystore.pubkey} not found in the validator set" + ) + + state validator_index -> - log_info(validator_index, "setup validator", slot: slot, root: root) - - duties = - Duties.maybe_update_duties( - state.duties, - beacon, - epoch, - validator_index, - state.keystore.privkey - ) - - join_subnets_for_duties(duties) - Duties.log_duties(duties, validator_index) - %{state | duties: duties, index: validator_index} + log_debug(validator_index, "Setup completed") + %{state | index: validator_index} end end ########################## - ### Private Functions - ########################## + # Target State - # @spec update_state(t(), Types.slot(), Types.root()) :: t() - - # defp update_state(%{slot: slot, root: root} = state, slot, root), do: state - - # # Epoch as part of the state now avoids recomputing the duties at every block - # defp update_state(%{epoch: last_epoch} = state, slot, head_root) do - # epoch = Misc.compute_epoch_at_slot(slot + 1) - - # if last_epoch == epoch do - # state - # else - # recompute_duties(state, last_epoch, epoch, slot, head_root) - # end - # end - - # @spec recompute_duties(t(), Types.epoch(), Types.epoch(), Types.slot(), Types.root()) :: t() - # defp recompute_duties(state, last_epoch, epoch, _slot, head_root) do - # start_slot = Misc.compute_start_slot_at_epoch(epoch) - - # # TODO: Why is this needed? something here seems wrong, why would i need to move to a different slot if - # # I'm calculating this at a new epoch? need to check it - # # target_root = if slot == start_slot, do: head_root, else: last_root - - # # Process the start of the new epoch - # # new_beacon = fetch_target_state(epoch, target_root) |> go_to_slot(start_slot) - # new_beacon = fetch_target_state(epoch, head_root) |> go_to_slot(start_slot) - - # new_duties = - # Duties.shift_duties(state.duties, epoch, last_epoch) - # |> Duties.maybe_update_duties(new_beacon, epoch, state.index, state.keystore.privkey) - - # move_subnets(state.duties, new_duties) - # Duties.log_duties(new_duties, state.index) - - # %{state | duties: new_duties, epoch: epoch} - # end + @spec fetch_target_state_and_go_to_slot(Types.epoch(), Types.slot(), Types.root()) :: + Types.BeaconState.t() + def fetch_target_state_and_go_to_slot(epoch, slot, root) do + epoch |> fetch_target_state(root) |> go_to_slot(slot) + end - @spec fetch_target_state(Types.epoch(), Types.root()) :: Types.BeaconState.t() defp fetch_target_state(epoch, root) do {:ok, state} = CheckpointStates.compute_target_checkpoint_state(epoch, root) state end - defp join_subnets_for_duties(%{attester: duties}) do - duties |> get_subnet_ids() |> join() - end - - defp get_subnet_ids(duties), - do: duties |> Stream.reject(&(&1 == :not_computed)) |> Enum.map(& &1.subnet_id) - - # defp move_subnets(%{attester: old_duties}, %{attester: new_duties}) do - # old_subnets = old_duties |> get_subnet_ids() |> MapSet.new() - # new_subnets = new_duties |> get_subnet_ids() |> MapSet.new() - - # # leave old subnets (except for recurring ones) - # MapSet.difference(old_subnets, new_subnets) |> leave() - - # # join new subnets (except for recurring ones) - # MapSet.difference(new_subnets, old_subnets) |> join() - # end + defp go_to_slot(%{slot: old_slot} = state, slot) when old_slot == slot, do: state - defp join(subnets) do - if not Enum.empty?(subnets) do - Logger.debug("Joining subnets: #{Enum.join(subnets, ", ")}") - Enum.each(subnets, &Gossip.Attestation.join/1) - end + defp go_to_slot(%{slot: old_slot} = state, slot) when old_slot < slot do + {:ok, st} = StateTransition.process_slots(state, slot) + st end - # defp leave(subnets) do - # if not Enum.empty?(subnets) do - # Logger.debug("Leaving subnets: #{Enum.join(subnets, ", ")}") - # Enum.each(subnets, &Gossip.Attestation.leave/1) - # end - # end + ########################## + # Attestations @spec attest(t(), Duties.attester_duty(), Types.root()) :: :ok def attest(%{index: validator_index, keystore: keystore}, current_duty, head_root) do @@ -202,7 +102,7 @@ defmodule LambdaEthereumConsensus.Validator do debug_log_msg = "publishing attestation on committee index: #{current_duty.committee_index} | as #{current_duty.index_in_committee}/#{current_duty.committee_length - 1} and pubkey: #{LambdaEthereumConsensus.Utils.format_shorten_binary(keystore.pubkey)}" - log_debug(validator_index, debug_log_msg, log_md) + log_info(validator_index, debug_log_msg, log_md) Gossip.Attestation.publish(subnet_id, attestation) |> log_info_result(validator_index, "published attestation", log_md) @@ -305,23 +205,15 @@ defmodule LambdaEthereumConsensus.Validator do } end - defp go_to_slot(%{slot: old_slot} = state, slot) when old_slot == slot, do: state - - defp go_to_slot(%{slot: old_slot} = state, slot) when old_slot < slot do - {:ok, st} = StateTransition.process_slots(state, slot) - st - end - - defp go_to_slot(%{latest_block_header: %{parent_root: parent_root}}, slot) do - BlockStates.get_state_info!(parent_root).beacon_state |> go_to_slot(slot) - end - @spec fetch_validator_index(Types.BeaconState.t(), Bls.pubkey()) :: non_neg_integer() | nil defp fetch_validator_index(beacon, pubkey) do Enum.find_index(beacon.validators, &(&1.pubkey == pubkey)) end + ################################ + # Payload building and proposing + @spec start_payload_builder(t(), Types.slot(), Types.root()) :: t() def start_payload_builder(%{payload_builder: {slot, root, _}} = state, slot, root), do: state @@ -380,7 +272,6 @@ defmodule LambdaEthereumConsensus.Validator do %{state | payload_builder: nil} end - # TODO: at least in kurtosis there are blocks that are proposed without a payload apparently, must investigate. def propose(%{payload_builder: nil} = state, _proposed_slot, _head_root) do log_error(state.index, "propose block", "lack of execution payload") state @@ -426,7 +317,8 @@ defmodule LambdaEthereumConsensus.Validator do rem(blob_index, ChainSpec.get("BLOB_SIDECAR_SUBNET_COUNT")) end - # Some Log Helpers to avoid repetition + ################################ + # Log Helpers defp log_info_result(result, index, message, metadata), do: log_result(result, :info, index, message, metadata) @@ -440,7 +332,7 @@ defmodule LambdaEthereumConsensus.Validator do defp log_info(index, message, metadata \\ []), do: Logger.info("[Validator] #{index} #{message}", metadata) - defp log_debug(index, message, metadata), + defp log_debug(index, message, metadata \\ []), do: Logger.debug("[Validator] #{index} #{message}", metadata) defp log_error(index, message, reason, metadata \\ []), diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index efab246ec..0183a0455 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -10,7 +10,6 @@ defmodule LambdaEthereumConsensus.ValidatorSet do require Logger alias LambdaEthereumConsensus.StateTransition.Misc - alias LambdaEthereumConsensus.Store.CheckpointStates alias LambdaEthereumConsensus.Validator alias LambdaEthereumConsensus.Validator.Duties @@ -48,7 +47,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do epoch = Misc.compute_epoch_at_slot(slot) set - |> update_state(epoch, head_root) + |> update_state(epoch, slot, head_root) |> attest(epoch, slot, head_root) |> build_next_payload(epoch, slot, head_root) end @@ -66,30 +65,22 @@ defmodule LambdaEthereumConsensus.ValidatorSet do epoch = Misc.compute_epoch_at_slot(slot) - process_tick(set, epoch, slot_data) - end - - @spec process_tick(t(), Types.epoch(), tuple()) :: t() - def process_tick(%{head_root: head_root} = set, epoch, {slot, :first_third}) do set - |> update_state(epoch, head_root) - |> propose(epoch, slot, head_root) + |> update_state(epoch, slot, head_root) + |> process_tick(epoch, slot_data) end - @spec process_tick(t(), Types.epoch(), tuple()) :: t() - def process_tick(%{head_root: head_root} = set, epoch, {slot, :second_third}) do + defp process_tick(%{head_root: head_root} = set, epoch, {slot, :first_third}), + do: propose(set, epoch, slot, head_root) + + defp process_tick(%{head_root: head_root} = set, epoch, {slot, :second_third}) do set - |> update_state(epoch, head_root) |> attest(epoch, slot, head_root) |> build_next_payload(epoch, slot, head_root) end - @spec process_tick(t(), Types.epoch(), tuple()) :: t() - def process_tick(%{head_root: head_root} = set, epoch, {slot, :last_third}) do - set - |> update_state(epoch, head_root) - |> publish_aggregate(epoch, slot) - end + defp process_tick(set, epoch, {slot, :last_third}), + do: publish_aggregate(set, epoch, slot) ############################## # Setup @@ -108,48 +99,43 @@ defmodule LambdaEthereumConsensus.ValidatorSet do epoch = Misc.compute_epoch_at_slot(slot) # This will be removed later when refactoring Validator new - beacon = fetch_target_beaconstate!(epoch, head_root) + beacon = Validator.fetch_target_state_and_go_to_slot(epoch, slot, head_root) validators = Map.new(validator_keystores, fn keystore -> - validator = Validator.new(keystore, epoch, slot, head_root, beacon) + validator = Validator.new(keystore, beacon) {validator.index, validator} end) Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") %__MODULE__{validators: validators} - |> update_state(epoch, head_root) + |> update_state(epoch, slot, head_root) end ############################## # State update - defp update_state(set, epoch, head_root) do + defp update_state(set, epoch, slot, head_root) do set |> update_head(head_root) - |> compute_duties(epoch, head_root) + |> compute_duties(epoch, slot, head_root) end defp update_head(%{head_root: head_root} = set, head_root), do: set defp update_head(set, head_root), do: %{set | head_root: head_root} - defp compute_duties(set, epoch, _head_root) + defp compute_duties(set, epoch, _slot, _head_root) when not is_nil(:erlang.map_get(epoch, set.duties)), do: set - defp compute_duties(set, epoch, head_root) do + defp compute_duties(set, epoch, slot, head_root) do epoch - |> fetch_target_beaconstate!(head_root) + |> Validator.fetch_target_state_and_go_to_slot(slot, head_root) |> compute_duties_for_epoch!(epoch, set.validators) |> merge_duties_and_prune(epoch, set) end - defp fetch_target_beaconstate!(epoch, head_root) do - {:ok, beaconstate} = CheckpointStates.compute_target_checkpoint_state(epoch, head_root) - beaconstate - end - defp compute_duties_for_epoch!(beacon, epoch, validators) do {:ok, proposers} = Duties.compute_proposers_for_epoch(beacon, epoch, validators) {:ok, attesters} = Duties.compute_attesters_for_epoch(beacon, epoch, validators) From 8c1d818601ea55a312fea83ac332cba500b58537 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 15 Aug 2024 11:46:01 -0300 Subject: [PATCH 32/42] Initial Duties cleanup --- .../validator/duties.ex | 237 ++---------------- .../validator/validator.ex | 8 +- .../validator/validator_set.ex | 11 +- 3 files changed, 31 insertions(+), 225 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 19472c529..579b51663 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -6,6 +6,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.Validator alias LambdaEthereumConsensus.Validator.Utils + alias LambdaEthereumConsensus.ValidatorSet alias Types.BeaconState require Logger @@ -22,26 +23,16 @@ defmodule LambdaEthereumConsensus.Validator.Duties do committee_length: Types.uint64(), index_in_committee: Types.uint64() } - @type proposer_duty :: Types.slot() - @type attester_duties :: list(:not_computed | attester_duty()) - @type proposer_duties :: :not_computed | list(Types.slot()) + @type proposer_duty :: Types.slot() - @type duties :: %{ - attester: attester_duties(), - proposer: proposer_duties() - } + @type attester_duties :: %{Types.slot() => [attester_duty()]} + @type proposer_duties :: %{Types.slot() => [proposer_duty()]} - @spec empty_duties() :: duties() - def empty_duties() do - %{ - # Order is: previous epoch, current epoch, next epoch - attester: [:not_computed, :not_computed, :not_computed], - proposer: :not_computed - } - end + @type duties :: %{attesters: attester_duties(), proposers: proposer_duties()} - @spec compute_proposers_for_epoch(BeaconState.t(), Types.epoch(), %{}) :: any() + @spec compute_proposers_for_epoch(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) :: + proposer_duties() def compute_proposers_for_epoch(%BeaconState{} = state, epoch, validators) do with {:ok, epoch} <- check_valid_epoch(state, epoch), {start_slot, end_slot} <- boundary_slots(epoch) do @@ -53,11 +44,12 @@ defmodule LambdaEthereumConsensus.Validator.Duties do do: [{slot, proposer_index}], else: [] end) - |> then(&{:ok, Map.new(&1)}) + |> Map.new() end end - @spec compute_attesters_for_epoch(BeaconState.t(), Types.epoch(), %{}) :: any() + @spec compute_attesters_for_epoch(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) :: + attester_duties() def compute_attesters_for_epoch(%BeaconState{} = state, epoch, validators) do with {:ok, epoch} <- check_valid_epoch(state, epoch), {start_slot, end_slot} <- boundary_slots(epoch) do @@ -66,20 +58,20 @@ defmodule LambdaEthereumConsensus.Validator.Duties do start_slot..end_slot |> Enum.flat_map(fn slot -> 0..(committee_count_per_slot - 1) - |> Enum.flat_map(&compute_attester_dutys(state, epoch, slot, validators, &1)) + |> Enum.flat_map(&compute_attester_duties(state, epoch, slot, validators, &1)) end) - |> then(&{:ok, Map.new(&1)}) + |> Map.new() end end - @spec compute_attester_dutys( + @spec compute_attester_duties( state :: BeaconState.t(), epoch :: Types.epoch(), slot :: Types.slot(), validators :: %{Types.validator_index() => Validator.t()}, committee_index :: Types.uint64() ) :: [{Types.slot(), attester_duty()}] - defp compute_attester_dutys(state, epoch, slot, validators, committee_index) do + defp compute_attester_duties(state, epoch, slot, validators, committee_index) do case Accessors.get_beacon_committee(state, slot, committee_index) do {:ok, committee} -> compute_cometee_duties(state, epoch, slot, committee, committee_index, validators) @@ -119,150 +111,6 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end end - defp check_valid_epoch(state, epoch) do - next_epoch = Accessors.get_current_epoch(state) + 1 - - if epoch > next_epoch do - {:error, "epoch must be <= next_epoch"} - else - {:ok, epoch} - end - end - - defp boundary_slots(epoch) do - start_slot = Misc.compute_start_slot_at_epoch(epoch) - end_slot = start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - 1 - - {start_slot, end_slot} - end - - @spec get_current_attester_duty(duties :: duties(), current_slot :: Types.slot()) :: - attester_duty() - def get_current_attester_duty(%{attester: attester_duties}, current_slot) do - Enum.find(attester_duties, fn - :not_computed -> false - duty -> duty.slot == current_slot - end) - end - - @spec replace_attester_duty( - duties :: duties(), - duty :: attester_duty(), - new_duty :: attester_duty() - ) :: duties() - def replace_attester_duty(duties, duty, new_duty) do - attester_duties = - Enum.map(duties.attester, fn - ^duty -> new_duty - d -> d - end) - - %{duties | attester: attester_duties} - end - - @spec log_duties(duties :: duties(), validator_index :: Types.validator_index()) :: :ok - def log_duties(%{attester: attester_duties, proposer: proposer_duties}, validator_index) do - attester_duties - # Drop the first element, which is the previous epoch's duty - |> Stream.drop(1) - |> Enum.each(fn %{ - index_in_committee: i, - committee_index: ci, - slot: slot, - should_aggregate?: sa - } -> - Logger.info( - "[Validator] #{validator_index} has to attest in committee #{ci} of slot #{slot} with index #{i}, and should_aggregate?: #{sa}" - ) - end) - - Enum.each(proposer_duties, fn slot -> - Logger.info("[Validator] #{validator_index} has to propose a block in slot #{slot}!") - end) - end - - @spec compute_proposer_duties( - beacon_state :: BeaconState.t(), - epoch :: Types.epoch(), - validator_index :: Types.validator_index() - ) :: proposer_duties() - # TODO: Remove, already moved to an epoch-based approach - def compute_proposer_duties(beacon_state, epoch, validator_index) do - start_slot = Misc.compute_start_slot_at_epoch(epoch) - - start_slot..(start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - 1) - |> Enum.flat_map(fn slot -> - # Can't fail - {:ok, proposer_index} = Accessors.get_beacon_proposer_index(beacon_state, slot) - if proposer_index == validator_index, do: [slot], else: [] - end) - end - - def maybe_update_duties(duties, beacon_state, epoch, validator_index, privkey) do - attester_duties = - maybe_update_attester_duties(duties.attester, beacon_state, epoch, validator_index, privkey) - - proposer_duties = compute_proposer_duties(beacon_state, epoch, validator_index) - # To avoid edge-cases - old_duty = - case duties.proposer do - :not_computed -> [] - old -> old |> Enum.reverse() |> Enum.take(1) - end - - %{duties | attester: attester_duties, proposer: old_duty ++ proposer_duties} - end - - defp maybe_update_attester_duties( - [epp, ep0, ep1], - beacon_state, - epoch, - validator_index, - privkey - ) do - duties = - Stream.with_index([ep0, ep1]) - |> Enum.map(fn - {:not_computed, i} -> - compute_attester_duties(beacon_state, epoch + i, validator_index, privkey) - - {d, _} -> - d - end) - - [epp | duties] - end - - def shift_duties(%{attester: [_ep0, ep1, ep2]} = duties, epoch, current_epoch) do - case current_epoch - epoch do - 1 -> %{duties | attester: [ep1, ep2, :not_computed]} - 2 -> %{duties | attester: [ep2, :not_computed, :not_computed]} - _ -> %{duties | attester: [:not_computed, :not_computed, :not_computed]} - end - end - - @spec compute_attester_duties( - beacon_state :: BeaconState.t(), - epoch :: Types.epoch(), - validator_index :: non_neg_integer(), - privkey :: Bls.privkey() - ) :: attester_duty() | nil - defp compute_attester_duties(beacon_state, epoch, validator_index, privkey) do - # Can't fail - {:ok, duty} = get_committee_assignment(beacon_state, epoch, validator_index) - - case duty do - nil -> - nil - - duty -> - duty - |> Map.put(:attested?, false) - |> update_with_aggregation_duty(beacon_state, privkey) - |> update_with_subnet_id(beacon_state, epoch) - end - end - defp update_with_aggregation_duty(duty, beacon_state, privkey) do proof = Utils.get_slot_signature(beacon_state, duty.slot, privkey) @@ -287,62 +135,23 @@ defmodule LambdaEthereumConsensus.Validator.Duties do Map.put(duty, :subnet_id, subnet_id) end - @doc """ - TODO: This is not the case anymore? - Return the committee assignment in the ``epoch`` for ``validator_index``. - ``assignment`` returned is a tuple of the following form: - * ``assignment[0]`` is the index of the validator in the committee - * ``assignment[1]`` is the index to which the committee is assigned - * ``assignment[2]`` is the slot at which the committee is assigned - Return `nil` if no assignment. - """ - @spec get_committee_assignment(BeaconState.t(), Types.epoch(), Types.validator_index()) :: - {:ok, nil | attester_duty()} | {:error, String.t()} - def get_committee_assignment(%BeaconState{} = state, epoch, validator_index) do + ############################ + # Helpers + + defp check_valid_epoch(state, epoch) do next_epoch = Accessors.get_current_epoch(state) + 1 if epoch > next_epoch do {:error, "epoch must be <= next_epoch"} else - start_slot = Misc.compute_start_slot_at_epoch(epoch) - committee_count_per_slot = Accessors.get_committee_count_per_slot(state, epoch) - end_slot = start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - - start_slot..end_slot - |> Stream.map(fn slot -> - 0..(committee_count_per_slot - 1) - |> Stream.map(&compute_attester_duty(state, slot, validator_index, &1)) - |> Enum.find(&(not is_nil(&1))) - end) - |> Enum.find(&(not is_nil(&1))) - |> then(&{:ok, &1}) + {:ok, epoch} end end - @spec compute_attester_duty( - state :: BeaconState.t(), - slot :: Types.slot(), - validator_index :: Types.validator_index(), - committee_index :: Types.uint64() - ) :: attester_duty() | nil - defp compute_attester_duty(state, slot, validator_index, committee_index) do - case Accessors.get_beacon_committee(state, slot, committee_index) do - {:ok, committee} -> - case Enum.find_index(committee, &(&1 == validator_index)) do - nil -> - nil - - index -> - %{ - index_in_committee: index, - committee_length: length(committee), - committee_index: committee_index, - slot: slot - } - end + defp boundary_slots(epoch) do + start_slot = Misc.compute_start_slot_at_epoch(epoch) + end_slot = start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - 1 - {:error, _} -> - nil - end + {start_slot, end_slot} end end diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index fb88caee7..5836ef3fa 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -28,10 +28,10 @@ defmodule LambdaEthereumConsensus.Validator do @default_graffiti_message "Lambda, so gentle, so good" - # TODO: Slot and Root are redundant, we should also have the duties separated and calculated - # just at the begining of every epoch, and then just update them as needed. + @type index :: non_neg_integer() + @type t :: %__MODULE__{ - index: non_neg_integer() | nil, + index: index() | nil, keystore: Keystore.t(), payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil } @@ -102,7 +102,7 @@ defmodule LambdaEthereumConsensus.Validator do debug_log_msg = "publishing attestation on committee index: #{current_duty.committee_index} | as #{current_duty.index_in_committee}/#{current_duty.committee_length - 1} and pubkey: #{LambdaEthereumConsensus.Utils.format_shorten_binary(keystore.pubkey)}" - log_info(validator_index, debug_log_msg, log_md) + log_debug(validator_index, debug_log_msg, log_md) Gossip.Attestation.publish(subnet_id, attestation) |> log_info_result(validator_index, "published attestation", log_md) diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 0183a0455..327853644 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -13,7 +13,8 @@ defmodule LambdaEthereumConsensus.ValidatorSet do alias LambdaEthereumConsensus.Validator alias LambdaEthereumConsensus.Validator.Duties - @type validators :: %{atom() => %{} | []} + @type validators :: %{Validator.index() => Validator.t()} + @type t :: %__MODULE__{ head_root: Types.root() | nil, duties: %{ @@ -137,17 +138,13 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end defp compute_duties_for_epoch!(beacon, epoch, validators) do - {:ok, proposers} = Duties.compute_proposers_for_epoch(beacon, epoch, validators) - {:ok, attesters} = Duties.compute_attesters_for_epoch(beacon, epoch, validators) + proposers = Duties.compute_proposers_for_epoch(beacon, epoch, validators) + attesters = Duties.compute_attesters_for_epoch(beacon, epoch, validators) Logger.info( "[Validator] Proposer duties for epoch #{epoch} are: #{inspect(proposers, pretty: true)}" ) - Logger.info( - "[Validator] Attester duties for epoch #{epoch} are: #{inspect(attesters, pretty: true)}" - ) - %{epoch => %{proposers: proposers, attesters: attesters}} end From e17a224312fc07a44a90e2d147a2a8c697fe07ec Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 15 Aug 2024 15:01:42 -0300 Subject: [PATCH 33/42] Further clean duties --- .../validator/duties.ex | 74 ++++++------------- .../validator/validator_set.ex | 4 + 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 579b51663..9304cb09d 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -4,7 +4,6 @@ defmodule LambdaEthereumConsensus.Validator.Duties do """ alias LambdaEthereumConsensus.StateTransition.Accessors alias LambdaEthereumConsensus.StateTransition.Misc - alias LambdaEthereumConsensus.Validator alias LambdaEthereumConsensus.Validator.Utils alias LambdaEthereumConsensus.ValidatorSet alias Types.BeaconState @@ -36,15 +35,12 @@ defmodule LambdaEthereumConsensus.Validator.Duties do def compute_proposers_for_epoch(%BeaconState{} = state, epoch, validators) do with {:ok, epoch} <- check_valid_epoch(state, epoch), {start_slot, end_slot} <- boundary_slots(epoch) do - start_slot..end_slot - |> Enum.flat_map(fn slot -> - {:ok, proposer_index} = Accessors.get_beacon_proposer_index(state, slot) - - if Map.has_key?(validators, proposer_index), - do: [{slot, proposer_index}], - else: [] - end) - |> Map.new() + for slot <- start_slot..end_slot, + {:ok, proposer_index} = Accessors.get_beacon_proposer_index(state, slot), + Map.has_key?(validators, proposer_index), + into: %{} do + {slot, proposer_index} + end end end @@ -58,59 +54,37 @@ defmodule LambdaEthereumConsensus.Validator.Duties do start_slot..end_slot |> Enum.flat_map(fn slot -> 0..(committee_count_per_slot - 1) - |> Enum.flat_map(&compute_attester_duties(state, epoch, slot, validators, &1)) + |> Enum.flat_map(&compute_duties_per_committee(state, epoch, slot, validators, &1)) + |> Enum.map(&{slot, &1}) end) |> Map.new() end end - @spec compute_attester_duties( - state :: BeaconState.t(), - epoch :: Types.epoch(), - slot :: Types.slot(), - validators :: %{Types.validator_index() => Validator.t()}, - committee_index :: Types.uint64() - ) :: [{Types.slot(), attester_duty()}] - defp compute_attester_duties(state, epoch, slot, validators, committee_index) do + defp compute_duties_per_committee(state, epoch, slot, validators, committee_index) do case Accessors.get_beacon_committee(state, slot, committee_index) do {:ok, committee} -> - compute_cometee_duties(state, epoch, slot, committee, committee_index, validators) + for {validator_index, index_in_committee} <- Enum.with_index(committee), + validator = Map.get(validators, validator_index), + duty = + %{ + slot: slot, + validator_index: validator_index, + index_in_committee: index_in_committee, + committee_length: length(committee), + committee_index: committee_index, + attested?: false + } + |> update_with_aggregation_duty(state, validator.keystore.privkey) + |> update_with_subnet_id(state, epoch) do + duty + end {:error, _} -> [] end end - defp compute_cometee_duties(state, epoch, slot, committee, committee_index, validators) do - committee - |> Stream.with_index() - |> Stream.flat_map(fn {validator_index, index_in_committee} -> - case Map.get(validators, validator_index) do - nil -> - [] - - validator -> - [ - %{ - slot: slot, - validator_index: validator_index, - index_in_committee: index_in_committee, - committee_length: length(committee), - committee_index: committee_index, - attested?: false - } - |> update_with_aggregation_duty(state, validator.keystore.privkey) - |> update_with_subnet_id(state, epoch) - ] - end - end) - |> Enum.into([]) - |> case do - [] -> [] - duties -> [{slot, duties}] - end - end - defp update_with_aggregation_duty(duty, beacon_state, privkey) do proof = Utils.get_slot_signature(beacon_state, duty.slot, privkey) diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 327853644..3336c18e3 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -145,6 +145,10 @@ defmodule LambdaEthereumConsensus.ValidatorSet do "[Validator] Proposer duties for epoch #{epoch} are: #{inspect(proposers, pretty: true)}" ) + Logger.info( + "[Validator] Attester duties for epoch #{epoch} are: #{inspect(attesters, pretty: true)}" + ) + %{epoch => %{proposers: proposers, attesters: attesters}} end From 0987fc22d227f950b54418768094a6201d4384cc Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 15 Aug 2024 15:05:46 -0300 Subject: [PATCH 34/42] Quick fix regarding attesters calculation --- lib/lambda_ethereum_consensus/validator/duties.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 9304cb09d..b3f95e851 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -55,7 +55,10 @@ defmodule LambdaEthereumConsensus.Validator.Duties do |> Enum.flat_map(fn slot -> 0..(committee_count_per_slot - 1) |> Enum.flat_map(&compute_duties_per_committee(state, epoch, slot, validators, &1)) - |> Enum.map(&{slot, &1}) + |> case do + [] -> [] + duties -> [{slot, duties}] + end end) |> Map.new() end From 18f05647635346162022eb8aba58be940a5bd284 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 15 Aug 2024 17:35:07 -0300 Subject: [PATCH 35/42] Simplified comute_attesters_for_epoch and removed slot from duties --- .../validator/duties.ex | 33 ++++++++----------- .../validator/validator.ex | 22 ++++++------- .../validator/validator_set.ex | 4 +-- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index b3f95e851..6fe4bc07f 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -16,7 +16,6 @@ defmodule LambdaEthereumConsensus.Validator.Duties do selection_proof: Bls.signature(), signing_domain: Types.domain(), subnet_id: Types.uint64(), - slot: Types.slot(), validator_index: Types.validator_index(), committee_index: Types.uint64(), committee_length: Types.uint64(), @@ -51,16 +50,13 @@ defmodule LambdaEthereumConsensus.Validator.Duties do {start_slot, end_slot} <- boundary_slots(epoch) do committee_count_per_slot = Accessors.get_committee_count_per_slot(state, epoch) - start_slot..end_slot - |> Enum.flat_map(fn slot -> - 0..(committee_count_per_slot - 1) - |> Enum.flat_map(&compute_duties_per_committee(state, epoch, slot, validators, &1)) - |> case do - [] -> [] - duties -> [{slot, duties}] - end - end) - |> Map.new() + for slot <- start_slot..end_slot, + committee_i <- 0..(committee_count_per_slot - 1), + reduce: %{} do + acc -> + new_duties = compute_duties_per_committee(state, epoch, slot, validators, committee_i) + Map.update(acc, slot, new_duties, &(new_duties ++ &1)) + end end end @@ -71,15 +67,14 @@ defmodule LambdaEthereumConsensus.Validator.Duties do validator = Map.get(validators, validator_index), duty = %{ - slot: slot, validator_index: validator_index, index_in_committee: index_in_committee, committee_length: length(committee), committee_index: committee_index, attested?: false } - |> update_with_aggregation_duty(state, validator.keystore.privkey) - |> update_with_subnet_id(state, epoch) do + |> update_with_aggregation_duty(state, slot, validator.keystore.privkey) + |> update_with_subnet_id(state, epoch, slot) do duty end @@ -88,11 +83,11 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end end - defp update_with_aggregation_duty(duty, beacon_state, privkey) do - proof = Utils.get_slot_signature(beacon_state, duty.slot, privkey) + defp update_with_aggregation_duty(duty, beacon_state, slot, privkey) do + proof = Utils.get_slot_signature(beacon_state, slot, privkey) if Utils.aggregator?(proof, duty.committee_length) do - epoch = Misc.compute_epoch_at_slot(duty.slot) + epoch = Misc.compute_epoch_at_slot(slot) domain = Accessors.get_domain(beacon_state, Constants.domain_aggregate_and_proof(), epoch) Map.put(duty, :should_aggregate?, true) @@ -103,11 +98,11 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end end - defp update_with_subnet_id(duty, beacon_state, epoch) do + defp update_with_subnet_id(duty, beacon_state, epoch, slot) do committees_per_slot = Accessors.get_committee_count_per_slot(beacon_state, epoch) subnet_id = - Utils.compute_subnet_for_attestation(committees_per_slot, duty.slot, duty.committee_index) + Utils.compute_subnet_for_attestation(committees_per_slot, slot, duty.committee_index) Map.put(duty, :subnet_id, subnet_id) end diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 5836ef3fa..fa234b6d9 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -90,14 +90,14 @@ defmodule LambdaEthereumConsensus.Validator do ########################## # Attestations - @spec attest(t(), Duties.attester_duty(), Types.root()) :: :ok - def attest(%{index: validator_index, keystore: keystore}, current_duty, head_root) do + @spec attest(t(), Duties.attester_duty(), Types.slot(), Types.root()) :: :ok + def attest(%{index: validator_index, keystore: keystore}, current_duty, slot, head_root) do subnet_id = current_duty.subnet_id - log_debug(validator_index, "attesting", slot: current_duty.slot, subnet_id: subnet_id) + log_debug(validator_index, "attesting", slot: slot, subnet_id: subnet_id) - attestation = produce_attestation(current_duty, head_root, keystore.privkey) + attestation = produce_attestation(current_duty, slot, head_root, keystore.privkey) - log_md = [slot: attestation.data.slot, attestation: attestation, subnet_id: subnet_id] + log_md = [slot: slot, attestation: attestation, subnet_id: subnet_id] debug_log_msg = "publishing attestation on committee index: #{current_duty.committee_index} | as #{current_duty.index_in_committee}/#{current_duty.committee_length - 1} and pubkey: #{LambdaEthereumConsensus.Utils.format_shorten_binary(keystore.pubkey)}" @@ -115,11 +115,12 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec publish_aggregate(Duties.attester_duty(), non_neg_integer(), Keystore.t()) :: :ok - def publish_aggregate(duty, validator_index, keystore) do + @spec publish_aggregate(Duties.attester_duty(), Types.slot(), non_neg_integer(), Keystore.t()) :: + :ok + def publish_aggregate(duty, slot, validator_index, keystore) do case Gossip.Attestation.stop_collecting(duty.subnet_id) do {:ok, attestations} -> - log_md = [slot: duty.slot, attestations: attestations] + log_md = [slot: slot, attestations: attestations] log_debug(validator_index, "publishing aggregate", log_md) aggregate_attestations(attestations) @@ -163,12 +164,11 @@ defmodule LambdaEthereumConsensus.Validator do %Types.SignedAggregateAndProof{message: aggregate_and_proof, signature: signature} end - defp produce_attestation(duty, head_root, privkey) do + defp produce_attestation(duty, slot, head_root, privkey) do %{ index_in_committee: index_in_committee, committee_length: committee_length, - committee_index: committee_index, - slot: slot + committee_index: committee_index } = duty head_state = BlockStates.get_state_info!(head_root).beacon_state |> go_to_slot(slot) diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 3336c18e3..29ab88ac3 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -170,7 +170,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do attesters -> Enum.map(attesters, fn {validator, duty} -> - Validator.attest(validator, duty, head_root) + Validator.attest(validator, duty, slot, head_root) # Duty.attested(duty) %{duty | attested?: true} @@ -186,7 +186,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do aggregators -> Enum.map(aggregators, fn {validator, duty} -> - Validator.publish_aggregate(duty, validator.index, validator.keystore) + Validator.publish_aggregate(duty, slot, validator.index, validator.keystore) # Duty.aggregated(duty) %{duty | should_aggregate?: false} From 14ce12fe3e33ce029fde3451a95652bcb3661872 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 16 Aug 2024 00:31:17 -0300 Subject: [PATCH 36/42] Refactored Duties functions out of the ValidatorSet --- .../validator/duties.ex | 64 ++++-- .../validator/validator.ex | 23 ++- .../validator/validator_set.ex | 187 ++++++++---------- 3 files changed, 159 insertions(+), 115 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 6fe4bc07f..005f702bc 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -27,8 +27,50 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @type attester_duties :: %{Types.slot() => [attester_duty()]} @type proposer_duties :: %{Types.slot() => [proposer_duty()]} + @type kind :: :proposers | :attesters @type duties :: %{attesters: attester_duties(), proposers: proposer_duties()} + ############################ + # Accessors + + @spec current_proposer(duties(), Types.epoch(), Types.slot()) :: proposer_duty() | nil + def current_proposer(duties, epoch, slot), + do: get_in(duties, [epoch, :proposers, slot]) + + @spec current_attesters(duties(), Types.epoch(), Types.slot()) :: [attester_duty()] + def current_attesters(duties, epoch, slot) do + for %{attested?: false} = duty <- attesters(duties, epoch, slot) do + duty + end + end + + @spec current_aggregators(duties(), Types.epoch(), Types.slot()) :: [attester_duty()] + def current_aggregators(duties, epoch, slot) do + for %{should_aggregate?: true} = duty <- attesters(duties, epoch, slot) do + duty + end + end + + defp attesters(duties, epoch, slot), do: get_in(duties, [epoch, :attesters, slot]) || [] + + ############################ + # Update functions + + @spec update_duties!(duties(), kind(), Types.epoch(), Types.slot(), [ + attester_duty() | proposer_duties() + ]) :: duties() + def update_duties!(duties, kind, epoch, slot, updated), + do: put_in(duties, [epoch, kind, slot], updated) + + @spec attested(attester_duty()) :: attester_duty() + def attested(duty), do: Map.put(duty, :attested?, true) + + @spec aggregated(attester_duty()) :: attester_duty() + def aggregated(duty), do: Map.put(duty, :should_aggregate?, false) + + ############################ + # Main functions + @spec compute_proposers_for_epoch(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) :: proposer_duties() def compute_proposers_for_epoch(%BeaconState{} = state, epoch, validators) do @@ -64,18 +106,16 @@ defmodule LambdaEthereumConsensus.Validator.Duties do case Accessors.get_beacon_committee(state, slot, committee_index) do {:ok, committee} -> for {validator_index, index_in_committee} <- Enum.with_index(committee), - validator = Map.get(validators, validator_index), - duty = - %{ - validator_index: validator_index, - index_in_committee: index_in_committee, - committee_length: length(committee), - committee_index: committee_index, - attested?: false - } - |> update_with_aggregation_duty(state, slot, validator.keystore.privkey) - |> update_with_subnet_id(state, epoch, slot) do - duty + validator = Map.get(validators, validator_index) do + %{ + validator_index: validator_index, + index_in_committee: index_in_committee, + committee_length: length(committee), + committee_index: committee_index, + attested?: false + } + |> update_with_aggregation_duty(state, slot, validator.keystore.privkey) + |> update_with_subnet_id(state, epoch, slot) end {:error, _} -> diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index fa234b6d9..1a3515c3e 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -41,7 +41,24 @@ defmodule LambdaEthereumConsensus.Validator do epoch = Misc.compute_epoch_at_slot(head_slot) beacon = fetch_target_state_and_go_to_slot(epoch, head_slot, head_root) - new(keystore, beacon) + state = %__MODULE__{ + index: nil, + keystore: keystore, + payload_builder: nil + } + + case fetch_validator_index(beacon, state.keystore.pubkey) do + nil -> + Logger.warning( + "[Validator] Public key #{state.keystore.pubkey} not found in the validator set" + ) + + state + + validator_index -> + log_debug(validator_index, "Setup completed") + %{state | index: validator_index} + end end @spec new(Keystore.t(), Types.BeaconState.t()) :: t() @@ -115,9 +132,9 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec publish_aggregate(Duties.attester_duty(), Types.slot(), non_neg_integer(), Keystore.t()) :: + @spec publish_aggregate(t(), Duties.attester_duty(), Types.slot()) :: :ok - def publish_aggregate(duty, slot, validator_index, keystore) do + def publish_aggregate(%{index: validator_index, keystore: keystore}, duty, slot) do case Gossip.Attestation.stop_collecting(duty.subnet_id) do {:ok, attestations} -> log_md = [slot: slot, attestations: attestations] diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 29ab88ac3..a53472730 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -38,18 +38,43 @@ defmodule LambdaEthereumConsensus.ValidatorSet do setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) end + defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) + when is_nil(keystore_dir) or is_nil(keystore_pass_dir) do + Logger.warning( + "[Validator] No keystore_dir or keystore_pass_dir provided. Validators won't start." + ) + + %__MODULE__{} + end + + defp setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) do + validator_keystores = decode_validator_keystores(keystore_dir, keystore_pass_dir) + epoch = Misc.compute_epoch_at_slot(slot) + + validators = + Map.new(validator_keystores, fn keystore -> + validator = Validator.new(keystore, slot, head_root) + {validator.index, validator} + end) + + Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") + + %__MODULE__{validators: validators} + |> update_state(epoch, slot, head_root) + end + @doc """ Notify all validators of a new head. """ @spec notify_head(t(), Types.slot(), Types.root()) :: t() def notify_head(set, slot, head_root) do # TODO: Just for testing purposes, remove it later - Logger.info("[Validator] Notifying all Validators with new_head", root: head_root, slot: slot) + Logger.info("[ValidatorSet] New Head", root: head_root, slot: slot) epoch = Misc.compute_epoch_at_slot(slot) set |> update_state(epoch, slot, head_root) - |> attest(epoch, slot, head_root) + |> attests(epoch, slot, head_root) |> build_next_payload(epoch, slot, head_root) end @@ -59,11 +84,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do @spec notify_tick(t(), tuple()) :: t() def notify_tick(%{head_root: head_root} = set, {slot, third} = slot_data) do # TODO: Just for testing purposes, remove it later - Logger.info("[Validator] Notifying all Validators with notify_tick: #{inspect(third)}", - root: head_root, - slot: slot - ) - + Logger.info("[ValidatorSet] Tick #{inspect(third)}", root: head_root, slot: slot) epoch = Misc.compute_epoch_at_slot(slot) set @@ -71,47 +92,18 @@ defmodule LambdaEthereumConsensus.ValidatorSet do |> process_tick(epoch, slot_data) end - defp process_tick(%{head_root: head_root} = set, epoch, {slot, :first_third}), - do: propose(set, epoch, slot, head_root) + defp process_tick(%{head_root: head_root} = set, epoch, {slot, :first_third}) do + propose(set, epoch, slot, head_root) + end defp process_tick(%{head_root: head_root} = set, epoch, {slot, :second_third}) do set - |> attest(epoch, slot, head_root) + |> attests(epoch, slot, head_root) |> build_next_payload(epoch, slot, head_root) end - defp process_tick(set, epoch, {slot, :last_third}), - do: publish_aggregate(set, epoch, slot) - - ############################## - # Setup - - defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) - when is_nil(keystore_dir) or is_nil(keystore_pass_dir) do - Logger.warning( - "[Validator] No keystore_dir or keystore_pass_dir provided. Validators won't start." - ) - - %__MODULE__{} - end - - defp setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) do - validator_keystores = decode_validator_keystores(keystore_dir, keystore_pass_dir) - epoch = Misc.compute_epoch_at_slot(slot) - - # This will be removed later when refactoring Validator new - beacon = Validator.fetch_target_state_and_go_to_slot(epoch, slot, head_root) - - validators = - Map.new(validator_keystores, fn keystore -> - validator = Validator.new(keystore, beacon) - {validator.index, validator} - end) - - Logger.info("[Validator] Initialized #{Enum.count(validators)} validators") - - %__MODULE__{validators: validators} - |> update_state(epoch, slot, head_root) + defp process_tick(set, epoch, {slot, :last_third}) do + publish_aggregates(set, epoch, slot) end ############################## @@ -161,91 +153,86 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end ############################## - # Attestation and proposal - - defp attest(set, epoch, slot, head_root) do - case current_attesters(set, epoch, slot) do - [] -> - set - - attesters -> - Enum.map(attesters, fn {validator, duty} -> - Validator.attest(validator, duty, slot, head_root) - - # Duty.attested(duty) - %{duty | attested?: true} - end) - |> then(&%{set | duties: put_in(set.duties, [epoch, :attesters, slot], &1)}) - end - end - - defp publish_aggregate(set, epoch, slot) do - case current_aggregators(set, epoch, slot) do - [] -> - set - - aggregators -> - Enum.map(aggregators, fn {validator, duty} -> - Validator.publish_aggregate(duty, slot, validator.index, validator.keystore) - - # Duty.aggregated(duty) - %{duty | should_aggregate?: false} - end) - |> then(&%{set | duties: put_in(set.duties, [epoch, :attesters, slot], &1)}) - end - end + # Block proposal defp build_next_payload(%{validators: validators} = set, epoch, slot, head_root) do - set - |> proposer(epoch, slot + 1) - |> case do + # FIXME: At a boundary slot epoch here is incorrect, we need to alway have the next epoch calculated + case Duties.current_proposer(set.duties, epoch, slot + 1) do nil -> set validator_index -> validators |> Map.update!(validator_index, &Validator.start_payload_builder(&1, slot + 1, head_root)) - |> then(&%{set | validators: &1}) + |> update_validators(set) end end defp propose(%{validators: validators} = set, epoch, slot, head_root) do - set - |> proposer(epoch, slot) - |> case do + case Duties.current_proposer(set.duties, epoch, slot) do nil -> set validator_index -> validators |> Map.update!(validator_index, &Validator.propose(&1, slot, head_root)) - |> then(&%{set | validators: &1}) + |> update_validators(set) end end + defp update_validators(new_validators, set), do: %{set | validators: new_validators} + ############################## - # Helpers + # Attestation - defp current_attesters(set, epoch, slot) do - set - |> attesters(epoch, slot) - |> Enum.flat_map(fn - %{attested?: false} = duty -> [{Map.get(set.validators, duty.validator_index), duty}] - _ -> [] - end) + defp attests(set, epoch, slot, head_root) do + case Duties.current_attesters(set.duties, epoch, slot) do + [] -> + set + + attester_duties -> + attester_duties + |> Enum.map(&attest(&1, slot, head_root, set.validators)) + |> update_duties(set, epoch, :attesters, slot) + end end - defp current_aggregators(set, epoch, slot) do - set - |> attesters(epoch, slot) - |> Enum.flat_map(fn - %{should_aggregate?: true} = duty -> [{Map.get(set.validators, duty.validator_index), duty}] - _ -> [] - end) + defp publish_aggregates(set, epoch, slot) do + case Duties.current_aggregators(set.duties, epoch, slot) do + [] -> + set + + aggregator_duties -> + aggregator_duties + |> Enum.map(&publish_aggregate(&1, slot, set.validators)) + |> update_duties(set, epoch, :attesters, slot) + end + end + + defp attest(duty, slot, head_root, validators) do + validators + |> Map.get(duty.validator_index) + |> Validator.attest(duty, slot, head_root) + + Duties.attested(duty) end - defp proposer(set, epoch, slot), do: get_in(set.duties, [epoch, :proposers, slot]) - defp attesters(set, epoch, slot), do: get_in(set.duties, [epoch, :attesters, slot]) || [] + defp publish_aggregate(duty, slot, validators) do + validators + |> Map.get(duty.validator_index) + |> Validator.publish_aggregate(duty, slot) + + Duties.aggregated(duty) + end + + defp update_duties(new_duties, set, epoch, kind, slot) do + set.duties + |> Duties.update_duties!(kind, epoch, slot, new_duties) + |> then(&%{set | duties: &1}) + end + + ############################## + # Key management @doc """ Get validator keystores from the keystore directory. From 7675b4be9eef1772bf4f33299a300d3b417cfc18 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 16 Aug 2024 11:09:41 -0300 Subject: [PATCH 37/42] Calculate next epoch duties ahead of time --- .../validator/duties.ex | 27 +++++++----- .../validator/validator_set.ex | 41 +++++++++++-------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 005f702bc..679758e84 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -24,11 +24,14 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @type proposer_duty :: Types.slot() - @type attester_duties :: %{Types.slot() => [attester_duty()]} - @type proposer_duties :: %{Types.slot() => [proposer_duty()]} + @type attester_duties :: [attester_duty()] + @type proposer_duties :: [proposer_duty()] + + @type attester_duties_per_slot :: %{Types.slot() => attester_duties()} + @type proposer_duties_per_slot :: %{Types.slot() => proposer_duties()} @type kind :: :proposers | :attesters - @type duties :: %{attesters: attester_duties(), proposers: proposer_duties()} + @type duties :: %{kind() => attester_duties_per_slot() | proposer_duties_per_slot()} ############################ # Accessors @@ -37,14 +40,14 @@ defmodule LambdaEthereumConsensus.Validator.Duties do def current_proposer(duties, epoch, slot), do: get_in(duties, [epoch, :proposers, slot]) - @spec current_attesters(duties(), Types.epoch(), Types.slot()) :: [attester_duty()] + @spec current_attesters(duties(), Types.epoch(), Types.slot()) :: attester_duties() def current_attesters(duties, epoch, slot) do for %{attested?: false} = duty <- attesters(duties, epoch, slot) do duty end end - @spec current_aggregators(duties(), Types.epoch(), Types.slot()) :: [attester_duty()] + @spec current_aggregators(duties(), Types.epoch(), Types.slot()) :: attester_duties() def current_aggregators(duties, epoch, slot) do for %{should_aggregate?: true} = duty <- attesters(duties, epoch, slot) do duty @@ -56,9 +59,13 @@ defmodule LambdaEthereumConsensus.Validator.Duties do ############################ # Update functions - @spec update_duties!(duties(), kind(), Types.epoch(), Types.slot(), [ - attester_duty() | proposer_duties() - ]) :: duties() + @spec update_duties!( + duties(), + kind(), + Types.epoch(), + Types.slot(), + attester_duties() | proposer_duties() + ) :: duties() def update_duties!(duties, kind, epoch, slot, updated), do: put_in(duties, [epoch, kind, slot], updated) @@ -72,7 +79,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do # Main functions @spec compute_proposers_for_epoch(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) :: - proposer_duties() + proposer_duties_per_slot() def compute_proposers_for_epoch(%BeaconState{} = state, epoch, validators) do with {:ok, epoch} <- check_valid_epoch(state, epoch), {start_slot, end_slot} <- boundary_slots(epoch) do @@ -86,7 +93,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end @spec compute_attesters_for_epoch(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) :: - attester_duties() + attester_duties_per_slot() def compute_attesters_for_epoch(%BeaconState{} = state, epoch, validators) do with {:ok, epoch} <- check_valid_epoch(state, epoch), {start_slot, end_slot} <- boundary_slots(epoch) do diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index a53472730..8802da882 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -26,6 +26,10 @@ defmodule LambdaEthereumConsensus.ValidatorSet do validators: validators() } + @doc "Check if the duties for the given epoch are already computed." + defguard is_duties_computed(set, epoch) + when is_map(set.duties) and not is_nil(:erlang.map_get(epoch, set.duties)) + @doc """ Initiate the set of validators, given the slot and head root. """ @@ -75,7 +79,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do set |> update_state(epoch, slot, head_root) |> attests(epoch, slot, head_root) - |> build_next_payload(epoch, slot, head_root) + |> build_payload(slot + 1, head_root) end @doc """ @@ -99,7 +103,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp process_tick(%{head_root: head_root} = set, epoch, {slot, :second_third}) do set |> attests(epoch, slot, head_root) - |> build_next_payload(epoch, slot, head_root) + |> build_payload(slot + 1, head_root) end defp process_tick(set, epoch, {slot, :last_third}) do @@ -119,19 +123,23 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp update_head(set, head_root), do: %{set | head_root: head_root} defp compute_duties(set, epoch, _slot, _head_root) - when not is_nil(:erlang.map_get(epoch, set.duties)), + when is_duties_computed(set, epoch) and is_duties_computed(set, epoch + 1), do: set defp compute_duties(set, epoch, slot, head_root) do - epoch - |> Validator.fetch_target_state_and_go_to_slot(slot, head_root) - |> compute_duties_for_epoch!(epoch, set.validators) + epochs_to_calculate = + [{epoch, slot}, {epoch + 1, Misc.compute_start_slot_at_epoch(epoch + 1)}] + |> Enum.reject(&Map.has_key?(set.duties, elem(&1, 0))) + + epochs_to_calculate + |> Map.new(&compute_duties_for_epoch!(set, &1, head_root)) |> merge_duties_and_prune(epoch, set) end - defp compute_duties_for_epoch!(beacon, epoch, validators) do - proposers = Duties.compute_proposers_for_epoch(beacon, epoch, validators) - attesters = Duties.compute_attesters_for_epoch(beacon, epoch, validators) + defp compute_duties_for_epoch!(set, {epoch, slot}, head_root) do + beacon = Validator.fetch_target_state_and_go_to_slot(epoch, slot, head_root) + proposers = Duties.compute_proposers_for_epoch(beacon, epoch, set.validators) + attesters = Duties.compute_attesters_for_epoch(beacon, epoch, set.validators) Logger.info( "[Validator] Proposer duties for epoch #{epoch} are: #{inspect(proposers, pretty: true)}" @@ -141,13 +149,13 @@ defmodule LambdaEthereumConsensus.ValidatorSet do "[Validator] Attester duties for epoch #{epoch} are: #{inspect(attesters, pretty: true)}" ) - %{epoch => %{proposers: proposers, attesters: attesters}} + {epoch, %{proposers: proposers, attesters: attesters}} end - defp merge_duties_and_prune(new_duties, epoch, set) do + defp merge_duties_and_prune(new_duties, current_epoch, set) do set.duties # Remove duties from epoch - 2 or older - |> Map.reject(fn {old_epoch, _} -> old_epoch < epoch - 2 end) + |> Map.reject(fn {old_epoch, _} -> old_epoch < current_epoch - 1 end) |> Map.merge(new_duties) |> then(fn current_duties -> %{set | duties: current_duties} end) end @@ -155,15 +163,16 @@ defmodule LambdaEthereumConsensus.ValidatorSet do ############################## # Block proposal - defp build_next_payload(%{validators: validators} = set, epoch, slot, head_root) do - # FIXME: At a boundary slot epoch here is incorrect, we need to alway have the next epoch calculated - case Duties.current_proposer(set.duties, epoch, slot + 1) do + defp build_payload(%{validators: validators} = set, slot, head_root) do + epoch = Misc.compute_epoch_at_slot(slot) + + case Duties.current_proposer(set.duties, epoch, slot) do nil -> set validator_index -> validators - |> Map.update!(validator_index, &Validator.start_payload_builder(&1, slot + 1, head_root)) + |> Map.update!(validator_index, &Validator.start_payload_builder(&1, slot, head_root)) |> update_validators(set) end end From 3defc249abba4e931dfa16498a5a6142e350ab04 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 16 Aug 2024 14:18:52 -0300 Subject: [PATCH 38/42] Cleaned up logging --- .../validator/duties.ex | 28 +++++++++++++++++++ .../validator/validator_set.ex | 22 ++++++--------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 679758e84..d45cdf4b3 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -157,6 +157,34 @@ defmodule LambdaEthereumConsensus.Validator.Duties do ############################ # Helpers + @spec log_duties_for_epoch(duties(), Types.epoch()) :: :ok + def log_duties_for_epoch(%{proposers: proposers, attesters: attesters}, epoch) do + Logger.info("[Duties] Proposers for epoch #{epoch} (slot=>validator): #{inspect(proposers)}") + + for {slot, att_duties} <- attesters do + Logger.info("[Duties] Attesters for epoch: #{epoch}, slot #{slot}:") + + for %{ + index_in_committee: ic, + committee_index: ci, + committee_length: cl, + subnet_id: si, + should_aggregate?: agg, + validator_index: vi + } <- att_duties do + Logger.info([ + "[Duties] Validator: #{vi}, will attest in committee #{ci} ", + "as #{ic}/#{cl - 1} in subnet: #{si}#{if agg, do: " and should Aggregate"}." + ]) + end + end + + :ok + end + + def log_duties_for_epoch(_duties, epoch), + do: Logger.info("[Duties] No duties for epoch: #{epoch}.") + defp check_valid_epoch(state, epoch) do next_epoch = Accessors.get_current_epoch(state) + 1 diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 8802da882..a433bf7d3 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -72,8 +72,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do """ @spec notify_head(t(), Types.slot(), Types.root()) :: t() def notify_head(set, slot, head_root) do - # TODO: Just for testing purposes, remove it later - Logger.info("[ValidatorSet] New Head", root: head_root, slot: slot) + Logger.debug("[ValidatorSet] New Head", root: head_root, slot: slot) epoch = Misc.compute_epoch_at_slot(slot) set @@ -87,8 +86,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do """ @spec notify_tick(t(), tuple()) :: t() def notify_tick(%{head_root: head_root} = set, {slot, third} = slot_data) do - # TODO: Just for testing purposes, remove it later - Logger.info("[ValidatorSet] Tick #{inspect(third)}", root: head_root, slot: slot) + Logger.debug("[ValidatorSet] Tick #{inspect(third)}", root: head_root, slot: slot) epoch = Misc.compute_epoch_at_slot(slot) set @@ -138,18 +136,15 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp compute_duties_for_epoch!(set, {epoch, slot}, head_root) do beacon = Validator.fetch_target_state_and_go_to_slot(epoch, slot, head_root) - proposers = Duties.compute_proposers_for_epoch(beacon, epoch, set.validators) - attesters = Duties.compute_attesters_for_epoch(beacon, epoch, set.validators) - Logger.info( - "[Validator] Proposer duties for epoch #{epoch} are: #{inspect(proposers, pretty: true)}" - ) + duties = %{ + proposers: Duties.compute_proposers_for_epoch(beacon, epoch, set.validators), + attesters: Duties.compute_attesters_for_epoch(beacon, epoch, set.validators) + } - Logger.info( - "[Validator] Attester duties for epoch #{epoch} are: #{inspect(attesters, pretty: true)}" - ) + Duties.log_duties_for_epoch(duties, epoch) - {epoch, %{proposers: proposers, attesters: attesters}} + {epoch, duties} end defp merge_duties_and_prune(new_duties, current_epoch, set) do @@ -164,6 +159,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do # Block proposal defp build_payload(%{validators: validators} = set, slot, head_root) do + # We calculate payloads from a previous slot, we need to recompute the epoch epoch = Misc.compute_epoch_at_slot(slot) case Duties.current_proposer(set.duties, epoch, slot) do From 666ff39e59f0218239f5abdaeb900c259555ebd1 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 16 Aug 2024 17:15:23 -0300 Subject: [PATCH 39/42] Added a comment from the previous implementation --- lib/lambda_ethereum_consensus/validator/validator_set.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index a433bf7d3..519e92bbd 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -75,6 +75,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Logger.debug("[ValidatorSet] New Head", root: head_root, slot: slot) epoch = Misc.compute_epoch_at_slot(slot) + # TODO: this doesn't take into account reorgs set |> update_state(epoch, slot, head_root) |> attests(epoch, slot, head_root) From 803941af622f66fe6ee5dfede5e2db34007a047c Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 22 Aug 2024 19:25:42 -0300 Subject: [PATCH 40/42] Added comments about should_aggregate? and use the same beacon for ValidatorSet setup --- .../validator/duties.ex | 3 +++ .../validator/validator.ex | 19 +------------------ .../validator/validator_set.ex | 3 ++- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index d45cdf4b3..83f0b09ce 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -12,6 +12,8 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @type attester_duty :: %{ attested?: boolean(), + # should_aggregate? is used to check if aggregation is needed for this attestation. + # and also to avoid double aggregation. should_aggregate?: boolean(), selection_proof: Bls.signature(), signing_domain: Types.domain(), @@ -73,6 +75,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do def attested(duty), do: Map.put(duty, :attested?, true) @spec aggregated(attester_duty()) :: attester_duty() + # should_aggregate? is set to false to avoid double aggregation. def aggregated(duty), do: Map.put(duty, :should_aggregate?, false) ############################ diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 1a3515c3e..07fbaa661 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -41,24 +41,7 @@ defmodule LambdaEthereumConsensus.Validator do epoch = Misc.compute_epoch_at_slot(head_slot) beacon = fetch_target_state_and_go_to_slot(epoch, head_slot, head_root) - state = %__MODULE__{ - index: nil, - keystore: keystore, - payload_builder: nil - } - - case fetch_validator_index(beacon, state.keystore.pubkey) do - nil -> - Logger.warning( - "[Validator] Public key #{state.keystore.pubkey} not found in the validator set" - ) - - state - - validator_index -> - log_debug(validator_index, "Setup completed") - %{state | index: validator_index} - end + new(keystore, beacon) end @spec new(Keystore.t(), Types.BeaconState.t()) :: t() diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 519e92bbd..5d9541104 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -54,10 +54,11 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) do validator_keystores = decode_validator_keystores(keystore_dir, keystore_pass_dir) epoch = Misc.compute_epoch_at_slot(slot) + beacon = Validator.fetch_target_state_and_go_to_slot(epoch, slot, head_root) validators = Map.new(validator_keystores, fn keystore -> - validator = Validator.new(keystore, slot, head_root) + validator = Validator.new(keystore, beacon) {validator.index, validator} end) From ec987de5869d5e9154bb04acd6639347a39e93d8 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 22 Aug 2024 20:48:18 -0300 Subject: [PATCH 41/42] renamed ValidatorSet functions to maybe_ indicating they depend on duties to do something --- .../validator/validator_set.ex | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 5d9541104..29710ba63 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -79,8 +79,8 @@ defmodule LambdaEthereumConsensus.ValidatorSet do # TODO: this doesn't take into account reorgs set |> update_state(epoch, slot, head_root) - |> attests(epoch, slot, head_root) - |> build_payload(slot + 1, head_root) + |> maybe_attests(epoch, slot, head_root) + |> maybe_build_payload(slot + 1, head_root) end @doc """ @@ -97,17 +97,17 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end defp process_tick(%{head_root: head_root} = set, epoch, {slot, :first_third}) do - propose(set, epoch, slot, head_root) + maybe_propose(set, epoch, slot, head_root) end defp process_tick(%{head_root: head_root} = set, epoch, {slot, :second_third}) do set - |> attests(epoch, slot, head_root) - |> build_payload(slot + 1, head_root) + |> maybe_attests(epoch, slot, head_root) + |> maybe_build_payload(slot + 1, head_root) end defp process_tick(set, epoch, {slot, :last_third}) do - publish_aggregates(set, epoch, slot) + maybe_publish_aggregates(set, epoch, slot) end ############################## @@ -160,7 +160,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do ############################## # Block proposal - defp build_payload(%{validators: validators} = set, slot, head_root) do + defp maybe_build_payload(%{validators: validators} = set, slot, head_root) do # We calculate payloads from a previous slot, we need to recompute the epoch epoch = Misc.compute_epoch_at_slot(slot) @@ -175,7 +175,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end end - defp propose(%{validators: validators} = set, epoch, slot, head_root) do + defp maybe_propose(%{validators: validators} = set, epoch, slot, head_root) do case Duties.current_proposer(set.duties, epoch, slot) do nil -> set @@ -192,7 +192,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do ############################## # Attestation - defp attests(set, epoch, slot, head_root) do + defp maybe_attests(set, epoch, slot, head_root) do case Duties.current_attesters(set.duties, epoch, slot) do [] -> set @@ -204,7 +204,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end end - defp publish_aggregates(set, epoch, slot) do + defp maybe_publish_aggregates(set, epoch, slot) do case Duties.current_aggregators(set.duties, epoch, slot) do [] -> set From 2e9595fbc120363d8b67c3afdef01194f2d9dd4a Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 23 Aug 2024 19:10:21 -0300 Subject: [PATCH 42/42] Add maybe_prefetch_committees and fix and issue when there were no validators --- .../validator/validator_set.ex | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 29710ba63..c06abb172 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -5,10 +5,11 @@ defmodule LambdaEthereumConsensus.ValidatorSet do simplify the delegation of work. """ - defstruct head_root: nil, duties: %{}, validators: [] + defstruct head_root: nil, duties: %{}, validators: %{} require Logger + alias LambdaEthereumConsensus.StateTransition.Accessors alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.Validator alias LambdaEthereumConsensus.Validator.Duties @@ -72,6 +73,9 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Notify all validators of a new head. """ @spec notify_head(t(), Types.slot(), Types.root()) :: t() + def notify_head(%{validators: validators} = state, _slot, _head_root) when validators == %{}, + do: state + def notify_head(set, slot, head_root) do Logger.debug("[ValidatorSet] New Head", root: head_root, slot: slot) epoch = Misc.compute_epoch_at_slot(slot) @@ -87,6 +91,9 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Notify all validators of a new tick. """ @spec notify_tick(t(), tuple()) :: t() + def notify_tick(%{validators: validators} = state, _slot_data) when validators == %{}, + do: state + def notify_tick(%{head_root: head_root} = set, {slot, third} = slot_data) do Logger.debug("[ValidatorSet] Tick #{inspect(third)}", root: head_root, slot: slot) epoch = Misc.compute_epoch_at_slot(slot) @@ -138,6 +145,9 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp compute_duties_for_epoch!(set, {epoch, slot}, head_root) do beacon = Validator.fetch_target_state_and_go_to_slot(epoch, slot, head_root) + # If committees are not already calculated for the epoch, this is way faster than + # calculating them on the fly. + Accessors.maybe_prefetch_committees(beacon, epoch) duties = %{ proposers: Duties.compute_proposers_for_epoch(beacon, epoch, set.validators),