Skip to content

Commit

Permalink
feat(rpc): add engine api abstraction, rm AttributesValidator (#102)
Browse files Browse the repository at this point in the history
This PR supercedes and replaces #98 

### Overview

* re-implemented engine functionality, using the latest Alloy jwt-auth
layer + OpEngineApi extension trait
* removed `AttributesValidator` trait because I don't think it will be
useful since we need to work with the engine API anyway for other
reasons, other than validating payloads: namely updating the fork choice
and driving the EL forward.

I believe the decision to remove the validator trait will pay off in the
future because it allows us to work with the engine api more freely.
However if it turns out to be a bad decision this can be reverted easily
as well.

---------

Co-authored-by: refcell <abigger87@gmail.com>
  • Loading branch information
merklefruit and refcell authored Oct 18, 2024
1 parent e6538c7 commit 798a949
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 158 deletions.
35 changes: 25 additions & 10 deletions Cargo.lock

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

28 changes: 17 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ op-alloy-genesis = { git = "https://github.com/alloy-rs/op-alloy", branch = "mai
op-alloy-rpc-types = { git = "https://github.com/alloy-rs/op-alloy", branch = "main" }
op-alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/op-alloy", branch = "main" }
op-alloy-rpc-jsonrpsee = { git = "https://github.com/alloy-rs/op-alloy", branch = "main" }
op-alloy-provider = { git = "https://github.com/alloy-rs/op-alloy", branch = "main" }

[workspace.dependencies]
# Workspace
ser = { path = "crates/ser" }
op-net = { path = "crates/net" }
rollup = { path = "crates/rollup" }
ser = { path = "crates/ser" }
kona-providers-local = { path = "crates/providers-local" }

# Kona
Expand All @@ -58,10 +59,13 @@ alloy-transport = { version = "0.4.2", default-features = false }
alloy-rpc-types = { version = "0.4.2", default-features = false }
alloy-consensus = { version = "0.4.2", default-features = false }
alloy-primitives = { version = "0.8.8", default-features = false }
alloy-rpc-client = { version = "0.4.2", default-features = false }
alloy-transport-http = { version = "0.4.2", default-features = false }
alloy-rpc-types-engine = { version = "0.4.2", default-features = false }

# Op Alloy
op-alloy-genesis = { version = "0.4.0", default-features = false }
op-alloy-provider = { version = "0.4.0", default-features = false }
op-alloy-protocol = { version = "0.4.0", default-features = false }
op-alloy-consensus = { version = "0.4.0", default-features = false }
op-alloy-rpc-types = { version = "0.4.0", default-features = false }
Expand All @@ -70,21 +74,21 @@ op-alloy-rpc-types-engine = { version = "0.4.0", default-features = false }

# Reth
reth = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-chainspec = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-evm = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-revm = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-exex = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-discv5 = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-execution-errors = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-execution-types = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-exex = { git = "https://github.com/paradigmxyz/reth", features = ["serde"], rev = "a846cbd" }
reth-network-peers = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-tracing = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-provider = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-node-api = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-chainspec = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-provider = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-revm = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-evm = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-tracing = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-rpc-eth-types = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-network-peers = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-execution-types = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }
reth-execution-errors = { git = "https://github.com/paradigmxyz/reth", rev = "a846cbd" }

# Tokio
tokio = { version = "1.21", default-features = false }
Expand Down Expand Up @@ -121,6 +125,8 @@ async-trait = "0.1.83"
hashbrown = "0.15.0"
parking_lot = "0.12.3"
unsigned-varint = "0.8.0"
tower = "0.5"
http-body-util = "0.1.2"
tracing-subscriber = "0.3.18"
rand = { version = "0.8.5", default-features = false }

Expand Down
9 changes: 8 additions & 1 deletion crates/rollup/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,27 @@ alloy-eips.workspace = true
alloy-primitives.workspace = true
alloy-provider = { workspace = true, features = ["ipc"] }
alloy-transport.workspace = true
alloy-transport-http = { workspace = true, features = ["jwt-auth"] }
alloy-network.workspace = true
alloy-consensus.workspace = true
alloy-rpc-types = { workspace = true, features = ["ssz"] }
alloy-rpc-client.workspace = true
alloy-rpc-types-engine.workspace = true

# Op Alloy
op-alloy-genesis.workspace = true
op-alloy-protocol.workspace = true
op-alloy-rpc-types-engine.workspace = true
op-alloy-provider.workspace = true

# Superchain
superchain = { workspace = true, default-features = false }

# Reth
reth.workspace = true
reth-exex.workspace = true
reth-node-api.workspace = true
reth-execution-types.workspace = true
reth-exex = { workspace = true, features = ["serde"] }

# Telemetry
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "fmt"] }
Expand All @@ -53,5 +56,9 @@ async-trait.workspace = true
tokio.workspace = true
futures.workspace = true
hashbrown.workspace = true
tower.workspace = true
http-body-util.workspace = true
clap = { workspace = true, features = ["derive", "env"] }

