Skip to content

Commit

Permalink
Add remote relay connection for getting validator data (ethereum#11)
Browse files Browse the repository at this point in the history
* Add remote relay connection for getting validator data
* Add block submission to remote relay
* Adjust readme
  • Loading branch information
Ruteri authored and avalonche committed Feb 6, 2023
1 parent bcca78c commit ba157a9
Show file tree
Hide file tree
Showing 17 changed files with 905 additions and 304 deletions.
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,17 @@ Ropsten test network is based on the Ethash proof-of-work consensus algorithm. A
it has certain extra overhead and is more susceptible to reorganization attacks due to the
network's low difficulty/security.

Builder API has two hooks into geth:
* Builder polls relay for the proposer registrations for the next epoch

Builder has two hooks into geth:
* On forkchoice update, changing the payload attributes feeRecipient to the one registered for next slot's validator
* On new sealed block, consuming the block as the next slot's proposed payload
* On new sealed block, consuming the block as the next slot's proposed payload and submits it to the relay

Local relay is enabled by default and overwrites remote relay data. This is only meant for the testnets!

## Limitations

* Blocks are only built on forkchoice update call from beacon node
* Only works post-Bellatrix, fork version is static
* Does not accept external blocks
* Does not have payload cache, only the latest block is available

Expand All @@ -153,13 +156,14 @@ Builder API options:
```
$ geth --help
BUILDER API OPTIONS:
--builder.validator_checks Enable the validator checks
--builder.secret_key value Builder API key used for signing headers (default: "0x2fc12ae741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2431e11") [$BUILDER_SECRET_KEY]
--builder.secret_key value Builder key used for signing blocks (default: "0x2fc12ae741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2431e11") [$BUILDER_SECRET_KEY]
--builder.relay_secret_key value Builder local relay API key used for signing headers (default: "0x2fc12ae741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2431e11") [$BUILDER_RELAY_SECRET_KEY]
--builder.listen_addr value Listening address for builder endpoint (default: ":28545") [$BUILDER_LISTEN_ADDR]
--builder.genesis_fork_version value Gensis fork version (default: "0x02000000") [$BUILDER_GENESIS_FORK_VERSION]
--builder.bellatrix_fork_version value Bellatrix fork version (default: "0x02000000") [$BUILDER_BELLATRIX_FORK_VERSION]
--builder.genesis_validators_root value Genesis validators root of the network (static). For kiln use 0x99b09fcd43e5905236c370f184056bec6e6638cfc31a323b304fc4aa789cb4ad (default: "0x0000000000000000000000000000000000000000000000000000000000000000") [$BUILDER_GENESIS_VALIDATORS_ROOT]
--builder.genesis_fork_version value Gensis fork version. For kiln use 0x70000069 (default: "0x00000000") [$BUILDER_GENESIS_FORK_VERSION]
--builder.bellatrix_fork_version value Bellatrix fork version. For kiln use 0x70000071 (default: "0x02000000") [$BUILDER_BELLATRIX_FORK_VERSION]
--builder.genesis_validators_root value Genesis validators root of the network. For kiln use 0x99b09fcd43e5905236c370f184056bec6e6638cfc31a323b304fc4aa789cb4ad (default: "0x0000000000000000000000000000000000000000000000000000000000000000") [$BUILDER_GENESIS_VALIDATORS_ROOT]
--builder.beacon_endpoint value Beacon endpoint to connect to for beacon chain data (default: "http://127.0.0.1:5052") [$BUILDER_BEACON_ENDPOINT]
--builder.remote_relay_endpoint value Relay endpoint to connect to for validator registration data, if not provided will expose validator registration locally [$BUILDER_REMOTE_RELAY_ENDPOINT]
```

This will start `geth` in snap-sync mode with a DB memory allowance of 1GB, as the
Expand Down
15 changes: 8 additions & 7 deletions builder/beacon_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

type testBeaconClient struct {
validator *ValidatorPrivateData
slot uint64
}

func (b *testBeaconClient) isValidator(pubkey PubkeyHex) bool {
Expand All @@ -24,8 +25,8 @@ func (b *testBeaconClient) isValidator(pubkey PubkeyHex) bool {
func (b *testBeaconClient) getProposerForNextSlot(requestedSlot uint64) (PubkeyHex, error) {
return PubkeyHex(hexutil.Encode(b.validator.Pk)), nil
}
func (b *testBeaconClient) onForkchoiceUpdate() (PubkeyHex, error) {
return PubkeyHex(hexutil.Encode(b.validator.Pk)), nil
func (b *testBeaconClient) onForkchoiceUpdate() (uint64, error) {
return b.slot, nil
}

type BeaconClient struct {
Expand Down Expand Up @@ -62,13 +63,13 @@ func (b *BeaconClient) getProposerForNextSlot(requestedSlot uint64) (PubkeyHex,

/* Returns next slot's proposer pubkey */
// TODO: what happens if no block for previous slot - should still get next slot
func (b *BeaconClient) onForkchoiceUpdate() (PubkeyHex, error) {
func (b *BeaconClient) onForkchoiceUpdate() (uint64, error) {
b.mu.Lock()
defer b.mu.Unlock()

currentSlot, err := fetchCurrentSlot(b.endpoint)
if err != nil {
return PubkeyHex(""), err
return 0, err
}

nextSlot := currentSlot + 1
Expand All @@ -80,7 +81,7 @@ func (b *BeaconClient) onForkchoiceUpdate() (PubkeyHex, error) {
// TODO: this should be prepared in advance, possibly just fetch for next epoch in advance
slotProposerMap, err := fetchEpochProposersMap(b.endpoint, nextSlotEpoch)
if err != nil {
return PubkeyHex(""), err
return 0, err
}

b.currentEpoch = nextSlotEpoch
Expand All @@ -90,10 +91,10 @@ func (b *BeaconClient) onForkchoiceUpdate() (PubkeyHex, error) {
nextSlotProposer, found := b.slotProposerMap[nextSlot]
if !found {
log.Error("inconsistent proposer mapping", "currentSlot", currentSlot, "slotProposerMap", b.slotProposerMap)
return PubkeyHex(""), errors.New("inconsistent proposer mapping")
return 0, errors.New("inconsistent proposer mapping")
}
b.nextSlotProposer = nextSlotProposer
return nextSlotProposer, nil
return nextSlot, nil
}

func fetchCurrentSlot(endpoint string) (uint64, error) {
Expand Down
20 changes: 10 additions & 10 deletions builder/beacon_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,11 @@ func TestOnForkchoiceUpdate(t *testing.T) {
}`)

bc := NewBeaconClient(mbn.srv.URL)
pubkeyHex, err := bc.onForkchoiceUpdate()
slot, err := bc.onForkchoiceUpdate()
require.NoError(t, err)
require.Equal(t, PubkeyHex("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74b"), pubkeyHex)
require.Equal(t, slot, uint64(32))

pubkeyHex, err = bc.getProposerForNextSlot(32)
pubkeyHex, err := bc.getProposerForNextSlot(32)
require.NoError(t, err)
require.Equal(t, PubkeyHex("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74b"), pubkeyHex)

Expand All @@ -221,9 +221,9 @@ func TestOnForkchoiceUpdate(t *testing.T) {
mbn.headersCode = 404
mbn.headersResp = []byte(`{ "code": 404, "message": "State not found" }`)

pubkeyHex, err = NewBeaconClient(mbn.srv.URL).onForkchoiceUpdate()
slot, err = NewBeaconClient(mbn.srv.URL).onForkchoiceUpdate()
require.EqualError(t, err, "State not found")
require.Equal(t, PubkeyHex(""), pubkeyHex)
require.Equal(t, slot, uint64(0))

// Check that client does not fetch new proposers if epoch did not change
mbn.headersCode = 200
Expand All @@ -238,9 +238,9 @@ func TestOnForkchoiceUpdate(t *testing.T) {
]
}`)

pubkeyHex, err = bc.onForkchoiceUpdate()
slot, err = bc.onForkchoiceUpdate()
require.NoError(t, err, "")
require.Equal(t, PubkeyHex("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74b"), pubkeyHex)
require.Equal(t, slot, uint64(32))

mbn.headersResp = []byte(`{ "data": [ { "header": { "message": { "slot": "63", "proposer_index": "1" } } } ] }`)
mbn.proposerDuties[2] = []byte(`{
Expand All @@ -253,16 +253,16 @@ func TestOnForkchoiceUpdate(t *testing.T) {
]
}`)

pubkeyHex, err = bc.onForkchoiceUpdate()
slot, err = bc.onForkchoiceUpdate()
require.NoError(t, err, "")
require.Equal(t, PubkeyHex("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74d"), pubkeyHex)
require.Equal(t, slot, uint64(64))

pubkeyHex, err = bc.getProposerForNextSlot(64)
require.NoError(t, err)
require.Equal(t, PubkeyHex("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74d"), pubkeyHex)

// Check proposers map error is routed out
mbn.headersResp = []byte(`{ "data": [ { "header": { "message": { "slot": "65", "proposer_index": "1" } } } ] }`)
pubkeyHex, err = bc.onForkchoiceUpdate()
slot, err = bc.onForkchoiceUpdate()
require.EqualError(t, err, "inconsistent proposer mapping")
}
178 changes: 178 additions & 0 deletions builder/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package builder

import (
"encoding/json"
_ "os"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/beacon"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"

"github.com/flashbots/go-boost-utils/bls"
boostTypes "github.com/flashbots/go-boost-utils/types"
)

type PubkeyHex string

type ValidatorData struct {
Pubkey PubkeyHex
FeeRecipient boostTypes.Address `json:"feeRecipient"`
GasLimit uint64 `json:"gasLimit"`
Timestamp uint64 `json:"timestamp"`
}

type IBeaconClient interface {
isValidator(pubkey PubkeyHex) bool
getProposerForNextSlot(requestedSlot uint64) (PubkeyHex, error)
onForkchoiceUpdate() (uint64, error)
}

type IRelay interface {
SubmitBlock(msg *boostTypes.BuilderSubmitBlockRequest) error
GetValidatorForSlot(nextSlot uint64) (ValidatorData, error)
}

type Builder struct {
beaconClient IBeaconClient
relay IRelay

builderSecretKey *bls.SecretKey
builderPublicKey boostTypes.PublicKey
builderSigningDomain boostTypes.Domain
}

func NewBuilder(sk *bls.SecretKey, bc IBeaconClient, relay IRelay, builderSigningDomain boostTypes.Domain) *Builder {
pkBytes := bls.PublicKeyFromSecretKey(sk).Compress()
pk := boostTypes.PublicKey{}
pk.FromSlice(pkBytes)

_, err := bc.onForkchoiceUpdate()
if err != nil {
log.Error("could not initialize beacon client", "err", err)
}

return &Builder{
beaconClient: bc,
relay: relay,
builderSecretKey: sk,
builderPublicKey: pk,

builderSigningDomain: builderSigningDomain,
}
}

func (b *Builder) onForkchoice(payloadAttributes *beacon.PayloadAttributesV1) {
dataJson, err := json.Marshal(payloadAttributes)
if err == nil {
log.Info("FCU", "data", string(dataJson))
} else {
log.Info("FCU", "data", payloadAttributes, "parsingError", err)

}

nextSlot, err := b.beaconClient.onForkchoiceUpdate()
if err != nil {
log.Error("FCU hook failed", "err", err)
return
}

if payloadAttributes != nil {
payloadAttributes.Slot = nextSlot
if vd, err := b.relay.GetValidatorForSlot(nextSlot); err == nil {
payloadAttributes.SuggestedFeeRecipient = [20]byte(vd.FeeRecipient)
payloadAttributes.GasLimit = vd.GasLimit
}
}
}

func (b *Builder) newSealedBlock(data *beacon.ExecutableDataV1, block *types.Block, payloadAttributes *beacon.PayloadAttributesV1) {
dataJson, err := json.Marshal(data)
if err == nil {
log.Info("newSealedBlock", "data", string(dataJson))
} else {
log.Info("newSealedBlock", "data", data, "parsingError", err)
}
payload, err := executableDataToExecutionPayload(data)
if err != nil {
log.Error("could not format execution payload", "err", err)
return
}

vd, err := b.relay.GetValidatorForSlot(payloadAttributes.Slot)
if err != nil {
log.Error("could not get validator while submitting block", "err", err, "slot", payloadAttributes.Slot)
return
}

pubkey, err := boostTypes.HexToPubkey(string(vd.Pubkey))
if err != nil {
log.Error("could not parse pubkey", "err", err, "pubkey", vd.Pubkey)
return
}

value := new(boostTypes.U256Str)
err = value.FromBig(block.Profit)
if err != nil {
log.Error("could not set block value", "err", err)
return
}

blockBidMsg := boostTypes.BidTraceMessage{
Slot: payloadAttributes.Slot,
ParentHash: payload.ParentHash,
BlockHash: payload.BlockHash,
BuilderPubkey: b.builderPublicKey,
ProposerPubkey: pubkey,
ProposerFeeRecipient: boostTypes.Address(payloadAttributes.SuggestedFeeRecipient),
Value: *value,
}

signature, err := boostTypes.SignMessage(&blockBidMsg, b.builderSigningDomain, b.builderSecretKey)
if err != nil {
log.Error("could not sign builder bid", "err", err)
return
}

blockSubmitReq := boostTypes.BuilderSubmitBlockRequest{
Signature: signature,
Message: &blockBidMsg,
ExecutionPayload: payload,
}

err = b.relay.SubmitBlock(&blockSubmitReq)
if err != nil {
log.Error("could not submit block", "err", err)
return
}
}

func executableDataToExecutionPayload(data *beacon.ExecutableDataV1) (*boostTypes.ExecutionPayload, error) {
transactionData := make([]hexutil.Bytes, len(data.Transactions))
for i, tx := range data.Transactions {
transactionData[i] = hexutil.Bytes(tx)
}

baseFeePerGas := new(boostTypes.U256Str)
err := baseFeePerGas.FromBig(data.BaseFeePerGas)
if err != nil {
return nil, err
}

return &boostTypes.ExecutionPayload{
ParentHash: [32]byte(data.ParentHash),
FeeRecipient: [20]byte(data.FeeRecipient),
StateRoot: [32]byte(data.StateRoot),
ReceiptsRoot: [32]byte(data.ReceiptsRoot),
LogsBloom: boostTypes.Bloom(types.BytesToBloom(data.LogsBloom)),
Random: [32]byte(data.Random),
BlockNumber: data.Number,
GasLimit: data.GasLimit,
GasUsed: data.GasUsed,
Timestamp: data.Timestamp,
ExtraData: data.ExtraData,
BaseFeePerGas: *baseFeePerGas,
BlockHash: [32]byte(data.BlockHash),
Transactions: transactionData,
}, nil
}
Loading

0 comments on commit ba157a9

Please sign in to comment.