From 2d541096630055db5f66e7c9ca85d5063c21e5b0 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Tue, 18 Mar 2025 13:10:33 +0700 Subject: [PATCH 1/2] feat(supervisor): add supervisor HTTP endpoint and safety level to RollupArgs - Introduced `supervisor_http` and `supervisor_safety_level` fields in `RollupArgs` for enhanced configuration. - Updated `OpPoolBuilder` and `OpTransactionValidator` to utilize the new supervisor client and safety level. - Enhanced transaction validation logic to incorporate supervisor checks. - Introduced `SupervisorClient` for handling interactions with the supervisor. - Updated `OpTransactionValidator` to include a supervisor client for cross-transaction validation. - Enhanced error handling for invalid cross transactions in the transaction pool. --- Cargo.lock | 7 + crates/optimism/node/src/args.rs | 20 +- crates/optimism/node/src/node.rs | 41 +++- crates/optimism/payload/Cargo.toml | 2 +- crates/optimism/primitives/Cargo.toml | 12 ++ crates/optimism/primitives/src/lib.rs | 5 + .../primitives/src/supervisor/access_list.rs | 41 ++++ .../optimism/primitives/src/supervisor/api.rs | 33 ++++ .../primitives/src/supervisor/constants.rs | 22 +++ .../primitives/src/supervisor/errors.rs | 175 ++++++++++++++++++ .../primitives/src/supervisor/message.rs | 36 ++++ .../optimism/primitives/src/supervisor/mod.rs | 40 ++++ .../primitives/src/supervisor/reqwest.rs | 48 +++++ .../src/supervisor/reth_supervisor.rs | 44 +++++ .../primitives/src/supervisor/safety.rs | 104 +++++++++++ .../primitives/src/supervisor/traits.rs | 89 +++++++++ crates/optimism/txpool/Cargo.toml | 5 +- crates/optimism/txpool/src/error.rs | 48 +++++ crates/optimism/txpool/src/lib.rs | 2 + crates/optimism/txpool/src/transaction.rs | 6 +- crates/optimism/txpool/src/validator.rs | 120 +++++++++++- 21 files changed, 883 insertions(+), 17 deletions(-) create mode 100644 crates/optimism/primitives/src/supervisor/access_list.rs create mode 100644 crates/optimism/primitives/src/supervisor/api.rs create mode 100644 crates/optimism/primitives/src/supervisor/constants.rs create mode 100644 crates/optimism/primitives/src/supervisor/errors.rs create mode 100644 crates/optimism/primitives/src/supervisor/message.rs create mode 100644 crates/optimism/primitives/src/supervisor/mod.rs create mode 100644 crates/optimism/primitives/src/supervisor/reqwest.rs create mode 100644 crates/optimism/primitives/src/supervisor/reth_supervisor.rs create mode 100644 crates/optimism/primitives/src/supervisor/safety.rs create mode 100644 crates/optimism/primitives/src/supervisor/traits.rs create mode 100644 crates/optimism/txpool/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 4832f7ac44b..4f4f66db745 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8905,9 +8905,11 @@ dependencies = [ "alloy-network", "alloy-primitives", "alloy-rlp", + "alloy-rpc-client", "alloy-rpc-types-eth", "alloy-serde", "arbitrary", + "async-trait", "bincode", "bytes", "derive_more 2.0.1", @@ -8926,6 +8928,8 @@ dependencies = [ "serde", "serde_json", "serde_with", + "thiserror 2.0.12", + "tokio", ] [[package]] @@ -9027,6 +9031,9 @@ dependencies = [ "reth-provider", "reth-storage-api", "reth-transaction-pool", + "thiserror 2.0.12", + "tokio", + "tracing", ] [[package]] diff --git a/crates/optimism/node/src/args.rs b/crates/optimism/node/src/args.rs index 1926f74dab1..d96c135e569 100644 --- a/crates/optimism/node/src/args.rs +++ b/crates/optimism/node/src/args.rs @@ -2,6 +2,8 @@ //! clap [Args](clap::Args) for optimism rollup configuration +use reth_optimism_primitives::supervisor::{SafetyLevel, DEFAULT_SUPERVISOR_URL}; + /// Parameters for rollup configuration #[derive(Debug, Clone, PartialEq, Eq, clap::Args)] #[command(next_help_heading = "Rollup")] @@ -37,9 +39,23 @@ pub struct RollupArgs { /// Enable transaction conditional support on sequencer #[arg(long = "rollup.enable-tx-conditional", default_value = "false")] pub enable_tx_conditional: bool, + + /// HTTP endpoint for the supervisor + #[arg( + long = "rollup.supervisor-http", + value_name = "SUPERVISOR_HTTP_URL", + default_value = DEFAULT_SUPERVISOR_URL + )] + pub supervisor_http: String, + + /// Safety level for the supervisor + #[arg( + long = "rollup.supervisor-safety-level", + default_value_t = SafetyLevel::CrossUnsafe, + )] + pub supervisor_safety_level: SafetyLevel, } -#[expect(clippy::derivable_impls)] impl Default for RollupArgs { fn default() -> Self { Self { @@ -49,6 +65,8 @@ impl Default for RollupArgs { compute_pending_block: false, discovery_v4: false, enable_tx_conditional: false, + supervisor_http: DEFAULT_SUPERVISOR_URL.to_string(), + supervisor_safety_level: SafetyLevel::CrossUnsafe, } } } diff --git a/crates/optimism/node/src/node.rs b/crates/optimism/node/src/node.rs index 4ebe6547053..7fecebf4ed8 100644 --- a/crates/optimism/node/src/node.rs +++ b/crates/optimism/node/src/node.rs @@ -33,7 +33,10 @@ use reth_optimism_payload_builder::{ builder::OpPayloadTransactions, config::{OpBuilderConfig, OpDAConfig}, }; -use reth_optimism_primitives::{DepositReceipt, OpPrimitives, OpReceipt, OpTransactionSigned}; +use reth_optimism_primitives::{ + supervisor::{SafetyLevel, DEFAULT_SUPERVISOR_URL}, + DepositReceipt, OpPrimitives, OpReceipt, OpTransactionSigned, SupervisorClient, +}; use reth_optimism_rpc::{ eth::{ext::OpEthExtApi, OpEthApiBuilder}, miner::{MinerApiExtServer, OpMinerExtApi}, @@ -113,7 +116,11 @@ impl OpNode { .node_types::() .pool( OpPoolBuilder::default() - .with_enable_tx_conditional(self.args.enable_tx_conditional), + .with_enable_tx_conditional(self.args.enable_tx_conditional) + .with_supervisor( + self.args.supervisor_http.clone(), + self.args.supervisor_safety_level, + ), ) .payload(BasicPayloadServiceBuilder::new( OpPayloadBuilder::new(compute_pending_block).with_da_config(self.da_config.clone()), @@ -479,6 +486,10 @@ pub struct OpPoolBuilder { pub pool_config_overrides: PoolBuilderConfigOverrides, /// Enable transaction conditionals. pub enable_tx_conditional: bool, + /// Supervisor client url + pub supervisor_http: String, + /// Supervisor safety level + pub supervisor_safety_level: SafetyLevel, /// Marker for the pooled transaction type. _pd: core::marker::PhantomData, } @@ -488,6 +499,8 @@ impl Default for OpPoolBuilder { Self { pool_config_overrides: Default::default(), enable_tx_conditional: false, + supervisor_http: DEFAULT_SUPERVISOR_URL.to_string(), + supervisor_safety_level: SafetyLevel::CrossUnsafe, _pd: Default::default(), } } @@ -508,6 +521,17 @@ impl OpPoolBuilder { self.pool_config_overrides = pool_config_overrides; self } + + /// Sets the supervisor client + pub fn with_supervisor( + mut self, + supervisor_client: String, + supervisor_safety_level: SafetyLevel, + ) -> Self { + self.supervisor_http = supervisor_client; + self.supervisor_safety_level = supervisor_safety_level; + self + } } impl PoolBuilder for OpPoolBuilder @@ -523,6 +547,17 @@ where let Self { pool_config_overrides, .. } = self; let data_dir = ctx.config().datadir(); let blob_store = DiskFileBlobStore::open(data_dir.blobstore(), Default::default())?; + // supervisor used for interop + if ctx.chain_spec().is_interop_active_at_timestamp(ctx.head().timestamp) && + self.supervisor_http == DEFAULT_SUPERVISOR_URL + { + info!(target: "reth::cli", + url=%DEFAULT_SUPERVISOR_URL, + "Default supervisor url is used, consider changing --rollup.supervisor-http." + ); + } + let supervisor_client = + SupervisorClient::new(self.supervisor_http.clone(), self.supervisor_safety_level).await; let validator = TransactionValidationTaskExecutor::eth_builder(ctx.provider().clone()) .no_eip4844() @@ -539,6 +574,8 @@ where // In --dev mode we can't require gas fees because we're unable to decode // the L1 block info .require_l1_data_gas_fee(!ctx.config().dev.dev) + // TODO: fix this clone + .with_supervisor(supervisor_client.clone()) }); let transaction_pool = reth_transaction_pool::Pool::new( diff --git a/crates/optimism/payload/Cargo.toml b/crates/optimism/payload/Cargo.toml index d4e41d29acd..f2615f7a6d1 100644 --- a/crates/optimism/payload/Cargo.toml +++ b/crates/optimism/payload/Cargo.toml @@ -31,7 +31,7 @@ reth-payload-validator.workspace = true # op-reth reth-optimism-evm.workspace = true reth-optimism-forks.workspace = true -reth-optimism-primitives.workspace = true +reth-optimism-primitives = { workspace = true, default-features = true, features = ["supervisor"] } reth-optimism-txpool.workspace = true # ethereum diff --git a/crates/optimism/primitives/Cargo.toml b/crates/optimism/primitives/Cargo.toml index ec76e79f342..1e20cbbaf65 100644 --- a/crates/optimism/primitives/Cargo.toml +++ b/crates/optimism/primitives/Cargo.toml @@ -32,6 +32,7 @@ op-alloy-consensus.workspace = true alloy-rpc-types-eth = { workspace = true, optional = true } alloy-network = { workspace = true, optional = true } alloy-serde = { workspace = true, optional = true } +alloy-rpc-client = { workspace = true, optional = true, features = ["reqwest", "default"] } # codec bytes = { workspace = true, optional = true } @@ -42,6 +43,9 @@ serde_with = { workspace = true, optional = true } # misc derive_more = { workspace = true, features = ["deref", "from", "into", "constructor"] } rand = { workspace = true, optional = true } +async-trait = { workspace = true, optional = true } +thiserror = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } # test arbitrary = { workspace = true, features = ["derive"], optional = true } @@ -80,6 +84,7 @@ std = [ "serde_json/std", "alloy-evm/std", "serde_with?/std", + "thiserror/std", ] alloy-compat = ["dep:alloy-network", "dep:alloy-serde", "dep:alloy-rpc-types-eth", "op-alloy-consensus/alloy-compat"] reth-codec = [ @@ -130,3 +135,10 @@ arbitrary = [ "alloy-rpc-types-eth?/arbitrary", "alloy-serde?/arbitrary", ] +supervisor = [ + "dep:serde", + "dep:thiserror", + "dep:tokio", + "dep:async-trait", + "dep:alloy-rpc-client", +] diff --git a/crates/optimism/primitives/src/lib.rs b/crates/optimism/primitives/src/lib.rs index dd367f9fa07..595fadc53e8 100644 --- a/crates/optimism/primitives/src/lib.rs +++ b/crates/optimism/primitives/src/lib.rs @@ -23,6 +23,11 @@ pub mod transaction; pub use transaction::{signed::OpTransactionSigned, tx_type::OpTxType}; mod receipt; +#[cfg(feature = "supervisor")] +pub mod supervisor; +#[cfg(feature = "supervisor")] +pub use supervisor::SupervisorClient; + pub use receipt::{DepositReceipt, OpReceipt}; /// Optimism-specific block type. diff --git a/crates/optimism/primitives/src/supervisor/access_list.rs b/crates/optimism/primitives/src/supervisor/access_list.rs new file mode 100644 index 00000000000..9b3e4b0f2b4 --- /dev/null +++ b/crates/optimism/primitives/src/supervisor/access_list.rs @@ -0,0 +1,41 @@ +// Source: https://github.com/op-rs/kona +// Copyright © 2023 kona contributors Copyright © 2024 Optimism +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +// associated documentation files (the “Software”), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +use crate::supervisor::CROSS_L2_INBOX_ADDRESS; +use alloy_eips::eip2930::AccessListItem; +use alloy_primitives::B256; + +/// Parses [`AccessListItem`]s to inbox entries. +/// +/// Return flattened iterator with all inbox entries. +pub fn parse_access_list_items_to_inbox_entries<'a>( + access_list_items: impl Iterator, +) -> impl Iterator { + access_list_items.filter_map(parse_access_list_item_to_inbox_entries).flatten() +} + +/// Parse [`AccessListItem`] to inbox entries, if any. +/// Max 3 inbox entries can exist per [`AccessListItem`] that points to [`CROSS_L2_INBOX_ADDRESS`]. +/// +/// Returns `Vec::new()` if [`AccessListItem`] address doesn't point to [`CROSS_L2_INBOX_ADDRESS`]. +// TODO: add url to spec once [pr](https://github.com/ethereum-optimism/specs/pull/612) is merged +fn parse_access_list_item_to_inbox_entries( + access_list_item: &AccessListItem, +) -> Option> { + (access_list_item.address == CROSS_L2_INBOX_ADDRESS) + .then(|| access_list_item.storage_keys.iter()) +} diff --git a/crates/optimism/primitives/src/supervisor/api.rs b/crates/optimism/primitives/src/supervisor/api.rs new file mode 100644 index 00000000000..9017d71320f --- /dev/null +++ b/crates/optimism/primitives/src/supervisor/api.rs @@ -0,0 +1,33 @@ +//! Interop RPC API. +// Source: https://github.com/op-rs/kona +// Copyright © 2023 kona contributors Copyright © 2024 Optimism +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +// associated documentation files (the “Software”), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +use crate::supervisor::{ExecutingDescriptor, InteropTxValidatorError, SafetyLevel}; +use alloy_primitives::B256; +use core::future::Future; + +/// Subset of `op-supervisor` API, used for validating interop events. +// TODO: add link once https://github.com/ethereum-optimism/optimism/pull/14784 merged +pub trait CheckAccessList { + /// Returns if the messages meet the minimum safety level. + fn check_access_list( + &self, + inbox_entries: &[B256], + min_safety: SafetyLevel, + executing_descriptor: ExecutingDescriptor, + ) -> impl Future> + Send; +} diff --git a/crates/optimism/primitives/src/supervisor/constants.rs b/crates/optimism/primitives/src/supervisor/constants.rs new file mode 100644 index 00000000000..dcc38ddac03 --- /dev/null +++ b/crates/optimism/primitives/src/supervisor/constants.rs @@ -0,0 +1,22 @@ +//! Constants for the OP Stack interop protocol. +// Source: https://github.com/op-rs/kona +// Copyright © 2023 kona contributors Copyright © 2024 Optimism +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +// associated documentation files (the “Software”), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +use alloy_primitives::{address, Address}; + +/// The address of the L2 cross chain inbox predeploy proxy. +pub const CROSS_L2_INBOX_ADDRESS: Address = address!("4200000000000000000000000000000000000022"); diff --git a/crates/optimism/primitives/src/supervisor/errors.rs b/crates/optimism/primitives/src/supervisor/errors.rs new file mode 100644 index 00000000000..30a14a73941 --- /dev/null +++ b/crates/optimism/primitives/src/supervisor/errors.rs @@ -0,0 +1,175 @@ +//! Error types for the `kona-interop` crate. +// Source: https://github.com/op-rs/kona +// Copyright © 2023 kona contributors Copyright © 2024 Optimism +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +// associated documentation files (the “Software”), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +use crate::supervisor::SafetyLevel; +use alloc::boxed::Box; +use core::error; +use thiserror::Error; + +/// Derived from op-supervisor +// todo: rm once resolved +const UNKNOWN_CHAIN_MSG: &str = "unknown chain: "; +/// Derived from [op-supervisor](https://github.com/ethereum-optimism/optimism/blob/4ba2eb00eafc3d7de2c8ceb6fd83913a8c0a2c0d/op-supervisor/supervisor/backend/backend.go#L479) +// todo: rm once resolved +const MINIMUM_SAFETY_MSG: &str = "does not meet the minimum safety"; + +/// Invalid inbox entry +#[derive(Error, Debug)] +pub enum InvalidInboxEntry { + /// Message does not meet minimum safety level + #[error("message does not meet min safety level, got: {got}, expected: {expected}")] + MinimumSafety { + /// Actual level of the message + got: SafetyLevel, + /// Minimum acceptable level that was passed to supervisor + expected: SafetyLevel, + }, + /// Invalid chain + #[error("unsupported chain id: {0}")] + UnknownChain(u64), +} + +impl InvalidInboxEntry { + /// Parses error message. Returns `None`, if message is not recognized. + // todo: match on error code instead of message string once resolved + pub fn parse_err_msg(err_msg: &str) -> Option { + // Check if it's invalid message call, message example: + // `failed to check message: failed to check log: unknown chain: 14417` + if err_msg.contains(UNKNOWN_CHAIN_MSG) { + if let Ok(chain_id) = + err_msg.split(' ').next_back().expect("message contains chain id").parse::() + { + return Some(Self::UnknownChain(chain_id)) + } + // Check if it's `does not meet the minimum safety` error, message example: + // `message {0x4200000000000000000000000000000000000023 4 1 1728507701 901} + // (safety level: unsafe) does not meet the minimum safety cross-unsafe"` + } else if err_msg.contains(MINIMUM_SAFETY_MSG) { + let message_safety = if err_msg.contains("safety level: safe") { + SafetyLevel::Safe + } else if err_msg.contains("safety level: local-safe") { + SafetyLevel::LocalSafe + } else if err_msg.contains("safety level: cross-unsafe") { + SafetyLevel::CrossUnsafe + } else if err_msg.contains("safety level: unsafe") { + SafetyLevel::Unsafe + } else if err_msg.contains("safety level: invalid") { + SafetyLevel::Invalid + } else { + // Unexpected level name + return None + }; + let expected_safety = if err_msg.contains("safety finalized") { + SafetyLevel::Finalized + } else if err_msg.contains("safety safe") { + SafetyLevel::Safe + } else if err_msg.contains("safety local-safe") { + SafetyLevel::LocalSafe + } else if err_msg.contains("safety cross-unsafe") { + SafetyLevel::CrossUnsafe + } else if err_msg.contains("safety unsafe") { + SafetyLevel::Unsafe + } else { + // Unexpected level name + return None + }; + + return Some(Self::MinimumSafety { expected: expected_safety, got: message_safety }) + } + + None + } +} + +/// Failures occurring during validation of inbox entries. +#[derive(thiserror::Error, Debug)] +pub enum InteropTxValidatorError { + /// Error validating interop event. + #[error(transparent)] + InvalidInboxEntry(#[from] InvalidInboxEntry), + + /// RPC client failure. + #[error("supervisor rpc client failure: {0}")] + RpcClientError(Box), + + /// Message validation against the Supervisor took longer than allowed. + #[error("message validation timed out, timeout: {0} secs")] + ValidationTimeout(u64), + + /// Catch-all variant for other supervisor server errors. + #[error("unexpected error from supervisor: {0}")] + SupervisorServerError(Box), +} + +impl InteropTxValidatorError { + /// Returns a new instance of [`RpcClientError`](Self::RpcClientError) variant. + pub fn client(err: impl error::Error + Send + Sync + 'static) -> Self { + Self::RpcClientError(Box::new(err)) + } + + /// Returns a new instance of [`RpcClientError`](Self::RpcClientError) variant. + pub fn server_unexpected(err: impl error::Error + Send + Sync + 'static) -> Self { + Self::SupervisorServerError(Box::new(err)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MIN_SAFETY_CROSS_UNSAFE_ERROR: &str = "message {0x4200000000000000000000000000000000000023 4 1 1728507701 901} (safety level: unsafe) does not meet the minimum safety cross-unsafe"; + const MIN_SAFETY_UNSAFE_ERROR: &str = "message {0x4200000000000000000000000000000000000023 1091637521 4369 0 901} (safety level: invalid) does not meet the minimum safety unsafe"; + const MIN_SAFETY_FINALIZED_ERROR: &str = "message {0x4200000000000000000000000000000000000023 1091600001 215 1170 901} (safety level: safe) does not meet the minimum safety finalized"; + const INVALID_CHAIN: &str = + "failed to check message: failed to check log: unknown chain: 14417"; + const RANDOM_ERROR: &str = "gibberish error"; + + #[test] + fn test_op_supervisor_error_parsing() { + assert!(matches!( + InvalidInboxEntry::parse_err_msg(MIN_SAFETY_CROSS_UNSAFE_ERROR).unwrap(), + InvalidInboxEntry::MinimumSafety { + expected: SafetyLevel::CrossUnsafe, + got: SafetyLevel::Unsafe + } + )); + + assert!(matches!( + InvalidInboxEntry::parse_err_msg(MIN_SAFETY_UNSAFE_ERROR).unwrap(), + InvalidInboxEntry::MinimumSafety { + expected: SafetyLevel::Unsafe, + got: SafetyLevel::Invalid + } + )); + + assert!(matches!( + InvalidInboxEntry::parse_err_msg(MIN_SAFETY_FINALIZED_ERROR).unwrap(), + InvalidInboxEntry::MinimumSafety { + expected: SafetyLevel::Finalized, + got: SafetyLevel::Safe, + } + )); + + assert!(matches!( + InvalidInboxEntry::parse_err_msg(INVALID_CHAIN).unwrap(), + InvalidInboxEntry::UnknownChain(14417) + )); + + assert!(InvalidInboxEntry::parse_err_msg(RANDOM_ERROR).is_none()); + } +} diff --git a/crates/optimism/primitives/src/supervisor/message.rs b/crates/optimism/primitives/src/supervisor/message.rs new file mode 100644 index 00000000000..84e86c39f63 --- /dev/null +++ b/crates/optimism/primitives/src/supervisor/message.rs @@ -0,0 +1,36 @@ +//! Interop message primitives. +// Source: https://github.com/op-rs/kona +// Copyright © 2023 kona contributors Copyright © 2024 Optimism +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +// associated documentation files (the “Software”), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +/// An [`ExecutingDescriptor`] is a part of the payload to `supervisor_checkAccessList` +/// Spec: +#[derive(Default, Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct ExecutingDescriptor { + /// The timestamp used to enforce timestamp [invariant](https://github.com/ethereum-optimism/specs/blob/main/specs/interop/derivation.md#invariants) + timestamp: u64, + /// The timeout that requests verification to still hold at `timestamp+timeout` + /// (message expiry may drop previously valid messages). + #[serde(skip_serializing_if = "Option::is_none")] + timeout: Option, +} + +impl ExecutingDescriptor { + /// Create a new [`ExecutingDescriptor`] from the timestamp and timeout + pub const fn new(timestamp: u64, timeout: Option) -> Self { + Self { timestamp, timeout } + } +} diff --git a/crates/optimism/primitives/src/supervisor/mod.rs b/crates/optimism/primitives/src/supervisor/mod.rs new file mode 100644 index 00000000000..f08c50e9ff8 --- /dev/null +++ b/crates/optimism/primitives/src/supervisor/mod.rs @@ -0,0 +1,40 @@ +//! Copied interop related kona code +// Source: https://github.com/op-rs/kona +// Copyright © 2023 kona contributors Copyright © 2024 Optimism +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +// associated documentation files (the “Software”), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// TODO: we should remove everything expect for supervisor once kona could be used as dependency in +// reth +mod access_list; +pub use access_list::parse_access_list_items_to_inbox_entries; +mod api; +pub use api::CheckAccessList; +mod safety; +pub use safety::SafetyLevel; +// TODO: this should be renamed to supervisor once we remove kona from here +mod reth_supervisor; +pub use reth_supervisor::{SupervisorClient, DEFAULT_SUPERVISOR_URL}; +mod constants; +pub use constants::CROSS_L2_INBOX_ADDRESS; +mod errors; +pub use errors::{InteropTxValidatorError, InvalidInboxEntry}; +mod message; +pub use message::ExecutingDescriptor; +mod reqwest; +pub use reqwest::SupervisorClient as ReqwestSupervisorClient; + +mod traits; +pub use traits::InteropTxValidator; diff --git a/crates/optimism/primitives/src/supervisor/reqwest.rs b/crates/optimism/primitives/src/supervisor/reqwest.rs new file mode 100644 index 00000000000..af3564f6eed --- /dev/null +++ b/crates/optimism/primitives/src/supervisor/reqwest.rs @@ -0,0 +1,48 @@ +//! RPC API implementation using `reqwest` +// Source: https://github.com/op-rs/kona +// Copyright © 2023 kona contributors Copyright © 2024 Optimism +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +// associated documentation files (the “Software”), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +use crate::supervisor::{ + api::CheckAccessList, ExecutingDescriptor, InteropTxValidatorError, SafetyLevel, +}; +use alloy_primitives::B256; +use alloy_rpc_client::ReqwestClient; +use derive_more::Constructor; + +/// A supervisor client. +#[derive(Debug, Clone, Constructor)] +pub struct SupervisorClient { + /// The inner RPC client. + client: ReqwestClient, +} + +impl CheckAccessList for SupervisorClient { + async fn check_access_list( + &self, + inbox_entries: &[B256], + min_safety: SafetyLevel, + executing_descriptor: ExecutingDescriptor, + ) -> Result<(), InteropTxValidatorError> { + self.client + .request( + "supervisor_checkAccessList", + (inbox_entries, min_safety, executing_descriptor), + ) + .await + .map_err(InteropTxValidatorError::client) + } +} diff --git a/crates/optimism/primitives/src/supervisor/reth_supervisor.rs b/crates/optimism/primitives/src/supervisor/reth_supervisor.rs new file mode 100644 index 00000000000..64ead541d73 --- /dev/null +++ b/crates/optimism/primitives/src/supervisor/reth_supervisor.rs @@ -0,0 +1,44 @@ +//! This is our custom implementation of validator struct + +use crate::supervisor::{InteropTxValidator, ReqwestSupervisorClient, SafetyLevel}; +use alloc::string::String; +use alloy_rpc_client::ReqwestClient; +use std::time::Duration; + +/// Supervisor hosted by op-labs +// TODO: This should be changes to actual supervisor url +pub const DEFAULT_SUPERVISOR_URL: &str = "http://localhost:1337/"; + +/// Implementation of the supervisor trait for the interop. +#[derive(Debug, Clone)] +pub struct SupervisorClient { + inner: ReqwestSupervisorClient, + safety: SafetyLevel, +} + +impl SupervisorClient { + /// Creates a new supervisor validator. + pub async fn new(supervisor_endpoint: impl Into, safety: SafetyLevel) -> Self { + let inner = ReqwestSupervisorClient::new( + ReqwestClient::builder() + .connect(supervisor_endpoint.into().as_str()) + .await + .expect("building supervisor client"), + ); + Self { inner, safety } + } + + /// Returns safely level + pub fn safety(&self) -> SafetyLevel { + self.safety + } +} + +impl InteropTxValidator for SupervisorClient { + type SupervisorClient = ReqwestSupervisorClient; + const DEFAULT_TIMEOUT: Duration = Duration::from_millis(100); + + fn supervisor_client(&self) -> &Self::SupervisorClient { + &self.inner + } +} diff --git a/crates/optimism/primitives/src/supervisor/safety.rs b/crates/optimism/primitives/src/supervisor/safety.rs new file mode 100644 index 00000000000..176acb5f7b3 --- /dev/null +++ b/crates/optimism/primitives/src/supervisor/safety.rs @@ -0,0 +1,104 @@ +//! Message safety level for interoperability. +// Source: https://github.com/op-rs/kona +// Copyright © 2023 kona contributors Copyright © 2024 Optimism +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +// associated documentation files (the “Software”), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +use alloc::string::{String, ToString}; +use core::str::FromStr; +use derive_more::Display; +use thiserror::Error; +/// The safety level of a message. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum SafetyLevel { + /// The message is finalized. + Finalized, + /// The message is safe. + Safe, + /// The message is safe locally. + LocalSafe, + /// The message is unsafe across chains. + CrossUnsafe, + /// The message is unsafe. + Unsafe, + /// The message is invalid. + Invalid, +} + +impl FromStr for SafetyLevel { + type Err = SafetyLevelParseError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "finalized" => Ok(Self::Finalized), + "safe" => Ok(Self::Safe), + "local-safe" | "localsafe" => Ok(Self::LocalSafe), + "cross-unsafe" | "crossunsafe" => Ok(Self::CrossUnsafe), + "unsafe" => Ok(Self::Unsafe), + "invalid" => Ok(Self::Invalid), + _ => Err(SafetyLevelParseError(s.to_string())), + } + } +} + +/// Error when parsing [`SafetyLevel`] from string. +#[derive(Error, Debug)] +#[error("Invalid SafetyLevel, error: {0}")] +pub struct SafetyLevelParseError(pub String); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(feature = "serde")] + fn test_safety_level_serde() { + let level = SafetyLevel::Finalized; + let json = serde_json::to_string(&level).unwrap(); + assert_eq!(json, r#""finalized""#); + + let level: SafetyLevel = serde_json::from_str(&json).unwrap(); + assert_eq!(level, SafetyLevel::Finalized); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_safety_level_fails() { + let json = r#""failed""#; + let level: Result = serde_json::from_str(json); + assert!(level.is_err()); + } + + #[test] + fn test_safety_level_from_str_valid() { + assert_eq!(SafetyLevel::from_str("finalized").unwrap(), SafetyLevel::Finalized); + assert_eq!(SafetyLevel::from_str("safe").unwrap(), SafetyLevel::Safe); + assert_eq!(SafetyLevel::from_str("local-safe").unwrap(), SafetyLevel::LocalSafe); + assert_eq!(SafetyLevel::from_str("localsafe").unwrap(), SafetyLevel::LocalSafe); + assert_eq!(SafetyLevel::from_str("cross-unsafe").unwrap(), SafetyLevel::CrossUnsafe); + assert_eq!(SafetyLevel::from_str("crossunsafe").unwrap(), SafetyLevel::CrossUnsafe); + assert_eq!(SafetyLevel::from_str("unsafe").unwrap(), SafetyLevel::Unsafe); + assert_eq!(SafetyLevel::from_str("invalid").unwrap(), SafetyLevel::Invalid); + } + + #[test] + fn test_safety_level_from_str_invalid() { + assert!(SafetyLevel::from_str("unknown").is_err()); + assert!(SafetyLevel::from_str("123").is_err()); + assert!(SafetyLevel::from_str("").is_err()); + assert!(SafetyLevel::from_str("safe ").is_err()); + } +} diff --git a/crates/optimism/primitives/src/supervisor/traits.rs b/crates/optimism/primitives/src/supervisor/traits.rs new file mode 100644 index 00000000000..2a06c1c6a94 --- /dev/null +++ b/crates/optimism/primitives/src/supervisor/traits.rs @@ -0,0 +1,89 @@ +//! RPC validator component used in interop. +//! +//! [`InteropTxValidator`] parses inbox entries from [`AccessListItem`]s, and queries a +//! superchain supervisor for their validity via RPC using the [`CheckAccessList`] API. +// Source: https://github.com/op-rs/kona +// Copyright © 2023 kona contributors Copyright © 2024 Optimism +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +// associated documentation files (the “Software”), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +use crate::supervisor::{ + parse_access_list_items_to_inbox_entries, CheckAccessList, ExecutingDescriptor, + InteropTxValidatorError, SafetyLevel, +}; +use alloc::boxed::Box; +use alloy_eips::eip2930::AccessListItem; +use alloy_primitives::B256; +use core::time::Duration; + +/// Interacts with a Supervisor to validate inbox entries extracted from [`AccessListItem`]s. +#[async_trait::async_trait] +pub trait InteropTxValidator { + /// The supervisor client type. + type SupervisorClient: CheckAccessList + Send + Sync; + + /// Default duration that message validation is not allowed to exceed. + /// + /// Note: this has no effect unless shorter than timeout configured for + /// [`Self::SupervisorClient`] type. + const DEFAULT_TIMEOUT: Duration; + + /// Returns reference to supervisor client. Used in default trait method implementations. + fn supervisor_client(&self) -> &Self::SupervisorClient; + + /// Extracts inbox entries from the [`AccessListItem`]s if there are any. + fn parse_access_list(access_list_items: &[AccessListItem]) -> impl Iterator { + parse_access_list_items_to_inbox_entries(access_list_items.iter()) + } + + /// Validates a list of inbox entries against a Supervisor. + /// + /// Times out RPC requests after given timeout, as long as this timeout is shorter + /// than the underlying request timeout configured for [`Self::SupervisorClient`] type. + async fn validate_messages_with_timeout( + &self, + inbox_entries: &[B256], + safety: SafetyLevel, + executing_descriptor: ExecutingDescriptor, + timeout: Duration, + ) -> Result<(), InteropTxValidatorError> { + // Validate messages against supervisor with timeout. + tokio::time::timeout( + timeout, + self.supervisor_client().check_access_list(inbox_entries, safety, executing_descriptor), + ) + .await + .map_err(|_| InteropTxValidatorError::ValidationTimeout(timeout.as_secs()))? + } + + /// Validates a list of inbox entries against a Supervisor. + /// + /// Times out RPC requests after [`Self::DEFAULT_TIMEOUT`], as long as this timeout is shorter + /// than the underlying request timeout configured for [`Self::SupervisorClient`] type. + async fn validate_messages( + &self, + inbox_entries: &[B256], + safety: SafetyLevel, + executing_descriptor: ExecutingDescriptor, + ) -> Result<(), InteropTxValidatorError> { + self.validate_messages_with_timeout( + inbox_entries, + safety, + executing_descriptor, + Self::DEFAULT_TIMEOUT, + ) + .await + } +} diff --git a/crates/optimism/txpool/Cargo.toml b/crates/optimism/txpool/Cargo.toml index c6ab3443b19..afecb6ef7ec 100644 --- a/crates/optimism/txpool/Cargo.toml +++ b/crates/optimism/txpool/Cargo.toml @@ -33,7 +33,7 @@ op-alloy-consensus.workspace = true op-alloy-flz.workspace = true reth-optimism-evm.workspace = true reth-optimism-forks.workspace = true -reth-optimism-primitives.workspace = true +reth-optimism-primitives = { workspace = true, default-features = true, features = ["supervisor"] } # metrics reth-metrics.workspace = true @@ -44,7 +44,10 @@ c-kzg.workspace = true derive_more.workspace = true futures-util.workspace = true parking_lot.workspace = true +tracing.workspace = true +thiserror.workspace = true [dev-dependencies] reth-optimism-chainspec.workspace = true reth-provider = { workspace = true, features = ["test-utils"] } +tokio.workspace = true diff --git a/crates/optimism/txpool/src/error.rs b/crates/optimism/txpool/src/error.rs new file mode 100644 index 00000000000..c9ff4843e23 --- /dev/null +++ b/crates/optimism/txpool/src/error.rs @@ -0,0 +1,48 @@ +use reth_optimism_primitives::supervisor::{ + InteropTxValidatorError, InvalidInboxEntry, SafetyLevel, +}; +use reth_transaction_pool::error::PoolTransactionError; +use std::any::Any; + +/// Wrapper for [`InteropTxValidatorError`] to implement [`PoolTransactionError`] for it. +#[derive(thiserror::Error, Debug)] +pub enum InvalidCrossTx { + /// Errors produced by supervisor validation + #[error(transparent)] + ValidationError(#[from] InteropTxValidatorError), + /// Error cause by cross chain tx during not active interop hardfork + #[error("cross chain tx is invalid before interop")] + CrossChainTxPreInterop, +} + +impl PoolTransactionError for InvalidCrossTx { + fn is_bad_transaction(&self) -> bool { + match self { + Self::ValidationError(err) => { + match err { + InteropTxValidatorError::InvalidInboxEntry(err) => match err { + // This transaction could become valid after a while + InvalidInboxEntry::MinimumSafety { got, .. } => match got { + // This transaction will never become valid + SafetyLevel::Invalid => true, + // This transaction will become valid when origin chain progress + _ => false, + }, + // This tx will not become valid unless supervisor is reconfigured + InvalidInboxEntry::UnknownChain(_) => true, + }, + // Rpc error or supervisor haven't responded in time + InteropTxValidatorError::RpcClientError(_) | + InteropTxValidatorError::ValidationTimeout(_) => false, + // Transaction caused unknown (for parsing) error in supervisor + InteropTxValidatorError::SupervisorServerError(_) => true, + } + } + Self::CrossChainTxPreInterop => true, + } + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/crates/optimism/txpool/src/lib.rs b/crates/optimism/txpool/src/lib.rs index cb3ea3f110b..fad0a9e654a 100644 --- a/crates/optimism/txpool/src/lib.rs +++ b/crates/optimism/txpool/src/lib.rs @@ -14,8 +14,10 @@ pub use validator::{OpL1BlockInfo, OpTransactionValidator}; pub mod conditional; mod transaction; pub use transaction::{OpPooledTransaction, OpPooledTx}; +mod error; pub mod interop; pub mod maintain; +pub use error::InvalidCrossTx; use reth_transaction_pool::{CoinbaseTipOrdering, Pool, TransactionValidationTaskExecutor}; diff --git a/crates/optimism/txpool/src/transaction.rs b/crates/optimism/txpool/src/transaction.rs index 5fe0c71f2f1..51334ae135c 100644 --- a/crates/optimism/txpool/src/transaction.rs +++ b/crates/optimism/txpool/src/transaction.rs @@ -287,8 +287,8 @@ mod tests { blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder, TransactionOrigin, TransactionValidationOutcome, }; - #[test] - fn validate_optimism_transaction() { + #[tokio::test] + async fn validate_optimism_transaction() { let client = MockEthProvider::default().with_chain_spec(OP_MAINNET.clone()); let validator = EthTransactionValidatorBuilder::new(client) .no_shanghai() @@ -313,7 +313,7 @@ mod tests { let signed_recovered = Recovered::new_unchecked(signed_tx, signer); let len = signed_recovered.encode_2718_len(); let pooled_tx: OpPooledTransaction = OpPooledTransaction::new(signed_recovered, len); - let outcome = validator.validate_one(origin, pooled_tx); + let outcome = validator.validate_one(origin, pooled_tx).await; let err = match outcome { TransactionValidationOutcome::Invalid(_, err) => err, diff --git a/crates/optimism/txpool/src/validator.rs b/crates/optimism/txpool/src/validator.rs index d97b529cacd..3459c0ad5d3 100644 --- a/crates/optimism/txpool/src/validator.rs +++ b/crates/optimism/txpool/src/validator.rs @@ -1,3 +1,4 @@ +use crate::InvalidCrossTx; use alloy_consensus::{BlockHeader, Transaction}; use alloy_eips::Encodable2718; use op_revm::L1BlockInfo; @@ -5,18 +6,25 @@ use parking_lot::RwLock; use reth_chainspec::ChainSpecProvider; use reth_optimism_evm::RethL1BlockInfo; use reth_optimism_forks::OpHardforks; +use reth_optimism_primitives::supervisor::{ + ExecutingDescriptor, InteropTxValidator, SupervisorClient, +}; use reth_primitives_traits::{ transaction::error::InvalidTransactionError, Block, BlockBody, GotExpected, SealedBlock, }; use reth_storage_api::{BlockReaderIdExt, StateProviderFactory}; use reth_transaction_pool::{ - EthPoolTransaction, EthTransactionValidator, TransactionOrigin, TransactionValidationOutcome, - TransactionValidator, + error::InvalidPoolTransactionError, EthPoolTransaction, EthTransactionValidator, + TransactionOrigin, TransactionValidationOutcome, TransactionValidator, }; use std::sync::{ - atomic::{AtomicU64, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, Arc, }; +use tracing::trace; + +/// The interval for which we check transaction against supervisor, 1 day. +const TRANSACTION_VALIDITY_WINDOW: u64 = 86400; /// Tracks additional infos for the current block. #[derive(Debug, Default)] @@ -40,6 +48,10 @@ pub struct OpTransactionValidator { /// derived from the tracked L1 block info that is extracted from the first transaction in the /// L2 block. require_l1_data_gas_fee: bool, + /// Client used to check transaction validity with op-supervisor + supervisor_client: Option, + /// tracks activated forks relevant for transaction validation + fork_tracker: Arc, } impl OpTransactionValidator { @@ -108,7 +120,19 @@ where inner: EthTransactionValidator, block_info: OpL1BlockInfo, ) -> Self { - Self { inner, block_info: Arc::new(block_info), require_l1_data_gas_fee: true } + Self { + inner, + block_info: Arc::new(block_info), + require_l1_data_gas_fee: true, + supervisor_client: None, + fork_tracker: Arc::new(OpForkTracker { interop: AtomicBool::from(false) }), + } + } + + /// Set the supervisor client and safety level + pub fn with_supervisor(mut self, supervisor_client: SupervisorClient) -> Self { + self.supervisor_client = Some(supervisor_client); + self } /// Update the L1 block info for the given header and system transaction, if any. @@ -125,6 +149,10 @@ where if let Some(Ok(cost_addition)) = tx.map(reth_optimism_evm::extract_l1_info_from_tx) { *self.block_info.l1_block_info.write() = cost_addition; } + + if self.chain_spec().is_interop_active_at_timestamp(header.timestamp()) { + self.fork_tracker.interop.store(true, Ordering::Relaxed); + } } /// Validates a single transaction. @@ -133,7 +161,7 @@ where /// /// This behaves the same as [`EthTransactionValidator::validate_one`], but in addition, ensures /// that the account has enough balance to cover the L1 gas cost. - pub fn validate_one( + pub async fn validate_one( &self, origin: TransactionOrigin, transaction: Tx, @@ -145,6 +173,20 @@ where ) } + // Interop cross tx validation + if let Some(Err(err)) = self.is_valid_cross_tx(&transaction).await { + if matches!(err, InvalidCrossTx::CrossChainTxPreInterop) { + return TransactionValidationOutcome::Invalid( + transaction, + InvalidTransactionError::TxTypeNotSupported.into(), + ); + } + return TransactionValidationOutcome::Invalid( + transaction, + InvalidPoolTransactionError::Other(Box::new(err)), + ); + } + let outcome = self.inner.validate_one(origin, transaction); self.apply_op_checks(outcome) @@ -155,11 +197,14 @@ where /// Returns all outcomes for the given transactions in the same order. /// /// See also [`Self::validate_one`] - pub fn validate_all( + pub async fn validate_all( &self, transactions: Vec<(TransactionOrigin, Tx)>, ) -> Vec> { - transactions.into_iter().map(|(origin, tx)| self.validate_one(origin, tx)).collect() + futures_util::future::join_all( + transactions.into_iter().map(|(origin, tx)| self.validate_one(origin, tx)), + ) + .await } /// Performs the necessary opstack specific checks based on top of the regular eth outcome. @@ -219,6 +264,49 @@ where } outcome } + + /// Extracts commitment from access list entries, pointing to 0x420..022 and validates them + /// against supervisor. + /// + /// If commitment present pre-interop tx rejected. + /// + /// Returns: + /// None - if tx is not cross chain, + /// Some(Ok(()) - if tx is valid cross chain, + /// Some(Err(e)) - if tx is not valid or interop is not active + pub async fn is_valid_cross_tx(&self, tx: &Tx) -> Option> { + // We don't need to check for deposit transaction in here, because they won't come from + // txpool + let access_list = tx.access_list()?; + let inbox_entries = + SupervisorClient::parse_access_list(access_list).copied().collect::>(); + if inbox_entries.is_empty() { + return None; + } + let timestamp = self.block_info.timestamp.load(Ordering::Relaxed); + // Interop check + if !self.fork_tracker.is_interop_activated() { + // No cross chain tx allowed before interop + return Some(Err(InvalidCrossTx::CrossChainTxPreInterop)) + } + let client = self + .supervisor_client + .as_ref() + .expect("supervisor client should be always set after interop is active"); + let safety_level = client.safety(); + if let Err(err) = client + .validate_messages( + inbox_entries.as_slice(), + safety_level, + ExecutingDescriptor::new(timestamp, Some(TRANSACTION_VALIDITY_WINDOW)), + ) + .await + { + trace!(target: "txpool", hash=%tx.hash(), err=%err, "Cross chain transaction invalid"); + return Some(Err(InvalidCrossTx::ValidationError(err))); + } + Some(Ok(())) + } } impl TransactionValidator for OpTransactionValidator @@ -233,14 +321,14 @@ where origin: TransactionOrigin, transaction: Self::Transaction, ) -> TransactionValidationOutcome { - self.validate_one(origin, transaction) + self.validate_one(origin, transaction).await } async fn validate_transactions( &self, transactions: Vec<(TransactionOrigin, Self::Transaction)>, ) -> Vec> { - self.validate_all(transactions) + self.validate_all(transactions).await } fn on_new_head_block(&self, new_tip_block: &SealedBlock) @@ -254,3 +342,17 @@ where ); } } + +/// Keeps track of whether certain forks are activated +#[derive(Debug)] +pub(crate) struct OpForkTracker { + /// Tracks if interop is activated at the block's timestamp. + interop: AtomicBool, +} + +impl OpForkTracker { + /// Returns `true` if Interop fork is activated. + pub(crate) fn is_interop_activated(&self) -> bool { + self.interop.load(Ordering::Relaxed) + } +} From c97d92bfeb9915af3a6f28bb82e4caeb99ddde18 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Thu, 27 Mar 2025 10:59:49 +0100 Subject: [PATCH 2/2] touchups --- Cargo.lock | 6 +- crates/optimism/node/src/args.rs | 3 +- crates/optimism/node/src/node.rs | 13 +-- crates/optimism/payload/Cargo.toml | 2 +- crates/optimism/primitives/Cargo.toml | 12 -- crates/optimism/primitives/src/lib.rs | 5 - .../optimism/primitives/src/supervisor/api.rs | 33 ------ .../primitives/src/supervisor/constants.rs | 22 ---- .../optimism/primitives/src/supervisor/mod.rs | 40 ------- .../primitives/src/supervisor/reqwest.rs | 48 -------- .../src/supervisor/reth_supervisor.rs | 44 ------- .../primitives/src/supervisor/safety.rs | 104 ----------------- .../primitives/src/supervisor/traits.rs | 89 --------------- crates/optimism/txpool/Cargo.toml | 6 +- crates/optimism/txpool/src/error.rs | 5 +- crates/optimism/txpool/src/lib.rs | 1 + .../src/supervisor/access_list.rs | 0 .../optimism/txpool/src/supervisor/client.rs | 107 ++++++++++++++++++ .../src/supervisor/errors.rs | 6 +- .../src/supervisor/message.rs | 0 crates/optimism/txpool/src/supervisor/mod.rs | 11 ++ crates/optimism/txpool/src/validator.rs | 54 +++++---- 22 files changed, 169 insertions(+), 442 deletions(-) delete mode 100644 crates/optimism/primitives/src/supervisor/api.rs delete mode 100644 crates/optimism/primitives/src/supervisor/constants.rs delete mode 100644 crates/optimism/primitives/src/supervisor/mod.rs delete mode 100644 crates/optimism/primitives/src/supervisor/reqwest.rs delete mode 100644 crates/optimism/primitives/src/supervisor/reth_supervisor.rs delete mode 100644 crates/optimism/primitives/src/supervisor/safety.rs delete mode 100644 crates/optimism/primitives/src/supervisor/traits.rs rename crates/optimism/{primitives => txpool}/src/supervisor/access_list.rs (100%) create mode 100644 crates/optimism/txpool/src/supervisor/client.rs rename crates/optimism/{primitives => txpool}/src/supervisor/errors.rs (98%) rename crates/optimism/{primitives => txpool}/src/supervisor/message.rs (100%) create mode 100644 crates/optimism/txpool/src/supervisor/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 4f4f66db745..ba865e6d032 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8905,11 +8905,9 @@ dependencies = [ "alloy-network", "alloy-primitives", "alloy-rlp", - "alloy-rpc-client", "alloy-rpc-types-eth", "alloy-serde", "arbitrary", - "async-trait", "bincode", "bytes", "derive_more 2.0.1", @@ -8928,8 +8926,6 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.12", - "tokio", ] [[package]] @@ -9011,6 +9007,7 @@ dependencies = [ "alloy-consensus", "alloy-eips", "alloy-primitives", + "alloy-rpc-client", "alloy-rpc-types-eth", "c-kzg", "derive_more 2.0.1", @@ -9031,6 +9028,7 @@ dependencies = [ "reth-provider", "reth-storage-api", "reth-transaction-pool", + "serde", "thiserror 2.0.12", "tokio", "tracing", diff --git a/crates/optimism/node/src/args.rs b/crates/optimism/node/src/args.rs index d96c135e569..037e7311286 100644 --- a/crates/optimism/node/src/args.rs +++ b/crates/optimism/node/src/args.rs @@ -2,7 +2,8 @@ //! clap [Args](clap::Args) for optimism rollup configuration -use reth_optimism_primitives::supervisor::{SafetyLevel, DEFAULT_SUPERVISOR_URL}; +use op_alloy_consensus::interop::SafetyLevel; +use reth_optimism_txpool::supervisor::DEFAULT_SUPERVISOR_URL; /// Parameters for rollup configuration #[derive(Debug, Clone, PartialEq, Eq, clap::Args)] diff --git a/crates/optimism/node/src/node.rs b/crates/optimism/node/src/node.rs index 7fecebf4ed8..b64c36d3eb9 100644 --- a/crates/optimism/node/src/node.rs +++ b/crates/optimism/node/src/node.rs @@ -6,7 +6,7 @@ use crate::{ txpool::{OpTransactionPool, OpTransactionValidator}, OpEngineApiBuilder, OpEngineTypes, }; -use op_alloy_consensus::OpPooledTransaction; +use op_alloy_consensus::{interop::SafetyLevel, OpPooledTransaction}; use reth_chainspec::{EthChainSpec, Hardforks}; use reth_evm::{execute::BasicBlockExecutorProvider, ConfigureEvm, EvmFactory, EvmFactoryFor}; use reth_network::{NetworkConfig, NetworkHandle, NetworkManager, NetworkPrimitives, PeersInfo}; @@ -33,10 +33,7 @@ use reth_optimism_payload_builder::{ builder::OpPayloadTransactions, config::{OpBuilderConfig, OpDAConfig}, }; -use reth_optimism_primitives::{ - supervisor::{SafetyLevel, DEFAULT_SUPERVISOR_URL}, - DepositReceipt, OpPrimitives, OpReceipt, OpTransactionSigned, SupervisorClient, -}; +use reth_optimism_primitives::{DepositReceipt, OpPrimitives, OpReceipt, OpTransactionSigned}; use reth_optimism_rpc::{ eth::{ext::OpEthExtApi, OpEthApiBuilder}, miner::{MinerApiExtServer, OpMinerExtApi}, @@ -44,7 +41,10 @@ use reth_optimism_rpc::{ OpEthApi, OpEthApiError, SequencerClient, }; use reth_optimism_txpool::{ - conditional::MaybeConditionalTransaction, interop::MaybeInteropTransaction, OpPooledTx, + conditional::MaybeConditionalTransaction, + interop::MaybeInteropTransaction, + supervisor::{SupervisorClient, DEFAULT_SUPERVISOR_URL}, + OpPooledTx, }; use reth_provider::{providers::ProviderFactoryBuilder, CanonStateSubscriptions, EthStorage}; use reth_rpc_api::DebugApiServer; @@ -574,7 +574,6 @@ where // In --dev mode we can't require gas fees because we're unable to decode // the L1 block info .require_l1_data_gas_fee(!ctx.config().dev.dev) - // TODO: fix this clone .with_supervisor(supervisor_client.clone()) }); diff --git a/crates/optimism/payload/Cargo.toml b/crates/optimism/payload/Cargo.toml index f2615f7a6d1..d4e41d29acd 100644 --- a/crates/optimism/payload/Cargo.toml +++ b/crates/optimism/payload/Cargo.toml @@ -31,7 +31,7 @@ reth-payload-validator.workspace = true # op-reth reth-optimism-evm.workspace = true reth-optimism-forks.workspace = true -reth-optimism-primitives = { workspace = true, default-features = true, features = ["supervisor"] } +reth-optimism-primitives.workspace = true reth-optimism-txpool.workspace = true # ethereum diff --git a/crates/optimism/primitives/Cargo.toml b/crates/optimism/primitives/Cargo.toml index 1e20cbbaf65..ec76e79f342 100644 --- a/crates/optimism/primitives/Cargo.toml +++ b/crates/optimism/primitives/Cargo.toml @@ -32,7 +32,6 @@ op-alloy-consensus.workspace = true alloy-rpc-types-eth = { workspace = true, optional = true } alloy-network = { workspace = true, optional = true } alloy-serde = { workspace = true, optional = true } -alloy-rpc-client = { workspace = true, optional = true, features = ["reqwest", "default"] } # codec bytes = { workspace = true, optional = true } @@ -43,9 +42,6 @@ serde_with = { workspace = true, optional = true } # misc derive_more = { workspace = true, features = ["deref", "from", "into", "constructor"] } rand = { workspace = true, optional = true } -async-trait = { workspace = true, optional = true } -thiserror = { workspace = true, optional = true } -tokio = { workspace = true, optional = true } # test arbitrary = { workspace = true, features = ["derive"], optional = true } @@ -84,7 +80,6 @@ std = [ "serde_json/std", "alloy-evm/std", "serde_with?/std", - "thiserror/std", ] alloy-compat = ["dep:alloy-network", "dep:alloy-serde", "dep:alloy-rpc-types-eth", "op-alloy-consensus/alloy-compat"] reth-codec = [ @@ -135,10 +130,3 @@ arbitrary = [ "alloy-rpc-types-eth?/arbitrary", "alloy-serde?/arbitrary", ] -supervisor = [ - "dep:serde", - "dep:thiserror", - "dep:tokio", - "dep:async-trait", - "dep:alloy-rpc-client", -] diff --git a/crates/optimism/primitives/src/lib.rs b/crates/optimism/primitives/src/lib.rs index 595fadc53e8..dd367f9fa07 100644 --- a/crates/optimism/primitives/src/lib.rs +++ b/crates/optimism/primitives/src/lib.rs @@ -23,11 +23,6 @@ pub mod transaction; pub use transaction::{signed::OpTransactionSigned, tx_type::OpTxType}; mod receipt; -#[cfg(feature = "supervisor")] -pub mod supervisor; -#[cfg(feature = "supervisor")] -pub use supervisor::SupervisorClient; - pub use receipt::{DepositReceipt, OpReceipt}; /// Optimism-specific block type. diff --git a/crates/optimism/primitives/src/supervisor/api.rs b/crates/optimism/primitives/src/supervisor/api.rs deleted file mode 100644 index 9017d71320f..00000000000 --- a/crates/optimism/primitives/src/supervisor/api.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Interop RPC API. -// Source: https://github.com/op-rs/kona -// Copyright © 2023 kona contributors Copyright © 2024 Optimism -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -// associated documentation files (the “Software”), to deal in the Software without restriction, -// including without limitation the rights to use, copy, modify, merge, publish, distribute, -// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -use crate::supervisor::{ExecutingDescriptor, InteropTxValidatorError, SafetyLevel}; -use alloy_primitives::B256; -use core::future::Future; - -/// Subset of `op-supervisor` API, used for validating interop events. -// TODO: add link once https://github.com/ethereum-optimism/optimism/pull/14784 merged -pub trait CheckAccessList { - /// Returns if the messages meet the minimum safety level. - fn check_access_list( - &self, - inbox_entries: &[B256], - min_safety: SafetyLevel, - executing_descriptor: ExecutingDescriptor, - ) -> impl Future> + Send; -} diff --git a/crates/optimism/primitives/src/supervisor/constants.rs b/crates/optimism/primitives/src/supervisor/constants.rs deleted file mode 100644 index dcc38ddac03..00000000000 --- a/crates/optimism/primitives/src/supervisor/constants.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Constants for the OP Stack interop protocol. -// Source: https://github.com/op-rs/kona -// Copyright © 2023 kona contributors Copyright © 2024 Optimism -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -// associated documentation files (the “Software”), to deal in the Software without restriction, -// including without limitation the rights to use, copy, modify, merge, publish, distribute, -// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -use alloy_primitives::{address, Address}; - -/// The address of the L2 cross chain inbox predeploy proxy. -pub const CROSS_L2_INBOX_ADDRESS: Address = address!("4200000000000000000000000000000000000022"); diff --git a/crates/optimism/primitives/src/supervisor/mod.rs b/crates/optimism/primitives/src/supervisor/mod.rs deleted file mode 100644 index f08c50e9ff8..00000000000 --- a/crates/optimism/primitives/src/supervisor/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Copied interop related kona code -// Source: https://github.com/op-rs/kona -// Copyright © 2023 kona contributors Copyright © 2024 Optimism -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -// associated documentation files (the “Software”), to deal in the Software without restriction, -// including without limitation the rights to use, copy, modify, merge, publish, distribute, -// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// TODO: we should remove everything expect for supervisor once kona could be used as dependency in -// reth -mod access_list; -pub use access_list::parse_access_list_items_to_inbox_entries; -mod api; -pub use api::CheckAccessList; -mod safety; -pub use safety::SafetyLevel; -// TODO: this should be renamed to supervisor once we remove kona from here -mod reth_supervisor; -pub use reth_supervisor::{SupervisorClient, DEFAULT_SUPERVISOR_URL}; -mod constants; -pub use constants::CROSS_L2_INBOX_ADDRESS; -mod errors; -pub use errors::{InteropTxValidatorError, InvalidInboxEntry}; -mod message; -pub use message::ExecutingDescriptor; -mod reqwest; -pub use reqwest::SupervisorClient as ReqwestSupervisorClient; - -mod traits; -pub use traits::InteropTxValidator; diff --git a/crates/optimism/primitives/src/supervisor/reqwest.rs b/crates/optimism/primitives/src/supervisor/reqwest.rs deleted file mode 100644 index af3564f6eed..00000000000 --- a/crates/optimism/primitives/src/supervisor/reqwest.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! RPC API implementation using `reqwest` -// Source: https://github.com/op-rs/kona -// Copyright © 2023 kona contributors Copyright © 2024 Optimism -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -// associated documentation files (the “Software”), to deal in the Software without restriction, -// including without limitation the rights to use, copy, modify, merge, publish, distribute, -// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -use crate::supervisor::{ - api::CheckAccessList, ExecutingDescriptor, InteropTxValidatorError, SafetyLevel, -}; -use alloy_primitives::B256; -use alloy_rpc_client::ReqwestClient; -use derive_more::Constructor; - -/// A supervisor client. -#[derive(Debug, Clone, Constructor)] -pub struct SupervisorClient { - /// The inner RPC client. - client: ReqwestClient, -} - -impl CheckAccessList for SupervisorClient { - async fn check_access_list( - &self, - inbox_entries: &[B256], - min_safety: SafetyLevel, - executing_descriptor: ExecutingDescriptor, - ) -> Result<(), InteropTxValidatorError> { - self.client - .request( - "supervisor_checkAccessList", - (inbox_entries, min_safety, executing_descriptor), - ) - .await - .map_err(InteropTxValidatorError::client) - } -} diff --git a/crates/optimism/primitives/src/supervisor/reth_supervisor.rs b/crates/optimism/primitives/src/supervisor/reth_supervisor.rs deleted file mode 100644 index 64ead541d73..00000000000 --- a/crates/optimism/primitives/src/supervisor/reth_supervisor.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! This is our custom implementation of validator struct - -use crate::supervisor::{InteropTxValidator, ReqwestSupervisorClient, SafetyLevel}; -use alloc::string::String; -use alloy_rpc_client::ReqwestClient; -use std::time::Duration; - -/// Supervisor hosted by op-labs -// TODO: This should be changes to actual supervisor url -pub const DEFAULT_SUPERVISOR_URL: &str = "http://localhost:1337/"; - -/// Implementation of the supervisor trait for the interop. -#[derive(Debug, Clone)] -pub struct SupervisorClient { - inner: ReqwestSupervisorClient, - safety: SafetyLevel, -} - -impl SupervisorClient { - /// Creates a new supervisor validator. - pub async fn new(supervisor_endpoint: impl Into, safety: SafetyLevel) -> Self { - let inner = ReqwestSupervisorClient::new( - ReqwestClient::builder() - .connect(supervisor_endpoint.into().as_str()) - .await - .expect("building supervisor client"), - ); - Self { inner, safety } - } - - /// Returns safely level - pub fn safety(&self) -> SafetyLevel { - self.safety - } -} - -impl InteropTxValidator for SupervisorClient { - type SupervisorClient = ReqwestSupervisorClient; - const DEFAULT_TIMEOUT: Duration = Duration::from_millis(100); - - fn supervisor_client(&self) -> &Self::SupervisorClient { - &self.inner - } -} diff --git a/crates/optimism/primitives/src/supervisor/safety.rs b/crates/optimism/primitives/src/supervisor/safety.rs deleted file mode 100644 index 176acb5f7b3..00000000000 --- a/crates/optimism/primitives/src/supervisor/safety.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! Message safety level for interoperability. -// Source: https://github.com/op-rs/kona -// Copyright © 2023 kona contributors Copyright © 2024 Optimism -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -// associated documentation files (the “Software”), to deal in the Software without restriction, -// including without limitation the rights to use, copy, modify, merge, publish, distribute, -// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -use alloc::string::{String, ToString}; -use core::str::FromStr; -use derive_more::Display; -use thiserror::Error; -/// The safety level of a message. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum SafetyLevel { - /// The message is finalized. - Finalized, - /// The message is safe. - Safe, - /// The message is safe locally. - LocalSafe, - /// The message is unsafe across chains. - CrossUnsafe, - /// The message is unsafe. - Unsafe, - /// The message is invalid. - Invalid, -} - -impl FromStr for SafetyLevel { - type Err = SafetyLevelParseError; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "finalized" => Ok(Self::Finalized), - "safe" => Ok(Self::Safe), - "local-safe" | "localsafe" => Ok(Self::LocalSafe), - "cross-unsafe" | "crossunsafe" => Ok(Self::CrossUnsafe), - "unsafe" => Ok(Self::Unsafe), - "invalid" => Ok(Self::Invalid), - _ => Err(SafetyLevelParseError(s.to_string())), - } - } -} - -/// Error when parsing [`SafetyLevel`] from string. -#[derive(Error, Debug)] -#[error("Invalid SafetyLevel, error: {0}")] -pub struct SafetyLevelParseError(pub String); - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - #[cfg(feature = "serde")] - fn test_safety_level_serde() { - let level = SafetyLevel::Finalized; - let json = serde_json::to_string(&level).unwrap(); - assert_eq!(json, r#""finalized""#); - - let level: SafetyLevel = serde_json::from_str(&json).unwrap(); - assert_eq!(level, SafetyLevel::Finalized); - } - - #[test] - #[cfg(feature = "serde")] - fn test_serde_safety_level_fails() { - let json = r#""failed""#; - let level: Result = serde_json::from_str(json); - assert!(level.is_err()); - } - - #[test] - fn test_safety_level_from_str_valid() { - assert_eq!(SafetyLevel::from_str("finalized").unwrap(), SafetyLevel::Finalized); - assert_eq!(SafetyLevel::from_str("safe").unwrap(), SafetyLevel::Safe); - assert_eq!(SafetyLevel::from_str("local-safe").unwrap(), SafetyLevel::LocalSafe); - assert_eq!(SafetyLevel::from_str("localsafe").unwrap(), SafetyLevel::LocalSafe); - assert_eq!(SafetyLevel::from_str("cross-unsafe").unwrap(), SafetyLevel::CrossUnsafe); - assert_eq!(SafetyLevel::from_str("crossunsafe").unwrap(), SafetyLevel::CrossUnsafe); - assert_eq!(SafetyLevel::from_str("unsafe").unwrap(), SafetyLevel::Unsafe); - assert_eq!(SafetyLevel::from_str("invalid").unwrap(), SafetyLevel::Invalid); - } - - #[test] - fn test_safety_level_from_str_invalid() { - assert!(SafetyLevel::from_str("unknown").is_err()); - assert!(SafetyLevel::from_str("123").is_err()); - assert!(SafetyLevel::from_str("").is_err()); - assert!(SafetyLevel::from_str("safe ").is_err()); - } -} diff --git a/crates/optimism/primitives/src/supervisor/traits.rs b/crates/optimism/primitives/src/supervisor/traits.rs deleted file mode 100644 index 2a06c1c6a94..00000000000 --- a/crates/optimism/primitives/src/supervisor/traits.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! RPC validator component used in interop. -//! -//! [`InteropTxValidator`] parses inbox entries from [`AccessListItem`]s, and queries a -//! superchain supervisor for their validity via RPC using the [`CheckAccessList`] API. -// Source: https://github.com/op-rs/kona -// Copyright © 2023 kona contributors Copyright © 2024 Optimism -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -// associated documentation files (the “Software”), to deal in the Software without restriction, -// including without limitation the rights to use, copy, modify, merge, publish, distribute, -// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -use crate::supervisor::{ - parse_access_list_items_to_inbox_entries, CheckAccessList, ExecutingDescriptor, - InteropTxValidatorError, SafetyLevel, -}; -use alloc::boxed::Box; -use alloy_eips::eip2930::AccessListItem; -use alloy_primitives::B256; -use core::time::Duration; - -/// Interacts with a Supervisor to validate inbox entries extracted from [`AccessListItem`]s. -#[async_trait::async_trait] -pub trait InteropTxValidator { - /// The supervisor client type. - type SupervisorClient: CheckAccessList + Send + Sync; - - /// Default duration that message validation is not allowed to exceed. - /// - /// Note: this has no effect unless shorter than timeout configured for - /// [`Self::SupervisorClient`] type. - const DEFAULT_TIMEOUT: Duration; - - /// Returns reference to supervisor client. Used in default trait method implementations. - fn supervisor_client(&self) -> &Self::SupervisorClient; - - /// Extracts inbox entries from the [`AccessListItem`]s if there are any. - fn parse_access_list(access_list_items: &[AccessListItem]) -> impl Iterator { - parse_access_list_items_to_inbox_entries(access_list_items.iter()) - } - - /// Validates a list of inbox entries against a Supervisor. - /// - /// Times out RPC requests after given timeout, as long as this timeout is shorter - /// than the underlying request timeout configured for [`Self::SupervisorClient`] type. - async fn validate_messages_with_timeout( - &self, - inbox_entries: &[B256], - safety: SafetyLevel, - executing_descriptor: ExecutingDescriptor, - timeout: Duration, - ) -> Result<(), InteropTxValidatorError> { - // Validate messages against supervisor with timeout. - tokio::time::timeout( - timeout, - self.supervisor_client().check_access_list(inbox_entries, safety, executing_descriptor), - ) - .await - .map_err(|_| InteropTxValidatorError::ValidationTimeout(timeout.as_secs()))? - } - - /// Validates a list of inbox entries against a Supervisor. - /// - /// Times out RPC requests after [`Self::DEFAULT_TIMEOUT`], as long as this timeout is shorter - /// than the underlying request timeout configured for [`Self::SupervisorClient`] type. - async fn validate_messages( - &self, - inbox_entries: &[B256], - safety: SafetyLevel, - executing_descriptor: ExecutingDescriptor, - ) -> Result<(), InteropTxValidatorError> { - self.validate_messages_with_timeout( - inbox_entries, - safety, - executing_descriptor, - Self::DEFAULT_TIMEOUT, - ) - .await - } -} diff --git a/crates/optimism/txpool/Cargo.toml b/crates/optimism/txpool/Cargo.toml index afecb6ef7ec..63f5108fe7c 100644 --- a/crates/optimism/txpool/Cargo.toml +++ b/crates/optimism/txpool/Cargo.toml @@ -17,6 +17,7 @@ alloy-consensus.workspace = true alloy-eips.workspace = true alloy-primitives.workspace = true alloy-rpc-types-eth.workspace = true +alloy-rpc-client = { workspace = true, features = ["reqwest", "default"] } # reth reth-chainspec.workspace = true @@ -33,7 +34,7 @@ op-alloy-consensus.workspace = true op-alloy-flz.workspace = true reth-optimism-evm.workspace = true reth-optimism-forks.workspace = true -reth-optimism-primitives = { workspace = true, default-features = true, features = ["supervisor"] } +reth-optimism-primitives.workspace = true # metrics reth-metrics.workspace = true @@ -44,10 +45,11 @@ c-kzg.workspace = true derive_more.workspace = true futures-util.workspace = true parking_lot.workspace = true +serde.workspace = true tracing.workspace = true thiserror.workspace = true +tokio = { workspace = true, features = ["time"] } [dev-dependencies] reth-optimism-chainspec.workspace = true reth-provider = { workspace = true, features = ["test-utils"] } -tokio.workspace = true diff --git a/crates/optimism/txpool/src/error.rs b/crates/optimism/txpool/src/error.rs index c9ff4843e23..4dc51243210 100644 --- a/crates/optimism/txpool/src/error.rs +++ b/crates/optimism/txpool/src/error.rs @@ -1,6 +1,5 @@ -use reth_optimism_primitives::supervisor::{ - InteropTxValidatorError, InvalidInboxEntry, SafetyLevel, -}; +use crate::supervisor::{InteropTxValidatorError, InvalidInboxEntry}; +use op_alloy_consensus::interop::SafetyLevel; use reth_transaction_pool::error::PoolTransactionError; use std::any::Any; diff --git a/crates/optimism/txpool/src/lib.rs b/crates/optimism/txpool/src/lib.rs index fad0a9e654a..d5de8774fd9 100644 --- a/crates/optimism/txpool/src/lib.rs +++ b/crates/optimism/txpool/src/lib.rs @@ -12,6 +12,7 @@ mod validator; pub use validator::{OpL1BlockInfo, OpTransactionValidator}; pub mod conditional; +pub mod supervisor; mod transaction; pub use transaction::{OpPooledTransaction, OpPooledTx}; mod error; diff --git a/crates/optimism/primitives/src/supervisor/access_list.rs b/crates/optimism/txpool/src/supervisor/access_list.rs similarity index 100% rename from crates/optimism/primitives/src/supervisor/access_list.rs rename to crates/optimism/txpool/src/supervisor/access_list.rs diff --git a/crates/optimism/txpool/src/supervisor/client.rs b/crates/optimism/txpool/src/supervisor/client.rs new file mode 100644 index 00000000000..f3a100ec2f2 --- /dev/null +++ b/crates/optimism/txpool/src/supervisor/client.rs @@ -0,0 +1,107 @@ +//! This is our custom implementation of validator struct + +use crate::supervisor::{ExecutingDescriptor, InteropTxValidatorError}; +use alloy_primitives::B256; +use alloy_rpc_client::ReqwestClient; +use futures_util::future::BoxFuture; +use op_alloy_consensus::interop::SafetyLevel; +use std::{borrow::Cow, future::IntoFuture, time::Duration}; + +/// Supervisor hosted by op-labs +// TODO: This should be changes to actual supervisor url +pub const DEFAULT_SUPERVISOR_URL: &str = "http://localhost:1337/"; + +/// The default request timeout to use +const DEFAULT_REQUEST_TIMOUT: Duration = Duration::from_millis(100); + +/// Implementation of the supervisor trait for the interop. +#[derive(Debug, Clone)] +pub struct SupervisorClient { + client: ReqwestClient, + /// The default + safety: SafetyLevel, + /// The default request timeout + timeout: Duration, +} + +impl SupervisorClient { + /// Creates a new supervisor validator. + pub async fn new(supervisor_endpoint: impl Into, safety: SafetyLevel) -> Self { + let client = ReqwestClient::builder() + .connect(supervisor_endpoint.into().as_str()) + .await + .expect("building supervisor client"); + Self { client, safety, timeout: DEFAULT_REQUEST_TIMOUT } + } + + /// Configures a custom timeout + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + /// Returns safely level + pub fn safety(&self) -> SafetyLevel { + self.safety + } + + /// Executes a `supervisor_checkAccessList` with the configured safety level. + pub fn check_access_list<'a>( + &self, + inbox_entries: &'a [B256], + executing_descriptor: ExecutingDescriptor, + ) -> CheckAccessListRequest<'a> { + CheckAccessListRequest { + client: self.client.clone(), + inbox_entries: Cow::Borrowed(inbox_entries), + executing_descriptor, + timeout: self.timeout, + safety: self.safety, + } + } +} + +/// A Request future that issues a `supervisor_checkAccessList` request. +#[derive(Debug, Clone)] +pub struct CheckAccessListRequest<'a> { + client: ReqwestClient, + inbox_entries: Cow<'a, [B256]>, + executing_descriptor: ExecutingDescriptor, + timeout: Duration, + safety: SafetyLevel, +} + +impl CheckAccessListRequest<'_> { + /// Configures the timeout to use for the request if any. + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + /// Configures the [`SafetyLevel`] for this request + pub fn with_safety(mut self, safety: SafetyLevel) -> Self { + self.safety = safety; + self + } +} + +impl<'a> IntoFuture for CheckAccessListRequest<'a> { + type Output = Result<(), InteropTxValidatorError>; + type IntoFuture = BoxFuture<'a, Self::Output>; + + fn into_future(self) -> Self::IntoFuture { + let Self { client, inbox_entries, executing_descriptor, timeout, safety } = self; + Box::pin(async move { + tokio::time::timeout( + timeout, + client.request( + "supervisor_checkAccessList", + (inbox_entries, safety, executing_descriptor), + ), + ) + .await + .map_err(|_| InteropTxValidatorError::ValidationTimeout(timeout.as_secs()))? + .map_err(InteropTxValidatorError::client) + }) + } +} diff --git a/crates/optimism/primitives/src/supervisor/errors.rs b/crates/optimism/txpool/src/supervisor/errors.rs similarity index 98% rename from crates/optimism/primitives/src/supervisor/errors.rs rename to crates/optimism/txpool/src/supervisor/errors.rs index 30a14a73941..e2c492e3d96 100644 --- a/crates/optimism/primitives/src/supervisor/errors.rs +++ b/crates/optimism/txpool/src/supervisor/errors.rs @@ -16,10 +16,8 @@ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -use crate::supervisor::SafetyLevel; -use alloc::boxed::Box; use core::error; -use thiserror::Error; +use op_alloy_consensus::interop::SafetyLevel; /// Derived from op-supervisor // todo: rm once resolved @@ -29,7 +27,7 @@ const UNKNOWN_CHAIN_MSG: &str = "unknown chain: "; const MINIMUM_SAFETY_MSG: &str = "does not meet the minimum safety"; /// Invalid inbox entry -#[derive(Error, Debug)] +#[derive(thiserror::Error, Debug)] pub enum InvalidInboxEntry { /// Message does not meet minimum safety level #[error("message does not meet min safety level, got: {got}, expected: {expected}")] diff --git a/crates/optimism/primitives/src/supervisor/message.rs b/crates/optimism/txpool/src/supervisor/message.rs similarity index 100% rename from crates/optimism/primitives/src/supervisor/message.rs rename to crates/optimism/txpool/src/supervisor/message.rs diff --git a/crates/optimism/txpool/src/supervisor/mod.rs b/crates/optimism/txpool/src/supervisor/mod.rs new file mode 100644 index 00000000000..28e9f83749c --- /dev/null +++ b/crates/optimism/txpool/src/supervisor/mod.rs @@ -0,0 +1,11 @@ +//! Supervisor support for interop +mod access_list; +pub use access_list::parse_access_list_items_to_inbox_entries; +pub use op_alloy_consensus::interop::*; + +mod client; +pub use client::{SupervisorClient, DEFAULT_SUPERVISOR_URL}; +mod errors; +pub use errors::{InteropTxValidatorError, InvalidInboxEntry}; +mod message; +pub use message::ExecutingDescriptor; diff --git a/crates/optimism/txpool/src/validator.rs b/crates/optimism/txpool/src/validator.rs index 3459c0ad5d3..031f8f5d1ee 100644 --- a/crates/optimism/txpool/src/validator.rs +++ b/crates/optimism/txpool/src/validator.rs @@ -1,4 +1,7 @@ -use crate::InvalidCrossTx; +use crate::{ + supervisor::{parse_access_list_items_to_inbox_entries, ExecutingDescriptor, SupervisorClient}, + InvalidCrossTx, +}; use alloy_consensus::{BlockHeader, Transaction}; use alloy_eips::Encodable2718; use op_revm::L1BlockInfo; @@ -6,9 +9,6 @@ use parking_lot::RwLock; use reth_chainspec::ChainSpecProvider; use reth_optimism_evm::RethL1BlockInfo; use reth_optimism_forks::OpHardforks; -use reth_optimism_primitives::supervisor::{ - ExecutingDescriptor, InteropTxValidator, SupervisorClient, -}; use reth_primitives_traits::{ transaction::error::InvalidTransactionError, Block, BlockBody, GotExpected, SealedBlock, }; @@ -24,7 +24,7 @@ use std::sync::{ use tracing::trace; /// The interval for which we check transaction against supervisor, 1 day. -const TRANSACTION_VALIDITY_WINDOW: u64 = 86400; +const TRANSACTION_VALIDITY_WINDOW_SECS: u64 = 86400; /// Tracks additional infos for the current block. #[derive(Debug, Default)] @@ -37,6 +37,13 @@ pub struct OpL1BlockInfo { number: AtomicU64, } +impl OpL1BlockInfo { + /// Returns the most recent timestamp + pub fn timestamp(&self) -> u64 { + self.timestamp.load(Ordering::Relaxed) + } +} + /// Validator for Optimism transactions. #[derive(Debug, Clone)] pub struct OpTransactionValidator { @@ -175,16 +182,13 @@ where // Interop cross tx validation if let Some(Err(err)) = self.is_valid_cross_tx(&transaction).await { - if matches!(err, InvalidCrossTx::CrossChainTxPreInterop) { - return TransactionValidationOutcome::Invalid( - transaction, - InvalidTransactionError::TxTypeNotSupported.into(), - ); - } - return TransactionValidationOutcome::Invalid( - transaction, - InvalidPoolTransactionError::Other(Box::new(err)), - ); + let err = match err { + InvalidCrossTx::CrossChainTxPreInterop => { + InvalidTransactionError::TxTypeNotSupported.into() + } + err => InvalidPoolTransactionError::Other(Box::new(err)), + }; + return TransactionValidationOutcome::Invalid(transaction, err) } let outcome = self.inner.validate_one(origin, transaction); @@ -278,27 +282,31 @@ where // We don't need to check for deposit transaction in here, because they won't come from // txpool let access_list = tx.access_list()?; - let inbox_entries = - SupervisorClient::parse_access_list(access_list).copied().collect::>(); + let inbox_entries = parse_access_list_items_to_inbox_entries(access_list.iter()) + .copied() + .collect::>(); if inbox_entries.is_empty() { return None; } - let timestamp = self.block_info.timestamp.load(Ordering::Relaxed); - // Interop check + + // Ensure interop is activated if !self.fork_tracker.is_interop_activated() { // No cross chain tx allowed before interop return Some(Err(InvalidCrossTx::CrossChainTxPreInterop)) } + let client = self .supervisor_client .as_ref() .expect("supervisor client should be always set after interop is active"); - let safety_level = client.safety(); + if let Err(err) = client - .validate_messages( + .check_access_list( inbox_entries.as_slice(), - safety_level, - ExecutingDescriptor::new(timestamp, Some(TRANSACTION_VALIDITY_WINDOW)), + ExecutingDescriptor::new( + self.block_info.timestamp(), + Some(TRANSACTION_VALIDITY_WINDOW_SECS), + ), ) .await {