[dev-dependencies]
reqwest = { workspace = true, features = ["rustls-tls-native-roots"] }
19 changes: 7 additions & 12 deletions crates/rollup/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,17 @@ pub struct HeraArgsExt {
/// same block and comparing the results.
/// - Engine API: use a local or remote engine API of an L2 execution client. Validation
/// happens by sending the `new_payload` to the API and expecting a VALID response.
#[clap(
long = "hera.validation-mode",
default_value = "trusted",
requires_ifs([("engine-api", "l2_engine_api_url"), ("engine-api", "l2_engine_jwt_secret")]),
)]
#[clap(long = "hera.validation-mode", default_value = "engine-api")]
pub validation_mode: ValidationMode,

/// If the mode is "engine api", we also need an URL for the engine API endpoint of
/// the execution client to validate the payload.
#[clap(long = "hera.l2-engine-api-url")]
pub l2_engine_api_url: Option<Url>,
/// URL of the engine API endpoint of an L2 execution client.
#[clap(long = "hera.l2-engine-api-url", env = "L2_ENGINE_API_URL")]
pub l2_engine_api_url: Url,

/// If the mode is "engine api", we also need a JWT secret for the auth-rpc.
/// JWT secret for the auth-rpc endpoint of the execution client.
/// This MUST be a valid path to a file containing the hex-encoded JWT secret.
#[clap(long = "hera.l2-engine-jwt-secret")]
pub l2_engine_jwt_secret: Option<PathBuf>,
#[clap(long = "hera.l2-engine-jwt-secret", env = "L2_ENGINE_JWT_SECRET")]
pub l2_engine_jwt_secret: PathBuf,

/// The maximum **number of blocks** to keep cached in the chain provider.
///
Expand Down
81 changes: 49 additions & 32 deletions crates/rollup/src/driver/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Rollup Node Driver

use alloy_eips::eip1898::BlockNumHash;
use reth::rpc::types::engine::JwtSecret;
use std::{fmt::Debug, sync::Arc};

