diff --git a/beaconclient/mock_multi_beacon_client.go b/beaconclient/mock_multi_beacon_client.go new file mode 100644 index 00000000..95230242 --- /dev/null +++ b/beaconclient/mock_multi_beacon_client.go @@ -0,0 +1,53 @@ +package beaconclient + +import ( + "sync" + + "github.com/flashbots/go-boost-utils/types" +) + +type MockMultiBeaconClient struct { + mu sync.RWMutex +} + +func NewMockMultiBeaconClient() *MockMultiBeaconClient { + return &MockMultiBeaconClient{ + mu: sync.RWMutex{}, + } +} + +func (*MockMultiBeaconClient) SubscribeToHeadEvents(slotC chan HeadEventData) {} + +func (*MockMultiBeaconClient) BestSyncStatus() (*SyncStatusPayloadData, error) { + return &SyncStatusPayloadData{HeadSlot: 1}, nil +} + +func (*MockMultiBeaconClient) FetchValidators(headSlot uint64) (map[types.PubkeyHex]ValidatorResponseEntry, error) { + return nil, nil +} + +func (*MockMultiBeaconClient) GetProposerDuties(epoch uint64) (*ProposerDutiesResponse, error) { + return nil, nil +} + +func (*MockMultiBeaconClient) PublishBlock(block *types.SignedBeaconBlock) (code int, err error) { + return 0, nil +} + +func (*MockMultiBeaconClient) GetGenesis() (*GetGenesisResponse, error) { + resp := &GetGenesisResponse{} + resp.Data.GenesisTime = 0 + return resp, nil +} + +func (*MockMultiBeaconClient) GetSpec() (spec *GetSpecResponse, err error) { + return nil, nil +} + +func (*MockMultiBeaconClient) GetBlock(blockID string) (block *GetBlockResponse, err error) { + return nil, nil +} + +func (*MockMultiBeaconClient) GetRandao(slot uint64) (spec *GetRandaoResponse, err error) { + return nil, nil +} diff --git a/common/common.go b/common/common.go index 22f92c86..56ac139d 100644 --- a/common/common.go +++ b/common/common.go @@ -21,3 +21,25 @@ type HTTPServerTimeouts struct { Write time.Duration // Timeout for writes. None if 0. Idle time.Duration // Timeout to disconnect idle client connections. None if 0. } + +type BuilderStatus uint8 + +const ( + LowPrio BuilderStatus = iota + HighPrio + Optimistic + Blacklisted +) + +func (b BuilderStatus) String() string { + switch b { + case HighPrio: + return "high-prio" + case Optimistic: + return "optimistic" + case Blacklisted: + return "blacklisted" + default: + return "low-prio" + } +} diff --git a/database/database.go b/database/database.go index 1566dc8b..9549d902 100644 --- a/database/database.go +++ b/database/database.go @@ -41,9 +41,12 @@ type IDatabaseService interface { GetBlockBuilders() ([]*BlockBuilderEntry, error) GetBlockBuilderByPubkey(pubkey string) (*BlockBuilderEntry, error) - SetBlockBuilderStatus(pubkey string, isHighPrio, isBlacklisted bool) error + SetBlockBuilderStatus(pubkey string, builderStatus common.BuilderStatus) error UpsertBlockBuilderEntryAfterSubmission(lastSubmission *BuilderBlockSubmissionEntry, isError bool) error IncBlockBuilderStatsAfterGetPayload(builderPubkey string) error + GetBlockBuildersFromCollateralID(collateralID uint64) ([]*BlockBuilderEntry, error) + + UpsertBuilderDemotion(submitBlockRequest *types.BuilderSubmitBlockRequest, signedBlindedBeaconBlock *types.SignedBlindedBeaconBlock, signedValidatorRegistration *types.SignedValidatorRegistration) error } type DatabaseService struct { @@ -436,8 +439,8 @@ func (s *DatabaseService) UpsertBlockBuilderEntryAfterSubmission(lastSubmission // Upsert query := `INSERT INTO ` + vars.TableBlockBuilder + ` - (builder_pubkey, description, is_high_prio, is_blacklisted, last_submission_id, last_submission_slot, num_submissions_total, num_submissions_simerror) VALUES - (:builder_pubkey, :description, :is_high_prio, :is_blacklisted, :last_submission_id, :last_submission_slot, :num_submissions_total, :num_submissions_simerror) + (builder_pubkey, description, status, collateral_value, collateral_id, last_submission_id, last_submission_slot, num_submissions_total, num_submissions_simerror) VALUES + (:builder_pubkey, :description, :status, :collateral_value, :collateral_id, :last_submission_id, :last_submission_slot, :num_submissions_total, :num_submissions_simerror) ON CONFLICT (builder_pubkey) DO UPDATE SET last_submission_id = :last_submission_id, last_submission_slot = :last_submission_slot, @@ -448,22 +451,22 @@ func (s *DatabaseService) UpsertBlockBuilderEntryAfterSubmission(lastSubmission } func (s *DatabaseService) GetBlockBuilders() ([]*BlockBuilderEntry, error) { - query := `SELECT id, inserted_at, builder_pubkey, description, is_high_prio, is_blacklisted, last_submission_id, last_submission_slot, num_submissions_total, num_submissions_simerror, num_sent_getpayload FROM ` + vars.TableBlockBuilder + ` ORDER BY id ASC;` + query := `SELECT id, inserted_at, builder_pubkey, description, status, collateral_value, collateral_id, last_submission_id, last_submission_slot, num_submissions_total, num_submissions_simerror, num_sent_getpayload FROM ` + vars.TableBlockBuilder + ` ORDER BY id ASC;` entries := []*BlockBuilderEntry{} err := s.DB.Select(&entries, query) return entries, err } func (s *DatabaseService) GetBlockBuilderByPubkey(pubkey string) (*BlockBuilderEntry, error) { - query := `SELECT id, inserted_at, builder_pubkey, description, is_high_prio, is_blacklisted, last_submission_id, last_submission_slot, num_submissions_total, num_submissions_simerror, num_sent_getpayload FROM ` + vars.TableBlockBuilder + ` WHERE builder_pubkey=$1;` + query := `SELECT id, inserted_at, builder_pubkey, description, status, collateral_value, collateral_id, last_submission_id, last_submission_slot, num_submissions_total, num_submissions_simerror, num_sent_getpayload FROM ` + vars.TableBlockBuilder + ` WHERE builder_pubkey=$1;` entry := &BlockBuilderEntry{} err := s.DB.Get(entry, query, pubkey) return entry, err } -func (s *DatabaseService) SetBlockBuilderStatus(pubkey string, isHighPrio, isBlacklisted bool) error { - query := `UPDATE ` + vars.TableBlockBuilder + ` SET is_high_prio=$1, is_blacklisted=$2 WHERE builder_pubkey=$3;` - _, err := s.DB.Exec(query, isHighPrio, isBlacklisted, pubkey) +func (s *DatabaseService) SetBlockBuilderStatus(pubkey string, builderStatusCode common.BuilderStatus) error { + query := `UPDATE ` + vars.TableBlockBuilder + ` SET status=$1 WHERE builder_pubkey=$2;` + _, err := s.DB.Exec(query, uint8(builderStatusCode), pubkey) return err } @@ -486,3 +489,75 @@ func (s *DatabaseService) DeleteExecutionPayloads(idFirst, idLast uint64) error _, err := s.DB.Exec(query, idFirst, idLast) return err } + +func (s *DatabaseService) UpsertBuilderDemotion(submitBlockRequest *types.BuilderSubmitBlockRequest, signedBlindedBeaconBlock *types.SignedBlindedBeaconBlock, signedValidatorRegistration *types.SignedValidatorRegistration) error { + if submitBlockRequest == nil { + return fmt.Errorf("nil submitBlockRequest invalid for UpsertBuilderDemotion") + } + + _submitBlockRequest, err := json.Marshal(submitBlockRequest) + if err != nil { + return err + } + bidTrace := submitBlockRequest.Message + builderDemotionEntry := BuilderDemotionEntry{ + SubmitBlockRequest: NewNullString(string(_submitBlockRequest)), + + Slot: bidTrace.Slot, + Epoch: bidTrace.Slot / uint64(common.SlotsPerEpoch), + + BuilderPubkey: bidTrace.BuilderPubkey.String(), + ProposerPubkey: bidTrace.ProposerPubkey.String(), + + Value: bidTrace.Value.String(), + + BlockHash: bidTrace.BlockHash.String(), + } + + if signedBlindedBeaconBlock != nil { + _signedBlindedBeaconBlock, err := json.Marshal(signedBlindedBeaconBlock) + if err != nil { + return err + } + builderDemotionEntry.SignedBlindedBeaconBlock = NewNullString(string(_signedBlindedBeaconBlock)) + } + + if signedValidatorRegistration != nil { + _signedValidatorRegistration, err := json.Marshal(signedValidatorRegistration) + if err != nil { + return err + } + builderDemotionEntry.SignedValidatorRegistration = NewNullString(string(_signedValidatorRegistration)) + builderDemotionEntry.FeeRecipient = signedValidatorRegistration.Message.FeeRecipient.String() + builderDemotionEntry.GasLimit = signedValidatorRegistration.Message.GasLimit + } + + var query string + // If block_hash conflicts and we have a published block, fill in fields needed for the refund. + if signedBlindedBeaconBlock != nil && signedValidatorRegistration != nil { + query = `INSERT INTO ` + vars.TableBuilderDemotions + ` + (submit_block_request, signed_blinded_beacon_block, signed_validator_registration, slot, epoch, builder_pubkey, proposer_pubkey, value, fee_recipient, gas_limit, block_hash) VALUES + (:submit_block_request, :signed_blinded_beacon_block, :signed_validator_registration, :slot, :epoch, :builder_pubkey, :proposer_pubkey, :value, :fee_recipient, :gas_limit, :block_hash) + ON CONFLICT (block_hash) DO UPDATE SET + signed_blinded_beacon_block = :signed_blinded_beacon_block, + signed_validator_registration = :signed_validator_registration, + fee_recipient = :fee_recipient, + gas_limit = :gas_limit; + ` + } else { + // If the block_hash conflicts, then all the relevant data must be there already. + query = `INSERT INTO ` + vars.TableBuilderDemotions + ` + (submit_block_request, signed_blinded_beacon_block, signed_validator_registration, slot, epoch, builder_pubkey, proposer_pubkey, value, fee_recipient, gas_limit, block_hash) VALUES + (:submit_block_request, :signed_blinded_beacon_block, :signed_validator_registration, :slot, :epoch, :builder_pubkey, :proposer_pubkey, :value, :fee_recipient, :gas_limit, :block_hash) + ON CONFLICT (block_hash) DO NOTHING` + } + _, err = s.DB.NamedExec(query, builderDemotionEntry) + return err +} + +func (s *DatabaseService) GetBlockBuildersFromCollateralID(collateralID uint64) ([]*BlockBuilderEntry, error) { + query := `SELECT builder_pubkey FROM ` + vars.TableBlockBuilder + ` WHERE collateral_id=$1 ORDER BY id ASC;` + entries := []*BlockBuilderEntry{} + err := s.DB.Select(&entries, query, collateralID) + return entries, err +} diff --git a/database/migrations/001_init_database.go b/database/migrations/001_init_database.go index 7710a5d9..af3ee2c0 100644 --- a/database/migrations/001_init_database.go +++ b/database/migrations/001_init_database.go @@ -120,8 +120,9 @@ var Migration001InitDatabase = &migrate.Migration{ builder_pubkey varchar(98) NOT NULL, description text NOT NULL, - is_high_prio boolean NOT NULL, - is_blacklisted boolean NOT NULL, + status bigint NOT NULL, + collateral_value NUMERIC(48, 0), + collateral_id bigint NOT NULL, last_submission_id bigint references ` + vars.TableBuilderBlockSubmission + `(id) on delete set null, last_submission_slot bigint NOT NULL, @@ -134,6 +135,28 @@ var Migration001InitDatabase = &migrate.Migration{ UNIQUE (builder_pubkey) ); + + CREATE TABLE IF NOT EXISTS ` + vars.TableBuilderDemotions + `( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + inserted_at timestamp NOT NULL default current_timestamp, + + submit_block_request json, + signed_blinded_beacon_block json, + signed_validator_registration json, + + epoch bigint NOT NULL, + slot bigint NOT NULL, + + builder_pubkey varchar(98) NOT NULL, + proposer_pubkey varchar(98) NOT NULL, + + value NUMERIC(48, 0), + + fee_recipient varchar(42) NOT NULL, + gas_limit bigint NOT NULL, + + block_hash varchar(66) NOT NULL, + ); `}, Down: []string{` DROP TABLE IF EXISTS ` + vars.TableBuilderBlockSubmission + `; @@ -141,6 +164,7 @@ var Migration001InitDatabase = &migrate.Migration{ DROP TABLE IF EXISTS ` + vars.TableBlockBuilder + `; DROP TABLE IF EXISTS ` + vars.TableExecutionPayload + `; DROP TABLE IF EXISTS ` + vars.TableValidatorRegistration + `; + DROP TABLE IF EXISTS ` + vars.TableBuilderDemotions + `; `}, DisableTransactionUp: false, DisableTransactionDown: false, diff --git a/database/mockdb.go b/database/mockdb.go index e6bc405e..c75ac865 100644 --- a/database/mockdb.go +++ b/database/mockdb.go @@ -7,7 +7,11 @@ import ( "github.com/flashbots/mev-boost-relay/common" ) -type MockDB struct{} +type MockDB struct { + Builders map[string]*BlockBuilderEntry + Demotions map[string]bool + Refunds map[string]bool +} func (db MockDB) NumRegisteredValidators() (count uint64, err error) { return 0, nil @@ -86,10 +90,12 @@ func (db MockDB) GetBlockBuilders() ([]*BlockBuilderEntry, error) { } func (db MockDB) GetBlockBuilderByPubkey(pubkey string) (*BlockBuilderEntry, error) { - return nil, nil + return db.Builders[pubkey], nil } -func (db MockDB) SetBlockBuilderStatus(pubkey string, isHighPrio, isBlacklisted bool) error { +func (db MockDB) SetBlockBuilderStatus(pubkey string, builderStatus common.BuilderStatus) error { + builder := db.Builders[pubkey] + builder.Status = uint8(builderStatus) return nil } @@ -100,3 +106,24 @@ func (db MockDB) IncBlockBuilderStatsAfterGetHeader(slot uint64, blockhash strin func (db MockDB) IncBlockBuilderStatsAfterGetPayload(builderPubkey string) error { return nil } + +func (db MockDB) UpsertBuilderDemotion(submitBlockRequest *types.BuilderSubmitBlockRequest, signedBlindedBeaconBlock *types.SignedBlindedBeaconBlock, signedValidatorRegistration *types.SignedValidatorRegistration) error { + pk := submitBlockRequest.Message.BuilderPubkey.String() + db.Demotions[pk] = true + + // Refundable case. + if signedBlindedBeaconBlock != nil && signedValidatorRegistration != nil { + db.Refunds[pk] = true + } + return nil +} + +func (db MockDB) GetBlockBuildersFromCollateralID(collateralID uint64) ([]*BlockBuilderEntry, error) { + res := []*BlockBuilderEntry{} + for _, v := range db.Builders { + if v.CollateralID == collateralID { + res = append(res, v) + } + } + return res, nil +} diff --git a/database/types.go b/database/types.go index 7ebe8e7e..da296854 100644 --- a/database/types.go +++ b/database/types.go @@ -188,8 +188,9 @@ type BlockBuilderEntry struct { BuilderPubkey string `db:"builder_pubkey" json:"builder_pubkey"` Description string `db:"description" json:"description"` - IsHighPrio bool `db:"is_high_prio" json:"is_high_prio"` - IsBlacklisted bool `db:"is_blacklisted" json:"is_blacklisted"` + Status uint8 `db:"status" json:"status"` + CollateralValue string `db:"collateral_value" json:"collateral_value"` + CollateralID uint64 `db:"collateral_id" json:"collateral_id"` LastSubmissionID sql.NullInt64 `db:"last_submission_id" json:"last_submission_id"` LastSubmissionSlot uint64 `db:"last_submission_slot" json:"last_submission_slot"` @@ -199,3 +200,25 @@ type BlockBuilderEntry struct { NumSentGetPayload uint64 `db:"num_sent_getpayload" json:"num_sent_getpayload"` } + +type BuilderDemotionEntry struct { + ID int64 `db:"id"` + InsertedAt time.Time `db:"inserted_at"` + + SubmitBlockRequest sql.NullString `db:"submit_block_request"` + SignedBlindedBeaconBlock sql.NullString `db:"signed_blinded_beacon_block"` + SignedValidatorRegistration sql.NullString `db:"signed_validator_registration"` + + Slot uint64 `db:"slot"` + Epoch uint64 `db:"epoch"` + + BuilderPubkey string `db:"builder_pubkey"` + ProposerPubkey string `db:"proposer_pubkey"` + + Value string `db:"value"` + + FeeRecipient string `db:"fee_recipient"` + GasLimit uint64 `db:"gas_limit"` + + BlockHash string `db:"block_hash"` +} diff --git a/database/vars/tables.go b/database/vars/tables.go index 5e812b8c..75466690 100644 --- a/database/vars/tables.go +++ b/database/vars/tables.go @@ -12,4 +12,5 @@ var ( TableBuilderBlockSubmission = tableBase + "_builder_block_submission" TableDeliveredPayload = tableBase + "_payload_delivered" TableBlockBuilder = tableBase + "_blockbuilder" + TableBuilderDemotions = tableBase + "_builder_demotions" ) diff --git a/datastore/redis.go b/datastore/redis.go index 1e15536f..b643981a 100644 --- a/datastore/redis.go +++ b/datastore/redis.go @@ -32,14 +32,6 @@ var ( ErrFailedUpdatingTopBidNoBids = errors.New("failed to update top bid because no bids were found") ) -type BlockBuilderStatus string - -var ( - RedisBlockBuilderStatusLowPrio BlockBuilderStatus = "" - RedisBlockBuilderStatusHighPrio BlockBuilderStatus = "high-prio" - RedisBlockBuilderStatusBlacklisted BlockBuilderStatus = "blacklisted" -) - func PubkeyHexToLowerStr(pk types.PubkeyHex) string { return strings.ToLower(string(pk)) } @@ -77,10 +69,11 @@ type RedisCache struct { keyKnownValidators string keyValidatorRegistrationTimestamp string - keyRelayConfig string - keyStats string - keyProposerDuties string - keyBlockBuilderStatus string + keyRelayConfig string + keyStats string + keyProposerDuties string + keyBlockBuilderStatus string + keyBlockBuilderCollateral string } func NewRedisCache(redisURI, prefix string) (*RedisCache, error) { @@ -105,9 +98,10 @@ func NewRedisCache(redisURI, prefix string) (*RedisCache, error) { keyValidatorRegistrationTimestamp: fmt.Sprintf("%s/%s:validator-registration-timestamp", redisPrefix, prefix), keyRelayConfig: fmt.Sprintf("%s/%s:relay-config", redisPrefix, prefix), - keyStats: fmt.Sprintf("%s/%s:stats", redisPrefix, prefix), - keyProposerDuties: fmt.Sprintf("%s/%s:proposer-duties", redisPrefix, prefix), - keyBlockBuilderStatus: fmt.Sprintf("%s/%s:block-builder-status", redisPrefix, prefix), + keyStats: fmt.Sprintf("%s/%s:stats", redisPrefix, prefix), + keyProposerDuties: fmt.Sprintf("%s/%s:proposer-duties", redisPrefix, prefix), + keyBlockBuilderStatus: fmt.Sprintf("%s/%s:block-builder-status", redisPrefix, prefix), + keyBlockBuilderCollateral: fmt.Sprintf("%s/%s:block-builder-collateral", redisPrefix, prefix), }, nil } @@ -323,18 +317,35 @@ func (r *RedisCache) GetBidTrace(slot uint64, proposerPubkey, blockHash string) return resp, err } -func (r *RedisCache) SetBlockBuilderStatus(builderPubkey string, status BlockBuilderStatus) (err error) { - return r.client.HSet(context.Background(), r.keyBlockBuilderStatus, builderPubkey, string(status)).Err() +func (r *RedisCache) SetBuilderStatus(builderPubkey string, status common.BuilderStatus) (err error) { + return r.client.HSet(context.Background(), r.keyBlockBuilderStatus, builderPubkey, uint8(status)).Err() } -func (r *RedisCache) GetBlockBuilderStatus(builderPubkey string) (isHighPrio, isBlacklisted bool, err error) { +func (r *RedisCache) GetBuilderStatus(builderPubkey string) (status common.BuilderStatus, err error) { res, err := r.client.HGet(context.Background(), r.keyBlockBuilderStatus, builderPubkey).Result() if errors.Is(err, redis.Nil) { - return false, false, nil + return status, nil + } + in, err := strconv.Atoi(res) + if err != nil { + return status, err + } + return common.BuilderStatus(in), nil +} + +func (r *RedisCache) SetBuilderCollateral(builderPubkey, value string) (err error) { + return r.client.HSet(context.Background(), r.keyBlockBuilderCollateral, builderPubkey, value).Err() +} + +func (r *RedisCache) GetBuilderCollateral(builderPubkey string) (value string, err error) { + res, err := r.client.HGet(context.Background(), r.keyBlockBuilderCollateral, builderPubkey).Result() + if errors.Is(err, redis.Nil) { + return "", nil + } + if err != nil { + return "", err } - isHighPrio = BlockBuilderStatus(res) == RedisBlockBuilderStatusHighPrio - isBlacklisted = BlockBuilderStatus(res) == RedisBlockBuilderStatusBlacklisted - return isHighPrio, isBlacklisted, err + return res, nil } func (r *RedisCache) GetBuilderLatestPayloadReceivedAt(slot uint64, builderPubkey, parentHash, proposerPubkey string) (int64, error) { diff --git a/datastore/redis_test.go b/datastore/redis_test.go index bb9d11a1..786c6fd2 100644 --- a/datastore/redis_test.go +++ b/datastore/redis_test.go @@ -256,3 +256,41 @@ func TestRedisURIs(t *testing.T) { _, err = NewRedisCache(malformURL, "") require.Error(t, err) } + +func TestBuilderStatus(t *testing.T) { + builderPK := "0xdeadbeef" + cache := setupTestRedis(t) + + // Base status should be low-prio. + status, err := cache.GetBuilderStatus(builderPK) + require.NoError(t, err) + require.Equal(t, status, common.LowPrio) + + // Update to optimisitic. + err = cache.SetBuilderStatus(builderPK, common.Optimistic) + require.NoError(t, err) + + // Now status should be optimistic. + status, err = cache.GetBuilderStatus(builderPK) + require.NoError(t, err) + require.Equal(t, status, common.Optimistic) +} + +func TestBuilderCollateral(t *testing.T) { + builderPK := "0xdeadbeef" + cache := setupTestRedis(t) + + // Base collateral should be an empty string. + collateral, err := cache.GetBuilderCollateral(builderPK) + require.NoError(t, err) + require.Equal(t, collateral, "") + + // Update to be non-empty string. + err = cache.SetBuilderCollateral(builderPK, "12345") + require.NoError(t, err) + + // Now collateral should be "12345". + collateral, err = cache.GetBuilderCollateral(builderPK) + require.NoError(t, err) + require.Equal(t, collateral, "12345") +} diff --git a/datastore/utils.go b/datastore/utils.go deleted file mode 100644 index 6a9c8f96..00000000 --- a/datastore/utils.go +++ /dev/null @@ -1,11 +0,0 @@ -package datastore - -func MakeBlockBuilderStatus(isHighPrio, isBlacklisted bool) BlockBuilderStatus { - if isBlacklisted { - return RedisBlockBuilderStatusBlacklisted - } else if isHighPrio { - return RedisBlockBuilderStatusHighPrio - } else { - return RedisBlockBuilderStatusLowPrio - } -} diff --git a/services/api/blocksim_ratelimiter.go b/services/api/blocksim_ratelimiter.go index 5c47da5f..b00b42eb 100644 --- a/services/api/blocksim_ratelimiter.go +++ b/services/api/blocksim_ratelimiter.go @@ -19,6 +19,11 @@ var ( ErrSimulationFailed = errors.New("simulation failed") ) +type IBlockSimRateLimiter interface { + send(context context.Context, payload *BuilderBlockValidationRequest, isHighPrio bool) error + currentCounter() int64 +} + var maxConcurrentBlocks = int64(cli.GetEnvInt("BLOCKSIM_MAX_CONCURRENT", 4)) // 0 for no maximum type BlockSimulationRateLimiter struct { diff --git a/services/api/mock_blocksim_ratelimiter.go b/services/api/mock_blocksim_ratelimiter.go new file mode 100644 index 00000000..65bde20f --- /dev/null +++ b/services/api/mock_blocksim_ratelimiter.go @@ -0,0 +1,17 @@ +package api + +import ( + "context" +) + +type MockBlockSimulationRateLimiter struct { + simulationError error +} + +func (m *MockBlockSimulationRateLimiter) send(context context.Context, payload *BuilderBlockValidationRequest, isHighPrio bool) error { + return m.simulationError +} + +func (m *MockBlockSimulationRateLimiter) currentCounter() int64 { + return 0 +} diff --git a/services/api/optimistic_test.go b/services/api/optimistic_test.go new file mode 100644 index 00000000..d29a52a6 --- /dev/null +++ b/services/api/optimistic_test.go @@ -0,0 +1,332 @@ +package api + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/flashbots/go-boost-utils/bls" + "github.com/flashbots/go-boost-utils/types" + "github.com/flashbots/mev-boost-relay/beaconclient" + "github.com/flashbots/mev-boost-relay/common" + "github.com/flashbots/mev-boost-relay/database" + "github.com/flashbots/mev-boost-relay/datastore" + "github.com/stretchr/testify/require" + blst "github.com/supranational/blst/bindings/go" +) + +const ( + slot = uint64(42) + collateral = 1000 + collateralID = 567 + randao = "01234567890123456789012345678901" + proposerInd = uint64(987) +) + +var ( + feeRecipient = types.Address{0x02} + errFake = fmt.Errorf("foo error") +) + +type optimisticTestOpts struct { + pubkey types.PublicKey + secretkey *blst.SecretKey + simulationErr error + blockValue types.U256Str +} + +func getTestRandomHash(t *testing.T) types.Hash { + var random types.Hash + err := random.FromSlice([]byte(randao)) + require.NoError(t, err) + return random +} + +func getTestBlockHash(t *testing.T) types.Hash { + var blockHash types.Hash + err := blockHash.FromSlice([]byte("98765432109876543210987654321098")) + require.NoError(t, err) + return blockHash +} + +func startTestBackend(t *testing.T) (types.PublicKey, *blst.SecretKey, *testBackend) { + // Setup test key pair. + sk, _, err := bls.GenerateNewKeypair() + require.NoError(t, err) + blsPubkey := bls.PublicKeyFromSecretKey(sk) + var pubkey types.PublicKey + err = pubkey.FromSlice(blsPubkey.Compress()) + require.NoError(t, err) + pkStr := pubkey.String() + + // Setup test backend. + backend := newTestBackend(t, 1) + backend.relay.expectedPrevRandao = randaoHelper{ + slot: slot, + prevRandao: getTestRandomHash(t).String(), + } + backend.relay.genesisInfo = &beaconclient.GetGenesisResponse{} + backend.relay.genesisInfo.Data.GenesisTime = 0 + backend.relay.proposerDutiesMap = map[uint64]*types.RegisterValidatorRequestMessage{ + slot: &types.RegisterValidatorRequestMessage{ + FeeRecipient: feeRecipient, + GasLimit: 5000, + Timestamp: 0xffffffff, + Pubkey: types.PublicKey{}, + }, + } + backend.relay.opts.BlockBuilderAPI = true + backend.relay.beaconClient = beaconclient.NewMockMultiBeaconClient() + backend.relay.blockSimRateLimiter = &MockBlockSimulationRateLimiter{} + backend.relay.db = &database.MockDB{ + Builders: map[string]*database.BlockBuilderEntry{ + pkStr: &database.BlockBuilderEntry{ + BuilderPubkey: pkStr, + CollateralID: collateralID, + }, + }, + Demotions: map[string]bool{}, + Refunds: map[string]bool{}, + } + go backend.relay.StartServer() + time.Sleep(1 * time.Second) + + // Prepare redis. + err = backend.redis.SetStats(datastore.RedisStatsFieldSlotLastPayloadDelivered, slot-1) + require.NoError(t, err) + err = backend.redis.SetBuilderStatus(pkStr, common.Optimistic) + require.NoError(t, err) + err = backend.redis.SetBuilderCollateral(pkStr, strconv.Itoa(collateral)) + require.NoError(t, err) + err = backend.redis.SetKnownValidator(pubkey.PubkeyHex(), proposerInd) + require.NoError(t, err) + err = backend.redis.SaveExecutionPayload( + slot, + pkStr, + getTestBlockHash(t).String(), + &types.GetPayloadResponse{ + Data: &types.ExecutionPayload{ + Transactions: []hexutil.Bytes{}, + }, + }, + ) + require.NoError(t, err) + err = backend.redis.SaveBidTrace(&common.BidTraceV2{ + BidTrace: types.BidTrace{ + Slot: slot, + ProposerPubkey: pubkey, + BlockHash: getTestBlockHash(t), + BuilderPubkey: pubkey, + }, + }) + require.NoError(t, err) + + // Prepare db. + err = backend.relay.db.SetBlockBuilderStatus(pkStr, common.Optimistic) + require.NoError(t, err) + + // Prepare datastore. + count, err := backend.relay.datastore.RefreshKnownValidators() + require.NoError(t, err) + require.Equal(t, count, 1) + + return pubkey, sk, backend +} + +func runOptimisticBlockSubmission(t *testing.T, opts optimisticTestOpts, backend *testBackend) *httptest.ResponseRecorder { + var txn hexutil.Bytes + err := txn.UnmarshalText([]byte("0x03")) + require.NoError(t, err) + + backend.relay.blockSimRateLimiter = &MockBlockSimulationRateLimiter{ + simulationError: opts.simulationErr, + } + + // Set up request. + bidTrace := &types.BidTrace{ + Slot: slot, + BuilderPubkey: opts.pubkey, + ProposerFeeRecipient: feeRecipient, + Value: opts.blockValue, + } + signature, err := types.SignMessage(bidTrace, backend.relay.opts.EthNetDetails.DomainBuilder, opts.secretkey) + require.NoError(t, err) + req := &types.BuilderSubmitBlockRequest{ + Message: bidTrace, + Signature: signature, + ExecutionPayload: &types.ExecutionPayload{ + Timestamp: slot * 12, // 12 seconds per slot. + Transactions: []hexutil.Bytes{txn}, + Random: getTestRandomHash(t), + }, + } + + rr := backend.request(http.MethodPost, pathSubmitNewBlock, req) + + // Let updates happen async. + time.Sleep(2 * time.Second) + + return rr +} + +func runOptimisticGetPayload(t *testing.T, opts optimisticTestOpts, backend *testBackend) { + var txn hexutil.Bytes + err := txn.UnmarshalText([]byte("0x03")) + require.NoError(t, err) + + backend.relay.blockSimRateLimiter = &MockBlockSimulationRateLimiter{ + simulationError: opts.simulationErr, + } + + block := &types.BlindedBeaconBlock{ + Slot: slot, + ProposerIndex: proposerInd, + Body: &types.BlindedBeaconBlockBody{ + ExecutionPayloadHeader: &types.ExecutionPayloadHeader{ + BlockHash: getTestBlockHash(t), + BlockNumber: 1234, + }, + Eth1Data: &types.Eth1Data{}, + SyncAggregate: &types.SyncAggregate{}, + }, + } + signature, err := types.SignMessage(block, backend.relay.opts.EthNetDetails.DomainBeaconProposer, opts.secretkey) + require.NoError(t, err) + req := &types.SignedBlindedBeaconBlock{ + Message: block, + Signature: signature, + } + + rr := backend.request(http.MethodPost, pathGetPayload, req) + require.Equal(t, rr.Code, http.StatusOK) + + // Let updates happen async. + time.Sleep(2 * time.Second) +} + +func TestBuilderApiSubmitNewBlockOptimistic(t *testing.T) { + testCases := []struct { + description string + wantStatus common.BuilderStatus + simulationErr error + expectDemotion bool + httpCode uint64 + blockValue types.U256Str + }{ + { + description: "success_value_less_than_collateral", + wantStatus: common.Optimistic, + simulationErr: nil, + expectDemotion: false, + httpCode: 200, // success + blockValue: types.IntToU256(uint64(collateral) - 1), + }, + { + description: "success_value_greater_than_collateral", + wantStatus: common.Optimistic, + simulationErr: nil, + expectDemotion: false, + httpCode: 200, // success + blockValue: types.IntToU256(uint64(collateral) + 1), + }, + { + description: "failure_value_less_than_collateral", + wantStatus: common.LowPrio, + simulationErr: errFake, + expectDemotion: true, + httpCode: 200, // success (in optimistic mode, block sim failure will happen async) + blockValue: types.IntToU256(uint64(collateral) - 1), + }, + { + description: "failure_value_more_than_collateral", + wantStatus: common.Optimistic, + simulationErr: errFake, + expectDemotion: false, + httpCode: 400, // failure (in pessimistic mode, block sim failure happens in response path) + blockValue: types.IntToU256(uint64(collateral) + 1), + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + pk, sk, backend := startTestBackend(t) + pkStr := pk.String() + rr := runOptimisticBlockSubmission(t, optimisticTestOpts{ + secretkey: sk, + pubkey: pk, + simulationErr: tc.simulationErr, + blockValue: tc.blockValue, + }, backend) + + // Check http code. + require.Equal(t, uint64(rr.Code), tc.httpCode) + + // Check status in redis. + outStatus, err := backend.redis.GetBuilderStatus(pkStr) + require.NoError(t, err) + require.Equal(t, outStatus, tc.wantStatus) + + // Check status in db. + dbBuilder, err := backend.relay.db.GetBlockBuilderByPubkey(pkStr) + require.NoError(t, err) + require.Equal(t, common.BuilderStatus(dbBuilder.Status), tc.wantStatus) + + // Check demotion status is set to expected. + mockDB := backend.relay.db.(*database.MockDB) + require.Equal(t, mockDB.Demotions[pkStr], tc.expectDemotion) + }) + } +} + +func TestProposerApiGetPayloadOptimistic(t *testing.T) { + testCases := []struct { + description string + wantStatus common.BuilderStatus + simulationErr error + expectRefund bool + }{ + { + description: "success", + wantStatus: common.Optimistic, + simulationErr: nil, + expectRefund: false, + }, + { + description: "sim_error_refund", + wantStatus: common.LowPrio, + simulationErr: errFake, + expectRefund: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + pk, sk, backend := startTestBackend(t) + pkStr := pk.String() + runOptimisticGetPayload(t, optimisticTestOpts{ + secretkey: sk, + pubkey: pk, + simulationErr: tc.simulationErr, + }, backend) + + // Check status in redis. + outStatus, err := backend.redis.GetBuilderStatus(pkStr) + require.NoError(t, err) + require.Equal(t, outStatus, tc.wantStatus) + + // Check status in db. + dbBuilder, err := backend.relay.db.GetBlockBuilderByPubkey(pkStr) + require.NoError(t, err) + require.Equal(t, common.BuilderStatus(dbBuilder.Status), tc.wantStatus) + + // Check demotion and refund statuses are set to expected. + mockDB := backend.relay.db.(*database.MockDB) + require.Equal(t, mockDB.Demotions[pkStr], tc.expectRefund) + require.Equal(t, mockDB.Refunds[pkStr], tc.expectRefund) + }) + } +} diff --git a/services/api/service.go b/services/api/service.go index 439ad6c0..8ec94ce5 100644 --- a/services/api/service.go +++ b/services/api/service.go @@ -98,6 +98,14 @@ type randaoHelper struct { prevRandao string } +// Data needed to issue a block validation request. +type blockSimOptions struct { + ctx context.Context + highPrio bool + log *logrus.Entry + req *BuilderBlockValidationRequest +} + // RelayAPI represents a single Relay instance type RelayAPI struct { opts RelayAPIOpts @@ -123,7 +131,7 @@ type RelayAPI struct { proposerDutiesSlot uint64 isUpdatingProposerDuties uberatomic.Bool - blockSimRateLimiter *BlockSimulationRateLimiter + blockSimRateLimiter IBlockSimRateLimiter activeValidatorC chan types.PubkeyHex validatorRegC chan types.SignedValidatorRegistration @@ -139,6 +147,9 @@ type RelayAPI struct { expectedPrevRandao randaoHelper expectedPrevRandaoLock sync.RWMutex expectedPrevRandaoUpdating uint64 + + // Channel used to process optimistic blocks asynchronously. + optimisticBlockC chan blockSimOptions } // NewRelayAPI creates a new service. if builders is nil, allow any builder @@ -197,6 +208,7 @@ func NewRelayAPI(opts RelayAPIOpts) (api *RelayAPI, err error) { activeValidatorC: make(chan types.PubkeyHex, 450_000), validatorRegC: make(chan types.SignedValidatorRegistration, 450_000), + optimisticBlockC: make(chan blockSimOptions, 450_000), } if os.Getenv("FORCE_GET_HEADER_204") == "1" { @@ -286,6 +298,9 @@ func (api *RelayAPI) StartServer() (err error) { if api.opts.BlockBuilderAPI { // Get current proposer duties blocking before starting, to have them ready api.updateProposerDuties(bestSyncStatus.HeadSlot) + + // TODO(mikeneuder): consider if we should use >1 optimisticBlockProcessors. + go api.startOptimisticBlockProcessor() } // start things specific for the proposer API @@ -381,6 +396,74 @@ func (api *RelayAPI) startValidatorRegistrationDBProcessor() { } } +// simulateBlock sends a request for a block simulation to blockSimRateLimiter. +func (api *RelayAPI) simulateBlock(opts blockSimOptions) error { + t := time.Now() + simErr := api.blockSimRateLimiter.send(opts.ctx, opts.req, opts.highPrio) + log := opts.log.WithFields(logrus.Fields{ + "duration": time.Since(t).Seconds(), + "numWaiting": api.blockSimRateLimiter.currentCounter(), + }) + if simErr != nil { + log.WithError(simErr).Error("block validation failed") + } else { + log.Info("block validation successful") + } + return simErr +} + +// Demotes a set of builders who have matching collateral IDs. This function +// continues to try demoting even if an error is encountered. +func (api *RelayAPI) demoteBuildersByCollateralID(builderPubkey string) { + // Fetch builder collateral_id. + builder, err := api.db.GetBlockBuilderByPubkey(builderPubkey) + if err != nil { + api.log.WithError(err).Error("unable to get builder from database") + } + collateralID := builder.CollateralID + + // Fetch additional builder pubkeys using the collateral_id. + allBuilders, err := api.db.GetBlockBuildersFromCollateralID(collateralID) + if err != nil { + api.log.WithError(err).Error("unable to get builders from collateral id") + } + + // Demote all the pubkeys in both redis and the db. + for _, b := range allBuilders { + pk := b.BuilderPubkey + err = api.redis.SetBuilderStatus(pk, common.LowPrio) + if err != nil { + api.log.WithError(err).Error("could not set builder status in redis") + } + + err = api.db.SetBlockBuilderStatus(pk, common.LowPrio) + if err != nil { + api.log.WithError(err).Error("could not set builder status in database") + } + } +} + +// startOptimisticBlockProcessor keeps listening on the channel and validating incoming blocks asynchronously. +func (api *RelayAPI) startOptimisticBlockProcessor() { + for opts := range api.optimisticBlockC { + err := api.simulateBlock(opts) + if err != nil { + api.log.WithError(err).Error("block simulation failed") + builderPubkey := opts.req.Message.BuilderPubkey.String() + // Validation failed, demote all builders with the collateral id. + api.demoteBuildersByCollateralID(builderPubkey) + + // Upsert into the builder demotion table but without the + // blinded block or the validator registration, because we don't + // know if this bid will be accepted. + err = api.db.UpsertBuilderDemotion(&opts.req.BuilderSubmitBlockRequest, nil, nil) + if err != nil { + api.log.WithError(err).Error("could not upsert bid trace") + } + } + } +} + func (api *RelayAPI) processNewSlot(headSlot uint64) { _apiHeadSlot := api.headSlot.Load() if headSlot <= _apiHeadSlot { @@ -830,6 +913,71 @@ func (api *RelayAPI) handleGetPayload(w http.ResponseWriter, req *http.Request) if err != nil { log.WithError(err).Error("failed to increment builder-stats after getPayload") } + + builderStatus, err := api.redis.GetBuilderStatus(bidTrace.BuilderPubkey.String()) + if err != nil { + log.WithError(err).Error("failed to get builder status") + } + + // Check if the block was valid in the optimistic case. + if builderStatus == common.Optimistic { + // Set to high-prio while we process the winning block. + err = api.redis.SetBuilderStatus(bidTrace.BuilderPubkey.String(), common.HighPrio) + if err != nil { + log.WithError(err).Error("failed to set builder builder status") + } + + submitBlockReq := types.BuilderSubmitBlockRequest{ + Signature: payload.Signature, + Message: &bidTrace.BidTrace, + ExecutionPayload: getPayloadResp.Data, + } + simErr := api.simulateBlock(blockSimOptions{ + ctx: req.Context(), + log: log, + highPrio: true, // manually set to true for these blocks. + req: &BuilderBlockValidationRequest{ + BuilderSubmitBlockRequest: submitBlockReq, + }, + }) + if simErr != nil { + log.WithError(err).Error("failed to simulate signed block") + builderPubkey := bidTrace.BuilderPubkey.String() + // Validation failed, demote all builders with the collateral id. + api.demoteBuildersByCollateralID(builderPubkey) + + signedRegistration := &types.SignedValidatorRegistration{} + registrationEntry, err := api.db.GetValidatorRegistration(bidTrace.ProposerPubkey.String()) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + api.log.WithError(err).Error("no validator registration found") + } else { + api.log.WithError(err).Error("error getting validator registration") + } + } + if registrationEntry != nil { + signedRegistration, err = registrationEntry.ToSignedValidatorRegistration() + if err != nil { + api.log.WithError(err).Error("error converting registration entry to signed validator registration") + } + } + + err = api.db.UpsertBuilderDemotion(&submitBlockReq, payload, signedRegistration) + if err != nil { + log.WithError(err).WithFields(logrus.Fields{ + "bidTrace": bidTrace, + "signedBlindedBeaconBlock": payload, + "signedValidatorRegistration": signedRegistration, + }).Error("failed to save validator refund to database") + } + } else { + // Set back to optimistic because the simulation was successful. + err = api.redis.SetBuilderStatus(bidTrace.BuilderPubkey.String(), common.Optimistic) + if err != nil { + log.WithError(err).Error("failed to set builder builder status") + } + } + } }() // Publish the signed beacon block via beacon-node @@ -839,7 +987,7 @@ func (api *RelayAPI) handleGetPayload(w http.ResponseWriter, req *http.Request) return } signedBeaconBlock := SignedBlindedBeaconBlockToBeaconBlock(payload, getPayloadResp.Data) - _, _ = api.beaconClient.PublishBlock(signedBeaconBlock) // errors are logged inside + _, _ = api.beaconClient.PublishBlock(signedBeaconBlock) // errors are logged inside. }() } @@ -945,15 +1093,43 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque } } - builderIsHighPrio, builderIsBlacklisted, err := api.redis.GetBlockBuilderStatus(payload.Message.BuilderPubkey.String()) + builderStatus, err := api.redis.GetBuilderStatus(payload.Message.BuilderPubkey.String()) log = log.WithFields(logrus.Fields{ - "builderIsHighPrio": builderIsHighPrio, - "builderIsBlacklisted": builderIsBlacklisted, + "builderStatus": builderStatus.String(), }) if err != nil { log.WithError(err).Error("could not get block builder status") } + if builderStatus == common.Blacklisted { + log.Info("builder is blacklisted") + time.Sleep(200 * time.Millisecond) + w.WriteHeader(http.StatusOK) + return + } + + // Check for collateral in optimistic case. + if builderStatus == common.Optimistic { + builderCollateralStr, err := api.redis.GetBuilderCollateral(payload.Message.BuilderPubkey.String()) + if err != nil { + log.WithError(err).Error("could not get block builder collateral") + builderCollateralStr = "" + } + + // Try to parse builder collateral string (U256Str) type. + var builderCollateral types.U256Str + err = builderCollateral.UnmarshalText([]byte(builderCollateralStr)) + if err != nil { + log.WithError(err).Error("could not parse builder collateral string") + builderCollateral = ZeroU256 + } + + // Check if builder collateral exceeds the value of the block. If not, set as just high prio instead of optimistic. + if builderCollateral.Cmp((*types.U256Str)(&payload.Message.Value)) < 0 { + builderStatus = common.HighPrio + } + } + // Timestamp check expectedTimestamp := api.genesisInfo.Data.GenesisTime + (payload.Message.Slot * 12) if payload.ExecutionPayload.Timestamp != expectedTimestamp { @@ -985,15 +1161,8 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque return } - if builderIsBlacklisted { - log.Info("builder is blacklisted") - time.Sleep(200 * time.Millisecond) - w.WriteHeader(http.StatusOK) - return - } - // In case only high-prio requests are accepted, fail others - if api.ffDisableLowPrioBuilders && !builderIsHighPrio { + if api.ffDisableLowPrioBuilders && builderStatus != common.HighPrio { log.Info("rejecting low-prio builder (ff-disable-low-prio-builders)") time.Sleep(200 * time.Millisecond) w.WriteHeader(http.StatusOK) @@ -1001,11 +1170,11 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque } log = log.WithFields(logrus.Fields{ - "builderHighPrio": builderIsHighPrio, - "proposerPubkey": payload.Message.ProposerPubkey.String(), - "parentHash": payload.Message.ParentHash.String(), - "value": payload.Message.Value.String(), - "tx": len(payload.ExecutionPayload.Transactions), + "builderStatus": builderStatus.String(), + "proposerPubkey": payload.Message.ProposerPubkey.String(), + "parentHash": payload.Message.ParentHash.String(), + "value": payload.Message.Value.String(), + "tx": len(payload.ExecutionPayload.Transactions), }) if payload.Message.Slot <= api.headSlot.Load() { @@ -1068,27 +1237,26 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque } }() - // Simulate the block submission and save to db - t := time.Now() - validationRequestPayload := &BuilderBlockValidationRequest{ - BuilderSubmitBlockRequest: *payload, - RegisteredGasLimit: slotDuty.GasLimit, - } - simErr = api.blockSimRateLimiter.send(req.Context(), validationRequestPayload, builderIsHighPrio) - - if simErr != nil { - log = log.WithField("simErr", simErr.Error()) - log.WithError(simErr).WithFields(logrus.Fields{ - "duration": time.Since(t).Seconds(), - "numWaiting": api.blockSimRateLimiter.currentCounter(), - }).Info("block validation failed") - api.RespondError(w, http.StatusBadRequest, simErr.Error()) - return + // Construct simulation request. + opts := blockSimOptions{ + ctx: req.Context(), + highPrio: builderStatus == common.HighPrio || builderStatus == common.Optimistic, + log: log, + req: &BuilderBlockValidationRequest{ + BuilderSubmitBlockRequest: *payload, + RegisteredGasLimit: slotDuty.GasLimit, + }, + } + + // Only perform simulation on hot path if we are in non-optimistic mode. + if builderStatus == common.Optimistic { + // Write optimistic block to channel for async validation. + api.optimisticBlockC <- opts } else { - log.WithFields(logrus.Fields{ - "duration": time.Since(t).Seconds(), - "numWaiting": api.blockSimRateLimiter.currentCounter(), - }).Info("block validation successful") + simErr = api.simulateBlock(opts) + if simErr != nil { + api.RespondError(w, http.StatusBadRequest, simErr.Error()) + } } // Ensure this request is still the latest one @@ -1198,26 +1366,34 @@ func (api *RelayAPI) handleInternalBuilderStatus(w http.ResponseWriter, req *htt return } else if req.Method == http.MethodPost || req.Method == http.MethodPut || req.Method == http.MethodPatch { args := req.URL.Query() + isOptimistic := args.Get("optimistic") == "true" isHighPrio := args.Get("high_prio") == "true" isBlacklisted := args.Get("blacklisted") == "true" api.log.WithFields(logrus.Fields{ "builderPubkey": builderPubkey, + "isOptimistic": isOptimistic, "isHighPrio": isHighPrio, "isBlacklisted": isBlacklisted, }).Info("updating builder status") - - newStatus := datastore.MakeBlockBuilderStatus(isHighPrio, isBlacklisted) - err := api.redis.SetBlockBuilderStatus(builderPubkey, newStatus) + status := common.LowPrio + if isBlacklisted { + status = common.Blacklisted + } else if isOptimistic { + status = common.Optimistic + } else if isHighPrio { + status = common.HighPrio + } + err := api.redis.SetBuilderStatus(builderPubkey, status) if err != nil { - api.log.WithError(err).Error("could not set block builder status in redis") + api.log.WithError(err).Error("could not set builder status in redis") } - err = api.db.SetBlockBuilderStatus(builderPubkey, isHighPrio, isBlacklisted) + err = api.db.SetBlockBuilderStatus(builderPubkey, status) if err != nil { - api.log.WithError(err).Error("could not set block builder status in database") + api.log.WithError(err).Error("could not set builder status in database") } - api.RespondOK(w, struct{ newStatus string }{newStatus: string(newStatus)}) + api.RespondOK(w, struct{ newStatus string }{newStatus: status.String()}) } } diff --git a/services/housekeeper/housekeeper.go b/services/housekeeper/housekeeper.go index e87bc3cd..9593d537 100644 --- a/services/housekeeper/housekeeper.go +++ b/services/housekeeper/housekeeper.go @@ -338,11 +338,11 @@ func (hk *Housekeeper) updateBlockBuildersInRedis() { hk.log.Infof("updating %d block builders in Redis...", len(builders)) for _, builder := range builders { - status := datastore.MakeBlockBuilderStatus(builder.IsHighPrio, builder.IsBlacklisted) - hk.log.Infof("updating block builder in Redis: %s - %s", builder.BuilderPubkey, status) - err = hk.redis.SetBlockBuilderStatus(builder.BuilderPubkey, status) + status := common.BuilderStatus(builder.Status) + hk.log.Infof("updating block builder in Redis: %s - %s", builder.BuilderPubkey, status.String()) + err = hk.redis.SetBuilderStatus(builder.BuilderPubkey, status) if err != nil { - hk.log.WithError(err).Error("failed to set block builder status in redis") + hk.log.WithError(err).Error("failed to set builder status in redis") } } }