cache db data in a map
This commit is contained in:
parent
7802d9b6f7
commit
80a3c74120
21
Cargo.lock
generated
21
Cargo.lock
generated
@ -335,9 +335,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.5.14"
|
version = "0.5.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c943a505c17b494638a38a9af129067f760c9c06794b9f57d499266909be8e72"
|
checksum = "9de18bc5f2e9df8f52da03856bf40e29b747de5a84e43aefff90e3dc4a21529b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
@ -1769,6 +1769,13 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fifomap"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"linkedhashmap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filetime"
|
name = "filetime"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@ -4023,9 +4030,9 @@ checksum = "930c0acf610d3fdb5e2ab6213019aaa04e227ebe9547b0649ba599b16d788bd7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.142"
|
version = "1.0.143"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e590c437916fb6b221e1d00df6e3294f3fccd70ca7e92541c475d6ed6ef5fee2"
|
checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
@ -4052,9 +4059,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.142"
|
version = "1.0.143"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34b5b8d809babe02f538c2cfec6f2c1ed10804c0e5a6a041a049a4f5588ccc2e"
|
checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -5209,12 +5216,12 @@ dependencies = [
|
|||||||
"entities",
|
"entities",
|
||||||
"ethers",
|
"ethers",
|
||||||
"fdlimit",
|
"fdlimit",
|
||||||
|
"fifomap",
|
||||||
"flume",
|
"flume",
|
||||||
"fstrings",
|
"fstrings",
|
||||||
"futures",
|
"futures",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"linkedhashmap",
|
|
||||||
"migration",
|
"migration",
|
||||||
"notify",
|
"notify",
|
||||||
"num",
|
"num",
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
members = [
|
members = [
|
||||||
"entities",
|
"entities",
|
||||||
"migration",
|
"migration",
|
||||||
|
"fifomap",
|
||||||
"linkedhashmap",
|
"linkedhashmap",
|
||||||
"redis-cell-client",
|
"redis-cell-client",
|
||||||
"web3_proxy",
|
"web3_proxy",
|
||||||
|
1
TODO.md
1
TODO.md
@ -64,6 +64,7 @@
|
|||||||
- [x] refactor result type on active handlers to use a cleaner success/error so we can use the try operator
|
- [x] refactor result type on active handlers to use a cleaner success/error so we can use the try operator
|
||||||
- [x] give users different rate limits looked up from the database
|
- [x] give users different rate limits looked up from the database
|
||||||
- [x] Add a "weight" key to the servers. Sort on that after block. keep most requests local
|
- [x] Add a "weight" key to the servers. Sort on that after block. keep most requests local
|
||||||
|
- [ ] cache db query results for user data. db is a big bottleneck right now
|
||||||
- [ ] allow blocking public requests
|
- [ ] allow blocking public requests
|
||||||
- [ ] use siwe messages and signatures for sign up and login
|
- [ ] use siwe messages and signatures for sign up and login
|
||||||
- [ ] basic request method stats
|
- [ ] basic request method stats
|
||||||
|
@ -3,7 +3,7 @@ services:
|
|||||||
# TODO: build in dev but use docker hub in prod?
|
# TODO: build in dev but use docker hub in prod?
|
||||||
build: .
|
build: .
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: --config /config.toml --workers 8
|
command: --config /config.toml --workers 32
|
||||||
environment:
|
environment:
|
||||||
#RUST_LOG: "info,web3_proxy=debug"
|
#RUST_LOG: "info,web3_proxy=debug"
|
||||||
RUST_LOG: info
|
RUST_LOG: info
|
||||||
|
@ -11,5 +11,5 @@ path = "src/mod.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sea-orm = "0.9.1"
|
sea-orm = "0.9.1"
|
||||||
serde = "1.0.142"
|
serde = "1.0.143"
|
||||||
uuid = "1.1.2"
|
uuid = "1.1.2"
|
||||||
|
9
fifomap/Cargo.toml
Normal file
9
fifomap/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "fifomap"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
linkedhashmap = { path = "../linkedhashmap", features = ["inline-more"] }
|
58
fifomap/src/fifo_count_map.rs
Normal file
58
fifomap/src/fifo_count_map.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use linkedhashmap::LinkedHashMap;
|
||||||
|
use std::{
|
||||||
|
borrow::Borrow,
|
||||||
|
collections::hash_map::RandomState,
|
||||||
|
hash::{BuildHasher, Hash},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct FifoCountMap<K, V, S = RandomState>
|
||||||
|
where
|
||||||
|
K: Hash + Eq + Clone,
|
||||||
|
S: BuildHasher + Default,
|
||||||
|
{
|
||||||
|
/// size limit for the map
|
||||||
|
max_count: usize,
|
||||||
|
/// FIFO
|
||||||
|
map: LinkedHashMap<K, V, S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V, S> FifoCountMap<K, V, S>
|
||||||
|
where
|
||||||
|
K: Hash + Eq + Clone,
|
||||||
|
S: BuildHasher + Default,
|
||||||
|
{
|
||||||
|
pub fn new(max_count: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
max_count,
|
||||||
|
map: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V, S> FifoCountMap<K, V, S>
|
||||||
|
where
|
||||||
|
K: Hash + Eq + Clone,
|
||||||
|
S: BuildHasher + Default,
|
||||||
|
{
|
||||||
|
/// if the size is larger than `self.max_size_bytes`, drop items (first in, first out)
|
||||||
|
/// no item is allowed to take more than `1/max_share` of the cache
|
||||||
|
pub fn insert(&mut self, key: K, value: V) {
|
||||||
|
// drop items until the cache has enough room for the new data
|
||||||
|
// TODO: this probably has wildly variable timings
|
||||||
|
if self.map.len() > self.max_count {
|
||||||
|
// TODO: this isn't an LRU. it's a "least recently created". does that have a fancy name? should we make it an lru? these caches only live for one block
|
||||||
|
self.map.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.map.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get an item from the cache, or None
|
||||||
|
pub fn get<Q>(&self, key: &Q) -> Option<&V>
|
||||||
|
where
|
||||||
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq,
|
||||||
|
{
|
||||||
|
self.map.get(key)
|
||||||
|
}
|
||||||
|
}
|
92
fifomap/src/fifo_sized_map.rs
Normal file
92
fifomap/src/fifo_sized_map.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use linkedhashmap::LinkedHashMap;
|
||||||
|
use std::{
|
||||||
|
borrow::Borrow,
|
||||||
|
collections::hash_map::RandomState,
|
||||||
|
hash::{BuildHasher, Hash},
|
||||||
|
mem::size_of_val,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: if values have wildly different sizes, this is good. but if they are all about the same, this could be simpler
|
||||||
|
pub struct FifoSizedMap<K, V, S = RandomState>
|
||||||
|
where
|
||||||
|
K: Hash + Eq + Clone,
|
||||||
|
S: BuildHasher + Default,
|
||||||
|
{
|
||||||
|
/// size limit in bytes for the map
|
||||||
|
max_size_bytes: usize,
|
||||||
|
/// size limit in bytes for a single item in the map
|
||||||
|
max_item_bytes: usize,
|
||||||
|
/// FIFO
|
||||||
|
map: LinkedHashMap<K, V, S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V, S> FifoSizedMap<K, V, S>
|
||||||
|
where
|
||||||
|
K: Hash + Eq + Clone,
|
||||||
|
S: BuildHasher + Default,
|
||||||
|
{
|
||||||
|
pub fn new(max_size_bytes: usize, max_share: usize) -> Self {
|
||||||
|
let max_item_bytes = max_size_bytes / max_share;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
max_size_bytes,
|
||||||
|
max_item_bytes,
|
||||||
|
map: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V, S> Default for FifoSizedMap<K, V, S>
|
||||||
|
where
|
||||||
|
K: Hash + Eq + Clone,
|
||||||
|
S: BuildHasher + Default,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(
|
||||||
|
// 100 MB default cache
|
||||||
|
100_000_000,
|
||||||
|
// items cannot take more than 1% of the cache
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V, S> FifoSizedMap<K, V, S>
|
||||||
|
where
|
||||||
|
K: Hash + Eq + Clone,
|
||||||
|
S: BuildHasher + Default,
|
||||||
|
{
|
||||||
|
/// if the size is larger than `self.max_size_bytes`, drop items (first in, first out)
|
||||||
|
/// no item is allowed to take more than `1/max_share` of the cache
|
||||||
|
pub fn insert(&mut self, key: K, value: V) -> bool {
|
||||||
|
// TODO: this might be too naive. not sure how much overhead the object has
|
||||||
|
let new_size = size_of_val(&key) + size_of_val(&value);
|
||||||
|
|
||||||
|
// no item is allowed to take more than 1% of the cache
|
||||||
|
// TODO: get this from config?
|
||||||
|
// TODO: trace logging
|
||||||
|
if new_size > self.max_item_bytes {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// drop items until the cache has enough room for the new data
|
||||||
|
// TODO: this probably has wildly variable timings
|
||||||
|
while size_of_val(&self.map) + new_size > self.max_size_bytes {
|
||||||
|
// TODO: this isn't an LRU. it's a "least recently created". does that have a fancy name? should we make it an lru? these caches only live for one block
|
||||||
|
self.map.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.map.insert(key, value);
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get an item from the cache, or None
|
||||||
|
pub fn get<Q>(&self, key: &Q) -> Option<&V>
|
||||||
|
where
|
||||||
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq,
|
||||||
|
{
|
||||||
|
self.map.get(key)
|
||||||
|
}
|
||||||
|
}
|
5
fifomap/src/lib.rs
Normal file
5
fifomap/src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mod fifo_count_map;
|
||||||
|
mod fifo_sized_map;
|
||||||
|
|
||||||
|
pub use fifo_count_map::FifoCountMap;
|
||||||
|
pub use fifo_sized_map::FifoSizedMap;
|
@ -8,10 +8,10 @@ pub use bb8_redis::{bb8, RedisConnectionManager};
|
|||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
pub type RedisClientPool = bb8::Pool<RedisConnectionManager>;
|
pub type RedisPool = bb8::Pool<RedisConnectionManager>;
|
||||||
|
|
||||||
pub struct RedisCellClient {
|
pub struct RedisCell {
|
||||||
pool: RedisClientPool,
|
pool: RedisPool,
|
||||||
key: String,
|
key: String,
|
||||||
default_max_burst: u32,
|
default_max_burst: u32,
|
||||||
default_count_per_period: u32,
|
default_count_per_period: u32,
|
||||||
@ -23,12 +23,12 @@ pub enum ThrottleResult {
|
|||||||
RetryAt(Instant),
|
RetryAt(Instant),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RedisCellClient {
|
impl RedisCell {
|
||||||
// todo: seems like this could be derived
|
// todo: seems like this could be derived
|
||||||
// TODO: take something generic for conn
|
// TODO: take something generic for conn
|
||||||
// TODO: use r2d2 for connection pooling?
|
// TODO: use r2d2 for connection pooling?
|
||||||
pub fn new(
|
pub fn new(
|
||||||
pool: RedisClientPool,
|
pool: RedisPool,
|
||||||
app: &str,
|
app: &str,
|
||||||
key: &str,
|
key: &str,
|
||||||
default_max_burst: u32,
|
default_max_burst: u32,
|
||||||
|
@ -19,7 +19,7 @@ migration = { path = "../migration" }
|
|||||||
anyhow = { version = "1.0.60", features = ["backtrace"] }
|
anyhow = { version = "1.0.60", features = ["backtrace"] }
|
||||||
arc-swap = "1.5.1"
|
arc-swap = "1.5.1"
|
||||||
argh = "0.1.8"
|
argh = "0.1.8"
|
||||||
axum = { version = "0.5.13", features = ["serde_json", "tokio-tungstenite", "ws"] }
|
axum = { version = "0.5.15", features = ["serde_json", "tokio-tungstenite", "ws"] }
|
||||||
axum-client-ip = "0.2.0"
|
axum-client-ip = "0.2.0"
|
||||||
counter = "0.5.6"
|
counter = "0.5.6"
|
||||||
dashmap = "5.3.4"
|
dashmap = "5.3.4"
|
||||||
@ -32,7 +32,7 @@ futures = { version = "0.3.21", features = ["thread-pool"] }
|
|||||||
fstrings = "0.2.3"
|
fstrings = "0.2.3"
|
||||||
hashbrown = { version = "0.12.3", features = ["serde"] }
|
hashbrown = { version = "0.12.3", features = ["serde"] }
|
||||||
indexmap = "1.9.1"
|
indexmap = "1.9.1"
|
||||||
linkedhashmap = { path = "../linkedhashmap", features = ["inline-more"] }
|
fifomap = { path = "../fifomap" }
|
||||||
notify = "4.0.17"
|
notify = "4.0.17"
|
||||||
num = "0.4.0"
|
num = "0.4.0"
|
||||||
parking_lot = { version = "0.12.1", features = ["arc_lock"] }
|
parking_lot = { version = "0.12.1", features = ["arc_lock"] }
|
||||||
@ -45,7 +45,7 @@ reqwest = { version = "0.11.11", default-features = false, features = ["json", "
|
|||||||
rustc-hash = "1.1.0"
|
rustc-hash = "1.1.0"
|
||||||
siwe = "0.4.1"
|
siwe = "0.4.1"
|
||||||
sea-orm = { version = "0.9.1", features = ["macros"] }
|
sea-orm = { version = "0.9.1", features = ["macros"] }
|
||||||
serde = { version = "1.0.142", features = [] }
|
serde = { version = "1.0.143", features = [] }
|
||||||
serde_json = { version = "1.0.83", default-features = false, features = ["alloc", "raw_value"] }
|
serde_json = { version = "1.0.83", default-features = false, features = ["alloc", "raw_value"] }
|
||||||
tokio = { version = "1.20.1", features = ["full", "tracing"] }
|
tokio = { version = "1.20.1", features = ["full", "tracing"] }
|
||||||
# TODO: make sure this uuid version matches what is in sea orm. PR on sea orm to put builder into prelude
|
# TODO: make sure this uuid version matches what is in sea orm. PR on sea orm to put builder into prelude
|
||||||
|
@ -1,35 +1,40 @@
|
|||||||
|
// TODO: this file is way too big now. move things into other modules
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use axum::extract::ws::Message;
|
use axum::extract::ws::Message;
|
||||||
use dashmap::mapref::entry::Entry as DashMapEntry;
|
use dashmap::mapref::entry::Entry as DashMapEntry;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
|
use derive_more::From;
|
||||||
use ethers::core::utils::keccak256;
|
use ethers::core::utils::keccak256;
|
||||||
use ethers::prelude::{Address, Block, BlockNumber, Bytes, Transaction, TxHash, H256, U64};
|
use ethers::prelude::{Address, Block, Bytes, Transaction, TxHash, H256, U64};
|
||||||
|
use fifomap::{FifoCountMap, FifoSizedMap};
|
||||||
use futures::future::Abortable;
|
use futures::future::Abortable;
|
||||||
use futures::future::{join_all, AbortHandle};
|
use futures::future::{join_all, AbortHandle};
|
||||||
use futures::stream::FuturesUnordered;
|
use futures::stream::FuturesUnordered;
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use linkedhashmap::LinkedHashMap;
|
|
||||||
use migration::{Migrator, MigratorTrait};
|
use migration::{Migrator, MigratorTrait};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use redis_cell_client::bb8::ErrorSink;
|
use redis_cell_client::bb8::ErrorSink;
|
||||||
use redis_cell_client::{bb8, RedisCellClient, RedisConnectionManager};
|
use redis_cell_client::{bb8, RedisCell, RedisConnectionManager, RedisPool};
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::mem::size_of_val;
|
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::atomic::{self, AtomicUsize};
|
use std::sync::atomic::{self, AtomicUsize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::sync::RwLock as AsyncRwLock;
|
||||||
use tokio::sync::{broadcast, watch};
|
use tokio::sync::{broadcast, watch};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio::time::timeout;
|
use tokio::time::{timeout, Instant};
|
||||||
use tokio_stream::wrappers::{BroadcastStream, WatchStream};
|
use tokio_stream::wrappers::{BroadcastStream, WatchStream};
|
||||||
use tracing::{info, info_span, instrument, trace, warn, Instrument};
|
use tracing::{info, info_span, instrument, trace, warn, Instrument};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::bb8_helpers;
|
use crate::bb8_helpers;
|
||||||
|
use crate::block_helpers::block_needed;
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::connections::Web3Connections;
|
use crate::connections::Web3Connections;
|
||||||
use crate::jsonrpc::JsonRpcForwardedResponse;
|
use crate::jsonrpc::JsonRpcForwardedResponse;
|
||||||
@ -48,14 +53,14 @@ static APP_USER_AGENT: &str = concat!(
|
|||||||
// block hash, method, params
|
// block hash, method, params
|
||||||
type CacheKey = (H256, String, Option<String>);
|
type CacheKey = (H256, String, Option<String>);
|
||||||
|
|
||||||
// TODO: make something more advanced that keeps track of cache size in bytes
|
type ResponseLrcCache = RwLock<FifoSizedMap<CacheKey, JsonRpcForwardedResponse>>;
|
||||||
type ResponseLrcCache = RwLock<LinkedHashMap<CacheKey, JsonRpcForwardedResponse>>;
|
|
||||||
|
|
||||||
type ActiveRequestsMap = DashMap<CacheKey, watch::Receiver<bool>>;
|
type ActiveRequestsMap = DashMap<CacheKey, watch::Receiver<bool>>;
|
||||||
|
|
||||||
pub type AnyhowJoinHandle<T> = JoinHandle<anyhow::Result<T>>;
|
pub type AnyhowJoinHandle<T> = JoinHandle<anyhow::Result<T>>;
|
||||||
|
|
||||||
/// flatten a JoinError into an anyhow error
|
/// flatten a JoinError into an anyhow error
|
||||||
|
/// Useful when joining multiple futures.
|
||||||
pub async fn flatten_handle<T>(handle: AnyhowJoinHandle<T>) -> anyhow::Result<T> {
|
pub async fn flatten_handle<T>(handle: AnyhowJoinHandle<T>) -> anyhow::Result<T> {
|
||||||
match handle.await {
|
match handle.await {
|
||||||
Ok(Ok(result)) => Ok(result),
|
Ok(Ok(result)) => Ok(result),
|
||||||
@ -79,173 +84,17 @@ pub async fn flatten_handles<T>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn block_num_to_u64(block_num: BlockNumber, latest_block: U64) -> (bool, U64) {
|
/// Connect to the database and run migrations
|
||||||
match block_num {
|
|
||||||
BlockNumber::Earliest => (false, U64::zero()),
|
|
||||||
BlockNumber::Latest => {
|
|
||||||
// change "latest" to a number
|
|
||||||
(true, latest_block)
|
|
||||||
}
|
|
||||||
BlockNumber::Number(x) => (false, x),
|
|
||||||
// TODO: think more about how to handle Pending
|
|
||||||
BlockNumber::Pending => (false, latest_block),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clean_block_number(
|
|
||||||
params: &mut serde_json::Value,
|
|
||||||
block_param_id: usize,
|
|
||||||
latest_block: U64,
|
|
||||||
) -> anyhow::Result<U64> {
|
|
||||||
match params.as_array_mut() {
|
|
||||||
None => Err(anyhow::anyhow!("params not an array")),
|
|
||||||
Some(params) => match params.get_mut(block_param_id) {
|
|
||||||
None => {
|
|
||||||
if params.len() != block_param_id - 1 {
|
|
||||||
return Err(anyhow::anyhow!("unexpected params length"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the latest block number to the end of the params
|
|
||||||
params.push(serde_json::to_value(latest_block)?);
|
|
||||||
|
|
||||||
Ok(latest_block)
|
|
||||||
}
|
|
||||||
Some(x) => {
|
|
||||||
// convert the json value to a BlockNumber
|
|
||||||
let block_num: BlockNumber = serde_json::from_value(x.clone())?;
|
|
||||||
|
|
||||||
let (modified, block_num) = block_num_to_u64(block_num, latest_block);
|
|
||||||
|
|
||||||
// if we changed "latest" to a number, update the params to match
|
|
||||||
if modified {
|
|
||||||
*x = serde_json::to_value(block_num)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(block_num)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: change this to return also return the hash needed
|
|
||||||
fn block_needed(
|
|
||||||
method: &str,
|
|
||||||
params: Option<&mut serde_json::Value>,
|
|
||||||
head_block: U64,
|
|
||||||
) -> Option<U64> {
|
|
||||||
let params = params?;
|
|
||||||
|
|
||||||
// TODO: double check these. i think some of the getBlock stuff will never need archive
|
|
||||||
let block_param_id = match method {
|
|
||||||
"eth_call" => 1,
|
|
||||||
"eth_estimateGas" => 1,
|
|
||||||
"eth_getBalance" => 1,
|
|
||||||
"eth_getBlockByHash" => {
|
|
||||||
// TODO: double check that any node can serve this
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
"eth_getBlockByNumber" => {
|
|
||||||
// TODO: double check that any node can serve this
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
"eth_getBlockTransactionCountByHash" => {
|
|
||||||
// TODO: double check that any node can serve this
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
"eth_getBlockTransactionCountByNumber" => 0,
|
|
||||||
"eth_getCode" => 1,
|
|
||||||
"eth_getLogs" => {
|
|
||||||
let obj = params[0].as_object_mut().unwrap();
|
|
||||||
|
|
||||||
if let Some(x) = obj.get_mut("fromBlock") {
|
|
||||||
let block_num: BlockNumber = serde_json::from_value(x.clone()).ok()?;
|
|
||||||
|
|
||||||
let (modified, block_num) = block_num_to_u64(block_num, head_block);
|
|
||||||
|
|
||||||
if modified {
|
|
||||||
*x = serde_json::to_value(block_num).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(block_num);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(x) = obj.get_mut("toBlock") {
|
|
||||||
let block_num: BlockNumber = serde_json::from_value(x.clone()).ok()?;
|
|
||||||
|
|
||||||
let (modified, block_num) = block_num_to_u64(block_num, head_block);
|
|
||||||
|
|
||||||
if modified {
|
|
||||||
*x = serde_json::to_value(block_num).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(block_num);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(x) = obj.get("blockHash") {
|
|
||||||
// TODO: check a linkedhashmap of recent hashes
|
|
||||||
// TODO: error if fromBlock or toBlock were set
|
|
||||||
todo!("handle blockHash {}", x);
|
|
||||||
}
|
|
||||||
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
"eth_getStorageAt" => 2,
|
|
||||||
"eth_getTransactionByHash" => {
|
|
||||||
// TODO: not sure how best to look these up
|
|
||||||
// try full nodes first. retry will use archive
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
"eth_getTransactionByBlockHashAndIndex" => {
|
|
||||||
// TODO: check a linkedhashmap of recent hashes
|
|
||||||
// try full nodes first. retry will use archive
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
"eth_getTransactionByBlockNumberAndIndex" => 0,
|
|
||||||
"eth_getTransactionCount" => 1,
|
|
||||||
"eth_getTransactionReceipt" => {
|
|
||||||
// TODO: not sure how best to look these up
|
|
||||||
// try full nodes first. retry will use archive
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
"eth_getUncleByBlockHashAndIndex" => {
|
|
||||||
// TODO: check a linkedhashmap of recent hashes
|
|
||||||
// try full nodes first. retry will use archive
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
"eth_getUncleByBlockNumberAndIndex" => 0,
|
|
||||||
"eth_getUncleCountByBlockHash" => {
|
|
||||||
// TODO: check a linkedhashmap of recent hashes
|
|
||||||
// try full nodes first. retry will use archive
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
"eth_getUncleCountByBlockNumber" => 0,
|
|
||||||
_ => {
|
|
||||||
// some other command that doesn't take block numbers as an argument
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match clean_block_number(params, block_param_id, head_block) {
|
|
||||||
Ok(block) => Some(block),
|
|
||||||
Err(err) => {
|
|
||||||
// TODO: seems unlikely that we will get here
|
|
||||||
// if this is incorrect, it should retry on an archive server
|
|
||||||
warn!(?err, "could not get block from params");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_migrated_db(
|
pub async fn get_migrated_db(
|
||||||
db_url: String,
|
db_url: String,
|
||||||
min_connections: u32,
|
min_connections: u32,
|
||||||
) -> anyhow::Result<DatabaseConnection> {
|
) -> anyhow::Result<DatabaseConnection> {
|
||||||
let mut db_opt = sea_orm::ConnectOptions::new(db_url);
|
let mut db_opt = sea_orm::ConnectOptions::new(db_url);
|
||||||
|
|
||||||
// TODO: load all these options from the config file
|
// TODO: load all these options from the config file. i think mysql default max is 100
|
||||||
// TODO: sqlx logging only in debug. way too verbose for production
|
// TODO: sqlx logging only in debug. way too verbose for production
|
||||||
db_opt
|
db_opt
|
||||||
.max_connections(100)
|
.max_connections(99)
|
||||||
.min_connections(min_connections)
|
.min_connections(min_connections)
|
||||||
.connect_timeout(Duration::from_secs(8))
|
.connect_timeout(Duration::from_secs(8))
|
||||||
.idle_timeout(Duration::from_secs(8))
|
.idle_timeout(Duration::from_secs(8))
|
||||||
@ -269,6 +118,13 @@ pub enum TxState {
|
|||||||
Orphaned(Transaction),
|
Orphaned(Transaction),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, From)]
|
||||||
|
pub struct UserCacheValue {
|
||||||
|
pub expires_at: Instant,
|
||||||
|
pub user_id: i64,
|
||||||
|
pub user_count_per_period: u32,
|
||||||
|
}
|
||||||
|
|
||||||
/// The application
|
/// The application
|
||||||
// TODO: this debug impl is way too verbose. make something smaller
|
// TODO: this debug impl is way too verbose. make something smaller
|
||||||
// TODO: if Web3ProxyApp is always in an Arc, i think we can avoid having at least some of these internal things in arcs
|
// TODO: if Web3ProxyApp is always in an Arc, i think we can avoid having at least some of these internal things in arcs
|
||||||
@ -278,18 +134,17 @@ pub struct Web3ProxyApp {
|
|||||||
balanced_rpcs: Arc<Web3Connections>,
|
balanced_rpcs: Arc<Web3Connections>,
|
||||||
/// Send private requests (like eth_sendRawTransaction) to all these servers
|
/// Send private requests (like eth_sendRawTransaction) to all these servers
|
||||||
private_rpcs: Arc<Web3Connections>,
|
private_rpcs: Arc<Web3Connections>,
|
||||||
/// Track active requests so that we don't 66
|
/// Track active requests so that we don't send the same query to multiple backends
|
||||||
///
|
|
||||||
active_requests: ActiveRequestsMap,
|
active_requests: ActiveRequestsMap,
|
||||||
/// bytes available to response_cache (it will be slightly larger than this)
|
|
||||||
response_cache_max_bytes: AtomicUsize,
|
|
||||||
response_cache: ResponseLrcCache,
|
response_cache: ResponseLrcCache,
|
||||||
// don't drop this or the sender will stop working
|
// don't drop this or the sender will stop working
|
||||||
// TODO: broadcast channel instead?
|
// TODO: broadcast channel instead?
|
||||||
head_block_receiver: watch::Receiver<Arc<Block<TxHash>>>,
|
head_block_receiver: watch::Receiver<Arc<Block<TxHash>>>,
|
||||||
pending_tx_sender: broadcast::Sender<TxState>,
|
pending_tx_sender: broadcast::Sender<TxState>,
|
||||||
pending_transactions: Arc<DashMap<TxHash, TxState>>,
|
pending_transactions: Arc<DashMap<TxHash, TxState>>,
|
||||||
rate_limiter: Option<RedisCellClient>,
|
user_cache: AsyncRwLock<FifoCountMap<Uuid, UserCacheValue>>,
|
||||||
|
redis_pool: Option<RedisPool>,
|
||||||
|
rate_limiter: Option<RedisCell>,
|
||||||
db_conn: Option<sea_orm::DatabaseConnection>,
|
db_conn: Option<sea_orm::DatabaseConnection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,18 +156,26 @@ impl fmt::Debug for Web3ProxyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Web3ProxyApp {
|
impl Web3ProxyApp {
|
||||||
pub fn db_conn(&self) -> &sea_orm::DatabaseConnection {
|
pub fn db_conn(&self) -> Option<&sea_orm::DatabaseConnection> {
|
||||||
self.db_conn.as_ref().unwrap()
|
self.db_conn.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pending_transactions(&self) -> &DashMap<TxHash, TxState> {
|
pub fn pending_transactions(&self) -> &DashMap<TxHash, TxState> {
|
||||||
&self.pending_transactions
|
&self.pending_transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rate_limiter(&self) -> Option<&RedisCellClient> {
|
pub fn rate_limiter(&self) -> Option<&RedisCell> {
|
||||||
self.rate_limiter.as_ref()
|
self.rate_limiter.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn redis_pool(&self) -> Option<&RedisPool> {
|
||||||
|
self.redis_pool.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_cache(&self) -> &AsyncRwLock<FifoCountMap<Uuid, UserCacheValue>> {
|
||||||
|
&self.user_cache
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: should we just take the rpc config as the only arg instead?
|
// TODO: should we just take the rpc config as the only arg instead?
|
||||||
pub async fn spawn(
|
pub async fn spawn(
|
||||||
app_config: AppConfig,
|
app_config: AppConfig,
|
||||||
@ -355,7 +218,7 @@ impl Web3ProxyApp {
|
|||||||
.build()?,
|
.build()?,
|
||||||
);
|
);
|
||||||
|
|
||||||
let redis_client_pool = match app_config.shared.redis_url {
|
let redis_pool = match app_config.shared.redis_url {
|
||||||
Some(redis_url) => {
|
Some(redis_url) => {
|
||||||
info!("Connecting to redis on {}", redis_url);
|
info!("Connecting to redis on {}", redis_url);
|
||||||
|
|
||||||
@ -399,7 +262,7 @@ impl Web3ProxyApp {
|
|||||||
app_config.shared.chain_id,
|
app_config.shared.chain_id,
|
||||||
balanced_rpcs,
|
balanced_rpcs,
|
||||||
http_client.clone(),
|
http_client.clone(),
|
||||||
redis_client_pool.clone(),
|
redis_pool.clone(),
|
||||||
Some(head_block_sender),
|
Some(head_block_sender),
|
||||||
Some(pending_tx_sender.clone()),
|
Some(pending_tx_sender.clone()),
|
||||||
pending_transactions.clone(),
|
pending_transactions.clone(),
|
||||||
@ -418,7 +281,7 @@ impl Web3ProxyApp {
|
|||||||
app_config.shared.chain_id,
|
app_config.shared.chain_id,
|
||||||
private_rpcs,
|
private_rpcs,
|
||||||
http_client.clone(),
|
http_client.clone(),
|
||||||
redis_client_pool.clone(),
|
redis_pool.clone(),
|
||||||
// subscribing to new heads here won't work well
|
// subscribing to new heads here won't work well
|
||||||
None,
|
None,
|
||||||
// TODO: subscribe to pending transactions on the private rpcs?
|
// TODO: subscribe to pending transactions on the private rpcs?
|
||||||
@ -436,9 +299,9 @@ impl Web3ProxyApp {
|
|||||||
// TODO: how much should we allow?
|
// TODO: how much should we allow?
|
||||||
let public_max_burst = app_config.shared.public_rate_limit_per_minute / 3;
|
let public_max_burst = app_config.shared.public_rate_limit_per_minute / 3;
|
||||||
|
|
||||||
let frontend_rate_limiter = redis_client_pool.as_ref().map(|redis_client_pool| {
|
let frontend_rate_limiter = redis_pool.as_ref().map(|redis_pool| {
|
||||||
RedisCellClient::new(
|
RedisCell::new(
|
||||||
redis_client_pool.clone(),
|
redis_pool.clone(),
|
||||||
"web3_proxy",
|
"web3_proxy",
|
||||||
"frontend",
|
"frontend",
|
||||||
public_max_burst,
|
public_max_burst,
|
||||||
@ -451,13 +314,20 @@ impl Web3ProxyApp {
|
|||||||
balanced_rpcs,
|
balanced_rpcs,
|
||||||
private_rpcs,
|
private_rpcs,
|
||||||
active_requests: Default::default(),
|
active_requests: Default::default(),
|
||||||
response_cache_max_bytes: AtomicUsize::new(app_config.shared.response_cache_max_bytes),
|
// TODO: make the share configurable
|
||||||
response_cache: Default::default(),
|
response_cache: RwLock::new(FifoSizedMap::new(
|
||||||
|
app_config.shared.response_cache_max_bytes,
|
||||||
|
100,
|
||||||
|
)),
|
||||||
head_block_receiver,
|
head_block_receiver,
|
||||||
pending_tx_sender,
|
pending_tx_sender,
|
||||||
pending_transactions,
|
pending_transactions,
|
||||||
rate_limiter: frontend_rate_limiter,
|
rate_limiter: frontend_rate_limiter,
|
||||||
db_conn,
|
db_conn,
|
||||||
|
redis_pool,
|
||||||
|
// TODO: make the size configurable
|
||||||
|
// TODO: why does this need to be async but the other one doesn't?
|
||||||
|
user_cache: AsyncRwLock::new(FifoCountMap::new(1_000)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = Arc::new(app);
|
let app = Arc::new(app);
|
||||||
@ -907,6 +777,7 @@ impl Web3ProxyApp {
|
|||||||
// returns Keccak-256 (not the standardized SHA3-256) of the given data.
|
// returns Keccak-256 (not the standardized SHA3-256) of the given data.
|
||||||
match &request.params {
|
match &request.params {
|
||||||
Some(serde_json::Value::Array(params)) => {
|
Some(serde_json::Value::Array(params)) => {
|
||||||
|
// TODO: make a struct and use serde conversion to clean this up
|
||||||
if params.len() != 1 || !params[0].is_string() {
|
if params.len() != 1 || !params[0].is_string() {
|
||||||
return Err(anyhow::anyhow!("invalid request"));
|
return Err(anyhow::anyhow!("invalid request"));
|
||||||
}
|
}
|
||||||
@ -1009,26 +880,10 @@ impl Web3ProxyApp {
|
|||||||
{
|
{
|
||||||
let mut response_cache = response_cache.write();
|
let mut response_cache = response_cache.write();
|
||||||
|
|
||||||
let response_cache_max_bytes = self
|
if response_cache.insert(cache_key.clone(), response.clone()) {
|
||||||
.response_cache_max_bytes
|
|
||||||
.load(atomic::Ordering::Acquire);
|
|
||||||
|
|
||||||
// TODO: this might be too naive. not sure how much overhead the object has
|
|
||||||
let new_size = size_of_val(&cache_key) + size_of_val(&response);
|
|
||||||
|
|
||||||
// no item is allowed to take more than 1% of the cache
|
|
||||||
// TODO: get this from config?
|
|
||||||
if new_size < response_cache_max_bytes / 100 {
|
|
||||||
// TODO: this probably has wildly variable timings
|
|
||||||
while size_of_val(&response_cache) + new_size >= response_cache_max_bytes {
|
|
||||||
// TODO: this isn't an LRU. it's a "least recently created". does that have a fancy name? should we make it an lru? these caches only live for one block
|
|
||||||
response_cache.pop_front();
|
|
||||||
}
|
|
||||||
|
|
||||||
response_cache.insert(cache_key.clone(), response.clone());
|
|
||||||
} else {
|
} else {
|
||||||
// TODO: emit a stat instead?
|
// TODO: emit a stat instead? what else should be in the log
|
||||||
warn!(?new_size, "value too large for caching");
|
trace!(?cache_key, "value too large for caching");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
163
web3_proxy/src/block_helpers.rs
Normal file
163
web3_proxy/src/block_helpers.rs
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
use ethers::prelude::{BlockNumber, U64};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
pub fn block_num_to_u64(block_num: BlockNumber, latest_block: U64) -> (bool, U64) {
|
||||||
|
match block_num {
|
||||||
|
BlockNumber::Earliest => (false, U64::zero()),
|
||||||
|
BlockNumber::Latest => {
|
||||||
|
// change "latest" to a number
|
||||||
|
(true, latest_block)
|
||||||
|
}
|
||||||
|
BlockNumber::Number(x) => (false, x),
|
||||||
|
BlockNumber::Pending => {
|
||||||
|
// TODO: think more about how to handle Pending
|
||||||
|
// modified is false because we probably want the backend to see "pending"
|
||||||
|
(false, latest_block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// modify params to always have a block number and not "latest"
|
||||||
|
pub fn clean_block_number(
|
||||||
|
params: &mut serde_json::Value,
|
||||||
|
block_param_id: usize,
|
||||||
|
latest_block: U64,
|
||||||
|
) -> anyhow::Result<U64> {
|
||||||
|
match params.as_array_mut() {
|
||||||
|
None => Err(anyhow::anyhow!("params not an array")),
|
||||||
|
Some(params) => match params.get_mut(block_param_id) {
|
||||||
|
None => {
|
||||||
|
if params.len() != block_param_id - 1 {
|
||||||
|
return Err(anyhow::anyhow!("unexpected params length"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the latest block number to the end of the params
|
||||||
|
params.push(serde_json::to_value(latest_block)?);
|
||||||
|
|
||||||
|
Ok(latest_block)
|
||||||
|
}
|
||||||
|
Some(x) => {
|
||||||
|
// convert the json value to a BlockNumber
|
||||||
|
let block_num: BlockNumber = serde_json::from_value(x.clone())?;
|
||||||
|
|
||||||
|
let (modified, block_num) = block_num_to_u64(block_num, latest_block);
|
||||||
|
|
||||||
|
// if we changed "latest" to a number, update the params to match
|
||||||
|
if modified {
|
||||||
|
*x = serde_json::to_value(block_num)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(block_num)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: change this to also return the hash needed
|
||||||
|
pub fn block_needed(
|
||||||
|
method: &str,
|
||||||
|
params: Option<&mut serde_json::Value>,
|
||||||
|
head_block: U64,
|
||||||
|
) -> Option<U64> {
|
||||||
|
let params = params?;
|
||||||
|
|
||||||
|
// TODO: double check these. i think some of the getBlock stuff will never need archive
|
||||||
|
let block_param_id = match method {
|
||||||
|
"eth_call" => 1,
|
||||||
|
"eth_estimateGas" => 1,
|
||||||
|
"eth_getBalance" => 1,
|
||||||
|
"eth_getBlockByHash" => {
|
||||||
|
// TODO: double check that any node can serve this
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
"eth_getBlockByNumber" => {
|
||||||
|
// TODO: double check that any node can serve this
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
"eth_getBlockTransactionCountByHash" => {
|
||||||
|
// TODO: double check that any node can serve this
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
"eth_getBlockTransactionCountByNumber" => 0,
|
||||||
|
"eth_getCode" => 1,
|
||||||
|
"eth_getLogs" => {
|
||||||
|
let obj = params[0].as_object_mut().unwrap();
|
||||||
|
|
||||||
|
if let Some(x) = obj.get_mut("fromBlock") {
|
||||||
|
let block_num: BlockNumber = serde_json::from_value(x.clone()).ok()?;
|
||||||
|
|
||||||
|
let (modified, block_num) = block_num_to_u64(block_num, head_block);
|
||||||
|
|
||||||
|
if modified {
|
||||||
|
*x = serde_json::to_value(block_num).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(block_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = obj.get_mut("toBlock") {
|
||||||
|
let block_num: BlockNumber = serde_json::from_value(x.clone()).ok()?;
|
||||||
|
|
||||||
|
let (modified, block_num) = block_num_to_u64(block_num, head_block);
|
||||||
|
|
||||||
|
if modified {
|
||||||
|
*x = serde_json::to_value(block_num).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(block_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = obj.get("blockHash") {
|
||||||
|
// TODO: check a linkedhashmap of recent hashes
|
||||||
|
// TODO: error if fromBlock or toBlock were set
|
||||||
|
todo!("handle blockHash {}", x);
|
||||||
|
}
|
||||||
|
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
"eth_getStorageAt" => 2,
|
||||||
|
"eth_getTransactionByHash" => {
|
||||||
|
// TODO: not sure how best to look these up
|
||||||
|
// try full nodes first. retry will use archive
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
"eth_getTransactionByBlockHashAndIndex" => {
|
||||||
|
// TODO: check a linkedhashmap of recent hashes
|
||||||
|
// try full nodes first. retry will use archive
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
"eth_getTransactionByBlockNumberAndIndex" => 0,
|
||||||
|
"eth_getTransactionCount" => 1,
|
||||||
|
"eth_getTransactionReceipt" => {
|
||||||
|
// TODO: not sure how best to look these up
|
||||||
|
// try full nodes first. retry will use archive
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
"eth_getUncleByBlockHashAndIndex" => {
|
||||||
|
// TODO: check a linkedhashmap of recent hashes
|
||||||
|
// try full nodes first. retry will use archive
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
"eth_getUncleByBlockNumberAndIndex" => 0,
|
||||||
|
"eth_getUncleCountByBlockHash" => {
|
||||||
|
// TODO: check a linkedhashmap of recent hashes
|
||||||
|
// try full nodes first. retry will use archive
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
"eth_getUncleCountByBlockNumber" => 0,
|
||||||
|
_ => {
|
||||||
|
// some other command that doesn't take block numbers as an argument
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match clean_block_number(params, block_param_id, head_block) {
|
||||||
|
Ok(block) => Some(block),
|
||||||
|
Err(err) => {
|
||||||
|
// TODO: seems unlikely that we will get here
|
||||||
|
// if this is incorrect, it should retry on an archive server
|
||||||
|
warn!(?err, "could not get block from params");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -70,7 +70,7 @@ impl Web3ConnectionConfig {
|
|||||||
// #[instrument(name = "try_build_Web3ConnectionConfig", skip_all)]
|
// #[instrument(name = "try_build_Web3ConnectionConfig", skip_all)]
|
||||||
pub async fn spawn(
|
pub async fn spawn(
|
||||||
self,
|
self,
|
||||||
redis_client_pool: Option<redis_cell_client::RedisClientPool>,
|
redis_client_pool: Option<redis_cell_client::RedisPool>,
|
||||||
chain_id: u64,
|
chain_id: u64,
|
||||||
http_client: Option<reqwest::Client>,
|
http_client: Option<reqwest::Client>,
|
||||||
http_interval_sender: Option<Arc<broadcast::Sender<()>>>,
|
http_interval_sender: Option<Arc<broadcast::Sender<()>>>,
|
||||||
|
@ -4,7 +4,8 @@ use derive_more::From;
|
|||||||
use ethers::prelude::{Block, Bytes, Middleware, ProviderError, TxHash, H256, U64};
|
use ethers::prelude::{Block, Bytes, Middleware, ProviderError, TxHash, H256, U64};
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use redis_cell_client::{RedisCellClient, ThrottleResult};
|
use parking_lot::RwLock;
|
||||||
|
use redis_cell_client::{RedisCell, ThrottleResult};
|
||||||
use serde::ser::{SerializeStruct, Serializer};
|
use serde::ser::{SerializeStruct, Serializer};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@ -12,7 +13,7 @@ use std::hash::{Hash, Hasher};
|
|||||||
use std::sync::atomic::{self, AtomicU32, AtomicU64};
|
use std::sync::atomic::{self, AtomicU32, AtomicU64};
|
||||||
use std::{cmp::Ordering, sync::Arc};
|
use std::{cmp::Ordering, sync::Arc};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock as AsyncRwLock;
|
||||||
use tokio::time::{interval, sleep, sleep_until, Duration, Instant, MissedTickBehavior};
|
use tokio::time::{interval, sleep, sleep_until, Duration, Instant, MissedTickBehavior};
|
||||||
use tracing::{error, info, info_span, instrument, trace, warn, Instrument};
|
use tracing::{error, info, info_span, instrument, trace, warn, Instrument};
|
||||||
|
|
||||||
@ -77,14 +78,14 @@ pub struct Web3Connection {
|
|||||||
/// keep track of currently open requests. We sort on this
|
/// keep track of currently open requests. We sort on this
|
||||||
active_requests: AtomicU32,
|
active_requests: AtomicU32,
|
||||||
/// provider is in a RwLock so that we can replace it if re-connecting
|
/// provider is in a RwLock so that we can replace it if re-connecting
|
||||||
provider: RwLock<Option<Arc<Web3Provider>>>,
|
provider: AsyncRwLock<Option<Arc<Web3Provider>>>,
|
||||||
/// rate limits are stored in a central redis so that multiple proxies can share their rate limits
|
/// rate limits are stored in a central redis so that multiple proxies can share their rate limits
|
||||||
hard_limit: Option<redis_cell_client::RedisCellClient>,
|
hard_limit: Option<redis_cell_client::RedisCell>,
|
||||||
/// used for load balancing to the least loaded server
|
/// used for load balancing to the least loaded server
|
||||||
soft_limit: u32,
|
soft_limit: u32,
|
||||||
block_data_limit: AtomicU64,
|
block_data_limit: AtomicU64,
|
||||||
weight: u32,
|
weight: u32,
|
||||||
head_block: parking_lot::RwLock<(H256, U64)>,
|
head_block: RwLock<(H256, U64)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for Web3Connection {
|
impl Serialize for Web3Connection {
|
||||||
@ -146,7 +147,7 @@ impl Web3Connection {
|
|||||||
// optional because this is only used for http providers. websocket providers don't use it
|
// optional because this is only used for http providers. websocket providers don't use it
|
||||||
http_client: Option<reqwest::Client>,
|
http_client: Option<reqwest::Client>,
|
||||||
http_interval_sender: Option<Arc<broadcast::Sender<()>>>,
|
http_interval_sender: Option<Arc<broadcast::Sender<()>>>,
|
||||||
hard_limit: Option<(u32, redis_cell_client::RedisClientPool)>,
|
hard_limit: Option<(u32, redis_cell_client::RedisPool)>,
|
||||||
// TODO: think more about this type
|
// TODO: think more about this type
|
||||||
soft_limit: u32,
|
soft_limit: u32,
|
||||||
block_sender: Option<flume::Sender<BlockAndRpc>>,
|
block_sender: Option<flume::Sender<BlockAndRpc>>,
|
||||||
@ -157,7 +158,7 @@ impl Web3Connection {
|
|||||||
let hard_limit = hard_limit.map(|(hard_rate_limit, redis_conection)| {
|
let hard_limit = hard_limit.map(|(hard_rate_limit, redis_conection)| {
|
||||||
// TODO: allow configurable period and max_burst
|
// TODO: allow configurable period and max_burst
|
||||||
let period = 1;
|
let period = 1;
|
||||||
RedisCellClient::new(
|
RedisCell::new(
|
||||||
redis_conection,
|
redis_conection,
|
||||||
"web3_proxy",
|
"web3_proxy",
|
||||||
&format!("{}:{}", chain_id, url_str),
|
&format!("{}:{}", chain_id, url_str),
|
||||||
@ -172,11 +173,11 @@ impl Web3Connection {
|
|||||||
let new_connection = Self {
|
let new_connection = Self {
|
||||||
url: url_str.clone(),
|
url: url_str.clone(),
|
||||||
active_requests: 0.into(),
|
active_requests: 0.into(),
|
||||||
provider: RwLock::new(Some(Arc::new(provider))),
|
provider: AsyncRwLock::new(Some(Arc::new(provider))),
|
||||||
hard_limit,
|
hard_limit,
|
||||||
soft_limit,
|
soft_limit,
|
||||||
block_data_limit: Default::default(),
|
block_data_limit: Default::default(),
|
||||||
head_block: parking_lot::RwLock::new((H256::zero(), 0isize.into())),
|
head_block: RwLock::new((H256::zero(), 0isize.into())),
|
||||||
weight,
|
weight,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,7 +37,6 @@ struct SyncedConnections {
|
|||||||
head_block_num: u64,
|
head_block_num: u64,
|
||||||
head_block_hash: H256,
|
head_block_hash: H256,
|
||||||
// TODO: this should be able to serialize, but it isn't
|
// TODO: this should be able to serialize, but it isn't
|
||||||
// TODO: use linkedhashmap?
|
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
conns: IndexSet<Arc<Web3Connection>>,
|
conns: IndexSet<Arc<Web3Connection>>,
|
||||||
}
|
}
|
||||||
@ -147,7 +146,7 @@ impl Web3Connections {
|
|||||||
chain_id: u64,
|
chain_id: u64,
|
||||||
server_configs: Vec<Web3ConnectionConfig>,
|
server_configs: Vec<Web3ConnectionConfig>,
|
||||||
http_client: Option<reqwest::Client>,
|
http_client: Option<reqwest::Client>,
|
||||||
redis_client_pool: Option<redis_cell_client::RedisClientPool>,
|
redis_client_pool: Option<redis_cell_client::RedisPool>,
|
||||||
head_block_sender: Option<watch::Sender<Arc<Block<TxHash>>>>,
|
head_block_sender: Option<watch::Sender<Arc<Block<TxHash>>>>,
|
||||||
pending_tx_sender: Option<broadcast::Sender<TxState>>,
|
pending_tx_sender: Option<broadcast::Sender<TxState>>,
|
||||||
pending_transactions: Arc<DashMap<TxHash, TxState>>,
|
pending_transactions: Arc<DashMap<TxHash, TxState>>,
|
||||||
|
@ -1,22 +1,26 @@
|
|||||||
use axum::{http::StatusCode, response::IntoResponse, Json};
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
use serde_json::value::RawValue;
|
use serde_json::value::RawValue;
|
||||||
|
|
||||||
use crate::jsonrpc::JsonRpcForwardedResponse;
|
use crate::jsonrpc::JsonRpcForwardedResponse;
|
||||||
|
|
||||||
pub async fn handler_404() -> impl IntoResponse {
|
pub async fn handler_404() -> Response {
|
||||||
let err = anyhow::anyhow!("nothing to see here");
|
let err = anyhow::anyhow!("nothing to see here");
|
||||||
|
|
||||||
handle_anyhow_error(Some(StatusCode::NOT_FOUND), None, err).await
|
handle_anyhow_error(Some(StatusCode::NOT_FOUND), None, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// handle errors by converting them into something that implements `IntoResponse`
|
/// handle errors by converting them into something that implements `IntoResponse`
|
||||||
/// TODO: use this. i can't get <https://docs.rs/axum/latest/axum/error_handling/index.html> to work
|
/// TODO: use this. i can't get <https://docs.rs/axum/latest/axum/error_handling/index.html> to work
|
||||||
/// TODO: i think we want a custom result type instead. put the anyhow result inside. then `impl IntoResponse for CustomResult`
|
/// TODO: i think we want a custom result type instead. put the anyhow result inside. then `impl IntoResponse for CustomResult`
|
||||||
pub async fn handle_anyhow_error(
|
pub fn handle_anyhow_error(
|
||||||
http_code: Option<StatusCode>,
|
http_code: Option<StatusCode>,
|
||||||
id: Option<Box<RawValue>>,
|
id: Option<Box<RawValue>>,
|
||||||
err: anyhow::Error,
|
err: anyhow::Error,
|
||||||
) -> impl IntoResponse {
|
) -> Response {
|
||||||
// TODO: we might have an id. like if this is for rate limiting, we can use it
|
// TODO: we might have an id. like if this is for rate limiting, we can use it
|
||||||
let id = id.unwrap_or_else(|| RawValue::from_string("null".to_string()).unwrap());
|
let id = id.unwrap_or_else(|| RawValue::from_string("null".to_string()).unwrap());
|
||||||
|
|
||||||
@ -27,5 +31,5 @@ pub async fn handle_anyhow_error(
|
|||||||
|
|
||||||
let code = http_code.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
let code = http_code.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
|
||||||
(code, Json(err))
|
(code, Json(err)).into_response()
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ use std::sync::Arc;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::errors::handle_anyhow_error;
|
use super::errors::handle_anyhow_error;
|
||||||
use super::rate_limit::{rate_limit_by_ip, rate_limit_by_key};
|
use super::rate_limit::handle_rate_limit_error_response;
|
||||||
use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum};
|
use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum};
|
||||||
|
|
||||||
pub async fn public_proxy_web3_rpc(
|
pub async fn public_proxy_web3_rpc(
|
||||||
@ -13,13 +13,15 @@ pub async fn public_proxy_web3_rpc(
|
|||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
ClientIp(ip): ClientIp,
|
ClientIp(ip): ClientIp,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if let Err(x) = rate_limit_by_ip(&app, &ip).await {
|
if let Some(err_response) =
|
||||||
return x.into_response();
|
handle_rate_limit_error_response(app.rate_limit_by_ip(&ip).await).await
|
||||||
|
{
|
||||||
|
return err_response.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
match app.proxy_web3_rpc(payload).await {
|
match app.proxy_web3_rpc(payload).await {
|
||||||
Ok(response) => (StatusCode::OK, Json(&response)).into_response(),
|
Ok(response) => (StatusCode::OK, Json(&response)).into_response(),
|
||||||
Err(err) => handle_anyhow_error(None, None, err).await.into_response(),
|
Err(err) => handle_anyhow_error(None, None, err).into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,12 +30,15 @@ pub async fn user_proxy_web3_rpc(
|
|||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
Path(user_key): Path<Uuid>,
|
Path(user_key): Path<Uuid>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if let Err(x) = rate_limit_by_key(&app, user_key).await {
|
// TODO: add a helper on this that turns RateLimitResult into error if its not allowed
|
||||||
return x.into_response();
|
if let Some(err_response) =
|
||||||
|
handle_rate_limit_error_response(app.rate_limit_by_key(user_key).await).await
|
||||||
|
{
|
||||||
|
return err_response.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
match app.proxy_web3_rpc(payload).await {
|
match app.proxy_web3_rpc(payload).await {
|
||||||
Ok(response) => (StatusCode::OK, Json(&response)).into_response(),
|
Ok(response) => (StatusCode::OK, Json(&response)).into_response(),
|
||||||
Err(err) => handle_anyhow_error(None, None, err).await.into_response(),
|
Err(err) => handle_anyhow_error(None, None, err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,146 +1,168 @@
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::Response;
|
||||||
use entities::user_keys;
|
use entities::user_keys;
|
||||||
use redis_cell_client::ThrottleResult;
|
use redis_cell_client::ThrottleResult;
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ColumnTrait, DeriveColumn, EntityTrait, EnumIter, IdenStatic, QueryFilter, QuerySelect,
|
ColumnTrait, DeriveColumn, EntityTrait, EnumIter, IdenStatic, QueryFilter, QuerySelect,
|
||||||
};
|
};
|
||||||
use std::net::IpAddr;
|
use std::{net::IpAddr, time::Duration};
|
||||||
|
use tokio::time::Instant;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::app::Web3ProxyApp;
|
use crate::app::{UserCacheValue, Web3ProxyApp};
|
||||||
|
|
||||||
use super::errors::handle_anyhow_error;
|
use super::errors::handle_anyhow_error;
|
||||||
|
|
||||||
pub async fn rate_limit_by_ip(app: &Web3ProxyApp, ip: &IpAddr) -> Result<(), impl IntoResponse> {
|
pub enum RateLimitResult {
|
||||||
let rate_limiter_key = format!("ip-{}", ip);
|
Allowed,
|
||||||
|
RateLimitExceeded,
|
||||||
// TODO: dry this up with rate_limit_by_key
|
UnknownKey,
|
||||||
if let Some(rate_limiter) = app.rate_limiter() {
|
|
||||||
match rate_limiter
|
|
||||||
.throttle_key(&rate_limiter_key, None, None, None)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(ThrottleResult::Allowed) => {}
|
|
||||||
Ok(ThrottleResult::RetryAt(_retry_at)) => {
|
|
||||||
// TODO: set headers so they know when they can retry
|
|
||||||
debug!(?rate_limiter_key, "rate limit exceeded"); // this is too verbose, but a stat might be good
|
|
||||||
// TODO: use their id if possible
|
|
||||||
return Err(handle_anyhow_error(
|
|
||||||
Some(StatusCode::TOO_MANY_REQUESTS),
|
|
||||||
None,
|
|
||||||
anyhow::anyhow!(format!("too many requests from this ip: {}", ip)),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
// internal error, not rate limit being hit
|
|
||||||
// TODO: i really want axum to do this for us in a single place.
|
|
||||||
return Err(handle_anyhow_error(
|
|
||||||
Some(StatusCode::INTERNAL_SERVER_ERROR),
|
|
||||||
None,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO: if no redis, rate limit with a local cache?
|
|
||||||
warn!("no rate limiter!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// if Ok(()), rate limits are acceptable
|
impl Web3ProxyApp {
|
||||||
/// if Err(response), rate limits exceeded
|
pub async fn rate_limit_by_ip(&self, ip: &IpAddr) -> anyhow::Result<RateLimitResult> {
|
||||||
pub async fn rate_limit_by_key(
|
let rate_limiter_key = format!("ip-{}", ip);
|
||||||
app: &Web3ProxyApp,
|
|
||||||
user_key: Uuid,
|
|
||||||
) -> Result<(), impl IntoResponse> {
|
|
||||||
let db = app.db_conn();
|
|
||||||
|
|
||||||
/// query just a few columns instead of the entire table
|
// TODO: dry this up with rate_limit_by_key
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
if let Some(rate_limiter) = self.rate_limiter() {
|
||||||
enum QueryAs {
|
match rate_limiter
|
||||||
UserId,
|
.throttle_key(&rate_limiter_key, None, None, None)
|
||||||
RequestsPerMinute,
|
.await
|
||||||
}
|
{
|
||||||
|
Ok(ThrottleResult::Allowed) => {}
|
||||||
// query the db to make sure this key is active
|
Ok(ThrottleResult::RetryAt(_retry_at)) => {
|
||||||
// TODO: probably want a cache on this
|
|
||||||
match user_keys::Entity::find()
|
|
||||||
.select_only()
|
|
||||||
.column_as(user_keys::Column::UserId, QueryAs::UserId)
|
|
||||||
.column_as(
|
|
||||||
user_keys::Column::RequestsPerMinute,
|
|
||||||
QueryAs::RequestsPerMinute,
|
|
||||||
)
|
|
||||||
.filter(user_keys::Column::ApiKey.eq(user_key))
|
|
||||||
.filter(user_keys::Column::Active.eq(true))
|
|
||||||
.into_values::<_, QueryAs>()
|
|
||||||
.one(db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok::<Option<(i64, u32)>, _>(Some((_user_id, user_count_per_period))) => {
|
|
||||||
// user key is valid
|
|
||||||
if let Some(rate_limiter) = app.rate_limiter() {
|
|
||||||
// TODO: how does max burst actually work? what should it be?
|
|
||||||
let user_max_burst = user_count_per_period / 3;
|
|
||||||
let user_period = 60;
|
|
||||||
|
|
||||||
if rate_limiter
|
|
||||||
.throttle_key(
|
|
||||||
&user_key.to_string(),
|
|
||||||
Some(user_max_burst),
|
|
||||||
Some(user_count_per_period),
|
|
||||||
Some(user_period),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
// TODO: set headers so they know when they can retry
|
// TODO: set headers so they know when they can retry
|
||||||
// warn!(?ip, "public rate limit exceeded"); // this is too verbose, but a stat might be good
|
debug!(?rate_limiter_key, "rate limit exceeded"); // this is too verbose, but a stat might be good
|
||||||
// TODO: use their id if possible
|
// TODO: use their id if possible
|
||||||
return Err(handle_anyhow_error(
|
return Ok(RateLimitResult::RateLimitExceeded);
|
||||||
Some(StatusCode::TOO_MANY_REQUESTS),
|
}
|
||||||
None,
|
Err(err) => {
|
||||||
// TODO: include the user id (NOT THE API KEY!) here
|
// internal error, not rate limit being hit
|
||||||
anyhow::anyhow!("too many requests from this key"),
|
// TODO: i really want axum to do this for us in a single place.
|
||||||
)
|
return Err(err);
|
||||||
.await
|
|
||||||
.into_response());
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// TODO: if no redis, rate limit with a local cache?
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: if no redis, rate limit with a local cache?
|
||||||
|
warn!("no rate limiter!");
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
|
||||||
// invalid user key
|
|
||||||
// TODO: rate limit by ip here, too? maybe tarpit?
|
|
||||||
return Err(handle_anyhow_error(
|
|
||||||
Some(StatusCode::FORBIDDEN),
|
|
||||||
None,
|
|
||||||
anyhow::anyhow!("unknown api key"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let err: anyhow::Error = err.into();
|
|
||||||
|
|
||||||
return Err(handle_anyhow_error(
|
Ok(RateLimitResult::Allowed)
|
||||||
Some(StatusCode::INTERNAL_SERVER_ERROR),
|
|
||||||
None,
|
|
||||||
err.context("failed checking database for user key"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_response());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
pub async fn rate_limit_by_key(&self, user_key: Uuid) -> anyhow::Result<RateLimitResult> {
|
||||||
|
let user_cache = self.user_cache();
|
||||||
|
|
||||||
|
// check the local cache
|
||||||
|
let user_data = if let Some(cached_user) = user_cache.read().await.get(&user_key) {
|
||||||
|
// TODO: also include the time this value was last checked! otherwise we cache forever!
|
||||||
|
if cached_user.expires_at < Instant::now() {
|
||||||
|
// old record found
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// this key was active in the database recently
|
||||||
|
Some(*cached_user)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// cache miss
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// if cache was empty, check the database
|
||||||
|
let user_data = if user_data.is_none() {
|
||||||
|
if let Some(db) = self.db_conn() {
|
||||||
|
/// helper enum for query just a few columns instead of the entire table
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||||
|
enum QueryAs {
|
||||||
|
UserId,
|
||||||
|
RequestsPerMinute,
|
||||||
|
}
|
||||||
|
let user_data = match user_keys::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.column_as(user_keys::Column::UserId, QueryAs::UserId)
|
||||||
|
.column_as(
|
||||||
|
user_keys::Column::RequestsPerMinute,
|
||||||
|
QueryAs::RequestsPerMinute,
|
||||||
|
)
|
||||||
|
.filter(user_keys::Column::ApiKey.eq(user_key))
|
||||||
|
.filter(user_keys::Column::Active.eq(true))
|
||||||
|
.into_values::<_, QueryAs>()
|
||||||
|
.one(db)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Some((user_id, requests_per_minute)) => {
|
||||||
|
UserCacheValue::from((
|
||||||
|
// TODO: how long should this cache last? get this from config
|
||||||
|
Instant::now() + Duration::from_secs(60),
|
||||||
|
user_id,
|
||||||
|
requests_per_minute,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(anyhow::anyhow!("unknown api key"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// save for the next run
|
||||||
|
user_cache.write().await.insert(user_key, user_data);
|
||||||
|
|
||||||
|
user_data
|
||||||
|
} else {
|
||||||
|
// TODO: rate limit with only local caches?
|
||||||
|
unimplemented!("no cache hit and no database connection")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// unwrap the cache's result
|
||||||
|
user_data.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// user key is valid. now check rate limits
|
||||||
|
if let Some(rate_limiter) = self.rate_limiter() {
|
||||||
|
// TODO: how does max burst actually work? what should it be?
|
||||||
|
let user_max_burst = user_data.user_count_per_period / 3;
|
||||||
|
let user_period = 60;
|
||||||
|
|
||||||
|
if rate_limiter
|
||||||
|
.throttle_key(
|
||||||
|
&user_key.to_string(),
|
||||||
|
Some(user_max_burst),
|
||||||
|
Some(user_data.user_count_per_period),
|
||||||
|
Some(user_period),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
// TODO: set headers so they know when they can retry
|
||||||
|
// warn!(?ip, "public rate limit exceeded"); // this is too verbose, but a stat might be good
|
||||||
|
// TODO: use their id if possible
|
||||||
|
// TODO: StatusCode::TOO_MANY_REQUESTS
|
||||||
|
return Err(anyhow::anyhow!("too many requests from this key"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: if no redis, rate limit with a local cache?
|
||||||
|
unimplemented!("no redis. cannot rate limit")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RateLimitResult::Allowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_rate_limit_error_response(
|
||||||
|
r: anyhow::Result<RateLimitResult>,
|
||||||
|
) -> Option<Response> {
|
||||||
|
match r {
|
||||||
|
Ok(RateLimitResult::Allowed) => None,
|
||||||
|
Ok(RateLimitResult::RateLimitExceeded) => Some(handle_anyhow_error(
|
||||||
|
Some(StatusCode::TOO_MANY_REQUESTS),
|
||||||
|
None,
|
||||||
|
anyhow::anyhow!("rate limit exceeded"),
|
||||||
|
)),
|
||||||
|
Ok(RateLimitResult::UnknownKey) => Some(handle_anyhow_error(
|
||||||
|
Some(StatusCode::FORBIDDEN),
|
||||||
|
None,
|
||||||
|
anyhow::anyhow!("unknown key"),
|
||||||
|
)),
|
||||||
|
Err(err) => Some(handle_anyhow_error(None, None, err)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,9 @@ use sea_orm::ActiveModelTrait;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{app::Web3ProxyApp, frontend::rate_limit::rate_limit_by_ip};
|
use crate::app::Web3ProxyApp;
|
||||||
|
|
||||||
|
use super::rate_limit::handle_rate_limit_error_response;
|
||||||
|
|
||||||
pub async fn create_user(
|
pub async fn create_user(
|
||||||
// this argument tells axum to parse the request body
|
// this argument tells axum to parse the request body
|
||||||
@ -24,11 +26,13 @@ pub async fn create_user(
|
|||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
ClientIp(ip): ClientIp,
|
ClientIp(ip): ClientIp,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if let Err(x) = rate_limit_by_ip(&app, &ip).await {
|
if let Some(err_response) =
|
||||||
return x;
|
handle_rate_limit_error_response(app.rate_limit_by_ip(&ip).await).await
|
||||||
|
{
|
||||||
|
return err_response.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: check invite_code against the app's config
|
// TODO: check invite_code against the app's config or database
|
||||||
if payload.invite_code != "llam4n0des!" {
|
if payload.invite_code != "llam4n0des!" {
|
||||||
todo!("proper error message")
|
todo!("proper error message")
|
||||||
}
|
}
|
||||||
@ -49,7 +53,7 @@ pub async fn create_user(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = app.db_conn();
|
let db = app.db_conn().unwrap();
|
||||||
|
|
||||||
// TODO: proper error message
|
// TODO: proper error message
|
||||||
let user = user.insert(db).await.unwrap();
|
let user = user.insert(db).await.unwrap();
|
||||||
|
@ -22,17 +22,19 @@ use crate::{
|
|||||||
jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest},
|
jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::rate_limit::{rate_limit_by_ip, rate_limit_by_key};
|
use super::rate_limit::handle_rate_limit_error_response;
|
||||||
|
|
||||||
pub async fn public_websocket_handler(
|
pub async fn public_websocket_handler(
|
||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
ClientIp(ip): ClientIp,
|
ClientIp(ip): ClientIp,
|
||||||
ws: Option<WebSocketUpgrade>,
|
ws_upgrade: Option<WebSocketUpgrade>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match ws {
|
match ws_upgrade {
|
||||||
Some(ws) => {
|
Some(ws) => {
|
||||||
if let Err(x) = rate_limit_by_ip(&app, &ip).await {
|
if let Some(err_response) =
|
||||||
return x.into_response();
|
handle_rate_limit_error_response(app.rate_limit_by_ip(&ip).await).await
|
||||||
|
{
|
||||||
|
return err_response.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.on_upgrade(|socket| proxy_web3_socket(app, socket))
|
ws.on_upgrade(|socket| proxy_web3_socket(app, socket))
|
||||||
@ -41,6 +43,7 @@ pub async fn public_websocket_handler(
|
|||||||
None => {
|
None => {
|
||||||
// this is not a websocket. give a friendly page
|
// this is not a websocket. give a friendly page
|
||||||
// TODO: make a friendly page
|
// TODO: make a friendly page
|
||||||
|
// TODO: rate limit this?
|
||||||
"hello, world".into_response()
|
"hello, world".into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,8 +54,10 @@ pub async fn user_websocket_handler(
|
|||||||
ws: WebSocketUpgrade,
|
ws: WebSocketUpgrade,
|
||||||
Path(user_key): Path<Uuid>,
|
Path(user_key): Path<Uuid>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Err(x) = rate_limit_by_key(&app, user_key).await {
|
if let Some(err_response) =
|
||||||
return x.into_response();
|
handle_rate_limit_error_response(app.rate_limit_by_key(user_key).await).await
|
||||||
|
{
|
||||||
|
return err_response;
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.on_upgrade(|socket| proxy_web3_socket(app, socket))
|
ws.on_upgrade(|socket| proxy_web3_socket(app, socket))
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod bb8_helpers;
|
pub mod bb8_helpers;
|
||||||
|
pub mod block_helpers;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
pub mod connections;
|
pub mod connections;
|
||||||
|
Loading…
Reference in New Issue
Block a user