Skip to content
Merged
Show file tree
Hide file tree
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
25 changes: 17 additions & 8 deletions .github/workflows/sf-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
# Maintained by StreamingFast and intentionally kept separate from upstream CI
# so that upstream merges stay clean.
#
# Triggers: `firehose/*` branch pushes, `*-fh*` tags, pull requests and manual
# dispatch. Pull requests opened from a fork get a read-only GITHUB_TOKEN, so the
# login/push steps are skipped for them (the image still builds, to validate the
# Dockerfile); same-repo ("inner") PRs and pushes authenticate and push normally.
# Triggers: `*-fh*` tags, pull requests and manual dispatch. Branch pushes do not
# build — open a PR (or push a tag) to get an image, which keeps every commit to a
# single build. Pull requests opened from a fork get a read-only GITHUB_TOKEN, so
# the login/push steps are skipped for them (the image still builds, to validate
# the Dockerfile); same-repo ("inner") PRs and tag pushes authenticate and push
# normally. Only tag builds use the full-LTO `maxperf` profile.
#
# Single architecture (linux/amd64): the op-reth `maxperf` full-node build is too
# heavy to reliably build on GitHub-hosted arm64 runners, so we do not produce a
Expand All @@ -16,8 +18,6 @@ name: Build, push and release (if tag)

on:
push:
branches:
- "firehose/*"
tags:
- "*-fh*"
pull_request:
Expand All @@ -41,8 +41,8 @@ jobs:
packages: write

env:
# Pushes (branch/tag), same-repo PRs and manual dispatch can authenticate
# to ghcr and push images. A PR opened from a fork gets a read-only
# Tag pushes, same-repo PRs and manual dispatch can authenticate to ghcr
# and push images. A PR opened from a fork gets a read-only
# GITHUB_TOKEN with no `packages: write`, so we must skip login and push for
# it — the build still runs to validate the Dockerfile compiles. The value
# is the string "true"/"false".
Expand Down Expand Up @@ -122,9 +122,18 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: false
# Cache the cargo-chef dependency layer across runs (ephemeral runners
# otherwise cold-rebuild all deps every time, ~13m). Keyed per profile by
# buildx, so PR (fast-build) and tag (maxperf) caches stay separate.
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ steps.extract-version.outputs.VERSION }}
FIREHOSE_ETHEREUM=${{ inputs.firehose_ethereum || 'latest' }}
# Full LTO `maxperf` is only worth its ~19m link tail for shippable
# builds (tags); PRs and manual dispatch use `fast-build` (no LTO) to
# validate faster.
BUILD_PROFILE=${{ startsWith(github.ref, 'refs/tags/') && 'maxperf' || 'fast-build' }}
Comment on lines 130 to +136

release:
if: startsWith(github.ref, 'refs/tags/')
Expand Down
1 change: 1 addition & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 22 additions & 2 deletions rust/op-reth/crates/evm/src/post_exec_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ use op_revm::OpSpecId;
use reth_chainspec::EthChainSpec;
use reth_evm::{
ConfigureEvm, Database,
execute::{BasicBlockBuilder, BlockBuilder},
execute::{BasicBlockBuilder, BlockBuilder, BlockExecutionError},
precompiles::PrecompilesMap,
};
use reth_optimism_forks::OpHardforks;
use reth_optimism_primitives::DepositReceipt;
use reth_primitives_traits::{NodePrimitives, SealedBlock, SealedHeader, SignedTransaction};
use reth_primitives_traits::{
NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader, SignedTransaction,
};
use revm::{context::BlockEnv, database::State};

use crate::{OpBlockExecutorFactory, OpEvmConfig, OpEvmFactory, OpTx, PostExecMode};
Expand Down Expand Up @@ -65,6 +67,24 @@ pub trait ConfigurePostExecEvm: ConfigureEvm {
> + 'a,
Self::Error,
>;

/// Firehose hook: re-execute an already-built block through the tracing executor so it
/// emits a `FIRE BLOCK`.
///
/// The OP payload builder constructs derivation (`no_tx_pool`) blocks via `getPayload`,
/// which never pass through the traced `newPayload` engine path, so those blocks are
/// otherwise invisible to Firehose. This hook lets a Firehose-aware EVM config re-run such
/// a block against a fresh parent `state` and emit it. The default is a no-op; only the
/// Firehose wrapper overrides it.
///
/// `state` must be a fresh [`State`] over the block's parent.
fn firehose_trace_built_block<DB: Database>(
&self,
_state: &mut State<DB>,
_block: &RecoveredBlock<<Self::Primitives as NodePrimitives>::Block>,
) -> Result<(), BlockExecutionError> {
Ok(())
}
}

