Skip to content

Commit

Permalink
Initial Duties cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
rodrigo-o committed Aug 15, 2024
1 parent c030f1b commit 8c1d818
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 225 deletions.
237 changes: 23 additions & 214 deletions lib/lambda_ethereum_consensus/validator/duties.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do
alias LambdaEthereumConsensus.StateTransition.Misc
alias LambdaEthereumConsensus.Validator
alias LambdaEthereumConsensus.Validator.Utils
alias LambdaEthereumConsensus.ValidatorSet
alias Types.BeaconState

require Logger
Expand All @@ -22,26 +23,16 @@ defmodule LambdaEthereumConsensus.Validator.Duties do
committee_length: Types.uint64(),
index_in_committee: Types.uint64()
}
@type proposer_duty :: Types.slot()

@type attester_duties :: list(:not_computed | attester_duty())
@type proposer_duties :: :not_computed | list(Types.slot())
@type proposer_duty :: Types.slot()

@type duties :: %{
attester: attester_duties(),
proposer: proposer_duties()
}
@type attester_duties :: %{Types.slot() => [attester_duty()]}
@type proposer_duties :: %{Types.slot() => [proposer_duty()]}

@spec empty_duties() :: duties()
def empty_duties() do
%{
# Order is: previous epoch, current epoch, next epoch
attester: [:not_computed, :not_computed, :not_computed],
proposer: :not_computed
}
end
@type duties :: %{attesters: attester_duties(), proposers: proposer_duties()}

@spec compute_proposers_for_epoch(BeaconState.t(), Types.epoch(), %{}) :: any()
@spec compute_proposers_for_epoch(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) ::
proposer_duties()
def compute_proposers_for_epoch(%BeaconState{} = state, epoch, validators) do
with {:ok, epoch} <- check_valid_epoch(state, epoch),
{start_slot, end_slot} <- boundary_slots(epoch) do
Expand All @@ -53,11 +44,12 @@ defmodule LambdaEthereumConsensus.Validator.Duties do
do: [{slot, proposer_index}],
else: []
end)
|> then(&{:ok, Map.new(&1)})
|> Map.new()
end
end

@spec compute_attesters_for_epoch(BeaconState.t(), Types.epoch(), %{}) :: any()
@spec compute_attesters_for_epoch(BeaconState.t(), Types.epoch(), ValidatorSet.validators()) ::
attester_duties()
def compute_attesters_for_epoch(%BeaconState{} = state, epoch, validators) do
with {:ok, epoch} <- check_valid_epoch(state, epoch),
{start_slot, end_slot} <- boundary_slots(epoch) do
Expand All @@ -66,20 +58,20 @@ defmodule LambdaEthereumConsensus.Validator.Duties do
start_slot..end_slot
|> Enum.flat_map(fn slot ->
0..(committee_count_per_slot - 1)
|> Enum.flat_map(&compute_attester_dutys(state, epoch, slot, validators, &1))
|> Enum.flat_map(&compute_attester_duties(state, epoch, slot, validators, &1))
end)
|> then(&{:ok, Map.new(&1)})
|> Map.new()
end
end

