1
1
defmodule LambdaEthereumConsensus.Validator do
2
2
@ moduledoc """
3
- GenServer that performs validator duties.
3
+ Module that performs validator duties.
4
4
"""
5
- use GenServer
6
5
require Logger
7
6
7
+ defstruct [
8
+ :slot ,
9
+ :root ,
10
+ :duties ,
11
+ :validator ,
12
+ :payload_builder
13
+ ]
14
+
15
+ alias LambdaEthereumConsensus.Beacon.Clock
8
16
alias LambdaEthereumConsensus.ForkChoice
9
17
alias LambdaEthereumConsensus.Libp2pPort
10
18
alias LambdaEthereumConsensus.P2P.Gossip
@@ -23,40 +31,25 @@ defmodule LambdaEthereumConsensus.Validator do
23
31
24
32
@ default_graffiti_message "Lambda, so gentle, so good"
25
33
26
- ##########################
27
- ### Public API
28
- ##########################
29
-
30
- def start_link ( { _ , _ , { pubkey , _ } } = opts ) do
31
- # TODO: if possible, use validator index instead of pubkey?
32
- name =
33
- Atom . to_string ( __MODULE__ ) <> LambdaEthereumConsensus.Utils . format_shorten_binary ( pubkey )
34
-
35
- GenServer . start_link ( __MODULE__ , opts , name: String . to_atom ( name ) )
36
- end
37
-
38
- def notify_new_block ( slot , head_root ) ,
39
- do: GenServer . cast ( __MODULE__ , { :new_block , slot , head_root } )
40
-
41
- ##########################
42
- ### GenServer Callbacks
43
- ##########################
44
-
45
- @ type validator :: any ( )
34
+ @ type validator :: % {
35
+ index: non_neg_integer ( ) | nil ,
36
+ pubkey: Bls . pubkey ( ) ,
37
+ privkey: Bls . privkey ( )
38
+ }
46
39
47
- @ type state :: % {
40
+ # TODO: Slot and Root are redundant, we should also have the duties separated and calculated
41
+ # just at the begining of every epoch, and then just update them as needed.
42
+ @ type state :: % __MODULE__ {
48
43
slot: Types . slot ( ) ,
49
44
root: Types . root ( ) ,
50
45
duties: Duties . duties ( ) ,
51
46
validator: validator ( ) ,
52
47
payload_builder: { Types . slot ( ) , Types . root ( ) , BlockBuilder . payload_id ( ) } | nil
53
48
}
54
49
55
- @ impl true
56
- @ spec init ( { Types . slot ( ) , Types . root ( ) , { Bls . pubkey ( ) , Bls . privkey ( ) } } ) ::
57
- { :ok , state , { :continue , any } }
58
- def init ( { head_slot , head_root , { pubkey , privkey } } ) do
59
- state = % {
50
+ @ spec new ( { Types . slot ( ) , Types . root ( ) , { Bls . pubkey ( ) , Bls . privkey ( ) } } ) :: state
51
+ def new ( { head_slot , head_root , { pubkey , privkey } } ) do
52
+ state = % __MODULE__ {
60
53
slot: head_slot ,
61
54
root: head_root ,
62
55
duties: Duties . empty_duties ( ) ,
@@ -68,19 +61,16 @@ defmodule LambdaEthereumConsensus.Validator do
68
61
payload_builder: nil
69
62
}
70
63
71
- { :ok , state , { :continue , nil } }
72
- end
73
-
74
- @ impl true
75
- @ spec handle_continue ( any ( ) , state ) :: { :noreply , state }
76
- def handle_continue ( _ , % { slot: slot , root: root } = state ) do
77
- case try_setup_validator ( state , slot , root ) do
64
+ case try_setup_validator ( state , head_slot , head_root ) do
78
65
nil ->
66
+ # TODO: Previously this was handled by the validator continously trying to setup itself,
67
+ # but now that they are processed syncronously, we should handle this case different.
68
+ # Right now it's just omitted and logged.
79
69
Logger . error ( "[Validator] Public key not found in the validator set" )
80
- { :noreply , state }
70
+ state
81
71
82
72
new_state ->
83
- { :noreply , new_state }
73
+ new_state
84
74
end
85
75
end
86
76
@@ -94,7 +84,7 @@ defmodule LambdaEthereumConsensus.Validator do
94
84
nil
95
85
96
86
validator_index ->
97
- Logger . info ( "[Validator] Setup for validator number #{ validator_index } complete" )
87
+ log_info ( validator_index , "setup validator" , slot: slot , root: root )
98
88
validator = % { state . validator | index: validator_index }
99
89
duties = Duties . maybe_update_duties ( state . duties , beacon , epoch , validator )
100
90
join_subnets_for_duties ( duties )
@@ -103,49 +93,43 @@ defmodule LambdaEthereumConsensus.Validator do
103
93
end
104
94
end
105
95
106
- @ spec handle_cast ( any , state ) :: { :noreply , state }
107
-
108
- @ impl true
109
- def handle_cast ( _ , % { validator: nil } = state ) , do: { :noreply , state }
96
+ @ spec handle_new_block ( Types . slot ( ) , Types . root ( ) , state ) :: state
97
+ def handle_new_block ( slot , head_root , % { validator: % { index: nil } } = state ) do
98
+ log_error ( "-1" , "setup validator" , "index not present handle block" ,
99
+ slot: slot ,
100
+ root: head_root
101
+ )
110
102
111
- # If we couldn't find the validator before, we just try again
112
- def handle_cast ( { :new_block , slot , head_root } = msg , % { validator: % { index: nil } } = state ) do
113
- case try_setup_validator ( state , slot , head_root ) do
114
- nil -> { :noreply , state }
115
- new_state -> handle_cast ( msg , new_state )
116
- end
103
+ state
117
104
end
118
105
119
- def handle_cast ( { :new_block , slot , head_root } , state ) ,
120
- do: { :noreply , handle_new_block ( slot , head_root , state ) }
121
-
122
- def handle_cast ( { :on_tick , _ } , % { validator: % { index: nil } } = state ) , do: { :noreply , state }
106
+ def handle_new_block ( slot , head_root , state ) do
107
+ log_debug ( state . validator . index , "recieved new block" , slot: slot , root: head_root )
123
108
124
- def handle_cast ( { :on_tick , logical_time } , state ) ,
125
- do: { :noreply , handle_tick ( logical_time , state ) }
126
-
127
- ##########################
128
- ### Private Functions
129
- ##########################
130
-
131
- @ spec handle_new_block ( Types . slot ( ) , Types . root ( ) , state ) :: state
132
- defp handle_new_block ( slot , head_root , state ) do
133
109
# TODO: this doesn't take into account reorgs
134
110
state
135
111
|> update_state ( slot , head_root )
136
112
|> maybe_attest ( slot )
137
113
|> maybe_build_payload ( slot + 1 )
138
114
end
139
115
140
- defp handle_tick ( { slot , :first_third } , state ) do
116
+ @ spec handle_tick ( Clock . logical_time ( ) , state ) :: state
117
+ def handle_tick ( _logical_time , % { validator: % { index: nil } } = state ) do
118
+ log_error ( "-1" , "setup validator" , "index not present for handle tick" )
119
+ state
120
+ end
121
+
122
+ def handle_tick ( { slot , :first_third } , state ) do
123
+ log_debug ( state . validator . index , "started first third" , slot: slot )
141
124
# Here we may:
142
125
# 1. propose our blocks
143
126
# 2. (TODO) start collecting attestations for aggregation
144
127
maybe_propose ( state , slot )
145
128
|> update_state ( slot , state . root )
146
129
end
147
130
148
- defp handle_tick ( { slot , :second_third } , state ) do
131
+ def handle_tick ( { slot , :second_third } , state ) do
132
+ log_debug ( state . validator . index , "started second third" , slot: slot )
149
133
# Here we may:
150
134
# 1. send our attestation for an empty slot
151
135
# 2. start building a payload
@@ -154,11 +138,16 @@ defmodule LambdaEthereumConsensus.Validator do
154
138
|> maybe_build_payload ( slot + 1 )
155
139
end
156
140
157
- defp handle_tick ( { slot , :last_third } , state ) do
141
+ def handle_tick ( { slot , :last_third } , state ) do
142
+ log_debug ( state . validator . index , "started last third" , slot: slot )
158
143
# Here we may publish our attestation aggregate
159
144
maybe_publish_aggregate ( state , slot )
160
145
end
161
146
147
+ ##########################
148
+ ### Private Functions
149
+ ##########################
150
+
162
151
@ spec update_state ( state , Types . slot ( ) , Types . root ( ) ) :: state
163
152
164
153
defp update_state ( % { slot: slot , root: root } = state , slot , root ) , do: state
@@ -256,16 +245,23 @@ defmodule LambdaEthereumConsensus.Validator do
256
245
end
257
246
258
247
@ spec attest ( state , Duties . attester_duty ( ) ) :: :ok
259
- defp attest ( state , current_duty ) do
248
+ defp attest ( % { validator: validator } = state , current_duty ) do
260
249
subnet_id = current_duty . subnet_id
250
+ log_debug ( validator . index , "attesting" , slot: current_duty . slot , subnet_id: subnet_id )
251
+
261
252
attestation = produce_attestation ( current_duty , state . root , state . validator . privkey )
262
253
263
- Logger . info ( "[Validator] Attesting in slot #{ attestation . data . slot } on subnet #{ subnet_id } " )
254
+ log_md = [ slot: attestation . data . slot , attestation: attestation , subnet_id: subnet_id ]
255
+ log_debug ( validator . index , "publishing attestation" , log_md )
256
+
264
257
Gossip.Attestation . publish ( subnet_id , attestation )
258
+ |> log_debug_result ( validator . index , "published attestation" , log_md )
265
259
266
260
if current_duty . should_aggregate? do
267
- Logger . info ( "[Validator] Collecting messages for future aggregation..." )
261
+ log_debug ( validator . index , "collecting for future aggregation" , log_md )
262
+
268
263
Gossip.Attestation . collect ( subnet_id , attestation )
264
+ |> log_debug_result ( validator . index , "collected attestation" , log_md )
269
265
end
270
266
end
271
267
@@ -288,14 +284,17 @@ defmodule LambdaEthereumConsensus.Validator do
288
284
defp publish_aggregate ( duty , validator ) do
289
285
case Gossip.Attestation . stop_collecting ( duty . subnet_id ) do
290
286
{ :ok , attestations } ->
291
- Logger . info ( "[Validator] Publishing aggregate of slot #{ duty . slot } " )
287
+ log_md = [ slot: duty . slot , attestations: attestations ]
288
+ log_debug ( validator . index , "publishing aggregate" , log_md )
292
289
293
290
aggregate_attestations ( attestations )
294
291
|> append_proof ( duty . selection_proof , validator )
295
292
|> append_signature ( duty . signing_domain , validator )
296
293
|> Gossip.Attestation . publish_aggregate ( )
294
+ |> log_info_result ( validator . index , "published aggregate" , log_md )
297
295
298
- _ ->
296
+ { :error , reason } ->
297
+ log_error ( validator . index , "stop collecting attestations" , reason )
299
298
:ok
300
299
end
301
300
end
@@ -378,7 +377,7 @@ defmodule LambdaEthereumConsensus.Validator do
378
377
BlockStates . get_state_info! ( parent_root ) . beacon_state |> go_to_slot ( slot )
379
378
end
380
379
381
- @ spec fetch_validator_index ( Types.BeaconState . t ( ) , % { index: nil , pubkey: Bls . pubkey ( ) } ) ::
380
+ @ spec fetch_validator_index ( Types.BeaconState . t ( ) , validator ( ) ) ::
382
381
non_neg_integer ( ) | nil
383
382
defp fetch_validator_index ( beacon , % { index: nil , pubkey: pk } ) do
384
383
Enum . find_index ( beacon . validators , & ( & 1 . pubkey == pk ) )
@@ -399,18 +398,18 @@ defmodule LambdaEthereumConsensus.Validator do
399
398
400
399
defp start_payload_builder ( % { payload_builder: { slot , root , _ } } = state , slot , root ) , do: state
401
400
402
- defp start_payload_builder ( state , proposed_slot , head_root ) do
401
+ defp start_payload_builder ( % { validator: validator } = state , proposed_slot , head_root ) do
403
402
# TODO: handle reorgs and late blocks
404
- Logger . info ( "[Validator] Starting to build payload for slot #{ proposed_slot } " )
403
+ log_debug ( validator . index , "starting building payload" , slot: proposed_slot )
405
404
406
405
case BlockBuilder . start_building_payload ( proposed_slot , head_root ) do
407
406
{ :ok , payload_id } ->
407
+ log_debug ( validator . index , "payload built" , slot: proposed_slot )
408
+
408
409
% { state | payload_builder: { proposed_slot , head_root , payload_id } }
409
410
410
411
{ :error , reason } ->
411
- Logger . error (
412
- "[Validator] Failed to start building payload for slot #{ proposed_slot } . Reason: #{ reason } "
413
- )
412
+ log_error ( validator . index , "start building payload" , reason , slot: proposed_slot )
414
413
415
414
% { state | payload_builder: nil }
416
415
end
@@ -432,6 +431,8 @@ defmodule LambdaEthereumConsensus.Validator do
432
431
} = state ,
433
432
proposed_slot
434
433
) do
434
+ log_debug ( validator . index , "building block" , slot: proposed_slot )
435
+
435
436
build_result =
436
437
BlockBuilder . build_block (
437
438
% BuildBlockRequest {
@@ -446,20 +447,19 @@ defmodule LambdaEthereumConsensus.Validator do
446
447
447
448
case build_result do
448
449
{ :ok , { signed_block , blob_sidecars } } ->
449
- publish_block ( signed_block )
450
- Enum . each ( blob_sidecars , & publish_sidecar / 1 )
450
+ publish_block ( validator . index , signed_block )
451
+ Enum . each ( blob_sidecars , & publish_sidecar ( validator . index , & 1 ) )
451
452
452
453
{ :error , reason } ->
453
- Logger . error (
454
- "[Validator] Failed to build block for slot #{ proposed_slot } . Reason: #{ reason } "
455
- )
454
+ log_error ( validator . index , "build block" , reason , slot: proposed_slot )
456
455
end
457
456
458
457
% { state | payload_builder: nil }
459
458
end
460
459
460
+ # TODO: at least in kurtosis there are blocks that are proposed without a payload apparently, must investigate.
461
461
defp propose ( % { payload_builder: nil } = state , _proposed_slot ) do
462
- Logger . error ( "[Validator] Tried to propose a block without an execution payload")
462
+ log_error ( state . validator . index , " propose block" , "lack of execution payload")
463
463
state
464
464
end
465
465
@@ -472,47 +472,57 @@ defmodule LambdaEthereumConsensus.Validator do
472
472
end
473
473
474
474
# TODO: there's a lot of repeated code here. We should move this to a separate module
475
- defp publish_block ( signed_block ) do
475
+ defp publish_block ( validator_index , signed_block ) do
476
476
{ :ok , ssz_encoded } = Ssz . to_ssz ( signed_block )
477
477
{ :ok , encoded_msg } = :snappyer . compress ( ssz_encoded )
478
478
fork_context = ForkChoice . get_fork_digest ( ) |> Base . encode16 ( case: :lower )
479
479
480
480
proposed_slot = signed_block . message . slot
481
481
482
- # TODO: we might want to send the block to ForkChoice
483
- case Libp2pPort . publish ( "/eth2/#{ fork_context } /beacon_block/ssz_snappy" , encoded_msg ) do
484
- :ok ->
485
- Logger . info ( "[Validator] Proposed block for slot #{ proposed_slot } " )
482
+ log_debug ( validator_index , "publishing block" , slot: proposed_slot )
486
483
487
- { :error , reason } ->
488
- Logger . error (
489
- "[Validator] Failed to publish block for slot #{ proposed_slot } . Reason: #{ reason } "
490
- )
491
- end
484
+ # TODO: we might want to send the block to ForkChoice
485
+ Libp2pPort . publish ( "/eth2/#{ fork_context } /beacon_block/ssz_snappy" , encoded_msg )
486
+ |> log_info_result ( validator_index , "published block" , slot: proposed_slot )
492
487
end
493
488
494
- defp publish_sidecar ( % Types.BlobSidecar { index: index } = sidecar ) do
489
+ defp publish_sidecar ( validator_index , % Types.BlobSidecar { index: index } = sidecar ) do
495
490
{ :ok , ssz_encoded } = Ssz . to_ssz ( sidecar )
496
491
{ :ok , encoded_msg } = :snappyer . compress ( ssz_encoded )
497
492
fork_context = ForkChoice . get_fork_digest ( ) |> Base . encode16 ( case: :lower )
498
493
499
494
subnet_id = compute_subnet_for_blob_sidecar ( index )
500
495
501
- case Libp2pPort . publish (
502
- "/eth2/#{ fork_context } /blob_sidecar_#{ subnet_id } /ssz_snappy" ,
503
- encoded_msg
504
- ) do
505
- :ok ->
506
- :ok
496
+ log_debug ( validator_index , "publishing sidecar" , sidecar_index: index )
507
497
508
- { :error , reason } ->
509
- Logger . error (
510
- "[Validator] Failed to publish sidecar with index #{ index } . Reason: #{ reason } "
511
- )
512
- end
498
+ Libp2pPort . publish ( "/eth2/#{ fork_context } /blob_sidecar_#{ subnet_id } /ssz_snappy" , encoded_msg )
499
+ |> log_debug_result ( validator_index , "published sidecar" , sidecar_index: index )
513
500
end
514
501
515
502
defp compute_subnet_for_blob_sidecar ( blob_index ) do
516
503
rem ( blob_index , ChainSpec . get ( "BLOB_SIDECAR_SUBNET_COUNT" ) )
517
504
end
505
+
506
+ # Some Log Helpers to avoid repetition
507
+
508
+ defp log_info_result ( result , index , message , metadata ) ,
509
+ do: log_result ( result , :info , index , message , metadata )
510
+
511
+ defp log_debug_result ( result , index , message , metadata ) ,
512
+ do: log_result ( result , :debug , index , message , metadata )
513
+
514
+ defp log_result ( :ok , :info , index , message , metadata ) , do: log_info ( index , message , metadata )
515
+ defp log_result ( :ok , :debug , index , message , metadata ) , do: log_debug ( index , message , metadata )
516
+
517
+ defp log_result ( { :error , reason } , _level , index , message , metadata ) ,
518
+ do: log_error ( index , message , reason , metadata )
519
+
520
+ defp log_info ( index , message , metadata ) ,
521
+ do: Logger . info ( "[Validator] #{ index } #{ message } " , metadata )
522
+
523
+ defp log_debug ( index , message , metadata ) ,
524
+ do: Logger . debug ( "[Validator] #{ index } #{ message } " , metadata )
525
+
526
+ defp log_error ( index , message , reason , metadata \\ [ ] ) ,
527
+ do: Logger . error ( "[Validator] #{ index } Failed to #{ message } . Reason: #{ reason } " , metadata )
518
528
end
0 commit comments