-
Notifications
You must be signed in to change notification settings - Fork 0
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
take #3 :-) #3
base: main
Are you sure you want to change the base?
take #3 :-) #3
Conversation
6831616
to
2b7f102
Compare
2b7f102
to
1e3adbf
Compare
1e3adbf
to
3fc6369
Compare
services/api/service.go
Outdated
// Upsert into the builder demotion table but without the signed block | ||
// or the signed registration, because we don't know if this bid will be | ||
// accepted. | ||
err = api.db.UpsertBuilderDemotion(&opts.req.BuilderSubmitBlockRequest, nil, nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should maybe use logs as a makeshift DB in case of an error here. We get that for free when we abstract away the logic in a separate function (as previously commented).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, this lead to a good cleanup IMO. i created a function called demoteBuilder
which wraps the calls to SetBlockBuilderStatusByCollateralID
and UpsertBuilderDemotion
which we call both from the optimisticBlockProcessor and the getPayload handler.
this function takes the logging that we reasoned we could use as our backup DB when we were discussing the getPayload logging, so now we will have consistency in our demotion logging. thanks!
services/api/service.go
Outdated
@@ -950,13 +1097,35 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque | |||
} | |||
} | |||
|
|||
builderIsHighPrio, builderIsBlacklisted, err := api.redis.GetBlockBuilderStatus(payload.Message.BuilderPubkey.String()) | |||
builderStatus, err := api.redis.GetBlockBuilderStatus(payload.Message.BuilderPubkey.String()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should maybe consider caching api.redis.GetBlockBuilderStatus
and api.redis.GetBlockBuilderCollateral
for all builders, e.g. taking a snapshot once per slot. Here's my thinking:
- With optimistic relaying we can have looser rate limiting (e.g. 100 bids/sec/builder). Each Redis read takes on the order of 1ms so these Redis calls can add up, even with just tens of builder pubkeys. The current codebase was designed for a 2 bids/sec/builder paradigm and may need a bit of performance refactoring to handle the load.
- The status of builders can change mid-slot in two different ways: a) through demotions, and b) through the internal API. As such there are potential race conditions whenever the internal API is used. It would be conceptually cleaner if at the beginning of every slot (e.g. in
updateOptimisticSlot
) we take a snapshot of the relevant subset ofvars.TableBlockBuilder
. - Those Redis calls are in the critical path so we're looking to shave a couple milliseconds of latency—a nice-to-have micro-optimisation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this is a nice change! i added logic to updateOptimisticSlot to cache (create a map as a field of the relay type) the builder status and collaterals at each slot by reading from the TableBlockBuilder as you mentioned. then in the critical path of the block submission, we can just read from this cache. this has a small added benefit of getting rid of our need for the Get and Set collateral on the redis interface (because we only ever read the collateral from the db). and it makes the code more readable on the fast path --- feels like a win!
i was thinking about if we should filter the builders put in the cache, but I don't really think its necessary. we could use a heuristic like "only add builders who have submitted a block in the last 100 slots or something", but unless we need to this feels like http://wiki.c2.com/?PrematureOptimization :-) especially considering this is happening at the beginning of each slot, one db read and iterating through the list of builders just doesn't seem that crazy to me. but lmk what you think!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lmk what you think!
I think it looks great :)
responded to your comments in this commit fc8ded6. i can squash down into the base commit once you have a chance to look, just figured it would be nice to see this specific diffs here. thanks :-) |
478e48f
to
c69f94b
Compare
c69f94b
to
a098ca9
Compare
22efc16
to
c1d4f69
Compare
datastore/datastore.go
Outdated
func (ds *Datastore) SetBlockBuilderStatusByCollateralID(builderPubkey string, status common.BuilderStatus) []error { | ||
// Fetch builder collateral_id. | ||
builder, err := ds.db.GetBlockBuilderByPubkey(builderPubkey) | ||
if err != nil { | ||
err = fmt.Errorf("unable to get builder from database: %v", err) | ||
ds.log.Error(err.Error()) | ||
return []error{err} | ||
} | ||
|
||
// Fetch all builder pubkeys using the collateral_id. | ||
builderPubkeys, err := ds.db.GetBlockBuilderPubkeysByCollateralID(builder.CollateralID) | ||
if err != nil { | ||
err = fmt.Errorf("unable to get builder pubkeys by collateral id: %v", err) | ||
ds.log.Error(err.Error()) | ||
return []error{err} | ||
} | ||
|
||
var errs []error | ||
for _, pubkey := range builderPubkeys { | ||
err := ds.db.SetBlockBuilderStatus(pubkey, status) | ||
if err != nil { | ||
err = fmt.Errorf("failed to set block builder: %v status in db: %v", pubkey, err) | ||
ds.log.Error(err.Error()) | ||
errs = append(errs, err) | ||
} | ||
} | ||
return errs | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since Redis is no longer involved should this go in database.go
? Also, would it be possible to do a one-shot SQL update (instead of 1 + 1 reads and n
writes)? That may be cleaner and more performant. A single error object would suffice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, good points! here is a commit that implements this. 8425fbc
the tl;dr; is we can bundle this all into a single SetBuilderStatus
function, which checks if the collateral ID of a builder is set. if it is, then update all the statuses in once DB update call. if it is not, then just update the single builder pubkey. this is a nice cleanup i think!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice!
if !ok { | ||
log.Warnf("unable to read builder: %x from the builder cache, using low-prio and no collateral", builderPubkey) | ||
builderEntry = &blockBuilderCacheEntry{ | ||
status: common.BuilderStatus{ | ||
IsHighPrio: false, | ||
}, | ||
collateral: ZeroU256, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm tempted to error out and return
. The reason is that this path should never happen. And if it does, alerting the builder instead of silently failing may be better. It's also fewer lines of code :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so the builder won't be in the DB until the first call to UpsertBlockBuilderEntryAfterSubmission
which takes place after this (line 1248 in current rev). since the cache reads from the DB, if we don't have this, then the builder will never have a way to get added to the table.
the builder status and collateral also can't be modified until the builder is in the table. we could add an endpoint to manually add the builder, but this would add complexity. IMO logging with warning here feels correct, but happy to continue discussing if you still think we should change it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an equivalent of log.Warnf
but as an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes! there is log.Errorf
i didn't use this because every new builder that connects will hit this code path, so it could potentially be a lot of noise in terms of errors that are actually expected. happy to do errorf if you think it is better though!
a497e50
to
9b9c476
Compare
aa587e8
to
49d43eb
Compare
49d43eb
to
3008327
Compare
read optimistic_submission field from db during GetBuilderSubmissions
tl;dr; This PR implements an "optimistic" version of flashbots' mev-boost-relay. The core idea is that we can improve the throughput and latency of block submissions by using asynchronous processing. Instead of validating each block that comes in immediately, we return early and add the block to the datastore before checking its validity. This (hopefully) allows builders to submit more blocks, and critically, submit blocks later in the time period of the slot (because they don't have to wait for the simulation of the block to return successfully). Proposers still use the Proposer API to select the highest bid, but there is no longer a guarantee that this block is valid. To submit blocks in the optimistic mode, a builder must put up a collateral that is greater than the value of the blocks they are proposing. If a proposer ends up signing an invalid block, collateral from the builder of that block will be used to refund the proposer.
The changes can be described through 3 sequences:
1. Submitting optimistic blocks.
Builder API
endpoint of the relay.a. if the builder collateral is greater than the value of the block and the builder is not
demoted
, run the block simulation optimistically.b. else send the builder to the appropriate queue of the
prio-load-balancer
.2. Validating optimistic blocks.
OptimisticBlockProcessor
sends the block ashigh-prio
to theprio-load-balancer
for simulation.OptimisticBlockProcessor
.3. Proposing optimistic blocks
mev-boost
callsgetHeader
on theProposer API
of the relay. This is part of theMEV-Block Block Proposal
as documented in https://docs.flashbots.net/flashbots-mev-boost/architecture-overview/block-proposal.mev-boost
callsgetPayload
on theProposer API
of the relay. This triggers the publication of aSignedBeaconBlock
.prio-load-balancer
for simulation.prio-load-balancer
runs the simulation of the block against the validation nodes.Builder Demotions Table
with the details of the failure so the refund can be justified.Misc notes