mirror of
https://github.com/sigp/lighthouse.git
synced 2026-06-15 09:48:20 +00:00
Merge remote-tracking branch 'origin/unstable' into tree-states
This commit is contained in:
@@ -3,12 +3,12 @@ name = "http_api"
|
||||
version = "0.1.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2021"
|
||||
autotests = false # using a single test binary compiles faster
|
||||
autotests = false # using a single test binary compiles faster
|
||||
|
||||
[dependencies]
|
||||
warp = { version = "0.3.2", features = ["tls"] }
|
||||
serde = { version = "1.0.116", features = ["derive"] }
|
||||
tokio = { version = "1.14.0", features = ["macros","sync"] }
|
||||
tokio = { version = "1.14.0", features = ["macros", "sync"] }
|
||||
tokio-stream = { version = "0.1.3", features = ["sync"] }
|
||||
types = { path = "../../consensus/types" }
|
||||
hex = "0.4.2"
|
||||
@@ -27,9 +27,9 @@ slot_clock = { path = "../../common/slot_clock" }
|
||||
ethereum_ssz = "0.5.0"
|
||||
bs58 = "0.4.0"
|
||||
futures = "0.3.8"
|
||||
execution_layer = {path = "../execution_layer"}
|
||||
execution_layer = { path = "../execution_layer" }
|
||||
parking_lot = "0.12.0"
|
||||
safe_arith = {path = "../../consensus/safe_arith"}
|
||||
safe_arith = { path = "../../consensus/safe_arith" }
|
||||
task_executor = { path = "../../common/task_executor" }
|
||||
lru = "0.7.7"
|
||||
tree_hash = "0.5.0"
|
||||
@@ -40,8 +40,10 @@ logging = { path = "../../common/logging" }
|
||||
ethereum_serde_utils = "0.5.0"
|
||||
operation_pool = { path = "../operation_pool" }
|
||||
sensitive_url = { path = "../../common/sensitive_url" }
|
||||
unused_port = {path = "../../common/unused_port"}
|
||||
unused_port = { path = "../../common/unused_port" }
|
||||
store = { path = "../store" }
|
||||
bytes = "1.1.0"
|
||||
beacon_processor = { path = "../beacon_processor" }
|
||||
|
||||
[dev-dependencies]
|
||||
environment = { path = "../../lighthouse/environment" }
|
||||
@@ -51,4 +53,4 @@ genesis = { path = "../genesis" }
|
||||
|
||||
[[test]]
|
||||
name = "bn_http_api_tests"
|
||||
path = "tests/main.rs"
|
||||
path = "tests/main.rs"
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
//! Contains the handler for the `GET validator/duties/attester/{epoch}` endpoint.
|
||||
|
||||
use crate::state_id::StateId;
|
||||
use beacon_chain::{
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes, MAXIMUM_GOSSIP_CLOCK_DISPARITY,
|
||||
};
|
||||
use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes};
|
||||
use eth2::types::{self as api_types};
|
||||
use slot_clock::SlotClock;
|
||||
use state_processing::state_advance::partial_state_advance;
|
||||
@@ -30,12 +28,11 @@ pub fn attester_duties<T: BeaconChainTypes>(
|
||||
// will equal `current_epoch + 1`
|
||||
let tolerant_current_epoch = chain
|
||||
.slot_clock
|
||||
.now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY)
|
||||
.now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity())
|
||||
.ok_or_else(|| warp_utils::reject::custom_server_error("unable to read slot clock".into()))?
|
||||
.epoch(T::EthSpec::slots_per_epoch());
|
||||
|
||||
if request_epoch == current_epoch
|
||||
|| request_epoch == tolerant_current_epoch
|
||||
|| request_epoch == current_epoch + 1
|
||||
|| request_epoch == tolerant_current_epoch + 1
|
||||
{
|
||||
@@ -46,7 +43,7 @@ pub fn attester_duties<T: BeaconChainTypes>(
|
||||
request_epoch, current_epoch
|
||||
)))
|
||||
} else {
|
||||
// request_epoch < current_epoch
|
||||
// request_epoch < current_epoch, in fact we only allow `request_epoch == current_epoch-1` in this case
|
||||
compute_historic_attester_duties(request_epoch, request_indices, chain)
|
||||
}
|
||||
}
|
||||
|
||||
72
beacon_node/http_api/src/builder_states.rs
Normal file
72
beacon_node/http_api/src/builder_states.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use crate::StateId;
|
||||
use beacon_chain::{BeaconChain, BeaconChainTypes};
|
||||
use safe_arith::SafeArith;
|
||||
use state_processing::per_block_processing::get_expected_withdrawals;
|
||||
use state_processing::state_advance::partial_state_advance;
|
||||
use std::sync::Arc;
|
||||
use types::{BeaconState, EthSpec, ForkName, Slot, Withdrawals};
|
||||
|
||||
const MAX_EPOCH_LOOKAHEAD: u64 = 2;
|
||||
|
||||
/// Get the withdrawals computed from the specified state, that will be included in the block
|
||||
/// that gets built on the specified state.
|
||||
pub fn get_next_withdrawals<T: BeaconChainTypes>(
|
||||
chain: &Arc<BeaconChain<T>>,
|
||||
mut state: BeaconState<T::EthSpec>,
|
||||
state_id: StateId,
|
||||
proposal_slot: Slot,
|
||||
) -> Result<Withdrawals<T::EthSpec>, warp::Rejection> {
|
||||
get_next_withdrawals_sanity_checks(chain, &state, proposal_slot)?;
|
||||
|
||||
// advance the state to the epoch of the proposal slot.
|
||||
let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
let (state_root, _, _) = state_id.root(chain)?;
|
||||
if proposal_epoch != state.current_epoch() {
|
||||
if let Err(e) =
|
||||
partial_state_advance(&mut state, Some(state_root), proposal_slot, &chain.spec)
|
||||
{
|
||||
return Err(warp_utils::reject::custom_server_error(format!(
|
||||
"failed to advance to the epoch of the proposal slot: {:?}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
match get_expected_withdrawals(&state, &chain.spec) {
|
||||
Ok(withdrawals) => Ok(withdrawals),
|
||||
Err(e) => Err(warp_utils::reject::custom_server_error(format!(
|
||||
"failed to get expected withdrawal: {:?}",
|
||||
e
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_next_withdrawals_sanity_checks<T: BeaconChainTypes>(
|
||||
chain: &BeaconChain<T>,
|
||||
state: &BeaconState<T::EthSpec>,
|
||||
proposal_slot: Slot,
|
||||
) -> Result<(), warp::Rejection> {
|
||||
if proposal_slot <= state.slot() {
|
||||
return Err(warp_utils::reject::custom_bad_request(
|
||||
"proposal slot must be greater than the pre-state slot".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let fork = chain.spec.fork_name_at_slot::<T::EthSpec>(proposal_slot);
|
||||
if let ForkName::Base | ForkName::Altair | ForkName::Merge = fork {
|
||||
return Err(warp_utils::reject::custom_bad_request(
|
||||
"the specified state is a pre-capella state.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let look_ahead_limit = MAX_EPOCH_LOOKAHEAD
|
||||
.safe_mul(T::EthSpec::slots_per_epoch())
|
||||
.map_err(warp_utils::reject::arith_error)?;
|
||||
if proposal_slot >= state.slot() + look_ahead_limit {
|
||||
return Err(warp_utils::reject::custom_bad_request(format!(
|
||||
"proposal slot is greater than or equal to the look ahead limit: {look_ahead_limit}"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
use crate::state_id::StateId;
|
||||
use beacon_chain::{
|
||||
beacon_proposer_cache::{compute_proposer_duties_from_head, ensure_state_is_in_epoch},
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes, MAXIMUM_GOSSIP_CLOCK_DISPARITY,
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes,
|
||||
};
|
||||
use eth2::types::{self as api_types};
|
||||
use safe_arith::SafeArith;
|
||||
@@ -33,7 +33,7 @@ pub fn proposer_duties<T: BeaconChainTypes>(
|
||||
// will equal `current_epoch + 1`
|
||||
let tolerant_current_epoch = chain
|
||||
.slot_clock
|
||||
.now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY)
|
||||
.now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity())
|
||||
.ok_or_else(|| warp_utils::reject::custom_server_error("unable to read slot clock".into()))?
|
||||
.epoch(T::EthSpec::slots_per_epoch());
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use beacon_chain::{
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, IntoGossipVerifiedBlock,
|
||||
NotifyExecutionLayer,
|
||||
};
|
||||
use eth2::types::BroadcastValidation;
|
||||
use eth2::types::{BroadcastValidation, ErrorMessage};
|
||||
use execution_layer::ProvenancedPayload;
|
||||
use lighthouse_network::PubsubMessage;
|
||||
use network::NetworkMessage;
|
||||
@@ -19,7 +19,8 @@ use types::{
|
||||
AbstractExecPayload, BeaconBlockRef, BlindedPayload, EthSpec, ExecPayload, ExecutionBlockHash,
|
||||
FullPayload, Hash256, SignedBeaconBlock,
|
||||
};
|
||||
use warp::Rejection;
|
||||
use warp::http::StatusCode;
|
||||
use warp::{reply::Response, Rejection, Reply};
|
||||
|
||||
pub enum ProvenancedBlock<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>> {
|
||||
/// The payload was built using a local EE.
|
||||
@@ -47,7 +48,8 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
|
||||
network_tx: &UnboundedSender<NetworkMessage<T::EthSpec>>,
|
||||
log: Logger,
|
||||
validation_level: BroadcastValidation,
|
||||
) -> Result<(), Rejection> {
|
||||
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),
|
||||
@@ -75,10 +77,30 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
|
||||
};
|
||||
|
||||
/* if we can form a `GossipVerifiedBlock`, we've passed our basic gossip checks */
|
||||
let gossip_verified_block = block.into_gossip_verified_block(&chain).map_err(|e| {
|
||||
warn!(log, "Not publishing block, not gossip verified"; "slot" => beacon_block.slot(), "error" => ?e);
|
||||
warp_utils::reject::custom_bad_request(e.to_string())
|
||||
})?;
|
||||
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 block_root = block_root.unwrap_or(gossip_verified_block.block_root);
|
||||
|
||||
@@ -167,8 +189,7 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
|
||||
&log,
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(warp::reply().into_response())
|
||||
}
|
||||
Err(BlockError::BeaconChainError(BeaconChainError::UnableToPublish)) => {
|
||||
Err(warp_utils::reject::custom_server_error(
|
||||
@@ -178,10 +199,6 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
|
||||
Err(BlockError::Slashable) => Err(warp_utils::reject::custom_bad_request(
|
||||
"proposal for this slot and proposer has already been seen".to_string(),
|
||||
)),
|
||||
Err(BlockError::BlockIsAlreadyKnown) => {
|
||||
info!(log, "Block from HTTP API already known"; "block" => ?block_root);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
if let BroadcastValidation::Gossip = validation_level {
|
||||
Err(warp_utils::reject::broadcast_without_import(format!("{e}")))
|
||||
@@ -208,7 +225,8 @@ pub async fn publish_blinded_block<T: BeaconChainTypes>(
|
||||
network_tx: &UnboundedSender<NetworkMessage<T::EthSpec>>,
|
||||
log: Logger,
|
||||
validation_level: BroadcastValidation,
|
||||
) -> Result<(), Rejection> {
|
||||
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?;
|
||||
@@ -219,6 +237,7 @@ pub async fn publish_blinded_block<T: BeaconChainTypes>(
|
||||
network_tx,
|
||||
log,
|
||||
validation_level,
|
||||
duplicate_status_code,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -89,9 +89,7 @@ impl StateId {
|
||||
} else {
|
||||
// This block is either old and finalized, or recent and unfinalized, so
|
||||
// it's safe to fallback to the optimistic status of the finalized block.
|
||||
chain
|
||||
.canonical_head
|
||||
.fork_choice_read_lock()
|
||||
fork_choice
|
||||
.is_optimistic_or_invalid_block(&hot_summary.latest_block_root)
|
||||
.map_err(BeaconChainError::ForkChoiceError)
|
||||
.map_err(warp_utils::reject::beacon_chain_error)?
|
||||
|
||||
@@ -6,7 +6,7 @@ use beacon_chain::sync_committee_verification::{
|
||||
};
|
||||
use beacon_chain::{
|
||||
validator_monitor::timestamp_now, BeaconChain, BeaconChainError, BeaconChainTypes,
|
||||
StateSkipConfig, MAXIMUM_GOSSIP_CLOCK_DISPARITY,
|
||||
StateSkipConfig,
|
||||
};
|
||||
use eth2::types::{self as api_types};
|
||||
use lighthouse_network::PubsubMessage;
|
||||
@@ -85,7 +85,7 @@ fn duties_from_state_load<T: BeaconChainTypes>(
|
||||
let current_epoch = chain.epoch()?;
|
||||
let tolerant_current_epoch = chain
|
||||
.slot_clock
|
||||
.now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY)
|
||||
.now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity())
|
||||
.ok_or(BeaconChainError::UnableToReadSlot)?
|
||||
.epoch(T::EthSpec::slots_per_epoch());
|
||||
|
||||
|
||||
192
beacon_node/http_api/src/task_spawner.rs
Normal file
192
beacon_node/http_api/src/task_spawner.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use beacon_processor::{BeaconProcessorSend, BlockingOrAsync, Work, WorkEvent};
|
||||
use serde::Serialize;
|
||||
use std::future::Future;
|
||||
use tokio::sync::{mpsc::error::TrySendError, oneshot};
|
||||
use types::EthSpec;
|
||||
use warp::reply::{Reply, Response};
|
||||
|
||||
/// Maps a request to a queue in the `BeaconProcessor`.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Priority {
|
||||
/// The highest priority.
|
||||
P0,
|
||||
/// The lowest priority.
|
||||
P1,
|
||||
}
|
||||
|
||||
impl Priority {
|
||||
/// Wrap `self` in a `WorkEvent` with an appropriate priority.
|
||||
fn work_event<E: EthSpec>(&self, process_fn: BlockingOrAsync) -> WorkEvent<E> {
|
||||
let work = match self {
|
||||
Priority::P0 => Work::ApiRequestP0(process_fn),
|
||||
Priority::P1 => Work::ApiRequestP1(process_fn),
|
||||
};
|
||||
WorkEvent {
|
||||
drop_during_sync: false,
|
||||
work,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns tasks on the `BeaconProcessor` or directly on the tokio executor.
|
||||
pub struct TaskSpawner<E: EthSpec> {
|
||||
/// Used to send tasks to the `BeaconProcessor`. The tokio executor will be
|
||||
/// used if this is `None`.
|
||||
beacon_processor_send: Option<BeaconProcessorSend<E>>,
|
||||
}
|
||||
|
||||
/// Convert a warp `Rejection` into a `Response`.
|
||||
///
|
||||
/// This function should *always* be used to convert rejections into responses. This prevents warp
|
||||
/// from trying to backtrack in strange ways. See: https://github.com/sigp/lighthouse/issues/3404
|
||||
pub async fn convert_rejection<T: Reply>(res: Result<T, warp::Rejection>) -> Response {
|
||||
match res {
|
||||
Ok(response) => response.into_response(),
|
||||
Err(e) => match warp_utils::reject::handle_rejection(e).await {
|
||||
Ok(reply) => reply.into_response(),
|
||||
Err(_) => warp::reply::with_status(
|
||||
warp::reply::json(&"unhandled error"),
|
||||
eth2::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
.into_response(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: EthSpec> TaskSpawner<E> {
|
||||
pub fn new(beacon_processor_send: Option<BeaconProcessorSend<E>>) -> Self {
|
||||
Self {
|
||||
beacon_processor_send,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a "blocking" (non-async) task which returns a `Response`.
|
||||
pub async fn blocking_response_task<F, T>(self, priority: Priority, func: F) -> Response
|
||||
where
|
||||
F: FnOnce() -> Result<T, warp::Rejection> + Send + Sync + 'static,
|
||||
T: Reply + Send + 'static,
|
||||
{
|
||||
if let Some(beacon_processor_send) = &self.beacon_processor_send {
|
||||
// Create a closure that will execute `func` and send the result to
|
||||
// a channel held by this thread.
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let process_fn = move || {
|
||||
// Execute the function, collect the return value.
|
||||
let func_result = func();
|
||||
// Send the result down the channel. Ignore any failures; the
|
||||
// send can only fail if the receiver is dropped.
|
||||
let _ = tx.send(func_result);
|
||||
};
|
||||
|
||||
// Send the function to the beacon processor for execution at some arbitrary time.
|
||||
let result = send_to_beacon_processor(
|
||||
beacon_processor_send,
|
||||
priority,
|
||||
BlockingOrAsync::Blocking(Box::new(process_fn)),
|
||||
rx,
|
||||
)
|
||||
.await
|
||||
.and_then(|x| x);
|
||||
convert_rejection(result).await
|
||||
} else {
|
||||
// There is no beacon processor so spawn a task directly on the
|
||||
// tokio executor.
|
||||
convert_rejection(warp_utils::task::blocking_response_task(func).await).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a "blocking" (non-async) task which returns a JSON-serializable
|
||||
/// object.
|
||||
pub async fn blocking_json_task<F, T>(self, priority: Priority, func: F) -> Response
|
||||
where
|
||||
F: FnOnce() -> Result<T, warp::Rejection> + Send + Sync + 'static,
|
||||
T: Serialize + Send + 'static,
|
||||
{
|
||||
let func = || func().map(|t| warp::reply::json(&t).into_response());
|
||||
self.blocking_response_task(priority, func).await
|
||||
}
|
||||
|
||||
/// Executes an async task which may return a `Rejection`, which will be converted to a response.
|
||||
pub async fn spawn_async_with_rejection(
|
||||
self,
|
||||
priority: Priority,
|
||||
func: impl Future<Output = Result<Response, warp::Rejection>> + Send + Sync + 'static,
|
||||
) -> Response {
|
||||
let result = self
|
||||
.spawn_async_with_rejection_no_conversion(priority, func)
|
||||
.await;
|
||||
convert_rejection(result).await
|
||||
}
|
||||
|
||||
/// Same as `spawn_async_with_rejection` but returning a result with the unhandled rejection.
|
||||
///
|
||||
/// If you call this function you MUST convert the rejection to a response and not let it
|
||||
/// propagate into Warp's filters. See `convert_rejection`.
|
||||
pub async fn spawn_async_with_rejection_no_conversion(
|
||||
self,
|
||||
priority: Priority,
|
||||
func: impl Future<Output = Result<Response, warp::Rejection>> + Send + Sync + 'static,
|
||||
) -> Result<Response, warp::Rejection> {
|
||||
if let Some(beacon_processor_send) = &self.beacon_processor_send {
|
||||
// Create a wrapper future that will execute `func` and send the
|
||||
// result to a channel held by this thread.
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let process_fn = async move {
|
||||
// Await the future, collect the return value.
|
||||
let func_result = func.await;
|
||||
// Send the result down the channel. Ignore any failures; the
|
||||
// send can only fail if the receiver is dropped.
|
||||
let _ = tx.send(func_result);
|
||||
};
|
||||
|
||||
// Send the function to the beacon processor for execution at some arbitrary time.
|
||||
send_to_beacon_processor(
|
||||
beacon_processor_send,
|
||||
priority,
|
||||
BlockingOrAsync::Async(Box::pin(process_fn)),
|
||||
rx,
|
||||
)
|
||||
.await
|
||||
.and_then(|x| x)
|
||||
} else {
|
||||
// There is no beacon processor so spawn a task directly on the
|
||||
// tokio executor.
|
||||
tokio::task::spawn(func)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
warp_utils::reject::custom_server_error("Tokio failed to spawn task".into())
|
||||
})
|
||||
.and_then(|x| x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a task to the beacon processor and await execution.
|
||||
///
|
||||
/// If the task is not executed, return an `Err` with an error message
|
||||
/// for the API consumer.
|
||||
async fn send_to_beacon_processor<E: EthSpec, T>(
|
||||
beacon_processor_send: &BeaconProcessorSend<E>,
|
||||
priority: Priority,
|
||||
process_fn: BlockingOrAsync,
|
||||
rx: oneshot::Receiver<T>,
|
||||
) -> Result<T, warp::Rejection> {
|
||||
let error_message = match beacon_processor_send.try_send(priority.work_event(process_fn)) {
|
||||
Ok(()) => {
|
||||
match rx.await {
|
||||
// The beacon processor executed the task and sent a result.
|
||||
Ok(func_result) => return Ok(func_result),
|
||||
// The beacon processor dropped the channel without sending a
|
||||
// result. The beacon processor dropped this task because its
|
||||
// queues are full or it's shutting down.
|
||||
Err(_) => "The task did not execute. The server is overloaded or shutting down.",
|
||||
}
|
||||
}
|
||||
Err(TrySendError::Full(_)) => "The task was dropped. The server is overloaded.",
|
||||
Err(TrySendError::Closed(_)) => "The task was dropped. The server is shutting down.",
|
||||
};
|
||||
|
||||
Err(warp_utils::reject::custom_server_error(
|
||||
error_message.to_string(),
|
||||
))
|
||||
}
|
||||
@@ -5,16 +5,14 @@ use beacon_chain::{
|
||||
},
|
||||
BeaconChain, BeaconChainTypes,
|
||||
};
|
||||
use beacon_processor::{BeaconProcessor, BeaconProcessorChannels, BeaconProcessorConfig};
|
||||
use directory::DEFAULT_ROOT_DIR;
|
||||
use eth2::{BeaconNodeHttpClient, Timeouts};
|
||||
use lighthouse_network::{
|
||||
discv5::enr::{CombinedKey, EnrBuilder},
|
||||
libp2p::{
|
||||
core::connection::ConnectionId,
|
||||
swarm::{
|
||||
behaviour::{ConnectionEstablished, FromSwarm},
|
||||
NetworkBehaviour,
|
||||
},
|
||||
libp2p::swarm::{
|
||||
behaviour::{ConnectionEstablished, FromSwarm},
|
||||
ConnectionId, NetworkBehaviour,
|
||||
},
|
||||
rpc::methods::{MetaData, MetaDataV2},
|
||||
types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield, SyncState},
|
||||
@@ -25,11 +23,11 @@ use network::{NetworkReceivers, NetworkSenders};
|
||||
use sensitive_url::SensitiveUrl;
|
||||
use slog::Logger;
|
||||
use std::future::Future;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use store::MemoryStore;
|
||||
use tokio::sync::oneshot;
|
||||
use task_executor::test_utils::TestRuntime;
|
||||
use types::{ChainSpec, EthSpec};
|
||||
|
||||
pub const TCP_PORT: u16 = 42;
|
||||
@@ -42,7 +40,6 @@ pub struct InteractiveTester<E: EthSpec> {
|
||||
pub harness: BeaconChainHarness<EphemeralHarnessType<E>>,
|
||||
pub client: BeaconNodeHttpClient,
|
||||
pub network_rx: NetworkReceivers<E>,
|
||||
_server_shutdown: oneshot::Sender<()>,
|
||||
}
|
||||
|
||||
/// The result of calling `create_api_server`.
|
||||
@@ -51,7 +48,6 @@ pub struct InteractiveTester<E: EthSpec> {
|
||||
pub struct ApiServer<E: EthSpec, SFut: Future<Output = ()>> {
|
||||
pub server: SFut,
|
||||
pub listening_socket: SocketAddr,
|
||||
pub shutdown_tx: oneshot::Sender<()>,
|
||||
pub network_rx: NetworkReceivers<E>,
|
||||
pub local_enr: Enr,
|
||||
pub external_peer_id: PeerId,
|
||||
@@ -99,10 +95,14 @@ impl<E: EthSpec> InteractiveTester<E> {
|
||||
let ApiServer {
|
||||
server,
|
||||
listening_socket,
|
||||
shutdown_tx: _server_shutdown,
|
||||
network_rx,
|
||||
..
|
||||
} = create_api_server(harness.chain.clone(), harness.logger().clone()).await;
|
||||
} = create_api_server(
|
||||
harness.chain.clone(),
|
||||
&harness.runtime,
|
||||
harness.logger().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
tokio::spawn(server);
|
||||
|
||||
@@ -120,22 +120,23 @@ impl<E: EthSpec> InteractiveTester<E> {
|
||||
harness,
|
||||
client,
|
||||
network_rx,
|
||||
_server_shutdown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_api_server<T: BeaconChainTypes>(
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
test_runtime: &TestRuntime,
|
||||
log: Logger,
|
||||
) -> ApiServer<T::EthSpec, impl Future<Output = ()>> {
|
||||
// Get a random unused port.
|
||||
let port = unused_port::unused_tcp4_port().unwrap();
|
||||
create_api_server_on_port(chain, log, port).await
|
||||
create_api_server_on_port(chain, test_runtime, log, port).await
|
||||
}
|
||||
|
||||
pub async fn create_api_server_on_port<T: BeaconChainTypes>(
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
test_runtime: &TestRuntime,
|
||||
log: Logger,
|
||||
port: u16,
|
||||
) -> ApiServer<T::EthSpec, impl Future<Output = ()>> {
|
||||
@@ -170,7 +171,7 @@ pub async fn create_api_server_on_port<T: BeaconChainTypes>(
|
||||
local_addr: EXTERNAL_ADDR.parse().unwrap(),
|
||||
send_back_addr: EXTERNAL_ADDR.parse().unwrap(),
|
||||
};
|
||||
let connection_id = ConnectionId::new(1);
|
||||
let connection_id = ConnectionId::new_unchecked(1);
|
||||
pm.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished {
|
||||
peer_id,
|
||||
connection_id,
|
||||
@@ -183,36 +184,60 @@ pub async fn create_api_server_on_port<T: BeaconChainTypes>(
|
||||
let eth1_service =
|
||||
eth1::Service::new(eth1::Config::default(), log.clone(), chain.spec.clone()).unwrap();
|
||||
|
||||
let beacon_processor_config = BeaconProcessorConfig {
|
||||
// The number of workers must be greater than one. Tests which use the
|
||||
// builder workflow sometimes require an internal HTTP request in order
|
||||
// to fulfill an already in-flight HTTP request, therefore having only
|
||||
// one worker will result in a deadlock.
|
||||
max_workers: 2,
|
||||
..BeaconProcessorConfig::default()
|
||||
};
|
||||
let BeaconProcessorChannels {
|
||||
beacon_processor_tx,
|
||||
beacon_processor_rx,
|
||||
work_reprocessing_tx,
|
||||
work_reprocessing_rx,
|
||||
} = BeaconProcessorChannels::new(&beacon_processor_config);
|
||||
|
||||
let beacon_processor_send = beacon_processor_tx;
|
||||
BeaconProcessor {
|
||||
network_globals: network_globals.clone(),
|
||||
executor: test_runtime.task_executor.clone(),
|
||||
current_workers: 0,
|
||||
config: beacon_processor_config,
|
||||
log: log.clone(),
|
||||
}
|
||||
.spawn_manager(
|
||||
beacon_processor_rx,
|
||||
work_reprocessing_tx,
|
||||
work_reprocessing_rx,
|
||||
None,
|
||||
chain.slot_clock.clone(),
|
||||
chain.spec.maximum_gossip_clock_disparity(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ctx = Arc::new(Context {
|
||||
config: Config {
|
||||
enabled: true,
|
||||
listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
|
||||
listen_port: port,
|
||||
allow_origin: None,
|
||||
tls_config: None,
|
||||
allow_sync_stalled: false,
|
||||
data_dir: std::path::PathBuf::from(DEFAULT_ROOT_DIR),
|
||||
spec_fork_name: None,
|
||||
..Config::default()
|
||||
},
|
||||
chain: Some(chain),
|
||||
network_senders: Some(network_senders),
|
||||
network_globals: Some(network_globals),
|
||||
beacon_processor_send: Some(beacon_processor_send),
|
||||
eth1_service: Some(eth1_service),
|
||||
sse_logging_components: None,
|
||||
log,
|
||||
});
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
let server_shutdown = async {
|
||||
// It's not really interesting why this triggered, just that it happened.
|
||||
let _ = shutdown_rx.await;
|
||||
};
|
||||
let (listening_socket, server) = crate::serve(ctx, server_shutdown).unwrap();
|
||||
let (listening_socket, server) = crate::serve(ctx, test_runtime.task_executor.exit()).unwrap();
|
||||
|
||||
ApiServer {
|
||||
server,
|
||||
listening_socket,
|
||||
shutdown_tx,
|
||||
network_rx: network_receivers,
|
||||
local_enr: enr,
|
||||
external_peer_id: peer_id,
|
||||
|
||||
21
beacon_node/http_api/src/validator.rs
Normal file
21
beacon_node/http_api/src/validator.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes};
|
||||
use types::*;
|
||||
|
||||
/// Uses the `chain.validator_pubkey_cache` to resolve a pubkey to a validator
|
||||
/// index and then ensures that the validator exists in the given `state`.
|
||||
pub fn pubkey_to_validator_index<T: BeaconChainTypes>(
|
||||
chain: &BeaconChain<T>,
|
||||
state: &BeaconState<T::EthSpec>,
|
||||
pubkey: &PublicKeyBytes,
|
||||
) -> Result<Option<usize>, BeaconChainError> {
|
||||
chain
|
||||
.validator_index(pubkey)?
|
||||
.filter(|&index| {
|
||||
state
|
||||
.validators()
|
||||
.get(index)
|
||||
.map_or(false, |v| *v.pubkey == *pubkey)
|
||||
})
|
||||
.map(Result::Ok)
|
||||
.transpose()
|
||||
}
|
||||
@@ -175,6 +175,48 @@ pub async fn gossip_full_pass() {
|
||||
.block_is_known_to_fork_choice(&block.canonical_root()));
|
||||
}
|
||||
|
||||
// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=gossip`.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
pub async fn gossip_full_pass_ssz() {
|
||||
/* this test targets gossip-level validation */
|
||||
let validation_level: Option<BroadcastValidation> = Some(BroadcastValidation::Gossip);
|
||||
|
||||
// Validator count needs to be at least 32 or proposer boost gets set to 0 when computing
|
||||
// `validator_count // 32`.
|
||||
let validator_count = 64;
|
||||
let num_initial: u64 = 31;
|
||||
let tester = InteractiveTester::<E>::new(None, validator_count).await;
|
||||
|
||||
// Create some chain depth.
|
||||
tester.harness.advance_slot();
|
||||
tester
|
||||
.harness
|
||||
.extend_chain(
|
||||
num_initial as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
tester.harness.advance_slot();
|
||||
|
||||
let slot_a = Slot::new(num_initial);
|
||||
let slot_b = slot_a + 1;
|
||||
|
||||
let state_a = tester.harness.get_current_state();
|
||||
let (block, _): (SignedBeaconBlock<E>, _) = tester.harness.make_block(state_a, slot_b).await;
|
||||
|
||||
let response: Result<(), eth2::Error> = tester
|
||||
.client
|
||||
.post_beacon_blocks_v2_ssz(&block, validation_level)
|
||||
.await;
|
||||
|
||||
assert!(response.is_ok());
|
||||
assert!(tester
|
||||
.harness
|
||||
.chain
|
||||
.block_is_known_to_fork_choice(&block.canonical_root()));
|
||||
}
|
||||
|
||||
/// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=consensus`.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
pub async fn consensus_invalid() {
|
||||
@@ -322,13 +364,14 @@ pub async fn consensus_partial_pass_only_consensus() {
|
||||
/* submit `block_b` which should induce equivocation */
|
||||
let channel = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let publication_result: Result<(), Rejection> = publish_block(
|
||||
let publication_result = publish_block(
|
||||
None,
|
||||
ProvenancedBlock::local(gossip_block_b.unwrap()),
|
||||
tester.harness.chain.clone(),
|
||||
&channel.0,
|
||||
test_logger,
|
||||
validation_level.unwrap(),
|
||||
StatusCode::ACCEPTED,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -599,13 +642,14 @@ pub async fn equivocation_consensus_late_equivocation() {
|
||||
|
||||
let channel = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let publication_result: Result<(), Rejection> = publish_block(
|
||||
let publication_result = publish_block(
|
||||
None,
|
||||
ProvenancedBlock::local(gossip_block_b.unwrap()),
|
||||
tester.harness.chain,
|
||||
&channel.0,
|
||||
test_logger,
|
||||
validation_level.unwrap(),
|
||||
StatusCode::ACCEPTED,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -809,6 +853,49 @@ pub async fn blinded_gossip_full_pass() {
|
||||
.block_is_known_to_fork_choice(&block.canonical_root()));
|
||||
}
|
||||
|
||||
// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=gossip`.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
pub async fn blinded_gossip_full_pass_ssz() {
|
||||
/* this test targets gossip-level validation */
|
||||
let validation_level: Option<BroadcastValidation> = Some(BroadcastValidation::Gossip);
|
||||
|
||||
// Validator count needs to be at least 32 or proposer boost gets set to 0 when computing
|
||||
// `validator_count // 32`.
|
||||
let validator_count = 64;
|
||||
let num_initial: u64 = 31;
|
||||
let tester = InteractiveTester::<E>::new(None, validator_count).await;
|
||||
|
||||
// Create some chain depth.
|
||||
tester.harness.advance_slot();
|
||||
tester
|
||||
.harness
|
||||
.extend_chain(
|
||||
num_initial as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
tester.harness.advance_slot();
|
||||
|
||||
let slot_a = Slot::new(num_initial);
|
||||
let slot_b = slot_a + 1;
|
||||
|
||||
let state_a = tester.harness.get_current_state();
|
||||
let (block, _): (SignedBlindedBeaconBlock<E>, _) =
|
||||
tester.harness.make_blinded_block(state_a, slot_b).await;
|
||||
|
||||
let response: Result<(), eth2::Error> = tester
|
||||
.client
|
||||
.post_beacon_blinded_blocks_v2_ssz(&block, validation_level)
|
||||
.await;
|
||||
|
||||
assert!(response.is_ok());
|
||||
assert!(tester
|
||||
.harness
|
||||
.chain
|
||||
.block_is_known_to_fork_choice(&block.canonical_root()));
|
||||
}
|
||||
|
||||
/// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=consensus`.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
pub async fn blinded_consensus_invalid() {
|
||||
@@ -1209,12 +1296,13 @@ pub async fn blinded_equivocation_consensus_late_equivocation() {
|
||||
|
||||
let channel = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let publication_result: Result<(), Rejection> = publish_blinded_block(
|
||||
let publication_result = publish_blinded_block(
|
||||
block_b,
|
||||
tester.harness.chain,
|
||||
&channel.0,
|
||||
test_logger,
|
||||
validation_level.unwrap(),
|
||||
StatusCode::ACCEPTED,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use beacon_chain::test_utils::RelativeSyncCommittee;
|
||||
use beacon_chain::{
|
||||
test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType},
|
||||
BeaconChain, StateSkipConfig, WhenSlotSkipped, MAXIMUM_GOSSIP_CLOCK_DISPARITY,
|
||||
BeaconChain, ChainConfig, StateSkipConfig, WhenSlotSkipped,
|
||||
};
|
||||
use environment::null_logger;
|
||||
use eth2::{
|
||||
mixin::{RequestAccept, ResponseForkName, ResponseOptional},
|
||||
reqwest::RequestBuilder,
|
||||
types::{BlockId as CoreBlockId, ForkChoiceNode, StateId as CoreStateId, *},
|
||||
BeaconNodeHttpClient, Error, Timeouts,
|
||||
BeaconNodeHttpClient, Error, StatusCode, Timeouts,
|
||||
};
|
||||
use execution_layer::test_utils::TestingBuilder;
|
||||
use execution_layer::test_utils::DEFAULT_BUILDER_THRESHOLD_WEI;
|
||||
@@ -28,9 +28,9 @@ use sensitive_url::SensitiveUrl;
|
||||
use slot_clock::SlotClock;
|
||||
use state_processing::per_block_processing::get_expected_withdrawals;
|
||||
use state_processing::per_slot_processing;
|
||||
use state_processing::state_advance::partial_state_advance;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::Duration;
|
||||
use tree_hash::TreeHash;
|
||||
use types::application_domain::ApplicationDomain;
|
||||
@@ -70,7 +70,6 @@ struct ApiTester {
|
||||
attester_slashing: AttesterSlashing<E>,
|
||||
proposer_slashing: ProposerSlashing,
|
||||
voluntary_exit: SignedVoluntaryExit,
|
||||
_server_shutdown: oneshot::Sender<()>,
|
||||
network_rx: NetworkReceivers<E>,
|
||||
local_enr: Enr,
|
||||
external_peer_id: PeerId,
|
||||
@@ -79,6 +78,7 @@ struct ApiTester {
|
||||
|
||||
struct ApiTesterConfig {
|
||||
spec: ChainSpec,
|
||||
retain_historic_states: bool,
|
||||
builder_threshold: Option<u128>,
|
||||
}
|
||||
|
||||
@@ -88,11 +88,19 @@ impl Default for ApiTesterConfig {
|
||||
spec.shard_committee_period = 2;
|
||||
Self {
|
||||
spec,
|
||||
retain_historic_states: false,
|
||||
builder_threshold: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiTesterConfig {
|
||||
fn retain_historic_states(mut self) -> Self {
|
||||
self.retain_historic_states = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiTester {
|
||||
pub async fn new() -> Self {
|
||||
// This allows for testing voluntary exits without building out a massive chain.
|
||||
@@ -120,6 +128,10 @@ impl ApiTester {
|
||||
let harness = Arc::new(
|
||||
BeaconChainHarness::builder(MainnetEthSpec)
|
||||
.spec(spec.clone())
|
||||
.chain_config(ChainConfig {
|
||||
reconstruct_historic_states: config.retain_historic_states,
|
||||
..ChainConfig::default()
|
||||
})
|
||||
.logger(logging::test_logger())
|
||||
.deterministic_keypairs(VALIDATOR_COUNT)
|
||||
.fresh_ephemeral_store()
|
||||
@@ -234,11 +246,10 @@ impl ApiTester {
|
||||
let ApiServer {
|
||||
server,
|
||||
listening_socket: _,
|
||||
shutdown_tx,
|
||||
network_rx,
|
||||
local_enr,
|
||||
external_peer_id,
|
||||
} = create_api_server_on_port(chain.clone(), log, port).await;
|
||||
} = create_api_server_on_port(chain.clone(), &harness.runtime, log, port).await;
|
||||
|
||||
harness.runtime.task_executor.spawn(server, "api_server");
|
||||
|
||||
@@ -266,7 +277,6 @@ impl ApiTester {
|
||||
attester_slashing,
|
||||
proposer_slashing,
|
||||
voluntary_exit,
|
||||
_server_shutdown: shutdown_tx,
|
||||
network_rx,
|
||||
local_enr,
|
||||
external_peer_id,
|
||||
@@ -320,11 +330,10 @@ impl ApiTester {
|
||||
let ApiServer {
|
||||
server,
|
||||
listening_socket,
|
||||
shutdown_tx,
|
||||
network_rx,
|
||||
local_enr,
|
||||
external_peer_id,
|
||||
} = create_api_server(chain.clone(), log).await;
|
||||
} = create_api_server(chain.clone(), &harness.runtime, log).await;
|
||||
|
||||
harness.runtime.task_executor.spawn(server, "api_server");
|
||||
|
||||
@@ -349,7 +358,6 @@ impl ApiTester {
|
||||
attester_slashing,
|
||||
proposer_slashing,
|
||||
voluntary_exit,
|
||||
_server_shutdown: shutdown_tx,
|
||||
network_rx,
|
||||
local_enr,
|
||||
external_peer_id,
|
||||
@@ -381,6 +389,7 @@ impl ApiTester {
|
||||
pub async fn new_mev_tester_no_builder_threshold() -> Self {
|
||||
let mut config = ApiTesterConfig {
|
||||
builder_threshold: Some(0),
|
||||
retain_historic_states: false,
|
||||
spec: E::default_spec(),
|
||||
};
|
||||
config.spec.altair_fork_epoch = Some(Epoch::new(0));
|
||||
@@ -1247,6 +1256,22 @@ impl ApiTester {
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_post_beacon_blocks_ssz_valid(mut self) -> Self {
|
||||
let next_block = &self.next_block;
|
||||
|
||||
self.client
|
||||
.post_beacon_blocks_ssz(next_block)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
self.network_rx.network_recv.recv().await.is_some(),
|
||||
"valid blocks should be sent to network"
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_post_beacon_blocks_invalid(mut self) -> Self {
|
||||
let block = self
|
||||
.harness
|
||||
@@ -1270,6 +1295,86 @@ impl ApiTester {
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_post_beacon_blocks_ssz_invalid(mut self) -> Self {
|
||||
let block = self
|
||||
.harness
|
||||
.make_block_with_modifier(
|
||||
self.harness.get_current_state(),
|
||||
self.harness.get_current_slot(),
|
||||
|b| {
|
||||
*b.state_root_mut() = Hash256::zero();
|
||||
},
|
||||
)
|
||||
.await
|
||||
.0;
|
||||
|
||||
assert!(self.client.post_beacon_blocks_ssz(&block).await.is_err());
|
||||
|
||||
assert!(
|
||||
self.network_rx.network_recv.recv().await.is_some(),
|
||||
"gossip valid blocks should be sent to network"
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_post_beacon_blocks_duplicate(self) -> Self {
|
||||
let block = self
|
||||
.harness
|
||||
.make_block(
|
||||
self.harness.get_current_state(),
|
||||
self.harness.get_current_slot(),
|
||||
)
|
||||
.await
|
||||
.0;
|
||||
|
||||
assert!(self.client.post_beacon_blocks(&block).await.is_ok());
|
||||
|
||||
let blinded_block = block.clone_as_blinded();
|
||||
|
||||
// Test all the POST methods in sequence, they should all behave the same.
|
||||
let responses = vec![
|
||||
self.client.post_beacon_blocks(&block).await.unwrap_err(),
|
||||
self.client
|
||||
.post_beacon_blocks_v2(&block, None)
|
||||
.await
|
||||
.unwrap_err(),
|
||||
self.client
|
||||
.post_beacon_blocks_ssz(&block)
|
||||
.await
|
||||
.unwrap_err(),
|
||||
self.client
|
||||
.post_beacon_blocks_v2_ssz(&block, None)
|
||||
.await
|
||||
.unwrap_err(),
|
||||
self.client
|
||||
.post_beacon_blinded_blocks(&blinded_block)
|
||||
.await
|
||||
.unwrap_err(),
|
||||
self.client
|
||||
.post_beacon_blinded_blocks_v2(&blinded_block, None)
|
||||
.await
|
||||
.unwrap_err(),
|
||||
self.client
|
||||
.post_beacon_blinded_blocks_ssz(&blinded_block)
|
||||
.await
|
||||
.unwrap_err(),
|
||||
self.client
|
||||
.post_beacon_blinded_blocks_v2_ssz(&blinded_block, None)
|
||||
.await
|
||||
.unwrap_err(),
|
||||
];
|
||||
for (i, response) in responses.into_iter().enumerate() {
|
||||
assert_eq!(
|
||||
response.status().unwrap(),
|
||||
StatusCode::ACCEPTED,
|
||||
"response {i}"
|
||||
);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_beacon_blocks(self) -> Self {
|
||||
for block_id in self.interesting_block_ids() {
|
||||
let expected = block_id
|
||||
@@ -2274,7 +2379,9 @@ impl ApiTester {
|
||||
.unwrap();
|
||||
|
||||
self.chain.slot_clock.set_current_time(
|
||||
current_epoch_start - MAXIMUM_GOSSIP_CLOCK_DISPARITY - Duration::from_millis(1),
|
||||
current_epoch_start
|
||||
- self.chain.spec.maximum_gossip_clock_disparity()
|
||||
- Duration::from_millis(1),
|
||||
);
|
||||
|
||||
let dependent_root = self
|
||||
@@ -2311,9 +2418,9 @@ impl ApiTester {
|
||||
"should not get attester duties outside of tolerance"
|
||||
);
|
||||
|
||||
self.chain
|
||||
.slot_clock
|
||||
.set_current_time(current_epoch_start - MAXIMUM_GOSSIP_CLOCK_DISPARITY);
|
||||
self.chain.slot_clock.set_current_time(
|
||||
current_epoch_start - self.chain.spec.maximum_gossip_clock_disparity(),
|
||||
);
|
||||
|
||||
self.client
|
||||
.get_validator_duties_proposer(current_epoch)
|
||||
@@ -2537,6 +2644,66 @@ impl ApiTester {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_blinded_block_production_ssz<Payload: AbstractExecPayload<E>>(&self) {
|
||||
let fork = self.chain.canonical_head.cached_head().head_fork();
|
||||
let genesis_validators_root = self.chain.genesis_validators_root;
|
||||
|
||||
for _ in 0..E::slots_per_epoch() * 3 {
|
||||
let slot = self.chain.slot().unwrap();
|
||||
let epoch = self.chain.epoch().unwrap();
|
||||
|
||||
let proposer_pubkey_bytes = self
|
||||
.client
|
||||
.get_validator_duties_proposer(epoch)
|
||||
.await
|
||||
.unwrap()
|
||||
.data
|
||||
.into_iter()
|
||||
.find(|duty| duty.slot == slot)
|
||||
.map(|duty| duty.pubkey)
|
||||
.unwrap();
|
||||
let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap();
|
||||
|
||||
let sk = self
|
||||
.validator_keypairs()
|
||||
.iter()
|
||||
.find(|kp| kp.pk == proposer_pubkey)
|
||||
.map(|kp| kp.sk.clone())
|
||||
.unwrap();
|
||||
|
||||
let randao_reveal = {
|
||||
let domain = self.chain.spec.get_domain(
|
||||
epoch,
|
||||
Domain::Randao,
|
||||
&fork,
|
||||
genesis_validators_root,
|
||||
);
|
||||
let message = epoch.signing_root(domain);
|
||||
sk.sign(message).into()
|
||||
};
|
||||
|
||||
let block = self
|
||||
.client
|
||||
.get_validator_blinded_blocks::<E, Payload>(slot, &randao_reveal, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec);
|
||||
|
||||
self.client
|
||||
.post_beacon_blinded_blocks_ssz(&signed_block)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This converts the generic `Payload` to a concrete type for comparison.
|
||||
let head_block = SignedBeaconBlock::from(signed_block.clone());
|
||||
assert_eq!(head_block, signed_block);
|
||||
|
||||
self.chain.slot_clock.set_slot(slot.as_u64() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_blinded_block_production_no_verify_randao<Payload: AbstractExecPayload<E>>(
|
||||
self,
|
||||
) -> Self {
|
||||
@@ -2980,6 +3147,69 @@ impl ApiTester {
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_post_validator_liveness_epoch(self) -> Self {
|
||||
let epoch = self.chain.epoch().unwrap();
|
||||
let head_state = self.chain.head_beacon_state_cloned();
|
||||
let indices = (0..head_state.validators().len())
|
||||
.map(|i| i as u64)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Construct the expected response
|
||||
let expected: Vec<StandardLivenessResponseData> = head_state
|
||||
.validators()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, _)| StandardLivenessResponseData {
|
||||
index: index as u64,
|
||||
is_live: false,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = self
|
||||
.client
|
||||
.post_validator_liveness_epoch(epoch, indices.clone())
|
||||
.await
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
assert_eq!(result, expected);
|
||||
|
||||
// Attest to the current slot
|
||||
self.client
|
||||
.post_beacon_pool_attestations(self.attestations.as_slice())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = self
|
||||
.client
|
||||
.post_validator_liveness_epoch(epoch, indices.clone())
|
||||
.await
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
let committees = head_state
|
||||
.get_beacon_committees_at_slot(self.chain.slot().unwrap())
|
||||
.unwrap();
|
||||
let attesting_validators: Vec<usize> = committees
|
||||
.into_iter()
|
||||
.flat_map(|committee| committee.committee.iter().cloned())
|
||||
.collect();
|
||||
// All attesters should now be considered live
|
||||
let expected = expected
|
||||
.into_iter()
|
||||
.map(|mut a| {
|
||||
if attesting_validators.contains(&(a.index as usize)) {
|
||||
a.is_live = true;
|
||||
}
|
||||
a
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(result, expected);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
// Helper function for tests that require a valid RANDAO signature.
|
||||
async fn get_test_randao(&self, slot: Slot, epoch: Epoch) -> (u64, SignatureBytes) {
|
||||
let fork = self.chain.canonical_head.cached_head().head_fork();
|
||||
@@ -4169,6 +4399,72 @@ impl ApiTester {
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_get_expected_withdrawals_invalid_state(self) -> Self {
|
||||
let state_id = CoreStateId::Root(Hash256::zero());
|
||||
|
||||
let result = self.client.get_expected_withdrawals(&state_id).await;
|
||||
|
||||
match result {
|
||||
Err(e) => {
|
||||
assert_eq!(e.status().unwrap(), 404);
|
||||
}
|
||||
_ => panic!("query did not fail correctly"),
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_get_expected_withdrawals_capella(self) -> Self {
|
||||
let slot = self.chain.slot().unwrap();
|
||||
let state_id = CoreStateId::Slot(slot);
|
||||
|
||||
// calculate the expected withdrawals
|
||||
let (mut state, _, _) = StateId(state_id).state(&self.chain).unwrap();
|
||||
let proposal_slot = state.slot() + 1;
|
||||
let proposal_epoch = proposal_slot.epoch(E::slots_per_epoch());
|
||||
let (state_root, _, _) = StateId(state_id).root(&self.chain).unwrap();
|
||||
if proposal_epoch != state.current_epoch() {
|
||||
let _ = partial_state_advance(
|
||||
&mut state,
|
||||
Some(state_root),
|
||||
proposal_slot,
|
||||
&self.chain.spec,
|
||||
);
|
||||
}
|
||||
let expected_withdrawals = get_expected_withdrawals(&state, &self.chain.spec).unwrap();
|
||||
|
||||
// fetch expected withdrawals from the client
|
||||
let result = self.client.get_expected_withdrawals(&state_id).await;
|
||||
match result {
|
||||
Ok(withdrawal_response) => {
|
||||
assert_eq!(withdrawal_response.execution_optimistic, Some(false));
|
||||
assert_eq!(withdrawal_response.finalized, Some(false));
|
||||
assert_eq!(withdrawal_response.data, expected_withdrawals.to_vec());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{:?}", e);
|
||||
panic!("query failed incorrectly");
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_get_expected_withdrawals_pre_capella(self) -> Self {
|
||||
let state_id = CoreStateId::Head;
|
||||
|
||||
let result = self.client.get_expected_withdrawals(&state_id).await;
|
||||
|
||||
match result {
|
||||
Err(e) => {
|
||||
assert_eq!(e.status().unwrap(), 400);
|
||||
}
|
||||
_ => panic!("query did not fail correctly"),
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_get_events_altair(self) -> Self {
|
||||
let topics = vec![EventTopic::ContributionAndProof];
|
||||
let mut events_future = self
|
||||
@@ -4388,6 +4684,22 @@ async fn post_beacon_blocks_valid() {
|
||||
ApiTester::new().await.test_post_beacon_blocks_valid().await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn post_beacon_blocks_ssz_valid() {
|
||||
ApiTester::new()
|
||||
.await
|
||||
.test_post_beacon_blocks_ssz_valid()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_post_beacon_blocks_ssz_invalid() {
|
||||
ApiTester::new()
|
||||
.await
|
||||
.test_post_beacon_blocks_ssz_invalid()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn post_beacon_blocks_invalid() {
|
||||
ApiTester::new()
|
||||
@@ -4396,6 +4708,14 @@ async fn post_beacon_blocks_invalid() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn post_beacon_blocks_duplicate() {
|
||||
ApiTester::new()
|
||||
.await
|
||||
.test_post_beacon_blocks_duplicate()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn beacon_pools_post_attestations_valid() {
|
||||
ApiTester::new()
|
||||
@@ -4531,7 +4851,7 @@ async fn get_validator_duties_attester_with_skip_slots() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn get_validator_duties_proposer() {
|
||||
ApiTester::new()
|
||||
ApiTester::new_from_config(ApiTesterConfig::default().retain_historic_states())
|
||||
.await
|
||||
.test_get_validator_duties_proposer()
|
||||
.await;
|
||||
@@ -4539,7 +4859,7 @@ async fn get_validator_duties_proposer() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn get_validator_duties_proposer_with_skip_slots() {
|
||||
ApiTester::new()
|
||||
ApiTester::new_from_config(ApiTesterConfig::default().retain_historic_states())
|
||||
.await
|
||||
.skip_slots(E::slots_per_epoch() * 2)
|
||||
.test_get_validator_duties_proposer()
|
||||
@@ -4584,6 +4904,14 @@ async fn blinded_block_production_full_payload_premerge() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn blinded_block_production_ssz_full_payload_premerge() {
|
||||
ApiTester::new()
|
||||
.await
|
||||
.test_blinded_block_production_ssz::<FullPayload<_>>()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn blinded_block_production_with_skip_slots_full_payload_premerge() {
|
||||
ApiTester::new()
|
||||
@@ -4593,6 +4921,15 @@ async fn blinded_block_production_with_skip_slots_full_payload_premerge() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn blinded_block_production_ssz_with_skip_slots_full_payload_premerge() {
|
||||
ApiTester::new()
|
||||
.await
|
||||
.skip_slots(E::slots_per_epoch() * 2)
|
||||
.test_blinded_block_production_ssz::<FullPayload<_>>()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn blinded_block_production_no_verify_randao_full_payload_premerge() {
|
||||
ApiTester::new()
|
||||
@@ -4854,6 +5191,7 @@ async fn builder_payload_chosen_by_profit() {
|
||||
async fn builder_works_post_capella() {
|
||||
let mut config = ApiTesterConfig {
|
||||
builder_threshold: Some(0),
|
||||
retain_historic_states: false,
|
||||
spec: E::default_spec(),
|
||||
};
|
||||
config.spec.altair_fork_epoch = Some(Epoch::new(0));
|
||||
@@ -4870,6 +5208,14 @@ async fn builder_works_post_capella() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn post_validator_liveness_epoch() {
|
||||
ApiTester::new()
|
||||
.await
|
||||
.test_post_validator_liveness_epoch()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn lighthouse_endpoints() {
|
||||
ApiTester::new()
|
||||
@@ -4909,3 +5255,37 @@ async fn optimistic_responses() {
|
||||
.test_check_optimistic_responses()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn expected_withdrawals_invalid_pre_capella() {
|
||||
let mut config = ApiTesterConfig::default();
|
||||
config.spec.altair_fork_epoch = Some(Epoch::new(0));
|
||||
ApiTester::new_from_config(config)
|
||||
.await
|
||||
.test_get_expected_withdrawals_pre_capella()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn expected_withdrawals_invalid_state() {
|
||||
let mut config = ApiTesterConfig::default();
|
||||
config.spec.altair_fork_epoch = Some(Epoch::new(0));
|
||||
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
|
||||
config.spec.capella_fork_epoch = Some(Epoch::new(0));
|
||||
ApiTester::new_from_config(config)
|
||||
.await
|
||||
.test_get_expected_withdrawals_invalid_state()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn expected_withdrawals_valid_capella() {
|
||||
let mut config = ApiTesterConfig::default();
|
||||
config.spec.altair_fork_epoch = Some(Epoch::new(0));
|
||||
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
|
||||
config.spec.capella_fork_epoch = Some(Epoch::new(0));
|
||||
ApiTester::new_from_config(config)
|
||||
.await
|
||||
.test_get_expected_withdrawals_capella()
|
||||
.await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user