Skip to content

Commit 4a5b6e9

Browse files
authored
refactor: moved add and remove logic to ValidatorSet and moved out keystore fuctions (#1293)
1 parent 88ee479 commit 4a5b6e9

File tree

5 files changed

+122
-103
lines changed

5 files changed

+122
-103
lines changed

lib/keystore.ex

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,53 @@ defmodule Keystore do
2626
readonly: boolean()
2727
}
2828

29+
require Logger
30+
31+
@doc """
32+
Get validator keystores from the keystore directory.
33+
This function expects two files for each validator:
34+
- <keystore_dir>/<public_key>.json
35+
- <keystore_pass_dir>/<public_key>.txt
36+
"""
37+
@spec decode_validator_keystores(binary(), binary()) :: list(t())
38+
def decode_validator_keystores(keystore_dir, keystore_pass_dir)
39+
when is_nil(keystore_dir) or is_nil(keystore_pass_dir),
40+
do: []
41+
42+
def decode_validator_keystores(keystore_dir, keystore_pass_dir)
43+
when is_binary(keystore_dir) and is_binary(keystore_pass_dir) do
44+
keystore_dir
45+
|> File.ls!()
46+
|> Enum.flat_map(&paths_from_filename(keystore_dir, keystore_pass_dir, &1, Path.extname(&1)))
47+
|> Enum.flat_map(&decode_key/1)
48+
end
49+
50+
defp paths_from_filename(keystore_dir, keystore_pass_dir, filename, ".json") do
51+
basename = Path.basename(filename, ".json")
52+
53+
keystore_file = Path.join(keystore_dir, "#{basename}.json")
54+
keystore_pass_file = Path.join(keystore_pass_dir, "#{basename}.txt")
55+
56+
[{keystore_file, keystore_pass_file}]
57+
end
58+
59+
defp paths_from_filename(_keystore_dir, _keystore_pass_dir, basename, _ext) do
60+
Logger.warning("[Keystore] Skipping file: #{basename}. Not a json keystore file.")
61+
[]
62+
end
63+
64+
defp decode_key({keystore_file, keystore_pass_file}) do
65+
# TODO: remove `try` and handle errors properly
66+
[Keystore.decode_from_files!(keystore_file, keystore_pass_file)]
67+
rescue
68+
error ->
69+
Logger.error(
70+
"[Keystore] Failed to decode keystore file: #{keystore_file}. Pass file: #{keystore_pass_file} Error: #{inspect(error)}"
71+
)
72+
73+
[]
74+
end
75+
2976
@spec decode_from_files!(Path.t(), Path.t()) :: t()
3077
def decode_from_files!(json, password) do
3178
password = File.read!(password)

lib/lambda_ethereum_consensus/validator/utils.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ defmodule LambdaEthereumConsensus.Validator.Utils do
9494
end
9595

9696
@doc """
97-
Returns a map of subcommittee index every one of each had a map of the validators
98-
present and their index in the subcommittee. E.g.:
97+
Returns a map of subcommittee index every one of each had a map of the validators
98+
present and their index in the subcommittee. E.g.:
9999
100100
%{0 => %{0 => [0], 1 => [1, 2]}, 1 => %{2 => [0, 2], 0 => [1]}}
101101

lib/lambda_ethereum_consensus/validator/validator.ex

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ defmodule LambdaEthereumConsensus.Validator do
2121
alias LambdaEthereumConsensus.Validator.BuildBlockRequest
2222
alias LambdaEthereumConsensus.Validator.Duties
2323
alias LambdaEthereumConsensus.Validator.Utils
24-
alias LambdaEthereumConsensus.ValidatorSet
2524
alias Types.Attestation
2625

2726
@default_graffiti_message "Lambda, so gentle, so good"
@@ -34,15 +33,6 @@ defmodule LambdaEthereumConsensus.Validator do
3433
payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil
3534
}
3635

37-
@spec new(Keystore.t(), Types.slot(), Types.root()) :: t()
38-
def new(keystore, head_slot, head_root) do
39-
epoch = Misc.compute_epoch_at_slot(head_slot)
40-
# TODO: (#1281) This should be handled in the ValidatorSet instead
41-
beacon = ValidatorSet.fetch_target_state_and_go_to_slot(epoch, head_slot, head_root)
42-
43-
new(keystore, beacon)
44-
end
45-
4636
@spec new(Keystore.t(), Types.BeaconState.t()) :: t()
4737
def new(keystore, beacon) do
4838
state = %__MODULE__{

lib/lambda_ethereum_consensus/validator/validator_set.ex

Lines changed: 60 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do
55
simplify the delegation of work.
66
"""
77

