//! Helper functions for turning ether's BlockNumber into numbers and updating incoming queries to match. use crate::rpcs::many::Web3Rpcs; use crate::{ errors::{Web3ProxyError, Web3ProxyResult}, rpcs::blockchain::Web3ProxyBlock, }; use anyhow::Context; use derive_more::From; use ethers::{ prelude::{BlockNumber, U64}, types::H256, }; use serde_json::json; use tracing::{error, trace, warn}; #[allow(non_snake_case)] pub fn BlockNumber_to_U64(block_num: BlockNumber, latest_block: &U64) -> (U64, bool) { match block_num { BlockNumber::Earliest => (U64::zero(), false), BlockNumber::Finalized => { warn!("finalized block requested! not yet implemented!"); (*latest_block - 10, false) } BlockNumber::Latest => { // change "latest" to a number (*latest_block, true) } BlockNumber::Number(x) => { // we already have a number (x, false) } BlockNumber::Pending => { // modified is false because we want the backend to see "pending" // TODO: think more about how to handle Pending (*latest_block, false) } BlockNumber::Safe => { warn!("safe block requested! not yet implemented!"); (*latest_block - 3, false) } } } #[derive(Clone, Debug, Eq, From, PartialEq)] pub struct BlockNumAndHash(U64, H256); impl BlockNumAndHash { pub fn num(&self) -> &U64 { &self.0 } pub fn hash(&self) -> &H256 { &self.1 } } impl From<&Web3ProxyBlock> for BlockNumAndHash { fn from(value: &Web3ProxyBlock) -> Self { let n = *value.number(); let h = *value.hash(); Self(n, h) } } /// modify params to always have a block hash and not "latest" /// TODO: this should replace all block numbers with hashes, not just "latest" pub async fn clean_block_number( params: &mut serde_json::Value, block_param_id: usize, latest_block: &Web3ProxyBlock, rpcs: &Web3Rpcs, ) -> Web3ProxyResult { match params.as_array_mut() { None => { // TODO: this needs the correct error code in the response Err(anyhow::anyhow!("params not an array").into()) } Some(params) => match params.get_mut(block_param_id) { None => { if params.len() == block_param_id { // add the latest block number to the end of the params params.push(json!(latest_block.number())); } else { // don't modify the request. only cache with current block // TODO: more useful log that include the warn!("unexpected params length"); } // don't modify params, just cache with the current block Ok(latest_block.into()) } Some(x) => { // dig into the json value to find a BlockNumber or similar block identifier trace!(?x, "inspecting"); let (block, change) = if let Some(obj) = x.as_object_mut() { // it might be a Map like `{"blockHash": String("0xa5626dc20d3a0a209b1de85521717a3e859698de8ce98bca1b16822b7501f74b")}` if let Some(block_hash) = obj.get("blockHash").cloned() { let block_hash: H256 = serde_json::from_value(block_hash).context("decoding blockHash")?; let block = rpcs .block(&block_hash, None, None) .await .context("fetching block number from hash")?; (BlockNumAndHash::from(&block), false) } else { return Err(anyhow::anyhow!("blockHash missing").into()); } } else { // it might be a string like "latest" or a block number or a block hash // TODO: "BlockNumber" needs a better name // TODO: move this to a helper function? if let Ok(block_num) = serde_json::from_value::(x.clone()) { let block_hash = rpcs .block_hash(&block_num) .await .context("fetching block hash from number")?; let block = rpcs .block(&block_hash, None, None) .await .context("fetching block from hash")?; // TODO: do true here? will that work for **all** methods on **all** chains? if not we need something smarter (BlockNumAndHash::from(&block), false) } else if let Ok(block_number) = serde_json::from_value::(x.clone()) { let (block_num, change) = BlockNumber_to_U64(block_number, latest_block.number()); if block_num == *latest_block.number() { (latest_block.into(), change) } else { let block_hash = rpcs .block_hash(&block_num) .await .context("fetching block hash from number")?; let block = rpcs .block(&block_hash, None, None) .await .context("fetching block from hash")?; (BlockNumAndHash::from(&block), change) } } else if let Ok(block_hash) = serde_json::from_value::(x.clone()) { let block = rpcs .block(&block_hash, None, None) .await .context("fetching block number from hash")?; (BlockNumAndHash::from(&block), false) } else { return Err(anyhow::anyhow!( "param not a block identifier, block number, or block hash" ) .into()); } }; // if we changed "latest" to an actual block, update the params to match // TODO: should we do hash or number? some functions work with either, but others need a number :cry: if change { trace!(old=%x, new=%block.num(), "changing block number"); *x = json!(block.num()); } Ok(block) } }, } } /// TODO: change this to also return the hash needed? #[derive(Debug, Eq, PartialEq)] pub enum CacheMode { CacheSuccessForever, CacheNever, Cache { block: BlockNumAndHash, /// cache jsonrpc errors (server errors are never cached) cache_errors: bool, }, CacheRange { from_block: BlockNumAndHash, to_block: BlockNumAndHash, /// cache jsonrpc errors (server errors are never cached) cache_errors: bool, }, } fn get_block_param_id(method: &str) -> Option { match method { "debug_traceBlockByHash" => Some(0), "debug_traceBlockByNumber" => Some(0), "debug_traceCall" => Some(1), "debug_traceTransaction" => None, "eth_call" => Some(1), "eth_estimateGas" => Some(1), "eth_feeHistory" => Some(1), "eth_getBalance" => Some(1), "eth_getBlockReceipts" => Some(0), "eth_getBlockTransactionCountByNumber" => Some(0), "eth_getCode" => Some(1), "eth_getStorageAt" => Some(2), "eth_getTransactionByBlockNumberAndIndex" => Some(0), "eth_getTransactionCount" => Some(1), "eth_getUncleByBlockNumberAndIndex" => Some(0), "eth_getUncleCountByBlockNumber" => Some(0), "trace_block" => Some(0), "trace_call" => Some(2), "trace_callMany" => Some(1), _ => None, } } impl CacheMode { pub async fn new( method: &str, params: &mut serde_json::Value, head_block: &Web3ProxyBlock, rpcs: &Web3Rpcs, ) -> Self { match Self::try_new(method, params, head_block, rpcs).await { Ok(x) => x, Err(Web3ProxyError::NoBlocksKnown) => { warn!(%method, ?params, "no servers available to get block from params. caching with head block"); CacheMode::Cache { block: head_block.into(), cache_errors: true, } } Err(err) => { error!(%method, ?params, ?err, "could not get block from params. caching with head block"); CacheMode::Cache { block: head_block.into(), cache_errors: true, } } } } pub async fn try_new( method: &str, params: &mut serde_json::Value, head_block: &Web3ProxyBlock, rpcs: &Web3Rpcs, ) -> Web3ProxyResult { if matches!(params, serde_json::Value::Null) { // no params given. cache with the head block return Ok(Self::Cache { block: head_block.into(), cache_errors: true, }); } if let Some(params) = params.as_array() { if params.is_empty() { // no params given. cache with the head block return Ok(Self::Cache { block: head_block.into(), cache_errors: true, }); } } match method { "debug_traceTransaction" => { // TODO: make sure re-orgs work properly! Ok(CacheMode::CacheSuccessForever) } "eth_gasPrice" => Ok(CacheMode::Cache { block: head_block.into(), cache_errors: false, }), "eth_getBlockByHash" => { // TODO: double check that any node can serve this // TODO: can a block change? like what if it gets orphaned? // TODO: make sure re-orgs work properly! Ok(CacheMode::CacheSuccessForever) } "eth_getBlockByNumber" => { // TODO: double check that any node can serve this // TODO: CacheSuccessForever if the block is old enough // TODO: make sure re-orgs work properly! Ok(CacheMode::Cache { block: head_block.into(), cache_errors: true, }) } "eth_getBlockTransactionCountByHash" => { // TODO: double check that any node can serve this Ok(CacheMode::CacheSuccessForever) } "eth_getLogs" => { // TODO: think about this more // TODO: jsonrpc has a specific code for this let obj = params .get_mut(0) .ok_or_else(|| Web3ProxyError::BadRequest("invalid format. no params".into()))? .as_object_mut() .ok_or_else(|| { Web3ProxyError::BadRequest("invalid format. params not object".into()) })?; if obj.contains_key("blockHash") { Ok(CacheMode::CacheSuccessForever) } else { let from_block = if let Some(x) = obj.get_mut("fromBlock") { // TODO: use .take instead of clone // what if its a hash? let block_num: BlockNumber = serde_json::from_value(x.clone())?; let (block_num, change) = BlockNumber_to_U64(block_num, head_block.number()); if change { // TODO: include the hash instead of the number? trace!("changing fromBlock in eth_getLogs. {} -> {}", x, block_num); *x = json!(block_num); } let block_hash = rpcs.block_hash(&block_num).await?; BlockNumAndHash(block_num, block_hash) } else { BlockNumAndHash(U64::zero(), H256::zero()) }; let to_block = if let Some(x) = obj.get_mut("toBlock") { // TODO: use .take instead of clone // what if its a hash? let block_num: BlockNumber = serde_json::from_value(x.clone())?; let (block_num, change) = BlockNumber_to_U64(block_num, head_block.number()); if change { trace!("changing toBlock in eth_getLogs. {} -> {}", x, block_num); *x = json!(block_num); } let block_hash = rpcs.block_hash(&block_num).await?; BlockNumAndHash(block_num, block_hash) } else { head_block.into() }; Ok(CacheMode::CacheRange { from_block, to_block, cache_errors: true, }) } } "eth_getTransactionByHash" => { // TODO: not sure how best to look these up // try full nodes first. retry will use archive Ok(CacheMode::Cache { block: head_block.into(), cache_errors: true, }) } "eth_getTransactionByBlockHashAndIndex" => { // TODO: check a Cache of recent hashes // try full nodes first. retry will use archive Ok(CacheMode::CacheSuccessForever) } "eth_getTransactionReceipt" => { // TODO: not sure how best to look these up // try full nodes first. retry will use archive Ok(CacheMode::Cache { block: head_block.into(), cache_errors: true, }) } "eth_getUncleByBlockHashAndIndex" => { // TODO: check a Cache of recent hashes // try full nodes first. retry will use archive // TODO: what happens if this block is uncled later? Ok(CacheMode::CacheSuccessForever) } "eth_getUncleCountByBlockHash" => { // TODO: check a Cache of recent hashes // try full nodes first. retry will use archive // TODO: what happens if this block is uncled later? Ok(CacheMode::CacheSuccessForever) } "eth_maxPriorityFeePerGas" => { // TODO: this might be too aggressive. i think it can change before a block is mined Ok(CacheMode::Cache { block: head_block.into(), cache_errors: false, }) } "net_listening" => Ok(CacheMode::CacheSuccessForever), "net_version" => Ok(CacheMode::CacheSuccessForever), method => match get_block_param_id(method) { Some(block_param_id) => { let block = clean_block_number(params, block_param_id, head_block, rpcs).await?; Ok(CacheMode::Cache { block, cache_errors: true, }) } None => Err(Web3ProxyError::UnhandledMethod(method.to_string().into())), }, } } } #[cfg(test)] mod test { use super::CacheMode; use crate::rpcs::{blockchain::Web3ProxyBlock, many::Web3Rpcs}; use ethers::types::{Block, H256}; use serde_json::json; use std::sync::Arc; #[test_log::test(tokio::test)] async fn test_fee_history() { let method = "eth_feeHistory"; let mut params = json!([4, "latest", [25, 75]]); let head_block = Block { number: Some(1.into()), hash: Some(H256::random()), ..Default::default() }; let head_block = Web3ProxyBlock::try_new(Arc::new(head_block)).unwrap(); let (empty, _handle, _ranked_rpc_reciver) = Web3Rpcs::spawn(1, None, 1, 1, "test".into(), None, None) .await .unwrap(); let x = CacheMode::try_new(method, &mut params, &head_block, &empty) .await .unwrap(); assert_eq!( x, CacheMode::Cache { block: (&head_block).into(), cache_errors: true } ); // "latest" should have been changed to the block number assert_eq!(params.get(1), Some(&json!(head_block.number()))); } }