Skip to content

Reth 1.3.4 debug #531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking β€œSign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,179 changes: 1,198 additions & 981 deletions Cargo.lock

Large diffs are not rendered by default.

16 changes: 13 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -18,7 +18,11 @@ members = [
"crates/sysperf",
"crates/test-relay",
]
default-members = ["crates/rbuilder", "crates/reth-rbuilder", "crates/test-relay"]
default-members = [
"crates/rbuilder",
"crates/reth-rbuilder",
"crates/test-relay",
]
resolver = "2"

# Like release, but with full debug symbols. Useful for e.g. `perf`.
@@ -41,6 +45,7 @@ reth-db-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.4" }
reth-errors = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.4" }
reth-payload-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.4" }
reth-node-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.4" }
reth-node-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.4" }
reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.4" }
reth-trie = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.4" }
reth-trie-parallel = { git = "https://github.com/paradigmxyz/reth", tag = "v1.3.4" }
@@ -115,7 +120,8 @@ alloy-genesis = { version = "0.12.6" }
alloy-trie = { version = "0.7.9" }

# optimism
alloy-op-evm = { version = "0.1.0-alpha.2", default-features = false }
alloy-op-evm = { version = "0.1.0-alpha.3", default-features = false }
alloy-op-hardforks = { version = "0.1.0-alpha.2", default-features = false }
op-alloy-rpc-types = { version = "0.11.2", default-features = false }
op-alloy-rpc-types-engine = { version = "0.11.2", default-features = false }
op-alloy-rpc-jsonrpsee = { version = "0.11.2", default-features = false }
@@ -157,4 +163,8 @@ time = { version = "0.3.36", features = ["macros", "formatting", "parsing"] }
eth-sparse-mpt = { path = "crates/eth-sparse-mpt" }
rbuilder = { path = "crates/rbuilder" }
sysperf = { path = "crates/sysperf" }
metrics_macros = { path = "crates/rbuilder/src/telemetry/metrics_macros"}
metrics_macros = { path = "crates/rbuilder/src/telemetry/metrics_macros" }

[patch.crates-io]
alloy-op-evm = { git = "https://github.com/avalonche/evm", rev = "ccaaa18d3f45d03deaa2e8f9020aa7d695e2b471", default-features = false }
alloy-evm = { git = "https://github.com/avalonche/evm", rev = "ccaaa18d3f45d03deaa2e8f9020aa7d695e2b471", default-features = false }
2 changes: 1 addition & 1 deletion crates/eth-sparse-mpt/src/reth_sparse_trie/change_set.rs
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};

