mirror of
https://github.com/sigp/lighthouse.git
synced 2026-06-16 18:28:42 +00:00
Merge remote-tracking branch 'origin/deneb-free-blobs' into tree-states
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
use crate::{state_id::checkpoint_slot_and_execution_optimistic, ExecutionOptimistic};
|
||||
use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped};
|
||||
use eth2::types::BlobIndicesQuery;
|
||||
use eth2::types::BlockId as CoreBlockId;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use types::{EthSpec, Hash256, SignedBeaconBlock, SignedBlindedBeaconBlock, Slot};
|
||||
use types::{BlobSidecarList, EthSpec, Hash256, SignedBeaconBlock, SignedBlindedBeaconBlock, Slot};
|
||||
|
||||
/// Wraps `eth2::types::BlockId` and provides a simple way to obtain a block or root for a given
|
||||
/// `BlockId`.
|
||||
@@ -250,6 +251,37 @@ impl BlockId {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the `BlobSidecarList` identified by `self`.
|
||||
pub async fn blob_sidecar_list<T: BeaconChainTypes>(
|
||||
&self,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<BlobSidecarList<T::EthSpec>, warp::Rejection> {
|
||||
let root = self.root(chain)?.0;
|
||||
chain
|
||||
.get_blobs(&root)
|
||||
.map_err(warp_utils::reject::beacon_chain_error)
|
||||
}
|
||||
|
||||
pub async fn blob_sidecar_list_filtered<T: BeaconChainTypes>(
|
||||
&self,
|
||||
indices: BlobIndicesQuery,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<BlobSidecarList<T::EthSpec>, warp::Rejection> {
|
||||
let blob_sidecar_list = self.blob_sidecar_list(chain).await?;
|
||||
let blob_sidecar_list_filtered = match indices.indices {
|
||||
Some(vec) => {
|
||||
let list = blob_sidecar_list
|
||||
.into_iter()
|
||||
.filter(|blob_sidecar| vec.contains(&blob_sidecar.index))
|
||||
.collect();
|
||||
BlobSidecarList::new(list)
|
||||
.map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e)))?
|
||||
}
|
||||
None => blob_sidecar_list,
|
||||
};
|
||||
Ok(blob_sidecar_list_filtered)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for BlockId {
|
||||
|
||||
62
beacon_node/http_api/src/build_block_contents.rs
Normal file
62
beacon_node/http_api/src/build_block_contents.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use beacon_chain::BlockProductionError;
|
||||
use eth2::types::{BeaconBlockAndBlobSidecars, BlindedBeaconBlockAndBlobSidecars, BlockContents};
|
||||
use types::{
|
||||
BeaconBlock, BlindedBlobSidecarList, BlindedPayload, BlobSidecarList, EthSpec, ForkName,
|
||||
FullPayload,
|
||||
};
|
||||
|
||||
type Error = warp::reject::Rejection;
|
||||
type FullBlockContents<E> = BlockContents<E, FullPayload<E>>;
|
||||
type BlindedBlockContents<E> = BlockContents<E, BlindedPayload<E>>;
|
||||
|
||||
pub fn build_block_contents<E: EthSpec>(
|
||||
fork_name: ForkName,
|
||||
block: BeaconBlock<E, FullPayload<E>>,
|
||||
maybe_blobs: Option<BlobSidecarList<E>>,
|
||||
) -> Result<FullBlockContents<E>, Error> {
|
||||
match fork_name {
|
||||
ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => {
|
||||
Ok(BlockContents::Block(block))
|
||||
}
|
||||
ForkName::Deneb => {
|
||||
if let Some(blob_sidecars) = maybe_blobs {
|
||||
let block_and_blobs = BeaconBlockAndBlobSidecars {
|
||||
block,
|
||||
blob_sidecars,
|
||||
};
|
||||
|
||||
Ok(BlockContents::BlockAndBlobSidecars(block_and_blobs))
|
||||
} else {
|
||||
Err(warp_utils::reject::block_production_error(
|
||||
BlockProductionError::MissingBlobs,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_blinded_block_contents<E: EthSpec>(
|
||||
fork_name: ForkName,
|
||||
block: BeaconBlock<E, BlindedPayload<E>>,
|
||||
maybe_blobs: Option<BlindedBlobSidecarList<E>>,
|
||||
) -> Result<BlindedBlockContents<E>, Error> {
|
||||
match fork_name {
|
||||
ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => {
|
||||
Ok(BlockContents::Block(block))
|
||||
}
|
||||
ForkName::Deneb => {
|
||||
if let Some(blinded_blob_sidecars) = maybe_blobs {
|
||||
let block_and_blobs = BlindedBeaconBlockAndBlobSidecars {
|
||||
blinded_block: block,
|
||||
blinded_blob_sidecars,
|
||||
};
|
||||
|
||||
Ok(BlockContents::BlindedBlockAndBlobSidecars(block_and_blobs))
|
||||
} else {
|
||||
Err(warp_utils::reject::block_production_error(
|
||||
BlockProductionError::MissingBlobs,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use beacon_chain::store::{metadata::CURRENT_SCHEMA_VERSION, AnchorInfo};
|
||||
use beacon_chain::store::metadata::CURRENT_SCHEMA_VERSION;
|
||||
use beacon_chain::{BeaconChain, BeaconChainTypes};
|
||||
use eth2::lighthouse::DatabaseInfo;
|
||||
use std::sync::Arc;
|
||||
use types::SignedBlindedBeaconBlock;
|
||||
|
||||
pub fn info<T: BeaconChainTypes>(
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
@@ -11,25 +10,13 @@ pub fn info<T: BeaconChainTypes>(
|
||||
let split = store.get_split_info();
|
||||
let config = store.get_config().clone();
|
||||
let anchor = store.get_anchor_info();
|
||||
let blob_info = store.get_blob_info();
|
||||
|
||||
Ok(DatabaseInfo {
|
||||
schema_version: CURRENT_SCHEMA_VERSION.as_u64(),
|
||||
config,
|
||||
split,
|
||||
anchor,
|
||||
blob_info,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn historical_blocks<T: BeaconChainTypes>(
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
blocks: Vec<Arc<SignedBlindedBeaconBlock<T::EthSpec>>>,
|
||||
) -> Result<AnchorInfo, warp::Rejection> {
|
||||
chain
|
||||
.import_historical_block_batch(blocks)
|
||||
.map_err(warp_utils::reject::beacon_chain_error)?;
|
||||
|
||||
let anchor = chain.store.get_anchor_info().ok_or_else(|| {
|
||||
warp_utils::reject::custom_bad_request("node is not checkpoint synced".to_string())
|
||||
})?;
|
||||
Ok(anchor)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ mod attester_duties;
|
||||
mod block_id;
|
||||
mod block_packing_efficiency;
|
||||
mod block_rewards;
|
||||
mod build_block_contents;
|
||||
mod builder_states;
|
||||
mod database;
|
||||
mod metrics;
|
||||
@@ -38,7 +39,8 @@ use bytes::Bytes;
|
||||
use directory::DEFAULT_ROOT_DIR;
|
||||
use eth2::types::{
|
||||
self as api_types, BroadcastValidation, EndpointVersion, ForkChoice, ForkChoiceNode,
|
||||
SkipRandaoVerification, ValidatorId, ValidatorStatus,
|
||||
SignedBlindedBlockContents, SignedBlockContents, SkipRandaoVerification, ValidatorId,
|
||||
ValidatorStatus,
|
||||
};
|
||||
use lighthouse_network::{types::SyncState, EnrExt, NetworkGlobals, PeerId, PubsubMessage};
|
||||
use lighthouse_version::version_with_platform;
|
||||
@@ -74,9 +76,8 @@ use types::{
|
||||
Attestation, AttestationData, AttestationShufflingId, AttesterSlashing, BeaconStateError,
|
||||
BlindedPayload, CommitteeCache, ConfigAndPreset, Epoch, EthSpec, ForkName, FullPayload,
|
||||
ProposerPreparationData, ProposerSlashing, RelativeEpoch, SignedAggregateAndProof,
|
||||
SignedBeaconBlock, SignedBlindedBeaconBlock, SignedBlsToExecutionChange,
|
||||
SignedContributionAndProof, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot,
|
||||
SyncCommitteeMessage, SyncContributionData,
|
||||
SignedBlsToExecutionChange, SignedContributionAndProof, SignedValidatorRegistrationData,
|
||||
SignedVoluntaryExit, Slot, SyncCommitteeMessage, SyncContributionData,
|
||||
};
|
||||
use validator::pubkey_to_validator_index;
|
||||
use version::{
|
||||
@@ -1287,7 +1288,7 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
.and(network_tx_filter.clone())
|
||||
.and(log_filter.clone())
|
||||
.then(
|
||||
move |block: Arc<SignedBeaconBlock<T::EthSpec>>,
|
||||
move |block_contents: SignedBlockContents<T::EthSpec>,
|
||||
task_spawner: TaskSpawner<T::EthSpec>,
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
|
||||
@@ -1295,7 +1296,7 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
task_spawner.spawn_async_with_rejection(Priority::P0, async move {
|
||||
publish_blocks::publish_block(
|
||||
None,
|
||||
ProvenancedBlock::local(block),
|
||||
ProvenancedBlock::local(block_contents),
|
||||
chain,
|
||||
&network_tx,
|
||||
log,
|
||||
@@ -1323,16 +1324,16 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
|
||||
log: Logger| {
|
||||
task_spawner.spawn_async_with_rejection(Priority::P0, async move {
|
||||
let block =
|
||||
SignedBeaconBlock::<T::EthSpec>::from_ssz_bytes(&block_bytes, &chain.spec)
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_bad_request(format!(
|
||||
"invalid SSZ: {e:?}"
|
||||
))
|
||||
})?;
|
||||
let block_contents = SignedBlockContents::<T::EthSpec>::from_ssz_bytes(
|
||||
&block_bytes,
|
||||
&chain.spec,
|
||||
)
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}"))
|
||||
})?;
|
||||
publish_blocks::publish_block(
|
||||
None,
|
||||
ProvenancedBlock::local(Arc::new(block)),
|
||||
ProvenancedBlock::local(block_contents),
|
||||
chain,
|
||||
&network_tx,
|
||||
log,
|
||||
@@ -1356,7 +1357,7 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
.and(log_filter.clone())
|
||||
.then(
|
||||
move |validation_level: api_types::BroadcastValidationQuery,
|
||||
block: Arc<SignedBeaconBlock<T::EthSpec>>,
|
||||
block_contents: SignedBlockContents<T::EthSpec>,
|
||||
task_spawner: TaskSpawner<T::EthSpec>,
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
|
||||
@@ -1364,7 +1365,7 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
task_spawner.spawn_async_with_rejection(Priority::P0, async move {
|
||||
publish_blocks::publish_block(
|
||||
None,
|
||||
ProvenancedBlock::local(block),
|
||||
ProvenancedBlock::local(block_contents),
|
||||
chain,
|
||||
&network_tx,
|
||||
log,
|
||||
@@ -1394,16 +1395,16 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
|
||||
log: Logger| {
|
||||
task_spawner.spawn_async_with_rejection(Priority::P0, async move {
|
||||
let block =
|
||||
SignedBeaconBlock::<T::EthSpec>::from_ssz_bytes(&block_bytes, &chain.spec)
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_bad_request(format!(
|
||||
"invalid SSZ: {e:?}"
|
||||
))
|
||||
})?;
|
||||
let block_contents = SignedBlockContents::<T::EthSpec>::from_ssz_bytes(
|
||||
&block_bytes,
|
||||
&chain.spec,
|
||||
)
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}"))
|
||||
})?;
|
||||
publish_blocks::publish_block(
|
||||
None,
|
||||
ProvenancedBlock::local(Arc::new(block)),
|
||||
ProvenancedBlock::local(block_contents),
|
||||
chain,
|
||||
&network_tx,
|
||||
log,
|
||||
@@ -1430,14 +1431,14 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
.and(network_tx_filter.clone())
|
||||
.and(log_filter.clone())
|
||||
.then(
|
||||
move |block: SignedBlindedBeaconBlock<T::EthSpec>,
|
||||
move |block_contents: SignedBlindedBlockContents<T::EthSpec>,
|
||||
task_spawner: TaskSpawner<T::EthSpec>,
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
|
||||
log: Logger| {
|
||||
task_spawner.spawn_async_with_rejection(Priority::P0, async move {
|
||||
publish_blocks::publish_blinded_block(
|
||||
block,
|
||||
block_contents,
|
||||
chain,
|
||||
&network_tx,
|
||||
log,
|
||||
@@ -1466,13 +1467,14 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
|
||||
log: Logger| {
|
||||
task_spawner.spawn_async_with_rejection(Priority::P0, async move {
|
||||
let block = SignedBlindedBeaconBlock::<T::EthSpec>::from_ssz_bytes(
|
||||
&block_bytes,
|
||||
&chain.spec,
|
||||
)
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}"))
|
||||
})?;
|
||||
let block =
|
||||
SignedBlockContents::<T::EthSpec, BlindedPayload<_>>::from_ssz_bytes(
|
||||
&block_bytes,
|
||||
&chain.spec,
|
||||
)
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}"))
|
||||
})?;
|
||||
publish_blocks::publish_blinded_block(
|
||||
block,
|
||||
chain,
|
||||
@@ -1498,14 +1500,14 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
.and(log_filter.clone())
|
||||
.then(
|
||||
move |validation_level: api_types::BroadcastValidationQuery,
|
||||
block: SignedBlindedBeaconBlock<T::EthSpec>,
|
||||
block_contents: SignedBlindedBlockContents<T::EthSpec>,
|
||||
task_spawner: TaskSpawner<T::EthSpec>,
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
|
||||
log: Logger| {
|
||||
task_spawner.spawn_async_with_rejection(Priority::P0, async move {
|
||||
publish_blocks::publish_blinded_block(
|
||||
block,
|
||||
block_contents,
|
||||
chain,
|
||||
&network_tx,
|
||||
log,
|
||||
@@ -1535,13 +1537,14 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
|
||||
log: Logger| {
|
||||
task_spawner.spawn_async_with_rejection(Priority::P0, async move {
|
||||
let block = SignedBlindedBeaconBlock::<T::EthSpec>::from_ssz_bytes(
|
||||
&block_bytes,
|
||||
&chain.spec,
|
||||
)
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}"))
|
||||
})?;
|
||||
let block =
|
||||
SignedBlockContents::<T::EthSpec, BlindedPayload<_>>::from_ssz_bytes(
|
||||
&block_bytes,
|
||||
&chain.spec,
|
||||
)
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}"))
|
||||
})?;
|
||||
publish_blocks::publish_blinded_block(
|
||||
block,
|
||||
chain,
|
||||
@@ -1707,6 +1710,47 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* beacon/blob_sidecars
|
||||
*/
|
||||
|
||||
// GET beacon/blob_sidecars/{block_id}
|
||||
let get_blobs = eth_v1
|
||||
.and(warp::path("beacon"))
|
||||
.and(warp::path("blob_sidecars"))
|
||||
.and(block_id_or_err)
|
||||
.and(warp::query::<api_types::BlobIndicesQuery>())
|
||||
.and(warp::path::end())
|
||||
.and(chain_filter.clone())
|
||||
.and(warp::header::optional::<api_types::Accept>("accept"))
|
||||
.and_then(
|
||||
|block_id: BlockId,
|
||||
indices: api_types::BlobIndicesQuery,
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
accept_header: Option<api_types::Accept>| {
|
||||
async move {
|
||||
let blob_sidecar_list_filtered =
|
||||
block_id.blob_sidecar_list_filtered(indices, &chain).await?;
|
||||
match accept_header {
|
||||
Some(api_types::Accept::Ssz) => Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.body(blob_sidecar_list_filtered.as_ssz_bytes().into())
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"failed to create response: {}",
|
||||
e
|
||||
))
|
||||
}),
|
||||
_ => Ok(warp::reply::json(&api_types::GenericResponse::from(
|
||||
blob_sidecar_list_filtered,
|
||||
))
|
||||
.into_response()),
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* beacon/pool
|
||||
*/
|
||||
@@ -3030,16 +3074,16 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
if query.skip_randao_verification == SkipRandaoVerification::Yes {
|
||||
if !randao_reveal.is_infinity() {
|
||||
return Err(warp_utils::reject::custom_bad_request(
|
||||
"randao_reveal must be point-at-infinity if verification is skipped"
|
||||
.into(),
|
||||
));
|
||||
"randao_reveal must be point-at-infinity if verification is skipped"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
ProduceBlockVerification::NoVerification
|
||||
} else {
|
||||
ProduceBlockVerification::VerifyRandao
|
||||
};
|
||||
|
||||
let (block, _) = chain
|
||||
let (block, _, maybe_blobs) = chain
|
||||
.produce_block_with_verification::<FullPayload<T::EthSpec>>(
|
||||
randao_reveal,
|
||||
slot,
|
||||
@@ -3053,11 +3097,14 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
.fork_name(&chain.spec)
|
||||
.map_err(inconsistent_fork_rejection)?;
|
||||
|
||||
let block_contents =
|
||||
build_block_contents::build_block_contents(fork_name, block, maybe_blobs)?;
|
||||
|
||||
match accept_header {
|
||||
Some(api_types::Accept::Ssz) => Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.body(block.as_ssz_bytes().into())
|
||||
.body(block_contents.as_ssz_bytes().into())
|
||||
.map(|res: Response<Bytes>| {
|
||||
add_consensus_version_header(res, fork_name)
|
||||
})
|
||||
@@ -3067,7 +3114,7 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
e
|
||||
))
|
||||
}),
|
||||
_ => fork_versioned_response(endpoint_version, fork_name, block)
|
||||
_ => fork_versioned_response(endpoint_version, fork_name, block_contents)
|
||||
.map(|response| warp::reply::json(&response).into_response())
|
||||
.map(|res| add_consensus_version_header(res, fork_name)),
|
||||
}
|
||||
@@ -3117,7 +3164,7 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
ProduceBlockVerification::VerifyRandao
|
||||
};
|
||||
|
||||
let (block, _) = chain
|
||||
let (block, _, maybe_blobs) = chain
|
||||
.produce_block_with_verification::<BlindedPayload<T::EthSpec>>(
|
||||
randao_reveal,
|
||||
slot,
|
||||
@@ -3131,11 +3178,17 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
.fork_name(&chain.spec)
|
||||
.map_err(inconsistent_fork_rejection)?;
|
||||
|
||||
let block_contents = build_block_contents::build_blinded_block_contents(
|
||||
fork_name,
|
||||
block,
|
||||
maybe_blobs,
|
||||
)?;
|
||||
|
||||
match accept_header {
|
||||
Some(api_types::Accept::Ssz) => Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.body(block.as_ssz_bytes().into())
|
||||
.body(block_contents.as_ssz_bytes().into())
|
||||
.map(|res: Response<Bytes>| {
|
||||
add_consensus_version_header(res, fork_name)
|
||||
})
|
||||
@@ -3146,7 +3199,7 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
))
|
||||
}),
|
||||
// Pose as a V2 endpoint so we return the fork `version`.
|
||||
_ => fork_versioned_response(V2, fork_name, block)
|
||||
_ => fork_versioned_response(V2, fork_name, block_contents)
|
||||
.map(|response| warp::reply::json(&response).into_response())
|
||||
.map(|res| add_consensus_version_header(res, fork_name)),
|
||||
}
|
||||
@@ -4260,31 +4313,6 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
},
|
||||
);
|
||||
|
||||
// POST lighthouse/database/historical_blocks
|
||||
let post_lighthouse_database_historical_blocks = database_path
|
||||
.and(warp::path("historical_blocks"))
|
||||
.and(warp::path::end())
|
||||
.and(warp::body::json())
|
||||
.and(task_spawner_filter.clone())
|
||||
.and(chain_filter.clone())
|
||||
.and(log_filter.clone())
|
||||
.then(
|
||||
|blocks: Vec<Arc<SignedBlindedBeaconBlock<T::EthSpec>>>,
|
||||
task_spawner: TaskSpawner<T::EthSpec>,
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
log: Logger| {
|
||||
info!(
|
||||
log,
|
||||
"Importing historical blocks";
|
||||
"count" => blocks.len(),
|
||||
"source" => "http_api"
|
||||
);
|
||||
task_spawner.blocking_json_task(Priority::P1, move || {
|
||||
database::historical_blocks(chain, blocks)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
// GET lighthouse/analysis/block_rewards
|
||||
let get_lighthouse_block_rewards = warp::path("lighthouse")
|
||||
.and(warp::path("analysis"))
|
||||
@@ -4518,6 +4546,7 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
.uor(get_beacon_block_attestations)
|
||||
.uor(get_beacon_blinded_block)
|
||||
.uor(get_beacon_block_root)
|
||||
.uor(get_blobs)
|
||||
.uor(get_beacon_pool_attestations)
|
||||
.uor(get_beacon_pool_attester_slashings)
|
||||
.uor(get_beacon_pool_proposer_slashings)
|
||||
@@ -4603,7 +4632,6 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
.uor(post_validator_liveness_epoch)
|
||||
.uor(post_lighthouse_liveness)
|
||||
.uor(post_lighthouse_database_reconstruct)
|
||||
.uor(post_lighthouse_database_historical_blocks)
|
||||
.uor(post_lighthouse_block_rewards)
|
||||
.uor(post_lighthouse_ui_validator_metrics)
|
||||
.uor(post_lighthouse_ui_validator_info)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use crate::metrics;
|
||||
|
||||
use beacon_chain::block_verification_types::{AsBlock, BlockContentsError};
|
||||
use beacon_chain::validator_monitor::{get_block_delay_ms, timestamp_now};
|
||||
use beacon_chain::{
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, IntoGossipVerifiedBlock,
|
||||
NotifyExecutionLayer,
|
||||
AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError,
|
||||
IntoGossipVerifiedBlockContents, NotifyExecutionLayer,
|
||||
};
|
||||
use eth2::types::{BroadcastValidation, ErrorMessage};
|
||||
use eth2::types::{FullPayloadContents, SignedBlockContents};
|
||||
use execution_layer::ProvenancedPayload;
|
||||
use lighthouse_network::PubsubMessage;
|
||||
use network::NetworkMessage;
|
||||
@@ -17,12 +20,12 @@ use tokio::sync::mpsc::UnboundedSender;
|
||||
use tree_hash::TreeHash;
|
||||
use types::{
|
||||
AbstractExecPayload, BeaconBlockRef, BlindedPayload, EthSpec, ExecPayload, ExecutionBlockHash,
|
||||
FullPayload, Hash256, SignedBeaconBlock,
|
||||
ForkName, FullPayload, FullPayloadMerge, Hash256, SignedBeaconBlock, SignedBlobSidecarList,
|
||||
};
|
||||
use warp::http::StatusCode;
|
||||
use warp::{reply::Response, Rejection, Reply};
|
||||
|
||||
pub enum ProvenancedBlock<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>> {
|
||||
pub enum ProvenancedBlock<T: BeaconChainTypes, B: IntoGossipVerifiedBlockContents<T>> {
|
||||
/// The payload was built using a local EE.
|
||||
Local(B, PhantomData<T>),
|
||||
/// The payload was build using a remote builder (e.g., via a mev-boost
|
||||
@@ -30,7 +33,7 @@ pub enum ProvenancedBlock<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>> {
|
||||
Builder(B, PhantomData<T>),
|
||||
}
|
||||
|
||||
impl<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>> ProvenancedBlock<T, B> {
|
||||
impl<T: BeaconChainTypes, B: IntoGossipVerifiedBlockContents<T>> ProvenancedBlock<T, B> {
|
||||
pub fn local(block: B) -> Self {
|
||||
Self::Local(block, PhantomData)
|
||||
}
|
||||
@@ -41,7 +44,7 @@ impl<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>> ProvenancedBlock<T, B>
|
||||
}
|
||||
|
||||
/// Handles a request from the HTTP API for full blocks.
|
||||
pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
|
||||
pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlockContents<T>>(
|
||||
block_root: Option<Hash256>,
|
||||
provenanced_block: ProvenancedBlock<T, B>,
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
@@ -51,16 +54,18 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
|
||||
duplicate_status_code: StatusCode,
|
||||
) -> Result<Response, Rejection> {
|
||||
let seen_timestamp = timestamp_now();
|
||||
let (block, is_locally_built_block) = match provenanced_block {
|
||||
ProvenancedBlock::Local(block, _) => (block, true),
|
||||
ProvenancedBlock::Builder(block, _) => (block, false),
|
||||
|
||||
let (block_contents, is_locally_built_block) = match provenanced_block {
|
||||
ProvenancedBlock::Local(block_contents, _) => (block_contents, true),
|
||||
ProvenancedBlock::Builder(block_contents, _) => (block_contents, false),
|
||||
};
|
||||
let beacon_block = block.inner();
|
||||
let delay = get_block_delay_ms(seen_timestamp, beacon_block.message(), &chain.slot_clock);
|
||||
debug!(log, "Signed block received in HTTP API"; "slot" => beacon_block.slot());
|
||||
let block = block_contents.inner_block();
|
||||
let delay = get_block_delay_ms(seen_timestamp, block.message(), &chain.slot_clock);
|
||||
debug!(log, "Signed block received in HTTP API"; "slot" => block.slot());
|
||||
|
||||
/* actually publish a block */
|
||||
let publish_block = move |block: Arc<SignedBeaconBlock<T::EthSpec>>,
|
||||
blobs_opt: Option<SignedBlobSidecarList<T::EthSpec>>,
|
||||
sender,
|
||||
log,
|
||||
seen_timestamp| {
|
||||
@@ -70,61 +75,101 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
|
||||
info!(log, "Signed block published to network via HTTP API"; "slot" => block.slot(), "publish_delay" => ?publish_delay);
|
||||
|
||||
let message = PubsubMessage::BeaconBlock(block);
|
||||
crate::publish_pubsub_message(&sender, message)
|
||||
.map_err(|_| BeaconChainError::UnableToPublish.into())
|
||||
// Send the block, regardless of whether or not it is valid. The API
|
||||
// specification is very clear that this is the desired behaviour.
|
||||
match block.as_ref() {
|
||||
SignedBeaconBlock::Base(_)
|
||||
| SignedBeaconBlock::Altair(_)
|
||||
| SignedBeaconBlock::Merge(_)
|
||||
| SignedBeaconBlock::Capella(_) => {
|
||||
crate::publish_pubsub_message(&sender, PubsubMessage::BeaconBlock(block.clone()))
|
||||
.map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish))?;
|
||||
}
|
||||
SignedBeaconBlock::Deneb(_) => {
|
||||
crate::publish_pubsub_message(&sender, PubsubMessage::BeaconBlock(block.clone()))
|
||||
.map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish))?;
|
||||
if let Some(signed_blobs) = blobs_opt {
|
||||
for (blob_index, blob) in signed_blobs.into_iter().enumerate() {
|
||||
crate::publish_pubsub_message(
|
||||
&sender,
|
||||
PubsubMessage::BlobSidecar(Box::new((blob_index as u64, blob))),
|
||||
)
|
||||
.map_err(|_| {
|
||||
BlockError::BeaconChainError(BeaconChainError::UnableToPublish)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
};
|
||||
|
||||
/* only publish if gossip- and consensus-valid and equivocation-free */
|
||||
let chain_clone = chain.clone();
|
||||
let slot = block.message().slot();
|
||||
let proposer_index = block.message().proposer_index();
|
||||
let sender_clone = network_tx.clone();
|
||||
let log_clone = log.clone();
|
||||
|
||||
// We can clone this because the blobs are `Arc`'d in `BlockContents`, but the block is not,
|
||||
// so we avoid cloning the block at this point.
|
||||
let blobs_opt = block_contents.inner_blobs();
|
||||
|
||||
/* if we can form a `GossipVerifiedBlock`, we've passed our basic gossip checks */
|
||||
let gossip_verified_block = match block.into_gossip_verified_block(&chain) {
|
||||
Ok(b) => b,
|
||||
Err(BlockError::BlockIsAlreadyKnown) => {
|
||||
// Allow the status code for duplicate blocks to be overridden based on config.
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ErrorMessage {
|
||||
code: duplicate_status_code.as_u16(),
|
||||
message: "duplicate block".to_string(),
|
||||
stacktraces: vec![],
|
||||
}),
|
||||
duplicate_status_code,
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
log,
|
||||
"Not publishing block - not gossip verified";
|
||||
"slot" => beacon_block.slot(),
|
||||
"error" => ?e
|
||||
);
|
||||
return Err(warp_utils::reject::custom_bad_request(e.to_string()));
|
||||
}
|
||||
};
|
||||
let (gossip_verified_block, gossip_verified_blobs) =
|
||||
match block_contents.into_gossip_verified_block(&chain) {
|
||||
Ok(b) => b,
|
||||
Err(BlockContentsError::BlockError(BlockError::BlockIsAlreadyKnown)) => {
|
||||
// Allow the status code for duplicate blocks to be overridden based on config.
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ErrorMessage {
|
||||
code: duplicate_status_code.as_u16(),
|
||||
message: "duplicate block".to_string(),
|
||||
stacktraces: vec![],
|
||||
}),
|
||||
duplicate_status_code,
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
log,
|
||||
"Not publishing block - not gossip verified";
|
||||
"slot" => slot,
|
||||
"error" => ?e
|
||||
);
|
||||
return Err(warp_utils::reject::custom_bad_request(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
// Clone here, so we can take advantage of the `Arc`. The block in `BlockContents` is not,
|
||||
// `Arc`'d but blobs are.
|
||||
let block = gossip_verified_block.block.block_cloned();
|
||||
|
||||
let block_root = block_root.unwrap_or(gossip_verified_block.block_root);
|
||||
|
||||
if let BroadcastValidation::Gossip = validation_level {
|
||||
publish_block(
|
||||
beacon_block.clone(),
|
||||
network_tx.clone(),
|
||||
block.clone(),
|
||||
blobs_opt.clone(),
|
||||
sender_clone.clone(),
|
||||
log.clone(),
|
||||
seen_timestamp,
|
||||
)
|
||||
.map_err(|_| warp_utils::reject::custom_server_error("unable to publish".into()))?;
|
||||
}
|
||||
|
||||
/* only publish if gossip- and consensus-valid and equivocation-free */
|
||||
let chain_clone = chain.clone();
|
||||
let block_clone = beacon_block.clone();
|
||||
let log_clone = log.clone();
|
||||
let sender_clone = network_tx.clone();
|
||||
let block_clone = block.clone();
|
||||
|
||||
let publish_fn = move || match validation_level {
|
||||
BroadcastValidation::Gossip => Ok(()),
|
||||
BroadcastValidation::Consensus => {
|
||||
publish_block(block_clone, sender_clone, log_clone, seen_timestamp)
|
||||
}
|
||||
BroadcastValidation::Consensus => publish_block(
|
||||
block_clone,
|
||||
blobs_opt,
|
||||
sender_clone,
|
||||
log_clone,
|
||||
seen_timestamp,
|
||||
),
|
||||
BroadcastValidation::ConsensusAndEquivocation => {
|
||||
if chain_clone
|
||||
.observed_block_producers
|
||||
@@ -140,11 +185,27 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
|
||||
);
|
||||
Err(BlockError::Slashable)
|
||||
} else {
|
||||
publish_block(block_clone, sender_clone, log_clone, seen_timestamp)
|
||||
publish_block(
|
||||
block_clone,
|
||||
blobs_opt,
|
||||
sender_clone,
|
||||
log_clone,
|
||||
seen_timestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(gossip_verified_blobs) = gossip_verified_blobs {
|
||||
for blob in gossip_verified_blobs {
|
||||
if let Err(e) = chain.process_blob(blob).await {
|
||||
return Err(warp_utils::reject::custom_bad_request(format!(
|
||||
"Invalid blob: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match chain
|
||||
.process_block(
|
||||
block_root,
|
||||
@@ -154,20 +215,20 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(root) => {
|
||||
Ok(AvailabilityProcessingStatus::Imported(root)) => {
|
||||
info!(
|
||||
log,
|
||||
"Valid block from HTTP API";
|
||||
"block_delay" => ?delay,
|
||||
"root" => format!("{}", root),
|
||||
"proposer_index" => beacon_block.message().proposer_index(),
|
||||
"slot" => beacon_block.slot(),
|
||||
"proposer_index" => proposer_index,
|
||||
"slot" =>slot,
|
||||
);
|
||||
|
||||
// Notify the validator monitor.
|
||||
chain.validator_monitor.read().register_api_block(
|
||||
seen_timestamp,
|
||||
beacon_block.message(),
|
||||
block.message(),
|
||||
root,
|
||||
&chain.slot_clock,
|
||||
);
|
||||
@@ -180,17 +241,23 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
|
||||
// blocks built with builders we consider the broadcast time to be
|
||||
// when the blinded block is published to the builder.
|
||||
if is_locally_built_block {
|
||||
late_block_logging(
|
||||
&chain,
|
||||
seen_timestamp,
|
||||
beacon_block.message(),
|
||||
root,
|
||||
"local",
|
||||
&log,
|
||||
)
|
||||
late_block_logging(&chain, seen_timestamp, block.message(), root, "local", &log)
|
||||
}
|
||||
Ok(warp::reply().into_response())
|
||||
}
|
||||
Ok(AvailabilityProcessingStatus::MissingComponents(_, block_root)) => {
|
||||
let msg = format!("Missing parts of block with root {:?}", block_root);
|
||||
if let BroadcastValidation::Gossip = validation_level {
|
||||
Err(warp_utils::reject::broadcast_without_import(msg))
|
||||
} else {
|
||||
error!(
|
||||
log,
|
||||
"Invalid block provided to HTTP API";
|
||||
"reason" => &msg
|
||||
);
|
||||
Err(warp_utils::reject::custom_bad_request(msg))
|
||||
}
|
||||
}
|
||||
Err(BlockError::BeaconChainError(BeaconChainError::UnableToPublish)) => {
|
||||
Err(warp_utils::reject::custom_server_error(
|
||||
"unable to publish to network channel".to_string(),
|
||||
@@ -220,16 +287,16 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
|
||||
/// Handles a request from the HTTP API for blinded blocks. This converts blinded blocks into full
|
||||
/// blocks before publishing.
|
||||
pub async fn publish_blinded_block<T: BeaconChainTypes>(
|
||||
block: SignedBeaconBlock<T::EthSpec, BlindedPayload<T::EthSpec>>,
|
||||
block_contents: SignedBlockContents<T::EthSpec, BlindedPayload<T::EthSpec>>,
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
network_tx: &UnboundedSender<NetworkMessage<T::EthSpec>>,
|
||||
log: Logger,
|
||||
validation_level: BroadcastValidation,
|
||||
duplicate_status_code: StatusCode,
|
||||
) -> Result<Response, Rejection> {
|
||||
let block_root = block.canonical_root();
|
||||
let full_block: ProvenancedBlock<T, Arc<SignedBeaconBlock<T::EthSpec>>> =
|
||||
reconstruct_block(chain.clone(), block_root, block, log.clone()).await?;
|
||||
let block_root = block_contents.signed_block().canonical_root();
|
||||
let full_block: ProvenancedBlock<T, SignedBlockContents<T::EthSpec>> =
|
||||
reconstruct_block(chain.clone(), block_root, block_contents, log.clone()).await?;
|
||||
publish_block::<T, _>(
|
||||
Some(block_root),
|
||||
full_block,
|
||||
@@ -248,28 +315,28 @@ pub async fn publish_blinded_block<T: BeaconChainTypes>(
|
||||
pub async fn reconstruct_block<T: BeaconChainTypes>(
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
block_root: Hash256,
|
||||
block: SignedBeaconBlock<T::EthSpec, BlindedPayload<T::EthSpec>>,
|
||||
block_contents: SignedBlockContents<T::EthSpec, BlindedPayload<T::EthSpec>>,
|
||||
log: Logger,
|
||||
) -> Result<ProvenancedBlock<T, Arc<SignedBeaconBlock<T::EthSpec>>>, Rejection> {
|
||||
) -> Result<ProvenancedBlock<T, SignedBlockContents<T::EthSpec>>, Rejection> {
|
||||
let block = block_contents.signed_block();
|
||||
let full_payload_opt = if let Ok(payload_header) = block.message().body().execution_payload() {
|
||||
let el = chain.execution_layer.as_ref().ok_or_else(|| {
|
||||
warp_utils::reject::custom_server_error("Missing execution layer".to_string())
|
||||
})?;
|
||||
|
||||
// If the execution block hash is zero, use an empty payload.
|
||||
let full_payload = if payload_header.block_hash() == ExecutionBlockHash::zero() {
|
||||
let payload = FullPayload::default_at_fork(
|
||||
chain
|
||||
.spec
|
||||
.fork_name_at_epoch(block.slot().epoch(T::EthSpec::slots_per_epoch())),
|
||||
)
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
"Default payload construction error: {e:?}"
|
||||
))
|
||||
})?
|
||||
.into();
|
||||
ProvenancedPayload::Local(payload)
|
||||
let full_payload_contents = if payload_header.block_hash() == ExecutionBlockHash::zero() {
|
||||
let fork_name = chain
|
||||
.spec
|
||||
.fork_name_at_epoch(block.slot().epoch(T::EthSpec::slots_per_epoch()));
|
||||
if fork_name == ForkName::Merge {
|
||||
let payload: FullPayload<T::EthSpec> = FullPayloadMerge::default().into();
|
||||
ProvenancedPayload::Local(FullPayloadContents::Payload(payload.into()))
|
||||
} else {
|
||||
Err(warp_utils::reject::custom_server_error(
|
||||
"Failed to construct full payload - block hash must be non-zero after Bellatrix.".to_string()
|
||||
))?
|
||||
}
|
||||
// If we already have an execution payload with this transactions root cached, use it.
|
||||
} else if let Some(cached_payload) =
|
||||
el.get_payload_by_root(&payload_header.tree_hash_root())
|
||||
@@ -293,7 +360,7 @@ pub async fn reconstruct_block<T: BeaconChainTypes>(
|
||||
);
|
||||
|
||||
let full_payload = el
|
||||
.propose_blinded_beacon_block(block_root, &block)
|
||||
.propose_blinded_beacon_block(block_root, &block_contents)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!(
|
||||
@@ -305,7 +372,7 @@ pub async fn reconstruct_block<T: BeaconChainTypes>(
|
||||
ProvenancedPayload::Builder(full_payload)
|
||||
};
|
||||
|
||||
Some(full_payload)
|
||||
Some(full_payload_contents)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -313,21 +380,18 @@ pub async fn reconstruct_block<T: BeaconChainTypes>(
|
||||
match full_payload_opt {
|
||||
// A block without a payload is pre-merge and we consider it locally
|
||||
// built.
|
||||
None => block
|
||||
.try_into_full_block(None)
|
||||
.map(Arc::new)
|
||||
None => block_contents
|
||||
.try_into_full_block_and_blobs(None)
|
||||
.map(ProvenancedBlock::local),
|
||||
Some(ProvenancedPayload::Local(full_payload)) => block
|
||||
.try_into_full_block(Some(full_payload))
|
||||
.map(Arc::new)
|
||||
Some(ProvenancedPayload::Local(full_payload_contents)) => block_contents
|
||||
.try_into_full_block_and_blobs(Some(full_payload_contents))
|
||||
.map(ProvenancedBlock::local),
|
||||
Some(ProvenancedPayload::Builder(full_payload)) => block
|
||||
.try_into_full_block(Some(full_payload))
|
||||
.map(Arc::new)
|
||||
Some(ProvenancedPayload::Builder(full_payload_contents)) => block_contents
|
||||
.try_into_full_block_and_blobs(Some(full_payload_contents))
|
||||
.map(ProvenancedBlock::builder),
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
warp_utils::reject::custom_server_error("Unable to add payload to block".to_string())
|
||||
.map_err(|e| {
|
||||
warp_utils::reject::custom_server_error(format!("Unable to add payload to block: {e:?}"))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::{Config, Context};
|
||||
use beacon_chain::{
|
||||
test_utils::{
|
||||
BeaconChainHarness, BoxedMutator, Builder as HarnessBuilder, EphemeralHarnessType,
|
||||
},
|
||||
test_utils::{BeaconChainHarness, BoxedMutator, Builder, EphemeralHarnessType},
|
||||
BeaconChain, BeaconChainTypes,
|
||||
};
|
||||
use beacon_processor::{BeaconProcessor, BeaconProcessorChannels, BeaconProcessorConfig};
|
||||
@@ -53,9 +51,8 @@ pub struct ApiServer<E: EthSpec, SFut: Future<Output = ()>> {
|
||||
pub external_peer_id: PeerId,
|
||||
}
|
||||
|
||||
type Initializer<E> = Box<
|
||||
dyn FnOnce(HarnessBuilder<EphemeralHarnessType<E>>) -> HarnessBuilder<EphemeralHarnessType<E>>,
|
||||
>;
|
||||
type HarnessBuilder<E> = Builder<EphemeralHarnessType<E>>;
|
||||
type Initializer<E> = Box<dyn FnOnce(HarnessBuilder<E>) -> HarnessBuilder<E>>;
|
||||
type Mutator<E> = BoxedMutator<E, MemoryStore<E>, MemoryStore<E>>;
|
||||
|
||||
impl<E: EthSpec> InteractiveTester<E> {
|
||||
|
||||
Reference in New Issue
Block a user