From b3c005e78717eaa8fda2c5a33f9bb98c7f78f7c0 Mon Sep 17 00:00:00 2001 From: Bryan Stitt Date: Wed, 20 Jul 2022 23:49:29 +0000 Subject: [PATCH] error if future block is requested --- TODO.md | 14 ++++++------ web3-proxy/src/app.rs | 10 ++++----- web3-proxy/src/connections.rs | 41 +++++++++++++++++++++++++++-------- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/TODO.md b/TODO.md index d20d9a85..66542442 100644 --- a/TODO.md +++ b/TODO.md @@ -39,8 +39,8 @@ - [x] automatically route to archive server when necessary - originally, no processing was done to params; they were just serde_json::RawValue. this is probably fastest, but we need to look for "latest" and count elements, so we have to use serde_json::Value - when getting the next server, filtering on "archive" isn't going to work well. need to check inner instead - - [ ] this works well for local servers, but public nodes seem to give unreliable results. likely because of load balancers. maybe have a "max block data limit" -- [ ] if the requested block is ahead of the best block, return without querying any backend servers + - [ ] this works well for local servers, but public nodes (especially on other chains) seem to give unreliable results. likely because of load balancers. maybe have a "max block data limit" +- [x] if the requested block is ahead of the best block, return without querying any backend servers - [ ] basic request method stats - [x] http servers should check block at the very start - [ ] if the fastest server has hit rate limits, we won't be able to serve any traffic until another server is synced. @@ -53,6 +53,7 @@ - create the app without applying any config to it - have a blocking future watching the config file and calling app.apply_config() on first load and on change - work started on this in the "config_reloads" branch. because of how we pass channels around during spawn, this requires a larger refactor. +- [ ] have a "backup" tier that is only used when the primary tier has no servers or is multiple blocks behind. we don't want the backup tier taking over with the head block if they happen to be fast at that (but overall low/expensive rps). only if the primary tier has fallen behind or gone entirely offline should we go to third parties - [ ] most things that are cached locally should probably be in shared redis caches - [ ] stats when forks are resolved (and what chain they were on?) - [ ] incoming rate limiting (by api key) @@ -76,6 +77,7 @@ - [ ] don't "unwrap" anywhere. give proper errors new endpoints for users: +- think about where to put this. a separate app might be better, especially so we don't get cloned too easily. open source code could just have a cli tool for managing users - [ ] GET /user/login/$address - returns a JSON string for the user to sign - [ ] POST /user/login/$address @@ -109,11 +111,7 @@ in another repo: event subscriber - [ ] automated soft limit - look at average request time for getBlock? i'm not sure how good a proxy that will be for serving eth_call, but its a start -- [ ] add a subscription that returns the head block number and hash but nothing else -- [ ] interval for http subscriptions should be based on block time. load from config is easy, but better to query -- [ ] ethers has a transactions_unsorted httprpc method that we should probably use. all rpcs probably don't support it, so make it okay for that to fail -- [ ] if chain split detected, don't send transactions? -- [ ] have a "backup" tier that is only used when the primary tier has no servers or is multiple blocks behind. we don't want the backup tier taking over with the head block if they happen to be fast at that (but overall low/expensive rps). only if the primary tier has fallen behind or gone entirely offline should we go to third parties +- [ ] interval for http subscriptions should be based on block time. load from config is easy, but better to query. currently hard coded to 13 seconds - [ ] more advanced automated soft limit - measure average latency of a node's responses and load balance on that @@ -141,3 +139,5 @@ in another repo: event subscriber - [x] document load tests: docker run --rm --name spam shazow/ethspam --rpc http://$LOCAL_IP:8544 | versus --concurrency=100 --stop-after=10000 http://$LOCAL_IP:8544; docker stop spam - [ ] if the call is something simple like "symbol" or "decimals", cache that too. though i think this could bite us. - [ ] Got warning: "WARN subscribe_new_heads:send_block: web3_proxy::connection: unable to get block from https://rpc.ethermine.org: Deserialization Error: expected value at line 1 column 1. Response: error code: 1015". this is cloudflare rate limiting on fetching a block, but this is a private rpc. why is there a block subscription? +- [ ] add a subscription that returns the head block number and hash but nothing else +- [ ] if chain split detected, what should we do? don't send transactions? diff --git a/web3-proxy/src/app.rs b/web3-proxy/src/app.rs index df7cad95..ba224c77 100644 --- a/web3-proxy/src/app.rs +++ b/web3-proxy/src/app.rs @@ -126,7 +126,7 @@ fn get_or_set_block_number( } } -// TODO: change this to return the height needed (OR hash if recent) +// TODO: change this to return also return the hash needed fn get_min_block_needed( method: &str, params: Option<&mut serde_json::Value>, @@ -682,7 +682,7 @@ impl Web3ProxyApp { async fn get_cached_response( &self, - // TODO: accept a block hash here also + // TODO: accept a block hash here also? min_block_needed: Option, request: &JsonRpcRequest, ) -> anyhow::Result<( @@ -693,10 +693,10 @@ impl Web3ProxyApp { // TODO: https://github.com/ethereum/web3.py/blob/master/web3/middleware/cache.py let request_block_hash = if let Some(min_block_needed) = min_block_needed { - let block_result = self.balanced_rpcs.get_block_hash(min_block_needed).await; - - block_result? + // TODO: maybe this should be on the app and not on balanced_rpcs + self.balanced_rpcs.get_block_hash(min_block_needed).await? } else { + // TODO: maybe this should be on the app and not on balanced_rpcs self.balanced_rpcs.get_head_block_hash() }; diff --git a/web3-proxy/src/connections.rs b/web3-proxy/src/connections.rs index 0fc28531..0fc40b34 100644 --- a/web3-proxy/src/connections.rs +++ b/web3-proxy/src/connections.rs @@ -377,13 +377,9 @@ impl Web3Connections { Ok(()) } - pub fn get_head_block_num(&self) -> u64 { - self.synced_connections.load().get_head_block_num() - } - pub async fn get_block_hash(&self, num: U64) -> anyhow::Result { - // TODO: this definitely needs caching - // first, try to get the hash from petgraph. but how do we get all blocks with a given num and then pick the one on the correct chain? + // first, try to get the hash from our cache + // TODO: move this cache to redis? if let Some(hash) = self.block_map.get(&num.as_u64()) { // for now, just return the first seen block. we actually want the winning block! return Ok(*hash.iter().next().unwrap()); @@ -391,17 +387,28 @@ impl Web3Connections { unimplemented!("use petgraph to find the heaviest chain"); } - // TODO: helper for this + let head_block_num = self.get_head_block_num(); + + if num.as_u64() > head_block_num { + return Err(anyhow::anyhow!( + "Head block is #{}, but #{} was requested", + head_block_num, + num + )); + } + + // TODO: helper for method+params => JsonRpcRequest let request = json!({ "id": "1", "method": "eth_getBlockByNumber", "params": (num, false) }); let request: JsonRpcRequest = serde_json::from_value(request)?; - // TODO: if error, retry + // TODO: if error, retry? let response = self .try_send_best_upstream_server(request, Some(num)) .await?; let block = response.result.unwrap().to_string(); + let block: Block = serde_json::from_str(&block)?; let hash = block.hash.unwrap(); @@ -411,12 +418,28 @@ impl Web3Connections { Ok(hash) } + pub fn get_head_block(&self) -> (u64, H256) { + let synced_connections = self.synced_connections.load(); + + let num = synced_connections.get_head_block_num(); + let hash = *synced_connections.get_head_block_hash(); + + (num, hash) + } + pub fn get_head_block_hash(&self) -> H256 { *self.synced_connections.load().get_head_block_hash() } + pub fn get_head_block_num(&self) -> u64 { + self.synced_connections.load().get_head_block_num() + } + pub fn has_synced_rpcs(&self) -> bool { - !self.synced_connections.load().inner.is_empty() + if self.synced_connections.load().inner.is_empty() { + return false; + } + self.get_head_block_num() > 0 } pub fn num_synced_rpcs(&self) -> usize {