diff --git a/common/types.go b/common/types.go index bc909676..e9f440c3 100644 --- a/common/types.go +++ b/common/types.go @@ -133,15 +133,15 @@ func NewEthNetworkDetails(networkName string) (ret *EthNetworkDetails, err error func (e *EthNetworkDetails) String() string { return fmt.Sprintf( `EthNetworkDetails{ - Name: %s, - GenesisForkVersionHex: %s, + Name: %s, + GenesisForkVersionHex: %s, GenesisValidatorsRootHex: %s, - BellatrixForkVersionHex: %s, - CapellaForkVersionHex: %s, + BellatrixForkVersionHex: %s, + CapellaForkVersionHex: %s, DenebForkVersionHex: %s, - DomainBuilder: %x, - DomainBeaconProposerBellatrix: %x, - DomainBeaconProposerCapella: %x, + DomainBuilder: %x, + DomainBeaconProposerBellatrix: %x, + DomainBeaconProposerCapella: %x, DomainBeaconProposerDeneb: %x }`, e.Name, @@ -1070,3 +1070,11 @@ func (s *SubmitBlockRequestV2Optimistic) SizeSSZ() (size int) { return } + +// BuilderBlockValidationResponseV2 is the expected response from the builder +// node simulation requests. It contains the modified gas limit and the new +// block hash. +type BuilderBlockValidationResponseV2 struct { + NewGasLimit uint64 `json:"new_gas_limit,string"` + NewBlockHash phase0.Hash32 `json:"new_block_hash"` +} diff --git a/database/database.go b/database/database.go index 939161a2..4aa5003c 100644 --- a/database/database.go +++ b/database/database.go @@ -241,7 +241,7 @@ func (s *DatabaseService) SaveBuilderBlockSubmission(payload *common.BuilderSubm } func (s *DatabaseService) GetBlockSubmissionEntry(slot uint64, proposerPubkey, blockHash string) (entry *BuilderBlockSubmissionEntry, err error) { - query := `SELECT id, inserted_at, received_at, eligible_at, execution_payload_id, sim_success, sim_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 + query := `SELECT id, inserted_at, received_at, eligible_at, execution_payload_id, sim_success, sim_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 FROM ` + vars.TableBuilderBlockSubmission + ` WHERE slot=$1 AND proposer_pubkey=$2 AND block_hash=$3 ORDER BY builder_pubkey ASC diff --git a/services/api/blocksim_ratelimiter.go b/services/api/blocksim_ratelimiter.go index 74fdbb8a..e269ce8d 100644 --- a/services/api/blocksim_ratelimiter.go +++ b/services/api/blocksim_ratelimiter.go @@ -28,7 +28,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.BuilderBlockValidationResponseV2, error, error) CurrentCounter() int64 } @@ -50,7 +50,7 @@ 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) (resp *common.BuilderBlockValidationResponseV2, requestErr, validationErr error) { b.cv.L.Lock() cnt := atomic.AddInt64(&b.counter, 1) if maxConcurrentBlocks > 0 && cnt > maxConcurrentBlocks { @@ -66,12 +66,12 @@ 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.Capella == nil { - return ErrNoCapellaPayload, nil + return nil, ErrNoCapellaPayload, nil } // TODO: add deneb support. @@ -87,8 +87,14 @@ func (b *BlockSimulationRateLimiter) Send(context context.Context, payload *comm // Create and fire off JSON-RPC request simReq = jsonrpc.NewJSONRPCRequest("1", "flashbots_validateBuilderSubmissionV2", payload) - _, requestErr, validationErr = SendJSONRPCRequest(&b.client, *simReq, b.blockSimURL, headers) - return requestErr, validationErr + rawResp, requestErr, validationErr := SendJSONRPCRequest(&b.client, *simReq, b.blockSimURL, headers) + + err := json.Unmarshal(rawResp.Result, resp) + if err != nil { + return nil, err, nil + } + + return resp, requestErr, validationErr } // CurrentCounter returns the number of waiting and active requests diff --git a/services/api/mock_blocksim_ratelimiter.go b/services/api/mock_blocksim_ratelimiter.go index 1d180e56..76c07b36 100644 --- a/services/api/mock_blocksim_ratelimiter.go +++ b/services/api/mock_blocksim_ratelimiter.go @@ -7,11 +7,14 @@ import ( ) type MockBlockSimulationRateLimiter struct { + overrides common.BuilderBlockValidationResponseV2 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.BuilderBlockValidationResponseV2, error, error, +) { + return &m.overrides, nil, m.simulationError } func (m *MockBlockSimulationRateLimiter) CurrentCounter() int64 { diff --git a/services/api/optimistic_test.go b/services/api/optimistic_test.go index b0f6e491..fb76b25a 100644 --- a/services/api/optimistic_test.go +++ b/services/api/optimistic_test.go @@ -180,7 +180,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{ diff --git a/services/api/service.go b/services/api/service.go index 41908b29..db55b73e 100644 --- a/services/api/service.go +++ b/services/api/service.go @@ -41,9 +41,11 @@ import ( ) const ( - ErrBlockAlreadyKnown = "simulation failed: block already known" - ErrBlockRequiresReorg = "simulation failed: block requires a reorg" - ErrMissingTrieNode = "missing trie node" + RelayActualGasLimit = 30_000_000 + RelayFictitiousGasLimit = 100_000_000 + ErrBlockAlreadyKnown = "simulation failed: block already known" + ErrBlockRequiresReorg = "simulation failed: block requires a reorg" + ErrMissingTrieNode = "missing trie node" ) var ( @@ -160,6 +162,7 @@ type blockSimResult struct { optimisticSubmission bool requestErr error validationErr error + overrides *common.BuilderBlockValidationResponseV2 } // RelayAPI represents a single Relay instance @@ -548,9 +551,10 @@ 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) (overrides *common.BuilderBlockValidationResponseV2, requestErr, validationErr error) { t := time.Now() - requestErr, validationErr = api.blockSimRateLimiter.Send(ctx, opts.req, opts.isHighPrio, opts.fastTrack) + overrides, 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(), @@ -561,18 +565,18 @@ 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 + return overrides, nil, nil } func (api *RelayAPI) demoteBuilder(pubkey string, req *common.BuilderSubmitBlockRequest, simError error) { @@ -618,8 +622,14 @@ 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", opts.req.BuilderSubmitBlockRequest.BlockHash()) - reqErr, simErr := api.simulateBlock(ctx, opts) - simResultC <- &blockSimResult{reqErr == nil, true, reqErr, simErr} + overrides, reqErr, simErr := api.simulateBlock(ctx, opts) + + overrideGasValues( + &opts.req.BuilderSubmitBlockRequest, + overrides, + ) + + simResultC <- &blockSimResult{reqErr == nil, true, reqErr, simErr, overrides} if reqErr != nil || simErr != nil { // Mark builder as non-optimistic. opts.builder.status.IsOptimistic = false @@ -752,6 +762,12 @@ func (api *RelayAPI) updateProposerDuties(headSlot uint64) { return } + // Changes the response to indicate that the target gas limit is + // RelayActualGasLimit, not the proposer-specified value. + for _, duty := range duties { + duty.Entry.Message.GasLimit = RelayActualGasLimit + } + // Prepare raw bytes for HTTP response respBytes, err := json.Marshal(duties) if err != nil { @@ -1484,23 +1500,23 @@ func (api *RelayAPI) handleBuilderGetValidators(w http.ResponseWriter, req *http } } -func (api *RelayAPI) checkSubmissionFeeRecipient(w http.ResponseWriter, log *logrus.Entry, payload *common.BuilderSubmitBlockRequest) (uint64, bool) { +func (api *RelayAPI) checkSubmissionFeeRecipient(w http.ResponseWriter, log *logrus.Entry, payload *common.BuilderSubmitBlockRequest) bool { api.proposerDutiesLock.RLock() slotDuty := api.proposerDutiesMap[payload.Slot()] api.proposerDutiesLock.RUnlock() if slotDuty == nil { log.Warn("could not find slot duty") api.RespondError(w, http.StatusBadRequest, "could not find slot duty") - return 0, false + return false } else if !strings.EqualFold(slotDuty.Entry.Message.FeeRecipient.String(), payload.ProposerFeeRecipient()) { log.WithFields(logrus.Fields{ "expectedFeeRecipient": slotDuty.Entry.Message.FeeRecipient.String(), "actualFeeRecipient": payload.ProposerFeeRecipient(), }).Info("fee recipient does not match") api.RespondError(w, http.StatusBadRequest, "fee recipient does not match") - return 0, false + return false } - return slotDuty.Entry.Message.GasLimit, true + return true } func (api *RelayAPI) checkSubmissionPayloadAttrs(w http.ResponseWriter, log *logrus.Entry, payload *common.BuilderSubmitBlockRequest) bool { @@ -1630,7 +1646,7 @@ func (api *RelayAPI) checkFloorBidValue(opts bidFloorOpts) (*big.Int, *logrus.En isBidBelowFloor := floorBidValue != nil && opts.payload.Value().Cmp(floorBidValue) == -1 isBidAtOrBelowFloor := floorBidValue != nil && opts.payload.Value().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, false, nil, nil, nil} opts.log.Info("submission below floor bid value, with cancellation") err := api.redis.DelBuilderBid(context.Background(), opts.tx, opts.payload.Slot(), opts.payload.ParentHash(), opts.payload.ProposerPubkey(), opts.payload.BuilderPubkey().String()) if err != nil { @@ -1641,7 +1657,7 @@ func (api *RelayAPI) checkFloorBidValue(opts bidFloorOpts) (*big.Int, *logrus.En api.Respond(opts.w, http.StatusAccepted, "accepted bid below floor, skipped validation") return nil, 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, false, nil, 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, nil, false @@ -1817,7 +1833,7 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque "timestampAfterChecks1": time.Now().UTC().UnixMilli(), }) - gasLimit, ok := api.checkSubmissionFeeRecipient(w, log, payload) + ok = api.checkSubmissionFeeRecipient(w, log, payload) if !ok { return } @@ -1887,9 +1903,12 @@ 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, false, nil, nil, nil} } + // override the gas values with the ones from the simulation + overrideGasValues(payload, simResult.overrides) + submissionEntry, err := api.db.SaveBuilderBlockSubmission(payload, simResult.requestErr, simResult.validationErr, receivedAt, eligibleAt, simResult.wasSimulated, savePayloadToDatabase, pf, simResult.optimisticSubmission) if err != nil { log.WithError(err).WithField("payload", payload).Error("saving builder block submission to database failed") @@ -1940,7 +1959,7 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque builder: builderEntry, req: &common.BuilderBlockValidationRequest{ BuilderSubmitBlockRequest: *payload, - RegisteredGasLimit: gasLimit, + RegisteredGasLimit: RelayFictitiousGasLimit, }, } // With sufficient collateral, process the block optimistically. @@ -1950,8 +1969,14 @@ 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} + overrides, requestErr, validationErr := api.simulateBlock(context.Background(), opts) // success/error logging happens inside + + overrideGasValues( + &opts.req.BuilderSubmitBlockRequest, + overrides, + ) + + simResultC <- &blockSimResult{requestErr == nil, false, requestErr, validationErr, overrides} validationDurationMs := time.Since(timeBeforeValidation).Milliseconds() log = log.WithFields(logrus.Fields{ "timestampAfterValidation": time.Now().UTC().UnixMilli(), @@ -2361,3 +2386,13 @@ func (api *RelayAPI) handleReadyz(w http.ResponseWriter, req *http.Request) { api.RespondMsg(w, http.StatusServiceUnavailable, "not ready") } } + +func overrideGasValues( + req *common.BuilderSubmitBlockRequest, + overrides *common.BuilderBlockValidationResponseV2, +) { + if overrides != nil && req != nil { + req.Capella.ExecutionPayload.BlockHash = overrides.NewBlockHash + req.Capella.ExecutionPayload.GasLimit = overrides.NewGasLimit + } +} diff --git a/services/api/service_test.go b/services/api/service_test.go index 1f5f13ef..8ddb4517 100644 --- a/services/api/service_test.go +++ b/services/api/service_test.go @@ -578,8 +578,7 @@ func TestCheckSubmissionFeeRecipient(t *testing.T) { w := httptest.NewRecorder() logger := logrus.New() log := logrus.NewEntry(logger) - gasLimit, ok := backend.relay.checkSubmissionFeeRecipient(w, log, tc.payload) - require.Equal(t, tc.expectGasLimit, gasLimit) + ok := backend.relay.checkSubmissionFeeRecipient(w, log, tc.payload) require.Equal(t, tc.expectOk, ok) }) }