From 8459dcd1f138b33bb9c72f5c44b31a9d98141123 Mon Sep 17 00:00:00 2001 From: Bryan Stitt Date: Sat, 24 Sep 2022 02:47:44 +0000 Subject: [PATCH] let the frontend handle their own cookies --- Cargo.lock | 101 +----------------- TODO.md | 20 ++-- entities/src/user_keys.rs | 2 +- migration/src/m20220921_181610_log_reverts.rs | 6 +- web3_proxy/Cargo.toml | 3 +- web3_proxy/src/app.rs | 12 +-- web3_proxy/src/bin/web3_proxy.rs | 1 + .../src/bin/web3_proxy_cli/create_user.rs | 1 + web3_proxy/src/config.rs | 6 ++ web3_proxy/src/frontend/authorization.rs | 20 ++-- web3_proxy/src/frontend/errors.rs | 4 +- web3_proxy/src/frontend/mod.rs | 3 - web3_proxy/src/frontend/users.rs | 64 +++++------ web3_proxy/src/rpcs/blockchain.rs | 6 +- web3_proxy/src/rpcs/connection.rs | 15 ++- web3_proxy/src/rpcs/connections.rs | 13 ++- web3_proxy/src/rpcs/request.rs | 59 +++++++--- 17 files changed, 138 insertions(+), 198 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f358090..111987f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,15 +27,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "aead" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" -dependencies = [ - "generic-array 0.14.5", -] - [[package]] name = "aes" version = "0.7.5" @@ -48,20 +39,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "aes-gcm" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - [[package]] name = "ahash" version = "0.7.6" @@ -1060,24 +1037,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" -[[package]] -name = "cookie" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" -dependencies = [ - "aes-gcm", - "base64 0.13.0", - "hkdf", - "hmac", - "percent-encoding", - "rand 0.8.5", - "sha2 0.10.2", - "subtle", - "time 0.3.14", - "version_check", -] - [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -2180,16 +2139,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ghash" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" -dependencies = [ - "opaque-debug 0.3.0", - "polyval", -] - [[package]] name = "gimli" version = "0.26.1" @@ -2358,15 +2307,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" -dependencies = [ - "hmac", -] - [[package]] name = "hmac" version = "0.12.1" @@ -3471,18 +3411,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "polyval" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug 0.3.0", - "universal-hash", -] - [[package]] name = "postgres-protocol" version = "0.6.4" @@ -5143,23 +5071,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower-cookies" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19833e336396f3953e5ab1513d72b5e5ea51d5ad39b78d306766a05740b48b97" -dependencies = [ - "async-trait", - "axum-core", - "cookie", - "futures-util", - "http", - "parking_lot 0.12.1", - "pin-project-lite", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-http" version = "0.3.4" @@ -5410,16 +5321,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" -[[package]] -name = "universal-hash" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" -dependencies = [ - "generic-array 0.14.5", - "subtle", -] - [[package]] name = "untrusted" version = "0.7.1" @@ -5625,6 +5526,7 @@ dependencies = [ "axum", "axum-client-ip", "axum-macros", + "chrono", "counter", "dashmap", "deferred-rate-limiter", @@ -5662,7 +5564,6 @@ dependencies = [ "tokio-stream", "toml", "tower", - "tower-cookies", "tower-http", "tower-request-id", "tracing", diff --git a/TODO.md b/TODO.md index 818777d5..29040319 100644 --- a/TODO.md +++ b/TODO.md @@ -157,19 +157,19 @@ These are roughly in order of completition - [x] change user creation script to have a "unlimited requests per minute" flag that sets it to u64::MAX (18446744073709551615) - [x] in /status, block hashes has a lower count than block numbers. how is that possible? - we weren't calling sync. now we are -- [-] opt-in debug mode that inspects responses for reverts and saves the request to the database for the user. - - [-] let them choose a % to log (or maybe x/second). someone like curve logging all reverts will be a BIG database very quickly - - this must be opt-in or spawned since it will slow things down and will make their calls less private +- [x] opt-in debug mode that inspects responses for reverts and saves the request to the database for the user. - [x] Api keys need option to lock to IP, cors header, referer, user agent, etc -- [ ] endpoint for creating/modifying api keys and their advanced security features +- [x] /user/logout to clear bearer token and jwt +- [x] bearer tokens should expire +- [-] user login should return the bearer token, the user keys, and a jwt (jsonwebtoken rust crate should make it easy) +- [-] let users choose a % to log (or maybe x/second). someone like curve logging all reverts will be a BIG database very quickly + - this must be opt-in or spawned since it will slow things down and will make their calls less private + - [ ] we currently default to 0.0 and don't expose a way to edit it. we have a database row, but we don't use it +- [-] add configurable size limits to all the Caches - [ ] active requests per second per api key - [ ] distribution of methods per api key (eth_call, eth_getLogs, etc.) -- [-] add configurable size limits to all the Caches -- [ ] /user/logout to clear bearer token and jwt +- [ ] endpoint for creating/modifying api keys and their advanced security features - [ ] BUG: i think if all backend servers stop, the server doesn't properly reconnect. It appears to stop listening on 8854, but not shut down. -- [ ] bearer tokens should expire -- [-] signed cookie jar -- [ ] user login should return both the bearer token and a jwt (jsonwebtoken rust crate should make it easy) - [ ] revert logs should have a maximum age and a maximum count to keep the database from being huge - [ ] Ulid instead of Uuid for user keys - @@ -177,6 +177,7 @@ These are roughly in order of completition - [ ] Ulid instead of Uuid for database ids - might have to use Uuid in sea-orm and then convert to Ulid on display - [ ] option to rotate api key +- [ ] read the cookie key from a file. easy to re-use and no giant blob of hex in our app config ## V1 @@ -380,3 +381,4 @@ in another repo: event subscriber - if we get a connection refused, we should remove the server's block info so it is taken out of rotation - [ ] web3_proxy_cli command should read database settings from config - [ ] how should we handle reverting transactions? they won't confirm for a while after we send them +- [ ] allow configuration of the expiration time of bearer tokens \ No newline at end of file diff --git a/entities/src/user_keys.rs b/entities/src/user_keys.rs index 19f57650..7d823a4e 100644 --- a/entities/src/user_keys.rs +++ b/entities/src/user_keys.rs @@ -17,7 +17,7 @@ pub struct Model { pub active: bool, pub requests_per_minute: Option, #[sea_orm(column_type = "Decimal(Some((5, 4)))")] - pub log_reverts: Decimal, + pub log_revert_chance: Decimal, #[sea_orm(column_type = "Text", nullable)] pub allowed_ips: Option, #[sea_orm(column_type = "Text", nullable)] diff --git a/migration/src/m20220921_181610_log_reverts.rs b/migration/src/m20220921_181610_log_reverts.rs index 34357929..3cb73b84 100644 --- a/migration/src/m20220921_181610_log_reverts.rs +++ b/migration/src/m20220921_181610_log_reverts.rs @@ -20,7 +20,7 @@ impl MigrationTrait for Migration { ) // add a column for logging reverts in the RevertLogs table .add_column( - ColumnDef::new(UserKeys::LogReverts) + ColumnDef::new(UserKeys::LogRevertChance) .decimal_len(5, 4) .not_null() .default("0.0"), @@ -90,7 +90,7 @@ impl MigrationTrait for Migration { .unsigned() .not_null(), ) - .drop_column(UserKeys::LogReverts) + .drop_column(UserKeys::LogRevertChance) .to_owned(), ) .await @@ -109,7 +109,7 @@ pub enum UserKeys { // PrivateTxs, // Active, RequestsPerMinute, - LogReverts, + LogRevertChance, AllowedIps, AllowedOrigins, AllowedReferers, diff --git a/web3_proxy/Cargo.toml b/web3_proxy/Cargo.toml index 9c8f1962..7676ed54 100644 --- a/web3_proxy/Cargo.toml +++ b/web3_proxy/Cargo.toml @@ -25,6 +25,8 @@ argh = "0.1.9" axum = { version = "0.5.16", features = ["headers", "serde_json", "tokio-tungstenite", "ws"] } axum-client-ip = "0.2.0" axum-macros = "0.2.3" +# TODO: import this from ethorm so we always have the same version +chrono = "0.4.22" counter = "0.5.6" dashmap = "5.4.0" derive_more = "0.99.17" @@ -59,7 +61,6 @@ time = "0.3.14" tokio = { version = "1.21.1", features = ["full", "tracing"] } # TODO: make sure this uuid version matches sea-orm. PR to put this in their prelude tokio-stream = { version = "0.1.10", features = ["sync"] } -tower-cookies = { version = "0.7.0", features = ["private"] } toml = "0.5.9" tower = "0.4.13" tower-request-id = "0.2.0" diff --git a/web3_proxy/src/app.rs b/web3_proxy/src/app.rs index 85b4b54f..51b6866b 100644 --- a/web3_proxy/src/app.rs +++ b/web3_proxy/src/app.rs @@ -29,6 +29,7 @@ use metered::{metered, ErrorCount, HitCount, ResponseTime, Throughput}; use migration::{Migrator, MigratorTrait}; use moka::future::Cache; use redis_rate_limiter::{DeadpoolRuntime, RedisConfig, RedisPool, RedisRateLimiter}; +use sea_orm::prelude::Decimal; use sea_orm::DatabaseConnection; use serde::Serialize; use serde_json::json; @@ -44,7 +45,6 @@ use tokio::sync::{broadcast, watch}; use tokio::task::JoinHandle; use tokio::time::timeout; use tokio_stream::wrappers::{BroadcastStream, WatchStream}; -use tower_cookies::Key; use tracing::{error, info, trace, warn}; use uuid::Uuid; @@ -64,7 +64,7 @@ type ResponseCache = pub type AnyhowJoinHandle = JoinHandle>; -#[derive(Clone, Debug, From)] +#[derive(Clone, Debug, Default, From)] /// TODO: rename this? pub struct UserKeyData { pub user_key_id: u64, @@ -78,6 +78,8 @@ pub struct UserKeyData { pub allowed_user_agents: Option>, /// if None, allow any IP Address pub allowed_ips: Option>, + /// Chance to save reverting eth_call, eth_estimateGas, and eth_sendRawTransaction to the database. + pub log_revert_chance: Decimal, } /// The application @@ -88,8 +90,6 @@ pub struct Web3ProxyApp { pub balanced_rpcs: Arc, /// Send private requests (like eth_sendRawTransaction) to all these servers pub private_rpcs: Option>, - // TODO: this lifetime is definitely wrong - pub cookie_key: Key, response_cache: ResponseCache, // don't drop this or the sender will stop working // TODO: broadcast channel instead? @@ -371,12 +371,8 @@ impl Web3ProxyApp { .time_to_live(Duration::from_secs(60)) .build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::new()); - // TODO: get this from the app's config - let cookie_key = Key::from(&[0; 64]); - let app = Self { config: top_config.app, - cookie_key, balanced_rpcs, private_rpcs, response_cache, diff --git a/web3_proxy/src/bin/web3_proxy.rs b/web3_proxy/src/bin/web3_proxy.rs index dd5a7481..21d00af8 100644 --- a/web3_proxy/src/bin/web3_proxy.rs +++ b/web3_proxy/src/bin/web3_proxy.rs @@ -204,6 +204,7 @@ mod tests { prometheus_port: 0, workers: 4, config: "./does/not/exist/test.toml".to_string(), + cookie_key_filename: "./does/not/exist/development_cookie_key".to_string(), }; // make a test AppConfig diff --git a/web3_proxy/src/bin/web3_proxy_cli/create_user.rs b/web3_proxy/src/bin/web3_proxy_cli/create_user.rs index dc2b8243..45595f4b 100644 --- a/web3_proxy/src/bin/web3_proxy_cli/create_user.rs +++ b/web3_proxy/src/bin/web3_proxy_cli/create_user.rs @@ -36,6 +36,7 @@ impl CreateUserSubCommand { // TODO: would be nice to use the fixed array instead of a Vec in the entities // TODO: how can we use custom types with + // TODO: take a simple String. If it starts with 0x, parse as address. otherwise convert ascii to hex let address = self.address.to_fixed_bytes().into(); let u = user::ActiveModel { diff --git a/web3_proxy/src/config.rs b/web3_proxy/src/config.rs index 823b5bf4..a63f29d9 100644 --- a/web3_proxy/src/config.rs +++ b/web3_proxy/src/config.rs @@ -32,6 +32,10 @@ pub struct CliConfig { /// number of worker threads. Defaults to the number of logical processors #[argh(option, default = "0")] pub workers: usize, + + /// path to a binary file used to encrypt cookies. Should be at least 64 bytes. + #[argh(option, default = "\"./data/development_cookie_key\".to_string()")] + pub cookie_key_filename: String, } #[derive(Debug, Deserialize)] @@ -46,6 +50,8 @@ pub struct TopConfig { pub struct AppConfig { // TODO: better type for chain_id? max of `u64::MAX / 2 - 36` https://github.com/ethereum/EIPs/issues/2294 pub chain_id: u64, + pub cookie_domain: Option, + pub cookie_secure: Option, pub db_url: Option, /// minimum size of the connection pool for the database /// If none, the number of workers are used diff --git a/web3_proxy/src/frontend/authorization.rs b/web3_proxy/src/frontend/authorization.rs index 19c7c41e..2d498358 100644 --- a/web3_proxy/src/frontend/authorization.rs +++ b/web3_proxy/src/frontend/authorization.rs @@ -5,7 +5,7 @@ use axum::headers::{Origin, Referer, UserAgent}; use deferred_rate_limiter::DeferredRateLimitResult; use entities::user_keys; use ipnet::IpNet; -use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; +use sea_orm::{prelude::Decimal, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; use serde::Serialize; use std::{net::IpAddr, sync::Arc}; use tokio::time::Instant; @@ -28,9 +28,10 @@ pub enum RateLimitResult { #[derive(Debug, Serialize)] pub struct AuthorizedKey { - ip: IpAddr, - origin: Option, - user_key_id: u64, + pub ip: IpAddr, + pub origin: Option, + pub user_key_id: u64, + pub log_revert_chance: Decimal, // TODO: what else? } @@ -96,6 +97,7 @@ impl AuthorizedKey { ip, origin, user_key_id: user_data.user_key_id, + log_revert_chance: user_data.log_revert_chance, }) } } @@ -269,16 +271,10 @@ impl Web3ProxyApp { allowed_origins, allowed_referers, allowed_user_agents, + log_revert_chance: user_key_model.log_revert_chance, }) } - None => Ok(UserKeyData { - user_key_id: 0, - user_max_requests_per_period: Some(0), - allowed_ips: None, - allowed_origins: None, - allowed_referers: None, - allowed_user_agents: None, - }), + None => Ok(UserKeyData::default()), } }) .await; diff --git a/web3_proxy/src/frontend/errors.rs b/web3_proxy/src/frontend/errors.rs index b372ab64..2ac5e832 100644 --- a/web3_proxy/src/frontend/errors.rs +++ b/web3_proxy/src/frontend/errors.rs @@ -81,7 +81,7 @@ impl IntoResponse for FrontendErrorResponse { ), ) } - Self::RateLimitedIp(ip, retry_at) => { + Self::RateLimitedIp(ip, _retry_at) => { // TODO: emit a stat // TODO: include retry_at in the error // TODO: if retry_at is None, give an unauthorized status code? @@ -95,7 +95,7 @@ impl IntoResponse for FrontendErrorResponse { ) } // TODO: this should actually by the id of the key. multiple users might control one key - Self::RateLimitedUser(user_data, retry_at) => { + Self::RateLimitedUser(user_data, _retry_at) => { // TODO: emit a stat // TODO: include retry_at in the error ( diff --git a/web3_proxy/src/frontend/mod.rs b/web3_proxy/src/frontend/mod.rs index a1f09bb7..6f4b2213 100644 --- a/web3_proxy/src/frontend/mod.rs +++ b/web3_proxy/src/frontend/mod.rs @@ -15,7 +15,6 @@ use axum::{ }; use std::net::SocketAddr; use std::sync::Arc; -use tower_cookies::CookieManagerLayer; use tower_http::trace::TraceLayer; use tower_request_id::{RequestId, RequestIdLayer}; use tracing::{error_span, info}; @@ -70,8 +69,6 @@ pub async fn serve(port: u16, proxy_app: Arc) -> anyhow::Result<() .layer(request_tracing_layer) // create a unique id for each request .layer(RequestIdLayer) - // signed cookies - .layer(CookieManagerLayer::new()) // 404 for any unknown routes .fallback(errors::handler_404.into_service()); diff --git a/web3_proxy/src/frontend/users.rs b/web3_proxy/src/frontend/users.rs index 2d5c209a..287148b6 100644 --- a/web3_proxy/src/frontend/users.rs +++ b/web3_proxy/src/frontend/users.rs @@ -30,7 +30,6 @@ use siwe::Message; use std::ops::Add; use std::sync::Arc; use time::{Duration, OffsetDateTime}; -use tower_cookies::Cookies; use ulid::Ulid; use uuid::Uuid; @@ -130,15 +129,16 @@ pub struct PostLogin { // signer: String, } +/// TODO: what information should we return? #[derive(Serialize)] pub struct PostLoginResponse { bearer_token: Ulid, - // TODO: change this Ulid - api_key: Uuid, + api_keys: Vec, } -#[debug_handler] /// Post to the user endpoint to register or login. +/// It is recommended to save the returned bearer this in a cookie and send bac +#[debug_handler] pub async fn post_login( Extension(app): Extension>, ClientIp(ip): ClientIp, @@ -183,7 +183,7 @@ pub async fn post_login( .await .unwrap(); - let (u, uk, response) = match u { + let (u, _uks, response) = match u { None => { let txn = db.begin().await?; @@ -209,71 +209,65 @@ pub async fn post_login( .await .context("Failed saving new user key")?; + let uks = vec![uk]; + txn.commit().await?; let response_json = PostLoginResponse { bearer_token, - api_key: uk.api_key, + api_keys: uks.iter().map(|uk| uk.api_key).collect(), }; let response = (StatusCode::CREATED, Json(response_json)).into_response(); - (u, uk, response) + (u, uks, response) } Some(u) => { // the user is already registered - // TODO: what if the user has multiple keys? - let uk = user_keys::Entity::find() + let uks = user_keys::Entity::find() .filter(user_keys::Column::UserId.eq(u.id)) - .one(db) + .all(db) .await - .context("failed loading user's key")? - .unwrap(); + .context("failed loading user's key")?; let response_json = PostLoginResponse { bearer_token, - api_key: uk.api_key, + api_keys: uks.iter().map(|uk| uk.api_key).collect(), }; let response = (StatusCode::OK, Json(response_json)).into_response(); - (u, uk, response) + (u, uks, response) } }; - // TODO: set a session cookie with the bearer token? - - // save the bearer token in redis with a long (7 or 30 day?) expiry. or in database? + // add bearer to redis let mut redis_conn = app.redis_conn().await?; - // TODO: move this into a struct so this is less fragile - let bearer_key = format!("bearer:{}", bearer_token); + let bearer_redis_key = format!("bearer:{}", bearer_token); - redis_conn.set(bearer_key, u.id.to_string()).await?; - - // TODO: save user_data. we already have uk, so this could be more efficient. it works for now + // expire in 4 weeks + // TODO: get expiration time from app config + // TODO: do we use this? + redis_conn + .set_ex(bearer_redis_key, u.id.to_string(), 2_419_200) + .await?; Ok(response) } +/// Log out the user connected to the given Authentication header. #[debug_handler] pub async fn get_logout( - cookies: Cookies, Extension(app): Extension>, + TypedHeader(Authorization(bearer)): TypedHeader>, ) -> FrontendResult { - // delete the cookie if it exists - let private_cookies = cookies.private(&app.cookie_key); + // TODO: i don't like this. move this to a helper function so it is less fragile + let bearer_cache_key = format!("bearer:{}", bearer.token()); - if let Some(c) = private_cookies.get("bearer") { - let bearer_cache_key = format!("bearer:{}", c.value()); + let mut redis_conn = app.redis_conn().await?; - // TODO: should deleting the cookie be last? redis being down shouldn't block the user - private_cookies.remove(c); - - let mut redis_conn = app.redis_conn().await?; - - redis_conn.del(bearer_cache_key).await?; - } + redis_conn.del(bearer_cache_key).await?; // TODO: what should the response be? probably json something Ok("goodbye".into_response()) @@ -348,8 +342,6 @@ impl ProtectedAction { // TODO: is this type correct? let u_id: Option = redis_conn.get(bearer_cache_key).await?; - // TODO: if not in redis, check the db? - // TODO: if auth_address == primary_address, allow // TODO: if auth_address != primary_address, only allow if they are a secondary user with the correct role todo!("verify token for the given user"); diff --git a/web3_proxy/src/rpcs/blockchain.rs b/web3_proxy/src/rpcs/blockchain.rs index 5351b1dd..af066496 100644 --- a/web3_proxy/src/rpcs/blockchain.rs +++ b/web3_proxy/src/rpcs/blockchain.rs @@ -108,7 +108,11 @@ impl Web3Connections { Some(rpc) => { rpc.wait_for_request_handle(authorization, Duration::from_secs(30)) .await? - .request("eth_getBlockByHash", &get_block_params, Level::ERROR.into()) + .request( + "eth_getBlockByHash", + &json!(get_block_params), + Level::ERROR.into(), + ) .await? } None => { diff --git a/web3_proxy/src/rpcs/connection.rs b/web3_proxy/src/rpcs/connection.rs index 84b7bb64..8912f98a 100644 --- a/web3_proxy/src/rpcs/connection.rs +++ b/web3_proxy/src/rpcs/connection.rs @@ -15,6 +15,7 @@ use redis_rate_limiter::{RedisPool, RedisRateLimitResult, RedisRateLimiter}; use sea_orm::DatabaseConnection; use serde::ser::{SerializeStruct, Serializer}; use serde::Serialize; +use serde_json::json; use std::cmp::min; use std::fmt; use std::hash::{Hash, Hasher}; @@ -121,7 +122,11 @@ impl Web3Connection { let found_chain_id: Result = new_connection .wait_for_request_handle(None, Duration::from_secs(30)) .await? - .request("eth_chainId", &Option::None::<()>, Level::ERROR.into()) + .request( + "eth_chainId", + &json!(Option::None::<()>), + Level::ERROR.into(), + ) .await; match found_chain_id { @@ -209,10 +214,10 @@ impl Web3Connection { .await? .request( "eth_getCode", - &( + &json!(( "0xdead00000000000000000000000000000000beef", maybe_archive_block, - ), + )), // error here are expected, so keep the level low tracing::Level::DEBUG.into(), ) @@ -543,7 +548,7 @@ impl Web3Connection { let block: Result, _> = active_request_handle .request( "eth_getBlockByNumber", - &("latest", false), + &json!(("latest", false)), tracing::Level::ERROR.into(), ) .await; @@ -619,7 +624,7 @@ impl Web3Connection { .await? .request( "eth_getBlockByNumber", - &("latest", false), + &json!(("latest", false)), tracing::Level::ERROR.into(), ) .await diff --git a/web3_proxy/src/rpcs/connections.rs b/web3_proxy/src/rpcs/connections.rs index 45b22474..04fa45a5 100644 --- a/web3_proxy/src/rpcs/connections.rs +++ b/web3_proxy/src/rpcs/connections.rs @@ -23,6 +23,7 @@ use petgraph::graphmap::DiGraphMap; use sea_orm::DatabaseConnection; use serde::ser::{SerializeStruct, Serializer}; use serde::Serialize; +use serde_json::json; use serde_json::value::RawValue; use std::cmp; use std::cmp::Reverse; @@ -320,7 +321,11 @@ impl Web3Connections { .into_iter() .map(|active_request_handle| async move { let result: Result, _> = active_request_handle - .request(method, ¶ms.cloned(), tracing::Level::ERROR.into()) + .request( + method, + &json!(params.cloned()), + tracing::Level::ERROR.into(), + ) .await; result }) @@ -517,12 +522,12 @@ impl Web3Connections { // save the rpc in case we get an error and want to retry on another server skip_rpcs.push(active_request_handle.clone_connection()); - // TODO: get the log percent from the user data? + // TODO: get the log percent from the user data let response_result = active_request_handle .request( &request.method, - &request.params, - RequestErrorHandler::SaveReverts(100.0), + &json!(request.params), + RequestErrorHandler::SaveReverts(0.0), ) .await; diff --git a/web3_proxy/src/rpcs/request.rs b/web3_proxy/src/rpcs/request.rs index f81f021d..cdd47a01 100644 --- a/web3_proxy/src/rpcs/request.rs +++ b/web3_proxy/src/rpcs/request.rs @@ -3,12 +3,17 @@ use super::provider::Web3Provider; use crate::frontend::authorization::AuthorizedRequest; use crate::metered::{JsonRpcErrorCount, ProviderErrorCount}; use anyhow::Context; +use chrono::Utc; +use entities::revert_logs; +use entities::sea_orm_active_enums::Method; use ethers::providers::{HttpClientError, ProviderError, WsClientError}; use metered::metered; use metered::HitCount; use metered::ResponseTime; use metered::Throughput; use rand::Rng; +use sea_orm::ActiveModelTrait; +use serde_json::json; use std::fmt; use std::sync::atomic::{self, AtomicBool, Ordering}; use std::sync::Arc; @@ -47,6 +52,15 @@ pub enum RequestErrorHandler { WarnLevel, } +#[derive(serde::Deserialize, serde::Serialize)] +struct EthCallParams { + method: Method, + // TODO: do this as Address instead + to: Vec, + // TODO: do this as a Bytes instead + data: String, +} + impl From for RequestErrorHandler { fn from(level: Level) -> Self { match level { @@ -60,13 +74,31 @@ impl From for RequestErrorHandler { impl AuthorizedRequest { /// Save a RPC call that return "execution reverted" to the database. - async fn save_revert(self: Arc, method: String, params: T) -> anyhow::Result<()> - where - T: Clone + fmt::Debug + serde::Serialize + Send + Sync + 'static, - { - let db_conn = self.db_conn().context("db_conn needed to save reverts")?; + async fn save_revert(self: Arc, params: EthCallParams) -> anyhow::Result<()> { + if let Self::User(Some(db_conn), authorized_request) = &*self { + // TODO: do this on the database side? + let timestamp = Utc::now(); - todo!("save the revert to the database"); + let rl = revert_logs::ActiveModel { + user_key_id: sea_orm::Set(authorized_request.user_key_id), + method: sea_orm::Set(params.method), + to: sea_orm::Set(params.to), + call_data: sea_orm::Set(params.data), + timestamp: sea_orm::Set(timestamp), + ..Default::default() + }; + + let rl = rl + .save(db_conn) + .await + .context("Failed saving new revert log")?; + + // TODO: what log level? + trace!(?rl); + } + + // TODO: return something useful + Ok(()) } } @@ -110,15 +142,15 @@ impl OpenRequestHandle { /// TODO: we no longer take self because metered doesn't like that /// TODO: ErrorCount includes too many types of errors, such as transaction reverts #[measure([JsonRpcErrorCount, HitCount, ProviderErrorCount, ResponseTime, Throughput])] - pub async fn request( + pub async fn request( &self, method: &str, - params: &T, + params: &P, error_handler: RequestErrorHandler, ) -> Result where // TODO: not sure about this type. would be better to not need clones, but measure and spawns combine to need it - T: Clone + fmt::Debug + serde::Serialize + Send + Sync + 'static, + P: Clone + fmt::Debug + serde::Serialize + Send + Sync + 'static, R: serde::Serialize + serde::de::DeserializeOwned + fmt::Debug, { // ensure this function only runs once @@ -212,11 +244,12 @@ impl OpenRequestHandle { if let Some(msg) = msg { if msg.starts_with("execution reverted") { + // TODO: is there a more efficient way to do this? + let params: EthCallParams = serde_json::from_value(json!(params)) + .expect("parsing eth_call"); + // spawn saving to the database so we don't slow down the request (or error if no db) - let f = self - .authorization - .clone() - .save_revert(method.to_string(), params.clone()); + let f = self.authorization.clone().save_revert(params); tokio::spawn(async move { f.await }); } else {