From 98ab00cc528a28c7abbf45686706110241e35309 Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 17 Jun 2021 02:10:47 +0000 Subject: [PATCH] Handle Geth pre-EIP-155 block sync error condition (#2304) ## Issue Addressed #2293 ## Proposed Changes - Modify the handler for the `eth_chainId` RPC (i.e., `get_chain_id`) to explicitly match against the Geth error string returned for pre-EIP-155 synced Geth nodes - ~~Add a new helper function, `rpc_error_msg`, to aid in the above point~~ - Refactor `response_result` into `response_result_or_error` and patch reliant RPC handlers accordingly (thanks to @pawanjay176) ## Additional Info Geth, as of Pangaea Expanse (v1.10.0), returns an explicit error when it is not synced past the EIP-155 block (2675000). Previously, Geth simply returned a chain ID of 0 (which was obviously much easier to handle on Lighthouse's part). Co-authored-by: Paul Hauner --- beacon_node/eth1/src/http.rs | 115 +++++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 39 deletions(-) diff --git a/beacon_node/eth1/src/http.rs b/beacon_node/eth1/src/http.rs index 9ec7576d2..af628c365 100644 --- a/beacon_node/eth1/src/http.rs +++ b/beacon_node/eth1/src/http.rs @@ -15,6 +15,7 @@ use reqwest::{header::CONTENT_TYPE, ClientBuilder, StatusCode}; use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use std::fmt; use std::ops::Range; use std::str::FromStr; use std::time::Duration; @@ -33,6 +34,9 @@ pub const DEPOSIT_COUNT_RESPONSE_BYTES: usize = 96; /// Number of bytes in deposit contract deposit root (value only). pub const DEPOSIT_ROOT_BYTES: usize = 32; +/// This error is returned during a `chainId` call by Geth. +pub const EIP155_ERROR_STR: &str = "chain not synced beyond EIP-155 replay-protection fork block"; + /// Represents an eth1 chain/network id. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub enum Eth1Id { @@ -48,6 +52,32 @@ pub enum BlockQuery { Latest, } +/// Represents an error received from a remote procecdure call. +#[derive(Debug, Serialize, Deserialize)] +pub enum RpcError { + NoResultField, + Eip155Error, + InvalidJson(String), + Error(String), +} + +impl fmt::Display for RpcError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RpcError::NoResultField => write!(f, "No result field in response"), + RpcError::Eip155Error => write!(f, "Not synced past EIP-155"), + RpcError::InvalidJson(e) => write!(f, "Malformed JSON received: {}", e), + RpcError::Error(s) => write!(f, "{}", s), + } + } +} + +impl From for String { + fn from(e: RpcError) -> String { + e.to_string() + } +} + impl Into for Eth1Id { fn into(self) -> u64 { match self { @@ -83,8 +113,7 @@ impl FromStr for Eth1Id { pub async fn get_network_id(endpoint: &SensitiveUrl, timeout: Duration) -> Result { let response_body = send_rpc_request(endpoint, "net_version", json!([]), timeout).await?; Eth1Id::from_str( - response_result(&response_body)? - .ok_or("No result was returned for network id")? + response_result_or_error(&response_body)? .as_str() .ok_or("Data was not string")?, ) @@ -92,14 +121,18 @@ pub async fn get_network_id(endpoint: &SensitiveUrl, timeout: Duration) -> Resul /// Get the eth1 chain id of the given endpoint. pub async fn get_chain_id(endpoint: &SensitiveUrl, timeout: Duration) -> Result { - let response_body = send_rpc_request(endpoint, "eth_chainId", json!([]), timeout).await?; - hex_to_u64_be( - response_result(&response_body)? - .ok_or("No result was returned for chain id")? - .as_str() - .ok_or("Data was not string")?, - ) - .map(Into::into) + let response_body: String = + send_rpc_request(endpoint, "eth_chainId", json!([]), timeout).await?; + + match response_result_or_error(&response_body) { + Ok(chain_id) => { + hex_to_u64_be(chain_id.as_str().ok_or("Data was not string")?).map(|id| id.into()) + } + // Geth returns this error when it's syncing lower blocks. Simply map this into `0` since + // Lighthouse does not raise errors for `0`, it simply waits for it to change. + Err(RpcError::Eip155Error) => Ok(Eth1Id::Custom(0)), + Err(e) => Err(e.to_string()), + } } #[derive(Debug, PartialEq, Clone)] @@ -115,8 +148,8 @@ pub struct Block { pub async fn get_block_number(endpoint: &SensitiveUrl, timeout: Duration) -> Result { let response_body = send_rpc_request(endpoint, "eth_blockNumber", json!([]), timeout).await?; hex_to_u64_be( - response_result(&response_body)? - .ok_or("No result field was returned for block number")? + response_result_or_error(&response_body) + .map_err(|e| format!("eth_blockNumber failed: {}", e))? .as_str() .ok_or("Data was not string")?, ) @@ -141,23 +174,24 @@ pub async fn get_block( ]); let response_body = send_rpc_request(endpoint, "eth_getBlockByNumber", params, timeout).await?; - let hash = hex_to_bytes( - response_result(&response_body)? - .ok_or("No result field was returned for block")? + let response = response_result_or_error(&response_body) + .map_err(|e| format!("eth_getBlockByNumber failed: {}", e))?; + + let hash: Vec = hex_to_bytes( + response .get("hash") .ok_or("No hash for block")? .as_str() .ok_or("Block hash was not string")?, )?; - let hash = if hash.len() == 32 { - Ok(Hash256::from_slice(&hash)) + let hash: Hash256 = if hash.len() == 32 { + Hash256::from_slice(&hash) } else { - Err(format!("Block has was not 32 bytes: {:?}", hash)) - }?; + return Err(format!("Block has was not 32 bytes: {:?}", hash)); + }; let timestamp = hex_to_u64_be( - response_result(&response_body)? - .ok_or("No result field was returned for timestamp")? + response .get("timestamp") .ok_or("No timestamp for block")? .as_str() @@ -165,8 +199,7 @@ pub async fn get_block( )?; let number = hex_to_u64_be( - response_result(&response_body)? - .ok_or("No result field was returned for number")? + response .get("number") .ok_or("No number for block")? .as_str() @@ -282,9 +315,9 @@ async fn call( ]); let response_body = send_rpc_request(endpoint, "eth_call", params, timeout).await?; - match response_result(&response_body)? { - None => Ok(None), - Some(result) => { + + match response_result_or_error(&response_body) { + Ok(result) => { let hex = result .as_str() .map(|s| s.to_string()) @@ -292,6 +325,9 @@ async fn call( Ok(Some(hex_to_bytes(&hex)?)) } + // It's valid for `eth_call` to return without a result. + Err(RpcError::NoResultField) => Ok(None), + Err(e) => Err(format!("eth_call failed: {}", e)), } } @@ -322,8 +358,8 @@ pub async fn get_deposit_logs_in_range( }]); let response_body = send_rpc_request(endpoint, "eth_getLogs", params, timeout).await?; - response_result(&response_body)? - .ok_or("No result field was returned for deposit logs")? + Ok(response_result_or_error(&response_body) + .map_err(|e| format!("eth_getLogs failed: {}", e))? .as_array() .cloned() .ok_or("'result' value was not an array")? @@ -347,7 +383,7 @@ pub async fn get_deposit_logs_in_range( }) }) .collect::, String>>() - .map_err(|e| format!("Failed to get logs in range: {}", e)) + .map_err(|e| format!("Failed to get logs in range: {}", e))?) } /// Sends an RPC request to `endpoint`, using a POST with the given `body`. @@ -408,19 +444,20 @@ pub async fn send_rpc_request( .map_err(|e| format!("Failed to receive body: {:?}", e)) } -/// Accepts an entire HTTP body (as a string) and returns the `result` field, as a serde `Value`. -fn response_result(response: &str) -> Result, String> { +/// Accepts an entire HTTP body (as a string) and returns either the `result` field or the `error['message']` field, as a serde `Value`. +fn response_result_or_error(response: &str) -> Result { let json = serde_json::from_str::(&response) - .map_err(|e| format!("Failed to parse response: {:?}", e))?; + .map_err(|e| RpcError::InvalidJson(e.to_string()))?; - if let Some(error) = json.get("error") { - Err(format!("Eth1 node returned error: {}", error)) + if let Some(error) = json.get("error").map(|e| e.get("message")).flatten() { + let error = error.to_string(); + if error.contains(EIP155_ERROR_STR) { + Err(RpcError::Eip155Error) + } else { + Err(RpcError::Error(error)) + } } else { - Ok(json - .get("result") - .cloned() - .map(Some) - .unwrap_or_else(|| None)) + json.get("result").cloned().ok_or(RpcError::NoResultField) } }