use crate::ChangedAccountData;

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ETHTrieChangeSet {
pub account_trie_deletes: Vec<Bytes>,

6 changes: 5 additions & 1 deletion crates/op-rbuilder/Cargo.toml
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ reth-chainspec.workspace = true
reth-primitives.workspace = true
reth-primitives-traits.workspace = true
reth-node-api.workspace = true
reth-node-builder.workspace = true
reth-basic-payload-builder.workspace = true
reth-payload-builder.workspace = true
reth-node-ethereum.workspace = true
@@ -58,10 +59,12 @@ alloy-serde.workspace = true
alloy-op-evm.workspace = true
op-alloy-consensus.workspace = true
op-alloy-rpc-types-engine.workspace = true
op-alloy-rpc-types.workspace = true
op-alloy-network.workspace = true
op-revm.workspace = true
alloy-op-hardforks.workspace = true

revm.workspace = true
op-revm.workspace = true

tracing.workspace = true
futures-util = "0.3.31"
@@ -79,6 +82,7 @@ derive_more.workspace = true
metrics.workspace = true
serde_json.workspace = true
tokio-util.workspace = true
thiserror.workspace = true

time = { version = "0.3.36", features = ["macros", "formatting", "parsing"] }
chrono = "0.4"
12 changes: 6 additions & 6 deletions crates/op-rbuilder/README.md
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ To run op-rbuilder with the op-stack, you need:
To run the op-rbuilder, run:

```bash
cargo run -p op-rbuilder --bin op-rbuilder --features optimism -- node \
cargo run -p op-rbuilder --bin op-rbuilder -- node \
--chain /path/to/chain-config.json \
--http \
--authrpc.port 9551 \
@@ -24,7 +24,7 @@ cargo run -p op-rbuilder --bin op-rbuilder --features optimism -- node \
To build the op-rbuilder, run:

```bash
cargo build -p op-rbuilder --bin op-rbuilder --features optimism
cargo build -p op-rbuilder --bin op-rbuilder
```

## Observability
@@ -50,13 +50,13 @@ To run the integration tests, run:

```bash
# Generate a genesis file
cargo run -p op-rbuilder --bin tester --features optimism -- genesis --output genesis.json
cargo run -p op-rbuilder --bin tester -- genesis --output genesis.json

# Build the op-rbuilder binary
cargo build -p op-rbuilder --bin op-rbuilder --features optimism
cargo build -p op-rbuilder --bin op-rbuilder

# Run the integration tests
cargo run -p op-rbuilder --bin tester --features optimism -- run
cargo run -p op-rbuilder --bin tester -- run
```

## Local Devnet
@@ -86,7 +86,7 @@ make devnet-clean && make devnet-down && make devnet-up
4. Run `op-rbuilder` in the `rbuilder` repo on port 8547:

```bash
cargo run -p op-rbuilder --bin op-rbuilder --features optimism -- node \
cargo run -p op-rbuilder --bin op-rbuilder -- node \
--chain ../optimism/.devnet/genesis-l2.json \
--http \
--http.port 8547 \
4 changes: 2 additions & 2 deletions crates/op-rbuilder/src/args.rs
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
//! clap [Args](clap::Args) for optimism rollup configuration
use reth_optimism_node::args::RollupArgs;

use crate::tx_signer::Signer;
use crate::tx_signer::OpSigner;

/// Parameters for rollup configuration
#[derive(Debug, Clone, Default, PartialEq, Eq, clap::Args)]
@@ -16,7 +16,7 @@ pub struct OpRbuilderArgs {
pub rollup_args: RollupArgs,
/// Builder secret key for signing last transaction in block
#[arg(long = "rollup.builder-secret-key", env = "BUILDER_SECRET_KEY")]
pub builder_signer: Option<Signer>,
pub builder_signer: Option<OpSigner>,
/// Websocket port for flashblock payload builder
#[arg(
long = "rollup.flashblocks-ws-url",
360 changes: 360 additions & 0 deletions crates/op-rbuilder/src/executor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
use alloy_consensus::{Eip658Value, Header, Transaction, TxReceipt};
use alloy_eips::{Encodable2718, Typed2718};
use alloy_op_evm::{
block::{receipt_builder::OpReceiptBuilder, OpAlloyReceiptBuilder},
OpBlockExecutionCtx, OpBlockExecutor, OpEvmFactory,
};
use alloy_op_hardforks::OpChainHardforks;
use op_alloy_consensus::{EIP1559ParamError, OpDepositReceipt};
use op_revm::{transaction::deposit::DEPOSIT_TRANSACTION_TYPE, OpSpecId, OpTransaction};
use reth::builder::{components::ExecutorBuilder, BuilderContext};
use reth_chainspec::EthChainSpec;
use reth_evm::{
block::{
BlockExecutionError, BlockExecutor, BlockExecutorFactory, BlockExecutorFor,
BlockValidationError, StateChangeSource,
},
eth::receipt_builder::ReceiptBuilderCtx,
ConfigureEvm, Database, Evm, EvmEnv, EvmFactory, FromRecoveredTx, InvalidTxError, OnStateHook,
};
use reth_node_api::{FullNodeTypes, NodePrimitives, NodeTypes};
use reth_node_ethereum::BasicBlockExecutorProvider;
use reth_optimism_chainspec::OpChainSpec;
use reth_optimism_evm::{
OpBlockAssembler, OpEvmConfig, OpNextBlockEnvAttributes, OpRethReceiptBuilder,
};
use reth_optimism_forks::OpHardforks;
use reth_optimism_primitives::{DepositReceipt, OpPrimitives};
use reth_primitives::{Recovered, SealedBlock, SealedHeader};
use reth_primitives_traits::SignedTransaction;
use reth_provider::BlockExecutionResult;
use reth_revm::State;
use revm::{
context::{result::ResultAndState, TxEnv},
DatabaseCommit, Inspector,
};
use std::sync::Arc;

#[derive(Debug, thiserror::Error)]
#[error("Reverting tx error: {message}")]
struct TransactionRevertedError {
message: String,
}

// Implement the InvalidTxError trait for it
impl InvalidTxError for TransactionRevertedError {
fn is_nonce_too_low(&self) -> bool {
false
}
}

/// A regular optimism evm and executor builder.
#[derive(Debug, Default, Clone, Copy)]
#[non_exhaustive]
pub struct OpRbuilderExecutorBuilder;

impl<Node> ExecutorBuilder<Node> for OpRbuilderExecutorBuilder
where
Node: FullNodeTypes<Types: NodeTypes<ChainSpec = OpChainSpec, Primitives = OpPrimitives>>,
{
type EVM = OpRbuilderEvmConfig;
type Executor = BasicBlockExecutorProvider<Self::EVM>;

async fn build_evm(
self,
ctx: &BuilderContext<Node>,
) -> eyre::Result<(Self::EVM, Self::Executor)> {
let evm_config =
OpRbuilderEvmConfig::new(ctx.chain_spec(), OpRethReceiptBuilder::default());
let executor = BasicBlockExecutorProvider::new(evm_config.clone());
Ok((evm_config, executor))
}
}

#[derive(Debug, Clone)]
pub struct OpRbuilderEvmConfig<
ChainSpec = OpChainSpec,
N: NodePrimitives = OpPrimitives,
R = OpRethReceiptBuilder,
> {
pub executor_factory: OpRbuilderBlockExecutorFactory<R, Arc<ChainSpec>>,
inner: OpEvmConfig<ChainSpec, N, R>,
}

impl<ChainSpec> OpRbuilderEvmConfig<ChainSpec> {
/// Creates a new [`OpEvmConfig`] with the given chain spec for OP chains.
pub fn optimism(chain_spec: Arc<ChainSpec>) -> Self {
Self::new(chain_spec, OpRethReceiptBuilder::default())
}
}

impl<ChainSpec, N: NodePrimitives, R> OpRbuilderEvmConfig<ChainSpec, N, R>
where
R: OpReceiptBuilder<Receipt: DepositReceipt, Transaction: SignedTransaction> + Clone,
{
/// Creates a new [`OpRbuilderEvmConfig`] with the given chain spec.
pub fn new(chain_spec: Arc<ChainSpec>, receipt_builder: R) -> Self {
Self {
inner: OpEvmConfig::new(chain_spec.clone(), receipt_builder.clone()),
executor_factory: OpRbuilderBlockExecutorFactory::new(
receipt_builder,
chain_spec,
OpEvmFactory::default(),
),
}
}
}

impl<ChainSpec, N, R> ConfigureEvm for OpRbuilderEvmConfig<ChainSpec, N, R>
where
ChainSpec: EthChainSpec + OpHardforks,
N: NodePrimitives<
Receipt = R::Receipt,
SignedTx = R::Transaction,
BlockHeader = Header,
BlockBody = alloy_consensus::BlockBody<R::Transaction>,
Block = alloy_consensus::Block<R::Transaction>,
>,
OpTransaction<TxEnv>: FromRecoveredTx<N::SignedTx>,
R: OpReceiptBuilder<Receipt: DepositReceipt, Transaction: SignedTransaction>,
OpEvmConfig<ChainSpec, N, R>: Send + Sync + Unpin + Clone,
Self: Send + Sync + Unpin + Clone + 'static,
{
type Primitives = N;
type Error = EIP1559ParamError;
type NextBlockEnvCtx = OpNextBlockEnvAttributes;
type BlockExecutorFactory = OpRbuilderBlockExecutorFactory<R, Arc<ChainSpec>>;
type BlockAssembler = OpBlockAssembler<ChainSpec>;

fn block_executor_factory(&self) -> &Self::BlockExecutorFactory {
&self.executor_factory
}

fn block_assembler(&self) -> &Self::BlockAssembler {
self.inner.block_assembler()
}

fn evm_env(&self, header: &Header) -> EvmEnv<OpSpecId> {
self.inner.evm_env(header)
}

fn next_evm_env(
&self,
parent: &Header,
attributes: &Self::NextBlockEnvCtx,
) -> Result<EvmEnv<OpSpecId>, Self::Error> {
self.inner.next_evm_env(parent, attributes)
}

fn context_for_block(&self, block: &'_ SealedBlock<N::Block>) -> OpBlockExecutionCtx {
self.inner.context_for_block(block)
}

fn context_for_next_block(
&self,
parent: &SealedHeader<N::BlockHeader>,
attributes: Self::NextBlockEnvCtx,
) -> OpBlockExecutionCtx {
self.inner.context_for_next_block(parent, attributes)
}
}

#[derive(Debug, Clone, Default, Copy)]
pub struct OpRbuilderBlockExecutorFactory<
R = OpAlloyReceiptBuilder,
Spec = OpChainHardforks,
EvmFactory = OpEvmFactory,
> {
/// Receipt builder.
receipt_builder: R,
/// Chain specification.
spec: Spec,
/// EVM factory.
evm_factory: EvmFactory,
}

impl<R, Spec, EvmFactory> OpRbuilderBlockExecutorFactory<R, Spec, EvmFactory> {
/// Creates a new [`OpRbuilderBlockExecutorFactory`] with the given spec, [`EvmFactory`], and
/// [`OpReceiptBuilder`].
pub const fn new(receipt_builder: R, spec: Spec, evm_factory: EvmFactory) -> Self {
Self {
receipt_builder,
spec,
evm_factory,
}
}
}

impl<R, Spec, EvmF> BlockExecutorFactory for OpRbuilderBlockExecutorFactory<R, Spec, EvmF>
where
R: OpReceiptBuilder<Transaction: Transaction + Encodable2718, Receipt: TxReceipt>,
Spec: OpHardforks,
EvmF: EvmFactory<Tx: FromRecoveredTx<R::Transaction>>,
Self: 'static,
{
type EvmFactory = EvmF;
type ExecutionCtx<'a> = OpBlockExecutionCtx;
type Transaction = R::Transaction;
type Receipt = R::Receipt;

fn evm_factory(&self) -> &Self::EvmFactory {
&self.evm_factory
}

fn create_executor<'a, DB, I>(
&'a self,
evm: EvmF::Evm<&'a mut State<DB>, I>,
ctx: Self::ExecutionCtx<'a>,
) -> impl BlockExecutorFor<'a, Self, DB, I>
where
DB: Database + 'a,
I: Inspector<EvmF::Context<&'a mut State<DB>>> + 'a,
{
OpRbuilderBlockExecutor {
inner: OpBlockExecutor::new(evm, ctx, &self.spec, &self.receipt_builder),
}
}
}

pub struct OpRbuilderBlockExecutor<Evm, R: OpReceiptBuilder, Spec> {
inner: OpBlockExecutor<Evm, R, Spec>,
}

impl<'db, DB, E, R, Spec> BlockExecutor for OpRbuilderBlockExecutor<E, R, Spec>
where
DB: Database + 'db,
E: Evm<DB = &'db mut State<DB>, Tx: FromRecoveredTx<R::Transaction>>,
R: OpReceiptBuilder<Transaction: Transaction + Encodable2718, Receipt: TxReceipt>,
Spec: OpHardforks,
{
type Transaction = R::Transaction;
type Receipt = R::Receipt;
type Evm = E;

fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
self.inner.apply_pre_execution_changes()
}

fn execute_transaction_with_result_closure(
&mut self,
tx: Recovered<&Self::Transaction>,
f: impl FnOnce(&revm::context::result::ExecutionResult<<Self::Evm as Evm>::HaltReason>),
) -> Result<u64, BlockExecutionError> {
let is_deposit = tx.ty() == DEPOSIT_TRANSACTION_TYPE;

// The sum of the transaction’s gas limit, Tg, and the gas utilized in this block prior,
// must be no greater than the block’s gasLimit.
let block_available_gas = self.inner.evm.block().gas_limit - self.inner.gas_used;
if tx.gas_limit() > block_available_gas && (self.inner.is_regolith || !is_deposit) {
return Err(
BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
transaction_gas_limit: tx.gas_limit(),
block_available_gas,
}
.into(),
);
}

// Cache the depositor account prior to the state transition for the deposit nonce.
//
// Note that this *only* needs to be done post-regolith hardfork, as deposit nonces
// were not introduced in Bedrock. In addition, regular transactions don't have deposit
// nonces, so we don't need to touch the DB for those.
let depositor = (self.inner.is_regolith && is_deposit)
.then(|| {
self.inner
.evm
.db_mut()
.load_cache_account(tx.signer())
.map(|acc| acc.account_info().unwrap_or_default())
})
.transpose()
.map_err(BlockExecutionError::other)?;

let hash = tx.trie_hash();

// Execute transaction.
let result_and_state = self
.inner
.evm
.transact(tx)
.map_err(move |err| BlockExecutionError::evm(err, hash))?;

if !result_and_state.result.is_success() {
return Err(BlockValidationError::InvalidTx {
hash,
error: Box::new(TransactionRevertedError {
message: "transaction reverted".to_string(), // TODO: add more context on error
}),
}
.into());
}

self.inner.system_caller.on_state(
StateChangeSource::Transaction(self.inner.receipts.len()),
&result_and_state.state,
);
let ResultAndState { result, state } = result_and_state;

f(&result);

let gas_used = result.gas_used();

// append gas used
self.inner.gas_used += gas_used;

self.inner.receipts.push(
match self.inner.receipt_builder.build_receipt(ReceiptBuilderCtx {
tx: tx.inner(),
result,
cumulative_gas_used: self.inner.gas_used,
evm: &self.inner.evm,
state: &state,
}) {
Ok(receipt) => receipt,
Err(ctx) => {
let receipt = alloy_consensus::Receipt {
// Success flag was added in `EIP-658: Embedding transaction status code
// in receipts`.
status: Eip658Value::Eip658(ctx.result.is_success()),
cumulative_gas_used: self.inner.gas_used,
logs: ctx.result.into_logs(),
};

self.inner
.receipt_builder
.build_deposit_receipt(OpDepositReceipt {
inner: receipt,
deposit_nonce: depositor.map(|account| account.nonce),
// The deposit receipt version was introduced in Canyon to indicate an
// update to how receipt hashes should be computed
// when set. The state transition process ensures
// this is only set for post-Canyon deposit
// transactions.
deposit_receipt_version: (is_deposit
&& self.inner.spec.is_canyon_active_at_timestamp(
self.inner.evm.block().timestamp,
))
.then_some(1),
})
}
},
);

self.inner.evm.db_mut().commit(state);

Ok(gas_used)
}

