diff --git a/lib/Cargo.lock b/lib/Cargo.lock index be2e790d0f..d96ce183ab 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -134,10 +134,12 @@ dependencies = [ "cxx-gen", "env_logger", "ethereum", + "ethereum-types", "heck 0.4.1", "hex", "jsonrpsee 0.15.1", "lazy_static", + "libsecp256k1", "log", "num-traits", "prettyplease 0.2.4", diff --git a/lib/ain-grpc/Cargo.toml b/lib/ain-grpc/Cargo.toml index c9a634e43e..9bdf4c5d46 100644 --- a/lib/ain-grpc/Cargo.toml +++ b/lib/ain-grpc/Cargo.toml @@ -12,6 +12,7 @@ env_logger = "0.10" jsonrpsee = { version = "0.15", features = ["http-server", "macros", "http-client"] } lazy_static = "1.4" log = "0.4" +libsecp256k1 = "0.7.1" num-traits = "0.2" prost = "0.11" serde = { version = "1.0", features = ["derive"] } @@ -20,6 +21,7 @@ tokio = { version = "1.1", features = ["rt-multi-thread"] } tonic = "0.9" primitive-types = "0.12.1" ethereum = "0.14.0" +ethereum-types = "0.14.1" hex = "0.4.3" async-trait = "0.1.68" rlp = "0.5.2" diff --git a/lib/ain-grpc/src/lib.rs b/lib/ain-grpc/src/lib.rs index db51595d54..c3ff076c46 100644 --- a/lib/ain-grpc/src/lib.rs +++ b/lib/ain-grpc/src/lib.rs @@ -10,6 +10,7 @@ mod impls; mod receipt; pub mod rpc; mod transaction; +mod transaction_request; mod utils; use jsonrpsee::core::server::rpc_module::Methods; diff --git a/lib/ain-grpc/src/rpc.rs b/lib/ain-grpc/src/rpc.rs index a17b90e7c3..4bc38c0950 100644 --- a/lib/ain-grpc/src/rpc.rs +++ b/lib/ain-grpc/src/rpc.rs @@ -4,16 +4,21 @@ use crate::call_request::CallRequest; use crate::codegen::types::EthTransactionInfo; use crate::receipt::ReceiptResult; +use crate::transaction_request::{TransactionMessage, TransactionRequest}; +use ain_cpp_imports::get_eth_priv_key; use ain_evm::executor::TxResponse; use ain_evm::handler::Handlers; use ain_evm::storage::traits::{BlockStorage, ReceiptStorage, TransactionStorage}; use ain_evm::transaction::{SignedTx, TransactionError}; +use ethereum::{EnvelopedEncodable, TransactionV2 as EthereumTransaction}; use jsonrpsee::core::{Error, RpcResult}; use jsonrpsee::proc_macros::rpc; +use libsecp256k1::SecretKey; use log::{debug, trace}; use primitive_types::{H160, H256, U256}; use std::convert::Into; +use std::str::FromStr; use std::sync::Arc; #[rpc(server, client)] @@ -165,11 +170,17 @@ pub trait MetachainRPC { // ---------------------------------------- // Send // ---------------------------------------- + /// Sends a signed transaction. /// Returns the transaction hash as a hexadecimal string. #[method(name = "eth_sendRawTransaction")] fn send_raw_transaction(&self, tx: &str) -> RpcResult; + /// Sends a transaction. + /// Returns the transaction hash as a hexadecimal string. + #[method(name = "eth_sendTransaction")] + fn send_transaction(&self, req: TransactionRequest) -> RpcResult; + // ---------------------------------------- // Gas // ---------------------------------------- @@ -461,6 +472,91 @@ impl MetachainRPCServer for MetachainRPCModule { .map_or(Ok(0), |b| Ok(b.transactions.len())) } + fn send_transaction(&self, request: TransactionRequest) -> RpcResult { + debug!(target:"rpc","[send_transaction] Sending transaction: {:?}", request); + + let from = match request.from { + Some(from) => from, + None => { + let accounts = self.accounts()?; + + match accounts.get(0) { + Some(account) => H160::from_str(account.as_str()).unwrap(), + None => return Err(Error::Custom(String::from("from is not available"))), + } + } + }; + + let chain_id = ain_cpp_imports::get_chain_id() + .map_err(|e| Error::Custom(format!("ain_cpp_imports::get_chain_id error : {e:?}")))?; + + let block_number = self.block_number()?; + + let nonce = match request.nonce { + Some(nonce) => nonce, + None => self + .handler + .evm + .get_nonce(from, block_number) + .map_err(|e| { + Error::Custom(format!("Error getting address transaction count : {e:?}")) + })?, + }; + + let gas_price = request.gas_price; + let gas_limit = match request.gas { + Some(gas_limit) => gas_limit, + // TODO(): get the gas_limit from block.header + // set 21000 (min gas_limit req) by default first + None => U256::from(21000), + }; + let max_fee_per_gas = request.max_fee_per_gas; + let message: Option = request.into(); + let message = match message { + Some(TransactionMessage::Legacy(mut m)) => { + m.nonce = nonce; + m.chain_id = Some(chain_id); + m.gas_limit = U256::from(1); + if gas_price.is_none() { + m.gas_price = self.gas_price().unwrap(); + } + TransactionMessage::Legacy(m) + } + Some(TransactionMessage::EIP2930(mut m)) => { + m.nonce = nonce; + m.chain_id = chain_id; + m.gas_limit = gas_limit; + if gas_price.is_none() { + m.gas_price = self.gas_price().unwrap(); + } + TransactionMessage::EIP2930(m) + } + Some(TransactionMessage::EIP1559(mut m)) => { + m.nonce = nonce; + m.chain_id = chain_id; + m.gas_limit = gas_limit; + if max_fee_per_gas.is_none() { + m.max_fee_per_gas = self.gas_price().unwrap(); + } + TransactionMessage::EIP1559(m) + } + _ => { + return Err(Error::Custom(String::from( + "invalid transaction parameters", + ))) + } + }; + + let transaction = sign(from, message).unwrap(); + + let encoded_bytes = transaction.encode(); + let encoded_string = hex::encode(encoded_bytes); + let encoded = encoded_string.as_str(); + let hash = self.send_raw_transaction(encoded)?; + + Ok(hash) + } + fn send_raw_transaction(&self, tx: &str) -> RpcResult { debug!(target:"rpc","[send_raw_transaction] Sending raw transaction: {:?}", tx); let raw_tx = tx.strip_prefix("0x").unwrap_or(tx); @@ -580,3 +676,85 @@ impl MetachainRPCServer for MetachainRPCModule { Ok(()) } } + +fn sign( + address: H160, + message: TransactionMessage, +) -> Result> { + let key_id = address.as_fixed_bytes().to_owned(); + let priv_key = get_eth_priv_key(key_id).unwrap(); + let secret_key = SecretKey::parse(&priv_key).unwrap(); + + let mut transaction = None; + + match message { + TransactionMessage::Legacy(m) => { + let signing_message = libsecp256k1::Message::parse_slice(&m.hash()[..]) + .map_err(|_| Error::Custom(String::from("invalid signing message")))?; + let (signature, recid) = libsecp256k1::sign(&signing_message, &secret_key); + let v = match m.chain_id { + None => 27 + recid.serialize() as u64, + Some(chain_id) => 2 * chain_id + 35 + recid.serialize() as u64, + }; + let rs = signature.serialize(); + let r = H256::from_slice(&rs[0..32]); + let s = H256::from_slice(&rs[32..64]); + transaction = Some(EthereumTransaction::Legacy(ethereum::LegacyTransaction { + nonce: m.nonce, + gas_price: m.gas_price, + gas_limit: m.gas_limit, + action: m.action, + value: m.value, + input: m.input, + signature: ethereum::TransactionSignature::new(v, r, s).ok_or_else(|| { + Error::Custom(String::from("signer generated invalid signature")) + })?, + })); + } + TransactionMessage::EIP2930(m) => { + let signing_message = libsecp256k1::Message::parse_slice(&m.hash()[..]) + .map_err(|_| Error::Custom(String::from("invalid signing message")))?; + let (signature, recid) = libsecp256k1::sign(&signing_message, &secret_key); + let rs = signature.serialize(); + let r = H256::from_slice(&rs[0..32]); + let s = H256::from_slice(&rs[32..64]); + transaction = Some(EthereumTransaction::EIP2930(ethereum::EIP2930Transaction { + chain_id: m.chain_id, + nonce: m.nonce, + gas_price: m.gas_price, + gas_limit: m.gas_limit, + action: m.action, + value: m.value, + input: m.input.clone(), + access_list: m.access_list, + odd_y_parity: recid.serialize() != 0, + r, + s, + })); + } + TransactionMessage::EIP1559(m) => { + let signing_message = libsecp256k1::Message::parse_slice(&m.hash()[..]) + .map_err(|_| Error::Custom(String::from("invalid signing message")))?; + let (signature, recid) = libsecp256k1::sign(&signing_message, &secret_key); + let rs = signature.serialize(); + let r = H256::from_slice(&rs[0..32]); + let s = H256::from_slice(&rs[32..64]); + transaction = Some(EthereumTransaction::EIP1559(ethereum::EIP1559Transaction { + chain_id: m.chain_id, + nonce: m.nonce, + max_priority_fee_per_gas: m.max_priority_fee_per_gas, + max_fee_per_gas: m.max_fee_per_gas, + gas_limit: m.gas_limit, + action: m.action, + value: m.value, + input: m.input.clone(), + access_list: m.access_list, + odd_y_parity: recid.serialize() != 0, + r, + s, + })); + } + } + + Ok(transaction.unwrap()) +} diff --git a/lib/ain-grpc/src/transaction_request.rs b/lib/ain-grpc/src/transaction_request.rs new file mode 100644 index 0000000000..8ab5ae2f67 --- /dev/null +++ b/lib/ain-grpc/src/transaction_request.rs @@ -0,0 +1,99 @@ +use crate::bytes::Bytes; +use ethereum::{ + AccessListItem, EIP1559TransactionMessage, EIP2930TransactionMessage, LegacyTransactionMessage, +}; +use ethereum_types::{H160, U256}; +use serde::{Deserialize, Serialize}; + +pub enum TransactionMessage { + Legacy(LegacyTransactionMessage), + EIP2930(EIP2930TransactionMessage), + EIP1559(EIP1559TransactionMessage), +} + +/// Transaction request coming from RPC +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] +pub struct TransactionRequest { + /// Sender + pub from: Option, + /// Recipient + pub to: Option, + /// Gas Price, legacy. + #[serde(default)] + pub gas_price: Option, + /// Max BaseFeePerGas the user is willing to pay. + #[serde(default)] + pub max_fee_per_gas: Option, + /// The miner's tip. + #[serde(default)] + pub max_priority_fee_per_gas: Option, + /// Gas + pub gas: Option, + /// Value of transaction in wei + pub value: Option, + /// Additional data sent with transaction + pub data: Option, + /// Transaction's nonce + pub nonce: Option, + /// Pre-pay to warm storage access. + #[serde(default)] + pub access_list: Option>, + /// EIP-2718 type + #[serde(rename = "type")] + pub transaction_type: Option, +} + +impl From for Option { + fn from(req: TransactionRequest) -> Self { + match (req.gas_price, req.max_fee_per_gas, req.access_list.clone()) { + // Legacy + (Some(_), None, None) => Some(TransactionMessage::Legacy(LegacyTransactionMessage { + nonce: U256::zero(), + gas_price: req.gas_price.unwrap_or_default(), + gas_limit: req.gas.unwrap_or_default(), + value: req.value.unwrap_or_default(), + input: req.data.map(|s| s.into_vec()).unwrap_or_default(), + action: match req.to { + Some(to) => ethereum::TransactionAction::Call(to), + None => ethereum::TransactionAction::Create, + }, + chain_id: None, + })), + // EIP2930 + (_, None, Some(_)) => Some(TransactionMessage::EIP2930(EIP2930TransactionMessage { + nonce: U256::zero(), + gas_price: req.gas_price.unwrap_or_default(), + gas_limit: req.gas.unwrap_or_default(), + value: req.value.unwrap_or_default(), + input: req.data.map(|s| s.into_vec()).unwrap_or_default(), + action: match req.to { + Some(to) => ethereum::TransactionAction::Call(to), + None => ethereum::TransactionAction::Create, + }, + chain_id: 0, + access_list: req.access_list.unwrap_or_default(), + })), + // EIP1559 + (None, Some(_), _) | (None, None, None) => { + // Empty fields fall back to the canonical transaction schema. + Some(TransactionMessage::EIP1559(EIP1559TransactionMessage { + nonce: U256::zero(), + max_fee_per_gas: req.max_fee_per_gas.unwrap_or_default(), + max_priority_fee_per_gas: req.max_priority_fee_per_gas.unwrap_or_default(), + gas_limit: req.gas.unwrap_or_default(), + value: req.value.unwrap_or_default(), + input: req.data.map(|s| s.into_vec()).unwrap_or_default(), + action: match req.to { + Some(to) => ethereum::TransactionAction::Call(to), + None => ethereum::TransactionAction::Create, + }, + chain_id: 0, + access_list: req.access_list.unwrap_or_default(), + })) + } + _ => None, + } + } +} diff --git a/test/functional/feature_evm_rpc_transaction.py b/test/functional/feature_evm_rpc_transaction.py index 6644d520fb..2bf8fe6cae 100755 --- a/test/functional/feature_evm_rpc_transaction.py +++ b/test/functional/feature_evm_rpc_transaction.py @@ -126,11 +126,51 @@ def test_send_raw_transaction(self): receipt = self.nodes[0].eth_getTransactionReceipt(hash) assert_is_hex_string(receipt['contractAddress']) + def test_send_transaction(self): + contractBytecode = '0x608060405234801561001057600080fd5b5060df8061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063165c4a1614602d575b600080fd5b603c6038366004605f565b604e565b60405190815260200160405180910390f35b600060588284607f565b9392505050565b600080604083850312156070578182fd5b50508035926020909101359150565b600081600019048311821515161560a457634e487b7160e01b81526011600452602481fd5b50029056fea2646970667358221220223df7833fd08eb1cd3ce363a9c4cb4619c1068a5f5517ea8bb862ed45d994f764736f6c63430008020033' + + # TODO(): test txLegacy + + tx2930 = { + 'value': '0x00', + 'data': contractBytecode, + 'gas': '0x7a120', # 500_000 + 'gasPrice': '0x22ecb25c00', # 150_000_000_000, + 'accessList': [ + { + 'address': self.ethAddress, + 'storageKeys': [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + ], + 'type': '0x1' + } + hash = self.nodes[0].eth_sendTransaction(tx2930) + self.nodes[0].generate(1) + receipt = self.nodes[0].eth_getTransactionReceipt(hash) + assert_is_hex_string(receipt['contractAddress']) + + tx1559 = { + 'value': '0x0', + 'data': contractBytecode, + 'gas': '0x18e70', # 102_000 + 'maxPriorityFeePerGas': '0x2363e7f000', # 152_000_000_000 + 'maxFeePerGas': '0x22ecb25c00', # 150_000_000_000 + 'type': '0x2' + } + hash = self.nodes[0].eth_sendTransaction(tx1559) + self.nodes[0].generate(1) + receipt = self.nodes[0].eth_getTransactionReceipt(hash) + assert_is_hex_string(receipt['contractAddress']) + def run_test(self): self.setup() self.test_send_raw_transaction() + self.test_send_transaction() + if __name__ == '__main__':