Skip to content

Commit b3c86e2

Browse files
authored
refactor: extract validator duties into its own module. (#1101)
1 parent e8768e9 commit b3c86e2

File tree

3 files changed

+269
-202
lines changed

3 files changed

+269
-202
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
defmodule LambdaEthereumConsensus.Validator.Duties do
2+
@moduledoc """
3+
Module to handle validator duties.
4+
"""
5+
alias LambdaEthereumConsensus.StateTransition.Accessors
6+
alias LambdaEthereumConsensus.StateTransition.Misc
7+
alias LambdaEthereumConsensus.Validator
8+
alias LambdaEthereumConsensus.Validator.Utils
9+
alias Types.BeaconState
10+
11+
require Logger
12+
13+
@type attester_duty :: %{
14+
attested?: boolean(),
15+
should_aggregate?: boolean(),
16+
selection_proof: Bls.signature(),
17+
signing_domain: Types.domain(),
18+
subnet_id: Types.uint64(),
19+
slot: Types.slot(),
20+
committee_index: Types.uint64(),
21+
committee_length: Types.uint64(),
22+
index_in_committee: Types.uint64()
23+
}
24+
@type proposer_duty :: Types.slot()
25+
26+
@type attester_duties :: list(:not_computed | attester_duty())
27+
@type proposer_duties :: :not_computed | list(Types.slot())
28+
29+
@type duties :: %{
30+
attester: attester_duties(),
31+
proposer: proposer_duties()
32+
}
33+
34+
@spec empty_duties() :: duties()
35+
def empty_duties() do
36+
%{
37+
# Order is: previous epoch, current epoch, next epoch
38+
attester: [:not_computed, :not_computed, :not_computed],
39+
proposer: :not_computed
40+
}
41+
end
42+
43+
@spec get_current_attester_duty(duties :: duties(), current_slot :: Types.slot()) ::
44+
attester_duty()
45+
def get_current_attester_duty(%{attester: attester_duties}, current_slot) do
46+
Enum.find(attester_duties, fn
47+
:not_computed -> false
48+
duty -> duty.slot == current_slot
49+
end)
50+
end
51+
52+
@spec replace_attester_duty(
53+
duties :: duties(),
54+
duty :: attester_duty(),
55+
new_duty :: attester_duty()
56+
) :: duties()
57+
def replace_attester_duty(duties, duty, new_duty) do
58+
attester_duties =
59+
Enum.map(duties.attester, fn
60+
^duty -> new_duty
61+
d -> d
62+
end)
63+
64+
%{duties | attester: attester_duties}
65+
end
66+
67+
@spec log_duties(duties :: duties(), validator_index :: Types.validator_index()) :: :ok
68+
def log_duties(%{attester: attester_duties, proposer: proposer_duties}, validator_index) do
69+
attester_duties
70+
# Drop the first element, which is the previous epoch's duty
71+
|> Stream.drop(1)
72+
|> Enum.each(fn %{index_in_committee: i, committee_index: ci, slot: slot} ->
73+
Logger.debug(
74+
"[Validator] #{validator_index} has to attest in committee #{ci} of slot #{slot} with index #{i}"
75+
)
76+
end)
77+
78+
Enum.each(proposer_duties, fn slot ->
79+
Logger.info("[Validator] #{validator_index} has to propose a block in slot #{slot}!")
80+
end)
81+
end
82+
83+
@spec compute_proposer_duties(
84+
beacon_state :: BeaconState.t(),
85+
epoch :: Types.epoch(),
86+
validator_index :: Types.validator_index()
87+
) :: proposer_duties()
88+
def compute_proposer_duties(beacon_state, epoch, validator_index) do
89+
start_slot = Misc.compute_start_slot_at_epoch(epoch)
90+
91+
start_slot..(start_slot + ChainSpec.get("SLOTS_PER_EPOCH") - 1)
92+
|> Enum.flat_map(fn slot ->
93+
# Can't fail
94+
{:ok, proposer_index} = Accessors.get_beacon_proposer_index(beacon_state, slot)
95+
if proposer_index == validator_index, do: [slot], else: []
96+
end)
97+
end
98+
99+
def maybe_update_duties(duties, beacon_state, epoch, validator) do
100+
attester_duties =
101+
maybe_update_attester_duties(duties.attester, beacon_state, epoch, validator)
102+
103+
proposer_duties = compute_proposer_duties(beacon_state, epoch, validator.index)
104+
# To avoid edge-cases
105+
old_duty =
106+
case duties.proposer do
107+
:not_computed -> []
108+
old -> old |> Enum.reverse() |> Enum.take(1)
109+
end
110+
111+
%{duties | attester: attester_duties, proposer: old_duty ++ proposer_duties}
112+
end
113+
114+
defp maybe_update_attester_duties([epp, ep0, ep1], beacon_state, epoch, validator) do
115+
duties =
116+
Stream.with_index([ep0, ep1])
117+
|> Enum.map(fn
118+
{:not_computed, i} -> compute_attester_duties(beacon_state, epoch + i, validator)
119+
{d, _} -> d
120+
end)
121+
122+
[epp | duties]
123+
end
124+
125+
def shift_duties(%{attester: [_ep0, ep1, ep2]} = duties, epoch, current_epoch) do
126+
case current_epoch - epoch do
127+
1 -> %{duties | attester: [ep1, ep2, :not_computed]}
128+
2 -> %{duties | attester: [ep2, :not_computed, :not_computed]}
129+
_ -> %{duties | attester: [:not_computed, :not_computed, :not_computed]}
130+
end
131+
end
132+
133+
@spec compute_attester_duties(
134+
beacon_state :: BeaconState.t(),
135+
epoch :: Types.epoch(),
136+
validator :: Validator.validator()
137+
) :: attester_duty() | nil
138+
defp compute_attester_duties(beacon_state, epoch, validator) do
139+
# Can't fail
140+
{:ok, duty} = get_committee_assignment(beacon_state, epoch, validator.index)
141+
142+
case duty do
143+
nil ->
144+
nil
145+
146+
duty ->
147+
duty
148+
|> Map.put(:attested?, false)
149+
|> update_with_aggregation_duty(beacon_state, validator.privkey)
150+
|> update_with_subnet_id(beacon_state, epoch)
151+
end
152+
end
153+
154+
defp update_with_aggregation_duty(duty, beacon_state, privkey) do
155+
proof = Utils.get_slot_signature(beacon_state, duty.slot, privkey)
156+
157+
if Utils.aggregator?(proof, duty.committee_length) do
158+
epoch = Misc.compute_epoch_at_slot(duty.slot)
159+
domain = Accessors.get_domain(beacon_state, Constants.domain_aggregate_and_proof(), epoch)
160+
161+
Map.put(duty, :should_aggregate?, true)
162+
|> Map.put(:selection_proof, proof)
163+
|> Map.put(:signing_domain, domain)
164+
else
165+
Map.put(duty, :should_aggregate?, false)
166+
end
167+
end
168+
169+
defp update_with_subnet_id(duty, beacon_state, epoch) do
170+
committees_per_slot = Accessors.get_committee_count_per_slot(beacon_state, epoch)
171+
172+
subnet_id =
173+
Utils.compute_subnet_for_attestation(committees_per_slot, duty.slot, duty.committee_index)
174+
175+
Map.put(duty, :subnet_id, subnet_id)
176+
end
177+
178+
@doc """
179+
Return the committee assignment in the ``epoch`` for ``validator_index``.
180+
``assignment`` returned is a tuple of the following form:
181+
* ``assignment[0]`` is the index of the validator in the committee
182+
* ``assignment[1]`` is the index to which the committee is assigned
183+
* ``assignment[2]`` is the slot at which the committee is assigned
184+
Return `nil` if no assignment.
185+
"""
186+
@spec get_committee_assignment(BeaconState.t(), Types.epoch(), Types.validator_index()) ::
187+
{:ok, nil | attester_duty()} | {:error, String.t()}
188+
def get_committee_assignment(%BeaconState{} = state, epoch, validator_index) do
189+
next_epoch = Accessors.get_current_epoch(state) + 1
190+
191+
if epoch > next_epoch do
192+
{:error, "epoch must be <= next_epoch"}
193+
else
194+
start_slot = Misc.compute_start_slot_at_epoch(epoch)
195+
committee_count_per_slot = Accessors.get_committee_count_per_slot(state, epoch)
196+
end_slot = start_slot + ChainSpec.get("SLOTS_PER_EPOCH")
197+
198+
start_slot..end_slot
199+
|> Stream.map(fn slot ->
200+
0..(committee_count_per_slot - 1)
201+
|> Stream.map(&compute_attester_duty(state, slot, validator_index, &1))
202+
|> Enum.find(&(not is_nil(&1)))
203+
end)
204+
|> Enum.find(&(not is_nil(&1)))
205+
|> then(&{:ok, &1})
206+
end
207+
end
208+
209+
@spec compute_attester_duty(
210+
state :: BeaconState.t(),
211+
slot :: Types.slot(),
212+
validator_index :: Types.validator_index(),
213+
committee_index :: Types.uint64()
214+
) :: attester_duty() | nil
215+
defp compute_attester_duty(state, slot, validator_index, committee_index) do
216+
case Accessors.get_beacon_committee(state, slot, committee_index) do
217+
{:ok, committee} ->
218+
case Enum.find_index(committee, &(&1 == validator_index)) do
219+
nil ->
220+
nil
221+
222+
index ->
223+
%{
224+
index_in_committee: index,
225+
committee_length: length(committee),
226+
committee_index: committee_index,
227+
slot: slot
228+
}
229+
end
230+
231+
{:error, _} ->
232+
nil
233+
end
234+
end
235+
end

lib/lambda_ethereum_consensus/validator/utils.ex

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,65 +7,6 @@ defmodule LambdaEthereumConsensus.Validator.Utils do
77
alias Types.AttestationData
88
alias Types.BeaconState
99

10-
@type duty() :: %{
11-
index_in_committee: Types.uint64(),
12-
committee_length: Types.uint64(),
13-
committee_index: Types.uint64(),
14-
slot: Types.slot()
15-
}
16-
17-
@doc """
18-
Return the committee assignment in the ``epoch`` for ``validator_index``.
19-
``assignment`` returned is a tuple of the following form:
20-
* ``assignment[0]`` is the index of the validator in the committee
21-
* ``assignment[1]`` is the index to which the committee is assigned
22-
* ``assignment[2]`` is the slot at which the committee is assigned
23-
Return `nil` if no assignment.
24-
"""
25-
@spec get_committee_assignment(BeaconState.t(), Types.epoch(), Types.validator_index()) ::
26-
{:ok, nil | duty()} | {:error, String.t()}
27-
def get_committee_assignment(%BeaconState{} = state, epoch, validator_index) do
28-
next_epoch = Accessors.get_current_epoch(state) + 1
29-
30-
if epoch > next_epoch do
31-
{:error, "epoch must be <= next_epoch"}
32-
else
33-
start_slot = Misc.compute_start_slot_at_epoch(epoch)
34-
committee_count_per_slot = Accessors.get_committee_count_per_slot(state, epoch)
35-
end_slot = start_slot + ChainSpec.get("SLOTS_PER_EPOCH")
36-
37-
start_slot..end_slot
38-
|> Stream.map(fn slot ->
39-
0..(committee_count_per_slot - 1)
40-
|> Stream.map(&compute_duties(state, slot, validator_index, &1))
41-
|> Enum.find(&(not is_nil(&1)))
42-
end)
43-
|> Enum.find(&(not is_nil(&1)))
44-
|> then(&{:ok, &1})
45-
end
46-
end
47-
48-
defp compute_duties(state, slot, validator_index, committee_index) do
49-
case Accessors.get_beacon_committee(state, slot, committee_index) do
50-
{:ok, committee} ->
51-
case Enum.find_index(committee, &(&1 == validator_index)) do
52-
nil ->
53-
nil
54-
55-
index ->
56-
%{
57-
index_in_committee: index,
58-
committee_length: length(committee),
59-
committee_index: committee_index,
60-
slot: slot
61-
}
62-
end
63-
64-
{:error, _} ->
65-
nil
66-
end
67-
end
68-
6910
@doc """
7011
Compute the correct subnet for an attestation.
7112
"""

0 commit comments

Comments
 (0)