mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-09 03:31:45 +00:00
[Merge] Implement execution_layer (#2635)
* Checkout serde_utils from rayonism
* Make eth1::http functions pub
* Add bones of execution_layer
* Modify decoding
* Expose Transaction, cargo fmt
* Add executePayload
* Add all minimal spec endpoints
* Start adding json rpc wrapper
* Finish custom JSON response handler
* Switch to new rpc sending method
* Add first test
* Fix camelCase
* Finish adding tests
* Begin threading execution layer into BeaconChain
* Fix clippy lints
* Fix clippy lints
* Thread execution layer into ClientBuilder
* Add CLI flags
* Add block processing methods to ExecutionLayer
* Add block_on to execution_layer
* Integrate execute_payload
* Add extra_data field
* Begin implementing payload handle
* Send consensus valid/invalid messages
* Fix minor type in task_executor
* Call forkchoiceUpdated
* Add search for TTD block
* Thread TTD into execution layer
* Allow producing block with execution payload
* Add LRU cache for execution blocks
* Remove duplicate 0x on ssz_types serialization
* Add tests for block getter methods
* Add basic block generator impl
* Add is_valid_terminal_block to EL
* Verify merge block in block_verification
* Partially implement --terminal-block-hash-override
* Add terminal_block_hash to ChainSpec
* Remove Option from terminal_block_hash in EL
* Revert merge changes to consensus/fork_choice
* Remove commented-out code
* Add bones for handling RPC methods on test server
* Add first ExecutionLayer tests
* Add testing for finding terminal block
* Prevent infinite loops
* Add insert_merge_block to block gen
* Add block gen test for pos blocks
* Start adding payloads to block gen
* Fix clippy lints
* Add execution payload to block gen
* Add execute_payload to block_gen
* Refactor block gen
* Add all routes to mock server
* Use Uint256 for base_fee_per_gas
* Add working execution chain build
* Remove unused var
* Revert "Use Uint256 for base_fee_per_gas"
This reverts commit 6c88f19ac4.
* Fix base_fee_for_gas Uint256
* Update execute payload handle
* Improve testing, fix bugs
* Fix default fee-recipient
* Fix fee-recipient address (again)
* Add check for terminal block, add comments, tidy
* Apply suggestions from code review
Co-authored-by: realbigsean <seananderson33@GMAIL.com>
* Fix is_none on handle Drop
* Remove commented-out tests
Co-authored-by: realbigsean <seananderson33@GMAIL.com>
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
use crate::engine_api::{
|
||||
http::JsonPreparePayloadRequest, ConsensusStatus, ExecutePayloadResponse, ExecutionBlock,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use tree_hash::TreeHash;
|
||||
use tree_hash_derive::TreeHash;
|
||||
use types::{EthSpec, ExecutionPayload, Hash256, Uint256};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[allow(clippy::large_enum_variant)] // This struct is only for testing.
|
||||
pub enum Block<T: EthSpec> {
|
||||
PoW(PoWBlock),
|
||||
PoS(ExecutionPayload<T>),
|
||||
}
|
||||
|
||||
impl<T: EthSpec> Block<T> {
|
||||
pub fn block_number(&self) -> u64 {
|
||||
match self {
|
||||
Block::PoW(block) => block.block_number,
|
||||
Block::PoS(payload) => payload.block_number,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parent_hash(&self) -> Hash256 {
|
||||
match self {
|
||||
Block::PoW(block) => block.parent_hash,
|
||||
Block::PoS(payload) => payload.parent_hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block_hash(&self) -> Hash256 {
|
||||
match self {
|
||||
Block::PoW(block) => block.block_hash,
|
||||
Block::PoS(payload) => payload.block_hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total_difficulty(&self) -> Option<Uint256> {
|
||||
match self {
|
||||
Block::PoW(block) => Some(block.total_difficulty),
|
||||
Block::PoS(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_execution_block(&self, total_difficulty: u64) -> ExecutionBlock {
|
||||
match self {
|
||||
Block::PoW(block) => ExecutionBlock {
|
||||
block_hash: block.block_hash,
|
||||
block_number: block.block_number,
|
||||
parent_hash: block.parent_hash,
|
||||
total_difficulty: block.total_difficulty,
|
||||
},
|
||||
Block::PoS(payload) => ExecutionBlock {
|
||||
block_hash: payload.block_hash,
|
||||
block_number: payload.block_number,
|
||||
parent_hash: payload.parent_hash,
|
||||
total_difficulty: total_difficulty.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, TreeHash)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PoWBlock {
|
||||
pub block_number: u64,
|
||||
pub block_hash: Hash256,
|
||||
pub parent_hash: Hash256,
|
||||
pub total_difficulty: Uint256,
|
||||
}
|
||||
|
||||
pub struct ExecutionBlockGenerator<T: EthSpec> {
|
||||
/*
|
||||
* Common database
|
||||
*/
|
||||
blocks: HashMap<Hash256, Block<T>>,
|
||||
block_hashes: HashMap<u64, Hash256>,
|
||||
/*
|
||||
* PoW block parameters
|
||||
*/
|
||||
pub terminal_total_difficulty: u64,
|
||||
pub terminal_block_number: u64,
|
||||
/*
|
||||
* PoS block parameters
|
||||
*/
|
||||
pub pending_payloads: HashMap<Hash256, ExecutionPayload<T>>,
|
||||
pub next_payload_id: u64,
|
||||
pub payload_ids: HashMap<u64, ExecutionPayload<T>>,
|
||||
}
|
||||
|
||||
impl<T: EthSpec> ExecutionBlockGenerator<T> {
|
||||
pub fn new(terminal_total_difficulty: u64, terminal_block_number: u64) -> Self {
|
||||
let mut gen = Self {
|
||||
blocks: <_>::default(),
|
||||
block_hashes: <_>::default(),
|
||||
terminal_total_difficulty,
|
||||
terminal_block_number,
|
||||
pending_payloads: <_>::default(),
|
||||
next_payload_id: 0,
|
||||
payload_ids: <_>::default(),
|
||||
};
|
||||
|
||||
gen.insert_pow_block(0).unwrap();
|
||||
|
||||
gen
|
||||
}
|
||||
|
||||
pub fn latest_block(&self) -> Option<Block<T>> {
|
||||
let hash = *self
|
||||
.block_hashes
|
||||
.iter()
|
||||
.max_by_key(|(number, _)| *number)
|
||||
.map(|(_, hash)| hash)?;
|
||||
|
||||
self.block_by_hash(hash)
|
||||
}
|
||||
|
||||
pub fn latest_execution_block(&self) -> Option<ExecutionBlock> {
|
||||
self.latest_block()
|
||||
.map(|block| block.as_execution_block(self.terminal_total_difficulty))
|
||||
}
|
||||
|
||||
pub fn block_by_number(&self, number: u64) -> Option<Block<T>> {
|
||||
let hash = *self.block_hashes.get(&number)?;
|
||||
self.block_by_hash(hash)
|
||||
}
|
||||
|
||||
pub fn execution_block_by_number(&self, number: u64) -> Option<ExecutionBlock> {
|
||||
self.block_by_number(number)
|
||||
.map(|block| block.as_execution_block(self.terminal_total_difficulty))
|
||||
}
|
||||
|
||||
pub fn block_by_hash(&self, hash: Hash256) -> Option<Block<T>> {
|
||||
self.blocks.get(&hash).cloned()
|
||||
}
|
||||
|
||||
pub fn execution_block_by_hash(&self, hash: Hash256) -> Option<ExecutionBlock> {
|
||||
self.block_by_hash(hash)
|
||||
.map(|block| block.as_execution_block(self.terminal_total_difficulty))
|
||||
}
|
||||
|
||||
pub fn insert_pow_blocks(
|
||||
&mut self,
|
||||
block_numbers: impl Iterator<Item = u64>,
|
||||
) -> Result<(), String> {
|
||||
for i in block_numbers {
|
||||
self.insert_pow_block(i)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_pow_block(&mut self, block_number: u64) -> Result<(), String> {
|
||||
if block_number > self.terminal_block_number {
|
||||
return Err(format!(
|
||||
"{} is beyond terminal pow block {}",
|
||||
block_number, self.terminal_block_number
|
||||
));
|
||||
}
|
||||
|
||||
let parent_hash = if block_number == 0 {
|
||||
Hash256::zero()
|
||||
} else if let Some(hash) = self.block_hashes.get(&(block_number - 1)) {
|
||||
*hash
|
||||
} else {
|
||||
return Err(format!(
|
||||
"parent with block number {} not found",
|
||||
block_number - 1
|
||||
));
|
||||
};
|
||||
|
||||
let increment = self
|
||||
.terminal_total_difficulty
|
||||
.checked_div(self.terminal_block_number)
|
||||
.expect("terminal block number must be non-zero");
|
||||
let total_difficulty = increment
|
||||
.checked_mul(block_number)
|
||||
.expect("overflow computing total difficulty")
|
||||
.into();
|
||||
|
||||
let mut block = PoWBlock {
|
||||
block_number,
|
||||
block_hash: Hash256::zero(),
|
||||
parent_hash,
|
||||
total_difficulty,
|
||||
};
|
||||
|
||||
block.block_hash = block.tree_hash_root();
|
||||
|
||||
self.insert_block(Block::PoW(block))
|
||||
}
|
||||
|
||||
pub fn insert_block(&mut self, block: Block<T>) -> Result<(), String> {
|
||||
if self.blocks.contains_key(&block.block_hash()) {
|
||||
return Err(format!("{:?} is already known", block.block_hash()));
|
||||
} else if self.block_hashes.contains_key(&block.block_number()) {
|
||||
return Err(format!(
|
||||
"block {} is already known, forking is not supported",
|
||||
block.block_number()
|
||||
));
|
||||
} else if block.parent_hash() != Hash256::zero()
|
||||
&& !self.blocks.contains_key(&block.parent_hash())
|
||||
{
|
||||
return Err(format!("parent block {:?} is unknown", block.parent_hash()));
|
||||
}
|
||||
|
||||
self.block_hashes
|
||||
.insert(block.block_number(), block.block_hash());
|
||||
self.blocks.insert(block.block_hash(), block);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prepare_payload(&mut self, payload: JsonPreparePayloadRequest) -> Result<u64, String> {
|
||||
if !self
|
||||
.blocks
|
||||
.iter()
|
||||
.any(|(_, block)| block.block_number() == self.terminal_block_number)
|
||||
{
|
||||
return Err("refusing to create payload id before terminal block".to_string());
|
||||
}
|
||||
|
||||
let parent = self
|
||||
.blocks
|
||||
.get(&payload.parent_hash)
|
||||
.ok_or_else(|| format!("unknown parent block {:?}", payload.parent_hash))?;
|
||||
|
||||
let id = self.next_payload_id;
|
||||
self.next_payload_id += 1;
|
||||
|
||||
let mut execution_payload = ExecutionPayload {
|
||||
parent_hash: payload.parent_hash,
|
||||
coinbase: payload.fee_recipient,
|
||||
receipt_root: Hash256::repeat_byte(42),
|
||||
state_root: Hash256::repeat_byte(43),
|
||||
logs_bloom: vec![0; 256].into(),
|
||||
random: payload.random,
|
||||
block_number: parent.block_number() + 1,
|
||||
gas_limit: 10,
|
||||
gas_used: 9,
|
||||
timestamp: payload.timestamp,
|
||||
extra_data: "block gen was here".as_bytes().to_vec().into(),
|
||||
base_fee_per_gas: Hash256::from_low_u64_le(1),
|
||||
block_hash: Hash256::zero(),
|
||||
transactions: vec![].into(),
|
||||
};
|
||||
|
||||
execution_payload.block_hash = execution_payload.tree_hash_root();
|
||||
|
||||
self.payload_ids.insert(id, execution_payload);
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn get_payload(&mut self, id: u64) -> Option<ExecutionPayload<T>> {
|
||||
self.payload_ids.remove(&id)
|
||||
}
|
||||
|
||||
pub fn execute_payload(&mut self, payload: ExecutionPayload<T>) -> ExecutePayloadResponse {
|
||||
let parent = if let Some(parent) = self.blocks.get(&payload.parent_hash) {
|
||||
parent
|
||||
} else {
|
||||
return ExecutePayloadResponse::Invalid;
|
||||
};
|
||||
|
||||
if payload.block_number != parent.block_number() + 1 {
|
||||
return ExecutePayloadResponse::Invalid;
|
||||
}
|
||||
|
||||
self.pending_payloads.insert(payload.block_hash, payload);
|
||||
|
||||
ExecutePayloadResponse::Valid
|
||||
}
|
||||
|
||||
pub fn consensus_validated(
|
||||
&mut self,
|
||||
block_hash: Hash256,
|
||||
status: ConsensusStatus,
|
||||
) -> Result<(), String> {
|
||||
let payload = self
|
||||
.pending_payloads
|
||||
.remove(&block_hash)
|
||||
.ok_or_else(|| format!("no pending payload for {:?}", block_hash))?;
|
||||
|
||||
match status {
|
||||
ConsensusStatus::Valid => self.insert_block(Block::PoS(payload)),
|
||||
ConsensusStatus::Invalid => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forkchoice_updated(
|
||||
&mut self,
|
||||
block_hash: Hash256,
|
||||
finalized_block_hash: Hash256,
|
||||
) -> Result<(), String> {
|
||||
if !self.blocks.contains_key(&block_hash) {
|
||||
return Err(format!("block hash {:?} unknown", block_hash));
|
||||
}
|
||||
|
||||
if finalized_block_hash != Hash256::zero()
|
||||
&& !self.blocks.contains_key(&finalized_block_hash)
|
||||
{
|
||||
return Err(format!(
|
||||
"finalized block hash {:?} is unknown",
|
||||
finalized_block_hash
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use types::MainnetEthSpec;
|
||||
|
||||
#[test]
|
||||
fn pow_chain_only() {
|
||||
const TERMINAL_DIFFICULTY: u64 = 10;
|
||||
const TERMINAL_BLOCK: u64 = 10;
|
||||
const DIFFICULTY_INCREMENT: u64 = 1;
|
||||
|
||||
let mut generator: ExecutionBlockGenerator<MainnetEthSpec> =
|
||||
ExecutionBlockGenerator::new(TERMINAL_DIFFICULTY, TERMINAL_BLOCK);
|
||||
|
||||
for i in 0..=TERMINAL_BLOCK {
|
||||
if i > 0 {
|
||||
generator.insert_pow_block(i).unwrap();
|
||||
}
|
||||
|
||||
/*
|
||||
* Generate a block, inspect it.
|
||||
*/
|
||||
|
||||
let block = generator.latest_block().unwrap();
|
||||
assert_eq!(block.block_number(), i);
|
||||
|
||||
let expected_parent = i
|
||||
.checked_sub(1)
|
||||
.map(|i| generator.block_by_number(i).unwrap().block_hash())
|
||||
.unwrap_or_else(Hash256::zero);
|
||||
assert_eq!(block.parent_hash(), expected_parent);
|
||||
|
||||
assert_eq!(
|
||||
block.total_difficulty().unwrap(),
|
||||
(i * DIFFICULTY_INCREMENT).into()
|
||||
);
|
||||
|
||||
assert_eq!(generator.block_by_hash(block.block_hash()).unwrap(), block);
|
||||
assert_eq!(generator.block_by_number(i).unwrap(), block);
|
||||
|
||||
/*
|
||||
* Check the parent is accessible.
|
||||
*/
|
||||
|
||||
if let Some(prev_i) = i.checked_sub(1) {
|
||||
assert_eq!(
|
||||
generator.block_by_number(prev_i).unwrap(),
|
||||
generator.block_by_hash(block.parent_hash()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Check the next block is inaccessible.
|
||||
*/
|
||||
|
||||
let next_i = i + 1;
|
||||
assert!(generator.block_by_number(next_i).is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
125
beacon_node/execution_layer/src/test_utils/handle_rpc.rs
Normal file
125
beacon_node/execution_layer/src/test_utils/handle_rpc.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use super::Context;
|
||||
use crate::engine_api::http::*;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::sync::Arc;
|
||||
use types::EthSpec;
|
||||
|
||||
pub async fn handle_rpc<T: EthSpec>(
|
||||
body: JsonValue,
|
||||
ctx: Arc<Context<T>>,
|
||||
) -> Result<JsonValue, String> {
|
||||
let method = body
|
||||
.get("method")
|
||||
.and_then(JsonValue::as_str)
|
||||
.ok_or_else(|| "missing/invalid method field".to_string())?;
|
||||
|
||||
let params = body
|
||||
.get("params")
|
||||
.ok_or_else(|| "missing/invalid params field".to_string())?;
|
||||
|
||||
match method {
|
||||
ETH_SYNCING => Ok(JsonValue::Bool(false)),
|
||||
ETH_GET_BLOCK_BY_NUMBER => {
|
||||
let tag = params
|
||||
.get(0)
|
||||
.and_then(JsonValue::as_str)
|
||||
.ok_or_else(|| "missing/invalid params[0] value".to_string())?;
|
||||
|
||||
match tag {
|
||||
"latest" => Ok(serde_json::to_value(
|
||||
ctx.execution_block_generator
|
||||
.read()
|
||||
.await
|
||||
.latest_execution_block(),
|
||||
)
|
||||
.unwrap()),
|
||||
other => Err(format!("The tag {} is not supported", other)),
|
||||
}
|
||||
}
|
||||
ETH_GET_BLOCK_BY_HASH => {
|
||||
let hash = params
|
||||
.get(0)
|
||||
.and_then(JsonValue::as_str)
|
||||
.ok_or_else(|| "missing/invalid params[0] value".to_string())
|
||||
.and_then(|s| {
|
||||
s.parse()
|
||||
.map_err(|e| format!("unable to parse hash: {:?}", e))
|
||||
})?;
|
||||
|
||||
Ok(serde_json::to_value(
|
||||
ctx.execution_block_generator
|
||||
.read()
|
||||
.await
|
||||
.execution_block_by_hash(hash),
|
||||
)
|
||||
.unwrap())
|
||||
}
|
||||
ENGINE_PREPARE_PAYLOAD => {
|
||||
let request = get_param_0(params)?;
|
||||
let payload_id = ctx
|
||||
.execution_block_generator
|
||||
.write()
|
||||
.await
|
||||
.prepare_payload(request)?;
|
||||
|
||||
Ok(serde_json::to_value(JsonPayloadId { payload_id }).unwrap())
|
||||
}
|
||||
ENGINE_EXECUTE_PAYLOAD => {
|
||||
let request: JsonExecutionPayload<T> = get_param_0(params)?;
|
||||
let response = ctx
|
||||
.execution_block_generator
|
||||
.write()
|
||||
.await
|
||||
.execute_payload(request.into());
|
||||
|
||||
Ok(serde_json::to_value(response).unwrap())
|
||||
}
|
||||
ENGINE_GET_PAYLOAD => {
|
||||
let request: JsonPayloadId = get_param_0(params)?;
|
||||
let id = request.payload_id;
|
||||
|
||||
let response = ctx
|
||||
.execution_block_generator
|
||||
.write()
|
||||
.await
|
||||
.get_payload(id)
|
||||
.ok_or_else(|| format!("no payload for id {}", id))?;
|
||||
|
||||
Ok(serde_json::to_value(JsonExecutionPayload::from(response)).unwrap())
|
||||
}
|
||||
|
||||
ENGINE_CONSENSUS_VALIDATED => {
|
||||
let request: JsonConsensusValidatedRequest = get_param_0(params)?;
|
||||
ctx.execution_block_generator
|
||||
.write()
|
||||
.await
|
||||
.consensus_validated(request.block_hash, request.status)?;
|
||||
|
||||
Ok(JsonValue::Null)
|
||||
}
|
||||
ENGINE_FORKCHOICE_UPDATED => {
|
||||
let request: JsonForkChoiceUpdatedRequest = get_param_0(params)?;
|
||||
ctx.execution_block_generator
|
||||
.write()
|
||||
.await
|
||||
.forkchoice_updated(request.head_block_hash, request.finalized_block_hash)?;
|
||||
|
||||
Ok(JsonValue::Null)
|
||||
}
|
||||
other => Err(format!(
|
||||
"The method {} does not exist/is not available",
|
||||
other
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_param_0<T: DeserializeOwned>(params: &JsonValue) -> Result<T, String> {
|
||||
params
|
||||
.get(0)
|
||||
.ok_or_else(|| "missing/invalid params[0] value".to_string())
|
||||
.and_then(|param| {
|
||||
serde_json::from_value(param.clone())
|
||||
.map_err(|e| format!("failed to deserialize param[0]: {:?}", e))
|
||||
})
|
||||
}
|
||||
230
beacon_node/execution_layer/src/test_utils/mod.rs
Normal file
230
beacon_node/execution_layer/src/test_utils/mod.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
//! Provides a mock execution engine HTTP JSON-RPC API for use in testing.
|
||||
|
||||
use crate::engine_api::http::JSONRPC_VERSION;
|
||||
use bytes::Bytes;
|
||||
use environment::null_logger;
|
||||
use execution_block_generator::ExecutionBlockGenerator;
|
||||
use handle_rpc::handle_rpc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use slog::{info, Logger};
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{oneshot, RwLock, RwLockWriteGuard};
|
||||
use types::EthSpec;
|
||||
use warp::Filter;
|
||||
|
||||
pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400;
|
||||
pub const DEFAULT_TERMINAL_BLOCK: u64 = 64;
|
||||
|
||||
mod execution_block_generator;
|
||||
mod handle_rpc;
|
||||
|
||||
pub struct MockServer<T: EthSpec> {
|
||||
_shutdown_tx: oneshot::Sender<()>,
|
||||
listen_socket_addr: SocketAddr,
|
||||
last_echo_request: Arc<RwLock<Option<Bytes>>>,
|
||||
pub ctx: Arc<Context<T>>,
|
||||
}
|
||||
|
||||
impl<T: EthSpec> MockServer<T> {
|
||||
pub fn unit_testing() -> Self {
|
||||
let last_echo_request = Arc::new(RwLock::new(None));
|
||||
let execution_block_generator =
|
||||
ExecutionBlockGenerator::new(DEFAULT_TERMINAL_DIFFICULTY, DEFAULT_TERMINAL_BLOCK);
|
||||
|
||||
let ctx: Arc<Context<T>> = Arc::new(Context {
|
||||
config: <_>::default(),
|
||||
log: null_logger().unwrap(),
|
||||
last_echo_request: last_echo_request.clone(),
|
||||
execution_block_generator: RwLock::new(execution_block_generator),
|
||||
_phantom: PhantomData,
|
||||
});
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
|
||||
let shutdown_future = async {
|
||||
// Ignore the result from the channel, shut down regardless.
|
||||
let _ = shutdown_rx.await;
|
||||
};
|
||||
|
||||
let (listen_socket_addr, server_future) = serve(ctx.clone(), shutdown_future).unwrap();
|
||||
|
||||
tokio::spawn(server_future);
|
||||
|
||||
Self {
|
||||
_shutdown_tx: shutdown_tx,
|
||||
listen_socket_addr,
|
||||
last_echo_request,
|
||||
ctx,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execution_block_generator(
|
||||
&self,
|
||||
) -> RwLockWriteGuard<'_, ExecutionBlockGenerator<T>> {
|
||||
self.ctx.execution_block_generator.write().await
|
||||
}
|
||||
|
||||
pub fn url(&self) -> String {
|
||||
format!(
|
||||
"http://{}:{}",
|
||||
self.listen_socket_addr.ip(),
|
||||
self.listen_socket_addr.port()
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn last_echo_request(&self) -> Bytes {
|
||||
self.last_echo_request
|
||||
.write()
|
||||
.await
|
||||
.take()
|
||||
.expect("last echo request is none")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Warp(warp::Error),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<warp::Error> for Error {
|
||||
fn from(e: warp::Error) -> Self {
|
||||
Error::Warp(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(e: String) -> Self {
|
||||
Error::Other(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MissingIdField;
|
||||
|
||||
impl warp::reject::Reject for MissingIdField {}
|
||||
|
||||
/// A wrapper around all the items required to spawn the HTTP server.
|
||||
///
|
||||
/// The server will gracefully handle the case where any fields are `None`.
|
||||
pub struct Context<T: EthSpec> {
|
||||
pub config: Config,
|
||||
pub log: Logger,
|
||||
pub last_echo_request: Arc<RwLock<Option<Bytes>>>,
|
||||
pub execution_block_generator: RwLock<ExecutionBlockGenerator<T>>,
|
||||
pub _phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
/// Configuration for the HTTP server.
|
||||
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub listen_addr: Ipv4Addr,
|
||||
pub listen_port: u16,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
listen_addr: Ipv4Addr::new(127, 0, 0, 1),
|
||||
listen_port: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a server that will serve requests using information from `ctx`.
|
||||
///
|
||||
/// The server will shut down gracefully when the `shutdown` future resolves.
|
||||
///
|
||||
/// ## Returns
|
||||
///
|
||||
/// This function will bind the server to the provided address and then return a tuple of:
|
||||
///
|
||||
/// - `SocketAddr`: the address that the HTTP server will listen on.
|
||||
/// - `Future`: the actual server future that will need to be awaited.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns an error if the server is unable to bind or there is another error during
|
||||
/// configuration.
|
||||
pub fn serve<T: EthSpec>(
|
||||
ctx: Arc<Context<T>>,
|
||||
shutdown: impl Future<Output = ()> + Send + Sync + 'static,
|
||||
) -> Result<(SocketAddr, impl Future<Output = ()>), Error> {
|
||||
let config = &ctx.config;
|
||||
let log = ctx.log.clone();
|
||||
|
||||
let inner_ctx = ctx.clone();
|
||||
let ctx_filter = warp::any().map(move || inner_ctx.clone());
|
||||
|
||||
// `/`
|
||||
//
|
||||
// Handles actual JSON-RPC requests.
|
||||
let root = warp::path::end()
|
||||
.and(warp::body::json())
|
||||
.and(ctx_filter.clone())
|
||||
.and_then(|body: serde_json::Value, ctx: Arc<Context<T>>| async move {
|
||||
let id = body
|
||||
.get("id")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.ok_or_else(|| warp::reject::custom(MissingIdField))?;
|
||||
|
||||
let response = match handle_rpc(body, ctx).await {
|
||||
Ok(result) => json!({
|
||||
"id": id,
|
||||
"jsonrpc": JSONRPC_VERSION,
|
||||
"result": result
|
||||
}),
|
||||
Err(message) => json!({
|
||||
"id": id,
|
||||
"jsonrpc": JSONRPC_VERSION,
|
||||
"error": {
|
||||
"code": -1234, // Junk error code.
|
||||
"message": message
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
Ok::<_, warp::reject::Rejection>(
|
||||
warp::http::Response::builder()
|
||||
.status(200)
|
||||
.body(serde_json::to_string(&response).expect("response must be valid JSON")),
|
||||
)
|
||||
});
|
||||
|
||||
// `/echo`
|
||||
//
|
||||
// Sends the body of the request to `ctx.last_echo_request` so we can inspect requests.
|
||||
let echo = warp::path("echo")
|
||||
.and(warp::body::bytes())
|
||||
.and(ctx_filter)
|
||||
.and_then(|bytes: Bytes, ctx: Arc<Context<T>>| async move {
|
||||
*ctx.last_echo_request.write().await = Some(bytes.clone());
|
||||
Ok::<_, warp::reject::Rejection>(
|
||||
warp::http::Response::builder().status(200).body(bytes),
|
||||
)
|
||||
});
|
||||
|
||||
let routes = warp::post()
|
||||
.and(root.or(echo))
|
||||
// Add a `Server` header.
|
||||
.map(|reply| warp::reply::with_header(reply, "Server", "lighthouse-mock-execution-client"));
|
||||
|
||||
let (listening_socket, server) = warp::serve(routes).try_bind_with_graceful_shutdown(
|
||||
SocketAddrV4::new(config.listen_addr, config.listen_port),
|
||||
async {
|
||||
shutdown.await;
|
||||
},
|
||||
)?;
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Metrics HTTP server started";
|
||||
"listen_address" => listening_socket.to_string(),
|
||||
);
|
||||
|
||||
Ok((listening_socket, server))
|
||||
}
|
||||
Reference in New Issue
Block a user