8-
defstruct head_root: nil, duties: %{}, validators: %{}
8+
defstruct slot: nil, head_root: nil, duties: %{}, validators: %{}
99

1010
require Logger
1111

@@ -18,6 +18,7 @@ defmodule LambdaEthereumConsensus.ValidatorSet do
1818
@type validators :: %{Validator.index() => Validator.t()}
1919

2020
@type t :: %__MODULE__{
21+
slot: Types.slot(),
2122
head_root: Types.root() | nil,
2223
duties: %{Types.epoch() => Duties.duties()},
2324
validators: validators()
@@ -36,41 +37,76 @@ defmodule LambdaEthereumConsensus.ValidatorSet do
3637
keystore_dir = Keyword.get(config, :keystore_dir)
3738
keystore_pass_dir = Keyword.get(config, :keystore_pass_dir)
3839

39-
setup_validators(slot, head_root, keystore_dir, keystore_pass_dir)
40+
initial_keystores = Keystore.decode_validator_keystores(keystore_dir, keystore_pass_dir)
41+
42+
setup_validators(%__MODULE__{}, slot, head_root, initial_keystores)
4043
end
4144

42-
defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir)
43-
when is_nil(keystore_dir) or is_nil(keystore_pass_dir) do
44-
Logger.warning(
45-
"[Validator] No keystore_dir or keystore_pass_dir provided. Validators won't start."
46-
)
45+
defp setup_validators(set, _s, _r, []) do
46+
Logger.warning("[ValidatorSet] No keystores provided. Validator's wont start.")
4747

48-
%__MODULE__{}
48+
set
4949
end
5050

51-
defp setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) do
52-
validator_keystores = decode_validator_keystores(keystore_dir, keystore_pass_dir)
51+
defp setup_validators(set, slot, head_root, validator_keystores) do
5352
epoch = Misc.compute_epoch_at_slot(slot)
5453
beacon = fetch_target_state_and_go_to_slot(epoch, slot, head_root)
5554

56-
validators =
55+
new_validators =
5756
Map.new(validator_keystores, fn keystore ->
5857
validator = Validator.new(keystore, beacon)
5958
{validator.index, validator}
6059
end)
6160

62-
Logger.info("[Validator] Initialized #{Enum.count(validators)} validators")
61+
Logger.info("[Validator] Initialized #{Enum.count(new_validators)} validators")
6362

64-
%__MODULE__{validators: validators}
63+
%{set | validators: Map.merge(set.validators, new_validators)}
6564
|> update_state(epoch, slot, head_root)
6665
end
6766

