Wallet rpcs (#1898)

* Add wallet RPCs.

* Add wordlist file.

* Add standard brain wallet tests.

* Allow import of JSON wallets.

* Address grumble.
This commit is contained in:
Gav Wood 2016-08-10 17:57:40 +02:00 committed by Arkadiy Paronyan
parent c32244ea4a
commit 286b67d54b
18 changed files with 7685 additions and 6 deletions

3
Cargo.lock generated
View File

@ -416,6 +416,7 @@ dependencies = [
"ethcore-ipc 1.4.0", "ethcore-ipc 1.4.0",
"ethcore-util 1.4.0", "ethcore-util 1.4.0",
"ethjson 0.1.0", "ethjson 0.1.0",
"ethstore 0.1.0",
"ethsync 1.4.0", "ethsync 1.4.0",
"json-ipc-server 0.2.4 (git+https://github.com/ethcore/json-ipc-server.git)", "json-ipc-server 0.2.4 (git+https://github.com/ethcore/json-ipc-server.git)",
"jsonrpc-core 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -505,6 +506,8 @@ name = "ethstore"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ethkey 0.2.0", "ethkey 0.2.0",
"itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -191,6 +191,18 @@ impl AccountProvider {
Ok(Address::from(address).into()) Ok(Address::from(address).into())
} }
/// Import a new presale wallet.
pub fn import_presale(&self, presale_json: &[u8], password: &str) -> Result<H160, Error> {
let address = try!(self.sstore.import_presale(presale_json, password));
Ok(Address::from(address).into())
}
/// Import a new presale wallet.
pub fn import_wallet(&self, json: &[u8], password: &str) -> Result<H160, Error> {
let address = try!(self.sstore.import_wallet(json, password));
Ok(Address::from(address).into())
}
/// Returns addresses of all accounts. /// Returns addresses of all accounts.
pub fn accounts(&self) -> Result<Vec<H160>, Error> { pub fn accounts(&self) -> Result<Vec<H160>, Error> {
let accounts = try!(self.sstore.accounts()).into_iter().map(|a| H160(a.into())).collect(); let accounts = try!(self.sstore.accounts()).into_iter().map(|a| H160(a.into())).collect();

View File

@ -5,7 +5,7 @@ authors = ["debris <marek.kotewicz@gmail.com>"]
[dependencies] [dependencies]
rand = "0.3.14" rand = "0.3.14"
lazy_static = "0.2.1" lazy_static = "0.2"
tiny-keccak = "1.0" tiny-keccak = "1.0"
eth-secp256k1 = { git = "https://github.com/ethcore/rust-secp256k1" } eth-secp256k1 = { git = "https://github.com/ethcore/rust-secp256k1" }
rustc-serialize = "0.3" rustc-serialize = "0.3"

View File

@ -16,6 +16,8 @@ rust-crypto = "0.2.36"
tiny-keccak = "1.0" tiny-keccak = "1.0"
docopt = { version = "0.6", optional = true } docopt = { version = "0.6", optional = true }
time = "0.1.34" time = "0.1.34"
lazy_static = "0.2"
itertools = "0.4"
[build-dependencies] [build-dependencies]
serde_codegen = { version = "0.7", optional = true } serde_codegen = { version = "0.7", optional = true }

7530
ethstore/res/wordlist.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -66,7 +66,7 @@ impl Into<json::KeyFile> for SafeAccount {
json::KeyFile { json::KeyFile {
id: From::from(self.id), id: From::from(self.id),
version: self.version.into(), version: self.version.into(),
address: self.address.into(), //From::from(self.address), address: self.address.into(),
crypto: self.crypto.into(), crypto: self.crypto.into(),
name: Some(self.name.into()), name: Some(self.name.into()),
meta: Some(self.meta.into()), meta: Some(self.meta.into()),
@ -150,13 +150,16 @@ impl SafeAccount {
} }
} }
pub fn from_file(json: json::KeyFile, filename: String) -> Self { /// Create a new `SafeAccount` from the given `json`; if it was read from a
/// file, the `filename` should be `Some` name. If it is as yet anonymous, then it
/// can be left `None`.
pub fn from_file(json: json::KeyFile, filename: Option<String>) -> Self {
SafeAccount { SafeAccount {
id: json.id.into(), id: json.id.into(),
version: json.version.into(), version: json.version.into(),
address: json.address.into(), address: json.address.into(),
crypto: json.crypto.into(), crypto: json.crypto.into(),
filename: Some(filename), filename: filename,
name: json.name.unwrap_or(String::new()), name: json.name.unwrap_or(String::new()),
meta: json.meta.unwrap_or("{}".to_owned()), meta: json.meta.unwrap_or("{}".to_owned()),
} }

View File

@ -87,7 +87,7 @@ impl DiskDirectory {
.zip(paths.into_iter()) .zip(paths.into_iter())
.map(|(file, path)| match file { .map(|(file, path)| match file {
Ok(file) => Ok((path.clone(), SafeAccount::from_file( Ok(file) => Ok((path.clone(), SafeAccount::from_file(
file, path.file_name().and_then(|n| n.to_str()).expect("Keys have valid UTF8 names only.").to_owned() file, Some(path.file_name().and_then(|n| n.to_str()).expect("Keys have valid UTF8 names only.").to_owned())
))), ))),
Err(err) => Err(Error::InvalidKeyFile(format!("{:?}: {}", path, err))), Err(err) => Err(Error::InvalidKeyFile(format!("{:?}: {}", path, err))),
}) })

View File

@ -24,7 +24,9 @@ use ethkey::{Signature, Address, Message, Secret};
use dir::KeyDirectory; use dir::KeyDirectory;
use account::SafeAccount; use account::SafeAccount;
use {Error, SecretStore}; use {Error, SecretStore};
use json;
use json::UUID; use json::UUID;
use presale::PresaleWallet;
pub struct EthStore { pub struct EthStore {
dir: Box<KeyDirectory>, dir: Box<KeyDirectory>,
@ -89,6 +91,23 @@ impl SecretStore for EthStore {
Ok(address) Ok(address)
} }
fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error> {
let json_wallet = try!(json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned())));
let wallet = PresaleWallet::from(json_wallet);
let keypair = try!(wallet.decrypt(password).map_err(|_| Error::InvalidPassword));
self.insert_account(keypair.secret().clone(), password)
}
fn import_wallet(&self, json: &[u8], password: &str) -> Result<Address, Error> {
let json_keyfile = try!(json::KeyFile::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned())));
let mut safe_account = SafeAccount::from_file(json_keyfile, None);
let secret = try!(safe_account.crypto.secret(password).map_err(|_| Error::InvalidPassword));
safe_account.address = try!(KeyPair::from_secret(secret)).address();
let address = safe_account.address.clone();
try!(self.save(safe_account));
Ok(address)
}
fn accounts(&self) -> Result<Vec<Address>, Error> { fn accounts(&self) -> Result<Vec<Address>, Error> {
try!(self.reload_accounts()); try!(self.reload_accounts());
Ok(self.cache.read().unwrap().keys().cloned().collect()) Ok(self.cache.read().unwrap().keys().cloned().collect())

View File

@ -18,6 +18,7 @@
#![cfg_attr(feature="nightly", plugin(serde_macros))] #![cfg_attr(feature="nightly", plugin(serde_macros))]
extern crate libc; extern crate libc;
extern crate itertools;
extern crate rand; extern crate rand;
extern crate time; extern crate time;
extern crate serde; extern crate serde;
@ -25,6 +26,8 @@ extern crate serde_json;
extern crate rustc_serialize; extern crate rustc_serialize;
extern crate crypto as rcrypto; extern crate crypto as rcrypto;
extern crate tiny_keccak; extern crate tiny_keccak;
#[macro_use]
extern crate lazy_static;
// reexport it nicely // reexport it nicely
extern crate ethkey as _ethkey; extern crate ethkey as _ethkey;
@ -48,4 +51,5 @@ pub use self::ethstore::EthStore;
pub use self::import::import_accounts; pub use self::import::import_accounts;
pub use self::presale::PresaleWallet; pub use self::presale::PresaleWallet;
pub use self::secret_store::SecretStore; pub use self::secret_store::SecretStore;
pub use self::random::random_phrase;

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use rand::{Rng, OsRng}; use rand::{Rng, OsRng};
use itertools::Itertools;
pub trait Random { pub trait Random {
fn random() -> Self where Self: Sized; fn random() -> Self where Self: Sized;
@ -37,3 +38,25 @@ impl Random for [u8; 32] {
result result
} }
} }
/// Generate a string which is a random phrase of a number of lowercase words.
///
/// `words` is the number of words, chosen from a dictionary of 7,530. An value of
/// 12 gives 155 bits of entropy (almost saturating address space); 20 gives 258 bits
/// which is enough to saturate 32-byte key space
pub fn random_phrase(words: usize) -> String {
lazy_static! {
static ref WORDS: Vec<String> = String::from_utf8_lossy(include_bytes!("../res/wordlist.txt"))
.split("\n")
.map(|s| s.to_owned())
.collect();
}
let mut rng = OsRng::new().unwrap();
(0..words).map(|_| rng.choose(&WORDS).unwrap()).join(" ")
}
#[test]
fn should_produce_right_number_of_words() {
let p = random_phrase(10);
assert_eq!(p.split(" ").count(), 10);
}

View File

@ -21,6 +21,10 @@ use json::UUID;
pub trait SecretStore: Send + Sync { pub trait SecretStore: Send + Sync {
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error>; fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error>;
fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error>;
fn import_wallet(&self, json: &[u8], password: &str) -> Result<Address, Error>;
fn accounts(&self) -> Result<Vec<Address>, Error>; fn accounts(&self) -> Result<Vec<Address>, Error>;
fn change_password(&self, account: &Address, old_password: &str, new_password: &str) -> Result<(), Error>; fn change_password(&self, account: &Address, old_password: &str, new_password: &str) -> Result<(), Error>;

View File

@ -17,6 +17,7 @@ jsonrpc-http-server = { git = "https://github.com/ethcore/jsonrpc-http-server.gi
ethcore-io = { path = "../util/io" } ethcore-io = { path = "../util/io" }
ethcore-util = { path = "../util" } ethcore-util = { path = "../util" }
ethcore = { path = "../ethcore" } ethcore = { path = "../ethcore" }
ethstore = { path = "../ethstore" }
ethash = { path = "../ethash" } ethash = { path = "../ethash" }
ethsync = { path = "../sync" } ethsync = { path = "../sync" }
ethjson = { path = "../json" } ethjson = { path = "../json" }

View File

@ -30,6 +30,7 @@ extern crate jsonrpc_http_server;
extern crate ethcore_util as util; extern crate ethcore_util as util;
extern crate ethcore_io as io; extern crate ethcore_io as io;
extern crate ethcore; extern crate ethcore;
extern crate ethstore;
extern crate ethsync; extern crate ethsync;
extern crate transient_hashmap; extern crate transient_hashmap;
extern crate json_ipc_server as ipc; extern crate json_ipc_server as ipc;

View File

@ -19,6 +19,7 @@ use util::{RotatingLogger};
use util::misc::version_data; use util::misc::version_data;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use std::collections::{BTreeMap}; use std::collections::{BTreeMap};
use ethstore::random_phrase;
use ethcore::client::{MiningBlockChainClient}; use ethcore::client::{MiningBlockChainClient};
use jsonrpc_core::*; use jsonrpc_core::*;
use ethcore::miner::MinerService; use ethcore::miner::MinerService;
@ -165,4 +166,11 @@ impl<C, M> Ethcore for EthcoreClient<C, M> where M: MinerService + 'static, C: M
Some(ref queue) => to_value(&queue.len()), Some(ref queue) => to_value(&queue.len()),
} }
} }
fn generate_secret_phrase(&self, params: Params) -> Result<Value, Error> {
try!(self.active());
try!(expect_no_params(params));
to_value(&random_phrase(12))
}
} }

