From 241c417b9eae5ccb4e16f8e9131f71932ffb62c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orkun=20Mahir=20K=C4=B1l=C4=B1=C3=A7?= Date: Fri, 22 Sep 2023 12:12:33 +0300 Subject: [PATCH] EVM: Implement SendTransaction and Storage access on RPC (#902) * feature gate DevSigner * ethers signer on devsigner * update branch * access storage in sov-ethereum rpc * remove unnecessary imports * cargo fmt * fix compilation problems * fix mutable working set * Add debug printlns * add more debugs * temporary fix into transaction method * lint: eth_sendTransaction * fix reviews * remove unnecessary result * error on conversions * enable local with experimental on evm --------- Co-authored-by: bkolad --- Cargo.lock | 4 +- examples/demo-rollup/Cargo.toml | 3 +- examples/demo-rollup/src/register_rpc.rs | 5 +- examples/demo-rollup/src/rollup.rs | 11 +- examples/demo-rollup/tests/evm/mod.rs | 57 +++++- full-node/sov-ethereum/Cargo.toml | 3 +- full-node/sov-ethereum/src/lib.rs | 232 +++++++++++++++++++---- 7 files changed, 266 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35b3f32d0..98c47ff3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] diff --git a/examples/demo-rollup/Cargo.toml b/examples/demo-rollup/Cargo.toml index 77f9207e3..e7fbbd669 100644 --- a/examples/demo-rollup/Cargo.toml +++ b/examples/demo-rollup/Cargo.toml @@ -73,12 +73,13 @@ revm = { workspace = true } [features] default = ["native"] # Deviate from convention by making the "native" feature active by default. This aligns with how this package is meant to be used (as a binary first, library second). -experimental = ["sov-ethereum/experimental", "reth-primitives", "secp256k1", "demo-stf/experimental"] +experimental = ["sov-ethereum/experimental", "reth-primitives", "secp256k1", "demo-stf/experimental", "local"] native = ["anyhow", "jsonrpsee", "serde", "serde_json", "tracing", "tokio", "tracing-subscriber", "demo-stf/native", "sov-modules-stf-template/native", "sov-risc0-adapter/native", "sov-modules-api/native", "sov-state/native", "sov-cli", "clap", "sov-celestia-adapter/native", "sov-db", "sov-sequencer", "sov-stf-runner/native", "sov-modules-api/native"] bench = ["native", "async-trait", "borsh", "hex"] +local = ["sov-ethereum/local"] [[bench]] name = "rollup_bench" diff --git a/examples/demo-rollup/src/register_rpc.rs b/examples/demo-rollup/src/register_rpc.rs index c1b862ef1..57d1a340b 100644 --- a/examples/demo-rollup/src/register_rpc.rs +++ b/examples/demo-rollup/src/register_rpc.rs @@ -42,12 +42,13 @@ pub fn register_ledger( #[cfg(feature = "experimental")] /// register ethereum methods. -pub fn register_ethereum( +pub fn register_ethereum( da_service: Da, eth_rpc_config: EthRpcConfig, + storage: C::Storage, methods: &mut jsonrpsee::RpcModule<()>, ) -> Result<(), anyhow::Error> { - let ethereum_rpc = sov_ethereum::get_ethereum_rpc(da_service, eth_rpc_config); + let ethereum_rpc = sov_ethereum::get_ethereum_rpc::(da_service, eth_rpc_config, storage); methods .merge(ethereum_rpc) diff --git a/examples/demo-rollup/src/rollup.rs b/examples/demo-rollup/src/rollup.rs index 77829deeb..2c3729681 100644 --- a/examples/demo-rollup/src/rollup.rs +++ b/examples/demo-rollup/src/rollup.rs @@ -137,6 +137,7 @@ pub async fn new_rollup_with_celestia_da( eth_rpc_config: EthRpcConfig { min_blob_size: Some(1), sov_tx_signer_priv_key: read_sov_tx_signer_priv_key()?, + #[cfg(feature = "local")] eth_signer, }, prover, @@ -185,6 +186,7 @@ pub fn new_rollup_with_mock_da_from_config( eth_rpc_config: EthRpcConfig { min_blob_size: Some(1), sov_tx_signer_priv_key: read_sov_tx_signer_priv_key()?, + #[cfg(feature = "local")] eth_signer, }, prover, @@ -224,14 +226,19 @@ impl + Clone> Rollup channel: Option>, ) -> Result<(), anyhow::Error> { let storage = self.app.get_storage(); - let mut methods = get_rpc_methods::(storage); + let mut methods = get_rpc_methods::(storage.clone()); // register rpc methods { register_ledger(self.ledger_db.clone(), &mut methods)?; register_sequencer(self.da_service.clone(), &mut self.app, &mut methods)?; #[cfg(feature = "experimental")] - register_ethereum(self.da_service.clone(), self.eth_rpc_config, &mut methods)?; + register_ethereum::( + self.da_service.clone(), + self.eth_rpc_config, + storage, + &mut methods, + )?; } let storage = self.app.get_storage(); diff --git a/examples/demo-rollup/tests/evm/mod.rs b/examples/demo-rollup/tests/evm/mod.rs index 3943bd9ce..c164942f0 100644 --- a/examples/demo-rollup/tests/evm/mod.rs +++ b/examples/demo-rollup/tests/evm/mod.rs @@ -84,6 +84,28 @@ impl TestClient { Ok(receipt_req) } + async fn set_value_unsigned( + &self, + contract_address: H160, + set_arg: u32, + ) -> PendingTransaction<'_, Http> { + let nonce = self.eth_get_transaction_count(self.from_addr).await; + + let req = Eip1559TransactionRequest::new() + .from(self.from_addr) + .to(contract_address) + .chain_id(self.chain_id) + .nonce(nonce) + .data(self.contract.set_call_data(set_arg)) + .max_priority_fee_per_gas(10u64) + .max_fee_per_gas(MAX_FEE_PER_GAS) + .gas(900000u64); + + let typed_transaction = TypedTransaction::Eip1559(req); + + self.eth_send_transaction(typed_transaction).await + } + async fn set_value( &self, contract_address: H160, @@ -131,6 +153,7 @@ impl TestClient { Ok(ethereum_types::U256::from(resp_array)) } + #[cfg(feature = "local")] async fn eth_accounts(&self) -> Vec
{ self.http_client .request("eth_accounts", rpc_params![]) @@ -138,6 +161,15 @@ impl TestClient { .unwrap() } + #[cfg(feature = "local")] + async fn eth_send_transaction(&self, tx: TypedTransaction) -> PendingTransaction<'_, Http> { + self.client + .provider() + .send_transaction(tx, None) + .await + .unwrap() + } + async fn eth_chain_id(&self) -> u64 { let chain_id: ethereum_types::U64 = self .http_client @@ -239,6 +271,24 @@ impl TestClient { assert_eq!(102, get_arg.as_u32()); } + #[cfg(feature = "local")] + { + let value = 103; + + let tx_hash = { + let set_value_req = self.set_value_unsigned(contract_address, value).await; + self.send_publish_batch_request().await; + set_value_req.await.unwrap().unwrap().transaction_hash + }; + + let latest_block = self.eth_get_block_by_number(None).await; + assert_eq!(latest_block.transactions.len(), 1); + assert_eq!(latest_block.transactions[0], tx_hash); + + let get_arg = self.query_contract(contract_address).await?; + assert_eq!(value, get_arg.as_u32()); + } + Ok(()) } } @@ -256,8 +306,11 @@ async fn send_tx_test_to_eth(rpc_address: SocketAddr) -> Result<(), Box, pub sov_tx_signer_priv_key: DefaultPrivateKey, - //TODO #839 + #[cfg(feature = "local")] pub eth_signer: DevSigner, } - pub fn get_ethereum_rpc( + pub fn get_ethereum_rpc( da_service: Da, eth_rpc_config: EthRpcConfig, - ) -> RpcModule> { + storage: C::Storage, + ) -> RpcModule> { let mut rpc = RpcModule::new(Ethereum::new( Default::default(), da_service, Arc::new(Mutex::new(EthBatchBuilder::default())), eth_rpc_config, + storage, )); register_rpc_methods(&mut rpc).expect("Failed to register sequencer RPC methods"); rpc } - pub struct Ethereum { + pub struct Ethereum { nonces: Mutex>, da_service: Da, batch_builder: Arc>, eth_rpc_config: EthRpcConfig, + storage: C::Storage, } - impl Ethereum { + impl Ethereum { fn new( nonces: Mutex>, da_service: Da, batch_builder: Arc>, eth_rpc_config: EthRpcConfig, + storage: C::Storage, ) -> Self { Self { nonces, da_service, batch_builder, eth_rpc_config, + storage, } } } - impl Ethereum { + impl Ethereum { fn make_raw_tx( &self, raw_tx: RlpEvmTransaction, @@ -104,6 +113,27 @@ pub mod experimental { Ok((H256::from(tx_hash), tx.try_to_vec()?)) } + async fn build_and_submit_batch( + &self, + txs: Vec>, + min_blob_size: Option, + ) -> Result<(), jsonrpsee::core::Error> { + let min_blob_size = min_blob_size.or(self.eth_rpc_config.min_blob_size); + + let batch = self + .batch_builder + .lock() + .unwrap() + .add_transactions_and_get_next_blob(min_blob_size, txs); + + if !batch.is_empty() { + self.submit_batch(batch) + .await + .map_err(|e| to_jsonrpsee_error_object(e, ETH_RPC_ERROR))?; + } + Ok(()) + } + async fn submit_batch(&self, raw_txs: Vec>) -> Result<(), jsonrpsee::core::Error> { let blob = raw_txs .try_to_vec() @@ -118,8 +148,8 @@ pub mod experimental { } } - fn register_rpc_methods( - rpc: &mut RpcModule>, + fn register_rpc_methods( + rpc: &mut RpcModule>, ) -> Result<(), jsonrpsee::core::Error> { rpc.register_async_method("eth_publishBatch", |params, ethereum| async move { let mut params_iter = params.sequence(); @@ -129,18 +159,11 @@ pub mod experimental { txs.push(tx) } - let blob = ethereum - .batch_builder - .lock() - .unwrap() - .add_transactions_and_get_next_blob(Some(1), txs); + ethereum + .build_and_submit_batch(txs, Some(1)) + .await + .map_err(|e| to_jsonrpsee_error_object(e, ETH_RPC_ERROR))?; - if !blob.is_empty() { - ethereum - .submit_batch(blob) - .await - .map_err(|e| to_jsonrpsee_error_object(e, ETH_RPC_ERROR))?; - } Ok::("Submitted transaction".to_string()) })?; @@ -155,35 +178,166 @@ pub mod experimental { .make_raw_tx(raw_evm_tx) .map_err(|e| to_jsonrpsee_error_object(e, ETH_RPC_ERROR))?; - let blob = ethereum - .batch_builder - .lock() - .unwrap() - .add_transactions_and_get_next_blob( - ethereum.eth_rpc_config.min_blob_size, - vec![raw_tx], - ); - - if !blob.is_empty() { - ethereum - .submit_batch(blob) - .await - .map_err(|e| to_jsonrpsee_error_object(e, ETH_RPC_ERROR))?; - } + ethereum + .build_and_submit_batch(vec![raw_tx], None) + .await + .map_err(|e| to_jsonrpsee_error_object(e, ETH_RPC_ERROR))?; + Ok::<_, ErrorObjectOwned>(tx_hash) }, )?; + #[cfg(feature = "local")] rpc.register_async_method("eth_accounts", |_parameters, ethereum| async move { Ok::<_, ErrorObjectOwned>(ethereum.eth_rpc_config.eth_signer.signers()) })?; - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/502 - rpc.register_async_method("eth_sendTransaction", |parameters, _ethereum| async move { - let _data: reth_rpc_types::TransactionRequest = parameters.one().unwrap(); - unimplemented!("eth_sendTransaction not implemented") + #[cfg(feature = "local")] + rpc.register_async_method("eth_sendTransaction", |parameters, ethereum| async move { + let mut transaction_request: TransactionRequest = parameters.one().unwrap(); + + let evm = Evm::::default(); + + // get from, return error if none + let from = transaction_request + .from + .ok_or(to_jsonrpsee_error_object("No from address", ETH_RPC_ERROR))?; + + // return error if not in signers + if !ethereum.eth_rpc_config.eth_signer.signers().contains(&from) { + return Err(to_jsonrpsee_error_object( + "From address not in signers", + ETH_RPC_ERROR, + )); + } + + let raw_evm_tx = { + let mut working_set = WorkingSet::::new(ethereum.storage.clone()); + if transaction_request.nonce.is_none() { + let nonce = evm + .get_transaction_count(from, None, &mut working_set) + .unwrap_or_default(); + + transaction_request.nonce = Some(reth_primitives::U256::from(nonce.as_u64())); + } + + let chain_id = evm + .chain_id(&mut working_set) + .expect("Failed to get chain id") + .map(|id| id.as_u64()) + .unwrap_or(1); + + // TODO: implement gas logic after gas estimation (#906) is implemented + // https://github.com/Sovereign-Labs/sovereign-sdk/issues/906 + let transaction_request = match transaction_request.into_typed_request() { + Some(TypedTransactionRequest::Legacy(mut m)) => { + m.chain_id = Some(chain_id); + + TypedTransactionRequest::Legacy(m) + } + Some(TypedTransactionRequest::EIP2930(mut m)) => { + m.chain_id = chain_id; + + TypedTransactionRequest::EIP2930(m) + } + Some(TypedTransactionRequest::EIP1559(mut m)) => { + m.chain_id = chain_id; + + TypedTransactionRequest::EIP1559(m) + } + None => { + return Err(to_jsonrpsee_error_object( + "Conflicting fee fields", + ETH_RPC_ERROR, + )); + } + }; + + let transaction = into_transaction(transaction_request).map_err(|_| { + to_jsonrpsee_error_object("Invalid types in transaction request", ETH_RPC_ERROR) + })?; + + let signed_tx = ethereum + .eth_rpc_config + .eth_signer + .sign_transaction(transaction, from) + .map_err(|e| to_jsonrpsee_error_object(e, ETH_RPC_ERROR))?; + + RlpEvmTransaction { + rlp: signed_tx.envelope_encoded().to_vec(), + } + }; + let (tx_hash, raw_tx) = ethereum + .make_raw_tx(raw_evm_tx) + .map_err(|e| to_jsonrpsee_error_object(e, ETH_RPC_ERROR))?; + + ethereum + .build_and_submit_batch(vec![raw_tx], None) + .await + .map_err(|e| to_jsonrpsee_error_object(e, ETH_RPC_ERROR))?; + + Ok::<_, ErrorObjectOwned>(tx_hash) })?; Ok(()) } + + // Temporary solution until https://github.com/paradigmxyz/reth/issues/4704 is resolved + // The problem is having wrong length nonce/gas_limt/value fields in the transaction request + fn into_transaction( + request: TypedTransactionRequest, + ) -> Result { + Ok(match request { + TypedTransactionRequest::Legacy(tx) => { + reth_primitives::Transaction::Legacy(reth_primitives::TxLegacy { + chain_id: tx.chain_id, + nonce: convert_u256_to_u64(tx.nonce)?, + gas_price: u128::from_be_bytes(tx.gas_price.to_be_bytes()), + gas_limit: convert_u256_to_u64(tx.gas_limit)?, + to: tx.kind.into(), + value: convert_u256_to_u128(tx.value)?, + input: tx.input, + }) + } + TypedTransactionRequest::EIP2930(tx) => { + reth_primitives::Transaction::Eip2930(reth_primitives::TxEip2930 { + chain_id: tx.chain_id, + nonce: convert_u256_to_u64(tx.nonce)?, + gas_price: u128::from_be_bytes(tx.gas_price.to_be_bytes()), + gas_limit: convert_u256_to_u64(tx.gas_limit)?, + to: tx.kind.into(), + value: convert_u256_to_u128(tx.value)?, + input: tx.input, + access_list: tx.access_list, + }) + } + TypedTransactionRequest::EIP1559(tx) => { + reth_primitives::Transaction::Eip1559(reth_primitives::TxEip1559 { + chain_id: tx.chain_id, + nonce: convert_u256_to_u64(tx.nonce)?, + max_fee_per_gas: u128::from_be_bytes(tx.max_fee_per_gas.to_be_bytes()), + gas_limit: convert_u256_to_u64(tx.gas_limit)?, + to: tx.kind.into(), + value: convert_u256_to_u128(tx.value)?, + input: tx.input, + access_list: tx.access_list, + max_priority_fee_per_gas: u128::from_be_bytes( + tx.max_priority_fee_per_gas.to_be_bytes(), + ), + }) + } + }) + } + + fn convert_u256_to_u64(u256: reth_primitives::U256) -> Result { + let bytes: [u8; 32] = u256.to_be_bytes(); + let bytes: [u8; 8] = bytes[24..].try_into()?; + Ok(u64::from_be_bytes(bytes)) + } + + fn convert_u256_to_u128(u256: reth_primitives::U256) -> Result { + let bytes: [u8; 32] = u256.to_be_bytes(); + let bytes: [u8; 16] = bytes[16..].try_into()?; + Ok(u128::from_be_bytes(bytes)) + } }