Skip to content
Open
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
24 changes: 19 additions & 5 deletions crates/relay/src/api/proposer/submit_builder_preferences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use helix_common::{
use helix_types::{BuilderPreferencesRequest, ForkName};
use hyper::StatusCode;
use ssz::Decode;
use tracing::info;
use tracing::{info, warn};

use super::{ProposerApi, get_payload::fork_name_from_header};
use crate::api::{Api, proposer::error::ProposerApiError};
Expand Down Expand Up @@ -41,11 +41,25 @@ impl<A: Api> ProposerApi<A> {
proposer_pubkey = ?params.proposer_pubkey,
slot = request.auth.message.slot,
max_execution_payment = request.preferences.max_execution_payment,
"validated submitBuilderPreferences request (not yet persisted)"
"validated submitBuilderPreferences request"
);

// TODO(gloas): reject stale slots, store preferences per proposer per slot, and honor
// max_execution_payment when serving bids.
Ok(StatusCode::ACCEPTED)
let Ok(rx) = proposer_api.auctioneer_handle.submit_builder_preferences(
params.proposer_pubkey,
request.auth.message.slot,
request.preferences.max_execution_payment,
) else {
return Err(ProposerApiError::InternalServerError);
};

// TODO(gloas): honor max_execution_payment when getExecutionPayloadBid serves a real
// bid; not consumed anywhere yet.
match rx.await {
Ok(res) => res.map(|()| StatusCode::ACCEPTED),
Err(err) => {
warn!(%err, "failed to store builder preferences");
Err(ProposerApiError::InternalServerError)
}
}
}
}
94 changes: 94 additions & 0 deletions crates/relay/src/auctioneer/builder_preferences.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use helix_types::BlsPublicKeyBytes;
use rustc_hash::FxHashMap;

/// Per-proposer-per-slot `max_execution_payment` preferences, submitted via
/// `submitBuilderPreferences` up to an epoch ahead of the slot they apply to. Lives on `Context`
/// (not `SlotContext`), since entries must survive across slot transitions until their own slot
/// arrives or passes.
#[derive(Default)]
pub struct BuilderPreferencesStore {
by_proposer_slot: FxHashMap<(BlsPublicKeyBytes, u64), u64>,
}

impl BuilderPreferencesStore {
pub fn store(
&mut self,
proposer_pubkey: BlsPublicKeyBytes,
slot: u64,
max_execution_payment: u64,
) {
self.by_proposer_slot.insert((proposer_pubkey, slot), max_execution_payment);
}

pub fn max_execution_payment(
&self,
proposer_pubkey: &BlsPublicKeyBytes,
slot: u64,
) -> Option<u64> {
self.by_proposer_slot.get(&(*proposer_pubkey, slot)).copied()
}

/// Drops entries for slots that have already passed, so a proposer's own resubmissions
/// (or one that never proposes) don't grow this unboundedly.
pub fn on_new_slot(&mut self, bid_slot: u64) {
self.by_proposer_slot.retain(|(_, slot), _| *slot >= bid_slot);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how many slots will we need to store here for each proposer? slots per epoch?

}

#[cfg(test)]
pub fn len(&self) -> usize {
self.by_proposer_slot.len()
}
}

#[cfg(test)]
mod tests {
use super::*;

fn pubkey(byte: u8) -> BlsPublicKeyBytes {
BlsPublicKeyBytes::repeat_byte(byte)
}

#[test]
fn stores_and_looks_up_per_proposer_per_slot() {
let mut store = BuilderPreferencesStore::default();
let alice = pubkey(1);
let bob = pubkey(2);

store.store(alice, 100, 500);
store.store(bob, 100, 900);
store.store(alice, 101, 700);

assert_eq!(store.max_execution_payment(&alice, 100), Some(500));
assert_eq!(store.max_execution_payment(&bob, 100), Some(900));
assert_eq!(store.max_execution_payment(&alice, 101), Some(700));
assert_eq!(store.max_execution_payment(&alice, 102), None);
}

#[test]
fn resubmission_overwrites_the_previous_value() {
let mut store = BuilderPreferencesStore::default();
let alice = pubkey(1);

store.store(alice, 100, 500);
store.store(alice, 100, 600);

assert_eq!(store.max_execution_payment(&alice, 100), Some(600));
}

#[test]
fn on_new_slot_prunes_entries_for_slots_already_passed() {
let mut store = BuilderPreferencesStore::default();
let alice = pubkey(1);

store.store(alice, 100, 500);
store.store(alice, 101, 600);
store.store(alice, 102, 700);

store.on_new_slot(101);

assert_eq!(store.max_execution_payment(&alice, 100), None);
assert_eq!(store.max_execution_payment(&alice, 101), Some(600));
assert_eq!(store.max_execution_payment(&alice, 102), Some(700));
assert_eq!(store.len(), 2);
}
}
4 changes: 4 additions & 0 deletions crates/relay/src/auctioneer/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use crate::{
bid_adjustor::BidAdjustor,
bid_sorter::BidSorter,
block_merger::BlockMerger,
builder_preferences::BuilderPreferencesStore,
types::{PayloadEntry, PendingPayload, SubmissionRef},
},
simulator::{SimRequest, tile::ValidationResult},
Expand Down Expand Up @@ -76,6 +77,7 @@ pub struct Context<B: BidAdjustor> {
pub failsafe_triggered: Arc<AtomicBool>,
pub alert_manager: Arc<AlertManager>,
pub operator_api: Option<Arc<OperatorPubSub>>,
pub builder_preferences: BuilderPreferencesStore,
}

