-
Notifications
You must be signed in to change notification settings - Fork 291
Expand file tree
/
Copy pathget_header.go
More file actions
342 lines (300 loc) · 11.4 KB
/
Copy pathget_header.go
File metadata and controls
342 lines (300 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
package server
import (
"context"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"sync"
"time"
builderApiBellatrix "github.com/attestantio/go-builder-client/api/bellatrix"
builderApiCapella "github.com/attestantio/go-builder-client/api/capella"
builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb"
builderApiElectra "github.com/attestantio/go-builder-client/api/electra"
builderSpec "github.com/attestantio/go-builder-client/spec"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/flashbots/mev-boost/config"
"github.com/flashbots/mev-boost/server/types"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
// getHeader requests a bid from each relay and returns the most profitable one
func (m *BoostService) getHeader(log *logrus.Entry, slot phase0.Slot, pubkey, parentHashHex string, ua UserAgent, proposerAcceptContentTypes string) (bidResp, error) {
// Ensure arguments are valid
if len(pubkey) != 98 {
return bidResp{}, errInvalidPubkey
}
if len(parentHashHex) != 66 {
return bidResp{}, errInvalidHash
}
// Make sure we have a uid for this slot
m.slotUIDLock.Lock()
if m.slotUID.slot < slot {
m.slotUID.slot = slot
m.slotUID.uid = uuid.New()
}
slotUID := m.slotUID.uid
m.slotUIDLock.Unlock()
log = log.WithField("slotUID", slotUID)
// Compute these once, instead of for each relay
userAgent := wrapUserAgent(ua)
startTime := fmt.Sprintf("%d", time.Now().UTC().UnixMilli())
// Log how late into the slot the request starts
slotStartTimestamp := m.genesisTime + uint64(slot)*config.SlotTimeSec
msIntoSlot := uint64(time.Now().UTC().UnixMilli()) - slotStartTimestamp*1000
log.WithFields(logrus.Fields{
"genesisTime": m.genesisTime,
"slotTimeSec": config.SlotTimeSec,
"msIntoSlot": msIntoSlot,
}).Infof("getHeader request start - %d milliseconds into slot %d", msIntoSlot, slot)
var (
mu sync.Mutex
wg sync.WaitGroup
// The final response, containing the highest bid (if any)
result = bidResp{}
// Relays that sent the bid for a specific blockHash
relays = make(map[BlockHashHex][]types.RelayEntry)
)
// Request a bid from each relay
for _, relay := range m.relays {
wg.Add(1)
go func(relay types.RelayEntry) {
defer wg.Done()
// Build the request URL
url := relay.GetURI(fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", slot, parentHashHex, pubkey))
log := log.WithField("url", url)
// Make a new request
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil {
log.WithError(err).Warn("error creating new request")
return
}
// Add header fields to this request
req.Header.Set(HeaderAccept, proposerAcceptContentTypes)
req.Header.Set(HeaderKeySlotUID, slotUID.String())
req.Header.Set(HeaderStartTimeUnixMS, startTime)
req.Header.Set(HeaderUserAgent, userAgent)
// Send the request
log.Debug("requesting header")
resp, err := m.httpClientGetHeader.Do(req)
if err != nil {
log.WithError(err).Warn("error calling getHeader on relay")
return
}
defer resp.Body.Close()
// Check if no header is available
if resp.StatusCode == http.StatusNoContent {
log.Debug("no-content response")
return
}
// Check that the response was successful
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("%w: %d", errHTTPErrorResponse, resp.StatusCode)
log.WithError(err).Warn("error status code")
return
}
// Get the resp body content
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
log.WithError(err).Warn("error reading response body")
return
}
// Get the response's content type, default to JSON
respContentType, _, err := mime.ParseMediaType(resp.Header.Get(HeaderContentType))
if err != nil {
log.WithError(err).Warn("error parsing response content type")
respContentType = MediaTypeJSON
}
log = log.WithField("respContentType", respContentType)
// Get the optional version, used with SSZ decoding
respEthConsensusVersion := resp.Header.Get(HeaderEthConsensusVersion)
log = log.WithField("respEthConsensusVersion", respEthConsensusVersion)
// Decode bid
bid := new(builderSpec.VersionedSignedBuilderBid)
err = decodeBid(respBytes, respContentType, respEthConsensusVersion, bid)
if err != nil {
log.WithError(err).Warn("error decoding bid")
return
}
// Skip if bid is empty
if bid.IsEmpty() {
log.Debug("skipping empty bid")
return
}
// Getting the bid info will check if there are missing fields in the response
bidInfo, err := parseBidInfo(bid)
if err != nil {
log.WithError(err).Warn("error parsing bid info")
return
}
// Ignore bids with an empty block
if bidInfo.blockHash == nilHash {
log.Warn("relay responded with empty block hash")
return
}
// Add some info about the bid to the logger
valueEth := weiBigIntToEthBigFloat(bidInfo.value.ToBig())
log = log.WithFields(logrus.Fields{
"blockNumber": bidInfo.blockNumber,
"blockHash": bidInfo.blockHash.String(),
"txRoot": bidInfo.txRoot.String(),
"value": valueEth.Text('f', 18),
})
// Ensure the bid uses the correct public key
if relay.PublicKey.String() != bidInfo.pubkey.String() {
log.Errorf("bid pubkey mismatch. expected: %s - got: %s", relay.PublicKey.String(), bidInfo.pubkey.String())
return
}
// Verify the relay signature in the relay response
if !config.SkipRelaySignatureCheck {
ok, err := checkRelaySignature(bid, m.builderSigningDomain, relay.PublicKey)
if err != nil {
log.WithError(err).Error("error verifying relay signature")
return
}
if !ok {
log.Error("failed to verify relay signature")
return
}
}
// Verify response coherence with proposer's input data
if bidInfo.parentHash.String() != parentHashHex {
log.WithFields(logrus.Fields{
"originalParentHash": parentHashHex,
"responseParentHash": bidInfo.parentHash.String(),
}).Error("proposer and relay parent hashes are not the same")
return
}
// Ignore bids with 0 value
isZeroValue := bidInfo.value.IsZero()
isEmptyListTxRoot := bidInfo.txRoot.String() == "0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1"
if isZeroValue || isEmptyListTxRoot {
log.Warn("ignoring bid with 0 value")
return
}
log.Debug("bid received")
// Skip if value is lower than the minimum bid
if bidInfo.value.CmpBig(m.relayMinBid.BigInt()) == -1 {
log.Debug("ignoring bid below min-bid value")
return
}
mu.Lock()
defer mu.Unlock()
// Create a copy of the relay instance with its encoding preference. If we request SSZ and the relay
// responds with JSON, we know that it does not support SSZ yet. This preference will be used in getPayload,
// because we must encode the blinded block in the request in such a way that the relay can decode it.
relayWithEncodingPreference := relay.Copy()
relayWithEncodingPreference.SupportsSSZ = respContentType == MediaTypeOctetStream
// Remember which relays delivered which bids (multiple relays might deliver the top bid)
relays[BlockHashHex(bidInfo.blockHash.String())] = append(relays[BlockHashHex(bidInfo.blockHash.String())], relayWithEncodingPreference)
// Compare the bid with already known top bid (if any)
if !result.response.IsEmpty() {
valueDiff := bidInfo.value.Cmp(result.bidInfo.value)
if valueDiff == -1 {
// The current bid is less profitable than already known one
log.Debug("ignoring less profitable bid")
return
} else if valueDiff == 0 {
// The current bid is equally profitable as already known one
// Use hash as tiebreaker
previousBidBlockHash := result.bidInfo.blockHash
if bidInfo.blockHash.String() >= previousBidBlockHash.String() {
log.Debug("equally profitable bid lost tiebreaker")
return
}
}
}
// Use this relay's response as mev-boost response because it's most profitable
log.Debug("new best bid")
result.response = *bid
result.bidInfo = bidInfo
result.t = time.Now()
}(relay)
}
wg.Wait()
// Set the winning relays before returning
result.relays = relays[BlockHashHex(result.bidInfo.blockHash.String())]
return result, nil
}
// decodeBid decodes a bid by SSZ or JSON, depending on the provided respContentType
func decodeBid(respBytes []byte, respContentType, ethConsensusVersion string, bid *builderSpec.VersionedSignedBuilderBid) error {
switch respContentType {
case MediaTypeOctetStream:
if ethConsensusVersion != "" {
// Do SSZ decoding
switch ethConsensusVersion {
case EthConsensusVersionBellatrix:
bid.Version = spec.DataVersionBellatrix
bid.Bellatrix = new(builderApiBellatrix.SignedBuilderBid)
return bid.Bellatrix.UnmarshalSSZ(respBytes)
case EthConsensusVersionCapella:
bid.Version = spec.DataVersionCapella
bid.Capella = new(builderApiCapella.SignedBuilderBid)
return bid.Capella.UnmarshalSSZ(respBytes)
case EthConsensusVersionDeneb:
bid.Version = spec.DataVersionDeneb
bid.Deneb = new(builderApiDeneb.SignedBuilderBid)
return bid.Deneb.UnmarshalSSZ(respBytes)
case EthConsensusVersionElectra:
bid.Version = spec.DataVersionElectra
bid.Electra = new(builderApiElectra.SignedBuilderBid)
return bid.Electra.UnmarshalSSZ(respBytes)
default:
return errInvalidForkVersion
}
} else {
return types.ErrMissingEthConsensusVersion
}
case MediaTypeJSON:
// Do JSON decoding
return json.Unmarshal(respBytes, bid)
}
return types.ErrInvalidContentType
}
// respondGetHeaderJSON responds to the proposer in JSON
func (m *BoostService) respondGetHeaderJSON(w http.ResponseWriter, result *bidResp) {
w.Header().Set(HeaderContentType, MediaTypeJSON)
w.WriteHeader(http.StatusOK)
// Serialize and write the data
if err := json.NewEncoder(w).Encode(&result.response); err != nil {
m.log.WithField("response", result.response).WithError(err).Error("could not write OK response")
http.Error(w, "", http.StatusInternalServerError)
}
}
// respondGetHeaderSSZ responds to the proposer in SSZ
func (m *BoostService) respondGetHeaderSSZ(w http.ResponseWriter, result *bidResp) {
// Serialize the response
var err error
var sszData []byte
switch result.response.Version {
case spec.DataVersionBellatrix:
w.Header().Set(HeaderEthConsensusVersion, EthConsensusVersionBellatrix)
sszData, err = result.response.Bellatrix.MarshalSSZ()
case spec.DataVersionCapella:
w.Header().Set(HeaderEthConsensusVersion, EthConsensusVersionCapella)
sszData, err = result.response.Capella.MarshalSSZ()
case spec.DataVersionDeneb:
w.Header().Set(HeaderEthConsensusVersion, EthConsensusVersionDeneb)
sszData, err = result.response.Deneb.MarshalSSZ()
case spec.DataVersionElectra:
w.Header().Set(HeaderEthConsensusVersion, EthConsensusVersionElectra)
sszData, err = result.response.Electra.MarshalSSZ()
case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair:
err = errInvalidForkVersion
}
if err != nil {
m.log.WithError(err).Error("error serializing response as SSZ")
http.Error(w, "failed to serialize response", http.StatusInternalServerError)
return
}
// Write the header
w.Header().Set(HeaderContentType, MediaTypeOctetStream)
w.WriteHeader(http.StatusOK)
// Write SSZ data
if _, err := w.Write(sszData); err != nil {
m.log.WithError(err).Error("error writing SSZ response")
http.Error(w, "failed to write response", http.StatusInternalServerError)
}
}