67+
##########################
68+
# Validator management
69+
70+
@doc """
71+
Get the validators keystores
72+
"""
73+
@spec get_keystores(t()) :: list(Keystore.t())
74+
def get_keystores(%{validators: validators}),
75+
do: Enum.map(validators, fn {_index, validator} -> validator.keystore end)
76+
77+
@doc """
78+
Add a validator to the set.
79+
"""
80+
@spec add_validator(t(), Keystore.t()) :: t()
81+
def add_validator(%{slot: slot, head_root: head_root} = set, validator_keystore),
82+
do: setup_validators(set, slot, head_root, [validator_keystore])
83+
84+
@doc """
85+
Remove a validator from the set.
86+
"""
87+
@spec remove_validator(t(), Validator.index()) :: {:ok, t()} | {:error, :validator_not_found}
88+
def remove_validator(%{validators: validators} = set, pubkey) do
89+
validators
90+
|> Enum.find(fn {_index, validator} -> validator.keystore.pubkey == pubkey end)
91+
|> case do
92+
{index, _validator} ->
93+
updated_validators = Map.delete(set.validators, index)
94+
{:ok, Map.put(set, :validators, updated_validators)}
95+
96+
_ ->
97+
{:error, :validator_not_found}
98+
end
99+
end
100+
101+
##########################
102+
# Notify Tick & Head
103+
68104
@doc """
69105
Notify all validators of a new head.
70106
"""
71107
@spec notify_head(t(), Types.slot(), Types.root()) :: t()
72-
def notify_head(%{validators: validators} = state, _slot, _head_root) when validators == %{},
73-
do: state
108+
def notify_head(%{validators: validators} = set, slot, head_root) when validators == %{},
109+
do: update_state(set, Misc.compute_epoch_at_slot(slot), slot, head_root)
74110