fn finish(self) -> Result<(Self::Evm, BlockExecutionResult<R::Receipt>), BlockExecutionError> {
self.inner.finish()
}

fn set_state_hook(&mut self, _hook: Option<Box<dyn OnStateHook>>) {
self.inner.set_state_hook(_hook)
}

fn evm_mut(&mut self) -> &mut Self::Evm {
self.inner.evm_mut()
}
}
482 changes: 335 additions & 147 deletions crates/op-rbuilder/src/generator.rs

Large diffs are not rendered by default.

83 changes: 51 additions & 32 deletions crates/op-rbuilder/src/integration/integration_test.rs
Original file line number Diff line number Diff line change
@@ -3,13 +3,13 @@ mod tests {
use crate::{
integration::{op_rbuilder::OpRbuilderConfig, op_reth::OpRethConfig, IntegrationFramework},
tester::{BlockGenerator, EngineApi},
tx_signer::Signer,
};
use alloy_consensus::{Transaction, TxEip1559};
use alloy_consensus::Transaction;
use alloy_eips::{eip1559::MIN_PROTOCOL_BASE_FEE, eip2718::Encodable2718};
use alloy_primitives::hex;
use alloy_provider::{Identity, Provider, ProviderBuilder};
use alloy_rpc_types_eth::BlockTransactionsKind;

use futures_util::StreamExt;
use op_alloy_consensus::OpTypedTransaction;
use op_alloy_network::Optimism;
@@ -22,6 +22,11 @@ mod tests {
use tokio_tungstenite::connect_async;
use uuid::Uuid;

use alloy_primitives::{Address, TxKind};
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};

use crate::tx_signer::OpSigner;

const BUILDER_PRIVATE_KEY: &str =
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";

@@ -150,33 +155,37 @@ mod tests {
);
for _ in 0..10 {
// Get builder's address
let known_wallet = Signer::try_from_secret(BUILDER_PRIVATE_KEY.parse()?)?;
let known_wallet: OpSigner = OpSigner::try_from_secret(BUILDER_PRIVATE_KEY.parse()?)?;
let builder_address = known_wallet.address;
// Get current nonce from chain
let nonce = provider.get_transaction_count(builder_address).await?;
// Transaction from builder should succeed
let tx_request = OpTypedTransaction::Eip1559(TxEip1559 {
chain_id: 901,
nonce,
gas_limit: 210000,
max_fee_per_gas: base_fee.into(),
let tx_request = TransactionRequest {
chain_id: Some(901),
nonce: Some(nonce),
gas: Some(210000),
max_fee_per_gas: Some(base_fee.into()),
max_priority_fee_per_gas: Some(0),
to: Some(TxKind::Call(Address::ZERO)),
..Default::default()
});
let signed_tx = known_wallet.sign_tx(tx_request)?;
};
let signed_tx = known_wallet.build_and_sign_tx(tx_request)?;
let known_tx = provider
.send_raw_transaction(signed_tx.encoded_2718().as_slice())
.await?;

// Create a reverting transaction
let tx_request = OpTypedTransaction::Eip1559(TxEip1559 {
chain_id: 901,
nonce: nonce + 1,
gas_limit: 300000,
max_fee_per_gas: base_fee.into(),
input: hex!("60006000fd").into(), // PUSH1 0x00 PUSH1 0x00 REVERT
let tx_request = TransactionRequest {
chain_id: Some(901),
nonce: Some(nonce + 1),
gas: Some(300000),
max_fee_per_gas: Some(base_fee.into()),
max_priority_fee_per_gas: Some(0),
to: Some(TxKind::Create),
input: TransactionInput::new(hex!("60006000fd").into()), // PUSH1 0x00 PUSH1 0x00 REVERT
..Default::default()
});
let signed_tx = known_wallet.sign_tx(tx_request)?;
};
let signed_tx = known_wallet.build_and_sign_tx(tx_request)?;
let reverting_tx = provider
.send_raw_transaction(signed_tx.encoded_2718().as_slice())
.await?;
@@ -204,7 +213,8 @@ mod tests {
.transactions
.hashes()
.any(|hash| hash == *reverting_tx.tx_hash()),
"reverted transaction unexpectedly included in block"
"reverted transaction {:?} unexpectedly included in block",
reverting_tx.tx_hash()
);
for hash in block.transactions.hashes() {
let receipt = provider
@@ -227,6 +237,13 @@ mod tests {
#[cfg(not(feature = "flashblocks"))]
async fn integration_test_fee_priority_ordering() -> eyre::Result<()> {
// This test validates that transactions are ordered by fee priority in blocks

use alloy_primitives::{Address, TxKind};
use alloy_rpc_types_eth::TransactionRequest;
use reth_optimism_primitives::OpPrimitives;
use reth_primitives::{Recovered, TxTy};

use crate::tx_signer::OpSigner;
let mut framework =
IntegrationFramework::new("integration_test_fee_priority_ordering").unwrap();

@@ -277,11 +294,11 @@ mod tests {
// Create transactions with increasing fee values
let priority_fees: [u128; 5] = [1, 3, 5, 2, 4]; // Deliberately not in order
let signers = vec![
Signer::random(),
Signer::random(),
Signer::random(),
Signer::random(),
Signer::random(),
OpSigner::random(),
OpSigner::random(),
OpSigner::random(),
OpSigner::random(),
OpSigner::random(),
];
let mut txs = Vec::new();

@@ -294,15 +311,17 @@ mod tests {

// Send transactions in non-optimal fee order
for (i, priority_fee) in priority_fees.iter().enumerate() {
let tx_request = OpTypedTransaction::Eip1559(TxEip1559 {
chain_id: 901,
nonce: 1,
gas_limit: 210000,
max_fee_per_gas: base_fee as u128 + *priority_fee,
max_priority_fee_per_gas: *priority_fee,
let tx_request = TransactionRequest {
chain_id: Some(901),
nonce: Some(1),
gas: Some(210000),
max_fee_per_gas: Some(base_fee as u128 + *priority_fee),
max_priority_fee_per_gas: Some(*priority_fee),
to: Some(TxKind::Call(Address::ZERO)),
..Default::default()
});
let signed_tx = signers[i].sign_tx(tx_request)?;
};
let signed_tx: Recovered<TxTy<OpPrimitives>> =
signers[i].build_and_sign_tx(tx_request)?;
let tx = provider
.send_raw_transaction(signed_tx.encoded_2718().as_slice())
.await?;
27 changes: 18 additions & 9 deletions crates/op-rbuilder/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use clap::Parser;
use executor::OpRbuilderExecutorBuilder;
use monitoring::Monitoring;
use reth::providers::CanonStateSubscriptions;
use reth_optimism_cli::{chainspec::OpChainSpecParser, Cli};
@@ -13,6 +14,7 @@ use reth_transaction_pool::TransactionPool;

/// CLI argument parsing.
pub mod args;
mod executor;
pub mod generator;
#[cfg(test)]
mod integration;
@@ -21,7 +23,6 @@ mod monitor_tx_pool;
mod monitoring;
#[cfg(feature = "flashblocks")]
pub mod payload_builder;
#[cfg(not(feature = "flashblocks"))]
mod payload_builder_vanilla;
mod primitives;
#[cfg(test)]
@@ -37,12 +38,20 @@ fn main() {
let op_node = OpNode::new(rollup_args.clone());
let handle = builder
.with_types::<OpNode>()
.with_components(op_node.components().payload(CustomOpPayloadBuilder::new(
builder_args.builder_signer,
builder_args.flashblocks_ws_url,
builder_args.chain_block_time,
builder_args.flashblock_block_time,
)))
.with_components(
op_node
.components()
.payload(
CustomOpPayloadBuilder::new(
builder_args.builder_signer,
builder_args.flashblocks_ws_url,
builder_args.chain_block_time,
builder_args.flashblock_block_time,
)
.with_da_config(op_node.da_config.clone()),
)
.executor(OpRbuilderExecutorBuilder::default()),
)
.with_add_ons(
OpAddOnsBuilder::default()
.with_sequencer(rollup_args.sequencer_http.clone())
@@ -51,7 +60,7 @@ fn main() {
)
.on_node_started(move |ctx| {
let new_canonical_blocks = ctx.provider().canonical_state_stream();
let builder_signer = builder_args.builder_signer;
// let builder_signer = builder_args.builder_signer;

if builder_args.log_pool_transactions {
tracing::info!("Logging pool transactions");
@@ -66,7 +75,7 @@ fn main() {
ctx.task_executor.spawn_critical(
"monitoring",
Box::pin(async move {
let monitoring = Monitoring::new(builder_signer);
let monitoring = Monitoring::new(builder_args.builder_signer);
let _ = monitoring.run_with_stream(new_canonical_blocks).await;
}),
);
28 changes: 26 additions & 2 deletions crates/op-rbuilder/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -18,8 +18,6 @@ pub struct OpRBuilderMetrics {
pub total_block_built_duration: Histogram,
/// Duration of fetching transactions from the pool
pub transaction_pool_fetch_duration: Histogram,
/// Duration of state root calculation
pub state_root_calculation_duration: Histogram,
/// Duration of sequencer transaction execution
pub sequencer_tx_duration: Histogram,
/// Duration of state merge transitions
@@ -71,3 +69,29 @@ impl OpRBuilderMetrics {
self.builder_balance.set(balance);
}
}

/// Transaction pool metrics
#[derive(Metrics)]
#[metrics(scope = "payloads")]
pub(crate) struct PayloadBuilderMetrics {
/// Total number of times an empty payload was returned because a built one was not ready.
pub(crate) requested_empty_payload: Counter,
/// Total number of initiated payload build attempts.
pub(crate) initiated_payload_builds: Counter,
/// Total number of failed payload build attempts.
pub(crate) failed_payload_builds: Counter,
}

impl PayloadBuilderMetrics {
pub(crate) fn inc_requested_empty_payload(&self) {
self.requested_empty_payload.increment(1);
}

pub(crate) fn inc_initiated_payload_builds(&self) {
self.initiated_payload_builds.increment(1);
}

pub(crate) fn inc_failed_payload_builds(&self) {
self.failed_payload_builds.increment(1);
}
}
10 changes: 5 additions & 5 deletions crates/op-rbuilder/src/monitoring.rs
Original file line number Diff line number Diff line change
@@ -10,18 +10,18 @@ use reth_primitives::{Block, RecoveredBlock};
use reth_provider::{Chain, ExecutionOutcome};
use tracing::{info, warn};

use crate::{metrics::OpRBuilderMetrics, tx_signer::Signer};
use crate::{metrics::OpRBuilderMetrics, tx_signer::OpSigner};

const OP_BUILDER_TX_PREFIX: &[u8] = b"Block Number:";

pub struct Monitoring {
builder_signer: Option<Signer>,
builder_signer: Option<OpSigner>,
metrics: OpRBuilderMetrics,
execution_outcome: ExecutionOutcome<OpReceipt>,
}

impl Monitoring {
pub fn new(builder_signer: Option<Signer>) -> Self {
pub fn new(builder_signer: Option<OpSigner>) -> Self {
Self {
builder_signer,
metrics: Default::default(),
@@ -140,7 +140,7 @@ impl Monitoring {
/// Decode chain of blocks and filter list to builder txs
fn decode_chain_into_builder_txs(
chain: &Chain<OpPrimitives>,
builder_signer: Option<Signer>,
builder_signer: Option<OpSigner>,
) -> Vec<(&RecoveredBlock<Block<OpTransactionSigned>>, bool)> {
chain
// Get all blocks and receipts
@@ -186,7 +186,7 @@ fn decode_chain_into_reverted_txs(chain: &Chain<OpPrimitives>) -> usize {
/// Decode state and find the last builder balance
fn decode_state_into_builder_balance(
execution_outcome: &ExecutionOutcome<OpReceipt>,
builder_signer: Option<Signer>,
builder_signer: Option<OpSigner>,
) -> Option<U256> {
builder_signer.and_then(|signer| {
execution_outcome
158 changes: 99 additions & 59 deletions crates/op-rbuilder/src/payload_builder.rs

Large diffs are not rendered by default.

1,125 changes: 426 additions & 699 deletions crates/op-rbuilder/src/payload_builder_vanilla.rs

Large diffs are not rendered by default.

47 changes: 31 additions & 16 deletions crates/op-rbuilder/src/primitives/reth/execution.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
//! Heavily influenced by [reth](https://github.com/paradigmxyz/reth/blob/1e965caf5fa176f244a31c0d2662ba1b590938db/crates/optimism/payload/src/builder.rs#L570)
use alloy_consensus::Transaction;
use alloy_primitives::{private::alloy_rlp::Encodable, Address, TxHash, B256, U256};
use alloy_primitives::private::alloy_rlp::Encodable;
use alloy_primitives::{Address, TxHash, B256, U256};
use reth_node_api::NodePrimitives;
use reth_optimism_primitives::OpReceipt;
use revm::context::BlockEnv;
use std::collections::HashSet;

/// Holds the state after execution
#[derive(Debug)]
#[allow(dead_code)]
pub struct ExecutedPayload<N: NodePrimitives> {
/// Tracked execution info
pub info: ExecutionInfo<N>,
pub info: ExecutionInfo,
/// Withdrawal hash.
pub withdrawals_root: Option<B256>,
/// executed transactions
pub executed_transactions: Vec<N::SignedTx>,
/// executed senders
pub executed_senders: Vec<Address>,
/// The transaction receipts.
pub receipts: Vec<N::Receipt>,
/// The block env used during execution.
pub block_env: BlockEnv,
}

impl<N: NodePrimitives> ExecutedPayload<N> {
/// Create a new instance with allocated slots.
#[allow(dead_code)]
pub fn with_capacity(capacity: usize) -> Self {
Self {
info: ExecutionInfo::new(),
withdrawals_root: None,
executed_transactions: Vec::with_capacity(capacity),
executed_senders: Vec::with_capacity(capacity),
receipts: Vec::with_capacity(capacity),
block_env: BlockEnv::default(),
}
}
}
#[derive(Default, Debug)]
pub struct ExecutionInfo<N: NodePrimitives> {
/// All executed transactions (unrecovered).
pub executed_transactions: Vec<N::SignedTx>,
/// The recovered senders for the executed transactions.
pub executed_senders: Vec<Address>,
/// The transaction receipts
pub receipts: Vec<OpReceipt>,
pub struct ExecutionInfo {
/// All gas used so far
pub cumulative_gas_used: u64,
/// Estimated DA size
@@ -35,13 +53,10 @@ pub struct ExecutionInfo<N: NodePrimitives> {
pub last_flashblock_index: usize,
}

impl<N: NodePrimitives> ExecutionInfo<N> {
impl ExecutionInfo {
/// Create a new instance with allocated slots.
pub fn with_capacity(capacity: usize) -> Self {
pub fn new() -> Self {
Self {
executed_transactions: Vec::with_capacity(capacity),
executed_senders: Vec::with_capacity(capacity),
receipts: Vec::with_capacity(capacity),
cumulative_gas_used: 0,
cumulative_da_bytes_used: 0,
total_fees: U256::ZERO,
@@ -59,7 +74,7 @@ impl<N: NodePrimitives> ExecutionInfo<N> {
/// maximum allowed DA limit per block.
pub fn is_tx_over_limits(
&self,
tx: &N::SignedTx,
tx: &(impl Encodable + Transaction),
block_gas_limit: u64,
tx_data_limit: Option<u64>,
block_data_limit: Option<u64>,
4 changes: 3 additions & 1 deletion crates/op-rbuilder/src/primitives/reth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
mod execution;
pub use execution::{ExecutedPayload, ExecutionInfo};
#[allow(unused_imports)]
pub use execution::ExecutedPayload;
pub use execution::ExecutionInfo;
16 changes: 9 additions & 7 deletions crates/op-rbuilder/src/tester/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::tx_signer::Signer;
use alloy_eips::eip2718::Encodable2718;
use alloy_eips::BlockNumberOrTag;
use alloy_eips::Encodable2718;
use alloy_primitives::address;
use alloy_primitives::hex;
use alloy_primitives::Address;
use alloy_primitives::Bytes;
use alloy_primitives::TxKind;
use alloy_primitives::B256;
use alloy_primitives::{hex, U256};
use alloy_primitives::U256;
use alloy_rpc_types_engine::ExecutionPayloadV1;
use alloy_rpc_types_engine::ExecutionPayloadV2;
use alloy_rpc_types_engine::PayloadAttributes;
@@ -31,6 +31,8 @@ use std::str::FromStr;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;

use crate::tx_signer::OpSigner;

/// Helper for engine api operations
pub struct EngineApi {
pub engine_api_client: HttpClient<AuthClientService<HttpBackend>>,
@@ -350,12 +352,12 @@ impl<'a> BlockGenerator<'a> {
mint: None,
value: U256::default(),
gas_limit: 210000,
is_system_transaction: true,
is_system_transaction: false,
input: FJORD_DATA.into(),
};

// Create a temporary signer for the deposit
let signer = Signer::random();
let signer = OpSigner::random();
let signed_tx = signer.sign_tx(OpTypedTransaction::Deposit(deposit_tx))?;
signed_tx.encoded_2718().into()
};
@@ -471,12 +473,12 @@ impl<'a> BlockGenerator<'a> {
mint: Some(value), // Amount to deposit
value: U256::default(),
gas_limit: 210000,
is_system_transaction: true,
is_system_transaction: false,
input: Bytes::default(),
};

// Create a temporary signer for the deposit
let signer = Signer::random();
let signer = OpSigner::random();
let signed_tx = signer.sign_tx(OpTypedTransaction::Deposit(deposit_tx))?;
let signed_tx_rlp = signed_tx.encoded_2718();

38 changes: 29 additions & 9 deletions crates/op-rbuilder/src/tx_signer.rs
Original file line number Diff line number Diff line change
@@ -3,44 +3,64 @@ use std::str::FromStr;
use alloy_consensus::SignableTransaction;
use alloy_primitives::{Address, PrimitiveSignature as Signature, B256, U256};
use op_alloy_consensus::OpTypedTransaction;
use op_alloy_rpc_types::OpTransactionRequest;
use reth_optimism_primitives::OpTransactionSigned;
use reth_primitives::{public_key_to_address, Recovered};
use secp256k1::{Message, SecretKey, SECP256K1};
use secp256k1::{ecdsa::RecoveryId, Message, SecretKey, SECP256K1};

#[derive(Debug, thiserror::Error)]
pub enum SignerError {
#[error("failed to sign tx: {0}")]
FailedToSignTx(#[from] secp256k1::Error),
#[error("failed to build tx")]
FailedToBuildTx,
}

/// Simple struct to sign txs/messages.
/// Mainly used to sign payout txs from the builder and to create test data.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Signer {
pub struct OpSigner {
pub address: Address,
pub secret: SecretKey,
}

impl Signer {
pub fn try_from_secret(secret: B256) -> Result<Self, secp256k1::Error> {
impl OpSigner {
pub fn try_from_secret(secret: B256) -> Result<Self, SignerError> {
let secret = SecretKey::from_slice(secret.as_ref())?;
let pubkey = secret.public_key(SECP256K1);
let address = public_key_to_address(pubkey);

Ok(Self { address, secret })
}

pub fn sign_message(&self, message: B256) -> Result<Signature, secp256k1::Error> {
pub fn sign_message(&self, message: B256) -> Result<Signature, SignerError> {
let s = SECP256K1
.sign_ecdsa_recoverable(&Message::from_digest_slice(&message[..])?, &self.secret);
let (rec_id, data) = s.serialize_compact();

let signature = Signature::new(
U256::try_from_be_slice(&data[..32]).expect("The slice has at most 32 bytes"),
U256::try_from_be_slice(&data[32..64]).expect("The slice has at most 32 bytes"),
i32::from(rec_id) != 0,
rec_id != RecoveryId::Zero,
);
Ok(signature)
}

pub fn build_and_sign_tx(
&self,
request: alloy_rpc_types_eth::TransactionRequest,
) -> Result<Recovered<OpTransactionSigned>, SignerError> {
let request: OpTransactionRequest = request.into();
let Ok(tx) = request.build_typed_tx() else {
return Err(SignerError::FailedToBuildTx);
};
self.sign_tx(tx)
}

pub fn sign_tx(
&self,
tx: OpTypedTransaction,
) -> Result<Recovered<OpTransactionSigned>, secp256k1::Error> {
) -> Result<Recovered<OpTransactionSigned>, SignerError> {
let signature_hash = match &tx {
OpTypedTransaction::Legacy(tx) => tx.signature_hash(),
OpTypedTransaction::Eip2930(tx) => tx.signature_hash(),
@@ -58,7 +78,7 @@ impl Signer {
}
}

impl FromStr for Signer {
impl FromStr for OpSigner {
type Err = eyre::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -78,7 +98,7 @@ mod test {
let secret =
fixed_bytes!("7a3233fcd52c19f9ffce062fd620a8888930b086fba48cfea8fc14aac98a4dce");
let address = address!("B2B9609c200CA9b7708c2a130b911dabf8B49B20");
let signer = Signer::try_from_secret(secret).expect("signer creation");
let signer = OpSigner::try_from_secret(secret).expect("signer creation");
assert_eq!(signer.address, address);

let tx = OpTypedTransaction::Eip1559(TxEip1559 {
2 changes: 0 additions & 2 deletions crates/rbuilder/Cargo.toml
Original file line number Diff line number Diff line change
@@ -151,8 +151,6 @@ assert_matches = "1.5.0"
criterion = { version = "0.5.1", features = ["html_reports", "async_tokio"] }

[features]
# TODO: remove?
optimism = []
redact-sensitive = []

[[bench]]
9 changes: 1 addition & 8 deletions crates/rbuilder/src/live_builder/mod.rs
Original file line number Diff line number Diff line change
@@ -357,15 +357,8 @@ where
Ok(())
}

// Currently we only need two timings config, depending on whether rbuilder is being
// used in the optimism context. If further customisation is required in the future
// this should be improved on.
fn timings(&self) -> TimingsConfig {
if cfg!(feature = "optimism") {
TimingsConfig::optimism()
} else {
TimingsConfig::ethereum()
}
TimingsConfig::ethereum()
}
}

4 changes: 2 additions & 2 deletions crates/rbuilder/src/utils/tx_signer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use alloy_consensus::SignableTransaction;
use alloy_primitives::{Address, PrimitiveSignature as Signature, B256, U256};
use reth_primitives::{public_key_to_address, Recovered, Transaction, TransactionSigned};
use secp256k1::{Message, SecretKey, SECP256K1};
use secp256k1::{ecdsa::RecoveryId, Message, SecretKey, SECP256K1};

/// Simple struct to sign txs/messages.
/// Mainly used to sign payout txs from the builder and to create test data.
@@ -28,7 +28,7 @@ impl Signer {
let signature = Signature::new(
U256::try_from_be_slice(&data[..32]).expect("The slice has at most 32 bytes"),
U256::try_from_be_slice(&data[32..64]).expect("The slice has at most 32 bytes"),
i32::from(rec_id) != 0,
rec_id != RecoveryId::Zero,
);
Ok(signature)
}