Skip to content

Commit

Permalink
Save simulated block value to database (#630)
Browse files Browse the repository at this point in the history
* Add block value information in validation api

* fix sql insert block value

* add more logging
  • Loading branch information
avalonche authored Jul 23, 2024
1 parent 4d4478a commit cf6fd5b
Show file tree
Hide file tree
Showing 11 changed files with 95 additions and 37 deletions.
5 changes: 5 additions & 0 deletions common/types_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/flashbots/go-boost-utils/bls"
"github.com/flashbots/go-boost-utils/ssz"
"github.com/flashbots/go-boost-utils/utils"
"github.com/holiman/uint256"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -276,6 +277,10 @@ func (r *BuilderBlockValidationRequest) MarshalJSON() ([]byte, error) {
}
}

type BuilderBlockValidationResponse struct {
BlockValue *uint256.Int `json:"block_value"` // true block value calculated from simulation
}

type VersionedSubmitBlockRequest struct {
builderSpec.VersionedSubmitBlockRequest
}
Expand Down
20 changes: 15 additions & 5 deletions database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package database

import (
"context"
"database/sql"
"encoding/json"
"fmt"
"os"
Expand All @@ -13,6 +14,7 @@ import (
"github.com/flashbots/mev-boost-relay/common"
"github.com/flashbots/mev-boost-relay/database/migrations"
"github.com/flashbots/mev-boost-relay/database/vars"
"github.com/holiman/uint256"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
migrate "github.com/rubenv/sql-migrate"
Expand All @@ -25,7 +27,7 @@ type IDatabaseService interface {
GetValidatorRegistration(pubkey string) (*ValidatorRegistrationEntry, error)
GetValidatorRegistrationsForPubkeys(pubkeys []string) ([]*ValidatorRegistrationEntry, error)

SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error)
SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool, blockValue *uint256.Int) (entry *BuilderBlockSubmissionEntry, err error)
GetBlockSubmissionEntry(slot uint64, proposerPubkey, blockHash string) (entry *BuilderBlockSubmissionEntry, err error)
GetBuilderSubmissions(filters GetBuilderSubmissionsFilters) ([]*BuilderBlockSubmissionEntry, error)
GetBuilderSubmissionsBySlots(slotFrom, slotTo uint64) (entries []*BuilderBlockSubmissionEntry, err error)
Expand Down Expand Up @@ -99,8 +101,8 @@ func (s *DatabaseService) prepareNamedQueries() (err error) {

// Insert block builder submission
query = `INSERT INTO ` + vars.TableBuilderBlockSubmission + `
(received_at, eligible_at, execution_payload_id, was_simulated, sim_success, sim_error, sim_req_error, signature, slot, parent_hash, block_hash, builder_pubkey, proposer_pubkey, proposer_fee_recipient, gas_used, gas_limit, num_tx, value, epoch, block_number, decode_duration, prechecks_duration, simulation_duration, redis_update_duration, total_duration, optimistic_submission) VALUES
(:received_at, :eligible_at, :execution_payload_id, :was_simulated, :sim_success, :sim_error, :sim_req_error, :signature, :slot, :parent_hash, :block_hash, :builder_pubkey, :proposer_pubkey, :proposer_fee_recipient, :gas_used, :gas_limit, :num_tx, :value, :epoch, :block_number, :decode_duration, :prechecks_duration, :simulation_duration, :redis_update_duration, :total_duration, :optimistic_submission)
(received_at, eligible_at, execution_payload_id, was_simulated, sim_success, sim_error, sim_req_error, signature, slot, parent_hash, block_hash, builder_pubkey, proposer_pubkey, proposer_fee_recipient, gas_used, gas_limit, num_tx, value, epoch, block_number, decode_duration, prechecks_duration, simulation_duration, redis_update_duration, total_duration, optimistic_submission, block_value) VALUES
(:received_at, :eligible_at, :execution_payload_id, :was_simulated, :sim_success, :sim_error, :sim_req_error, :signature, :slot, :parent_hash, :block_hash, :builder_pubkey, :proposer_pubkey, :proposer_fee_recipient, :gas_used, :gas_limit, :num_tx, :value, :epoch, :block_number, :decode_duration, :prechecks_duration, :simulation_duration, :redis_update_duration, :total_duration, :optimistic_submission, :block_value)
RETURNING id`
s.nstmtInsertBlockBuilderSubmission, err = s.DB.PrepareNamed(query)
return err
Expand Down Expand Up @@ -175,7 +177,7 @@ func (s *DatabaseService) GetLatestValidatorRegistrations(timestampOnly bool) ([
return registrations, err
}

func (s *DatabaseService) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) {
func (s *DatabaseService) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool, blockValue *uint256.Int) (entry *BuilderBlockSubmissionEntry, err error) {
// Save execution_payload: insert, or if already exists update to be able to return the id ('on conflict do nothing' doesn't return an id)
execPayloadEntry, err := PayloadToExecPayloadEntry(payload)
if err != nil {
Expand All @@ -200,6 +202,10 @@ func (s *DatabaseService) SaveBuilderBlockSubmission(payload *common.VersionedSu
requestErrStr = requestError.Error()
}

blockValueStr := ""
if blockValue != nil {
blockValueStr = blockValue.Dec()
}
submission, err := common.GetBlockSubmissionInfo(payload)
if err != nil {
return nil, err
Expand All @@ -214,6 +220,10 @@ func (s *DatabaseService) SaveBuilderBlockSubmission(payload *common.VersionedSu
SimSuccess: wasSimulated && validationError == nil,
SimError: simErrStr,
SimReqError: requestErrStr,
BlockValue: sql.NullString{
String: blockValueStr,
Valid: blockValue != nil,
},

Signature: submission.Signature.String(),

Expand Down Expand Up @@ -403,7 +413,7 @@ func (s *DatabaseService) GetBuilderSubmissions(filters GetBuilderSubmissionsFil
"builder_pubkey": filters.BuilderPubkey,
}

fields := "id, inserted_at, received_at, eligible_at, slot, epoch, builder_pubkey, proposer_pubkey, proposer_fee_recipient, parent_hash, block_hash, block_number, num_tx, value, gas_used, gas_limit, optimistic_submission"
fields := "id, inserted_at, received_at, eligible_at, slot, epoch, builder_pubkey, proposer_pubkey, proposer_fee_recipient, parent_hash, block_hash, block_number, num_tx, value, gas_used, gas_limit, optimistic_submission, block_value"
limit := "LIMIT :limit"

whereConds := []string{
Expand Down
5 changes: 4 additions & 1 deletion database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const (
slot = uint64(42)
collateral = 1000
collateralStr = "1000"
blockValue = 1234
blockValueStr = "1234"
builderID = "builder0x69"
randao = "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"
optimisticSubmission = true
Expand Down Expand Up @@ -88,7 +90,7 @@ func insertTestBuilder(t *testing.T, db IDatabaseService) string {
Value: uint256.NewInt(collateral),
},
}, spec.DataVersionDeneb)
entry, err := db.SaveBuilderBlockSubmission(req, nil, nil, time.Now(), time.Now().Add(time.Second), true, true, profile, optimisticSubmission)
entry, err := db.SaveBuilderBlockSubmission(req, nil, nil, time.Now(), time.Now().Add(time.Second), true, true, profile, optimisticSubmission, uint256.NewInt(blockValue))
require.NoError(t, err)
err = db.UpsertBlockBuilderEntryAfterSubmission(entry, false)
require.NoError(t, err)
Expand Down Expand Up @@ -450,6 +452,7 @@ func TestGetBuilderSubmissions(t *testing.T) {
require.Equal(t, pubkey, e.BuilderPubkey)
require.Equal(t, feeRecipient.String(), e.ProposerFeeRecipient)
require.Equal(t, strconv.Itoa(collateral), e.Value)
require.Equal(t, NewNullString(blockValueStr), e.BlockValue)
}

func TestUpsertTooLateGetPayload(t *testing.T) {
Expand Down
17 changes: 17 additions & 0 deletions database/migrations/011_bid_add_simulated_block_value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package migrations

import (
"github.com/flashbots/mev-boost-relay/database/vars"
migrate "github.com/rubenv/sql-migrate"
)

var Migration011AddSimulatedBlockValue = &migrate.Migration{
Id: "011-add-simulated-block-value",
Up: []string{`
ALTER TABLE ` + vars.TableBuilderBlockSubmission + ` ADD block_value NUMERIC(48, 0);
`},
Down: []string{},

DisableTransactionUp: true,
DisableTransactionDown: true,
}
1 change: 1 addition & 0 deletions database/migrations/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ var Migrations = migrate.MemoryMigrationSource{
Migration008Optimistic,
Migration009BlockBuilderRemoveReference,
Migration010PayloadAddBlobFields,
Migration011AddSimulatedBlockValue,
},
}
3 changes: 2 additions & 1 deletion database/mockdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

builderApiV1 "github.com/attestantio/go-builder-client/api/v1"
"github.com/flashbots/mev-boost-relay/common"
"github.com/holiman/uint256"
)

type MockDB struct {
Expand Down Expand Up @@ -36,7 +37,7 @@ func (db MockDB) GetLatestValidatorRegistrations(timestampOnly bool) ([]*Validat
return nil, nil
}

func (db MockDB) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) {
func (db MockDB) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool, blockValue *uint256.Int) (entry *BuilderBlockSubmissionEntry, err error) {
return nil, nil
}

Expand Down
9 changes: 5 additions & 4 deletions database/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,11 @@ type BuilderBlockSubmissionEntry struct {
ExecutionPayloadID sql.NullInt64 `db:"execution_payload_id"`

// Sim Result
WasSimulated bool `db:"was_simulated"`
SimSuccess bool `db:"sim_success"`
SimError string `db:"sim_error"`
SimReqError string `db:"sim_req_error"`
WasSimulated bool `db:"was_simulated"`
SimSuccess bool `db:"sim_success"`
SimError string `db:"sim_error"`
SimReqError string `db:"sim_req_error"`
BlockValue sql.NullString `db:"block_value"`

// BidTrace data
Signature string `db:"signature"`
Expand Down
27 changes: 19 additions & 8 deletions services/api/blocksim_ratelimiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ var (
)

type IBlockSimRateLimiter interface {
Send(context context.Context, payload *common.BuilderBlockValidationRequest, isHighPrio, fastTrack bool) (error, error)
Send(context context.Context, payload *common.BuilderBlockValidationRequest, isHighPrio, fastTrack bool) (*common.BuilderBlockValidationResponse, error, error)
CurrentCounter() int64
}

Expand Down Expand Up @@ -58,7 +58,12 @@ func NewBlockSimulationRateLimiter(blockSimURL string) *BlockSimulationRateLimit
}
}

func (b *BlockSimulationRateLimiter) Send(context context.Context, payload *common.BuilderBlockValidationRequest, isHighPrio, fastTrack bool) (requestErr, validationErr error) {
func (b *BlockSimulationRateLimiter) Send(
context context.Context,
payload *common.BuilderBlockValidationRequest,
isHighPrio,
fastTrack bool,
) (response *common.BuilderBlockValidationResponse, requestErr, validationErr error) {
b.cv.L.Lock()
cnt := atomic.AddInt64(&b.counter, 1)
if maxConcurrentBlocks > 0 && cnt > maxConcurrentBlocks {
Expand All @@ -74,21 +79,21 @@ func (b *BlockSimulationRateLimiter) Send(context context.Context, payload *comm
}()

if err := context.Err(); err != nil {
return fmt.Errorf("%w, %w", ErrRequestClosed, err), nil
return nil, fmt.Errorf("%w, %w", ErrRequestClosed, err), nil
}

var simReq *jsonrpc.JSONRPCRequest
if payload.Version == spec.DataVersionCapella && payload.Capella == nil {
return ErrNoCapellaPayload, nil
return nil, ErrNoCapellaPayload, nil
}

if payload.Version == spec.DataVersionDeneb && payload.Deneb == nil {
return ErrNoDenebPayload, nil
return nil, ErrNoDenebPayload, nil
}

submission, err := common.GetBlockSubmissionInfo(payload.VersionedSubmitBlockRequest)
if err != nil {
return err, nil
return nil, err, nil
}

// Prepare headers
Expand All @@ -107,8 +112,14 @@ func (b *BlockSimulationRateLimiter) Send(context context.Context, payload *comm
} else {
simReq = jsonrpc.NewJSONRPCRequest("1", "flashbots_validateBuilderSubmissionV2", payload)
}
_, requestErr, validationErr = SendJSONRPCRequest(&b.client, *simReq, b.blockSimURL, headers)
return requestErr, validationErr
res, requestErr, validationErr := SendJSONRPCRequest(&b.client, *simReq, b.blockSimURL, headers)
response = new(common.BuilderBlockValidationResponse)
if res != nil {
if err := json.Unmarshal(res.Result, response); err != nil {
return nil, fmt.Errorf("unable to unmarshal response: %w", err), validationErr
}
}
return response, requestErr, validationErr
}

// CurrentCounter returns the number of waiting and active requests
Expand Down
4 changes: 2 additions & 2 deletions services/api/mock_blocksim_ratelimiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ type MockBlockSimulationRateLimiter struct {
simulationError error
}

func (m *MockBlockSimulationRateLimiter) Send(context context.Context, payload *common.BuilderBlockValidationRequest, isHighPrio, fastTrack bool) (error, error) {
return nil, m.simulationError
func (m *MockBlockSimulationRateLimiter) Send(context context.Context, payload *common.BuilderBlockValidationRequest, isHighPrio, fastTrack bool) (*common.BuilderBlockValidationResponse, error, error) {
return nil, nil, m.simulationError
}

func (m *MockBlockSimulationRateLimiter) CurrentCounter() int64 {
Expand Down
2 changes: 1 addition & 1 deletion services/api/optimistic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func TestSimulateBlock(t *testing.T) {
backend.relay.blockSimRateLimiter = &MockBlockSimulationRateLimiter{
simulationError: tc.simulationError,
}
_, simErr := backend.relay.simulateBlock(context.Background(), blockSimOptions{
_, _, simErr := backend.relay.simulateBlock(context.Background(), blockSimOptions{
isHighPrio: true,
log: backend.relay.log,
builder: &blockBuilderCacheEntry{
Expand Down
39 changes: 24 additions & 15 deletions services/api/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ type blockBuilderCacheEntry struct {

type blockSimResult struct {
wasSimulated bool
blockValue *uint256.Int
optimisticSubmission bool
requestErr error
validationErr error
Expand Down Expand Up @@ -585,9 +586,9 @@ func (api *RelayAPI) startValidatorRegistrationDBProcessor() {
}

// simulateBlock sends a request for a block simulation to blockSimRateLimiter.
func (api *RelayAPI) simulateBlock(ctx context.Context, opts blockSimOptions) (requestErr, validationErr error) {
func (api *RelayAPI) simulateBlock(ctx context.Context, opts blockSimOptions) (blockValue *uint256.Int, requestErr, validationErr error) {
t := time.Now()
requestErr, validationErr = api.blockSimRateLimiter.Send(ctx, opts.req, opts.isHighPrio, opts.fastTrack)
response, requestErr, validationErr := api.blockSimRateLimiter.Send(ctx, opts.req, opts.isHighPrio, opts.fastTrack)
log := opts.log.WithFields(logrus.Fields{
"durationMs": time.Since(t).Milliseconds(),
"numWaiting": api.blockSimRateLimiter.CurrentCounter(),
Expand All @@ -598,18 +599,23 @@ func (api *RelayAPI) simulateBlock(ctx context.Context, opts blockSimOptions) (r
ignoreError := validationErr.Error() == ErrBlockAlreadyKnown || validationErr.Error() == ErrBlockRequiresReorg || strings.Contains(validationErr.Error(), ErrMissingTrieNode)
if ignoreError {
log.WithError(validationErr).Warn("block validation failed with ignorable error")
return nil, nil
return nil, nil, nil
}
}
log.WithError(validationErr).Warn("block validation failed")
return nil, validationErr
return nil, nil, validationErr
}
if requestErr != nil {
log.WithError(requestErr).Warn("block validation failed: request error")
return requestErr, nil
return nil, requestErr, nil
}

log.Info("block validation successful")
return nil, nil
if response == nil {
log.Warn("block validation response is nil")
return nil, nil, nil
}
return response.BlockValue, nil, nil
}

func (api *RelayAPI) demoteBuilder(pubkey string, req *common.VersionedSubmitBlockRequest, simError error) {
Expand Down Expand Up @@ -674,8 +680,8 @@ func (api *RelayAPI) processOptimisticBlock(opts blockSimOptions, simResultC cha
// it for logging, it is not atomic to avoid the performance impact.
"optBlocksInFlight": api.optimisticBlocksInFlight,
}).Infof("simulating optimistic block with hash: %v", submission.BidTrace.BlockHash.String())
reqErr, simErr := api.simulateBlock(ctx, opts)
simResultC <- &blockSimResult{reqErr == nil, true, reqErr, simErr}
blockValue, reqErr, simErr := api.simulateBlock(ctx, opts)
simResultC <- &blockSimResult{reqErr == nil, blockValue, true, reqErr, simErr}
if reqErr != nil || simErr != nil {
// Mark builder as non-optimistic.
opts.builder.status.IsOptimistic = false
Expand Down Expand Up @@ -1804,7 +1810,7 @@ func (api *RelayAPI) checkFloorBidValue(opts bidFloorOpts) (*big.Int, bool) {
isBidBelowFloor := floorBidValue != nil && opts.submission.BidTrace.Value.ToBig().Cmp(floorBidValue) == -1
isBidAtOrBelowFloor := floorBidValue != nil && opts.submission.BidTrace.Value.ToBig().Cmp(floorBidValue) < 1
if opts.cancellationsEnabled && isBidBelowFloor { // with cancellations: if below floor -> delete previous bid
opts.simResultC <- &blockSimResult{false, false, nil, nil}
opts.simResultC <- &blockSimResult{false, nil, false, nil, nil}
opts.log.Info("submission below floor bid value, with cancellation")
err := api.redis.DelBuilderBid(context.Background(), opts.tx, opts.submission.BidTrace.Slot, opts.submission.BidTrace.ParentHash.String(), opts.submission.BidTrace.ProposerPubkey.String(), opts.submission.BidTrace.BuilderPubkey.String())
if err != nil {
Expand All @@ -1815,7 +1821,7 @@ func (api *RelayAPI) checkFloorBidValue(opts bidFloorOpts) (*big.Int, bool) {
api.Respond(opts.w, http.StatusAccepted, "accepted bid below floor, skipped validation")
return nil, false
} else if !opts.cancellationsEnabled && isBidAtOrBelowFloor { // without cancellations: if at or below floor -> ignore
opts.simResultC <- &blockSimResult{false, false, nil, nil}
opts.simResultC <- &blockSimResult{false, nil, false, nil, nil}
opts.log.Info("submission at or below floor bid value, without cancellation")
api.RespondMsg(opts.w, http.StatusAccepted, "accepted bid below floor, skipped validation")
return nil, false
Expand Down Expand Up @@ -2091,12 +2097,15 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque
case simResult = <-simResultC:
case <-time.After(10 * time.Second):
log.Warn("timed out waiting for simulation result")
simResult = &blockSimResult{false, false, nil, nil}
simResult = &blockSimResult{false, nil, false, nil, nil}
}

submissionEntry, err := api.db.SaveBuilderBlockSubmission(payload, simResult.requestErr, simResult.validationErr, receivedAt, eligibleAt, simResult.wasSimulated, savePayloadToDatabase, pf, simResult.optimisticSubmission)
submissionEntry, err := api.db.SaveBuilderBlockSubmission(payload, simResult.requestErr, simResult.validationErr, receivedAt, eligibleAt, simResult.wasSimulated, savePayloadToDatabase, pf, simResult.optimisticSubmission, simResult.blockValue)
if err != nil {
log.WithError(err).WithField("payload", payload).Error("saving builder block submission to database failed")
log.WithError(err).WithFields(logrus.Fields{
"payload": payload,
"simResult": simResult,
}).Error("saving builder block submission to database failed")
return
}

Expand Down Expand Up @@ -2161,8 +2170,8 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque
go api.processOptimisticBlock(opts, simResultC)
} else {
// Simulate block (synchronously).
requestErr, validationErr := api.simulateBlock(context.Background(), opts) // success/error logging happens inside
simResultC <- &blockSimResult{requestErr == nil, false, requestErr, validationErr}
blockValue, requestErr, validationErr := api.simulateBlock(context.Background(), opts) // success/error logging happens inside
simResultC <- &blockSimResult{requestErr == nil, blockValue, false, requestErr, validationErr}
validationDurationMs := time.Since(timeBeforeValidation).Milliseconds()
log = log.WithFields(logrus.Fields{
"timestampAfterValidation": time.Now().UTC().UnixMilli(),
Expand Down

0 comments on commit cf6fd5b

Please sign in to comment.