stricter configs

This commit is contained in:
Bryan Stitt 2022-10-18 23:27:33 +00:00
parent 69a090522b
commit f6c2d29d0e
9 changed files with 114 additions and 72 deletions

View File

@ -180,8 +180,11 @@ These are roughly in order of completition
- [x] user create script should allow a description field - [x] user create script should allow a description field
- [x] change stats to using the database - [x] change stats to using the database
- [x] emit user stat on retry - [x] emit user stat on retry
- [ ] include if archive query or not in the stats - [x] improve `web3_proxy_cli check_config`
- this is already partially done, but we need to double check it works. preferrably with tests - 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 - [-] 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 - 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): - [-] 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 - allow setting things such as private relay, revert logging, ip/origin/etc checks
- [ ] GET logged reverts on an endpoint that requires authentication. - [ ] GET logged reverts on an endpoint that requires authentication.
- [ ] endpoint for creating/modifying api keys and their advanced security features - [ ] 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 }) - [ ] 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 - 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 - why is it failing to get the block from params when its set to None? That should be the simple case

View File

@ -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

View File

@ -1,9 +1,9 @@
[shared] [shared]
chain_id = 1 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 # TODO: how do we find the optimal db_max_connections? too high actually ends up being slower
db_max_connections = 99 db_max_connections = 99
db_url = "mysql://root:dev_web3_proxy@dev-db:3306/dev_web3_proxy"
min_sum_soft_limit = 2000 min_sum_soft_limit = 2000
min_synced_rpcs = 2 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_public_url = "https://llamanodes.com/free-rpc-stats"
redirect_user_url = "https://llamanodes.com/user-rpc-stats/{{user_id}}" 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 # 1GB of cache
response_cache_max_bytes = 10000000000 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 # 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/" url = "https://api.edennetwork.io/v1/"
soft_limit = 1_805 soft_limit = 1_805
weight = 0 weight = 0
[private_rpcs_off.eden_beta] [private_rpcs.eden_beta]
disabled = true
url = "https://api.edennetwork.io/v1/beta" url = "https://api.edennetwork.io/v1/beta"
soft_limit = 5_861 soft_limit = 5_861
weight = 0 weight = 0
[private_rpcs_off.ethermine] [private_rpcs.ethermine]
disabled = true
url = "https://rpc.ethermine.org" url = "https://rpc.ethermine.org"
soft_limit = 5_861 soft_limit = 5_861
weight = 0 weight = 0
[private_rpcs_off.flashbots] [private_rpcs.flashbots]
disabled = true
url = "https://rpc.flashbots.net/fast" url = "https://rpc.flashbots.net/fast"
soft_limit = 7074 soft_limit = 7074
weight = 0 weight = 0
[private_rpcs_off.securerpc] [private_rpcs.securerpc]
disabled = true
url = "https://gibson.securerpc.com/v1" url = "https://gibson.securerpc.com/v1"
soft_limit = 4560 soft_limit = 4560
weight = 0 weight = 0

View File

