diff --git a/Dockerfile b/Dockerfile index d763616..f59d9fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,16 @@ FROM lukemathwalker/cargo-chef:latest AS chef WORKDIR /app +# Install system dependencies RUN apt-get update && apt-get -y upgrade && apt-get install -y libclang-dev pkg-config +# Prepare build plan FROM chef AS planner COPY ./Cargo.toml ./Cargo.lock ./ COPY ./src ./src RUN cargo chef prepare +# Build application FROM chef AS builder COPY --from=planner /app/recipe.json . RUN cargo chef cook --release @@ -15,6 +18,9 @@ COPY . . RUN cargo build --release FROM debian:stable-slim AS runtime + WORKDIR /app + COPY --from=builder /app/target/release/rollup-boost /usr/local/bin/ + ENTRYPOINT ["/usr/local/bin/rollup-boost"] \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 771c641..18f1edd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -159,7 +159,7 @@ mod tests { use super::*; - const AUTH_PORT: u32 = 8551; + const AUTH_PORT: u32 = 8550; const AUTH_ADDR: &str = "0.0.0.0"; const SECRET: &str = "f79ae8046bc11c9927afe911db7143c51a806c4a537cc08e0d37140b0192f430"; diff --git a/src/server.rs b/src/server.rs index 3190e2f..c40150d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -4,15 +4,13 @@ use alloy_rpc_types_engine::{ PayloadStatus, }; use jsonrpsee::core::{async_trait, ClientError, RpcResult}; -use jsonrpsee::http_client::transport::HttpBackend; -use jsonrpsee::http_client::HttpClient; use jsonrpsee::proc_macros::rpc; use jsonrpsee::types::error::INVALID_REQUEST_CODE; use jsonrpsee::types::{ErrorCode, ErrorObject}; use op_alloy_rpc_types_engine::{ AsInnerPayload, OptimismExecutionPayloadEnvelopeV3, OptimismPayloadAttributes, }; -use reth_rpc_layer::AuthClientService; + use std::sync::Arc; use tracing::{error, info}; @@ -40,18 +38,17 @@ pub trait EngineApi { ) -> RpcResult; } -pub struct EthEngineApi> { - l2_client: Arc>, - builder_client: Arc>, +pub struct EthEngineApi { + l2_client: Arc, + builder_client: Arc, boost_sync: bool, } -impl EthEngineApi { - pub fn new( - l2_client: Arc>, - builder_client: Arc>, - boost_sync: bool, - ) -> Self { +impl EthEngineApi +where + C: EngineApiClient, +{ + pub fn new(l2_client: Arc, builder_client: Arc, boost_sync: bool) -> Self { Self { l2_client, builder_client, @@ -61,7 +58,10 @@ impl EthEngineApi { } #[async_trait] -impl EngineApiServer for EthEngineApi { +impl EngineApiServer for EthEngineApi +where + C: EngineApiClient + Send + Sync + 'static, +{ async fn fork_choice_updated_v3( &self, fork_choice_state: ForkchoiceState, @@ -155,7 +155,6 @@ impl EngineApiServer for EthEngineApi { }); let (l2_payload, builder_payload) = tokio::join!(l2_client_future, builder_client_future); - builder_payload.or(l2_payload).map_err(|e| match e { ClientError::Call(err) => err, // Already an ErrorObjectOwned, so just return it other_error => { @@ -214,3 +213,327 @@ impl EngineApiServer for EthEngineApi { }) } } + +#[cfg(test)] +mod tests { + use std::net::SocketAddr; + use std::sync::Mutex; + + use super::*; + + use alloy::hex; + use alloy_primitives::{FixedBytes, U256}; + use alloy_rpc_types_engine::{ + BlobsBundleV1, ExecutionPayloadV1, ExecutionPayloadV2, PayloadStatusEnum, + }; + use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; + use jsonrpsee::server::{ServerBuilder, ServerHandle}; + use jsonrpsee::RpcModule; + + const L2_ADDR: &str = "0.0.0.0:8554"; + const BUILDER_ADDR: &str = "0.0.0.0:8555"; + const SERVER_ADDR: &str = "0.0.0.0:8556"; + + #[derive(Debug, Clone)] + pub struct MockEngineServer { + fcu_requests: Arc)>>>, + get_payload_requests: Arc>>, + new_payload_requests: Arc, B256)>>>, + fcu_response: RpcResult, + get_payload_response: RpcResult, + new_payload_response: RpcResult, + } + + impl MockEngineServer { + pub fn new() -> Self { + Self { + fcu_requests: Arc::new(Mutex::new(vec![])), + get_payload_requests: Arc::new(Mutex::new(vec![])), + new_payload_requests: Arc::new(Mutex::new(vec![])), + fcu_response: Ok(ForkchoiceUpdated::new(PayloadStatus::from_status(PayloadStatusEnum::Valid))), + get_payload_response: Ok(OptimismExecutionPayloadEnvelopeV3{ + execution_payload: ExecutionPayloadV3 { + payload_inner: ExecutionPayloadV2 { + payload_inner: ExecutionPayloadV1 { + base_fee_per_gas: U256::from(7u64), + block_number: 0xa946u64, + block_hash: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(), + logs_bloom: hex!("00200004000000000000000080000000000200000000000000000000000000000000200000000000000000000000000000000000800000000200000000000000000000000000000000000008000000200000000000000000000001000000000000000000000000000000800000000000000000000100000000000030000000000000000040000000000000000000000000000000000800080080404000000000000008000000000008200000000000200000000000000000000000000000000000000002000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000").into(), + extra_data: hex!("d883010d03846765746888676f312e32312e31856c696e7578").into(), + gas_limit: 0x1c9c380, + gas_used: 0x1f4a9, + timestamp: 0x651f35b8, + fee_recipient: hex!("f97e180c050e5ab072211ad2c213eb5aee4df134").into(), + parent_hash: hex!("d829192799c73ef28a7332313b3c03af1f2d5da2c36f8ecfafe7a83a3bfb8d1e").into(), + prev_randao: hex!("753888cc4adfbeb9e24e01c84233f9d204f4a9e1273f0e29b43c4c148b2b8b7e").into(), + receipts_root: hex!("4cbc48e87389399a0ea0b382b1c46962c4b8e398014bf0cc610f9c672bee3155").into(), + state_root: hex!("017d7fa2b5adb480f5e05b2c95cb4186e12062eed893fc8822798eed134329d1").into(), + transactions: vec![], + }, + withdrawals: vec![], + }, + blob_gas_used: 0xc0000, + excess_blob_gas: 0x580000, + }, + block_value: U256::from(0), + blobs_bundle: BlobsBundleV1{ + commitments: vec![], + proofs: vec![], + blobs: vec![], + }, + should_override_builder: false, + parent_beacon_block_root: B256::ZERO, + }), + new_payload_response: Ok(PayloadStatus::from_status(PayloadStatusEnum::Valid)), + } + } + } + + struct TestHarness { + l2_server: ServerHandle, + l2_mock: MockEngineServer, + builder_server: ServerHandle, + builder_mock: MockEngineServer, + proxy_server: ServerHandle, + client: HttpClient, + } + + impl TestHarness { + async fn new( + boost_sync: bool, + l2_mock: Option, + builder_mock: Option, + ) -> Self { + let l2_client = HttpClientBuilder::new() + .build(format!("http://{L2_ADDR}")) + .unwrap(); + let builder_client = HttpClientBuilder::new() + .build(format!("http://{BUILDER_ADDR}")) + .unwrap(); + + let eth_engine_api = + EthEngineApi::new(Arc::new(l2_client), Arc::new(builder_client), boost_sync); + let mut module: RpcModule<()> = RpcModule::new(()); + module.merge(eth_engine_api.into_rpc()).unwrap(); + + let proxy_server = ServerBuilder::default() + .build("0.0.0.0:8556".parse::().unwrap()) + .await + .unwrap() + .start(module); + let l2_mock = l2_mock.unwrap_or(MockEngineServer::new()); + let builder_mock = builder_mock.unwrap_or(MockEngineServer::new()); + let l2_server = spawn_server(l2_mock.clone(), L2_ADDR).await; + let builder_server = spawn_server(builder_mock.clone(), BUILDER_ADDR).await; + TestHarness { + l2_server, + l2_mock, + builder_server, + builder_mock, + proxy_server, + client: HttpClient::builder() + .build(format!("http://{SERVER_ADDR}")) + .unwrap(), + } + } + + async fn cleanup(self) { + self.l2_server.stop().unwrap(); + self.l2_server.stopped().await; + self.builder_server.stop().unwrap(); + self.builder_server.stopped().await; + self.proxy_server.stop().unwrap(); + self.proxy_server.stopped().await; + } + } + + #[tokio::test] + async fn test_server() { + engine_success().await; + boost_sync_enabled().await; + builder_payload_err().await; + } + + async fn engine_success() { + let test_harness = TestHarness::new(false, None, None).await; + + // test fork_choice_updated_v3 success + let fcu = ForkchoiceState { + head_block_hash: FixedBytes::random(), + safe_block_hash: FixedBytes::random(), + finalized_block_hash: FixedBytes::random(), + }; + let fcu_response = test_harness.client.fork_choice_updated_v3(fcu, None).await; + assert!(fcu_response.is_ok()); + let fcu_requests = test_harness.l2_mock.fcu_requests.clone(); + let fcu_requests_mu = fcu_requests.lock().unwrap(); + let fcu_requests_builder = test_harness.builder_mock.fcu_requests.clone(); + let fcu_requests_builder_mu = fcu_requests_builder.lock().unwrap(); + assert_eq!(fcu_requests_mu.len(), 1); + assert_eq!(fcu_requests_builder_mu.len(), 0); + let req: &(ForkchoiceState, Option) = + fcu_requests_mu.get(0).unwrap(); + assert_eq!(req.0, fcu); + assert_eq!(req.1, None); + + // test new_payload_v3 success + let new_payload_response = test_harness + .client + .new_payload_v3( + test_harness + .l2_mock + .get_payload_response + .clone() + .unwrap() + .execution_payload + .clone(), + vec![], + B256::ZERO, + ) + .await; + assert!(new_payload_response.is_ok()); + let new_payload_requests = test_harness.l2_mock.new_payload_requests.clone(); + let new_payload_requests_mu = new_payload_requests.lock().unwrap(); + let new_payload_requests_builder = test_harness.builder_mock.new_payload_requests.clone(); + let new_payload_requests_builder_mu = new_payload_requests_builder.lock().unwrap(); + assert_eq!(new_payload_requests_mu.len(), 1); + assert_eq!(new_payload_requests_builder_mu.len(), 0); + let req: &(ExecutionPayloadV3, Vec>, B256) = + new_payload_requests_mu.get(0).unwrap(); + assert_eq!( + req.0, + test_harness + .l2_mock + .get_payload_response + .clone() + .unwrap() + .execution_payload + .clone() + ); + assert_eq!(req.1, Vec::>::new()); + assert_eq!(req.2, B256::ZERO); + drop(new_payload_requests_mu); + + // test get_payload_v3 success + let get_payload_response = test_harness + .client + .get_payload_v3(PayloadId::new([0, 0, 0, 0, 0, 0, 0, 1])) + .await; + assert!(get_payload_response.is_ok()); + let get_payload_requests = test_harness.l2_mock.get_payload_requests.clone(); + let get_payload_requests_mu = get_payload_requests.lock().unwrap(); + let get_payload_requests_builder = test_harness.builder_mock.get_payload_requests.clone(); + let get_payload_requests_builder_mu = get_payload_requests_builder.lock().unwrap(); + let new_payload_requests = test_harness.l2_mock.new_payload_requests.clone(); + let new_payload_requests_mu = new_payload_requests.lock().unwrap(); + assert_eq!(get_payload_requests_builder_mu.len(), 1); + assert_eq!(get_payload_requests_mu.len(), 1); + assert_eq!(new_payload_requests_mu.len(), 2); + let req: &PayloadId = get_payload_requests_mu.get(0).unwrap(); + assert_eq!(*req, PayloadId::new([0, 0, 0, 0, 0, 0, 0, 1])); + + test_harness.cleanup().await; + } + + async fn boost_sync_enabled() { + let test_harness = TestHarness::new(true, None, None).await; + + // test fork_choice_updated_v3 success + let fcu = ForkchoiceState { + head_block_hash: FixedBytes::random(), + safe_block_hash: FixedBytes::random(), + finalized_block_hash: FixedBytes::random(), + }; + let fcu_response = test_harness.client.fork_choice_updated_v3(fcu, None).await; + assert!(fcu_response.is_ok()); + let fcu_requests = test_harness.l2_mock.fcu_requests.clone(); + let fcu_requests_mu = fcu_requests.lock().unwrap(); + let fcu_requests_builder = test_harness.builder_mock.fcu_requests.clone(); + let fcu_requests_builder_mu = fcu_requests_builder.lock().unwrap(); + assert_eq!(fcu_requests_mu.len(), 1); + assert_eq!(fcu_requests_builder_mu.len(), 1); + + // test new_payload_v3 success + let new_payload_response = test_harness + .client + .new_payload_v3( + test_harness + .l2_mock + .get_payload_response + .clone() + .unwrap() + .execution_payload + .clone(), + vec![], + B256::ZERO, + ) + .await; + assert!(new_payload_response.is_ok()); + let new_payload_requests = test_harness.l2_mock.new_payload_requests.clone(); + let new_payload_requests_mu = new_payload_requests.lock().unwrap(); + let new_payload_requests_builder = test_harness.builder_mock.new_payload_requests.clone(); + let new_payload_requests_builder_mu = new_payload_requests_builder.lock().unwrap(); + assert_eq!(new_payload_requests_mu.len(), 1); + assert_eq!(new_payload_requests_builder_mu.len(), 1); + + test_harness.cleanup().await; + } + + async fn builder_payload_err() { + let mut l2_mock = MockEngineServer::new(); + l2_mock.new_payload_response = l2_mock.new_payload_response.clone().map(|mut status| { + status.status = PayloadStatusEnum::Invalid { + validation_error: "test".to_string(), + }; + status + }); + l2_mock.get_payload_response = l2_mock.get_payload_response.clone().map(|mut payload| { + payload.block_value = U256::from(10); + payload + }); + let test_harness = TestHarness::new(true, Some(l2_mock), None).await; + + // test get_payload_v3 return l2 payload if builder payload is invalid + let get_payload_response = test_harness + .client + .get_payload_v3(PayloadId::new([0, 0, 0, 0, 0, 0, 0, 0])) + .await; + assert!(get_payload_response.is_ok()); + assert_eq!(get_payload_response.unwrap().block_value, U256::from(10)); + + test_harness.cleanup().await; + } + + async fn spawn_server(mock_engine_server: MockEngineServer, addr: &str) -> ServerHandle { + let server = ServerBuilder::default().build(addr).await.unwrap(); + let mut module: RpcModule<()> = RpcModule::new(()); + module + .register_method("engine_forkchoiceUpdatedV3", move |params, _, _| { + let params: (ForkchoiceState, Option) = + params.parse()?; + let mut fcu_requests = mock_engine_server.fcu_requests.lock().unwrap(); + fcu_requests.push(params); + mock_engine_server.fcu_response.clone() + }) + .unwrap(); + module + .register_method("engine_getPayloadV3", move |params, _, _| { + let params: (PayloadId,) = params.parse()?; + let mut get_payload_requests = + mock_engine_server.get_payload_requests.lock().unwrap(); + get_payload_requests.push(params.0); + mock_engine_server.get_payload_response.clone() + }) + .unwrap(); + module + .register_method("engine_newPayloadV3", move |params, _, _| { + let params: (ExecutionPayloadV3, Vec, B256) = params.parse()?; + let mut new_payload_requests = + mock_engine_server.new_payload_requests.lock().unwrap(); + new_payload_requests.push(params); + mock_engine_server.new_payload_response.clone() + }) + .unwrap(); + server.start(module) + } +}