diff --git a/crates/builder/src/validation/server.rs b/crates/builder/src/validation/server.rs index 56fa0a0d1..808eaeb67 100644 --- a/crates/builder/src/validation/server.rs +++ b/crates/builder/src/validation/server.rs @@ -79,7 +79,7 @@ fn decode_submission( match params { Some(params) => { let mut buf = Vec::new(); - let (submission, _, _) = SubmissionDecoder::new(¶ms).decode(bytes, &mut buf)?; + let (submission, _, _, _) = SubmissionDecoder::new(¶ms).decode(bytes, &mut buf)?; match submission { Submission::Full(submission) => Ok(Some(submission.into())), Submission::Dehydrated(_) => Ok(None), diff --git a/crates/common/src/decoder.rs b/crates/common/src/decoder.rs index d545b04c9..59f12a548 100644 --- a/crates/common/src/decoder.rs +++ b/crates/common/src/decoder.rs @@ -7,11 +7,11 @@ use axum::response::{IntoResponse, Response}; use flate2::read::GzDecoder; use flux_profiler::timed; use helix_types::{ - BidAdjustmentData, BlockMergingData, Compression, DehydratedBidSubmission, - DehydratedBidSubmissionFuluWithAdjustments, + BidAdjustmentData, BlockAccessListBytes, BlockMergingData, Compression, + DehydratedBidSubmission, DehydratedBidSubmissionFuluWithAdjustments, DehydratedBidSubmissionFuluWithAdjustmentsAndMergingData, DehydratedBidSubmissionFuluWithMergingData, ForkName, ForkVersionDecode, MergeType, - SignedBidSubmission, SignedBidSubmissionWithAdjustments, + SignedBidSubmission, SignedBidSubmissionGloas, SignedBidSubmissionWithAdjustments, SignedBidSubmissionWithAdjustmentsAndMergingData, SignedBidSubmissionWithMergingData, Submission, }; @@ -38,6 +38,11 @@ use crate::{ }, }; +/// What one submission decodes into: the submission itself plus the sidecars +/// only some forks and headers carry. +pub type DecodedParts = + (Submission, Option, Option, Option); + #[derive(Debug, thiserror::Error)] pub enum DecoderError { #[error("json decode error: {0}")] @@ -51,6 +56,9 @@ pub enum DecoderError { #[error("failed to decode payload")] PayloadDecode, + + #[error("unsupported combination: {0}")] + UnsupportedCombination(&'static str), } impl IntoResponse for DecoderError { @@ -77,7 +85,8 @@ impl DecoderError { DecoderError::JsonDecodeError(_) | DecoderError::SszDecode(_) | DecoderError::IOError(_) | - DecoderError::PayloadDecode => StatusCode::BAD_REQUEST, + DecoderError::PayloadDecode | + DecoderError::UnsupportedCombination(_) => StatusCode::BAD_REQUEST, } } } @@ -259,8 +268,7 @@ impl SubmissionDecoder { &mut self, payload: &[u8], buf: &mut Vec, - ) -> Result<(Submission, Option, Option), DecoderError> - { + ) -> Result { let body: &[u8] = match self.decompress(payload, buf) { None => payload, Some(Ok(())) => buf, @@ -277,11 +285,7 @@ impl SubmissionDecoder { } #[timed] - fn decode_dehydrated( - &mut self, - body: &[u8], - ) -> Result<(Submission, Option, Option), DecoderError> - { + fn decode_dehydrated(&mut self, body: &[u8]) -> Result { if self.merge_type == MergeType::Mergeable { if self.with_adjustments { let sub: DehydratedBidSubmissionFuluWithAdjustmentsAndMergingData = @@ -292,6 +296,7 @@ impl SubmissionDecoder { Submission::Dehydrated(submission), Some(merging_data), Some(adjustment_data), + None, )); } @@ -299,7 +304,7 @@ impl SubmissionDecoder { self.decode_by_fork(body, self.fork_name)?; let (submission, merging_data) = sub_with_merging.split(); - return Ok((Submission::Dehydrated(submission), Some(merging_data), None)); + return Ok((Submission::Dehydrated(submission), Some(merging_data), None, None)); } let (submission, bid_adjustment) = if self.with_adjustments { @@ -332,15 +337,11 @@ impl SubmissionDecoder { MergeType::Pause => None, }; - Ok((Submission::Dehydrated(submission), merging_data, bid_adjustment)) + Ok((Submission::Dehydrated(submission), merging_data, bid_adjustment, None)) } #[timed] - fn decode_merge( - &mut self, - body: &[u8], - ) -> Result<(Submission, Option, Option), DecoderError> - { + fn decode_merge(&mut self, body: &[u8]) -> Result { let (submission, merging_data, bid_adjustment) = if self.with_adjustments { let sub: SignedBidSubmissionWithAdjustmentsAndMergingData = self._decode(body)?; let (submission, adjustment_data, merging_data) = sub.split(); @@ -366,24 +367,33 @@ impl SubmissionDecoder { MergeType::None => Some(merging_data), MergeType::Pause => None, }; - Ok((Submission::Full(submission), merging_data, bid_adjustment)) + // Gloas merging data comes with the merge builder's own step. + Ok((Submission::Full(submission), merging_data, bid_adjustment, None)) } #[timed] - fn decode_default( - &mut self, - body: &[u8], - ) -> Result<(Submission, Option, Option), DecoderError> - { - let (submission, bid_adjustment) = if self.with_adjustments { + fn decode_default(&mut self, body: &[u8]) -> Result { + let is_gloas = self.fork_name == ForkName::Gloas; + let (submission, bid_adjustment, block_access_list) = if self.with_adjustments { + if is_gloas { + // Refused rather than decoded into the wrong shape. Adjustments + // are a BuilderNet feature and Gloas does not need them yet. + return Err(DecoderError::UnsupportedCombination("Gloas with bid adjustments")); + } let sub_with_adjustment: SignedBidSubmissionWithAdjustments = self._decode(body)?; let (sub, adjustment_data) = sub_with_adjustment.split(); - (sub, Some(adjustment_data)) + (sub, Some(adjustment_data), None) + } else if is_gloas { + // Gloas carries the builder's EIP-7928 block access list. + let gloas: SignedBidSubmissionGloas = self._decode(body)?; + let (submission, block_access_list) = gloas.split(); + + (submission, None, Some(block_access_list)) } else { let submission: SignedBidSubmission = self._decode(body)?; - (submission, None) + (submission, None, None) }; let merging_data = match self.merge_type { @@ -407,7 +417,8 @@ impl SubmissionDecoder { } MergeType::Pause => None, }; - Ok((Submission::Full(submission), merging_data, bid_adjustment)) + // Gloas merging data comes with the merge builder's own step. + Ok((Submission::Full(submission), merging_data, bid_adjustment, block_access_list)) } // TODO: pass a buffer pool to avoid allocations @@ -513,7 +524,7 @@ mod tests { BidAdjData, BidAdjustmentDataV1, BlobsBundle, BundleOrder, DehydratedBidSubmission, DehydratedBidSubmissionFuluWithAdjustmentsAndMergingData, DehydratedBidSubmissionFuluWithMergingData, MergeType, Order, - SignedBidSubmissionWithAdjustmentsAndMergingData, TestRandom, + SignedBidSubmissionWithAdjustmentsAndMergingData, TestRandom, TestRandomSeed, }; use ssz::Encode; @@ -548,6 +559,73 @@ mod tests { assert_eq!(MergeType::AppendOnly.as_ref(), "append_only"); } + /// Plain SSZ params for `fork`, nothing else enabled. + fn plain_params(fork: ForkName) -> SubmissionDecoderParams { + SubmissionDecoderParams { + compression: Compression::None, + encoding: Encoding::Ssz, + merge_type: MergeType::None, + is_dehydrated: false, + with_mergeable_data: false, + with_adjustments: false, + mark_all_txs_mergeable: false, + fork_name: fork, + } + } + + #[test] + fn the_decoder_selects_the_gloas_shape_by_fork() { + let mut submission = SignedBidSubmissionGloas::test_random(); + submission.blobs_bundle = Default::default(); + submission.block_access_list = BlockAccessListBytes(vec![3u8; 32].into()); + let body = submission.as_ssz_bytes(); + + let params = plain_params(ForkName::Gloas); + let mut buf = Vec::new(); + let (_, _, _, block_access_list) = SubmissionDecoder::new(¶ms) + .decode(&body, &mut buf) + .expect("a Gloas submission must decode"); + + assert_eq!(block_access_list.expect("Gloas carries a block access list").to_vec(), vec![ + 3u8; + 32 + ],); + } + + #[test] + fn the_decoder_keeps_the_fulu_shape_for_fulu() { + let mut submission = SignedBidSubmission::test_random(); + submission.blobs_bundle = Default::default(); + let body = submission.as_ssz_bytes(); + + let params = plain_params(ForkName::Fulu); + let mut buf = Vec::new(); + let (_, _, _, block_access_list) = SubmissionDecoder::new(¶ms) + .decode(&body, &mut buf) + .expect("the Fulu shape must be unchanged"); + + assert!(block_access_list.is_none(), "only Gloas carries one"); + } + + #[test] + fn gloas_with_adjustments_is_refused() { + let mut submission = SignedBidSubmissionGloas::test_random(); + submission.blobs_bundle = Default::default(); + let body = submission.as_ssz_bytes(); + + let mut params = plain_params(ForkName::Gloas); + params.with_adjustments = true; + let mut buf = Vec::new(); + let err = SubmissionDecoder::new(¶ms) + .decode(&body, &mut buf) + .expect_err("the combination has no wire shape"); + + assert!( + matches!(err, DecoderError::UnsupportedCombination(_)), + "refused explicitly, not decoded into the wrong shape: {err}", + ); + } + #[test] fn test_merge_type_deserialization() { assert_eq!("mergeable".parse::().unwrap(), MergeType::Mergeable); @@ -592,7 +670,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, merging_data, bid_adjustment_data) = + let (decoded_submission, merging_data, bid_adjustment_data, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Dehydrated(_))); @@ -624,7 +702,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, merging_data, bid_adjustment_data) = + let (decoded_submission, merging_data, bid_adjustment_data, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Full(_))); @@ -653,7 +731,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, merging_data, bid_adjustment_data) = + let (decoded_submission, merging_data, bid_adjustment_data, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Dehydrated(_))); @@ -696,7 +774,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, merging_data, bid_adjustment) = + let (decoded_submission, merging_data, bid_adjustment, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Full(_))); @@ -725,7 +803,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, decoded_merging_data, bid_adjustment) = + let (decoded_submission, decoded_merging_data, bid_adjustment, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Full(_))); @@ -771,7 +849,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, decoded_merging_data, _) = + let (decoded_submission, decoded_merging_data, _, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Full(_))); @@ -812,7 +890,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, merging_data, _) = + let (decoded_submission, merging_data, _, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Full(_))); @@ -854,7 +932,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, merging_data, bid_adjustment) = + let (decoded_submission, merging_data, bid_adjustment, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Dehydrated(_))); @@ -880,7 +958,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, merging_data, bid_adjustment) = + let (decoded_submission, merging_data, bid_adjustment, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Full(_))); @@ -907,7 +985,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, merging_data, bid_adjustment) = + let (decoded_submission, merging_data, bid_adjustment, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Dehydrated(_))); @@ -935,7 +1013,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, merging_data, bid_adjustment_data) = + let (decoded_submission, merging_data, bid_adjustment_data, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Dehydrated(_))); @@ -969,7 +1047,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, merging_data, bid_adjustment_data) = + let (decoded_submission, merging_data, bid_adjustment_data, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); match decoded_submission { @@ -1023,7 +1101,7 @@ mod tests { }; let mut decoder = SubmissionDecoder::new(¶ms); let mut buf = Vec::new(); - let (decoded_submission, merging_data, bid_adjustment_data) = + let (decoded_submission, merging_data, bid_adjustment_data, _) = decoder.decode(&body, &mut buf).expect("decode should succeed"); assert!(matches!(decoded_submission, Submission::Full(_))); diff --git a/crates/relay/src/auctioneer/get_execution_payload_bid.rs b/crates/relay/src/auctioneer/get_execution_payload_bid.rs index a47d501e4..edbca72cd 100644 --- a/crates/relay/src/auctioneer/get_execution_payload_bid.rs +++ b/crates/relay/src/auctioneer/get_execution_payload_bid.rs @@ -72,7 +72,7 @@ pub(super) fn build_signed_bid( ) -> Result { let slot = Slot::new(params.slot); - let payload = entry.execution_payload().to_lighthouse_gloas_payload(slot).map_err(|err| { + let payload = entry.execution_payload().to_lighthouse_gloas_payload(slot, &entry.block_access_list()).map_err(|err| { warn!(%err, block_hash =% entry.block_hash(), "failed to convert held payload to Gloas shape for bid"); ProposerApiError::InternalServerError })?; @@ -228,6 +228,51 @@ mod tests { GloasBuilderIdentity { builder_index, keypair: helix_types::BlsKeypair::random() } } + /// A submission entry carrying a block access list, as Gloas requires. + fn gloas_submission_entry(block_access_list: Vec) -> PayloadEntry { + use helix_types::{BlockAccessListBytes, SignedBidSubmission, TestRandomSeed}; + + let mut submission = SignedBidSubmission::test_random(); + submission.blobs_bundle = Default::default(); + + PayloadEntry::new_submission( + submission, + B256::ZERO, + None, + None, + Some(BlockAccessListBytes(block_access_list.into())), + helix_types::SubmissionVersion::new(0, None), + Default::default(), + None, + ) + } + + #[test] + fn the_stored_payload_keeps_the_block_access_list_for_the_bid() { + let entry = gloas_submission_entry(vec![5u8; 96]); + + let bal = entry.block_access_list(); + + assert_eq!(bal.to_vec(), vec![5u8; 96], "the bid's payload would be invalid without it"); + + // And it reaches the converted Gloas payload. + let payload = entry + .execution_payload() + .to_lighthouse_gloas_payload(Slot::new(1), &bal) + .expect("conversion must succeed"); + assert_eq!(payload.block_access_list.len(), 96); + } + + #[test] + fn a_gossip_entry_has_no_block_access_list() { + let entry = payload_entry(B256::repeat_byte(0x11), 1); + + assert!( + entry.block_access_list().is_empty(), + "a gossiped payload carries none, so Gloas cannot be served from one", + ); + } + #[test] fn build_signed_bid_uses_the_entrys_data_and_configured_identity() { let chain_info = ChainInfo::default(); diff --git a/crates/relay/src/auctioneer/gloas_payload.rs b/crates/relay/src/auctioneer/gloas_payload.rs index 91f6867ff..92a5ee7ec 100644 --- a/crates/relay/src/auctioneer/gloas_payload.rs +++ b/crates/relay/src/auctioneer/gloas_payload.rs @@ -18,7 +18,10 @@ impl Context { res_tx: oneshot::Sender>, ) { let held = self.payloads.get(&block_hash).and_then(|entry| { - let payload = match entry.execution_payload().to_lighthouse_gloas_payload(slot) { + let payload = match entry + .execution_payload() + .to_lighthouse_gloas_payload(slot, &entry.block_access_list()) + { Ok(payload) => payload, Err(err) => { warn!(%block_hash, %err, "failed to convert held payload to Gloas shape"); diff --git a/crates/relay/src/auctioneer/submit_block.rs b/crates/relay/src/auctioneer/submit_block.rs index ba0de7338..102fb192c 100644 --- a/crates/relay/src/auctioneer/submit_block.rs +++ b/crates/relay/src/auctioneer/submit_block.rs @@ -120,6 +120,7 @@ impl Context { payload_attributes.withdrawals_root, maybe_tx_root, submission_data.bid_adjustment_data, + submission_data.block_access_list, submission_data.version, submission_data.trace, payload_attributes.parent_beacon_block_root, diff --git a/crates/relay/src/auctioneer/types.rs b/crates/relay/src/auctioneer/types.rs index e69bae17b..416735cdf 100644 --- a/crates/relay/src/auctioneer/types.rs +++ b/crates/relay/src/auctioneer/types.rs @@ -13,10 +13,11 @@ use helix_common::{ }; use helix_tcp_types::{BidSubmissionFlags, BidSubmissionHeader}; use helix_types::{ - BidAdjustmentData, BlockMergingData, BlsPublicKeyBytes, BuilderBid, Compression, - ExecutionPayload, ForkName, GetPayloadResponse, MergeType, PayloadAndBlobs, PayloadBidData, - PayloadBidDataRef, SignedBidSubmission, SignedBlindedBeaconBlock, SignedExecutionPayloadBid, - Slot, Submission, SubmissionVersion, VersionedSignedProposal, mock_public_key_bytes, + BidAdjustmentData, BlockAccessListBytes, BlockMergingData, BlsPublicKeyBytes, BuilderBid, + Compression, ExecutionPayload, ForkName, GetPayloadResponse, MergeType, PayloadAndBlobs, + PayloadBidData, PayloadBidDataRef, SignedBidSubmission, SignedBlindedBeaconBlock, + SignedExecutionPayloadBid, Slot, Submission, SubmissionVersion, VersionedSignedProposal, + mock_public_key_bytes, }; use http::{ HeaderMap, HeaderValue, @@ -219,6 +220,7 @@ pub struct SubmissionData { pub submission: Submission, pub merging_data: Option, pub bid_adjustment_data: Option, + pub block_access_list: Option, pub version: SubmissionVersion, pub withdrawals_root: B256, pub trace: SubmissionTrace, @@ -246,6 +248,8 @@ pub struct SubmissionPayload { pub withdrawals_root: B256, pub tx_root: Option, pub bid_adjustment_data: Option, + /// The builder's EIP-7928 list, present only for Gloas submissions. + pub block_access_list: Option, pub is_adjusted: bool, pub submission_version: SubmissionVersion, pub submission_trace: SubmissionTrace, @@ -260,16 +264,19 @@ pub enum PayloadEntry { } impl PayloadEntry { + #[allow(clippy::too_many_arguments)] pub fn new_submission( signed_bid_submission: SignedBidSubmission, withdrawals_root: B256, tx_root: Option, bid_adjustment_data: Option, + block_access_list: Option, submission_version: SubmissionVersion, submission_trace: SubmissionTrace, parent_beacon_block_root: Option, ) -> Self { Self::Submission(SubmissionPayload { + block_access_list, signed_bid_submission, withdrawals_root, tx_root, @@ -334,6 +341,16 @@ impl PayloadEntry { } } + /// The submitted EIP-7928 list. Empty when the fork does not carry one, in + /// which case a Gloas conversion would produce an invalid payload -- the + /// caller is expected to only reach this on a Gloas submission. + pub fn block_access_list(&self) -> BlockAccessListBytes { + match self { + Self::Submission(bid) => bid.block_access_list.clone().unwrap_or_default(), + Self::Gossip(_) => BlockAccessListBytes::default(), + } + } + pub fn execution_payload_make_mut(&mut self) -> &mut ExecutionPayload { match self { Self::Submission(bid) => bid.signed_bid_submission.execution_payload_make_mut(), diff --git a/crates/relay/src/bid_decoder/tile.rs b/crates/relay/src/bid_decoder/tile.rs index 698dce8c2..35d10e9fc 100644 --- a/crates/relay/src/bid_decoder/tile.rs +++ b/crates/relay/src/bid_decoder/tile.rs @@ -18,8 +18,8 @@ use helix_common::{ record_submission_step, }; use helix_types::{ - BidAdjustmentData, BlockMergingData, BlsPublicKeyBytes, MergeType, SignedBidSubmission, - Submission, SubmissionVersion, + BidAdjustmentData, BlockAccessListBytes, BlockMergingData, BlsPublicKeyBytes, MergeType, + SignedBidSubmission, Submission, SubmissionVersion, }; use rustc_hash::FxHashMap; use tracing::{info, trace}; @@ -271,6 +271,7 @@ impl DecoderTile { version, merging_data, bid_adjustment_data, + block_access_list, decoder_params, ) = Self::try_handle_block_submission( cache, @@ -315,6 +316,7 @@ impl DecoderTile { version, merging_data, bid_adjustment_data, + block_access_list, withdrawals_root, trace, decoder_params, @@ -342,6 +344,7 @@ impl DecoderTile { SubmissionVersion, Option, Option, + Option, SubmissionDecoderParams, ), BuilderApiError, @@ -362,7 +365,7 @@ impl DecoderTile { }; let mut decoder = SubmissionDecoder::new(&decoder_params); - let (mut submission, merging_data, bid_adjustment_data) = + let (mut submission, merging_data, bid_adjustment_data, block_access_list) = decoder.decode(payload, buffer)?; trace.decoded_ns = Nanos::now(); @@ -410,6 +413,7 @@ impl DecoderTile { version, merging_data, bid_adjustment_data, + block_access_list, decoder_params, )) } diff --git a/crates/relay/src/block_merging/tile.rs b/crates/relay/src/block_merging/tile.rs index d89a00632..579bcf626 100644 --- a/crates/relay/src/block_merging/tile.rs +++ b/crates/relay/src/block_merging/tile.rs @@ -1416,6 +1416,7 @@ mod tests { // this submission carries no blobs so it isn't touched. signed.blobs_bundle = Arc::new(Default::default()); let submission_data = SubmissionData { + block_access_list: None, submission_ref: SubmissionRef::Internal, submission: Submission::Full(signed), merging_data: Some(BlockMergingData { diff --git a/crates/simulator/src/ssz_server.rs b/crates/simulator/src/ssz_server.rs index 77d3ef4c9..d46b8edd7 100644 --- a/crates/simulator/src/ssz_server.rs +++ b/crates/simulator/src/ssz_server.rs @@ -48,7 +48,7 @@ fn decode_submission( Some(decode_params) => { let mut buf = vec![]; let mut decoder = SubmissionDecoder::new(&decode_params); - let (submission, _, _) = decoder.decode(signed_bid_submission, &mut buf)?; + let (submission, _, _, _) = decoder.decode(signed_bid_submission, &mut buf)?; match submission { Submission::Full(s) => Ok(s.into()), Submission::Dehydrated(_) => { diff --git a/crates/types/src/bid_submission.rs b/crates/types/src/bid_submission.rs index f0383aa68..5d422b863 100644 --- a/crates/types/src/bid_submission.rs +++ b/crates/types/src/bid_submission.rs @@ -20,7 +20,7 @@ use crate::{ PayloadAndBlobs, SszError, TestRandom, bid_adjustment_data::{BidAdjData, BidAdjustmentData, BidAdjustmentDataV1}, error::SigError, - fields::ExecutionRequests, + fields::{BlockAccessListBytes, ExecutionRequests}, }; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, TreeHash)] @@ -736,6 +736,50 @@ impl SignedBidSubmissionWithAdjustments { } } +/// Gloas carries the block access list the builder produced (EIP-7928): +/// core fields ++ block_access_list. +/// +/// A separate type rather than a fork-gated field, because `Encode` is derived +/// on [`SignedBidSubmission`] and an extra field would change the bytes for +/// every fork. +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] +pub struct SignedBidSubmissionGloas { + pub message: BidTrace, + pub execution_payload: Arc, + pub blobs_bundle: Arc, + pub execution_requests: Arc, + pub signature: BlsSignatureBytes, + pub block_access_list: BlockAccessListBytes, +} + +impl TestRandom for SignedBidSubmissionGloas { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { + Self { + message: BidTrace::random_for_test(rng), + execution_payload: ExecutionPayload::random_for_test(rng).into(), + blobs_bundle: BlobsBundle::random_for_test(rng).into(), + execution_requests: ExecutionRequests::random_for_test(rng).into(), + signature: BlsSignatureBytes::random(), + block_access_list: BlockAccessListBytes::random_for_test(rng), + } + } +} + +impl SignedBidSubmissionGloas { + pub fn split(self) -> (SignedBidSubmission, BlockAccessListBytes) { + ( + SignedBidSubmission { + message: self.message, + execution_payload: self.execution_payload, + blobs_bundle: self.blobs_bundle, + execution_requests: self.execution_requests, + signature: self.signature, + }, + self.block_access_list, + ) + } +} + /// Flat combination of [`SignedBidSubmissionWithAdjustments`] and merging data: /// core fields ++ bid_adjustment_data ++ merging_data. #[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] @@ -936,3 +980,66 @@ mod tests { assert!(bytes.ends_with(&tail)); } } + +#[cfg(test)] +mod gloas_submission_tests { + use ssz::{Decode, Encode}; + + use super::*; + use crate::TestRandomSeed; + + /// `BlobsBundle::random_for_test` makes one proof per blob while `Decode` + /// demands 128, so a random bundle cannot round-trip. These tests are about + /// the block access list, so they use an empty one. + fn decodable_gloas_submission() -> SignedBidSubmissionGloas { + let mut submission = SignedBidSubmissionGloas::test_random(); + submission.blobs_bundle = Default::default(); + submission + } + + #[test] + fn a_gloas_submission_round_trips_through_ssz() { + let submission = decodable_gloas_submission(); + + let bytes = submission.as_ssz_bytes(); + let decoded = SignedBidSubmissionGloas::from_ssz_bytes(&bytes).unwrap(); + + assert_eq!(decoded.message, submission.message); + assert_eq!(decoded.block_access_list, submission.block_access_list); + assert_eq!(bytes, decoded.as_ssz_bytes()); + } + + #[test] + fn splitting_a_gloas_submission_yields_the_base_and_the_bal() { + let submission = decodable_gloas_submission(); + let expected_bal = submission.block_access_list.clone(); + let expected_hash = submission.message.block_hash; + + let (base, bal) = submission.split(); + + assert_eq!(bal, expected_bal); + assert_eq!(base.message.block_hash, expected_hash); + } + + #[test] + fn the_gloas_shape_is_distinct_from_fulu() { + // Neither decodes as the other, so no existing Fulu path can silently + // accept a Gloas submission or vice versa. + let mut gloas = decodable_gloas_submission(); + gloas.block_access_list = BlockAccessListBytes(vec![7u8; 64].into()); + + assert!( + SignedBidSubmission::from_ssz_bytes(&gloas.as_ssz_bytes()).is_err(), + "a Gloas submission must not decode as Fulu", + ); + assert!( + SignedBidSubmissionGloas::from_ssz_bytes(&{ + let mut fulu = SignedBidSubmission::test_random(); + fulu.blobs_bundle = Default::default(); + fulu.as_ssz_bytes() + }) + .is_err(), + "a Fulu submission must not decode as Gloas", + ); + } +} diff --git a/crates/types/src/execution_payload.rs b/crates/types/src/execution_payload.rs index fe2968593..782b1d879 100644 --- a/crates/types/src/execution_payload.rs +++ b/crates/types/src/execution_payload.rs @@ -157,11 +157,13 @@ impl ExecutionPayload { } /// Converts to the real, progressive-list Gloas execution payload shape used on-chain. - /// `block_access_list` is left empty -- TODO(gloas): populate once EIP-7928 block-access-list - /// tracking exists. + /// + /// `block_access_list` is the opaque EIP-7928 list the builder submitted: only + /// an execution client can produce it, so it travels on the submission. pub fn to_lighthouse_gloas_payload( &self, slot: lh_types::Slot, + block_access_list: &crate::fields::BlockAccessListBytes, ) -> Result, SszError> { Ok(lh_types::ExecutionPayloadGloas { parent_hash: self.parent_hash.into(), @@ -181,7 +183,7 @@ impl ExecutionPayload { withdrawals: self.withdrawals.iter().cloned().collect(), blob_gas_used: self.blob_gas_used, excess_blob_gas: self.excess_blob_gas, - block_access_list: Default::default(), + block_access_list: block_access_list.iter().copied().collect(), slot_number: slot, }) } @@ -370,12 +372,39 @@ mod tests { assert_eq!(our_payload.tree_hash_root(), decoded.tree_hash_root()); } + #[test] + fn the_gloas_payload_carries_the_submitted_block_access_list() { + let payload = ExecutionPayload::test_random(); + let bal = crate::fields::BlockAccessListBytes(vec![9u8; 128].into()); + + let gloas = payload.to_lighthouse_gloas_payload(lh_types::Slot::new(1), &bal).unwrap(); + + // Before this it was always empty, which made the envelope invalid. + assert_eq!(gloas.block_access_list.len(), 128); + assert_eq!(gloas.block_access_list.to_vec(), bal.to_vec()); + } + + #[test] + fn an_empty_block_access_list_still_converts() { + let payload = ExecutionPayload::test_random(); + + let gloas = payload + .to_lighthouse_gloas_payload( + lh_types::Slot::new(1), + &crate::fields::BlockAccessListBytes::default(), + ) + .unwrap(); + + assert!(gloas.block_access_list.is_empty()); + } + #[test] fn to_lighthouse_gloas_payload_preserves_fields() { let our_payload = ExecutionPayload::test_random(); let slot = lh_types::Slot::new(42); - let gloas = our_payload.to_lighthouse_gloas_payload(slot).unwrap(); + let bal = crate::fields::BlockAccessListBytes(vec![1u8, 2, 3].into()); + let gloas = our_payload.to_lighthouse_gloas_payload(slot, &bal).unwrap(); assert_eq!(gloas.parent_hash.0, our_payload.parent_hash); assert_eq!(gloas.block_hash.0, our_payload.block_hash); @@ -391,7 +420,6 @@ mod tests { assert_eq!(gloas.blob_gas_used, our_payload.blob_gas_used); assert_eq!(gloas.excess_blob_gas, our_payload.excess_blob_gas); assert_eq!(gloas.slot_number, slot); - assert!(gloas.block_access_list.is_empty()); assert_eq!(gloas.transactions.len(), our_payload.transactions.len()); for (converted, original) in diff --git a/crates/types/src/fields.rs b/crates/types/src/fields.rs index 90bc481ba..058938ba8 100644 --- a/crates/types/src/fields.rs +++ b/crates/types/src/fields.rs @@ -85,6 +85,23 @@ ssz_bytes_wrapper! { max = ::MaxBytesPerTransaction; } +ssz_bytes_wrapper! { + /// The opaque encoded EIP-7928 block access list, as the builder produced + /// it. Gloas's own `BlockAccessList` is a `ProgressiveVariableList`, + /// so nothing here mirrors its structure. + pub struct BlockAccessListBytes; + max = ::MaxBytesPerTransaction; +} + +impl TestRandom for BlockAccessListBytes { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { + let n = rng.random_range(0..=1000) as usize; + let mut bytes = vec![0u8; n]; + rng.fill_bytes(&mut bytes); + Self(bytes.into()) + } +} + impl TestRandom for Transaction { fn random_for_test(rng: &mut impl rand::RngCore) -> Self { let n = rng.random_range(0..=1000) as usize;