@spec compute_attester_dutys(
@spec compute_attester_duties(
state :: BeaconState.t(),
epoch :: Types.epoch(),
slot :: Types.slot(),
validators :: %{Types.validator_index() => Validator.t()},
committee_index :: Types.uint64()
) :: [{Types.slot(), attester_duty()}]
defp compute_attester_dutys(state, epoch, slot, validators, committee_index) do
defp compute_attester_duties(state, epoch, slot, validators, committee_index) do
case Accessors.get_beacon_committee(state, slot, committee_index) do
{:ok, committee} ->
compute_cometee_duties(state, epoch, slot, committee, committee_index, validators)
Expand Down Expand Up @@ -119,150 +111,6 @@ defmodule LambdaEthereumConsensus.Validator.Duties do
end
end

defp check_valid_epoch(state, epoch) do
next_epoch = Accessors.get_current_epoch(state) + 1

if epoch > next_epoch do
{:error, "epoch must be <= next_epoch"}
else
{:ok, epoch}
end
end

defp boundary_slots(epoch) do
start_slot = Misc.compute_start_slot_at_epoch(epoch)
end_slot = start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - 1

{start_slot, end_slot}
end

@spec get_current_attester_duty(duties :: duties(), current_slot :: Types.slot()) ::
attester_duty()
def get_current_attester_duty(%{attester: attester_duties}, current_slot) do
Enum.find(attester_duties, fn
:not_computed -> false
duty -> duty.slot == current_slot
end)
end

@spec replace_attester_duty(
duties :: duties(),
duty :: attester_duty(),
new_duty :: attester_duty()
) :: duties()
def replace_attester_duty(duties, duty, new_duty) do
attester_duties =
Enum.map(duties.attester, fn
^duty -> new_duty
d -> d
end)

%{duties | attester: attester_duties}
end

@spec log_duties(duties :: duties(), validator_index :: Types.validator_index()) :: :ok
def log_duties(%{attester: attester_duties, proposer: proposer_duties}, validator_index) do
attester_duties
# Drop the first element, which is the previous epoch's duty
|> Stream.drop(1)
|> Enum.each(fn %{
index_in_committee: i,
committee_index: ci,
slot: slot,
should_aggregate?: sa
} ->
Logger.info(
"[Validator] #{validator_index} has to attest in committee #{ci} of slot #{slot} with index #{i}, and should_aggregate?: #{sa}"
)
end)

Enum.each(proposer_duties, fn slot ->
Logger.info("[Validator] #{validator_index} has to propose a block in slot #{slot}!")
end)
end

@spec compute_proposer_duties(
beacon_state :: BeaconState.t(),
epoch :: Types.epoch(),
validator_index :: Types.validator_index()
) :: proposer_duties()
# TODO: Remove, already moved to an epoch-based approach
def compute_proposer_duties(beacon_state, epoch, validator_index) do
start_slot = Misc.compute_start_slot_at_epoch(epoch)

start_slot..(start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - 1)
|> Enum.flat_map(fn slot ->
# Can't fail
{:ok, proposer_index} = Accessors.get_beacon_proposer_index(beacon_state, slot)
if proposer_index == validator_index, do: [slot], else: []
end)
end

def maybe_update_duties(duties, beacon_state, epoch, validator_index, privkey) do
attester_duties =
maybe_update_attester_duties(duties.attester, beacon_state, epoch, validator_index, privkey)

proposer_duties = compute_proposer_duties(beacon_state, epoch, validator_index)
# To avoid edge-cases
old_duty =
case duties.proposer do
:not_computed -> []
old -> old |> Enum.reverse() |> Enum.take(1)
end

%{duties | attester: attester_duties, proposer: old_duty ++ proposer_duties}
end

defp maybe_update_attester_duties(
[epp, ep0, ep1],
beacon_state,
epoch,
validator_index,
privkey
) do
duties =
Stream.with_index([ep0, ep1])
|> Enum.map(fn
{:not_computed, i} ->
compute_attester_duties(beacon_state, epoch + i, validator_index, privkey)

{d, _} ->
d
end)

[epp | duties]
end

def shift_duties(%{attester: [_ep0, ep1, ep2]} = duties, epoch, current_epoch) do
case current_epoch - epoch do
1 -> %{duties | attester: [ep1, ep2, :not_computed]}
2 -> %{duties | attester: [ep2, :not_computed, :not_computed]}
_ -> %{duties | attester: [:not_computed, :not_computed, :not_computed]}
end
end

@spec compute_attester_duties(
beacon_state :: BeaconState.t(),
epoch :: Types.epoch(),
validator_index :: non_neg_integer(),
privkey :: Bls.privkey()
) :: attester_duty() | nil
defp compute_attester_duties(beacon_state, epoch, validator_index, privkey) do
# Can't fail
{:ok, duty} = get_committee_assignment(beacon_state, epoch, validator_index)

case duty do
nil ->
nil

duty ->
duty
|> Map.put(:attested?, false)
|> update_with_aggregation_duty(beacon_state, privkey)
|> update_with_subnet_id(beacon_state, epoch)
end
end

defp update_with_aggregation_duty(duty, beacon_state, privkey) do
proof = Utils.get_slot_signature(beacon_state, duty.slot, privkey)

Expand All @@ -287,62 +135,23 @@ defmodule LambdaEthereumConsensus.Validator.Duties do
Map.put(duty, :subnet_id, subnet_id)
end

@doc """
TODO: This is not the case anymore?
Return the committee assignment in the ``epoch`` for ``validator_index``.
``assignment`` returned is a tuple of the following form:
* ``assignment[0]`` is the index of the validator in the committee
* ``assignment[1]`` is the index to which the committee is assigned
* ``assignment[2]`` is the slot at which the committee is assigned
Return `nil` if no assignment.
"""
@spec get_committee_assignment(BeaconState.t(), Types.epoch(), Types.validator_index()) ::
{:ok, nil | attester_duty()} | {:error, String.t()}
def get_committee_assignment(%BeaconState{} = state, epoch, validator_index) do
############################
# Helpers

defp check_valid_epoch(state, epoch) do
next_epoch = Accessors.get_current_epoch(state) + 1

if epoch > next_epoch do
{:error, "epoch must be <= next_epoch"}
else
start_slot = Misc.compute_start_slot_at_epoch(epoch)
committee_count_per_slot = Accessors.get_committee_count_per_slot(state, epoch)
end_slot = start_slot + ChainSpec.get("SLOTS_PER_EPOCH")

start_slot..end_slot
|> Stream.map(fn slot ->
0..(committee_count_per_slot - 1)
|> Stream.map(&compute_attester_duty(state, slot, validator_index, &1))
|> Enum.find(&(not is_nil(&1)))
end)
|> Enum.find(&(not is_nil(&1)))
|> then(&{:ok, &1})
{:ok, epoch}
end
end

@spec compute_attester_duty(
state :: BeaconState.t(),
slot :: Types.slot(),
validator_index :: Types.validator_index(),
committee_index :: Types.uint64()
) :: attester_duty() | nil
defp compute_attester_duty(state, slot, validator_index, committee_index) do
case Accessors.get_beacon_committee(state, slot, committee_index) do
{:ok, committee} ->
case Enum.find_index(committee, &(&1 == validator_index)) do
nil ->
nil

index ->
%{
index_in_committee: index,
committee_length: length(committee),
committee_index: committee_index,
slot: slot
}
end
defp boundary_slots(epoch) do
start_slot = Misc.compute_start_slot_at_epoch(epoch)
end_slot = start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - 1

{:error, _} ->
nil
end
{start_slot, end_slot}
end
end
8 changes: 4 additions & 4 deletions lib/lambda_ethereum_consensus/validator/validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ defmodule LambdaEthereumConsensus.Validator do

@default_graffiti_message "Lambda, so gentle, so good"

# TODO: Slot and Root are redundant, we should also have the duties separated and calculated
# just at the begining of every epoch, and then just update them as needed.
@type index :: non_neg_integer()

@type t :: %__MODULE__{
index: non_neg_integer() | nil,
index: index() | nil,
keystore: Keystore.t(),
payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil
}
Expand Down Expand Up @@ -102,7 +102,7 @@ defmodule LambdaEthereumConsensus.Validator do
debug_log_msg =
"publishing attestation on committee index: #{current_duty.committee_index} | as #{current_duty.index_in_committee}/#{current_duty.committee_length - 1} and pubkey: #{LambdaEthereumConsensus.Utils.format_shorten_binary(keystore.pubkey)}"

log_info(validator_index, debug_log_msg, log_md)
log_debug(validator_index, debug_log_msg, log_md)

Gossip.Attestation.publish(subnet_id, attestation)
|> log_info_result(validator_index, "published attestation", log_md)
Expand Down
11 changes: 4 additions & 7 deletions lib/lambda_ethereum_consensus/validator/validator_set.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ defmodule LambdaEthereumConsensus.ValidatorSet do
alias LambdaEthereumConsensus.Validator
alias LambdaEthereumConsensus.Validator.Duties

@type validators :: %{atom() => %{} | []}
@type validators :: %{Validator.index() => Validator.t()}

@type t :: %__MODULE__{
head_root: Types.root() | nil,
duties: %{
Expand Down Expand Up @@ -137,17 +138,13 @@ defmodule LambdaEthereumConsensus.ValidatorSet do
end

defp compute_duties_for_epoch!(beacon, epoch, validators) do
{:ok, proposers} = Duties.compute_proposers_for_epoch(beacon, epoch, validators)
{:ok, attesters} = Duties.compute_attesters_for_epoch(beacon, epoch, validators)
proposers = Duties.compute_proposers_for_epoch(beacon, epoch, validators)
attesters = Duties.compute_attesters_for_epoch(beacon, epoch, validators)

Logger.info(
"[Validator] Proposer duties for epoch #{epoch} are: #{inspect(proposers, pretty: true)}"
)

Logger.info(
"[Validator] Attester duties for epoch #{epoch} are: #{inspect(attesters, pretty: true)}"
)

%{epoch => %{proposers: proposers, attesters: attesters}}
end

Expand Down

0 comments on commit 8c1d818

Please sign in to comment.