From 34cbcfb87b8ff9d805547a9bf992f4e78ef2e1eb Mon Sep 17 00:00:00 2001 From: parazyd Date: Mon, 25 Oct 2021 21:02:47 +0200 Subject: [PATCH] service/eth: Add proof of concept Geth interface. --- Cargo.lock | 29 +++++ Cargo.toml | 11 +- src/bin/eth.rs | 75 +++++++++++++ src/service/eth.rs | 263 +++++++++++++++++++++++++++++++++++++++++++++ src/service/mod.rs | 3 + 5 files changed, 378 insertions(+), 3 deletions(-) create mode 100644 src/bin/eth.rs create mode 100644 src/service/eth.rs diff --git a/Cargo.lock b/Cargo.lock index f8c7adff9..7161c336b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1259,8 +1259,10 @@ dependencies = [ "ff", "futures 0.3.17", "group", + "hash-db", "hex", "jubjub", + "keccak-hasher", "lazy_static", "log", "native-tls", @@ -1939,6 +1941,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash-db" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" + +[[package]] +name = "hash256-std-hasher" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.9.1" @@ -2246,6 +2263,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" +[[package]] +name = "keccak-hasher" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711adba9940a039f4374fc5724c0a5eaca84a2d558cce62256bfe26f0dbef05e" +dependencies = [ + "hash-db", + "hash256-std-hasher", + "tiny-keccak", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -2556,6 +2584,7 @@ dependencies = [ "num-integer", "num-traits", "rand 0.7.3", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 90d98affd..2ff0d5ebe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ hex = "0.4.2" bs58 = "0.4.0" prettytable-rs = "0.8" num_cpus = "1.13.0" -num-bigint = {version = "0.3.2", features = ["rand"]} +num-bigint = {version = "0.3.2", features = ["rand", "serde"]} smol = "1.2.5" futures = "0.3.17" @@ -73,11 +73,16 @@ spl-token = {version = "3.2.0", features = ["no-entrypoint"], optional = true} spl-associated-token-account = {version = "1.0.3", features = ["no-entrypoint"], optional = true} ## Cashier Bitcoin Dependencies -bitcoin = {version = "0.27.0", optional = true } +bitcoin = {version = "0.27.0", optional = true} bitcoin_hashes = "0.10.0" secp256k1 = {version = "0.20.3", default-features = false, features = ["rand-std"], optional = true} -electrum-client = {version = "0.8.0", optional = true } +electrum-client = {version = "0.8.0", optional = true} + +## Cashier Ethereum Dependencies +hash-db = {version = "0.15.2", optional = true} +keccak-hasher = {version = "0.15.3", optional = true} [features] btc = ["bitcoin", "secp256k1", "electrum-client"] sol = ["solana-sdk", "solana-client", "spl-token", "spl-associated-token-account"] +eth = ["keccak-hasher", "hash-db"] diff --git a/src/bin/eth.rs b/src/bin/eth.rs new file mode 100644 index 000000000..6aca5bf8b --- /dev/null +++ b/src/bin/eth.rs @@ -0,0 +1,75 @@ +use num_bigint::BigUint; + +use drk::{ + service::eth::{erc20_transfer_data, generate_privkey, EthClient, EthTx}, + util::{decode_base10, encode_base10}, + Result, +}; + +#[async_std::main] +async fn main() -> Result<()> { + simple_logger::init_with_level(log::Level::Debug)?; + + let eth = EthClient::new("/home/parazyd/.ethereum/ropsten/geth.ipc".to_string()); + + //let key = generate_privkey(); + //let passphrase = "foobar".to_string(); + //let rep = eth.import_privkey(&key, &passphrase).await?; + //println!("{:#?}", rep); + + let acc = "0x113b6648f34f4d0340d04ff171cbcf0b49d47827".to_string(); + let _key = "67cbb73cb293eea5fa2a7025d5479dbd50319010c03fd8821917ad0d9d53276c".to_string(); + let passphrase = "foobar".to_string(); + + // Recipient address + let dest = "0xcD640A363305c21255c58Ba9C8c1C508e6997a12".to_string(); + + // Latest known block, used to calculate present balance. + let block = eth.block_number().await?; + let block = block.as_str().unwrap(); + + // Native ETH balance + let hexbalance = eth.get_eth_balance(&acc, &block).await?; + let hexbalance = hexbalance.as_str().unwrap().trim_start_matches("0x"); + let balance = BigUint::parse_bytes(hexbalance.as_bytes(), 16).unwrap(); + println!("{}", encode_base10(balance, 18)); + + /* + // Transfer native ETH + let tx = EthTx::new( + &acc, + &dest, + None, + None, + Some(decode_base10("0.051", 18, true)?), + None, + None, + ); + + let rep = eth.send_transaction(&tx, &passphrase).await?; + println!("TXID: {}", rep.as_str().unwrap()); + */ + + // ERC20 Token balance + let mint = "0xad6d458402f60fd3bd25163575031acdce07538d"; // Ropsten DAI (get on Uniswap) + let hexbalance = eth.get_erc20_balance(&acc, mint).await?; + let hexbalance = hexbalance.as_str().unwrap().trim_start_matches("0x"); + let balance = BigUint::parse_bytes(hexbalance.as_bytes(), 16).unwrap(); + println!("{}", encode_base10(balance, 18)); + + // Transfer ERC20 token + let tx = EthTx::new( + &acc, + mint, + None, + None, + None, + Some(erc20_transfer_data(&dest, decode_base10("1", 18, true)?)), + None, + ); + + let rep = eth.send_transaction(&tx, &passphrase).await?; + println!("TXID: {}", rep.as_str().unwrap()); + + Ok(()) +} diff --git a/src/service/eth.rs b/src/service/eth.rs new file mode 100644 index 000000000..57ecb2ae0 --- /dev/null +++ b/src/service/eth.rs @@ -0,0 +1,263 @@ +use std::convert::TryInto; + +use hash_db::Hasher; +use keccak_hasher::KeccakHasher; +use lazy_static::lazy_static; +use log::debug; +use num_bigint::{BigUint, RandBigInt}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +use crate::{rpc::jsonrpc, rpc::jsonrpc::JsonResult, Error, Result}; + +// An ERC-20 token transfer transaction's data is as follows: +// +// 1. The first 4 bytes of the keccak256 hash of "transfer(address,uint256)". +// 2. The address of the recipient, left-zero-padded to be 32 bytes. +// 3. The amount to be transferred: amount * 10^decimals + +// This is the entire ERC20 ABI +lazy_static! { + static ref ERC20_NAME_METHOD: [u8; 4] = { + let method = b"name()"; + KeccakHasher::hash(method)[0..4].try_into().expect("nope") + }; + static ref ERC20_APPROVE_METHOD: [u8; 4] = { + let method = b"approve(address,uint256)"; + KeccakHasher::hash(method)[0..4].try_into().expect("nope") + }; + static ref ERC20_TOTALSUPPLY_METHOD: [u8; 4] = { + let method = b"totalSupply()"; + KeccakHasher::hash(method)[0..4].try_into().expect("nope") + }; + static ref ERC20_TRANSFERFROM_METHOD: [u8; 4] = { + let method = b"transferFrom(address,address,uint256)"; + KeccakHasher::hash(method)[0..4].try_into().expect("nope") + }; + static ref ERC20_DECIMALS_METHOD: [u8; 4] = { + let method = b"decimals()"; + KeccakHasher::hash(method)[0..4].try_into().expect("nope") + }; + static ref ERC20_VERSION_METHOD: [u8; 4] = { + let method = b"version()"; + KeccakHasher::hash(method)[0..4].try_into().expect("nope") + }; + static ref ERC20_BALANCEOF_METHOD: [u8; 4] = { + let method = b"balanceOf(address)"; + KeccakHasher::hash(method)[0..4].try_into().expect("nope") + }; + static ref ERC20_SYMBOL_METHOD: [u8; 4] = { + let method = b"symbol()"; + KeccakHasher::hash(method)[0..4].try_into().expect("nope") + }; + static ref ERC20_TRANSFER_METHOD: [u8; 4] = { + let method = b"transfer(address,uint256)"; + KeccakHasher::hash(method)[0..4].try_into().expect("nope") + }; + static ref ERC20_APPROVEANDCALL_METHOD: [u8; 4] = { + let method = b"approveAndCall(address,uint256,bytes)"; + KeccakHasher::hash(method)[0..4].try_into().expect("nope") + }; + static ref ERC20_ALLOWANCE_METHOD: [u8; 4] = { + let method = b"allowance(address,address)"; + KeccakHasher::hash(method)[0..4].try_into().expect("nope") + }; +} + +pub fn erc20_transfer_data(recipient: &str, amount: BigUint) -> String { + let rec = recipient.trim_start_matches("0x"); + let rec_padded = format!("{:0>64}", rec); + + let amnt_bytes = amount.to_bytes_be(); + let amnt_hex = hex::encode(amnt_bytes); + let amnt_hex_padded = format!("{:0>64}", amnt_hex); + + format!( + "0x{}{}{}", + hex::encode(*ERC20_TRANSFER_METHOD), + rec_padded, + amnt_hex_padded + ) +} + +pub fn erc20_balanceof_data(account: &str) -> String { + let acc = account.trim_start_matches("0x"); + let acc_padded = format!("{:0>64}", acc); + + format!("0x{}{}", hex::encode(*ERC20_BALANCEOF_METHOD), acc_padded) +} + +fn to_eth_hex(val: BigUint) -> String { + let bytes = val.to_bytes_be(); + let h = hex::encode(bytes); + format!("0x{}", h.trim_start_matches('0')) +} + +/// Generate a 256-bit ETH private key. +pub fn generate_privkey() -> String { + let mut rng = rand::thread_rng(); + let token = rng.gen_bigint(256); + let token_bytes = token.to_bytes_le().1; + let key = KeccakHasher::hash(&token_bytes); + hex::encode(key) +} + +#[allow(non_snake_case)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EthTx { + pub from: String, + + pub to: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub gas: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub gasPrice: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub nonce: Option, +} + +impl EthTx { + pub fn new( + from: &str, + to: &str, + gas: Option, + gas_price: Option, + value: Option, + data: Option, + nonce: Option, + ) -> Self { + let gas_hex = match gas { + Some(v) => Some(to_eth_hex(v)), + None => None, + }; + + let gasprice_hex = match gas_price { + Some(v) => Some(to_eth_hex(v)), + None => None, + }; + + let value_hex = match value { + Some(v) => Some(to_eth_hex(v)), + None => None, + }; + + EthTx { + from: from.to_string(), + to: to.to_string(), + gas: gas_hex, + gasPrice: gasprice_hex, + value: value_hex, + data, + nonce, + } + } +} + +// JSON-RPC interface to Geth. +// https://eth.wiki/json-rpc/API +// https://geth.ethereum.org/docs/rpc/ +// +// geth can be started with: $ geth --ropsten --syncmode light +// It should then show an Unix socket endpoint like so: +// INFO [10-25|19:47:32.845] IPC endpoint opened: url=/home/x/.ethereum/ropsten/geth.ipc +// +pub struct EthClient { + socket_path: String, +} + +impl EthClient { + pub fn new(socket_path: String) -> Self { + Self { socket_path } + } + + async fn request(&self, r: jsonrpc::JsonRequest) -> Result { + debug!(target: "ETH RPC", "--> {}", serde_json::to_string(&r)?); + let reply: JsonResult = match jsonrpc::send_unix_request(&self.socket_path, json!(r)).await + { + Ok(v) => v, + Err(e) => return Err(e), + }; + + match reply { + JsonResult::Resp(r) => { + debug!(target: "ETH RPC", "<-- {}", serde_json::to_string(&r)?); + return Ok(r.result); + } + + JsonResult::Err(e) => { + debug!(target: "ETH RPC", "<-- {}", serde_json::to_string(&e)?); + return Err(Error::JsonRpcError(e.error.message.to_string())); + } + + JsonResult::Notif(n) => { + debug!(target: "ETH RPC", "<-- {}", serde_json::to_string(&n)?); + return Err(Error::JsonRpcError("Unexpected reply".to_string())); + } + } + } + + pub async fn import_privkey(&self, key: &str, passphrase: &str) -> Result { + let req = jsonrpc::request(json!("personal_importRawKey"), json!([key, passphrase])); + Ok(self.request(req).await?) + } + + /* + pub async fn estimate_gas(&self, tx: &EthTx) -> Result { + let req = jsonrpc::request(json!("eth_estimateGas"), json!([tx])); + Ok(self.request(req).await?) + } + */ + + pub async fn block_number(&self) -> Result { + let req = jsonrpc::request(json!("eth_blockNumber"), json!([])); + Ok(self.request(req).await?) + } + + pub async fn get_eth_balance(&self, acc: &str, block: &str) -> Result { + let req = jsonrpc::request(json!("eth_getBalance"), json!([acc, block])); + Ok(self.request(req).await?) + } + + pub async fn get_erc20_balance(&self, acc: &str, mint: &str) -> Result { + let tx = EthTx::new( + acc, + mint, + None, + None, + None, + Some(erc20_balanceof_data(acc)), + None, + ); + let req = jsonrpc::request(json!("eth_call"), json!([tx, "latest"])); + Ok(self.request(req).await?) + } + + pub async fn send_transaction(&self, tx: &EthTx, passphrase: &str) -> Result { + let req = jsonrpc::request(json!("personal_sendTransaction"), json!([tx, passphrase])); + Ok(self.request(req).await?) + } +} + +#[allow(unused_imports)] +mod tests { + use super::*; + use num_bigint::ToBigUint; + use std::str::FromStr; + + #[test] + fn test_erc20_transfer_data() { + let recipient = "0x5b7b3b499fb69c40c365343cb0dc842fe8c23887"; + let amnt = BigUint::from_str("34765403556934000640").unwrap(); + + assert_eq!(erc20_transfer_data(recipient, amnt), "0xa9059cbb0000000000000000000000005b7b3b499fb69c40c365343cb0dc842fe8c23887000000000000000000000000000000000000000000000001e27786570c272000"); + } +} diff --git a/src/service/mod.rs b/src/service/mod.rs index e4c6c7506..84d25a1ef 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -13,4 +13,7 @@ pub mod sol; #[cfg(feature = "sol")] pub use sol::{SolClient, SolFailed, SolResult}; +#[cfg(feature = "eth")] +pub mod eth; + pub use gateway::{GatewayClient, GatewayService, GatewaySlabsSubscriber};