diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7a3322128..6350b1323 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 @@ -90,7 +95,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 7234662b9..07a3fa2c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,17 @@ [workspace] -members = ["bin/*", "crates/*"] +members = [ + "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" @@ -18,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" @@ -48,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 c8ef5ccd1..2e73669d6 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 futures-util.workspace = true +reqwest.workspace = true thiserror.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..8a43ef1e8 100644 --- a/crates/provider/src/alloy/da/arbitrum.rs +++ b/crates/provider/src/alloy/da/arbitrum.rs @@ -15,6 +15,7 @@ use alloy_primitives::{Address, Bytes}; use alloy_provider::Provider as AlloyProvider; use alloy_sol_types::sol; use alloy_transport::Transport; +use rundler_types::da::{DAGasBlockData, DAGasUOData}; use NodeInterface::NodeInterfaceInstance; use super::DAGasOracle; @@ -63,17 +64,43 @@ where { async fn estimate_da_gas( &self, - to_address: Address, - data: Bytes, + uo_data: Bytes, + to: Address, block: BlockHashOrNumber, _gas_price: u128, - ) -> ProviderResult { + ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { let ret = self .node_interface - .gasEstimateL1Component(to_address, true, data) + .gasEstimateL1Component(to, true, uo_data) .block(block.into()) .call() .await?; - Ok(ret.gasEstimateForL1 as u128) + Ok(( + ret.gasEstimateForL1 as u128, + DAGasUOData::Empty, + DAGasBlockData::Empty, + )) + } + + async fn block_data(&self, _block: BlockHashOrNumber) -> ProviderResult { + Ok(DAGasBlockData::Empty) + } + + async fn uo_data( + &self, + _uo_data: Bytes, + _to: Address, + _block: BlockHashOrNumber, + ) -> ProviderResult { + Ok(DAGasUOData::Empty) + } + + fn calc_da_gas_sync( + &self, + _uo_data: &DAGasUOData, + _block_data: &DAGasBlockData, + _gas_price: u128, + ) -> u128 { + panic!("ArbitrumNitroDAGasOracle does not support calc_da_gas_sync") } } 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..9e9f92b44 --- /dev/null +++ b/crates/provider/src/alloy/da/local/bedrock.rs @@ -0,0 +1,245 @@ +// 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_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_types::da::{BedrockDAGasBlockData, BedrockDAGasUOData, DAGasBlockData, DAGasUOData}; +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_data_cache: TokioMutex>, + blocking_task_pool: BlockingTaskPool, +} + +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_data_cache: TokioMutex::new(LruMap::new(100)), + 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, + data: Bytes, + to: Address, + block: BlockHashOrNumber, + gas_price: u128, + ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { + let block_data = self.block_data(block).await?; + let uo_data = self.uo_data(data, to, block).await?; + let da_gas = self.calc_da_gas_sync(&uo_data, &block_data, gas_price); + Ok((da_gas, uo_data, block_data)) + } + + async fn block_data(&self, block: BlockHashOrNumber) -> ProviderResult { + let mut cache = self.block_data_cache.lock().await; + match cache.get(&block) { + Some(block_data) => Ok(DAGasBlockData::Bedrock(block_data.clone())), + None => { + let block_data = self.get_block_data(block).await?; + cache.insert(block, block_data.clone()); + Ok(DAGasBlockData::Bedrock(block_data)) + } + } + } + + async fn uo_data( + &self, + uo_data: Bytes, + _to: Address, + _block: BlockHashOrNumber, + ) -> ProviderResult { + let uo_data = self.get_uo_data(uo_data).await?; + Ok(DAGasUOData::Bedrock(uo_data)) + } + + fn calc_da_gas_sync( + &self, + uo_data: &DAGasUOData, + block_data: &DAGasBlockData, + gas_price: u128, + ) -> u128 { + let block_da_data = match block_data { + DAGasBlockData::Bedrock(block_da_data) => block_da_data, + _ => panic!("LocalBedrockDAGasOracle only supports Bedrock block data"), + }; + let uo_data = match uo_data { + DAGasUOData::Bedrock(uo_data) => uo_data, + _ => panic!("LocalBedrockDAGasOracle only supports Bedrock user operation data"), + }; + + 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; + let l1_fee = (uo_data.uo_units as u128 * fee_scaled) / DECIMAL_SCALAR; + 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_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(BedrockDAGasBlockData { + base_fee_scalar, + l1_base_fee, + blob_base_fee_scalar, + blob_base_fee, + }) + } + + async fn get_uo_data(&self, data: Bytes) -> ProviderResult { + // 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]; + rundler_bindings_fastlz::compress(&data, &mut buf).len() as u64 + }) + .await + .map_err(|e| anyhow::anyhow!("failed to compress data: {:?}", e))?; + + let compressed_with_buffer = compressed_len + 68; + + let estimated_size = COST_INTERCEPT + COST_FASTLZ_COEF * compressed_with_buffer as i128; + let uo_units = estimated_size.clamp(MIN_TRANSACTION_SIZE, u64::MAX as i128); + + Ok(BedrockDAGasUOData { + uo_units: uo_units 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..678f58f76 --- /dev/null +++ b/crates/provider/src/alloy/da/local/nitro.rs @@ -0,0 +1,236 @@ +// 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_provider::Provider as AlloyProvider; +use alloy_transport::Transport; +use anyhow::Context; +use rundler_types::da::{DAGasBlockData, DAGasUOData, NitroDAGasBlockData, NitroDAGasUOData}; +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_data_cache: TokioMutex>, +} + +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_data_cache: TokioMutex::new(LruMap::new(100)), + } + } +} + +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, + data: Bytes, + to: Address, + block: BlockHashOrNumber, + _gas_price: u128, + ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { + let mut cache = self.block_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.clone(); + drop(cache); + + let uo_data = self.get_uo_data(to, data, block).await?; + let uo_data = DAGasUOData::Nitro(uo_data); + let block_data = DAGasBlockData::Nitro(block_da_data); + let l1_gas_estimate = self.calc_da_gas_sync(&uo_data, &block_data, _gas_price); + + Ok((l1_gas_estimate, uo_data, block_data)) + } + None => { + // Block cache miss, make remote call to get the da fee + let (gas_estimate_for_l1, block_da_data) = + self.call_oracle_contract(to, data, block).await?; + cache.insert(block, block_da_data.clone()); + drop(cache); + + let uo_units = calculate_uo_units(gas_estimate_for_l1, &block_da_data); + + Ok(( + gas_estimate_for_l1, + DAGasUOData::Nitro(NitroDAGasUOData { uo_units }), + DAGasBlockData::Nitro(block_da_data), + )) + } + } + } + + async fn block_data(&self, block: BlockHashOrNumber) -> ProviderResult { + let mut cache = self.block_data_cache.lock().await; + match cache.get(&block) { + Some(block_data) => Ok(DAGasBlockData::Nitro(block_data.clone())), + None => { + let block_data = self.get_block_data(block).await?; + cache.insert(block, block_data.clone()); + Ok(DAGasBlockData::Nitro(block_data)) + } + } + } + + async fn uo_data( + &self, + uo_data: Bytes, + to: Address, + block: BlockHashOrNumber, + ) -> ProviderResult { + let uo_data = self.get_uo_data(to, uo_data, block).await?; + Ok(DAGasUOData::Nitro(uo_data)) + } + + fn calc_da_gas_sync( + &self, + uo_data: &DAGasUOData, + block_data: &DAGasBlockData, + _gas_price: u128, + ) -> u128 { + let uo_units = match uo_data { + DAGasUOData::Nitro(uo_data) => uo_data.uo_units, + _ => panic!("NitroDAGasOracle only supports Nitro DAGasUOData"), + }; + let block_data = match block_data { + DAGasBlockData::Nitro(block_data) => block_data, + _ => panic!("NitroDAGasOracle only supports Nitro DAGasBlockData"), + }; + + calculate_da_fee(uo_units, block_data) + } +} + +impl CachedNitroDAGasOracle +where + AP: AlloyProvider, + T: Transport + Clone, +{ + async fn get_block_data( + &self, + block: BlockHashOrNumber, + ) -> ProviderResult { + // phantom data, not important for the calculation + let data = Bytes::new(); + let to = Address::ZERO; + + let (_, block_data) = self.call_oracle_contract(to, data, block).await?; + + Ok(block_data) + } + + async fn get_uo_data( + &self, + to: Address, + data: Bytes, + block: BlockHashOrNumber, + ) -> ProviderResult { + let (l1_gas_estimate, block_da_data) = self.call_oracle_contract(to, data, block).await?; + let uo_units = calculate_uo_units(l1_gas_estimate, &block_da_data); + Ok(NitroDAGasUOData { uo_units }) + } + + async fn call_oracle_contract( + &self, + to: Address, + data: Bytes, + block: BlockHashOrNumber, + ) -> ProviderResult<(u128, NitroDAGasBlockData)> { + 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")?; + + Ok(( + ret.gasEstimateForL1 as u128, + NitroDAGasBlockData { + l1_base_fee, + base_fee, + }, + )) + } +} + +// 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 cached scaled UO units +fn calculate_da_fee(uo_units: u128, block_da_data: &NitroDAGasBlockData) -> 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: &NitroDAGasBlockData) -> 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..b9d86762a 100644 --- a/crates/provider/src/alloy/da/mod.rs +++ b/crates/provider/src/alloy/da/mod.rs @@ -14,27 +14,19 @@ use alloy_primitives::{Address, Bytes}; use alloy_provider::Provider as AlloyProvider; use alloy_transport::Transport; -use rundler_types::chain::{ChainSpec, DAGasOracleContractType}; +use rundler_types::{ + chain::ChainSpec, + da::{DAGasBlockData, DAGasOracleContractType, DAGasUOData}, +}; -use crate::{BlockHashOrNumber, ProviderResult}; +use crate::{BlockHashOrNumber, DAGasOracle, ProviderResult}; mod arbitrum; use arbitrum::ArbitrumNitroDAGasOracle; mod optimism; use optimism::OptimismBedrockDAGasOracle; - -/// Trait for a DA gas oracle -#[async_trait::async_trait] -#[auto_impl::auto_impl(&, &mut, Rc, Arc, Box)] -pub(crate) trait DAGasOracle: Send + Sync { - async fn estimate_da_gas( - &self, - to: Address, - data: Bytes, - block: BlockHashOrNumber, - gas_price: u128, - ) -> ProviderResult; -} +mod local; +use local::{CachedNitroDAGasOracle, LocalBedrockDAGasOracle}; struct ZeroDAGasOracle; @@ -42,12 +34,34 @@ struct ZeroDAGasOracle; impl DAGasOracle for ZeroDAGasOracle { async fn estimate_da_gas( &self, - _to: Address, _data: Bytes, + _to: Address, + _block: BlockHashOrNumber, + _gas_price: u128, + ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { + Ok((0, DAGasUOData::Empty, DAGasBlockData::Empty)) + } + + async fn block_data(&self, _block: BlockHashOrNumber) -> ProviderResult { + Ok(DAGasBlockData::Empty) + } + + async fn uo_data( + &self, + _uo_data: Bytes, + _to: Address, _block: BlockHashOrNumber, + ) -> ProviderResult { + Ok(DAGasUOData::Empty) + } + + fn calc_da_gas_sync( + &self, + _uo_data: &DAGasUOData, + _block_data: &DAGasBlockData, _gas_price: u128, - ) -> ProviderResult { - Ok(0) + ) -> u128 { + panic!("ZeroDAGasOracle does not support calc_da_gas_sync") } } @@ -56,7 +70,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 +82,204 @@ 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(); + + 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 = test_uo_data_1(); + + compare_arb_and_cached_on_data(provider, block.into(), uo).await; + } + + #[tokio::test] + #[ignore] + async fn compare_arb_fixed() { + let provider = arb_provider(); + let block: BlockHashOrNumber = 262113260.into(); + let uo = test_uo_data_1(); + + compare_arb_and_cached_on_data(provider, block, uo).await; + } + + #[tokio::test] + #[ignore] + async fn compare_arb_multiple() { + let uo1 = test_uo_data_1(); + let uo2 = 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, uo1.clone()).await; + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, uo2.clone()).await; + + let block: BlockHashOrNumber = 262113261.into(); + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, uo1.clone()).await; + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, uo2.clone()).await; + + let block: BlockHashOrNumber = 262113262.into(); + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, uo1).await; + compare_oracles_on_data(&contract_oracle, &cached_oracle, block, uo2).await; + } + + async fn compare_oracles( + oracle_a: &impl DAGasOracle, + oracle_b: &impl DAGasOracle, + block: BlockHashOrNumber, + ) { + let uo = test_uo_data_1(); + compare_oracles_on_data(oracle_a, oracle_b, block, uo).await; + } + + async fn compare_oracles_on_data( + oracle_a: &impl DAGasOracle, + oracle_b: &impl DAGasOracle, + block: BlockHashOrNumber, + data: Bytes, + ) { + let gas_price = 1; + let to = Address::random(); + + let (gas_a, _, _) = oracle_a + .estimate_da_gas(data.clone(), to, block, gas_price) + .await + .unwrap(); + let (gas_b, _, _) = oracle_b + .estimate_da_gas(data, to, block, gas_price) + .await + .unwrap(); + + // Allow for some variance with oracle b being within 0.1% smaller than oracle a + 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, + 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, 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 { + 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() + } + + fn test_uo_data_2() -> Bytes { + 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() + } + + 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..adce5de1d 100644 --- a/crates/provider/src/alloy/da/optimism.rs +++ b/crates/provider/src/alloy/da/optimism.rs @@ -16,6 +16,7 @@ use alloy_provider::Provider as AlloyProvider; use alloy_sol_types::sol; use alloy_transport::Transport; use anyhow::Context; +use rundler_types::da::{DAGasBlockData, DAGasUOData}; use GasPriceOracle::GasPriceOracleInstance; use super::DAGasOracle; @@ -25,6 +26,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,11 +60,11 @@ where { async fn estimate_da_gas( &self, - _to: Address, data: Bytes, + _to: Address, block: BlockHashOrNumber, gas_price: u128, - ) -> ProviderResult { + ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { let l1_fee: u128 = self .oracle .getL1Fee(data) @@ -67,6 +75,32 @@ where .try_into() .context("failed to convert DA fee to u128")?; - Ok(l1_fee.checked_div(gas_price).unwrap_or(u128::MAX)) + Ok(( + l1_fee.checked_div(gas_price).unwrap_or(u128::MAX), + DAGasUOData::Empty, + DAGasBlockData::Empty, + )) + } + + async fn block_data(&self, _block: BlockHashOrNumber) -> ProviderResult { + Ok(DAGasBlockData::Empty) + } + + async fn uo_data( + &self, + _uo_data: Bytes, + _to: Address, + _block: BlockHashOrNumber, + ) -> ProviderResult { + Ok(DAGasUOData::Empty) + } + + fn calc_da_gas_sync( + &self, + _uo_data: &DAGasUOData, + _block_data: &DAGasBlockData, + _gas_price: u128, + ) -> u128 { + panic!("OptimismBedrockDAGasOracle does not support calc_da_gas_sync") } } diff --git a/crates/provider/src/alloy/entry_point/v0_6.rs b/crates/provider/src/alloy/entry_point/v0_6.rs index 3317128ff..ccf95b6b6 100644 --- a/crates/provider/src/alloy/entry_point/v0_6.rs +++ b/crates/provider/src/alloy/entry_point/v0_6.rs @@ -31,15 +31,16 @@ use rundler_contracts::v0_6::{ UserOperation as ContractUserOperation, UserOpsPerAggregator as UserOpsPerAggregatorV0_6, }; use rundler_types::{ - chain::ChainSpec, v0_6::UserOperation, GasFees, UserOperation as _, UserOpsPerAggregator, - ValidationOutput, ValidationRevert, + chain::ChainSpec, + da::{DAGasBlockData, DAGasUOData}, + v0_6::UserOperation, + GasFees, UserOperation as _, UserOpsPerAggregator, ValidationOutput, ValidationRevert, }; use crate::{ - alloy::da::{self, DAGasOracle}, - AggregatorOut, AggregatorSimOut, BlockHashOrNumber, BundleHandler, DAGasProvider, DepositInfo, - EntryPoint, EntryPointProvider as EntryPointProviderTrait, EvmCall, ExecutionResult, - HandleOpsOut, ProviderResult, SignatureAggregator, SimulationProvider, + alloy::da, AggregatorOut, AggregatorSimOut, BlockHashOrNumber, BundleHandler, DAGasOracle, + DAGasProvider, DepositInfo, EntryPoint, EntryPointProvider as EntryPointProviderTrait, EvmCall, + ExecutionResult, HandleOpsOut, ProviderResult, SignatureAggregator, SimulationProvider, }; /// Entry point provider for v0.6 @@ -317,7 +318,7 @@ where user_op: UserOperation, block: BlockHashOrNumber, gas_price: u128, - ) -> ProviderResult { + ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { let data = self .i_entry_point .handleOps(vec![user_op.into()], Address::random()) @@ -330,9 +331,45 @@ 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(bundle_data, *self.i_entry_point.address(), block, gas_price) .await } + + async fn block_data(&self, block: BlockHashOrNumber) -> ProviderResult { + self.da_gas_oracle.block_data(block).await + } + + async fn uo_data( + &self, + uo: UserOperation, + block: BlockHashOrNumber, + ) -> ProviderResult { + let gas_price = uo.max_fee_per_gas; + let data = self + .i_entry_point + .handleOps(vec![uo.into()], Address::random()) + .into_transaction_request() + .input + .into_input() + .unwrap(); + + let bundle_data = + super::max_bundle_transaction_data(*self.i_entry_point.address(), data, gas_price); + + self.da_gas_oracle + .uo_data(bundle_data, *self.i_entry_point.address(), block) + .await + } + + fn calc_da_gas_sync( + &self, + uo_data: &DAGasUOData, + block_data: &DAGasBlockData, + gas_price: u128, + ) -> u128 { + self.da_gas_oracle + .calc_da_gas_sync(uo_data, block_data, gas_price) + } } #[async_trait::async_trait] diff --git a/crates/provider/src/alloy/entry_point/v0_7.rs b/crates/provider/src/alloy/entry_point/v0_7.rs index df48c2b10..6bd450d15 100644 --- a/crates/provider/src/alloy/entry_point/v0_7.rs +++ b/crates/provider/src/alloy/entry_point/v0_7.rs @@ -38,15 +38,17 @@ use rundler_contracts::v0_7::{ ENTRY_POINT_SIMULATIONS_V0_7_DEPLOYED_BYTECODE, }; use rundler_types::{ - chain::ChainSpec, v0_7::UserOperation, GasFees, UserOperation as _, UserOpsPerAggregator, - ValidationOutput, ValidationRevert, + chain::ChainSpec, + da::{DAGasBlockData, DAGasUOData}, + v0_7::UserOperation, + GasFees, UserOperation as _, UserOpsPerAggregator, ValidationOutput, ValidationRevert, }; use crate::{ - alloy::da::{self, DAGasOracle}, - AggregatorOut, AggregatorSimOut, BlockHashOrNumber, BundleHandler, DAGasProvider, DepositInfo, - EntryPoint, EntryPointProvider as EntryPointProviderTrait, EvmCall, ExecutionResult, - HandleOpsOut, ProviderResult, SignatureAggregator, SimulationProvider, + alloy::da::{self}, + AggregatorOut, AggregatorSimOut, BlockHashOrNumber, BundleHandler, DAGasOracle, DAGasProvider, + DepositInfo, EntryPoint, EntryPointProvider as EntryPointProviderTrait, EvmCall, + ExecutionResult, HandleOpsOut, ProviderResult, SignatureAggregator, SimulationProvider, }; /// Entry point provider for v0.7 @@ -306,7 +308,7 @@ where user_op: UserOperation, block: BlockHashOrNumber, gas_price: u128, - ) -> ProviderResult { + ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { let data = self .i_entry_point .handleOps(vec![user_op.pack()], Address::random()) @@ -319,9 +321,45 @@ 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(bundle_data, *self.i_entry_point.address(), block, gas_price) .await } + + async fn block_data(&self, block: BlockHashOrNumber) -> ProviderResult { + self.da_gas_oracle.block_data(block).await + } + + async fn uo_data( + &self, + uo: UserOperation, + block: BlockHashOrNumber, + ) -> ProviderResult { + let gas_price = uo.max_fee_per_gas; + let data = self + .i_entry_point + .handleOps(vec![uo.pack()], Address::random()) + .into_transaction_request() + .input + .into_input() + .unwrap(); + + let bundle_data = + super::max_bundle_transaction_data(*self.i_entry_point.address(), data, gas_price); + + self.da_gas_oracle + .uo_data(bundle_data, *self.i_entry_point.address(), block) + .await + } + + fn calc_da_gas_sync( + &self, + uo_data: &DAGasUOData, + block_data: &DAGasBlockData, + gas_price: u128, + ) -> u128 { + self.da_gas_oracle + .calc_da_gas_sync(uo_data, block_data, gas_price) + } } #[async_trait::async_trait] diff --git a/crates/provider/src/traits/da.rs b/crates/provider/src/traits/da.rs new file mode 100644 index 000000000..75de2a7fc --- /dev/null +++ b/crates/provider/src/traits/da.rs @@ -0,0 +1,66 @@ +// 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 rundler_types::da::{DAGasBlockData, DAGasUOData}; + +use crate::{BlockHashOrNumber, ProviderResult}; + +// Trait for a DA gas oracle +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, &mut, Rc, Arc, Box)] +pub(crate) trait DAGasOracle: Send + Sync { + /// Estimate the DA gas for a given user operation's bytes + /// + /// Returns the estimated gas, as well as both the UO DA data and the block DA data. + /// These fields can be safely ignored if the caller is only interested in the gas estimate and + /// is not implementing any caching logic. + async fn estimate_da_gas( + &self, + uo_bytes: Bytes, + to: Address, + block: BlockHashOrNumber, + gas_price: u128, + ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)>; + + /// Retrieve the DA gas data for a given block. This value can change block to block and + /// thus must be retrieved fresh from the DA for every block. + async fn block_data(&self, block: BlockHashOrNumber) -> ProviderResult; + + /// Retrieve the DA gas data for a given user operation's bytes + /// + /// This should not change block to block, but may change after underlying hard forks, + /// thus a block number is required. + /// + /// It is safe to calculate this once and re-use if the same UO is used for multiple calls within + /// a small time period (no hard forks impacting DA calculations) + async fn uo_data( + &self, + uo_data: Bytes, + to: Address, + block: BlockHashOrNumber, + ) -> ProviderResult; + + /// Synchronously calculate the DA gas for a given user operation data and block data. + /// These values must have been previously retrieved from a DA oracle of the same implementation + /// else this function will PANIC. + /// + /// This function is intended to allow synchronous DA gas calculation from a cached UO data and a + /// recently retrieved block data. + fn calc_da_gas_sync( + &self, + uo_data: &DAGasUOData, + block_data: &DAGasBlockData, + gas_price: u128, + ) -> u128; +} diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index 0538359c0..5c683739e 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -13,6 +13,7 @@ use alloy_primitives::{Address, Bytes, U256}; use rundler_types::{ + da::{DAGasBlockData, DAGasUOData}, GasFees, Timestamp, UserOperation, UserOpsPerAggregator, ValidationOutput, ValidationRevert, }; @@ -164,13 +165,43 @@ pub trait DAGasProvider: Send + Sync { /// Calculate the DA portion of the gas for a user operation /// - /// Returns zero for operations that do not require DA gas + /// Returns the da gas plus any calculated DAGasUOData and DAGasBlockData. + /// These values can be cached and re-used if the same UO is used for multiple calls within + /// a small time period (no hard forks impacting DA calculations). + /// + /// Returns zero for operations that do not require DA gas. async fn calc_da_gas( &self, - op: Self::UO, + uo: Self::UO, block: BlockHashOrNumber, gas_price: u128, - ) -> ProviderResult; + ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)>; + + /// Retrieve the DA gas data for a given block. This value can change block to block and + /// thus must be retrieved fresh from the DA for every block. + async fn block_data(&self, block: BlockHashOrNumber) -> ProviderResult; + + /// Retrieve the DA gas data for a given user operation's bytes + /// + /// This should not change block to block, but may change after underlying hard forks, + /// thus a block number is required. + /// + /// It is safe to calculate this once and re-use if the same UO is used for multiple calls within + /// a small time period (no hard forks impacting DA calculations) + async fn uo_data(&self, uo: Self::UO, block: BlockHashOrNumber) -> ProviderResult; + + /// Synchronously calculate the DA gas for a given user operation data and block data. + /// These values must have been previously retrieved from a DA provider of the same implementation + /// else this function will PANIC. + /// + /// This function is intended to allow synchronous DA gas calculation from a cached UO data and a + /// recently retrieved block data. + fn calc_da_gas_sync( + &self, + uo_data: &DAGasUOData, + block_data: &DAGasBlockData, + gas_price: u128, + ) -> u128; } /// Trait for simulating user operations on an entry point contract diff --git a/crates/provider/src/traits/mod.rs b/crates/provider/src/traits/mod.rs index ff12b2acd..2f38d1876 100644 --- a/crates/provider/src/traits/mod.rs +++ b/crates/provider/src/traits/mod.rs @@ -13,6 +13,9 @@ //! Traits for the provider module. +mod da; +pub(crate) use da::DAGasOracle; + mod error; pub use error::*; diff --git a/crates/provider/src/traits/test_utils.rs b/crates/provider/src/traits/test_utils.rs index 17dc55774..442953cf5 100644 --- a/crates/provider/src/traits/test_utils.rs +++ b/crates/provider/src/traits/test_utils.rs @@ -22,6 +22,7 @@ use alloy_rpc_types_trace::geth::{ }; use rundler_contracts::utils::GetGasUsed::GasUsedResult; use rundler_types::{ + da::{DAGasBlockData, DAGasUOData}, v0_6, v0_7, GasFees, UserOpsPerAggregator, ValidationOutput, ValidationRevert, }; @@ -176,7 +177,13 @@ mockall::mock! { op: v0_6::UserOperation, block: BlockHashOrNumber, gas_price: u128, - ) -> ProviderResult; + ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)>; + + async fn block_data(&self, block: BlockHashOrNumber) -> ProviderResult; + + async fn uo_data(&self, uo: v0_6::UserOperation, block: BlockHashOrNumber) -> ProviderResult; + + fn calc_da_gas_sync(&self, uo_data: &DAGasUOData, block_data: &DAGasBlockData, gas_price: u128) -> u128; } #[async_trait::async_trait] @@ -264,7 +271,13 @@ mockall::mock! { op: v0_7::UserOperation, block: BlockHashOrNumber, gas_price: u128, - ) -> ProviderResult; + ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)>; + + async fn block_data(&self, block: BlockHashOrNumber) -> ProviderResult; + + async fn uo_data(&self, uo: v0_7::UserOperation, block: BlockHashOrNumber) -> ProviderResult; + + fn calc_da_gas_sync(&self, uo_data: &DAGasUOData, block_data: &DAGasBlockData, gas_price: u128) -> u128; } #[async_trait::async_trait] diff --git a/crates/sim/src/estimation/v0_6.rs b/crates/sim/src/estimation/v0_6.rs index 8827dbd72..f08940f53 100644 --- a/crates/sim/src/estimation/v0_6.rs +++ b/crates/sim/src/estimation/v0_6.rs @@ -436,7 +436,7 @@ mod tests { EvmCall, ExecutionResult, GasUsedResult, MockEntryPointV0_6, MockEvmProvider, }; use rundler_types::{ - chain::DAGasOracleContractType, + da::DAGasOracleContractType, v0_6::{UserOperation, UserOperationOptionalGas, UserOperationRequiredFields}, GasFees, UserOperation as UserOperationTrait, ValidationRevert, }; @@ -631,7 +631,7 @@ mod tests { let (mut entry, provider) = create_base_config(); entry .expect_calc_da_gas() - .returning(|_a, _b, _c| Ok(TEST_FEE)); + .returning(|_a, _b, _c| Ok((TEST_FEE, Default::default(), Default::default()))); let settings = Settings { max_verification_gas: 10000000000, @@ -707,7 +707,7 @@ mod tests { entry .expect_calc_da_gas() - .returning(|_a, _b, _c| Ok(TEST_FEE)); + .returning(|_a, _b, _c| Ok((TEST_FEE, Default::default(), Default::default()))); let settings = Settings { max_verification_gas: 10000000000, diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index dbffd795b..512f29687 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -51,6 +51,7 @@ pub async fn estimate_pre_verification_gas< entry_point .calc_da_gas(random_op.clone(), block, gas_price) .await? + .0 } else { 0 }; @@ -85,6 +86,7 @@ pub async fn calc_required_pre_verification_gas< entry_point .calc_da_gas(op.clone(), block, gas_price) .await? + .0 } else { 0 }; diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index c31a90f7f..c86172216 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -17,7 +17,7 @@ alloy-sol-types.workspace = true anyhow.workspace = true async-trait.workspace = true chrono = "0.4.38" -const-hex = "1.12.0" +const-hex.workspace = true constcat = "0.5.0" futures-util.workspace = true metrics.workspace = true diff --git a/crates/types/src/chain.rs b/crates/types/src/chain.rs index 885296182..080f2e92a 100644 --- a/crates/types/src/chain.rs +++ b/crates/types/src/chain.rs @@ -18,6 +18,8 @@ use std::str::FromStr; use alloy_primitives::Address; use serde::{Deserialize, Serialize}; +use crate::da::DAGasOracleContractType; + const ENTRY_POINT_ADDRESS_V6_0: &str = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; const ENTRY_POINT_ADDRESS_V7_0: &str = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; @@ -118,19 +120,6 @@ pub struct ChainSpec { pub chain_history_size: u64, } -/// Type of gas oracle contract for pricing calldata in preVerificationGas -#[derive(Clone, Copy, Debug, Deserialize, Default, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum DAGasOracleContractType { - /// No gas oracle contract - #[default] - None, - /// Arbitrum Nitro type gas oracle contract - ArbitrumNitro, - /// Optimism Bedrock type gas oracle contract - OptimismBedrock, -} - /// Type of oracle for estimating priority fees #[derive(Clone, Debug, Deserialize, Default, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] diff --git a/crates/types/src/da.rs b/crates/types/src/da.rs new file mode 100644 index 000000000..f09fe7532 --- /dev/null +++ b/crates/types/src/da.rs @@ -0,0 +1,96 @@ +// 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/. + +//! Types associated with DA gas calculations + +use serde::{Deserialize, Serialize}; + +/// Type of gas oracle contract for pricing calldata in preVerificationGas +#[derive(Clone, Copy, Debug, Deserialize, Default, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DAGasOracleContractType { + /// No gas oracle contract + #[default] + None, + /// Arbitrum Nitro type gas oracle contract + ArbitrumNitro, + /// Optimism Bedrock type gas oracle contract + OptimismBedrock, + /// Local Bedrock type gas oracle contract + LocalBedrock, + /// Cached Nitro type gas oracle contract + CachedNitro, +} + +/// Data associated with a user operation for Nitro DA gas calculations +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NitroDAGasUOData { + /// The calculated user operation units as they apply to DA gas. Only have meaning when used + /// with the NitroDAGasBlockData that was used to calculate them and combined with a NitroDAGasBlockData. + pub uo_units: u128, +} + +/// Data associated with a user operation for Bedrock DA gas calculations +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BedrockDAGasUOData { + /// The calculated user operation units as they apply to DA gas. Only have meaning when used + /// with the BedrockDAGasBlockData that was used to calculate them and combined with a + /// BedrockDAGasBlockData. + pub uo_units: u64, +} + +/// Data associated with a user operation for DA gas calculations +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub enum DAGasUOData { + /// Empty, no data + #[default] + Empty, + /// Nitro DA + Nitro(NitroDAGasUOData), + /// Bedrock DA + Bedrock(BedrockDAGasUOData), +} + +/// Data associated with a block for DA gas calculations +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub enum DAGasBlockData { + /// Empty, no data + #[default] + Empty, + /// Nitro DA + Nitro(NitroDAGasBlockData), + /// Bedrock DA + Bedrock(BedrockDAGasBlockData), +} + +/// Data associated with a block for Nitro DA gas calculations +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NitroDAGasBlockData { + /// L1 base fee retrieved from the nitro node interface precompile. + pub l1_base_fee: u128, + /// Base fee retrieved from the nitro node interface precompile. + pub base_fee: u128, +} + +/// Data associated with a block for Bedrock DA gas calculations +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BedrockDAGasBlockData { + /// Base fee scalar retrieved from the bedrock gas oracle. + pub base_fee_scalar: u64, + /// L1 base fee retrieved from the bedrock gas oracle. + pub l1_base_fee: u64, + /// Blob base fee scalar retrieved from the bedrock gas oracle. + pub blob_base_fee_scalar: u64, + /// Blob base fee retrieved from the bedrock gas oracle. + pub blob_base_fee: u64, +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 840844e01..dccb061b0 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -24,6 +24,8 @@ pub mod builder; pub mod chain; +pub mod da; + mod entity; pub use entity::{Entity, EntityInfo, EntityInfos, EntityType, EntityUpdate, EntityUpdateType};