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 <paul@paulhauner.com>
This commit is contained in:
Jack 2021-06-17 02:10:47 +00:00
parent b1657a60e9
commit 98ab00cc52

View File

@ -15,6 +15,7 @@ use reqwest::{header::CONTENT_TYPE, ClientBuilder, StatusCode};
use sensitive_url::SensitiveUrl; use sensitive_url::SensitiveUrl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::fmt;
use std::ops::Range; use std::ops::Range;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; 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). /// Number of bytes in deposit contract deposit root (value only).
pub const DEPOSIT_ROOT_BYTES: usize = 32; 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. /// Represents an eth1 chain/network id.
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub enum Eth1Id { pub enum Eth1Id {
@ -48,6 +52,32 @@ pub enum BlockQuery {
Latest, 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<RpcError> for String {
fn from(e: RpcError) -> String {
e.to_string()
}
}
impl Into<u64> for Eth1Id { impl Into<u64> for Eth1Id {
fn into(self) -> u64 { fn into(self) -> u64 {
match self { match self {
@ -83,8 +113,7 @@ impl FromStr for Eth1Id {
pub async fn get_network_id(endpoint: &SensitiveUrl, timeout: Duration) -> Result<Eth1Id, String> { pub async fn get_network_id(endpoint: &SensitiveUrl, timeout: Duration) -> Result<Eth1Id, String> {
let response_body = send_rpc_request(endpoint, "net_version", json!([]), timeout).await?; let response_body = send_rpc_request(endpoint, "net_version", json!([]), timeout).await?;
Eth1Id::from_str( Eth1Id::from_str(
response_result(&response_body)? response_result_or_error(&response_body)?
.ok_or("No result was returned for network id")?
.as_str() .as_str()
.ok_or("Data was not string")?, .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. /// Get the eth1 chain id of the given endpoint.
pub async fn get_chain_id(endpoint: &SensitiveUrl, timeout: Duration) -> Result<Eth1Id, String> { pub async fn get_chain_id(endpoint: &SensitiveUrl, timeout: Duration) -> Result<Eth1Id, String> {
let response_body = send_rpc_request(endpoint, "eth_chainId", json!([]), timeout).await?; let response_body: String =
hex_to_u64_be( send_rpc_request(endpoint, "eth_chainId", json!([]), timeout).await?;
response_result(&response_body)?
.ok_or("No result was returned for chain id")? match response_result_or_error(&response_body) {
.as_str() Ok(chain_id) => {
.ok_or("Data was not string")?, hex_to_u64_be(chain_id.as_str().ok_or("Data was not string")?).map(|id| id.into())
) }
.map(Into::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)] #[derive(Debug, PartialEq, Clone)]
@ -115,8 +148,8 @@ pub struct Block {
pub async fn get_block_number(endpoint: &SensitiveUrl, timeout: Duration) -> Result<u64, String> { pub async fn get_block_number(endpoint: &SensitiveUrl, timeout: Duration) -> Result<u64, String> {
let response_body = send_rpc_request(endpoint, "eth_blockNumber", json!([]), timeout).await?; let response_body = send_rpc_request(endpoint, "eth_blockNumber", json!([]), timeout).await?;
hex_to_u64_be( hex_to_u64_be(
response_result(&response_body)? response_result_or_error(&response_body)
.ok_or("No result field was returned for block number")? .map_err(|e| format!("eth_blockNumber failed: {}", e))?
.as_str() .as_str()
.ok_or("Data was not string")?, .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 response_body = send_rpc_request(endpoint, "eth_getBlockByNumber", params, timeout).await?;
let hash = hex_to_bytes( let response = response_result_or_error(&response_body)
response_result(&response_body)? .map_err(|e| format!("eth_getBlockByNumber failed: {}", e))?;
.ok_or("No result field was returned for block")?
let hash: Vec<u8> = hex_to_bytes(
response
.get("hash") .get("hash")
.ok_or("No hash for block")? .ok_or("No hash for block")?
.as_str() .as_str()
.ok_or("Block hash was not string")?, .ok_or("Block hash was not string")?,
)?; )?;
let hash = if hash.len() == 32 { let hash: Hash256 = if hash.len() == 32 {
Ok(Hash256::from_slice(&hash)) Hash256::from_slice(&hash)
} else { } 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( let timestamp = hex_to_u64_be(
response_result(&response_body)? response
.ok_or("No result field was returned for timestamp")?
.get("timestamp") .get("timestamp")
.ok_or("No timestamp for block")? .ok_or("No timestamp for block")?
.as_str() .as_str()
@ -165,8 +199,7 @@ pub async fn get_block(
)?; )?;
let number = hex_to_u64_be( let number = hex_to_u64_be(
response_result(&response_body)? response
.ok_or("No result field was returned for number")?
.get("number") .get("number")
.ok_or("No number for block")? .ok_or("No number for block")?
.as_str() .as_str()
@ -282,9 +315,9 @@ async fn call(
]); ]);
let response_body = send_rpc_request(endpoint, "eth_call", params, timeout).await?; let response_body = send_rpc_request(endpoint, "eth_call", params, timeout).await?;
match response_result(&response_body)? {
None => Ok(None), match response_result_or_error(&response_body) {
Some(result) => { Ok(result) => {
let hex = result let hex = result
.as_str() .as_str()
.map(|s| s.to_string()) .map(|s| s.to_string())
@ -292,6 +325,9 @@ async fn call(
Ok(Some(hex_to_bytes(&hex)?)) 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?; let response_body = send_rpc_request(endpoint, "eth_getLogs", params, timeout).await?;
response_result(&response_body)? Ok(response_result_or_error(&response_body)
.ok_or("No result field was returned for deposit logs")? .map_err(|e| format!("eth_getLogs failed: {}", e))?
.as_array() .as_array()
.cloned() .cloned()
.ok_or("'result' value was not an array")? .ok_or("'result' value was not an array")?
@ -347,7 +383,7 @@ pub async fn get_deposit_logs_in_range(
}) })
}) })
.collect::<Result<Vec<Log>, String>>() .collect::<Result<Vec<Log>, 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`. /// 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)) .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`. /// 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(response: &str) -> Result<Option<Value>, String> { fn response_result_or_error(response: &str) -> Result<Value, RpcError> {
let json = serde_json::from_str::<Value>(&response) let json = serde_json::from_str::<Value>(&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") { if let Some(error) = json.get("error").map(|e| e.get("message")).flatten() {
Err(format!("Eth1 node returned error: {}", error)) let error = error.to_string();
if error.contains(EIP155_ERROR_STR) {
Err(RpcError::Eip155Error)
} else {
Err(RpcError::Error(error))
}
} else { } else {
Ok(json json.get("result").cloned().ok_or(RpcError::NoResultField)
.get("result")
.cloned()
.map(Some)
.unwrap_or_else(|| None))
} }
} }