Skip to content

Commit 37838ce

Browse files
committed
Validate execution payload bid
1 parent 93c5c38 commit 37838ce

File tree

4 files changed

+199
-8
lines changed

4 files changed

+199
-8
lines changed

fork_choice_store/src/error.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use types::{
77
bellatrix::containers::PowBlock,
88
combined::{Attestation, DataColumnSidecar, SignedAggregateAndProof, SignedBeaconBlock},
99
deneb::containers::BlobSidecar,
10-
gloas::containers::PayloadAttestationMessage,
10+
gloas::containers::{PayloadAttestationMessage, SignedExecutionPayloadBid},
1111
phase0::primitives::{Slot, SubnetId, ValidatorIndex},
1212
preset::{Mainnet, Preset},
1313
};
@@ -138,6 +138,26 @@ pub enum Error<P: Preset> {
138138
data_column_sidecar: Arc<DataColumnSidecar<P>>,
139139
computed: ValidatorIndex,
140140
},
141+
#[error("execution payload bid's builder is not active: {payload_bid:?}")]
142+
ExecutionPayloadBidBuilderInactive {
143+
payload_bid: Arc<SignedExecutionPayloadBid>,
144+
},
145+
#[error("execution payload bid's builder has been slashed: {payload_bid:?}")]
146+
ExecutionPayloadBidBuilderSlashed {
147+
payload_bid: Arc<SignedExecutionPayloadBid>,
148+
},
149+
#[error("execution payload bid's builder has invalid withdrawal credentials: {payload_bid:?}")]
150+
ExecutionPayloadBidBuilderInvalid {
151+
payload_bid: Arc<SignedExecutionPayloadBid>,
152+
},
153+
#[error("off-protocol payment is disallowed in gossip: {payload_bid:?}")]
154+
ExecutionPayloadBidOffProtocolPaymentDisallowed {
155+
payload_bid: Arc<SignedExecutionPayloadBid>,
156+
},
157+
#[error("execution payload bid has invalid signature: {payload_bid:?}")]
158+
InvalidExecutionPayloadBidSignature {
159+
payload_bid: Arc<SignedExecutionPayloadBid>,
160+
},
141161
#[error("aggregate and proof has invalid signature: {aggregate_and_proof:?}")]
142162
InvalidAggregateAndProofSignature {
143163
aggregate_and_proof: Arc<SignedAggregateAndProof<P>>,

fork_choice_store/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ pub use crate::{
8282
AttestationAction, AttestationItem, AttestationOrigin, AttestationValidationError,
8383
AttesterSlashingOrigin, BlobSidecarAction, BlobSidecarOrigin, BlockAction, BlockOrigin,
8484
ChainLink, DataAvailabilityPolicy, DataColumnSidecarAction, DataColumnSidecarOrigin,
85-
PartialBlockAction, PayloadAction, PayloadAttestationAction, PayloadAttestationOrigin,
86-
Storage, ValidAttestation,
85+
ExecutionPayloadBidAction, ExecutionPayloadBidOrigin, PartialBlockAction, PayloadAction,
86+
PayloadAttestationAction, PayloadAttestationOrigin, Storage, ValidAttestation,
8787
},
8888
segment::Segment,
8989
state_cache_processor::{Error as StateCacheError, StateCacheProcessor},

fork_choice_store/src/misc.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use types::{
2323
SignedBeaconBlock,
2424
},
2525
deneb::containers::BlobSidecar,
26-
gloas::containers::PayloadAttestationMessage,
26+
gloas::containers::{PayloadAttestationMessage, SignedExecutionPayloadBid},
2727
nonstandard::{PayloadStatus, Publishable, ValidationOutcome},
2828
phase0::{
2929
containers::{AttestationData, Checkpoint},
@@ -278,6 +278,75 @@ impl<I> AggregateAndProofOrigin<I> {
278278
}
279279
}
280280

281+
#[derive(Debug, AsRefStr)]
282+
pub enum ExecutionPayloadBidOrigin<I> {
283+
Gossip(I),
284+
Api(OneshotSender<Result<ValidationOutcome>>),
285+
}
286+
287+
impl Serialize for ExecutionPayloadBidOrigin<GossipId> {
288+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
289+
where
290+
S: Serializer,
291+
{
292+
serializer.serialize_str(self.as_ref())
293+
}
294+
}
295+
296+
impl<I> ExecutionPayloadBidOrigin<I> {
297+
#[must_use]
298+
pub fn split(self) -> (Option<I>, Option<OneshotSender<Result<ValidationOutcome>>>) {
299+
match self {
300+
Self::Gossip(gossip_id) => (Some(gossip_id), None),
301+
Self::Api(sender) => (None, Some(sender)),
302+
}
303+
}
304+
305+
#[must_use]
306+
pub fn gossip_id(self) -> Option<I> {
307+
match self {
308+
Self::Gossip(gossip_id) => Some(gossip_id),
309+
Self::Api(_) => None,
310+
}
311+
}
312+
313+
#[must_use]
314+
pub const fn gossip_id_ref(&self) -> Option<&I> {
315+
match self {
316+
Self::Gossip(gossip_id) => Some(gossip_id),
317+
Self::Api(_) => None,
318+
}
319+
}
320+
321+
#[must_use]
322+
pub const fn is_from_gossip(&self) -> bool {
323+
matches!(self, Self::Gossip(_))
324+
}
325+
326+
#[must_use]
327+
pub const fn verify_signatures(&self) -> bool {
328+
match self {
329+
Self::Gossip(_) | Self::Api(_) => true,
330+
}
331+
}
332+
333+
#[must_use]
334+
pub const fn send_to_validator(&self) -> bool {
335+
match self {
336+
Self::Gossip(_) | Self::Api(_) => true,
337+
}
338+
}
339+
340+
// TODO: use Debug instead
341+
#[must_use]
342+
pub const fn metrics_label(&self) -> &str {
343+
match self {
344+
Self::Gossip(_) => "Gossip",
345+
Self::Api(_) => "Api",
346+
}
347+
}
348+
}
349+
281350
#[derive(Debug)]
282351
pub struct AttestationItem<P: Preset, I> {
283352
pub item: Arc<Attestation<P>>,
@@ -803,6 +872,11 @@ pub enum PayloadAttestationAction {
803872
DelayUntilBlock(Arc<PayloadAttestationMessage>, H256),
804873
}
805874

875+
pub enum ExecutionPayloadBidAction {
876+
Accept(Arc<SignedExecutionPayloadBid>),
877+
Ignore(Publishable),
878+
}
879+
806880
pub enum PartialBlockAction {
807881
Accept,
808882
Ignore,

fork_choice_store/src/store.rs

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ use types::{
5555
},
5656
gloas::containers::{
5757
DataColumnSidecar as GloasDataColumnSidecar, ExecutionPayloadBid,
58-
PayloadAttestationMessage, SignedExecutionPayloadEnvelope,
58+
PayloadAttestationMessage, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope,
5959
},
6060
nonstandard::{BlobSidecarWithId, DataColumnSidecarWithId, PayloadStatus, Phase, WithStatus},
6161
phase0::{
@@ -87,7 +87,8 @@ use crate::{
8787
store_config::StoreConfig,
8888
supersets::MultiPhaseAggregateAndProofSets as AggregateAndProofSupersets,
8989
validations::validate_merge_block,
90-
AttestationOrigin, PayloadAttestationAction, PayloadAttestationOrigin,
90+
AttestationOrigin, ExecutionPayloadBidAction, ExecutionPayloadBidOrigin,
91+
PayloadAttestationAction, PayloadAttestationOrigin,
9192
};
9293

9394
/// [`Store`] from the Fork Choice specification.
@@ -233,7 +234,7 @@ pub struct Store<P: Preset, S: Storage<P>> {
233234
(Slot, H256, ColumnIndex),
234235
ContiguousList<KzgCommitment, P::MaxBlobCommitmentsPerBlock>,
235236
>,
236-
accepted_payload_bids: HashMap<(Slot, H256), ExecutionPayloadBid>,
237+
accepted_payload_bids: HashMap<(Slot, H256), HashMap<ValidatorIndex, ExecutionPayloadBid>>,
237238
blob_cache: BlobCache<P>,
238239
state_cache: Arc<StateCacheProcessor<P>>,
239240
storage: Arc<S>,
@@ -1278,6 +1279,98 @@ impl<P: Preset, S: Storage<P>> Store<P, S> {
12781279
Ok(BlockAction::Accept(chain_link, attester_slashing_results))
12791280
}
12801281

1282+
pub fn validate_execution_payload_bid<I>(
1283+
&self,
1284+
payload_bid: Arc<SignedExecutionPayloadBid>,
1285+
origin: &ExecutionPayloadBidOrigin<I>,
1286+
) -> Result<ExecutionPayloadBidAction> {
1287+
let bid = payload_bid.message;
1288+
let builder_index = bid.builder_index;
1289+
1290+
// > off-protocol payment is disallowed in gossip, the `bid.execution_payment` MUST be zero
1291+
if origin.is_from_gossip() {
1292+
ensure!(
1293+
bid.execution_payment == 0,
1294+
Error::<P>::ExecutionPayloadBidOffProtocolPaymentDisallowed { payload_bid }
1295+
);
1296+
}
1297+
1298+
// > the `bid.slot` is the current slot or the next slot
1299+
if bid.slot > self.slot() + 1 {
1300+
return Ok(ExecutionPayloadBidAction::Ignore(false));
1301+
}
1302+
1303+
// > the `bid.parent_block_hash` is the block hash of a known execution payload in fork choice
1304+
if !self
1305+
.execution_payload_locations
1306+
.contains_key(&bid.parent_block_hash)
1307+
{
1308+
return Ok(ExecutionPayloadBidAction::Ignore(true));
1309+
}
1310+
1311+
// > the `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice
1312+
let Some(state) = self.state_by_block_root(bid.parent_block_root) else {
1313+
return Ok(ExecutionPayloadBidAction::Ignore(true));
1314+
};
1315+
1316+
// > the builder is active, and non-slashed builder.
1317+
let current_epoch = accessors::get_current_epoch(&state);
1318+
let builder = state.validators().get(builder_index)?;
1319+
ensure!(
1320+
!builder.slashed,
1321+
Error::<P>::ExecutionPayloadBidBuilderSlashed { payload_bid }
1322+
);
1323+
ensure!(
1324+
predicates::is_active_validator(builder, current_epoch),
1325+
Error::<P>::ExecutionPayloadBidBuilderInactive { payload_bid }
1326+
);
1327+
1328+
// > the builder's withdrawal credentials' prefix is BUILDER_WITHDRAWAL_PREFIX
1329+
ensure!(
1330+
predicates::has_builder_withdrawal_credential(builder),
1331+
Error::<P>::ExecutionPayloadBidBuilderInvalid { payload_bid }
1332+
);
1333+
1334+
// > the `bid.value` is less or equal than the builder's excess balance
1335+
let builder_balance = *state.balances().get(builder_index)?;
1336+
if bid.value + P::MIN_ACTIVATION_BALANCE > builder_balance {
1337+
return Ok(ExecutionPayloadBidAction::Ignore(false));
1338+
}
1339+
1340+
if origin.verify_signatures() {
1341+
let pubkey = self.pubkey_cache.get_or_insert(builder.pubkey)?;
1342+
1343+
// > `signed_execution_payload_bid.signature` is valid builder's signature
1344+
if let Err(error) =
1345+
bid.verify(&self.chain_config, &state, payload_bid.signature, pubkey)
1346+
{
1347+
bail!(
1348+
error.context(Error::<P>::InvalidExecutionPayloadBidSignature { payload_bid })
1349+
);
1350+
}
1351+
}
1352+
1353+
if let Some(payload_bids) = self
1354+
.accepted_payload_bids
1355+
.get(&(bid.slot, bid.parent_block_root))
1356+
{
1357+
// > this is the first signed bid seen from the given builder for this slot
1358+
if payload_bids.contains_key(&builder_index) {
1359+
return Ok(ExecutionPayloadBidAction::Ignore(true));
1360+
}
1361+
1362+
// > this bid is the highest value bid seen for the corresponding slot and the given parent block hash.
1363+
if let Some(highest_bid) = payload_bids.values().max_by_key(|bid| bid.value) {
1364+
if bid.value <= highest_bid.value {
1365+
// This bid doesn't have a higher value than the existing bid
1366+
return Ok(ExecutionPayloadBidAction::Ignore(false));
1367+
}
1368+
}
1369+
}
1370+
1371+
Ok(ExecutionPayloadBidAction::Accept(payload_bid))
1372+
}
1373+
12811374
#[expect(clippy::too_many_lines)]
12821375
pub fn validate_aggregate_and_proof<I>(
12831376
&self,
@@ -2320,7 +2413,11 @@ impl<P: Preset, S: Storage<P>> Store<P, S> {
23202413
);
23212414

23222415
// [IGNORE] The sidecar's beacon_block_root has been seen via a valid signed execution payload bid.
2323-
let Some(payload_bid) = self.accepted_payload_bids.get(&(slot, block_root)) else {
2416+
let Some(payload_bid) = self
2417+
.accepted_payload_bids
2418+
.get(&(slot, block_root))
2419+
.and_then(|bids| bids.values().max_by_key(|bid| bid.value))
2420+
else {
23242421
return Ok(DataColumnSidecarAction::DelayUntilState(
23252422
data_column_sidecar,
23262423
block_root,

0 commit comments

Comments
 (0)