@@ -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