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 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<RpcError> for String {
fn from(e: RpcError) -> String {
e.to_string()
}
}
impl Into<u64> 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<Eth1Id, String> {
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<Eth1Id, String> {
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<u64, String> {
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<u8> = 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::<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`.
@ -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<Option<Value>, 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<Value, RpcError> {
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") {
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)
}
}