From 3c9803de3b52848a39cae7e85b1ff0864a88118b Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Mon, 26 Aug 2024 18:30:23 -0300 Subject: [PATCH 01/23] Merge branch 'sync-committee-message-production' into sync_committee_contribution - squash commit 2a24d5b3f3065b1c7ea19b1eedfa4697488c7c63 Author: Rodrigo Oliveri Date: Mon Aug 26 18:27:10 2024 -0300 Removed function needed in next PRs but not used commit f8b84b774279dc1e2a2c39453e469414933c14ee Author: Rodrigo Oliveri Date: Mon Aug 26 18:15:39 2024 -0300 Format commit d60cea4821e4f1910205f5a8b3d5cd31d2242c54 Author: Rodrigo Oliveri Date: Mon Aug 26 18:11:28 2024 -0300 Fixed an issue at Sync committee period change after second change commit bb56817baac8f633b6fae057ae36982c14070742 Author: Rodrigo Oliveri Date: Mon Aug 26 13:43:25 2024 -0300 Some additional clean-up commit 57ef3403a86d5a5df90a492c55933df64596eb1c Author: Rodrigo Oliveri Date: Fri Aug 23 20:58:56 2024 -0300 duties clean up + logging commit 97e6b3acf225653a981ffd5d5d49156fec89cead Merge: 3215928 9835380 Author: Rodrigo Oliveri Date: Fri Aug 23 20:44:27 2024 -0300 Merge branch 'main' into sync-committee-message-production commit 32159282ccc4e80deec3595f2aeeefa069cfdbce Author: Rodrigo Oliveri Date: Thu Aug 22 20:04:40 2024 -0300 format commit fc07190d53585e523cc02b0146b7b47af9bbf535 Author: Rodrigo Oliveri Date: Thu Aug 22 20:03:28 2024 -0300 Checked duties working on period recalculation commit 9f685160dc5bb50041ca229c7e430cd730929fd9 Author: Rodrigo Oliveri Date: Wed Aug 21 18:55:55 2024 -0300 Initial sync_committee duty recalculation commit 4d72f181dd00fe90a9c3a9b3f62e14a9e17891b2 Author: Rodrigo Oliveri Date: Wed Aug 21 12:55:42 2024 -0300 Messages broadcasted to the network commit 868bac46f3fae8ea51d9c8c36ebe5f909adb2e2b Author: Rodrigo Oliveri Date: Tue Aug 20 18:48:19 2024 -0300 Fixed small issue during duties update commit d9106c7e8b58866874914ad2bd35502808e10cfb Author: Rodrigo Oliveri Date: Tue Aug 20 18:06:26 2024 -0300 Initial test of sync aggregates without publishing commit 75501d5ff6ed92e729bd49d6187b33ba6f0e0885 Merge: eab7a05 666ff39 Author: Rodrigo Oliveri Date: Mon Aug 19 17:30:35 2024 -0300 Merge branch 'validator-state-management-refactor' into sync-committee-message-production commit 666ff39e59f0218239f5abdaeb900c259555ebd1 Author: Rodrigo Oliveri Date: Fri Aug 16 17:15:23 2024 -0300 Added a comment from the previous implementation commit 5ff062bb7bace33abb3f1941ced6b883c8a686b9 Merge: 3defc24 43a40d4 Author: Rodrigo Oliveri Date: Fri Aug 16 16:45:09 2024 -0300 Merge branch 'main' into validator-state-management-refactor commit 3defc249abba4e931dfa16498a5a6142e350ab04 Author: Rodrigo Oliveri Date: Fri Aug 16 14:18:52 2024 -0300 Cleaned up logging commit 2d4690dee02808a6520f230e46e23dd0dc6d612c Merge: 7675b4b 84ee168 Author: Rodrigo Oliveri Date: Fri Aug 16 13:39:21 2024 -0300 Merge branch 'main' into validator-state-management-refactor commit 7675b4be9eef1772bf4f33299a300d3b417cfc18 Author: Rodrigo Oliveri Date: Fri Aug 16 11:09:41 2024 -0300 Calculate next epoch duties ahead of time commit 14ce12fe3e33ce029fde3451a95652bcb3661872 Author: Rodrigo Oliveri Date: Fri Aug 16 00:31:17 2024 -0300 Refactored Duties functions out of the ValidatorSet commit d9e8d21839d25dcb86aef909f9327c1b56e41be6 Merge: 18f0564 f0f8111 Author: Rodrigo Oliveri Date: Thu Aug 15 18:03:57 2024 -0300 Merge branch 'main' into validator-state-management-refactor commit 18f05647635346162022eb8aba58be940a5bd284 Author: Rodrigo Oliveri Date: Thu Aug 15 17:35:07 2024 -0300 Simplified comute_attesters_for_epoch and removed slot from duties commit 0987fc22d227f950b54418768094a6201d4384cc Author: Rodrigo Oliveri Date: Thu Aug 15 15:05:46 2024 -0300 Quick fix regarding attesters calculation commit e17a224312fc07a44a90e2d147a2a8c697fe07ec Author: Rodrigo Oliveri Date: Thu Aug 15 15:01:42 2024 -0300 Further clean duties commit 35d2c51f85b64db3a1def47de6fe9df62919bd05 Merge: 8c1d818 2f77b11 Author: Rodrigo Oliveri Date: Thu Aug 15 11:47:42 2024 -0300 Merge branch 'main' into validator-state-management-refactor commit 8c1d818601ea55a312fea83ac332cba500b58537 Author: Rodrigo Oliveri Date: Thu Aug 15 11:46:01 2024 -0300 Initial Duties cleanup commit c030f1bef695c96cd3a3e311ebc79b61141043bb Author: Rodrigo Oliveri Date: Thu Aug 15 11:10:15 2024 -0300 ValidatorSet and Validator cleanup commit 5331a6323f3bed52396c2cc9f2be8c5c3a54307a Author: Rodrigo Oliveri Date: Wed Aug 14 17:11:30 2024 -0300 Epoch 0 completely working from ValidatorSet commit c99729f6983e0a027d926c74f9f872ef67de6fb5 Author: Rodrigo Oliveri Date: Wed Aug 14 11:57:45 2024 -0300 handle_tick completely moved to ValidatorSet commit 3abb064fe6e31bc5e2b0de0ee4a462844152c4c1 Merge: 37a593d 52f5e0f Author: Rodrigo Oliveri Date: Wed Aug 14 10:29:38 2024 -0300 Merge branch 'main' into validator-state-management-refactor commit 37a593dea26faab5294a1068cf97117ba805453b Author: Rodrigo Oliveri Date: Tue Aug 13 17:40:30 2024 -0300 Fix some warnings from compile and credo commit 654b5ea59801e47f84764bd741d6623082a8cf5f Author: Rodrigo Oliveri Date: Tue Aug 13 17:12:15 2024 -0300 Formatted and fixed all lint and dialyzer issues commit cde101c6ea19c41bfa2e085fcba1a141dd89bc1b Author: Rodrigo Oliveri Date: Tue Aug 13 16:36:11 2024 -0300 Fixed how keystore functions handle the validator set commit 0c85ce1a72dd874eea1dace838df8dca6e932be7 Merge: 616f892 f38288c Author: Rodrigo Oliveri Date: Tue Aug 13 15:05:12 2024 -0300 Merge branch 'main' into validator-state-management-refactor commit 616f8924a6b4fdeb4dfe2bdb16c079fcf2e23e89 Author: Rodrigo Oliveri Date: Tue Aug 13 12:28:01 2024 -0300 Removed slot and root from Validator and started cleaning up the ValidatorSet commit cfc5efe150b4c04db244dfe4397efe6f204537b2 Author: Rodrigo Oliveri Date: Mon Aug 12 23:56:23 2024 -0300 Hybrid version working with new_head handled at the ValidatorSet commit a6042130ad76cc02aabf2a1325d9e3db38915b90 Author: Rodrigo Oliveri Date: Fri Aug 9 19:38:39 2024 -0300 Notify Head now working from the ValidatorSet instead of Validators commit 69150f2706dd9ebe45d8408d0761efb198c70846 Author: Rodrigo Oliveri Date: Thu Aug 8 20:57:01 2024 -0300 Initial attesters_for_epoch implementation working commit cb0b5807480e25b668c6ff52e5f140b149be3eeb Author: Rodrigo Oliveri Date: Thu Aug 8 14:09:25 2024 -0300 Calculate proposers for the whole epoch commit 6046c52fcef6ae02f182191581e7dddaed8abd61 Author: Rodrigo Oliveri Date: Wed Aug 7 20:06:54 2024 -0300 ValidatorSet rename + started moving duties to all validators commit 9a3b4c3fb8f89f120af6a610f682ec5e24a7c3a2 Author: Rodrigo Oliveri Date: Wed Aug 7 15:08:58 2024 -0300 Created the new ValidatorPool and moved everything to work with it instead of Setup commit e7faf6961df5ffb9f56aae8cac93d406bb1ee9f1 Merge: 2055e78 4c814a3 Author: Rodrigo Oliveri Date: Mon Aug 5 20:05:20 2024 -0300 Merge branch 'main' into validator-state-management-refactor commit 2055e785b88b62fc34bb2fc70e2646a2447bcc49 Author: Rodrigo Oliveri Date: Fri Aug 2 15:48:53 2024 -0300 Small cleanup of the Validator.Setup commit b9423e1fc276d3424fe37f69ab1a5b96c00dc8aa Author: Rodrigo Oliveri Date: Thu Aug 1 12:12:07 2024 -0300 Small fix regarding an info message that should be debug and a unused var commit b1e9a3ed587c9fb9e43f93bef183258c85bdd348 Author: Rodrigo Oliveri Date: Thu Aug 1 11:56:01 2024 -0300 Ticker removal commit 170db89724f0b46e25ea51061631b461648d96c1 Merge: 5721ccf ae42589 Author: Rodrigo Oliveri Date: Wed Jul 31 17:31:26 2024 -0300 Merge branch 'main' into validator-manager-genserver-removal commit 5721ccf61ca3b1488b8a6f94d2479ab4242a611e Author: Rodrigo Oliveri Date: Wed Jul 31 17:27:03 2024 -0300 Remove unneded diffs commit 9eed36594be537c777ac80ca247604e073cde210 Author: Rodrigo Oliveri Date: Wed Jul 31 17:20:39 2024 -0300 Added an async subscribe to topic to avoid issues in test commit 55c5d90fb5d30bfefc04620a693fbf1baa5e9a11 Author: Rodrigo Oliveri Date: Tue Jul 30 21:19:05 2024 -0300 Simplify a diff commit 46efa3a1a575616cbda8cb2f4a2e4d1e56641e89 Author: Rodrigo Oliveri Date: Tue Jul 30 21:11:17 2024 -0300 Fixed some dialyzer issues removing unused functions commit ff423015de63a757ecf9b18436450afd94e0c752 Author: Rodrigo Oliveri Date: Tue Jul 30 21:05:25 2024 -0300 Simplified the Ticker and added dialyzer to the 'make lint' task commit 2de70bff7d15cfc2e336494ca810b96e1ab7ea53 Author: Rodrigo Oliveri Date: Tue Jul 30 17:45:34 2024 -0300 Small fixe after renaming the ticker commit 3802005622f064d3805a978f4fa3e45d8c425312 Author: Rodrigo Oliveri Date: Tue Jul 30 17:14:06 2024 -0300 renamed validator manager and clock to Validator.Setup and Ticker commit e3d732741c205f25af46e66a4558763410779f54 Author: Rodrigo Oliveri Date: Tue Jul 30 17:05:33 2024 -0300 ValidatorsManager Genserver removal commit cb99b049da9be5c8ad4f6ff4bb3e2c45ddcd629c Author: Rodrigo Oliveri Date: Tue Jul 30 15:07:07 2024 -0300 Format and genesis_time addition to libp2p starts on tests commit b5c75b4c6d9d3d6796d720363762a7144305a5f9 Merge: 9985081 40faca6 Author: Rodrigo Oliveri Date: Tue Jul 30 13:17:23 2024 -0300 Merge branch 'main' into validator-manager-genserver-removal commit 99850819b622b72bb0722c217fc80a2b600fb1e5 Author: Rodrigo Oliveri Date: Tue Jul 30 12:03:13 2024 -0300 Remove unneded diffs commit b8623ee728260815bb8153dbd3b9067ca47d11b5 Author: Rodrigo Oliveri Date: Mon Jul 29 20:10:28 2024 -0300 Make the clock a ticker commit 3c5961fcad5b8d90b52003d24bd463635cc4ef3d Author: Rodrigo Oliveri Date: Fri Jul 26 14:11:21 2024 -0300 Finally fixing the issue commit 11c9b724a35465bef1b28ae29f83d7b8192da823 Author: Rodrigo Oliveri Date: Tue Jul 23 18:53:19 2024 -0300 Just deactivated attestation publish to check the node running without issues commit c85151eac197cd483bc90f6986de180ceed9423b Author: Rodrigo Oliveri Date: Tue Jul 23 14:22:25 2024 -0300 Moved notify_tick to libp2p to be sure the issues is just with new blocks and not with libp2p calling the ValidatorManager commit b6c2bed9d31c231062fe97e5fa33b296b54acc2f Author: Rodrigo Oliveri Date: Tue Jul 23 12:46:23 2024 -0300 This are the minimum changes to reproduce the invalid signature error --- lib/constants.ex | 3 + .../p2p/gossip/sync_committee.ex | 27 +++ .../state_transition/misc.ex | 9 + .../validator/duties.ex | 196 ++++++++++++++---- .../validator/utils.ex | 49 +++++ .../validator/validator.ex | 48 +++++ .../validator/validator_set.ex | 49 +++-- network_params.yaml | 2 + 8 files changed, 320 insertions(+), 63 deletions(-) create mode 100644 lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex diff --git a/lib/constants.ex b/lib/constants.ex index 1beccc676..81bffa8e2 100644 --- a/lib/constants.ex +++ b/lib/constants.ex @@ -36,6 +36,9 @@ defmodule Constants do @spec target_aggregators_per_committee() :: non_neg_integer() def target_aggregators_per_committee(), do: 16 + @spec target_aggregators_per_sync_subcommittee() :: non_neg_integer() + def target_aggregators_per_sync_subcommittee(), do: 16 + ### Withdrawal prefixes @spec bls_withdrawal_prefix() :: Types.bytes1() diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex new file mode 100644 index 000000000..9d21a2ff0 --- /dev/null +++ b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex @@ -0,0 +1,27 @@ +defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do + @moduledoc """ + This module handles sync committee from specific gossip subnets. + Used by validators to fulfill aggregation duties. + """ + alias LambdaEthereumConsensus.ForkChoice + alias LambdaEthereumConsensus.Libp2pPort + + require Logger + + @spec publish(Types.SyncCommitteeMessage.t(), [non_neg_integer()]) :: :ok + def publish(%Types.SyncCommitteeMessage{} = sync_committee_msg, subnet_ids) do + Enum.each(subnet_ids, fn subnet_id -> + topic = topic(subnet_id) + + {:ok, encoded} = SszEx.encode(sync_committee_msg, Types.SyncCommitteeMessage) + {:ok, message} = :snappyer.compress(encoded) + Libp2pPort.publish(topic, message) + end) + end + + defp topic(subnet_id) do + # TODO: this doesn't take into account fork digest changes + fork_context = ForkChoice.get_fork_digest() |> Base.encode16(case: :lower) + "/eth2/#{fork_context}/sync_committee_#{subnet_id}/ssz_snappy" + end +end diff --git a/lib/lambda_ethereum_consensus/state_transition/misc.ex b/lib/lambda_ethereum_consensus/state_transition/misc.ex index 1be4d915e..1cb101f03 100644 --- a/lib/lambda_ethereum_consensus/state_transition/misc.ex +++ b/lib/lambda_ethereum_consensus/state_transition/misc.ex @@ -281,6 +281,15 @@ defmodule LambdaEthereumConsensus.StateTransition.Misc do {committee_start, committee_end} end + @doc """ + Compute the sync committee period for the given ``epoch``. This is used to determine the + period in which a validator is assigned to the sync committee. + """ + @spec compute_sync_committee_period(Types.epoch()) :: Types.uint64() + def compute_sync_committee_period(epoch) do + div(epoch, ChainSpec.get("EPOCHS_PER_SYNC_COMMITTEE_PERIOD")) + end + @doc """ Return the 32-byte fork data root for the ``current_version`` and ``genesis_validators_root``. This is used primarily in signature domains to avoid collisions across forks/chains. diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 83f0b09ce..c95d3452d 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 LambdaEthereumConsensus.ValidatorSet alias Types.BeaconState @@ -24,66 +25,76 @@ defmodule LambdaEthereumConsensus.Validator.Duties do index_in_committee: Types.uint64() } - @type proposer_duty :: Types.slot() + @type proposer_duty :: Types.validator_index() + + @type sync_committee_duty :: %{ + last_slot_broadcasted: Types.slot(), + subnet_ids: [Types.uint64()], + validator_index: Types.validator_index() + } @type attester_duties :: [attester_duty()] @type proposer_duties :: [proposer_duty()] + @type sync_committee_duties :: [sync_committee_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 :: %{kind() => attester_duties_per_slot() | proposer_duties_per_slot()} + @type kind :: :proposers | :attesters | :sync_committees + @type duties :: %{ + kind() => + attester_duties_per_slot() | proposer_duties_per_slot() | sync_committee_duties() + } ############################ - # Accessors + # Main Compute functions - @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 compute_duties_for_epochs( + %{Types.epoch() => duties()}, + [{Types.epoch(), Types.slot()}], + Types.root(), + ValidatorSet.validators() + ) :: duties() + def compute_duties_for_epochs(duties_map, epochs_and_start_slots, head_root, validators) do + Logger.debug("[Duties] Computing duties for epochs: #{inspect(epochs_and_start_slots)}") - @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 + for {epoch, slot} <- epochs_and_start_slots, reduce: duties_map do + duties_map -> + 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) - @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 - end - end + last_epoch = Map.keys(duties_map) |> Enum.max(fn -> 0 end) - defp attesters(duties, epoch, slot), do: get_in(duties, [epoch, :attesters, slot]) || [] + new_proposers = compute_proposers_for_epoch(beacon, epoch, validators) + new_attesters = compute_attesters_for_epoch(beacon, epoch, validators) - ############################ - # Update functions + new_sync_committees = + case sync_committee_compute_check(epoch, {last_epoch, Map.get(duties_map, last_epoch)}) do + {:already_computed, sync_committees} -> + sync_committees - @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) + {:not_computed, period} -> + Logger.debug("[Duties] Computing sync committees for period: #{period}.") - @spec attested(attester_duty()) :: attester_duty() - def attested(duty), do: Map.put(duty, :attested?, true) + compute_current_sync_committees(beacon, validators) + end - @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) + new_duties = %{ + proposers: new_proposers, + attesters: new_attesters, + sync_committees: new_sync_committees + } - ############################ - # Main functions + log_duties_for_epoch(new_duties, epoch) + Map.put(duties_map, epoch, new_duties) + end + end @spec compute_proposers_for_epoch(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) :: proposer_duties_per_slot() - def compute_proposers_for_epoch(%BeaconState{} = state, epoch, validators) do + defp 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 for slot <- start_slot..end_slot, @@ -95,9 +106,34 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end end + @spec compute_current_sync_committees(BeaconState.t(), ValidatorSet.validators()) :: + sync_committee_duties() + defp compute_current_sync_committees(%BeaconState{} = state, validators) do + for validator_index <- Map.keys(validators), + subnet_ids = Utils.compute_subnets_for_sync_committee(state, validator_index), + length(subnet_ids) > 0 do + %{ + last_slot_broadcasted: -1, + subnet_ids: subnet_ids, + validator_index: validator_index + } + end + end + + defp sync_committee_compute_check(_epoch, {_last_epoch, nil}), do: :not_computed + + defp sync_committee_compute_check(epoch, {last_epoch, last_duties}) do + last_period = Misc.compute_sync_committee_period(last_epoch) + current_period = Misc.compute_sync_committee_period(epoch) + + if last_period == current_period, + do: {:already_computed, last_duties.sync_committees}, + else: {:not_computed, current_period} + end + @spec compute_attesters_for_epoch(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) :: attester_duties_per_slot() - def compute_attesters_for_epoch(%BeaconState{} = state, epoch, validators) do + defp 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) @@ -157,15 +193,87 @@ defmodule LambdaEthereumConsensus.Validator.Duties do Map.put(duty, :subnet_id, subnet_id) end + ############################ + # 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_sync_committee(duties(), Types.epoch(), Types.slot()) :: + sync_committee_duties() + def current_sync_committee(duties, epoch, slot) do + for %{last_slot_broadcasted: last_slot} = duty <- sync_committee(duties, epoch), + last_slot < slot do + duty + end + end + + @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_duties() + def current_aggregators(duties, epoch, slot) do + for %{should_aggregate?: true} = duty <- attesters(duties, epoch, slot) do + duty + end + end + + defp sync_committee(duties, epoch), do: get_in(duties, [epoch, :sync_committees]) || [] + defp attesters(duties, epoch, slot), do: get_in(duties, [epoch, :attesters, slot]) || [] + + ############################ + # Update functions + + @spec update_duties!( + duties(), + kind(), + Types.epoch(), + Types.slot(), + attester_duties() | proposer_duties() + ) :: duties() + def update_duties!(duties, :sync_committees, epoch, _slot, updated), + do: put_in(duties, [epoch, :sync_committees], updated) + + 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() + # should_aggregate? is set to false to avoid double aggregation. + def aggregated(duty), do: Map.put(duty, :should_aggregate?, false) + + @spec sync_committee_broadcasted(sync_committee_duty(), Types.slot()) :: sync_committee_duty() + def sync_committee_broadcasted(duty, slot), do: Map.put(duty, :last_slot_broadcasted, slot) + ############################ # Helpers @spec log_duties_for_epoch(duties(), Types.epoch()) :: :ok - def log_duties_for_epoch(%{proposers: proposers, attesters: attesters}, epoch) do + def log_duties_for_epoch( + %{proposers: proposers, attesters: attesters, sync_committees: sync_committees}, + 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 %{ + subnet_ids: si, + validator_index: vi + } <- sync_committees do + Logger.debug( + "[Duties] Sync committee for epoch: #{epoch}, validator_index: #{vi} will broadcast on subnet_ids: #{inspect(si)}." + ) + end + + for {slot, att_duties} <- attesters, + length(att_duties) > 0 do + Logger.debug("[Duties] Attesters for epoch: #{epoch}, slot #{slot}:") for %{ index_in_committee: ic, @@ -175,7 +283,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do should_aggregate?: agg, validator_index: vi } <- att_duties do - Logger.info([ + Logger.debug([ "[Duties] Validator: #{vi}, will attest in committee #{ci} ", "as #{ic}/#{cl - 1} in subnet: #{si}#{if agg, do: " and should Aggregate"}." ]) diff --git a/lib/lambda_ethereum_consensus/validator/utils.ex b/lib/lambda_ethereum_consensus/validator/utils.ex index 75af1581e..d2149226a 100644 --- a/lib/lambda_ethereum_consensus/validator/utils.ex +++ b/lib/lambda_ethereum_consensus/validator/utils.ex @@ -52,4 +52,53 @@ defmodule LambdaEthereumConsensus.Validator.Utils do |> :binary.decode_unsigned(:little) |> rem(modulo) == 0 end + + @spec compute_subnets_for_sync_committee(BeaconState.t(), Types.validator_index()) :: [ + Types.uint64() + ] + def compute_subnets_for_sync_committee(%BeaconState{} = state, validator_index) do + target_pubkey = state.validators[validator_index].pubkey + current_epoch = Accessors.get_current_epoch(state) + next_slot_epoch = Misc.compute_epoch_at_slot(state.slot + 1) + current_sync_committee_period = Misc.compute_sync_committee_period(current_epoch) + next_slot_sync_committee_period = Misc.compute_sync_committee_period(next_slot_epoch) + + sync_committee = + if current_sync_committee_period == next_slot_sync_committee_period, + do: state.current_sync_committee, + else: state.next_sync_committee + + sync_committee_subnet_size = + div(ChainSpec.get("SYNC_COMMITTEE_SIZE"), Constants.sync_committee_subnet_count()) + + for {pubkey, index} <- Enum.with_index(sync_committee.pubkeys), + pubkey == target_pubkey do + div(index, sync_committee_subnet_size) + end + |> Enum.dedup() + end + + # `is_assigned_to_sync_committee` equivalent + @spec assigned_to_sync_committee?(BeaconState.t(), Types.epoch(), Types.validator_index()) :: + boolean() + def assigned_to_sync_committee?(%BeaconState{} = state, epoch, validator_index) do + sync_committee_period = Misc.compute_sync_committee_period(epoch) + current_epoch = Accessors.get_current_epoch(state) + current_sync_committee_period = Misc.compute_sync_committee_period(current_epoch) + next_sync_committee_period = current_sync_committee_period + 1 + + pubkey = state.validators[validator_index].pubkey + + case sync_committee_period do + ^current_sync_committee_period -> + Enum.member?(state.current_sync_committee.pubkeys, pubkey) + + ^next_sync_committee_period -> + Enum.member?(state.next_sync_committee.pubkeys, pubkey) + + _ -> + raise ArgumentError, + "Invalid epoch #{epoch}, should be in the current or next sync committee period" + end + end end diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 07fbaa661..c83fe858c 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -211,6 +211,54 @@ defmodule LambdaEthereumConsensus.Validator do Enum.find_index(beacon.validators, &(&1.pubkey == pubkey)) end + ################################ + # Sync Committee + + @spec sync_committee_message_broadcast( + t(), + Duties.sync_committee_duty(), + Types.slot(), + Types.root() + ) :: + :ok + def sync_committee_message_broadcast( + %{index: validator_index, keystore: keystore}, + current_duty, + slot, + head_root + ) do + %{subnet_ids: subnet_ids} = current_duty + + head_state = BlockStates.get_state_info!(head_root).beacon_state |> go_to_slot(slot) + log_debug(validator_index, "broadcasting sync committee message", slot: slot) + + head_state + |> get_sync_committee_message(head_root, validator_index, keystore.privkey) + |> Gossip.SyncCommittee.publish(subnet_ids) + |> log_info_result(validator_index, "published sync committee message", slot: slot) + end + + @spec get_sync_committee_message( + Types.BeaconState.t(), + Types.root(), + Types.validator_index(), + Bls.privkey() + ) :: + Types.SyncCommitteeMessage.t() + def get_sync_committee_message(head_state, head_root, validator_index, privkey) do + epoch = Accessors.get_current_epoch(head_state) + domain = Accessors.get_domain(head_state, Constants.domain_sync_committee(), epoch) + signing_root = Misc.compute_signing_root(head_root, domain) + {:ok, signature} = Bls.sign(privkey, signing_root) + + %Types.SyncCommitteeMessage{ + slot: head_state.slot, + beacon_block_root: head_root, + validator_index: validator_index, + signature: signature + } + end + ################################ # Payload building and proposing diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index c06abb172..3ed4354f5 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -9,7 +9,6 @@ defmodule LambdaEthereumConsensus.ValidatorSet do require Logger - alias LambdaEthereumConsensus.StateTransition.Accessors alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.Validator alias LambdaEthereumConsensus.Validator.Duties @@ -85,6 +84,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do |> update_state(epoch, slot, head_root) |> maybe_attests(epoch, slot, head_root) |> maybe_build_payload(slot + 1, head_root) + |> maybe_sync_committee_broadcasts(slot, head_root) end @doc """ @@ -111,6 +111,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do set |> maybe_attests(epoch, slot, head_root) |> maybe_build_payload(slot + 1, head_root) + |> maybe_sync_committee_broadcasts(slot, head_root) end defp process_tick(set, epoch, {slot, :last_third}) do @@ -138,27 +139,11 @@ defmodule LambdaEthereumConsensus.ValidatorSet do [{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)) + set.duties + |> Duties.compute_duties_for_epochs(epochs_to_calculate, head_root, set.validators) |> merge_duties_and_prune(epoch, set) end - 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), - attesters: Duties.compute_attesters_for_epoch(beacon, epoch, set.validators) - } - - Duties.log_duties_for_epoch(duties, epoch) - - {epoch, duties} - end - defp merge_duties_and_prune(new_duties, current_epoch, set) do set.duties # Remove duties from epoch - 2 or older @@ -199,6 +184,32 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp update_validators(new_validators, set), do: %{set | validators: new_validators} + ############################## + # Sync committee + + defp maybe_sync_committee_broadcasts(set, slot, head_root) do + # Sync committee is broadcasted for the next slot, so we take the duties for the correct epoch. + epoch = Misc.compute_epoch_at_slot(slot + 1) + + case Duties.current_sync_committee(set.duties, epoch, slot) do + [] -> + set + + sync_committee_duties -> + sync_committee_duties + |> Enum.map(&sync_committee_broadcast(&1, slot, head_root, set.validators)) + |> update_duties(set, epoch, :sync_committees, slot) + end + end + + defp sync_committee_broadcast(duty, slot, head_root, validators) do + validators + |> Map.get(duty.validator_index) + |> Validator.sync_committee_message_broadcast(duty, slot, head_root) + + Duties.sync_committee_broadcasted(duty, slot) + end + ############################## # Attestation diff --git a/network_params.yaml b/network_params.yaml index 102796866..76f0325e1 100644 --- a/network_params.yaml +++ b/network_params.yaml @@ -11,3 +11,5 @@ participants: validator_count: 32 cl_max_mem: 4096 keymanager_enabled: true +network_params: + preset: minimal From 324474da76a64e4bd98cd89c7361e63b484d2c15 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Mon, 26 Aug 2024 18:35:14 -0300 Subject: [PATCH 02/23] Added sync_committee_aggregator? function --- lib/lambda_ethereum_consensus/validator/utils.ex | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/lambda_ethereum_consensus/validator/utils.ex b/lib/lambda_ethereum_consensus/validator/utils.ex index d2149226a..953b2dec7 100644 --- a/lib/lambda_ethereum_consensus/validator/utils.ex +++ b/lib/lambda_ethereum_consensus/validator/utils.ex @@ -101,4 +101,19 @@ defmodule LambdaEthereumConsensus.Validator.Utils do "Invalid epoch #{epoch}, should be in the current or next sync committee period" end end + + # `is_sync_committee_aggregator` equivalent + @spec sync_committee_aggregator?(Types.bls_signature()) :: boolean() + def sync_committee_aggregator?(signature) do + modulo = + ChainSpec.get("SYNC_COMMITTEE_SIZE") + |> div(Constants.sync_committee_subnet_count()) + |> div(Constants.target_aggregators_per_sync_subcommittee()) + |> max(1) + + SszEx.hash(signature) + |> binary_part(0, 8) + |> :binary.decode_unsigned(:little) + |> rem(modulo) == 0 + end end From d9c6d832610c53ea24de078864e8115ac49cbede Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Mon, 26 Aug 2024 19:01:26 -0300 Subject: [PATCH 03/23] Added get_sync_committee_selection proof --- .../validator/utils.ex | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/lambda_ethereum_consensus/validator/utils.ex b/lib/lambda_ethereum_consensus/validator/utils.ex index 953b2dec7..c657fbd6b 100644 --- a/lib/lambda_ethereum_consensus/validator/utils.ex +++ b/lib/lambda_ethereum_consensus/validator/utils.ex @@ -102,6 +102,29 @@ defmodule LambdaEthereumConsensus.Validator.Utils do end end + @spec get_sync_committee_selection_proof( + BeaconState.t(), + Types.slot(), + non_neg_integer(), + Bls.privkey() + ) :: + Types.bls_signature() + def get_sync_committee_selection_proof(%BeaconState{} = state, slot, subcommittee_i, privkey) do + domain_sc_selection_proof = Constants.domain_sync_committee_selection_proof() + epoch = Misc.compute_epoch_at_slot(slot) + domain = Accessors.get_domain(state, domain_sc_selection_proof, epoch) + + signing_data = %Types.SyncAggregatorSelectionData{ + slot: slot, + subcommittee_index: subcommittee_i + } + + signing_root = Misc.compute_signing_root(signing_data, domain) + + {:ok, signature} = Bls.sign(privkey, signing_root) + signature + end + # `is_sync_committee_aggregator` equivalent @spec sync_committee_aggregator?(Types.bls_signature()) :: boolean() def sync_committee_aggregator?(signature) do From 7fce4c0e5c06c9a91a61dc8dcbb893f8ced3f921 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 27 Aug 2024 14:06:52 -0300 Subject: [PATCH 04/23] Initial aggregation duties calculation effort --- .../validator/duties.ex | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index c95d3452d..8cc3532d6 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -30,7 +30,15 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @type sync_committee_duty :: %{ last_slot_broadcasted: Types.slot(), subnet_ids: [Types.uint64()], - validator_index: Types.validator_index() + validator_index: Types.validator_index(), + aggregation: %{Type.slot() => [ + %{ + aggregated?: boolean(), + selection_proof: Bls.signature(), + signing_domain: Types.domain(), + subcommittee_index: Types.uint64() + } + ]} } @type attester_duties :: [attester_duty()] @@ -109,18 +117,27 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @spec compute_current_sync_committees(BeaconState.t(), ValidatorSet.validators()) :: sync_committee_duties() defp compute_current_sync_committees(%BeaconState{} = state, validators) do + epoch = Accessors.get_current_epoch(state) + for validator_index <- Map.keys(validators), subnet_ids = Utils.compute_subnets_for_sync_committee(state, validator_index), length(subnet_ids) > 0 do + validator_privkey = Map.get(validators, validator_index).keystore.privkey + + aggregation_data = + sync_committee_aggreagtion_data(state, epoch, subnet_ids, validator_privkey) + %{ last_slot_broadcasted: -1, subnet_ids: subnet_ids, - validator_index: validator_index + validator_index: validator_index, + aggregation: aggregation_data } end end - defp sync_committee_compute_check(_epoch, {_last_epoch, nil}), do: :not_computed + defp sync_committee_compute_check(epoch, {_last_epoch, nil}), + do: {:not_computed, Misc.compute_sync_committee_period(epoch)} defp sync_committee_compute_check(epoch, {last_epoch, last_duties}) do last_period = Misc.compute_sync_committee_period(last_epoch) @@ -131,6 +148,26 @@ defmodule LambdaEthereumConsensus.Validator.Duties do else: {:not_computed, current_period} end + defp sync_committee_aggreagtion_data(beacon_state, epoch, subnet_ids, validator_privkey) do + {start_slot, end_slot} = boundary_slots(epoch) + + for slot <- start_slot..end_slot, + subcommittee_index <- subnet_ids, reduce: %{} do + acc -> + proof = Utils.get_sync_committee_selection_proof(beacon_state, slot, subcommittee_index, validator_privkey) + domain_sc_selection_proof = Constants.domain_sync_committee_selection_proof() + domain = Accessors.get_domain(beacon_state, domain_sc_selection_proof, epoch) + + if Utils.sync_committee_aggregator?(proof) do + aggregation = %{aggregated?: false, selection_proof: proof, signing_domain: domain, subcommittee_index: subcommittee_index} + + Map.update(acc, slot, [], &[aggregation | &1]) + else + acc + end + end + end + @spec compute_attesters_for_epoch(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) :: attester_duties_per_slot() defp compute_attesters_for_epoch(%BeaconState{} = state, epoch, validators) do @@ -264,10 +301,11 @@ defmodule LambdaEthereumConsensus.Validator.Duties do for %{ subnet_ids: si, - validator_index: vi + validator_index: vi, + aggregation: agg } <- sync_committees do - Logger.debug( - "[Duties] Sync committee for epoch: #{epoch}, validator_index: #{vi} will broadcast on subnet_ids: #{inspect(si)}." + Logger.info( + "[Duties] Sync committee for epoch: #{epoch}, validator_index: #{vi} will broadcast on subnet_ids: #{inspect(si)}.\n#{inspect(agg, pretty: true)}" ) end From e8bfecf486604af9e47c52b19d1e117976c8d55e Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 28 Aug 2024 11:55:39 -0300 Subject: [PATCH 05/23] Make the sync committee aggregator check work at calculating duties, and do it for every epoch --- .../validator/duties.ex | 57 ++++++++++++------- .../validator/utils.ex | 3 +- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 8cc3532d6..c461eca2f 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -31,14 +31,16 @@ defmodule LambdaEthereumConsensus.Validator.Duties do last_slot_broadcasted: Types.slot(), subnet_ids: [Types.uint64()], validator_index: Types.validator_index(), - aggregation: %{Type.slot() => [ - %{ - aggregated?: boolean(), - selection_proof: Bls.signature(), - signing_domain: Types.domain(), - subcommittee_index: Types.uint64() - } - ]} + aggregation: %{ + Types.slot() => [ + %{ + aggregated?: boolean(), + selection_proof: Bls.signature(), + signing_domain: Types.domain(), + subcommittee_index: Types.uint64() + } + ] + } } @type attester_duties :: [attester_duty()] @@ -78,16 +80,17 @@ defmodule LambdaEthereumConsensus.Validator.Duties do new_proposers = compute_proposers_for_epoch(beacon, epoch, validators) new_attesters = compute_attesters_for_epoch(beacon, epoch, validators) - new_sync_committees = - case sync_committee_compute_check(epoch, {last_epoch, Map.get(duties_map, last_epoch)}) do - {:already_computed, sync_committees} -> - sync_committees + new_sync_committees = compute_current_sync_committees(beacon, validators) + + # case sync_committee_compute_check(epoch, {last_epoch, Map.get(duties_map, last_epoch)}) do + # {:already_computed, sync_committees} -> + # sync_committees - {:not_computed, period} -> - Logger.debug("[Duties] Computing sync committees for period: #{period}.") + # {:not_computed, period} -> + # Logger.debug("[Duties] Computing sync committees for period: #{period}.") - compute_current_sync_committees(beacon, validators) - end + # compute_current_sync_committees(beacon, validators) + # end new_duties = %{ proposers: new_proposers, @@ -120,6 +123,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do epoch = Accessors.get_current_epoch(state) for validator_index <- Map.keys(validators), + # TODO: This could be computed just once per period! subnet_ids = Utils.compute_subnets_for_sync_committee(state, validator_index), length(subnet_ids) > 0 do validator_privkey = Map.get(validators, validator_index).keystore.privkey @@ -151,17 +155,28 @@ defmodule LambdaEthereumConsensus.Validator.Duties do defp sync_committee_aggreagtion_data(beacon_state, epoch, subnet_ids, validator_privkey) do {start_slot, end_slot} = boundary_slots(epoch) - for slot <- start_slot..end_slot, - subcommittee_index <- subnet_ids, reduce: %{} do + for slot <- start_slot..end_slot, subcommittee_index <- subnet_ids, reduce: %{} do acc -> - proof = Utils.get_sync_committee_selection_proof(beacon_state, slot, subcommittee_index, validator_privkey) + proof = + Utils.get_sync_committee_selection_proof( + beacon_state, + slot, + subcommittee_index, + validator_privkey + ) + domain_sc_selection_proof = Constants.domain_sync_committee_selection_proof() domain = Accessors.get_domain(beacon_state, domain_sc_selection_proof, epoch) if Utils.sync_committee_aggregator?(proof) do - aggregation = %{aggregated?: false, selection_proof: proof, signing_domain: domain, subcommittee_index: subcommittee_index} + aggregation = %{ + aggregated?: false, + selection_proof: proof, + signing_domain: domain, + subcommittee_index: subcommittee_index + } - Map.update(acc, slot, [], &[aggregation | &1]) + Map.update(acc, slot, [aggregation], &[aggregation | &1]) else acc end diff --git a/lib/lambda_ethereum_consensus/validator/utils.ex b/lib/lambda_ethereum_consensus/validator/utils.ex index c657fbd6b..a4e969913 100644 --- a/lib/lambda_ethereum_consensus/validator/utils.ex +++ b/lib/lambda_ethereum_consensus/validator/utils.ex @@ -119,7 +119,8 @@ defmodule LambdaEthereumConsensus.Validator.Utils do subcommittee_index: subcommittee_i } - signing_root = Misc.compute_signing_root(signing_data, domain) + signing_root = + Misc.compute_signing_root(signing_data, Types.SyncAggregatorSelectionData, domain) {:ok, signature} = Bls.sign(privkey, signing_root) signature From e6918c33c88c52ee81f46bf4770f12c7adb1079c Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 28 Aug 2024 12:07:12 -0300 Subject: [PATCH 06/23] Fixed warning temporarily --- .../validator/duties.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index c461eca2f..3e2a66f20 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -140,17 +140,17 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end end - defp sync_committee_compute_check(epoch, {_last_epoch, nil}), - do: {:not_computed, Misc.compute_sync_committee_period(epoch)} + # defp sync_committee_compute_check(epoch, {_last_epoch, nil}), + # do: {:not_computed, Misc.compute_sync_committee_period(epoch)} - defp sync_committee_compute_check(epoch, {last_epoch, last_duties}) do - last_period = Misc.compute_sync_committee_period(last_epoch) - current_period = Misc.compute_sync_committee_period(epoch) + # defp sync_committee_compute_check(epoch, {last_epoch, last_duties}) do + # last_period = Misc.compute_sync_committee_period(last_epoch) + # current_period = Misc.compute_sync_committee_period(epoch) - if last_period == current_period, - do: {:already_computed, last_duties.sync_committees}, - else: {:not_computed, current_period} - end + # if last_period == current_period, + # do: {:already_computed, last_duties.sync_committees}, + # else: {:not_computed, current_period} + # end defp sync_committee_aggreagtion_data(beacon_state, epoch, subnet_ids, validator_privkey) do {start_slot, end_slot} = boundary_slots(epoch) From b39c5d43dc7856d565c996f8a1bbd52bf545ca89 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 28 Aug 2024 15:33:12 -0300 Subject: [PATCH 07/23] Addition of collect and start looking into aggregate publish --- .../p2p/gossip/sync_committee.ex | 42 ++++++++++++++++++- .../validator/validator.ex | 15 ++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex index 9d21a2ff0..81c4afa79 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex @@ -8,15 +8,35 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do require Logger + @spec join([non_neg_integer()]) :: :ok + def join(subnet_ids) when is_list(subnet_ids) do + for subnet_id <- subnet_ids do + topic = topic(subnet_id) + Libp2pPort.join_topic(topic) + + P2P.Metadata.set_syncnet(subnet_id) + end + + P2P.Metadata.get_metadata() + |> update_enr() + end + @spec publish(Types.SyncCommitteeMessage.t(), [non_neg_integer()]) :: :ok def publish(%Types.SyncCommitteeMessage{} = sync_committee_msg, subnet_ids) do - Enum.each(subnet_ids, fn subnet_id -> + for subnet_id <- subnet_ids do topic = topic(subnet_id) {:ok, encoded} = SszEx.encode(sync_committee_msg, Types.SyncCommitteeMessage) {:ok, message} = :snappyer.compress(encoded) Libp2pPort.publish(topic, message) - end) + end + end + + @spec collect(non_neg_integer(), Types.SyncCommitteeMessage.t()) :: :ok + def collect(subnet_id, _messages) do + join(subnet_id) + #SubnetInfo.new_subnet_with_attestation(subnet_id, attestation) + Libp2pPort.async_subscribe_to_topic(topic(subnet_id), __MODULE__) end defp topic(subnet_id) do @@ -24,4 +44,22 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do fork_context = ForkChoice.get_fork_digest() |> Base.encode16(case: :lower) "/eth2/#{fork_context}/sync_committee_#{subnet_id}/ssz_snappy" end + + defp update_enr(%{attnets: attnets, syncnets: syncnets}) do + enr_fork_id = compute_enr_fork_id() + Libp2pPort.update_enr(enr_fork_id, attnets, syncnets) + end + + defp compute_enr_fork_id() do + current_version = ForkChoice.get_fork_version() + + fork_digest = + Misc.compute_fork_digest(current_version, ChainSpec.get_genesis_validators_root()) + + %Types.EnrForkId{ + fork_digest: fork_digest, + next_fork_version: current_version, + next_fork_epoch: Constants.far_future_epoch() + } + end end diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index c83fe858c..c67134195 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -232,10 +232,21 @@ defmodule LambdaEthereumConsensus.Validator do head_state = BlockStates.get_state_info!(head_root).beacon_state |> go_to_slot(slot) log_debug(validator_index, "broadcasting sync committee message", slot: slot) - head_state - |> get_sync_committee_message(head_root, validator_index, keystore.privkey) + message = get_sync_committee_message(head_state, head_root, validator_index, keystore.privkey) + + message |> Gossip.SyncCommittee.publish(subnet_ids) |> log_info_result(validator_index, "published sync committee message", slot: slot) + + aggregate_slot = current_duty |> Map.get(:aggregation) |> Map.get(slot) + if aggregate_slot && length(aggregate_slot) > 0 do + log_debug(validator_index, "collecting for future contribution", slot: slot) + + aggregate_slot + |> Enum.map(& &1.subcommittee_index) + |> Gossip.SyncCommittee.collect(message) + |> log_debug_result(validator_index, "collected sync committee messages", slot: slot) + end end @spec get_sync_committee_message( From 7573e024f39eee18a8543c2b75da24fac4492363 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 28 Aug 2024 17:48:57 -0300 Subject: [PATCH 08/23] Added sync_subnet_info and renamed the old subnet_info module --- .../p2p/gossip/attestation.ex | 8 +- .../p2p/gossip/sync_committee.ex | 51 +++++++- .../validator/duties.ex | 2 +- .../validator/validator.ex | 1 + lib/types/subnet_info.ex | 8 +- lib/types/sync_subnet_info.ex | 119 ++++++++++++++++++ test/unit/subnet_info.exs | 14 +-- 7 files changed, 182 insertions(+), 21 deletions(-) create mode 100644 lib/types/sync_subnet_info.ex diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex b/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex index 5e6943a60..b5c7f5a43 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex @@ -9,7 +9,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.Attestation do alias LambdaEthereumConsensus.P2P alias LambdaEthereumConsensus.P2P.Gossip.Handler alias LambdaEthereumConsensus.StateTransition.Misc - alias Types.SubnetInfo + alias Types.AttSubnetInfo @behaviour Handler @@ -36,7 +36,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.Attestation do # TODO: validate before accepting Libp2pPort.validate_message(msg_id, :accept) - SubnetInfo.add_attestation!(subnet_id, attestation) + AttSubnetInfo.add_attestation!(subnet_id, attestation) else {:error, _} -> Libp2pPort.validate_message(msg_id, :reject) end @@ -70,7 +70,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.Attestation do @spec collect(non_neg_integer(), Types.Attestation.t()) :: :ok def collect(subnet_id, attestation) do join(subnet_id) - SubnetInfo.new_subnet_with_attestation(subnet_id, attestation) + AttSubnetInfo.new_subnet_with_attestation(subnet_id, attestation) Libp2pPort.async_subscribe_to_topic(topic(subnet_id), __MODULE__) end @@ -81,7 +81,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.Attestation do topic = topic(subnet_id) Libp2pPort.leave_topic(topic) Libp2pPort.join_topic(topic) - SubnetInfo.stop_collecting(subnet_id) + AttSubnetInfo.stop_collecting(subnet_id) end defp topic(subnet_id) do diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex index 81c4afa79..7dcbbf90c 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex @@ -2,9 +2,17 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do @moduledoc """ This module handles sync committee from specific gossip subnets. Used by validators to fulfill aggregation duties. + + TODO: THIS IS EXACTLY THE SAME AS ATTSUBNET. ALSO NEEDS TESTS """ alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.Libp2pPort + alias LambdaEthereumConsensus.P2P + alias LambdaEthereumConsensus.P2P.Gossip.Handler + alias LambdaEthereumConsensus.StateTransition.Misc + alias Types.SyncSubnetInfo + + @behaviour Handler require Logger @@ -21,6 +29,26 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do |> update_enr() end + @impl true + def handle_gossip_message(store, topic, msg_id, message) do + handle_gossip_message(topic, msg_id, message) + store + end + + def handle_gossip_message(topic, msg_id, message) do + subnet_id = extract_subnet_id(topic) + + with {:ok, uncompressed} <- :snappyer.decompress(message), + {:ok, sync_committee_msg} <- Ssz.from_ssz(uncompressed, Types.Attestation) do + # TODO: validate before accepting + Libp2pPort.validate_message(msg_id, :accept) + + SyncSubnetInfo.add_message!(subnet_id, sync_committee_msg) + else + {:error, _} -> Libp2pPort.validate_message(msg_id, :reject) + end + end + @spec publish(Types.SyncCommitteeMessage.t(), [non_neg_integer()]) :: :ok def publish(%Types.SyncCommitteeMessage{} = sync_committee_msg, subnet_ids) do for subnet_id <- subnet_ids do @@ -30,13 +58,20 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do {:ok, message} = :snappyer.compress(encoded) Libp2pPort.publish(topic, message) end + + :ok end - @spec collect(non_neg_integer(), Types.SyncCommitteeMessage.t()) :: :ok - def collect(subnet_id, _messages) do - join(subnet_id) - #SubnetInfo.new_subnet_with_attestation(subnet_id, attestation) - Libp2pPort.async_subscribe_to_topic(topic(subnet_id), __MODULE__) + @spec collect([non_neg_integer()], Types.SyncCommitteeMessage.t()) :: :ok + def collect(subnet_ids, message) do + join(subnet_ids) + + for subnet_id <- subnet_ids do + SyncSubnetInfo.new_subnet_with_message(subnet_id, message) + Libp2pPort.async_subscribe_to_topic(topic(subnet_id), __MODULE__) + end + + :ok end defp topic(subnet_id) do @@ -62,4 +97,10 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do next_fork_epoch: Constants.far_future_epoch() } end + + @subnet_id_start byte_size("/eth2/00000000/sync_committee_") + + defp extract_subnet_id(<<_::binary-size(@subnet_id_start)>> <> id_with_trailer) do + id_with_trailer |> String.trim_trailing("/ssz_snappy") |> String.to_integer() + end end diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 3e2a66f20..dd24793e2 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -75,7 +75,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do # calculating them on the fly. Accessors.maybe_prefetch_committees(beacon, epoch) - last_epoch = Map.keys(duties_map) |> Enum.max(fn -> 0 end) + _last_epoch = Map.keys(duties_map) |> Enum.max(fn -> 0 end) new_proposers = compute_proposers_for_epoch(beacon, epoch, validators) new_attesters = compute_attesters_for_epoch(beacon, epoch, validators) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index c67134195..a1d303eb1 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -239,6 +239,7 @@ defmodule LambdaEthereumConsensus.Validator do |> log_info_result(validator_index, "published sync committee message", slot: slot) aggregate_slot = current_duty |> Map.get(:aggregation) |> Map.get(slot) + if aggregate_slot && length(aggregate_slot) > 0 do log_debug(validator_index, "collecting for future contribution", slot: slot) diff --git a/lib/types/subnet_info.ex b/lib/types/subnet_info.ex index 1b90ce0c0..6f0c2594b 100644 --- a/lib/types/subnet_info.ex +++ b/lib/types/subnet_info.ex @@ -1,4 +1,4 @@ -defmodule Types.SubnetInfo do +defmodule Types.AttSubnetInfo do @moduledoc """ Struct to hold subnet attestations for easier db storing: - data: An attestation data. @@ -13,7 +13,7 @@ defmodule Types.SubnetInfo do attestations: list(Types.Attestation.t()) } - @subnet_prefix "subnet" + @subnet_prefix "att_subnet" @doc """ Creates a SubnetInfo from an Attestation and stores it into the database. @@ -73,7 +73,7 @@ defmodule Types.SubnetInfo do {Db.put( key, value - ), %{module: "subnet", action: "persist"}} + ), %{module: @subnet_prefix, action: "persist"}} end) end @@ -82,7 +82,7 @@ defmodule Types.SubnetInfo do result = :telemetry.span([:db, :latency], %{}, fn -> {Db.get(@subnet_prefix <> Integer.to_string(subnet_id)), - %{module: "subnet", action: "fetch"}} + %{module: @subnet_prefix, action: "fetch"}} end) case result do diff --git a/lib/types/sync_subnet_info.ex b/lib/types/sync_subnet_info.ex new file mode 100644 index 000000000..64b3cc946 --- /dev/null +++ b/lib/types/sync_subnet_info.ex @@ -0,0 +1,119 @@ +defmodule Types.SyncSubnetInfo do + @moduledoc """ + Struct to hold subnet messages for easier db storing: + - data: A Sync Committee message data (slot + root). + - messages: List of all the collected SyncCommitteeMessages. + """ + alias LambdaEthereumConsensus.Store.Db + + defstruct [:data, :messages] + + @type t :: %__MODULE__{ + data: {Types.slot(), Types.root()}, + messages: list(Types.SyncCommitteeMessage.t()) + } + + @subnet_prefix "sync_subnet" + + @doc """ + Creates a SubnetInfo from an SyncCommitteeMessage and stores it into the database. + The message is typically built by a validator before starting to collect others' messages. + This message will be used to filter other added messages. + """ + @spec new_subnet_with_message(non_neg_integer(), Types.SyncCommitteeMessage.t()) :: :ok + def new_subnet_with_message( + subnet_id, + %Types.SyncCommitteeMessage{slot: slot, beacon_block_root: root} = message + ) do + new_subnet_info = %__MODULE__{data: {slot, root}, messages: [message]} + persist_subnet_info(subnet_id, new_subnet_info) + end + + @doc """ + Removes the associated SubnetInfo from the database and returns all the collected messages. + """ + @spec stop_collecting(non_neg_integer()) :: + {:ok, list(Types.SyncCommitteeMessage.t())} | {:error, String.t()} + def stop_collecting(subnet_id) do + case fetch_subnet_info(subnet_id) do + {:ok, subnet_info} -> + delete_subnet(subnet_id) + {:ok, subnet_info.messages} + + :not_found -> + {:error, "subnet not joined"} + end + end + + @doc """ + Adds a new SyncCommitteeMessage to the SubnetInfo if the message's data matches the base one. + Assumes that the SubnetInfo already exists. + """ + @spec add_message!(non_neg_integer(), Types.SyncCommitteeMessage.t()) :: :ok + def add_message!( + subnet_id, + %Types.SyncCommitteeMessage{slot: slot, beacon_block_root: root} = message + ) do + subnet_info = fetch_subnet_info!(subnet_id) + + if subnet_info.data == {slot, root} do + new_subnet_info = %__MODULE__{ + subnet_info + | messages: [message | subnet_info.messages] + } + + persist_subnet_info(subnet_id, new_subnet_info) + end + end + + ########################## + ### Database Calls + ########################## + + @spec persist_subnet_info(non_neg_integer(), t()) :: :ok + defp persist_subnet_info(subnet_id, subnet_info) do + key = @subnet_prefix <> Integer.to_string(subnet_id) + value = encode(subnet_info) + + :telemetry.span([:db, :latency], %{}, fn -> + {Db.put( + key, + value + ), %{module: @subnet_prefix, action: "persist"}} + end) + end + + @spec fetch_subnet_info(non_neg_integer()) :: {:ok, t()} | :not_found + defp fetch_subnet_info(subnet_id) do + result = + :telemetry.span([:db, :latency], %{}, fn -> + {Db.get(@subnet_prefix <> Integer.to_string(subnet_id)), + %{module: @subnet_prefix, action: "fetch"}} + end) + + case result do + {:ok, binary} -> {:ok, decode(binary)} + :not_found -> result + end + end + + @spec fetch_subnet_info!(non_neg_integer()) :: t() + defp fetch_subnet_info!(subnet_id) do + {:ok, subnet_info} = fetch_subnet_info(subnet_id) + subnet_info + end + + @spec delete_subnet(non_neg_integer()) :: :ok + defp delete_subnet(subnet_id), do: Db.delete(@subnet_prefix <> Integer.to_string(subnet_id)) + + @spec encode(t()) :: binary() + defp encode(%__MODULE__{} = subnet_info) do + {subnet_info.data, subnet_info.messages} |> :erlang.term_to_binary() + end + + @spec decode(binary()) :: t() + defp decode(bin) do + {data, messages} = :erlang.binary_to_term(bin) + %__MODULE__{data: data, messages: messages} + end +end diff --git a/test/unit/subnet_info.exs b/test/unit/subnet_info.exs index be4829cd9..dac626357 100644 --- a/test/unit/subnet_info.exs +++ b/test/unit/subnet_info.exs @@ -1,13 +1,13 @@ defmodule Unit.AttestationTest do alias LambdaEthereumConsensus.Store.Db alias Types.AttestationData + alias Types.AttSubnetInfo alias Types.Checkpoint - alias Types.SubnetInfo use ExUnit.Case use Patch - doctest SubnetInfo + doctest AttSubnetInfo setup %{tmp_dir: tmp_dir} do start_link_supervised!({Db, dir: tmp_dir}) @@ -50,9 +50,9 @@ defmodule Unit.AttestationTest do signature: <<>> } - SubnetInfo.new_subnet_with_attestation(subnet_id, expected_attestation) + AttSubnetInfo.new_subnet_with_attestation(subnet_id, expected_attestation) - {:ok, attestations} = SubnetInfo.stop_collecting(subnet_id) + {:ok, attestations} = AttSubnetInfo.stop_collecting(subnet_id) assert [expected_attestation] == attestations end @@ -81,11 +81,11 @@ defmodule Unit.AttestationTest do 2, 2, 2, 2, 2, 2, 2, 2, 2>> } - SubnetInfo.new_subnet_with_attestation(subnet_id, attestation1) + AttSubnetInfo.new_subnet_with_attestation(subnet_id, attestation1) - SubnetInfo.add_attestation!(subnet_id, attestation2) + AttSubnetInfo.add_attestation!(subnet_id, attestation2) - {:ok, attestations} = SubnetInfo.stop_collecting(subnet_id) + {:ok, attestations} = AttSubnetInfo.stop_collecting(subnet_id) assert [attestation2, attestation1] == attestations end From 6c864467eb94ebaf24cfe93ebf3c976bd3f77e5b Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 28 Aug 2024 18:22:55 -0300 Subject: [PATCH 09/23] Fixed typo on handle_gossip --- lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex index 7dcbbf90c..6661377f5 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex @@ -39,7 +39,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do subnet_id = extract_subnet_id(topic) with {:ok, uncompressed} <- :snappyer.decompress(message), - {:ok, sync_committee_msg} <- Ssz.from_ssz(uncompressed, Types.Attestation) do + {:ok, sync_committee_msg} <- Ssz.from_ssz(uncompressed, Types.SyncCommitteeMessage) do # TODO: validate before accepting Libp2pPort.validate_message(msg_id, :accept) From 1c00037fd7558f75101cd31dafd104ceb0672ee7 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 29 Aug 2024 19:22:49 -0300 Subject: [PATCH 10/23] Added all the path to reach to aggregation, aggregate still WIP --- .../p2p/gossip/sync_committee.ex | 11 +++++++ .../validator/duties.ex | 17 ++++++++++ .../validator/validator.ex | 32 +++++++++++++++++++ .../validator/validator_set.ex | 26 +++++++++++++-- 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex index 6661377f5..dade74b98 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex @@ -74,6 +74,17 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do :ok end + @spec stop_collecting(non_neg_integer()) :: + {:ok, list(Types.Attestation.t())} | {:error, String.t()} + def stop_collecting(subnet_id) do + # TODO from Attestation: implement some way to unsubscribe without leaving the topic + # TODO: This handle individual subnet_id while the other ones handle lists. + topic = topic(subnet_id) + Libp2pPort.leave_topic(topic) + Libp2pPort.join_topic(topic) + SyncSubnetInfo.stop_collecting(subnet_id) + end + defp topic(subnet_id) do # TODO: this doesn't take into account fork digest changes fork_context = ForkChoice.get_fork_digest() |> Base.encode16(case: :lower) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index c7fae79c9..7e3fc3e66 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -264,6 +264,15 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end end + @spec current_sync_aggregators(duties(), Types.epoch(), Types.slot()) :: sync_committee_duties() + def current_sync_aggregators(duties, epoch, slot) do + for %{aggregation: aggregation} = duty <- sync_committee(duties, epoch), + Map.get(aggregation, slot), + Enum.any?(aggregation[slot], &(not Map.get(&1, :aggregated?))) do + duty + end + end + @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 @@ -307,6 +316,14 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @spec sync_committee_broadcasted(sync_committee_duty(), Types.slot()) :: sync_committee_duty() def sync_committee_broadcasted(duty, slot), do: Map.put(duty, :last_slot_broadcasted, slot) + @spec sync_committee_aggregated(sync_committee_duty(), Types.slot()) :: sync_committee_duty() + def sync_committee_aggregated(duty, slot) do + updated_aggreagtion = + Enum.map(duty.aggregation[slot], fn agg -> Map.put(agg, :aggregated?, true) end) + + put_in(duty, [:aggregation, slot], updated_aggreagtion) + end + ############################ # Helpers diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index a1d303eb1..960c5689f 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -271,6 +271,38 @@ defmodule LambdaEthereumConsensus.Validator do } end + @spec publish_sync_aggregate(t(), Duties.sync_committee_duty(), Types.slot()) :: :ok + def publish_sync_aggregate(%{index: validator_index, keystore: _keystore}, duty, slot) do + for subnet_id <- duty.subnet_ids do + case Gossip.SyncCommittee.stop_collecting(subnet_id) do + {:ok, messages} -> + log_md = [slot: slot, messages: messages] + log_info(validator_index, "publishing sync committee aggregate", log_md) + + # aggregate_messages(messages, subnet_id) + # |> append_proof(duty.selection_proof, validator_index) + # |> append_signature(duty.signing_domain, keystore) + # |> Gossip.SyncCommittee.publish_aggregate() + # |> log_info_result(validator_index, "published sync committee aggregate", log_md) + + {:error, reason} -> + log_error(validator_index, "stop collecting sync committee messages", reason) + :ok + end + end + + :ok + end + + # defp aggregate_messages(messages, subnet_id) do + # %Types.SyncCommitteeContribution{ + # slot: List.first(messages).slot, + # beacon_block_root: List.first(messages).beacon_block_root, + # subcommittee_index: subnet_id, + # signature: Bls.aggregate(List.map(messages, & &1.signature)) + # } + # end + ################################ # Payload building and proposing diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 367f82ebd..6d7942337 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -110,7 +110,9 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end defp process_tick(set, epoch, {slot, :last_third}) do - maybe_publish_aggregates(set, epoch, slot) + set + |> maybe_publish_attestation_aggregates(epoch, slot) + |> maybe_publish_sync_aggregates(epoch, slot) end ############################## @@ -197,6 +199,18 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end end + defp maybe_publish_sync_aggregates(set, epoch, slot) do + case Duties.current_sync_aggregators(set.duties, epoch, slot) do + [] -> + set + + aggregator_duties -> + aggregator_duties + |> Enum.map(&publish_sync_aggregate(&1, slot, set.validators)) + |> update_duties(set, epoch, :sync_committees, slot) + end + end + defp sync_committee_broadcast(duty, slot, head_root, validators) do validators |> Map.get(duty.validator_index) @@ -205,6 +219,14 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Duties.sync_committee_broadcasted(duty, slot) end + defp publish_sync_aggregate(duty, slot, validators) do + validators + |> Map.get(duty.validator_index) + |> Validator.publish_sync_aggregate(duty, slot) + + Duties.sync_committee_aggregated(duty, slot) + end + ############################## # Attestation @@ -220,7 +242,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end end - defp maybe_publish_aggregates(set, epoch, slot) do + defp maybe_publish_attestation_aggregates(set, epoch, slot) do case Duties.current_aggregators(set.duties, epoch, slot) do [] -> set From 1a59abf95b9b02c662b384081c8688209e338eef Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Mon, 2 Sep 2024 22:08:59 -0300 Subject: [PATCH 11/23] Contribution generated and published, but not validated to be picked up --- .../p2p/gossip/sync_committee.ex | 9 ++ .../state_transition/accessors.ex | 22 +++++ .../validator/duties.ex | 4 +- .../validator/utils.ex | 34 ++++---- .../validator/validator.ex | 83 +++++++++++++++---- .../validator/validator_set.ex | 12 +-- lib/utils/bit_list.ex | 10 ++- 7 files changed, 134 insertions(+), 40 deletions(-) diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex index dade74b98..8a91de6c3 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex @@ -62,6 +62,15 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do :ok end + @spec publish_contribution(Types.SignedContributionAndProof.t()) :: :ok + def publish_contribution(%Types.SignedContributionAndProof{} = signed_contribution) do + fork_context = ForkChoice.get_fork_digest() |> Base.encode16(case: :lower) + topic = "/eth2/#{fork_context}/sync_committee_contribution_and_proof/ssz_snappy" + {:ok, encoded} = SszEx.encode(signed_contribution, Types.SignedContributionAndProof) + {:ok, message} = :snappyer.compress(encoded) + Libp2pPort.publish(topic, message) + end + @spec collect([non_neg_integer()], Types.SyncCommitteeMessage.t()) :: :ok def collect(subnet_ids, message) do join(subnet_ids) diff --git a/lib/lambda_ethereum_consensus/state_transition/accessors.ex b/lib/lambda_ethereum_consensus/state_transition/accessors.ex index 3d631c80f..a2b041aec 100644 --- a/lib/lambda_ethereum_consensus/state_transition/accessors.ex +++ b/lib/lambda_ethereum_consensus/state_transition/accessors.ex @@ -19,6 +19,28 @@ defmodule LambdaEthereumConsensus.StateTransition.Accessors do @max_random_byte 2 ** 8 - 1 + @doc """ + Compute the correct sync committee for a given `epoch`. + """ + def get_sync_committee_for_epoch!(%BeaconState{} = state, epoch) do + sync_committee_period = Misc.compute_sync_committee_period(epoch) + current_epoch = get_current_epoch(state) + current_sync_committee_period = Misc.compute_sync_committee_period(current_epoch) + next_sync_committee_period = current_sync_committee_period + 1 + + case sync_committee_period do + ^current_sync_committee_period -> + state.current_sync_committee + + ^next_sync_committee_period -> + state.next_sync_committee + + _ -> + raise ArgumentError, + "Invalid epoch #{epoch}, should be in the current or next sync committee period" + end + end + @doc """ Return the next sync committee, with possible pubkey duplicates. """ diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 7e3fc3e66..e0ba1ec19 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -168,8 +168,8 @@ defmodule LambdaEthereumConsensus.Validator.Duties do validator_privkey ) - domain_sc_selection_proof = Constants.domain_sync_committee_selection_proof() - domain = Accessors.get_domain(beacon_state, domain_sc_selection_proof, epoch) + domain_contribution_and_proof = Constants.domain_contribution_and_proof() + domain = Accessors.get_domain(beacon_state, domain_contribution_and_proof, epoch) if Utils.sync_committee_aggregator?(proof) do aggregation = %{ diff --git a/lib/lambda_ethereum_consensus/validator/utils.ex b/lib/lambda_ethereum_consensus/validator/utils.ex index a4e969913..7595e57bf 100644 --- a/lib/lambda_ethereum_consensus/validator/utils.ex +++ b/lib/lambda_ethereum_consensus/validator/utils.ex @@ -82,24 +82,28 @@ defmodule LambdaEthereumConsensus.Validator.Utils do @spec assigned_to_sync_committee?(BeaconState.t(), Types.epoch(), Types.validator_index()) :: boolean() def assigned_to_sync_committee?(%BeaconState{} = state, epoch, validator_index) do - sync_committee_period = Misc.compute_sync_committee_period(epoch) - current_epoch = Accessors.get_current_epoch(state) - current_sync_committee_period = Misc.compute_sync_committee_period(current_epoch) - next_sync_committee_period = current_sync_committee_period + 1 + target_pubkey = state.validators |> Map.get(validator_index, %{}) |> Map.get(:pubkey) - pubkey = state.validators[validator_index].pubkey - - case sync_committee_period do - ^current_sync_committee_period -> - Enum.member?(state.current_sync_committee.pubkeys, pubkey) + target_pubkey && target_pubkey in Accessors.get_sync_committee_for_epoch!(state, epoch) + end - ^next_sync_committee_period -> - Enum.member?(state.next_sync_committee.pubkeys, pubkey) + @spec participants_per_sync_subcommittee(BeaconState.t(), Types.epoch()) :: + %{non_neg_integer() => [Bls.pubkey()]} + def participants_per_sync_subcommittee(state, epoch) do + sync_committee_subnet_size = + div(ChainSpec.get("SYNC_COMMITTEE_SIZE"), Constants.sync_committee_subnet_count()) - _ -> - raise ArgumentError, - "Invalid epoch #{epoch}, should be in the current or next sync committee period" - end + state + |> Accessors.get_sync_committee_for_epoch!(epoch) + |> Map.get(:pubkeys) + |> Enum.chunk_every(sync_committee_subnet_size) + |> Enum.with_index() + |> Map.new(fn {pubkeys, i} -> + indices_by_pubkeys = + pubkeys |> Enum.with_index() |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) + + {i, indices_by_pubkeys} + end) end @spec get_sync_committee_selection_proof( diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 960c5689f..4f8f6cdea 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -271,19 +271,40 @@ defmodule LambdaEthereumConsensus.Validator do } end - @spec publish_sync_aggregate(t(), Duties.sync_committee_duty(), Types.slot()) :: :ok - def publish_sync_aggregate(%{index: validator_index, keystore: _keystore}, duty, slot) do + @spec publish_sync_aggregate( + t(), + Duties.sync_committee_duty(), + Types.epoch(), + Types.slot(), + Types.root() + ) :: :ok + def publish_sync_aggregate( + %{index: validator_index, keystore: keystore}, + duty, + epoch, + slot, + head_root + ) do + head_state = BlockStates.get_state_info!(head_root).beacon_state |> go_to_slot(slot) + for subnet_id <- duty.subnet_ids do case Gossip.SyncCommittee.stop_collecting(subnet_id) do {:ok, messages} -> log_md = [slot: slot, messages: messages] log_info(validator_index, "publishing sync committee aggregate", log_md) - # aggregate_messages(messages, subnet_id) - # |> append_proof(duty.selection_proof, validator_index) - # |> append_signature(duty.signing_domain, keystore) - # |> Gossip.SyncCommittee.publish_aggregate() - # |> log_info_result(validator_index, "published sync committee aggregate", log_md) + aggregation_bits = + sync_committee_aggregation_bits(head_state, subnet_id, epoch, messages) + + aggregation_duty = + Enum.find(duty.aggregation[slot], &(&1.subcommittee_index == subnet_id)) + + messages + |> sync_committee_contribution(subnet_id, aggregation_bits) + |> append_sync_proof(aggregation_duty.selection_proof, validator_index) + |> append_sync_signature(aggregation_duty.signing_domain, keystore) + |> Gossip.SyncCommittee.publish_contribution() + |> log_info_result(validator_index, "published sync committee aggregate", log_md) {:error, reason} -> log_error(validator_index, "stop collecting sync committee messages", reason) @@ -294,14 +315,46 @@ defmodule LambdaEthereumConsensus.Validator do :ok end - # defp aggregate_messages(messages, subnet_id) do - # %Types.SyncCommitteeContribution{ - # slot: List.first(messages).slot, - # beacon_block_root: List.first(messages).beacon_block_root, - # subcommittee_index: subnet_id, - # signature: Bls.aggregate(List.map(messages, & &1.signature)) - # } - # end + defp sync_committee_contribution(messages, subnet_id, aggregation_bits) do + {:ok, signature} = Bls.aggregate(Enum.map(messages, & &1.signature)) + + %Types.SyncCommitteeContribution{ + slot: List.first(messages).slot, + beacon_block_root: List.first(messages).beacon_block_root, + subcommittee_index: subnet_id, + aggregation_bits: aggregation_bits, + signature: signature + } + end + + defp append_sync_proof(contribution, proof, validator_index) do + %Types.ContributionAndProof{ + aggregator_index: validator_index, + contribution: contribution, + selection_proof: proof + } + end + + defp append_sync_signature(contribution_and_proof, signing_domain, %{privkey: privkey}) do + signing_root = Misc.compute_signing_root(contribution_and_proof, signing_domain) + {:ok, signature} = Bls.sign(privkey, signing_root) + %Types.SignedContributionAndProof{message: contribution_and_proof, signature: signature} + end + + defp sync_committee_aggregation_bits(state, subnet_id, epoch, messages) do + indexes_in_subcommittee = + state + |> Utils.participants_per_sync_subcommittee(epoch) + |> Map.get(subnet_id) + |> Map.new(fn {pubkey, indexes} -> {fetch_validator_index(state, pubkey), indexes} end) + + aggregation_bits = indexes_in_subcommittee |> Enum.count() |> BitList.zero() + + for %{validator_index: validator_index} <- messages, reduce: aggregation_bits do + acc -> + BitList.set(acc, indexes_in_subcommittee |> Map.get(validator_index)) + end + end ################################ # Payload building and proposing diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 6d7942337..10b7cb6ee 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -109,10 +109,10 @@ defmodule LambdaEthereumConsensus.ValidatorSet do |> maybe_sync_committee_broadcasts(slot, head_root) end - defp process_tick(set, epoch, {slot, :last_third}) do + defp process_tick(%{head_root: head_root} = set, epoch, {slot, :last_third}) do set |> maybe_publish_attestation_aggregates(epoch, slot) - |> maybe_publish_sync_aggregates(epoch, slot) + |> maybe_publish_sync_aggregates(epoch, slot, head_root) end ############################## @@ -199,14 +199,14 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end end - defp maybe_publish_sync_aggregates(set, epoch, slot) do + defp maybe_publish_sync_aggregates(set, epoch, slot, head_root) do case Duties.current_sync_aggregators(set.duties, epoch, slot) do [] -> set aggregator_duties -> aggregator_duties - |> Enum.map(&publish_sync_aggregate(&1, slot, set.validators)) + |> Enum.map(&publish_sync_aggregate(&1, epoch, slot, head_root, set.validators)) |> update_duties(set, epoch, :sync_committees, slot) end end @@ -219,10 +219,10 @@ defmodule LambdaEthereumConsensus.ValidatorSet do Duties.sync_committee_broadcasted(duty, slot) end - defp publish_sync_aggregate(duty, slot, validators) do + defp publish_sync_aggregate(duty, epoch, slot, head_root, validators) do validators |> Map.get(duty.validator_index) - |> Validator.publish_sync_aggregate(duty, slot) + |> Validator.publish_sync_aggregate(duty, epoch, slot, head_root) Duties.sync_committee_aggregated(duty, slot) end diff --git a/lib/utils/bit_list.ex b/lib/utils/bit_list.ex index fdb5f0786..7000d3271 100644 --- a/lib/utils/bit_list.ex +++ b/lib/utils/bit_list.ex @@ -67,9 +67,15 @@ defmodule LambdaEthereumConsensus.Utils.BitList do def set?(bit_list, index), do: BitField.set?(bit_list, index) @doc """ - Sets a bit (turns it to 1). - Equivalent to bit_list[index] = 1. + Set a bit or list of bits (turns them to 1). + Equivalent to bit_list[index] = 1. If indexes is a list, + it will do it for every index in the list. """ + @spec set(t, [non_neg_integer]) :: t + def set(bit_list, indexes) when is_list(indexes) do + Enum.reduce(indexes, bit_list, fn index, acc -> set(acc, index) end) + end + @spec set(t, non_neg_integer) :: t def set(bit_list, index), do: BitField.set(bit_list, index) From 22bfd13988985c4892acbf71037cd89c3290ff26 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 3 Sep 2024 15:59:16 -0300 Subject: [PATCH 12/23] Fixed an issue regarding agregation bit size outside of minimal and filtering aggregators only in the subnet they aggreagte --- .../validator/validator.ex | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 4f8f6cdea..5d4b338b3 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -287,7 +287,8 @@ defmodule LambdaEthereumConsensus.Validator do ) do head_state = BlockStates.get_state_info!(head_root).beacon_state |> go_to_slot(slot) - for subnet_id <- duty.subnet_ids do + for subnet_id <- duty.subnet_ids, + agg_duty = Enum.find(duty.aggregation[slot], &(&1.subcommittee_index == subnet_id)) do case Gossip.SyncCommittee.stop_collecting(subnet_id) do {:ok, messages} -> log_md = [slot: slot, messages: messages] @@ -296,13 +297,10 @@ defmodule LambdaEthereumConsensus.Validator do aggregation_bits = sync_committee_aggregation_bits(head_state, subnet_id, epoch, messages) - aggregation_duty = - Enum.find(duty.aggregation[slot], &(&1.subcommittee_index == subnet_id)) - messages |> sync_committee_contribution(subnet_id, aggregation_bits) - |> append_sync_proof(aggregation_duty.selection_proof, validator_index) - |> append_sync_signature(aggregation_duty.signing_domain, keystore) + |> append_sync_proof(agg_duty.selection_proof, validator_index) + |> append_sync_signature(agg_duty.signing_domain, keystore) |> Gossip.SyncCommittee.publish_contribution() |> log_info_result(validator_index, "published sync committee aggregate", log_md) @@ -348,7 +346,8 @@ defmodule LambdaEthereumConsensus.Validator do |> Map.get(subnet_id) |> Map.new(fn {pubkey, indexes} -> {fetch_validator_index(state, pubkey), indexes} end) - aggregation_bits = indexes_in_subcommittee |> Enum.count() |> BitList.zero() + # TODO: This calculation should be in another place. + aggregation_bits = div(ChainSpec.get("SYNC_COMMITTEE_SIZE"), Constants.sync_committee_subnet_count()) |> BitList.zero() for %{validator_index: validator_index} <- messages, reduce: aggregation_bits do acc -> From 74f102c19bd8487c4ecca93a859bc492354eeeb7 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 3 Sep 2024 16:00:05 -0300 Subject: [PATCH 13/23] format --- lib/lambda_ethereum_consensus/validator/validator.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 5d4b338b3..622374ced 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -347,7 +347,9 @@ defmodule LambdaEthereumConsensus.Validator do |> Map.new(fn {pubkey, indexes} -> {fetch_validator_index(state, pubkey), indexes} end) # TODO: This calculation should be in another place. - aggregation_bits = div(ChainSpec.get("SYNC_COMMITTEE_SIZE"), Constants.sync_committee_subnet_count()) |> BitList.zero() + aggregation_bits = + div(ChainSpec.get("SYNC_COMMITTEE_SIZE"), Constants.sync_committee_subnet_count()) + |> BitList.zero() for %{validator_index: validator_index} <- messages, reduce: aggregation_bits do acc -> From 603a4280d6977de52259a7d5b285107f288b52b7 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 4 Sep 2024 21:06:05 -0300 Subject: [PATCH 14/23] Fixed boundary check + sync committe message duplication in contribution --- .../state_transition/misc.ex | 5 +++ .../validator/duties.ex | 11 ++++-- .../validator/utils.ex | 10 ++---- .../validator/validator.ex | 34 +++++++++---------- .../validator/validator_set.ex | 7 ++-- .../validator/sync_committee_contribution.ex | 14 ++------ 6 files changed, 39 insertions(+), 42 deletions(-) diff --git a/lib/lambda_ethereum_consensus/state_transition/misc.ex b/lib/lambda_ethereum_consensus/state_transition/misc.ex index 1cb101f03..375988237 100644 --- a/lib/lambda_ethereum_consensus/state_transition/misc.ex +++ b/lib/lambda_ethereum_consensus/state_transition/misc.ex @@ -290,6 +290,11 @@ defmodule LambdaEthereumConsensus.StateTransition.Misc do div(epoch, ChainSpec.get("EPOCHS_PER_SYNC_COMMITTEE_PERIOD")) end + @spec sync_subcommittee_size() :: Types.uint64() + def sync_subcommittee_size() do + div(ChainSpec.get("SYNC_COMMITTEE_SIZE"), Constants.sync_committee_subnet_count()) + end + @doc """ Return the 32-byte fork data root for the ``current_version`` and ``genesis_validators_root``. This is used primarily in signature domains to avoid collisions across forks/chains. diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index e0ba1ec19..015bdbecb 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -158,7 +158,10 @@ defmodule LambdaEthereumConsensus.Validator.Duties do defp sync_committee_aggreagtion_data(beacon_state, epoch, subnet_ids, validator_privkey) do {start_slot, end_slot} = boundary_slots(epoch) - for slot <- start_slot..end_slot, subcommittee_index <- subnet_ids, reduce: %{} do + # Slots for a particular epoch in sync committess go from start of the epoch - 1 to the end of the epoch - 1. + for slot <- max(0, start_slot - 1)..(end_slot - 1), + subcommittee_index <- subnet_ids, + reduce: %{} do acc -> proof = Utils.get_sync_committee_selection_proof( @@ -332,7 +335,9 @@ defmodule LambdaEthereumConsensus.Validator.Duties do %{proposers: proposers, attesters: attesters, sync_committees: sync_committees}, epoch ) do - Logger.info("[Duties] Proposers for epoch #{epoch} (slot=>validator): #{inspect(proposers)}") + Logger.info( + "[Duties] Proposers for epoch #{epoch} (slot=>validator):\n #{inspect(proposers)}" + ) for %{ subnet_ids: si, @@ -340,7 +345,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do aggregation: agg } <- sync_committees do Logger.info( - "[Duties] Sync committee for epoch: #{epoch}, validator_index: #{vi} will broadcast on subnet_ids: #{inspect(si)}.\n#{inspect(agg, pretty: true)}" + "[Duties] Sync committee for epoch: #{epoch}, validator_index: #{vi} will broadcast on subnet_ids: #{inspect(si)}.\n Slots: #{inspect(agg |> Map.keys() |> Enum.join(", "))}" ) end diff --git a/lib/lambda_ethereum_consensus/validator/utils.ex b/lib/lambda_ethereum_consensus/validator/utils.ex index 7595e57bf..8a8ba07a1 100644 --- a/lib/lambda_ethereum_consensus/validator/utils.ex +++ b/lib/lambda_ethereum_consensus/validator/utils.ex @@ -68,12 +68,9 @@ defmodule LambdaEthereumConsensus.Validator.Utils do do: state.current_sync_committee, else: state.next_sync_committee - sync_committee_subnet_size = - div(ChainSpec.get("SYNC_COMMITTEE_SIZE"), Constants.sync_committee_subnet_count()) - for {pubkey, index} <- Enum.with_index(sync_committee.pubkeys), pubkey == target_pubkey do - div(index, sync_committee_subnet_size) + div(index, Misc.sync_subcommittee_size()) end |> Enum.dedup() end @@ -90,13 +87,10 @@ defmodule LambdaEthereumConsensus.Validator.Utils do @spec participants_per_sync_subcommittee(BeaconState.t(), Types.epoch()) :: %{non_neg_integer() => [Bls.pubkey()]} def participants_per_sync_subcommittee(state, epoch) do - sync_committee_subnet_size = - div(ChainSpec.get("SYNC_COMMITTEE_SIZE"), Constants.sync_committee_subnet_count()) - state |> Accessors.get_sync_committee_for_epoch!(epoch) |> Map.get(:pubkeys) - |> Enum.chunk_every(sync_committee_subnet_size) + |> Enum.chunk_every(Misc.sync_subcommittee_size()) |> Enum.with_index() |> Map.new(fn {pubkeys, i} -> indices_by_pubkeys = diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 622374ced..546443108 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -313,7 +313,23 @@ defmodule LambdaEthereumConsensus.Validator do :ok end + defp sync_committee_aggregation_bits(state, subnet_id, epoch, messages) do + indexes_in_subcommittee = + state + |> Utils.participants_per_sync_subcommittee(epoch) + |> Map.get(subnet_id) + |> Map.new(fn {pubkey, indexes} -> {fetch_validator_index(state, pubkey), indexes} end) + + aggregation_bits = Misc.sync_subcommittee_size() |> BitList.zero() + + for %{validator_index: validator_index} <- messages, reduce: aggregation_bits do + acc -> + BitList.set(acc, indexes_in_subcommittee |> Map.get(validator_index)) + end + end + defp sync_committee_contribution(messages, subnet_id, aggregation_bits) do + messages = Enum.uniq(messages) {:ok, signature} = Bls.aggregate(Enum.map(messages, & &1.signature)) %Types.SyncCommitteeContribution{ @@ -339,24 +355,6 @@ defmodule LambdaEthereumConsensus.Validator do %Types.SignedContributionAndProof{message: contribution_and_proof, signature: signature} end - defp sync_committee_aggregation_bits(state, subnet_id, epoch, messages) do - indexes_in_subcommittee = - state - |> Utils.participants_per_sync_subcommittee(epoch) - |> Map.get(subnet_id) - |> Map.new(fn {pubkey, indexes} -> {fetch_validator_index(state, pubkey), indexes} end) - - # TODO: This calculation should be in another place. - aggregation_bits = - div(ChainSpec.get("SYNC_COMMITTEE_SIZE"), Constants.sync_committee_subnet_count()) - |> BitList.zero() - - for %{validator_index: validator_index} <- messages, reduce: aggregation_bits do - acc -> - BitList.set(acc, indexes_in_subcommittee |> Map.get(validator_index)) - end - end - ################################ # Payload building and proposing diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 10b7cb6ee..6094692b7 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -112,7 +112,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp process_tick(%{head_root: head_root} = set, epoch, {slot, :last_third}) do set |> maybe_publish_attestation_aggregates(epoch, slot) - |> maybe_publish_sync_aggregates(epoch, slot, head_root) + |> maybe_publish_sync_aggregates(slot, head_root) end ############################## @@ -199,7 +199,10 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end end - defp maybe_publish_sync_aggregates(set, epoch, slot, head_root) do + defp maybe_publish_sync_aggregates(set, slot, head_root) do + # Sync committee is broadcasted for the next slot, so we take the duties for the correct epoch. + epoch = Misc.compute_epoch_at_slot(slot + 1) + case Duties.current_sync_aggregators(set.duties, epoch, slot) do [] -> set diff --git a/lib/types/validator/sync_committee_contribution.ex b/lib/types/validator/sync_committee_contribution.ex index 30874ddfe..917e2ea09 100644 --- a/lib/types/validator/sync_committee_contribution.ex +++ b/lib/types/validator/sync_committee_contribution.ex @@ -4,6 +4,7 @@ defmodule Types.SyncCommitteeContribution do Related definitions in `native/ssz_nif/src/types/`. """ use LambdaEthereumConsensus.Container + alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.Utils.BitVector fields = [ @@ -31,12 +32,7 @@ defmodule Types.SyncCommitteeContribution do {:slot, TypeAliases.slot()}, {:beacon_block_root, TypeAliases.root()}, {:subcommittee_index, TypeAliases.uint64()}, - {:aggregation_bits, - {:bitvector, - div( - ChainSpec.get("SYNC_COMMITTEE_SIZE"), - Constants.sync_committee_subnet_count() - )}}, + {:aggregation_bits, {:bitvector, Misc.sync_subcommittee_size()}}, {:signature, TypeAliases.bls_signature()} ] end @@ -50,11 +46,7 @@ defmodule Types.SyncCommitteeContribution do def decode(%__MODULE__{} = map) do # NOTE: this isn't really needed - aggregation_bits_count = - div( - ChainSpec.get("SYNC_COMMITTEE_SIZE"), - Constants.sync_committee_subnet_count() - ) + aggregation_bits_count = Misc.sync_subcommittee_size() map |> Map.update!(:aggregation_bits, &BitVector.new(&1, aggregation_bits_count)) From 03663efa96c6e7c352beca89c0c1c902c515732a Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 5 Sep 2024 12:59:27 -0300 Subject: [PATCH 15/23] Fixed sync contribution signature calculation for validators repeated in the same subcommittee --- Makefile | 10 +++-- .../validator/validator.ex | 42 ++++++++++++------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 758d47531..b724ba73c 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,8 @@ KURTOSIS_GRAFANA_DASHBOARDS_DIR ?= $(KURTOSIS_DIR)/static_files/grafana-config/d KURTOSIS_COOKIE ?= secret # Name of the kurtosis service pointing to the lambdaconsesus node KURTOSIS_SERVICE ?= cl-3-lambda-geth +# Name of the enclave to be used with kurtosis +KURTOSIS_ENCLAVE ?= lambdanet ##### TARGETS ##### @@ -75,18 +77,18 @@ kurtosis.setup.lambdaconsensus: #💻 kurtosis.start: @ Starts the kurtosis environment kurtosis.start: - kurtosis run --enclave lambdanet $(KURTOSIS_DIR) --args-file network_params.yaml + kurtosis run --enclave $(KURTOSIS_ENCLAVE) $(KURTOSIS_DIR) --args-file network_params.yaml #💻 kurtosis.build-and-start: @ Builds the lambdaconsensus Docker image and starts the kurtosis environment. kurtosis.clean-start: kurtosis.clean kurtosis.setup.lambdaconsensus kurtosis.start #💻 kurtosis.stop: @ Stops the kurtosis environment kurtosis.stop: - kurtosis enclave stop lambdanet + kurtosis enclave stop $(KURTOSIS_ENCLAVE) #💻 kurtosis.remove: @ Removes the kurtosis environment kurtosis.remove: - kurtosis enclave rm lambdanet + kurtosis enclave rm $(KURTOSIS_ENCLAVE) #💻 kurtosis.clean: @ Clean the kurtosis environment kurtosis.clean: @@ -97,7 +99,7 @@ kurtosis.purge: kurtosis.stop kurtosis.remove kurtosis.clean #💻 kurtosis.connect: @ Connects to the client running in kurtosis, KURTOSIS_SERVICE could be given kurtosis.connect: - kurtosis service shell lambdanet $(KURTOSIS_SERVICE) + kurtosis service shell $(KURTOSIS_ENCLAVE) $(KURTOSIS_SERVICE) #💻 kurtosis.connect.iex: @ Connects to iex ONCE INSIDE THE KURTOSIS SERVICE kurtosis.connect.iex: diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 546443108..758d75660 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -294,11 +294,11 @@ defmodule LambdaEthereumConsensus.Validator do log_md = [slot: slot, messages: messages] log_info(validator_index, "publishing sync committee aggregate", log_md) - aggregation_bits = - sync_committee_aggregation_bits(head_state, subnet_id, epoch, messages) + {aggregation_bits, indexes_in_subcomittee} = + sync_committee_aggregation_bits_and_indexes(head_state, subnet_id, epoch, messages) messages - |> sync_committee_contribution(subnet_id, aggregation_bits) + |> sync_committee_contribution(subnet_id, aggregation_bits, indexes_in_subcomittee) |> append_sync_proof(agg_duty.selection_proof, validator_index) |> append_sync_signature(agg_duty.signing_domain, keystore) |> Gossip.SyncCommittee.publish_contribution() @@ -313,34 +313,48 @@ defmodule LambdaEthereumConsensus.Validator do :ok end - defp sync_committee_aggregation_bits(state, subnet_id, epoch, messages) do + defp sync_committee_aggregation_bits_and_indexes(state, subnet_id, epoch, messages) do indexes_in_subcommittee = state |> Utils.participants_per_sync_subcommittee(epoch) |> Map.get(subnet_id) |> Map.new(fn {pubkey, indexes} -> {fetch_validator_index(state, pubkey), indexes} end) - aggregation_bits = Misc.sync_subcommittee_size() |> BitList.zero() + aggregation_bits = + Enum.reduce(messages, BitList.zero(Misc.sync_subcommittee_size()), fn message, acc -> + BitList.set(acc, indexes_in_subcommittee |> Map.get(message.validator_index)) + end) - for %{validator_index: validator_index} <- messages, reduce: aggregation_bits do - acc -> - BitList.set(acc, indexes_in_subcommittee |> Map.get(validator_index)) - end + {aggregation_bits, indexes_in_subcommittee} end - defp sync_committee_contribution(messages, subnet_id, aggregation_bits) do - messages = Enum.uniq(messages) - {:ok, signature} = Bls.aggregate(Enum.map(messages, & &1.signature)) - + defp sync_committee_contribution(messages, subnet_id, aggregation_bits, indexes_in_subcommittee) do %Types.SyncCommitteeContribution{ slot: List.first(messages).slot, beacon_block_root: List.first(messages).beacon_block_root, subcommittee_index: subnet_id, aggregation_bits: aggregation_bits, - signature: signature + signature: aggregate_sync_committee_signature(messages, indexes_in_subcommittee) } end + defp aggregate_sync_committee_signature(messages, indexes_in_subcommittee) do + # TODO: as with attestations, we need to check why we recieve duplicate sync messages + unique_messages = messages |> Enum.uniq() + + signatures_to_aggregate = + Enum.flat_map(unique_messages, fn message -> + # Here we duplicate the signature by n, being n the times a validator appears + # in the same subcommittee + indexes_in_subcommittee + |> Map.get(message.validator_index) + |> Enum.map(fn _ -> message.signature end) + end) + + {:ok, signature} = Bls.aggregate(individual_signatures) + signature + end + defp append_sync_proof(contribution, proof, validator_index) do %Types.ContributionAndProof{ aggregator_index: validator_index, From 64a0d4679ee0474090deff5d4db036748598bec2 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 5 Sep 2024 18:58:08 -0300 Subject: [PATCH 16/23] Fixed Duties update that reduced the ammount of broadcasters every slot --- .../validator/duties.ex | 24 +++++- .../validator/validator.ex | 64 +++++---------- .../validator/validator_set.ex | 81 +++++++++++++------ 3 files changed, 101 insertions(+), 68 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 015bdbecb..25fc41904 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 @@ -73,7 +72,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do for {epoch, slot} <- epochs_and_start_slots, reduce: duties_map do duties_map -> - beacon = Validator.fetch_target_state_and_go_to_slot(epoch, slot, head_root) + beacon = ValidatorSet.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) @@ -298,7 +297,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @spec update_duties!( duties(), - kind(), + kind() | :sync_committees_contribution, Types.epoch(), Types.slot(), attester_duties() | proposer_duties() @@ -306,9 +305,28 @@ defmodule LambdaEthereumConsensus.Validator.Duties do def update_duties!(duties, :sync_committees, epoch, _slot, updated), do: put_in(duties, [epoch, :sync_committees], updated) + def update_duties!(duties, :sync_committees_contribution, epoch, _slot, updated) do + to_update = get_in(duties, [epoch, :sync_committees]) + + updated_duties = Enum.reduce(updated, to_update, &replace_duty_in_list/2) + + put_in(duties, [epoch, :sync_committees], updated_duties) + end + def update_duties!(duties, kind, epoch, slot, updated), do: put_in(duties, [epoch, kind, slot], updated) + # FIXME: This is awful, The sync_committee duties structure is wrong and generates this complexities, I'll split it. + defp replace_duty_in_list(duty, list) do + Enum.map(list, fn d -> + if d.validator_index == duty.validator_index do + duty + else + d + end + end) + end + @spec attested(attester_duty()) :: attester_duty() def attested(duty), do: Map.put(duty, :attested?, true) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 758d75660..71aab00ce 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -13,17 +13,15 @@ defmodule LambdaEthereumConsensus.Validator do alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.Libp2pPort alias LambdaEthereumConsensus.P2P.Gossip - alias LambdaEthereumConsensus.StateTransition alias LambdaEthereumConsensus.StateTransition.Accessors alias LambdaEthereumConsensus.StateTransition.Misc - alias LambdaEthereumConsensus.Store.BlockStates - alias LambdaEthereumConsensus.Store.CheckpointStates alias LambdaEthereumConsensus.Utils.BitField alias LambdaEthereumConsensus.Utils.BitList alias LambdaEthereumConsensus.Validator.BlockBuilder alias LambdaEthereumConsensus.Validator.BuildBlockRequest alias LambdaEthereumConsensus.Validator.Duties alias LambdaEthereumConsensus.Validator.Utils + alias LambdaEthereumConsensus.ValidatorSet alias Types.Attestation @default_graffiti_message "Lambda, so gentle, so good" @@ -39,7 +37,8 @@ defmodule LambdaEthereumConsensus.Validator do @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_and_go_to_slot(epoch, head_slot, head_root) + # TODO: This should be handled in the ValidatorSet instead, part of #1281 + beacon = ValidatorSet.fetch_target_state_and_go_to_slot(epoch, head_slot, head_root) new(keystore, beacon) end @@ -66,36 +65,21 @@ defmodule LambdaEthereumConsensus.Validator do end end - ########################## - # Target State - - @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 - - defp fetch_target_state(epoch, root) do - {:ok, state} = CheckpointStates.compute_target_checkpoint_state(epoch, root) - state - 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 - ########################## # Attestations - @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 + @spec attest(t(), Duties.attester_duty(), Types.BeaconState.t(), Types.slot(), Types.root()) :: + :ok + def attest( + %{index: validator_index, keystore: keystore}, + %{subnet_id: subnet_id} = current_duty, + head_state, + slot, + head_root + ) do log_debug(validator_index, "attesting", slot: slot, subnet_id: subnet_id) - attestation = produce_attestation(current_duty, slot, head_root, keystore.privkey) + attestation = produce_attestation(current_duty, head_state, slot, head_root, keystore.privkey) log_md = [slot: slot, attestation: attestation, subnet_id: subnet_id] @@ -164,14 +148,13 @@ defmodule LambdaEthereumConsensus.Validator do %Types.SignedAggregateAndProof{message: aggregate_and_proof, signature: signature} end - defp produce_attestation(duty, slot, head_root, privkey) do + defp produce_attestation(duty, head_state, slot, head_root, privkey) do %{ index_in_committee: index_in_committee, committee_length: committee_length, committee_index: committee_index } = duty - head_state = BlockStates.get_state_info!(head_root).beacon_state |> go_to_slot(slot) head_epoch = Misc.compute_epoch_at_slot(slot) epoch_boundary_block_root = @@ -217,19 +200,18 @@ defmodule LambdaEthereumConsensus.Validator do @spec sync_committee_message_broadcast( t(), Duties.sync_committee_duty(), + Types.BeaconState.t(), Types.slot(), Types.root() ) :: :ok def sync_committee_message_broadcast( %{index: validator_index, keystore: keystore}, - current_duty, + %{subnet_ids: subnet_ids} = current_duty, + head_state, slot, head_root ) do - %{subnet_ids: subnet_ids} = current_duty - - head_state = BlockStates.get_state_info!(head_root).beacon_state |> go_to_slot(slot) log_debug(validator_index, "broadcasting sync committee message", slot: slot) message = get_sync_committee_message(head_state, head_root, validator_index, keystore.privkey) @@ -274,19 +256,17 @@ defmodule LambdaEthereumConsensus.Validator do @spec publish_sync_aggregate( t(), Duties.sync_committee_duty(), + Types.BeaconState.t(), Types.epoch(), - Types.slot(), - Types.root() + Types.slot() ) :: :ok def publish_sync_aggregate( %{index: validator_index, keystore: keystore}, duty, + %Types.BeaconState{} = head_state, epoch, - slot, - head_root + slot ) do - head_state = BlockStates.get_state_info!(head_root).beacon_state |> go_to_slot(slot) - for subnet_id <- duty.subnet_ids, agg_duty = Enum.find(duty.aggregation[slot], &(&1.subcommittee_index == subnet_id)) do case Gossip.SyncCommittee.stop_collecting(subnet_id) do @@ -351,7 +331,7 @@ defmodule LambdaEthereumConsensus.Validator do |> Enum.map(fn _ -> message.signature end) end) - {:ok, signature} = Bls.aggregate(individual_signatures) + {:ok, signature} = Bls.aggregate(signatures_to_aggregate) signature end diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 6094692b7..9e8cf008d 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -9,7 +9,9 @@ defmodule LambdaEthereumConsensus.ValidatorSet do require Logger + alias LambdaEthereumConsensus.StateTransition alias LambdaEthereumConsensus.StateTransition.Misc + alias LambdaEthereumConsensus.Store.CheckpointStates alias LambdaEthereumConsensus.Validator alias LambdaEthereumConsensus.Validator.Duties @@ -49,7 +51,7 @@ 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) + beacon = fetch_target_state_and_go_to_slot(epoch, slot, head_root) validators = Map.new(validator_keystores, fn keystore -> @@ -73,13 +75,14 @@ defmodule LambdaEthereumConsensus.ValidatorSet do 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) + head_state = fetch_target_state_and_go_to_slot(epoch, slot, head_root) # TODO: this doesn't take into account reorgs set |> update_state(epoch, slot, head_root) - |> maybe_attests(epoch, slot, head_root) + |> maybe_attests(head_state, epoch, slot, head_root) |> maybe_build_payload(slot + 1, head_root) - |> maybe_sync_committee_broadcasts(slot, head_root) + |> maybe_sync_committee_broadcasts(head_state, slot, head_root) end @doc """ @@ -92,27 +95,28 @@ defmodule LambdaEthereumConsensus.ValidatorSet do 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) + head_state = fetch_target_state_and_go_to_slot(epoch, slot, head_root) set |> update_state(epoch, slot, head_root) - |> process_tick(epoch, slot_data) + |> process_tick(head_state, epoch, slot_data) end - defp process_tick(%{head_root: head_root} = set, epoch, {slot, :first_third}) do + defp process_tick(%{head_root: head_root} = set, _head_state, epoch, {slot, :first_third}) do maybe_propose(set, epoch, slot, head_root) end - defp process_tick(%{head_root: head_root} = set, epoch, {slot, :second_third}) do + defp process_tick(%{head_root: head_root} = set, head_state, epoch, {slot, :second_third}) do set - |> maybe_attests(epoch, slot, head_root) + |> maybe_attests(head_state, epoch, slot, head_root) |> maybe_build_payload(slot + 1, head_root) - |> maybe_sync_committee_broadcasts(slot, head_root) + |> maybe_sync_committee_broadcasts(head_state, slot, head_root) end - defp process_tick(%{head_root: head_root} = set, epoch, {slot, :last_third}) do + defp process_tick(set, head_state, epoch, {slot, :last_third}) do set |> maybe_publish_attestation_aggregates(epoch, slot) - |> maybe_publish_sync_aggregates(slot, head_root) + |> maybe_publish_sync_aggregates(head_state, slot) end ############################## @@ -184,7 +188,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do ############################## # Sync committee - defp maybe_sync_committee_broadcasts(set, slot, head_root) do + defp maybe_sync_committee_broadcasts(set, head_state, slot, head_root) do # Sync committee is broadcasted for the next slot, so we take the duties for the correct epoch. epoch = Misc.compute_epoch_at_slot(slot + 1) @@ -194,12 +198,12 @@ defmodule LambdaEthereumConsensus.ValidatorSet do sync_committee_duties -> sync_committee_duties - |> Enum.map(&sync_committee_broadcast(&1, slot, head_root, set.validators)) + |> Enum.map(&sync_committee_broadcast(&1, head_state, slot, head_root, set.validators)) |> update_duties(set, epoch, :sync_committees, slot) end end - defp maybe_publish_sync_aggregates(set, slot, head_root) do + defp maybe_publish_sync_aggregates(set, head_state, slot) do # Sync committee is broadcasted for the next slot, so we take the duties for the correct epoch. epoch = Misc.compute_epoch_at_slot(slot + 1) @@ -209,23 +213,23 @@ defmodule LambdaEthereumConsensus.ValidatorSet do aggregator_duties -> aggregator_duties - |> Enum.map(&publish_sync_aggregate(&1, epoch, slot, head_root, set.validators)) - |> update_duties(set, epoch, :sync_committees, slot) + |> Enum.map(&publish_sync_aggregate(&1, head_state, epoch, slot, set.validators)) + |> update_duties(set, epoch, :sync_committees_contribution, slot) end end - defp sync_committee_broadcast(duty, slot, head_root, validators) do + defp sync_committee_broadcast(duty, head_state, slot, head_root, validators) do validators |> Map.get(duty.validator_index) - |> Validator.sync_committee_message_broadcast(duty, slot, head_root) + |> Validator.sync_committee_message_broadcast(duty, head_state, slot, head_root) Duties.sync_committee_broadcasted(duty, slot) end - defp publish_sync_aggregate(duty, epoch, slot, head_root, validators) do + defp publish_sync_aggregate(duty, head_state, epoch, slot, validators) do validators |> Map.get(duty.validator_index) - |> Validator.publish_sync_aggregate(duty, epoch, slot, head_root) + |> Validator.publish_sync_aggregate(duty, head_state, epoch, slot) Duties.sync_committee_aggregated(duty, slot) end @@ -233,14 +237,14 @@ defmodule LambdaEthereumConsensus.ValidatorSet do ############################## # Attestation - defp maybe_attests(set, epoch, slot, head_root) do + defp maybe_attests(set, head_state, 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)) + |> Enum.map(&attest(&1, head_state, slot, head_root, set.validators)) |> update_duties(set, epoch, :attesters, slot) end end @@ -257,10 +261,10 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end end - defp attest(duty, slot, head_root, validators) do + defp attest(duty, head_state, slot, head_root, validators) do validators |> Map.get(duty.validator_index) - |> Validator.attest(duty, slot, head_root) + |> Validator.attest(duty, head_state, slot, head_root) Duties.attested(duty) end @@ -279,6 +283,37 @@ defmodule LambdaEthereumConsensus.ValidatorSet do |> then(&%{set | duties: &1}) end + ########################## + # Target State + + @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 + {time, result} = + :timer.tc(fn -> + epoch |> fetch_target_state(root) |> go_to_slot(slot) + end) + + Logger.debug("[Validator] Fetched target state in #{time / 1_000}ms", + epoch: epoch, + slot: slot + ) + + result + end + + defp fetch_target_state(epoch, root) do + {:ok, state} = CheckpointStates.compute_target_checkpoint_state(epoch, root) + state + 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 + ############################## # Key management From f937fabfbfb0a69ddc9f61d88a643ffdd1a1a372 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 6 Sep 2024 11:33:59 -0300 Subject: [PATCH 17/23] Documented the participants in sync committee and return the validator index instead of pubkeys --- .../validator/utils.ex | 25 ++++++++++++++++--- .../validator/validator.ex | 9 +------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/utils.ex b/lib/lambda_ethereum_consensus/validator/utils.ex index 8a8ba07a1..63099a664 100644 --- a/lib/lambda_ethereum_consensus/validator/utils.ex +++ b/lib/lambda_ethereum_consensus/validator/utils.ex @@ -7,6 +7,15 @@ defmodule LambdaEthereumConsensus.Validator.Utils do alias Types.AttestationData alias Types.BeaconState + @doc """ + Returns the index of a validator in the state's validator list given it's pubkey. + """ + @spec fetch_validator_index(Types.BeaconState.t(), Bls.pubkey()) :: + non_neg_integer() | nil + def fetch_validator_index(state, pubkey) do + Enum.find_index(state.validators, &(&1.pubkey == pubkey)) + end + @doc """ Compute the correct subnet for an attestation. """ @@ -84,6 +93,14 @@ defmodule LambdaEthereumConsensus.Validator.Utils do target_pubkey && target_pubkey in Accessors.get_sync_committee_for_epoch!(state, epoch) end + @doc """ + Returns a map of subcommittee index wich had a map of each validator present and + their index in the subcommittee. E.g.: + %{0 => %{0 => [0], 1 => [1, 2]}, 1 => %{2 => [0, 2], 0 => [1]}} + For subcommittee 0, validator 0 is at index 0 and validator 1 is at index 1, 2 + For subcommittee 1, validator 2 is at index 0 and 2, validator 0 is at index 1 + ``` + """ @spec participants_per_sync_subcommittee(BeaconState.t(), Types.epoch()) :: %{non_neg_integer() => [Bls.pubkey()]} def participants_per_sync_subcommittee(state, epoch) do @@ -93,10 +110,12 @@ defmodule LambdaEthereumConsensus.Validator.Utils do |> Enum.chunk_every(Misc.sync_subcommittee_size()) |> Enum.with_index() |> Map.new(fn {pubkeys, i} -> - indices_by_pubkeys = - pubkeys |> Enum.with_index() |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) + indices_by_validator = + pubkeys + |> Enum.with_index() + |> Enum.group_by(&fetch_validator_index(state, elem(&1, 0)), &elem(&1, 1)) - {i, indices_by_pubkeys} + {i, indices_by_validator} end) end diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 71aab00ce..4a98f03d5 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -51,7 +51,7 @@ defmodule LambdaEthereumConsensus.Validator do payload_builder: nil } - case fetch_validator_index(beacon, state.keystore.pubkey) do + case Utils.fetch_validator_index(beacon, state.keystore.pubkey) do nil -> Logger.warning( "[Validator] Public key #{state.keystore.pubkey} not found in the validator set" @@ -188,12 +188,6 @@ defmodule LambdaEthereumConsensus.Validator do } 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 - ################################ # Sync Committee @@ -298,7 +292,6 @@ defmodule LambdaEthereumConsensus.Validator do state |> Utils.participants_per_sync_subcommittee(epoch) |> Map.get(subnet_id) - |> Map.new(fn {pubkey, indexes} -> {fetch_validator_index(state, pubkey), indexes} end) aggregation_bits = Enum.reduce(messages, BitList.zero(Misc.sync_subcommittee_size()), fn message, acc -> From 46a59a57769a48e4319189bae27108260effc010 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 6 Sep 2024 16:47:33 -0300 Subject: [PATCH 18/23] Addded some comments and tests for SyncSubnetInfo matching the previous ones for AttSubnetInfo --- .../p2p/gossip/sync_committee.ex | 3 +- .../validator/utils.ex | 25 +++--- lib/types/sync_subnet_info.ex | 3 + test/unit/sync_subnet_info.exs | 78 +++++++++++++++++++ 4 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 test/unit/sync_subnet_info.exs diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex index 8a91de6c3..c66063084 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex @@ -3,7 +3,8 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do This module handles sync committee from specific gossip subnets. Used by validators to fulfill aggregation duties. - TODO: THIS IS EXACTLY THE SAME AS ATTSUBNET. ALSO NEEDS TESTS + TODO: This module borrows almost all of its logic from Attestation, + this could be refactored to a common module if needed in the future. """ alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.Libp2pPort diff --git a/lib/lambda_ethereum_consensus/validator/utils.ex b/lib/lambda_ethereum_consensus/validator/utils.ex index 63099a664..595d58e82 100644 --- a/lib/lambda_ethereum_consensus/validator/utils.ex +++ b/lib/lambda_ethereum_consensus/validator/utils.ex @@ -94,12 +94,13 @@ defmodule LambdaEthereumConsensus.Validator.Utils do end @doc """ - Returns a map of subcommittee index wich had a map of each validator present and - their index in the subcommittee. E.g.: - %{0 => %{0 => [0], 1 => [1, 2]}, 1 => %{2 => [0, 2], 0 => [1]}} - For subcommittee 0, validator 0 is at index 0 and validator 1 is at index 1, 2 - For subcommittee 1, validator 2 is at index 0 and 2, validator 0 is at index 1 - ``` + Returns a map of subcommittee index every one of each had a map of the validators + present and their index in the subcommittee. E.g.: + + %{0 => %{0 => [0], 1 => [1, 2]}, 1 => %{2 => [0, 2], 0 => [1]}} + + - For subcommittee 0, validator 0 is at index 0 and validator 1 is at index 1, 2 + - For subcommittee 1, validator 2 is at index 0 and 2, validator 0 is at index 1 """ @spec participants_per_sync_subcommittee(BeaconState.t(), Types.epoch()) :: %{non_neg_integer() => [Bls.pubkey()]} @@ -109,13 +110,11 @@ defmodule LambdaEthereumConsensus.Validator.Utils do |> Map.get(:pubkeys) |> Enum.chunk_every(Misc.sync_subcommittee_size()) |> Enum.with_index() - |> Map.new(fn {pubkeys, i} -> - indices_by_validator = - pubkeys - |> Enum.with_index() - |> Enum.group_by(&fetch_validator_index(state, elem(&1, 0)), &elem(&1, 1)) - - {i, indices_by_validator} + |> Map.new(fn {pubkeys, subcommittee_i} -> + pubkeys + |> Enum.with_index() + |> Enum.group_by(&fetch_validator_index(state, elem(&1, 0)), &elem(&1, 1)) + |> then(fn indexes_by_validator -> {subcommittee_i, indexes_by_validator} end) end) end diff --git a/lib/types/sync_subnet_info.ex b/lib/types/sync_subnet_info.ex index 64b3cc946..7bf3c9102 100644 --- a/lib/types/sync_subnet_info.ex +++ b/lib/types/sync_subnet_info.ex @@ -3,6 +3,9 @@ defmodule Types.SyncSubnetInfo do Struct to hold subnet messages for easier db storing: - data: A Sync Committee message data (slot + root). - messages: List of all the collected SyncCommitteeMessages. + + TODO: This module borrows almost all of its logic from AttSubnetInfo, + this could be refactored to a common module if needed in the future. """ alias LambdaEthereumConsensus.Store.Db diff --git a/test/unit/sync_subnet_info.exs b/test/unit/sync_subnet_info.exs new file mode 100644 index 000000000..295af20e9 --- /dev/null +++ b/test/unit/sync_subnet_info.exs @@ -0,0 +1,78 @@ +defmodule Unit.AttestationTest do + alias LambdaEthereumConsensus.Store.Db + alias Types.Checkpoint + alias Types.SyncCommitteeMessage + alias Types.SyncSubnetInfo + + use ExUnit.Case + use Patch + + doctest SyncSubnetInfo + + setup %{tmp_dir: tmp_dir} do + start_link_supervised!({Db, dir: tmp_dir}) + :ok + end + + defp sync_committee_message(validator_index \\ 0) do + %SyncCommitteeMessage{ + slot: 5_057_010_135_270_197_978, + beacon_block_root: + <<31, 38, 101, 174, 248, 168, 116, 226, 15, 39, 218, 148, 42, 8, 80, 80, 241, 149, 162, + 32, 176, 208, 120, 120, 89, 123, 136, 115, 154, 28, 21, 174>>, + validator_index: validator_index, + signature: <<>> + } + end + + @tag :tmp_dir + test "stop collecting with one attestation" do + subnet_id = 1 + + expected_message = %SyncCommitteeMessage{ + slot: 5_057_010_135_270_197_978, + beacon_block_root: + <<31, 38, 101, 174, 248, 168, 116, 226, 15, 39, 218, 148, 42, 8, 80, 80, 241, 149, 162, + 32, 176, 208, 120, 120, 89, 123, 136, 115, 154, 28, 21, 174>>, + validator_index: 0, + signature: <<>> + } + + SyncSubnetInfo.new_subnet_with_message(subnet_id, sync_committee_message()) + + {:ok, messages} = SyncSubnetInfo.stop_collecting(subnet_id) + + assert [expected_message] == messages + end + + @tag :tmp_dir + test "stop collecting with two attestations" do + subnet_id = 1 + + expected_message_1 = %SyncCommitteeMessage{ + slot: 5_057_010_135_270_197_978, + beacon_block_root: + <<31, 38, 101, 174, 248, 168, 116, 226, 15, 39, 218, 148, 42, 8, 80, 80, 241, 149, 162, + 32, 176, 208, 120, 120, 89, 123, 136, 115, 154, 28, 21, 174>>, + validator_index: 1, + signature: <<>> + } + + expected_message_2 = %SyncCommitteeMessage{ + slot: 5_057_010_135_270_197_978, + beacon_block_root: + <<31, 38, 101, 174, 248, 168, 116, 226, 15, 39, 218, 148, 42, 8, 80, 80, 241, 149, 162, + 32, 176, 208, 120, 120, 89, 123, 136, 115, 154, 28, 21, 174>>, + validator_index: 2, + signature: <<>> + } + + SyncSubnetInfo.new_subnet_with_message(subnet_id, sync_committee_message(1)) + + SyncSubnetInfo.add_message!(subnet_id, sync_committee_message(2)) + + {:ok, messages} = SyncSubnetInfo.stop_collecting(subnet_id) + + assert [expected_message_2, expected_message_1] == messages + end +end From 3fc50d6b85a130969cf1f8e331ca688e5eeb81d9 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Sat, 7 Sep 2024 17:53:04 -0300 Subject: [PATCH 19/23] Duties refactored --- .../validator/duties.ex | 283 ++++++++++-------- .../validator/utils.ex | 6 +- .../validator/validator.ex | 72 +++-- .../validator/validator_set.ex | 38 +-- lib/utils/bit_list.ex | 6 +- test/unit/subnet_info.exs | 2 +- test/unit/sync_subnet_info.exs | 2 +- 7 files changed, 225 insertions(+), 184 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 25fc41904..98978c356 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -26,36 +26,38 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @type proposer_duty :: Types.validator_index() + @type sync_committee_aggregator_duty :: %{ + aggregated?: boolean(), + selection_proof: Bls.signature(), + contribution_domain: Types.domain(), + validator_index: Types.validator_index(), + subcommittee_index: Types.uint64() + } + @type sync_committee_duty :: %{ - # Given that we send messages in EVERY slot, instead - # of tracking them all, we just keep track of the last - # slot we broadcasted to avoid double publishs. last_slot_broadcasted: Types.slot(), - subnet_ids: [Types.uint64()], + message_domain: Types.domain(), validator_index: Types.validator_index(), - aggregation: %{ - Types.slot() => [ - %{ - aggregated?: boolean(), - selection_proof: Bls.signature(), - signing_domain: Types.domain(), - subcommittee_index: Types.uint64() - } - ] - } + subnet_ids: [Types.uint64()], + aggregation: [sync_committee_aggregator_duty()] } + @type misc_data :: %{sync_subcommittee_participants: %{}} + @type attester_duties :: [attester_duty()] @type proposer_duties :: [proposer_duty()] - @type sync_committee_duties :: [sync_committee_duty()] @type attester_duties_per_slot :: %{Types.slot() => attester_duties()} @type proposer_duties_per_slot :: %{Types.slot() => proposer_duties()} + @type sync_committee_duties_per_slot :: %{Types.slot() => [sync_committee_duty()]} - @type kind :: :proposers | :attesters | :sync_committees + @type kind :: :proposers | :attesters | :sync_committees | :misc_data @type duties :: %{ kind() => - attester_duties_per_slot() | proposer_duties_per_slot() | sync_committee_duties() + attester_duties_per_slot() + | proposer_duties_per_slot() + | sync_committee_duties_per_slot() + | misc_data() } ############################ @@ -77,27 +79,31 @@ defmodule LambdaEthereumConsensus.Validator.Duties do # calculating them on the fly. Accessors.maybe_prefetch_committees(beacon, epoch) - _last_epoch = Map.keys(duties_map) |> Enum.max(fn -> 0 end) + last_epoch = Map.keys(duties_map) |> Enum.max(fn -> 0 end) new_proposers = compute_proposers_for_epoch(beacon, epoch, validators) new_attesters = compute_attesters_for_epoch(beacon, epoch, validators) - new_sync_committees = compute_current_sync_committees(beacon, validators) + {new_sync_committees, new_misc_data} = + case sync_committee_compute_check(epoch, {last_epoch, Map.get(duties_map, last_epoch)}) do + {:already_computed, sync_committee_duties} -> + sync_committee_duties + |> recompute_sync_committee_duties(beacon, epoch, validators) + |> then(&{&1, misc_data(duties_map, last_epoch)}) - # case sync_committee_compute_check(epoch, {last_epoch, Map.get(duties_map, last_epoch)}) do - # {:already_computed, sync_committees} -> - # sync_committees + {:not_computed, period} -> + Logger.debug("[Duties] Computing sync committees for period: #{period}.") - # {:not_computed, period} -> - # Logger.debug("[Duties] Computing sync committees for period: #{period}.") - - # compute_current_sync_committees(beacon, validators) - # end + beacon + |> compute_sync_committee_duties(epoch, validators) + |> then(&{&1, compute_misc_data(beacon, epoch)}) + end new_duties = %{ proposers: new_proposers, attesters: new_attesters, - sync_committees: new_sync_committees + sync_committees: new_sync_committees, + misc_data: new_misc_data } log_duties_for_epoch(new_duties, epoch) @@ -107,11 +113,11 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @spec compute_proposers_for_epoch(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) :: proposer_duties_per_slot() - defp compute_proposers_for_epoch(%BeaconState{} = state, epoch, validators) do - with {:ok, epoch} <- check_valid_epoch(state, epoch), + defp compute_proposers_for_epoch(%BeaconState{} = beacon, epoch, validators) do + with {:ok, epoch} <- check_valid_epoch(beacon, epoch), {start_slot, end_slot} <- boundary_slots(epoch) do for slot <- start_slot..end_slot, - {:ok, proposer_index} = Accessors.get_beacon_proposer_index(state, slot), + {:ok, proposer_index} = Accessors.get_beacon_proposer_index(beacon, slot), Map.has_key?(validators, proposer_index), into: %{} do {slot, proposer_index} @@ -119,72 +125,124 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end end - @spec compute_current_sync_committees(BeaconState.t(), ValidatorSet.validators()) :: - sync_committee_duties() - defp compute_current_sync_committees(%BeaconState{} = state, validators) do - epoch = Accessors.get_current_epoch(state) + @spec compute_misc_data(BeaconState.t(), Types.epoch()) :: misc_data() + defp compute_misc_data(beacon, epoch) do + %{ + sync_subcommittee_participants: Utils.sync_subcommittee_participants(beacon, epoch) + } + end - for validator_index <- Map.keys(validators), - # TODO: This could be computed just once per period! - subnet_ids = Utils.compute_subnets_for_sync_committee(state, validator_index), - length(subnet_ids) > 0 do - validator_privkey = Map.get(validators, validator_index).keystore.privkey + defp sync_committee_compute_check(epoch, {_last_epoch, nil}), + do: {:not_computed, Misc.compute_sync_committee_period(epoch)} - aggregation_data = - sync_committee_aggreagtion_data(state, epoch, subnet_ids, validator_privkey) + defp sync_committee_compute_check(epoch, {last_epoch, last_duties}) do + last_period = Misc.compute_sync_committee_period(last_epoch) + current_period = Misc.compute_sync_committee_period(epoch) - %{ - last_slot_broadcasted: -1, - subnet_ids: subnet_ids, - validator_index: validator_index, - aggregation: aggregation_data - } - end + if last_period == current_period, + do: {:already_computed, last_duties.sync_committees}, + else: {:not_computed, current_period} end - # defp sync_committee_compute_check(epoch, {_last_epoch, nil}), - # do: {:not_computed, Misc.compute_sync_committee_period(epoch)} + @spec compute_sync_committee_duties(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) :: + sync_committee_duties_per_slot() + defp compute_sync_committee_duties(%BeaconState{} = beacon, epoch, validators) do + {start_slot, end_slot} = boundary_slots(epoch) + message_domain = Accessors.get_domain(beacon, Constants.domain_sync_committee(), epoch) + cont_domain = Accessors.get_domain(beacon, Constants.domain_contribution_and_proof(), epoch) - # defp sync_committee_compute_check(epoch, {last_epoch, last_duties}) do - # last_period = Misc.compute_sync_committee_period(last_epoch) - # current_period = Misc.compute_sync_committee_period(epoch) + # Slots for a particular epoch in sync committess go from start of the epoch - 1 to the end of the epoch - 1. + for slot <- max(0, start_slot - 1)..(end_slot - 1), + validator_index <- Map.keys(validators), + subnet_ids = Utils.compute_subnets_for_sync_committee(beacon, validator_index), + length(subnet_ids) > 0, + reduce: %{} do + acc -> + aggregation = + compute_sync_contribution( + beacon, + slot, + cont_domain, + subnet_ids, + validator_index, + validators + ) + + sync_committee = %{ + last_slot_broadcasted: -1, + message_domain: message_domain, + validator_index: validator_index, + subnet_ids: subnet_ids, + aggregation: aggregation + } - # if last_period == current_period, - # do: {:already_computed, last_duties.sync_committees}, - # else: {:not_computed, current_period} - # end + Map.update(acc, slot, [sync_committee], &[sync_committee | &1]) + end + end - defp sync_committee_aggreagtion_data(beacon_state, epoch, subnet_ids, validator_privkey) do + # Recomputes the sync committee duties for the given epoch without recalculating subnet_ids and + # ignoring validators already known to be outside the sync committee. + # + # Unfortunatelly, extracting the common logic between this function and `compute_sync_committee_duties` + # directly impacts readability. + defp recompute_sync_committee_duties( + sync_committee_duties, + %BeaconState{} = beacon, + epoch, + validators + ) do {start_slot, end_slot} = boundary_slots(epoch) + message_domain = Accessors.get_domain(beacon, Constants.domain_sync_committee(), epoch) + cont_domain = Accessors.get_domain(beacon, Constants.domain_contribution_and_proof(), epoch) + + [{_, sync_committee_participants}] = Enum.take(sync_committee_duties, 1) - # Slots for a particular epoch in sync committess go from start of the epoch - 1 to the end of the epoch - 1. for slot <- max(0, start_slot - 1)..(end_slot - 1), - subcommittee_index <- subnet_ids, + %{subnet_ids: subnet_ids, validator_index: validator_index} <- + sync_committee_participants, reduce: %{} do acc -> - proof = - Utils.get_sync_committee_selection_proof( - beacon_state, + aggregation = + compute_sync_contribution( + beacon, slot, - subcommittee_index, - validator_privkey + cont_domain, + subnet_ids, + validator_index, + validators ) - domain_contribution_and_proof = Constants.domain_contribution_and_proof() - domain = Accessors.get_domain(beacon_state, domain_contribution_and_proof, epoch) + sync_committee = %{ + last_slot_broadcasted: -1, + message_domain: message_domain, + validator_index: validator_index, + subnet_ids: subnet_ids, + aggregation: aggregation + } - if Utils.sync_committee_aggregator?(proof) do - aggregation = %{ - aggregated?: false, - selection_proof: proof, - signing_domain: domain, - subcommittee_index: subcommittee_index - } + Map.update(acc, slot, [sync_committee], &[sync_committee | &1]) + end + end - Map.update(acc, slot, [aggregation], &[aggregation | &1]) - else - acc - end + defp compute_sync_contribution(beacon, slot, domain, subnet_ids, validator_i, validators) do + validator_privkey = Map.get(validators, validator_i).keystore.privkey + + for subcommittee_index <- subnet_ids, + proof = + Utils.get_sync_committee_selection_proof( + beacon, + slot, + subcommittee_index, + validator_privkey + ), + Utils.sync_committee_aggregator?(proof) do + %{ + aggregated?: false, + selection_proof: proof, + contribution_domain: domain, + validator_index: validator_i, + subcommittee_index: subcommittee_index + } end end @@ -258,19 +316,19 @@ defmodule LambdaEthereumConsensus.Validator.Duties do do: get_in(duties, [epoch, :proposers, slot]) @spec current_sync_committee(duties(), Types.epoch(), Types.slot()) :: - sync_committee_duties() + [sync_committee_duty()] def current_sync_committee(duties, epoch, slot) do - for %{last_slot_broadcasted: last_slot} = duty <- sync_committee(duties, epoch), - last_slot < slot do + for %{last_slot_broadcasted: last} = duty <- sync_committee(duties, epoch, slot), + last < slot do duty end end - @spec current_sync_aggregators(duties(), Types.epoch(), Types.slot()) :: sync_committee_duties() + @spec current_sync_aggregators(duties(), Types.epoch(), Types.slot()) :: + [sync_committee_duty()] def current_sync_aggregators(duties, epoch, slot) do - for %{aggregation: aggregation} = duty <- sync_committee(duties, epoch), - Map.get(aggregation, slot), - Enum.any?(aggregation[slot], &(not Map.get(&1, :aggregated?))) do + for duty <- sync_committee(duties, epoch, slot), + Enum.any?(duty.aggregation, &(not &1.aggregated?)) do duty end end @@ -289,7 +347,12 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end end - defp sync_committee(duties, epoch), do: get_in(duties, [epoch, :sync_committees]) || [] + @spec misc_data(duties(), Types.epoch()) :: misc_data() + def misc_data(duties, epoch), do: get_in(duties, [epoch, :misc_data]) || %{} + + defp sync_committee(duties, epoch, slot), + do: get_in(duties, [epoch, :sync_committees, slot]) || [] + defp attesters(duties, epoch, slot), do: get_in(duties, [epoch, :attesters, slot]) || [] ############################ @@ -297,35 +360,13 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @spec update_duties!( duties(), - kind() | :sync_committees_contribution, + kind(), Types.epoch(), Types.slot(), attester_duties() | proposer_duties() ) :: duties() - def update_duties!(duties, :sync_committees, epoch, _slot, updated), - do: put_in(duties, [epoch, :sync_committees], updated) - - def update_duties!(duties, :sync_committees_contribution, epoch, _slot, updated) do - to_update = get_in(duties, [epoch, :sync_committees]) - - updated_duties = Enum.reduce(updated, to_update, &replace_duty_in_list/2) - - put_in(duties, [epoch, :sync_committees], updated_duties) - end - - def update_duties!(duties, kind, epoch, slot, updated), - do: put_in(duties, [epoch, kind, slot], updated) - - # FIXME: This is awful, The sync_committee duties structure is wrong and generates this complexities, I'll split it. - defp replace_duty_in_list(duty, list) do - Enum.map(list, fn d -> - if d.validator_index == duty.validator_index do - duty - else - d - end - end) - end + def update_duties!(duties, kind, epoch, slot_or_atom, updated), + do: put_in(duties, [epoch, kind, slot_or_atom], updated) @spec attested(attester_duty()) :: attester_duty() def attested(duty), do: Map.put(duty, :attested?, true) @@ -334,15 +375,17 @@ defmodule LambdaEthereumConsensus.Validator.Duties do # should_aggregate? is set to false to avoid double aggregation. def aggregated(duty), do: Map.put(duty, :should_aggregate?, false) - @spec sync_committee_broadcasted(sync_committee_duty(), Types.slot()) :: sync_committee_duty() - def sync_committee_broadcasted(duty, slot), do: Map.put(duty, :last_slot_broadcasted, slot) - - @spec sync_committee_aggregated(sync_committee_duty(), Types.slot()) :: sync_committee_duty() - def sync_committee_aggregated(duty, slot) do - updated_aggreagtion = - Enum.map(duty.aggregation[slot], fn agg -> Map.put(agg, :aggregated?, true) end) + @spec sync_committee_broadcasted(sync_committee_duty(), Types.slot()) :: + sync_committee_duty() + def sync_committee_broadcasted(duty, slot), + do: Map.put(duty, :last_slot_broadcasted, slot) - put_in(duty, [:aggregation, slot], updated_aggreagtion) + @spec sync_committee_aggregated(sync_committee_duty()) :: + sync_committee_duty() + def sync_committee_aggregated(duty) do + Map.update(duty, :aggregation, [], fn agg -> + Enum.map(agg, &Map.put(&1, :aggregated?, true)) + end) end ############################ diff --git a/lib/lambda_ethereum_consensus/validator/utils.ex b/lib/lambda_ethereum_consensus/validator/utils.ex index 595d58e82..db2880d1a 100644 --- a/lib/lambda_ethereum_consensus/validator/utils.ex +++ b/lib/lambda_ethereum_consensus/validator/utils.ex @@ -102,9 +102,9 @@ defmodule LambdaEthereumConsensus.Validator.Utils do - For subcommittee 0, validator 0 is at index 0 and validator 1 is at index 1, 2 - For subcommittee 1, validator 2 is at index 0 and 2, validator 0 is at index 1 """ - @spec participants_per_sync_subcommittee(BeaconState.t(), Types.epoch()) :: + @spec sync_subcommittee_participants(BeaconState.t(), Types.epoch()) :: %{non_neg_integer() => [Bls.pubkey()]} - def participants_per_sync_subcommittee(state, epoch) do + def sync_subcommittee_participants(state, epoch) do state |> Accessors.get_sync_committee_for_epoch!(epoch) |> Map.get(:pubkeys) @@ -114,7 +114,7 @@ defmodule LambdaEthereumConsensus.Validator.Utils do pubkeys |> Enum.with_index() |> Enum.group_by(&fetch_validator_index(state, elem(&1, 0)), &elem(&1, 1)) - |> then(fn indexes_by_validator -> {subcommittee_i, indexes_by_validator} end) + |> then(fn indices_by_validator -> {subcommittee_i, indices_by_validator} end) end) end diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 4a98f03d5..856c604bd 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -194,7 +194,6 @@ defmodule LambdaEthereumConsensus.Validator do @spec sync_committee_message_broadcast( t(), Duties.sync_committee_duty(), - Types.BeaconState.t(), Types.slot(), Types.root() ) :: @@ -202,24 +201,24 @@ defmodule LambdaEthereumConsensus.Validator do def sync_committee_message_broadcast( %{index: validator_index, keystore: keystore}, %{subnet_ids: subnet_ids} = current_duty, - head_state, slot, head_root ) do log_debug(validator_index, "broadcasting sync committee message", slot: slot) - message = get_sync_committee_message(head_state, head_root, validator_index, keystore.privkey) + message = + get_sync_committee_message(current_duty, slot, head_root, validator_index, keystore.privkey) message |> Gossip.SyncCommittee.publish(subnet_ids) |> log_info_result(validator_index, "published sync committee message", slot: slot) - aggregate_slot = current_duty |> Map.get(:aggregation) |> Map.get(slot) + aggregation = current_duty |> Map.get(:aggregation, []) - if aggregate_slot && length(aggregate_slot) > 0 do + if Enum.any?(aggregation, &(not &1.aggregated?)) do log_debug(validator_index, "collecting for future contribution", slot: slot) - aggregate_slot + aggregation |> Enum.map(& &1.subcommittee_index) |> Gossip.SyncCommittee.collect(message) |> log_debug_result(validator_index, "collected sync committee messages", slot: slot) @@ -227,20 +226,25 @@ defmodule LambdaEthereumConsensus.Validator do end @spec get_sync_committee_message( - Types.BeaconState.t(), + Duties.sync_committee_duty(), + Types.slot(), Types.root(), Types.validator_index(), Bls.privkey() ) :: Types.SyncCommitteeMessage.t() - def get_sync_committee_message(head_state, head_root, validator_index, privkey) do - epoch = Accessors.get_current_epoch(head_state) - domain = Accessors.get_domain(head_state, Constants.domain_sync_committee(), epoch) + def get_sync_committee_message( + %{message_domain: domain}, + slot, + head_root, + validator_index, + privkey + ) do signing_root = Misc.compute_signing_root(head_root, domain) {:ok, signature} = Bls.sign(privkey, signing_root) %Types.SyncCommitteeMessage{ - slot: head_state.slot, + slot: slot, beacon_block_root: head_root, validator_index: validator_index, signature: signature @@ -250,31 +254,31 @@ defmodule LambdaEthereumConsensus.Validator do @spec publish_sync_aggregate( t(), Duties.sync_committee_duty(), - Types.BeaconState.t(), - Types.epoch(), + %{}, Types.slot() ) :: :ok def publish_sync_aggregate( %{index: validator_index, keystore: keystore}, duty, - %Types.BeaconState{} = head_state, - epoch, + sync_subcommittee_participants, slot ) do - for subnet_id <- duty.subnet_ids, - agg_duty = Enum.find(duty.aggregation[slot], &(&1.subcommittee_index == subnet_id)) do + for %{subcommittee_index: subnet_id} = aggregation_duty <- duty.aggregation do case Gossip.SyncCommittee.stop_collecting(subnet_id) do {:ok, messages} -> log_md = [slot: slot, messages: messages] log_info(validator_index, "publishing sync committee aggregate", log_md) - {aggregation_bits, indexes_in_subcomittee} = - sync_committee_aggregation_bits_and_indexes(head_state, subnet_id, epoch, messages) + indices_in_subcommittee = + sync_subcommittee_participants |> Map.get(subnet_id) + + aggregation_bits = + sync_committee_aggregation_bits(indices_in_subcommittee, messages) messages - |> sync_committee_contribution(subnet_id, aggregation_bits, indexes_in_subcomittee) - |> append_sync_proof(agg_duty.selection_proof, validator_index) - |> append_sync_signature(agg_duty.signing_domain, keystore) + |> sync_committee_contribution(subnet_id, aggregation_bits, indices_in_subcommittee) + |> append_sync_proof(aggregation_duty.selection_proof, validator_index) + |> append_sync_signature(aggregation_duty.contribution_domain, keystore) |> Gossip.SyncCommittee.publish_contribution() |> log_info_result(validator_index, "published sync committee aggregate", log_md) @@ -287,31 +291,23 @@ defmodule LambdaEthereumConsensus.Validator do :ok end - defp sync_committee_aggregation_bits_and_indexes(state, subnet_id, epoch, messages) do - indexes_in_subcommittee = - state - |> Utils.participants_per_sync_subcommittee(epoch) - |> Map.get(subnet_id) - - aggregation_bits = - Enum.reduce(messages, BitList.zero(Misc.sync_subcommittee_size()), fn message, acc -> - BitList.set(acc, indexes_in_subcommittee |> Map.get(message.validator_index)) - end) - - {aggregation_bits, indexes_in_subcommittee} + defp sync_committee_aggregation_bits(indices_in_subcommittee, messages) do + Enum.reduce(messages, BitList.zero(Misc.sync_subcommittee_size()), fn message, acc -> + BitList.set(acc, indices_in_subcommittee |> Map.get(message.validator_index)) + end) end - defp sync_committee_contribution(messages, subnet_id, aggregation_bits, indexes_in_subcommittee) do + defp sync_committee_contribution(messages, subnet_id, aggregation_bits, indices_in_subcommittee) do %Types.SyncCommitteeContribution{ slot: List.first(messages).slot, beacon_block_root: List.first(messages).beacon_block_root, subcommittee_index: subnet_id, aggregation_bits: aggregation_bits, - signature: aggregate_sync_committee_signature(messages, indexes_in_subcommittee) + signature: aggregate_sync_committee_signature(messages, indices_in_subcommittee) } end - defp aggregate_sync_committee_signature(messages, indexes_in_subcommittee) do + defp aggregate_sync_committee_signature(messages, indices_in_subcommittee) do # TODO: as with attestations, we need to check why we recieve duplicate sync messages unique_messages = messages |> Enum.uniq() @@ -319,7 +315,7 @@ defmodule LambdaEthereumConsensus.Validator do Enum.flat_map(unique_messages, fn message -> # Here we duplicate the signature by n, being n the times a validator appears # in the same subcommittee - indexes_in_subcommittee + indices_in_subcommittee |> Map.get(message.validator_index) |> Enum.map(fn _ -> message.signature end) end) diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 9e8cf008d..e0f948ecf 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -82,7 +82,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do |> update_state(epoch, slot, head_root) |> maybe_attests(head_state, epoch, slot, head_root) |> maybe_build_payload(slot + 1, head_root) - |> maybe_sync_committee_broadcasts(head_state, slot, head_root) + |> maybe_sync_committee_broadcasts(slot, head_root) end @doc """ @@ -95,28 +95,29 @@ defmodule LambdaEthereumConsensus.ValidatorSet do 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) - head_state = fetch_target_state_and_go_to_slot(epoch, slot, head_root) set |> update_state(epoch, slot, head_root) - |> process_tick(head_state, epoch, slot_data) + |> process_tick(epoch, slot_data) end - defp process_tick(%{head_root: head_root} = set, _head_state, epoch, {slot, :first_third}) do + defp process_tick(%{head_root: head_root} = set, epoch, {slot, :first_third}) do maybe_propose(set, epoch, slot, head_root) end - defp process_tick(%{head_root: head_root} = set, head_state, epoch, {slot, :second_third}) do + defp process_tick(%{head_root: head_root} = set, epoch, {slot, :second_third}) do + head_state = fetch_target_state_and_go_to_slot(epoch, slot, head_root) + set |> maybe_attests(head_state, epoch, slot, head_root) |> maybe_build_payload(slot + 1, head_root) - |> maybe_sync_committee_broadcasts(head_state, slot, head_root) + |> maybe_sync_committee_broadcasts(slot, head_root) end - defp process_tick(set, head_state, epoch, {slot, :last_third}) do + defp process_tick(set, epoch, {slot, :last_third}) do set |> maybe_publish_attestation_aggregates(epoch, slot) - |> maybe_publish_sync_aggregates(head_state, slot) + |> maybe_publish_sync_aggregates(slot) end ############################## @@ -188,7 +189,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do ############################## # Sync committee - defp maybe_sync_committee_broadcasts(set, head_state, slot, head_root) do + defp maybe_sync_committee_broadcasts(set, slot, head_root) do # Sync committee is broadcasted for the next slot, so we take the duties for the correct epoch. epoch = Misc.compute_epoch_at_slot(slot + 1) @@ -198,14 +199,15 @@ defmodule LambdaEthereumConsensus.ValidatorSet do sync_committee_duties -> sync_committee_duties - |> Enum.map(&sync_committee_broadcast(&1, head_state, slot, head_root, set.validators)) + |> Enum.map(&sync_committee_broadcast(&1, slot, head_root, set.validators)) |> update_duties(set, epoch, :sync_committees, slot) end end - defp maybe_publish_sync_aggregates(set, head_state, slot) do + defp maybe_publish_sync_aggregates(set, slot) do # Sync committee is broadcasted for the next slot, so we take the duties for the correct epoch. epoch = Misc.compute_epoch_at_slot(slot + 1) + %{sync_subcommittee_participants: participants} = Duties.misc_data(set.duties, epoch) case Duties.current_sync_aggregators(set.duties, epoch, slot) do [] -> @@ -213,25 +215,25 @@ defmodule LambdaEthereumConsensus.ValidatorSet do aggregator_duties -> aggregator_duties - |> Enum.map(&publish_sync_aggregate(&1, head_state, epoch, slot, set.validators)) - |> update_duties(set, epoch, :sync_committees_contribution, slot) + |> Enum.map(&publish_sync_aggregate(&1, participants, slot, set.validators)) + |> update_duties(set, epoch, :sync_committees, slot) end end - defp sync_committee_broadcast(duty, head_state, slot, head_root, validators) do + defp sync_committee_broadcast(duty, slot, head_root, validators) do validators |> Map.get(duty.validator_index) - |> Validator.sync_committee_message_broadcast(duty, head_state, slot, head_root) + |> Validator.sync_committee_message_broadcast(duty, slot, head_root) Duties.sync_committee_broadcasted(duty, slot) end - defp publish_sync_aggregate(duty, head_state, epoch, slot, validators) do + defp publish_sync_aggregate(duty, participants, slot, validators) do validators |> Map.get(duty.validator_index) - |> Validator.publish_sync_aggregate(duty, head_state, epoch, slot) + |> Validator.publish_sync_aggregate(duty, participants, slot) - Duties.sync_committee_aggregated(duty, slot) + Duties.sync_committee_aggregated(duty) end ############################## diff --git a/lib/utils/bit_list.ex b/lib/utils/bit_list.ex index 7000d3271..1f60411f0 100644 --- a/lib/utils/bit_list.ex +++ b/lib/utils/bit_list.ex @@ -68,12 +68,12 @@ defmodule LambdaEthereumConsensus.Utils.BitList do @doc """ Set a bit or list of bits (turns them to 1). - Equivalent to bit_list[index] = 1. If indexes is a list, + Equivalent to bit_list[index] = 1. If indices is a list, it will do it for every index in the list. """ @spec set(t, [non_neg_integer]) :: t - def set(bit_list, indexes) when is_list(indexes) do - Enum.reduce(indexes, bit_list, fn index, acc -> set(acc, index) end) + def set(bit_list, indices) when is_list(indices) do + Enum.reduce(indices, bit_list, fn index, acc -> set(acc, index) end) end @spec set(t, non_neg_integer) :: t diff --git a/test/unit/subnet_info.exs b/test/unit/subnet_info.exs index dac626357..12faeabc1 100644 --- a/test/unit/subnet_info.exs +++ b/test/unit/subnet_info.exs @@ -1,4 +1,4 @@ -defmodule Unit.AttestationTest do +defmodule Unit.AttSubnetInfoTest do alias LambdaEthereumConsensus.Store.Db alias Types.AttestationData alias Types.AttSubnetInfo diff --git a/test/unit/sync_subnet_info.exs b/test/unit/sync_subnet_info.exs index 295af20e9..137d11470 100644 --- a/test/unit/sync_subnet_info.exs +++ b/test/unit/sync_subnet_info.exs @@ -1,4 +1,4 @@ -defmodule Unit.AttestationTest do +defmodule Unit.SyncSubnetInfoTest do alias LambdaEthereumConsensus.Store.Db alias Types.Checkpoint alias Types.SyncCommitteeMessage From ef353260807088351106c5bf2be63ecca8b931f5 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Sun, 8 Sep 2024 23:32:30 -0300 Subject: [PATCH 20/23] Replaced the old last_slot_broadcasted and fixed an issue in duties recalculation --- .../validator/duties.ex | 30 +++++++++---------- .../validator/validator.ex | 4 +-- .../validator/validator_set.ex | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 98978c356..0d0ebd45f 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -30,12 +30,11 @@ defmodule LambdaEthereumConsensus.Validator.Duties do aggregated?: boolean(), selection_proof: Bls.signature(), contribution_domain: Types.domain(), - validator_index: Types.validator_index(), subcommittee_index: Types.uint64() } @type sync_committee_duty :: %{ - last_slot_broadcasted: Types.slot(), + broadcasted?: boolean(), message_domain: Types.domain(), validator_index: Types.validator_index(), subnet_ids: [Types.uint64()], @@ -46,10 +45,11 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @type attester_duties :: [attester_duty()] @type proposer_duties :: [proposer_duty()] + @type sync_committee_duties :: [sync_committee_duty()] @type attester_duties_per_slot :: %{Types.slot() => attester_duties()} @type proposer_duties_per_slot :: %{Types.slot() => proposer_duties()} - @type sync_committee_duties_per_slot :: %{Types.slot() => [sync_committee_duty()]} + @type sync_committee_duties_per_slot :: %{Types.slot() => sync_committee_duties()} @type kind :: :proposers | :attesters | :sync_committees | :misc_data @type duties :: %{ @@ -169,7 +169,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do ) sync_committee = %{ - last_slot_broadcasted: -1, + broadcasted?: false, message_domain: message_domain, validator_index: validator_index, subnet_ids: subnet_ids, @@ -195,7 +195,9 @@ defmodule LambdaEthereumConsensus.Validator.Duties do message_domain = Accessors.get_domain(beacon, Constants.domain_sync_committee(), epoch) cont_domain = Accessors.get_domain(beacon, Constants.domain_contribution_and_proof(), epoch) - [{_, sync_committee_participants}] = Enum.take(sync_committee_duties, 1) + # We need to take the second slot because it wasn't yet updated, + # the first one corresponds to the previous epoch. + [_, {_, sync_committee_participants}] = Enum.take(sync_committee_duties, 2) for slot <- max(0, start_slot - 1)..(end_slot - 1), %{subnet_ids: subnet_ids, validator_index: validator_index} <- @@ -213,7 +215,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do ) sync_committee = %{ - last_slot_broadcasted: -1, + broadcasted?: false, message_domain: message_domain, validator_index: validator_index, subnet_ids: subnet_ids, @@ -240,7 +242,6 @@ defmodule LambdaEthereumConsensus.Validator.Duties do aggregated?: false, selection_proof: proof, contribution_domain: domain, - validator_index: validator_i, subcommittee_index: subcommittee_index } end @@ -318,8 +319,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @spec current_sync_committee(duties(), Types.epoch(), Types.slot()) :: [sync_committee_duty()] def current_sync_committee(duties, epoch, slot) do - for %{last_slot_broadcasted: last} = duty <- sync_committee(duties, epoch, slot), - last < slot do + for %{broadcasted?: false} = duty <- sync_committee(duties, epoch, slot) do duty end end @@ -363,10 +363,10 @@ defmodule LambdaEthereumConsensus.Validator.Duties do kind(), Types.epoch(), Types.slot(), - attester_duties() | proposer_duties() + attester_duties() | proposer_duties() | sync_committee_duties() ) :: duties() - def update_duties!(duties, kind, epoch, slot_or_atom, updated), - do: put_in(duties, [epoch, kind, slot_or_atom], updated) + 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) @@ -375,10 +375,10 @@ defmodule LambdaEthereumConsensus.Validator.Duties do # should_aggregate? is set to false to avoid double aggregation. def aggregated(duty), do: Map.put(duty, :should_aggregate?, false) - @spec sync_committee_broadcasted(sync_committee_duty(), Types.slot()) :: + @spec sync_committee_broadcasted(sync_committee_duty()) :: sync_committee_duty() - def sync_committee_broadcasted(duty, slot), - do: Map.put(duty, :last_slot_broadcasted, slot) + def sync_committee_broadcasted(duty), + do: Map.put(duty, :broadcasted?, true) @spec sync_committee_aggregated(sync_committee_duty()) :: sync_committee_duty() diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 856c604bd..34b98da6f 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -234,7 +234,7 @@ defmodule LambdaEthereumConsensus.Validator do ) :: Types.SyncCommitteeMessage.t() def get_sync_committee_message( - %{message_domain: domain}, + %{validator_index: validator_index, message_domain: domain}, slot, head_root, validator_index, @@ -259,7 +259,7 @@ defmodule LambdaEthereumConsensus.Validator do ) :: :ok def publish_sync_aggregate( %{index: validator_index, keystore: keystore}, - duty, + %{validator_index: validator_index} = duty, sync_subcommittee_participants, slot ) do diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index e0f948ecf..3fb19f74e 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -225,7 +225,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do |> Map.get(duty.validator_index) |> Validator.sync_committee_message_broadcast(duty, slot, head_root) - Duties.sync_committee_broadcasted(duty, slot) + Duties.sync_committee_broadcasted(duty) end defp publish_sync_aggregate(duty, participants, slot, validators) do From f82adb2f5affe16fe1a21fe6b0542722cadce9f0 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Mon, 9 Sep 2024 19:22:49 -0300 Subject: [PATCH 21/23] Move the fetch of the beacon state when we know its neeeded --- .../validator/validator_set.ex | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index 3fb19f74e..e0b07e855 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -75,12 +75,11 @@ defmodule LambdaEthereumConsensus.ValidatorSet do 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) - head_state = fetch_target_state_and_go_to_slot(epoch, slot, head_root) # TODO: this doesn't take into account reorgs set |> update_state(epoch, slot, head_root) - |> maybe_attests(head_state, epoch, slot, head_root) + |> maybe_attests(epoch, slot, head_root) |> maybe_build_payload(slot + 1, head_root) |> maybe_sync_committee_broadcasts(slot, head_root) end @@ -106,10 +105,8 @@ defmodule LambdaEthereumConsensus.ValidatorSet do end defp process_tick(%{head_root: head_root} = set, epoch, {slot, :second_third}) do - head_state = fetch_target_state_and_go_to_slot(epoch, slot, head_root) - set - |> maybe_attests(head_state, epoch, slot, head_root) + |> maybe_attests(epoch, slot, head_root) |> maybe_build_payload(slot + 1, head_root) |> maybe_sync_committee_broadcasts(slot, head_root) end @@ -239,12 +236,14 @@ defmodule LambdaEthereumConsensus.ValidatorSet do ############################## # Attestation - defp maybe_attests(set, head_state, epoch, slot, head_root) do + defp maybe_attests(set, epoch, slot, head_root) do case Duties.current_attesters(set.duties, epoch, slot) do [] -> set attester_duties -> + head_state = fetch_target_state_and_go_to_slot(epoch, slot, head_root) + attester_duties |> Enum.map(&attest(&1, head_state, slot, head_root, set.validators)) |> update_duties(set, epoch, :attesters, slot) From 0e2fa721b7328c3197818d554099c587ea04e1c5 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 10 Sep 2024 16:59:05 -0300 Subject: [PATCH 22/23] Comments addressed --- .../p2p/gossip/attestation.ex | 2 +- .../p2p/gossip/sync_committee.ex | 5 ++- .../validator/duties.ex | 33 ++++++++++--------- .../validator/utils.ex | 2 +- .../validator/validator.ex | 8 +++-- .../validator/validator_set.ex | 5 +-- test/unit/sync_subnet_info.exs | 28 ++-------------- 7 files changed, 33 insertions(+), 50 deletions(-) diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex b/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex index b5c7f5a43..29bb008ff 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/attestation.ex @@ -77,7 +77,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.Attestation do @spec stop_collecting(non_neg_integer()) :: {:ok, list(Types.Attestation.t())} | {:error, String.t()} def stop_collecting(subnet_id) do - # TODO: implement some way to unsubscribe without leaving the topic + # TODO: (#1289) implement some way to unsubscribe without leaving the topic topic = topic(subnet_id) Libp2pPort.leave_topic(topic) Libp2pPort.join_topic(topic) diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex index c66063084..753261055 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/sync_committee.ex @@ -85,10 +85,9 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.SyncCommittee do end @spec stop_collecting(non_neg_integer()) :: - {:ok, list(Types.Attestation.t())} | {:error, String.t()} + {:ok, list(Types.SyncCommitteeMessage.t())} | {:error, String.t()} def stop_collecting(subnet_id) do - # TODO from Attestation: implement some way to unsubscribe without leaving the topic - # TODO: This handle individual subnet_id while the other ones handle lists. + # TODO: (#1289) implement some way to unsubscribe without leaving the topic topic = topic(subnet_id) Libp2pPort.leave_topic(topic) Libp2pPort.join_topic(topic) diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index 0d0ebd45f..d61c4bcb7 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -41,7 +41,8 @@ defmodule LambdaEthereumConsensus.Validator.Duties do aggregation: [sync_committee_aggregator_duty()] } - @type misc_data :: %{sync_subcommittee_participants: %{}} + @typedoc "Useful precalculated data not tied to a particular slot/duty." + @type shared_data_for_duties :: %{sync_subcommittee_participants: %{}} @type attester_duties :: [attester_duty()] @type proposer_duties :: [proposer_duty()] @@ -51,13 +52,13 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @type proposer_duties_per_slot :: %{Types.slot() => proposer_duties()} @type sync_committee_duties_per_slot :: %{Types.slot() => sync_committee_duties()} - @type kind :: :proposers | :attesters | :sync_committees | :misc_data + @type kind :: :proposers | :attesters | :sync_committees | :shared @type duties :: %{ kind() => attester_duties_per_slot() | proposer_duties_per_slot() | sync_committee_duties_per_slot() - | misc_data() + | shared_data_for_duties() } ############################ @@ -84,26 +85,26 @@ defmodule LambdaEthereumConsensus.Validator.Duties do new_proposers = compute_proposers_for_epoch(beacon, epoch, validators) new_attesters = compute_attesters_for_epoch(beacon, epoch, validators) - {new_sync_committees, new_misc_data} = + {new_sync_committees, sync_subcommittee_participants} = case sync_committee_compute_check(epoch, {last_epoch, Map.get(duties_map, last_epoch)}) do {:already_computed, sync_committee_duties} -> sync_committee_duties |> recompute_sync_committee_duties(beacon, epoch, validators) - |> then(&{&1, misc_data(duties_map, last_epoch)}) + |> then(&{&1, sync_subcommittee_participants(duties_map, last_epoch)}) {:not_computed, period} -> Logger.debug("[Duties] Computing sync committees for period: #{period}.") beacon |> compute_sync_committee_duties(epoch, validators) - |> then(&{&1, compute_misc_data(beacon, epoch)}) + |> then(&{&1, compute_sync_subcommittee_participants(beacon, epoch)}) end new_duties = %{ proposers: new_proposers, attesters: new_attesters, sync_committees: new_sync_committees, - misc_data: new_misc_data + shared: %{sync_subcommittee_participants: sync_subcommittee_participants} } log_duties_for_epoch(new_duties, epoch) @@ -125,12 +126,11 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end end - @spec compute_misc_data(BeaconState.t(), Types.epoch()) :: misc_data() - defp compute_misc_data(beacon, epoch) do - %{ - sync_subcommittee_participants: Utils.sync_subcommittee_participants(beacon, epoch) - } - end + @spec compute_sync_subcommittee_participants(BeaconState.t(), Types.epoch()) :: %{ + non_neg_integer() => [non_neg_integer()] + } + defp compute_sync_subcommittee_participants(beacon, epoch), + do: Utils.sync_subcommittee_participants(beacon, epoch) defp sync_committee_compute_check(epoch, {_last_epoch, nil}), do: {:not_computed, Misc.compute_sync_committee_period(epoch)} @@ -347,8 +347,11 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end end - @spec misc_data(duties(), Types.epoch()) :: misc_data() - def misc_data(duties, epoch), do: get_in(duties, [epoch, :misc_data]) || %{} + @spec sync_subcommittee_participants(duties(), Types.epoch()) :: %{ + non_neg_integer() => [non_neg_integer()] + } + def sync_subcommittee_participants(duties, epoch), + do: get_in(duties, [epoch, :shared, :sync_subcommittee_participants]) || %{} defp sync_committee(duties, epoch, slot), do: get_in(duties, [epoch, :sync_committees, slot]) || [] diff --git a/lib/lambda_ethereum_consensus/validator/utils.ex b/lib/lambda_ethereum_consensus/validator/utils.ex index db2880d1a..ad6a6d05e 100644 --- a/lib/lambda_ethereum_consensus/validator/utils.ex +++ b/lib/lambda_ethereum_consensus/validator/utils.ex @@ -103,7 +103,7 @@ defmodule LambdaEthereumConsensus.Validator.Utils do - For subcommittee 1, validator 2 is at index 0 and 2, validator 0 is at index 1 """ @spec sync_subcommittee_participants(BeaconState.t(), Types.epoch()) :: - %{non_neg_integer() => [Bls.pubkey()]} + %{non_neg_integer() => [non_neg_integer()]} def sync_subcommittee_participants(state, epoch) do state |> Accessors.get_sync_committee_for_epoch!(epoch) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 34b98da6f..44aa30780 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -37,7 +37,7 @@ defmodule LambdaEthereumConsensus.Validator do @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) - # TODO: This should be handled in the ValidatorSet instead, part of #1281 + # TODO: (#1281) This should be handled in the ValidatorSet instead beacon = ValidatorSet.fetch_target_state_and_go_to_slot(epoch, head_slot, head_root) new(keystore, beacon) @@ -102,6 +102,7 @@ defmodule LambdaEthereumConsensus.Validator do @spec publish_aggregate(t(), Duties.attester_duty(), Types.slot()) :: :ok def publish_aggregate(%{index: validator_index, keystore: keystore}, duty, slot) do + # TODO: (#1286) after stop collecting for the first validator in a slot the others are not able to publish case Gossip.Attestation.stop_collecting(duty.subnet_id) do {:ok, attestations} -> log_md = [slot: slot, attestations: attestations] @@ -120,7 +121,7 @@ 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 + # TODO: (#1254) We need to check why we are producing duplicate attestations, this was generating invalid signatures unique_attestations = attestations |> Enum.uniq() aggregation_bits = @@ -263,6 +264,7 @@ defmodule LambdaEthereumConsensus.Validator do sync_subcommittee_participants, slot ) do + # TODO: (#1286) after stop collecting for the first validator in a slot the others are not able to publish for %{subcommittee_index: subnet_id} = aggregation_duty <- duty.aggregation do case Gossip.SyncCommittee.stop_collecting(subnet_id) do {:ok, messages} -> @@ -308,7 +310,7 @@ defmodule LambdaEthereumConsensus.Validator do end defp aggregate_sync_committee_signature(messages, indices_in_subcommittee) do - # TODO: as with attestations, we need to check why we recieve duplicate sync messages + # TODO: (#1254) as with attestations, we need to check why we recieve duplicate sync messages unique_messages = messages |> Enum.uniq() signatures_to_aggregate = diff --git a/lib/lambda_ethereum_consensus/validator/validator_set.ex b/lib/lambda_ethereum_consensus/validator/validator_set.ex index e0b07e855..86f579e03 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_set.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_set.ex @@ -204,13 +204,14 @@ defmodule LambdaEthereumConsensus.ValidatorSet do defp maybe_publish_sync_aggregates(set, slot) do # Sync committee is broadcasted for the next slot, so we take the duties for the correct epoch. epoch = Misc.compute_epoch_at_slot(slot + 1) - %{sync_subcommittee_participants: participants} = Duties.misc_data(set.duties, epoch) case Duties.current_sync_aggregators(set.duties, epoch, slot) do [] -> set aggregator_duties -> + participants = Duties.sync_subcommittee_participants(set.duties, epoch) + aggregator_duties |> Enum.map(&publish_sync_aggregate(&1, participants, slot, set.validators)) |> update_duties(set, epoch, :sync_committees, slot) @@ -286,7 +287,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do ########################## # Target State - + # TODO: (#1278) This should be taken from the store as noted by arkenan. @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 diff --git a/test/unit/sync_subnet_info.exs b/test/unit/sync_subnet_info.exs index 137d11470..f699c534c 100644 --- a/test/unit/sync_subnet_info.exs +++ b/test/unit/sync_subnet_info.exs @@ -29,14 +29,7 @@ defmodule Unit.SyncSubnetInfoTest do test "stop collecting with one attestation" do subnet_id = 1 - expected_message = %SyncCommitteeMessage{ - slot: 5_057_010_135_270_197_978, - beacon_block_root: - <<31, 38, 101, 174, 248, 168, 116, 226, 15, 39, 218, 148, 42, 8, 80, 80, 241, 149, 162, - 32, 176, 208, 120, 120, 89, 123, 136, 115, 154, 28, 21, 174>>, - validator_index: 0, - signature: <<>> - } + expected_message = sync_committee_message() SyncSubnetInfo.new_subnet_with_message(subnet_id, sync_committee_message()) @@ -49,23 +42,8 @@ defmodule Unit.SyncSubnetInfoTest do test "stop collecting with two attestations" do subnet_id = 1 - expected_message_1 = %SyncCommitteeMessage{ - slot: 5_057_010_135_270_197_978, - beacon_block_root: - <<31, 38, 101, 174, 248, 168, 116, 226, 15, 39, 218, 148, 42, 8, 80, 80, 241, 149, 162, - 32, 176, 208, 120, 120, 89, 123, 136, 115, 154, 28, 21, 174>>, - validator_index: 1, - signature: <<>> - } - - expected_message_2 = %SyncCommitteeMessage{ - slot: 5_057_010_135_270_197_978, - beacon_block_root: - <<31, 38, 101, 174, 248, 168, 116, 226, 15, 39, 218, 148, 42, 8, 80, 80, 241, 149, 162, - 32, 176, 208, 120, 120, 89, 123, 136, 115, 154, 28, 21, 174>>, - validator_index: 2, - signature: <<>> - } + expected_message_1 = sync_committee_message(1) + expected_message_2 = sync_committee_message(2) SyncSubnetInfo.new_subnet_with_message(subnet_id, sync_committee_message(1)) From c616a66de45bba327d830403c995009015a9f094 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Tue, 10 Sep 2024 17:01:51 -0300 Subject: [PATCH 23/23] Renamed SubnetInfo --- lib/types/{subnet_info.ex => att_subnet_info.ex} | 0 test/unit/{subnet_info.exs => att_subnet_info.exs} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename lib/types/{subnet_info.ex => att_subnet_info.ex} (100%) rename test/unit/{subnet_info.exs => att_subnet_info.exs} (100%) diff --git a/lib/types/subnet_info.ex b/lib/types/att_subnet_info.ex similarity index 100% rename from lib/types/subnet_info.ex rename to lib/types/att_subnet_info.ex diff --git a/test/unit/subnet_info.exs b/test/unit/att_subnet_info.exs similarity index 100% rename from test/unit/subnet_info.exs rename to test/unit/att_subnet_info.exs