impl<ChainSpec, N, R> ConfigurePostExecEvm for OpEvmConfig<ChainSpec, N, R>
Expand Down
46 changes: 45 additions & 1 deletion rust/op-reth/crates/firehose/src/evm_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use reth_evm::{
};
use reth_firehose::{
ChainHooks, FirehoseBlockExecutor, FirehoseBlockTracer, FirehoseWrappedExecutor,
mapper::SignatureFields,
is_tracer_initialized, mapper::SignatureFields,
};
use reth_optimism_evm::{ConfigurePostExecEvm, OpTx, PostExecExecutorExt, PostExecMode};
use reth_optimism_primitives::OpPrimitives;
Expand Down Expand Up @@ -283,4 +283,48 @@ where
> {
self.inner.post_exec_builder_for_next_block(db, parent, attributes, post_exec_mode)
}

/// Re-executes a locally-built (derivation `no_tx_pool`) block through the tracing executor
/// so it emits a `FIRE BLOCK`.
///
/// Such blocks are produced via `getPayload` and inserted as canonical without passing
/// through the traced `newPayload` engine path, so this is their only tracing opportunity.
/// The re-execution is deterministic and reuses the exact [`OpChainHooks`] wrapping (same
/// `OpPostTxExtras` / `OpPreTxAdjust`) as the pipeline and engine paths, so the emitted block
/// is byte-identical to what those paths would produce. `state` must be a fresh [`State`]
/// over the block's parent.
fn firehose_trace_built_block<DB: Database>(
&self,
state: &mut State<DB>,
block: &RecoveredBlock<<Self::Primitives as NodePrimitives>::Block>,
) -> Result<(), BlockExecutionError> {
if !is_tracer_initialized() {
return Ok(());
}

let finalized = Some(firehose_tracer::types::FinalizedBlockRef {
number: block.header().number(),
hash: Some(block.hash()),
});
let mut tracer =
FirehoseBlockTracer::start::<Self::Primitives>(block.sealed_block(), finalized);

// The builder always extends an existing parent, so block 1 never reaches here; guard
// anyway since the wrapped executor would panic on the genesis marker.
if tracer.is_genesis() {
tracer.mark_verified();
return Ok(());
}
Comment on lines +314 to +317

match OpChainHooks.execute_one_traced(&self.inner, state, block, &mut tracer) {
Ok(_) => {
tracer.mark_verified();
Ok(())
}
Err(err) => {
tracer.mark_failed(&err);
Err(err)
}
}
}
}
1 change: 1 addition & 0 deletions rust/op-reth/crates/payload/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ reth-payload-primitives.workspace = true
reth-basic-payload-builder.workspace = true
reth-payload-validator.workspace = true
reth-metrics.workspace = true
reth-firehose.workspace = true

# op-reth
reth-optimism-evm.workspace = true
Expand Down
34 changes: 28 additions & 6 deletions rust/op-reth/crates/payload/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use reth_optimism_txpool::{
interop::{MaybeInteropTransaction, is_interop_tx, is_valid_interop},
};
use reth_payload_builder_primitives::PayloadBuilderError;
use reth_payload_primitives::{BuildNextEnv, BuiltPayloadExecutedBlock};
use reth_payload_primitives::{BuildNextEnv, BuiltPayload, BuiltPayloadExecutedBlock};
use reth_payload_util::{BestPayloadTransactions, NoopPayloadTransactions, PayloadTransactions};
use reth_primitives_traits::{
HeaderTy, NodePrimitives, SealedHeader, SealedHeaderFor, SignedTransaction, TxTy,
Expand Down Expand Up @@ -270,13 +270,35 @@ where
let state_provider = self.client.state_by_block_hash(ctx.parent().hash())?;
let state = StateProviderDatabase::new(&state_provider);

if ctx.attributes().no_tx_pool() {
builder.build(state, &state_provider, ctx)
let outcome = if ctx.attributes().no_tx_pool() {
let outcome = builder.build(state, &state_provider, ctx)?;

// Firehose: `no_tx_pool` (derivation) blocks are constructed here and inserted as
// canonical without going through the traced `newPayload` engine path, so they would
// otherwise never emit a `FIRE BLOCK`. Re-execute the frozen block through the tracing
// executor. Re-execution is confined to these built blocks (older, L1-derived range),
// never the latency-critical live gossip path. No-op unless the tracer is installed.
if reth_firehose::is_tracer_initialized() {
if let BuildOutcomeKind::Freeze(payload) = &outcome {
if let Some(executed) = payload.executed_block() {
let mut trace_state = State::builder()
.with_database(StateProviderDatabase::new(&state_provider))
.with_bundle_update()
.build();
self.evm_config
.firehose_trace_built_block(&mut trace_state, &executed.recovered_block)
.map_err(|err| PayloadBuilderError::Internal(err.into()))?;
}
}
}

outcome
} else {
// sequencer mode we can reuse cachedreads from previous runs
builder.build(cached_reads.as_db_mut(state), &state_provider, ctx)
}
.map(|out| out.with_cached_reads(cached_reads))
builder.build(cached_reads.as_db_mut(state), &state_provider, ctx)?
};

Ok(outcome.with_cached_reads(cached_reads))
}

/// Computes the witness for the payload.
Expand Down
Loading