mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-12 02:14:10 +00:00
Merge remote-tracking branch 'origin/unstable' into tree-states
This commit is contained in:
@@ -18,6 +18,7 @@ serde_json = "1.0.58"
|
||||
serde = { version = "1.0.116", features = ["derive"] }
|
||||
eth1 = { path = "../eth1" }
|
||||
warp = { git = "https://github.com/macladson/warp", rev ="dfa259e", features = ["tls"] }
|
||||
jsonwebtoken = "8"
|
||||
environment = { path = "../../lighthouse/environment" }
|
||||
bytes = "1.1.0"
|
||||
task_executor = { path = "../../common/task_executor" }
|
||||
@@ -29,3 +30,8 @@ tree_hash = "0.4.1"
|
||||
tree_hash_derive = { path = "../../consensus/tree_hash_derive"}
|
||||
parking_lot = "0.11.0"
|
||||
slot_clock = { path = "../../common/slot_clock" }
|
||||
tempfile = "3.1.0"
|
||||
rand = "0.7.3"
|
||||
zeroize = { version = "1.4.2", features = ["zeroize_derive"] }
|
||||
lighthouse_metrics = { path = "../../common/lighthouse_metrics" }
|
||||
lazy_static = "1.4.0"
|
||||
@@ -1,12 +1,15 @@
|
||||
use async_trait::async_trait;
|
||||
use eth1::http::RpcError;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const LATEST_TAG: &str = "latest";
|
||||
|
||||
use crate::engines::ForkChoiceState;
|
||||
pub use json_structures::TransitionConfigurationV1;
|
||||
pub use types::{Address, EthSpec, ExecutionBlockHash, ExecutionPayload, Hash256, Uint256};
|
||||
|
||||
pub mod auth;
|
||||
pub mod http;
|
||||
pub mod json_structures;
|
||||
|
||||
@@ -15,6 +18,7 @@ pub type PayloadId = [u8; 8];
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Reqwest(reqwest::Error),
|
||||
Auth(auth::Error),
|
||||
BadResponse(String),
|
||||
RequestFailed(String),
|
||||
InvalidExecutePayloadResponse(&'static str),
|
||||
@@ -27,11 +31,19 @@ pub enum Error {
|
||||
ExecutionHeadBlockNotFound,
|
||||
ParentHashEqualsBlockHash(ExecutionBlockHash),
|
||||
PayloadIdUnavailable,
|
||||
TransitionConfigurationMismatch,
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
Error::Reqwest(e)
|
||||
if matches!(
|
||||
e.status(),
|
||||
Some(StatusCode::UNAUTHORIZED) | Some(StatusCode::FORBIDDEN)
|
||||
) {
|
||||
Error::Auth(auth::Error::InvalidToken)
|
||||
} else {
|
||||
Error::Reqwest(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +53,12 @@ impl From<serde_json::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<auth::Error> for Error {
|
||||
fn from(e: auth::Error) -> Self {
|
||||
Error::Auth(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic interface for an execution engine API.
|
||||
#[async_trait]
|
||||
pub trait EngineApi {
|
||||
@@ -71,6 +89,11 @@ pub trait EngineApi {
|
||||
forkchoice_state: ForkChoiceState,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
) -> Result<ForkchoiceUpdatedResponse, Error>;
|
||||
|
||||
async fn exchange_transition_configuration_v1(
|
||||
&self,
|
||||
transition_configuration: TransitionConfigurationV1,
|
||||
) -> Result<TransitionConfigurationV1, Error>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -107,10 +130,10 @@ pub struct ExecutionBlock {
|
||||
pub total_difficulty: Uint256,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PayloadAttributes {
|
||||
pub timestamp: u64,
|
||||
pub random: Hash256,
|
||||
pub prev_randao: Hash256,
|
||||
pub suggested_fee_recipient: Address,
|
||||
}
|
||||
|
||||
|
||||
148
beacon_node/execution_layer/src/engine_api/auth.rs
Normal file
148
beacon_node/execution_layer/src/engine_api/auth.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use jsonwebtoken::{encode, get_current_timestamp, Algorithm, EncodingKey, Header};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// Default algorithm used for JWT token signing.
|
||||
const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256;
|
||||
|
||||
/// JWT secret length in bytes.
|
||||
pub const JWT_SECRET_LENGTH: usize = 32;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
JWT(jsonwebtoken::errors::Error),
|
||||
InvalidToken,
|
||||
}
|
||||
|
||||
impl From<jsonwebtoken::errors::Error> for Error {
|
||||
fn from(e: jsonwebtoken::errors::Error) -> Self {
|
||||
Error::JWT(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides wrapper around `[u8; JWT_SECRET_LENGTH]` that implements `Zeroize`.
|
||||
#[derive(Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct JwtKey([u8; JWT_SECRET_LENGTH as usize]);
|
||||
|
||||
impl JwtKey {
|
||||
/// Wrap given slice in `Self`. Returns an error if slice.len() != `JWT_SECRET_LENGTH`.
|
||||
pub fn from_slice(key: &[u8]) -> Result<Self, String> {
|
||||
if key.len() != JWT_SECRET_LENGTH {
|
||||
return Err(format!(
|
||||
"Invalid key length. Expected {} got {}",
|
||||
JWT_SECRET_LENGTH,
|
||||
key.len()
|
||||
));
|
||||
}
|
||||
let mut res = [0; JWT_SECRET_LENGTH];
|
||||
res.copy_from_slice(key);
|
||||
Ok(Self(res))
|
||||
}
|
||||
|
||||
/// Generate a random secret.
|
||||
pub fn random() -> Self {
|
||||
Self(rand::thread_rng().gen::<[u8; JWT_SECRET_LENGTH]>())
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying byte array.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Returns the hex encoded `String` for the secret.
|
||||
pub fn hex_string(&self) -> String {
|
||||
hex::encode(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the JWT secret and claims parameters.
|
||||
pub struct Auth {
|
||||
key: EncodingKey,
|
||||
id: Option<String>,
|
||||
clv: Option<String>,
|
||||
}
|
||||
|
||||
impl Auth {
|
||||
pub fn new(secret: JwtKey, id: Option<String>, clv: Option<String>) -> Self {
|
||||
Self {
|
||||
key: EncodingKey::from_secret(secret.as_bytes()),
|
||||
id,
|
||||
clv,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a JWT token with `claims.iat` set to current time.
|
||||
pub fn generate_token(&self) -> Result<String, Error> {
|
||||
let claims = self.generate_claims_at_timestamp();
|
||||
self.generate_token_with_claims(&claims)
|
||||
}
|
||||
|
||||
/// Generate a JWT token with the given claims.
|
||||
fn generate_token_with_claims(&self, claims: &Claims) -> Result<String, Error> {
|
||||
let header = Header::new(DEFAULT_ALGORITHM);
|
||||
Ok(encode(&header, claims, &self.key)?)
|
||||
}
|
||||
|
||||
/// Generate a `Claims` struct with `iat` set to current time
|
||||
fn generate_claims_at_timestamp(&self) -> Claims {
|
||||
Claims {
|
||||
iat: get_current_timestamp(),
|
||||
id: self.id.clone(),
|
||||
clv: self.clv.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate a JWT token given the secret key and return the originally signed `TokenData`.
|
||||
pub fn validate_token(
|
||||
token: &str,
|
||||
secret: &JwtKey,
|
||||
) -> Result<jsonwebtoken::TokenData<Claims>, Error> {
|
||||
let mut validation = jsonwebtoken::Validation::new(DEFAULT_ALGORITHM);
|
||||
validation.validate_exp = false;
|
||||
validation.required_spec_claims.remove("exp");
|
||||
|
||||
jsonwebtoken::decode::<Claims>(
|
||||
token,
|
||||
&jsonwebtoken::DecodingKey::from_secret(secret.as_bytes()),
|
||||
&validation,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Claims struct as defined in https://github.com/ethereum/execution-apis/blob/main/src/engine/authentication.md#jwt-claims
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Claims {
|
||||
/// issued-at claim. Represented as seconds passed since UNIX_EPOCH.
|
||||
iat: u64,
|
||||
/// Optional unique identifier for the CL node.
|
||||
id: Option<String>,
|
||||
/// Optional client version for the CL node.
|
||||
clv: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::JWT_SECRET;
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip() {
|
||||
let auth = Auth::new(
|
||||
JwtKey::from_slice(&JWT_SECRET).unwrap(),
|
||||
Some("42".into()),
|
||||
Some("Lighthouse".into()),
|
||||
);
|
||||
let claims = auth.generate_claims_at_timestamp();
|
||||
let token = auth.generate_token_with_claims(&claims).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Auth::validate_token(&token, &JwtKey::from_slice(&JWT_SECRET).unwrap())
|
||||
.unwrap()
|
||||
.claims,
|
||||
claims
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Contains an implementation of `EngineAPI` using the JSON-RPC API via HTTP.
|
||||
|
||||
use super::*;
|
||||
use crate::auth::Auth;
|
||||
use crate::json_structures::*;
|
||||
use async_trait::async_trait;
|
||||
use eth1::http::EIP155_ERROR_STR;
|
||||
@@ -36,9 +37,15 @@ pub const ENGINE_GET_PAYLOAD_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
pub const ENGINE_FORKCHOICE_UPDATED_V1: &str = "engine_forkchoiceUpdatedV1";
|
||||
pub const ENGINE_FORKCHOICE_UPDATED_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
|
||||
pub const ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1: &str =
|
||||
"engine_exchangeTransitionConfigurationV1";
|
||||
pub const ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1_TIMEOUT: Duration =
|
||||
Duration::from_millis(500);
|
||||
|
||||
pub struct HttpJsonRpc {
|
||||
pub client: Client,
|
||||
pub url: SensitiveUrl,
|
||||
auth: Option<Auth>,
|
||||
}
|
||||
|
||||
impl HttpJsonRpc {
|
||||
@@ -46,6 +53,15 @@ impl HttpJsonRpc {
|
||||
Ok(Self {
|
||||
client: Client::builder().build()?,
|
||||
url,
|
||||
auth: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_auth(url: SensitiveUrl, auth: Auth) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
client: Client::builder().build()?,
|
||||
url,
|
||||
auth: Some(auth),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -62,17 +78,19 @@ impl HttpJsonRpc {
|
||||
id: STATIC_ID,
|
||||
};
|
||||
|
||||
let body: JsonResponseBody = self
|
||||
let mut request = self
|
||||
.client
|
||||
.post(self.url.full.clone())
|
||||
.timeout(timeout)
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
.json(&body);
|
||||
|
||||
// Generate and add a jwt token to the header if auth is defined.
|
||||
if let Some(auth) = &self.auth {
|
||||
request = request.bearer_auth(auth.generate_token()?);
|
||||
};
|
||||
|
||||
let body: JsonResponseBody = request.send().await?.error_for_status()?.json().await?;
|
||||
|
||||
match (body.result, body.error) {
|
||||
(result, None) => serde_json::from_value(result).map_err(Into::into),
|
||||
@@ -179,12 +197,30 @@ impl EngineApi for HttpJsonRpc {
|
||||
|
||||
Ok(response.into())
|
||||
}
|
||||
|
||||
async fn exchange_transition_configuration_v1(
|
||||
&self,
|
||||
transition_configuration: TransitionConfigurationV1,
|
||||
) -> Result<TransitionConfigurationV1, Error> {
|
||||
let params = json!([transition_configuration]);
|
||||
|
||||
let response = self
|
||||
.rpc_request(
|
||||
ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1,
|
||||
params,
|
||||
ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::auth::JwtKey;
|
||||
use super::*;
|
||||
use crate::test_utils::MockServer;
|
||||
use crate::test_utils::{MockServer, JWT_SECRET};
|
||||
use std::future::Future;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -197,14 +233,25 @@ mod test {
|
||||
}
|
||||
|
||||
impl Tester {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(with_auth: bool) -> Self {
|
||||
let server = MockServer::unit_testing();
|
||||
|
||||
let rpc_url = SensitiveUrl::parse(&server.url()).unwrap();
|
||||
let rpc_client = Arc::new(HttpJsonRpc::new(rpc_url).unwrap());
|
||||
|
||||
let echo_url = SensitiveUrl::parse(&format!("{}/echo", server.url())).unwrap();
|
||||
let echo_client = Arc::new(HttpJsonRpc::new(echo_url).unwrap());
|
||||
// Create rpc clients that include JWT auth headers if `with_auth` is true.
|
||||
let (rpc_client, echo_client) = if with_auth {
|
||||
let rpc_auth = Auth::new(JwtKey::from_slice(&JWT_SECRET).unwrap(), None, None);
|
||||
let echo_auth = Auth::new(JwtKey::from_slice(&JWT_SECRET).unwrap(), None, None);
|
||||
(
|
||||
Arc::new(HttpJsonRpc::new_with_auth(rpc_url, rpc_auth).unwrap()),
|
||||
Arc::new(HttpJsonRpc::new_with_auth(echo_url, echo_auth).unwrap()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Arc::new(HttpJsonRpc::new(rpc_url).unwrap()),
|
||||
Arc::new(HttpJsonRpc::new(echo_url).unwrap()),
|
||||
)
|
||||
};
|
||||
|
||||
Self {
|
||||
server,
|
||||
@@ -235,6 +282,22 @@ mod test {
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn assert_auth_failure<R, F, T>(self, request_func: R) -> Self
|
||||
where
|
||||
R: Fn(Arc<HttpJsonRpc>) -> F,
|
||||
F: Future<Output = Result<T, Error>>,
|
||||
T: std::fmt::Debug,
|
||||
{
|
||||
let res = request_func(self.echo_client.clone()).await;
|
||||
if !matches!(res, Err(Error::Auth(_))) {
|
||||
panic!(
|
||||
"No authentication provided, rpc call should have failed.\nResult: {:?}",
|
||||
res
|
||||
)
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn with_preloaded_responses<R, F>(
|
||||
self,
|
||||
preloaded_responses: Vec<serde_json::Value>,
|
||||
@@ -288,7 +351,7 @@ mod test {
|
||||
"stateRoot": HASH_01,
|
||||
"receiptsRoot": HASH_00,
|
||||
"logsBloom": LOGS_BLOOM_01,
|
||||
"random": HASH_01,
|
||||
"prevRandao": HASH_01,
|
||||
"blockNumber": "0x0",
|
||||
"gasLimit": "0x1",
|
||||
"gasUsed": "0x2",
|
||||
@@ -391,7 +454,7 @@ mod test {
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_block_by_number_request() {
|
||||
Tester::new()
|
||||
Tester::new(true)
|
||||
.assert_request_equals(
|
||||
|client| async move {
|
||||
let _ = client
|
||||
@@ -406,11 +469,19 @@ mod test {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
Tester::new(false)
|
||||
.assert_auth_failure(|client| async move {
|
||||
client
|
||||
.get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG))
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_block_by_hash_request() {
|
||||
Tester::new()
|
||||
Tester::new(true)
|
||||
.assert_request_equals(
|
||||
|client| async move {
|
||||
let _ = client
|
||||
@@ -425,11 +496,19 @@ mod test {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
Tester::new(false)
|
||||
.assert_auth_failure(|client| async move {
|
||||
client
|
||||
.get_block_by_hash(ExecutionBlockHash::repeat_byte(1))
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forkchoice_updated_v1_with_payload_attributes_request() {
|
||||
Tester::new()
|
||||
Tester::new(true)
|
||||
.assert_request_equals(
|
||||
|client| async move {
|
||||
let _ = client
|
||||
@@ -441,7 +520,7 @@ mod test {
|
||||
},
|
||||
Some(PayloadAttributes {
|
||||
timestamp: 5,
|
||||
random: Hash256::zero(),
|
||||
prev_randao: Hash256::zero(),
|
||||
suggested_fee_recipient: Address::repeat_byte(0),
|
||||
}),
|
||||
)
|
||||
@@ -458,17 +537,36 @@ mod test {
|
||||
},
|
||||
{
|
||||
"timestamp":"0x5",
|
||||
"random": HASH_00,
|
||||
"prevRandao": HASH_00,
|
||||
"suggestedFeeRecipient": ADDRESS_00
|
||||
}]
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
Tester::new(false)
|
||||
.assert_auth_failure(|client| async move {
|
||||
client
|
||||
.forkchoice_updated_v1(
|
||||
ForkChoiceState {
|
||||
head_block_hash: ExecutionBlockHash::repeat_byte(1),
|
||||
safe_block_hash: ExecutionBlockHash::repeat_byte(1),
|
||||
finalized_block_hash: ExecutionBlockHash::zero(),
|
||||
},
|
||||
Some(PayloadAttributes {
|
||||
timestamp: 5,
|
||||
prev_randao: Hash256::zero(),
|
||||
suggested_fee_recipient: Address::repeat_byte(0),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_payload_v1_request() {
|
||||
Tester::new()
|
||||
Tester::new(true)
|
||||
.assert_request_equals(
|
||||
|client| async move {
|
||||
let _ = client.get_payload_v1::<MainnetEthSpec>([42; 8]).await;
|
||||
@@ -481,11 +579,17 @@ mod test {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
Tester::new(false)
|
||||
.assert_auth_failure(|client| async move {
|
||||
client.get_payload_v1::<MainnetEthSpec>([42; 8]).await
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn new_payload_v1_request() {
|
||||
Tester::new()
|
||||
Tester::new(true)
|
||||
.assert_request_equals(
|
||||
|client| async move {
|
||||
let _ = client
|
||||
@@ -495,7 +599,7 @@ mod test {
|
||||
state_root: Hash256::repeat_byte(1),
|
||||
receipts_root: Hash256::repeat_byte(0),
|
||||
logs_bloom: vec![1; 256].into(),
|
||||
random: Hash256::repeat_byte(1),
|
||||
prev_randao: Hash256::repeat_byte(1),
|
||||
block_number: 0,
|
||||
gas_limit: 1,
|
||||
gas_used: 2,
|
||||
@@ -517,7 +621,7 @@ mod test {
|
||||
"stateRoot": HASH_01,
|
||||
"receiptsRoot": HASH_00,
|
||||
"logsBloom": LOGS_BLOOM_01,
|
||||
"random": HASH_01,
|
||||
"prevRandao": HASH_01,
|
||||
"blockNumber": "0x0",
|
||||
"gasLimit": "0x1",
|
||||
"gasUsed": "0x2",
|
||||
@@ -530,11 +634,34 @@ mod test {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
Tester::new(false)
|
||||
.assert_auth_failure(|client| async move {
|
||||
client
|
||||
.new_payload_v1::<MainnetEthSpec>(ExecutionPayload {
|
||||
parent_hash: ExecutionBlockHash::repeat_byte(0),
|
||||
fee_recipient: Address::repeat_byte(1),
|
||||
state_root: Hash256::repeat_byte(1),
|
||||
receipts_root: Hash256::repeat_byte(0),
|
||||
logs_bloom: vec![1; 256].into(),
|
||||
prev_randao: Hash256::repeat_byte(1),
|
||||
block_number: 0,
|
||||
gas_limit: 1,
|
||||
gas_used: 2,
|
||||
timestamp: 42,
|
||||
extra_data: vec![].into(),
|
||||
base_fee_per_gas: Uint256::from(1),
|
||||
block_hash: ExecutionBlockHash::repeat_byte(1),
|
||||
transactions: vec![].into(),
|
||||
})
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forkchoice_updated_v1_request() {
|
||||
Tester::new()
|
||||
Tester::new(true)
|
||||
.assert_request_equals(
|
||||
|client| async move {
|
||||
let _ = client
|
||||
@@ -560,6 +687,21 @@ mod test {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
Tester::new(false)
|
||||
.assert_auth_failure(|client| async move {
|
||||
client
|
||||
.forkchoice_updated_v1(
|
||||
ForkChoiceState {
|
||||
head_block_hash: ExecutionBlockHash::repeat_byte(0),
|
||||
safe_block_hash: ExecutionBlockHash::repeat_byte(0),
|
||||
finalized_block_hash: ExecutionBlockHash::repeat_byte(1),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
fn str_to_payload_id(s: &str) -> PayloadId {
|
||||
@@ -583,7 +725,7 @@ mod test {
|
||||
/// The `id` field has been modified on these vectors to match the one we use.
|
||||
#[tokio::test]
|
||||
async fn geth_test_vectors() {
|
||||
Tester::new()
|
||||
Tester::new(true)
|
||||
.assert_request_equals(
|
||||
// engine_forkchoiceUpdatedV1 (prepare payload) REQUEST validation
|
||||
|client| async move {
|
||||
@@ -596,7 +738,7 @@ mod test {
|
||||
},
|
||||
Some(PayloadAttributes {
|
||||
timestamp: 5,
|
||||
random: Hash256::zero(),
|
||||
prev_randao: Hash256::zero(),
|
||||
suggested_fee_recipient: Address::from_str("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(),
|
||||
})
|
||||
)
|
||||
@@ -613,7 +755,7 @@ mod test {
|
||||
},
|
||||
{
|
||||
"timestamp":"0x5",
|
||||
"random": HASH_00,
|
||||
"prevRandao": HASH_00,
|
||||
"suggestedFeeRecipient":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"
|
||||
}]
|
||||
})
|
||||
@@ -643,7 +785,7 @@ mod test {
|
||||
},
|
||||
Some(PayloadAttributes {
|
||||
timestamp: 5,
|
||||
random: Hash256::zero(),
|
||||
prev_randao: Hash256::zero(),
|
||||
suggested_fee_recipient: Address::from_str("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(),
|
||||
})
|
||||
)
|
||||
@@ -687,7 +829,7 @@ mod test {
|
||||
"stateRoot":"0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45",
|
||||
"receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||
"logsBloom": LOGS_BLOOM_00,
|
||||
"random": HASH_00,
|
||||
"prevRandao": HASH_00,
|
||||
"blockNumber":"0x1",
|
||||
"gasLimit":"0x1c95111",
|
||||
"gasUsed":"0x0",
|
||||
@@ -710,7 +852,7 @@ mod test {
|
||||
state_root: Hash256::from_str("0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45").unwrap(),
|
||||
receipts_root: Hash256::from_str("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(),
|
||||
logs_bloom: vec![0; 256].into(),
|
||||
random: Hash256::zero(),
|
||||
prev_randao: Hash256::zero(),
|
||||
block_number: 1,
|
||||
gas_limit: u64::from_str_radix("1c95111",16).unwrap(),
|
||||
gas_used: 0,
|
||||
@@ -735,7 +877,7 @@ mod test {
|
||||
state_root: Hash256::from_str("0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45").unwrap(),
|
||||
receipts_root: Hash256::from_str("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(),
|
||||
logs_bloom: vec![0; 256].into(),
|
||||
random: Hash256::zero(),
|
||||
prev_randao: Hash256::zero(),
|
||||
block_number: 1,
|
||||
gas_limit: u64::from_str_radix("1c9c380",16).unwrap(),
|
||||
gas_used: 0,
|
||||
@@ -757,7 +899,7 @@ mod test {
|
||||
"stateRoot":"0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45",
|
||||
"receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||
"logsBloom": LOGS_BLOOM_00,
|
||||
"random": HASH_00,
|
||||
"prevRandao": HASH_00,
|
||||
"blockNumber":"0x1",
|
||||
"gasLimit":"0x1c9c380",
|
||||
"gasUsed":"0x0",
|
||||
|
||||
@@ -65,7 +65,7 @@ pub struct JsonExecutionPayloadV1<T: EthSpec> {
|
||||
pub receipts_root: Hash256,
|
||||
#[serde(with = "serde_logs_bloom")]
|
||||
pub logs_bloom: FixedVector<u8, T::BytesPerLogsBloom>,
|
||||
pub random: Hash256,
|
||||
pub prev_randao: Hash256,
|
||||
#[serde(with = "eth2_serde_utils::u64_hex_be")]
|
||||
pub block_number: u64,
|
||||
#[serde(with = "eth2_serde_utils::u64_hex_be")]
|
||||
@@ -92,7 +92,7 @@ impl<T: EthSpec> From<ExecutionPayload<T>> for JsonExecutionPayloadV1<T> {
|
||||
state_root,
|
||||
receipts_root,
|
||||
logs_bloom,
|
||||
random,
|
||||
prev_randao,
|
||||
block_number,
|
||||
gas_limit,
|
||||
gas_used,
|
||||
@@ -109,7 +109,7 @@ impl<T: EthSpec> From<ExecutionPayload<T>> for JsonExecutionPayloadV1<T> {
|
||||
state_root,
|
||||
receipts_root,
|
||||
logs_bloom,
|
||||
random,
|
||||
prev_randao,
|
||||
block_number,
|
||||
gas_limit,
|
||||
gas_used,
|
||||
@@ -131,7 +131,7 @@ impl<T: EthSpec> From<JsonExecutionPayloadV1<T>> for ExecutionPayload<T> {
|
||||
state_root,
|
||||
receipts_root,
|
||||
logs_bloom,
|
||||
random,
|
||||
prev_randao,
|
||||
block_number,
|
||||
gas_limit,
|
||||
gas_used,
|
||||
@@ -148,7 +148,7 @@ impl<T: EthSpec> From<JsonExecutionPayloadV1<T>> for ExecutionPayload<T> {
|
||||
state_root,
|
||||
receipts_root,
|
||||
logs_bloom,
|
||||
random,
|
||||
prev_randao,
|
||||
block_number,
|
||||
gas_limit,
|
||||
gas_used,
|
||||
@@ -166,7 +166,7 @@ impl<T: EthSpec> From<JsonExecutionPayloadV1<T>> for ExecutionPayload<T> {
|
||||
pub struct JsonPayloadAttributesV1 {
|
||||
#[serde(with = "eth2_serde_utils::u64_hex_be")]
|
||||
pub timestamp: u64,
|
||||
pub random: Hash256,
|
||||
pub prev_randao: Hash256,
|
||||
pub suggested_fee_recipient: Address,
|
||||
}
|
||||
|
||||
@@ -175,13 +175,13 @@ impl From<PayloadAttributes> for JsonPayloadAttributesV1 {
|
||||
// Use this verbose deconstruction pattern to ensure no field is left unused.
|
||||
let PayloadAttributes {
|
||||
timestamp,
|
||||
random,
|
||||
prev_randao,
|
||||
suggested_fee_recipient,
|
||||
} = p;
|
||||
|
||||
Self {
|
||||
timestamp,
|
||||
random,
|
||||
prev_randao,
|
||||
suggested_fee_recipient,
|
||||
}
|
||||
}
|
||||
@@ -192,13 +192,13 @@ impl From<JsonPayloadAttributesV1> for PayloadAttributes {
|
||||
// Use this verbose deconstruction pattern to ensure no field is left unused.
|
||||
let JsonPayloadAttributesV1 {
|
||||
timestamp,
|
||||
random,
|
||||
prev_randao,
|
||||
suggested_fee_recipient,
|
||||
} = j;
|
||||
|
||||
Self {
|
||||
timestamp,
|
||||
random,
|
||||
prev_randao,
|
||||
suggested_fee_recipient,
|
||||
}
|
||||
}
|
||||
@@ -364,6 +364,15 @@ impl From<ForkchoiceUpdatedResponse> for JsonForkchoiceUpdatedV1Response {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransitionConfigurationV1 {
|
||||
pub terminal_total_difficulty: Uint256,
|
||||
pub terminal_block_hash: ExecutionBlockHash,
|
||||
#[serde(with = "eth2_serde_utils::u64_hex_be")]
|
||||
pub terminal_block_number: u64,
|
||||
}
|
||||
|
||||
/// Serializes the `logs_bloom` field of an `ExecutionPayload`.
|
||||
pub mod serde_logs_bloom {
|
||||
use super::*;
|
||||
|
||||
@@ -21,6 +21,7 @@ enum EngineState {
|
||||
Synced,
|
||||
Offline,
|
||||
Syncing,
|
||||
AuthFailed,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
@@ -50,7 +51,7 @@ impl Logging {
|
||||
struct PayloadIdCacheKey {
|
||||
pub head_block_hash: ExecutionBlockHash,
|
||||
pub timestamp: u64,
|
||||
pub random: Hash256,
|
||||
pub prev_randao: Hash256,
|
||||
pub suggested_fee_recipient: Address,
|
||||
}
|
||||
|
||||
@@ -77,7 +78,7 @@ impl<T> Engine<T> {
|
||||
&self,
|
||||
head_block_hash: ExecutionBlockHash,
|
||||
timestamp: u64,
|
||||
random: Hash256,
|
||||
prev_randao: Hash256,
|
||||
suggested_fee_recipient: Address,
|
||||
) -> Option<PayloadId> {
|
||||
self.payload_id_cache
|
||||
@@ -86,7 +87,7 @@ impl<T> Engine<T> {
|
||||
.get(&PayloadIdCacheKey {
|
||||
head_block_hash,
|
||||
timestamp,
|
||||
random,
|
||||
prev_randao,
|
||||
suggested_fee_recipient,
|
||||
})
|
||||
.cloned()
|
||||
@@ -135,6 +136,7 @@ pub struct Engines<T> {
|
||||
pub enum EngineError {
|
||||
Offline { id: String },
|
||||
Api { id: String, error: EngineApiError },
|
||||
Auth { id: String },
|
||||
}
|
||||
|
||||
impl<T: EngineApi> Engines<T> {
|
||||
@@ -150,6 +152,16 @@ impl<T: EngineApi> Engines<T> {
|
||||
let latest_forkchoice_state = self.get_latest_forkchoice_state().await;
|
||||
|
||||
if let Some(forkchoice_state) = latest_forkchoice_state {
|
||||
if forkchoice_state.head_block_hash == ExecutionBlockHash::zero() {
|
||||
debug!(
|
||||
self.log,
|
||||
"No need to call forkchoiceUpdated";
|
||||
"msg" => "head does not have execution enabled",
|
||||
"id" => &engine.id,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
info!(
|
||||
self.log,
|
||||
"Issuing forkchoiceUpdated";
|
||||
@@ -226,6 +238,18 @@ impl<T: EngineApi> Engines<T> {
|
||||
|
||||
*state_lock = EngineState::Syncing
|
||||
}
|
||||
Err(EngineApiError::Auth(err)) => {
|
||||
if logging.is_enabled() {
|
||||
warn!(
|
||||
self.log,
|
||||
"Failed jwt authorization";
|
||||
"error" => ?err,
|
||||
"id" => &engine.id
|
||||
);
|
||||
}
|
||||
|
||||
*state_lock = EngineState::AuthFailed
|
||||
}
|
||||
Err(e) => {
|
||||
if logging.is_enabled() {
|
||||
warn!(
|
||||
@@ -295,7 +319,13 @@ impl<T: EngineApi> Engines<T> {
|
||||
let mut errors = vec![];
|
||||
|
||||
for engine in &self.engines {
|
||||
let engine_synced = *engine.state.read().await == EngineState::Synced;
|
||||
let (engine_synced, engine_auth_failed) = {
|
||||
let state = engine.state.read().await;
|
||||
(
|
||||
*state == EngineState::Synced,
|
||||
*state == EngineState::AuthFailed,
|
||||
)
|
||||
};
|
||||
if engine_synced {
|
||||
match func(engine).await {
|
||||
Ok(result) => return Ok(result),
|
||||
@@ -313,6 +343,10 @@ impl<T: EngineApi> Engines<T> {
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if engine_auth_failed {
|
||||
errors.push(EngineError::Auth {
|
||||
id: engine.id.clone(),
|
||||
})
|
||||
} else {
|
||||
errors.push(EngineError::Offline {
|
||||
id: engine.id.clone(),
|
||||
@@ -393,7 +427,7 @@ impl PayloadIdCacheKey {
|
||||
Self {
|
||||
head_block_hash: state.head_block_hash,
|
||||
timestamp: attributes.timestamp,
|
||||
random: attributes.random,
|
||||
prev_randao: attributes.prev_randao,
|
||||
suggested_fee_recipient: attributes.suggested_fee_recipient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,32 +4,42 @@
|
||||
//! This crate only provides useful functionality for "The Merge", it does not provide any of the
|
||||
//! deposit-contract functionality that the `beacon_node/eth1` crate already provides.
|
||||
|
||||
use auth::{Auth, JwtKey};
|
||||
use engine_api::{Error as ApiError, *};
|
||||
use engines::{Engine, EngineError, Engines, ForkChoiceState, Logging};
|
||||
use lru::LruCache;
|
||||
use payload_status::process_multiple_payload_statuses;
|
||||
use sensitive_url::SensitiveUrl;
|
||||
use slog::{crit, debug, error, info, Logger};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use slog::{crit, debug, error, info, trace, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use task_executor::TaskExecutor;
|
||||
use tokio::{
|
||||
sync::{Mutex, MutexGuard},
|
||||
sync::{Mutex, MutexGuard, RwLock},
|
||||
time::{sleep, sleep_until, Instant},
|
||||
};
|
||||
use types::{ChainSpec, Epoch, ExecutionBlockHash, ProposerPreparationData};
|
||||
use types::{ChainSpec, Epoch, ExecutionBlockHash, ProposerPreparationData, Slot};
|
||||
|
||||
pub use engine_api::{http::HttpJsonRpc, PayloadAttributes, PayloadStatusV1Status};
|
||||
pub use engine_api::{
|
||||
http::HttpJsonRpc, json_structures, PayloadAttributes, PayloadStatusV1Status,
|
||||
};
|
||||
pub use payload_status::PayloadStatus;
|
||||
|
||||
mod engine_api;
|
||||
mod engines;
|
||||
mod metrics;
|
||||
mod payload_status;
|
||||
pub mod test_utils;
|
||||
|
||||
/// Name for the default file used for the jwt secret.
|
||||
pub const DEFAULT_JWT_FILE: &str = "jwt.hex";
|
||||
|
||||
/// Each time the `ExecutionLayer` retrieves a block from an execution node, it stores that block
|
||||
/// in an LRU cache to avoid redundant lookups. This is the size of that cache.
|
||||
const EXECUTION_BLOCKS_LRU_CACHE_SIZE: usize = 128;
|
||||
@@ -44,6 +54,8 @@ const EXECUTION_BLOCKS_LRU_CACHE_SIZE: usize = 128;
|
||||
const DEFAULT_SUGGESTED_FEE_RECIPIENT: [u8; 20] =
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
|
||||
|
||||
const CONFIG_POLL_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
NoEngines,
|
||||
@@ -54,6 +66,7 @@ pub enum Error {
|
||||
FeeRecipientUnspecified,
|
||||
ConsensusFailure,
|
||||
MissingLatestValidHash,
|
||||
InvalidJWTSecret(String),
|
||||
}
|
||||
|
||||
impl From<ApiError> for Error {
|
||||
@@ -62,21 +75,60 @@ impl From<ApiError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct ProposerPreparationDataEntry {
|
||||
update_epoch: Epoch,
|
||||
preparation_data: ProposerPreparationData,
|
||||
}
|
||||
|
||||
#[derive(Hash, PartialEq, Eq)]
|
||||
pub struct ProposerKey {
|
||||
slot: Slot,
|
||||
head_block_root: Hash256,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub struct Proposer {
|
||||
validator_index: u64,
|
||||
payload_attributes: PayloadAttributes,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
engines: Engines<HttpJsonRpc>,
|
||||
execution_engine_forkchoice_lock: Mutex<()>,
|
||||
suggested_fee_recipient: Option<Address>,
|
||||
proposer_preparation_data: Mutex<HashMap<u64, ProposerPreparationDataEntry>>,
|
||||
execution_blocks: Mutex<LruCache<ExecutionBlockHash, ExecutionBlock>>,
|
||||
proposers: RwLock<HashMap<ProposerKey, Proposer>>,
|
||||
executor: TaskExecutor,
|
||||
log: Logger,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Endpoint urls for EL nodes that are running the engine api.
|
||||
pub execution_endpoints: Vec<SensitiveUrl>,
|
||||
/// JWT secrets for the above endpoints running the engine api.
|
||||
pub secret_files: Vec<PathBuf>,
|
||||
/// The default fee recipient to use on the beacon node if none if provided from
|
||||
/// the validator client during block preparation.
|
||||
pub suggested_fee_recipient: Option<Address>,
|
||||
/// An optional id for the beacon node that will be passed to the EL in the JWT token claim.
|
||||
pub jwt_id: Option<String>,
|
||||
/// An optional client version for the beacon node that will be passed to the EL in the JWT token claim.
|
||||
pub jwt_version: Option<String>,
|
||||
/// Default directory for the jwt secret if not provided through cli.
|
||||
pub default_datadir: PathBuf,
|
||||
}
|
||||
|
||||
fn strip_prefix(s: &str) -> &str {
|
||||
if let Some(stripped) = s.strip_prefix("0x") {
|
||||
stripped
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides access to one or more execution engines and provides a neat interface for consumption
|
||||
/// by the `BeaconChain`.
|
||||
///
|
||||
@@ -92,22 +144,73 @@ pub struct ExecutionLayer {
|
||||
}
|
||||
|
||||
impl ExecutionLayer {
|
||||
/// Instantiate `Self` with `urls.len()` engines, all using the JSON-RPC via HTTP.
|
||||
pub fn from_urls(
|
||||
urls: Vec<SensitiveUrl>,
|
||||
suggested_fee_recipient: Option<Address>,
|
||||
executor: TaskExecutor,
|
||||
log: Logger,
|
||||
) -> Result<Self, Error> {
|
||||
/// Instantiate `Self` with Execution engines specified using `Config`, all using the JSON-RPC via HTTP.
|
||||
pub fn from_config(config: Config, executor: TaskExecutor, log: Logger) -> Result<Self, Error> {
|
||||
let Config {
|
||||
execution_endpoints: urls,
|
||||
mut secret_files,
|
||||
suggested_fee_recipient,
|
||||
jwt_id,
|
||||
jwt_version,
|
||||
default_datadir,
|
||||
} = config;
|
||||
|
||||
if urls.is_empty() {
|
||||
return Err(Error::NoEngines);
|
||||
}
|
||||
|
||||
let engines = urls
|
||||
// Extend the jwt secret files with the default jwt secret path if not provided via cli.
|
||||
// This ensures that we have a jwt secret for every EL.
|
||||
secret_files.extend(vec![
|
||||
default_datadir.join(DEFAULT_JWT_FILE);
|
||||
urls.len().saturating_sub(secret_files.len())
|
||||
]);
|
||||
|
||||
let secrets: Vec<(JwtKey, PathBuf)> = secret_files
|
||||
.iter()
|
||||
.map(|p| {
|
||||
// Read secret from file if it already exists
|
||||
if p.exists() {
|
||||
std::fs::read_to_string(p)
|
||||
.map_err(|e| {
|
||||
format!("Failed to read JWT secret file {:?}, error: {:?}", p, e)
|
||||
})
|
||||
.and_then(|ref s| {
|
||||
let secret = JwtKey::from_slice(
|
||||
&hex::decode(strip_prefix(s))
|
||||
.map_err(|e| format!("Invalid hex string: {:?}", e))?,
|
||||
)?;
|
||||
Ok((secret, p.to_path_buf()))
|
||||
})
|
||||
} else {
|
||||
// Create a new file and write a randomly generated secret to it if file does not exist
|
||||
std::fs::File::options()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(p)
|
||||
.map_err(|e| {
|
||||
format!("Failed to open JWT secret file {:?}, error: {:?}", p, e)
|
||||
})
|
||||
.and_then(|mut f| {
|
||||
let secret = auth::JwtKey::random();
|
||||
f.write_all(secret.hex_string().as_bytes()).map_err(|e| {
|
||||
format!("Failed to write to JWT secret file: {:?}", e)
|
||||
})?;
|
||||
Ok((secret, p.to_path_buf()))
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Result<_, _>>()
|
||||
.map_err(Error::InvalidJWTSecret)?;
|
||||
|
||||
let engines: Vec<Engine<_>> = urls
|
||||
.into_iter()
|
||||
.map(|url| {
|
||||
.zip(secrets.into_iter())
|
||||
.map(|(url, (secret, path))| {
|
||||
let id = url.to_string();
|
||||
let api = HttpJsonRpc::new(url)?;
|
||||
let auth = Auth::new(secret, jwt_id.clone(), jwt_version.clone());
|
||||
debug!(log, "Loaded execution endpoint"; "endpoint" => %id, "jwt_path" => ?path);
|
||||
let api = HttpJsonRpc::new_with_auth(url, auth)?;
|
||||
Ok(Engine::new(id, api))
|
||||
})
|
||||
.collect::<Result<_, ApiError>>()?;
|
||||
@@ -118,8 +221,10 @@ impl ExecutionLayer {
|
||||
latest_forkchoice_state: <_>::default(),
|
||||
log: log.clone(),
|
||||
},
|
||||
execution_engine_forkchoice_lock: <_>::default(),
|
||||
suggested_fee_recipient,
|
||||
proposer_preparation_data: Mutex::new(HashMap::new()),
|
||||
proposers: RwLock::new(HashMap::new()),
|
||||
execution_blocks: Mutex::new(LruCache::new(EXECUTION_BLOCKS_LRU_CACHE_SIZE)),
|
||||
executor,
|
||||
log,
|
||||
@@ -154,10 +259,18 @@ impl ExecutionLayer {
|
||||
self.inner.proposer_preparation_data.lock().await
|
||||
}
|
||||
|
||||
fn proposers(&self) -> &RwLock<HashMap<ProposerKey, Proposer>> {
|
||||
&self.inner.proposers
|
||||
}
|
||||
|
||||
fn log(&self) -> &Logger {
|
||||
&self.inner.log
|
||||
}
|
||||
|
||||
pub async fn execution_engine_forkchoice_lock(&self) -> MutexGuard<'_, ()> {
|
||||
self.inner.execution_engine_forkchoice_lock.lock().await
|
||||
}
|
||||
|
||||
/// Convenience function to allow calling async functions in a non-async context.
|
||||
pub fn block_on<'a, T, U, V>(&'a self, generate_future: T) -> Result<V, Error>
|
||||
where
|
||||
@@ -261,8 +374,8 @@ impl ExecutionLayer {
|
||||
self.engines().upcheck_not_synced(Logging::Disabled).await;
|
||||
}
|
||||
|
||||
/// Spawns a routine which cleans the cached proposer preparations periodically.
|
||||
pub fn spawn_clean_proposer_preparation_routine<S: SlotClock + 'static, T: EthSpec>(
|
||||
/// Spawns a routine which cleans the cached proposer data periodically.
|
||||
pub fn spawn_clean_proposer_caches_routine<S: SlotClock + 'static, T: EthSpec>(
|
||||
&self,
|
||||
slot_clock: S,
|
||||
) {
|
||||
@@ -280,7 +393,7 @@ impl ExecutionLayer {
|
||||
.map(|slot| slot.epoch(T::slots_per_epoch()))
|
||||
{
|
||||
Some(current_epoch) => el
|
||||
.clean_proposer_preparation(current_epoch)
|
||||
.clean_proposer_caches::<T>(current_epoch)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
@@ -303,6 +416,24 @@ impl ExecutionLayer {
|
||||
self.spawn(preparation_cleaner, "exec_preparation_cleanup");
|
||||
}
|
||||
|
||||
/// Spawns a routine that polls the `exchange_transition_configuration` endpoint.
|
||||
pub fn spawn_transition_configuration_poll(&self, spec: ChainSpec) {
|
||||
let routine = |el: ExecutionLayer| async move {
|
||||
loop {
|
||||
if let Err(e) = el.exchange_transition_configuration(&spec).await {
|
||||
error!(
|
||||
el.log(),
|
||||
"Failed to check transition config";
|
||||
"error" => ?e
|
||||
);
|
||||
}
|
||||
sleep(CONFIG_POLL_INTERVAL).await;
|
||||
}
|
||||
};
|
||||
|
||||
self.spawn(routine, "exec_config_poll");
|
||||
}
|
||||
|
||||
/// Returns `true` if there is at least one synced and reachable engine.
|
||||
pub async fn is_synced(&self) -> bool {
|
||||
self.engines().any_synced().await
|
||||
@@ -317,7 +448,7 @@ impl ExecutionLayer {
|
||||
self.block_on_generic(|_| async move {
|
||||
self.update_proposer_preparation(update_epoch, preparation_data)
|
||||
.await
|
||||
})?
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates the proposer preparation data provided by validators
|
||||
@@ -325,23 +456,25 @@ impl ExecutionLayer {
|
||||
&self,
|
||||
update_epoch: Epoch,
|
||||
preparation_data: &[ProposerPreparationData],
|
||||
) -> Result<(), Error> {
|
||||
) {
|
||||
let mut proposer_preparation_data = self.proposer_preparation_data().await;
|
||||
for preparation_entry in preparation_data {
|
||||
proposer_preparation_data.insert(
|
||||
preparation_entry.validator_index,
|
||||
ProposerPreparationDataEntry {
|
||||
update_epoch,
|
||||
preparation_data: preparation_entry.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
let new = ProposerPreparationDataEntry {
|
||||
update_epoch,
|
||||
preparation_data: preparation_entry.clone(),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
let existing =
|
||||
proposer_preparation_data.insert(preparation_entry.validator_index, new.clone());
|
||||
|
||||
if existing != Some(new) {
|
||||
metrics::inc_counter(&metrics::EXECUTION_LAYER_PROPOSER_DATA_UPDATED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes expired entries from cached proposer preparations
|
||||
async fn clean_proposer_preparation(&self, current_epoch: Epoch) -> Result<(), Error> {
|
||||
/// Removes expired entries from proposer_preparation_data and proposers caches
|
||||
async fn clean_proposer_caches<T: EthSpec>(&self, current_epoch: Epoch) -> Result<(), Error> {
|
||||
let mut proposer_preparation_data = self.proposer_preparation_data().await;
|
||||
|
||||
// Keep all entries that have been updated in the last 2 epochs
|
||||
@@ -349,12 +482,33 @@ impl ExecutionLayer {
|
||||
proposer_preparation_data.retain(|_validator_index, preparation_entry| {
|
||||
preparation_entry.update_epoch >= retain_epoch
|
||||
});
|
||||
drop(proposer_preparation_data);
|
||||
|
||||
let retain_slot = retain_epoch.start_slot(T::slots_per_epoch());
|
||||
self.proposers()
|
||||
.write()
|
||||
.await
|
||||
.retain(|proposer_key, _proposer| proposer_key.slot >= retain_slot);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if there have been any validators registered via
|
||||
/// `Self::update_proposer_preparation`.
|
||||
pub async fn has_any_proposer_preparation_data(&self) -> bool {
|
||||
!self.proposer_preparation_data().await.is_empty()
|
||||
}
|
||||
|
||||
/// Returns `true` if the `proposer_index` has registered as a local validator via
|
||||
/// `Self::update_proposer_preparation`.
|
||||
pub async fn has_proposer_preparation_data(&self, proposer_index: u64) -> bool {
|
||||
self.proposer_preparation_data()
|
||||
.await
|
||||
.contains_key(&proposer_index)
|
||||
}
|
||||
|
||||
/// Returns the fee-recipient address that should be used to build a block
|
||||
async fn get_suggested_fee_recipient(&self, proposer_index: u64) -> Address {
|
||||
pub async fn get_suggested_fee_recipient(&self, proposer_index: u64) -> Address {
|
||||
if let Some(preparation_data_entry) =
|
||||
self.proposer_preparation_data().await.get(&proposer_index)
|
||||
{
|
||||
@@ -392,27 +546,36 @@ impl ExecutionLayer {
|
||||
&self,
|
||||
parent_hash: ExecutionBlockHash,
|
||||
timestamp: u64,
|
||||
random: Hash256,
|
||||
prev_randao: Hash256,
|
||||
finalized_block_hash: ExecutionBlockHash,
|
||||
proposer_index: u64,
|
||||
) -> Result<ExecutionPayload<T>, Error> {
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::EXECUTION_LAYER_REQUEST_TIMES,
|
||||
&[metrics::GET_PAYLOAD],
|
||||
);
|
||||
|
||||
let suggested_fee_recipient = self.get_suggested_fee_recipient(proposer_index).await;
|
||||
|
||||
debug!(
|
||||
self.log(),
|
||||
"Issuing engine_getPayload";
|
||||
"suggested_fee_recipient" => ?suggested_fee_recipient,
|
||||
"random" => ?random,
|
||||
"prev_randao" => ?prev_randao,
|
||||
"timestamp" => timestamp,
|
||||
"parent_hash" => ?parent_hash,
|
||||
);
|
||||
self.engines()
|
||||
.first_success(|engine| async move {
|
||||
let payload_id = if let Some(id) = engine
|
||||
.get_payload_id(parent_hash, timestamp, random, suggested_fee_recipient)
|
||||
.get_payload_id(parent_hash, timestamp, prev_randao, suggested_fee_recipient)
|
||||
.await
|
||||
{
|
||||
// The payload id has been cached for this engine.
|
||||
metrics::inc_counter_vec(
|
||||
&metrics::EXECUTION_LAYER_PRE_PREPARED_PAYLOAD_ID,
|
||||
&[metrics::HIT],
|
||||
);
|
||||
id
|
||||
} else {
|
||||
// The payload id has *not* been cached for this engine. Trigger an artificial
|
||||
@@ -421,6 +584,10 @@ impl ExecutionLayer {
|
||||
// TODO(merge): a better algorithm might try to favour a node that already had a
|
||||
// cached payload id, since a payload that has had more time to produce is
|
||||
// likely to be more profitable.
|
||||
metrics::inc_counter_vec(
|
||||
&metrics::EXECUTION_LAYER_PRE_PREPARED_PAYLOAD_ID,
|
||||
&[metrics::MISS],
|
||||
);
|
||||
let fork_choice_state = ForkChoiceState {
|
||||
head_block_hash: parent_hash,
|
||||
safe_block_hash: parent_hash,
|
||||
@@ -428,7 +595,7 @@ impl ExecutionLayer {
|
||||
};
|
||||
let payload_attributes = PayloadAttributes {
|
||||
timestamp,
|
||||
random,
|
||||
prev_randao,
|
||||
suggested_fee_recipient,
|
||||
};
|
||||
|
||||
@@ -475,7 +642,12 @@ impl ExecutionLayer {
|
||||
&self,
|
||||
execution_payload: &ExecutionPayload<T>,
|
||||
) -> Result<PayloadStatus, Error> {
|
||||
debug!(
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::EXECUTION_LAYER_REQUEST_TIMES,
|
||||
&[metrics::NEW_PAYLOAD],
|
||||
);
|
||||
|
||||
trace!(
|
||||
self.log(),
|
||||
"Issuing engine_newPayload";
|
||||
"parent_hash" => ?execution_payload.parent_hash,
|
||||
@@ -495,6 +667,64 @@ impl ExecutionLayer {
|
||||
)
|
||||
}
|
||||
|
||||
/// Register that the given `validator_index` is going to produce a block at `slot`.
|
||||
///
|
||||
/// The block will be built atop `head_block_root` and the EL will need to prepare an
|
||||
/// `ExecutionPayload` as defined by the given `payload_attributes`.
|
||||
pub async fn insert_proposer(
|
||||
&self,
|
||||
slot: Slot,
|
||||
head_block_root: Hash256,
|
||||
validator_index: u64,
|
||||
payload_attributes: PayloadAttributes,
|
||||
) -> bool {
|
||||
let proposers_key = ProposerKey {
|
||||
slot,
|
||||
head_block_root,
|
||||
};
|
||||
|
||||
let existing = self.proposers().write().await.insert(
|
||||
proposers_key,
|
||||
Proposer {
|
||||
validator_index,
|
||||
payload_attributes,
|
||||
},
|
||||
);
|
||||
|
||||
if existing.is_none() {
|
||||
metrics::inc_counter(&metrics::EXECUTION_LAYER_PROPOSER_INSERTED);
|
||||
}
|
||||
|
||||
existing.is_some()
|
||||
}
|
||||
|
||||
/// If there has been a proposer registered via `Self::insert_proposer` with a matching `slot`
|
||||
/// `head_block_root`, then return the appropriate `PayloadAttributes` for inclusion in
|
||||
/// `forkchoiceUpdated` calls.
|
||||
pub async fn payload_attributes(
|
||||
&self,
|
||||
current_slot: Slot,
|
||||
head_block_root: Hash256,
|
||||
) -> Option<PayloadAttributes> {
|
||||
let proposers_key = ProposerKey {
|
||||
slot: current_slot,
|
||||
head_block_root,
|
||||
};
|
||||
|
||||
let proposer = self.proposers().read().await.get(&proposers_key).cloned()?;
|
||||
|
||||
debug!(
|
||||
self.log(),
|
||||
"Beacon proposer found";
|
||||
"payload_attributes" => ?proposer.payload_attributes,
|
||||
"head_block_root" => ?head_block_root,
|
||||
"slot" => current_slot,
|
||||
"validator_index" => proposer.validator_index,
|
||||
);
|
||||
|
||||
Some(proposer.payload_attributes)
|
||||
}
|
||||
|
||||
/// Maps to the `engine_consensusValidated` JSON-RPC call.
|
||||
///
|
||||
/// ## Fallback Behaviour
|
||||
@@ -512,15 +742,44 @@ impl ExecutionLayer {
|
||||
&self,
|
||||
head_block_hash: ExecutionBlockHash,
|
||||
finalized_block_hash: ExecutionBlockHash,
|
||||
payload_attributes: Option<PayloadAttributes>,
|
||||
current_slot: Slot,
|
||||
head_block_root: Hash256,
|
||||
) -> Result<PayloadStatus, Error> {
|
||||
debug!(
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::EXECUTION_LAYER_REQUEST_TIMES,
|
||||
&[metrics::FORKCHOICE_UPDATED],
|
||||
);
|
||||
|
||||
trace!(
|
||||
self.log(),
|
||||
"Issuing engine_forkchoiceUpdated";
|
||||
"finalized_block_hash" => ?finalized_block_hash,
|
||||
"head_block_hash" => ?head_block_hash,
|
||||
);
|
||||
|
||||
let next_slot = current_slot + 1;
|
||||
let payload_attributes = self.payload_attributes(next_slot, head_block_root).await;
|
||||
|
||||
// Compute the "lookahead", the time between when the payload will be produced and now.
|
||||
if let Some(payload_attributes) = payload_attributes {
|
||||
if let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
let timestamp = Duration::from_secs(payload_attributes.timestamp);
|
||||
if let Some(lookahead) = timestamp.checked_sub(now) {
|
||||
metrics::observe_duration(
|
||||
&metrics::EXECUTION_LAYER_PAYLOAD_ATTRIBUTES_LOOKAHEAD,
|
||||
lookahead,
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
self.log(),
|
||||
"Late payload attributes";
|
||||
"timestamp" => ?timestamp,
|
||||
"now" => ?now,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// see https://hackmd.io/@n0ble/kintsugi-spec#Engine-API
|
||||
// for now, we must set safe_block_hash = head_block_hash
|
||||
let forkchoice_state = ForkChoiceState {
|
||||
@@ -551,6 +810,65 @@ impl ExecutionLayer {
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn exchange_transition_configuration(&self, spec: &ChainSpec) -> Result<(), Error> {
|
||||
let local = TransitionConfigurationV1 {
|
||||
terminal_total_difficulty: spec.terminal_total_difficulty,
|
||||
terminal_block_hash: spec.terminal_block_hash,
|
||||
terminal_block_number: 0,
|
||||
};
|
||||
|
||||
let broadcast_results = self
|
||||
.engines()
|
||||
.broadcast(|engine| engine.api.exchange_transition_configuration_v1(local))
|
||||
.await;
|
||||
|
||||
let mut errors = vec![];
|
||||
for (i, result) in broadcast_results.into_iter().enumerate() {
|
||||
match result {
|
||||
Ok(remote) => {
|
||||
if local.terminal_total_difficulty != remote.terminal_total_difficulty
|
||||
|| local.terminal_block_hash != remote.terminal_block_hash
|
||||
{
|
||||
error!(
|
||||
self.log(),
|
||||
"Execution client config mismatch";
|
||||
"msg" => "ensure lighthouse and the execution client are up-to-date and \
|
||||
configured consistently",
|
||||
"execution_endpoint" => i,
|
||||
"remote" => ?remote,
|
||||
"local" => ?local,
|
||||
);
|
||||
errors.push(EngineError::Api {
|
||||
id: i.to_string(),
|
||||
error: ApiError::TransitionConfigurationMismatch,
|
||||
});
|
||||
} else {
|
||||
debug!(
|
||||
self.log(),
|
||||
"Execution client config is OK";
|
||||
"execution_endpoint" => i
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
self.log(),
|
||||
"Unable to get transition config";
|
||||
"error" => ?e,
|
||||
"execution_endpoint" => i,
|
||||
);
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::EngineErrors(errors))
|
||||
}
|
||||
}
|
||||
|
||||
/// Used during block production to determine if the merge has been triggered.
|
||||
///
|
||||
/// ## Specification
|
||||
@@ -562,6 +880,11 @@ impl ExecutionLayer {
|
||||
&self,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Option<ExecutionBlockHash>, Error> {
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::EXECUTION_LAYER_REQUEST_TIMES,
|
||||
&[metrics::GET_TERMINAL_POW_BLOCK_HASH],
|
||||
);
|
||||
|
||||
let hash_opt = self
|
||||
.engines()
|
||||
.first_success(|engine| async move {
|
||||
@@ -673,6 +996,11 @@ impl ExecutionLayer {
|
||||
block_hash: ExecutionBlockHash,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Option<bool>, Error> {
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::EXECUTION_LAYER_REQUEST_TIMES,
|
||||
&[metrics::IS_VALID_TERMINAL_POW_BLOCK_HASH],
|
||||
);
|
||||
|
||||
let broadcast_results = self
|
||||
.engines()
|
||||
.broadcast(|engine| async move {
|
||||
@@ -793,6 +1121,7 @@ mod test {
|
||||
MockExecutionLayer::default_params()
|
||||
.move_to_block_prior_to_terminal_block()
|
||||
.with_terminal_block(|spec, el, _| async move {
|
||||
el.engines().upcheck_not_synced(Logging::Disabled).await;
|
||||
assert_eq!(el.get_terminal_pow_block_hash(&spec).await.unwrap(), None)
|
||||
})
|
||||
.await
|
||||
@@ -811,6 +1140,7 @@ mod test {
|
||||
MockExecutionLayer::default_params()
|
||||
.move_to_terminal_block()
|
||||
.with_terminal_block(|spec, el, terminal_block| async move {
|
||||
el.engines().upcheck_not_synced(Logging::Disabled).await;
|
||||
assert_eq!(
|
||||
el.is_valid_terminal_pow_block_hash(terminal_block.unwrap().block_hash, &spec)
|
||||
.await
|
||||
@@ -826,6 +1156,7 @@ mod test {
|
||||
MockExecutionLayer::default_params()
|
||||
.move_to_terminal_block()
|
||||
.with_terminal_block(|spec, el, terminal_block| async move {
|
||||
el.engines().upcheck_not_synced(Logging::Disabled).await;
|
||||
let invalid_terminal_block = terminal_block.unwrap().parent_hash;
|
||||
|
||||
assert_eq!(
|
||||
@@ -843,6 +1174,7 @@ mod test {
|
||||
MockExecutionLayer::default_params()
|
||||
.move_to_terminal_block()
|
||||
.with_terminal_block(|spec, el, _| async move {
|
||||
el.engines().upcheck_not_synced(Logging::Disabled).await;
|
||||
let missing_terminal_block = ExecutionBlockHash::repeat_byte(42);
|
||||
|
||||
assert_eq!(
|
||||
|
||||
34
beacon_node/execution_layer/src/metrics.rs
Normal file
34
beacon_node/execution_layer/src/metrics.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
pub use lighthouse_metrics::*;
|
||||
|
||||
pub const HIT: &str = "hit";
|
||||
pub const MISS: &str = "miss";
|
||||
pub const GET_PAYLOAD: &str = "get_payload";
|
||||
pub const NEW_PAYLOAD: &str = "new_payload";
|
||||
pub const FORKCHOICE_UPDATED: &str = "forkchoice_updated";
|
||||
pub const GET_TERMINAL_POW_BLOCK_HASH: &str = "get_terminal_pow_block_hash";
|
||||
pub const IS_VALID_TERMINAL_POW_BLOCK_HASH: &str = "is_valid_terminal_pow_block_hash";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref EXECUTION_LAYER_PROPOSER_INSERTED: Result<IntCounter> = try_create_int_counter(
|
||||
"execution_layer_proposer_inserted",
|
||||
"Count of times a new proposer is known",
|
||||
);
|
||||
pub static ref EXECUTION_LAYER_PROPOSER_DATA_UPDATED: Result<IntCounter> = try_create_int_counter(
|
||||
"execution_layer_proposer_data_updated",
|
||||
"Count of times new proposer data is supplied",
|
||||
);
|
||||
pub static ref EXECUTION_LAYER_REQUEST_TIMES: Result<HistogramVec> = try_create_histogram_vec(
|
||||
"execution_layer_request_times",
|
||||
"Duration of calls to ELs",
|
||||
&["method"]
|
||||
);
|
||||
pub static ref EXECUTION_LAYER_PAYLOAD_ATTRIBUTES_LOOKAHEAD: Result<Histogram> = try_create_histogram(
|
||||
"execution_layer_payload_attributes_lookahead",
|
||||
"Duration between an fcU call with PayloadAttributes and when the block should be produced",
|
||||
);
|
||||
pub static ref EXECUTION_LAYER_PRE_PREPARED_PAYLOAD_ID: Result<IntCounterVec> = try_create_int_counter_vec(
|
||||
"execution_layer_pre_prepared_payload_id",
|
||||
"Indicates hits or misses for already having prepared a payload id before payload production",
|
||||
&["event"]
|
||||
);
|
||||
}
|
||||
@@ -326,7 +326,7 @@ impl<T: EthSpec> ExecutionBlockGenerator<T> {
|
||||
receipts_root: Hash256::repeat_byte(42),
|
||||
state_root: Hash256::repeat_byte(43),
|
||||
logs_bloom: vec![0; 256].into(),
|
||||
random: attributes.random,
|
||||
prev_randao: attributes.prev_randao,
|
||||
block_number: parent.block_number() + 1,
|
||||
gas_limit: GAS_LIMIT,
|
||||
gas_used: GAS_USED,
|
||||
|
||||
@@ -10,6 +10,8 @@ pub async fn handle_rpc<T: EthSpec>(
|
||||
body: JsonValue,
|
||||
ctx: Arc<Context<T>>,
|
||||
) -> Result<JsonValue, String> {
|
||||
*ctx.previous_request.lock() = Some(body.clone());
|
||||
|
||||
let method = body
|
||||
.get("method")
|
||||
.and_then(JsonValue::as_str)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use crate::{
|
||||
test_utils::{MockServer, DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_DIFFICULTY},
|
||||
*,
|
||||
test_utils::{MockServer, DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_DIFFICULTY, JWT_SECRET},
|
||||
Config, *,
|
||||
};
|
||||
use environment::null_logger;
|
||||
use sensitive_url::SensitiveUrl;
|
||||
use std::sync::Arc;
|
||||
use task_executor::TaskExecutor;
|
||||
use tempfile::NamedTempFile;
|
||||
use types::{Address, ChainSpec, Epoch, EthSpec, Hash256, Uint256};
|
||||
|
||||
pub struct ExecutionLayerRuntime {
|
||||
@@ -85,10 +86,19 @@ impl<T: EthSpec> MockExecutionLayer<T> {
|
||||
);
|
||||
|
||||
let url = SensitiveUrl::parse(&server.url()).unwrap();
|
||||
let file = NamedTempFile::new().unwrap();
|
||||
|
||||
let el = ExecutionLayer::from_urls(
|
||||
vec![url],
|
||||
Some(Address::repeat_byte(42)),
|
||||
let path = file.path().into();
|
||||
std::fs::write(&path, hex::encode(JWT_SECRET)).unwrap();
|
||||
|
||||
let config = Config {
|
||||
execution_endpoints: vec![url],
|
||||
secret_files: vec![path],
|
||||
suggested_fee_recipient: Some(Address::repeat_byte(42)),
|
||||
..Default::default()
|
||||
};
|
||||
let el = ExecutionLayer::from_config(
|
||||
config,
|
||||
el_runtime.task_executor.clone(),
|
||||
el_runtime.log.clone(),
|
||||
)
|
||||
@@ -111,18 +121,32 @@ impl<T: EthSpec> MockExecutionLayer<T> {
|
||||
let parent_hash = latest_execution_block.block_hash();
|
||||
let block_number = latest_execution_block.block_number() + 1;
|
||||
let timestamp = block_number;
|
||||
let random = Hash256::from_low_u64_be(block_number);
|
||||
let prev_randao = Hash256::from_low_u64_be(block_number);
|
||||
let finalized_block_hash = parent_hash;
|
||||
|
||||
// Insert a proposer to ensure the fork choice updated command works.
|
||||
let slot = Slot::new(0);
|
||||
let head_block_root = Hash256::repeat_byte(42);
|
||||
let validator_index = 0;
|
||||
self.el
|
||||
.insert_proposer(
|
||||
slot,
|
||||
head_block_root,
|
||||
validator_index,
|
||||
PayloadAttributes {
|
||||
timestamp,
|
||||
prev_randao,
|
||||
suggested_fee_recipient: Address::repeat_byte(42),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
self.el
|
||||
.notify_forkchoice_updated(
|
||||
parent_hash,
|
||||
ExecutionBlockHash::zero(),
|
||||
Some(PayloadAttributes {
|
||||
timestamp,
|
||||
random,
|
||||
suggested_fee_recipient: Address::repeat_byte(42),
|
||||
}),
|
||||
slot,
|
||||
head_block_root,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -133,7 +157,7 @@ impl<T: EthSpec> MockExecutionLayer<T> {
|
||||
.get_payload::<T>(
|
||||
parent_hash,
|
||||
timestamp,
|
||||
random,
|
||||
prev_randao,
|
||||
finalized_block_hash,
|
||||
validator_index,
|
||||
)
|
||||
@@ -143,13 +167,21 @@ impl<T: EthSpec> MockExecutionLayer<T> {
|
||||
assert_eq!(payload.parent_hash, parent_hash);
|
||||
assert_eq!(payload.block_number, block_number);
|
||||
assert_eq!(payload.timestamp, timestamp);
|
||||
assert_eq!(payload.random, random);
|
||||
assert_eq!(payload.prev_randao, prev_randao);
|
||||
|
||||
let status = self.el.notify_new_payload(&payload).await.unwrap();
|
||||
assert_eq!(status, PayloadStatus::Valid);
|
||||
|
||||
// Use junk values for slot/head-root to ensure there is no payload supplied.
|
||||
let slot = Slot::new(0);
|
||||
let head_block_root = Hash256::repeat_byte(13);
|
||||
self.el
|
||||
.notify_forkchoice_updated(block_hash, ExecutionBlockHash::zero(), None)
|
||||
.notify_forkchoice_updated(
|
||||
block_hash,
|
||||
ExecutionBlockHash::zero(),
|
||||
slot,
|
||||
head_block_root,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Provides a mock execution engine HTTP JSON-RPC API for use in testing.
|
||||
|
||||
use crate::engine_api::{http::JSONRPC_VERSION, PayloadStatusV1, PayloadStatusV1Status};
|
||||
use crate::engine_api::auth::JwtKey;
|
||||
use crate::engine_api::{
|
||||
auth::Auth, http::JSONRPC_VERSION, PayloadStatusV1, PayloadStatusV1Status,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use environment::null_logger;
|
||||
use execution_block_generator::{Block, PoWBlock};
|
||||
@@ -9,19 +12,21 @@ use parking_lot::{Mutex, RwLock, RwLockWriteGuard};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use slog::{info, Logger};
|
||||
use std::convert::Infallible;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::sync::Arc;
|
||||
use tokio::{runtime, sync::oneshot};
|
||||
use types::{EthSpec, ExecutionBlockHash, Uint256};
|
||||
use warp::Filter;
|
||||
use warp::{http::StatusCode, Filter, Rejection};
|
||||
|
||||
pub use execution_block_generator::{generate_pow_block, ExecutionBlockGenerator};
|
||||
pub use mock_execution_layer::{ExecutionLayerRuntime, MockExecutionLayer};
|
||||
|
||||
pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400;
|
||||
pub const DEFAULT_TERMINAL_BLOCK: u64 = 64;
|
||||
pub const JWT_SECRET: [u8; 32] = [42; 32];
|
||||
|
||||
mod execution_block_generator;
|
||||
mod handle_rpc;
|
||||
@@ -60,6 +65,7 @@ impl<T: EthSpec> MockServer<T> {
|
||||
log: null_logger().unwrap(),
|
||||
last_echo_request: last_echo_request.clone(),
|
||||
execution_block_generator: RwLock::new(execution_block_generator),
|
||||
previous_request: <_>::default(),
|
||||
preloaded_responses,
|
||||
static_new_payload_response: <_>::default(),
|
||||
_phantom: PhantomData,
|
||||
@@ -115,6 +121,10 @@ impl<T: EthSpec> MockServer<T> {
|
||||
self.ctx.preloaded_responses.lock().push(response)
|
||||
}
|
||||
|
||||
pub fn take_previous_request(&self) -> Option<serde_json::Value> {
|
||||
self.ctx.previous_request.lock().take()
|
||||
}
|
||||
|
||||
pub fn all_payloads_valid(&self) {
|
||||
let response = StaticNewPayloadResponse {
|
||||
status: PayloadStatusV1 {
|
||||
@@ -222,6 +232,10 @@ pub struct StaticNewPayloadResponse {
|
||||
status: PayloadStatusV1,
|
||||
should_import: bool,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct AuthError(String);
|
||||
|
||||
impl warp::reject::Reject for AuthError {}
|
||||
|
||||
/// A wrapper around all the items required to spawn the HTTP server.
|
||||
///
|
||||
@@ -232,6 +246,7 @@ pub struct Context<T: EthSpec> {
|
||||
pub last_echo_request: Arc<RwLock<Option<Bytes>>>,
|
||||
pub execution_block_generator: RwLock<ExecutionBlockGenerator<T>>,
|
||||
pub preloaded_responses: Arc<Mutex<Vec<serde_json::Value>>>,
|
||||
pub previous_request: Arc<Mutex<Option<serde_json::Value>>>,
|
||||
pub static_new_payload_response: Arc<Mutex<Option<StaticNewPayloadResponse>>>,
|
||||
pub _phantom: PhantomData<T>,
|
||||
}
|
||||
@@ -252,6 +267,66 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// An API error serializable to JSON.
|
||||
#[derive(Serialize)]
|
||||
struct ErrorMessage {
|
||||
code: u16,
|
||||
message: String,
|
||||
}
|
||||
|
||||
/// Returns a `warp` header which filters out request that has a missing or incorrectly
|
||||
/// signed JWT token.
|
||||
fn auth_header_filter() -> warp::filters::BoxedFilter<()> {
|
||||
warp::any()
|
||||
.and(warp::filters::header::optional("Authorization"))
|
||||
.and_then(move |authorization: Option<String>| async move {
|
||||
match authorization {
|
||||
None => Err(warp::reject::custom(AuthError(
|
||||
"auth absent from request".to_string(),
|
||||
))),
|
||||
Some(auth) => {
|
||||
if let Some(token) = auth.strip_prefix("Bearer ") {
|
||||
let secret = JwtKey::from_slice(&JWT_SECRET).unwrap();
|
||||
match Auth::validate_token(token, &secret) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(warp::reject::custom(AuthError(format!(
|
||||
"Auth failure: {:?}",
|
||||
e
|
||||
)))),
|
||||
}
|
||||
} else {
|
||||
Err(warp::reject::custom(AuthError(
|
||||
"Bearer token not present in auth header".to_string(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.untuple_one()
|
||||
.boxed()
|
||||
}
|
||||
/// This function receives a `Rejection` and tries to return a custom
|
||||
/// value on invalid auth, otherwise simply passes the rejection along.
|
||||
async fn handle_rejection(err: Rejection) -> Result<impl warp::Reply, Infallible> {
|
||||
let code;
|
||||
let message;
|
||||
|
||||
if let Some(e) = err.find::<AuthError>() {
|
||||
message = format!("Authorization error: {:?}", e);
|
||||
code = StatusCode::UNAUTHORIZED;
|
||||
} else {
|
||||
message = "BAD_REQUEST".to_string();
|
||||
code = StatusCode::BAD_REQUEST;
|
||||
}
|
||||
|
||||
let json = warp::reply::json(&ErrorMessage {
|
||||
code: code.as_u16(),
|
||||
message,
|
||||
});
|
||||
|
||||
Ok(warp::reply::with_status(json, code))
|
||||
}
|
||||
|
||||
/// Creates a server that will serve requests using information from `ctx`.
|
||||
///
|
||||
/// The server will shut down gracefully when the `shutdown` future resolves.
|
||||
@@ -288,7 +363,6 @@ pub fn serve<T: EthSpec>(
|
||||
.get("id")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.ok_or_else(|| warp::reject::custom(MissingIdField))?;
|
||||
|
||||
let preloaded_response = {
|
||||
let mut preloaded_responses = ctx.preloaded_responses.lock();
|
||||
if !preloaded_responses.is_empty() {
|
||||
@@ -339,7 +413,9 @@ pub fn serve<T: EthSpec>(
|
||||
});
|
||||
|
||||
let routes = warp::post()
|
||||
.and(auth_header_filter())
|
||||
.and(root.or(echo))
|
||||
.recover(handle_rejection)
|
||||
// Add a `Server` header.
|
||||
.map(|reply| warp::reply::with_header(reply, "Server", "lighthouse-mock-execution-client"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user