75111
def notify_head(set, slot, head_root) do
76112
Logger.debug("[ValidatorSet] New Head", root: head_root, slot: slot)
@@ -88,8 +124,8 @@ defmodule LambdaEthereumConsensus.ValidatorSet do
88124
Notify all validators of a new tick.
89125
"""
90126
@spec notify_tick(t(), tuple()) :: t()
91-
def notify_tick(%{validators: validators} = state, _slot_data) when validators == %{},
92-
do: state
127+
def notify_tick(%{validators: validators} = set, _slot_data) when validators == %{},
128+
do: set
93129

94130
def notify_tick(%{head_root: head_root} = set, {slot, third} = slot_data) do
95131
Logger.debug("[ValidatorSet] Tick #{inspect(third)}", root: head_root, slot: slot)
@@ -122,12 +158,16 @@ defmodule LambdaEthereumConsensus.ValidatorSet do
122158

123159
defp update_state(set, epoch, slot, head_root) do
124160
set
125-
|> update_head(head_root)
161+
|> update_slot_and_head(slot, head_root)
126162
|> compute_duties(epoch, slot, head_root)
127163
end
128164

129-
defp update_head(%{head_root: head_root} = set, head_root), do: set
130-
defp update_head(set, head_root), do: %{set | head_root: head_root}
165+
defp update_slot_and_head(%{slot: slot, head_root: head_root} = set, slot, head_root), do: set
166+
defp update_slot_and_head(set, slot, head_root), do: %{set | slot: slot, head_root: head_root}
167+
168+
defp compute_duties(%{validators: validators} = set, _epoch, _slot, _head_root)
169+
when validators == %{},
170+
do: set
131171

132172
defp compute_duties(set, epoch, _slot, _head_root)
133173
when is_duties_computed(set, epoch) and is_duties_computed(set, epoch + 1),
@@ -315,49 +355,4 @@ defmodule LambdaEthereumConsensus.ValidatorSet do
315355
{:ok, st} = StateTransition.process_slots(state, slot)
316356
st
317357
end
318-
319-
##############################
320-
# Key management
321-
322-
@doc """
323-
Get validator keystores from the keystore directory.
324-
This function expects two files for each validator:
325-
- <keystore_dir>/<public_key>.json
326-
- <keystore_pass_dir>/<public_key>.txt
327-
"""
328-
@spec decode_validator_keystores(binary(), binary()) ::
329-
list(Keystore.t())
330-
def decode_validator_keystores(keystore_dir, keystore_pass_dir)
331-
when is_binary(keystore_dir) and is_binary(keystore_pass_dir) do
332-
keystore_dir
333-
|> File.ls!()
334-
|> Enum.flat_map(&paths_from_filename(keystore_dir, keystore_pass_dir, &1, Path.extname(&1)))
335-
|> Enum.flat_map(&decode_key/1)
336-
end
337-
338-
defp paths_from_filename(keystore_dir, keystore_pass_dir, filename, ".json") do
339-
basename = Path.basename(filename, ".json")
340-
341-
keystore_file = Path.join(keystore_dir, "#{basename}.json")
342-
keystore_pass_file = Path.join(keystore_pass_dir, "#{basename}.txt")
343-
344-
[{keystore_file, keystore_pass_file}]
345-
end
346-
347-
defp paths_from_filename(_keystore_dir, _keystore_pass_dir, basename, _ext) do
348-
Logger.warning("[Validator] Skipping file: #{basename}. Not a json keystore file.")
349-
[]
350-
end
351-
352-
defp decode_key({keystore_file, keystore_pass_file}) do
353-
# TODO: remove `try` and handle errors properly
354-
[Keystore.decode_from_files!(keystore_file, keystore_pass_file)]
355-
rescue
356-
error ->
357-
Logger.error(
358-
"[Validator] Failed to decode keystore file: #{keystore_file}. Pass file: #{keystore_pass_file} Error: #{inspect(error)}"
359-
)
360-
361-
[]
362-
end
363358
end

lib/libp2p_port.ex

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ defmodule LambdaEthereumConsensus.Libp2pPort do
2222
alias LambdaEthereumConsensus.P2P.Peerbook
2323
alias LambdaEthereumConsensus.StateTransition.Misc
2424
alias LambdaEthereumConsensus.Utils.BitVector
25-
alias LambdaEthereumConsensus.Validator
2625
alias LambdaEthereumConsensus.ValidatorSet
2726
alias Libp2pProto.AddPeer
2827
alias Libp2pProto.Command
@@ -551,41 +550,29 @@ defmodule LambdaEthereumConsensus.Libp2pPort do
551550

552551
@impl GenServer
553552
def handle_call(:get_keystores, _from, %{validator_set: validator_set} = state),
554-
do:
555-
{:reply,
556-
Enum.map(validator_set.validators, fn {_index, validator} -> validator.keystore end),
557-
state}
553+
do: {:reply, ValidatorSet.get_keystores(validator_set), state}
558554

559555
@impl GenServer
560556
def handle_call({:delete_validator, pubkey}, _from, %{validator_set: validator_set} = state) do
561-
validator_set.validators
562-
|> Enum.find(fn {_index, validator} -> validator.keystore.pubkey == pubkey end)
563-
|> case do
564-
{index, _validator} ->
565-
Logger.warning("[Libp2pPort] Deleting validator with index #{inspect(index)}.")
566-
updated_validators = Map.delete(validator_set.validators, index)
567-
{:reply, :ok, Map.put(state.validator_set, :validators, updated_validators)}
568-
569-
_ ->
570-
{:error, "Pubkey #{inspect(pubkey)} not found."}
557+
case ValidatorSet.remove_validator(validator_set, pubkey) do
558+
{:ok, validator_set} ->
559+
Logger.warning("[Libp2pPort] Deleted validator with pubkey #{inspect(pubkey)}.")
560+
561+
{:reply, :ok, %{state | validator_set: validator_set}}
562+
563+
{:error, :validator_not_found} ->
564+
{:reply, {:error, "Validator #{inspect(pubkey)} not found."}, state}
571565
end
572566
end
573567

574568
@impl GenServer
575-
def handle_call(
576-
{:add_validator, keystore},
577-
_from,
578-
%{validator_set: %{head_root: head_root}, slot_data: {slot, _third}} =
579-
state
580-
) do
569+
def handle_call({:add_validator, keystore}, _from, %{validator_set: validator_set} = state) do
581570
# TODO (#1263): handle 0 validators
582-
validator = Validator.new(keystore, slot, head_root)
571+
validator_set = ValidatorSet.add_validator(validator_set, keystore)
583572

584-
Logger.warning(
585-
"[Libp2pPort] Adding validator with index #{inspect(validator.index)}. head_slot: #{inspect(slot)}."
586-
)
573+
Logger.warning("[Libp2pPort] Added validator #{keystore.pubkey} to the set.")
587574

588-
{:reply, :ok, put_in(state.validator_set, [:validators, validator.index], validator)}
575+
{:reply, :ok, %{state | validator_set: validator_set}}
589576
end
590577

591578
######################

0 commit comments

Comments
 (0)