diff --git a/Cargo.lock b/Cargo.lock index 116509c1d..d31d266dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5684,6 +5684,7 @@ dependencies = [ "alloy-chains", "alloy-rlp", "aquamarine", + "assert_matches", "backon", "boyer-moore-magiclen", "clap", @@ -5702,6 +5703,7 @@ dependencies = [ "itertools 0.12.0", "jemalloc-ctl", "jemallocator", + "jsonrpsee", "metrics", "metrics-exporter-prometheus", "metrics-process", diff --git a/bin/reth/Cargo.toml b/bin/reth/Cargo.toml index 0508764c8..ff09ef2d9 100644 --- a/bin/reth/Cargo.toml +++ b/bin/reth/Cargo.toml @@ -116,6 +116,10 @@ itertools.workspace = true rayon.workspace = true futures-util.workspace = true +[dev-dependencies] +jsonrpsee.workspace = true +assert_matches = "1.5.0" + [target.'cfg(not(windows))'.dependencies] jemallocator = { version = "0.5.0", optional = true } jemalloc-ctl = { version = "0.5.0", optional = true } diff --git a/bin/reth/src/builder/mod.rs b/bin/reth/src/builder/mod.rs index 90fda562a..61d436142 100644 --- a/bin/reth/src/builder/mod.rs +++ b/bin/reth/src/builder/mod.rs @@ -278,9 +278,9 @@ impl NodeConfig { self } - /// Set the chain for the node - pub fn with_chain(mut self, chain: Arc) -> Self { - self.chain = chain; + /// Set the [ChainSpec] for the node + pub fn with_chain(mut self, chain: impl Into>) -> Self { + self.chain = chain.into(); self } @@ -296,12 +296,6 @@ impl NodeConfig { self } - /// Set the [ChainSpec] for the node - pub fn with_chain_spec(mut self, chain: Arc) -> Self { - self.chain = chain; - self - } - /// Set the trusted setup file for the node pub fn with_trusted_setup_file(mut self, trusted_setup_file: impl Into) -> Self { self.trusted_setup_file = Some(trusted_setup_file.into()); @@ -1371,7 +1365,6 @@ impl NodeHandle { /// Waits for the node to exit, if it was configured to exit. pub async fn wait_for_node_exit(self) -> eyre::Result<()> { self.consensus_engine_rx.await??; - info!(target: "reth::cli", "Consensus engine has exited."); if self.terminate { Ok(()) @@ -1459,4 +1452,276 @@ mod tests { handles.push(handle); } } + + #[cfg(feature = "optimism")] + #[tokio::test] + async fn optimism_pre_canyon_no_withdrawals_valid() { + reth_tracing::init_test_tracing(); + use alloy_chains::Chain; + use jsonrpsee::http_client::HttpClient; + use reth_primitives::Genesis; + use reth_rpc_api::EngineApiClient; + use reth_rpc_types::engine::{ + ForkchoiceState, OptimismPayloadAttributes, PayloadAttributes, + }; + + // this launches a test node with http + let rpc_args = RpcServerArgs::default().with_http(); + + // create optimism genesis with canyon at block 2 + let spec = ChainSpec::builder() + .chain(Chain::optimism_mainnet()) + .genesis(Genesis::default()) + .regolith_activated() + .build(); + + let genesis_hash = spec.genesis_hash(); + + // create node config + let node_config = NodeConfig::test().with_rpc(rpc_args).with_instance(7).with_chain(spec); + + let (handle, _manager) = spawn_node(node_config).await.unwrap(); + + // call a function on the node + let client = handle.rpc_server_handles().auth.http_client(); + let block_number = client.block_number().await.unwrap(); + + // it should be zero, since this is an ephemeral test node + assert_eq!(block_number, U256::ZERO); + + // call the engine_forkchoiceUpdated function with payload attributes + let forkchoice_state = ForkchoiceState { + head_block_hash: genesis_hash, + safe_block_hash: genesis_hash, + finalized_block_hash: genesis_hash, + }; + + let payload_attributes = OptimismPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: 1, + prev_randao: Default::default(), + suggested_fee_recipient: Default::default(), + // canyon is _not_ in the chain spec, so this should cause the engine call to fail + withdrawals: None, + parent_beacon_block_root: None, + }, + no_tx_pool: None, + gas_limit: Some(1), + transactions: None, + }; + + // call the engine_forkchoiceUpdated function with payload attributes + let res = >::fork_choice_updated_v2( + &client, + forkchoice_state, + Some(payload_attributes), + ) + .await; + res.expect("pre-canyon engine call without withdrawals should succeed"); + } + + #[cfg(feature = "optimism")] + #[tokio::test] + async fn optimism_pre_canyon_withdrawals_invalid() { + reth_tracing::init_test_tracing(); + use alloy_chains::Chain; + use assert_matches::assert_matches; + use jsonrpsee::{core::Error, http_client::HttpClient, types::error::INVALID_PARAMS_CODE}; + use reth_primitives::Genesis; + use reth_rpc_api::EngineApiClient; + use reth_rpc_types::engine::{ + ForkchoiceState, OptimismPayloadAttributes, PayloadAttributes, + }; + + // this launches a test node with http + let rpc_args = RpcServerArgs::default().with_http(); + + // create optimism genesis with canyon at block 2 + let spec = ChainSpec::builder() + .chain(Chain::optimism_mainnet()) + .genesis(Genesis::default()) + .regolith_activated() + .build(); + + let genesis_hash = spec.genesis_hash(); + + // create node config + let node_config = NodeConfig::test().with_rpc(rpc_args).with_instance(8).with_chain(spec); + + let (handle, _manager) = spawn_node(node_config).await.unwrap(); + + // call a function on the node + let client = handle.rpc_server_handles().auth.http_client(); + let block_number = client.block_number().await.unwrap(); + + // it should be zero, since this is an ephemeral test node + assert_eq!(block_number, U256::ZERO); + + // call the engine_forkchoiceUpdated function with payload attributes + let forkchoice_state = ForkchoiceState { + head_block_hash: genesis_hash, + safe_block_hash: genesis_hash, + finalized_block_hash: genesis_hash, + }; + + let payload_attributes = OptimismPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: 1, + prev_randao: Default::default(), + suggested_fee_recipient: Default::default(), + // canyon is _not_ in the chain spec, so this should cause the engine call to fail + withdrawals: Some(vec![]), + parent_beacon_block_root: None, + }, + no_tx_pool: None, + gas_limit: Some(1), + transactions: None, + }; + + // call the engine_forkchoiceUpdated function with payload attributes + let res = >::fork_choice_updated_v2( + &client, + forkchoice_state, + Some(payload_attributes), + ) + .await; + let err = res.expect_err("pre-canyon engine call with withdrawals should fail"); + assert_matches!(err, Error::Call(ref object) if object.code() == INVALID_PARAMS_CODE); + } + + #[cfg(feature = "optimism")] + #[tokio::test] + async fn optimism_post_canyon_no_withdrawals_invalid() { + reth_tracing::init_test_tracing(); + use alloy_chains::Chain; + use assert_matches::assert_matches; + use jsonrpsee::{core::Error, http_client::HttpClient, types::error::INVALID_PARAMS_CODE}; + use reth_primitives::Genesis; + use reth_rpc_api::EngineApiClient; + use reth_rpc_types::engine::{ + ForkchoiceState, OptimismPayloadAttributes, PayloadAttributes, + }; + + // this launches a test node with http + let rpc_args = RpcServerArgs::default().with_http(); + + // create optimism genesis with canyon at block 2 + let spec = ChainSpec::builder() + .chain(Chain::optimism_mainnet()) + .genesis(Genesis::default()) + .canyon_activated() + .build(); + + let genesis_hash = spec.genesis_hash(); + + // create node config + let node_config = NodeConfig::test().with_rpc(rpc_args).with_instance(9).with_chain(spec); + + let (handle, _manager) = spawn_node(node_config).await.unwrap(); + + // call a function on the node + let client = handle.rpc_server_handles().auth.http_client(); + let block_number = client.block_number().await.unwrap(); + + // it should be zero, since this is an ephemeral test node + assert_eq!(block_number, U256::ZERO); + + // call the engine_forkchoiceUpdated function with payload attributes + let forkchoice_state = ForkchoiceState { + head_block_hash: genesis_hash, + safe_block_hash: genesis_hash, + finalized_block_hash: genesis_hash, + }; + + let payload_attributes = OptimismPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: 1, + prev_randao: Default::default(), + suggested_fee_recipient: Default::default(), + // canyon is _not_ in the chain spec, so this should cause the engine call to fail + withdrawals: None, + parent_beacon_block_root: None, + }, + no_tx_pool: None, + gas_limit: Some(1), + transactions: None, + }; + + // call the engine_forkchoiceUpdated function with payload attributes + let res = >::fork_choice_updated_v2( + &client, + forkchoice_state, + Some(payload_attributes), + ) + .await; + let err = res.expect_err("post-canyon engine call with no withdrawals should fail"); + assert_matches!(err, Error::Call(ref object) if object.code() == INVALID_PARAMS_CODE); + } + + #[cfg(feature = "optimism")] + #[tokio::test] + async fn optimism_post_canyon_withdrawals_valid() { + reth_tracing::init_test_tracing(); + use alloy_chains::Chain; + use jsonrpsee::http_client::HttpClient; + use reth_primitives::Genesis; + use reth_rpc_api::EngineApiClient; + use reth_rpc_types::engine::{ + ForkchoiceState, OptimismPayloadAttributes, PayloadAttributes, + }; + + // this launches a test node with http + let rpc_args = RpcServerArgs::default().with_http(); + + // create optimism genesis with canyon at block 2 + let spec = ChainSpec::builder() + .chain(Chain::optimism_mainnet()) + .genesis(Genesis::default()) + .canyon_activated() + .build(); + + let genesis_hash = spec.genesis_hash(); + + // create node config + let node_config = NodeConfig::test().with_rpc(rpc_args).with_instance(10).with_chain(spec); + + let (handle, _manager) = spawn_node(node_config).await.unwrap(); + + // call a function on the node + let client = handle.rpc_server_handles().auth.http_client(); + let block_number = client.block_number().await.unwrap(); + + // it should be zero, since this is an ephemeral test node + assert_eq!(block_number, U256::ZERO); + + // call the engine_forkchoiceUpdated function with payload attributes + let forkchoice_state = ForkchoiceState { + head_block_hash: genesis_hash, + safe_block_hash: genesis_hash, + finalized_block_hash: genesis_hash, + }; + + let payload_attributes = OptimismPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: 1, + prev_randao: Default::default(), + suggested_fee_recipient: Default::default(), + // canyon is _not_ in the chain spec, so this should cause the engine call to fail + withdrawals: Some(vec![]), + parent_beacon_block_root: None, + }, + no_tx_pool: None, + gas_limit: Some(1), + transactions: None, + }; + + // call the engine_forkchoiceUpdated function with payload attributes + let res = >::fork_choice_updated_v2( + &client, + forkchoice_state, + Some(payload_attributes), + ) + .await; + res.expect("post-canyon engine call with withdrawals should succeed"); + } } diff --git a/crates/rpc/rpc-types/src/eth/engine/optimism.rs b/crates/rpc/rpc-types/src/eth/engine/optimism.rs index 1f35e99f7..9f8b8012a 100644 --- a/crates/rpc/rpc-types/src/eth/engine/optimism.rs +++ b/crates/rpc/rpc-types/src/eth/engine/optimism.rs @@ -26,7 +26,7 @@ pub struct OptimismPayloadAttributes { #[cfg(test)] mod tests { - use crate::engine::ExecutionPayloadInputV2; + use crate::engine::{ExecutionPayloadInputV2, OptimismPayloadAttributes}; // #[test] @@ -34,4 +34,11 @@ mod tests { let payload = r#"{"parentHash":"0x24e8df372a61cdcdb1a163b52aaa1785e0c869d28c3b742ac09e826bbb524723","feeRecipient":"0x4200000000000000000000000000000000000011","stateRoot":"0x9a5db45897f1ff1e620a6c14b0a6f1b3bcdbed59f2adc516a34c9a9d6baafa71","receiptsRoot":"0x8af6f74835d47835deb5628ca941d00e0c9fd75585f26dabdcb280ec7122e6af","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xf37b24eeff594848072a05f74c8600001706c83e489a9132e55bf43a236e42ec","blockNumber":"0xe3d5d8","gasLimit":"0x17d7840","gasUsed":"0xb705","timestamp":"0x65a118c0","extraData":"0x","baseFeePerGas":"0x7a0ff32","blockHash":"0xf5c147b2d60a519b72434f0a8e082e18599021294dd9085d7597b0ffa638f1c0","withdrawals":[],"transactions":["0x7ef90159a05ba0034ffdcb246703298224564720b66964a6a69d0d7e9ffd970c546f7c048094deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b90104015d8eb900000000000000000000000000000000000000000000000000000000009e1c4a0000000000000000000000000000000000000000000000000000000065a11748000000000000000000000000000000000000000000000000000000000000000a4b479e5fa8d52dd20a8a66e468b56e993bdbffcccf729223aabff06299ab36db000000000000000000000000000000000000000000000000000000000000000400000000000000000000000073b4168cc87f35cc239200a20eb841cded23493b000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240"]}"#; let _payload = serde_json::from_str::(payload).unwrap(); } + + #[test] + fn deserialize_op_payload_attributes() { + let payload = r#"{"prevRandao":"0x24e8df372a61cdcdb1a163b52aaa1785e0c869d28c3b742ac09e826bbb524723","suggestedFeeRecipient":"0x4200000000000000000000000000000000000011","timestamp":"1","gasLimit":"0x17d7840","transactions":[],"no_tx_pool":"true","withdrawals":[]}"#; + let _payload = serde_json::from_str::(payload).unwrap(); + assert!(_payload.payload_attributes.withdrawals.is_some()) + } }