From 74b14adc7ace4cf2713369ab641adf1034c49445 Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Tue, 11 Feb 2020 12:40:31 +0100 Subject: [PATCH] Reimplement transaction signing with license compatible crate (#325) * Revert "Revert "Add transaction signing with in `accounts` sub-namespace. (#279)" (#315)" This reverts commit 9928849b3dc4a0e749098ce1325bc1a3eec454a4. * implemented signing and recovery with secp256k1 crate * attempt to protect key from being leaked --- Cargo.toml | 8 +- src/api/accounts.rs | 606 ++++++++++++++++++++++++++++++++++++++++++ src/api/mod.rs | 7 + src/error.rs | 7 + src/helpers.rs | 2 +- src/types/mod.rs | 4 + src/types/recovery.rs | 207 +++++++++++++++ src/types/signed.rs | 138 ++++++++++ 8 files changed, 976 insertions(+), 3 deletions(-) create mode 100644 src/api/accounts.rs create mode 100644 src/types/recovery.rs create mode 100644 src/types/signed.rs diff --git a/Cargo.toml b/Cargo.toml index ab72dd0b..cbab4d24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,19 +13,23 @@ edition = "2018" [dependencies] arrayvec = "0.5.0" +base64 = "0.11.0" +derive_more = "0.99.1" ethabi = "11.0.0" ethereum-types = "0.8.0" futures = "0.1.26" jsonrpc-core = "14.0.0" log = "0.4.6" parking_lot = "0.10.0" +rlp = "0.4" rustc-hex = "2.0.1" +secp256k1 = { version = "0.17", features = ["recovery"] } serde = { version = "1.0.90", features = ["derive"] } serde_json = "1.0.39" +tiny-keccak = { version = "2.0.1", features = ["keccak"] } tokio-timer = "0.1" url = "2.1.0" -base64 = "0.11.0" -derive_more = "0.99.1" +zeroize = "1.1.0" # Optional deps hyper = { version = "0.12.25", optional = true } hyper-tls = { version = "0.3.2", optional = true } diff --git a/src/api/accounts.rs b/src/api/accounts.rs new file mode 100644 index 00000000..3fc52733 --- /dev/null +++ b/src/api/accounts.rs @@ -0,0 +1,606 @@ +//! Partial implementation of the `Accounts` namespace. + +use crate::api::{Namespace, Web3}; +use crate::error::Error; +use crate::helpers::CallFuture; +use crate::types::{ + Address, Bytes, Recovery, RecoveryMessage, SignedData, SignedTransaction, TransactionParameters, H256, U256, +}; +use crate::Transport; +use futures::future::{self, Either, FutureResult, Join3}; +use futures::{Async, Future, Poll}; +use rlp::RlpStream; +use secp256k1::key::ONE_KEY; +use secp256k1::{Message, PublicKey, Secp256k1, SecretKey}; +use std::convert::TryInto; +use std::mem; +use std::ops::Deref; +use tiny_keccak::{Hasher, Keccak}; +use zeroize::{DefaultIsZeroes, Zeroize}; + +/// `Accounts` namespace +#[derive(Debug, Clone)] +pub struct Accounts { + transport: T, +} + +impl Namespace for Accounts { + fn new(transport: T) -> Self + where + Self: Sized, + { + Accounts { transport } + } + + fn transport(&self) -> &T { + &self.transport + } +} + +impl Accounts { + /// Gets the parent `web3` namespace + fn web3(&self) -> Web3 { + Web3::new(self.transport.clone()) + } + + /// Signs an Ethereum transaction with a given private key. + pub fn sign_transaction(&self, tx: TransactionParameters, key: &SecretKey) -> SignTransactionFuture { + SignTransactionFuture::new(self, tx, key) + } + + /// Hash a message according to EIP-191. + /// + /// The data is a UTF-8 encoded string and will enveloped as follows: + /// `"\x19Ethereum Signed Message:\n" + message.length + message` and hashed + /// using keccak256. + pub fn hash_message(&self, message: S) -> H256 + where + S: AsRef<[u8]>, + { + let message = message.as_ref(); + + let mut eth_message = format!("\x19Ethereum Signed Message:\n{}", message.len()).into_bytes(); + eth_message.extend_from_slice(message); + + keccak256(ð_message).into() + } + + /// Sign arbitrary string data. + /// + /// The data is UTF-8 encoded and enveloped the same way as with + /// `hash_message`. The returned signed data's signature is in 'Electrum' + /// notation, that is the recovery value `v` is either `27` or `28` (as + /// opposed to the standard notation where `v` is either `0` or `1`). This + /// is important to consider when using this signature with other crates. + pub fn sign(&self, message: S, key: &SecretKey) -> SignedData + where + S: AsRef<[u8]>, + { + let message = message.as_ref(); + let message_hash = self.hash_message(message); + + let sig_message = Message::from_slice(message_hash.as_bytes()).expect("hash is non-zero 32-bytes; qed"); + let signature = sign(&sig_message, key, None); + let v = signature + .v + .try_into() + .expect("signature recovery in electrum notation always fits in a u8"); + + let signature_bytes = Bytes({ + let mut bytes = Vec::with_capacity(65); + bytes.extend_from_slice(signature.r.as_bytes()); + bytes.extend_from_slice(signature.s.as_bytes()); + bytes.push(v); + bytes + }); + + // We perform this allocation only after all previous fallible actions have completed successfully. + let message = message.to_owned(); + + SignedData { + message, + message_hash, + v, + r: signature.r, + s: signature.s, + signature: signature_bytes, + } + } + + /// Recovers the Ethereum address which was used to sign the given data. + /// + /// Recovery signature data uses 'Electrum' notation, this means the `v` + /// value is expected to be either `27` or `28`. + pub fn recover(&self, recovery: R) -> Result + where + R: Into, + { + let recovery = recovery.into(); + let message_hash = match recovery.message { + RecoveryMessage::Data(ref message) => self.hash_message(message), + RecoveryMessage::Hash(hash) => hash, + }; + let signature = recovery.as_signature()?; + + let message = Message::from_slice(message_hash.as_bytes())?; + let public_key = Secp256k1::verification_only().recover(&message, &signature)?; + + Ok(public_key_address(&public_key)) + } +} + +/// Compute the Keccak-256 hash of input bytes. +pub fn keccak256(bytes: &[u8]) -> [u8; 32] { + let mut output = [0u8; 32]; + let mut hasher = Keccak::v256(); + hasher.update(bytes); + hasher.finalize(&mut output); + output +} + +/// Gets the public address of a private key. +fn secret_key_address(key: &SecretKey) -> Address { + let secp = Secp256k1::signing_only(); + let public_key = PublicKey::from_secret_key(&secp, key); + public_key_address(&public_key) +} + +/// Gets the address of a public key. +/// +/// The public address is defined as the low 20 bytes of the keccak hash of +/// the public key. Note that the public key returned from the `secp256k1` +/// crate is 65 bytes long, that is because it is prefixed by `0x04` to +/// indicate an uncompressed public key; this first byte is ignored when +/// computing the hash. +fn public_key_address(public_key: &PublicKey) -> Address { + let public_key = public_key.serialize_uncompressed(); + + debug_assert_eq!(public_key[0], 0x04); + let hash = keccak256(&public_key[1..]); + + Address::from_slice(&hash[12..]) +} + +type MaybeReady = Either, CallFuture::Out>>; + +type TxParams = Join3, MaybeReady, MaybeReady>; + +/// Future resolving when transaction signing is complete. +/// +/// Transaction signing can perform RPC requests in order to fill missing +/// parameters required for signing `nonce`, `gas_price` and `chain_id`. Note +/// that if all transaction parameters were provided, this future will resolve +/// immediately. +pub struct SignTransactionFuture { + tx: TransactionParameters, + key: ZeroizeSecretKey, + inner: TxParams, +} + +impl SignTransactionFuture { + /// Creates a new SignTransactionFuture with accounts and transaction data. + pub fn new(accounts: &Accounts, tx: TransactionParameters, key: &SecretKey) -> SignTransactionFuture { + macro_rules! maybe { + ($o: expr, $f: expr) => { + match $o.clone() { + Some(value) => Either::A(future::ok(value)), + None => Either::B($f), + } + }; + } + + let from = secret_key_address(key); + let inner = Future::join3( + maybe!(tx.nonce, accounts.web3().eth().transaction_count(from, None)), + maybe!(tx.gas_price, accounts.web3().eth().gas_price()), + maybe!(tx.chain_id.map(U256::from), accounts.web3().eth().chain_id()), + ); + + SignTransactionFuture { + tx, + key: ZeroizeSecretKey(*key), + inner, + } + } +} + +impl Future for SignTransactionFuture { + type Item = SignedTransaction; + type Error = Error; + + fn poll(&mut self) -> Poll { + let (nonce, gas_price, chain_id) = try_ready!(self.inner.poll()); + let chain_id = chain_id.as_u64(); + + let data = mem::replace(&mut self.tx.data, Bytes::default()); + let tx = Transaction { + to: self.tx.to, + nonce, + gas: self.tx.gas, + gas_price, + value: self.tx.value, + data: data.0, + }; + let signed = tx.sign(&self.key, chain_id); + + Ok(Async::Ready(signed)) + } +} + +impl Drop for SignTransactionFuture { + fn drop(&mut self) { + self.key.zeroize(); + } +} + +/// A struct that represents a the components of a secp256k1 signature. +struct Signature { + v: u64, + r: H256, + s: H256, +} + +/// Sign a message with a secret key and optional chain ID. +/// +/// When a chain ID is provided, the `Signature`'s V-value will have chain relay +/// protection added (as per EIP-155). Otherwise, the V-value will be in +/// 'Electrum' notation. +fn sign(message: &Message, key: &SecretKey, chain_id: Option) -> Signature { + let (recovery_id, signature) = Secp256k1::signing_only() + .sign_recoverable(message, key) + .serialize_compact(); + + let standard_v = recovery_id.to_i32() as u64; + let v = if let Some(chain_id) = chain_id { + // When signing with a chain ID, add chain replay protection. + standard_v + 35 + chain_id * 2 + } else { + // Otherwise, convert to 'Electrum' notation. + standard_v + 27 + }; + let r = H256::from_slice(&signature[..32]); + let s = H256::from_slice(&signature[32..]); + + Signature { v, r, s } +} + +/// A transaction used for RLP encoding, hashing and signing. +struct Transaction { + to: Option
, + nonce: U256, + gas: U256, + gas_price: U256, + value: U256, + data: Vec, +} + +impl Transaction { + /// RLP encode an unsigned transaction for the specified chain ID. + fn rlp_append_unsigned(&self, rlp: &mut RlpStream, chain_id: u64) { + rlp.begin_list(9); + rlp.append(&self.nonce); + rlp.append(&self.gas_price); + rlp.append(&self.gas); + if let Some(to) = self.to { + rlp.append(&to); + } else { + rlp.append(&""); + } + rlp.append(&self.value); + rlp.append(&self.data); + rlp.append(&chain_id); + rlp.append(&0u8); + rlp.append(&0u8); + } + + /// RLP encode a signed transaction with the specified signature. + fn rlp_append_signed(&self, rlp: &mut RlpStream, signature: &Signature) { + rlp.begin_list(9); + rlp.append(&self.nonce); + rlp.append(&self.gas_price); + rlp.append(&self.gas); + if let Some(to) = self.to { + rlp.append(&to); + } else { + rlp.append(&""); + } + rlp.append(&self.value); + rlp.append(&self.data); + rlp.append(&signature.v); + rlp.append(&U256::from_big_endian(signature.r.as_bytes())); + rlp.append(&U256::from_big_endian(signature.s.as_bytes())); + } + + /// Sign and return a raw signed transaction. + fn sign(self, key: &SecretKey, chain_id: u64) -> SignedTransaction { + let mut rlp = RlpStream::new(); + self.rlp_append_unsigned(&mut rlp, chain_id); + + let hash = keccak256(rlp.as_raw()); + let message = Message::from_slice(&hash).expect("hash is non-zero 32-bytes; qed"); + let signature = sign(&message, key, Some(chain_id)); + + rlp.clear(); + self.rlp_append_signed(&mut rlp, &signature); + + let transaction_hash = keccak256(rlp.as_raw()).into(); + let raw_transaction = rlp.out().into(); + + SignedTransaction { + message_hash: hash.into(), + v: signature.v, + r: signature.r, + s: signature.s, + raw_transaction, + transaction_hash, + } + } +} + +/// A wrapper type around `SecretKey` to prevent leaking secret key data. This +/// type will properly zeroize the secret key to `ONE_KEY` in a way that will +/// not get optimized away by the compiler nor be prone to leaks that take +/// advantage of access reordering. +/// +/// This is required since the `SignTransactionFuture` needs to retain a copy +/// of the `SecretKey`. +#[derive(Clone, Copy)] +struct ZeroizeSecretKey(SecretKey); + +impl Default for ZeroizeSecretKey { + fn default() -> Self { + ZeroizeSecretKey(ONE_KEY) + } +} + +impl Deref for ZeroizeSecretKey { + type Target = SecretKey; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DefaultIsZeroes for ZeroizeSecretKey {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::tests::TestTransport; + use crate::types::Bytes; + use rustc_hex::FromHex; + use serde_json::json; + + #[test] + fn accounts_sign_transaction() { + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction + + let tx = TransactionParameters { + to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse().unwrap()), + value: 1_000_000_000.into(), + gas: 2_000_000.into(), + ..Default::default() + }; + let key: SecretKey = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + .parse() + .unwrap(); + let nonce = U256::zero(); + let gas_price = U256::from(21_000_000_000u128); + let chain_id = "0x1"; + let from: Address = secret_key_address(&key); + + let mut transport = TestTransport::default(); + transport.add_response(json!(nonce)); + transport.add_response(json!(gas_price)); + transport.add_response(json!(chain_id)); + + let signed = { + let accounts = Accounts::new(&transport); + accounts.sign_transaction(tx, &key).wait() + }; + + transport.assert_request( + "eth_getTransactionCount", + &[json!(from).to_string(), json!("latest").to_string()], + ); + transport.assert_request("eth_gasPrice", &[]); + transport.assert_request("eth_chainId", &[]); + transport.assert_no_more_requests(); + + let expected = SignedTransaction { + message_hash: "88cfbd7e51c7a40540b233cf68b62ad1df3e92462f1c6018d6d67eae0f3b08f5" + .parse() + .unwrap(), + v: 0x25, + r: "c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895" + .parse() + .unwrap(), + s: "727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68" + .parse() + .unwrap(), + raw_transaction: Bytes( + "f869808504e3b29200831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a0c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895a0727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68" + .from_hex() + .unwrap(), + ), + transaction_hash: "de8db924885b0803d2edc335f745b2b8750c8848744905684c20b987443a9593" + .parse() + .unwrap(), + }; + + assert_eq!(signed, Ok(expected)); + } + + #[test] + fn accounts_sign_transaction_with_all_parameters() { + let key: SecretKey = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + .parse() + .unwrap(); + + let accounts = Accounts::new(TestTransport::default()); + accounts + .sign_transaction( + TransactionParameters { + nonce: Some(0.into()), + gas_price: Some(1.into()), + chain_id: Some(42), + ..Default::default() + }, + &key, + ) + .wait() + .unwrap(); + + // sign_transaction makes no requests when all parameters are specified + accounts.transport().assert_no_more_requests(); + } + + #[test] + fn accounts_hash_message() { + // test vector taken from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#hashmessage + + let accounts = Accounts::new(TestTransport::default()); + let hash = accounts.hash_message("Hello World"); + + assert_eq!( + hash, + "a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2" + .parse() + .unwrap() + ); + + // this method does not actually make any requests. + accounts.transport().assert_no_more_requests(); + } + + #[test] + fn accounts_sign() { + // test vector taken from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#sign + + let accounts = Accounts::new(TestTransport::default()); + + let key: SecretKey = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + .parse() + .unwrap(); + let signed = accounts.sign("Some data", &key); + + assert_eq!( + signed.message_hash, + "1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655" + .parse() + .unwrap() + ); + assert_eq!( + signed.signature.0, + "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c" + .from_hex::>() + .unwrap() + ); + + // this method does not actually make any requests. + accounts.transport().assert_no_more_requests(); + } + + #[test] + fn accounts_recover() { + // test vector taken from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#recover + + let accounts = Accounts::new(TestTransport::default()); + + let v = 0x1cu64; + let r: H256 = "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd" + .parse() + .unwrap(); + let s: H256 = "6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029" + .parse() + .unwrap(); + + let recovery = Recovery::new("Some data", v, r, s); + assert_eq!( + accounts.recover(recovery).unwrap(), + "2c7536E3605D9C16a7a3D7b1898e529396a65c23".parse().unwrap() + ); + + // this method does not actually make any requests. + accounts.transport().assert_no_more_requests(); + } + + #[test] + fn accounts_recover_signed() { + let key: SecretKey = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + .parse() + .unwrap(); + let address: Address = secret_key_address(&key); + + let accounts = Accounts::new(TestTransport::default()); + + let signed = accounts.sign("rust-web3 rocks!", &key); + let recovered = accounts.recover(&signed).unwrap(); + assert_eq!(recovered, address); + + let signed = accounts + .sign_transaction( + TransactionParameters { + nonce: Some(0.into()), + gas_price: Some(1.into()), + chain_id: Some(42), + ..Default::default() + }, + &key, + ) + .wait() + .unwrap(); + let recovered = accounts.recover(&signed).unwrap(); + assert_eq!(recovered, address); + + // these methods make no requests + accounts.transport().assert_no_more_requests(); + } + + #[test] + fn sign_transaction_data() { + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#eth-accounts-signtransaction + + let tx = Transaction { + nonce: 0.into(), + gas: 2_000_000.into(), + gas_price: 234_567_897_654_321u64.into(), + to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse().unwrap()), + value: 1_000_000_000.into(), + data: Vec::new(), + }; + let key: SecretKey = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + .parse() + .unwrap(); + + let signed = tx.sign(&key, 1); + + let expected = SignedTransaction { + message_hash: "6893a6ee8df79b0f5d64a180cd1ef35d030f3e296a5361cf04d02ce720d32ec5" + .parse() + .unwrap(), + v: 0x25, + r: "09ebb6ca057a0535d6186462bc0b465b561c94a295bdb0621fc19208ab149a9c" + .parse() + .unwrap(), + s: "440ffd775ce91a833ab410777204d5341a6f9fa91216a6f3ee2c051fea6a0428" + .parse() + .unwrap(), + raw_transaction: Bytes( + "f86a8086d55698372431831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a009ebb6ca057a0535d6186462bc0b465b561c94a295bdb0621fc19208ab149a9ca0440ffd775ce91a833ab410777204d5341a6f9fa91216a6f3ee2c051fea6a0428" + .from_hex() + .unwrap(), + ), + transaction_hash: "d8f64a42b57be0d565f385378db2f6bf324ce14a594afc05de90436e9ce01f60" + .parse() + .unwrap(), + }; + + assert_eq!(signed, expected); + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 240283b9..911c22db 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,6 @@ //! `Web3` implementation +mod accounts; mod eth; mod eth_filter; mod eth_subscribe; @@ -11,6 +12,7 @@ mod personal; mod traces; mod web3; +pub use self::accounts::{Accounts, SignTransactionFuture}; pub use self::eth::Eth; pub use self::eth_filter::{BaseFilter, CreateFilter, EthFilter, FilterStream}; pub use self::eth_subscribe::{EthSubscribe, SubscriptionId, SubscriptionResult, SubscriptionStream}; @@ -58,6 +60,11 @@ impl Web3 { A::new(self.transport.clone()) } + /// Access methods from `accounts` namespace + pub fn accounts(&self) -> accounts::Accounts { + self.api() + } + /// Access methods from `eth` namespace pub fn eth(&self) -> eth::Eth { self.api() diff --git a/src/error.rs b/src/error.rs index 9edea04e..538137e2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,7 @@ //! Web3 Error use crate::rpc::error::Error as RPCError; use derive_more::{Display, From}; +use secp256k1::Error as Secp256k1Error; use serde_json::Error as SerdeError; use std::io::Error as IoError; @@ -27,6 +28,9 @@ pub enum Error { /// io error #[display(fmt = "IO error: {}", _0)] Io(IoError), + /// signing error + #[display(fmt = "Signing error: {}", _0)] + Signing(Secp256k1Error), /// web3 internal error #[display(fmt = "Internal Web3 error")] Internal, @@ -39,6 +43,7 @@ impl std::error::Error for Error { Unreachable | Decoder(_) | InvalidResponse(_) | Transport(_) | Internal => None, Rpc(ref e) => Some(e), Io(ref e) => Some(e), + Signing(ref e) => Some(e), } } } @@ -59,6 +64,7 @@ impl Clone for Error { Transport(s) => Transport(s.clone()), Rpc(e) => Rpc(e.clone()), Io(e) => Io(IoError::from(e.kind())), + Signing(e) => Signing(*e), Internal => Internal, } } @@ -74,6 +80,7 @@ impl PartialEq for Error { } (Rpc(a), Rpc(b)) => a == b, (Io(a), Io(b)) => a.kind() == b.kind(), + (Signing(a), Signing(b)) => a == b, _ => false, } } diff --git a/src/helpers.rs b/src/helpers.rs index 448c0639..a439bcb7 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -145,7 +145,7 @@ pub mod tests { assert_eq!(p, params); } - pub fn assert_no_more_requests(&mut self) { + pub fn assert_no_more_requests(&self) { let requests = self.requests.borrow(); assert_eq!( self.asserted, diff --git a/src/types/mod.rs b/src/types/mod.rs index 3df0e2fc..5b4a9bd3 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -4,6 +4,8 @@ mod block; mod bytes; mod log; mod parity_peers; +mod recovery; +mod signed; mod sync_state; mod trace_filtering; mod traces; @@ -19,6 +21,8 @@ pub use self::log::{Filter, FilterBuilder, Log}; pub use self::parity_peers::{ EthProtocolInfo, ParityPeerInfo, ParityPeerType, PeerNetworkInfo, PeerProtocolsInfo, PipProtocolInfo, }; +pub use self::recovery::{Recovery, RecoveryMessage}; +pub use self::signed::{SignedData, SignedTransaction, TransactionParameters}; pub use self::sync_state::{SyncInfo, SyncState}; pub use self::trace_filtering::{ Action, ActionType, Call, CallResult, CallType, Create, CreateResult, Res, Reward, RewardType, Suicide, Trace, diff --git a/src/types/recovery.rs b/src/types/recovery.rs new file mode 100644 index 00000000..da860a1c --- /dev/null +++ b/src/types/recovery.rs @@ -0,0 +1,207 @@ +use crate::types::{SignedData, SignedTransaction, H256}; +use secp256k1::recovery::{RecoverableSignature, RecoveryId}; +use secp256k1::Error as Secp256k1Error; +use std::error::Error; +use std::fmt::{Display, Formatter, Result as FmtResult}; + +/// Data for recovering the public address of signed data. +/// +/// Note that the signature data is in 'Electrum' notation and may have chain +/// replay protection applied. That means that `v` is expected to be `27`, `28`, +/// or `35 + chain_id * 2` or `36 + chain_id * 2`. +#[derive(Clone, Debug, PartialEq)] +pub struct Recovery { + /// The message to recover + pub message: RecoveryMessage, + /// V value. + pub v: u64, + /// R value. + pub r: H256, + /// S value. + pub s: H256, +} + +impl Recovery { + /// Creates new recovery data from its parts. + pub fn new(message: M, v: u64, r: H256, s: H256) -> Recovery + where + M: Into, + { + Recovery { + message: message.into(), + v, + r, + s, + } + } + + /// Creates new recovery data from a raw signature. + /// + /// This parses a raw signature which is expected to be 65 bytes long where + /// the first 32 bytes is the `r` value, the second 32 bytes the `s` value + /// and the final byte is the `v` value in 'Electrum' notation. + pub fn from_raw_signature(message: M, raw_signature: B) -> Result + where + M: Into, + B: AsRef<[u8]>, + { + let bytes = raw_signature.as_ref(); + + if bytes.len() != 65 { + return Err(ParseSignatureError); + } + + let v = bytes[64]; + let r = H256::from_slice(&bytes[0..32]); + let s = H256::from_slice(&bytes[32..64]); + + Ok(Recovery::new(message, v as _, r, s)) + } + + /// Retrieve the recovery ID. + pub fn recovery_id(&self) -> Result { + let standard_v = match self.v { + 27 => 0, + 28 => 1, + v if v >= 35 => ((v - 1) % 2) as _, + _ => 4, + }; + + RecoveryId::from_i32(standard_v) + } + + /// Retrieves the recovery signature. + pub fn as_signature(&self) -> Result { + let recovery_id = self.recovery_id()?; + let signature = { + let mut sig = [0u8; 64]; + sig[..32].copy_from_slice(self.r.as_bytes()); + sig[32..].copy_from_slice(self.s.as_bytes()); + sig + }; + + RecoverableSignature::from_compact(&signature, recovery_id) + } +} + +impl<'a> From<&'a SignedData> for Recovery { + fn from(signed: &'a SignedData) -> Self { + Recovery::new(signed.message_hash, signed.v as _, signed.r, signed.s) + } +} + +impl<'a> From<&'a SignedTransaction> for Recovery { + fn from(tx: &'a SignedTransaction) -> Self { + Recovery::new(tx.message_hash, tx.v, tx.r, tx.s) + } +} + +/// Recovery message data. +/// +/// The message data can either be a binary message that is first hashed +/// according to EIP-191 and then recovered based on the signature or a +/// precomputed hash. +#[derive(Clone, Debug, PartialEq)] +pub enum RecoveryMessage { + /// Message bytes + Data(Vec), + /// Message hash + Hash(H256), +} + +impl From<&[u8]> for RecoveryMessage { + fn from(s: &[u8]) -> Self { + s.to_owned().into() + } +} + +impl From> for RecoveryMessage { + fn from(s: Vec) -> Self { + RecoveryMessage::Data(s) + } +} + +impl From<&str> for RecoveryMessage { + fn from(s: &str) -> Self { + s.as_bytes().to_owned().into() + } +} + +impl From for RecoveryMessage { + fn from(s: String) -> Self { + RecoveryMessage::Data(s.into_bytes()) + } +} + +impl From<[u8; 32]> for RecoveryMessage { + fn from(hash: [u8; 32]) -> Self { + H256(hash).into() + } +} + +impl From for RecoveryMessage { + fn from(hash: H256) -> Self { + RecoveryMessage::Hash(hash) + } +} + +/// An error parsing a raw signature. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct ParseSignatureError; + +impl Display for ParseSignatureError { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "error parsing raw signature: wrong number of bytes, expected 65") + } +} + +impl Error for ParseSignatureError {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::Bytes; + use rustc_hex::FromHex; + + #[test] + fn recovery_signature() { + let message = "Some data"; + let v = 0x1cu8; + let r: H256 = "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd" + .parse() + .unwrap(); + let s: H256 = "6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029" + .parse() + .unwrap(); + + let signed = SignedData { + message: message.as_bytes().to_owned(), + message_hash: "1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655" + .parse() + .unwrap(), + v, + r, + s, + signature: Bytes( + "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c" + .from_hex() + .unwrap() + ), + }; + let expected_signature = RecoverableSignature::from_compact( + &"b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029" + .from_hex::>() + .unwrap(), + RecoveryId::from_i32(1).unwrap(), + ); + + assert_eq!(Recovery::from(&signed).as_signature(), expected_signature); + assert_eq!(Recovery::new(message, v as _, r, s).as_signature(), expected_signature); + assert_eq!( + Recovery::from_raw_signature(message, &signed.signature.0) + .unwrap() + .as_signature(), + expected_signature + ); + } +} diff --git a/src/types/signed.rs b/src/types/signed.rs new file mode 100644 index 00000000..c9cbb60a --- /dev/null +++ b/src/types/signed.rs @@ -0,0 +1,138 @@ +use crate::types::{Address, Bytes, CallRequest, H256, U256}; +use serde::{Deserialize, Serialize}; + +/// Struct representing signed data returned from `Accounts::sign` method. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub struct SignedData { + /// The original message that was signed. + pub message: Vec, + /// The keccak256 hash of the signed data. + #[serde(rename = "messageHash")] + pub message_hash: H256, + /// V value in 'Electrum' notation. + pub v: u8, + /// R value. + pub r: H256, + /// S value. + pub s: H256, + /// The signature bytes. + pub signature: Bytes, +} + +/// Transaction data for signing. +/// +/// The `Accounts::sign_transaction` method will fill optional fields with sane +/// defaults when they are omitted. Specifically the signing account's current +/// transaction count will be used for the `nonce`, the estimated recommended +/// gas price will be used for `gas_price`, and the current network ID will be +/// used for the `chain_id`. +/// +/// It is worth noting that the chain ID is not equivalent to the network ID. +/// They happen to be the same much of the time but it is recommended to set +/// this for signing transactions. +/// +/// `TransactionParameters` implements `Default` and uses `100_000` as the +/// default `gas` to use for the transaction. This is more than enough for +/// simple transactions sending ETH between accounts but may not be enough when +/// interacting with complex contracts. It is recommended when interacting +/// with contracts to use `Eth::estimate_gas` to estimate the required gas for +/// the transaction. +#[derive(Clone, Debug, PartialEq)] +pub struct TransactionParameters { + /// Transaction nonce (None for account transaction count) + pub nonce: Option, + /// To address + pub to: Option
, + /// Supplied gas + pub gas: U256, + /// Gas price (None for estimated gas price) + pub gas_price: Option, + /// Transfered value + pub value: U256, + /// Data + pub data: Bytes, + /// The chain ID (None for network ID) + pub chain_id: Option, +} + +/// The default fas for transactions. +/// +/// Unfortunatly there is no way to construct `U256`s with const functions for +/// constants so we just build it from it's `u64` words. Note that there is a +/// unit test to verify that it is constructed correctly and holds the expected +/// value of 100_000. +const TRANSACTION_DEFAULT_GAS: U256 = U256([100_000, 0, 0, 0]); + +impl Default for TransactionParameters { + fn default() -> Self { + TransactionParameters { + nonce: None, + to: None, + gas: TRANSACTION_DEFAULT_GAS, + gas_price: None, + value: U256::zero(), + data: Bytes::default(), + chain_id: None, + } + } +} + +impl From for TransactionParameters { + fn from(call: CallRequest) -> Self { + let to = if call.to != Address::zero() { + Some(call.to) + } else { + None + }; + + TransactionParameters { + nonce: None, + to, + gas: call.gas.unwrap_or(TRANSACTION_DEFAULT_GAS), + gas_price: call.gas_price, + value: call.value.unwrap_or_default(), + data: call.data.unwrap_or_default(), + chain_id: None, + } + } +} + +impl Into for TransactionParameters { + fn into(self) -> CallRequest { + CallRequest { + from: None, + to: self.to.unwrap_or_default(), + gas: Some(self.gas), + gas_price: self.gas_price, + value: Some(self.value), + data: Some(self.data), + } + } +} + +/// Data for offline signed transaction +#[derive(Clone, Debug, PartialEq)] +pub struct SignedTransaction { + /// The given message hash + pub message_hash: H256, + /// V value with chain replay protection. + pub v: u64, + /// R value. + pub r: H256, + /// S value. + pub s: H256, + /// The raw signed transaction ready to be sent with `send_raw_transaction` + pub raw_transaction: Bytes, + /// The transaction hash for the RLP encoded transaction. + pub transaction_hash: H256, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_transaction_default_gas() { + assert_eq!(TRANSACTION_DEFAULT_GAS, U256::from(100_000)); + } +}