Use Local Payload if More Profitable than Builder (#3934)
* Use Local Payload if More Profitable than Builder * Rename clone -> clone_from_ref * Minimize Clones of GetPayloadResponse * Cleanup & Fix Tests * Added Tests for Payload Choice by Profit * Fix Outdated Comments
This commit is contained in:
parent
4d0955cd0b
commit
90b6ae62e6
@ -426,6 +426,7 @@ where
|
|||||||
DEFAULT_TERMINAL_BLOCK,
|
DEFAULT_TERMINAL_BLOCK,
|
||||||
shanghai_time,
|
shanghai_time,
|
||||||
eip4844_time,
|
eip4844_time,
|
||||||
|
None,
|
||||||
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
|
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
|
||||||
spec,
|
spec,
|
||||||
None,
|
None,
|
||||||
@ -435,7 +436,11 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mock_execution_layer_with_builder(mut self, beacon_url: SensitiveUrl) -> Self {
|
pub fn mock_execution_layer_with_builder(
|
||||||
|
mut self,
|
||||||
|
beacon_url: SensitiveUrl,
|
||||||
|
builder_threshold: Option<u128>,
|
||||||
|
) -> Self {
|
||||||
// Get a random unused port
|
// Get a random unused port
|
||||||
let port = unused_port::unused_tcp_port().unwrap();
|
let port = unused_port::unused_tcp_port().unwrap();
|
||||||
let builder_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap();
|
let builder_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap();
|
||||||
@ -452,6 +457,7 @@ where
|
|||||||
DEFAULT_TERMINAL_BLOCK,
|
DEFAULT_TERMINAL_BLOCK,
|
||||||
shanghai_time,
|
shanghai_time,
|
||||||
eip4844_time,
|
eip4844_time,
|
||||||
|
builder_threshold,
|
||||||
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
|
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
|
||||||
spec.clone(),
|
spec.clone(),
|
||||||
Some(builder_url.clone()),
|
Some(builder_url.clone()),
|
||||||
|
@ -14,8 +14,8 @@ use std::convert::TryFrom;
|
|||||||
use strum::IntoStaticStr;
|
use strum::IntoStaticStr;
|
||||||
use superstruct::superstruct;
|
use superstruct::superstruct;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
Address, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadHeader, FixedVector,
|
Address, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadHeader,
|
||||||
ForkName, Hash256, Uint256, VariableList, Withdrawal,
|
ExecutionPayloadRef, FixedVector, ForkName, Hash256, Uint256, VariableList, Withdrawal,
|
||||||
};
|
};
|
||||||
use types::{ExecutionPayloadCapella, ExecutionPayloadEip4844, ExecutionPayloadMerge};
|
use types::{ExecutionPayloadCapella, ExecutionPayloadEip4844, ExecutionPayloadMerge};
|
||||||
|
|
||||||
@ -322,6 +322,8 @@ pub struct ProposeBlindedBlockResponse {
|
|||||||
#[superstruct(
|
#[superstruct(
|
||||||
variants(Merge, Capella, Eip4844),
|
variants(Merge, Capella, Eip4844),
|
||||||
variant_attributes(derive(Clone, Debug, PartialEq),),
|
variant_attributes(derive(Clone, Debug, PartialEq),),
|
||||||
|
map_into(ExecutionPayload),
|
||||||
|
map_ref_into(ExecutionPayloadRef),
|
||||||
cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"),
|
cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"),
|
||||||
partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant")
|
partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant")
|
||||||
)]
|
)]
|
||||||
@ -336,19 +338,44 @@ pub struct GetPayloadResponse<T: EthSpec> {
|
|||||||
pub block_value: Uint256,
|
pub block_value: Uint256,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, T: EthSpec> From<GetPayloadResponseRef<'a, T>> for ExecutionPayloadRef<'a, T> {
|
||||||
|
fn from(response: GetPayloadResponseRef<'a, T>) -> Self {
|
||||||
|
map_get_payload_response_ref_into_execution_payload_ref!(&'a _, response, |inner, cons| {
|
||||||
|
cons(&inner.execution_payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: EthSpec> From<GetPayloadResponse<T>> for ExecutionPayload<T> {
|
||||||
|
fn from(response: GetPayloadResponse<T>) -> Self {
|
||||||
|
map_get_payload_response_into_execution_payload!(response, |inner, cons| {
|
||||||
|
cons(inner.execution_payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: EthSpec> From<GetPayloadResponse<T>> for (ExecutionPayload<T>, Uint256) {
|
||||||
|
fn from(response: GetPayloadResponse<T>) -> Self {
|
||||||
|
match response {
|
||||||
|
GetPayloadResponse::Merge(inner) => (
|
||||||
|
ExecutionPayload::Merge(inner.execution_payload),
|
||||||
|
inner.block_value,
|
||||||
|
),
|
||||||
|
GetPayloadResponse::Capella(inner) => (
|
||||||
|
ExecutionPayload::Capella(inner.execution_payload),
|
||||||
|
inner.block_value,
|
||||||
|
),
|
||||||
|
GetPayloadResponse::Eip4844(inner) => (
|
||||||
|
ExecutionPayload::Eip4844(inner.execution_payload),
|
||||||
|
inner.block_value,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: EthSpec> GetPayloadResponse<T> {
|
impl<T: EthSpec> GetPayloadResponse<T> {
|
||||||
pub fn execution_payload(self) -> ExecutionPayload<T> {
|
pub fn execution_payload_ref(&self) -> ExecutionPayloadRef<T> {
|
||||||
match self {
|
self.to_ref().into()
|
||||||
GetPayloadResponse::Merge(response) => {
|
|
||||||
ExecutionPayload::Merge(response.execution_payload)
|
|
||||||
}
|
|
||||||
GetPayloadResponse::Capella(response) => {
|
|
||||||
ExecutionPayload::Capella(response.execution_payload)
|
|
||||||
}
|
|
||||||
GetPayloadResponse::Eip4844(response) => {
|
|
||||||
ExecutionPayload::Eip4844(response.execution_payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -804,7 +804,7 @@ impl HttpJsonRpc {
|
|||||||
pub async fn get_payload_v1<T: EthSpec>(
|
pub async fn get_payload_v1<T: EthSpec>(
|
||||||
&self,
|
&self,
|
||||||
payload_id: PayloadId,
|
payload_id: PayloadId,
|
||||||
) -> Result<ExecutionPayload<T>, Error> {
|
) -> Result<GetPayloadResponse<T>, Error> {
|
||||||
let params = json!([JsonPayloadIdRequest::from(payload_id)]);
|
let params = json!([JsonPayloadIdRequest::from(payload_id)]);
|
||||||
|
|
||||||
let payload_v1: JsonExecutionPayloadV1<T> = self
|
let payload_v1: JsonExecutionPayloadV1<T> = self
|
||||||
@ -815,7 +815,11 @@ impl HttpJsonRpc {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(JsonExecutionPayload::V1(payload_v1).into())
|
Ok(GetPayloadResponse::Merge(GetPayloadResponseMerge {
|
||||||
|
execution_payload: payload_v1.into(),
|
||||||
|
// Have to guess zero here as we don't know the value
|
||||||
|
block_value: Uint256::zero(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_payload_v2<T: EthSpec>(
|
pub async fn get_payload_v2<T: EthSpec>(
|
||||||
@ -1015,16 +1019,10 @@ impl HttpJsonRpc {
|
|||||||
&self,
|
&self,
|
||||||
fork_name: ForkName,
|
fork_name: ForkName,
|
||||||
payload_id: PayloadId,
|
payload_id: PayloadId,
|
||||||
) -> Result<ExecutionPayload<T>, Error> {
|
) -> Result<GetPayloadResponse<T>, Error> {
|
||||||
let engine_capabilities = self.get_engine_capabilities(None).await?;
|
let engine_capabilities = self.get_engine_capabilities(None).await?;
|
||||||
if engine_capabilities.get_payload_v2 {
|
if engine_capabilities.get_payload_v2 {
|
||||||
// TODO: modify this method to return GetPayloadResponse instead
|
self.get_payload_v2(fork_name, payload_id).await
|
||||||
// of throwing away the `block_value` and returning only the
|
|
||||||
// ExecutionPayload
|
|
||||||
Ok(self
|
|
||||||
.get_payload_v2(fork_name, payload_id)
|
|
||||||
.await?
|
|
||||||
.execution_payload())
|
|
||||||
} else if engine_capabilities.new_payload_v1 {
|
} else if engine_capabilities.new_payload_v1 {
|
||||||
self.get_payload_v1(payload_id).await
|
self.get_payload_v1(payload_id).await
|
||||||
} else {
|
} else {
|
||||||
@ -1675,10 +1673,11 @@ mod test {
|
|||||||
}
|
}
|
||||||
})],
|
})],
|
||||||
|client| async move {
|
|client| async move {
|
||||||
let payload = client
|
let payload: ExecutionPayload<_> = client
|
||||||
.get_payload_v1::<MainnetEthSpec>(str_to_payload_id("0xa247243752eb10b4"))
|
.get_payload_v1::<MainnetEthSpec>(str_to_payload_id("0xa247243752eb10b4"))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
|
||||||
let expected = ExecutionPayload::Merge(ExecutionPayloadMerge {
|
let expected = ExecutionPayload::Merge(ExecutionPayloadMerge {
|
||||||
parent_hash: ExecutionBlockHash::from_str("0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a").unwrap(),
|
parent_hash: ExecutionBlockHash::from_str("0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a").unwrap(),
|
||||||
|
@ -119,9 +119,13 @@ impl From<ApiError> for Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub enum BlockProposalContents<T: EthSpec, Payload: AbstractExecPayload<T>> {
|
pub enum BlockProposalContents<T: EthSpec, Payload: AbstractExecPayload<T>> {
|
||||||
Payload(Payload),
|
Payload {
|
||||||
|
payload: Payload,
|
||||||
|
block_value: Uint256,
|
||||||
|
},
|
||||||
PayloadAndBlobs {
|
PayloadAndBlobs {
|
||||||
payload: Payload,
|
payload: Payload,
|
||||||
|
block_value: Uint256,
|
||||||
kzg_commitments: Vec<KzgCommitment>,
|
kzg_commitments: Vec<KzgCommitment>,
|
||||||
blobs: Vec<Blob<T>>,
|
blobs: Vec<Blob<T>>,
|
||||||
},
|
},
|
||||||
@ -130,9 +134,13 @@ pub enum BlockProposalContents<T: EthSpec, Payload: AbstractExecPayload<T>> {
|
|||||||
impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Payload> {
|
impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Payload> {
|
||||||
pub fn payload(&self) -> &Payload {
|
pub fn payload(&self) -> &Payload {
|
||||||
match self {
|
match self {
|
||||||
Self::Payload(payload) => payload,
|
Self::Payload {
|
||||||
|
payload,
|
||||||
|
block_value: _,
|
||||||
|
} => payload,
|
||||||
Self::PayloadAndBlobs {
|
Self::PayloadAndBlobs {
|
||||||
payload,
|
payload,
|
||||||
|
block_value: _,
|
||||||
kzg_commitments: _,
|
kzg_commitments: _,
|
||||||
blobs: _,
|
blobs: _,
|
||||||
} => payload,
|
} => payload,
|
||||||
@ -140,9 +148,13 @@ impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Paylo
|
|||||||
}
|
}
|
||||||
pub fn to_payload(self) -> Payload {
|
pub fn to_payload(self) -> Payload {
|
||||||
match self {
|
match self {
|
||||||
Self::Payload(payload) => payload,
|
Self::Payload {
|
||||||
|
payload,
|
||||||
|
block_value: _,
|
||||||
|
} => payload,
|
||||||
Self::PayloadAndBlobs {
|
Self::PayloadAndBlobs {
|
||||||
payload,
|
payload,
|
||||||
|
block_value: _,
|
||||||
kzg_commitments: _,
|
kzg_commitments: _,
|
||||||
blobs: _,
|
blobs: _,
|
||||||
} => payload,
|
} => payload,
|
||||||
@ -150,9 +162,13 @@ impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Paylo
|
|||||||
}
|
}
|
||||||
pub fn kzg_commitments(&self) -> Option<&[KzgCommitment]> {
|
pub fn kzg_commitments(&self) -> Option<&[KzgCommitment]> {
|
||||||
match self {
|
match self {
|
||||||
Self::Payload(_) => None,
|
Self::Payload {
|
||||||
|
payload: _,
|
||||||
|
block_value: _,
|
||||||
|
} => None,
|
||||||
Self::PayloadAndBlobs {
|
Self::PayloadAndBlobs {
|
||||||
payload: _,
|
payload: _,
|
||||||
|
block_value: _,
|
||||||
kzg_commitments,
|
kzg_commitments,
|
||||||
blobs: _,
|
blobs: _,
|
||||||
} => Some(kzg_commitments),
|
} => Some(kzg_commitments),
|
||||||
@ -160,21 +176,43 @@ impl<T: EthSpec, Payload: AbstractExecPayload<T>> BlockProposalContents<T, Paylo
|
|||||||
}
|
}
|
||||||
pub fn blobs(&self) -> Option<&[Blob<T>]> {
|
pub fn blobs(&self) -> Option<&[Blob<T>]> {
|
||||||
match self {
|
match self {
|
||||||
Self::Payload(_) => None,
|
Self::Payload {
|
||||||
|
payload: _,
|
||||||
|
block_value: _,
|
||||||
|
} => None,
|
||||||
Self::PayloadAndBlobs {
|
Self::PayloadAndBlobs {
|
||||||
payload: _,
|
payload: _,
|
||||||
|
block_value: _,
|
||||||
kzg_commitments: _,
|
kzg_commitments: _,
|
||||||
blobs,
|
blobs,
|
||||||
} => Some(blobs),
|
} => Some(blobs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn block_value(&self) -> &Uint256 {
|
||||||
|
match self {
|
||||||
|
Self::Payload {
|
||||||
|
payload: _,
|
||||||
|
block_value,
|
||||||
|
} => block_value,
|
||||||
|
Self::PayloadAndBlobs {
|
||||||
|
payload: _,
|
||||||
|
block_value,
|
||||||
|
kzg_commitments: _,
|
||||||
|
blobs: _,
|
||||||
|
} => block_value,
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn default_at_fork(fork_name: ForkName) -> Result<Self, BeaconStateError> {
|
pub fn default_at_fork(fork_name: ForkName) -> Result<Self, BeaconStateError> {
|
||||||
Ok(match fork_name {
|
Ok(match fork_name {
|
||||||
ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => {
|
ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => {
|
||||||
BlockProposalContents::Payload(Payload::default_at_fork(fork_name)?)
|
BlockProposalContents::Payload {
|
||||||
|
payload: Payload::default_at_fork(fork_name)?,
|
||||||
|
block_value: Uint256::zero(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ForkName::Eip4844 => BlockProposalContents::PayloadAndBlobs {
|
ForkName::Eip4844 => BlockProposalContents::PayloadAndBlobs {
|
||||||
payload: Payload::default_at_fork(fork_name)?,
|
payload: Payload::default_at_fork(fork_name)?,
|
||||||
|
block_value: Uint256::zero(),
|
||||||
blobs: vec![],
|
blobs: vec![],
|
||||||
kzg_commitments: vec![],
|
kzg_commitments: vec![],
|
||||||
},
|
},
|
||||||
@ -366,12 +404,12 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
|||||||
&self.inner.builder
|
&self.inner.builder
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cache a full payload, keyed on the `tree_hash_root` of its `transactions` field.
|
/// Cache a full payload, keyed on the `tree_hash_root` of the payload
|
||||||
fn cache_payload(&self, payload: &ExecutionPayload<T>) -> Option<ExecutionPayload<T>> {
|
fn cache_payload(&self, payload: ExecutionPayloadRef<T>) -> Option<ExecutionPayload<T>> {
|
||||||
self.inner.payload_cache.put(payload.clone())
|
self.inner.payload_cache.put(payload.clone_from_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to retrieve a full payload from the payload cache by the `transactions_root`.
|
/// Attempt to retrieve a full payload from the payload cache by the payload root
|
||||||
pub fn get_payload_by_root(&self, root: &Hash256) -> Option<ExecutionPayload<T>> {
|
pub fn get_payload_by_root(&self, root: &Hash256) -> Option<ExecutionPayload<T>> {
|
||||||
self.inner.payload_cache.pop(root)
|
self.inner.payload_cache.pop(root)
|
||||||
}
|
}
|
||||||
@ -808,6 +846,18 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
|||||||
"parent_hash" => ?parent_hash,
|
"parent_hash" => ?parent_hash,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let relay_value = relay.data.message.value;
|
||||||
|
let local_value = *local.block_value();
|
||||||
|
if local_value >= relay_value {
|
||||||
|
info!(
|
||||||
|
self.log(),
|
||||||
|
"Local block is more profitable than relay block";
|
||||||
|
"local_block_value" => %local_value,
|
||||||
|
"relay_value" => %relay_value
|
||||||
|
);
|
||||||
|
return Ok(ProvenancedPayload::Local(local));
|
||||||
|
}
|
||||||
|
|
||||||
match verify_builder_bid(
|
match verify_builder_bid(
|
||||||
&relay,
|
&relay,
|
||||||
parent_hash,
|
parent_hash,
|
||||||
@ -818,7 +868,10 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
|||||||
spec,
|
spec,
|
||||||
) {
|
) {
|
||||||
Ok(()) => Ok(ProvenancedPayload::Builder(
|
Ok(()) => Ok(ProvenancedPayload::Builder(
|
||||||
BlockProposalContents::Payload(relay.data.message.header),
|
BlockProposalContents::Payload {
|
||||||
|
payload: relay.data.message.header,
|
||||||
|
block_value: relay.data.message.value,
|
||||||
|
},
|
||||||
)),
|
)),
|
||||||
Err(reason) if !reason.payload_invalid() => {
|
Err(reason) if !reason.payload_invalid() => {
|
||||||
info!(
|
info!(
|
||||||
@ -869,12 +922,18 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
|||||||
spec,
|
spec,
|
||||||
) {
|
) {
|
||||||
Ok(()) => Ok(ProvenancedPayload::Builder(
|
Ok(()) => Ok(ProvenancedPayload::Builder(
|
||||||
BlockProposalContents::Payload(relay.data.message.header),
|
BlockProposalContents::Payload {
|
||||||
|
payload: relay.data.message.header,
|
||||||
|
block_value: relay.data.message.value,
|
||||||
|
},
|
||||||
)),
|
)),
|
||||||
// If the payload is valid then use it. The local EE failed
|
// If the payload is valid then use it. The local EE failed
|
||||||
// to produce a payload so we have no alternative.
|
// to produce a payload so we have no alternative.
|
||||||
Err(e) if !e.payload_invalid() => Ok(ProvenancedPayload::Builder(
|
Err(e) if !e.payload_invalid() => Ok(ProvenancedPayload::Builder(
|
||||||
BlockProposalContents::Payload(relay.data.message.header),
|
BlockProposalContents::Payload {
|
||||||
|
payload: relay.data.message.header,
|
||||||
|
block_value: relay.data.message.value,
|
||||||
|
},
|
||||||
)),
|
)),
|
||||||
Err(reason) => {
|
Err(reason) => {
|
||||||
metrics::inc_counter_vec(
|
metrics::inc_counter_vec(
|
||||||
@ -988,7 +1047,7 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
|||||||
payload_attributes: &PayloadAttributes,
|
payload_attributes: &PayloadAttributes,
|
||||||
forkchoice_update_params: ForkchoiceUpdateParameters,
|
forkchoice_update_params: ForkchoiceUpdateParameters,
|
||||||
current_fork: ForkName,
|
current_fork: ForkName,
|
||||||
f: fn(&ExecutionLayer<T>, &ExecutionPayload<T>) -> Option<ExecutionPayload<T>>,
|
f: fn(&ExecutionLayer<T>, ExecutionPayloadRef<T>) -> Option<ExecutionPayload<T>>,
|
||||||
) -> Result<BlockProposalContents<T, Payload>, Error> {
|
) -> Result<BlockProposalContents<T, Payload>, Error> {
|
||||||
self.engine()
|
self.engine()
|
||||||
.request(move |engine| async move {
|
.request(move |engine| async move {
|
||||||
@ -1071,9 +1130,9 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
|||||||
);
|
);
|
||||||
engine.api.get_payload::<T>(current_fork, payload_id).await
|
engine.api.get_payload::<T>(current_fork, payload_id).await
|
||||||
};
|
};
|
||||||
let (blob, payload) = tokio::join!(blob_fut, payload_fut);
|
let (blob, payload_response) = tokio::join!(blob_fut, payload_fut);
|
||||||
let payload = payload.map(|full_payload| {
|
let (execution_payload, block_value) = payload_response.map(|payload_response| {
|
||||||
if full_payload.fee_recipient() != payload_attributes.suggested_fee_recipient() {
|
if payload_response.execution_payload_ref().fee_recipient() != payload_attributes.suggested_fee_recipient() {
|
||||||
error!(
|
error!(
|
||||||
self.log(),
|
self.log(),
|
||||||
"Inconsistent fee recipient";
|
"Inconsistent fee recipient";
|
||||||
@ -1082,28 +1141,32 @@ impl<T: EthSpec> ExecutionLayer<T> {
|
|||||||
indicate that fees are being diverted to another address. Please \
|
indicate that fees are being diverted to another address. Please \
|
||||||
ensure that the value of suggested_fee_recipient is set correctly and \
|
ensure that the value of suggested_fee_recipient is set correctly and \
|
||||||
that the Execution Engine is trusted.",
|
that the Execution Engine is trusted.",
|
||||||
"fee_recipient" => ?full_payload.fee_recipient(),
|
"fee_recipient" => ?payload_response.execution_payload_ref().fee_recipient(),
|
||||||
"suggested_fee_recipient" => ?payload_attributes.suggested_fee_recipient(),
|
"suggested_fee_recipient" => ?payload_attributes.suggested_fee_recipient(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if f(self, &full_payload).is_some() {
|
if f(self, payload_response.execution_payload_ref()).is_some() {
|
||||||
warn!(
|
warn!(
|
||||||
self.log(),
|
self.log(),
|
||||||
"Duplicate payload cached, this might indicate redundant proposal \
|
"Duplicate payload cached, this might indicate redundant proposal \
|
||||||
attempts."
|
attempts."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
full_payload.into()
|
payload_response.into()
|
||||||
})?;
|
})?;
|
||||||
if let Some(blob) = blob.transpose()? {
|
if let Some(blob) = blob.transpose()? {
|
||||||
// FIXME(sean) cache blobs
|
// FIXME(sean) cache blobs
|
||||||
Ok(BlockProposalContents::PayloadAndBlobs {
|
Ok(BlockProposalContents::PayloadAndBlobs {
|
||||||
payload,
|
payload: execution_payload.into(),
|
||||||
|
block_value,
|
||||||
blobs: blob.blobs,
|
blobs: blob.blobs,
|
||||||
kzg_commitments: blob.kzgs,
|
kzg_commitments: blob.kzgs,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(BlockProposalContents::Payload(payload))
|
Ok(BlockProposalContents::Payload {
|
||||||
|
payload: execution_payload.into(),
|
||||||
|
block_value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@ -2089,7 +2152,10 @@ mod test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn noop<T: EthSpec>(_: &ExecutionLayer<T>, _: &ExecutionPayload<T>) -> Option<ExecutionPayload<T>> {
|
fn noop<T: EthSpec>(
|
||||||
|
_: &ExecutionLayer<T>,
|
||||||
|
_: ExecutionPayloadRef<T>,
|
||||||
|
) -> Option<ExecutionPayload<T>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use super::Context;
|
use super::Context;
|
||||||
use crate::engine_api::{http::*, *};
|
use crate::engine_api::{http::*, *};
|
||||||
use crate::json_structures::*;
|
use crate::json_structures::*;
|
||||||
|
use crate::test_utils::DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -211,14 +212,14 @@ pub async fn handle_rpc<T: EthSpec>(
|
|||||||
JsonExecutionPayload::V1(execution_payload) => {
|
JsonExecutionPayload::V1(execution_payload) => {
|
||||||
serde_json::to_value(JsonGetPayloadResponseV1 {
|
serde_json::to_value(JsonGetPayloadResponseV1 {
|
||||||
execution_payload,
|
execution_payload,
|
||||||
block_value: 0.into(),
|
block_value: DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI.into(),
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
JsonExecutionPayload::V2(execution_payload) => {
|
JsonExecutionPayload::V2(execution_payload) => {
|
||||||
serde_json::to_value(JsonGetPayloadResponseV2 {
|
serde_json::to_value(JsonGetPayloadResponseV2 {
|
||||||
execution_payload,
|
execution_payload,
|
||||||
block_value: 0.into(),
|
block_value: DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI.into(),
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::test_utils::DEFAULT_JWT_SECRET;
|
use crate::test_utils::{DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_JWT_SECRET};
|
||||||
use crate::{Config, ExecutionLayer, PayloadAttributes};
|
use crate::{Config, ExecutionLayer, PayloadAttributes};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use eth2::types::{BlockId, StateId, ValidatorId};
|
use eth2::types::{BlockId, StateId, ValidatorId};
|
||||||
@ -328,7 +328,7 @@ impl<E: EthSpec> mev_build_rs::BlindedBlockProvider for MockBuilder<E> {
|
|||||||
|
|
||||||
let mut message = BuilderBid {
|
let mut message = BuilderBid {
|
||||||
header,
|
header,
|
||||||
value: ssz_rs::U256::default(),
|
value: to_ssz_rs(&Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI))?,
|
||||||
public_key: self.builder_sk.public_key(),
|
public_key: self.builder_sk.public_key(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ impl<T: EthSpec> MockExecutionLayer<T> {
|
|||||||
DEFAULT_TERMINAL_BLOCK,
|
DEFAULT_TERMINAL_BLOCK,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
|
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
|
||||||
spec,
|
spec,
|
||||||
None,
|
None,
|
||||||
@ -41,6 +42,7 @@ impl<T: EthSpec> MockExecutionLayer<T> {
|
|||||||
terminal_block: u64,
|
terminal_block: u64,
|
||||||
shanghai_time: Option<u64>,
|
shanghai_time: Option<u64>,
|
||||||
eip4844_time: Option<u64>,
|
eip4844_time: Option<u64>,
|
||||||
|
builder_threshold: Option<u128>,
|
||||||
jwt_key: Option<JwtKey>,
|
jwt_key: Option<JwtKey>,
|
||||||
spec: ChainSpec,
|
spec: ChainSpec,
|
||||||
builder_url: Option<SensitiveUrl>,
|
builder_url: Option<SensitiveUrl>,
|
||||||
@ -69,7 +71,7 @@ impl<T: EthSpec> MockExecutionLayer<T> {
|
|||||||
builder_url,
|
builder_url,
|
||||||
secret_files: vec![path],
|
secret_files: vec![path],
|
||||||
suggested_fee_recipient: Some(Address::repeat_byte(42)),
|
suggested_fee_recipient: Some(Address::repeat_byte(42)),
|
||||||
builder_profit_threshold: DEFAULT_BUILDER_THRESHOLD_WEI,
|
builder_profit_threshold: builder_threshold.unwrap_or(DEFAULT_BUILDER_THRESHOLD_WEI),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let el =
|
let el =
|
||||||
|
@ -32,6 +32,8 @@ pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400;
|
|||||||
pub const DEFAULT_TERMINAL_BLOCK: u64 = 64;
|
pub const DEFAULT_TERMINAL_BLOCK: u64 = 64;
|
||||||
pub const DEFAULT_JWT_SECRET: [u8; 32] = [42; 32];
|
pub const DEFAULT_JWT_SECRET: [u8; 32] = [42; 32];
|
||||||
pub const DEFAULT_BUILDER_THRESHOLD_WEI: u128 = 1_000_000_000_000_000_000;
|
pub const DEFAULT_BUILDER_THRESHOLD_WEI: u128 = 1_000_000_000_000_000_000;
|
||||||
|
pub const DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI: u128 = 10_000_000_000_000_000;
|
||||||
|
pub const DEFAULT_BUILDER_PAYLOAD_VALUE_WEI: u128 = 20_000_000_000_000_000;
|
||||||
pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities {
|
pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities {
|
||||||
new_payload_v1: true,
|
new_payload_v1: true,
|
||||||
new_payload_v2: true,
|
new_payload_v2: true,
|
||||||
|
@ -11,9 +11,11 @@ use eth2::{
|
|||||||
types::{BlockId as CoreBlockId, StateId as CoreStateId, *},
|
types::{BlockId as CoreBlockId, StateId as CoreStateId, *},
|
||||||
BeaconNodeHttpClient, Error, StatusCode, Timeouts,
|
BeaconNodeHttpClient, Error, StatusCode, Timeouts,
|
||||||
};
|
};
|
||||||
use execution_layer::test_utils::Operation;
|
|
||||||
use execution_layer::test_utils::TestingBuilder;
|
use execution_layer::test_utils::TestingBuilder;
|
||||||
use execution_layer::test_utils::DEFAULT_BUILDER_THRESHOLD_WEI;
|
use execution_layer::test_utils::DEFAULT_BUILDER_THRESHOLD_WEI;
|
||||||
|
use execution_layer::test_utils::{
|
||||||
|
Operation, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI,
|
||||||
|
};
|
||||||
use futures::stream::{Stream, StreamExt};
|
use futures::stream::{Stream, StreamExt};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use http_api::{BlockId, StateId};
|
use http_api::{BlockId, StateId};
|
||||||
@ -72,38 +74,53 @@ struct ApiTester {
|
|||||||
mock_builder: Option<Arc<TestingBuilder<E>>>,
|
mock_builder: Option<Arc<TestingBuilder<E>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ApiTesterConfig {
|
||||||
|
spec: ChainSpec,
|
||||||
|
builder_threshold: Option<u128>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ApiTesterConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut spec = E::default_spec();
|
||||||
|
spec.shard_committee_period = 2;
|
||||||
|
Self {
|
||||||
|
spec,
|
||||||
|
builder_threshold: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ApiTester {
|
impl ApiTester {
|
||||||
pub async fn new() -> Self {
|
pub async fn new() -> Self {
|
||||||
// This allows for testing voluntary exits without building out a massive chain.
|
// This allows for testing voluntary exits without building out a massive chain.
|
||||||
let mut spec = E::default_spec();
|
Self::new_from_config(ApiTesterConfig::default()).await
|
||||||
spec.shard_committee_period = 2;
|
|
||||||
Self::new_from_spec(spec).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn new_with_hard_forks(altair: bool, bellatrix: bool) -> Self {
|
pub async fn new_with_hard_forks(altair: bool, bellatrix: bool) -> Self {
|
||||||
let mut spec = E::default_spec();
|
let mut config = ApiTesterConfig::default();
|
||||||
spec.shard_committee_period = 2;
|
|
||||||
// Set whether the chain has undergone each hard fork.
|
// Set whether the chain has undergone each hard fork.
|
||||||
if altair {
|
if altair {
|
||||||
spec.altair_fork_epoch = Some(Epoch::new(0));
|
config.spec.altair_fork_epoch = Some(Epoch::new(0));
|
||||||
}
|
}
|
||||||
if bellatrix {
|
if bellatrix {
|
||||||
spec.bellatrix_fork_epoch = Some(Epoch::new(0));
|
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
|
||||||
}
|
}
|
||||||
Self::new_from_spec(spec).await
|
Self::new_from_config(config).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn new_from_spec(spec: ChainSpec) -> Self {
|
pub async fn new_from_config(config: ApiTesterConfig) -> Self {
|
||||||
// Get a random unused port
|
// Get a random unused port
|
||||||
|
let spec = config.spec;
|
||||||
let port = unused_port::unused_tcp_port().unwrap();
|
let port = unused_port::unused_tcp_port().unwrap();
|
||||||
let beacon_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap();
|
let beacon_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap();
|
||||||
|
|
||||||
let harness = Arc::new(
|
let harness = Arc::new(
|
||||||
BeaconChainHarness::builder(MainnetEthSpec)
|
BeaconChainHarness::builder(MainnetEthSpec)
|
||||||
.spec(spec.clone())
|
.spec(spec.clone())
|
||||||
|
.logger(logging::test_logger())
|
||||||
.deterministic_keypairs(VALIDATOR_COUNT)
|
.deterministic_keypairs(VALIDATOR_COUNT)
|
||||||
.fresh_ephemeral_store()
|
.fresh_ephemeral_store()
|
||||||
.mock_execution_layer_with_builder(beacon_url.clone())
|
.mock_execution_layer_with_builder(beacon_url.clone(), config.builder_threshold)
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -358,6 +375,28 @@ impl ApiTester {
|
|||||||
tester
|
tester
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn new_mev_tester_no_builder_threshold() -> Self {
|
||||||
|
let mut config = ApiTesterConfig {
|
||||||
|
builder_threshold: Some(0),
|
||||||
|
spec: E::default_spec(),
|
||||||
|
};
|
||||||
|
config.spec.altair_fork_epoch = Some(Epoch::new(0));
|
||||||
|
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
|
||||||
|
let tester = Self::new_from_config(config)
|
||||||
|
.await
|
||||||
|
.test_post_validator_register_validator()
|
||||||
|
.await;
|
||||||
|
tester
|
||||||
|
.mock_builder
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.builder
|
||||||
|
.add_operation(Operation::Value(Uint256::from(
|
||||||
|
DEFAULT_BUILDER_PAYLOAD_VALUE_WEI,
|
||||||
|
)));
|
||||||
|
tester
|
||||||
|
}
|
||||||
|
|
||||||
fn skip_slots(self, count: u64) -> Self {
|
fn skip_slots(self, count: u64) -> Self {
|
||||||
for _ in 0..count {
|
for _ in 0..count {
|
||||||
self.chain
|
self.chain
|
||||||
@ -3278,6 +3317,117 @@ impl ApiTester {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn test_builder_payload_chosen_when_more_profitable(self) -> Self {
|
||||||
|
// Mutate value.
|
||||||
|
self.mock_builder
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.builder
|
||||||
|
.add_operation(Operation::Value(Uint256::from(
|
||||||
|
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1,
|
||||||
|
)));
|
||||||
|
|
||||||
|
let slot = self.chain.slot().unwrap();
|
||||||
|
let epoch = self.chain.epoch().unwrap();
|
||||||
|
|
||||||
|
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
|
||||||
|
|
||||||
|
let payload: BlindedPayload<E> = self
|
||||||
|
.client
|
||||||
|
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.data
|
||||||
|
.body()
|
||||||
|
.execution_payload()
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
|
||||||
|
// The builder's payload should've been chosen, so this cache should not be populated
|
||||||
|
assert!(self
|
||||||
|
.chain
|
||||||
|
.execution_layer
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get_payload_by_root(&payload.tree_hash_root())
|
||||||
|
.is_none());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test_local_payload_chosen_when_equally_profitable(self) -> Self {
|
||||||
|
// Mutate value.
|
||||||
|
self.mock_builder
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.builder
|
||||||
|
.add_operation(Operation::Value(Uint256::from(
|
||||||
|
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI,
|
||||||
|
)));
|
||||||
|
|
||||||
|
let slot = self.chain.slot().unwrap();
|
||||||
|
let epoch = self.chain.epoch().unwrap();
|
||||||
|
|
||||||
|
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
|
||||||
|
|
||||||
|
let payload: BlindedPayload<E> = self
|
||||||
|
.client
|
||||||
|
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.data
|
||||||
|
.body()
|
||||||
|
.execution_payload()
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
|
||||||
|
// The local payload should've been chosen, so this cache should be populated
|
||||||
|
assert!(self
|
||||||
|
.chain
|
||||||
|
.execution_layer
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get_payload_by_root(&payload.tree_hash_root())
|
||||||
|
.is_some());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test_local_payload_chosen_when_more_profitable(self) -> Self {
|
||||||
|
// Mutate value.
|
||||||
|
self.mock_builder
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.builder
|
||||||
|
.add_operation(Operation::Value(Uint256::from(
|
||||||
|
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI - 1,
|
||||||
|
)));
|
||||||
|
|
||||||
|
let slot = self.chain.slot().unwrap();
|
||||||
|
let epoch = self.chain.epoch().unwrap();
|
||||||
|
|
||||||
|
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
|
||||||
|
|
||||||
|
let payload: BlindedPayload<E> = self
|
||||||
|
.client
|
||||||
|
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.data
|
||||||
|
.body()
|
||||||
|
.execution_payload()
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
|
||||||
|
// The local payload should've been chosen, so this cache should be populated
|
||||||
|
assert!(self
|
||||||
|
.chain
|
||||||
|
.execution_layer
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get_payload_by_root(&payload.tree_hash_root())
|
||||||
|
.is_some());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub async fn test_get_lighthouse_health(self) -> Self {
|
pub async fn test_get_lighthouse_health(self) -> Self {
|
||||||
self.client.get_lighthouse_health().await.unwrap();
|
self.client.get_lighthouse_health().await.unwrap();
|
||||||
@ -3747,9 +3897,9 @@ async fn get_events() {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn get_events_altair() {
|
async fn get_events_altair() {
|
||||||
let mut spec = E::default_spec();
|
let mut config = ApiTesterConfig::default();
|
||||||
spec.altair_fork_epoch = Some(Epoch::new(0));
|
config.spec.altair_fork_epoch = Some(Epoch::new(0));
|
||||||
ApiTester::new_from_spec(spec)
|
ApiTester::new_from_config(config)
|
||||||
.await
|
.await
|
||||||
.test_get_events_altair()
|
.test_get_events_altair()
|
||||||
.await;
|
.await;
|
||||||
@ -4262,6 +4412,18 @@ async fn builder_inadequate_builder_threshold() {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn builder_payload_chosen_by_profit() {
|
||||||
|
ApiTester::new_mev_tester_no_builder_threshold()
|
||||||
|
.await
|
||||||
|
.test_builder_payload_chosen_when_more_profitable()
|
||||||
|
.await
|
||||||
|
.test_local_payload_chosen_when_equally_profitable()
|
||||||
|
.await
|
||||||
|
.test_local_payload_chosen_when_more_profitable()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn lighthouse_endpoints() {
|
async fn lighthouse_endpoints() {
|
||||||
ApiTester::new()
|
ApiTester::new()
|
||||||
|
@ -87,6 +87,16 @@ pub struct ExecutionPayload<T: EthSpec> {
|
|||||||
pub withdrawals: Withdrawals<T>,
|
pub withdrawals: Withdrawals<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, T: EthSpec> ExecutionPayloadRef<'a, T> {
|
||||||
|
// this emulates clone on a normal reference type
|
||||||
|
pub fn clone_from_ref(&self) -> ExecutionPayload<T> {
|
||||||
|
map_execution_payload_ref!(&'a _, self, move |payload, cons| {
|
||||||
|
cons(payload);
|
||||||
|
payload.clone().into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: EthSpec> ExecutionPayload<T> {
|
impl<T: EthSpec> ExecutionPayload<T> {
|
||||||
pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result<Self, ssz::DecodeError> {
|
pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result<Self, ssz::DecodeError> {
|
||||||
match fork_name {
|
match fork_name {
|
||||||
|
Loading…
Reference in New Issue
Block a user