1
1
package builder
2
2
3
3
import (
4
+ "context"
4
5
"encoding/json"
5
6
"errors"
6
7
"fmt"
@@ -10,11 +11,23 @@ import (
10
11
"sync"
11
12
"time"
12
13
14
+ "github.com/attestantio/go-eth2-client/spec/capella"
13
15
"github.com/ethereum/go-ethereum/common"
14
16
"github.com/ethereum/go-ethereum/common/hexutil"
17
+ "github.com/ethereum/go-ethereum/core/types"
15
18
"github.com/ethereum/go-ethereum/log"
19
+ "github.com/r3labs/sse"
20
+ "golang.org/x/exp/slices"
16
21
)
17
22
23
+ type IBeaconClient interface {
24
+ isValidator (pubkey PubkeyHex ) bool
25
+ getProposerForNextSlot (requestedSlot uint64 ) (PubkeyHex , error )
26
+ SubscribeToPayloadAttributesEvents (payloadAttrC chan types.BuilderPayloadAttributes )
27
+ Start () error
28
+ Stop ()
29
+ }
30
+
18
31
type testBeaconClient struct {
19
32
validator * ValidatorPrivateData
20
33
slot uint64
@@ -30,8 +43,110 @@ func (b *testBeaconClient) isValidator(pubkey PubkeyHex) bool {
30
43
func (b * testBeaconClient ) getProposerForNextSlot (requestedSlot uint64 ) (PubkeyHex , error ) {
31
44
return PubkeyHex (hexutil .Encode (b .validator .Pk )), nil
32
45
}
33
- func (b * testBeaconClient ) Start () error {
34
- return nil
46
+
47
+ func (b * testBeaconClient ) SubscribeToPayloadAttributesEvents (payloadAttrC chan types.BuilderPayloadAttributes ) {
48
+ }
49
+
50
+ func (b * testBeaconClient ) Start () error { return nil }
51
+
52
+ type NilBeaconClient struct {}
53
+
54
+ func (b * NilBeaconClient ) isValidator (pubkey PubkeyHex ) bool {
55
+ return false
56
+ }
57
+
58
+ func (b * NilBeaconClient ) getProposerForNextSlot (requestedSlot uint64 ) (PubkeyHex , error ) {
59
+ return PubkeyHex ("" ), nil
60
+ }
61
+
62
+ func (b * NilBeaconClient ) SubscribeToPayloadAttributesEvents (payloadAttrC chan types.BuilderPayloadAttributes ) {
63
+ }
64
+
65
+ func (b * NilBeaconClient ) Start () error { return nil }
66
+
67
+ func (b * NilBeaconClient ) Stop () {}
68
+
69
+ type MultiBeaconClient struct {
70
+ clients []* BeaconClient
71
+ closeCh chan struct {}
72
+ }
73
+
74
+ func NewMultiBeaconClient (endpoints []string , slotsInEpoch uint64 , secondsInSlot uint64 ) * MultiBeaconClient {
75
+ clients := []* BeaconClient {}
76
+ for _ , endpoint := range endpoints {
77
+ client := NewBeaconClient (endpoint , slotsInEpoch , secondsInSlot )
78
+ clients = append (clients , client )
79
+ }
80
+
81
+ return & MultiBeaconClient {
82
+ clients : clients ,
83
+ closeCh : make (chan struct {}),
84
+ }
85
+ }
86
+
87
+ func (m * MultiBeaconClient ) isValidator (pubkey PubkeyHex ) bool {
88
+ for _ , c := range m .clients {
89
+ // Pick the first one, always true
90
+ return c .isValidator (pubkey )
91
+ }
92
+
93
+ return false
94
+ }
95
+
96
+ func (m * MultiBeaconClient ) getProposerForNextSlot (requestedSlot uint64 ) (PubkeyHex , error ) {
97
+ var allErrs error
98
+ for _ , c := range m .clients {
99
+ pk , err := c .getProposerForNextSlot (requestedSlot )
100
+ if err != nil {
101
+ allErrs = errors .Join (allErrs , err )
102
+ continue
103
+ }
104
+
105
+ return pk , nil
106
+ }
107
+ return PubkeyHex ("" ), allErrs
108
+ }
109
+
110
+ func payloadAttributesMatch (l types.BuilderPayloadAttributes , r types.BuilderPayloadAttributes ) bool {
111
+ if l .Timestamp != r .Timestamp ||
112
+ l .Random != r .Random ||
113
+ l .SuggestedFeeRecipient != r .SuggestedFeeRecipient ||
114
+ l .Slot != r .Slot ||
115
+ l .HeadHash != r .HeadHash ||
116
+ l .GasLimit != r .GasLimit {
117
+ return false
118
+ }
119
+
120
+ if ! slices .Equal (l .Withdrawals , r .Withdrawals ) {
121
+ return false
122
+ }
123
+
124
+ return true
125
+ }
126
+
127
+ func (m * MultiBeaconClient ) SubscribeToPayloadAttributesEvents (payloadAttrC chan types.BuilderPayloadAttributes ) {
128
+ for _ , c := range m .clients {
129
+ go c .SubscribeToPayloadAttributesEvents (payloadAttrC )
130
+ }
131
+ }
132
+
133
+ func (m * MultiBeaconClient ) Start () error {
134
+ var allErrs error
135
+ for _ , c := range m .clients {
136
+ err := c .Start ()
137
+ if err != nil {
138
+ allErrs = errors .Join (allErrs , err )
139
+ }
140
+ }
141
+ return allErrs
142
+ }
143
+
144
+ func (m * MultiBeaconClient ) Stop () {
145
+ for _ , c := range m .clients {
146
+ c .Stop ()
147
+ }
148
+
149
+ close (m .closeCh )
35
150
}
36
151
37
152
type BeaconClient struct {
@@ -42,21 +157,24 @@ type BeaconClient struct {
42
157
mu sync.Mutex
43
158
slotProposerMap map [uint64 ]PubkeyHex
44
159
45
- closeCh chan struct {}
160
+ ctx context.Context
161
+ cancelFn context.CancelFunc
46
162
}
47
163
48
164
func NewBeaconClient (endpoint string , slotsInEpoch uint64 , secondsInSlot uint64 ) * BeaconClient {
165
+ ctx , cancelFn := context .WithCancel (context .Background ())
49
166
return & BeaconClient {
50
167
endpoint : endpoint ,
51
168
slotsInEpoch : slotsInEpoch ,
52
169
secondsInSlot : secondsInSlot ,
53
170
slotProposerMap : make (map [uint64 ]PubkeyHex ),
54
- closeCh : make (chan struct {}),
171
+ ctx : ctx ,
172
+ cancelFn : cancelFn ,
55
173
}
56
174
}
57
175
58
176
func (b * BeaconClient ) Stop () {
59
- close ( b . closeCh )
177
+ b . cancelFn ( )
60
178
}
61
179
62
180
func (b * BeaconClient ) isValidator (pubkey PubkeyHex ) bool {
@@ -109,7 +227,7 @@ func (b *BeaconClient) UpdateValidatorMapForever() {
109
227
defer timer .Stop ()
110
228
for true {
111
229
select {
112
- case <- b .closeCh :
230
+ case <- b .ctx . Done () :
113
231
return
114
232
case <- timer .C :
115
233
}
@@ -154,6 +272,70 @@ func (b *BeaconClient) UpdateValidatorMapForever() {
154
272
}
155
273
}
156
274
275
+ // PayloadAttributesEvent represents the data of a payload_attributes event
276
+ // {"version": "capella", "data": {"proposer_index": "123", "proposal_slot": "10", "parent_block_number": "9", "parent_block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "parent_block_hash": "0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf", "payload_attributes": {"timestamp": "123456", "prev_randao": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "suggested_fee_recipient": "0x0000000000000000000000000000000000000000", "withdrawals": [{"index": "5", "validator_index": "10", "address": "0x0000000000000000000000000000000000000000", "amount": "15640"}]}}}
277
+ type PayloadAttributesEvent struct {
278
+ Version string `json:"version"`
279
+ Data PayloadAttributesEventData `json:"data"`
280
+ }
281
+
282
+ type PayloadAttributesEventData struct {
283
+ ProposalSlot uint64 `json:"proposal_slot,string"`
284
+ ParentBlockHash common.Hash `json:"parent_block_hash"`
285
+ PayloadAttributes PayloadAttributes `json:"payload_attributes"`
286
+ }
287
+
288
+ type PayloadAttributes struct {
289
+ Timestamp uint64 `json:"timestamp,string"`
290
+ PrevRandao common.Hash `json:"prev_randao"`
291
+ SuggestedFeeRecipient common.Address `json:"suggested_fee_recipient"`
292
+ Withdrawals []* capella.Withdrawal `json:"withdrawals"`
293
+ }
294
+
295
+ // SubscribeToPayloadAttributesEvents subscribes to payload attributes events to validate fields such as prevrandao and withdrawals
296
+ func (b * BeaconClient ) SubscribeToPayloadAttributesEvents (payloadAttrC chan types.BuilderPayloadAttributes ) {
297
+ payloadAttributesResp := new (PayloadAttributesEvent )
298
+
299
+ eventsURL := fmt .Sprintf ("%s/eth/v1/events?topics=payload_attributes" , b .endpoint )
300
+ log .Info ("subscribing to payload_attributes events" )
301
+
302
+ for {
303
+ client := sse .NewClient (eventsURL )
304
+ err := client .SubscribeRawWithContext (b .ctx , func (msg * sse.Event ) {
305
+ err := json .Unmarshal (msg .Data , payloadAttributesResp )
306
+ if err != nil {
307
+ log .Error ("could not unmarshal payload_attributes event" , "err" , err )
308
+ } else {
309
+ // convert capella.Withdrawal to types.Withdrawal
310
+ var withdrawals []* types.Withdrawal
311
+ for _ , w := range payloadAttributesResp .Data .PayloadAttributes .Withdrawals {
312
+ withdrawals = append (withdrawals , & types.Withdrawal {
313
+ Index : uint64 (w .Index ),
314
+ Validator : uint64 (w .ValidatorIndex ),
315
+ Address : common .Address (w .Address ),
316
+ Amount : uint64 (w .Amount ),
317
+ })
318
+ }
319
+
320
+ data := types.BuilderPayloadAttributes {
321
+ Slot : payloadAttributesResp .Data .ProposalSlot ,
322
+ HeadHash : payloadAttributesResp .Data .ParentBlockHash ,
323
+ Timestamp : hexutil .Uint64 (payloadAttributesResp .Data .PayloadAttributes .Timestamp ),
324
+ Random : payloadAttributesResp .Data .PayloadAttributes .PrevRandao ,
325
+ SuggestedFeeRecipient : payloadAttributesResp .Data .PayloadAttributes .SuggestedFeeRecipient ,
326
+ Withdrawals : withdrawals ,
327
+ }
328
+ payloadAttrC <- data
329
+ }
330
+ })
331
+ if err != nil {
332
+ log .Error ("failed to subscribe to payload_attributes events" , "err" , err )
333
+ time .Sleep (1 * time .Second )
334
+ }
335
+ log .Warn ("beaconclient SubscribeRaw ended, reconnecting" )
336
+ }
337
+ }
338
+
157
339
func fetchCurrentSlot (endpoint string ) (uint64 , error ) {
158
340
headerRes := & struct {
159
341
Data []struct {
0 commit comments