Fix json_strucutres for gloas variant and add tests

This commit is contained in:
Eitan Seri-Levi
2026-06-23 13:25:53 +03:00
parent 524c82a295
commit caabd0c771

View File

@@ -487,8 +487,7 @@ pub struct JsonExecutionRequests(pub Vec<String>);
impl<E: EthSpec> From<ExecutionRequests<E>> for JsonExecutionRequests { impl<E: EthSpec> From<ExecutionRequests<E>> for JsonExecutionRequests {
fn from(requests: ExecutionRequests<E>) -> Self { fn from(requests: ExecutionRequests<E>) -> Self {
// Each element is a `RequestType`-prefixed, SSZ-encoded request list (EIP-7685). // Each element is a `RequestType`-prefixed, SSZ-encoded request list.
// The Gloas variant additionally emits builder deposit/exit requests.
let result = requests let result = requests
.get_execution_requests_list() .get_execution_requests_list()
.into_iter() .into_iter()
@@ -498,123 +497,124 @@ impl<E: EthSpec> From<ExecutionRequests<E>> for JsonExecutionRequests {
} }
} }
impl<E: EthSpec> TryFrom<JsonExecutionRequests> for ExecutionRequests<E> { /// Parse an EIP-7685 `JsonExecutionRequests` list into its component request lists.
type Error = RequestsError; ///
/// Returns the deposit, withdrawal, consolidation, builder deposit and builder exit lists.
/// Builder lists are empty pre-gloas or post-gloas when no builder requests are present.
#[allow(clippy::type_complexity)]
fn parse_execution_requests<E: EthSpec>(
value: JsonExecutionRequests,
) -> Result<
(
DepositRequests<E>,
WithdrawalRequests<E>,
ConsolidationRequests<E>,
BuilderDepositRequests<E>,
BuilderExitRequests<E>,
),
RequestsError,
> {
let mut deposits = DepositRequests::<E>::default();
let mut withdrawals = WithdrawalRequests::<E>::default();
let mut consolidations = ConsolidationRequests::<E>::default();
let mut builder_deposits = BuilderDepositRequests::<E>::default();
let mut builder_exits = BuilderExitRequests::<E>::default();
let mut prev_prefix: Option<RequestType> = None;
for (i, request) in value.0.into_iter().enumerate() {
// hex string
let decoded_bytes = hex::decode(request.strip_prefix("0x").unwrap_or(&request))
.map_err(RequestsError::InvalidHex)?;
fn try_from(value: JsonExecutionRequests) -> Result<Self, Self::Error> { // The first byte of each element is the `request_type` and the remaining bytes are the `request_data`.
let mut deposits = DepositRequests::<E>::default(); // Elements with empty `request_data` **MUST** be excluded from the list.
let mut withdrawals = WithdrawalRequests::<E>::default(); let Some((prefix_byte, request_bytes)) = decoded_bytes.split_first() else {
let mut consolidations = ConsolidationRequests::<E>::default(); return Err(RequestsError::EmptyRequest(i));
let mut builder_deposits = BuilderDepositRequests::<E>::default(); };
let mut builder_exits = BuilderExitRequests::<E>::default(); if request_bytes.is_empty() {
// [New in Gloas:EIP8282] The presence of builder requests determines the variant: the return Err(RequestsError::EmptyRequest(i));
// EIP-7685 list is fork-agnostic, so we only know it is Gloas-shaped once a builder }
// request type appears. // Elements of the list **MUST** be ordered by `request_type` in ascending order
let mut has_builder_requests = false; let current_prefix =
let mut prev_prefix: Option<RequestType> = None; RequestType::from_u8(*prefix_byte).ok_or(RequestsError::InvalidPrefix(*prefix_byte))?;
for (i, request) in value.0.into_iter().enumerate() { if let Some(prev) = prev_prefix
// hex string && prev.to_u8() >= current_prefix.to_u8()
let decoded_bytes = hex::decode(request.strip_prefix("0x").unwrap_or(&request)) {
.map_err(RequestsError::InvalidHex)?; return Err(RequestsError::InvalidOrdering);
}
prev_prefix = Some(current_prefix);
// The first byte of each element is the `request_type` and the remaining bytes are the `request_data`. match current_prefix {
// Elements with empty `request_data` **MUST** be excluded from the list. RequestType::Deposit => {
let Some((prefix_byte, request_bytes)) = decoded_bytes.split_first() else { deposits = DepositRequests::<E>::from_ssz_bytes(request_bytes).map_err(|e| {
return Err(RequestsError::EmptyRequest(i)); RequestsError::DecodeError(format!(
}; "Failed to decode DepositRequest from EL: {:?}",
if request_bytes.is_empty() { e
return Err(RequestsError::EmptyRequest(i)); ))
})?;
} }
// Elements of the list **MUST** be ordered by `request_type` in ascending order RequestType::Withdrawal => {
let current_prefix = RequestType::from_u8(*prefix_byte) withdrawals =
.ok_or(RequestsError::InvalidPrefix(*prefix_byte))?; WithdrawalRequests::<E>::from_ssz_bytes(request_bytes).map_err(|e| {
if let Some(prev) = prev_prefix RequestsError::DecodeError(format!(
&& prev.to_u8() >= current_prefix.to_u8() "Failed to decode WithdrawalRequest from EL: {:?}",
{ e
return Err(RequestsError::InvalidOrdering); ))
})?;
} }
prev_prefix = Some(current_prefix); RequestType::Consolidation => {
consolidations = ConsolidationRequests::<E>::from_ssz_bytes(request_bytes)
match current_prefix { .map_err(|e| {
RequestType::Deposit => { RequestsError::DecodeError(format!(
deposits = "Failed to decode ConsolidationRequest from EL: {:?}",
DepositRequests::<E>::from_ssz_bytes(request_bytes).map_err(|e| { e
RequestsError::DecodeError(format!( ))
"Failed to decode DepositRequest from EL: {:?}", })?;
e }
)) RequestType::BuilderDeposit => {
})?; builder_deposits = BuilderDepositRequests::<E>::from_ssz_bytes(request_bytes)
} .map_err(|e| {
RequestType::Withdrawal => {
withdrawals =
WithdrawalRequests::<E>::from_ssz_bytes(request_bytes).map_err(|e| {
RequestsError::DecodeError(format!(
"Failed to decode WithdrawalRequest from EL: {:?}",
e
))
})?;
}
RequestType::Consolidation => {
consolidations = ConsolidationRequests::<E>::from_ssz_bytes(request_bytes)
.map_err(|e| {
RequestsError::DecodeError(format!(
"Failed to decode ConsolidationRequest from EL: {:?}",
e
))
})?;
}
RequestType::BuilderDeposit => {
builder_deposits = BuilderDepositRequests::<E>::from_ssz_bytes(request_bytes)
.map_err(|e| {
RequestsError::DecodeError(format!( RequestsError::DecodeError(format!(
"Failed to decode BuilderDepositRequest from EL: {:?}", "Failed to decode BuilderDepositRequest from EL: {:?}",
e e
)) ))
})?; })?;
has_builder_requests = true; }
} RequestType::BuilderExit => {
RequestType::BuilderExit => { builder_exits =
builder_exits = BuilderExitRequests::<E>::from_ssz_bytes(request_bytes) BuilderExitRequests::<E>::from_ssz_bytes(request_bytes).map_err(|e| {
.map_err(|e| { RequestsError::DecodeError(format!(
RequestsError::DecodeError(format!( "Failed to decode BuilderExitRequest from EL: {:?}",
"Failed to decode BuilderExitRequest from EL: {:?}", e
e ))
)) })?;
})?;
has_builder_requests = true;
}
} }
} }
// Without any builder requests the list is indistinguishable from a pre-Gloas one, so we
// produce the Electra-shaped variant. Consumers that require the Gloas variant lift it
// (carrying empty builder lists) at their boundary.
if has_builder_requests {
Ok(ExecutionRequests::Gloas(ExecutionRequestsGloas {
deposits,
withdrawals,
consolidations,
builder_deposits,
builder_exits,
}))
} else {
Ok(ExecutionRequests::Electra(ExecutionRequestsElectra {
deposits,
withdrawals,
consolidations,
}))
}
} }
Ok((
deposits,
withdrawals,
consolidations,
builder_deposits,
builder_exits,
))
} }
impl<E: EthSpec> TryFrom<JsonExecutionRequests> for ExecutionRequestsElectra<E> { impl<E: EthSpec> TryFrom<JsonExecutionRequests> for ExecutionRequestsElectra<E> {
type Error = RequestsError; type Error = RequestsError;
fn try_from(value: JsonExecutionRequests) -> Result<Self, Self::Error> { fn try_from(value: JsonExecutionRequests) -> Result<Self, Self::Error> {
match ExecutionRequests::<E>::try_from(value)? { let (deposits, withdrawals, consolidations, builder_deposits, builder_exits) =
ExecutionRequests::Electra(requests) => Ok(requests), parse_execution_requests::<E>(value)?;
ExecutionRequests::Gloas(_) => Err(RequestsError::VariantMismatch), // Builder requests are not valid pre-Gloas.
if !builder_deposits.is_empty() || !builder_exits.is_empty() {
return Err(RequestsError::VariantMismatch);
} }
Ok(ExecutionRequestsElectra {
deposits,
withdrawals,
consolidations,
})
} }
} }
@@ -622,10 +622,15 @@ impl<E: EthSpec> TryFrom<JsonExecutionRequests> for ExecutionRequestsGloas<E> {
type Error = RequestsError; type Error = RequestsError;
fn try_from(value: JsonExecutionRequests) -> Result<Self, Self::Error> { fn try_from(value: JsonExecutionRequests) -> Result<Self, Self::Error> {
match ExecutionRequests::<E>::try_from(value)? { let (deposits, withdrawals, consolidations, builder_deposits, builder_exits) =
ExecutionRequests::Gloas(requests) => Ok(requests), parse_execution_requests::<E>(value)?;
ExecutionRequests::Electra(_) => Err(RequestsError::VariantMismatch), Ok(ExecutionRequestsGloas {
} deposits,
withdrawals,
consolidations,
builder_deposits,
builder_exits,
})
} }
} }
@@ -1224,7 +1229,8 @@ mod tests {
use bls::{PublicKeyBytes, SignatureBytes}; use bls::{PublicKeyBytes, SignatureBytes};
use ssz::Encode; use ssz::Encode;
use types::{ use types::{
ConsolidationRequest, DepositRequest, MainnetEthSpec, RequestType, WithdrawalRequest, BuilderDepositRequest, BuilderExitRequest, ConsolidationRequest, DepositRequest,
MainnetEthSpec, RequestType, WithdrawalRequest,
}; };
use super::*; use super::*;
@@ -1269,7 +1275,7 @@ mod tests {
// First check a valid request with all requests // First check a valid request with all requests
assert!( assert!(
ExecutionRequests::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![ ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Deposit.to_u8(), &deposit_request), create_request_string(RequestType::Deposit.to_u8(), &deposit_request),
create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request),
create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request),
@@ -1279,21 +1285,21 @@ mod tests {
// Single requests // Single requests
assert!( assert!(
ExecutionRequests::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![ ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Deposit.to_u8(), &deposit_request), create_request_string(RequestType::Deposit.to_u8(), &deposit_request),
])) ]))
.is_ok() .is_ok()
); );
assert!( assert!(
ExecutionRequests::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![ ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request),
])) ]))
.is_ok() .is_ok()
); );
assert!( assert!(
ExecutionRequests::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![ ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request),
])) ]))
.is_ok() .is_ok()
@@ -1301,7 +1307,7 @@ mod tests {
// Out of order // Out of order
assert!(matches!( assert!(matches!(
ExecutionRequests::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![ ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request),
create_request_string(RequestType::Deposit.to_u8(), &deposit_request), create_request_string(RequestType::Deposit.to_u8(), &deposit_request),
])) ]))
@@ -1310,7 +1316,7 @@ mod tests {
)); ));
assert!(matches!( assert!(matches!(
ExecutionRequests::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![ ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request),
create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request),
])) ]))
@@ -1319,7 +1325,7 @@ mod tests {
)); ));
assert!(matches!( assert!(matches!(
ExecutionRequests::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![ ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request),
create_request_string(RequestType::Deposit.to_u8(), &deposit_request), create_request_string(RequestType::Deposit.to_u8(), &deposit_request),
])) ]))
@@ -1329,7 +1335,7 @@ mod tests {
// Multiple requests of same type // Multiple requests of same type
assert!(matches!( assert!(matches!(
ExecutionRequests::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![ ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Deposit.to_u8(), &deposit_request), create_request_string(RequestType::Deposit.to_u8(), &deposit_request),
create_request_string(RequestType::Deposit.to_u8(), &deposit_request), create_request_string(RequestType::Deposit.to_u8(), &deposit_request),
])) ]))
@@ -1339,7 +1345,7 @@ mod tests {
// Invalid prefix // Invalid prefix
assert!(matches!( assert!(matches!(
ExecutionRequests::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![ ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(42, &deposit_request), create_request_string(42, &deposit_request),
])) ]))
.unwrap_err(), .unwrap_err(),
@@ -1348,7 +1354,7 @@ mod tests {
// Prefix followed by no data // Prefix followed by no data
assert!(matches!( assert!(matches!(
ExecutionRequests::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![ ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Deposit.to_u8(), &deposit_request), create_request_string(RequestType::Deposit.to_u8(), &deposit_request),
create_request_string( create_request_string(
RequestType::Consolidation.to_u8(), RequestType::Consolidation.to_u8(),
@@ -1360,12 +1366,141 @@ mod tests {
)); ));
// Empty request // Empty request
assert!(matches!( assert!(matches!(
ExecutionRequests::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![ ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Deposit.to_u8(), &deposit_request), create_request_string(RequestType::Deposit.to_u8(), &deposit_request),
"0x".to_string() "0x".to_string()
])) ]))
.unwrap_err(), .unwrap_err(),
RequestsError::EmptyRequest(1) RequestsError::EmptyRequest(1)
)); ));
// Builder requests are not valid pre-gloas.
let builder_deposit_request = BuilderDepositRequest {
pubkey: PublicKeyBytes::empty(),
withdrawal_credentials: Hash256::random(),
amount: 32,
signature: SignatureBytes::empty(),
};
assert!(matches!(
ExecutionRequestsElectra::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(
RequestType::BuilderDeposit.to_u8(),
&builder_deposit_request
),
]))
.unwrap_err(),
RequestsError::VariantMismatch
));
}
#[test]
fn test_gloas_execution_requests() {
let deposit_request = DepositRequest {
pubkey: PublicKeyBytes::empty(),
withdrawal_credentials: Hash256::random(),
amount: 32,
signature: SignatureBytes::empty(),
index: 0,
};
let withdrawal_request = WithdrawalRequest {
amount: 32,
source_address: Address::random(),
validator_pubkey: PublicKeyBytes::empty(),
};
let consolidation_request = ConsolidationRequest {
source_address: Address::random(),
source_pubkey: PublicKeyBytes::empty(),
target_pubkey: PublicKeyBytes::empty(),
};
let builder_deposit_request = BuilderDepositRequest {
pubkey: PublicKeyBytes::empty(),
withdrawal_credentials: Hash256::random(),
amount: 32,
signature: SignatureBytes::empty(),
};
let builder_exit_request = BuilderExitRequest {
source_address: Address::random(),
pubkey: PublicKeyBytes::empty(),
};
// Valid request with all five request types, in ascending prefix order.
assert!(
ExecutionRequestsGloas::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Deposit.to_u8(), &deposit_request),
create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request),
create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request),
create_request_string(
RequestType::BuilderDeposit.to_u8(),
&builder_deposit_request
),
create_request_string(RequestType::BuilderExit.to_u8(), &builder_exit_request),
]))
.is_ok()
);
// A builder-less list is a valid Gloas value (builder lists are simply empty).
assert!(
ExecutionRequestsGloas::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::Deposit.to_u8(), &deposit_request),
]))
.is_ok()
);
// Only builder requests.
assert!(
ExecutionRequestsGloas::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(
RequestType::BuilderDeposit.to_u8(),
&builder_deposit_request
),
create_request_string(RequestType::BuilderExit.to_u8(), &builder_exit_request),
]))
.is_ok()
);
// Out of order: builder exit must come after a builder deposit.
assert!(matches!(
ExecutionRequestsGloas::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(RequestType::BuilderExit.to_u8(), &builder_exit_request),
create_request_string(
RequestType::BuilderDeposit.to_u8(),
&builder_deposit_request
),
]))
.unwrap_err(),
RequestsError::InvalidOrdering
));
// Duplicate builder request type.
assert!(matches!(
ExecutionRequestsGloas::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(
RequestType::BuilderDeposit.to_u8(),
&builder_deposit_request
),
create_request_string(
RequestType::BuilderDeposit.to_u8(),
&builder_deposit_request
),
]))
.unwrap_err(),
RequestsError::InvalidOrdering
));
// Empty builder request data.
assert!(matches!(
ExecutionRequestsGloas::<MainnetEthSpec>::try_from(JsonExecutionRequests(vec![
create_request_string(
RequestType::BuilderDeposit.to_u8(),
&Vec::<BuilderDepositRequest>::new()
),
]))
.unwrap_err(),
RequestsError::EmptyRequest(0)
));
} }
} }