Engine API v1.0.0.alpha.6 + interop tests (#3024)

## Issue Addressed

NA

## Proposed Changes

This PR extends #3018 to address my review comments there and add automated integration tests with Geth (and other implementations, in the future).

I've also de-duplicated the "unused port" logic by creating an  `common/unused_port` crate.

## Additional Info

I'm not sure if we want to merge this PR, or update #3018 and merge that. I don't mind, I'm primarily opening this PR to make sure CI works.


Co-authored-by: Mark Mackey <mark@sigmaprime.io>
This commit is contained in:
Paul Hauner
2022-02-17 21:47:06 +00:00
parent 2f8531dc60
commit 0a6a8ea3b0
40 changed files with 1125 additions and 363 deletions

View File

@@ -55,10 +55,10 @@ pub trait EngineApi {
block_hash: Hash256,
) -> Result<Option<ExecutionBlock>, Error>;
async fn notify_new_payload_v1<T: EthSpec>(
async fn new_payload_v1<T: EthSpec>(
&self,
execution_payload: ExecutionPayload<T>,
) -> Result<ExecutePayloadResponse, Error>;
) -> Result<PayloadStatusV1, Error>;
async fn get_payload_v1<T: EthSpec>(
&self,
@@ -73,15 +73,18 @@ pub trait EngineApi {
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ExecutePayloadResponseStatus {
pub enum PayloadStatusV1Status {
Valid,
Invalid,
Syncing,
Accepted,
InvalidBlockHash,
InvalidTerminalBlock,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExecutePayloadResponse {
pub status: ExecutePayloadResponseStatus,
pub struct PayloadStatusV1 {
pub status: PayloadStatusV1Status,
pub latest_valid_hash: Option<Hash256>,
pub validation_error: Option<String>,
}
@@ -110,13 +113,8 @@ pub struct PayloadAttributes {
pub suggested_fee_recipient: Address,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ForkchoiceUpdatedResponseStatus {
Success,
Syncing,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ForkchoiceUpdatedResponse {
pub status: ForkchoiceUpdatedResponseStatus,
pub payload_status: PayloadStatusV1,
pub payload_id: Option<PayloadId>,
}

View File

@@ -27,8 +27,8 @@ pub const ETH_GET_BLOCK_BY_HASH_TIMEOUT: Duration = Duration::from_secs(1);
pub const ETH_SYNCING: &str = "eth_syncing";
pub const ETH_SYNCING_TIMEOUT: Duration = Duration::from_millis(250);
pub const ENGINE_EXECUTE_PAYLOAD_V1: &str = "engine_executePayloadV1";
pub const ENGINE_EXECUTE_PAYLOAD_TIMEOUT: Duration = Duration::from_secs(2);
pub const ENGINE_NEW_PAYLOAD_V1: &str = "engine_newPayloadV1";
pub const ENGINE_NEW_PAYLOAD_TIMEOUT: Duration = Duration::from_secs(2);
pub const ENGINE_GET_PAYLOAD_V1: &str = "engine_getPayloadV1";
pub const ENGINE_GET_PAYLOAD_TIMEOUT: Duration = Duration::from_secs(2);
@@ -133,18 +133,14 @@ impl EngineApi for HttpJsonRpc {
.await
}
async fn notify_new_payload_v1<T: EthSpec>(
async fn new_payload_v1<T: EthSpec>(
&self,
execution_payload: ExecutionPayload<T>,
) -> Result<ExecutePayloadResponse, Error> {
) -> Result<PayloadStatusV1, Error> {
let params = json!([JsonExecutionPayloadV1::from(execution_payload)]);
let response: JsonExecutePayloadV1Response = self
.rpc_request(
ENGINE_EXECUTE_PAYLOAD_V1,
params,
ENGINE_EXECUTE_PAYLOAD_TIMEOUT,
)
let response: JsonPayloadStatusV1 = self
.rpc_request(ENGINE_NEW_PAYLOAD_V1, params, ENGINE_NEW_PAYLOAD_TIMEOUT)
.await?;
Ok(response.into())
@@ -486,12 +482,12 @@ mod test {
}
#[tokio::test]
async fn notify_new_payload_v1_request() {
async fn new_payload_v1_request() {
Tester::new()
.assert_request_equals(
|client| async move {
let _ = client
.notify_new_payload_v1::<MainnetEthSpec>(ExecutionPayload {
.new_payload_v1::<MainnetEthSpec>(ExecutionPayload {
parent_hash: Hash256::repeat_byte(0),
fee_recipient: Address::repeat_byte(1),
state_root: Hash256::repeat_byte(1),
@@ -512,7 +508,7 @@ mod test {
json!({
"id": STATIC_ID,
"jsonrpc": JSONRPC_VERSION,
"method": ENGINE_EXECUTE_PAYLOAD_V1,
"method": ENGINE_NEW_PAYLOAD_V1,
"params": [{
"parentHash": HASH_00,
"feeRecipient": ADDRESS_01,
@@ -627,7 +623,11 @@ mod test {
"id": STATIC_ID,
"jsonrpc": JSONRPC_VERSION,
"result": {
"status": "SUCCESS",
"payloadStatus": {
"status": "VALID",
"latestValidHash": HASH_00,
"validationError": ""
},
"payloadId": "0xa247243752eb10b4"
}
})],
@@ -648,7 +648,11 @@ mod test {
.await
.unwrap();
assert_eq!(response, ForkchoiceUpdatedResponse {
status: ForkchoiceUpdatedResponseStatus::Success,
payload_status: PayloadStatusV1 {
status: PayloadStatusV1Status::Valid,
latest_valid_hash: Some(Hash256::zero()),
validation_error: Some(String::new()),
},
payload_id:
Some(str_to_payload_id("0xa247243752eb10b4")),
});
@@ -683,12 +687,12 @@ mod test {
"logsBloom": LOGS_BLOOM_00,
"random": HASH_00,
"blockNumber":"0x1",
"gasLimit":"0x1c9c380",
"gasLimit":"0x1c95111",
"gasUsed":"0x0",
"timestamp":"0x5",
"extraData":"0x",
"baseFeePerGas":"0x7",
"blockHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858",
"blockHash":"0x6359b8381a370e2f54072a5784ddd78b6ed024991558c511d4452eb4f6ac898c",
"transactions":[]
}
})],
@@ -706,12 +710,12 @@ mod test {
logs_bloom: vec![0; 256].into(),
random: Hash256::zero(),
block_number: 1,
gas_limit: u64::from_str_radix("1c9c380",16).unwrap(),
gas_limit: u64::from_str_radix("1c95111",16).unwrap(),
gas_used: 0,
timestamp: 5,
extra_data: vec![].into(),
base_fee_per_gas: Uint256::from(7),
block_hash: Hash256::from_str("0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858").unwrap(),
block_hash: Hash256::from_str("0x6359b8381a370e2f54072a5784ddd78b6ed024991558c511d4452eb4f6ac898c").unwrap(),
transactions: vec![].into(),
};
@@ -720,10 +724,10 @@ mod test {
)
.await
.assert_request_equals(
// engine_executePayloadV1 REQUEST validation
// engine_newPayloadV1 REQUEST validation
|client| async move {
let _ = client
.notify_new_payload_v1::<MainnetEthSpec>(ExecutionPayload {
.new_payload_v1::<MainnetEthSpec>(ExecutionPayload {
parent_hash: Hash256::from_str("0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a").unwrap(),
fee_recipient: Address::from_str("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(),
state_root: Hash256::from_str("0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45").unwrap(),
@@ -744,7 +748,7 @@ mod test {
json!({
"id": STATIC_ID,
"jsonrpc": JSONRPC_VERSION,
"method": ENGINE_EXECUTE_PAYLOAD_V1,
"method": ENGINE_NEW_PAYLOAD_V1,
"params": [{
"parentHash":"0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a",
"feeRecipient":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
@@ -765,26 +769,27 @@ mod test {
)
.await
.with_preloaded_responses(
// engine_executePayloadV1 RESPONSE validation
// engine_newPayloadV1 RESPONSE validation
vec![json!({
"jsonrpc": JSONRPC_VERSION,
"id": STATIC_ID,
"result":{
"status":"VALID",
"latestValidHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858"
"latestValidHash":"0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858",
"validationError":"",
}
})],
|client| async move {
let response = client
.notify_new_payload_v1::<MainnetEthSpec>(ExecutionPayload::default())
.new_payload_v1::<MainnetEthSpec>(ExecutionPayload::default())
.await
.unwrap();
assert_eq!(response,
ExecutePayloadResponse {
status: ExecutePayloadResponseStatus::Valid,
PayloadStatusV1 {
status: PayloadStatusV1Status::Valid,
latest_valid_hash: Some(Hash256::from_str("0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858").unwrap()),
validation_error: None
validation_error: Some(String::new()),
}
);
},
@@ -819,14 +824,15 @@ mod test {
.await
.with_preloaded_responses(
// engine_forkchoiceUpdatedV1 RESPONSE validation
//
// Note: this test was modified to provide `null` rather than `0x`. The geth vectors
// are invalid.
vec![json!({
"jsonrpc": JSONRPC_VERSION,
"id": STATIC_ID,
"result": {
"status":"SUCCESS",
"payloadStatus": {
"status": "VALID",
"latestValidHash": HASH_00,
"validationError": ""
},
"payloadId": JSON_NULL,
}
})],
@@ -843,7 +849,11 @@ mod test {
.await
.unwrap();
assert_eq!(response, ForkchoiceUpdatedResponse {
status: ForkchoiceUpdatedResponseStatus::Success,
payload_status: PayloadStatusV1 {
status: PayloadStatusV1Status::Valid,
latest_valid_hash: Some(Hash256::zero()),
validation_error: Some(String::new()),
},
payload_id: None,
});
},

View File

@@ -247,47 +247,60 @@ impl From<JsonForkChoiceStateV1> for ForkChoiceState {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum JsonExecutePayloadV1ResponseStatus {
pub enum JsonPayloadStatusV1Status {
Valid,
Invalid,
Syncing,
Accepted,
InvalidBlockHash,
InvalidTerminalBlock,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonExecutePayloadV1Response {
pub status: JsonExecutePayloadV1ResponseStatus,
pub struct JsonPayloadStatusV1 {
pub status: JsonPayloadStatusV1Status,
pub latest_valid_hash: Option<Hash256>,
pub validation_error: Option<String>,
}
impl From<ExecutePayloadResponseStatus> for JsonExecutePayloadV1ResponseStatus {
fn from(e: ExecutePayloadResponseStatus) -> Self {
impl From<PayloadStatusV1Status> for JsonPayloadStatusV1Status {
fn from(e: PayloadStatusV1Status) -> Self {
match e {
ExecutePayloadResponseStatus::Valid => JsonExecutePayloadV1ResponseStatus::Valid,
ExecutePayloadResponseStatus::Invalid => JsonExecutePayloadV1ResponseStatus::Invalid,
ExecutePayloadResponseStatus::Syncing => JsonExecutePayloadV1ResponseStatus::Syncing,
PayloadStatusV1Status::Valid => JsonPayloadStatusV1Status::Valid,
PayloadStatusV1Status::Invalid => JsonPayloadStatusV1Status::Invalid,
PayloadStatusV1Status::Syncing => JsonPayloadStatusV1Status::Syncing,
PayloadStatusV1Status::Accepted => JsonPayloadStatusV1Status::Accepted,
PayloadStatusV1Status::InvalidBlockHash => JsonPayloadStatusV1Status::InvalidBlockHash,
PayloadStatusV1Status::InvalidTerminalBlock => {
JsonPayloadStatusV1Status::InvalidTerminalBlock
}
}
}
}
impl From<JsonExecutePayloadV1ResponseStatus> for ExecutePayloadResponseStatus {
fn from(j: JsonExecutePayloadV1ResponseStatus) -> Self {
impl From<JsonPayloadStatusV1Status> for PayloadStatusV1Status {
fn from(j: JsonPayloadStatusV1Status) -> Self {
match j {
JsonExecutePayloadV1ResponseStatus::Valid => ExecutePayloadResponseStatus::Valid,
JsonExecutePayloadV1ResponseStatus::Invalid => ExecutePayloadResponseStatus::Invalid,
JsonExecutePayloadV1ResponseStatus::Syncing => ExecutePayloadResponseStatus::Syncing,
JsonPayloadStatusV1Status::Valid => PayloadStatusV1Status::Valid,
JsonPayloadStatusV1Status::Invalid => PayloadStatusV1Status::Invalid,
JsonPayloadStatusV1Status::Syncing => PayloadStatusV1Status::Syncing,
JsonPayloadStatusV1Status::Accepted => PayloadStatusV1Status::Accepted,
JsonPayloadStatusV1Status::InvalidBlockHash => PayloadStatusV1Status::InvalidBlockHash,
JsonPayloadStatusV1Status::InvalidTerminalBlock => {
PayloadStatusV1Status::InvalidTerminalBlock
}
}
}
}
impl From<ExecutePayloadResponse> for JsonExecutePayloadV1Response {
fn from(e: ExecutePayloadResponse) -> Self {
impl From<PayloadStatusV1> for JsonPayloadStatusV1 {
fn from(p: PayloadStatusV1) -> Self {
// Use this verbose deconstruction pattern to ensure no field is left unused.
let ExecutePayloadResponse {
let PayloadStatusV1 {
status,
latest_valid_hash,
validation_error,
} = e;
} = p;
Self {
status: status.into(),
@@ -297,10 +310,10 @@ impl From<ExecutePayloadResponse> for JsonExecutePayloadV1Response {
}
}
impl From<JsonExecutePayloadV1Response> for ExecutePayloadResponse {
fn from(j: JsonExecutePayloadV1Response) -> Self {
impl From<JsonPayloadStatusV1> for PayloadStatusV1 {
fn from(j: JsonPayloadStatusV1) -> Self {
// Use this verbose deconstruction pattern to ensure no field is left unused.
let JsonExecutePayloadV1Response {
let JsonPayloadStatusV1 {
status,
latest_valid_hash,
validation_error,
@@ -314,50 +327,23 @@ impl From<JsonExecutePayloadV1Response> for ExecutePayloadResponse {
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum JsonForkchoiceUpdatedV1ResponseStatus {
Success,
Syncing,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonForkchoiceUpdatedV1Response {
pub status: JsonForkchoiceUpdatedV1ResponseStatus,
pub payload_status: JsonPayloadStatusV1,
pub payload_id: Option<TransparentJsonPayloadId>,
}
impl From<JsonForkchoiceUpdatedV1ResponseStatus> for ForkchoiceUpdatedResponseStatus {
fn from(j: JsonForkchoiceUpdatedV1ResponseStatus) -> Self {
match j {
JsonForkchoiceUpdatedV1ResponseStatus::Success => {
ForkchoiceUpdatedResponseStatus::Success
}
JsonForkchoiceUpdatedV1ResponseStatus::Syncing => {
ForkchoiceUpdatedResponseStatus::Syncing
}
}
}
}
impl From<ForkchoiceUpdatedResponseStatus> for JsonForkchoiceUpdatedV1ResponseStatus {
fn from(f: ForkchoiceUpdatedResponseStatus) -> Self {
match f {
ForkchoiceUpdatedResponseStatus::Success => {
JsonForkchoiceUpdatedV1ResponseStatus::Success
}
ForkchoiceUpdatedResponseStatus::Syncing => {
JsonForkchoiceUpdatedV1ResponseStatus::Syncing
}
}
}
}
impl From<JsonForkchoiceUpdatedV1Response> for ForkchoiceUpdatedResponse {
fn from(j: JsonForkchoiceUpdatedV1Response) -> Self {
// Use this verbose deconstruction pattern to ensure no field is left unused.
let JsonForkchoiceUpdatedV1Response { status, payload_id } = j;
let JsonForkchoiceUpdatedV1Response {
payload_status: status,
payload_id,
} = j;
Self {
status: status.into(),
payload_status: status.into(),
payload_id: payload_id.map(Into::into),
}
}
@@ -365,10 +351,13 @@ impl From<JsonForkchoiceUpdatedV1Response> for ForkchoiceUpdatedResponse {
impl From<ForkchoiceUpdatedResponse> for JsonForkchoiceUpdatedV1Response {
fn from(f: ForkchoiceUpdatedResponse) -> Self {
// Use this verbose deconstruction pattern to ensure no field is left unused.
let ForkchoiceUpdatedResponse { status, payload_id } = f;
let ForkchoiceUpdatedResponse {
payload_status: status,
payload_id,
} = f;
Self {
status: status.into(),
payload_status: status.into(),
payload_id: payload_id.map(Into::into),
}
}

View File

@@ -1,6 +1,8 @@
//! Provides generic behaviour for multiple execution engines, specifically fallback behaviour.
use crate::engine_api::{EngineApi, Error as EngineApiError, PayloadAttributes, PayloadId};
use crate::engine_api::{
EngineApi, Error as EngineApiError, ForkchoiceUpdatedResponse, PayloadAttributes, PayloadId,
};
use futures::future::join_all;
use lru::LruCache;
use slog::{crit, debug, info, warn, Logger};
@@ -97,7 +99,7 @@ impl<T: EngineApi> Engine<T> {
forkchoice_state: ForkChoiceState,
payload_attributes: Option<PayloadAttributes>,
log: &Logger,
) -> Result<Option<PayloadId>, EngineApiError> {
) -> Result<ForkchoiceUpdatedResponse, EngineApiError> {
let response = self
.api
.forkchoice_updated_v1(forkchoice_state, payload_attributes)
@@ -117,7 +119,7 @@ impl<T: EngineApi> Engine<T> {
}
}
Ok(response.payload_id)
Ok(response)
}
}

View File

@@ -10,7 +10,7 @@ use lru::LruCache;
use sensitive_url::SensitiveUrl;
use slog::{crit, debug, error, info, Logger};
use slot_clock::SlotClock;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
@@ -21,7 +21,7 @@ use tokio::{
};
use types::{ChainSpec, Epoch, ProposerPreparationData};
pub use engine_api::{http::HttpJsonRpc, ExecutePayloadResponseStatus};
pub use engine_api::{http::HttpJsonRpc, PayloadAttributes, PayloadStatusV1Status};
mod engine_api;
mod engines;
@@ -49,6 +49,7 @@ pub enum Error {
NotSynced,
ShuttingDown,
FeeRecipientUnspecified,
ConsensusFailure,
}
impl From<ApiError> for Error {
@@ -249,7 +250,7 @@ impl ExecutionLayer {
}
/// Performs a single execution of the watchdog routine.
async fn watchdog_task(&self) {
pub async fn watchdog_task(&self) {
// Disable logging since this runs frequently and may get annoying.
self.engines().upcheck_not_synced(Logging::Disabled).await;
}
@@ -431,7 +432,8 @@ impl ExecutionLayer {
Some(payload_attributes),
self.log(),
)
.await?
.await
.map(|response| response.payload_id)?
.ok_or(ApiError::PayloadIdUnavailable)?
};
@@ -449,6 +451,7 @@ impl ExecutionLayer {
/// failure) from all nodes and then return based on the first of these conditions which
/// returns true:
///
/// - Error::ConsensusFailure if some nodes return valid and some return invalid
/// - Valid, if any nodes return valid.
/// - Invalid, if any nodes return invalid.
/// - Syncing, if any nodes return syncing.
@@ -456,10 +459,10 @@ impl ExecutionLayer {
pub async fn notify_new_payload<T: EthSpec>(
&self,
execution_payload: &ExecutionPayload<T>,
) -> Result<(ExecutePayloadResponseStatus, Option<Hash256>), Error> {
) -> Result<(PayloadStatusV1Status, Option<Vec<Hash256>>), Error> {
debug!(
self.log(),
"Issuing engine_executePayload";
"Issuing engine_newPayload";
"parent_hash" => ?execution_payload.parent_hash,
"block_hash" => ?execution_payload.block_hash,
"block_number" => execution_payload.block_number,
@@ -467,46 +470,55 @@ impl ExecutionLayer {
let broadcast_results = self
.engines()
.broadcast(|engine| engine.api.notify_new_payload_v1(execution_payload.clone()))
.broadcast(|engine| engine.api.new_payload_v1(execution_payload.clone()))
.await;
let mut errors = vec![];
let mut valid = 0;
let mut invalid = 0;
let mut syncing = 0;
let mut invalid_latest_valid_hash = vec![];
let mut invalid_latest_valid_hash = HashSet::new();
for result in broadcast_results {
match result.map(|response| (response.latest_valid_hash, response.status)) {
Ok((Some(latest_hash), ExecutePayloadResponseStatus::Valid)) => {
if latest_hash == execution_payload.block_hash {
valid += 1;
} else {
invalid += 1;
errors.push(EngineError::Api {
id: "unknown".to_string(),
error: engine_api::Error::BadResponse(
format!(
"notify_new_payload: response.status = Valid but invalid latest_valid_hash. Expected({:?}) Found({:?})",
execution_payload.block_hash,
latest_hash,
)
),
});
invalid_latest_valid_hash.push(latest_hash);
match result {
Ok(response) => match (&response.latest_valid_hash, &response.status) {
(Some(latest_hash), &PayloadStatusV1Status::Valid) => {
// According to a strict interpretation of the spec, the EE should never
// respond with `VALID` *and* a `latest_valid_hash`.
//
// For the sake of being liberal with what we accept, we will accept a
// `latest_valid_hash` *only if* it matches the submitted payload.
// Otherwise, register an error.
if latest_hash == &execution_payload.block_hash {
valid += 1;
} else {
errors.push(EngineError::Api {
id: "unknown".to_string(),
error: engine_api::Error::BadResponse(
format!(
"new_payload: response.status = Valid but invalid latest_valid_hash. Expected({:?}) Found({:?})",
execution_payload.block_hash,
latest_hash,
)
),
});
}
}
}
Ok((Some(latest_hash), ExecutePayloadResponseStatus::Invalid)) => {
invalid += 1;
invalid_latest_valid_hash.push(latest_hash);
}
Ok((_, ExecutePayloadResponseStatus::Syncing)) => syncing += 1,
Ok((None, status)) => errors.push(EngineError::Api {
id: "unknown".to_string(),
error: engine_api::Error::BadResponse(format!(
"notify_new_payload: status {:?} returned with null latest_valid_hash",
status
)),
}),
(Some(latest_hash), &PayloadStatusV1Status::Invalid) => {
invalid += 1;
invalid_latest_valid_hash.insert(*latest_hash);
}
(None, &PayloadStatusV1Status::InvalidBlockHash)
| (None, &PayloadStatusV1Status::InvalidTerminalBlock) => invalid += 1,
(None, &PayloadStatusV1Status::Syncing)
| (None, &PayloadStatusV1Status::Accepted) => syncing += 1,
_ => errors.push(EngineError::Api {
id: "unknown".to_string(),
error: engine_api::Error::BadResponse(format!(
"new_payload: response does not conform to engine API spec: {:?}",
response,
)),
}),
},
Err(e) => errors.push(e),
}
}
@@ -515,19 +527,24 @@ impl ExecutionLayer {
crit!(
self.log(),
"Consensus failure between execution nodes";
"method" => "notify_new_payload"
"method" => "new_payload"
);
// In this situation, better to have a failure of liveness than vote on a potentially invalid chain
return Err(Error::ConsensusFailure);
}
if valid > 0 {
Ok((
ExecutePayloadResponseStatus::Valid,
Some(execution_payload.block_hash),
PayloadStatusV1Status::Valid,
Some(vec![execution_payload.block_hash]),
))
} else if invalid > 0 {
Ok((ExecutePayloadResponseStatus::Invalid, None))
Ok((
PayloadStatusV1Status::Invalid,
Some(invalid_latest_valid_hash.into_iter().collect()),
))
} else if syncing > 0 {
Ok((ExecutePayloadResponseStatus::Syncing, None))
Ok((PayloadStatusV1Status::Syncing, None))
} else {
Err(Error::EngineErrors(errors))
}
@@ -541,14 +558,17 @@ impl ExecutionLayer {
/// failure) from all nodes and then return based on the first of these conditions which
/// returns true:
///
/// - Ok, if any node returns successfully.
/// - Error::ConsensusFailure if some nodes return valid and some return invalid
/// - Valid, if any nodes return valid.
/// - Invalid, if any nodes return invalid.
/// - Syncing, if any nodes return syncing.
/// - An error, if all nodes return an error.
pub async fn notify_forkchoice_updated(
&self,
head_block_hash: Hash256,
finalized_block_hash: Hash256,
payload_attributes: Option<PayloadAttributes>,
) -> Result<(), Error> {
) -> Result<(PayloadStatusV1Status, Option<Vec<Hash256>>), Error> {
debug!(
self.log(),
"Issuing engine_forkchoiceUpdated";
@@ -577,13 +597,76 @@ impl ExecutionLayer {
})
.await;
if broadcast_results.iter().any(Result::is_ok) {
Ok(())
let mut errors = vec![];
let mut valid = 0;
let mut invalid = 0;
let mut syncing = 0;
let mut invalid_latest_valid_hash = HashSet::new();
for result in broadcast_results {
match result {
Ok(response) => match (&response.payload_status.latest_valid_hash, &response.payload_status.status) {
// TODO(bellatrix) a strict interpretation of the v1.0.0.alpha.6 spec says that
// `latest_valid_hash` *cannot* be `None`. However, we accept it to maintain
// Geth compatibility for the short term. See:
//
// https://github.com/ethereum/go-ethereum/issues/24404
(None, &PayloadStatusV1Status::Valid) => valid += 1,
(Some(latest_hash), &PayloadStatusV1Status::Valid) => {
if latest_hash == &head_block_hash {
valid += 1;
} else {
errors.push(EngineError::Api {
id: "unknown".to_string(),
error: engine_api::Error::BadResponse(
format!(
"forkchoice_updated: payload_status = Valid but invalid latest_valid_hash. Expected({:?}) Found({:?})",
head_block_hash,
*latest_hash,
)
),
});
}
}
(Some(latest_hash), &PayloadStatusV1Status::Invalid) => {
invalid += 1;
invalid_latest_valid_hash.insert(*latest_hash);
}
(None, &PayloadStatusV1Status::InvalidTerminalBlock) => invalid += 1,
(None, &PayloadStatusV1Status::Syncing) => syncing += 1,
_ => {
errors.push(EngineError::Api {
id: "unknown".to_string(),
error: engine_api::Error::BadResponse(format!(
"forkchoice_updated: response does not conform to engine API spec: {:?}",
response
)),
})
}
}
Err(e) => errors.push(e),
}
}
if valid > 0 && invalid > 0 {
crit!(
self.log(),
"Consensus failure between execution nodes";
"method" => "forkchoice_updated"
);
// In this situation, better to have a failure of liveness than vote on a potentially invalid chain
return Err(Error::ConsensusFailure);
}
if valid > 0 {
Ok((PayloadStatusV1Status::Valid, Some(vec![head_block_hash])))
} else if invalid > 0 {
Ok((
PayloadStatusV1Status::Invalid,
Some(invalid_latest_valid_hash.into_iter().collect()),
))
} else if syncing > 0 {
Ok((PayloadStatusV1Status::Syncing, None))
} else {
let errors = broadcast_results
.into_iter()
.filter_map(Result::err)
.collect();
Err(Error::EngineErrors(errors))
}
}

View File

@@ -1,6 +1,5 @@
use crate::engine_api::{
ExecutePayloadResponse, ExecutePayloadResponseStatus, ExecutionBlock, PayloadAttributes,
PayloadId,
ExecutionBlock, PayloadAttributes, PayloadId, PayloadStatusV1, PayloadStatusV1Status,
};
use crate::engines::ForkChoiceState;
use serde::{Deserialize, Serialize};
@@ -235,20 +234,20 @@ impl<T: EthSpec> ExecutionBlockGenerator<T> {
self.payload_ids.remove(id)
}
pub fn notify_new_payload(&mut self, payload: ExecutionPayload<T>) -> ExecutePayloadResponse {
pub fn new_payload(&mut self, payload: ExecutionPayload<T>) -> PayloadStatusV1 {
let parent = if let Some(parent) = self.blocks.get(&payload.parent_hash) {
parent
} else {
return ExecutePayloadResponse {
status: ExecutePayloadResponseStatus::Syncing,
return PayloadStatusV1 {
status: PayloadStatusV1Status::Syncing,
latest_valid_hash: None,
validation_error: None,
};
};
if payload.block_number != parent.block_number() + 1 {
return ExecutePayloadResponse {
status: ExecutePayloadResponseStatus::Invalid,
return PayloadStatusV1 {
status: PayloadStatusV1Status::Invalid,
latest_valid_hash: Some(parent.block_hash()),
validation_error: Some("invalid block number".to_string()),
};
@@ -257,8 +256,8 @@ impl<T: EthSpec> ExecutionBlockGenerator<T> {
let valid_hash = payload.block_hash;
self.pending_payloads.insert(payload.block_hash, payload);
ExecutePayloadResponse {
status: ExecutePayloadResponseStatus::Valid,
PayloadStatusV1 {
status: PayloadStatusV1Status::Valid,
latest_valid_hash: Some(valid_hash),
validation_error: None,
}

View File

@@ -1,5 +1,5 @@
use super::Context;
use crate::engine_api::{http::*, ExecutePayloadResponse, ExecutePayloadResponseStatus};
use crate::engine_api::{http::*, PayloadStatusV1, PayloadStatusV1Status};
use crate::json_structures::*;
use serde::de::DeserializeOwned;
use serde_json::Value as JsonValue;
@@ -54,30 +54,30 @@ pub async fn handle_rpc<T: EthSpec>(
)
.unwrap())
}
ENGINE_EXECUTE_PAYLOAD_V1 => {
ENGINE_NEW_PAYLOAD_V1 => {
let request: JsonExecutionPayloadV1<T> = get_param(params, 0)?;
let response = if let Some(status) = *ctx.static_notify_new_payload_response.lock() {
let response = if let Some(status) = *ctx.static_new_payload_response.lock() {
match status {
ExecutePayloadResponseStatus::Valid => ExecutePayloadResponse {
PayloadStatusV1Status::Valid => PayloadStatusV1 {
status,
latest_valid_hash: Some(request.block_hash),
validation_error: None,
},
ExecutePayloadResponseStatus::Syncing => ExecutePayloadResponse {
PayloadStatusV1Status::Syncing => PayloadStatusV1 {
status,
latest_valid_hash: None,
validation_error: None,
},
_ => unimplemented!("invalid static executePayloadResponse"),
_ => unimplemented!("invalid static newPayloadResponse"),
}
} else {
ctx.execution_block_generator
.write()
.notify_new_payload(request.into())
.new_payload(request.into())
};
Ok(serde_json::to_value(JsonExecutePayloadV1Response::from(response)).unwrap())
Ok(serde_json::to_value(JsonPayloadStatusV1::from(response)).unwrap())
}
ENGINE_GET_PAYLOAD_V1 => {
let request: JsonPayloadIdRequest = get_param(params, 0)?;
@@ -94,6 +94,8 @@ pub async fn handle_rpc<T: EthSpec>(
ENGINE_FORKCHOICE_UPDATED_V1 => {
let forkchoice_state: JsonForkChoiceStateV1 = get_param(params, 0)?;
let payload_attributes: Option<JsonPayloadAttributesV1> = get_param(params, 1)?;
let head_block_hash = forkchoice_state.head_block_hash;
let id = ctx
.execution_block_generator
.write()
@@ -103,7 +105,11 @@ pub async fn handle_rpc<T: EthSpec>(
)?;
Ok(serde_json::to_value(JsonForkchoiceUpdatedV1Response {
status: JsonForkchoiceUpdatedV1ResponseStatus::Success,
payload_status: JsonPayloadStatusV1 {
status: JsonPayloadStatusV1Status::Valid,
latest_valid_hash: Some(head_block_hash),
validation_error: None,
},
payload_id: id.map(Into::into),
})
.unwrap())

View File

@@ -147,8 +147,8 @@ impl<T: EthSpec> MockExecutionLayer<T> {
let (payload_response, latest_valid_hash) =
self.el.notify_new_payload(&payload).await.unwrap();
assert_eq!(payload_response, ExecutePayloadResponseStatus::Valid);
assert_eq!(latest_valid_hash, Some(payload.block_hash));
assert_eq!(payload_response, PayloadStatusV1Status::Valid);
assert_eq!(latest_valid_hash, Some(vec![payload.block_hash]));
self.el
.notify_forkchoice_updated(block_hash, Hash256::zero(), None)

View File

@@ -1,7 +1,7 @@
//! Provides a mock execution engine HTTP JSON-RPC API for use in testing.
use crate::engine_api::http::JSONRPC_VERSION;
use crate::engine_api::ExecutePayloadResponseStatus;
use crate::engine_api::PayloadStatusV1Status;
use bytes::Bytes;
use environment::null_logger;
use execution_block_generator::{Block, PoWBlock};
@@ -62,7 +62,7 @@ impl<T: EthSpec> MockServer<T> {
last_echo_request: last_echo_request.clone(),
execution_block_generator: RwLock::new(execution_block_generator),
preloaded_responses,
static_notify_new_payload_response: <_>::default(),
static_new_payload_response: <_>::default(),
_phantom: PhantomData,
});
@@ -117,8 +117,7 @@ impl<T: EthSpec> MockServer<T> {
}
pub fn all_payloads_valid(&self) {
*self.ctx.static_notify_new_payload_response.lock() =
Some(ExecutePayloadResponseStatus::Valid)
*self.ctx.static_new_payload_response.lock() = Some(PayloadStatusV1Status::Valid)
}
pub fn insert_pow_block(
@@ -188,7 +187,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 static_notify_new_payload_response: Arc<Mutex<Option<ExecutePayloadResponseStatus>>>,
pub static_new_payload_response: Arc<Mutex<Option<PayloadStatusV1Status>>>,
pub _phantom: PhantomData<T>,
}