diff --git a/Cargo.lock b/Cargo.lock index 803a89b1..17fe675b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,9 +97,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.0.20" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae58d888221eecf621595e2096836ce7cfc37be06bfa39d7f64aa6a3ea4c9e5b" +checksum = "48dff4dd98e17de00203f851800bbc8b76eb29a4d4e3e44074614338b7a3308d" dependencies = [ "alloy-consensus", "alloy-contract", @@ -132,9 +132,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6093bc69509849435a2d68237a2e9fea79d27390c8e62f1e4012c460aabad8" +checksum = "eda689f7287f15bd3582daba6be8d1545bad3740fd1fb778f629a1fe866bb43b" dependencies = [ "alloy-eips", "alloy-primitives 1.3.0", @@ -158,9 +158,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d1cfed4fefd13b5620cb81cdb6ba397866ff0de514c1b24806e6e79cdff5570" +checksum = "2b5659581e41e8fe350ecc3593cb5c9dcffddfd550896390f2b78a07af67b0fa" dependencies = [ "alloy-consensus", "alloy-eips", @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28074a21cd4f7c3a7ab218c4f38fae6be73944e1feae3b670c68b60bf85ca40" +checksum = "944085cf3ac8f32d96299aa26c03db7c8ca6cdaafdbc467910b889f0328e6b70" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -269,9 +269,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5937e2d544e9b71000942d875cbc57965b32859a666ea543cc57aae5a06d602d" +checksum = "6f35887da30b5fc50267109a3c61cd63e6ca1f45967983641053a40ee83468c1" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -332,9 +332,9 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51b4c13e02a8104170a4de02ccf006d7c233e6c10ab290ee16e7041e6ac221d" +checksum = "11d4009efea6f403b3a80531f9c6f70fc242399498ff71196a1688cc1c901f44" dependencies = [ "alloy-eips", "alloy-primitives 1.3.0", @@ -384,9 +384,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b590caa6b6d8bc10e6e7a7696c59b1e550e89f27f50d1ee13071150d3a3e3f66" +checksum = "883dee3b4020fcb5667ee627b4f401e899dad82bf37b246620339dd980720ed9" dependencies = [ "alloy-primitives 1.3.0", "alloy-sol-types 1.3.0", @@ -399,9 +399,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36fe5af1fca03277daa56ad4ce5f6d623d3f4c2273ea30b9ee8674d18cefc1fa" +checksum = "cd6e5b8ac1654a05c224390008e43634a2bdc74e181e02cf8ed591d8b3d4ad08" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -425,9 +425,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793df1e3457573877fbde8872e4906638fde565ee2d3bd16d04aad17d43dbf0e" +checksum = "80d7980333dd9391719756ac28bc2afa9baa705fc70ffd11dc86ab078dd64477" dependencies = [ "alloy-consensus", "alloy-eips", @@ -541,9 +541,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59879a772ebdcde9dc4eb38b2535d32e8503d3175687cc09e763a625c5fcf32" +checksum = "478a42fe167057b7b919cd8b0c2844f0247f667473340dad100eaf969de5754e" dependencies = [ "alloy-chains", "alloy-consensus", @@ -570,7 +570,6 @@ dependencies = [ "either", "futures", "futures-utils-wasm", - "http", "lru 0.13.0", "parking_lot", "pin-project", @@ -586,13 +585,14 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbdfb2899b54b7cb0063fa8e61938320f9be6b81b681be69c203abf130a87baa" +checksum = "b0a99b17987f40a066b29b6b56d75e84cd193b866cac27cae17b59f40338de95" dependencies = [ "alloy-json-rpc", "alloy-primitives 1.3.0", "alloy-transport", + "auto_impl", "bimap", "futures", "parking_lot", @@ -629,9 +629,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f060e3bb9f319eb01867a2d6d1ff9e0114e8877f5ca8f5db447724136106cae" +checksum = "8a0c6d723fbdf4a87454e2e3a275e161be27edcfbf46e2e3255dd66c138634b6" dependencies = [ "alloy-json-rpc", "alloy-primitives 1.3.0", @@ -655,9 +655,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d47b637369245d2dafef84b223b1ff5ea59e6cd3a98d2d3516e32788a0b216df" +checksum = "c41492dac39365b86a954de86c47ec23dcc7452cdb2fde591caadc194b3e34c6" dependencies = [ "alloy-primitives 1.3.0", "alloy-rpc-types-engine", @@ -692,9 +692,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e26b4dd90b33bd158975307fb9cf5fafa737a0e33cbb772a8648bf8be13c104" +checksum = "8f7eb22670a972ad6c222a6c6dac3eef905579acffe9d63ab42be24c7d158535" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -731,9 +731,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f9cbf5f781b9ee39cfdddea078fdef6015424f4c8282ef0e5416d15ca352c4" +checksum = "e24c171377c0684e3860385f6d93fbfcc8ecc74f6cce8304c822bf1a50bacce0" dependencies = [ "alloy-consensus", "alloy-eips", @@ -751,9 +751,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46586ec3c278639fc0e129f0eb73dbfa3d57f683c44b2ff5e066fab7ba63fa1f" +checksum = "b777b98526bbe5b7892ca22a7fd5f18ed624ff664a79f40d0f9f2bf94ba79a84" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -802,9 +802,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3aaf142f4f6c0bdd06839c422179bae135024407d731e6f365380f88cd4730e" +checksum = "3cc803e9b8d16154c856a738c376e002abe4b388e5fef91c8aebc8373e99fd45" dependencies = [ "alloy-primitives 1.3.0", "alloy-rpc-types-eth", @@ -814,9 +814,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e1722bc30feef87cc0fa824e43c9013f9639cc6c037be7be28a31361c788be2" +checksum = "ee8d2c52adebf3e6494976c8542fbdf12f10123b26e11ad56f77274c16a2a039" dependencies = [ "alloy-primitives 1.3.0", "arbitrary", @@ -826,9 +826,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3674beb29e68fbbc7be302b611cf35fe07b736e308012a280861df5a2361395" +checksum = "7c0494d1e0f802716480aabbe25549c7f6bc2a25ff33b08fd332bbb4b7d06894" dependencies = [ "alloy-primitives 1.3.0", "async-trait", @@ -841,9 +841,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad7094c39cd41b03ed642145b0bd37251e31a9cf2ed19e1ce761f089867356a6" +checksum = "59c2435eb8979a020763ced3fb478932071c56e5f75ea86db41f320915d325ba" dependencies = [ "alloy-consensus", "alloy-network", @@ -1001,12 +1001,13 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89bec2f59a41c0e259b6fe92f78dfc49862c17d10f938db9c33150d5a7f42b6" +checksum = "3c0107675e10c7f248bf7273c1e7fdb02409a717269cc744012e6f3c39959bfb" dependencies = [ "alloy-json-rpc", "alloy-primitives 1.3.0", + "auto_impl", "base64 0.22.1", "derive_more", "futures", @@ -1024,9 +1025,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d3615ec64d775fec840f4e9d5c8e1f739eb1854d8d28db093fb3d4805e0cb53" +checksum = "78e3736701b5433afd06eecff08f0688a71a10e0e1352e0bbf0bed72f0dd4e35" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -1039,9 +1040,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374db72669d8ee09063b9aa1a316e812d5cdfce7fc9a99a3eceaa0e5512300d2" +checksum = "c79064b5a08259581cb5614580010007c2df6deab1e8f3e8c7af8d7e9227008f" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -1059,9 +1060,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5dbaa6851875d59c8803088f4b6ec72eaeddf7667547ae8995c1a19fbca6303" +checksum = "77fd607158cb9bc54cbcfcaab4c5f36c5b26994c7dc58b6f095ce27a54f270f3" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -1097,9 +1098,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f916ff6d52f219c44a9684aea764ce2c7e1d53bd4a724c9b127863aeacc30bb" +checksum = "6acb36318dfa50817154064fea7932adf2eec3f51c86680e2b37d7e8906c66bb" dependencies = [ "alloy-primitives 1.3.0", "darling", @@ -6295,6 +6296,7 @@ dependencies = [ "alloy-op-evm 0.15.0", "alloy-primitives 1.3.0", "alloy-provider", + "alloy-rlp", "alloy-rpc-client", "alloy-rpc-types-beacon", "alloy-rpc-types-engine", @@ -6317,6 +6319,7 @@ dependencies = [ "futures-util", "hex", "http", + "itertools 0.14.0", "jsonrpsee 0.25.1 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpsee-core 0.25.1 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpsee-types 0.25.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -6332,6 +6335,7 @@ dependencies = [ "op-revm 8.0.3", "opentelemetry 0.29.1", "parking_lot", + "pod-sdk", "rand 0.9.1", "reth", "reth-basic-payload-builder 1.6.0", @@ -6964,6 +6968,66 @@ dependencies = [ "crunchy", ] +[[package]] +name = "pod-examples-solidity" +version = "0.1.0" +source = "git+https://github.com/podnetwork/pod-sdk?rev=cab43b2075fb683f3eb5c34cf7b738f420b0c31d#cab43b2075fb683f3eb5c34cf7b738f420b0c31d" +dependencies = [ + "alloy", + "serde", +] + +[[package]] +name = "pod-sdk" +version = "0.1.6" +source = "git+https://github.com/podnetwork/pod-sdk?rev=1c665bb9926960c83f3253ced3f3af2b972f7657#1c665bb9926960c83f3253ced3f3af2b972f7657" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-json-rpc", + "alloy-network", + "alloy-primitives 1.3.0", + "alloy-provider", + "alloy-pubsub", + "alloy-rpc-types", + "alloy-signer", + "alloy-signer-local", + "alloy-sol-types 1.3.0", + "alloy-transport", + "anyhow", + "async-trait", + "futures", + "hex", + "pod-examples-solidity", + "pod-types", + "serde", + "tracing", +] + +[[package]] +name = "pod-types" +version = "0.1.6" +source = "git+https://github.com/podnetwork/pod-sdk?rev=1c665bb9926960c83f3253ced3f3af2b972f7657#1c665bb9926960c83f3253ced3f3af2b972f7657" +dependencies = [ + "alloy-consensus", + "alloy-primitives 1.3.0", + "alloy-rpc-types", + "alloy-signer", + "alloy-signer-local", + "alloy-sol-types 1.3.0", + "anyhow", + "base64 0.22.1", + "bytes", + "hex", + "itertools 0.13.0", + "serde", + "serde_with", + "thiserror 2.0.12", + "tokio", + "tracing", + "utoipa", +] + [[package]] name = "pollster" version = "0.4.0" @@ -13948,6 +14012,29 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap 2.10.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "uuid" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 1c1d32d8..28b02453 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/flashbots/op-rbuilder" exclude = [".github/"] [workspace] -members = [ "crates/op-rbuilder", "crates/tdx-quote-provider"] +members = ["crates/op-rbuilder", "crates/tdx-quote-provider"] default-members = ["crates/op-rbuilder"] resolver = "2" @@ -140,6 +140,7 @@ alloy-rpc-types-eth = { version = "1.0.23" } alloy-signer-local = { version = "1.0.23" } alloy-rpc-client = { version = "1.0.23" } alloy-genesis = { version = "1.0.23" } +alloy-rlp = { version = "0.3.12" } alloy-trie = { version = "0.9.0" } # optimism diff --git a/crates/op-rbuilder/Cargo.toml b/crates/op-rbuilder/Cargo.toml index a2bf891e..374d73a5 100644 --- a/crates/op-rbuilder/Cargo.toml +++ b/crates/op-rbuilder/Cargo.toml @@ -71,6 +71,7 @@ alloy-serde.workspace = true alloy-json-rpc.workspace = true alloy-signer-local.workspace = true alloy-sol-types.workspace = true +alloy-rlp.workspace = true # op alloy-op-evm.workspace = true @@ -132,6 +133,8 @@ ctor = { version = "0.4.2", optional = true } rlimit = { version = "0.10", optional = true } macros = { path = "src/tests/framework/macros", optional = true } testcontainers = "0.24.0" +pod-sdk = { git = "https://github.com/podnetwork/pod-sdk", rev = "1c665bb9926960c83f3253ced3f3af2b972f7657" } +itertools = "0.14.0" [target.'cfg(unix)'.dependencies] tikv-jemallocator = { version = "0.6", optional = true } diff --git a/crates/op-rbuilder/src/args/mod.rs b/crates/op-rbuilder/src/args/mod.rs index 0c0b1a4e..16e53962 100644 --- a/crates/op-rbuilder/src/args/mod.rs +++ b/crates/op-rbuilder/src/args/mod.rs @@ -75,6 +75,9 @@ impl CliExt for Cli { if node_command.ext.flashblocks.enabled { return BuilderMode::Flashblocks; } + if node_command.ext.pod.is_enabled { + return BuilderMode::Pod; + } } BuilderMode::Standard } diff --git a/crates/op-rbuilder/src/args/op.rs b/crates/op-rbuilder/src/args/op.rs index cd0d968d..2f86aae3 100644 --- a/crates/op-rbuilder/src/args/op.rs +++ b/crates/op-rbuilder/src/args/op.rs @@ -5,6 +5,7 @@ //! clap [Args](clap::Args) for optimism rollup configuration use crate::{flashtestations::args::FlashtestationsArgs, tx_signer::Signer}; +use alloy_primitives::Address; use anyhow::{anyhow, Result}; use clap::Parser; use reth_optimism_cli::commands::Commands; @@ -51,6 +52,8 @@ pub struct OpRbuilderArgs { )] pub playground: Option, #[command(flatten)] + pub pod: PodArgs, + #[command(flatten)] pub flashblocks: FlashblocksArgs, #[command(flatten)] pub telemetry: TelemetryArgs, @@ -150,6 +153,26 @@ impl Default for FlashblocksArgs { } } +#[derive(Debug, Clone, PartialEq, Eq, clap::Args)] +pub struct PodArgs { + #[arg(long = "pod.enabled", default_value = "false", env = "ENABLE_POD")] + pub is_enabled: bool, + + #[arg( + long = "pod.rpc-url", + env = "POD_RPC_URL", + default_value = "wss://rpc.v2.pod.network" + )] + pub pod_rpc_url: String, + + #[arg( + long = "pod.contract-address", + env = "POD_CONTRACT_ADDRESS", + default_value = "0xedd0670497e00ded712a398563ea938a29dd28c7" + )] + pub pod_contract_address: Address, +} + /// Parameters for telemetry configuration #[derive(Debug, Clone, Default, PartialEq, Eq, clap::Args)] pub struct TelemetryArgs { diff --git a/crates/op-rbuilder/src/builders/mod.rs b/crates/op-rbuilder/src/builders/mod.rs index 1c41f370..9c74f404 100644 --- a/crates/op-rbuilder/src/builders/mod.rs +++ b/crates/op-rbuilder/src/builders/mod.rs @@ -18,10 +18,12 @@ mod builder_tx; mod context; mod flashblocks; mod generator; +mod pod; mod standard; pub use builder_tx::BuilderTx; pub use flashblocks::FlashblocksBuilder; +pub use pod::PodBuilder; pub use standard::StandardBuilder; /// Defines the payload building mode for the OP builder. @@ -34,6 +36,10 @@ pub enum BuilderMode { /// block every short interval and makes it available through a websocket update /// then merges them into a full block every chain block time. Flashblocks, + /// Uses the pod payload builder that builds blocks every chain block time + /// but picks transactions from an auction running on pod, selecting the highest + /// bidding transactions. + Pod, } /// Defines the interface for any block builder implementation API entry point. diff --git a/crates/op-rbuilder/src/builders/pod/config.rs b/crates/op-rbuilder/src/builders/pod/config.rs new file mode 100644 index 00000000..90fe7d24 --- /dev/null +++ b/crates/op-rbuilder/src/builders/pod/config.rs @@ -0,0 +1,20 @@ +use crate::args::OpRbuilderArgs; +use alloy_primitives::Address; + +/// Configuration values that are specific to the flashblocks builder. +#[derive(Debug, Clone)] +pub struct Config { + pub rpc_url: String, + pub contract_address: Address, +} + +impl TryFrom for Config { + type Error = eyre::Report; + + fn try_from(args: OpRbuilderArgs) -> Result { + Ok(Self { + rpc_url: args.pod.pod_rpc_url, + contract_address: args.pod.pod_contract_address, + }) + } +} diff --git a/crates/op-rbuilder/src/builders/pod/mod.rs b/crates/op-rbuilder/src/builders/pod/mod.rs new file mode 100644 index 00000000..f43dd548 --- /dev/null +++ b/crates/op-rbuilder/src/builders/pod/mod.rs @@ -0,0 +1,37 @@ +use config::Config; +use payload::PodPayloadBuilderBuilder; +use reth_node_builder::components::BasicPayloadServiceBuilder; + +use crate::traits::{NodeBounds, PoolBounds}; + +use super::BuilderConfig; + +mod config; +mod payload; +mod pod_client; + +/// Block building strategy that builds blocks using auction running on pod by +/// producing blocks every chain block time. +pub struct PodBuilder; + +impl super::PayloadBuilder for PodBuilder { + type Config = Config; + + type ServiceBuilder + = BasicPayloadServiceBuilder + where + Node: NodeBounds, + Pool: PoolBounds; + + fn new_service( + config: BuilderConfig, + ) -> eyre::Result> + where + Node: NodeBounds, + Pool: PoolBounds, + { + Ok(BasicPayloadServiceBuilder::new(PodPayloadBuilderBuilder( + config, + ))) + } +} diff --git a/crates/op-rbuilder/src/builders/pod/payload.rs b/crates/op-rbuilder/src/builders/pod/payload.rs new file mode 100644 index 00000000..c5098ff1 --- /dev/null +++ b/crates/op-rbuilder/src/builders/pod/payload.rs @@ -0,0 +1,577 @@ +use crate::{ + builders::{generator::BuildArguments, BuilderConfig}, + metrics::OpRBuilderMetrics, + primitives::reth::ExecutionInfo, + traits::{ClientBounds, NodeBounds, PayloadTxsBounds, PoolBounds}, + tx::FBPooledTransaction, +}; +use alloy_consensus::{ + constants::EMPTY_WITHDRAWALS, proofs, BlockBody, Header, EMPTY_OMMER_ROOT_HASH, +}; +use alloy_eips::{eip7685::EMPTY_REQUESTS_HASH, merge::BEACON_NONCE}; +use alloy_primitives::U256; +use reth::payload::PayloadBuilderAttributes; +use reth_basic_payload_builder::{BuildOutcome, BuildOutcomeKind, MissingPayloadBehaviour}; +use reth_chain_state::{ExecutedBlock, ExecutedBlockWithTrieUpdates, ExecutedTrieUpdates}; +use reth_evm::{execute::BlockBuilder, ConfigureEvm}; +use reth_node_api::{Block, PayloadBuilderError}; +use reth_node_builder::{components::PayloadBuilderBuilder, BuilderContext}; +use reth_optimism_consensus::{calculate_receipt_root_no_memo_optimism, isthmus}; +use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; +use reth_optimism_forks::OpHardforks; +use reth_optimism_node::{OpBuiltPayload, OpPayloadBuilderAttributes}; +use reth_optimism_primitives::{OpPrimitives, OpTransactionSigned}; +use reth_payload_util::NoopPayloadTransactions; +use reth_primitives::RecoveredBlock; +use reth_provider::{ + ExecutionOutcome, HashedPostStateProvider, ProviderError, StateRootProvider, + StorageRootProvider, +}; +use reth_revm::{ + database::StateProviderDatabase, db::states::bundle_state::BundleRetention, State, +}; +use reth_tasks::TaskExecutor; +use reth_transaction_pool::BestTransactionsAttributes; +use revm::Database; +use std::{sync::Arc, time::Instant}; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; + +use super::{ + super::context::{estimate_gas_for_builder_tx, OpPayloadBuilderCtx}, + config::Config, + pod_client::PodClient, +}; + +pub struct PodPayloadBuilderBuilder(pub BuilderConfig); + +impl PayloadBuilderBuilder for PodPayloadBuilderBuilder +where + Node: NodeBounds, + Pool: PoolBounds, +{ + type PayloadBuilder = PodOpPayloadBuilder; + + async fn build_payload_builder( + self, + ctx: &BuilderContext, + _: Pool, + _evm_config: OpEvmConfig, + ) -> eyre::Result { + PodOpPayloadBuilder::new( + OpEvmConfig::optimism(ctx.chain_spec()), + ctx.task_executor().clone(), + ctx.provider().clone(), + self.0.clone(), + ) + .await + } +} + +/// Optimism's payload builder +#[derive(Debug, Clone)] +pub struct PodOpPayloadBuilder { + /// The type responsible for creating the evm. + pub evm_config: OpEvmConfig, + /// Node client + pub client: Client, + pod_client: Arc, + executor: TaskExecutor, + /// Settings for the builder, e.g. DA settings. + pub config: BuilderConfig, + /// The metrics for the builder + pub metrics: Arc, +} + +impl PodOpPayloadBuilder { + pub async fn new( + evm_config: OpEvmConfig, + executor: TaskExecutor, + client: Client, + config: BuilderConfig, + ) -> eyre::Result { + info!(target: "payload_builder", contract_address = %config.specific.contract_address, rpc_url = config.specific.rpc_url, "Initializing PodOpPayloadBuilder"); + Ok(Self { + client, + executor, + pod_client: Arc::new( + PodClient::new( + config.specific.rpc_url.clone(), + config.specific.contract_address, + ) + .await?, + ), + config, + evm_config, + metrics: Default::default(), + }) + } +} + +impl reth_basic_payload_builder::PayloadBuilder for PodOpPayloadBuilder +where + Client: ClientBounds, +{ + type Attributes = OpPayloadBuilderAttributes; + type BuiltPayload = OpBuiltPayload; + + fn try_build( + &self, + args: reth_basic_payload_builder::BuildArguments, + ) -> Result, PayloadBuilderError> { + let reth_basic_payload_builder::BuildArguments { + cached_reads, + config, + cancel: _, // TODO + best_payload: _, + } = args; + + let block_number = config.parent_header.header().number + 1; + let args = BuildArguments { + cached_reads, + config, + cancel: CancellationToken::new(), + }; + + let span = tracing::info_span!("build_payload", block_number); + let _enter = span.enter(); + + self.build_payload(args, |timestamp, _attrs| { + let pod_client = self.pod_client.clone(); + let handle = self.executor.handle().clone(); + let span = span.clone(); + let txs = std::thread::spawn(move || { + let _enter = span.enter(); + handle.block_on(pod_client.best_transactions(timestamp)) + }) + .join() + .unwrap() + .unwrap(); + + info!(target: "payload_builder", len=txs.len(), "fetched best transactions"); + txs + }) + } + + fn on_missing_payload( + &self, + _args: reth_basic_payload_builder::BuildArguments, + ) -> MissingPayloadBehaviour { + MissingPayloadBehaviour::AwaitInProgress + } + + fn build_empty_payload( + &self, + config: reth_basic_payload_builder::PayloadConfig< + Self::Attributes, + reth_basic_payload_builder::HeaderForPayload, + >, + ) -> Result { + let args = BuildArguments { + config, + cached_reads: Default::default(), + cancel: Default::default(), + }; + self.build_payload(args, |_, _| { + NoopPayloadTransactions::::default() + })? + .into_payload() + .ok_or_else(|| PayloadBuilderError::MissingPayload) + } +} + +impl PodOpPayloadBuilder +where + Client: ClientBounds, +{ + /// Constructs an Optimism payload from the transactions sent via the + /// Payload attributes by the sequencer. If the `no_tx_pool` argument is passed in + /// the payload attributes, the transaction pool will be ignored and the only transactions + /// included in the payload will be those sent through the attributes. + /// + /// Given build arguments including an Optimism client, transaction pool, + /// and configuration, this function creates a transaction payload. Returns + /// a result indicating success with the payload or an error in case of failure. + fn build_payload<'a, Txs, F>( + &self, + args: BuildArguments, OpBuiltPayload>, + best: F, + ) -> Result, PayloadBuilderError> + where + Txs: PayloadTxsBounds, + F: FnOnce(u64, BestTransactionsAttributes) -> Txs + Send + Sync + 'a, + { + let block_build_start_time = Instant::now(); + + let BuildArguments { + mut cached_reads, + config, + cancel, + } = args; + + let chain_spec = self.client.chain_spec(); + let timestamp = config.attributes.timestamp(); + let block_env_attributes = OpNextBlockEnvAttributes { + timestamp, + suggested_fee_recipient: config.attributes.suggested_fee_recipient(), + prev_randao: config.attributes.prev_randao(), + gas_limit: config + .attributes + .gas_limit + .unwrap_or(config.parent_header.gas_limit), + parent_beacon_block_root: config + .attributes + .payload_attributes + .parent_beacon_block_root, + extra_data: if chain_spec.is_holocene_active_at_timestamp(timestamp) { + config + .attributes + .get_holocene_extra_data(chain_spec.base_fee_params_at_timestamp(timestamp)) + .map_err(PayloadBuilderError::other)? + } else { + Default::default() + }, + }; + + let evm_env = self + .evm_config + .next_evm_env(&config.parent_header, &block_env_attributes) + .map_err(PayloadBuilderError::other)?; + + let ctx = OpPayloadBuilderCtx { + evm_config: self.evm_config.clone(), + da_config: self.config.da_config.clone(), + chain_spec, + config, + evm_env, + block_env_attributes, + cancel, + builder_signer: self.config.builder_signer, + metrics: self.metrics.clone(), + extra_ctx: Default::default(), + }; + + let builder = OpBuilder::new(best); + + let state_provider = self.client.state_by_block_hash(ctx.parent().hash())?; + let state = StateProviderDatabase::new(state_provider); + let metrics = ctx.metrics.clone(); + + if ctx.attributes().no_tx_pool { + let db = State::builder() + .with_database(state) + .with_bundle_update() + .build(); + builder.build(db, ctx) + } else { + // sequencer mode we can reuse cachedreads from previous runs + let db = State::builder() + .with_database(cached_reads.as_db_mut(state)) + .with_bundle_update() + .build(); + builder.build(db, ctx) + } + .map(|out| { + metrics + .total_block_built_duration + .record(block_build_start_time.elapsed()); + + out.with_cached_reads(cached_reads) + }) + } +} + +/// The type that builds the payload. +/// +/// Payload building for optimism is composed of several steps. +/// The first steps are mandatory and defined by the protocol. +/// +/// 1. first all System calls are applied. +/// 2. After canyon the forced deployed `create2deployer` must be loaded +/// 3. all sequencer transactions are executed (part of the payload attributes) +/// +/// Depending on whether the node acts as a sequencer and is allowed to include additional +/// transactions (`no_tx_pool == false`): +/// 4. include additional transactions +/// +/// And finally +/// 5. build the block: compute all roots (txs, state) +#[derive(derive_more::Debug)] +pub struct OpBuilder Txs> { + /// Yields the best transaction to include if transactions from the mempool are allowed. + best: F, +} + +impl OpBuilder +where + F: FnOnce(u64, BestTransactionsAttributes) -> Txs + Send + Sync, +{ + fn new(best: F) -> Self { + Self { best } + } +} + +/// Holds the state after execution +#[derive(Debug)] +pub struct ExecutedPayload { + /// Tracked execution info + pub info: ExecutionInfo, +} + +impl Txs> OpBuilder { + /// Executes the payload and returns the outcome. + pub fn execute( + self, + state: &mut State, + ctx: &OpPayloadBuilderCtx, + ) -> Result, PayloadBuilderError> + where + DB: Database + AsRef

+ std::fmt::Debug, + P: StorageRootProvider, + { + let Self { best } = self; + info!(target: "payload_builder", id=%ctx.payload_id(), parent_header = ?ctx.parent().hash(), parent_number = ctx.parent().number, "building new payload"); + + // 1. apply pre-execution changes + ctx.evm_config + .builder_for_next_block(state, ctx.parent(), ctx.block_env_attributes.clone()) + .map_err(PayloadBuilderError::other)? + .apply_pre_execution_changes()?; + + let sequencer_tx_start_time = Instant::now(); + + // 3. execute sequencer transactions + let mut info = ctx.execute_sequencer_transactions(state)?; + + ctx.metrics + .sequencer_tx_duration + .record(sequencer_tx_start_time.elapsed()); + + // 4. if mem pool transactions are requested we execute them + + // gas reserved for builder tx + let message = format!("Block Number: {}", ctx.block_number()) + .as_bytes() + .to_vec(); + let builder_tx_gas = ctx + .builder_signer() + .map_or(0, |_| estimate_gas_for_builder_tx(message.clone())); + let block_gas_limit = ctx.block_gas_limit() - builder_tx_gas; + // Save some space in the block_da_limit for builder tx + let builder_tx_da_size = ctx + .estimate_builder_tx_da_size(state, builder_tx_gas, message.clone()) + .unwrap_or(0); + let block_da_limit = ctx + .da_config + .max_da_block_size() + .map(|da_limit| { + let da_limit = da_limit.saturating_sub(builder_tx_da_size); + if da_limit == 0 { + error!("Builder tx da size subtraction caused max_da_block_size to be 0. No transaction would be included."); + } + da_limit + }); + + if !ctx.attributes().no_tx_pool { + let best_txs_start_time = Instant::now(); + let best_txs = best( + ctx.attributes().timestamp(), + ctx.best_transaction_attributes(), + ); + ctx.metrics + .transaction_pool_fetch_duration + .record(best_txs_start_time.elapsed()); + if ctx + .execute_best_transactions( + &mut info, + state, + best_txs, + block_gas_limit, + block_da_limit, + )? + .is_some() + { + return Ok(BuildOutcomeKind::Cancelled); + } + } + + // Add builder tx to the block + ctx.add_builder_tx(&mut info, state, builder_tx_gas, message); + + let state_merge_start_time = Instant::now(); + + // merge all transitions into bundle state, this would apply the withdrawal balance changes + // and 4788 contract call + state.merge_transitions(BundleRetention::Reverts); + + ctx.metrics + .state_transition_merge_duration + .record(state_merge_start_time.elapsed()); + ctx.metrics + .payload_num_tx + .record(info.executed_transactions.len() as f64); + + let payload = ExecutedPayload { info }; + + ctx.metrics.block_built_success.increment(1); + Ok(BuildOutcomeKind::Better { payload }) + } + + /// Builds the payload on top of the state. + pub fn build( + self, + mut state: State, + ctx: OpPayloadBuilderCtx, + ) -> Result, PayloadBuilderError> + where + DB: Database + AsRef

+ std::fmt::Debug, + P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, + { + let ExecutedPayload { info } = match self.execute(&mut state, &ctx)? { + BuildOutcomeKind::Better { payload } | BuildOutcomeKind::Freeze(payload) => payload, + BuildOutcomeKind::Cancelled => return Ok(BuildOutcomeKind::Cancelled), + BuildOutcomeKind::Aborted { fees } => return Ok(BuildOutcomeKind::Aborted { fees }), + }; + + let block_number = ctx.block_number(); + let execution_outcome = ExecutionOutcome::new( + state.take_bundle(), + vec![info.receipts], + block_number, + Vec::new(), + ); + let receipts_root = execution_outcome + .generic_receipts_root_slow(block_number, |receipts| { + calculate_receipt_root_no_memo_optimism( + receipts, + &ctx.chain_spec, + ctx.attributes().timestamp(), + ) + }) + .expect("Number is in range"); + let logs_bloom = execution_outcome + .block_logs_bloom(block_number) + .expect("Number is in range"); + + // calculate the state root + let state_root_start_time = Instant::now(); + + let state_provider = state.database.as_ref(); + let hashed_state = state_provider.hashed_post_state(execution_outcome.state()); + let (state_root, trie_output) = { + state + .database + .as_ref() + .state_root_with_updates(hashed_state.clone()) + .inspect_err(|err| { + warn!(target: "payload_builder", + parent_header=%ctx.parent().hash(), + %err, + "failed to calculate state root for payload" + ); + })? + }; + + ctx.metrics + .state_root_calculation_duration + .record(state_root_start_time.elapsed()); + + let (withdrawals_root, requests_hash) = if ctx.is_isthmus_active() { + // withdrawals root field in block header is used for storage root of L2 predeploy + // `l2tol1-message-passer` + ( + Some( + isthmus::withdrawals_root(execution_outcome.state(), state.database.as_ref()) + .map_err(PayloadBuilderError::other)?, + ), + Some(EMPTY_REQUESTS_HASH), + ) + } else if ctx.is_canyon_active() { + (Some(EMPTY_WITHDRAWALS), None) + } else { + (None, None) + }; + + // create the block header + let transactions_root = proofs::calculate_transaction_root(&info.executed_transactions); + + // OP doesn't support blobs/EIP-4844. + // https://specs.optimism.io/protocol/exec-engine.html#ecotone-disable-blob-transactions + // Need [Some] or [None] based on hardfork to match block hash. + let (excess_blob_gas, blob_gas_used) = ctx.blob_fields(); + let extra_data = ctx.extra_data()?; + + let header = Header { + parent_hash: ctx.parent().hash(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: ctx.evm_env.block_env.beneficiary, + state_root, + transactions_root, + receipts_root, + withdrawals_root, + logs_bloom, + timestamp: ctx.attributes().payload_attributes.timestamp, + mix_hash: ctx.attributes().payload_attributes.prev_randao, + nonce: BEACON_NONCE.into(), + base_fee_per_gas: Some(ctx.base_fee()), + number: ctx.parent().number + 1, + gas_limit: ctx.block_gas_limit(), + difficulty: U256::ZERO, + gas_used: info.cumulative_gas_used, + extra_data, + parent_beacon_block_root: ctx.attributes().payload_attributes.parent_beacon_block_root, + blob_gas_used, + excess_blob_gas, + requests_hash, + }; + + // seal the block + let block = alloy_consensus::Block::::new( + header, + BlockBody { + transactions: info.executed_transactions, + ommers: vec![], + withdrawals: ctx.withdrawals().cloned(), + }, + ); + + let sealed_block = Arc::new(block.seal_slow()); + info!(target: "payload_builder", id=%ctx.attributes().payload_id(), "sealed built block"); + + // create the executed block data + let executed: ExecutedBlockWithTrieUpdates = ExecutedBlockWithTrieUpdates { + block: ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::< + alloy_consensus::Block, + >::new_sealed( + sealed_block.as_ref().clone(), info.executed_senders + )), + execution_output: Arc::new(execution_outcome), + hashed_state: Arc::new(hashed_state), + }, + trie: ExecutedTrieUpdates::Present(Arc::new(trie_output)), + }; + + let no_tx_pool = ctx.attributes().no_tx_pool; + + let payload = OpBuiltPayload::new( + ctx.payload_id(), + sealed_block, + info.total_fees, + Some(executed), + ); + + ctx.metrics + .payload_byte_size + .record(payload.block().size() as f64); + + if no_tx_pool { + // if `no_tx_pool` is set only transactions from the payload attributes will be included + // in the payload. In other words, the payload is deterministic and we can + // freeze it once we've successfully built it. + Ok(BuildOutcomeKind::Freeze(payload)) + } else { + Ok(BuildOutcomeKind::Better { payload }) + } + } +} diff --git a/crates/op-rbuilder/src/builders/pod/pod_client.rs b/crates/op-rbuilder/src/builders/pod/pod_client.rs new file mode 100644 index 00000000..95b7e5f3 --- /dev/null +++ b/crates/op-rbuilder/src/builders/pod/pod_client.rs @@ -0,0 +1,140 @@ +use std::{collections::VecDeque, fmt::Debug, time::Duration}; + +use eyre::Context; +use alloy_consensus::Transaction; +use alloy_primitives::{Address, U256}; +use alloy_rlp::Decodable; +use itertools::Itertools; +use pod_sdk::{auctions::client::AuctionClient, provider::PodProviderBuilder}; +use reth_optimism_primitives::OpTransactionSigned; +use reth_optimism_txpool::OpPooledTransaction; +use reth_payload_util::PayloadTransactions; +use reth_primitives::Recovered; +use tokio::sync::OnceCell; + +use crate::tx::FBPooledTransaction; + +pub(super) struct PodClient { + client: OnceCell, + rpc_url: String, + contract: Address, +} + +#[derive(Debug)] +pub(super) struct Transactions(VecDeque); + +impl PodClient { + pub async fn new(rpc_url: String, contract: Address) -> eyre::Result { + Ok(Self { + client: OnceCell::new(), + rpc_url, + contract, + }) + } + + async fn get_client(&self) -> eyre::Result<&AuctionClient> { + self.client + .get_or_try_init(|| async { + let provider = PodProviderBuilder::new() + .on_url(self.rpc_url.clone()) + .await + .map_err(eyre::Error::from)?; + + Ok(AuctionClient::new(provider, self.contract)) + }) + .await + } + + #[tracing::instrument(skip(self))] + pub async fn best_transactions(&self, timestamp_secs: u64) -> eyre::Result { + let auction = self.get_client().await?; + let auction_deadline = timestamp_secs - 2; + tracing::trace!(target: "payload_builder", auction_deadline, "querying best transactions"); + let auction_deadline = pod_sdk::Timestamp::from_seconds(auction_deadline); + + tracing::info!("waiting for past perfect time"); + // FIXME: The current PPT implementation in pod is dummy and waits 5s which is far too long. + // Use auction.wait_for_auction_end() when it's fixed. + auction + .auction + .provider() + .wait_past_perfect_time( + auction_deadline - Duration::from_secs(5) + Duration::from_millis(200), + ) + .await + .context("waiting for auction end (past perfect time)")?; + tracing::info!("past perfect time reached"); + + let bids = auction + .fetch_bids_for_deadline(auction_deadline.into()) + // .fetch_bids(U256::from(auction_deadline )) // auction ID is in microseconds + .await + .unwrap_or_else(|err| { + tracing::error!(target: "payload_builder", ?err, "failed to fetch bids from pod"); + Vec::new() + }); + let transactions = bids + .into_iter() + .sorted_unstable_by_key(|bid| { + bid.amount + }) + .rev() + .filter_map(|bid| { + let recovered = match + Recovered::::decode(&mut bid.data.as_slice()) { + Ok(tx) => { + tracing::info!(target: "payload_builder", tx=%tx.tx_hash(), bid=%bid.amount, "fetched tx from pod: {tx:?}"); + Some(tx) + }, + Err(error) => { + tracing::warn!(target: "payload_builder", ?error, "failed to decode transaction from pod"); + None + } + }?; + if U256::from(recovered.max_priority_fee_per_gas().unwrap_or(0)) != bid.amount { + tracing::error!(target: "payload_builder", tx=%recovered.tx_hash(), "ignoring tx with different max priority fee per gas than bid amount"); + } + + Some(OpPooledTransaction::new(recovered, bid.data.len()).into()) + }) + .collect::>(); + Ok(Transactions(transactions)) + } +} + +impl Debug for PodClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PodClient") + .field("rpc_url", &self.rpc_url) + .finish() + } +} + +impl PayloadTransactions for Transactions { + type Transaction = FBPooledTransaction; + /// Returns the next transaction to include in the block. + fn next( + &mut self, + // In the future, `ctx` can include access to state for block building purposes. + _ctx: (), + ) -> Option { + self.0.pop_front() + } + + /// Exclude descendants of the transaction with given sender and nonce from the iterator, + /// because this transaction won't be included in the block. + fn mark_invalid(&mut self, sender: Address, nonce: u64) { + tracing::warn!( + target: "payload_builder", + %sender, + nonce, + "mark_invalid called" + ); + } +} + +impl Transactions { + pub fn len(&self) -> usize { + self.0.len() + } +} diff --git a/crates/op-rbuilder/src/launcher.rs b/crates/op-rbuilder/src/launcher.rs index 7379c91d..db4e5cf9 100644 --- a/crates/op-rbuilder/src/launcher.rs +++ b/crates/op-rbuilder/src/launcher.rs @@ -3,7 +3,9 @@ use reth_optimism_rpc::OpEthApiBuilder; use crate::{ args::*, - builders::{BuilderConfig, BuilderMode, FlashblocksBuilder, PayloadBuilder, StandardBuilder}, + builders::{ + BuilderConfig, BuilderMode, FlashblocksBuilder, PayloadBuilder, PodBuilder, StandardBuilder, + }, metrics::{record_flag_gauge_metrics, VERSION}, monitor_tx_pool::monitor_tx_pool, primitives::reth::engine_api_builder::OpEngineApiBuilder, @@ -57,6 +59,11 @@ pub fn launch() -> Result<()> { let launcher = BuilderLauncher::::new(); cli_app.run(launcher)?; } + BuilderMode::Pod => { + tracing::info!("Starting OP builder in pod mode"); + let launcher = BuilderLauncher::::new(); + cli_app.run(launcher)?; + } } Ok(()) }