@ -349,9 +349,8 @@ impl Web3ProxyApp {
block_map, block_map,
// subscribing to new heads here won't work well. if they are fast, they might be ahead of balanced_rpcs // subscribing to new heads here won't work well. if they are fast, they might be ahead of balanced_rpcs
None, None,
// minimum doesn't really matter on private rpcs 0,
1, 0,
1,
// TODO: subscribe to pending transactions on the private rpcs? they seem to have low rate limits // TODO: subscribe to pending transactions on the private rpcs? they seem to have low rate limits
None, None,
pending_transactions.clone(), pending_transactions.clone(),
@ -376,7 +375,11 @@ impl Web3ProxyApp {
let rpc_rrl = RedisRateLimiter::new( let rpc_rrl = RedisRateLimiter::new(
"web3_proxy", "web3_proxy",
"frontend", "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, 60.0,
redis_pool.clone(), redis_pool.clone(),
); );

View File

@ -215,10 +215,10 @@ mod tests {
let app_config = TopConfig { let app_config = TopConfig {
app: AppConfig { app: AppConfig {
chain_id: 31337, 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_sum_soft_limit: 1,
min_synced_rpcs: 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), response_cache_max_bytes: 10_usize.pow(7),
redirect_public_url: Some("example.com/".to_string()), redirect_public_url: Some("example.com/".to_string()),
redirect_user_url: Some("example.com/{{user_id}}".to_string()), redirect_user_url: Some("example.com/{{user_id}}".to_string()),
@ -227,11 +227,11 @@ mod tests {
balanced_rpcs: HashMap::from([ balanced_rpcs: HashMap::from([
( (
"anvil".to_string(), "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(), "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, private_rpcs: None,

View File

@ -1,13 +1,13 @@
use argh::FromArgs; use argh::FromArgs;
use std::fs; use std::fs;
use tracing::info; use tracing::{info, warn};
use web3_proxy::config::TopConfig; use web3_proxy::config::TopConfig;
#[derive(FromArgs, PartialEq, Eq, Debug)] #[derive(FromArgs, PartialEq, Eq, Debug)]
/// Second subcommand. /// Second subcommand.
#[argh(subcommand, name = "check_config")] #[argh(subcommand, name = "check_config")]
pub struct CheckConfigSubCommand { pub struct CheckConfigSubCommand {
#[argh(option)] #[argh(positional)]
/// path to the configuration toml. /// path to the configuration toml.
path: String, path: String,
} }
@ -18,7 +18,56 @@ impl CheckConfigSubCommand {
let top_config: String = fs::read_to_string(self.path)?; let top_config: String = fs::read_to_string(self.path)?;
let top_config: TopConfig = toml::from_str(&top_config)?; 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(()) Ok(())
} }

View File

@ -38,6 +38,7 @@ pub struct CliConfig {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TopConfig { pub struct TopConfig {
pub app: AppConfig, pub app: AppConfig,
pub balanced_rpcs: HashMap<String, Web3ConnectionConfig>, pub balanced_rpcs: HashMap<String, Web3ConnectionConfig>,
@ -47,6 +48,7 @@ pub struct TopConfig {
/// shared configuration between Web3Connections /// shared configuration between Web3Connections
// TODO: no String, only &str // TODO: no String, only &str
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AppConfig { pub struct AppConfig {
/// EVM chain id. 1 for ETH /// 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 /// 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. /// Default request limit for registered users.
/// 0 = block all requests /// 0 = block all requests
/// None = allow all requests /// None = allow all requests
pub default_requests_per_minute: Option<u64>, pub default_user_requests_per_minute: Option<u64>,
/// Restrict user registration. /// Restrict user registration.
/// None = no code needed /// None = no code needed
pub invite_code: Option<String>, pub invite_code: Option<String>,
@ -74,9 +76,10 @@ pub struct AppConfig {
#[serde(default = "default_min_synced_rpcs")] #[serde(default = "default_min_synced_rpcs")]
pub min_synced_rpcs: usize, pub min_synced_rpcs: usize,
/// Request limit for anonymous users. /// Request limit for anonymous users.
/// Set to 0 to block all anonymous requests. /// Some(0) = block all requests
#[serde(default = "default_frontend_rate_limit_per_minute")] /// None = allow all requests
pub frontend_rate_limit_per_minute: u64, #[serde(default = "default_public_requests_per_minute")]
pub public_requests_per_minute: Option<u64>,
/// Rate limit for the login entrypoint. /// Rate limit for the login entrypoint.
/// This is separate from the rpc limits. /// This is separate from the rpc limits.
#[serde(default = "default_login_rate_limit_per_minute")] #[serde(default = "default_login_rate_limit_per_minute")]
@ -107,8 +110,8 @@ fn default_min_synced_rpcs() -> usize {
} }
/// 0 blocks anonymous requests by default. /// 0 blocks anonymous requests by default.
fn default_frontend_rate_limit_per_minute() -> u64 { fn default_public_requests_per_minute() -> Option<u64> {
0 Some(0)
} }
/// Having a low amount of requests per minute for login is safest. /// 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) 10_usize.pow(8)
} }
/// Configuration for a backend web3 RPC server
#[derive(Debug, Deserialize, Constructor)] #[derive(Debug, Deserialize, Constructor)]
#[serde(deny_unknown_fields)]
pub struct Web3ConnectionConfig { pub struct Web3ConnectionConfig {
/// simple way to disable a connection without deleting the row
#[serde(default)]
pub disabled: bool,
/// websocket (or http if no websocket) /// websocket (or http if no websocket)
url: String, pub url: String,
/// the requests per second at which the server starts slowing down /// 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) /// the requests per second at which the server throws errors (rate limit or otherwise)
hard_limit: Option<u64>, pub hard_limit: Option<u64>,
/// All else equal, a server with a lower weight receives requests /// All else equal, a server with a lower weight receives requests
weight: u32, pub weight: u32,
/// Subscribe to the firehose of pending transactions /// Subscribe to the firehose of pending transactions
/// Don't do this with free rpcs /// Don't do this with free rpcs
subscribe_txs: Option<bool>, #[serde(default)]
pub subscribe_txs: Option<bool>,
} }
impl Web3ConnectionConfig { impl Web3ConnectionConfig {

View File

@ -221,7 +221,7 @@ pub async fn user_login_post(
let uk = user_keys::ActiveModel { let uk = user_keys::ActiveModel {
user_id: sea_orm::Set(u.id), user_id: sea_orm::Set(u.id),
api_key: sea_orm::Set(user_key.into()), 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() ..Default::default()
}; };

View File

@ -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) // 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 let spawn_handles: Vec<_> = server_configs
.into_iter() .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 http_client = http_client.clone();
let redis_pool = redis_pool.clone(); let redis_pool = redis_pool.clone();
let http_interval_sender = http_interval_sender.clone(); let http_interval_sender = http_interval_sender.clone();
@ -128,7 +132,7 @@ impl Web3Connections {
let block_map = block_map.clone(); let block_map = block_map.clone();
let open_request_handle_metrics = open_request_handle_metrics.clone(); let open_request_handle_metrics = open_request_handle_metrics.clone();
tokio::spawn(async move { let handle = tokio::spawn(async move {
server_config server_config
.spawn( .spawn(
server_name, server_name,
@ -142,7 +146,9 @@ impl Web3Connections {
open_request_handle_metrics, open_request_handle_metrics,
) )
.await .await
}) });
Some(handle)
}) })
.collect(); .collect();
@ -169,9 +175,10 @@ impl Web3Connections {
} }
} }
// TODO: now this errors for private rpcs when we disable all!
if connections.len() < min_synced_rpcs { if connections.len() < min_synced_rpcs {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"Only {}/{} connections!", "Only {}/{} connections! Add more connections or reduce min_synced_rpcs.",
connections.len(), connections.len(),
min_synced_rpcs min_synced_rpcs
)); ));