const EXPECTED_PAYLOADS_PER_SLOT: usize = 5000;
Expand Down Expand Up @@ -146,6 +148,7 @@ impl<B: BidAdjustor> Context<B> {
failsafe_triggered,
alert_manager,
operator_api,
builder_preferences: BuilderPreferencesStore::default(),
}
}

Expand Down Expand Up @@ -257,6 +260,7 @@ impl<B: BidAdjustor> Context<B> {

self.block_merger.on_new_slot(bid_slot.as_u64());
self.bid_adjustor.on_new_slot(bid_slot.as_u64());
self.builder_preferences.on_new_slot(bid_slot.as_u64());
self.auctioneer_handle.clear_inflight_payloads();
self.decoded.clear();

Expand Down
24 changes: 23 additions & 1 deletion crates/relay/src/auctioneer/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ use tracing::trace;

use crate::{
api::proposer::{ProposerApiError, get_payload::ProposerApiVersion},
auctioneer::types::{Event, GetExecutionPayloadBidResult, GetHeaderResult, GetPayloadResult},
auctioneer::types::{
Event, GetExecutionPayloadBidResult, GetHeaderResult, GetPayloadResult,
SubmitBuilderPreferencesResult,
},
gossip::BroadcastPayloadParams,
};

Expand Down Expand Up @@ -85,6 +88,25 @@ impl AuctioneerHandle {
Ok(rx)
}

pub fn submit_builder_preferences(
&self,
proposer_pubkey: BlsPublicKeyBytes,
slot: u64,
max_execution_payment: u64,
) -> Result<oneshot::Receiver<SubmitBuilderPreferencesResult>, ChannelFull> {
let (tx, rx) = oneshot::channel();
trace!("sending to auctioneer");
self.auctioneer
.try_send(Event::SubmitBuilderPreferences {
proposer_pubkey,
slot,
max_execution_payment,
res_tx: tx,
})
.map_err(|_| ChannelFull)?;
Ok(rx)
}

pub fn get_payload(
&self,
chain_info: &ChainInfo,
Expand Down
23 changes: 23 additions & 0 deletions crates/relay/src/auctioneer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod bid_adjustor;
mod bid_sorter;
mod block_merger;
mod builder_preferences;
mod context;
mod get_execution_payload_bid;
mod get_header;
Expand Down Expand Up @@ -669,6 +670,28 @@ impl State {
) => {
ctx.handle_builder_demotion(slot, builder_pubkey, block_hash, reason, false);
}

// submit_builder_preferences (Gloas), valid regardless of state
(
State::Slot { .. } | State::Sorting(_) | State::Broadcasting { .. },
Event::SubmitBuilderPreferences {
proposer_pubkey,
slot,
max_execution_payment,
res_tx,
},
) => {
let bid_slot = self.bid_slot();
if slot < bid_slot {
let _ = res_tx.send(Err(ProposerApiError::RequestForPastSlot {
request_slot: slot.into(),
head_slot: bid_slot.into(),
}));
} else {
ctx.builder_preferences.store(proposer_pubkey, slot, max_execution_payment);
let _ = res_tx.send(Ok(()));
}
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions crates/relay/src/auctioneer/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub enum SubmissionRef {
pub type GetHeaderResult = Result<PayloadEntry, ProposerApiError>;
pub type GetPayloadResult = Result<GetPayloadResultData, ProposerApiError>;
pub type GetExecutionPayloadBidResult = Result<SignedExecutionPayloadBid, ProposerApiError>;
pub type SubmitBuilderPreferencesResult = Result<(), ProposerApiError>;

#[derive(Debug, Clone, Copy)]
#[repr(C)]
Expand Down Expand Up @@ -441,6 +442,14 @@ pub enum Event {
res_tx: oneshot::Sender<GetExecutionPayloadBidResult>,
span: tracing::Span,
},
/// Gloas (ePBS) `submitBuilderPreferences`: a proposer's per-builder `max_execution_payment`
/// for a future slot, valid regardless of the current auctioneer state.
SubmitBuilderPreferences {
proposer_pubkey: BlsPublicKeyBytes,
slot: u64,
max_execution_payment: u64,
res_tx: oneshot::Sender<SubmitBuilderPreferencesResult>,
},
// Receive multiple of these potentially, assume some light validation
GetPayload {
block_hash: B256,
Expand All @@ -467,6 +476,7 @@ impl Event {
Event::Submission { .. } => "Submission",
Event::GetHeader { .. } => "GetHeader",
Event::GetExecutionPayloadBid { .. } => "GetExecutionPayloadBid",
Event::SubmitBuilderPreferences { .. } => "SubmitBuilderPreferences",
Event::GetPayload { .. } => "GetPayload",
Event::GossipPayload(_) => "GossipPayload",
Event::SimResult(_) => "SimResult",
Expand Down
Loading