diff --git a/Cargo.lock b/Cargo.lock index 803a89b..744f1ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6290,6 +6290,7 @@ dependencies = [ "alloy-consensus", "alloy-contract", "alloy-eips", + "alloy-evm 0.15.0", "alloy-json-rpc", "alloy-network", "alloy-op-evm 0.15.0", @@ -6320,6 +6321,7 @@ dependencies = [ "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)", + "k256", "macros", "metrics", "moka", diff --git a/Cargo.toml b/Cargo.toml index 1c1d32d..059d961 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ revm = { version = "27.0.3", features = [ "optional_balance_check", ], default-features = false } revm-inspectors = { version = "0.27", default-features = false } -op-revm = { version = "8.0.2", default-features = false } +op-revm = { version = "8.0.2", features = ["serde"], default-features = false } ethereum_ssz_derive = "0.9.0" ethereum_ssz = "0.9.0" diff --git a/README.md b/README.md index df30098..35b1837 100644 --- a/README.md +++ b/README.md @@ -44,23 +44,31 @@ cargo run -p op-rbuilder --bin op-rbuilder -- node \ ### Flashtestations +Flashtestations is a feature that enables Trusted Execution Environment (TEE) attestation for block building. It provides cryptographic proof that blocks were built within a secure enclave, ensuring the integrity and confidentiality of the block building process. + +#### Usage + To run op-rbuilder with flashtestations: ```bash -cargo run -p op-rbuilder --bin op-rbuilder --features=flashtestations -- node \ +cargo run -p op-rbuilder --bin op-rbuilder -- node \ --chain /path/to/chain-config.json \ --http \ --authrpc.port 9551 \ --authrpc.jwtsecret /path/to/jwt.hex \ --flashtestations.enabled \ - --flashtestations.rpc-url your-rpc-url \ # rpc to submit the attestation transaction to --flashtestations.funding-amount 0.01 \ # amount in ETH to fund the TEE generated key --flashtestations.funding-key secret-key \ # funding key for the TEE key --flashtestations.registry-address 0xFlashtestationsRegistryAddress \ - flashtestations.builder-policy-address 0xBuilderPolicyAddress + --flashtestations.builder-policy-address 0xBuilderPolicyAddress ``` -Note that `--rollup.builder-secret-key` must be set and funded in order for the flashtestations key to be funded and submit the attestation on-chain. +#### Additional CLI Config + +- `--flashtestations.enable-block-proofs`: Enable end-of-block transaction proofs that verify the block was built within a TEE +- `--flashtestations.debug`: Enable debug mode with a deterministic TEE key and debug attestation server for testing and development +- `--flashtestations.quote-provider `: Specify a remote URL to provide an attestation instead of generating a quote in process +- `--flashtestations.rpc-url `: Use a remote rpc provider to submit attestations to ## Observability @@ -189,4 +197,4 @@ More instructions on installing and configuring `act` can be found on [their web ### Known issues - Running actions locally require a Github Token. You can generate one by following instructions on [Github Docs](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). After generating a token you will need to pass it to `act` either through the command line using `-s GITHUB_TOKEN=` or by adding it to the `~/.config/act/actrc` file. -- You might get an error about missing or incompatible `warp-ubuntu-latest-x64-32x` platform. This can be mitigated by adding `-P warp-ubuntu-latest-x64-32x=ghcr.io/catthehacker/ubuntu:act-latest` on the command line when calling `act` or appending this flag to `~/.config/act/actrc` +- You might get an error about missing or incompatible `warp-ubuntu-latest-x64-32x` platform. This can be mitigated by adding `-P warp-ubuntu-latest-x64-32x=ghcr.io/catthehacker/ubuntu:act-latest` on the command line when calling `act` or appending this flag to `~/.config/act/actrc` \ No newline at end of file diff --git a/crates/op-rbuilder/Cargo.toml b/crates/op-rbuilder/Cargo.toml index a2bf891..9581962 100644 --- a/crates/op-rbuilder/Cargo.toml +++ b/crates/op-rbuilder/Cargo.toml @@ -59,6 +59,7 @@ alloy-primitives.workspace = true alloy-consensus.workspace = true alloy-contract.workspace = true alloy-eips.workspace = true +alloy-evm.workspace = true alloy-rpc-types-beacon.workspace = true alloy-rpc-types-engine.workspace = true alloy-transport-http.workspace = true @@ -121,6 +122,7 @@ http = "1.0" sha3 = "0.10" hex = "0.4" ureq = "2.10" +k256 = "0.13.4" rollup-boost = { git = "http://github.com/flashbots/rollup-boost", branch = "main" } diff --git a/crates/op-rbuilder/src/builders/builder_tx.rs b/crates/op-rbuilder/src/builders/builder_tx.rs index 3dcb898..a26af66 100644 --- a/crates/op-rbuilder/src/builders/builder_tx.rs +++ b/crates/op-rbuilder/src/builders/builder_tx.rs @@ -1,32 +1,169 @@ +use alloy_evm::Database; +use alloy_primitives::{ + map::foldhash::{HashSet, HashSetExt}, + Address, +}; +use core::fmt::Debug; +use op_revm::OpTransactionError; +use reth_evm::{eth::receipt_builder::ReceiptBuilderCtx, ConfigureEvm, Evm}; +use reth_node_api::PayloadBuilderError; use reth_optimism_primitives::OpTransactionSigned; use reth_primitives::Recovered; +use reth_provider::{ProviderError, StateProvider}; +use reth_revm::{ + database::StateProviderDatabase, db::states::bundle_state::BundleRetention, State, +}; +use revm::{ + context::result::{EVMError, ResultAndState}, + DatabaseCommit, +}; +use tracing::{debug, warn}; -use crate::tx_signer::Signer; +use crate::{builders::context::OpPayloadBuilderCtx, primitives::reth::ExecutionInfo}; -pub trait BuilderTx { - fn estimated_builder_tx_gas(&self) -> u64; - fn estimated_builder_tx_da_size(&self) -> Option; - fn signed_builder_tx(&self) -> Result, secp256k1::Error>; +#[derive(Debug, Clone)] +pub struct BuilderTransactionCtx { + pub gas_used: u64, + pub da_size: u64, + pub signed_tx: Recovered, } -// Scaffolding for how to construct the end of block builder transaction -// This will be the regular end of block transaction without the TEE key -#[derive(Clone)] -pub struct StandardBuilderTx { - #[allow(dead_code)] - pub signer: Option, +/// Possible error variants during construction of builder txs. +#[derive(Debug, thiserror::Error)] +pub enum BuilderTransactionError { + /// Thrown when builder account load fails to get builder nonce + #[error("failed to load account {0}")] + AccountLoadFailed(Address), + /// Thrown when signature signing fails + #[error("failed to sign transaction: {0}")] + SigningError(secp256k1::Error), + /// Unrecoverable error during evm execution. + #[error("evm execution error {0}")] + EvmExecutionError(Box), + /// Any other builder transaction errors. + #[error(transparent)] + Other(Box), } -impl BuilderTx for StandardBuilderTx { - fn estimated_builder_tx_gas(&self) -> u64 { - todo!() +impl From for BuilderTransactionError { + fn from(error: secp256k1::Error) -> Self { + BuilderTransactionError::SigningError(error) } +} + +impl From> for BuilderTransactionError { + fn from(error: EVMError) -> Self { + BuilderTransactionError::EvmExecutionError(Box::new(error)) + } +} - fn estimated_builder_tx_da_size(&self) -> Option { - todo!() +impl From for PayloadBuilderError { + fn from(error: BuilderTransactionError) -> Self { + match error { + BuilderTransactionError::EvmExecutionError(e) => { + PayloadBuilderError::EvmExecutionError(e) + } + _ => PayloadBuilderError::Other(Box::new(error)), + } } +} + +pub trait BuilderTransactions: Debug { + fn simulate_builder_txs( + &self, + state_provider: impl StateProvider + Clone, + info: &mut ExecutionInfo, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + ) -> Result, BuilderTransactionError>; + + fn add_builder_txs( + &self, + state_provider: impl StateProvider + Clone, + info: &mut ExecutionInfo, + builder_ctx: &OpPayloadBuilderCtx, + db: &mut State, + ) -> Result, BuilderTransactionError> { + { + let mut evm = builder_ctx + .evm_config + .evm_with_env(&mut *db, builder_ctx.evm_env.clone()); + + let mut invalid: HashSet
= HashSet::new(); + + let builder_txs = + self.simulate_builder_txs(state_provider, info, builder_ctx, evm.db_mut())?; + for builder_tx in builder_txs.iter() { + if invalid.contains(&builder_tx.signed_tx.signer()) { + debug!(target: "payload_builder", tx_hash = ?builder_tx.signed_tx.tx_hash(), "builder signer invalid as previous builder tx reverted"); + continue; + } + + let ResultAndState { result, state } = evm + .transact(&builder_tx.signed_tx) + .map_err(|err| BuilderTransactionError::EvmExecutionError(Box::new(err)))?; + + if !result.is_success() { + warn!(target: "payload_builder", tx_hash = ?builder_tx.signed_tx.tx_hash(), "builder tx reverted"); + invalid.insert(builder_tx.signed_tx.signer()); + continue; + } + + // Add gas used by the transaction to cumulative gas used, before creating the receipt + let gas_used = result.gas_used(); + info.cumulative_gas_used += gas_used; + + let ctx = ReceiptBuilderCtx { + tx: builder_tx.signed_tx.inner(), + evm: &evm, + result, + state: &state, + cumulative_gas_used: info.cumulative_gas_used, + }; + info.receipts.push(builder_ctx.build_receipt(ctx, None)); + + // Commit changes + evm.db_mut().commit(state); + + // Append sender and transaction to the respective lists + info.executed_senders.push(builder_tx.signed_tx.signer()); + info.executed_transactions + .push(builder_tx.signed_tx.clone().into_inner()); + } + + // Release the db reference by dropping evm + drop(evm); + + Ok(builder_txs) + } + } + + fn simulate_builder_txs_state( + &self, + state_provider: impl StateProvider + Clone, + builder_txs: Vec<&BuilderTransactionCtx>, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + ) -> Result>, BuilderTransactionError> { + let state = StateProviderDatabase::new(state_provider.clone()); + let mut simulation_state = State::builder() + .with_database(state) + .with_bundle_prestate(db.bundle_state.clone()) + .with_bundle_update() + .build(); + let mut evm = ctx + .evm_config + .evm_with_env(&mut simulation_state, ctx.evm_env.clone()); + + for builder_tx in builder_txs { + let ResultAndState { state, .. } = evm + .transact(&builder_tx.signed_tx) + .map_err(|err| BuilderTransactionError::EvmExecutionError(Box::new(err)))?; + + evm.db_mut().commit(state); + evm.db_mut().merge_transitions(BundleRetention::Reverts); + } - fn signed_builder_tx(&self) -> Result, secp256k1::Error> { - todo!() + Ok(simulation_state) } } diff --git a/crates/op-rbuilder/src/builders/context.rs b/crates/op-rbuilder/src/builders/context.rs index e440877..fe8d539 100644 --- a/crates/op-rbuilder/src/builders/context.rs +++ b/crates/op-rbuilder/src/builders/context.rs @@ -1,12 +1,11 @@ -use alloy_consensus::{ - conditional::BlockConditionalAttributes, Eip658Value, Transaction, TxEip1559, -}; -use alloy_eips::{eip7623::TOTAL_COST_FLOOR_PER_TOKEN, Encodable2718, Typed2718}; +use alloy_consensus::{conditional::BlockConditionalAttributes, Eip658Value, Transaction}; +use alloy_eips::Typed2718; +use alloy_evm::Database; use alloy_op_evm::block::receipt_builder::OpReceiptBuilder; -use alloy_primitives::{Address, Bytes, TxKind, U256}; +use alloy_primitives::{BlockHash, Bytes, U256}; use alloy_rpc_types_eth::Withdrawals; use core::fmt::Debug; -use op_alloy_consensus::{OpDepositReceipt, OpTypedTransaction}; +use op_alloy_consensus::OpDepositReceipt; use op_revm::OpSpecId; use reth::payload::PayloadBuilderAttributes; use reth_basic_payload_builder::PayloadConfig; @@ -27,17 +26,14 @@ use reth_optimism_txpool::{ interop::{is_valid_interop, MaybeInteropTransaction}, }; use reth_payload_builder::PayloadId; -use reth_primitives::{Recovered, SealedHeader}; +use reth_primitives::SealedHeader; use reth_primitives_traits::{InMemorySize, SignedTransaction}; -use reth_provider::ProviderError; use reth_revm::{context::Block, State}; use reth_transaction_pool::{BestTransactionsAttributes, PoolTransaction}; -use revm::{ - context::result::ResultAndState, interpreter::as_u64_saturated, Database, DatabaseCommit, -}; +use revm::{context::result::ResultAndState, interpreter::as_u64_saturated, DatabaseCommit}; use std::{sync::Arc, time::Instant}; use tokio_util::sync::CancellationToken; -use tracing::{debug, info, trace, warn}; +use tracing::{debug, info, trace}; use crate::{ metrics::OpRBuilderMetrics, @@ -191,6 +187,16 @@ impl OpPayloadBuilderCtx { self.chain_spec.chain_id() } + /// Returns the parent hash + pub fn parent_hash(&self) -> BlockHash { + self.parent().hash() + } + + /// Returns the timestamp + pub fn timestamp(&self) -> u64 { + self.attributes().timestamp() + } + /// Returns the builder signer pub fn builder_signer(&self) -> Option { self.builder_signer @@ -199,7 +205,7 @@ impl OpPayloadBuilderCtx { impl OpPayloadBuilderCtx { /// Constructs a receipt for the given transaction. - fn build_receipt( + pub fn build_receipt( &self, ctx: ReceiptBuilderCtx<'_, OpTransactionSigned, E>, deposit_nonce: Option, @@ -231,13 +237,10 @@ impl OpPayloadBuilderCtx { } /// Executes all sequencer transactions that are included in the payload attributes. - pub fn execute_sequencer_transactions( + pub fn execute_sequencer_transactions( &self, - db: &mut State, - ) -> Result, PayloadBuilderError> - where - DB: Database + std::fmt::Debug, - { + db: &mut State, + ) -> Result, PayloadBuilderError> { let mut info = ExecutionInfo::with_capacity(self.attributes().transactions.len()); let mut evm = self.evm_config.evm_with_env(&mut *db, self.evm_env.clone()); @@ -318,17 +321,14 @@ impl OpPayloadBuilderCtx { /// Executes the given best transactions and updates the execution info. /// /// Returns `Ok(Some(())` if the job was cancelled. - pub fn execute_best_transactions( + pub fn execute_best_transactions( &self, info: &mut ExecutionInfo, - db: &mut State, + db: &mut State, mut best_txs: impl PayloadTxsBounds, block_gas_limit: u64, block_da_limit: Option, - ) -> Result, PayloadBuilderError> - where - DB: Database + std::fmt::Debug, - { + ) -> Result, PayloadBuilderError> { let execute_txs_start_time = Instant::now(); let mut num_txs_considered = 0; let mut num_txs_simulated = 0; @@ -539,147 +539,4 @@ impl OpPayloadBuilderCtx { ); Ok(None) } - - pub fn add_builder_tx( - &self, - info: &mut ExecutionInfo, - db: &mut State, - builder_tx_gas: u64, - message: Vec, - ) -> Option<()> - where - DB: Database + std::fmt::Debug, - { - self.builder_signer() - .map(|signer| { - let base_fee = self.base_fee(); - let chain_id = self.chain_id(); - // Create and sign the transaction - let builder_tx = - signed_builder_tx(db, builder_tx_gas, message, signer, base_fee, chain_id)?; - - let mut evm = self.evm_config.evm_with_env(&mut *db, self.evm_env.clone()); - - let ResultAndState { result, state } = evm - .transact(&builder_tx) - .map_err(|err| PayloadBuilderError::EvmExecutionError(Box::new(err)))?; - - // Add gas used by the transaction to cumulative gas used, before creating the receipt - let gas_used = result.gas_used(); - info.cumulative_gas_used += gas_used; - - let ctx = ReceiptBuilderCtx { - tx: builder_tx.inner(), - evm: &evm, - result, - state: &state, - cumulative_gas_used: info.cumulative_gas_used, - }; - info.receipts.push(self.build_receipt(ctx, None)); - - // Release the db reference by dropping evm - drop(evm); - // Commit changes - db.commit(state); - - // Append sender and transaction to the respective lists - info.executed_senders.push(builder_tx.signer()); - info.executed_transactions.push(builder_tx.into_inner()); - Ok(()) - }) - .transpose() - .unwrap_or_else(|err: PayloadBuilderError| { - warn!(target: "payload_builder", %err, "Failed to add builder transaction"); - None - }) - } - - /// Calculates EIP 2718 builder transaction size - // TODO: this function could be improved, ideally we shouldn't take mut ref to db and maybe - // it's possible to do this without db at all - pub fn estimate_builder_tx_da_size( - &self, - db: &mut State, - builder_tx_gas: u64, - message: Vec, - ) -> Option - where - DB: Database, - { - self.builder_signer() - .map(|signer| { - let base_fee = self.base_fee(); - let chain_id = self.chain_id(); - // Create and sign the transaction - let builder_tx = - signed_builder_tx(db, builder_tx_gas, message, signer, base_fee, chain_id)?; - Ok(op_alloy_flz::tx_estimated_size_fjord_bytes( - builder_tx.encoded_2718().as_slice(), - )) - }) - .transpose() - .unwrap_or_else(|err: PayloadBuilderError| { - warn!(target: "payload_builder", %err, "Failed to add builder transaction"); - None - }) - } -} - -pub fn estimate_gas_for_builder_tx(input: Vec) -> u64 { - // Count zero and non-zero bytes - let (zero_bytes, nonzero_bytes) = input.iter().fold((0, 0), |(zeros, nonzeros), &byte| { - if byte == 0 { - (zeros + 1, nonzeros) - } else { - (zeros, nonzeros + 1) - } - }); - - // Calculate gas cost (4 gas per zero byte, 16 gas per non-zero byte) - let zero_cost = zero_bytes * 4; - let nonzero_cost = nonzero_bytes * 16; - - // Tx gas should be not less than floor gas https://eips.ethereum.org/EIPS/eip-7623 - let tokens_in_calldata = zero_bytes + nonzero_bytes * 4; - let floor_gas = 21_000 + tokens_in_calldata * TOTAL_COST_FLOOR_PER_TOKEN; - - std::cmp::max(zero_cost + nonzero_cost + 21_000, floor_gas) -} - -/// Creates signed builder tx to Address::ZERO and specified message as input -pub fn signed_builder_tx( - db: &mut State, - builder_tx_gas: u64, - message: Vec, - signer: Signer, - base_fee: u64, - chain_id: u64, -) -> Result, PayloadBuilderError> -where - DB: Database, -{ - // Create message with block number for the builder to sign - let nonce = db - .load_cache_account(signer.address) - .map(|acc| acc.account_info().unwrap_or_default().nonce) - .map_err(|_| { - PayloadBuilderError::other(OpPayloadBuilderError::AccountLoadFailed(signer.address)) - })?; - - // Create the EIP-1559 transaction - let tx = OpTypedTransaction::Eip1559(TxEip1559 { - chain_id, - nonce, - gas_limit: builder_tx_gas, - max_fee_per_gas: base_fee.into(), - max_priority_fee_per_gas: 0, - to: TxKind::Call(Address::ZERO), - // Include the message as part of the transaction data - input: message.into(), - ..Default::default() - }); - // Sign the transaction - let builder_tx = signer.sign_tx(tx).map_err(PayloadBuilderError::other)?; - - Ok(builder_tx) } diff --git a/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs new file mode 100644 index 0000000..f095390 --- /dev/null +++ b/crates/op-rbuilder/src/builders/flashblocks/builder_tx.rs @@ -0,0 +1,155 @@ +use alloy_consensus::TxEip1559; +use alloy_eips::{eip7623::TOTAL_COST_FLOOR_PER_TOKEN, Encodable2718}; +use alloy_evm::Database; +use alloy_primitives::{Address, TxKind}; +use core::fmt::Debug; +use op_alloy_consensus::OpTypedTransaction; +use reth_optimism_primitives::OpTransactionSigned; +use reth_primitives::Recovered; +use reth_provider::StateProvider; +use reth_revm::State; + +use crate::{ + builders::{ + context::OpPayloadBuilderCtx, flashblocks::payload::FlashblocksExtraCtx, + BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, + }, + flashtestations::builder_tx::FlashtestationsBuilderTx, + primitives::reth::ExecutionInfo, + tx_signer::Signer, +}; + +// This will be the end of block transaction of a regular block +#[derive(Debug, Clone)] +pub struct FlashblocksBuilderTx { + pub signer: Option, + pub flashtestations_builder_tx: Option, +} + +impl FlashblocksBuilderTx { + pub fn new( + signer: Option, + flashtestations_builder_tx: Option, + ) -> Self { + Self { + signer, + flashtestations_builder_tx, + } + } + + pub fn simulate_builder_tx( + &self, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + ) -> Result, BuilderTransactionError> { + match self.signer { + Some(signer) => { + let message: Vec = format!("Block Number: {}", ctx.block_number()).into_bytes(); + let gas_used = self.estimate_builder_tx_gas(&message); + let signed_tx = self.signed_builder_tx(ctx, db, signer, gas_used, message)?; + let da_size = op_alloy_flz::tx_estimated_size_fjord_bytes( + signed_tx.encoded_2718().as_slice(), + ); + Ok(Some(BuilderTransactionCtx { + gas_used, + da_size, + signed_tx, + })) + } + None => Ok(None), + } + } + + fn estimate_builder_tx_gas(&self, input: &[u8]) -> u64 { + // Count zero and non-zero bytes + let (zero_bytes, nonzero_bytes) = input.iter().fold((0, 0), |(zeros, nonzeros), &byte| { + if byte == 0 { + (zeros + 1, nonzeros) + } else { + (zeros, nonzeros + 1) + } + }); + + // Calculate gas cost (4 gas per zero byte, 16 gas per non-zero byte) + let zero_cost = zero_bytes * 4; + let nonzero_cost = nonzero_bytes * 16; + + // Tx gas should be not less than floor gas https://eips.ethereum.org/EIPS/eip-7623 + let tokens_in_calldata = zero_bytes + nonzero_bytes * 4; + let floor_gas = 21_000 + tokens_in_calldata * TOTAL_COST_FLOOR_PER_TOKEN; + + std::cmp::max(zero_cost + nonzero_cost + 21_000, floor_gas) + } + + fn signed_builder_tx( + &self, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + signer: Signer, + gas_used: u64, + message: Vec, + ) -> Result, BuilderTransactionError> { + let nonce = db + .load_cache_account(signer.address) + .map(|acc| acc.account_info().unwrap_or_default().nonce) + .map_err(|_| BuilderTransactionError::AccountLoadFailed(signer.address))?; + + // Create the EIP-1559 transaction + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id: ctx.chain_id(), + nonce, + gas_limit: gas_used, + max_fee_per_gas: ctx.base_fee().into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(Address::ZERO), + // Include the message as part of the transaction data + input: message.into(), + ..Default::default() + }); + // Sign the transaction + let builder_tx = signer + .sign_tx(tx) + .map_err(BuilderTransactionError::SigningError)?; + + Ok(builder_tx) + } +} + +impl BuilderTransactions for FlashblocksBuilderTx { + fn simulate_builder_txs( + &self, + state_provider: impl StateProvider + Clone, + info: &mut ExecutionInfo, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + ) -> Result, BuilderTransactionError> { + let mut builder_txs = Vec::::new(); + if ctx.is_first_fallback_block() { + let flashblocks_builder_tx = self.simulate_builder_tx(ctx, db)?; + builder_txs.extend(flashblocks_builder_tx.clone()); + } + + if ctx.is_last_flashblock() { + let flashblocks_builder_tx = self.simulate_builder_tx(ctx, db)?; + builder_txs.extend(flashblocks_builder_tx.clone()); + if let Some(flashtestations_builder_tx) = &self.flashtestations_builder_tx { + // We only include flashtestations txs in the last flashblock + + let mut simulation_state = self.simulate_builder_txs_state::( + state_provider.clone(), + flashblocks_builder_tx.iter().collect(), + ctx, + db, + )?; + let flashtestations_builder_txs = flashtestations_builder_tx.simulate_builder_txs( + state_provider, + info, + ctx, + &mut simulation_state, + )?; + builder_txs.extend(flashtestations_builder_txs); + } + } + Ok(builder_txs) + } +} diff --git a/crates/op-rbuilder/src/builders/flashblocks/mod.rs b/crates/op-rbuilder/src/builders/flashblocks/mod.rs index d85259a..7470be4 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/mod.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/mod.rs @@ -3,8 +3,8 @@ use crate::traits::{NodeBounds, PoolBounds}; use config::FlashblocksConfig; use service::FlashblocksServiceBuilder; +mod builder_tx; mod config; -//mod context; mod payload; mod service; mod wspub; diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index 1403b76..8ba2c20 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -1,10 +1,11 @@ use super::{config::FlashblocksConfig, wspub::WebSocketPublisher}; use crate::{ builders::{ - context::{estimate_gas_for_builder_tx, OpPayloadBuilderCtx}, + builder_tx::BuilderTransactions, + context::OpPayloadBuilderCtx, flashblocks::config::FlashBlocksConfigExt, generator::{BlockCell, BuildArguments}, - BuilderConfig, BuilderTx, + BuilderConfig, }, metrics::OpRBuilderMetrics, primitives::reth::ExecutionInfo, @@ -59,11 +60,13 @@ struct ExtraExecutionInfo { } #[derive(Debug, Default)] -struct FlashblocksExtraCtx { +pub struct FlashblocksExtraCtx { /// Current flashblock index pub flashblock_index: u64, /// Target flashblock count pub target_flashblock_count: u64, + /// Whether the current flashblock is the first fallback block + pub is_first_fallback_block: bool, } impl OpPayloadBuilderCtx { @@ -77,6 +80,11 @@ impl OpPayloadBuilderCtx { self.extra_ctx.target_flashblock_count } + /// Returns if the flashblock is the first fallback block + pub fn is_first_fallback_block(&self) -> bool { + self.extra_ctx.is_first_fallback_block + } + /// Increments the flashblock index pub fn increment_flashblock_index(&mut self) -> u64 { self.extra_ctx.flashblock_index += 1; @@ -89,6 +97,11 @@ impl OpPayloadBuilderCtx { self.extra_ctx.target_flashblock_count } + /// Sets the first fallback block flag + pub fn set_first_fallback_block(&mut self, is_first_fallback_block: bool) { + self.extra_ctx.is_first_fallback_block = is_first_fallback_block; + } + /// Returns if the flashblock is the last one pub fn is_last_flashblock(&self) -> bool { self.flashblock_index() == self.target_flashblock_count() - 1 @@ -97,7 +110,7 @@ impl OpPayloadBuilderCtx { /// Optimism's payload builder #[derive(Debug, Clone)] -pub struct OpPayloadBuilder { +pub struct OpPayloadBuilder { /// The type responsible for creating the evm. pub evm_config: OpEvmConfig, /// The transaction pool @@ -112,18 +125,17 @@ pub struct OpPayloadBuilder { /// The metrics for the builder pub metrics: Arc, /// The end of builder transaction type - #[allow(dead_code)] - pub builder_tx: BT, + pub builder_tx: BuilderTx, } -impl OpPayloadBuilder { +impl OpPayloadBuilder { /// `OpPayloadBuilder` constructor. pub fn new( evm_config: OpEvmConfig, pool: Pool, client: Client, config: BuilderConfig, - builder_tx: BT, + builder_tx: BuilderTx, ) -> eyre::Result { let metrics = Arc::new(OpRBuilderMetrics::default()); let ws_pub = WebSocketPublisher::new(config.specific.ws_addr, Arc::clone(&metrics))?.into(); @@ -139,12 +151,12 @@ impl OpPayloadBuilder { } } -impl reth_basic_payload_builder::PayloadBuilder - for OpPayloadBuilder +impl reth_basic_payload_builder::PayloadBuilder + for OpPayloadBuilder where Pool: Clone + Send + Sync, Client: Clone + Send + Sync, - BT: Clone + Send + Sync, + BuilderTx: Clone + Send + Sync, { type Attributes = OpPayloadBuilderAttributes; type BuiltPayload = OpBuiltPayload; @@ -167,11 +179,11 @@ where } } -impl OpPayloadBuilder +impl OpPayloadBuilder where Pool: PoolBounds, Client: ClientBounds, - BT: BuilderTx, + BuilderTx: BuilderTransactions, { /// Constructs an Optimism payload from the transactions sent via the /// Payload attributes by the sequencer. If the `no_tx_pool` argument is passed in @@ -250,6 +262,7 @@ where extra_ctx: FlashblocksExtraCtx { flashblock_index: 0, target_flashblock_count: self.config.flashblocks_per_block(), + is_first_fallback_block: true, }, }; @@ -263,24 +276,19 @@ where .with_bundle_update() .build(); - // We subtract gas limit and da limit for builder transaction from the whole limit - let message = format!("Block Number: {}", ctx.block_number()).into_bytes(); - let builder_tx_gas = ctx - .builder_signer() - .map_or(0, |_| estimate_gas_for_builder_tx(message.clone())); - let builder_tx_da_size = ctx - .estimate_builder_tx_da_size(&mut db, builder_tx_gas, message.clone()) - .unwrap_or(0); - let mut info = execute_pre_steps(&mut db, &ctx)?; let sequencer_tx_time = sequencer_tx_start_time.elapsed(); ctx.metrics.sequencer_tx_duration.record(sequencer_tx_time); ctx.metrics.sequencer_tx_gauge.set(sequencer_tx_time); // If we have payload with txpool we add first builder tx right after deposits - if !ctx.attributes().no_tx_pool { - ctx.add_builder_tx(&mut info, &mut db, builder_tx_gas, message.clone()); - } + let builder_txs = + self.builder_tx + .add_builder_txs(&state_provider, &mut info, &ctx, &mut db)?; + + // We subtract gas limit and da limit for builder transaction from the whole limit + let builder_tx_gas = builder_txs.iter().fold(0, |acc, tx| acc + tx.gas_used); + let builder_tx_da_size: u64 = builder_txs.iter().fold(0, |acc, tx| acc + tx.da_size); let (payload, fb_payload, mut bundle_state) = build_block(db, &ctx, &mut info)?; @@ -288,6 +296,8 @@ where self.ws_pub .publish(&fb_payload) .map_err(PayloadBuilderError::other)?; + // We set the first fallback block flag to false after building the first fallback block + ctx.set_first_fallback_block(false); info!( target: "payload_builder", @@ -348,7 +358,9 @@ where if let Some(da_limit) = da_per_batch { // We error if we can't insert any tx aside from builder tx in flashblock if da_limit / 2 < builder_tx_da_size { - error!("Builder tx da size subtraction caused max_da_block_size to be 0. No transaction would be included."); + error!( + "Builder tx da size subtraction caused max_da_block_size to be 0. No transaction would be included." + ); } } let mut total_da_per_batch = da_per_batch; @@ -415,20 +427,28 @@ where ); let flashblock_build_start_time = Instant::now(); let state = StateProviderDatabase::new(&state_provider); - // If it is the last flashblock, we need to account for the builder tx - if ctx.is_last_flashblock() { - total_gas_per_batch = total_gas_per_batch.saturating_sub(builder_tx_gas); - // saturating sub just in case, we will log an error if da_limit too small for builder_tx_da_size - if let Some(da_limit) = total_da_per_batch.as_mut() { - *da_limit = da_limit.saturating_sub(builder_tx_da_size); - } - } let mut db = State::builder() .with_database(state) .with_bundle_update() - .with_bundle_prestate(bundle_state) + .with_bundle_prestate(bundle_state.clone()) .build(); + let builder_txs = self.builder_tx.simulate_builder_txs( + &state_provider, + &mut info, + &ctx, + &mut db, + )?; + let builder_tx_gas = builder_txs.iter().fold(0, |acc, tx| acc + tx.gas_used); + let builder_tx_da_size: u64 = + builder_txs.iter().fold(0, |acc, tx| acc + tx.da_size); + + total_gas_per_batch = total_gas_per_batch.saturating_sub(builder_tx_gas); + // saturating sub just in case, we will log an error if da_limit too small for builder_tx_da_size + if let Some(da_limit) = total_da_per_batch.as_mut() { + *da_limit = da_limit.saturating_sub(builder_tx_da_size); + } + let best_txs_start_time = Instant::now(); let best_txs = BestPayloadTransactions::new( // We are not using without_updates in here, so arriving transaction could target the current block @@ -474,10 +494,8 @@ where .payload_tx_simulation_gauge .set(payload_tx_simulation_time); - // If it is the last flashblocks, add the builder txn to the block if enabled - if ctx.is_last_flashblock() { - ctx.add_builder_tx(&mut info, &mut db, builder_tx_gas, message.clone()); - }; + self.builder_tx + .add_builder_txs(&state_provider, &mut info, &ctx, &mut db)?; let total_block_built_duration = Instant::now(); let build_result = build_block(db, &ctx, &mut info); @@ -531,7 +549,9 @@ where if let Some(da) = total_da_per_batch.as_mut() { *da += da_limit; } else { - error!("Builder end up in faulty invariant, if da_per_batch is set then total_da_per_batch must be set"); + error!( + "Builder end up in faulty invariant, if da_per_batch is set then total_da_per_batch must be set" + ); } } @@ -677,12 +697,12 @@ where } } -impl crate::builders::generator::PayloadBuilder - for OpPayloadBuilder +impl crate::builders::generator::PayloadBuilder + for OpPayloadBuilder where Pool: PoolBounds, Client: ClientBounds, - BT: BuilderTx + Clone + Send + Sync, + BuilderTx: BuilderTransactions + Clone + Send + Sync, { type Attributes = OpPayloadBuilderAttributes; type BuiltPayload = OpBuiltPayload; diff --git a/crates/op-rbuilder/src/builders/flashblocks/service.rs b/crates/op-rbuilder/src/builders/flashblocks/service.rs index c045a0c..534b40e 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/service.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/service.rs @@ -1,10 +1,12 @@ use super::{payload::OpPayloadBuilder, FlashblocksConfig}; use crate::{ builders::{ - builder_tx::StandardBuilderTx, generator::BlockPayloadJobGenerator, BuilderConfig, - BuilderTx, + builder_tx::BuilderTransactions, + flashblocks::{builder_tx::FlashblocksBuilderTx, payload::FlashblocksExtraCtx}, + generator::BlockPayloadJobGenerator, + BuilderConfig, }, - flashtestations::service::spawn_flashtestations_service, + flashtestations::service::bootstrap_flashtestations, traits::{NodeBounds, PoolBounds}, }; use reth_basic_payload_builder::BasicPayloadJobGeneratorConfig; @@ -17,16 +19,16 @@ use reth_provider::CanonStateSubscriptions; pub struct FlashblocksServiceBuilder(pub BuilderConfig); impl FlashblocksServiceBuilder { - fn spawn_payload_builder_service( + fn spawn_payload_builder_service( self, ctx: &BuilderContext, pool: Pool, - builder_tx: BT, + builder_tx: BuilderTx, ) -> eyre::Result::Payload>> where Node: NodeBounds, Pool: PoolBounds, - BT: BuilderTx + Unpin + Clone + Send + Sync + 'static, + BuilderTx: BuilderTransactions + Unpin + Clone + Send + Sync + 'static, { let payload_builder = OpPayloadBuilder::new( OpEvmConfig::optimism(ctx.chain_spec()), @@ -70,30 +72,22 @@ where pool: Pool, _: OpEvmConfig, ) -> eyre::Result::Payload>> { - tracing::debug!("Spawning flashblocks payload builder service"); let signer = self.0.builder_signer; - if self.0.flashtestations_config.flashtestations_enabled { - let flashtestations_service = match spawn_flashtestations_service( - self.0.flashtestations_config.clone(), - ctx, - ) - .await - { - Ok(service) => service, + let flashtestations_builder_tx = if self.0.flashtestations_config.flashtestations_enabled { + match bootstrap_flashtestations(self.0.flashtestations_config.clone(), ctx).await { + Ok(builder_tx) => Some(builder_tx), Err(e) => { - tracing::warn!(error = %e, "Failed to spawn flashtestations service, falling back to standard builder tx"); - return self.spawn_payload_builder_service( - ctx, - pool, - StandardBuilderTx { signer }, - ); + tracing::warn!(error = %e, "Failed to bootstrap flashtestations, builderb will not include flashtestations txs"); + None } - }; - - if self.0.flashtestations_config.enable_block_proofs { - return self.spawn_payload_builder_service(ctx, pool, flashtestations_service); } - } - self.spawn_payload_builder_service(ctx, pool, StandardBuilderTx { signer }) + } else { + None + }; + self.spawn_payload_builder_service( + ctx, + pool, + FlashblocksBuilderTx::new(signer, flashtestations_builder_tx), + ) } } diff --git a/crates/op-rbuilder/src/builders/mod.rs b/crates/op-rbuilder/src/builders/mod.rs index 1c41f37..4b7630b 100644 --- a/crates/op-rbuilder/src/builders/mod.rs +++ b/crates/op-rbuilder/src/builders/mod.rs @@ -20,7 +20,8 @@ mod flashblocks; mod generator; mod standard; -pub use builder_tx::BuilderTx; +pub use builder_tx::{BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions}; +pub use context::OpPayloadBuilderCtx; pub use flashblocks::FlashblocksBuilder; pub use standard::StandardBuilder; diff --git a/crates/op-rbuilder/src/builders/standard/builder_tx.rs b/crates/op-rbuilder/src/builders/standard/builder_tx.rs new file mode 100644 index 0000000..865e5fc --- /dev/null +++ b/crates/op-rbuilder/src/builders/standard/builder_tx.rs @@ -0,0 +1,146 @@ +use alloy_consensus::TxEip1559; +use alloy_eips::{eip7623::TOTAL_COST_FLOOR_PER_TOKEN, Encodable2718}; +use alloy_evm::Database; +use alloy_primitives::{Address, TxKind}; +use core::fmt::Debug; +use op_alloy_consensus::OpTypedTransaction; +use reth_optimism_primitives::OpTransactionSigned; +use reth_primitives::Recovered; +use reth_provider::StateProvider; +use reth_revm::State; + +use crate::{ + builders::{ + context::OpPayloadBuilderCtx, BuilderTransactionCtx, BuilderTransactionError, + BuilderTransactions, + }, + flashtestations::builder_tx::FlashtestationsBuilderTx, + primitives::reth::ExecutionInfo, + tx_signer::Signer, +}; + +// This will be the end of block transaction of a regular block +#[derive(Debug, Clone)] +pub struct StandardBuilderTx { + pub signer: Option, + pub flashtestations_builder_tx: Option, +} + +impl StandardBuilderTx { + pub fn new( + signer: Option, + flashtestations_builder_tx: Option, + ) -> Self { + Self { + signer, + flashtestations_builder_tx, + } + } + + pub fn simulate_builder_tx( + &self, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + ) -> Result, BuilderTransactionError> { + match self.signer { + Some(signer) => { + let message: Vec = format!("Block Number: {}", ctx.block_number()).into_bytes(); + let gas_used = self.estimate_builder_tx_gas(&message); + let signed_tx = self.signed_builder_tx(ctx, db, signer, gas_used, message)?; + let da_size = op_alloy_flz::tx_estimated_size_fjord_bytes( + signed_tx.encoded_2718().as_slice(), + ); + Ok(Some(BuilderTransactionCtx { + gas_used, + da_size, + signed_tx, + })) + } + None => Ok(None), + } + } + + fn estimate_builder_tx_gas(&self, input: &[u8]) -> u64 { + // Count zero and non-zero bytes + let (zero_bytes, nonzero_bytes) = input.iter().fold((0, 0), |(zeros, nonzeros), &byte| { + if byte == 0 { + (zeros + 1, nonzeros) + } else { + (zeros, nonzeros + 1) + } + }); + + // Calculate gas cost (4 gas per zero byte, 16 gas per non-zero byte) + let zero_cost = zero_bytes * 4; + let nonzero_cost = nonzero_bytes * 16; + + // Tx gas should be not less than floor gas https://eips.ethereum.org/EIPS/eip-7623 + let tokens_in_calldata = zero_bytes + nonzero_bytes * 4; + let floor_gas = 21_000 + tokens_in_calldata * TOTAL_COST_FLOOR_PER_TOKEN; + + std::cmp::max(zero_cost + nonzero_cost + 21_000, floor_gas) + } + + fn signed_builder_tx( + &self, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + signer: Signer, + gas_used: u64, + message: Vec, + ) -> Result, BuilderTransactionError> { + let nonce = db + .load_cache_account(signer.address) + .map(|acc| acc.account_info().unwrap_or_default().nonce) + .map_err(|_| BuilderTransactionError::AccountLoadFailed(signer.address))?; + + // Create the EIP-1559 transaction + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id: ctx.chain_id(), + nonce, + gas_limit: gas_used, + max_fee_per_gas: ctx.base_fee().into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(Address::ZERO), + // Include the message as part of the transaction data + input: message.into(), + ..Default::default() + }); + // Sign the transaction + let builder_tx = signer + .sign_tx(tx) + .map_err(BuilderTransactionError::SigningError)?; + + Ok(builder_tx) + } +} + +impl BuilderTransactions for StandardBuilderTx { + fn simulate_builder_txs( + &self, + state_provider: impl StateProvider + Clone, + info: &mut ExecutionInfo, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + ) -> Result, BuilderTransactionError> { + let mut builder_txs = Vec::::new(); + let standard_builder_tx = self.simulate_builder_tx(ctx, db)?; + builder_txs.extend(standard_builder_tx.clone()); + if let Some(flashtestations_builder_tx) = &self.flashtestations_builder_tx { + let mut simulation_state = self.simulate_builder_txs_state::<()>( + state_provider.clone(), + standard_builder_tx.iter().collect(), + ctx, + db, + )?; + let flashtestations_builder_txs = flashtestations_builder_tx.simulate_builder_txs( + state_provider, + info, + ctx, + &mut simulation_state, + )?; + builder_txs.extend(flashtestations_builder_txs); + } + Ok(builder_txs) + } +} diff --git a/crates/op-rbuilder/src/builders/standard/mod.rs b/crates/op-rbuilder/src/builders/standard/mod.rs index 98e9b44..e26bbc6 100644 --- a/crates/op-rbuilder/src/builders/standard/mod.rs +++ b/crates/op-rbuilder/src/builders/standard/mod.rs @@ -1,11 +1,13 @@ -use payload::StandardPayloadBuilderBuilder; -use reth_node_builder::components::BasicPayloadServiceBuilder; - -use crate::traits::{NodeBounds, PoolBounds}; +use crate::{ + builders::standard::service::StandardServiceBuilder, + traits::{NodeBounds, PoolBounds}, +}; use super::BuilderConfig; +mod builder_tx; mod payload; +mod service; /// Block building strategy that builds blocks using the standard approach by /// producing blocks every chain block time. @@ -15,7 +17,7 @@ impl super::PayloadBuilder for StandardBuilder { type Config = (); type ServiceBuilder - = BasicPayloadServiceBuilder + = StandardServiceBuilder where Node: NodeBounds, Pool: PoolBounds; @@ -27,8 +29,6 @@ impl super::PayloadBuilder for StandardBuilder { Node: NodeBounds, Pool: PoolBounds, { - Ok(BasicPayloadServiceBuilder::new( - StandardPayloadBuilderBuilder(config), - )) + Ok(StandardServiceBuilder(config)) } } diff --git a/crates/op-rbuilder/src/builders/standard/payload.rs b/crates/op-rbuilder/src/builders/standard/payload.rs index 2a1b846..b384ad5 100644 --- a/crates/op-rbuilder/src/builders/standard/payload.rs +++ b/crates/op-rbuilder/src/builders/standard/payload.rs @@ -1,21 +1,21 @@ +use super::super::context::OpPayloadBuilderCtx; use crate::{ - builders::{generator::BuildArguments, BuilderConfig}, - flashtestations::service::spawn_flashtestations_service, + builders::{generator::BuildArguments, BuilderConfig, BuilderTransactions}, metrics::OpRBuilderMetrics, primitives::reth::ExecutionInfo, - traits::{ClientBounds, NodeBounds, PayloadTxsBounds, PoolBounds}, + traits::{ClientBounds, PayloadTxsBounds, PoolBounds}, }; 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_evm::Database; 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; @@ -23,69 +23,20 @@ use reth_optimism_node::{OpBuiltPayload, OpPayloadBuilderAttributes}; use reth_optimism_primitives::{OpPrimitives, OpTransactionSigned}; use reth_payload_util::{BestPayloadTransactions, NoopPayloadTransactions, PayloadTransactions}; use reth_primitives::RecoveredBlock; -use reth_provider::{ - ExecutionOutcome, HashedPostStateProvider, ProviderError, StateRootProvider, - StorageRootProvider, -}; +use reth_provider::{ExecutionOutcome, StateProvider}; use reth_revm::{ database::StateProviderDatabase, db::states::bundle_state::BundleRetention, State, }; use reth_transaction_pool::{ BestTransactions, BestTransactionsAttributes, PoolTransaction, TransactionPool, }; -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}; - -pub struct StandardPayloadBuilderBuilder(pub BuilderConfig<()>); - -impl PayloadBuilderBuilder for StandardPayloadBuilderBuilder -where - Node: NodeBounds, - Pool: PoolBounds, -{ - type PayloadBuilder = StandardOpPayloadBuilder; - - async fn build_payload_builder( - self, - ctx: &BuilderContext, - pool: Pool, - _evm_config: OpEvmConfig, - ) -> eyre::Result { - if self.0.flashtestations_config.flashtestations_enabled { - match spawn_flashtestations_service(self.0.flashtestations_config.clone(), ctx).await { - Ok(service) => service, - Err(e) => { - tracing::warn!(error = %e, "Failed to spawn flashtestations service, falling back to standard builder tx"); - return Ok(StandardOpPayloadBuilder::new( - OpEvmConfig::optimism(ctx.chain_spec()), - pool, - ctx.provider().clone(), - self.0.clone(), - )); - } - }; - - if self.0.flashtestations_config.enable_block_proofs { - // TODO: flashtestations end of block transaction - } - } - - Ok(StandardOpPayloadBuilder::new( - OpEvmConfig::optimism(ctx.chain_spec()), - pool, - ctx.provider().clone(), - self.0.clone(), - )) - } -} - /// Optimism's payload builder #[derive(Debug, Clone)] -pub struct StandardOpPayloadBuilder { +pub struct StandardOpPayloadBuilder { /// The type responsible for creating the evm. pub evm_config: OpEvmConfig, /// The transaction pool @@ -99,15 +50,18 @@ pub struct StandardOpPayloadBuilder { pub best_transactions: Txs, /// The metrics for the builder pub metrics: Arc, + /// The type responsible for creating the builder transactions + pub builder_tx: BuilderTx, } -impl StandardOpPayloadBuilder { +impl StandardOpPayloadBuilder { /// `OpPayloadBuilder` constructor. pub fn new( evm_config: OpEvmConfig, pool: Pool, client: Client, config: BuilderConfig<()>, + builder_tx: BuilderTx, ) -> Self { Self { pool, @@ -116,6 +70,7 @@ impl StandardOpPayloadBuilder { evm_config, best_transactions: (), metrics: Default::default(), + builder_tx, } } } @@ -146,11 +101,12 @@ impl OpPayloadTransactions for () { } } -impl reth_basic_payload_builder::PayloadBuilder - for StandardOpPayloadBuilder +impl reth_basic_payload_builder::PayloadBuilder + for StandardOpPayloadBuilder where Pool: PoolBounds, Client: ClientBounds, + BuilderTx: BuilderTransactions + Clone + Send + Sync, Txs: OpPayloadTransactions, { type Attributes = OpPayloadBuilderAttributes; @@ -209,10 +165,11 @@ where } } -impl StandardOpPayloadBuilder +impl StandardOpPayloadBuilder where Pool: PoolBounds, Client: ClientBounds, + BuilderTx: BuilderTransactions + Clone, { /// Constructs an Optimism payload from the transactions sent via the /// Payload attributes by the sequencer. If the `no_tx_pool` argument is passed in @@ -280,22 +237,18 @@ where let builder = OpBuilder::new(best); let state_provider = self.client.state_by_block_hash(ctx.parent().hash())?; - let state = StateProviderDatabase::new(state_provider); + 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) + builder.build(state, &state_provider, ctx, self.builder_tx.clone()) } 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) + builder.build( + cached_reads.as_db_mut(state), + &state_provider, + ctx, + self.builder_tx.clone(), + ) } .map(|out| { let total_block_building_time = block_build_start_time.elapsed(); @@ -349,28 +302,29 @@ pub struct ExecutedPayload { impl OpBuilder<'_, Txs> { /// Executes the payload and returns the outcome. - pub fn execute( + pub fn execute( self, - state: &mut State, + state_provider: impl StateProvider, + db: &mut State, ctx: &OpPayloadBuilderCtx, + builder_tx: BuilderTx, ) -> Result, PayloadBuilderError> where - DB: Database + AsRef

+ std::fmt::Debug, - P: StorageRootProvider, + BuilderTx: BuilderTransactions, { 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()) + .builder_for_next_block(db, 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)?; + let mut info = ctx.execute_sequencer_transactions(db)?; let sequencer_tx_time = sequencer_tx_start_time.elapsed(); ctx.metrics.sequencer_tx_duration.record(sequencer_tx_time); @@ -379,17 +333,14 @@ impl OpBuilder<'_, Txs> { // 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; + let builder_txs = builder_tx.simulate_builder_txs(&state_provider, &mut info, ctx, db)?; + let builder_tx_gas = builder_txs.iter().fold(0, |acc, tx| acc + tx.gas_used); + let block_gas_limit = ctx.block_gas_limit().saturating_sub(builder_tx_gas); + if block_gas_limit == 0 { + error!("Builder tx gas subtraction resulted in block gas limit to be 0. No transactions would be included"); + } // 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 builder_tx_da_size = builder_txs.iter().fold(0, |acc, tx| acc + tx.da_size); let block_da_limit = ctx .da_config .max_da_block_size() @@ -415,7 +366,7 @@ impl OpBuilder<'_, Txs> { if ctx .execute_best_transactions( &mut info, - state, + db, best_txs, block_gas_limit, block_da_limit, @@ -427,13 +378,13 @@ impl OpBuilder<'_, Txs> { } // Add builder tx to the block - ctx.add_builder_tx(&mut info, state, builder_tx_gas, message); + builder_tx.add_builder_txs(&state_provider, &mut info, ctx, db)?; 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); + db.merge_transitions(BundleRetention::Reverts); let state_transition_merge_time = state_merge_start_time.elapsed(); ctx.metrics @@ -457,24 +408,32 @@ impl OpBuilder<'_, Txs> { } /// Builds the payload on top of the state. - pub fn build( + pub fn build( self, - mut state: State, + state: impl Database, + state_provider: impl StateProvider, ctx: OpPayloadBuilderCtx, + builder_tx: BuilderTx, ) -> Result, PayloadBuilderError> where - DB: Database + AsRef

+ std::fmt::Debug, - P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, + BuilderTx: BuilderTransactions, { - 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 mut db = State::builder() + .with_database(state) + .with_bundle_update() + .build(); + let ExecutedPayload { info } = + match self.execute(&state_provider, &mut db, &ctx, builder_tx)? { + 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(), + db.take_bundle(), vec![info.receipts], block_number, Vec::new(), @@ -495,12 +454,9 @@ impl OpBuilder<'_, Txs> { // 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_provider .state_root_with_updates(hashed_state.clone()) .inspect_err(|err| { warn!(target: "payload_builder", @@ -524,7 +480,7 @@ impl OpBuilder<'_, Txs> { // `l2tol1-message-passer` ( Some( - isthmus::withdrawals_root(execution_outcome.state(), state.database.as_ref()) + isthmus::withdrawals_root(execution_outcome.state(), state_provider) .map_err(PayloadBuilderError::other)?, ), Some(EMPTY_REQUESTS_HASH), diff --git a/crates/op-rbuilder/src/builders/standard/service.rs b/crates/op-rbuilder/src/builders/standard/service.rs new file mode 100644 index 0000000..6a3c7ce --- /dev/null +++ b/crates/op-rbuilder/src/builders/standard/service.rs @@ -0,0 +1,96 @@ +use reth_basic_payload_builder::{BasicPayloadJobGenerator, BasicPayloadJobGeneratorConfig}; +use reth_node_api::NodeTypes; +use reth_node_builder::{components::PayloadServiceBuilder, BuilderContext}; +use reth_optimism_evm::OpEvmConfig; +use reth_payload_builder::{PayloadBuilderHandle, PayloadBuilderService}; +use reth_provider::CanonStateSubscriptions; + +use crate::{ + builders::{ + standard::{builder_tx::StandardBuilderTx, payload::StandardOpPayloadBuilder}, + BuilderConfig, BuilderTransactions, + }, + flashtestations::service::bootstrap_flashtestations, + traits::{NodeBounds, PoolBounds}, +}; + +pub struct StandardServiceBuilder(pub BuilderConfig<()>); + +impl StandardServiceBuilder { + pub fn spawn_payload_builder_service( + self, + evm_config: OpEvmConfig, + ctx: &BuilderContext, + pool: Pool, + builder_tx: BuilderTx, + ) -> eyre::Result::Payload>> + where + Node: NodeBounds, + Pool: PoolBounds, + BuilderTx: BuilderTransactions + Unpin + Clone + Send + Sync + 'static, + { + let payload_builder = StandardOpPayloadBuilder::new( + evm_config, + pool, + ctx.provider().clone(), + self.0.clone(), + builder_tx, + ); + + let conf = ctx.config().builder.clone(); + + let payload_job_config = BasicPayloadJobGeneratorConfig::default() + .interval(conf.interval) + .deadline(conf.deadline) + .max_payload_tasks(conf.max_payload_tasks); + + let payload_generator = BasicPayloadJobGenerator::with_builder( + ctx.provider().clone(), + ctx.task_executor().clone(), + payload_job_config, + payload_builder, + ); + let (payload_service, payload_service_handle) = + PayloadBuilderService::new(payload_generator, ctx.provider().canonical_state_stream()); + + ctx.task_executor() + .spawn_critical("payload builder service", Box::pin(payload_service)); + + Ok(payload_service_handle) + } +} + +impl PayloadServiceBuilder for StandardServiceBuilder +where + Node: NodeBounds, + Pool: PoolBounds, +{ + async fn spawn_payload_builder_service( + self, + ctx: &BuilderContext, + pool: Pool, + evm_config: OpEvmConfig, + ) -> eyre::Result::Payload>> { + let signer = self.0.builder_signer; + let flashtestations_builder_tx = if self.0.flashtestations_config.flashtestations_enabled { + match bootstrap_flashtestations::(self.0.flashtestations_config.clone(), ctx) + .await + { + Ok(builder_tx) => Some(builder_tx), + Err(e) => { + tracing::warn!(error = %e, "Failed to bootstrap flashtestations, builderb will not include flashtestations txs"); + None + } + } + } else { + None + }; + + self.spawn_payload_builder_service( + evm_config, + ctx, + pool, + StandardBuilderTx::new(signer, flashtestations_builder_tx), + ) + } +} diff --git a/crates/op-rbuilder/src/flashtestations/args.rs b/crates/op-rbuilder/src/flashtestations/args.rs index 4af8342..86f0b83 100644 --- a/crates/op-rbuilder/src/flashtestations/args.rs +++ b/crates/op-rbuilder/src/flashtestations/args.rs @@ -24,16 +24,23 @@ pub struct FlashtestationsArgs { pub debug: bool, // Debug url for attestations - #[arg(long = "flashtestations.debug-url", env = "FLASHTESTATIONS_DEBUG_URL")] - pub debug_url: Option, + #[arg( + long = "flashtestations.debug-tee-key-seed", + env = "FLASHTESTATIONS_DEBUG_TEE_KEY_SEED", + default_value = "debug" + )] + pub debug_tee_key_seed: String, - /// The rpc url to post the onchain attestation requests to + // Remote url for attestations #[arg( - long = "flashtestations.rpc-url", - env = "FLASHTESTATIONS_RPC_URL", - default_value = "http://localhost:8545" + long = "flashtestations.quote-provider", + env = "FLASHTESTATIONS_QUOTE_PROVIDER" )] - pub rpc_url: String, + pub quote_provider: Option, + + /// The rpc url to post the onchain attestation requests to + #[arg(long = "flashtestations.rpc-url", env = "FLASHTESTATIONS_RPC_URL")] + pub rpc_url: Option, /// Funding key for the TEE key #[arg( diff --git a/crates/op-rbuilder/src/flashtestations/attestation.rs b/crates/op-rbuilder/src/flashtestations/attestation.rs index e52f588..2086343 100644 --- a/crates/op-rbuilder/src/flashtestations/attestation.rs +++ b/crates/op-rbuilder/src/flashtestations/attestation.rs @@ -9,8 +9,8 @@ const DEBUG_QUOTE_SERVICE_URL: &str = "http://ns31695324.ip-141-94-163.eu:10080/ pub struct AttestationConfig { /// If true, uses the debug HTTP service instead of real TDX hardware pub debug: bool, - /// The URL of the debug HTTP service - pub debug_url: Option, + /// The URL of the quote provider + pub quote_provider: Option, } /// Trait for attestation providers @@ -18,18 +18,18 @@ pub trait AttestationProvider { fn get_attestation(&self, report_data: [u8; 64]) -> eyre::Result>; } -/// Debug HTTP service attestation provider -pub struct DebugAttestationProvider { +/// Remote attestation provider +pub struct RemoteAttestationProvider { service_url: String, } -impl DebugAttestationProvider { +impl RemoteAttestationProvider { pub fn new(service_url: String) -> Self { Self { service_url } } } -impl AttestationProvider for DebugAttestationProvider { +impl AttestationProvider for RemoteAttestationProvider { fn get_attestation(&self, report_data: [u8; 64]) -> eyre::Result> { let report_data_hex = hex::encode(report_data); let url = format!("{}/{}", self.service_url, report_data_hex); @@ -47,19 +47,21 @@ impl AttestationProvider for DebugAttestationProvider { } } +#[allow(clippy::if_same_then_else)] pub fn get_attestation_provider( config: AttestationConfig, ) -> Box { if config.debug { - Box::new(DebugAttestationProvider::new( + Box::new(RemoteAttestationProvider::new( config - .debug_url + .quote_provider .unwrap_or(DEBUG_QUOTE_SERVICE_URL.to_string()), )) } else { - // TODO: replace with real attestation provider - Box::new(DebugAttestationProvider::new( - DEBUG_QUOTE_SERVICE_URL.to_string(), + Box::new(RemoteAttestationProvider::new( + config + .quote_provider + .unwrap_or(DEBUG_QUOTE_SERVICE_URL.to_string()), // TODO: remove this once we have a real quote provider )) } } diff --git a/crates/op-rbuilder/src/flashtestations/builder_tx.rs b/crates/op-rbuilder/src/flashtestations/builder_tx.rs new file mode 100644 index 0000000..570c048 --- /dev/null +++ b/crates/op-rbuilder/src/flashtestations/builder_tx.rs @@ -0,0 +1,597 @@ +use alloy_consensus::TxEip1559; +use alloy_eips::Encodable2718; +use alloy_evm::Database; +use alloy_op_evm::OpEvm; +use alloy_primitives::{keccak256, map::foldhash::HashMap, Address, Bytes, TxKind, B256, U256}; +use alloy_sol_types::{SolCall, SolEvent, SolValue}; +use core::fmt::Debug; +use op_alloy_consensus::OpTypedTransaction; +use reth_evm::{precompiles::PrecompilesMap, ConfigureEvm, Evm, EvmError}; +use reth_optimism_primitives::OpTransactionSigned; +use reth_primitives::{Log, Recovered}; +use reth_provider::StateProvider; +use reth_revm::{database::StateProviderDatabase, State}; +use revm::{ + context::result::{ExecutionResult, ResultAndState}, + inspector::NoOpInspector, + state::Account, + DatabaseCommit, +}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use tracing::{debug, info, warn}; + +use crate::{ + builders::{ + BuilderTransactionCtx, BuilderTransactionError, BuilderTransactions, OpPayloadBuilderCtx, + }, + flashtestations::{ + BlockBuilderPolicyError, BlockBuilderProofVerified, BlockData, FlashtestationRegistryError, + FlashtestationRevertReason, IBlockBuilderPolicy, IFlashtestationRegistry, + TEEServiceRegistered, + }, + primitives::reth::ExecutionInfo, + tx_signer::Signer, +}; + +pub struct FlashtestationsBuilderTxArgs { + pub attestation: Vec, + pub extra_registration_data: Bytes, + pub tee_service_signer: Signer, + pub funding_key: Signer, + pub funding_amount: U256, + pub registry_address: Address, + pub builder_policy_address: Address, + pub builder_proof_version: u8, + pub enable_block_proofs: bool, + pub registered: bool, +} + +#[derive(Debug, Clone)] +pub struct FlashtestationsBuilderTx { + // Attestation for the builder + attestation: Vec, + // Extra registration data for the builder + extra_registration_data: Bytes, + // TEE service generated key + tee_service_signer: Signer, + // Funding key for the TEE signer + funding_key: Signer, + // Funding amount for the TEE signer + funding_amount: U256, + // Registry address for the attestation + registry_address: Address, + // Builder policy address for the block builder proof + builder_policy_address: Address, + // Builder proof version + builder_proof_version: u8, + // Whether the workload and address has been registered + registered: Arc, + // Whether block proofs are enabled + enable_block_proofs: bool, +} + +#[derive(Debug, Default)] +pub struct TxSimulateResult { + pub gas_used: u64, + pub success: bool, + pub state_changes: HashMap, + pub revert_reason: Option, + pub logs: Vec, +} + +impl FlashtestationsBuilderTx { + pub fn new(args: FlashtestationsBuilderTxArgs) -> Self { + Self { + attestation: args.attestation, + extra_registration_data: args.extra_registration_data, + tee_service_signer: args.tee_service_signer, + funding_key: args.funding_key, + funding_amount: args.funding_amount, + registry_address: args.registry_address, + builder_policy_address: args.builder_policy_address, + builder_proof_version: args.builder_proof_version, + registered: Arc::new(AtomicBool::new(args.registered)), + enable_block_proofs: args.enable_block_proofs, + } + } + + fn signed_funding_tx( + &self, + to: Address, + from: Signer, + amount: U256, + base_fee: u64, + chain_id: u64, + nonce: u64, + ) -> Result, secp256k1::Error> { + // Create the EIP-1559 transaction + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id, + nonce, + gas_limit: 21000, + max_fee_per_gas: base_fee.into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(to), + value: amount, + ..Default::default() + }); + from.sign_tx(tx) + } + + fn signed_register_tee_service_tx( + &self, + attestation: Vec, + gas_limit: u64, + base_fee: u64, + chain_id: u64, + nonce: u64, + ) -> Result, secp256k1::Error> { + let quote_bytes = Bytes::from(attestation); + let calldata = IFlashtestationRegistry::registerTEEServiceCall { + rawQuote: quote_bytes, + extendedRegistrationData: self.extra_registration_data.clone(), + } + .abi_encode(); + + // Create the EIP-1559 transaction + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas: base_fee.into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(self.registry_address), + input: calldata.into(), + ..Default::default() + }); + self.tee_service_signer.sign_tx(tx) + } + + fn signed_block_builder_proof_tx( + &self, + block_content_hash: B256, + ctx: &OpPayloadBuilderCtx, + gas_limit: u64, + nonce: u64, + ) -> Result, secp256k1::Error> { + let calldata = IBlockBuilderPolicy::verifyBlockBuilderProofCall { + version: self.builder_proof_version, + blockContentHash: block_content_hash, + } + .abi_encode(); + // Create the EIP-1559 transaction + let tx = OpTypedTransaction::Eip1559(TxEip1559 { + chain_id: ctx.chain_id(), + nonce, + gas_limit, + max_fee_per_gas: ctx.base_fee().into(), + max_priority_fee_per_gas: 0, + to: TxKind::Call(self.builder_policy_address), + input: calldata.into(), + ..Default::default() + }); + self.tee_service_signer.sign_tx(tx) + } + + /// Computes the block content hash according to the formula: + /// keccak256(abi.encode(parentHash, blockNumber, timestamp, transactionHashes)) + /// https://github.com/flashbots/rollup-boost/blob/main/specs/flashtestations.md#block-building-process + fn compute_block_content_hash( + transactions: Vec, + parent_hash: B256, + block_number: u64, + timestamp: u64, + ) -> B256 { + // Create ordered list of transaction hashes + let transaction_hashes: Vec = transactions + .iter() + .map(|tx| { + // RLP encode the transaction and hash it + let mut encoded = Vec::new(); + tx.encode_2718(&mut encoded); + keccak256(&encoded) + }) + .collect(); + + // Create struct and ABI encode + let block_data = BlockData { + parentHash: parent_hash, + blockNumber: U256::from(block_number), + timestamp: U256::from(timestamp), + transactionHashes: transaction_hashes, + }; + + let encoded = block_data.abi_encode(); + keccak256(&encoded) + } + + fn simulate_register_tee_service_tx( + &self, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let nonce = get_nonce(evm.db_mut(), self.tee_service_signer.address)?; + + let register_tx = self.signed_register_tee_service_tx( + self.attestation.clone(), + ctx.block_gas_limit(), + ctx.base_fee(), + ctx.chain_id(), + nonce, + )?; + let ResultAndState { result, state } = match evm.transact(®ister_tx) { + Ok(res) => res, + Err(err) => { + if err.is_invalid_tx_err() { + warn!(target: "flashtestations", %err, "register tee service tx failed"); + return Ok(TxSimulateResult::default()); + } else { + return Err(BuilderTransactionError::EvmExecutionError(Box::new(err))); + } + } + }; + match result { + ExecutionResult::Success { gas_used, logs, .. } => Ok(TxSimulateResult { + gas_used, + success: true, + state_changes: state, + revert_reason: None, + logs, + }), + ExecutionResult::Revert { output, .. } => { + let revert_reason = FlashtestationRegistryError::from(output); + Ok(TxSimulateResult { + gas_used: 0, + success: false, + state_changes: state, + revert_reason: Some(FlashtestationRevertReason::FlashtestationRegistry( + revert_reason, + )), + logs: vec![], + }) + } + ExecutionResult::Halt { reason, .. } => Ok(TxSimulateResult { + gas_used: 0, + success: false, + state_changes: state, + revert_reason: Some(FlashtestationRevertReason::Halt( + serde_json::to_string(&reason).unwrap_or_default(), + )), + logs: vec![], + }), + } + } + + fn check_tee_address_registered_log(&self, logs: Vec, address: Address) -> bool { + for log in logs { + if log.topics().first() == Some(&TEEServiceRegistered::SIGNATURE_HASH) { + if let Ok(decoded) = TEEServiceRegistered::decode_log(&log) { + if decoded.teeAddress == address { + return true; + } + }; + } + } + false + } + + fn simulate_verify_block_proof_tx( + &self, + block_content_hash: B256, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result { + let nonce = get_nonce(evm.db_mut(), self.tee_service_signer.address)?; + + let verify_block_proof_tx = self.signed_block_builder_proof_tx( + block_content_hash, + ctx, + ctx.block_gas_limit(), + nonce, + )?; + let ResultAndState { result, state } = match evm.transact(&verify_block_proof_tx) { + Ok(res) => res, + Err(err) => { + if err.is_invalid_tx_err() { + warn!(target: "flashtestations", %err, "verify block proof tx failed"); + return Ok(TxSimulateResult::default()); + } else { + return Err(BuilderTransactionError::EvmExecutionError(Box::new(err))); + } + } + }; + match result { + ExecutionResult::Success { gas_used, logs, .. } => Ok(TxSimulateResult { + gas_used, + success: true, + state_changes: state, + revert_reason: None, + logs, + }), + ExecutionResult::Revert { output, .. } => { + let revert_reason = BlockBuilderPolicyError::from(output); + Ok(TxSimulateResult { + gas_used: 0, + success: false, + state_changes: state, + revert_reason: Some(FlashtestationRevertReason::BlockBuilderPolicy( + revert_reason, + )), + logs: vec![], + }) + } + ExecutionResult::Halt { reason, .. } => Ok(TxSimulateResult { + gas_used: 0, + success: false, + state_changes: state, + revert_reason: Some(FlashtestationRevertReason::Halt( + serde_json::to_string(&reason).unwrap_or_default(), + )), + logs: vec![], + }), + } + } + + fn check_verify_block_proof_log(&self, logs: Vec) -> bool { + for log in logs { + if log.topics().first() == Some(&BlockBuilderProofVerified::SIGNATURE_HASH) { + return true; + } + } + false + } + + fn fund_tee_service_tx( + &self, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result, BuilderTransactionError> { + let balance = get_balance(evm.db_mut(), self.tee_service_signer.address)?; + if balance.is_zero() { + let funding_nonce = get_nonce(evm.db_mut(), self.funding_key.address)?; + let funding_tx = self.signed_funding_tx( + self.tee_service_signer.address, + self.funding_key, + self.funding_amount, + ctx.base_fee(), + ctx.chain_id(), + funding_nonce, + )?; + let da_size = + op_alloy_flz::tx_estimated_size_fjord_bytes(funding_tx.encoded_2718().as_slice()); + let ResultAndState { state, .. } = match evm.transact(&funding_tx) { + Ok(res) => res, + Err(err) => { + if err.is_invalid_tx_err() { + warn!(target: "flashtestations", %err, "funding tx failed"); + return Ok(None); + } else { + return Err(BuilderTransactionError::EvmExecutionError(Box::new(err))); + } + } + }; + info!(target: "flashtestations", block_number = ctx.block_number(), tx_hash = ?funding_tx.tx_hash(), "adding funding tx to builder txs"); + evm.db_mut().commit(state); + Ok(Some(BuilderTransactionCtx { + gas_used: 21000, + da_size, + signed_tx: funding_tx, + })) + } else { + Ok(None) + } + } + + fn register_tee_service_tx( + &self, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result<(Option, bool), BuilderTransactionError> { + let TxSimulateResult { + gas_used, + success, + state_changes, + revert_reason, + logs, + } = self.simulate_register_tee_service_tx(ctx, evm)?; + if success { + let has_log = + self.check_tee_address_registered_log(logs, self.tee_service_signer.address); + if !has_log { + warn!(target: "flashtestations", "transaction did not emit TEEServiceRegistered log, FlashtestationRegistry contract address may be incorrect"); + Ok((None, false)) + } else { + let nonce = get_nonce(evm.db_mut(), self.tee_service_signer.address)?; + let register_tx = self.signed_register_tee_service_tx( + self.attestation.clone(), + gas_used * 64 / 63, // Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer + ctx.base_fee(), + ctx.chain_id(), + nonce, + )?; + let da_size = op_alloy_flz::tx_estimated_size_fjord_bytes( + register_tx.encoded_2718().as_slice(), + ); + info!(target: "flashtestations", block_number = ctx.block_number(), tx_hash = ?register_tx.tx_hash(), "adding register tee tx to builder txs"); + evm.db_mut().commit(state_changes); + Ok(( + Some(BuilderTransactionCtx { + gas_used, + da_size, + signed_tx: register_tx, + }), + false, + )) + } + } else if let Some(FlashtestationRevertReason::FlashtestationRegistry( + FlashtestationRegistryError::TEEServiceAlreadyRegistered(_), + )) = revert_reason + { + Ok((None, true)) + } else { + warn!(target: "flashtestations", reason = ?revert_reason, "register tee service tx failed"); + Ok((None, false)) + } + } + + fn verify_block_proof_tx( + &self, + transactions: Vec, + ctx: &OpPayloadBuilderCtx, + evm: &mut OpEvm< + &mut State>, + NoOpInspector, + PrecompilesMap, + >, + ) -> Result, BuilderTransactionError> { + let block_content_hash = Self::compute_block_content_hash( + transactions.clone(), + ctx.parent_hash(), + ctx.block_number(), + ctx.timestamp(), + ); + + let TxSimulateResult { + gas_used, + success, + revert_reason, + logs, + .. + } = self.simulate_verify_block_proof_tx(block_content_hash, ctx, evm)?; + if success { + let has_log = self.check_verify_block_proof_log(logs); + if !has_log { + warn!(target: "flashtestations", "transaction did not emit BlockBuilderProofVerified log, BlockBuilderPolicy contract address may be incorrect"); + Ok(None) + } else { + let nonce = get_nonce(evm.db_mut(), self.tee_service_signer.address)?; + // Due to EIP-150, only 63/64 of available gas is forwarded to external calls so need to add a buffer + let verify_block_proof_tx = self.signed_block_builder_proof_tx( + block_content_hash, + ctx, + gas_used * 64 / 63, + nonce, + )?; + let da_size = op_alloy_flz::tx_estimated_size_fjord_bytes( + verify_block_proof_tx.encoded_2718().as_slice(), + ); + debug!(target: "flashtestations", block_number = ctx.block_number(), tx_hash = ?verify_block_proof_tx.tx_hash(), "adding verify block proof tx to builder txs"); + Ok(Some(BuilderTransactionCtx { + gas_used, + da_size, + signed_tx: verify_block_proof_tx, + })) + } + } else { + warn!(target: "flashtestations", reason = ?revert_reason, "verify block proof tx failed, falling back to standard builder tx"); + Ok(None) + } + } + + fn set_registered( + &self, + state_provider: impl StateProvider + Clone, + ctx: &OpPayloadBuilderCtx, + ) { + let state = StateProviderDatabase::new(state_provider.clone()); + let mut simulation_state = State::builder() + .with_database(state) + .with_bundle_update() + .build(); + let mut evm = ctx + .evm_config + .evm_with_env(&mut simulation_state, ctx.evm_env.clone()); + evm.modify_cfg(|cfg| { + cfg.disable_balance_check = true; + }); + match self.register_tee_service_tx(ctx, &mut evm) { + Ok((_, registered)) => { + self.registered.store(registered, Ordering::Relaxed); + } + Err(e) => { + debug!(target: "flashtestations", error = ?e, "simulation error when checking if registered"); + } + } + } +} + +impl BuilderTransactions for FlashtestationsBuilderTx { + fn simulate_builder_txs( + &self, + state_provider: impl StateProvider + Clone, + info: &mut ExecutionInfo, + ctx: &OpPayloadBuilderCtx, + db: &mut State, + ) -> Result, BuilderTransactionError> { + let state = StateProviderDatabase::new(state_provider.clone()); + let mut simulation_state = State::builder() + .with_database(state) + .with_bundle_prestate(db.bundle_state.clone()) + .with_bundle_update() + .build(); + + let mut evm = ctx + .evm_config + .evm_with_env(&mut simulation_state, ctx.evm_env.clone()); + evm.modify_cfg(|cfg| { + cfg.disable_balance_check = true; + }); + + let mut builder_txs = Vec::::new(); + + if !self.registered.load(Ordering::Relaxed) { + info!(target: "flashtestations", "tee service not registered yet, attempting to register"); + self.set_registered(state_provider, ctx); + builder_txs.extend(self.fund_tee_service_tx(ctx, &mut evm)?); + let (register_tx, _) = self.register_tee_service_tx(ctx, &mut evm)?; + builder_txs.extend(register_tx); + } + + if self.enable_block_proofs { + // add verify block proof tx + builder_txs.extend(self.verify_block_proof_tx( + info.executed_transactions.clone(), + ctx, + &mut evm, + )?); + } + Ok(builder_txs) + } +} + +fn get_nonce(db: &mut State, address: Address) -> Result +where + DB: revm::Database, +{ + db.load_cache_account(address) + .map(|acc| acc.account_info().unwrap_or_default().nonce) + .map_err(|_| BuilderTransactionError::AccountLoadFailed(address)) +} + +fn get_balance(db: &mut State, address: Address) -> Result +where + DB: revm::Database, +{ + db.load_cache_account(address) + .map(|acc| acc.account_info().unwrap_or_default().balance) + .map_err(|_| BuilderTransactionError::AccountLoadFailed(address)) +} diff --git a/crates/op-rbuilder/src/flashtestations/mod.rs b/crates/op-rbuilder/src/flashtestations/mod.rs index a5b16e7..aacd5c4 100644 --- a/crates/op-rbuilder/src/flashtestations/mod.rs +++ b/crates/op-rbuilder/src/flashtestations/mod.rs @@ -1,4 +1,210 @@ +use alloy_primitives::{Address, Bytes, FixedBytes, B256, U256}; +use alloy_sol_types::{sol, SolError}; + +sol!( + #[sol(rpc, abi)] + interface IFlashtestationRegistry { + function registerTEEService(bytes calldata rawQuote, bytes calldata extendedRegistrationData) external; + } + + #[sol(rpc, abi)] + interface IBlockBuilderPolicy { + function verifyBlockBuilderProof(uint8 version, bytes32 blockContentHash) external; + } + + struct BlockData { + bytes32 parentHash; + uint256 blockNumber; + uint256 timestamp; + bytes32[] transactionHashes; + } + + type WorkloadId is bytes32; + + event TEEServiceRegistered(address teeAddress, bytes rawQuote, bool alreadyExists); + + event BlockBuilderProofVerified( + address caller, + WorkloadId workloadId, + uint256 blockNumber, + uint8 version, + bytes32 blockContentHash, + string commit_hash + ); + + // FlashtestationRegistry errors + error InvalidQuote(bytes output); + error TEEServiceAlreadyRegistered(address teeAddress); + error InvalidRegistrationDataHash(bytes32 expected, bytes32 received); + error SenderMustMatchTEEAddress(address sender, address teeAddress); + error ByteSizeExceeded(uint256 size); + + // QuoteParser errors + error InvalidTEEType(bytes4 teeType); + error InvalidTEEVersion(uint16 version); + error InvalidReportDataLength(uint256 length); + error InvalidQuoteLength(uint256 length); + + // BlockBuilderPolicy errors + error UnauthorizedBlockBuilder(address caller); + error UnsupportedVersion(uint8 version); + + // EIP-712 permit errors + error InvalidSignature(); + error InvalidNonce(uint256 expected, uint256 provided); +); + +#[derive(Debug, thiserror::Error)] +pub enum FlashtestationRevertReason { + #[error("flashtestation registry error: {0}")] + FlashtestationRegistry(FlashtestationRegistryError), + #[error("block builder policy error: {0}")] + BlockBuilderPolicy(BlockBuilderPolicyError), + #[error("halt: {0}")] + Halt(String), +} + +#[derive(Debug, thiserror::Error)] +pub enum FlashtestationRegistryError { + #[error("invalid quote: {0}")] + InvalidQuote(Bytes), + #[error("tee address {0} already registered")] + TEEServiceAlreadyRegistered(Address), + #[error("invalid registration data hash: expected {0}, received {1}")] + InvalidRegistrationDataHash(B256, B256), + #[error("byte size exceeded: {0}")] + ByteSizeExceeded(U256), + #[error("sender address {0} must match quote tee address {1}")] + SenderMustMatchTEEAddress(Address, Address), + #[error("invalid tee type: {0}")] + InvalidTEEType(FixedBytes<4>), + #[error("invalid tee version: {0}")] + InvalidTEEVersion(u16), + #[error("invalid report data length: {0}")] + InvalidReportDataLength(U256), + #[error("invalid quote length: {0}")] + InvalidQuoteLength(U256), + #[error("invalid signature")] + InvalidSignature(), + #[error("invalid nonce: expected {0}, provided {1}")] + InvalidNonce(U256, U256), + #[error("unknown revert: {0}")] + Unknown(String), +} + +impl From for FlashtestationRegistryError { + fn from(value: Bytes) -> Self { + // Empty revert + if value.is_empty() { + return FlashtestationRegistryError::Unknown( + "Transaction reverted without reason".to_string(), + ); + } + + // Try to decode each custom error type + if let Ok(InvalidQuote { output }) = InvalidQuote::abi_decode(&value) { + return FlashtestationRegistryError::InvalidQuote(output); + } + + if let Ok(TEEServiceAlreadyRegistered { teeAddress }) = + TEEServiceAlreadyRegistered::abi_decode(&value) + { + return FlashtestationRegistryError::TEEServiceAlreadyRegistered(teeAddress); + } + + if let Ok(InvalidRegistrationDataHash { expected, received }) = + InvalidRegistrationDataHash::abi_decode(&value) + { + return FlashtestationRegistryError::InvalidRegistrationDataHash(expected, received); + } + + if let Ok(ByteSizeExceeded { size }) = ByteSizeExceeded::abi_decode(&value) { + return FlashtestationRegistryError::ByteSizeExceeded(size); + } + + if let Ok(SenderMustMatchTEEAddress { sender, teeAddress }) = + SenderMustMatchTEEAddress::abi_decode(&value) + { + return FlashtestationRegistryError::SenderMustMatchTEEAddress(sender, teeAddress); + } + + if let Ok(InvalidTEEType { teeType }) = InvalidTEEType::abi_decode(&value) { + return FlashtestationRegistryError::InvalidTEEType(teeType); + } + + if let Ok(InvalidTEEVersion { version }) = InvalidTEEVersion::abi_decode(&value) { + return FlashtestationRegistryError::InvalidTEEVersion(version); + } + + if let Ok(InvalidReportDataLength { length }) = InvalidReportDataLength::abi_decode(&value) + { + return FlashtestationRegistryError::InvalidReportDataLength(length); + } + + if let Ok(InvalidQuoteLength { length }) = InvalidQuoteLength::abi_decode(&value) { + return FlashtestationRegistryError::InvalidQuoteLength(length); + } + + if let Ok(InvalidSignature {}) = InvalidSignature::abi_decode(&value) { + return FlashtestationRegistryError::InvalidSignature(); + } + + if let Ok(InvalidNonce { expected, provided }) = InvalidNonce::abi_decode(&value) { + return FlashtestationRegistryError::InvalidNonce(expected, provided); + } + + FlashtestationRegistryError::Unknown(hex::encode(value)) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum BlockBuilderPolicyError { + #[error("unauthorized block builder: {0}")] + UnauthorizedBlockBuilder(Address), + #[error("unsupported version: {0}")] + UnsupportedVersion(u8), + #[error("invalid signature")] + InvalidSignature(), + #[error("invalid nonce: expected {0}, provided {1}")] + InvalidNonce(U256, U256), + #[error("unknown revert: {0}")] + Unknown(String), +} + +impl From for BlockBuilderPolicyError { + fn from(value: Bytes) -> Self { + // Empty revert + if value.is_empty() { + return BlockBuilderPolicyError::Unknown( + "Transaction reverted without reason".to_string(), + ); + } + + // Try to decode each custom error type + if let Ok(UnauthorizedBlockBuilder { caller }) = + UnauthorizedBlockBuilder::abi_decode(&value) + { + return BlockBuilderPolicyError::UnauthorizedBlockBuilder(caller); + } + + if let Ok(UnsupportedVersion { version }) = UnsupportedVersion::abi_decode(&value) { + return BlockBuilderPolicyError::UnsupportedVersion(version); + } + + if let Ok(InvalidSignature {}) = InvalidSignature::abi_decode(&value) { + return BlockBuilderPolicyError::InvalidSignature(); + } + + if let Ok(InvalidNonce { expected, provided }) = InvalidNonce::abi_decode(&value) { + return BlockBuilderPolicyError::InvalidNonce(expected, provided); + } + + BlockBuilderPolicyError::Unknown(hex::encode(value)) + } +} + pub mod args; pub mod attestation; +pub mod builder_tx; pub mod service; pub mod tx_manager; diff --git a/crates/op-rbuilder/src/flashtestations/service.rs b/crates/op-rbuilder/src/flashtestations/service.rs index 650bce0..1509a1e 100644 --- a/crates/op-rbuilder/src/flashtestations/service.rs +++ b/crates/op-rbuilder/src/flashtestations/service.rs @@ -1,180 +1,139 @@ -use std::sync::Arc; - -use alloy_primitives::U256; +use alloy_primitives::{keccak256, Bytes}; use reth_node_builder::BuilderContext; -use reth_optimism_primitives::OpTransactionSigned; -use reth_primitives::Recovered; use tracing::{info, warn}; use crate::{ - builders::BuilderTx, + flashtestations::builder_tx::{FlashtestationsBuilderTx, FlashtestationsBuilderTxArgs}, traits::NodeBounds, - tx_signer::{generate_ethereum_keypair, Signer}, + tx_signer::{generate_ethereum_keypair, generate_key_from_seed, Signer}, }; use super::{ args::FlashtestationsArgs, - attestation::{get_attestation_provider, AttestationConfig, AttestationProvider}, + attestation::{get_attestation_provider, AttestationConfig}, tx_manager::TxManager, }; -#[derive(Clone)] -pub struct FlashtestationsService { - // Attestation provider generating attestations - attestation_provider: Arc>, - // Handles the onchain attestation and TEE block building proofs - tx_manager: TxManager, - // TEE service generated key - tee_service_signer: Signer, - // Funding amount for the TEE signer - funding_amount: U256, -} - -// TODO: FlashtestationsService error types -impl FlashtestationsService { - pub fn new(args: FlashtestationsArgs) -> Self { - let (private_key, public_key, address) = generate_ethereum_keypair(); - let tee_service_signer = Signer { - address, - pubkey: public_key, - secret: private_key, - }; - - let attestation_provider = Arc::new(get_attestation_provider(AttestationConfig { - debug: args.debug, - debug_url: args.debug_url, - })); - - let tx_manager = TxManager::new( - tee_service_signer, - args.funding_key - .expect("funding key required when flashtestations enabled"), - args.rpc_url, - args.registry_address - .expect("registry address required when flashtestations enabled"), - args.builder_policy_address - .expect("builder policy address required when flashtestations enabled"), - args.builder_proof_version, - ); - - Self { - attestation_provider, - tx_manager, - tee_service_signer, - funding_amount: args.funding_amount, - } - } - - pub async fn bootstrap(&self) -> eyre::Result<()> { - // Prepare report data with public key (64 bytes, no 0x04 prefix) - let mut report_data = [0u8; 64]; - let pubkey_uncompressed = self.tee_service_signer.pubkey.serialize_uncompressed(); - report_data.copy_from_slice(&pubkey_uncompressed[1..65]); // Skip 0x04 prefix - - // Request TDX attestation - info!(target: "flashtestations", "requesting TDX attestation"); - let attestation = self.attestation_provider.get_attestation(report_data)?; - - // Submit report onchain by registering the key of the tee service - self.tx_manager - .fund_and_register_tee_service(attestation, self.funding_amount) - .await - } - - pub async fn clean_up(&self) -> eyre::Result<()> { - self.tx_manager.clean_up().await - } -} - -impl BuilderTx for FlashtestationsService { - fn estimated_builder_tx_gas(&self) -> u64 { - todo!() - } - - fn estimated_builder_tx_da_size(&self) -> Option { - todo!() - } - - fn signed_builder_tx(&self) -> Result, secp256k1::Error> { - todo!() - } -} - -pub async fn spawn_flashtestations_service( +pub async fn bootstrap_flashtestations( args: FlashtestationsArgs, ctx: &BuilderContext, -) -> eyre::Result +) -> eyre::Result where Node: NodeBounds, { - info!("Flashtestations enabled"); - - let flashtestations_service = FlashtestationsService::new(args.clone()); - // Generates new key and registers the attestation onchain - flashtestations_service.bootstrap().await?; + let (private_key, public_key, address) = if args.debug { + info!("Flashtestations debug mode enabled, generating debug key"); + // Generate deterministic key for debugging purposes + generate_key_from_seed(&args.debug_tee_key_seed) + } else { + generate_ethereum_keypair() + }; + + info!("Flashtestations key generated: {}", address); + + let tee_service_signer = Signer { + address, + pubkey: public_key, + secret: private_key, + }; + + let funding_key = args + .funding_key + .expect("funding key required when flashtestations enabled"); + let registry_address = args + .registry_address + .expect("registry address required when flashtestations enabled"); + let builder_policy_address = args + .builder_policy_address + .expect("builder policy address required when flashtestations enabled"); + + let attestation_provider = get_attestation_provider(AttestationConfig { + debug: args.debug, + quote_provider: args.quote_provider, + }); + + // Prepare report data: + // - TEE address (20 bytes) at reportData[0:20] + // - Extended registration data hash (32 bytes) at reportData[20:52] + // - Total: 52 bytes, padded to 64 bytes with zeros + + // Extract TEE address as 20 bytes + let tee_address_bytes: [u8; 20] = tee_service_signer.address.into(); + + // Calculate keccak256 hash of empty bytes (32 bytes) + let ext_data = Bytes::from(b""); + let ext_data_hash = keccak256(&ext_data); + + // Create 64-byte report data array + let mut report_data = [0u8; 64]; + + // Copy TEE address (20 bytes) to positions 0-19 + report_data[0..20].copy_from_slice(&tee_address_bytes); + + // Copy extended registration data hash (32 bytes) to positions 20-51 + report_data[20..52].copy_from_slice(ext_data_hash.as_ref()); + + // Request TDX attestation + info!(target: "flashtestations", "requesting TDX attestation"); + let attestation = attestation_provider.get_attestation(report_data)?; + + let (tx_manager, registered) = if let Some(rpc_url) = args.rpc_url { + let tx_manager = TxManager::new( + tee_service_signer, + funding_key, + rpc_url.clone(), + registry_address, + ); + // Submit report onchain by registering the key of the tee service + match tx_manager + .fund_and_register_tee_service( + attestation.clone(), + ext_data.clone(), + args.funding_amount, + ) + .await + { + Ok(_) => (Some(tx_manager), true), + Err(e) => { + warn!(error = %e, "Failed to register tee service via rpc"); + (Some(tx_manager), false) + } + } + } else { + (None, false) + }; + + let flashtestations_builder_tx = FlashtestationsBuilderTx::new(FlashtestationsBuilderTxArgs { + attestation, + extra_registration_data: ext_data, + tee_service_signer, + funding_key, + funding_amount: args.funding_amount, + registry_address, + builder_policy_address, + builder_proof_version: args.builder_proof_version, + enable_block_proofs: args.enable_block_proofs, + registered, + }); - let flashtestations_clone = flashtestations_service.clone(); ctx.task_executor() .spawn_critical_with_graceful_shutdown_signal( "flashtestations clean up task", |shutdown| { Box::pin(async move { let graceful_guard = shutdown.await; - if let Err(e) = flashtestations_clone.clean_up().await { - warn!( - error = %e, - "Failed to complete clean up for flashtestations service", - ) - }; + if let Some(tx_manager) = tx_manager { + if let Err(e) = tx_manager.clean_up().await { + warn!( + error = %e, + "Failed to complete clean up for flashtestations service", + ); + } + } drop(graceful_guard) }) }, ); - Ok(flashtestations_service) -} - -#[cfg(test)] -mod tests { - use alloy_primitives::Address; - use secp256k1::{PublicKey, Secp256k1, SecretKey}; - use sha3::{Digest, Keccak256}; - - use crate::tx_signer::public_key_to_address; - - /// Derives Ethereum address from report data using the same logic as the Solidity contract - fn derive_ethereum_address_from_report_data(pubkey_64_bytes: &[u8]) -> Address { - // This exactly matches the Solidity implementation: - // address(uint160(uint256(keccak256(reportData)))) - - // Step 1: keccak256(reportData) - let hash = Keccak256::digest(pubkey_64_bytes); - - // Step 2: Take last 20 bytes (same as uint256 -> uint160 conversion) - let mut address_bytes = [0u8; 20]; - address_bytes.copy_from_slice(&hash[12..32]); - - Address::from(address_bytes) - } - - #[test] - fn test_address_derivation_matches() { - // Test that our manual derivation is correct - let secp = Secp256k1::new(); - let private_key = SecretKey::from_slice(&[0x01; 32]).unwrap(); - let public_key = PublicKey::from_secret_key(&secp, &private_key); - - // Get address using our implementation - let our_address = public_key_to_address(&public_key); - - // Get address using our manual derivation (matching Solidity) - let pubkey_bytes = public_key.serialize_uncompressed(); - let report_data = &pubkey_bytes[1..65]; // Skip 0x04 prefix - let manual_address = derive_ethereum_address_from_report_data(report_data); - - assert_eq!( - our_address, manual_address, - "Address derivation should match" - ); - } + Ok(flashtestations_builder_tx) } diff --git a/crates/op-rbuilder/src/flashtestations/tx_manager.rs b/crates/op-rbuilder/src/flashtestations/tx_manager.rs index d6c6345..1dae98a 100644 --- a/crates/op-rbuilder/src/flashtestations/tx_manager.rs +++ b/crates/op-rbuilder/src/flashtestations/tx_manager.rs @@ -1,41 +1,40 @@ -use alloy_consensus::TxEip1559; -use alloy_eips::Encodable2718; +use alloy_json_rpc::RpcError; use alloy_network::ReceiptResponse; -use alloy_primitives::{keccak256, Address, Bytes, TxHash, TxKind, B256, U256}; +use alloy_primitives::{Address, Bytes, TxHash, TxKind, U256}; use alloy_rpc_types_eth::TransactionRequest; -use alloy_transport::TransportResult; -use op_alloy_consensus::OpTypedTransaction; -use reth_optimism_node::OpBuiltPayload; -use reth_optimism_primitives::OpTransactionSigned; -use reth_primitives::Recovered; +use alloy_sol_types::SolCall; +use alloy_transport::{TransportError, TransportErrorKind, TransportResult}; +use k256::ecdsa; use std::time::Duration; -use alloy_provider::{PendingTransactionBuilder, Provider, ProviderBuilder}; +use alloy_provider::{ + PendingTransactionBuilder, PendingTransactionError, Provider, ProviderBuilder, +}; use alloy_signer_local::PrivateKeySigner; -use alloy_sol_types::{sol, SolCall, SolValue}; use op_alloy_network::Optimism; -use tracing::{debug, error, info}; - -use crate::tx_signer::Signer; - -sol!( - #[sol(rpc, abi)] - interface IFlashtestationRegistry { - function registerTEEService(bytes calldata rawQuote) external; - } - - #[sol(rpc, abi)] - interface IBlockBuilderPolicy { - function verifyBlockBuilderProof(uint8 version, bytes32 blockContentHash) external; - } +use tracing::{debug, error, info, warn}; + +use crate::{flashtestations::IFlashtestationRegistry, tx_signer::Signer}; + +#[derive(Debug, thiserror::Error)] +pub enum TxManagerError { + #[error("rpc error: {0}")] + RpcError(TransportError), + #[error("tx reverted: {0}")] + TxReverted(TxHash), + #[error("error checking tx confirmation: {0}")] + TxConfirmationError(PendingTransactionError), + #[error("tx rpc error: {0}")] + TxRpcError(RpcError), + #[error("signer error: {0}")] + SignerError(ecdsa::Error), +} - struct BlockData { - bytes32 parentHash; - uint256 blockNumber; - uint256 timestamp; - bytes32[] transactionHashes; +impl From for TxManagerError { + fn from(e: TransportError) -> Self { + TxManagerError::RpcError(e) } -); +} #[derive(Debug, Clone)] pub struct TxManager { @@ -43,8 +42,6 @@ pub struct TxManager { funding_signer: Signer, rpc_url: String, registry_address: Address, - builder_policy_address: Address, - builder_proof_version: u8, } impl TxManager { @@ -53,21 +50,23 @@ impl TxManager { funding_signer: Signer, rpc_url: String, registry_address: Address, - builder_policy_address: Address, - builder_proof_version: u8, ) -> Self { Self { tee_service_signer, funding_signer, rpc_url, registry_address, - builder_policy_address, - builder_proof_version, } } - pub async fn fund_address(&self, from: Signer, to: Address, amount: U256) -> eyre::Result<()> { - let funding_wallet = PrivateKeySigner::from_bytes(&from.secret.secret_bytes().into())?; + pub async fn fund_address( + &self, + from: Signer, + to: Address, + amount: U256, + ) -> Result<(), TxManagerError> { + let funding_wallet = PrivateKeySigner::from_bytes(&from.secret.secret_bytes().into()) + .map_err(TxManagerError::SignerError)?; let provider = ProviderBuilder::new() .disable_recommended_fillers() .fetch_chain_id() @@ -91,36 +90,41 @@ impl TxManager { match Self::process_pending_tx(provider.send_transaction(funding_tx.into()).await).await { Ok(tx_hash) => { info!(target: "flashtestations", tx_hash = %tx_hash, "funding transaction confirmed successfully"); + Ok(()) } Err(e) => { - error!(target: "flashtestations", error = %e, "funding transaction failed"); - return Err(e); + warn!(target: "flashtestations", error = %e, "funding transaction failed"); + Err(e) } } - - Ok(()) } pub async fn fund_and_register_tee_service( &self, attestation: Vec, + extra_registration_data: Bytes, funding_amount: U256, - ) -> eyre::Result<()> { + ) -> Result<(), TxManagerError> { info!(target: "flashtestations", "funding TEE address at {}", self.tee_service_signer.address); self.fund_address( self.funding_signer, self.tee_service_signer.address, funding_amount, ) - .await?; + .await + .unwrap_or_else(|e| { + warn!(target: "flashtestations", error = %e, "Failed to fund TEE address, attempting to register without funding"); + }); let quote_bytes = Bytes::from(attestation); let wallet = - PrivateKeySigner::from_bytes(&self.tee_service_signer.secret.secret_bytes().into())?; + PrivateKeySigner::from_bytes(&self.tee_service_signer.secret.secret_bytes().into()) + .map_err(TxManagerError::SignerError)?; let provider = ProviderBuilder::new() .disable_recommended_fillers() .fetch_chain_id() .with_gas_estimation() + .with_cached_nonce_management() .wallet(wallet) .network::() .connect(self.rpc_url.as_str()) @@ -128,62 +132,29 @@ impl TxManager { info!(target: "flashtestations", "submitting quote to registry at {}", self.registry_address); - // TODO: add retries let calldata = IFlashtestationRegistry::registerTEEServiceCall { rawQuote: quote_bytes, + extendedRegistrationData: extra_registration_data, } .abi_encode(); let tx = TransactionRequest { from: Some(self.tee_service_signer.address), to: Some(TxKind::Call(self.registry_address)), - // gas: Some(10_000_000), // Set gas limit manually as the contract is gas heavy - nonce: Some(0), input: calldata.into(), ..Default::default() }; match Self::process_pending_tx(provider.send_transaction(tx.into()).await).await { Ok(tx_hash) => { info!(target: "flashtestations", tx_hash = %tx_hash, "attestation transaction confirmed successfully"); - Ok(()) } Err(e) => { - error!(target: "flashtestations", error = %e, "attestation transaction failed to be sent"); - Err(e) + warn!(target: "flashtestations", error = %e, "attestation transaction failed to be sent"); } } + Ok(()) } - pub fn signed_block_builder_proof( - &self, - payload: OpBuiltPayload, - gas_limit: u64, - base_fee: u64, - chain_id: u64, - nonce: u64, - ) -> Result, secp256k1::Error> { - let block_content_hash = Self::compute_block_content_hash(payload); - - info!(target: "flashtestations", block_content_hash = ?block_content_hash, "submitting block builder proof transaction"); - let calldata = IBlockBuilderPolicy::verifyBlockBuilderProofCall { - version: self.builder_proof_version, - blockContentHash: block_content_hash, - } - .abi_encode(); - // Create the EIP-1559 transaction - let tx = OpTypedTransaction::Eip1559(TxEip1559 { - chain_id, - nonce, - gas_limit, - max_fee_per_gas: base_fee.into(), - max_priority_fee_per_gas: 0, - to: TxKind::Call(self.builder_policy_address), - input: calldata.into(), - ..Default::default() - }); - self.tee_service_signer.sign_tx(tx) - } - - pub async fn clean_up(&self) -> eyre::Result<()> { + pub async fn clean_up(&self) -> Result<(), TxManagerError> { info!(target: "flashtestations", "sending funds back from TEE generated key to funding address"); let provider = ProviderBuilder::new() .disable_recommended_fillers() @@ -202,7 +173,7 @@ impl TxManager { self.funding_signer.address, balance.saturating_sub(gas_cost), ) - .await? + .await?; } Ok(()) } @@ -210,7 +181,7 @@ impl TxManager { /// Processes a pending transaction and logs whether the transaction succeeded or not async fn process_pending_tx( pending_tx_result: TransportResult>, - ) -> eyre::Result { + ) -> Result { match pending_tx_result { Ok(pending_tx) => { let tx_hash = *pending_tx.tx_hash(); @@ -226,42 +197,13 @@ impl TxManager { if receipt.status() { Ok(receipt.transaction_hash()) } else { - Err(eyre::eyre!("Transaction reverted: {}", tx_hash)) + Err(TxManagerError::TxReverted(tx_hash)) } } - Err(e) => Err(e.into()), + Err(e) => Err(TxManagerError::TxConfirmationError(e)), } } - Err(e) => Err(e.into()), + Err(e) => Err(TxManagerError::TxRpcError(e)), } } - - /// Computes the block content hash according to the formula: - /// keccak256(abi.encode(parentHash, blockNumber, timestamp, transactionHashes)) - fn compute_block_content_hash(payload: OpBuiltPayload) -> B256 { - let block = payload.block(); - let body = block.clone().into_body(); - let transactions = body.transactions(); - - // Create ordered list of transaction hashes - let transaction_hashes: Vec = transactions - .map(|tx| { - // RLP encode the transaction and hash it - let mut encoded = Vec::new(); - tx.encode_2718(&mut encoded); - keccak256(&encoded) - }) - .collect(); - - // Create struct and ABI encode - let block_data = BlockData { - parentHash: block.parent_hash, - blockNumber: U256::from(block.number), - timestamp: U256::from(block.timestamp), - transactionHashes: transaction_hashes, - }; - - let encoded = block_data.abi_encode(); - keccak256(&encoded) - } } diff --git a/crates/op-rbuilder/src/tx_signer.rs b/crates/op-rbuilder/src/tx_signer.rs index f10e5d4..6b8b847 100644 --- a/crates/op-rbuilder/src/tx_signer.rs +++ b/crates/op-rbuilder/src/tx_signer.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use alloy_consensus::SignableTransaction; use alloy_primitives::{Address, Signature, B256, U256}; +use k256::sha2::Sha256; use op_alloy_consensus::OpTypedTransaction; use reth_optimism_primitives::OpTransactionSigned; use reth_primitives::Recovered; @@ -100,6 +101,23 @@ pub fn public_key_to_address(public_key: &PublicKey) -> Address { Address::from_slice(&hash[12..32]) } +// Generate a key deterministically from a seed for debug and testing +// Do not use in production +pub fn generate_key_from_seed(seed: &str) -> (SecretKey, PublicKey, Address) { + // Hash the seed + let mut hasher = Sha256::new(); + hasher.update(seed.as_bytes()); + let hash = hasher.finalize(); + + // Create signing key + let secp = Secp256k1::new(); + let private_key = SecretKey::from_slice(&hash).expect("Failed to create private key"); + let public_key = PublicKey::from_secret_key(&secp, &private_key); + let address = public_key_to_address(&public_key); + + (private_key, public_key, address) +} + #[cfg(test)] mod test { use super::*;