-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support external mev builder (#10492)
- enable builder mode by configuring `--beacon.api=builder` and setup `--mev-relay-url` to run in builder mode ([code](https://github.com/ledgerwatch/erigon/pull/10492/files#diff-aca4b9462813c7b1d2ff5d7929408096522e963a92a092fcb1ffc3240c9e43b9R111)) - Set mev relay url by `--caplin.mev-relay-url` as run erigon if would like to run caplin in builder mode. eg: ``` --caplin.mev-relay-url=https://boost-relay-sepolia.flashbots.net/ ``` - Follow builder specs [here](https://ethereum.github.io/builder-specs/#/) to impl [builder client](https://github.com/ledgerwatch/erigon/pull/10492/files#diff-d2932cc9922a090127fd132ae180802b3686ab83e26f1946c985f4c0da6ffc23R12) - validator registration api `/v1/validator/register_validator` - choose proposed block (either blinded or unblinded block) api `/v3/validator/blocks/{slot}` - post blinded block api `/v1/beacon/blinded_blocks` `/v2/beacon/blinded_blocks`
- Loading branch information
Showing
43 changed files
with
1,829 additions
and
132 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
package builder | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
|
||
"github.com/ledgerwatch/erigon-lib/common" | ||
"github.com/ledgerwatch/erigon-lib/log/v3" | ||
"github.com/ledgerwatch/erigon/cl/clparams" | ||
"github.com/ledgerwatch/erigon/cl/cltypes" | ||
"github.com/ledgerwatch/erigon/turbo/engineapi/engine_types" | ||
) | ||
|
||
var _ BuilderClient = &builderClient{} | ||
|
||
var ( | ||
ErrNoContent = fmt.Errorf("no http content") | ||
) | ||
|
||
type builderClient struct { | ||
// ref: https://ethereum.github.io/builder-specs/#/ | ||
httpClient *http.Client | ||
url *url.URL | ||
beaconConfig *clparams.BeaconChainConfig | ||
} | ||
|
||
func NewBlockBuilderClient(baseUrl string, beaconConfig *clparams.BeaconChainConfig) *builderClient { | ||
u, err := url.Parse(baseUrl) | ||
if err != nil { | ||
panic(err) | ||
} | ||
c := &builderClient{ | ||
httpClient: &http.Client{}, | ||
url: u, | ||
beaconConfig: beaconConfig, | ||
} | ||
if err := c.GetStatus(context.Background()); err != nil { | ||
log.Error("cannot connect to builder client", "url", baseUrl, "error", err) | ||
panic("cannot connect to builder client") | ||
} | ||
log.Info("Builder client is ready", "url", baseUrl) | ||
return c | ||
} | ||
|
||
func (b *builderClient) RegisterValidator(ctx context.Context, registers []*cltypes.ValidatorRegistration) error { | ||
// https://ethereum.github.io/builder-specs/#/Builder/registerValidator | ||
path := "/eth/v1/builder/validators" | ||
url := b.url.JoinPath(path).String() | ||
if len(registers) == 0 { | ||
return errors.New("empty registers") | ||
} | ||
payload, err := json.Marshal(registers) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = httpCall[json.RawMessage](ctx, b.httpClient, http.MethodPost, url, nil, bytes.NewBuffer(payload)) | ||
if err == ErrNoContent { | ||
// no content is ok | ||
return nil | ||
} | ||
if err != nil { | ||
log.Warn("[mev builder] httpCall error on RegisterValidator", "err", err) | ||
} | ||
return err | ||
} | ||
|
||
func (b *builderClient) GetHeader(ctx context.Context, slot int64, parentHash common.Hash, pubKey common.Bytes48) (*ExecutionHeader, error) { | ||
// https://ethereum.github.io/builder-specs/#/Builder/getHeader | ||
path := fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", slot, parentHash.Hex(), pubKey.Hex()) | ||
url := b.url.JoinPath(path).String() | ||
header, err := httpCall[ExecutionHeader](ctx, b.httpClient, http.MethodGet, url, nil, nil) | ||
if err != nil { | ||
log.Warn("[mev builder] httpCall error on GetExecutionPayloadHeader", "err", err, "slot", slot, "parentHash", parentHash.Hex(), "pubKey", pubKey.Hex()) | ||
return nil, err | ||
} | ||
return header, nil | ||
} | ||
|
||
func (b *builderClient) SubmitBlindedBlocks(ctx context.Context, block *cltypes.SignedBlindedBeaconBlock) (*cltypes.Eth1Block, *engine_types.BlobsBundleV1, error) { | ||
// https://ethereum.github.io/builder-specs/#/Builder/submitBlindedBlocks | ||
path := "/eth/v1/builder/blinded_blocks" | ||
url := b.url.JoinPath(path).String() | ||
payload, err := json.Marshal(block) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
headers := map[string]string{ | ||
"Eth-Consensus-Version": block.Version().String(), | ||
} | ||
resp, err := httpCall[BlindedBlockResponse](ctx, b.httpClient, http.MethodPost, url, headers, bytes.NewBuffer(payload)) | ||
if err != nil { | ||
log.Warn("[mev builder] httpCall error on SubmitBlindedBlocks", "err", err, "slot", block.Block.Slot) | ||
return nil, nil, err | ||
} | ||
|
||
var eth1Block *cltypes.Eth1Block | ||
var blobsBundle *engine_types.BlobsBundleV1 | ||
switch resp.Version { | ||
case "bellatrix", "capella": | ||
eth1Block = &cltypes.Eth1Block{} | ||
if err := json.Unmarshal(resp.Data, block); err != nil { | ||
return nil, nil, err | ||
} | ||
case "deneb": | ||
denebResp := &struct { | ||
ExecutionPayload *cltypes.Eth1Block `json:"execution_payload"` | ||
BlobsBundle *engine_types.BlobsBundleV1 `json:"blobs_bundle"` | ||
}{ | ||
ExecutionPayload: cltypes.NewEth1Block(clparams.DenebVersion, b.beaconConfig), | ||
BlobsBundle: &engine_types.BlobsBundleV1{}, | ||
} | ||
if err := json.Unmarshal(resp.Data, denebResp); err != nil { | ||
return nil, nil, err | ||
} | ||
eth1Block = denebResp.ExecutionPayload | ||
blobsBundle = denebResp.BlobsBundle | ||
} | ||
return eth1Block, blobsBundle, nil | ||
} | ||
|
||
func (b *builderClient) GetStatus(ctx context.Context) error { | ||
path := "/eth/v1/builder/status" | ||
url := b.url.JoinPath(path).String() | ||
_, err := httpCall[json.RawMessage](ctx, b.httpClient, http.MethodGet, url, nil, nil) | ||
if err == ErrNoContent { | ||
// no content is ok, we just need to check if the server is up | ||
return nil | ||
} | ||
return err | ||
} | ||
|
||
func httpCall[T any](ctx context.Context, client *http.Client, method, url string, headers map[string]string, payloadReader io.Reader) (*T, error) { | ||
request, err := http.NewRequestWithContext(ctx, method, url, payloadReader) | ||
if err != nil { | ||
log.Warn("[mev builder] http.NewRequest failed", "err", err, "url", url, "method", method) | ||
return nil, err | ||
} | ||
request.Header.Set("Content-Type", "application/json") | ||
for k, v := range headers { | ||
request.Header.Set(k, v) | ||
} | ||
// send request | ||
response, err := client.Do(request) | ||
if err != nil { | ||
log.Warn("[mev builder] client.Do failed", "err", err, "url", url, "method", method) | ||
return nil, err | ||
} | ||
defer func() { | ||
if response.Body != nil { | ||
response.Body.Close() | ||
} | ||
}() | ||
if response.StatusCode < 200 || response.StatusCode > 299 { | ||
// read response body | ||
if response.Body == nil { | ||
return nil, fmt.Errorf("status code: %d", response.StatusCode) | ||
} | ||
bytes, err := io.ReadAll(response.Body) | ||
if err != nil { | ||
log.Warn("[mev builder] io.ReadAll failed", "err", err, "url", url, "method", method) | ||
} else { | ||
log.Debug("[mev builder] httpCall failed", "status", response.Status, "content", string(bytes)) | ||
} | ||
return nil, fmt.Errorf("status code: %d", response.StatusCode) | ||
} | ||
if response.StatusCode == http.StatusNoContent { | ||
return nil, ErrNoContent | ||
} | ||
|
||
// read response body | ||
var body T | ||
if response.Body == nil { | ||
return &body, nil | ||
} | ||
bytes, err := io.ReadAll(response.Body) | ||
if err != nil { | ||
log.Warn("[mev builder] io.ReadAll failed", "err", err, "url", url, "method", method) | ||
return nil, err | ||
} | ||
if len(bytes) == 0 { | ||
return &body, nil | ||
} | ||
if err := json.Unmarshal(bytes, &body); err != nil { | ||
log.Warn("[mev builder] json.Unmarshal error", "err", err, "content", string(bytes)) | ||
return nil, err | ||
} | ||
return &body, nil | ||
} |
Oops, something went wrong.