Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lib): add rpc eth_sendTransaction #1984

Merged
merged 21 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/Cargo.lock

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

2 changes: 2 additions & 0 deletions lib/ain-grpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"
Expand Down
1 change: 1 addition & 0 deletions lib/ain-grpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
178 changes: 178 additions & 0 deletions lib/ain-grpc/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<String>;

/// Sends a transaction.
/// Returns the transaction hash as a hexadecimal string.
#[method(name = "eth_sendTransaction")]
fn send_transaction(&self, req: TransactionRequest) -> RpcResult<String>;

// ----------------------------------------
// Gas
// ----------------------------------------
Expand Down Expand Up @@ -461,6 +472,91 @@ impl MetachainRPCServer for MetachainRPCModule {
.map_or(Ok(0), |b| Ok(b.transactions.len()))
}

fn send_transaction(&self, request: TransactionRequest) -> RpcResult<String> {
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<TransactionMessage> = 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<String> {
debug!(target:"rpc","[send_raw_transaction] Sending raw transaction: {:?}", tx);
let raw_tx = tx.strip_prefix("0x").unwrap_or(tx);
Expand Down Expand Up @@ -580,3 +676,85 @@ impl MetachainRPCServer for MetachainRPCModule {
Ok(())
}
}

fn sign(
address: H160,
message: TransactionMessage,
) -> Result<EthereumTransaction, Box<dyn std::error::Error>> {
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())
}
99 changes: 99 additions & 0 deletions lib/ain-grpc/src/transaction_request.rs
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we re-use CallRequest or keep only this one instead ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm.. yea i have doubt on this earlier.. question.. why splitting 1 struct to 2 structs since they contain same field (slightly diff)..
btw i think current design is make more sense.. obviously splitting into 2 structs is to serve for own specific purpose.. readability and easier to maintain

/// Sender
pub from: Option<H160>,
/// Recipient
pub to: Option<H160>,
/// Gas Price, legacy.
#[serde(default)]
pub gas_price: Option<U256>,
/// Max BaseFeePerGas the user is willing to pay.
#[serde(default)]
pub max_fee_per_gas: Option<U256>,
/// The miner's tip.
#[serde(default)]
pub max_priority_fee_per_gas: Option<U256>,
/// Gas
pub gas: Option<U256>,
/// Value of transaction in wei
pub value: Option<U256>,
/// Additional data sent with transaction
pub data: Option<Bytes>,
/// Transaction's nonce
pub nonce: Option<U256>,
/// Pre-pay to warm storage access.
#[serde(default)]
pub access_list: Option<Vec<AccessListItem>>,
/// EIP-2718 type
#[serde(rename = "type")]
pub transaction_type: Option<U256>,
}

impl From<TransactionRequest> for Option<TransactionMessage> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better to implement TryFrom here and have it return an error instead of None on last match arm

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noted that.. its a great practice.. but we ady have error handling in another way round

ain/lib/ain-grpc/src/rpc.rs

Lines 543 to 547 in fde4c9d

_ => {
return Err(Error::Custom(String::from(
"invalid transaction parameters",
)))
}

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,
}
}
}
Loading