Skip to content

Commit

Permalink
Handle Geth pre-EIP-155 block sync error condition (#2304)
Browse files Browse the repository at this point in the history
## 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 <[email protected]>
  • Loading branch information
jmcph4 and paulhauner committed Jun 17, 2021
1 parent b1657a6 commit 98ab00c
Showing 1 changed file with 77 additions and 40 deletions.
117 changes: 77 additions & 40 deletions beacon_node/eth1/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -83,23 +113,26 @@ 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")?,
)
}

/// 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)]
Expand All @@ -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")?,
)
Expand All @@ -141,32 +174,32 @@ 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()
.ok_or("Block timestamp was not string")?,
)?;

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()
Expand Down Expand Up @@ -282,16 +315,19 @@ 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())
.ok_or("'result' value was not a string")?;

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)),
}
}

Expand Down Expand Up @@ -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")?
Expand All @@ -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`.
Expand Down Expand Up @@ -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))?;

if let Some(error) = json.get("error") {
Err(format!("Eth1 node returned error: {}", error))
.map_err(|e| RpcError::InvalidJson(e.to_string()))?;

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)
}
}

Expand Down

0 comments on commit 98ab00c

Please sign in to comment.