Merge branch 'main' into 19-admin-imitate
This commit is contained in:
commit
2c8c4306fa
48
Cargo.lock
generated
48
Cargo.lock
generated
@ -282,13 +282,13 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.6.4"
|
version = "0.6.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5694b64066a2459918d8074c2ce0d5a88f409431994c2356617c8ae0c4721fc"
|
checksum = "4e246206a63c9830e118d12c894f56a82033da1a2361f5544deeee3df85c99d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"base64 0.20.0",
|
"base64 0.21.0",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -347,9 +347,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-macros"
|
name = "axum-macros"
|
||||||
version = "0.3.2"
|
version = "0.3.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9dbcf61bed07d554bd5c225cd07bc41b793eab63e79c6f0ceac7e1aed2f1c670"
|
checksum = "5fbf955307ff8addb48d2399393c9e2740dd491537ec562b66ab364fc4a38841"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.4.0",
|
"heck 0.4.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -419,12 +419,6 @@ version = "0.13.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "base64"
|
|
||||||
version = "0.20.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
@ -1809,6 +1803,12 @@ version = "2.5.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ewma"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f20267f3a8b678b7151c0c508002e79126144a5d47badddec7f31ddc1f4c754"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eyre"
|
name = "eyre"
|
||||||
version = "0.6.8"
|
version = "0.6.8"
|
||||||
@ -2891,9 +2891,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moka"
|
name = "moka"
|
||||||
version = "0.9.7"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19b9268097a2cf211ac9955b1cc95e80fa84fff5c2d13ba292916445dc8a311f"
|
checksum = "2b6446f16d504e3d575df79cabb11bfbe9f24b17e9562d964a815db7b28ae3ec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-io",
|
"async-io",
|
||||||
"async-lock",
|
"async-lock",
|
||||||
@ -3093,9 +3093,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.17.0"
|
version = "1.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opaque-debug"
|
name = "opaque-debug"
|
||||||
@ -3134,6 +3134,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-float"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d84eb1409416d254e4a9c8fa56cc24701755025b458f0fcd8e59e1f5f40c23bf"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_info"
|
name = "os_info"
|
||||||
version = "3.6.0"
|
version = "3.6.0"
|
||||||
@ -4520,9 +4529,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_prometheus"
|
name = "serde_prometheus"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1bfb6048d9e4ebc41f7d1a42c79b04c5b460633be307620a0e34a8f81970ea47"
|
checksum = "9c1a4ca38f4e746460d1dbd3711b8ca8ae314d1b21247edeff61dd20325b5a6f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heapless",
|
"heapless",
|
||||||
"nom",
|
"nom",
|
||||||
@ -5761,7 +5770,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web3_proxy"
|
name = "web3_proxy"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argh",
|
"argh",
|
||||||
@ -5776,6 +5785,7 @@ dependencies = [
|
|||||||
"entities",
|
"entities",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"ethers",
|
"ethers",
|
||||||
|
"ewma",
|
||||||
"fdlimit",
|
"fdlimit",
|
||||||
"flume",
|
"flume",
|
||||||
"futures",
|
"futures",
|
||||||
@ -5793,6 +5803,8 @@ dependencies = [
|
|||||||
"notify",
|
"notify",
|
||||||
"num",
|
"num",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"once_cell",
|
||||||
|
"ordered-float",
|
||||||
"pagerduty-rs",
|
"pagerduty-rs",
|
||||||
"parking_lot 0.12.1",
|
"parking_lot 0.12.1",
|
||||||
"prettytable",
|
"prettytable",
|
||||||
|
11
Jenkinsfile
vendored
11
Jenkinsfile
vendored
@ -1,19 +1,20 @@
|
|||||||
def buildAndPush() {
|
def buildAndPush() {
|
||||||
|
// env.ARCH is the system architecture. some apps can be generic (amd64, arm64),
|
||||||
|
// but apps that compile for specific hardware (like web3-proxy) will need more specific tags (amd64_epyc2, arm64_graviton2, intel_xeon3, etc.)
|
||||||
// env.BRANCH_NAME is set to the git branch name by default
|
// env.BRANCH_NAME is set to the git branch name by default
|
||||||
// env.REGISTRY is the repository url for this pipeline
|
// env.REGISTRY is the repository url for this pipeline
|
||||||
// env.GIT_SHORT is the git short hash of the currently checked out repo
|
// env.GIT_SHORT is the git short hash of the currently checked out repo
|
||||||
// env.LATEST_BRANCH is the branch name that gets tagged latest
|
// env.LATEST_BRANCH is the branch name that gets tagged latest
|
||||||
// env.ARCH is the system architecture. some apps can be generic (amd64, arm64),
|
|
||||||
// but apps that compile for specific hardware (like web3-proxy) will need more specific tags (amd64_epyc2, arm64_graviton2, intel_xeon3, etc.)
|
|
||||||
|
|
||||||
// TODO: check that this system actually matches the given arch
|
// TODO: check that this system actually matches the given arch
|
||||||
sh '''#!/bin/bash
|
sh '''#!/bin/bash
|
||||||
set -eux -o pipefail
|
set -eux -o pipefail
|
||||||
|
|
||||||
[ -n "$GIT_SHORT" ]
|
|
||||||
[ -n "$GIT_SHORT" ]
|
|
||||||
[ -n "$REGISTRY" ]
|
|
||||||
[ -n "$ARCH" ]
|
[ -n "$ARCH" ]
|
||||||
|
[ -n "$BRANCH_NAME" ]
|
||||||
|
[ -n "$REGISTRY" ]
|
||||||
|
[ -n "$GIT_SHORT" ]
|
||||||
|
[ -n "$LATEST_BRANCH" ]
|
||||||
|
|
||||||
# deterministic mtime on .git keeps Dockerfiles that do 'ADD . .' or similar
|
# deterministic mtime on .git keeps Dockerfiles that do 'ADD . .' or similar
|
||||||
# without this, the build process always thinks the directory has changes
|
# without this, the build process always thinks the directory has changes
|
||||||
|
12
TODO.md
12
TODO.md
@ -330,6 +330,11 @@ These are not yet ordered. There might be duplicates. We might not actually need
|
|||||||
- [x] block all admin_ rpc commands
|
- [x] block all admin_ rpc commands
|
||||||
- [x] remove the "metered" crate now that we save aggregate queries?
|
- [x] remove the "metered" crate now that we save aggregate queries?
|
||||||
- [x] add archive depth to app config
|
- [x] add archive depth to app config
|
||||||
|
- [x] use from_block and to_block so that eth_getLogs is routed correctly
|
||||||
|
- [x] improve eth_sendRawTransaction server selection
|
||||||
|
- [x] don't cache methods that are usually very large
|
||||||
|
- [x] use http provider when available
|
||||||
|
- [ ] don't use new_head_provider anywhere except new head subscription
|
||||||
- [-] proxy mode for benchmarking all backends
|
- [-] proxy mode for benchmarking all backends
|
||||||
- [-] proxy mode for sending to multiple backends
|
- [-] proxy mode for sending to multiple backends
|
||||||
- [-] let users choose a % of reverts to log (or maybe x/second). someone like curve logging all reverts will be a BIG database very quickly
|
- [-] let users choose a % of reverts to log (or maybe x/second). someone like curve logging all reverts will be a BIG database very quickly
|
||||||
@ -339,6 +344,12 @@ These are not yet ordered. There might be duplicates. We might not actually need
|
|||||||
- [-] add configurable size limits to all the Caches
|
- [-] add configurable size limits to all the Caches
|
||||||
- instead of configuring each cache with MB sizes, have one value for total memory footprint and then percentages for each cache
|
- instead of configuring each cache with MB sizes, have one value for total memory footprint and then percentages for each cache
|
||||||
- https://github.com/moka-rs/moka/issues/201
|
- https://github.com/moka-rs/moka/issues/201
|
||||||
|
- [ ] have multiple providers on each backend rpc. one websocket for newHeads. and then http providers for handling requests
|
||||||
|
- erigon only streams the JSON over HTTP. that code isn't enabled for websockets. so this should save memory on the erigon servers
|
||||||
|
- i think this also means we don't need to worry about changing the id that the user gives us.
|
||||||
|
- have the healthcheck get the block over http. if it errors, or doesn't match what the websocket says, something is wrong (likely a deadlock in the websocket code)
|
||||||
|
- [ ] maybe we shouldn't route eth_getLogs to syncing nodes. serving queries slows down sync significantly
|
||||||
|
- change the send_best function to only include servers that are at least close to fully synced
|
||||||
- [ ] have private transactions be enabled by a url setting rather than a setting on the key
|
- [ ] have private transactions be enabled by a url setting rather than a setting on the key
|
||||||
- [ ] cli for adding rpc keys to an existing user
|
- [ ] cli for adding rpc keys to an existing user
|
||||||
- [ ] rate limiting/throttling on query_user_stats
|
- [ ] rate limiting/throttling on query_user_stats
|
||||||
@ -349,6 +360,7 @@ These are not yet ordered. There might be duplicates. We might not actually need
|
|||||||
- if total difficulty is not on the block and we aren't on ETH, fetch the full block instead of just the header
|
- if total difficulty is not on the block and we aren't on ETH, fetch the full block instead of just the header
|
||||||
- if total difficulty is set and non-zero, use it for consensus instead of just the number
|
- if total difficulty is set and non-zero, use it for consensus instead of just the number
|
||||||
- [ ] query_user_stats cache hit rate
|
- [ ] query_user_stats cache hit rate
|
||||||
|
- [ ] need debounce on reconnect. websockets are closing on us and then we reconnect twice. locks on ProviderState need more thought
|
||||||
- [ ] having the whole block in status is very verbose. trim it down
|
- [ ] having the whole block in status is very verbose. trim it down
|
||||||
- [ ] `cost estimate` script
|
- [ ] `cost estimate` script
|
||||||
- sum bytes and number of requests. prompt hosting costs. divide
|
- sum bytes and number of requests. prompt hosting costs. divide
|
||||||
|
@ -52,50 +52,50 @@ response_cache_max_bytes = 10_000_000_000
|
|||||||
|
|
||||||
[balanced_rpcs.ankr]
|
[balanced_rpcs.ankr]
|
||||||
display_name = "Ankr"
|
display_name = "Ankr"
|
||||||
url = "https://rpc.ankr.com/eth"
|
http_url = "https://rpc.ankr.com/eth"
|
||||||
soft_limit = 1_000
|
soft_limit = 1_000
|
||||||
tier = 0
|
tier = 0
|
||||||
|
|
||||||
[balanced_rpcs.cloudflare]
|
[balanced_rpcs.cloudflare]
|
||||||
display_name = "Cloudflare"
|
display_name = "Cloudflare"
|
||||||
url = "https://cloudflare-eth.com"
|
http_url = "https://cloudflare-eth.com"
|
||||||
soft_limit = 1_000
|
soft_limit = 1_000
|
||||||
tier = 1
|
tier = 1
|
||||||
|
|
||||||
[balanced_rpcs.blastapi]
|
[balanced_rpcs.blastapi]
|
||||||
display_name = "Blast"
|
display_name = "Blast"
|
||||||
url = "https://eth-mainnet.public.blastapi.io"
|
http_url = "https://eth-mainnet.public.blastapi.io"
|
||||||
soft_limit = 1_000
|
soft_limit = 1_000
|
||||||
tier = 1
|
tier = 1
|
||||||
|
|
||||||
[balanced_rpcs.mycryptoapi]
|
[balanced_rpcs.mycryptoapi]
|
||||||
display_name = "MyCrypto"
|
display_name = "MyCrypto"
|
||||||
disabled = true
|
disabled = true
|
||||||
url = "https://api.mycryptoapi.com/eth"
|
http_url = "https://api.mycryptoapi.com/eth"
|
||||||
soft_limit = 1_000
|
soft_limit = 1_000
|
||||||
tier = 2
|
tier = 2
|
||||||
|
|
||||||
[balanced_rpcs.pokt-v1]
|
[balanced_rpcs.pokt-v1]
|
||||||
display_name = "Pokt #1"
|
display_name = "Pokt #1"
|
||||||
url = "https://eth-mainnet.gateway.pokt.network/v1/5f3453978e354ab992c4da79"
|
http_url = "https://eth-mainnet.gateway.pokt.network/v1/5f3453978e354ab992c4da79"
|
||||||
soft_limit = 500
|
soft_limit = 500
|
||||||
tier = 2
|
tier = 2
|
||||||
|
|
||||||
[balanced_rpcs.pokt]
|
[balanced_rpcs.pokt]
|
||||||
display_name = "Pokt #2"
|
display_name = "Pokt #2"
|
||||||
url = "https://eth-rpc.gateway.pokt.network"
|
http_url = "https://eth-rpc.gateway.pokt.network"
|
||||||
soft_limit = 500
|
soft_limit = 500
|
||||||
tier = 3
|
tier = 3
|
||||||
|
|
||||||
[balanced_rpcs.linkpool]
|
[balanced_rpcs.linkpool]
|
||||||
display_name = "Linkpool"
|
display_name = "Linkpool"
|
||||||
url = "https://main-rpc.linkpool.io"
|
http_url = "https://main-rpc.linkpool.io"
|
||||||
soft_limit = 500
|
soft_limit = 500
|
||||||
tier = 4
|
tier = 4
|
||||||
|
|
||||||
[balanced_rpcs.runonflux]
|
[balanced_rpcs.runonflux]
|
||||||
display_name = "Run on Flux (light)"
|
display_name = "Run on Flux (light)"
|
||||||
url = "https://ethereumnodelight.app.runonflux.io"
|
http_url = "https://ethereumnodelight.app.runonflux.io"
|
||||||
soft_limit = 1_000
|
soft_limit = 1_000
|
||||||
tier = 5
|
tier = 5
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ response_cache_max_bytes = 10_000_000_000
|
|||||||
[balanced_rpcs.linkpool-light]
|
[balanced_rpcs.linkpool-light]
|
||||||
display_name = "Linkpool (light)"
|
display_name = "Linkpool (light)"
|
||||||
disabled = true
|
disabled = true
|
||||||
url = "https://main-light.eth.linkpool.io"
|
http_url = "https://main-light.eth.linkpool.io"
|
||||||
soft_limit = 100
|
soft_limit = 100
|
||||||
tier = 5
|
tier = 5
|
||||||
|
|
||||||
@ -114,34 +114,34 @@ response_cache_max_bytes = 10_000_000_000
|
|||||||
[private_rpcs.eden]
|
[private_rpcs.eden]
|
||||||
disabled = true
|
disabled = true
|
||||||
display_name = "Eden network"
|
display_name = "Eden network"
|
||||||
url = "https://api.edennetwork.io/v1/"
|
http_url = "https://api.edennetwork.io/v1/"
|
||||||
soft_limit = 1_805
|
soft_limit = 1_805
|
||||||
tier = 0
|
tier = 0
|
||||||
|
|
||||||
[private_rpcs.eden_beta]
|
[private_rpcs.eden_beta]
|
||||||
disabled = true
|
disabled = true
|
||||||
display_name = "Eden network beta"
|
display_name = "Eden network beta"
|
||||||
url = "https://api.edennetwork.io/v1/beta"
|
http_url = "https://api.edennetwork.io/v1/beta"
|
||||||
soft_limit = 5_861
|
soft_limit = 5_861
|
||||||
tier = 0
|
tier = 0
|
||||||
|
|
||||||
[private_rpcs.ethermine]
|
[private_rpcs.ethermine]
|
||||||
disabled = true
|
disabled = true
|
||||||
display_name = "Ethermine"
|
display_name = "Ethermine"
|
||||||
url = "https://rpc.ethermine.org"
|
http_url = "https://rpc.ethermine.org"
|
||||||
soft_limit = 5_861
|
soft_limit = 5_861
|
||||||
tier = 0
|
tier = 0
|
||||||
|
|
||||||
[private_rpcs.flashbots]
|
[private_rpcs.flashbots]
|
||||||
disabled = true
|
disabled = true
|
||||||
display_name = "Flashbots Fast"
|
display_name = "Flashbots Fast"
|
||||||
url = "https://rpc.flashbots.net/fast"
|
http_url = "https://rpc.flashbots.net/fast"
|
||||||
soft_limit = 7_074
|
soft_limit = 7_074
|
||||||
tier = 0
|
tier = 0
|
||||||
|
|
||||||
[private_rpcs.securerpc]
|
[private_rpcs.securerpc]
|
||||||
disabled = true
|
disabled = true
|
||||||
display_name = "SecureRPC"
|
display_name = "SecureRPC"
|
||||||
url = "https://gibson.securerpc.com/v1"
|
http_url = "https://gibson.securerpc.com/v1"
|
||||||
soft_limit = 4_560
|
soft_limit = 4_560
|
||||||
tier = 0
|
tier = 0
|
||||||
|
@ -16,17 +16,26 @@ response_cache_max_bytes = 1_000_000_000
|
|||||||
|
|
||||||
[balanced_rpcs]
|
[balanced_rpcs]
|
||||||
|
|
||||||
[balanced_rpcs.llama_public_wss]
|
[balanced_rpcs.llama_public_both]
|
||||||
# TODO: what should we do if all rpcs are disabled? warn and wait for a config change?
|
# TODO: what should we do if all rpcs are disabled? warn and wait for a config change?
|
||||||
disabled = false
|
disabled = false
|
||||||
display_name = "LlamaNodes WSS"
|
display_name = "LlamaNodes Both"
|
||||||
url = "wss://eth.llamarpc.com/"
|
ws_url = "wss://eth.llamarpc.com/"
|
||||||
|
http_url = "https://eth.llamarpc.com/"
|
||||||
soft_limit = 1_000
|
soft_limit = 1_000
|
||||||
tier = 0
|
tier = 0
|
||||||
|
|
||||||
[balanced_rpcs.llama_public_https]
|
[balanced_rpcs.llama_public_https]
|
||||||
disabled = false
|
disabled = false
|
||||||
display_name = "LlamaNodes HTTPS"
|
display_name = "LlamaNodes HTTPS"
|
||||||
url = "https://eth.llamarpc.com/"
|
http_url = "https://eth.llamarpc.com/"
|
||||||
|
soft_limit = 1_000
|
||||||
|
tier = 0
|
||||||
|
|
||||||
|
[balanced_rpcs.llama_public_wss]
|
||||||
|
# TODO: what should we do if all rpcs are disabled? warn and wait for a config change?
|
||||||
|
disabled = false
|
||||||
|
display_name = "LlamaNodes WSS"
|
||||||
|
ws_url = "wss://eth.llamarpc.com/"
|
||||||
soft_limit = 1_000
|
soft_limit = 1_000
|
||||||
tier = 0
|
tier = 0
|
||||||
|
@ -10,5 +10,5 @@ redis-rate-limiter = { path = "../redis-rate-limiter" }
|
|||||||
anyhow = "1.0.69"
|
anyhow = "1.0.69"
|
||||||
hashbrown = "0.13.2"
|
hashbrown = "0.13.2"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
moka = { version = "0.9.7", default-features = false, features = ["future"] }
|
moka = { version = "0.10.0", default-features = false, features = ["future"] }
|
||||||
tokio = "1.25.0"
|
tokio = "1.25.0"
|
||||||
|
10
docs/curl login.md
Normal file
10
docs/curl login.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# log in with curl
|
||||||
|
|
||||||
|
1. curl http://127.0.0.1:8544/user/login/$ADDRESS
|
||||||
|
2. Sign the text with a site like https://www.myetherwallet.com/wallet/sign
|
||||||
|
3. POST the signed data:
|
||||||
|
|
||||||
|
curl -X POST http://127.0.0.1:8544/user/login -H 'Content-Type: application/json' -d
|
||||||
|
'{ "address": "0x9eb9e3dc2543dc9ff4058e2a2da43a855403f1fd", "msg": "0x6c6c616d616e6f6465732e636f6d2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078396562396533646332353433646339464634303538653241324441343341383535343033463166440a0af09fa699f09fa699f09fa699f09fa699f09fa6990a0a5552493a2068747470733a2f2f6c6c616d616e6f6465732e636f6d2f0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a203031474d37373330375344324448333854454d3957545156454a0a4973737565642041743a20323032322d31322d31345430323a32333a31372e3735333736335a0a45787069726174696f6e2054696d653a20323032322d31322d31345430323a34333a31372e3735333736335a", "sig": "16bac055345279723193737c6c67cf995e821fd7c038d31fd6f671102088c7b85ab4b13069fd2ed02da186cf549530e315d8d042d721bf81289b3ffdbe8cf9ce1c", "version": "3", "signer": "MEW" }'
|
||||||
|
|
||||||
|
4. The response will include a bearer token. Use it with curl ... -H 'Authorization: Bearer $TOKEN'
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "web3_proxy"
|
name = "web3_proxy"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "web3_proxy_cli"
|
default-run = "web3_proxy_cli"
|
||||||
|
|
||||||
@ -27,9 +27,9 @@ thread-fast-rng = { path = "../thread-fast-rng" }
|
|||||||
|
|
||||||
anyhow = { version = "1.0.69", features = ["backtrace"] }
|
anyhow = { version = "1.0.69", features = ["backtrace"] }
|
||||||
argh = "0.1.10"
|
argh = "0.1.10"
|
||||||
axum = { version = "0.6.4", features = ["headers", "ws"] }
|
axum = { version = "0.6.6", features = ["headers", "ws"] }
|
||||||
axum-client-ip = "0.4.0"
|
axum-client-ip = "0.4.0"
|
||||||
axum-macros = "0.3.2"
|
axum-macros = "0.3.4"
|
||||||
chrono = "0.4.23"
|
chrono = "0.4.23"
|
||||||
counter = "0.5.7"
|
counter = "0.5.7"
|
||||||
derive_more = "0.99.17"
|
derive_more = "0.99.17"
|
||||||
@ -48,10 +48,11 @@ http = "0.2.8"
|
|||||||
ipnet = "2.7.1"
|
ipnet = "2.7.1"
|
||||||
itertools = "0.10.5"
|
itertools = "0.10.5"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
moka = { version = "0.9.7", default-features = false, features = ["future"] }
|
moka = { version = "0.10.0", default-features = false, features = ["future"] }
|
||||||
notify = "5.1.0"
|
notify = "5.1.0"
|
||||||
num = "0.4.0"
|
num = "0.4.0"
|
||||||
num-traits = "0.2.15"
|
num-traits = "0.2.15"
|
||||||
|
once_cell = { version = "1.17.1" }
|
||||||
pagerduty-rs = { version = "0.1.6", default-features = false, features = ["async", "rustls", "sync"] }
|
pagerduty-rs = { version = "0.1.6", default-features = false, features = ["async", "rustls", "sync"] }
|
||||||
parking_lot = { version = "0.12.1", features = ["arc_lock"] }
|
parking_lot = { version = "0.12.1", features = ["arc_lock"] }
|
||||||
prettytable = "*"
|
prettytable = "*"
|
||||||
@ -62,7 +63,7 @@ rustc-hash = "1.1.0"
|
|||||||
sentry = { version = "0.29.3", default-features = false, features = ["backtrace", "contexts", "panic", "anyhow", "reqwest", "rustls", "log", "sentry-log"] }
|
sentry = { version = "0.29.3", default-features = false, features = ["backtrace", "contexts", "panic", "anyhow", "reqwest", "rustls", "log", "sentry-log"] }
|
||||||
serde = { version = "1.0.152", features = [] }
|
serde = { version = "1.0.152", features = [] }
|
||||||
serde_json = { version = "1.0.93", default-features = false, features = ["alloc", "raw_value"] }
|
serde_json = { version = "1.0.93", default-features = false, features = ["alloc", "raw_value"] }
|
||||||
serde_prometheus = "0.2.0"
|
serde_prometheus = "0.2.1"
|
||||||
siwe = "0.5.0"
|
siwe = "0.5.0"
|
||||||
time = "0.3.17"
|
time = "0.3.17"
|
||||||
tokio = { version = "1.25.0", features = ["full"] }
|
tokio = { version = "1.25.0", features = ["full"] }
|
||||||
@ -73,3 +74,5 @@ tower-http = { version = "0.3.5", features = ["cors", "sensitive-headers"] }
|
|||||||
ulid = { version = "1.0.0", features = ["serde"] }
|
ulid = { version = "1.0.0", features = ["serde"] }
|
||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
uuid = "1.3.0"
|
uuid = "1.3.0"
|
||||||
|
ewma = "0.1.1"
|
||||||
|
ordered-float = "3.4.0"
|
||||||
|
@ -10,7 +10,7 @@ use crate::frontend::rpc_proxy_ws::ProxyMode;
|
|||||||
use crate::jsonrpc::{
|
use crate::jsonrpc::{
|
||||||
JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest, JsonRpcRequestEnum,
|
JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest, JsonRpcRequestEnum,
|
||||||
};
|
};
|
||||||
use crate::rpcs::blockchain::{ArcBlock, SavedBlock};
|
use crate::rpcs::blockchain::{BlockHashesCache, Web3ProxyBlock};
|
||||||
use crate::rpcs::many::Web3Rpcs;
|
use crate::rpcs::many::Web3Rpcs;
|
||||||
use crate::rpcs::one::Web3Rpc;
|
use crate::rpcs::one::Web3Rpc;
|
||||||
use crate::rpcs::transactions::TxStatus;
|
use crate::rpcs::transactions::TxStatus;
|
||||||
@ -23,7 +23,7 @@ use derive_more::From;
|
|||||||
use entities::sea_orm_active_enums::LogLevel;
|
use entities::sea_orm_active_enums::LogLevel;
|
||||||
use entities::user;
|
use entities::user;
|
||||||
use ethers::core::utils::keccak256;
|
use ethers::core::utils::keccak256;
|
||||||
use ethers::prelude::{Address, Block, Bytes, Transaction, TxHash, H256, U64};
|
use ethers::prelude::{Address, Bytes, Transaction, TxHash, H256, U64};
|
||||||
use ethers::types::U256;
|
use ethers::types::U256;
|
||||||
use ethers::utils::rlp::{Decodable, Rlp};
|
use ethers::utils::rlp::{Decodable, Rlp};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
@ -69,9 +69,9 @@ pub static REQUEST_PERIOD: u64 = 60;
|
|||||||
#[derive(From)]
|
#[derive(From)]
|
||||||
struct ResponseCacheKey {
|
struct ResponseCacheKey {
|
||||||
// if none, this is cached until evicted
|
// if none, this is cached until evicted
|
||||||
from_block: Option<SavedBlock>,
|
from_block: Option<Web3ProxyBlock>,
|
||||||
// to_block is only set when ranges of blocks are requested (like with eth_getLogs)
|
// to_block is only set when ranges of blocks are requested (like with eth_getLogs)
|
||||||
to_block: Option<SavedBlock>,
|
to_block: Option<Web3ProxyBlock>,
|
||||||
method: String,
|
method: String,
|
||||||
// TODO: better type for this
|
// TODO: better type for this
|
||||||
params: Option<serde_json::Value>,
|
params: Option<serde_json::Value>,
|
||||||
@ -204,7 +204,7 @@ pub struct Web3ProxyApp {
|
|||||||
response_cache: ResponseCache,
|
response_cache: ResponseCache,
|
||||||
// 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?
|
||||||
watch_consensus_head_receiver: watch::Receiver<ArcBlock>,
|
watch_consensus_head_receiver: watch::Receiver<Option<Web3ProxyBlock>>,
|
||||||
pending_tx_sender: broadcast::Sender<TxStatus>,
|
pending_tx_sender: broadcast::Sender<TxStatus>,
|
||||||
pub config: AppConfig,
|
pub config: AppConfig,
|
||||||
pub db_conn: Option<sea_orm::DatabaseConnection>,
|
pub db_conn: Option<sea_orm::DatabaseConnection>,
|
||||||
@ -482,7 +482,7 @@ impl Web3ProxyApp {
|
|||||||
let http_client = Some(
|
let http_client = Some(
|
||||||
reqwest::ClientBuilder::new()
|
reqwest::ClientBuilder::new()
|
||||||
.connect_timeout(Duration::from_secs(5))
|
.connect_timeout(Duration::from_secs(5))
|
||||||
.timeout(Duration::from_secs(60))
|
.timeout(Duration::from_secs(5 * 60))
|
||||||
.user_agent(APP_USER_AGENT)
|
.user_agent(APP_USER_AGENT)
|
||||||
.build()?,
|
.build()?,
|
||||||
);
|
);
|
||||||
@ -541,8 +541,7 @@ impl Web3ProxyApp {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// TODO: i don't like doing Block::default here! Change this to "None"?
|
// TODO: i don't like doing Block::default here! Change this to "None"?
|
||||||
let (watch_consensus_head_sender, watch_consensus_head_receiver) =
|
let (watch_consensus_head_sender, watch_consensus_head_receiver) = watch::channel(None);
|
||||||
watch::channel(Arc::new(Block::default()));
|
|
||||||
// TODO: will one receiver lagging be okay? how big should this be?
|
// TODO: will one receiver lagging be okay? how big should this be?
|
||||||
let (pending_tx_sender, pending_tx_receiver) = broadcast::channel(256);
|
let (pending_tx_sender, pending_tx_receiver) = broadcast::channel(256);
|
||||||
|
|
||||||
@ -557,33 +556,40 @@ impl Web3ProxyApp {
|
|||||||
// TODO: ttl on this? or is max_capacity fine?
|
// TODO: ttl on this? or is max_capacity fine?
|
||||||
let pending_transactions = Cache::builder()
|
let pending_transactions = Cache::builder()
|
||||||
.max_capacity(10_000)
|
.max_capacity(10_000)
|
||||||
|
// TODO: different chains might handle this differently
|
||||||
|
// TODO: what should we set? 5 minutes is arbitrary. the nodes themselves hold onto transactions for much longer
|
||||||
|
.time_to_idle(Duration::from_secs(300))
|
||||||
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::default());
|
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::default());
|
||||||
|
|
||||||
// keep 1GB of blocks in the cache
|
// keep 1GB/5 minutes of blocks in the cache
|
||||||
// TODO: limits from config
|
// TODO: limits from config
|
||||||
// these blocks don't have full transactions, but they do have rather variable amounts of transaction hashes
|
// these blocks don't have full transactions, but they do have rather variable amounts of transaction hashes
|
||||||
// TODO: how can we do the weigher better?
|
// TODO: how can we do the weigher better?
|
||||||
let block_map = Cache::builder()
|
let block_map: BlockHashesCache = Cache::builder()
|
||||||
.max_capacity(1024 * 1024 * 1024)
|
.max_capacity(1024 * 1024 * 1024)
|
||||||
.weigher(|_k, v: &ArcBlock| {
|
.weigher(|_k, v: &Web3ProxyBlock| {
|
||||||
// TODO: is this good enough?
|
// TODO: is this good enough?
|
||||||
1 + v.transactions.len().try_into().unwrap_or(u32::MAX)
|
1 + v.block.transactions.len().try_into().unwrap_or(u32::MAX)
|
||||||
})
|
})
|
||||||
|
// TODO: what should we set? 5 minutes is arbitrary. the nodes themselves hold onto transactions for much longer
|
||||||
|
.time_to_idle(Duration::from_secs(300))
|
||||||
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::default());
|
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::default());
|
||||||
|
|
||||||
// connect to the load balanced rpcs
|
// connect to the load balanced rpcs
|
||||||
let (balanced_rpcs, balanced_handle) = Web3Rpcs::spawn(
|
let (balanced_rpcs, balanced_handle) = Web3Rpcs::spawn(
|
||||||
|
block_map.clone(),
|
||||||
top_config.app.chain_id,
|
top_config.app.chain_id,
|
||||||
db_conn.clone(),
|
db_conn.clone(),
|
||||||
balanced_rpcs,
|
|
||||||
http_client.clone(),
|
http_client.clone(),
|
||||||
vredis_pool.clone(),
|
top_config.app.max_block_age,
|
||||||
block_map.clone(),
|
top_config.app.max_block_lag,
|
||||||
Some(watch_consensus_head_sender),
|
|
||||||
top_config.app.min_sum_soft_limit,
|
|
||||||
top_config.app.min_synced_rpcs,
|
top_config.app.min_synced_rpcs,
|
||||||
Some(pending_tx_sender.clone()),
|
top_config.app.min_sum_soft_limit,
|
||||||
pending_transactions.clone(),
|
pending_transactions.clone(),
|
||||||
|
Some(pending_tx_sender.clone()),
|
||||||
|
vredis_pool.clone(),
|
||||||
|
balanced_rpcs,
|
||||||
|
Some(watch_consensus_head_sender),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.context("spawning balanced rpcs")?;
|
.context("spawning balanced rpcs")?;
|
||||||
@ -599,26 +605,30 @@ impl Web3ProxyApp {
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let (private_rpcs, private_handle) = Web3Rpcs::spawn(
|
let (private_rpcs, private_handle) = Web3Rpcs::spawn(
|
||||||
|
block_map,
|
||||||
top_config.app.chain_id,
|
top_config.app.chain_id,
|
||||||
db_conn.clone(),
|
db_conn.clone(),
|
||||||
private_rpcs,
|
|
||||||
http_client.clone(),
|
http_client.clone(),
|
||||||
|
// private rpcs don't get subscriptions, so no need for max_block_age or max_block_lag
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
pending_transactions.clone(),
|
||||||
|
// TODO: subscribe to pending transactions on the private rpcs? they seem to have low rate limits, but they should have
|
||||||
|
None,
|
||||||
vredis_pool.clone(),
|
vredis_pool.clone(),
|
||||||
block_map,
|
private_rpcs,
|
||||||
// subscribing to new heads here won't work well. if they are fast, they might be ahead of balanced_rpcs
|
// subscribing to new heads here won't work well. if they are fast, they might be ahead of balanced_rpcs
|
||||||
// they also often have low rate limits
|
// they also often have low rate limits
|
||||||
// however, they are well connected to miners/validators. so maybe using them as a safety check would be good
|
// however, they are well connected to miners/validators. so maybe using them as a safety check would be good
|
||||||
|
// TODO: but maybe we could include privates in the "backup" tier
|
||||||
None,
|
None,
|
||||||
0,
|
|
||||||
0,
|
|
||||||
// TODO: subscribe to pending transactions on the private rpcs? they seem to have low rate limits
|
|
||||||
None,
|
|
||||||
pending_transactions.clone(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.context("spawning private_rpcs")?;
|
.context("spawning private_rpcs")?;
|
||||||
|
|
||||||
if private_rpcs.conns.is_empty() {
|
if private_rpcs.by_name.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
// save the handle to catch any errors
|
// save the handle to catch any errors
|
||||||
@ -685,6 +695,8 @@ impl Web3ProxyApp {
|
|||||||
u32::MAX
|
u32::MAX
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// TODO: what should we set? 10 minutes is arbitrary. the nodes themselves hold onto transactions for much longer
|
||||||
|
.time_to_idle(Duration::from_secs(600))
|
||||||
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::default());
|
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::default());
|
||||||
|
|
||||||
// all the users are the same size, so no need for a weigher
|
// all the users are the same size, so no need for a weigher
|
||||||
@ -734,7 +746,7 @@ impl Web3ProxyApp {
|
|||||||
Ok((app, cancellable_handles, important_background_handles).into())
|
Ok((app, cancellable_handles, important_background_handles).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn head_block_receiver(&self) -> watch::Receiver<ArcBlock> {
|
pub fn head_block_receiver(&self) -> watch::Receiver<Option<Web3ProxyBlock>> {
|
||||||
self.watch_consensus_head_receiver.clone()
|
self.watch_consensus_head_receiver.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -932,7 +944,7 @@ impl Web3ProxyApp {
|
|||||||
JsonRpcRequestEnum::Single(request) => {
|
JsonRpcRequestEnum::Single(request) => {
|
||||||
let (response, rpcs) = timeout(
|
let (response, rpcs) = timeout(
|
||||||
max_time,
|
max_time,
|
||||||
self.proxy_cached_request(&authorization, request, proxy_mode),
|
self.proxy_cached_request(&authorization, request, proxy_mode, None),
|
||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
@ -965,10 +977,26 @@ impl Web3ProxyApp {
|
|||||||
|
|
||||||
// TODO: spawn so the requests go in parallel? need to think about rate limiting more if we do that
|
// TODO: spawn so the requests go in parallel? need to think about rate limiting more if we do that
|
||||||
// TODO: improve flattening
|
// TODO: improve flattening
|
||||||
|
|
||||||
|
// get the head block now so that any requests that need it all use the same block
|
||||||
|
// TODO: FrontendErrorResponse that handles "no servers synced" in a consistent way
|
||||||
|
// TODO: this still has an edge condition if there is a reorg in the middle of the request!!!
|
||||||
|
let head_block_num = self
|
||||||
|
.balanced_rpcs
|
||||||
|
.head_block_num()
|
||||||
|
.context(anyhow::anyhow!("no servers synced"))?;
|
||||||
|
|
||||||
let responses = join_all(
|
let responses = join_all(
|
||||||
requests
|
requests
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|request| self.proxy_cached_request(authorization, request, proxy_mode))
|
.map(|request| {
|
||||||
|
self.proxy_cached_request(
|
||||||
|
authorization,
|
||||||
|
request,
|
||||||
|
proxy_mode,
|
||||||
|
Some(head_block_num),
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@ -1017,6 +1045,7 @@ impl Web3ProxyApp {
|
|||||||
authorization: &Arc<Authorization>,
|
authorization: &Arc<Authorization>,
|
||||||
mut request: JsonRpcRequest,
|
mut request: JsonRpcRequest,
|
||||||
proxy_mode: ProxyMode,
|
proxy_mode: ProxyMode,
|
||||||
|
head_block_num: Option<U64>,
|
||||||
) -> Result<(JsonRpcForwardedResponse, Vec<Arc<Web3Rpc>>), FrontendErrorResponse> {
|
) -> Result<(JsonRpcForwardedResponse, Vec<Arc<Web3Rpc>>), FrontendErrorResponse> {
|
||||||
// trace!("Received request: {:?}", request);
|
// trace!("Received request: {:?}", request);
|
||||||
|
|
||||||
@ -1035,9 +1064,17 @@ impl Web3ProxyApp {
|
|||||||
| "db_getString"
|
| "db_getString"
|
||||||
| "db_putHex"
|
| "db_putHex"
|
||||||
| "db_putString"
|
| "db_putString"
|
||||||
|
| "debug_accountRange"
|
||||||
|
| "debug_backtraceAt"
|
||||||
|
| "debug_blockProfile"
|
||||||
| "debug_chaindbCompact"
|
| "debug_chaindbCompact"
|
||||||
|
| "debug_chaindbProperty"
|
||||||
|
| "debug_cpuProfile"
|
||||||
|
| "debug_freeOSMemory"
|
||||||
| "debug_freezeClient"
|
| "debug_freezeClient"
|
||||||
|
| "debug_gcStats"
|
||||||
| "debug_goTrace"
|
| "debug_goTrace"
|
||||||
|
| "debug_memStats"
|
||||||
| "debug_mutexProfile"
|
| "debug_mutexProfile"
|
||||||
| "debug_setBlockProfileRate"
|
| "debug_setBlockProfileRate"
|
||||||
| "debug_setGCPercent"
|
| "debug_setGCPercent"
|
||||||
@ -1125,7 +1162,7 @@ impl Web3ProxyApp {
|
|||||||
serde_json::Value::Array(vec![])
|
serde_json::Value::Array(vec![])
|
||||||
}
|
}
|
||||||
"eth_blockNumber" => {
|
"eth_blockNumber" => {
|
||||||
match self.balanced_rpcs.head_block_num() {
|
match head_block_num.or(self.balanced_rpcs.head_block_num()) {
|
||||||
Some(head_block_num) => {
|
Some(head_block_num) => {
|
||||||
json!(head_block_num)
|
json!(head_block_num)
|
||||||
}
|
}
|
||||||
@ -1138,9 +1175,7 @@ impl Web3ProxyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"eth_chainId" => {
|
"eth_chainId" => json!(U64::from(self.config.chain_id)),
|
||||||
json!(U64::from(self.config.chain_id))
|
|
||||||
}
|
|
||||||
// TODO: eth_callBundle (https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle)
|
// TODO: eth_callBundle (https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_callbundle)
|
||||||
// TODO: eth_cancelPrivateTransaction (https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_cancelprivatetransaction, but maybe just reject)
|
// TODO: eth_cancelPrivateTransaction (https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_cancelprivatetransaction, but maybe just reject)
|
||||||
// TODO: eth_sendPrivateTransaction (https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_sendprivatetransaction)
|
// TODO: eth_sendPrivateTransaction (https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#eth_sendprivatetransaction)
|
||||||
@ -1158,6 +1193,7 @@ impl Web3ProxyApp {
|
|||||||
request,
|
request,
|
||||||
Some(&request_metadata),
|
Some(&request_metadata),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -1193,7 +1229,7 @@ impl Web3ProxyApp {
|
|||||||
}
|
}
|
||||||
"eth_mining" => {
|
"eth_mining" => {
|
||||||
// no stats on this. its cheap
|
// no stats on this. its cheap
|
||||||
json!(false)
|
serde_json::Value::Bool(false)
|
||||||
}
|
}
|
||||||
// TODO: eth_sendBundle (flashbots command)
|
// TODO: eth_sendBundle (flashbots command)
|
||||||
// broadcast transactions to all private rpcs at once
|
// broadcast transactions to all private rpcs at once
|
||||||
@ -1222,12 +1258,19 @@ impl Web3ProxyApp {
|
|||||||
(&self.balanced_rpcs, default_num)
|
(&self.balanced_rpcs, default_num)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let head_block_num = head_block_num
|
||||||
|
.or(self.balanced_rpcs.head_block_num())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("no servers synced"))?;
|
||||||
|
|
||||||
|
// TODO: error/wait if no head block!
|
||||||
|
|
||||||
// try_send_all_upstream_servers puts the request id into the response. no need to do that ourselves here.
|
// try_send_all_upstream_servers puts the request id into the response. no need to do that ourselves here.
|
||||||
let mut response = private_rpcs
|
let mut response = private_rpcs
|
||||||
.try_send_all_synced_connections(
|
.try_send_all_synced_connections(
|
||||||
authorization,
|
authorization,
|
||||||
&request,
|
&request,
|
||||||
Some(request_metadata.clone()),
|
Some(request_metadata.clone()),
|
||||||
|
Some(&head_block_num),
|
||||||
None,
|
None,
|
||||||
Level::Trace,
|
Level::Trace,
|
||||||
num,
|
num,
|
||||||
@ -1318,7 +1361,7 @@ impl Web3ProxyApp {
|
|||||||
"eth_syncing" => {
|
"eth_syncing" => {
|
||||||
// no stats on this. its cheap
|
// no stats on this. its cheap
|
||||||
// TODO: return a real response if all backends are syncing or if no servers in sync
|
// TODO: return a real response if all backends are syncing or if no servers in sync
|
||||||
json!(false)
|
serde_json::Value::Bool(false)
|
||||||
}
|
}
|
||||||
"eth_subscribe" => {
|
"eth_subscribe" => {
|
||||||
return Ok((
|
return Ok((
|
||||||
@ -1343,12 +1386,12 @@ impl Web3ProxyApp {
|
|||||||
"net_listening" => {
|
"net_listening" => {
|
||||||
// no stats on this. its cheap
|
// no stats on this. its cheap
|
||||||
// TODO: only if there are some backends on balanced_rpcs?
|
// TODO: only if there are some backends on balanced_rpcs?
|
||||||
json!(true)
|
serde_json::Value::Bool(true)
|
||||||
}
|
}
|
||||||
"net_peerCount" => {
|
"net_peerCount" => {
|
||||||
// no stats on this. its cheap
|
// no stats on this. its cheap
|
||||||
// TODO: do something with proxy_mode here?
|
// TODO: do something with proxy_mode here?
|
||||||
self.balanced_rpcs.num_synced_rpcs().into()
|
json!(U64::from(self.balanced_rpcs.num_synced_rpcs()))
|
||||||
}
|
}
|
||||||
"web3_clientVersion" => {
|
"web3_clientVersion" => {
|
||||||
// no stats on this. its cheap
|
// no stats on this. its cheap
|
||||||
@ -1422,9 +1465,8 @@ impl Web3ProxyApp {
|
|||||||
// emit stats
|
// emit stats
|
||||||
|
|
||||||
// TODO: if no servers synced, wait for them to be synced? probably better to error and let haproxy retry another server
|
// TODO: if no servers synced, wait for them to be synced? probably better to error and let haproxy retry another server
|
||||||
let head_block_num = self
|
let head_block_num = head_block_num
|
||||||
.balanced_rpcs
|
.or(self.balanced_rpcs.head_block_num())
|
||||||
.head_block_num()
|
|
||||||
.context("no servers synced")?;
|
.context("no servers synced")?;
|
||||||
|
|
||||||
// we do this check before checking caches because it might modify the request params
|
// we do this check before checking caches because it might modify the request params
|
||||||
@ -1468,7 +1510,7 @@ impl Web3ProxyApp {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Some(ResponseCacheKey {
|
Some(ResponseCacheKey {
|
||||||
from_block: Some(SavedBlock::new(request_block)),
|
from_block: Some(request_block),
|
||||||
to_block: None,
|
to_block: None,
|
||||||
method: method.to_string(),
|
method: method.to_string(),
|
||||||
// TODO: hash here?
|
// TODO: hash here?
|
||||||
@ -1508,8 +1550,8 @@ impl Web3ProxyApp {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Some(ResponseCacheKey {
|
Some(ResponseCacheKey {
|
||||||
from_block: Some(SavedBlock::new(from_block)),
|
from_block: Some(from_block),
|
||||||
to_block: Some(SavedBlock::new(to_block)),
|
to_block: Some(to_block),
|
||||||
method: method.to_string(),
|
method: method.to_string(),
|
||||||
// TODO: hash here?
|
// TODO: hash here?
|
||||||
params: request.params.clone(),
|
params: request.params.clone(),
|
||||||
@ -1524,7 +1566,8 @@ impl Web3ProxyApp {
|
|||||||
let authorization = authorization.clone();
|
let authorization = authorization.clone();
|
||||||
|
|
||||||
if let Some(cache_key) = cache_key {
|
if let Some(cache_key) = cache_key {
|
||||||
let from_block_num = cache_key.from_block.as_ref().map(|x| x.number());
|
let from_block_num = cache_key.from_block.as_ref().map(|x| *x.number());
|
||||||
|
let to_block_num = cache_key.to_block.as_ref().map(|x| *x.number());
|
||||||
|
|
||||||
self.response_cache
|
self.response_cache
|
||||||
.try_get_with(cache_key, async move {
|
.try_get_with(cache_key, async move {
|
||||||
@ -1537,6 +1580,7 @@ impl Web3ProxyApp {
|
|||||||
request,
|
request,
|
||||||
Some(&request_metadata),
|
Some(&request_metadata),
|
||||||
from_block_num.as_ref(),
|
from_block_num.as_ref(),
|
||||||
|
to_block_num.as_ref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -1545,7 +1589,7 @@ impl Web3ProxyApp {
|
|||||||
|
|
||||||
// TODO: only cache the inner response
|
// TODO: only cache the inner response
|
||||||
// TODO: how are we going to stream this?
|
// TODO: how are we going to stream this?
|
||||||
// TODO: check response size. if its very large, return it in a custom Error type that bypasses caching
|
// TODO: check response size. if its very large, return it in a custom Error type that bypasses caching? or will moka do that for us?
|
||||||
Ok::<_, anyhow::Error>(response)
|
Ok::<_, anyhow::Error>(response)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@ -1565,6 +1609,7 @@ impl Web3ProxyApp {
|
|||||||
request,
|
request,
|
||||||
Some(&request_metadata),
|
Some(&request_metadata),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,12 @@ impl Web3ProxyApp {
|
|||||||
);
|
);
|
||||||
|
|
||||||
while let Some(new_head) = head_block_receiver.next().await {
|
while let Some(new_head) = head_block_receiver.next().await {
|
||||||
|
let new_head = if let Some(new_head) = new_head {
|
||||||
|
new_head
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: what should the payload for RequestMetadata be?
|
// TODO: what should the payload for RequestMetadata be?
|
||||||
let request_metadata =
|
let request_metadata =
|
||||||
Arc::new(RequestMetadata::new(REQUEST_PERIOD, 0).unwrap());
|
Arc::new(RequestMetadata::new(REQUEST_PERIOD, 0).unwrap());
|
||||||
@ -72,7 +78,7 @@ impl Web3ProxyApp {
|
|||||||
"params": {
|
"params": {
|
||||||
"subscription": subscription_id,
|
"subscription": subscription_id,
|
||||||
// TODO: option to include full transaction objects instead of just the hashes?
|
// TODO: option to include full transaction objects instead of just the hashes?
|
||||||
"result": new_head.as_ref(),
|
"result": new_head.block,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
22
web3_proxy/src/atomics.rs
Normal file
22
web3_proxy/src/atomics.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
pub struct AtomicF64 {
|
||||||
|
storage: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AtomicF64 {
|
||||||
|
pub fn new(value: f64) -> Self {
|
||||||
|
let as_u64 = value.to_bits();
|
||||||
|
Self {
|
||||||
|
storage: AtomicU64::new(as_u64),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn store(&self, value: f64, ordering: Ordering) {
|
||||||
|
let as_u64 = value.to_bits();
|
||||||
|
self.storage.store(as_u64, ordering)
|
||||||
|
}
|
||||||
|
pub fn load(&self, ordering: Ordering) -> f64 {
|
||||||
|
let as_u64 = self.storage.load(ordering);
|
||||||
|
f64::from_bits(as_u64)
|
||||||
|
}
|
||||||
|
}
|
@ -64,7 +64,7 @@ async fn run(
|
|||||||
));
|
));
|
||||||
|
|
||||||
// wait until the app has seen its first consensus head block
|
// wait until the app has seen its first consensus head block
|
||||||
// TODO: if backups were included, wait a little longer
|
// TODO: if backups were included, wait a little longer?
|
||||||
let _ = spawned_app.app.head_block_receiver().changed().await;
|
let _ = spawned_app.app.head_block_receiver().changed().await;
|
||||||
|
|
||||||
// start the frontend port
|
// start the frontend port
|
||||||
@ -205,31 +205,27 @@ mod tests {
|
|||||||
(
|
(
|
||||||
"anvil".to_string(),
|
"anvil".to_string(),
|
||||||
Web3RpcConfig {
|
Web3RpcConfig {
|
||||||
disabled: false,
|
http_url: Some(anvil.endpoint()),
|
||||||
display_name: None,
|
|
||||||
url: anvil.endpoint(),
|
|
||||||
backup: Some(false),
|
|
||||||
block_data_limit: None,
|
|
||||||
soft_limit: 100,
|
soft_limit: 100,
|
||||||
hard_limit: None,
|
|
||||||
tier: 0,
|
tier: 0,
|
||||||
subscribe_txs: Some(false),
|
..Default::default()
|
||||||
extra: Default::default(),
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"anvil_ws".to_string(),
|
"anvil_ws".to_string(),
|
||||||
Web3RpcConfig {
|
Web3RpcConfig {
|
||||||
disabled: false,
|
ws_url: Some(anvil.ws_endpoint()),
|
||||||
display_name: None,
|
|
||||||
url: anvil.ws_endpoint(),
|
|
||||||
backup: Some(false),
|
|
||||||
block_data_limit: None,
|
|
||||||
soft_limit: 100,
|
soft_limit: 100,
|
||||||
hard_limit: None,
|
|
||||||
tier: 0,
|
tier: 0,
|
||||||
subscribe_txs: Some(false),
|
..Default::default()
|
||||||
extra: Default::default(),
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"anvil_both".to_string(),
|
||||||
|
Web3RpcConfig {
|
||||||
|
http_url: Some(anvil.endpoint()),
|
||||||
|
ws_url: Some(anvil.ws_endpoint()),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
@ -80,12 +80,7 @@ pub async fn clean_block_number(
|
|||||||
.context("fetching block number from hash")?;
|
.context("fetching block number from hash")?;
|
||||||
|
|
||||||
// TODO: set change to true? i think not we should probably use hashes for everything.
|
// TODO: set change to true? i think not we should probably use hashes for everything.
|
||||||
(
|
(*block.number(), false)
|
||||||
block
|
|
||||||
.number
|
|
||||||
.expect("blocks here should always have numbers"),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!("blockHash missing"));
|
return Err(anyhow::anyhow!("blockHash missing"));
|
||||||
}
|
}
|
||||||
@ -132,6 +127,12 @@ pub async fn block_needed(
|
|||||||
head_block_num: U64,
|
head_block_num: U64,
|
||||||
rpcs: &Web3Rpcs,
|
rpcs: &Web3Rpcs,
|
||||||
) -> anyhow::Result<BlockNeeded> {
|
) -> anyhow::Result<BlockNeeded> {
|
||||||
|
// some requests have potentially very large responses
|
||||||
|
// TODO: only skip caching if the response actually is large
|
||||||
|
if method.starts_with("trace_") || method == "debug_traceTransaction" {
|
||||||
|
return Ok(BlockNeeded::CacheNever);
|
||||||
|
}
|
||||||
|
|
||||||
let params = if let Some(params) = params {
|
let params = if let Some(params) = params {
|
||||||
// grab the params so we can inspect and potentially modify them
|
// grab the params so we can inspect and potentially modify them
|
||||||
params
|
params
|
||||||
@ -215,8 +216,8 @@ pub async fn block_needed(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return Ok(BlockNeeded::CacheRange {
|
return Ok(BlockNeeded::CacheRange {
|
||||||
from_block_num: from_block_num,
|
from_block_num,
|
||||||
to_block_num: to_block_num,
|
to_block_num,
|
||||||
cache_errors: true,
|
cache_errors: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use crate::rpcs::blockchain::BlockHashesCache;
|
use crate::app::AnyhowJoinHandle;
|
||||||
|
use crate::rpcs::blockchain::{BlockHashesCache, Web3ProxyBlock};
|
||||||
use crate::rpcs::one::Web3Rpc;
|
use crate::rpcs::one::Web3Rpc;
|
||||||
use crate::{app::AnyhowJoinHandle, rpcs::blockchain::ArcBlock};
|
|
||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
use ethers::prelude::TxHash;
|
use ethers::prelude::TxHash;
|
||||||
use ethers::types::U256;
|
use ethers::types::{U256, U64};
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use migration::sea_orm::DatabaseConnection;
|
use migration::sea_orm::DatabaseConnection;
|
||||||
@ -11,7 +11,7 @@ use serde::Deserialize;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
pub type BlockAndRpc = (Option<ArcBlock>, Arc<Web3Rpc>);
|
pub type BlockAndRpc = (Option<Web3ProxyBlock>, Arc<Web3Rpc>);
|
||||||
pub type TxHashAndRpc = (TxHash, Arc<Web3Rpc>);
|
pub type TxHashAndRpc = (TxHash, Arc<Web3Rpc>);
|
||||||
|
|
||||||
#[derive(Debug, FromArgs)]
|
#[derive(Debug, FromArgs)]
|
||||||
@ -105,6 +105,12 @@ pub struct AppConfig {
|
|||||||
pub invite_code: Option<String>,
|
pub invite_code: Option<String>,
|
||||||
pub login_domain: Option<String>,
|
pub login_domain: Option<String>,
|
||||||
|
|
||||||
|
/// do not serve any requests if the best known block is older than this many seconds.
|
||||||
|
pub max_block_age: Option<u64>,
|
||||||
|
|
||||||
|
/// do not serve any requests if the best known block is behind the best known block by more than this many blocks.
|
||||||
|
pub max_block_lag: Option<U64>,
|
||||||
|
|
||||||
/// Rate limit for bearer token authenticated entrypoints.
|
/// Rate limit for bearer token authenticated entrypoints.
|
||||||
/// This is separate from the rpc limits.
|
/// This is separate from the rpc limits.
|
||||||
#[serde(default = "default_bearer_token_max_concurrent_requests")]
|
#[serde(default = "default_bearer_token_max_concurrent_requests")]
|
||||||
@ -197,15 +203,19 @@ fn default_response_cache_max_bytes() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for a backend web3 RPC server
|
/// Configuration for a backend web3 RPC server
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
pub struct Web3RpcConfig {
|
pub struct Web3RpcConfig {
|
||||||
/// simple way to disable a connection without deleting the row
|
/// simple way to disable a connection without deleting the row
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
/// a name used in /status and other user facing messages
|
/// a name used in /status and other user facing messages
|
||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
/// websocket (or http if no websocket)
|
/// (deprecated) rpc url
|
||||||
pub url: String,
|
pub url: Option<String>,
|
||||||
|
/// while not absolutely required, a ws:// or wss:// connection will be able to subscribe to head blocks
|
||||||
|
pub ws_url: Option<String>,
|
||||||
|
/// while not absolutely required, a http:// or https:// connection will allow erigon to stream JSON
|
||||||
|
pub http_url: Option<String>,
|
||||||
/// block data limit. If None, will be queried
|
/// block data limit. If None, will be queried
|
||||||
pub block_data_limit: Option<u64>,
|
pub block_data_limit: Option<u64>,
|
||||||
/// the requests per second at which the server starts slowing down
|
/// the requests per second at which the server starts slowing down
|
||||||
@ -213,14 +223,15 @@ pub struct Web3RpcConfig {
|
|||||||
/// the requests per second at which the server throws errors (rate limit or otherwise)
|
/// the requests per second at which the server throws errors (rate limit or otherwise)
|
||||||
pub hard_limit: Option<u64>,
|
pub hard_limit: Option<u64>,
|
||||||
/// only use this rpc if everything else is lagging too far. this allows us to ignore fast but very low limit rpcs
|
/// only use this rpc if everything else is lagging too far. this allows us to ignore fast but very low limit rpcs
|
||||||
pub backup: Option<bool>,
|
#[serde(default)]
|
||||||
|
pub backup: bool,
|
||||||
/// All else equal, a server with a lower tier receives all requests
|
/// All else equal, a server with a lower tier receives all requests
|
||||||
#[serde(default = "default_tier")]
|
#[serde(default = "default_tier")]
|
||||||
pub tier: u64,
|
pub tier: u64,
|
||||||
/// Subscribe to the firehose of pending transactions
|
/// Subscribe to the firehose of pending transactions
|
||||||
/// Don't do this with free rpcs
|
/// Don't do this with free rpcs
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub subscribe_txs: Option<bool>,
|
pub subscribe_txs: bool,
|
||||||
/// unknown config options get put here
|
/// unknown config options get put here
|
||||||
#[serde(flatten, default = "HashMap::default")]
|
#[serde(flatten, default = "HashMap::default")]
|
||||||
pub extra: HashMap<String, serde_json::Value>,
|
pub extra: HashMap<String, serde_json::Value>,
|
||||||
@ -245,47 +256,24 @@ impl Web3RpcConfig {
|
|||||||
block_map: BlockHashesCache,
|
block_map: BlockHashesCache,
|
||||||
block_sender: Option<flume::Sender<BlockAndRpc>>,
|
block_sender: Option<flume::Sender<BlockAndRpc>>,
|
||||||
tx_id_sender: Option<flume::Sender<TxHashAndRpc>>,
|
tx_id_sender: Option<flume::Sender<TxHashAndRpc>>,
|
||||||
|
reconnect: bool,
|
||||||
) -> anyhow::Result<(Arc<Web3Rpc>, AnyhowJoinHandle<()>)> {
|
) -> anyhow::Result<(Arc<Web3Rpc>, AnyhowJoinHandle<()>)> {
|
||||||
if !self.extra.is_empty() {
|
if !self.extra.is_empty() {
|
||||||
warn!("unknown Web3RpcConfig fields!: {:?}", self.extra.keys());
|
warn!("unknown Web3RpcConfig fields!: {:?}", self.extra.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
let hard_limit = match (self.hard_limit, redis_pool) {
|
|
||||||
(None, None) => None,
|
|
||||||
(Some(hard_limit), Some(redis_client_pool)) => Some((hard_limit, redis_client_pool)),
|
|
||||||
(None, Some(_)) => None,
|
|
||||||
(Some(_hard_limit), None) => {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"no redis client pool! needed for hard limit"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let tx_id_sender = if self.subscribe_txs.unwrap_or(false) {
|
|
||||||
tx_id_sender
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let backup = self.backup.unwrap_or(false);
|
|
||||||
|
|
||||||
Web3Rpc::spawn(
|
Web3Rpc::spawn(
|
||||||
|
self,
|
||||||
name,
|
name,
|
||||||
self.display_name,
|
|
||||||
chain_id,
|
chain_id,
|
||||||
db_conn,
|
db_conn,
|
||||||
self.url,
|
|
||||||
http_client,
|
http_client,
|
||||||
http_interval_sender,
|
http_interval_sender,
|
||||||
hard_limit,
|
redis_pool,
|
||||||
self.soft_limit,
|
|
||||||
backup,
|
|
||||||
self.block_data_limit,
|
|
||||||
block_map,
|
block_map,
|
||||||
block_sender,
|
block_sender,
|
||||||
tx_id_sender,
|
tx_id_sender,
|
||||||
true,
|
reconnect,
|
||||||
self.tier,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod app_stats;
|
pub mod app_stats;
|
||||||
pub mod admin_queries;
|
pub mod admin_queries;
|
||||||
|
pub mod atomics;
|
||||||
pub mod block_number;
|
pub mod block_number;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod frontend;
|
pub mod frontend;
|
||||||
|
397
web3_proxy/src/peak_ewma.rs
Normal file
397
web3_proxy/src/peak_ewma.rs
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
//! Code from [tower](https://github.com/tower-rs/tower/blob/3f31ffd2cf15f1e905142e5f43ab39ac995c22ed/tower/src/load/peak_ewma.rs)
|
||||||
|
//! Measures load using the PeakEWMA response latency.
|
||||||
|
//! TODO: refactor to work with our code
|
||||||
|
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
use std::{
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tokio::time::Instant;
|
||||||
|
use tower_service::Service;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
/// Measures the load of the underlying service using Peak-EWMA load measurement.
|
||||||
|
///
|
||||||
|
/// [`PeakEwma`] implements [`Load`] with the [`Cost`] metric that estimates the amount of
|
||||||
|
/// pending work to an endpoint. Work is calculated by multiplying the
|
||||||
|
/// exponentially-weighted moving average (EWMA) of response latencies by the number of
|
||||||
|
/// pending requests. The Peak-EWMA algorithm is designed to be especially sensitive to
|
||||||
|
/// worst-case latencies. Over time, the peak latency value decays towards the moving
|
||||||
|
/// average of latencies to the endpoint.
|
||||||
|
///
|
||||||
|
/// When no latency information has been measured for an endpoint, an arbitrary default
|
||||||
|
/// RTT of 1 second is used to prevent the endpoint from being overloaded before a
|
||||||
|
/// meaningful baseline can be established..
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This is derived from [Finagle][finagle], which is distributed under the Apache V2
|
||||||
|
/// license. Copyright 2017, Twitter Inc.
|
||||||
|
///
|
||||||
|
/// [finagle]:
|
||||||
|
/// https://github.com/twitter/finagle/blob/9cc08d15216497bb03a1cafda96b7266cfbbcff1/finagle-core/src/main/scala/com/twitter/finagle/loadbalancer/PeakEwma.scala
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PeakEwma<S, C = CompleteOnResponse> {
|
||||||
|
service: S,
|
||||||
|
decay_ns: f64,
|
||||||
|
rtt_estimate: Arc<Mutex<RttEstimate>>,
|
||||||
|
completion: C,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "discover")]
|
||||||
|
pin_project! {
|
||||||
|
/// Wraps a `D`-typed stream of discovered services with `PeakEwma`.
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "discover")))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PeakEwmaDiscover<D, C = CompleteOnResponse> {
|
||||||
|
#[pin]
|
||||||
|
discover: D,
|
||||||
|
decay_ns: f64,
|
||||||
|
default_rtt: Duration,
|
||||||
|
completion: C,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the relative cost of communicating with a service.
|
||||||
|
///
|
||||||
|
/// The underlying value estimates the amount of pending work to a service: the Peak-EWMA
|
||||||
|
/// latency estimate multiplied by the number of pending requests.
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||||
|
pub struct Cost(f64);
|
||||||
|
|
||||||
|
/// Tracks an in-flight request and updates the RTT-estimate on Drop.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Handle {
|
||||||
|
sent_at: Instant,
|
||||||
|
decay_ns: f64,
|
||||||
|
rtt_estimate: Arc<Mutex<RttEstimate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds the current RTT estimate and the last time this value was updated.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct RttEstimate {
|
||||||
|
update_at: Instant,
|
||||||
|
rtt_ns: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
const NANOS_PER_MILLI: f64 = 1_000_000.0;
|
||||||
|
|
||||||
|
// ===== impl PeakEwma =====
|
||||||
|
|
||||||
|
impl<S, C> PeakEwma<S, C> {
|
||||||
|
/// Wraps an `S`-typed service so that its load is tracked by the EWMA of its peak latency.
|
||||||
|
pub fn new(service: S, default_rtt: Duration, decay_ns: f64, completion: C) -> Self {
|
||||||
|
debug_assert!(decay_ns > 0.0, "decay_ns must be positive");
|
||||||
|
Self {
|
||||||
|
service,
|
||||||
|
decay_ns,
|
||||||
|
rtt_estimate: Arc::new(Mutex::new(RttEstimate::new(nanos(default_rtt)))),
|
||||||
|
completion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(&self) -> Handle {
|
||||||
|
Handle {
|
||||||
|
decay_ns: self.decay_ns,
|
||||||
|
sent_at: Instant::now(),
|
||||||
|
rtt_estimate: self.rtt_estimate.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, C, Request> Service<Request> for PeakEwma<S, C>
|
||||||
|
where
|
||||||
|
S: Service<Request>,
|
||||||
|
C: TrackCompletion<Handle, S::Response>,
|
||||||
|
{
|
||||||
|
type Response = C::Output;
|
||||||
|
type Error = S::Error;
|
||||||
|
type Future = TrackCompletionFuture<S::Future, C, Handle>;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
self.service.poll_ready(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, req: Request) -> Self::Future {
|
||||||
|
TrackCompletionFuture::new(
|
||||||
|
self.completion.clone(),
|
||||||
|
self.handle(),
|
||||||
|
self.service.call(req),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, C> Load for PeakEwma<S, C> {
|
||||||
|
type Metric = Cost;
|
||||||
|
|
||||||
|
fn load(&self) -> Self::Metric {
|
||||||
|
let pending = Arc::strong_count(&self.rtt_estimate) as u32 - 1;
|
||||||
|
|
||||||
|
// Update the RTT estimate to account for decay since the last update.
|
||||||
|
// If an estimate has not been established, a default is provided
|
||||||
|
let estimate = self.update_estimate();
|
||||||
|
|
||||||
|
let cost = Cost(estimate * f64::from(pending + 1));
|
||||||
|
trace!(
|
||||||
|
"load estimate={:.0}ms pending={} cost={:?}",
|
||||||
|
estimate / NANOS_PER_MILLI,
|
||||||
|
pending,
|
||||||
|
cost,
|
||||||
|
);
|
||||||
|
cost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, C> PeakEwma<S, C> {
|
||||||
|
fn update_estimate(&self) -> f64 {
|
||||||
|
let mut rtt = self.rtt_estimate.lock().expect("peak ewma prior_estimate");
|
||||||
|
rtt.decay(self.decay_ns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== impl PeakEwmaDiscover =====
|
||||||
|
|
||||||
|
#[cfg(feature = "discover")]
|
||||||
|
impl<D, C> PeakEwmaDiscover<D, C> {
|
||||||
|
/// Wraps a `D`-typed [`Discover`] so that services have a [`PeakEwma`] load metric.
|
||||||
|
///
|
||||||
|
/// The provided `default_rtt` is used as the default RTT estimate for newly
|
||||||
|
/// added services.
|
||||||
|
///
|
||||||
|
/// They `decay` value determines over what time period a RTT estimate should
|
||||||
|
/// decay.
|
||||||
|
pub fn new<Request>(discover: D, default_rtt: Duration, decay: Duration, completion: C) -> Self
|
||||||
|
where
|
||||||
|
D: Discover,
|
||||||
|
D::Service: Service<Request>,
|
||||||
|
C: TrackCompletion<Handle, <D::Service as Service<Request>>::Response>,
|
||||||
|
{
|
||||||
|
PeakEwmaDiscover {
|
||||||
|
discover,
|
||||||
|
decay_ns: nanos(decay),
|
||||||
|
default_rtt,
|
||||||
|
completion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "discover")]
|
||||||
|
impl<D, C> Stream for PeakEwmaDiscover<D, C>
|
||||||
|
where
|
||||||
|
D: Discover,
|
||||||
|
C: Clone,
|
||||||
|
{
|
||||||
|
type Item = Result<Change<D::Key, PeakEwma<D::Service, C>>, D::Error>;
|
||||||
|
|
||||||
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
let this = self.project();
|
||||||
|
let change = match ready!(this.discover.poll_discover(cx)).transpose()? {
|
||||||
|
None => return Poll::Ready(None),
|
||||||
|
Some(Change::Remove(k)) => Change::Remove(k),
|
||||||
|
Some(Change::Insert(k, svc)) => {
|
||||||
|
let peak_ewma = PeakEwma::new(
|
||||||
|
svc,
|
||||||
|
*this.default_rtt,
|
||||||
|
*this.decay_ns,
|
||||||
|
this.completion.clone(),
|
||||||
|
);
|
||||||
|
Change::Insert(k, peak_ewma)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Poll::Ready(Some(Ok(change)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== impl RttEstimate =====
|
||||||
|
|
||||||
|
impl RttEstimate {
|
||||||
|
fn new(rtt_ns: f64) -> Self {
|
||||||
|
debug_assert!(0.0 < rtt_ns, "rtt must be positive");
|
||||||
|
Self {
|
||||||
|
rtt_ns,
|
||||||
|
update_at: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decays the RTT estimate with a decay period of `decay_ns`.
|
||||||
|
fn decay(&mut self, decay_ns: f64) -> f64 {
|
||||||
|
// Updates with a 0 duration so that the estimate decays towards 0.
|
||||||
|
let now = Instant::now();
|
||||||
|
self.update(now, now, decay_ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the Peak-EWMA RTT estimate.
|
||||||
|
///
|
||||||
|
/// The elapsed time from `sent_at` to `recv_at` is added
|
||||||
|
fn update(&mut self, sent_at: Instant, recv_at: Instant, decay_ns: f64) -> f64 {
|
||||||
|
debug_assert!(
|
||||||
|
sent_at <= recv_at,
|
||||||
|
"recv_at={:?} after sent_at={:?}",
|
||||||
|
recv_at,
|
||||||
|
sent_at
|
||||||
|
);
|
||||||
|
let rtt = nanos(recv_at.saturating_duration_since(sent_at));
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
debug_assert!(
|
||||||
|
self.update_at <= now,
|
||||||
|
"update_at={:?} in the future",
|
||||||
|
self.update_at
|
||||||
|
);
|
||||||
|
|
||||||
|
self.rtt_ns = if self.rtt_ns < rtt {
|
||||||
|
// For Peak-EWMA, always use the worst-case (peak) value as the estimate for
|
||||||
|
// subsequent requests.
|
||||||
|
trace!(
|
||||||
|
"update peak rtt={}ms prior={}ms",
|
||||||
|
rtt / NANOS_PER_MILLI,
|
||||||
|
self.rtt_ns / NANOS_PER_MILLI,
|
||||||
|
);
|
||||||
|
rtt
|
||||||
|
} else {
|
||||||
|
// When an RTT is observed that is less than the estimated RTT, we decay the
|
||||||
|
// prior estimate according to how much time has elapsed since the last
|
||||||
|
// update. The inverse of the decay is used to scale the estimate towards the
|
||||||
|
// observed RTT value.
|
||||||
|
let elapsed = nanos(now.saturating_duration_since(self.update_at));
|
||||||
|
let decay = (-elapsed / decay_ns).exp();
|
||||||
|
let recency = 1.0 - decay;
|
||||||
|
let next_estimate = (self.rtt_ns * decay) + (rtt * recency);
|
||||||
|
trace!(
|
||||||
|
"update rtt={:03.0}ms decay={:06.0}ns; next={:03.0}ms",
|
||||||
|
rtt / NANOS_PER_MILLI,
|
||||||
|
self.rtt_ns - next_estimate,
|
||||||
|
next_estimate / NANOS_PER_MILLI,
|
||||||
|
);
|
||||||
|
next_estimate
|
||||||
|
};
|
||||||
|
self.update_at = now;
|
||||||
|
|
||||||
|
self.rtt_ns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== impl Handle =====
|
||||||
|
|
||||||
|
impl Drop for Handle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let recv_at = Instant::now();
|
||||||
|
|
||||||
|
if let Ok(mut rtt) = self.rtt_estimate.lock() {
|
||||||
|
rtt.update(self.sent_at, recv_at, self.decay_ns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== impl Cost =====
|
||||||
|
|
||||||
|
// Utility that converts durations to nanos in f64.
|
||||||
|
//
|
||||||
|
// Due to a lossy transformation, the maximum value that can be represented is ~585 years,
|
||||||
|
// which, I hope, is more than enough to represent request latencies.
|
||||||
|
fn nanos(d: Duration) -> f64 {
|
||||||
|
const NANOS_PER_SEC: u64 = 1_000_000_000;
|
||||||
|
let n = f64::from(d.subsec_nanos());
|
||||||
|
let s = d.as_secs().saturating_mul(NANOS_PER_SEC) as f64;
|
||||||
|
n + s
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use futures_util::future;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time;
|
||||||
|
use tokio_test::{assert_ready, assert_ready_ok, task};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct Svc;
|
||||||
|
impl Service<()> for Svc {
|
||||||
|
type Response = ();
|
||||||
|
type Error = ();
|
||||||
|
type Future = future::Ready<Result<(), ()>>;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), ()>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, (): ()) -> Self::Future {
|
||||||
|
future::ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default RTT estimate decays, so that new nodes are considered if the
|
||||||
|
/// default RTT is too high.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn default_decay() {
|
||||||
|
time::pause();
|
||||||
|
|
||||||
|
let svc = PeakEwma::new(
|
||||||
|
Svc,
|
||||||
|
Duration::from_millis(10),
|
||||||
|
NANOS_PER_MILLI * 1_000.0,
|
||||||
|
CompleteOnResponse,
|
||||||
|
);
|
||||||
|
let Cost(load) = svc.load();
|
||||||
|
assert_eq!(load, 10.0 * NANOS_PER_MILLI);
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(100)).await;
|
||||||
|
let Cost(load) = svc.load();
|
||||||
|
assert!(9.0 * NANOS_PER_MILLI < load && load < 10.0 * NANOS_PER_MILLI);
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(100)).await;
|
||||||
|
let Cost(load) = svc.load();
|
||||||
|
assert!(8.0 * NANOS_PER_MILLI < load && load < 9.0 * NANOS_PER_MILLI);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The default RTT estimate decays, so that new nodes are considered if the default RTT is too
|
||||||
|
// high.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn compound_decay() {
|
||||||
|
time::pause();
|
||||||
|
|
||||||
|
let mut svc = PeakEwma::new(
|
||||||
|
Svc,
|
||||||
|
Duration::from_millis(20),
|
||||||
|
NANOS_PER_MILLI * 1_000.0,
|
||||||
|
CompleteOnResponse,
|
||||||
|
);
|
||||||
|
assert_eq!(svc.load(), Cost(20.0 * NANOS_PER_MILLI));
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(100)).await;
|
||||||
|
let mut rsp0 = task::spawn(svc.call(()));
|
||||||
|
assert!(svc.load() > Cost(20.0 * NANOS_PER_MILLI));
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(100)).await;
|
||||||
|
let mut rsp1 = task::spawn(svc.call(()));
|
||||||
|
assert!(svc.load() > Cost(40.0 * NANOS_PER_MILLI));
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(100)).await;
|
||||||
|
let () = assert_ready_ok!(rsp0.poll());
|
||||||
|
assert_eq!(svc.load(), Cost(400_000_000.0));
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(100)).await;
|
||||||
|
let () = assert_ready_ok!(rsp1.poll());
|
||||||
|
assert_eq!(svc.load(), Cost(200_000_000.0));
|
||||||
|
|
||||||
|
// Check that values decay as time elapses
|
||||||
|
time::advance(Duration::from_secs(1)).await;
|
||||||
|
assert!(svc.load() < Cost(100_000_000.0));
|
||||||
|
|
||||||
|
time::advance(Duration::from_secs(10)).await;
|
||||||
|
assert!(svc.load() < Cost(100_000.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nanos() {
|
||||||
|
assert_eq!(super::nanos(Duration::new(0, 0)), 0.0);
|
||||||
|
assert_eq!(super::nanos(Duration::new(0, 123)), 123.0);
|
||||||
|
assert_eq!(super::nanos(Duration::new(1, 23)), 1_000_000_023.0);
|
||||||
|
assert_eq!(
|
||||||
|
super::nanos(Duration::new(::std::u64::MAX, 999_999_999)),
|
||||||
|
18446744074709553000.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
588
web3_proxy/src/rpcs/consensus.rs
Normal file
588
web3_proxy/src/rpcs/consensus.rs
Normal file
@ -0,0 +1,588 @@
|
|||||||
|
use crate::frontend::authorization::Authorization;
|
||||||
|
|
||||||
|
use super::blockchain::Web3ProxyBlock;
|
||||||
|
use super::many::Web3Rpcs;
|
||||||
|
use super::one::Web3Rpc;
|
||||||
|
use anyhow::Context;
|
||||||
|
use ethers::prelude::{H256, U64};
|
||||||
|
use hashbrown::{HashMap, HashSet};
|
||||||
|
use log::{debug, trace, warn};
|
||||||
|
use moka::future::Cache;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::time::Instant;
|
||||||
|
|
||||||
|
/// A collection of Web3Rpcs that are on the same block.
|
||||||
|
/// Serialize is so we can print it on our debug endpoint
|
||||||
|
#[derive(Clone, Default, Serialize)]
|
||||||
|
pub struct ConsensusWeb3Rpcs {
|
||||||
|
pub(super) tier: u64,
|
||||||
|
pub(super) head_block: Option<Web3ProxyBlock>,
|
||||||
|
// TODO: this should be able to serialize, but it isn't
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub(super) rpcs: Vec<Arc<Web3Rpc>>,
|
||||||
|
pub(super) backups_voted: Option<Web3ProxyBlock>,
|
||||||
|
pub(super) backups_needed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConsensusWeb3Rpcs {
|
||||||
|
pub fn num_conns(&self) -> usize {
|
||||||
|
self.rpcs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sum_soft_limit(&self) -> u32 {
|
||||||
|
self.rpcs.iter().fold(0, |sum, rpc| sum + rpc.soft_limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: sum_hard_limit?
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for ConsensusWeb3Rpcs {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
// TODO: the default formatter takes forever to write. this is too quiet though
|
||||||
|
// TODO: print the actual conns?
|
||||||
|
f.debug_struct("ConsensusConnections")
|
||||||
|
.field("head_block", &self.head_block)
|
||||||
|
.field("num_conns", &self.rpcs.len())
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Web3Rpcs {
|
||||||
|
// TODO: return a ref?
|
||||||
|
pub fn head_block(&self) -> Option<Web3ProxyBlock> {
|
||||||
|
self.watch_consensus_head_receiver
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|x| x.borrow().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return a ref?
|
||||||
|
pub fn head_block_hash(&self) -> Option<H256> {
|
||||||
|
self.head_block().map(|x| *x.hash())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return a ref?
|
||||||
|
pub fn head_block_num(&self) -> Option<U64> {
|
||||||
|
self.head_block().map(|x| *x.number())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn synced(&self) -> bool {
|
||||||
|
!self.watch_consensus_rpcs_sender.borrow().rpcs.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn num_synced_rpcs(&self) -> usize {
|
||||||
|
self.watch_consensus_rpcs_sender.borrow().rpcs.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirstSeenCache = Cache<H256, Instant, hashbrown::hash_map::DefaultHashBuilder>;
|
||||||
|
|
||||||
|
pub struct ConnectionsGroup {
|
||||||
|
rpc_name_to_block: HashMap<String, Web3ProxyBlock>,
|
||||||
|
// TODO: what if there are two blocks with the same number?
|
||||||
|
highest_block: Option<Web3ProxyBlock>,
|
||||||
|
/// used to track rpc.head_latency. The same cache should be shared between all ConnectionsGroups
|
||||||
|
first_seen: FirstSeenCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionsGroup {
|
||||||
|
pub fn new(first_seen: FirstSeenCache) -> Self {
|
||||||
|
Self {
|
||||||
|
rpc_name_to_block: Default::default(),
|
||||||
|
highest_block: Default::default(),
|
||||||
|
first_seen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.rpc_name_to_block.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, rpc_name: &str) -> Option<Web3ProxyBlock> {
|
||||||
|
if let Some(removed_block) = self.rpc_name_to_block.remove(rpc_name) {
|
||||||
|
match self.highest_block.as_mut() {
|
||||||
|
None => {}
|
||||||
|
Some(current_highest_block) => {
|
||||||
|
if removed_block.hash() == current_highest_block.hash() {
|
||||||
|
for maybe_highest_block in self.rpc_name_to_block.values() {
|
||||||
|
if maybe_highest_block.number() > current_highest_block.number() {
|
||||||
|
*current_highest_block = maybe_highest_block.clone();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(removed_block)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert(&mut self, rpc: &Web3Rpc, block: Web3ProxyBlock) -> Option<Web3ProxyBlock> {
|
||||||
|
let first_seen = self
|
||||||
|
.first_seen
|
||||||
|
.get_with(*block.hash(), async move { Instant::now() })
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// TODO: this should be 0 if we are first seen, but i think it will be slightly non-zero.
|
||||||
|
// calculate elapsed time before trying to lock.
|
||||||
|
let latency = first_seen.elapsed();
|
||||||
|
|
||||||
|
rpc.head_latency.write().record(latency);
|
||||||
|
|
||||||
|
// TODO: what about a reorg to the same height?
|
||||||
|
if Some(block.number()) > self.highest_block.as_ref().map(|x| x.number()) {
|
||||||
|
self.highest_block = Some(block.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rpc_name_to_block.insert(rpc.name.clone(), block)
|
||||||
|
}
|
||||||
|
|
||||||
|
// // TODO: do this during insert/remove?
|
||||||
|
// pub(self) async fn highest_block(
|
||||||
|
// &self,
|
||||||
|
// authorization: &Arc<Authorization>,
|
||||||
|
// web3_rpcs: &Web3Rpcs,
|
||||||
|
// ) -> Option<ArcBlock> {
|
||||||
|
// let mut checked_heads = HashSet::with_capacity(self.rpc_name_to_hash.len());
|
||||||
|
// let mut highest_block = None::<ArcBlock>;
|
||||||
|
|
||||||
|
// for (rpc_name, rpc_head_hash) in self.rpc_name_to_hash.iter() {
|
||||||
|
// // don't waste time checking the same hash multiple times
|
||||||
|
// if checked_heads.contains(rpc_head_hash) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let rpc_block = match web3_rpcs
|
||||||
|
// .get_block_from_rpc(rpc_name, rpc_head_hash, authorization)
|
||||||
|
// .await
|
||||||
|
// {
|
||||||
|
// Ok(x) => x,
|
||||||
|
// Err(err) => {
|
||||||
|
// warn!(
|
||||||
|
// "failed getting block {} from {} while finding highest block number: {:?}",
|
||||||
|
// rpc_head_hash, rpc_name, err,
|
||||||
|
// );
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// checked_heads.insert(rpc_head_hash);
|
||||||
|
|
||||||
|
// // if this is the first block we've tried
|
||||||
|
// // or if this rpc's newest block has a higher number
|
||||||
|
// // we used to check total difficulty, but that isn't a thing anymore on ETH
|
||||||
|
// // TODO: we still need total difficulty on some other PoW chains. whats annoying is it isn't considered part of the "block header" just the block. so websockets don't return it
|
||||||
|
// let highest_num = highest_block
|
||||||
|
// .as_ref()
|
||||||
|
// .map(|x| x.number.expect("blocks here should always have a number"));
|
||||||
|
// let rpc_num = rpc_block.as_ref().number;
|
||||||
|
|
||||||
|
// if rpc_num > highest_num {
|
||||||
|
// highest_block = Some(rpc_block);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// highest_block
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// min_consensus_block_num keeps us from ever going backwards.
|
||||||
|
/// TODO: think about min_consensus_block_num more. i think this might cause an outage if the chain is doing weird things. but 503s is probably better than broken data.
|
||||||
|
pub(self) async fn consensus_head_connections(
|
||||||
|
&self,
|
||||||
|
authorization: &Arc<Authorization>,
|
||||||
|
web3_rpcs: &Web3Rpcs,
|
||||||
|
min_consensus_block_num: Option<U64>,
|
||||||
|
tier: &u64,
|
||||||
|
) -> anyhow::Result<ConsensusWeb3Rpcs> {
|
||||||
|
let mut maybe_head_block = match self.highest_block.clone() {
|
||||||
|
None => return Err(anyhow::anyhow!("no blocks known")),
|
||||||
|
Some(x) => x,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: take max_distance_consensus_to_highest as an argument?
|
||||||
|
// TODO: what if someone's backup node is misconfigured and goes on a really fast forked chain?
|
||||||
|
let max_lag_consensus_to_highest =
|
||||||
|
if let Some(min_consensus_block_num) = min_consensus_block_num {
|
||||||
|
maybe_head_block
|
||||||
|
.number()
|
||||||
|
.saturating_add(1.into())
|
||||||
|
.saturating_sub(min_consensus_block_num)
|
||||||
|
.as_u64()
|
||||||
|
} else {
|
||||||
|
10
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"max_lag_consensus_to_highest: {}",
|
||||||
|
max_lag_consensus_to_highest
|
||||||
|
);
|
||||||
|
|
||||||
|
let num_known = self.rpc_name_to_block.len();
|
||||||
|
|
||||||
|
if num_known < web3_rpcs.min_head_rpcs {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"not enough rpcs connected: {}/{}",
|
||||||
|
num_known,
|
||||||
|
web3_rpcs.min_head_rpcs,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut primary_rpcs_voted: Option<Web3ProxyBlock> = None;
|
||||||
|
let mut backup_rpcs_voted: Option<Web3ProxyBlock> = None;
|
||||||
|
|
||||||
|
// track rpcs on this heaviest chain so we can build a new ConsensusConnections
|
||||||
|
let mut primary_consensus_rpcs = HashSet::<&str>::new();
|
||||||
|
let mut backup_consensus_rpcs = HashSet::<&str>::new();
|
||||||
|
|
||||||
|
// a running total of the soft limits covered by the rpcs that agree on the head block
|
||||||
|
let mut primary_sum_soft_limit: u32 = 0;
|
||||||
|
let mut backup_sum_soft_limit: u32 = 0;
|
||||||
|
|
||||||
|
// TODO: also track the sum of *available* hard_limits. if any servers have no hard limits, use their soft limit or no limit?
|
||||||
|
|
||||||
|
// check the highest work block for a set of rpcs that can serve our request load
|
||||||
|
// if it doesn't have enough rpcs for our request load, check the parent block
|
||||||
|
// TODO: loop for how many parent blocks? we don't want to serve blocks that are too far behind. probably different per chain
|
||||||
|
// TODO: this loop is pretty long. any way to clean up this code?
|
||||||
|
for _ in 0..max_lag_consensus_to_highest {
|
||||||
|
let maybe_head_hash = maybe_head_block.hash();
|
||||||
|
|
||||||
|
// find all rpcs with maybe_head_hash as their current head
|
||||||
|
for (rpc_name, rpc_head) in self.rpc_name_to_block.iter() {
|
||||||
|
if rpc_head.hash() != maybe_head_hash {
|
||||||
|
// connection is not on the desired block
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if backup_consensus_rpcs.contains(rpc_name.as_str()) {
|
||||||
|
// connection is on a later block in this same chain
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if primary_consensus_rpcs.contains(rpc_name.as_str()) {
|
||||||
|
// connection is on a later block in this same chain
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rpc) = web3_rpcs.by_name.get(rpc_name.as_str()) {
|
||||||
|
if backup_rpcs_voted.is_some() {
|
||||||
|
// backups already voted for a head block. don't change it
|
||||||
|
} else {
|
||||||
|
backup_consensus_rpcs.insert(rpc_name);
|
||||||
|
backup_sum_soft_limit += rpc.soft_limit;
|
||||||
|
}
|
||||||
|
if !rpc.backup {
|
||||||
|
primary_consensus_rpcs.insert(rpc_name);
|
||||||
|
primary_sum_soft_limit += rpc.soft_limit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// i don't think this is an error. i think its just if a reconnect is currently happening
|
||||||
|
warn!("connection missing: {}", rpc_name);
|
||||||
|
debug!("web3_rpcs.conns: {:#?}", web3_rpcs.by_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if primary_sum_soft_limit >= web3_rpcs.min_sum_soft_limit
|
||||||
|
&& primary_consensus_rpcs.len() >= web3_rpcs.min_head_rpcs
|
||||||
|
{
|
||||||
|
// we have enough servers with enough requests! yey!
|
||||||
|
primary_rpcs_voted = Some(maybe_head_block.clone());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if backup_rpcs_voted.is_none()
|
||||||
|
&& backup_consensus_rpcs != primary_consensus_rpcs
|
||||||
|
&& backup_sum_soft_limit >= web3_rpcs.min_sum_soft_limit
|
||||||
|
&& backup_consensus_rpcs.len() >= web3_rpcs.min_head_rpcs
|
||||||
|
{
|
||||||
|
// if we include backup servers, we have enough servers with high enough limits
|
||||||
|
backup_rpcs_voted = Some(maybe_head_block.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// not enough rpcs on this block. check the parent block
|
||||||
|
match web3_rpcs
|
||||||
|
.block(authorization, &maybe_head_block.parent_hash(), None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(parent_block) => {
|
||||||
|
// trace!(
|
||||||
|
// child=%maybe_head_hash, parent=%parent_block.hash.unwrap(), "avoiding thundering herd. checking consensus on parent block",
|
||||||
|
// );
|
||||||
|
maybe_head_block = parent_block.into();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let soft_limit_percent = (primary_sum_soft_limit as f32
|
||||||
|
/ web3_rpcs.min_sum_soft_limit as f32)
|
||||||
|
* 100.0;
|
||||||
|
|
||||||
|
let err_msg = format!("ran out of parents to check. rpcs {}/{}/{}. soft limit: {:.2}% ({}/{}). err: {:#?}",
|
||||||
|
primary_consensus_rpcs.len(),
|
||||||
|
num_known,
|
||||||
|
web3_rpcs.min_head_rpcs,
|
||||||
|
primary_sum_soft_limit,
|
||||||
|
web3_rpcs.min_sum_soft_limit,
|
||||||
|
soft_limit_percent,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
|
||||||
|
if backup_rpcs_voted.is_some() {
|
||||||
|
warn!("{}", err_msg);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!(err_msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: if consensus_head_rpcs.is_empty, try another method of finding the head block. will need to change the return Err above into breaks.
|
||||||
|
|
||||||
|
// we've done all the searching for the heaviest block that we can
|
||||||
|
if (primary_consensus_rpcs.len() < web3_rpcs.min_head_rpcs
|
||||||
|
|| primary_sum_soft_limit < web3_rpcs.min_sum_soft_limit)
|
||||||
|
&& backup_rpcs_voted.is_none()
|
||||||
|
{
|
||||||
|
// if we get here, not enough servers are synced. return an error
|
||||||
|
let soft_limit_percent =
|
||||||
|
(primary_sum_soft_limit as f32 / web3_rpcs.min_sum_soft_limit as f32) * 100.0;
|
||||||
|
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Not enough resources. rpcs {}/{}/{}. soft limit: {:.2}% ({}/{})",
|
||||||
|
primary_consensus_rpcs.len(),
|
||||||
|
num_known,
|
||||||
|
web3_rpcs.min_head_rpcs,
|
||||||
|
primary_sum_soft_limit,
|
||||||
|
web3_rpcs.min_sum_soft_limit,
|
||||||
|
soft_limit_percent,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// success! this block has enough soft limit and nodes on it (or on later blocks)
|
||||||
|
let rpcs: Vec<Arc<Web3Rpc>> = primary_consensus_rpcs
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|conn_name| web3_rpcs.by_name.get(conn_name).cloned())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let _ = maybe_head_block.hash();
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let _ = maybe_head_block.number();
|
||||||
|
|
||||||
|
Ok(ConsensusWeb3Rpcs {
|
||||||
|
tier: *tier,
|
||||||
|
head_block: Some(maybe_head_block),
|
||||||
|
rpcs,
|
||||||
|
backups_voted: backup_rpcs_voted,
|
||||||
|
backups_needed: primary_rpcs_voted.is_none(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A ConsensusConnections builder that tracks all connection heads across multiple groups of servers
|
||||||
|
pub struct ConsensusFinder {
|
||||||
|
/// backups for all tiers are only used if necessary
|
||||||
|
/// tiers[0] = only tier 0.
|
||||||
|
/// tiers[1] = tier 0 and tier 1
|
||||||
|
/// tiers[n] = tier 0..=n
|
||||||
|
/// This is a BTreeMap and not a Vec because sometimes a tier is empty
|
||||||
|
tiers: BTreeMap<u64, ConnectionsGroup>,
|
||||||
|
/// never serve blocks that are too old
|
||||||
|
max_block_age: Option<u64>,
|
||||||
|
/// tier 0 will be prefered as long as the distance between it and the other tiers is <= max_tier_lag
|
||||||
|
max_block_lag: Option<U64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConsensusFinder {
|
||||||
|
pub fn new(
|
||||||
|
configured_tiers: &[u64],
|
||||||
|
max_block_age: Option<u64>,
|
||||||
|
max_block_lag: Option<U64>,
|
||||||
|
) -> Self {
|
||||||
|
// TODO: what's a good capacity for this?
|
||||||
|
let first_seen = Cache::builder()
|
||||||
|
.max_capacity(16)
|
||||||
|
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::default());
|
||||||
|
|
||||||
|
// TODO: this will need some thought when config reloading is written
|
||||||
|
let tiers = configured_tiers
|
||||||
|
.iter()
|
||||||
|
.map(|x| (*x, ConnectionsGroup::new(first_seen.clone())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
tiers,
|
||||||
|
max_block_age,
|
||||||
|
max_block_lag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.tiers.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the ConnectionsGroup that contains all rpcs
|
||||||
|
/// panics if there are no tiers
|
||||||
|
pub fn all_rpcs_group(&self) -> Option<&ConnectionsGroup> {
|
||||||
|
self.tiers.values().last()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the mutable ConnectionsGroup that contains all rpcs
|
||||||
|
pub fn all_mut(&mut self) -> Option<&mut ConnectionsGroup> {
|
||||||
|
self.tiers.values_mut().last()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, rpc: &Web3Rpc) -> Option<Web3ProxyBlock> {
|
||||||
|
let mut removed = None;
|
||||||
|
|
||||||
|
for (i, tier_group) in self.tiers.iter_mut().rev() {
|
||||||
|
if i < &rpc.tier {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let x = tier_group.remove(rpc.name.as_str());
|
||||||
|
|
||||||
|
if removed.is_none() && x.is_some() {
|
||||||
|
removed = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the block that the rpc was on before updating to the new_block
|
||||||
|
pub async fn insert(
|
||||||
|
&mut self,
|
||||||
|
rpc: &Web3Rpc,
|
||||||
|
new_block: Web3ProxyBlock,
|
||||||
|
) -> Option<Web3ProxyBlock> {
|
||||||
|
let mut old = None;
|
||||||
|
|
||||||
|
// TODO: error if rpc.tier is not in self.tiers
|
||||||
|
|
||||||
|
for (i, tier_group) in self.tiers.iter_mut().rev() {
|
||||||
|
if i < &rpc.tier {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: should new_block be a ref?
|
||||||
|
let x = tier_group.insert(rpc, new_block.clone()).await;
|
||||||
|
|
||||||
|
if old.is_none() && x.is_some() {
|
||||||
|
old = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
old
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update our tracking of the rpc and return true if something changed
|
||||||
|
pub(crate) async fn update_rpc(
|
||||||
|
&mut self,
|
||||||
|
rpc_head_block: Option<Web3ProxyBlock>,
|
||||||
|
rpc: Arc<Web3Rpc>,
|
||||||
|
// we need this so we can save the block to caches. i don't like it though. maybe we should use a lazy_static Cache wrapper that has a "save_block" method?. i generally dislike globals but i also dislike all the types having to pass eachother around
|
||||||
|
web3_connections: &Web3Rpcs,
|
||||||
|
) -> anyhow::Result<bool> {
|
||||||
|
// add the rpc's block to connection_heads, or remove the rpc from connection_heads
|
||||||
|
let changed = match rpc_head_block {
|
||||||
|
Some(mut rpc_head_block) => {
|
||||||
|
// we don't know if its on the heaviest chain yet
|
||||||
|
rpc_head_block = web3_connections
|
||||||
|
.try_cache_block(rpc_head_block, false)
|
||||||
|
.await
|
||||||
|
.context("failed caching block")?;
|
||||||
|
|
||||||
|
// if let Some(max_block_lag) = max_block_lag {
|
||||||
|
// if rpc_head_block.number() < ??? {
|
||||||
|
// trace!("rpc_head_block from {} is too far behind! {}", rpc, rpc_head_block);
|
||||||
|
// return Ok(self.remove(&rpc).is_some());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
if let Some(max_age) = self.max_block_age {
|
||||||
|
if rpc_head_block.age() > max_age {
|
||||||
|
trace!("rpc_head_block from {} is too old! {}", rpc, rpc_head_block);
|
||||||
|
return Ok(self.remove(&rpc).is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(prev_block) = self.insert(&rpc, rpc_head_block.clone()).await {
|
||||||
|
if prev_block.hash() == rpc_head_block.hash() {
|
||||||
|
// this block was already sent by this rpc. return early
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
// new block for this rpc
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// first block for this rpc
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if self.remove(&rpc).is_none() {
|
||||||
|
// this rpc was already removed
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
// rpc head changed from being synced to not
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(changed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn best_consensus_connections(
|
||||||
|
&mut self,
|
||||||
|
authorization: &Arc<Authorization>,
|
||||||
|
web3_connections: &Web3Rpcs,
|
||||||
|
) -> anyhow::Result<ConsensusWeb3Rpcs> {
|
||||||
|
// TODO: attach context to these?
|
||||||
|
let highest_known_block = self
|
||||||
|
.all_rpcs_group()
|
||||||
|
.context("no rpcs")?
|
||||||
|
.highest_block
|
||||||
|
.as_ref()
|
||||||
|
.context("no highest block")?;
|
||||||
|
|
||||||
|
trace!("highest_known_block: {}", highest_known_block);
|
||||||
|
|
||||||
|
let min_block_num = self
|
||||||
|
.max_block_lag
|
||||||
|
.map(|x| highest_known_block.number().saturating_sub(x))
|
||||||
|
// we also want to be sure we don't ever go backwards!
|
||||||
|
.max(web3_connections.head_block_num());
|
||||||
|
|
||||||
|
trace!("min_block_num: {:#?}", min_block_num);
|
||||||
|
|
||||||
|
// TODO Should this be a Vec<Result<Option<_, _>>>?
|
||||||
|
// TODO: how should errors be handled?
|
||||||
|
// TODO: find the best tier with a connectionsgroup. best case, this only queries the first tier
|
||||||
|
// TODO: do we need to calculate all of them? I think having highest_known_block included as part of min_block_num should make that unnecessary
|
||||||
|
for (tier, x) in self.tiers.iter() {
|
||||||
|
trace!("checking tier {}: {:#?}", tier, x.rpc_name_to_block);
|
||||||
|
if let Ok(consensus_head_connections) = x
|
||||||
|
.consensus_head_connections(authorization, web3_connections, min_block_num, tier)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
trace!("success on tier {}", tier);
|
||||||
|
// we got one! hopefully it didn't need to use any backups.
|
||||||
|
// but even if it did need backup servers, that is better than going to a worse tier
|
||||||
|
return Ok(consensus_head_connections);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(anyhow::anyhow!("failed finding consensus on all tiers"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
// #[test]
|
||||||
|
// fn test_simplest_case_consensus_head_connections() {
|
||||||
|
// todo!();
|
||||||
|
// }
|
||||||
|
}
|
0
web3_proxy/src/rpcs/grpc_erigon.rs
Normal file
0
web3_proxy/src/rpcs/grpc_erigon.rs
Normal file
0
web3_proxy/src/rpcs/http.rs
Normal file
0
web3_proxy/src/rpcs/http.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,8 @@
|
|||||||
// TODO: all pub, or export useful things here instead?
|
// TODO: all pub, or export useful things here instead?
|
||||||
pub mod blockchain;
|
pub mod blockchain;
|
||||||
|
pub mod consensus;
|
||||||
pub mod many;
|
pub mod many;
|
||||||
pub mod one;
|
pub mod one;
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod synced_connections;
|
|
||||||
pub mod transactions;
|
pub mod transactions;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -2,22 +2,45 @@ use anyhow::Context;
|
|||||||
use derive_more::From;
|
use derive_more::From;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
// TODO: our own structs for these that handle streaming large responses
|
||||||
|
type EthersHttpProvider = ethers::providers::Provider<ethers::providers::Http>;
|
||||||
|
type EthersWsProvider = ethers::providers::Provider<ethers::providers::Ws>;
|
||||||
|
|
||||||
/// Use HTTP and WS providers.
|
/// Use HTTP and WS providers.
|
||||||
// TODO: instead of an enum, I tried to use Box<dyn Provider>, but hit <https://github.com/gakonst/ethers-rs/issues/592>
|
// TODO: instead of an enum, I tried to use Box<dyn Provider>, but hit <https://github.com/gakonst/ethers-rs/issues/592>
|
||||||
|
// TODO: custom types that let us stream JSON responses
|
||||||
#[derive(From)]
|
#[derive(From)]
|
||||||
pub enum Web3Provider {
|
pub enum Web3Provider {
|
||||||
Http(ethers::providers::Provider<ethers::providers::Http>),
|
Both(EthersHttpProvider, EthersWsProvider),
|
||||||
Ws(ethers::providers::Provider<ethers::providers::Ws>),
|
Http(EthersHttpProvider),
|
||||||
// TODO: only include this for tests.
|
// TODO: deadpool? custom tokio-tungstenite
|
||||||
|
Ws(EthersWsProvider),
|
||||||
|
#[cfg(test)]
|
||||||
Mock,
|
Mock,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Web3Provider {
|
impl Web3Provider {
|
||||||
pub fn ready(&self) -> bool {
|
pub fn ready(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Mock => true,
|
Self::Both(_, ws) => ws.as_ref().ready(),
|
||||||
Self::Http(_) => true,
|
Self::Http(_) => true,
|
||||||
Self::Ws(provider) => provider.as_ref().ready(),
|
Self::Ws(ws) => ws.as_ref().ready(),
|
||||||
|
#[cfg(test)]
|
||||||
|
Self::Mock => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn http(&self) -> Option<&EthersHttpProvider> {
|
||||||
|
match self {
|
||||||
|
Self::Http(x) => Some(x),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ws(&self) -> Option<&EthersWsProvider> {
|
||||||
|
match self {
|
||||||
|
Self::Both(_, x) | Self::Ws(x) => Some(x),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use super::one::Web3Rpc;
|
use super::one::Web3Rpc;
|
||||||
use super::provider::Web3Provider;
|
use super::provider::Web3Provider;
|
||||||
use crate::frontend::authorization::{Authorization, AuthorizationType};
|
use crate::frontend::authorization::Authorization;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use entities::revert_log;
|
use entities::revert_log;
|
||||||
@ -11,7 +11,6 @@ use log::{debug, error, trace, warn, Level};
|
|||||||
use migration::sea_orm::{self, ActiveEnum, ActiveModelTrait};
|
use migration::sea_orm::{self, ActiveEnum, ActiveModelTrait};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::atomic;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use thread_fast_rng::rand::Rng;
|
use thread_fast_rng::rand::Rng;
|
||||||
use tokio::time::{sleep, Duration, Instant};
|
use tokio::time::{sleep, Duration, Instant};
|
||||||
@ -21,20 +20,20 @@ pub enum OpenRequestResult {
|
|||||||
Handle(OpenRequestHandle),
|
Handle(OpenRequestHandle),
|
||||||
/// Unable to start a request. Retry at the given time.
|
/// Unable to start a request. Retry at the given time.
|
||||||
RetryAt(Instant),
|
RetryAt(Instant),
|
||||||
/// Unable to start a request because the server is not synced
|
/// Unable to start a request because no servers are synced
|
||||||
/// contains "true" if backup servers were attempted
|
NotReady,
|
||||||
NotReady(bool),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make RPC requests through this handle and drop it when you are done.
|
/// Make RPC requests through this handle and drop it when you are done.
|
||||||
|
/// Opening this handle checks rate limits. Developers, try to keep opening a handle and using it as close together as possible
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct OpenRequestHandle {
|
pub struct OpenRequestHandle {
|
||||||
authorization: Arc<Authorization>,
|
authorization: Arc<Authorization>,
|
||||||
conn: Arc<Web3Rpc>,
|
rpc: Arc<Web3Rpc>,
|
||||||
provider: Arc<Web3Provider>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Depending on the context, RPC errors can require different handling.
|
/// Depending on the context, RPC errors can require different handling.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
pub enum RequestRevertHandler {
|
pub enum RequestRevertHandler {
|
||||||
/// Log at the trace level. Use when errors are expected.
|
/// Log at the trace level. Use when errors are expected.
|
||||||
TraceLevel,
|
TraceLevel,
|
||||||
@ -123,79 +122,30 @@ impl Authorization {
|
|||||||
|
|
||||||
impl OpenRequestHandle {
|
impl OpenRequestHandle {
|
||||||
pub async fn new(authorization: Arc<Authorization>, conn: Arc<Web3Rpc>) -> Self {
|
pub async fn new(authorization: Arc<Authorization>, conn: Arc<Web3Rpc>) -> Self {
|
||||||
// TODO: take request_id as an argument?
|
|
||||||
// TODO: attach a unique id to this? customer requests have one, but not internal queries
|
|
||||||
// TODO: what ordering?!
|
|
||||||
conn.active_requests.fetch_add(1, atomic::Ordering::Relaxed);
|
|
||||||
|
|
||||||
let mut provider = None;
|
|
||||||
let mut logged = false;
|
|
||||||
while provider.is_none() {
|
|
||||||
// trace!("waiting on provider: locking...");
|
|
||||||
|
|
||||||
let ready_provider = conn
|
|
||||||
.provider_state
|
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
// TODO: hard code true, or take a bool in the `new` function?
|
|
||||||
.provider(true)
|
|
||||||
.await
|
|
||||||
.cloned();
|
|
||||||
// trace!("waiting on provider: unlocked!");
|
|
||||||
|
|
||||||
match ready_provider {
|
|
||||||
None => {
|
|
||||||
if !logged {
|
|
||||||
logged = true;
|
|
||||||
warn!("no provider for {}!", conn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: how should this work? a reconnect should be in progress. but maybe force one now?
|
|
||||||
// TODO: sleep how long? subscribe to something instead? maybe use a watch handle?
|
|
||||||
// TODO: this is going to be way too verbose!
|
|
||||||
sleep(Duration::from_millis(100)).await
|
|
||||||
}
|
|
||||||
Some(x) => provider = Some(x),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let provider = provider.expect("provider was checked already");
|
|
||||||
|
|
||||||
// TODO: handle overflows?
|
|
||||||
// TODO: what ordering?
|
|
||||||
match authorization.as_ref().authorization_type {
|
|
||||||
AuthorizationType::Frontend => {
|
|
||||||
conn.frontend_requests
|
|
||||||
.fetch_add(1, atomic::Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
AuthorizationType::Internal => {
|
|
||||||
conn.internal_requests
|
|
||||||
.fetch_add(1, atomic::Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
authorization,
|
authorization,
|
||||||
conn,
|
rpc: conn,
|
||||||
provider,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connection_name(&self) -> String {
|
pub fn connection_name(&self) -> String {
|
||||||
self.conn.name.clone()
|
self.rpc.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn clone_connection(&self) -> Arc<Web3Rpc> {
|
pub fn clone_connection(&self) -> Arc<Web3Rpc> {
|
||||||
self.conn.clone()
|
self.rpc.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a web3 request
|
/// Send a web3 request
|
||||||
/// By having the request method here, we ensure that the rate limiter was called and connection counts were properly incremented
|
/// By having the request method here, we ensure that the rate limiter was called and connection counts were properly incremented
|
||||||
|
/// depending on how things are locked, you might need to pass the provider in
|
||||||
pub async fn request<P, R>(
|
pub async fn request<P, R>(
|
||||||
self,
|
self,
|
||||||
method: &str,
|
method: &str,
|
||||||
params: &P,
|
params: &P,
|
||||||
revert_handler: RequestRevertHandler,
|
revert_handler: RequestRevertHandler,
|
||||||
|
unlocked_provider: Option<Arc<Web3Provider>>,
|
||||||
) -> Result<R, ProviderError>
|
) -> Result<R, ProviderError>
|
||||||
where
|
where
|
||||||
// TODO: not sure about this type. would be better to not need clones, but measure and spawns combine to need it
|
// TODO: not sure about this type. would be better to not need clones, but measure and spawns combine to need it
|
||||||
@ -205,14 +155,57 @@ impl OpenRequestHandle {
|
|||||||
// TODO: use tracing spans
|
// TODO: use tracing spans
|
||||||
// TODO: including params in this log is way too verbose
|
// TODO: including params in this log is way too verbose
|
||||||
// trace!(rpc=%self.conn, %method, "request");
|
// trace!(rpc=%self.conn, %method, "request");
|
||||||
|
trace!("requesting from {}", self.rpc);
|
||||||
|
|
||||||
|
let mut provider = if unlocked_provider.is_some() {
|
||||||
|
unlocked_provider
|
||||||
|
} else {
|
||||||
|
self.rpc.provider.read().await.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut logged = false;
|
||||||
|
while provider.is_none() {
|
||||||
|
// trace!("waiting on provider: locking...");
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
if !logged {
|
||||||
|
debug!("no provider for open handle on {}", self.rpc);
|
||||||
|
logged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = self.rpc.provider.read().await.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let provider = provider.expect("provider was checked already");
|
||||||
|
|
||||||
|
self.rpc
|
||||||
|
.total_requests
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
|
self.rpc
|
||||||
|
.active_requests
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
|
// let latency = Instant::now();
|
||||||
|
|
||||||
// TODO: replace ethers-rs providers with our own that supports streaming the responses
|
// TODO: replace ethers-rs providers with our own that supports streaming the responses
|
||||||
let response = match &*self.provider {
|
let response = match provider.as_ref() {
|
||||||
|
#[cfg(test)]
|
||||||
Web3Provider::Mock => unimplemented!(),
|
Web3Provider::Mock => unimplemented!(),
|
||||||
Web3Provider::Http(provider) => provider.request(method, params).await,
|
Web3Provider::Ws(p) => p.request(method, params).await,
|
||||||
Web3Provider::Ws(provider) => provider.request(method, params).await,
|
Web3Provider::Http(p) | Web3Provider::Both(p, _) => {
|
||||||
|
// TODO: i keep hearing that http is faster. but ws has always been better for me. investigate more with actual benchmarks
|
||||||
|
p.request(method, params).await
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// note. we intentionally do not record this latency now. we do NOT want to measure errors
|
||||||
|
// let latency = latency.elapsed();
|
||||||
|
|
||||||
|
self.rpc
|
||||||
|
.active_requests
|
||||||
|
.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
// // TODO: i think ethers already has trace logging (and does it much more fancy)
|
// // TODO: i think ethers already has trace logging (and does it much more fancy)
|
||||||
// trace!(
|
// trace!(
|
||||||
// "response from {} for {} {:?}: {:?}",
|
// "response from {} for {} {:?}: {:?}",
|
||||||
@ -266,8 +259,22 @@ impl OpenRequestHandle {
|
|||||||
// check for "execution reverted" here
|
// check for "execution reverted" here
|
||||||
let response_type = if let ProviderError::JsonRpcClientError(err) = err {
|
let response_type = if let ProviderError::JsonRpcClientError(err) = err {
|
||||||
// Http and Ws errors are very similar, but different types
|
// Http and Ws errors are very similar, but different types
|
||||||
let msg = match &*self.provider {
|
let msg = match &*provider {
|
||||||
|
#[cfg(test)]
|
||||||
Web3Provider::Mock => unimplemented!(),
|
Web3Provider::Mock => unimplemented!(),
|
||||||
|
Web3Provider::Both(_, _) => {
|
||||||
|
if let Some(HttpClientError::JsonRpcError(err)) =
|
||||||
|
err.downcast_ref::<HttpClientError>()
|
||||||
|
{
|
||||||
|
Some(&err.message)
|
||||||
|
} else if let Some(WsClientError::JsonRpcError(err)) =
|
||||||
|
err.downcast_ref::<WsClientError>()
|
||||||
|
{
|
||||||
|
Some(&err.message)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
Web3Provider::Http(_) => {
|
Web3Provider::Http(_) => {
|
||||||
if let Some(HttpClientError::JsonRpcError(err)) =
|
if let Some(HttpClientError::JsonRpcError(err)) =
|
||||||
err.downcast_ref::<HttpClientError>()
|
err.downcast_ref::<HttpClientError>()
|
||||||
@ -290,10 +297,10 @@ impl OpenRequestHandle {
|
|||||||
|
|
||||||
if let Some(msg) = msg {
|
if let Some(msg) = msg {
|
||||||
if msg.starts_with("execution reverted") {
|
if msg.starts_with("execution reverted") {
|
||||||
trace!("revert from {}", self.conn);
|
trace!("revert from {}", self.rpc);
|
||||||
ResponseTypes::Revert
|
ResponseTypes::Revert
|
||||||
} else if msg.contains("limit") || msg.contains("request") {
|
} else if msg.contains("limit") || msg.contains("request") {
|
||||||
trace!("rate limit from {}", self.conn);
|
trace!("rate limit from {}", self.rpc);
|
||||||
ResponseTypes::RateLimit
|
ResponseTypes::RateLimit
|
||||||
} else {
|
} else {
|
||||||
ResponseTypes::Ok
|
ResponseTypes::Ok
|
||||||
@ -306,10 +313,10 @@ impl OpenRequestHandle {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if matches!(response_type, ResponseTypes::RateLimit) {
|
if matches!(response_type, ResponseTypes::RateLimit) {
|
||||||
if let Some(hard_limit_until) = self.conn.hard_limit_until.as_ref() {
|
if let Some(hard_limit_until) = self.rpc.hard_limit_until.as_ref() {
|
||||||
let retry_at = Instant::now() + Duration::from_secs(1);
|
let retry_at = Instant::now() + Duration::from_secs(1);
|
||||||
|
|
||||||
trace!("retry {} at: {:?}", self.conn, retry_at);
|
trace!("retry {} at: {:?}", self.rpc, retry_at);
|
||||||
|
|
||||||
hard_limit_until.send_replace(retry_at);
|
hard_limit_until.send_replace(retry_at);
|
||||||
}
|
}
|
||||||
@ -322,14 +329,14 @@ impl OpenRequestHandle {
|
|||||||
if matches!(response_type, ResponseTypes::Revert) {
|
if matches!(response_type, ResponseTypes::Revert) {
|
||||||
debug!(
|
debug!(
|
||||||
"bad response from {}! method={} params={:?} err={:?}",
|
"bad response from {}! method={} params={:?} err={:?}",
|
||||||
self.conn, method, params, err
|
self.rpc, method, params, err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RequestRevertHandler::TraceLevel => {
|
RequestRevertHandler::TraceLevel => {
|
||||||
trace!(
|
trace!(
|
||||||
"bad response from {}! method={} params={:?} err={:?}",
|
"bad response from {}! method={} params={:?} err={:?}",
|
||||||
self.conn,
|
self.rpc,
|
||||||
method,
|
method,
|
||||||
params,
|
params,
|
||||||
err
|
err
|
||||||
@ -339,20 +346,20 @@ impl OpenRequestHandle {
|
|||||||
// TODO: include params if not running in release mode
|
// TODO: include params if not running in release mode
|
||||||
error!(
|
error!(
|
||||||
"bad response from {}! method={} err={:?}",
|
"bad response from {}! method={} err={:?}",
|
||||||
self.conn, method, err
|
self.rpc, method, err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
RequestRevertHandler::WarnLevel => {
|
RequestRevertHandler::WarnLevel => {
|
||||||
// TODO: include params if not running in release mode
|
// TODO: include params if not running in release mode
|
||||||
warn!(
|
warn!(
|
||||||
"bad response from {}! method={} err={:?}",
|
"bad response from {}! method={} err={:?}",
|
||||||
self.conn, method, err
|
self.rpc, method, err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
RequestRevertHandler::Save => {
|
RequestRevertHandler::Save => {
|
||||||
trace!(
|
trace!(
|
||||||
"bad response from {}! method={} params={:?} err={:?}",
|
"bad response from {}! method={} params={:?} err={:?}",
|
||||||
self.conn,
|
self.rpc,
|
||||||
method,
|
method,
|
||||||
params,
|
params,
|
||||||
err
|
err
|
||||||
@ -372,16 +379,16 @@ impl OpenRequestHandle {
|
|||||||
tokio::spawn(f);
|
tokio::spawn(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: record request latency
|
||||||
|
// let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
|
||||||
|
// TODO: is this lock here a problem? should this be done through a channel? i started to code it, but it didn't seem to matter
|
||||||
|
// let mut latency_recording = self.rpc.request_latency.write();
|
||||||
|
|
||||||
|
// latency_recording.record(latency_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for OpenRequestHandle {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.conn
|
|
||||||
.active_requests
|
|
||||||
.fetch_sub(1, atomic::Ordering::AcqRel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
use super::blockchain::{ArcBlock, SavedBlock};
|
|
||||||
use super::many::Web3Rpcs;
|
|
||||||
use super::one::Web3Rpc;
|
|
||||||
use ethers::prelude::{H256, U64};
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::fmt;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
/// A collection of Web3Rpcs that are on the same block.
|
|
||||||
/// Serialize is so we can print it on our debug endpoint
|
|
||||||
#[derive(Clone, Default, Serialize)]
|
|
||||||
pub struct ConsensusWeb3Rpcs {
|
|
||||||
// TODO: store ArcBlock instead?
|
|
||||||
pub(super) head_block: Option<SavedBlock>,
|
|
||||||
// TODO: this should be able to serialize, but it isn't
|
|
||||||
#[serde(skip_serializing)]
|
|
||||||
pub(super) conns: Vec<Arc<Web3Rpc>>,
|
|
||||||
pub(super) num_checked_conns: usize,
|
|
||||||
pub(super) includes_backups: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConsensusWeb3Rpcs {
|
|
||||||
pub fn num_conns(&self) -> usize {
|
|
||||||
self.conns.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sum_soft_limit(&self) -> u32 {
|
|
||||||
self.conns.iter().fold(0, |sum, rpc| sum + rpc.soft_limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: sum_hard_limit?
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for ConsensusWeb3Rpcs {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
// TODO: the default formatter takes forever to write. this is too quiet though
|
|
||||||
// TODO: print the actual conns?
|
|
||||||
f.debug_struct("ConsensusConnections")
|
|
||||||
.field("head_block", &self.head_block)
|
|
||||||
.field("num_conns", &self.conns.len())
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Web3Rpcs {
|
|
||||||
pub fn head_block(&self) -> Option<ArcBlock> {
|
|
||||||
self.watch_consensus_head_receiver
|
|
||||||
.as_ref()
|
|
||||||
.map(|x| x.borrow().clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn head_block_hash(&self) -> Option<H256> {
|
|
||||||
self.head_block().and_then(|x| x.hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn head_block_num(&self) -> Option<U64> {
|
|
||||||
self.head_block().and_then(|x| x.number)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn synced(&self) -> bool {
|
|
||||||
!self
|
|
||||||
.watch_consensus_connections_sender
|
|
||||||
.borrow()
|
|
||||||
.conns
|
|
||||||
.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn num_synced_rpcs(&self) -> usize {
|
|
||||||
self.watch_consensus_connections_sender.borrow().conns.len()
|
|
||||||
}
|
|
||||||
}
|
|
@ -28,13 +28,15 @@ impl Web3Rpcs {
|
|||||||
// TODO: might not be a race. might be a nonce thats higher than the current account nonce. geth discards chains
|
// TODO: might not be a race. might be a nonce thats higher than the current account nonce. geth discards chains
|
||||||
// TODO: yearn devs have had better luck with batching these, but i think that's likely just adding a delay itself
|
// TODO: yearn devs have had better luck with batching these, but i think that's likely just adding a delay itself
|
||||||
// TODO: if one rpc fails, try another?
|
// TODO: if one rpc fails, try another?
|
||||||
let tx: Transaction = match rpc.try_request_handle(authorization, false).await {
|
// TODO: try_request_handle, or wait_for_request_handle? I think we want wait here
|
||||||
|
let tx: Transaction = match rpc.try_request_handle(authorization, None).await {
|
||||||
Ok(OpenRequestResult::Handle(handle)) => {
|
Ok(OpenRequestResult::Handle(handle)) => {
|
||||||
handle
|
handle
|
||||||
.request(
|
.request(
|
||||||
"eth_getTransactionByHash",
|
"eth_getTransactionByHash",
|
||||||
&(pending_tx_id,),
|
&(pending_tx_id,),
|
||||||
Level::Error.into(),
|
Level::Error.into(),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
0
web3_proxy/src/rpcs/ws.rs
Normal file
0
web3_proxy/src/rpcs/ws.rs
Normal file
Loading…
Reference in New Issue
Block a user