From 5579582a52c5628fcc22f26fe6f063601c9205bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Mon, 23 May 2016 11:30:11 +0200 Subject: [PATCH 1/3] Sign and send transaction --- rpc/src/v1/impls/eth.rs | 57 ++++++++++++-------- rpc/src/v1/tests/eth.rs | 75 ++++++++++++++++++++++++++ rpc/src/v1/traits/eth.rs | 4 ++ util/src/keys/store.rs | 45 +++++++++++++++- util/src/keys/test_account_provider.rs | 11 ++++ 5 files changed, 168 insertions(+), 24 deletions(-) diff --git a/rpc/src/v1/impls/eth.rs b/rpc/src/v1/impls/eth.rs index 21f89eee6..f6543ec04 100644 --- a/rpc/src/v1/impls/eth.rs +++ b/rpc/src/v1/impls/eth.rs @@ -47,7 +47,7 @@ pub struct EthClient where A: AccountProvider, M: MinerService, EM: ExternalMinerService { - + client: Weak, sync: Weak, accounts: Weak, @@ -153,6 +153,27 @@ impl EthClient where } } + fn sign_and_dispatch(&self, request: TransactionRequest, secret: H256) -> Result { + let signed_transaction = { + let client = take_weak!(self.client); + let miner = take_weak!(self.miner); + EthTransaction { + nonce: request.nonce + .or_else(|| miner + .last_nonce(&request.from) + .map(|nonce| nonce + U256::one())) + .unwrap_or_else(|| client.nonce(&request.from)), + action: request.to.map_or(Action::Create, Action::Call), + gas: request.gas.unwrap_or_else(|| miner.sensible_gas_limit()), + gas_price: request.gas_price.unwrap_or_else(|| miner.sensible_gas_price()), + value: request.value.unwrap_or_else(U256::zero), + data: request.data.map_or_else(Vec::new, |d| d.to_vec()), + }.sign(&secret) + }; + trace!(target: "miner", "send_transaction: dispatching tx: {}", encode(&signed_transaction).to_vec().pretty()); + self.dispatch_transaction(signed_transaction) + } + fn sign_call(&self, request: CallRequest) -> Result { let client = take_weak!(self.client); let miner = take_weak!(self.miner); @@ -483,27 +504,19 @@ impl Eth for EthClient where .and_then(|(request, )| { let accounts = take_weak!(self.accounts); match accounts.account_secret(&request.from) { - Ok(secret) => { - let signed_transaction = { - let client = take_weak!(self.client); - let miner = take_weak!(self.miner); - EthTransaction { - nonce: request.nonce - .or_else(|| miner - .last_nonce(&request.from) - .map(|nonce| nonce + U256::one())) - .unwrap_or_else(|| client.nonce(&request.from)), - action: request.to.map_or(Action::Create, Action::Call), - gas: request.gas.unwrap_or_else(|| miner.sensible_gas_limit()), - gas_price: request.gas_price.unwrap_or_else(|| miner.sensible_gas_price()), - value: request.value.unwrap_or_else(U256::zero), - data: request.data.map_or_else(Vec::new, |d| d.to_vec()), - }.sign(&secret) - }; - trace!(target: "miner", "send_transaction: dispatching tx: {}", encode(&signed_transaction).to_vec().pretty()); - self.dispatch_transaction(signed_transaction) - }, - Err(_) => { to_value(&H256::zero()) } + Ok(secret) => self.sign_and_dispatch(request, secret), + Err(_) => to_value(&H256::zero()) + } + }) + } + + fn sign_and_send_transaction(&self, params: Params) -> Result { + from_params::<(TransactionRequest, String)>(params) + .and_then(|(request, password)| { + let accounts = take_weak!(self.accounts); + match accounts.locked_account_secret(&request.from, &password) { + Ok(secret) => self.sign_and_dispatch(request, secret), + Err(_) => to_value(&H256::zero()), } }) } diff --git a/rpc/src/v1/tests/eth.rs b/rpc/src/v1/tests/eth.rs index 1e6aa05b2..cb2911b21 100644 --- a/rpc/src/v1/tests/eth.rs +++ b/rpc/src/v1/tests/eth.rs @@ -521,6 +521,81 @@ fn rpc_eth_send_transaction() { assert_eq!(tester.io.handle_request(request.as_ref()), Some(response)); } +#[test] +fn rpc_eth_sign_and_send_transaction_with_invalid_password() { + let account = TestAccount::new("password123"); + let address = account.address(); + + let tester = EthTester::default(); + tester.accounts_provider.accounts.write().unwrap().insert(address.clone(), account); + let request = r#"{ + "jsonrpc": "2.0", + "method": "eth_signAndSendTransaction", + "params": [{ + "from": ""#.to_owned() + format!("0x{:?}", address).as_ref() + r#"", + "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567", + "gas": "0x76c0", + "gasPrice": "0x9184e72a000", + "value": "0x9184e72a" + }, "password321"], + "id": 1 + }"#; + + let response = r#"{"jsonrpc":"2.0","result":"0x0000000000000000000000000000000000000000000000000000000000000000","id":1}"#; + + assert_eq!(tester.io.handle_request(request.as_ref()), Some(response.into())); +} + +#[test] +fn rpc_eth_sign_and_send_transaction() { + let account = TestAccount::new("password123"); + let address = account.address(); + let secret = account.secret.clone(); + + let tester = EthTester::default(); + tester.accounts_provider.accounts.write().unwrap().insert(address.clone(), account); + let request = r#"{ + "jsonrpc": "2.0", + "method": "eth_signAndSendTransaction", + "params": [{ + "from": ""#.to_owned() + format!("0x{:?}", address).as_ref() + r#"", + "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567", + "gas": "0x76c0", + "gasPrice": "0x9184e72a000", + "value": "0x9184e72a" + }, "password123"], + "id": 1 + }"#; + + let t = Transaction { + nonce: U256::zero(), + gas_price: U256::from(0x9184e72a000u64), + gas: U256::from(0x76c0), + action: Action::Call(Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap()), + value: U256::from(0x9184e72au64), + data: vec![] + }.sign(&secret); + + let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", t.hash()).as_ref() + r#"","id":1}"#; + + assert_eq!(tester.io.handle_request(request.as_ref()), Some(response)); + + tester.miner.last_nonces.write().unwrap().insert(address.clone(), U256::zero()); + + let t = Transaction { + nonce: U256::one(), + gas_price: U256::from(0x9184e72a000u64), + gas: U256::from(0x76c0), + action: Action::Call(Address::from_str("d46e8dd67c5d32be8058bb8eb970870f07244567").unwrap()), + value: U256::from(0x9184e72au64), + data: vec![] + }.sign(&secret); + + let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", t.hash()).as_ref() + r#"","id":1}"#; + + assert_eq!(tester.io.handle_request(request.as_ref()), Some(response)); +} + #[test] #[ignore] fn rpc_eth_send_raw_transaction() { diff --git a/rpc/src/v1/traits/eth.rs b/rpc/src/v1/traits/eth.rs index a28f72c5c..9cded5eb9 100644 --- a/rpc/src/v1/traits/eth.rs +++ b/rpc/src/v1/traits/eth.rs @@ -80,6 +80,9 @@ pub trait Eth: Sized + Send + Sync + 'static { /// Sends transaction. fn send_transaction(&self, _: Params) -> Result { rpc_unimplemented!() } + /// Sends transaction and signs it in single call. The account is not unlocked in such case. + fn sign_and_send_transaction(&self, _: Params) -> Result { rpc_unimplemented!() } + /// Sends signed transaction. fn send_raw_transaction(&self, _: Params) -> Result { rpc_unimplemented!() } @@ -152,6 +155,7 @@ pub trait Eth: Sized + Send + Sync + 'static { delegate.add_method("eth_getCode", Eth::code_at); delegate.add_method("eth_sign", Eth::sign); delegate.add_method("eth_sendTransaction", Eth::send_transaction); + delegate.add_method("eth_signAndSendTransaction", Eth::sign_and_send_transaction); delegate.add_method("eth_sendRawTransaction", Eth::send_raw_transaction); delegate.add_method("eth_call", Eth::call); delegate.add_method("eth_estimateGas", Eth::estimate_gas); diff --git a/util/src/keys/store.rs b/util/src/keys/store.rs index 13e53bdb5..3ecabc07c 100644 --- a/util/src/keys/store.rs +++ b/util/src/keys/store.rs @@ -58,13 +58,15 @@ pub enum EncryptedHashMapError { InvalidValueFormat(FromBytesError), } -/// Error retrieving value from encrypted hashmap +/// Error while signing a message #[derive(Debug)] pub enum SigningError { /// Account passed does not exist NoAccount, /// Account passed is not unlocked AccountNotUnlocked, + /// Invalid passphrase + InvalidPassword, /// Invalid secret in store InvalidSecret } @@ -96,6 +98,8 @@ pub trait AccountProvider : Send + Sync { fn new_account(&self, pass: &str) -> Result; /// Returns secret for unlocked `account`. fn account_secret(&self, account: &Address) -> Result; + /// Returns secret for locked account given passphrase. + fn locked_account_secret(&self, account: &Address, pass: &str) -> Result; /// Returns signature when unlocked `account` signs `message`. fn sign(&self, account: &Address, message: &H256) -> Result { self.account_secret(account).and_then(|s| crypto::ec::sign(&s, message).map_err(|_| SigningError::InvalidSecret)) @@ -127,7 +131,11 @@ impl AccountProvider for AccountService { fn account_secret(&self, account: &Address) -> Result { self.secret_store.read().unwrap().account_secret(account) } - /// Returns secret for unlocked account + /// Returns secret for locked account given passphrase. + fn locked_account_secret(&self, account: &Address, pass: &str) -> Result { + self.secret_store.read().unwrap().locked_account_secret(account, pass) + } + /// Signs a message using key of given unlocked account address. fn sign(&self, account: &Address, message: &H256) -> Result { self.secret_store.read().unwrap().sign(account, message) } @@ -317,6 +325,16 @@ impl SecretStore { ret } + /// Returns secret for unlocked account. + pub fn locked_account_secret(&self, account: &Address, pass: &str) -> Result { + let secret_id = try!(self.account(&account).ok_or(SigningError::NoAccount)); + self.get(&secret_id, pass).or_else(|e| Err(match e { + EncryptedHashMapError::InvalidPassword => SigningError::InvalidPassword, + EncryptedHashMapError::UnknownIdentifier => SigningError::NoAccount, + EncryptedHashMapError::InvalidValueFormat(_) => SigningError::InvalidSecret, + })) + } + /// Makes account unlocks expire and removes unused key files from memory pub fn collect_garbage(&mut self) { let mut garbage_lock = self.unlocks.write().unwrap(); @@ -679,6 +697,29 @@ mod tests { assert_eq!(Address::from(kp.public().sha3()), addr); } + + #[test] + fn secret_for_locked_account() { + // given + let temp = RandomTempPath::create_dir(); + let mut sstore = SecretStore::new_test(&temp); + let addr = sstore.new_account("test-pass").unwrap(); + + // when + // Invalid pass + let secret1 = sstore.locked_account_secret(&addr, "test-pass123"); + // Valid pass + let secret2 = sstore.locked_account_secret(&addr, "test-pass"); + // Account not unlocked + let secret3 = sstore.account_secret(&addr); + + + assert!(secret1.is_err(), "Invalid password should not return secret."); + assert!(secret2.is_ok(), "Should return secret provided valid passphrase."); + assert!(secret3.is_err(), "Account should still be locked."); + } + + #[test] fn can_create_service() { let temp = RandomTempPath::create_dir(); diff --git a/util/src/keys/test_account_provider.rs b/util/src/keys/test_account_provider.rs index b8aed85ce..539c6ca8c 100644 --- a/util/src/keys/test_account_provider.rs +++ b/util/src/keys/test_account_provider.rs @@ -103,5 +103,16 @@ impl AccountProvider for TestAccountProvider { .ok_or(SigningError::NoAccount) .map(|acc| acc.secret.clone()) } + + fn locked_account_secret(&self, address: &Address, pass: &str) -> Result { + let accounts = self.accounts.read().unwrap(); + match accounts.get(address) { + Some(ref acc) if acc.password == pass => { + Ok(acc.secret.clone()) + }, + Some(ref _acc) => Err(SigningError::InvalidPassword), + _ => Err(SigningError::NoAccount), + } + } } From fb0be9e730f2141e2bc0edadce6e0e07be0ee318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Mon, 23 May 2016 11:55:10 +0200 Subject: [PATCH 2/3] Updating status page (new docs) --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 14a73958a..3ea77cf45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -863,7 +863,7 @@ dependencies = [ [[package]] name = "parity-status" version = "0.4.3" -source = "git+https://github.com/ethcore/parity-status.git#1d383d74010f6ebcd712b60b8fc5ff547b44f4e5" +source = "git+https://github.com/ethcore/parity-status.git#606dfe6d272792f2fdb90909318f2439d58330a5" dependencies = [ "parity-webapp 0.2.0 (git+https://github.com/ethcore/parity-webapp.git)", ] From b1cfbc460ef468722ccc06bf1d64d893c7d00954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Mon, 23 May 2016 12:56:18 +0200 Subject: [PATCH 3/3] Using signAndSend in topbar --- Cargo.lock | 6 +++--- webapp/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ea77cf45..13c107bbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,7 +357,7 @@ dependencies = [ "jsonrpc-http-server 5.1.0 (git+https://github.com/ethcore/jsonrpc-http-server.git)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "parity-daodapp 0.2.1 (git+https://github.com/ethcore/parity-daodapp-rs.git)", - "parity-idmanager 0.2.2 (git+https://github.com/ethcore/parity-idmanager-rs.git)", + "parity-idmanager 0.3.2 (git+https://github.com/ethcore/parity-idmanager-rs.git)", "parity-makerotc 0.1.3 (git+https://github.com/ethcore/parity-makerotc-rs.git)", "parity-status 0.4.3 (git+https://github.com/ethcore/parity-status.git)", "parity-wallet 0.4.1 (git+https://github.com/ethcore/parity-wallet.git)", @@ -846,8 +846,8 @@ dependencies = [ [[package]] name = "parity-idmanager" -version = "0.2.2" -source = "git+https://github.com/ethcore/parity-idmanager-rs.git#19dd79ca7b7afb8824ad072b43ca63babe2ba9bc" +version = "0.3.2" +source = "git+https://github.com/ethcore/parity-idmanager-rs.git#5bc85d456455f7b9f6f257021dab9394de9e66b4" dependencies = [ "parity-webapp 0.2.0 (git+https://github.com/ethcore/parity-webapp.git)", ] diff --git a/webapp/Cargo.toml b/webapp/Cargo.toml index 7d13f9260..0b6c0d075 100644 --- a/webapp/Cargo.toml +++ b/webapp/Cargo.toml @@ -23,7 +23,7 @@ ethcore-util = { path = "../util" } parity-webapp = { git = "https://github.com/ethcore/parity-webapp.git", version = "0.2" } # List of apps parity-status = { git = "https://github.com/ethcore/parity-status.git", version = "0.4.3" } -parity-idmanager = { git = "https://github.com/ethcore/parity-idmanager-rs.git", version = "0.2.2" } +parity-idmanager = { git = "https://github.com/ethcore/parity-idmanager-rs.git", version = "0.3.2" } parity-wallet = { git = "https://github.com/ethcore/parity-wallet.git", version = "0.4.1", optional = true } parity-daodapp = { git = "https://github.com/ethcore/parity-daodapp-rs.git", version = "0.2.1", optional = true } parity-makerotc = { git = "https://github.com/ethcore/parity-makerotc-rs.git", version = "0.1.3", optional = true }