View File

@ -24,7 +24,7 @@ use v1::helpers::{errors, TransactionRequest as TRequest};
use v1::helpers::params::expect_no_params; use v1::helpers::params::expect_no_params;
use v1::helpers::dispatch::unlock_sign_and_dispatch; use v1::helpers::dispatch::unlock_sign_and_dispatch;
use ethcore::account_provider::AccountProvider; use ethcore::account_provider::AccountProvider;
use util::Address; use util::{Address, KeyPair};
use ethcore::client::MiningBlockChainClient; use ethcore::client::MiningBlockChainClient;
use ethcore::miner::MinerService; use ethcore::miner::MinerService;
@ -89,6 +89,32 @@ impl<C: 'static, M: 'static> Personal for PersonalClient<C, M> where C: MiningBl
) )
} }
fn new_account_from_phrase(&self, params: Params) -> Result<Value, Error> {
try!(self.active());
from_params::<(String, String, )>(params).and_then(
|(phrase, pass, )| {
let store = take_weak!(self.accounts);
match store.insert_account(*KeyPair::from_phrase(&phrase).secret(), &pass) {
Ok(address) => to_value(&RpcH160::from(address)),
Err(e) => Err(errors::account("Could not create account.", e)),
}
}
)
}
fn new_account_from_wallet(&self, params: Params) -> Result<Value, Error> {
try!(self.active());
from_params::<(String, String, )>(params).and_then(
|(json, pass, )| {
let store = take_weak!(self.accounts);
match store.import_presale(json.as_bytes(), &pass).or_else(|_| store.import_wallet(json.as_bytes(), &pass)) {
Ok(address) => to_value(&RpcH160::from(address)),
Err(e) => Err(errors::account("Could not create account.", e)),
}
}
)
}
fn unlock_account(&self, params: Params) -> Result<Value, Error> { fn unlock_account(&self, params: Params) -> Result<Value, Error> {
try!(self.active()); try!(self.active());
from_params::<(RpcH160, String, Option<u64>)>(params).and_then( from_params::<(RpcH160, String, Option<u64>)>(params).and_then(

View File

@ -67,6 +67,9 @@ pub trait Ethcore: Sized + Send + Sync + 'static {
/// Returns error when signer is disabled /// Returns error when signer is disabled
fn unsigned_transactions_count(&self, _: Params) -> Result<Value, Error>; fn unsigned_transactions_count(&self, _: Params) -> Result<Value, Error>;
/// Returns a cryptographically random phrase sufficient for securely seeding a secret key.
fn generate_secret_phrase(&self, _: Params) -> Result<Value, Error>;
/// Should be used to convert object to io delegate. /// Should be used to convert object to io delegate.
fn to_delegate(self) -> IoDelegate<Self> { fn to_delegate(self) -> IoDelegate<Self> {
let mut delegate = IoDelegate::new(Arc::new(self)); let mut delegate = IoDelegate::new(Arc::new(self));
@ -86,6 +89,7 @@ pub trait Ethcore: Sized + Send + Sync + 'static {
delegate.add_method("ethcore_defaultExtraData", Ethcore::default_extra_data); delegate.add_method("ethcore_defaultExtraData", Ethcore::default_extra_data);
delegate.add_method("ethcore_gasPriceStatistics", Ethcore::gas_price_statistics); delegate.add_method("ethcore_gasPriceStatistics", Ethcore::gas_price_statistics);
delegate.add_method("ethcore_unsignedTransactionsCount", Ethcore::unsigned_transactions_count); delegate.add_method("ethcore_unsignedTransactionsCount", Ethcore::unsigned_transactions_count);
delegate.add_method("ethcore_generateSecretPhrase", Ethcore::generate_secret_phrase);
delegate delegate
} }

View File

@ -25,8 +25,17 @@ pub trait Personal: Sized + Send + Sync + 'static {
fn accounts(&self, _: Params) -> Result<Value, Error>; fn accounts(&self, _: Params) -> Result<Value, Error>;
/// Creates new account (it becomes new current unlocked account) /// Creates new account (it becomes new current unlocked account)
/// Param is the password for the account.
fn new_account(&self, _: Params) -> Result<Value, Error>; fn new_account(&self, _: Params) -> Result<Value, Error>;
/// Creates new account from the given phrase using standard brainwallet mechanism.
/// Second parameter is password for the new account.
fn new_account_from_phrase(&self, _: Params) -> Result<Value, Error>;
/// Creates new account from the given JSON wallet.
/// Second parameter is password for the wallet and the new account.
fn new_account_from_wallet(&self, params: Params) -> Result<Value, Error>;
/// Unlocks specified account for use (can only be one unlocked account at one moment) /// Unlocks specified account for use (can only be one unlocked account at one moment)
fn unlock_account(&self, _: Params) -> Result<Value, Error>; fn unlock_account(&self, _: Params) -> Result<Value, Error>;
@ -51,6 +60,8 @@ pub trait Personal: Sized + Send + Sync + 'static {
delegate.add_method("personal_signerEnabled", Personal::signer_enabled); delegate.add_method("personal_signerEnabled", Personal::signer_enabled);
delegate.add_method("personal_listAccounts", Personal::accounts); delegate.add_method("personal_listAccounts", Personal::accounts);
delegate.add_method("personal_newAccount", Personal::new_account); delegate.add_method("personal_newAccount", Personal::new_account);
delegate.add_method("personal_newAccountFromPhrase", Personal::new_account_from_phrase);
delegate.add_method("personal_newAccountFromWallet", Personal::new_account_from_wallet);
delegate.add_method("personal_unlockAccount", Personal::unlock_account); delegate.add_method("personal_unlockAccount", Personal::unlock_account);
delegate.add_method("personal_signAndSendTransaction", Personal::sign_and_send_transaction); delegate.add_method("personal_signAndSendTransaction", Personal::sign_and_send_transaction);
delegate.add_method("personal_setAccountName", Personal::set_account_name); delegate.add_method("personal_setAccountName", Personal::set_account_name);

View File

@ -134,6 +134,29 @@ impl KeyPair {
public: p, public: p,
}) })
} }
// TODO: move to ethstore/secret.rs once @debris has refactored necessary dependencies into own crate
/// Convert the given phrase into a secret as per brain-wallet spec.
/// Taken from https://github.com/ethereum/wiki/wiki/Brain-Wallet
/// Note particularly secure for low-entropy keys.
pub fn from_phrase(phrase: &str) -> KeyPair {
let mut h = phrase.as_bytes().sha3();
for _ in 0..16384 {
h = h.sha3();
}
loop {
let r = KeyPair::from_secret(h);
if r.is_ok() {
let r = r.unwrap();
if r.address()[0] == 0 {
return r;
}
}
h = h.sha3();
}
}
/// Create a new random key pair /// Create a new random key pair
pub fn create() -> Result<KeyPair, CryptoError> { pub fn create() -> Result<KeyPair, CryptoError> {
let context = &SECP256K1; let context = &SECP256K1;
@ -443,6 +466,11 @@ mod tests {
assert_eq!(pair.public().hex(), "101b3ef5a4ea7a1c7928e24c4c75fd053c235d7b80c22ae5c03d145d0ac7396e2a4ffff9adee3133a7b05044a5cee08115fd65145e5165d646bde371010d803c"); assert_eq!(pair.public().hex(), "101b3ef5a4ea7a1c7928e24c4c75fd053c235d7b80c22ae5c03d145d0ac7396e2a4ffff9adee3133a7b05044a5cee08115fd65145e5165d646bde371010d803c");
} }
#[test]
fn test_key_from_phrase() {
assert_eq!(KeyPair::from_phrase("correct horse battery staple").address(), "0021f80b7f29b9c84e8099c2c6c74a46ed2268c4".into());
}
#[test] #[test]
fn ecies_shared() { fn ecies_shared() {
let kp = KeyPair::create().unwrap(); let kp = KeyPair::create().unwrap();