Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[code health] refactor floor bid checks to independent function #513

Merged
merged 1 commit into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions datastore/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,13 @@ func (r *RedisCache) GetFloorBidValue(ctx context.Context, tx redis.Pipeliner, s
return floorValue, nil
}

// SetFloorBidValue is used only for testing.
func (r *RedisCache) SetFloorBidValue(slot uint64, parentHash, proposerPubkey, value string) error {
keyFloorBidValue := r.keyFloorBidValue(slot, parentHash, proposerPubkey)
err := r.client.Set(context.Background(), keyFloorBidValue, value, 0).Err()
return err
}

func (r *RedisCache) NewPipeline() redis.Pipeliner { //nolint:ireturn,nolintlint
return r.client.Pipeline()
}
Expand Down
111 changes: 66 additions & 45 deletions services/api/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1595,6 +1595,59 @@ func (api *RelayAPI) checkBuilderEntry(w http.ResponseWriter, log *logrus.Entry,
return builderEntry, true
}

type bidFloorOpts struct {
w http.ResponseWriter
tx redis.Pipeliner
log *logrus.Entry
cancellationsEnabled bool
simResultC chan *blockSimResult
payload *common.BuilderSubmitBlockRequest
}

func (api *RelayAPI) checkFloorBidValue(opts bidFloorOpts) (*big.Int, *logrus.Entry, bool) {
// Reject new submissions once the payload for this slot was delivered - TODO: store in memory as well
slotLastPayloadDelivered, err := api.redis.GetLastSlotDelivered(context.Background(), opts.tx)
if err != nil && !errors.Is(err, redis.Nil) {
opts.log.WithError(err).Error("failed to get delivered payload slot from redis")
} else if opts.payload.Slot() <= slotLastPayloadDelivered {
opts.log.Info("rejecting submission because payload for this slot was already delivered")
api.RespondError(opts.w, http.StatusBadRequest, "payload for this slot was already delivered")
return nil, nil, false
}

// Grab floor bid value
floorBidValue, err := api.redis.GetFloorBidValue(context.Background(), opts.tx, opts.payload.Slot(), opts.payload.ParentHash(), opts.payload.ProposerPubkey())
if err != nil {
opts.log.WithError(err).Error("failed to get floor bid value from redis")
} else {
opts.log = opts.log.WithField("floorBidValue", floorBidValue.String())
}

// --------------------------------------------
// Skip submission if below the floor bid value
// --------------------------------------------
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.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 {
opts.log.WithError(err).Error("failed processing cancellable bid below floor")
api.RespondError(opts.w, http.StatusInternalServerError, "failed processing cancellable bid below floor")
return nil, nil, false
}
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.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
}
return floorBidValue, opts.log, true
}

func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Request) {
var pf common.Profile
var prevTime, nextTime time.Time
Expand Down Expand Up @@ -1764,24 +1817,23 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque
// Create the redis pipeline tx
tx := api.redis.NewTxPipeline()

// Reject new submissions once the payload for this slot was delivered - TODO: store in memory as well
slotLastPayloadDelivered, err := api.redis.GetLastSlotDelivered(context.Background(), tx)
if err != nil && !errors.Is(err, redis.Nil) {
log.WithError(err).Error("failed to get delivered payload slot from redis")
} else if payload.Slot() <= slotLastPayloadDelivered {
log.Info("rejecting submission because payload for this slot was already delivered")
api.RespondError(w, http.StatusBadRequest, "payload for this slot was already delivered")
return
}

// -------------------------------------------------------------------
// SUBMISSION SIGNATURE IS VALIDATED AND BID IS GENERALLY LOOKING GOOD
// -------------------------------------------------------------------

// channel to send simulation result to the deferred function
simResultC := make(chan *blockSimResult, 1)
var eligibleAt time.Time // will be set once the bid is ready

bfOpts := bidFloorOpts{
w: w,
tx: tx,
log: log,
cancellationsEnabled: isCancellationEnabled,
simResultC: simResultC,
payload: payload,
}
floorBidValue, log, ok := api.checkFloorBidValue(bfOpts)
if !ok {
return
}

// Deferred saving of the builder submission to database (whenever this function ends)
defer func() {
savePayloadToDatabase := !api.ffDisablePayloadDBStorage
Expand All @@ -1805,37 +1857,6 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque
}
}()

