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

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