Skip to content

Commit

Permalink
Support external mev builder (#10492)
Browse files Browse the repository at this point in the history
- 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
domiwei authored Jun 25, 2024
1 parent 60943b2 commit 382f1a9
Show file tree
Hide file tree
Showing 43 changed files with 1,829 additions and 132 deletions.
194 changes: 194 additions & 0 deletions cl/beacon/builder/client.go
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
}
Loading

0 comments on commit 382f1a9

Please sign in to comment.