From 01031931d96b5193664572d612720759b61bcc99 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Fri, 1 Oct 2021 10:59:04 +1000 Subject: [PATCH] [Merge] Add execution API test vectors from Geth (#2651) * Add geth request vectors * Add geth response vectors * Fix clippy lints --- .../execution_layer/src/engine_api/http.rs | 235 +++++++++++++++++- .../src/test_utils/handle_rpc.rs | 8 +- .../execution_layer/src/test_utils/mod.rs | 50 ++-- 3 files changed, 265 insertions(+), 28 deletions(-) diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index eae19390b..65b5b102b 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -152,7 +152,7 @@ impl EngineApi for HttpJsonRpc { fee_recipient }]); - let response: JsonPayloadId = self + let response: JsonPayloadIdResponse = self .rpc_request( ENGINE_PREPARE_PAYLOAD, params, @@ -169,19 +169,22 @@ impl EngineApi for HttpJsonRpc { ) -> Result { let params = json!([JsonExecutionPayload::from(execution_payload)]); - self.rpc_request( - ENGINE_EXECUTE_PAYLOAD, - params, - ENGINE_EXECUTE_PAYLOAD_TIMEOUT, - ) - .await + let result: ExecutePayloadResponseWrapper = self + .rpc_request( + ENGINE_EXECUTE_PAYLOAD, + params, + ENGINE_EXECUTE_PAYLOAD_TIMEOUT, + ) + .await?; + + Ok(result.status) } async fn get_payload( &self, payload_id: PayloadId, ) -> Result, Error> { - let params = json!([JsonPayloadId { payload_id }]); + let params = json!([JsonPayloadIdRequest { payload_id }]); let response: JsonExecutionPayload = self .rpc_request(ENGINE_GET_PAYLOAD, params, ENGINE_GET_PAYLOAD_TIMEOUT) @@ -260,13 +263,28 @@ pub struct JsonPreparePayloadRequest { pub fee_recipient: Address, } +/// On the request, just provide the `payload_id`, without the object wrapper (transparent). #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(transparent, rename_all = "camelCase")] -pub struct JsonPayloadId { +pub struct JsonPayloadIdRequest { #[serde(with = "eth2_serde_utils::u64_hex_be")] pub payload_id: u64, } +/// On the response, expect without the object wrapper (non-transparent). +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JsonPayloadIdResponse { + #[serde(with = "eth2_serde_utils::u64_hex_be")] + pub payload_id: u64, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutePayloadResponseWrapper { + pub status: ExecutePayloadResponse, +} + #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(bound = "T: EthSpec", rename_all = "camelCase")] pub struct JsonExecutionPayload { @@ -464,22 +482,29 @@ mod test { use super::*; use crate::test_utils::MockServer; use std::future::Future; + use std::str::FromStr; use std::sync::Arc; use types::MainnetEthSpec; struct Tester { server: MockServer, + rpc_client: Arc, echo_client: Arc, } impl Tester { pub fn new() -> 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()); Self { server, + rpc_client, echo_client, } } @@ -506,6 +531,22 @@ mod test { } self } + + pub async fn with_preloaded_responses( + self, + preloaded_responses: Vec, + request_func: R, + ) -> Self + where + R: Fn(Arc) -> F, + F: Future, + { + for response in preloaded_responses { + self.server.push_preloaded_response(response).await; + } + request_func(self.rpc_client.clone()).await; + self + } } const HASH_00: &str = "0x0000000000000000000000000000000000000000000000000000000000000000"; @@ -843,4 +884,180 @@ mod test { ) .await; } + + /// Test vectors provided by Geth: + /// + /// https://notes.ethereum.org/@9AeMAlpyQYaAAyuj47BzRw/rkwW3ceVY + /// + /// The `id` field has been modified on these vectors to match the one we use. + #[tokio::test] + async fn geth_test_vectors() { + Tester::new() + .assert_request_equals( + |client| async move { + let _ = client + .prepare_payload( + Hash256::from_str("0xa0513a503d5bd6e89a144c3268e5b7e9da9dbf63df125a360e3950a7d0d67131").unwrap(), + 5, + Hash256::zero(), + Address::zero(), + ) + .await; + }, + serde_json::from_str(r#"{"jsonrpc":"2.0","method":"engine_preparePayload","params":[{"parentHash":"0xa0513a503d5bd6e89a144c3268e5b7e9da9dbf63df125a360e3950a7d0d67131", "timestamp":"0x5", "random":"0x0000000000000000000000000000000000000000000000000000000000000000", "feeRecipient":"0x0000000000000000000000000000000000000000"}],"id": 1}"#).unwrap() + ) + .await + .with_preloaded_responses( + vec![serde_json::from_str(r#"{"jsonrpc":"2.0","id":1,"result":{"payloadId":"0x0"}}"#).unwrap()], + |client| async move { + let payload_id = client + .prepare_payload( + Hash256::from_str("0xa0513a503d5bd6e89a144c3268e5b7e9da9dbf63df125a360e3950a7d0d67131").unwrap(), + 5, + Hash256::zero(), + Address::zero(), + ) + .await + .unwrap(); + + assert_eq!(payload_id, 0); + }, + ) + .await + .assert_request_equals( + |client| async move { + let _ = client + .get_payload::(0) + .await; + }, + serde_json::from_str(r#"{"jsonrpc":"2.0","method":"engine_getPayload","params":["0x0"],"id":1}"#).unwrap() + ) + .await + .with_preloaded_responses( + // Note: this response has been modified due to errors in the test vectors: + // + // https://github.com/ethereum/go-ethereum/pull/23607#issuecomment-930668512 + vec![serde_json::from_str(r#"{"jsonrpc":"2.0","id":67,"result":{"blockHash":"0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174","parentHash":"0xa0513a503d5bd6e89a144c3268e5b7e9da9dbf63df125a360e3950a7d0d67131","coinbase":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b","stateRoot":"0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45","receiptRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","random":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","gasLimit":"0x989680","gasUsed":"0x0","timestamp":"0x5","extraData":"0x","baseFeePerGas":"0x0","transactions":[]}}"#).unwrap()], + |client| async move { + let payload = client + .get_payload::(0) + .await + .unwrap(); + + let expected = ExecutionPayload { + parent_hash: Hash256::from_str("0xa0513a503d5bd6e89a144c3268e5b7e9da9dbf63df125a360e3950a7d0d67131").unwrap(), + coinbase: Address::from_str("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(), + state_root: Hash256::from_str("0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45").unwrap(), + receipt_root: Hash256::from_str("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), + logs_bloom: vec![0; 256].into(), + random: Hash256::zero(), + block_number: 1, + gas_limit: 10000000, + gas_used: 0, + timestamp: 5, + extra_data: vec![].into(), + base_fee_per_gas: uint256_to_hash256(Uint256::from(0)), + block_hash: Hash256::from_str("0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174").unwrap(), + transactions: vec![].into(), + }; + + assert_eq!(payload, expected); + }, + ) + .await + .assert_request_equals( + |client| async move { + let _ = client + .execute_payload::(ExecutionPayload { + parent_hash: Hash256::from_str("0xa0513a503d5bd6e89a144c3268e5b7e9da9dbf63df125a360e3950a7d0d67131").unwrap(), + coinbase: Address::from_str("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(), + state_root: Hash256::from_str("0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45").unwrap(), + receipt_root: Hash256::from_str("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), + logs_bloom: vec![0; 256].into(), + random: Hash256::zero(), + block_number: 1, + gas_limit: 10000000, + gas_used: 0, + timestamp: 5, + extra_data: vec![].into(), + base_fee_per_gas: uint256_to_hash256(Uint256::from(0)), + block_hash: Hash256::from_str("0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174").unwrap(), + transactions: vec![].into(), + }) + .await; + }, + // Note: I have renamed the `recieptsRoot` field to `recieptRoot` and `number` to `blockNumber` since I think + // Geth has an issue. See: + // + // https://github.com/ethereum/go-ethereum/pull/23607#issuecomment-930668512 + serde_json::from_str(r#"{"jsonrpc":"2.0","method":"engine_executePayload","params":[{"blockHash":"0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174","parentHash":"0xa0513a503d5bd6e89a144c3268e5b7e9da9dbf63df125a360e3950a7d0d67131","coinbase":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b","stateRoot":"0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45","receiptRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","random":"0x0000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0x1","gasLimit":"0x989680","gasUsed":"0x0","timestamp":"0x5","extraData":"0x","baseFeePerGas":"0x0","transactions":[]}],"id":1}"#).unwrap() + ) + .await + .with_preloaded_responses( + vec![serde_json::from_str(r#"{"jsonrpc":"2.0","id":67,"result":{"status":"VALID"}}"#).unwrap()], + |client| async move { + let response = client + .execute_payload::(ExecutionPayload::default()) + .await + .unwrap(); + + assert_eq!(response, ExecutePayloadResponse::Valid); + }, + ) + .await + .assert_request_equals( + |client| async move { + let _ = client + .consensus_validated( + Hash256::from_str("0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174").unwrap(), + ConsensusStatus::Valid + ) + .await; + }, + serde_json::from_str(r#"{"jsonrpc":"2.0","method":"engine_consensusValidated","params":[{"blockHash":"0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174", "status":"VALID"}],"id":1}"#).unwrap() + ) + .await + .with_preloaded_responses( + vec![serde_json::from_str(r#"{"jsonrpc":"2.0","id":67,"result":null}"#).unwrap()], + |client| async move { + let _: () = client + .consensus_validated( + Hash256::zero(), + ConsensusStatus::Valid + ) + .await + .unwrap(); + }, + ) + .await + .assert_request_equals( + |client| async move { + let _ = client + .forkchoice_updated( + Hash256::from_str("0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174").unwrap(), + Hash256::from_str("0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174").unwrap(), + ) + .await; + }, + // Note: Geth incorrectly uses `engine_forkChoiceUpdated` (capital `C`). I've + // modified this vector to correct this. See: + // + // https://github.com/ethereum/go-ethereum/pull/23607#issuecomment-930668512 + serde_json::from_str(r#"{"jsonrpc":"2.0","method":"engine_forkchoiceUpdated","params":[{"headBlockHash":"0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174", "finalizedBlockHash":"0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174"}],"id":1}"#).unwrap() + ) + .await + .with_preloaded_responses( + vec![serde_json::from_str(r#"{"jsonrpc":"2.0","id":67,"result":null}"#).unwrap()], + |client| async move { + let _: () = client + .forkchoice_updated( + Hash256::zero(), + Hash256::zero(), + ) + .await + .unwrap(); + }, + ) + .await; + } } diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 00fd8101e..38a0f211b 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -63,20 +63,20 @@ pub async fn handle_rpc( .await .prepare_payload(request)?; - Ok(serde_json::to_value(JsonPayloadId { payload_id }).unwrap()) + Ok(serde_json::to_value(JsonPayloadIdResponse { payload_id }).unwrap()) } ENGINE_EXECUTE_PAYLOAD => { let request: JsonExecutionPayload = get_param_0(params)?; - let response = ctx + let status = ctx .execution_block_generator .write() .await .execute_payload(request.into()); - Ok(serde_json::to_value(response).unwrap()) + Ok(serde_json::to_value(ExecutePayloadResponseWrapper { status }).unwrap()) } ENGINE_GET_PAYLOAD => { - let request: JsonPayloadId = get_param_0(params)?; + let request: JsonPayloadIdRequest = get_param_0(params)?; let id = request.payload_id; let response = ctx diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index d5ec89f87..b7969d0c3 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -12,7 +12,7 @@ use std::future::Future; use std::marker::PhantomData; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::Arc; -use tokio::sync::{oneshot, RwLock, RwLockWriteGuard}; +use tokio::sync::{oneshot, Mutex, RwLock, RwLockWriteGuard}; use types::EthSpec; use warp::Filter; @@ -32,6 +32,7 @@ pub struct MockServer { impl MockServer { pub fn unit_testing() -> Self { let last_echo_request = Arc::new(RwLock::new(None)); + let preloaded_responses = Arc::new(Mutex::new(vec![])); let execution_block_generator = ExecutionBlockGenerator::new(DEFAULT_TERMINAL_DIFFICULTY, DEFAULT_TERMINAL_BLOCK); @@ -40,6 +41,7 @@ impl MockServer { log: null_logger().unwrap(), last_echo_request: last_echo_request.clone(), execution_block_generator: RwLock::new(execution_block_generator), + preloaded_responses, _phantom: PhantomData, }); @@ -83,6 +85,10 @@ impl MockServer { .take() .expect("last echo request is none") } + + pub async fn push_preloaded_response(&self, response: serde_json::Value) { + self.ctx.preloaded_responses.lock().await.push(response) + } } #[derive(Debug)] @@ -116,6 +122,7 @@ pub struct Context { pub log: Logger, pub last_echo_request: Arc>>, pub execution_block_generator: RwLock>, + pub preloaded_responses: Arc>>, pub _phantom: PhantomData, } @@ -172,20 +179,33 @@ pub fn serve( .and_then(serde_json::Value::as_u64) .ok_or_else(|| warp::reject::custom(MissingIdField))?; - let response = match handle_rpc(body, ctx).await { - Ok(result) => json!({ - "id": id, - "jsonrpc": JSONRPC_VERSION, - "result": result - }), - Err(message) => json!({ - "id": id, - "jsonrpc": JSONRPC_VERSION, - "error": { - "code": -1234, // Junk error code. - "message": message - } - }), + let preloaded_response = { + let mut preloaded_responses = ctx.preloaded_responses.lock().await; + if !preloaded_responses.is_empty() { + Some(preloaded_responses.remove(0)) + } else { + None + } + }; + + let response = if let Some(preloaded_response) = preloaded_response { + preloaded_response + } else { + match handle_rpc(body, ctx).await { + Ok(result) => json!({ + "id": id, + "jsonrpc": JSONRPC_VERSION, + "result": result + }), + Err(message) => json!({ + "id": id, + "jsonrpc": JSONRPC_VERSION, + "error": { + "code": -1234, // Junk error code. + "message": message + } + }), + } }; Ok::<_, warp::reject::Rejection>(