use eyre::{bail, eyre, Result};
Expand All @@ -14,16 +15,13 @@ use kona_providers_alloy::{AlloyChainProvider, AlloyL2ChainProvider, OnlineBlobP
use kona_providers_local::{DurableBlobProvider, InMemoryChainProvider, LayeredBlobProvider};
use op_alloy_genesis::RollupConfig;
use op_alloy_protocol::{BlockInfo, L2BlockInfo};
use reth::rpc::types::engine::JwtSecret;
use reth_exex::ExExContext;
use reth_node_api::FullNodeComponents;
use tracing::{debug, error, info, trace, warn};

use crate::{
cli::ValidationMode,
new_rollup_pipeline,
validator::{EngineApiValidator, TrustedValidator},
AttributesValidator, HeraArgsExt, RollupPipeline,
cli::ValidationMode, new_rollup_pipeline, validator::TrustedPayloadValidator, Engine,
HeraArgsExt, RollupPipeline,
};

mod context;
Expand All @@ -48,7 +46,9 @@ pub struct Driver<DC, CP, BP> {
/// Cursor to keep track of the L2 tip
cursor: SyncCursor,
/// The validator to verify newly derived L2 attributes
validator: Box<dyn AttributesValidator>,
trusted_validator: Option<TrustedPayloadValidator>,
/// The engine API handler
engine: Engine,
}

impl<N: FullNodeComponents> Driver<ExExHeraContext<N>, InMemoryChainProvider, LayeredBlobProvider> {
Expand Down Expand Up @@ -77,7 +77,7 @@ impl Driver<StandaloneHeraContext, AlloyChainProvider, DurableBlobProvider> {
.with_primary(args.l1_beacon_client_url.as_str().trim_end_matches('/').to_string())
.with_fallback(
args.l1_blob_archiver_url
.clone()
.as_ref()
.map(|url| url.as_str().trim_end_matches('/').to_string()),
)
.build();
Expand Down Expand Up @@ -105,23 +105,29 @@ where
l1_chain_provider: CP,
blob_provider: BP,
) -> Self {
let cursor = SyncCursor::new(cfg.channel_timeout);
let validator: Box<dyn AttributesValidator> = match args.validation_mode {
ValidationMode::Trusted => Box::new(TrustedValidator::new_http(
args.l2_rpc_url.clone(),
cfg.canyon_time.unwrap_or(0),
)),
ValidationMode::EngineApi => Box::new(EngineApiValidator::new_http(
args.l2_engine_api_url.expect("Missing L2 engine API URL"),
match args.l2_engine_jwt_secret.as_ref() {
Some(fpath) => JwtSecret::from_file(fpath).expect("Invalid L2 JWT secret file"),
None => panic!("Missing L2 engine JWT secret"),
},
)),
let trusted_validator = if matches!(args.validation_mode, ValidationMode::Trusted) {
let canyon_activation = cfg.canyon_time.unwrap_or(0);
Some(TrustedPayloadValidator::new_http(args.l2_rpc_url.clone(), canyon_activation))
} else {
None
};

let l2_jwt_secret = JwtSecret::from_file(&args.l2_engine_jwt_secret).expect("jwt secret");
let engine = Engine::new_http(args.l2_engine_api_url, l2_jwt_secret);

let cursor = SyncCursor::new(cfg.channel_timeout);
let l2_chain_provider = AlloyL2ChainProvider::new_http(args.l2_rpc_url, cfg.clone());

Self { cfg, ctx, l1_chain_provider, blob_provider, l2_chain_provider, cursor, validator }
Self {
cfg,
ctx,
l1_chain_provider,
l2_chain_provider,
blob_provider,
cursor,
trusted_validator,
engine,
}
}

/// Wait for the L2 genesis' corresponding L1 block to be available in the L1 chain.
Expand Down Expand Up @@ -184,11 +190,17 @@ where
},
}

let derived_attributes = if let Some(attributes) = pipeline.peek() {
match self.validator.validate(attributes).await {
let Some(derived_attributes) = pipeline.peek() else {
debug!("No attributes available to validate");
return false;
};
let derived_block_number = derived_attributes.parent.block_info.number + 1;

if let Some(trusted_validator) = &self.trusted_validator {
match trusted_validator.validate_payload(derived_attributes).await {
Ok(true) => {
trace!("Validated payload attributes");
pipeline.next().expect("Peeked attributes must be available")
pipeline.next().expect("Peeked attributes must be available");
}
Ok(false) => {
error!("Failed payload attributes validation");
Expand All @@ -202,12 +214,14 @@ where
}
}
} else {
debug!("No attributes available to validate");
return false;
};
if let Err(err) = self.engine.validate_payload_fcu(derived_attributes).await {
error!("Failed to validate payload attributes: {:?}", err);
return false;
}
pipeline.next().expect("Peeked attributes must be available");
}

let derived = derived_attributes.parent.block_info.number + 1;
let (new_l1_origin, new_l2_tip) = match self.fetch_new_tip(derived).await {
let (new_l1_origin, new_l2_tip) = match self.fetch_new_tip(derived_block_number).await {
Ok(tip_info) => tip_info,
Err(err) => {
// TODO: add a retry mechanism?
Expand All @@ -217,14 +231,17 @@ where
};

// Perform a sanity check on the new tip
if new_l2_tip.block_info.number != derived {
error!("Expected L2 block number {} but got {}", derived, new_l2_tip.block_info.number);
if new_l2_tip.block_info.number != derived_block_number {
error!(
"Expected L2 block {} but got {}",
derived_block_number, new_l2_tip.block_info.number
);
return false;
}

// Advance the cursor to the new L2 block
self.cursor.advance(new_l1_origin, new_l2_tip);
info!("Advanced derivation pipeline to L2 block: {}", derived);
info!("Advanced derivation pipeline to L2 block: {}", derived_block_number);
true
}

Expand Down
Loading

0 comments on commit 798a949

Please sign in to comment.