// Grab floor bid value
floorBidValue, err := api.redis.GetFloorBidValue(context.Background(), tx, payload.Slot(), payload.ParentHash(), payload.ProposerPubkey())
if err != nil {
log.WithError(err).Error("failed to get floor bid value from redis")
} else {
log = log.WithField("floorBidValue", floorBidValue.String())
}

// --------------------------------------------
// Skip submission if below the floor bid value
// --------------------------------------------
isBidBelowFloor := floorBidValue != nil && payload.Value().Cmp(floorBidValue) == -1
isBidAtOrBelowFloor := floorBidValue != nil && payload.Value().Cmp(floorBidValue) < 1
if isCancellationEnabled && isBidBelowFloor { // with cancellations: if below floor -> delete previous bid
simResultC <- &blockSimResult{false, false, nil, nil}
log.Info("submission below floor bid value, with cancellation")
err := api.redis.DelBuilderBid(context.Background(), tx, payload.Slot(), payload.ParentHash(), payload.ProposerPubkey(), payload.BuilderPubkey().String())
if err != nil {
log.WithError(err).Error("failed processing cancellable bid below floor")
api.RespondError(w, http.StatusInternalServerError, "failed processing cancellable bid below floor")
return
}
api.Respond(w, http.StatusAccepted, "accepted bid below floor, skipped validation")
return
} else if !isCancellationEnabled && isBidAtOrBelowFloor { // without cancellations: if at or below floor -> ignore
simResultC <- &blockSimResult{false, false, nil, nil}
log.Info("submission below floor bid value, without cancellation")
api.RespondMsg(w, http.StatusAccepted, "accepted bid below floor, skipped validation")
return
}

// ---------------------------------
// THE BID WILL BE SIMULATED SHORTLY
// ---------------------------------
Expand Down
87 changes: 87 additions & 0 deletions services/api/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,93 @@ func TestCheckBuilderEntry(t *testing.T) {
}
}

func TestCheckFloorBidValue(t *testing.T) {
cases := []struct {
description string
payload *common.BuilderSubmitBlockRequest
cancellationsEnabled bool
floorValue string
expectOk bool
}{
{
description: "success",
payload: &common.BuilderSubmitBlockRequest{
Capella: &builderCapella.SubmitBlockRequest{
Message: &v1.BidTrace{
Slot: testSlot,
Value: uint256.NewInt(1),
},
},
},
expectOk: true,
},
{
description: "failure_slot_already_delivered",
payload: &common.BuilderSubmitBlockRequest{
Capella: &builderCapella.SubmitBlockRequest{
Message: &v1.BidTrace{
Slot: 0,
},
},
},
expectOk: false,
},
{
description: "failure_cancellations_below_floor",
payload: &common.BuilderSubmitBlockRequest{
Capella: &builderCapella.SubmitBlockRequest{
Message: &v1.BidTrace{
Slot: testSlot,
Value: uint256.NewInt(1),
},
},
},
expectOk: false,
cancellationsEnabled: true,
floorValue: "2",
},
{
description: "failure_no_cancellations_at_floor",
payload: &common.BuilderSubmitBlockRequest{
Capella: &builderCapella.SubmitBlockRequest{
Message: &v1.BidTrace{
Slot: testSlot,
Value: uint256.NewInt(0),
},
},
},
expectOk: false,
},
}
for _, tc := range cases {
t.Run(tc.description, func(t *testing.T) {
_, _, backend := startTestBackend(t)
err := backend.redis.SetFloorBidValue(tc.payload.Slot(), tc.payload.ParentHash(), tc.payload.ProposerPubkey(), tc.floorValue)
require.Nil(t, err)

w := httptest.NewRecorder()
logger := logrus.New()
log := logrus.NewEntry(logger)
tx := backend.redis.NewTxPipeline()
simResultC := make(chan *blockSimResult, 1)
bfOpts := bidFloorOpts{
w: w,
tx: tx,
log: log,
cancellationsEnabled: tc.cancellationsEnabled,
simResultC: simResultC,
payload: tc.payload,
}
floor, log, ok := backend.relay.checkFloorBidValue(bfOpts)
require.Equal(t, tc.expectOk, ok)
if ok {
require.NotNil(t, floor)
require.NotNil(t, log)
}
})
}
}

func gzipBytes(t *testing.T, b []byte) []byte {
t.Helper()
var buf bytes.Buffer
Expand Down
Loading