From 012c946361ef346f0aa9b81ee8783dab29ec31e4 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Wed, 9 Oct 2024 11:01:20 -0500 Subject: [PATCH] feat(provider): add cached DA oracles for bedrock/nitro feat(provider): add a local bedrock DA provider feat(provider): add a cached arbitrum nitro da gas oracle --- .github/workflows/ci.yaml | 11 +- .github/workflows/deny.yaml | 2 +- .github/workflows/docker-release.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/unit.yaml | 4 +- .gitmodules | 3 + Cargo.lock | 251 +++++++++++++++--- Cargo.toml | 17 +- crates/bindings/fastlz/Cargo.toml | 12 + crates/bindings/fastlz/build.rs | 42 +++ crates/bindings/fastlz/fastlz | 1 + crates/bindings/fastlz/src/lib.rs | 51 ++++ crates/contracts/Cargo.toml | 2 +- crates/provider/Cargo.toml | 7 +- crates/provider/src/alloy/da/arbitrum.rs | 7 +- crates/provider/src/alloy/da/local/bedrock.rs | 244 +++++++++++++++++ crates/provider/src/alloy/da/local/mod.rs | 20 ++ .../provider/src/alloy/da/local/multicall.rs | 60 +++++ crates/provider/src/alloy/da/local/nitro.rs | 187 +++++++++++++ crates/provider/src/alloy/da/mod.rs | 217 ++++++++++++++- crates/provider/src/alloy/da/optimism.rs | 10 +- crates/provider/src/alloy/entry_point/v0_6.rs | 9 +- crates/provider/src/alloy/entry_point/v0_7.rs | 9 +- crates/types/Cargo.toml | 2 +- crates/types/src/chain.rs | 4 + 25 files changed, 1123 insertions(+), 53 deletions(-) create mode 100644 crates/bindings/fastlz/Cargo.toml create mode 100644 crates/bindings/fastlz/build.rs create mode 160000 crates/bindings/fastlz/fastlz create mode 100644 crates/bindings/fastlz/src/lib.rs create mode 100644 crates/provider/src/alloy/da/local/bedrock.rs create mode 100644 crates/provider/src/alloy/da/local/mod.rs create mode 100644 crates/provider/src/alloy/da/local/multicall.rs create mode 100644 crates/provider/src/alloy/da/local/nitro.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6ea5b0668..6ec5ee1a6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,7 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install protobuf run: sudo apt-get -y install protobuf-compiler - name: Install toolchain @@ -61,7 +64,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + submodules: recursive - name: Install codespell run: sudo pip3 install codespell tomli @@ -80,7 +85,7 @@ jobs: runs-on: ubuntu-latest name: check conventional commit compliance steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # pick the pr HEAD instead of the merge commit diff --git a/.github/workflows/deny.yaml b/.github/workflows/deny.yaml index 3daedde8a..7489f7d3a 100644 --- a/.github/workflows/deny.yaml +++ b/.github/workflows/deny.yaml @@ -8,7 +8,7 @@ jobs: cargo-deny: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v1 with: command: check bans licenses sources diff --git a/.github/workflows/docker-release.yaml b/.github/workflows/docker-release.yaml index 99aed36d7..89debe543 100644 --- a/.github/workflows/docker-release.yaml +++ b/.github/workflows/docker-release.yaml @@ -23,7 +23,7 @@ jobs: contents: read steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index da7626c7c..1ebe17247 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -139,7 +139,7 @@ jobs: steps: # This is necessary for generating the changelog. It has to come before "Download Artifacts" or else it deletes the artifacts. - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/unit.yaml b/.github/workflows/unit.yaml index 1615b9735..518ce700c 100644 --- a/.github/workflows/unit.yaml +++ b/.github/workflows/unit.yaml @@ -16,9 +16,9 @@ jobs: timeout-minutes: 60 steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: - submodules: 'recursive' + submodules: recursive - name: Install toolchain uses: dtolnay/rust-toolchain@1.81.0 diff --git a/.gitmodules b/.gitmodules index a6acc126d..aaa528b9f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -26,3 +26,6 @@ path = test/spec-tests/v0_7/bundler-spec-tests url = git@github.com:alchemyplatform/bundler-spec-tests.git ignore = dirty +[submodule "crates/bindings/fastlz/fastlz"] + path = crates/bindings/fastlz/fastlz + url = https://github.com/ariya/FastLZ diff --git a/Cargo.lock b/Cargo.lock index a62d6aec0..edd7b5b7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1009,7 +1009,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0e249228c6ad2d240c2dc94b714d711629d52bad946075d8e9b2f5391f0703" dependencies = [ - "bindgen", + "bindgen 0.69.4", "cc", "cmake", "dunce", @@ -1243,13 +1243,17 @@ dependencies = [ "aws-smithy-types", "bytes", "fastrand 2.1.1", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "http-body 1.0.1", "httparse", + "hyper 0.14.30", + "hyper-rustls 0.24.2", "once_cell", "pin-project-lite", "pin-utils", + "rustls 0.21.12", "tokio", "tracing", ] @@ -1436,6 +1440,26 @@ dependencies = [ "which", ] +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.77", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -1858,6 +1882,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -2486,6 +2520,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.5.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.6" @@ -2647,6 +2700,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.4.1" @@ -2656,7 +2733,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "httparse", @@ -2668,6 +2745,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.30", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.2" @@ -2676,13 +2769,13 @@ checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper", + "hyper 1.4.1", "hyper-util", "log", - "rustls", + "rustls 0.23.12", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tower-service", "webpki-roots", ] @@ -2693,7 +2786,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" dependencies = [ - "hyper", + "hyper 1.4.1", "hyper-util", "pin-project-lite", "tokio", @@ -2711,7 +2804,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper", + "hyper 1.4.1", "pin-project-lite", "socket2 0.5.7", "tokio", @@ -2946,13 +3039,13 @@ dependencies = [ "http 1.1.0", "jsonrpsee-core", "pin-project", - "rustls", + "rustls 0.23.12", "rustls-pki-types", "rustls-platform-verifier", "soketto", "thiserror", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tokio-util", "tracing", "url", @@ -2994,12 +3087,12 @@ dependencies = [ "async-trait", "base64 0.22.1", "http-body 1.0.1", - "hyper", - "hyper-rustls", + "hyper 1.4.1", + "hyper-rustls 0.27.2", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", - "rustls", + "rustls 0.23.12", "rustls-platform-verifier", "serde", "serde_json", @@ -3033,7 +3126,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.4.1", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", @@ -3167,7 +3260,7 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae9ea4b75e1a81675429dafe43441df1caea70081e82246a8cccf514884a88bb" dependencies = [ - "bindgen", + "bindgen 0.69.4", "errno", "libc", ] @@ -3288,7 +3381,7 @@ checksum = "b4f0c8427b39666bf970460908b213ec09b3b350f20c0c2eabcbba51704a08e6" dependencies = [ "base64 0.22.1", "http-body-util", - "hyper", + "hyper 1.4.1", "hyper-util", "indexmap 2.5.0", "ipnet", @@ -4025,7 +4118,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls", + "rustls 0.23.12", "socket2 0.5.7", "thiserror", "tokio", @@ -4042,7 +4135,7 @@ dependencies = [ "rand", "ring", "rustc-hash 2.0.0", - "rustls", + "rustls 0.23.12", "slab", "thiserror", "tinyvec", @@ -4136,6 +4229,26 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redis" version = "0.24.0" @@ -4230,8 +4343,8 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.4.1", + "hyper-rustls 0.27.2", "hyper-util", "ipnet", "js-sys", @@ -4241,15 +4354,15 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", - "rustls-pemfile", + "rustls 0.23.12", + "rustls-pemfile 2.1.3", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tower-service", "url", "wasm-bindgen", @@ -4288,6 +4401,8 @@ dependencies = [ "dyn-clone", "futures-util", "metrics", + "pin-project", + "rayon", "reth-metrics", "thiserror", "tokio", @@ -4433,7 +4548,7 @@ dependencies = [ "sscanf", "tokio", "tokio-metrics", - "tokio-rustls", + "tokio-rustls 0.26.0", "tokio-util", "tracing", "tracing-appender", @@ -4441,6 +4556,14 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "rundler-bindings-fastlz" +version = "0.3.0" +dependencies = [ + "bindgen 0.70.1", + "cc", +] + [[package]] name = "rundler-builder" version = "0.3.0" @@ -4560,9 +4683,12 @@ dependencies = [ "anyhow", "async-trait", "auto_impl", + "const-hex", "futures-util", "mockall", "reqwest", + "reth-tasks", + "rundler-bindings-fastlz", "rundler-contracts", "rundler-provider", "rundler-types", @@ -4785,6 +4911,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.12" @@ -4796,11 +4934,23 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.7", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + [[package]] name = "rustls-native-certs" version = "0.7.3" @@ -4808,12 +4958,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 2.1.3", "rustls-pki-types", "schannel", "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustls-pemfile" version = "2.1.3" @@ -4841,10 +5000,10 @@ dependencies = [ "jni", "log", "once_cell", - "rustls", - "rustls-native-certs", + "rustls 0.23.12", + "rustls-native-certs 0.7.3", "rustls-platform-verifier-android", - "rustls-webpki", + "rustls-webpki 0.102.7", "security-framework", "security-framework-sys", "webpki-roots", @@ -4857,6 +5016,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.102.7" @@ -4928,6 +5097,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sec1" version = "0.7.3" @@ -5538,13 +5717,23 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls", + "rustls 0.23.12", "rustls-pki-types", "tokio", ] @@ -5620,11 +5809,11 @@ dependencies = [ "axum", "base64 0.22.1", "bytes", - "h2", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.4.1", "hyper-timeout", "hyper-util", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index 7d3ca9840..8ecdcc656 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,16 @@ [workspace] members = [ - "bin/*", - "crates/*", + "bin/rundler/", + "crates/bindings/fastlz/", + "crates/builder/", + "crates/contracts/", + "crates/pool/", + "crates/provider/", + "crates/rpc/", + "crates/sim/", + "crates/task/", + "crates/types/", + "crates/utils/", ] default-members = ["bin/rundler"] resolver = "2" @@ -21,6 +30,7 @@ rundler-sim = { path = "crates/sim" } rundler-task = { path = "crates/task" } rundler-types = { path = "crates/types" } rundler-utils = { path = "crates/utils" } +rundler-bindings-fastlz = { path = "crates/bindings/fastlz" } # alloy core alloy-primitives = "0.8.5" @@ -51,8 +61,9 @@ reth-tasks = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.0.7" } anyhow = "1.0.89" async-trait = "0.1.83" auto_impl = "1.2.0" -aws-config = { version = "1.5.6", default-features = false } +aws-config = { version = "1.5.6", default-features = false, features = ["rt-tokio", "rustls"] } cargo-husky = { version = "1", default-features = false, features = ["user-hooks"] } +const-hex = "1.12.0" futures = "0.3.30" futures-util = "0.3.30" itertools = "0.13.0" diff --git a/crates/bindings/fastlz/Cargo.toml b/crates/bindings/fastlz/Cargo.toml new file mode 100644 index 000000000..6b3113508 --- /dev/null +++ b/crates/bindings/fastlz/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rundler-bindings-fastlz" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish = false + +[build-dependencies] +bindgen = "0.70.1" +cc = { version = "1", features = ["parallel"] } diff --git a/crates/bindings/fastlz/build.rs b/crates/bindings/fastlz/build.rs new file mode 100644 index 000000000..81b035bc4 --- /dev/null +++ b/crates/bindings/fastlz/build.rs @@ -0,0 +1,42 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +// Credit to https://github.com/mvertescher/fastlz-rs/blob/master/fastlz-sys/build.rs + +extern crate cc; + +use std::{env, path::PathBuf}; + +fn main() { + let mut build = cc::Build::new(); + build.include("fastlz"); + + #[cfg(target_os = "linux")] + build.flag("-Wno-unused-parameter"); + + let files = ["fastlz/fastlz.c"]; + + build.files(files.iter()).compile("fastlz"); + println!("cargo:rustc-link-lib=static=fastlz"); + + // Generate bindings + let bindings = bindgen::Builder::default() + .header("fastlz/fastlz.h") + .generate() + .expect("Unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!") +} diff --git a/crates/bindings/fastlz/fastlz b/crates/bindings/fastlz/fastlz new file mode 160000 index 000000000..344eb4025 --- /dev/null +++ b/crates/bindings/fastlz/fastlz @@ -0,0 +1 @@ +Subproject commit 344eb4025f9ae866ebf7a2ec48850f7113a97a42 diff --git a/crates/bindings/fastlz/src/lib.rs b/crates/bindings/fastlz/src/lib.rs new file mode 100644 index 000000000..0de4539ed --- /dev/null +++ b/crates/bindings/fastlz/src/lib.rs @@ -0,0 +1,51 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +//! Raw FastLZ FFI bindings + +// Credit to https://github.com/mvertescher/fastlz-rs/blob/master/src/lib.rs + +use core::ffi::c_void; + +// This is a generated binding of the fastlz C library at commit +// 344eb4025f9ae866ebf7a2ec48850f7113a97a42 as required by the fastlz implementation by +// solady's LibZip.sol here: https://github.com/Vectorized/solady/blob/8b0601e1573ed17a583fdab2b2ebfb895507ec15/src/utils/LibZip.sol#L19 +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +/// Compress a block of data in the input buffer and returns the size of +/// compressed block. The size of input buffer is specified by length. The +/// minimum input buffer size is 16. +/// +/// The output buffer must be at least 5% larger than the input buffer +/// and can not be smaller than 66 bytes. +/// +/// If the input is not compressible, the return value might be larger than +/// length (input buffer size). +/// +/// The input buffer and the output buffer can not overlap. +/// +/// MODIFICATION: Always use level 1 compression to match LibZip.sol +/// +/// Original credit to https://github.com/mvertescher/fastlz-rs/blob/master/src/lib.rs +pub fn compress<'a>(input: &[u8], output: &'a mut [u8]) -> &'a mut [u8] { + let in_ptr: *const c_void = input as *const _ as *const c_void; + let out_ptr: *mut c_void = output as *mut _ as *mut c_void; + let size = unsafe { fastlz_compress_level(1, in_ptr, input.len() as i32, out_ptr) }; + if size as usize > output.len() { + panic!("Output buffer overflow!"); + } + + let ret: &mut [u8] = + unsafe { core::slice::from_raw_parts_mut(out_ptr as *mut _, size as usize) }; + ret +} diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml index df058d0c4..416328f15 100644 --- a/crates/contracts/Cargo.toml +++ b/crates/contracts/Cargo.toml @@ -12,7 +12,7 @@ alloy-contract.workspace = true alloy-primitives.workspace = true alloy-sol-macro = { workspace = true, features = ["json"] } alloy-sol-types.workspace = true -const-hex = "1.11.3" +const-hex.workspace = true [build-dependencies] serde_json.workspace = true diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml index 30a6b390b..c9565df12 100644 --- a/crates/provider/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -8,6 +8,7 @@ repository.workspace = true publish = false [dependencies] +rundler-bindings-fastlz.workspace = true rundler-contracts.workspace = true rundler-types.workspace = true rundler-utils.workspace = true @@ -24,13 +25,17 @@ alloy-rpc-types-trace.workspace = true alloy-sol-types.workspace = true alloy-transport.workspace = true alloy-transport-http.workspace = true -reqwest.workspace = true + +reth-tasks = { workspace = true, features = ["rayon"] } anyhow.workspace = true async-trait.workspace = true auto_impl.workspace = true +const-hex.workspace = true thiserror.workspace = true futures-util.workspace = true +reqwest.workspace = true +tokio.workspace = true tower.workspace = true tracing.workspace = true url.workspace = true diff --git a/crates/provider/src/alloy/da/arbitrum.rs b/crates/provider/src/alloy/da/arbitrum.rs index 637109b1e..0e5f1b113 100644 --- a/crates/provider/src/alloy/da/arbitrum.rs +++ b/crates/provider/src/alloy/da/arbitrum.rs @@ -11,7 +11,7 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use alloy_primitives::{Address, Bytes}; +use alloy_primitives::{Address, Bytes, B256}; use alloy_provider::Provider as AlloyProvider; use alloy_sol_types::sol; use alloy_transport::Transport; @@ -63,14 +63,15 @@ where { async fn estimate_da_gas( &self, - to_address: Address, + _hash: B256, + to: Address, data: Bytes, block: BlockHashOrNumber, _gas_price: u128, ) -> ProviderResult { let ret = self .node_interface - .gasEstimateL1Component(to_address, true, data) + .gasEstimateL1Component(to, true, data) .block(block.into()) .call() .await?; diff --git a/crates/provider/src/alloy/da/local/bedrock.rs b/crates/provider/src/alloy/da/local/bedrock.rs new file mode 100644 index 000000000..c65b184c1 --- /dev/null +++ b/crates/provider/src/alloy/da/local/bedrock.rs @@ -0,0 +1,244 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::sync::Mutex as StdMutex; + +use alloy_primitives::{Address, Bytes, B256}; +use alloy_provider::Provider as AlloyProvider; +use alloy_rpc_types_eth::state::{AccountOverride, StateOverride}; +use alloy_transport::Transport; +use anyhow::Context; +use reth_tasks::pool::BlockingTaskPool; +use rundler_utils::cache::LruMap; +use tokio::sync::Mutex as TokioMutex; + +use super::multicall::{self, Multicall::MulticallInstance, MULTICALL_BYTECODE}; +use crate::{ + alloy::da::{ + optimism::GasPriceOracle::{ + baseFeeScalarCall, blobBaseFeeCall, blobBaseFeeScalarCall, l1BaseFeeCall, + GasPriceOracleCalls, GasPriceOracleInstance, + }, + DAGasOracle, + }, + BlockHashOrNumber, ProviderResult, +}; + +// From https://github.com/ethereum-optimism/optimism/blob/f93f9f40adcd448168c6ea27820aeee5da65fcbd/packages/contracts-bedrock/src/L2/GasPriceOracle.sol#L26 +const DECIMAL_SCALAR: u128 = 1_000_000_000_000; +const COST_INTERCEPT: i128 = -42_585_600; +const COST_FASTLZ_COEF: i128 = 836_500; +const MIN_TRANSACTION_SIZE: i128 = 100_000_000; + +/// Local Bedrock DA gas oracle +/// +/// Details: https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/fjord/exec-engine.md#fjord-l1-cost-fee-changes-fastlz-estimator +#[derive(Debug)] +pub(crate) struct LocalBedrockDAGasOracle { + oracle: GasPriceOracleInstance, + multicaller: MulticallInstance, + block_da_data_cache: TokioMutex>, + uo_cache: StdMutex>, + blocking_task_pool: BlockingTaskPool, +} + +#[derive(Debug, Clone, Copy)] +struct BlockDAData { + base_fee_scalar: u64, + l1_base_fee: u64, + blob_base_fee_scalar: u64, + blob_base_fee: u64, +} + +impl LocalBedrockDAGasOracle +where + AP: AlloyProvider + Clone, + T: Transport + Clone, +{ + pub(crate) fn new(oracle_address: Address, provider: AP) -> Self { + let oracle = GasPriceOracleInstance::new(oracle_address, provider.clone()); + let multicaller = MulticallInstance::new(Address::random(), provider); + Self { + oracle, + multicaller, + block_da_data_cache: TokioMutex::new(LruMap::new(100)), + uo_cache: StdMutex::new(LruMap::new(10000)), + blocking_task_pool: BlockingTaskPool::build() + .expect("failed to build blocking task pool"), + } + } +} + +#[async_trait::async_trait] +impl DAGasOracle for LocalBedrockDAGasOracle +where + AP: AlloyProvider, + T: Transport + Clone, +{ + async fn estimate_da_gas( + &self, + hash: B256, + _to: Address, + data: Bytes, + block: BlockHashOrNumber, + gas_price: u128, + ) -> ProviderResult { + let block_da_data = { + let mut cache = self.block_da_data_cache.lock().await; + + match cache.get(&block) { + Some(block_da_data) => *block_da_data, + None => { + let block_da_data = self.get_block_da_data(block).await?; + cache.insert(block, block_da_data); + block_da_data + } + } + }; + + let l1_fee = self.fjord_l1_fee(hash, data, &block_da_data).await?; + Ok(l1_fee.checked_div(gas_price).unwrap_or(u128::MAX)) + } +} + +impl LocalBedrockDAGasOracle +where + AP: AlloyProvider, + T: Transport + Clone, +{ + async fn is_fjord(&self) -> bool { + self.oracle + .isFjord() + .call() + .await + .map(|r| r.isFjord) + .unwrap_or(false) + } + + async fn get_block_da_data(&self, block: BlockHashOrNumber) -> ProviderResult { + assert!(self.is_fjord().await); + + let calls = vec![ + multicall::create_call( + *self.oracle.address(), + GasPriceOracleCalls::baseFeeScalar(baseFeeScalarCall {}), + ), + multicall::create_call( + *self.oracle.address(), + GasPriceOracleCalls::l1BaseFee(l1BaseFeeCall {}), + ), + multicall::create_call( + *self.oracle.address(), + GasPriceOracleCalls::blobBaseFeeScalar(blobBaseFeeScalarCall {}), + ), + multicall::create_call( + *self.oracle.address(), + GasPriceOracleCalls::blobBaseFee(blobBaseFeeCall {}), + ), + ]; + + let mut overrides = StateOverride::default(); + let account = AccountOverride { + code: Some(MULTICALL_BYTECODE.clone()), + ..Default::default() + }; + overrides.insert(*self.multicaller.address(), account); + + let result = self + .multicaller + .aggregate3(calls) + .call() + .overrides(&overrides) + .block(block.into()) + .await?; + + if result.returnData.len() != 4 { + Err(anyhow::anyhow!( + "multicall returned unexpected number of results" + ))?; + } else if result.returnData.iter().any(|r| !r.success) { + Err(anyhow::anyhow!("multicall returned some failed results"))?; + } + + let base_fee_scalar = + multicall::decode_result::(&result.returnData[0].returnData)?._0 + as u64; + let l1_base_fee = + multicall::decode_result::(&result.returnData[1].returnData)? + ._0 + .try_into() + .context("l1_base_fee too large for u64")?; + let blob_base_fee_scalar = + multicall::decode_result::(&result.returnData[2].returnData)?._0 + as u64; + let blob_base_fee = + multicall::decode_result::(&result.returnData[3].returnData)? + ._0 + .try_into() + .context("blob_base_fee too large for u64")?; + + Ok(BlockDAData { + base_fee_scalar, + l1_base_fee, + blob_base_fee_scalar, + blob_base_fee, + }) + } + + async fn fjord_l1_fee( + &self, + hash: B256, + data: Bytes, + block_da_data: &BlockDAData, + ) -> ProviderResult { + let maybe_uo_units = self.uo_cache.lock().unwrap().get(&hash).cloned(); + let uo_units = match maybe_uo_units { + Some(uo_units) => uo_units, + None => { + // Blocking call compressing potentially a lot of data. + // Generally takes more than 100µs so should be spawned on blocking threadpool. + // https://ryhl.io/blog/async-what-is-blocking/ + // https://docs.rs/tokio/latest/tokio/index.html#cpu-bound-tasks-and-blocking-code + let compressed_len = self + .blocking_task_pool + .spawn(move || { + let mut buf = vec![0; data.len() * 2]; + let compressed = rundler_bindings_fastlz::compress(&data, &mut buf); + compressed.len() as u64 + }) + .await + .map_err(|e| anyhow::anyhow!("failed to compress data: {:?}", e))?; + + let uo_units = compressed_len + 68; + self.uo_cache.lock().unwrap().insert(hash, uo_units); + uo_units + } + }; + + Ok(Self::fjord_l1_cost(uo_units, block_da_data)) + } + + fn fjord_l1_cost(fast_lz_size: u64, block_da_data: &BlockDAData) -> u128 { + let estimated_size = Self::fjord_linear_regression(fast_lz_size) as u128; + let fee_scaled = (block_da_data.base_fee_scalar * 16 * block_da_data.l1_base_fee + + block_da_data.blob_base_fee_scalar * block_da_data.blob_base_fee) + as u128; + (estimated_size * fee_scaled) / DECIMAL_SCALAR + } + + fn fjord_linear_regression(fast_lz_size: u64) -> u64 { + let estimated_size = COST_INTERCEPT + COST_FASTLZ_COEF * fast_lz_size as i128; + let ret = estimated_size.clamp(MIN_TRANSACTION_SIZE, u64::MAX as i128); + ret as u64 + } +} diff --git a/crates/provider/src/alloy/da/local/mod.rs b/crates/provider/src/alloy/da/local/mod.rs new file mode 100644 index 000000000..7caaf45e8 --- /dev/null +++ b/crates/provider/src/alloy/da/local/mod.rs @@ -0,0 +1,20 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +mod bedrock; +pub(crate) use bedrock::LocalBedrockDAGasOracle; + +mod nitro; +pub(crate) use nitro::CachedNitroDAGasOracle; + +mod multicall; diff --git a/crates/provider/src/alloy/da/local/multicall.rs b/crates/provider/src/alloy/da/local/multicall.rs new file mode 100644 index 000000000..b2c94bd78 --- /dev/null +++ b/crates/provider/src/alloy/da/local/multicall.rs @@ -0,0 +1,60 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use alloy_primitives::{Address, Bytes}; +use alloy_sol_types::{sol, SolCall, SolInterface}; +use anyhow::Context; + +use crate::ProviderResult; + +// https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11#code +const __MULTICALL_BYTECODE: [u8; 3808] = const { + match const_hex::const_decode_to_array(b"0x6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c0033") { + Ok(a) => a, + Err(_) => panic!("Failed to decode __MULTICALL_BYTECODE hex"), + } +}; + +pub(crate) const MULTICALL_BYTECODE: Bytes = Bytes::from_static(&__MULTICALL_BYTECODE); + +// From https://github.com/mds1/multicall +sol! { + #[sol(rpc)] + interface Multicall { + struct Call3 { + address target; + bool allowFailure; + bytes callData; + } + + struct Result { + bool success; + bytes returnData; + } + + function aggregate3(Call3[] calldata calls) public payable returns (Result[] memory returnData); + } +} + +pub(crate) fn create_call(target: Address, call: impl SolInterface) -> Multicall::Call3 { + Multicall::Call3 { + target, + allowFailure: false, + callData: call.abi_encode().into(), + } +} + +pub(crate) fn decode_result(data: &[u8]) -> ProviderResult { + Ok(T::abi_decode_returns(data, false) + .context(format!("failed to decode {:?}", T::SIGNATURE))?) +} diff --git a/crates/provider/src/alloy/da/local/nitro.rs b/crates/provider/src/alloy/da/local/nitro.rs new file mode 100644 index 000000000..31e831975 --- /dev/null +++ b/crates/provider/src/alloy/da/local/nitro.rs @@ -0,0 +1,187 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::sync::Mutex as StdMutex; + +use alloy_primitives::{Address, Bytes, B256}; +use alloy_provider::Provider as AlloyProvider; +use alloy_transport::Transport; +use anyhow::Context; +use rundler_utils::cache::LruMap; +use tokio::sync::Mutex as TokioMutex; + +use crate::{ + alloy::da::{arbitrum::NodeInterface::NodeInterfaceInstance, DAGasOracle}, + BlockHashOrNumber, ProviderResult, +}; + +/// Cached Arbitrum Nitro DA gas oracle +/// +/// The goal of this oracle is to only need to make maximum +/// 1 network call per block + 1 network call per distinct UO +pub(crate) struct CachedNitroDAGasOracle { + node_interface: NodeInterfaceInstance, + // Use a tokio::Mutex here to ensure only one network call per block, other threads can async wait for the result + block_da_data_cache: TokioMutex>, + // Use a std::sync::Mutex here as both reads and writes to the LRU cache require interior mutability + uo_cache: StdMutex>, +} + +#[derive(Debug, Clone, Copy)] +struct BlockDAData { + l1_base_fee: u128, + base_fee: u128, +} + +impl CachedNitroDAGasOracle +where + AP: AlloyProvider, + T: Transport + Clone, +{ + pub(crate) fn new(oracle_address: Address, provider: AP) -> Self { + Self { + node_interface: NodeInterfaceInstance::new(oracle_address, provider), + block_da_data_cache: TokioMutex::new(LruMap::new(100)), + uo_cache: StdMutex::new(LruMap::new(10000)), + } + } +} + +const CACHE_UNITS_SCALAR: u128 = 1_000_000; + +#[async_trait::async_trait] +impl DAGasOracle for CachedNitroDAGasOracle +where + AP: AlloyProvider, + T: Transport + Clone, +{ + async fn estimate_da_gas( + &self, + hash: B256, + to: Address, + data: Bytes, + block: BlockHashOrNumber, + _gas_price: u128, + ) -> ProviderResult { + let mut cache = self.block_da_data_cache.lock().await; + match cache.get(&block) { + Some(block_da_data) => { + // Found the block, drop the block cache + let block_da_data = *block_da_data; + drop(cache); + + // Check if we have uo units cached + let maybe_uo_units = self.uo_cache.lock().unwrap().get(&hash).cloned(); + + if let Some(uo_units) = maybe_uo_units { + // Double cache hit, calculate the da fee from the cached uo units + Ok(calculate_da_fee(uo_units, &block_da_data)) + } else { + // UO cache miss, make remote call to get the da fee + let (_, uo_units, gas_estimate_for_l1) = + self.get_da_data(to, data, block).await?; + self.uo_cache.lock().unwrap().insert(hash, uo_units); + Ok(gas_estimate_for_l1) + } + } + None => { + // Block cache miss, make remote call to get the da fee + let (block_da_data, uo_units, gas_estimate_for_l1) = + self.get_da_data(to, data, block).await?; + cache.insert(block, block_da_data); + drop(cache); + + self.uo_cache.lock().unwrap().insert(hash, uo_units); + Ok(gas_estimate_for_l1) + } + } + } +} + +impl CachedNitroDAGasOracle +where + AP: AlloyProvider, + T: Transport + Clone, +{ + async fn get_da_data( + &self, + to: Address, + data: Bytes, + block: BlockHashOrNumber, + ) -> ProviderResult<(BlockDAData, u128, u128)> { + let ret = self + .node_interface + .gasEstimateL1Component(to, true, data) + .block(block.into()) + .call() + .await?; + + let l1_base_fee = ret + .l1BaseFeeEstimate + .try_into() + .context("Arbitrum NodeInterface returned l1BaseFeeEstimate too big for u128")?; + let base_fee = ret + .baseFee + .try_into() + .context("Arbitrum NodeInterface returned baseFee too big for u128")?; + + let block_da_data = BlockDAData { + l1_base_fee, + base_fee, + }; + + // Calculate UO units from the gas estimate for L1 + let uo_units = calculate_uo_units(ret.gasEstimateForL1 as u128, &block_da_data); + + Ok((block_da_data, uo_units, ret.gasEstimateForL1 as u128)) + } +} + +// DA Fee to gas units conversion. +// +// See https://github.com/OffchainLabs/nitro/blob/32c3f4b36d5eb0b4bbd37a82afe6c0c707ebe78d/execution/nodeInterface/NodeInterface.go#L515 +// and https://github.com/OffchainLabs/nitro/blob/32c3f4b36d5eb0b4bbd37a82afe6c0c707ebe78d/arbos/l1pricing/l1pricing.go#L582 +// +// Errors should round down + +// Calculate the da fee from the cahed scaled UO units +fn calculate_da_fee(uo_units: u128, block_da_data: &BlockDAData) -> u128 { + // Multiply by l1_base_fee + let a = uo_units.saturating_mul(block_da_data.l1_base_fee); + // Add 10% + let b = a.saturating_mul(11).saturating_div(10); + // Divide by base_fee + let c = if block_da_data.base_fee == 0 { + 0 // avoid division by zero, if this is the case then there is no way to pay for DA fee so we might as well return 0 + } else { + b.saturating_div(block_da_data.base_fee) + }; + // Scale down + c / CACHE_UNITS_SCALAR +} + +// Calculate scaled UO units from the da fee +fn calculate_uo_units(da_fee: u128, block_da_data: &BlockDAData) -> u128 { + // Undo base fee division, scale up to reduce rounding error + let a = da_fee + .saturating_mul(CACHE_UNITS_SCALAR) + .saturating_mul(block_da_data.base_fee); + // Undo 10% increase + let b = a.saturating_mul(10).saturating_div(11); + // Undo l1_base_fee division + if block_da_data.l1_base_fee == 0 { + 0 + } else { + b.saturating_div(block_da_data.l1_base_fee) + } +} diff --git a/crates/provider/src/alloy/da/mod.rs b/crates/provider/src/alloy/da/mod.rs index 115a45ff3..a6436cc37 100644 --- a/crates/provider/src/alloy/da/mod.rs +++ b/crates/provider/src/alloy/da/mod.rs @@ -11,7 +11,7 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use alloy_primitives::{Address, Bytes}; +use alloy_primitives::{Address, Bytes, B256}; use alloy_provider::Provider as AlloyProvider; use alloy_transport::Transport; use rundler_types::chain::{ChainSpec, DAGasOracleContractType}; @@ -22,6 +22,8 @@ mod arbitrum; use arbitrum::ArbitrumNitroDAGasOracle; mod optimism; use optimism::OptimismBedrockDAGasOracle; +mod local; +use local::{CachedNitroDAGasOracle, LocalBedrockDAGasOracle}; /// Trait for a DA gas oracle #[async_trait::async_trait] @@ -29,6 +31,7 @@ use optimism::OptimismBedrockDAGasOracle; pub(crate) trait DAGasOracle: Send + Sync { async fn estimate_da_gas( &self, + hash: B256, to: Address, data: Bytes, block: BlockHashOrNumber, @@ -42,6 +45,7 @@ struct ZeroDAGasOracle; impl DAGasOracle for ZeroDAGasOracle { async fn estimate_da_gas( &self, + _hash: B256, _to: Address, _data: Bytes, _block: BlockHashOrNumber, @@ -56,7 +60,7 @@ pub(crate) fn da_gas_oracle_for_chain<'a, AP, T>( provider: AP, ) -> Box where - AP: AlloyProvider + 'a, + AP: AlloyProvider + Clone + 'a, T: Transport + Clone, { match chain_spec.da_gas_oracle_contract_type { @@ -68,6 +72,215 @@ where chain_spec.da_gas_oracle_contract_address, provider, )), + DAGasOracleContractType::LocalBedrock => Box::new(LocalBedrockDAGasOracle::new( + chain_spec.da_gas_oracle_contract_address, + provider, + )), + DAGasOracleContractType::CachedNitro => Box::new(CachedNitroDAGasOracle::new( + chain_spec.da_gas_oracle_contract_address, + provider, + )), DAGasOracleContractType::None => Box::new(ZeroDAGasOracle), } } + +#[cfg(test)] +mod tests { + use alloy_primitives::{address, b256, bytes, uint, U256}; + use alloy_provider::ProviderBuilder; + use alloy_sol_types::SolValue; + use rundler_contracts::v0_7::PackedUserOperation; + + use super::*; + + // Run these tests locally with `ALCHEMY_API_KEY= cargo test -- --ignored` + + // This test may begin to fail if an optimism sepolia fork changes how the L1 gas oracle works. + // If that happens, we should update the local bedrock oracle to match the new fork logic in + // a backwards compatible way based on the fork booleans in the contract. + #[tokio::test] + #[ignore] + async fn compare_opt_latest() { + let provider = opt_provider(); + let block = provider.get_block_number().await.unwrap(); + println!("block: {}", block); + + compare_opt_and_local_bedrock(provider, block.into()).await; + } + + // This test should never fail for a block on the Fjord fork of optimism sepolia. + #[tokio::test] + #[ignore] + async fn compare_opt_fixed() { + let provider = opt_provider(); + // let block: BlockHashOrNumber = 18343127.into(); + let block: BlockHashOrNumber = 18434733.into(); + + compare_opt_and_local_bedrock(provider, block).await; + } + + #[tokio::test] + #[ignore] + async fn compare_arb_latest() { + let provider = arb_provider(); + let block = provider.get_block_number().await.unwrap(); + let (uo, hash) = test_uo_data_1(); + + compare_arb_and_cached_on_data(provider, block.into(), hash, uo).await; + } + + #[tokio::test] + #[ignore] + async fn compare_arb_fixed() { + let provider = arb_provider(); + let block: BlockHashOrNumber = 262113260.into(); + let (uo, hash) = test_uo_data_1(); + + compare_arb_and_cached_on_data(provider, block, hash, uo).await; + } + + #[tokio::test] + #[ignore] + async fn compare_arb_multiple() { + let (uo1, hash1) = test_uo_data_1(); + let (uo2, hash2) = test_uo_data_2(); + let provider = arb_provider(); + + let oracle_addr = address!("00000000000000000000000000000000000000C8"); + let contract_oracle = ArbitrumNitroDAGasOracle::new(oracle_addr, provider.clone()); + let cached_oracle = CachedNitroDAGasOracle::new(oracle_addr, provider); + + let block: BlockHashOrNumber = 262113260.into(); + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, hash1, uo1.clone()).await; + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, hash2, uo2.clone()).await; + + let block: BlockHashOrNumber = 262113261.into(); + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, hash1, uo1.clone()).await; + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, hash2, uo2.clone()).await; + + let block: BlockHashOrNumber = 262113262.into(); + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, hash1, uo1).await; + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, hash2, uo2).await; + } + + async fn compare_oracles( + oracle_a: &impl DAGasOracle, + oracle_b: &impl DAGasOracle, + block: BlockHashOrNumber, + ) { + let (uo, hash) = test_uo_data_1(); + compare_oracles_on_data(oracle_a, oracle_b, block, hash, uo).await; + } + + async fn compare_oracles_on_data( + oracle_a: &impl DAGasOracle, + oracle_b: &impl DAGasOracle, + block: BlockHashOrNumber, + hash: B256, + data: Bytes, + ) { + let gas_price = 1; + let to = Address::random(); + + let gas_a = oracle_a + .estimate_da_gas(hash, to, data.clone(), block, gas_price) + .await + .unwrap(); + let gas_b = oracle_b + .estimate_da_gas(hash, to, data, block, gas_price) + .await + .unwrap(); + + // Allow for some variance with oracle b being within 0.1% smaller than oracle a + println!("gas_a: {}", gas_a); + println!("gas_b: {}", gas_b); + let ratio = gas_b as f64 / gas_a as f64; + assert!((0.999..=1.000).contains(&ratio)); + } + + async fn compare_opt_and_local_bedrock( + provider: impl AlloyProvider + Clone, + block: BlockHashOrNumber, + ) { + let oracle_addr = address!("420000000000000000000000000000000000000F"); + + let contract_oracle = OptimismBedrockDAGasOracle::new(oracle_addr, provider.clone()); + let local_oracle = LocalBedrockDAGasOracle::new(oracle_addr, provider); + + compare_oracles(&contract_oracle, &local_oracle, block).await; + } + + async fn compare_arb_and_cached_on_data( + provider: impl AlloyProvider + Clone, + block: BlockHashOrNumber, + hash: B256, + data: Bytes, + ) { + let oracle_addr = address!("00000000000000000000000000000000000000C8"); + + let contract_oracle = ArbitrumNitroDAGasOracle::new(oracle_addr, provider.clone()); + let cached_oracle = CachedNitroDAGasOracle::new(oracle_addr, provider); + + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, hash, data).await; + } + + fn opt_provider() -> impl AlloyProvider + Clone { + ProviderBuilder::new() + .on_http( + format!("https://opt-sepolia.g.alchemy.com/v2/{}", get_api_key()) + .parse() + .unwrap(), + ) + .boxed() + } + + fn arb_provider() -> impl AlloyProvider + Clone { + ProviderBuilder::new() + .on_http( + format!("https://arb-mainnet.g.alchemy.com/v2/{}", get_api_key()) + .parse() + .unwrap(), + ) + .boxed() + } + + fn test_uo_data_1() -> (Bytes, B256) { + let puo = PackedUserOperation { + sender: address!("f497A8026717FbbA3944c3dd2533c0716b7685e2"), + nonce: uint!(0x23_U256), + initCode: Bytes::default(), + callData: bytes!("b61d27f6000000000000000000000000f497a8026717fbba3944c3dd2533c0716b7685e2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d087d28800000000000000000000000000000000000000000000000000000000"), + accountGasLimits: b256!("000000000000000000000000000114fc0000000000000000000000000012c9b5"), + preVerificationGas: U256::from(48916), + gasFees: b256!("000000000000000000000000524121000000000000000000000000109a4a441a"), + paymasterAndData: Bytes::default(), + signature: bytes!("0b83faeeac250d4c4a2459c1d6e1f8427f96af246d7fb3027b10bb05d934912f23a9491c16ab97ab32ac88179f279e871387c23547aa2e27b83fc358058e71fa1c"), + }; + ( + puo.abi_encode().into(), + b256!("0000000000000000000000000000000000000000000000000000000000000001"), + ) + } + + fn test_uo_data_2() -> (Bytes, B256) { + let puo = PackedUserOperation { + sender: address!("f497A8026717FbbA3944c3dd2533c0716b7685e2"), + nonce: uint!(0x24_U256), // changed this + initCode: Bytes::default(), + callData: bytes!("b61d27f6000000000000000000000000f497a8026717fbba3944c3dd2533c0716b7685e2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d087d28800000000000000000000000000000000000000000000000000000000"), + accountGasLimits: b256!("000000000000000000000000000114fc0000000000000000000000000012c9b5"), + preVerificationGas: U256::from(48916), + gasFees: b256!("000000000000000000000000524121000000000000000000000000109a4a441a"), + paymasterAndData: Bytes::default(), + signature: bytes!("00"), // removed this, should result in different costs + }; + ( + puo.abi_encode().into(), + b256!("0000000000000000000000000000000000000000000000000000000000000002"), + ) + } + + fn get_api_key() -> String { + std::env::var("ALCHEMY_API_KEY").expect("ALCHEMY_API_KEY must be set") + } +} diff --git a/crates/provider/src/alloy/da/optimism.rs b/crates/provider/src/alloy/da/optimism.rs index ed8758ee9..1be6ac53f 100644 --- a/crates/provider/src/alloy/da/optimism.rs +++ b/crates/provider/src/alloy/da/optimism.rs @@ -11,7 +11,7 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use alloy_primitives::{Address, Bytes}; +use alloy_primitives::{Address, Bytes, B256}; use alloy_provider::Provider as AlloyProvider; use alloy_sol_types::sol; use alloy_transport::Transport; @@ -25,6 +25,13 @@ use crate::{BlockHashOrNumber, ProviderResult}; sol! { #[sol(rpc)] interface GasPriceOracle { + bool public isFjord; + + function baseFeeScalar() public view returns (uint32); + function l1BaseFee() public view returns (uint256); + function blobBaseFeeScalar() public view returns (uint32); + function blobBaseFee() public view returns (uint256); + function getL1Fee(bytes memory _data) external view returns (uint256); } } @@ -52,6 +59,7 @@ where { async fn estimate_da_gas( &self, + _hash: B256, _to: Address, data: Bytes, block: BlockHashOrNumber, diff --git a/crates/provider/src/alloy/entry_point/v0_6.rs b/crates/provider/src/alloy/entry_point/v0_6.rs index 3317128ff..0e32e0432 100644 --- a/crates/provider/src/alloy/entry_point/v0_6.rs +++ b/crates/provider/src/alloy/entry_point/v0_6.rs @@ -318,6 +318,7 @@ where block: BlockHashOrNumber, gas_price: u128, ) -> ProviderResult { + let hash = user_op.hash(*self.i_entry_point.address(), self.chain_spec.id); let data = self .i_entry_point .handleOps(vec![user_op.into()], Address::random()) @@ -330,7 +331,13 @@ where super::max_bundle_transaction_data(*self.i_entry_point.address(), data, gas_price); self.da_gas_oracle - .estimate_da_gas(*self.i_entry_point.address(), bundle_data, block, gas_price) + .estimate_da_gas( + hash, + *self.i_entry_point.address(), + bundle_data, + block, + gas_price, + ) .await } } diff --git a/crates/provider/src/alloy/entry_point/v0_7.rs b/crates/provider/src/alloy/entry_point/v0_7.rs index df48c2b10..349461201 100644 --- a/crates/provider/src/alloy/entry_point/v0_7.rs +++ b/crates/provider/src/alloy/entry_point/v0_7.rs @@ -307,6 +307,7 @@ where block: BlockHashOrNumber, gas_price: u128, ) -> ProviderResult { + let hash = user_op.hash(*self.i_entry_point.address(), self.chain_spec.id); let data = self .i_entry_point .handleOps(vec![user_op.pack()], Address::random()) @@ -319,7 +320,13 @@ where super::max_bundle_transaction_data(*self.i_entry_point.address(), data, gas_price); self.da_gas_oracle - .estimate_da_gas(*self.i_entry_point.address(), bundle_data, block, gas_price) + .estimate_da_gas( + hash, + *self.i_entry_point.address(), + bundle_data, + block, + gas_price, + ) .await } } diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 5a7e979fe..622f041a6 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -18,7 +18,7 @@ anyhow.workspace = true async-trait.workspace = true chrono = "0.4.38" constcat = "0.5.0" -const-hex = "1.12.0" +const-hex.workspace = true futures-util.workspace = true num_enum = "0.7.3" parse-display.workspace = true diff --git a/crates/types/src/chain.rs b/crates/types/src/chain.rs index 885296182..3671cd5a7 100644 --- a/crates/types/src/chain.rs +++ b/crates/types/src/chain.rs @@ -129,6 +129,10 @@ pub enum DAGasOracleContractType { ArbitrumNitro, /// Optimism Bedrock type gas oracle contract OptimismBedrock, + /// Local Bedrock type gas oracle contract + LocalBedrock, + /// Cached Nitro type gas oracle contract + CachedNitro, } /// Type of oracle for estimating priority fees