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:
parent
b1657a60e9
commit
98ab00cc52
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user