Merge remote-tracking branch 'origin/unstable' into tree-states

This commit is contained in:
Michael Sproul
2022-03-28 09:24:09 +11:00
187 changed files with 5903 additions and 2368 deletions

View File

@@ -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"

View File

@@ -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,
}

View 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
);
}
}

View File

@@ -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",

View File

@@ -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::*;

View File

@@ -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,
}
}

View File

@@ -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!(

View 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"]
);
}

View File

@@ -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,

View File

@@ -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)

View File

@@ -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();

View File

@@ -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"));