diff --git a/TODO.md b/TODO.md index 2e3f44a9..7d082f06 100644 --- a/TODO.md +++ b/TODO.md @@ -180,8 +180,11 @@ These are roughly in order of completition - [x] user create script should allow a description field - [x] change stats to using the database - [x] emit user stat on retry -- [ ] include if archive query or not in the stats - - this is already partially done, but we need to double check it works. preferrably with tests +- [x] improve `web3_proxy_cli check_config` + - print out warnings if important settings are missing +- [x] if unknown config items, error + - unknown configs are almost always a mistake. usually from me changing config parsing on my side and old fields not being updated to the new way + - [x] also need to change how we disable rpcs since i was using an unknown field - [-] ability to domain lock or ip lock said key - the code to check the database and use these entries already exists, but users don't have a way to set them - [-] new endpoints for users (not totally sure about the exact paths, but these features are all needed): @@ -198,6 +201,8 @@ These are roughly in order of completition - allow setting things such as private relay, revert logging, ip/origin/etc checks - [ ] GET logged reverts on an endpoint that requires authentication. - [ ] endpoint for creating/modifying api keys and their advanced security features +- [ ] include if archive query or not in the stats + - this is already partially done, but we need to double check it works. preferrably with tests - [ ] WARN http_request:request: web3_proxy::block_number: could not get block from params err=unexpected params length id=01GF4HTRKM4JV6NX52XSF9AYMW method=POST authorized_request=User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 }) - ERROR http_request:request:try_send_all_upstream_servers: web3_proxy::rpcs::request: bad response! err=JsonRpcClientError(JsonRpcError(JsonRpcError { code: -32000, message: "INTERNAL_ERROR: existing tx with same hash", data: None })) method=eth_sendRawTransaction rpc=local_erigon_alpha_archive id=01GF4HV03Y4ZNKQV8DW5NDQ5CG method=POST authorized_request=User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 }) self=Web3Connections { conns: {"local_erigon_alpha_archive_ws": Web3Connection { name: "local_erigon_alpha_archive_ws", blocks: "all", .. }, "local_geth_ws": Web3Connection { name: "local_geth_ws", blocks: 64, .. }, "local_erigon_alpha_archive": Web3Connection { name: "local_erigon_alpha_archive", blocks: "all", .. }}, .. } authorized_request=Some(User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 })) request=JsonRpcRequest { id: RawValue(39), method: "eth_sendRawTransaction", .. } request_metadata=Some(RequestMetadata { datetime: 2022-10-11T22:14:57.406829095Z, period_seconds: 60, request_bytes: 633, backend_requests: 0, no_servers: 0, error_response: false, response_bytes: 0, response_millis: 0 }) block_needed=None - why is it failing to get the block from params when its set to None? That should be the simple case diff --git a/config/example.bac b/config/example.bac deleted file mode 100644 index 7dd72107..00000000 --- a/config/example.bac +++ /dev/null @@ -1,36 +0,0 @@ -[app] -chain_id = 1 -public_rate_limit_per_minute = 60_000 - -[balanced_rpcs] - - [balanced_rpcs.erigon_archive] - url = "http://127.0.0.1:8549" - # TODO: double check soft_limit on erigon - soft_limit = 100_000 - - [balanced_rpcs.geth] - url = "http://127.0.0.1:8545" - soft_limit = 200_000 - -[private_rpcs] - - [private_rpcs.eden] - url = "https://api.edennetwork.io/v1/" - soft_limit = 1_805 - - [private_rpcs.eden_beta] - url = "https://api.edennetwork.io/v1/beta" - soft_limit = 5_861 - - [private_rpcs.ethermine] - url = "https://rpc.ethermine.org" - soft_limit = 5_861 - - [private_rpcs.flashbots_fast] - url = "https://rpc.flashbots.net" - soft_limit = 7074 - - [private_rpcs.securerpc] - url = "https://gibson.securerpc.com/v1" - soft_limit = 4560 diff --git a/config/example.toml b/config/example.toml index 82aed485..bf654820 100644 --- a/config/example.toml +++ b/config/example.toml @@ -1,9 +1,9 @@ [shared] chain_id = 1 -db_url = "mysql://root:dev_web3_proxy@dev-db:3306/dev_web3_proxy" # TODO: how do we find the optimal db_max_connections? too high actually ends up being slower db_max_connections = 99 +db_url = "mysql://root:dev_web3_proxy@dev-db:3306/dev_web3_proxy" min_sum_soft_limit = 2000 min_synced_rpcs = 2 @@ -15,7 +15,7 @@ volatile_redis_url = "redis://dev-vredis:6379/" redirect_public_url = "https://llamanodes.com/free-rpc-stats" redirect_user_url = "https://llamanodes.com/user-rpc-stats/{{user_id}}" -frontend_rate_limit_per_minute = 0 +public_requests_per_minute = 0 # 1GB of cache response_cache_max_bytes = 10000000000 @@ -72,27 +72,32 @@ response_cache_max_bytes = 10000000000 # these worked well on ETH 1.0, but 2.0 ends up not working as well. we will re-assess as more validators turn on private transactions - [private_rpcs_off.eden] + [private_rpcs.eden] + disabled = true url = "https://api.edennetwork.io/v1/" soft_limit = 1_805 weight = 0 - [private_rpcs_off.eden_beta] + [private_rpcs.eden_beta] + disabled = true url = "https://api.edennetwork.io/v1/beta" soft_limit = 5_861 weight = 0 - [private_rpcs_off.ethermine] + [private_rpcs.ethermine] + disabled = true url = "https://rpc.ethermine.org" soft_limit = 5_861 weight = 0 - [private_rpcs_off.flashbots] + [private_rpcs.flashbots] + disabled = true url = "https://rpc.flashbots.net/fast" soft_limit = 7074 weight = 0 - [private_rpcs_off.securerpc] + [private_rpcs.securerpc] + disabled = true url = "https://gibson.securerpc.com/v1" soft_limit = 4560 weight = 0 diff --git a/web3_proxy/src/app.rs b/web3_proxy/src/app.rs index e6760cd8..e4f43491 100644 --- a/web3_proxy/src/app.rs +++ b/web3_proxy/src/app.rs @@ -349,9 +349,8 @@ impl Web3ProxyApp { block_map, // subscribing to new heads here won't work well. if they are fast, they might be ahead of balanced_rpcs None, - // minimum doesn't really matter on private rpcs - 1, - 1, + 0, + 0, // TODO: subscribe to pending transactions on the private rpcs? they seem to have low rate limits None, pending_transactions.clone(), @@ -376,7 +375,11 @@ impl Web3ProxyApp { let rpc_rrl = RedisRateLimiter::new( "web3_proxy", "frontend", - top_config.app.frontend_rate_limit_per_minute, + // TODO: think about this unwrapping + top_config + .app + .public_requests_per_minute + .unwrap_or(u64::MAX), 60.0, redis_pool.clone(), ); diff --git a/web3_proxy/src/bin/web3_proxy.rs b/web3_proxy/src/bin/web3_proxy.rs index 4c8fe4cd..2c2dca45 100644 --- a/web3_proxy/src/bin/web3_proxy.rs +++ b/web3_proxy/src/bin/web3_proxy.rs @@ -215,10 +215,10 @@ mod tests { let app_config = TopConfig { app: AppConfig { chain_id: 31337, - default_requests_per_minute: Some(6_000_000), + default_user_requests_per_minute: Some(6_000_000), min_sum_soft_limit: 1, min_synced_rpcs: 1, - frontend_rate_limit_per_minute: 1_000_000, + public_requests_per_minute: Some(1_000_000), response_cache_max_bytes: 10_usize.pow(7), redirect_public_url: Some("example.com/".to_string()), redirect_user_url: Some("example.com/{{user_id}}".to_string()), @@ -227,11 +227,11 @@ mod tests { balanced_rpcs: HashMap::from([ ( "anvil".to_string(), - Web3ConnectionConfig::new(anvil.endpoint(), 100, None, 1, Some(false)), + Web3ConnectionConfig::new(false, anvil.endpoint(), 100, None, 1, Some(false)), ), ( "anvil_ws".to_string(), - Web3ConnectionConfig::new(anvil.ws_endpoint(), 100, None, 0, Some(true)), + Web3ConnectionConfig::new(false, anvil.ws_endpoint(), 100, None, 0, Some(true)), ), ]), private_rpcs: None, diff --git a/web3_proxy/src/bin/web3_proxy_cli/check_config.rs b/web3_proxy/src/bin/web3_proxy_cli/check_config.rs index 700a6287..286650ca 100644 --- a/web3_proxy/src/bin/web3_proxy_cli/check_config.rs +++ b/web3_proxy/src/bin/web3_proxy_cli/check_config.rs @@ -1,13 +1,13 @@ use argh::FromArgs; use std::fs; -use tracing::info; +use tracing::{info, warn}; use web3_proxy::config::TopConfig; #[derive(FromArgs, PartialEq, Eq, Debug)] /// Second subcommand. #[argh(subcommand, name = "check_config")] pub struct CheckConfigSubCommand { - #[argh(option)] + #[argh(positional)] /// path to the configuration toml. path: String, } @@ -18,7 +18,56 @@ impl CheckConfigSubCommand { let top_config: String = fs::read_to_string(self.path)?; let top_config: TopConfig = toml::from_str(&top_config)?; - info!("config: {:?}", top_config); + // TODO: pretty print + info!("config: {:#?}", top_config); + + if top_config.app.db_url.is_none() { + warn!("app.db_url is not set! Some features disabled") + } + + match top_config.app.public_requests_per_minute { + None => { + info!("app.public_requests_per_minute is None. Fully open to public requests!") + } + Some(0) => { + info!("app.public_requests_per_minute is 0. Public requests are blocked!") + } + Some(_) => {} + } + + match top_config.app.default_user_requests_per_minute { + None => { + info!("app.default_user_requests_per_minute is None. Fully open to registered requests!") + } + Some(0) => warn!("app.default_user_requests_per_minute is 0. Registered user's requests are blocked! Are you sure you want that?"), + Some(_) => { + // TODO: make sure this isn't < anonymous requests per minute + } + } + + match top_config.app.invite_code { + None => info!("app.invite_code is None. Registration is open"), + Some(_) => info!("app.invite_code is set. Registration is limited"), + } + + // TODO: check min_sum_soft_limit is a reasonable amount + // TODO: check min_synced_rpcs is a reasonable amount + // TODO: check frontend_rate_limit_per_minute is a reasonable amount. requires redis + // TODO: check login_rate_limit_per_minute is a reasonable amount. requires redis + + if top_config.app.volatile_redis_url.is_none() { + warn!("app.volatile_redis_url is not set! Some features disabled") + } + + // TODO: check response_cache_max_bytes is a reasonable amount + + if top_config.app.redirect_public_url.is_none() { + warn!("app.redirect_public_url is None. Anonyoumous users will get an error page instead of a redirect") + } + + if top_config.app.redirect_user_url.is_none() { + warn!("app.redirect_public_url is None. Registered users will get an error page instead of a redirect") + } Ok(()) } diff --git a/web3_proxy/src/config.rs b/web3_proxy/src/config.rs index dc8995a9..f9caafd5 100644 --- a/web3_proxy/src/config.rs +++ b/web3_proxy/src/config.rs @@ -38,6 +38,7 @@ pub struct CliConfig { } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TopConfig { pub app: AppConfig, pub balanced_rpcs: HashMap, @@ -47,6 +48,7 @@ pub struct TopConfig { /// shared configuration between Web3Connections // TODO: no String, only &str #[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] pub struct AppConfig { /// EVM chain id. 1 for ETH /// TODO: better type for chain_id? max of `u64::MAX / 2 - 36` https://github.com/ethereum/EIPs/issues/2294 @@ -63,7 +65,7 @@ pub struct AppConfig { /// Default request limit for registered users. /// 0 = block all requests /// None = allow all requests - pub default_requests_per_minute: Option, + pub default_user_requests_per_minute: Option, /// Restrict user registration. /// None = no code needed pub invite_code: Option, @@ -74,9 +76,10 @@ pub struct AppConfig { #[serde(default = "default_min_synced_rpcs")] pub min_synced_rpcs: usize, /// Request limit for anonymous users. - /// Set to 0 to block all anonymous requests. - #[serde(default = "default_frontend_rate_limit_per_minute")] - pub frontend_rate_limit_per_minute: u64, + /// Some(0) = block all requests + /// None = allow all requests + #[serde(default = "default_public_requests_per_minute")] + pub public_requests_per_minute: Option, /// Rate limit for the login entrypoint. /// This is separate from the rpc limits. #[serde(default = "default_login_rate_limit_per_minute")] @@ -107,8 +110,8 @@ fn default_min_synced_rpcs() -> usize { } /// 0 blocks anonymous requests by default. -fn default_frontend_rate_limit_per_minute() -> u64 { - 0 +fn default_public_requests_per_minute() -> Option { + Some(0) } /// Having a low amount of requests per minute for login is safest. @@ -122,19 +125,25 @@ fn default_response_cache_max_bytes() -> usize { 10_usize.pow(8) } +/// Configuration for a backend web3 RPC server #[derive(Debug, Deserialize, Constructor)] +#[serde(deny_unknown_fields)] pub struct Web3ConnectionConfig { + /// simple way to disable a connection without deleting the row + #[serde(default)] + pub disabled: bool, /// websocket (or http if no websocket) - url: String, + pub url: String, /// the requests per second at which the server starts slowing down - soft_limit: u32, + pub soft_limit: u32, /// the requests per second at which the server throws errors (rate limit or otherwise) - hard_limit: Option, + pub hard_limit: Option, /// All else equal, a server with a lower weight receives requests - weight: u32, + pub weight: u32, /// Subscribe to the firehose of pending transactions /// Don't do this with free rpcs - subscribe_txs: Option, + #[serde(default)] + pub subscribe_txs: Option, } impl Web3ConnectionConfig { diff --git a/web3_proxy/src/frontend/users.rs b/web3_proxy/src/frontend/users.rs index 1cb75a79..fe6709eb 100644 --- a/web3_proxy/src/frontend/users.rs +++ b/web3_proxy/src/frontend/users.rs @@ -221,7 +221,7 @@ pub async fn user_login_post( let uk = user_keys::ActiveModel { user_id: sea_orm::Set(u.id), api_key: sea_orm::Set(user_key.into()), - requests_per_minute: sea_orm::Set(app.config.default_requests_per_minute), + requests_per_minute: sea_orm::Set(app.config.default_user_requests_per_minute), ..Default::default() }; diff --git a/web3_proxy/src/rpcs/connections.rs b/web3_proxy/src/rpcs/connections.rs index 07a55534..28af4ae3 100644 --- a/web3_proxy/src/rpcs/connections.rs +++ b/web3_proxy/src/rpcs/connections.rs @@ -113,7 +113,11 @@ impl Web3Connections { // TODO: move this into a helper function. then we can use it when configs change (will need a remove function too) let spawn_handles: Vec<_> = server_configs .into_iter() - .map(|(server_name, server_config)| { + .filter_map(|(server_name, server_config)| { + if server_config.disabled { + return None; + } + let http_client = http_client.clone(); let redis_pool = redis_pool.clone(); let http_interval_sender = http_interval_sender.clone(); @@ -128,7 +132,7 @@ impl Web3Connections { let block_map = block_map.clone(); let open_request_handle_metrics = open_request_handle_metrics.clone(); - tokio::spawn(async move { + let handle = tokio::spawn(async move { server_config .spawn( server_name, @@ -142,7 +146,9 @@ impl Web3Connections { open_request_handle_metrics, ) .await - }) + }); + + Some(handle) }) .collect(); @@ -169,9 +175,10 @@ impl Web3Connections { } } + // TODO: now this errors for private rpcs when we disable all! if connections.len() < min_synced_rpcs { return Err(anyhow::anyhow!( - "Only {}/{} connections!", + "Only {}/{} connections! Add more connections or reduce min_synced_rpcs.", connections.len(), min_synced_rpcs ));