Merge pull request #5390 from paritytech/mh-publicnode-perf

Public node: perf and fixes
This commit is contained in:
Maciej Hirsz 2017-04-05 10:42:59 +02:00 committed by GitHub
commit 237bac4500
13 changed files with 316 additions and 121 deletions

View File

@ -176,7 +176,7 @@
"geopattern": "1.2.3", "geopattern": "1.2.3",
"isomorphic-fetch": "2.2.1", "isomorphic-fetch": "2.2.1",
"js-sha3": "0.5.5", "js-sha3": "0.5.5",
"keythereum": "0.4.3", "keythereum": "0.4.6",
"lodash": "4.17.2", "lodash": "4.17.2",
"loglevel": "1.4.1", "loglevel": "1.4.1",
"marked": "0.3.6", "marked": "0.3.6",

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { keythereum } from '../ethkey'; import { createKeyObject, decryptPrivateKey } from '../ethkey';
export default class Account { export default class Account {
constructor (persist, data) { constructor (persist, data) {
@ -31,12 +31,14 @@ export default class Account {
} }
isValidPassword (password) { isValidPassword (password) {
try { return decryptPrivateKey(this._keyObject, password)
keythereum.recover(Buffer.from(password), this._keyObject); .then((privateKey) => {
return true; if (!privateKey) {
} catch (e) { return false;
return false; }
}
return true;
});
} }
get address () { get address () {
@ -68,21 +70,23 @@ export default class Account {
} }
decryptPrivateKey (password) { decryptPrivateKey (password) {
return keythereum.recover(Buffer.from(password), this._keyObject); return decryptPrivateKey(this._keyObject, password);
}
changePassword (key, password) {
return createKeyObject(key, password).then((keyObject) => {
this._keyObject = keyObject;
this._persist();
});
} }
static fromPrivateKey (persist, key, password) { static fromPrivateKey (persist, key, password) {
const iv = keythereum.crypto.randomBytes(16); return createKeyObject(key, password).then((keyObject) => {
const salt = keythereum.crypto.randomBytes(32); const account = new Account(persist, { keyObject });
// Keythereum will fail if `password` is an empty string return account;
password = Buffer.from(password); });
const keyObject = keythereum.dump(password, key, salt, iv);
const account = new Account(persist, { keyObject });
return account;
} }
toJSON () { toJSON () {

View File

@ -38,14 +38,23 @@ export default class Accounts {
create (secret, password) { create (secret, password) {
const privateKey = Buffer.from(secret.slice(2), 'hex'); const privateKey = Buffer.from(secret.slice(2), 'hex');
const account = Account.fromPrivateKey(this.persist, privateKey, password);
this._store.push(account); return Account
this.lastAddress = account.address; .fromPrivateKey(this.persist, privateKey, password)
.then((account) => {
const { address } = account;
this.persist(); if (this._store.find((account) => account.address === address)) {
throw new Error(`Account ${address} already exists!`);
}
return account.address; this._store.push(account);
this.lastAddress = address;
this.persist();
return account.address;
});
} }
set lastAddress (value) { set lastAddress (value) {
@ -73,28 +82,41 @@ export default class Accounts {
remove (address, password) { remove (address, password) {
address = address.toLowerCase(); address = address.toLowerCase();
const account = this.get(address);
if (!account) {
return false;
}
return account
.isValidPassword(password)
.then((isValid) => {
if (!isValid) {
return false;
}
if (address === this.lastAddress) {
this.lastAddress = NULL_ADDRESS;
}
this.removeUnsafe(address);
return true;
});
}
removeUnsafe (address) {
address = address.toLowerCase();
const index = this._store.findIndex((account) => account.address === address); const index = this._store.findIndex((account) => account.address === address);
if (index === -1) { if (index === -1) {
return false; return;
}
const account = this._store[index];
if (!account.isValidPassword(password)) {
console.log('invalid password');
return false;
}
if (address === this.lastAddress) {
this.lastAddress = NULL_ADDRESS;
} }
this._store.splice(index, 1); this._store.splice(index, 1);
this.persist(); this.persist();
return true;
} }
mapArray (mapper) { mapArray (mapper) {

View File

@ -14,31 +14,35 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
// Allow a web worker in the browser, with a fallback for Node.js import workerPool from './workerPool';
const hasWebWorkers = typeof Worker !== 'undefined';
const KeyWorker = hasWebWorkers ? require('worker-loader!./worker')
: require('./worker').KeyWorker;
// Local accounts should never be used outside of the browser export function createKeyObject (key, password) {
export let keythereum = null; return workerPool.getWorker().action('createKeyObject', { key, password })
.then((obj) => JSON.parse(obj));
}
if (hasWebWorkers) { export function decryptPrivateKey (keyObject, password) {
require('keythereum/dist/keythereum'); return workerPool
.getWorker()
.action('decryptPrivateKey', { keyObject, password })
.then((privateKey) => {
if (privateKey) {
return Buffer.from(privateKey);
}
keythereum = window.keythereum; return null;
});
} }
export function phraseToAddress (phrase) { export function phraseToAddress (phrase) {
return phraseToWallet(phrase).then((wallet) => wallet.address); return phraseToWallet(phrase)
.then((wallet) => wallet.address);
} }
export function phraseToWallet (phrase) { export function phraseToWallet (phrase) {
return new Promise((resolve, reject) => { return workerPool.getWorker().action('phraseToWallet', phrase);
const worker = new KeyWorker(); }
worker.postMessage(phrase); export function verifySecret (secret) {
worker.onmessage = ({ data }) => { return workerPool.getWorker().action('verifySecret', secret);
resolve(data);
};
});
} }

View File

@ -14,58 +14,104 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { keccak_256 as keccak256 } from 'js-sha3';
import secp256k1 from 'secp256k1/js'; import secp256k1 from 'secp256k1/js';
import { keccak_256 as keccak256 } from 'js-sha3';
import { bytesToHex } from '~/api/util/format';
const isWorker = typeof self !== 'undefined';
// Stay compatible between environments // Stay compatible between environments
if (typeof self !== 'object') { if (!isWorker) {
const scope = typeof global === 'undefined' ? window : global; const scope = typeof global === 'undefined' ? window : global;
scope.self = scope; scope.self = scope;
} }
function bytesToHex (bytes) { // keythereum should never be used outside of the browser
return '0x' + Array.from(bytes).map(n => ('0' + n.toString(16)).slice(-2)).join(''); let keythereum = null;
if (isWorker) {
require('keythereum/dist/keythereum');
keythereum = self.keythereum;
} }
// Logic ported from /ethkey/src/brain.rs function route ({ action, payload }) {
function phraseToWallet (phrase) { if (action in actions) {
let secret = keccak256.array(phrase); return actions[action](payload);
for (let i = 0; i < 16384; i++) {
secret = keccak256.array(secret);
} }
while (true) { return null;
secret = keccak256.array(secret); }
const secretBuf = Buffer.from(secret); const actions = {
phraseToWallet (phrase) {
let secret = keccak256.array(phrase);
if (secp256k1.privateKeyVerify(secretBuf)) { for (let i = 0; i < 16384; i++) {
// No compression, slice out last 64 bytes secret = keccak256.array(secret);
const publicBuf = secp256k1.publicKeyCreate(secretBuf, false).slice(-64); }
const address = keccak256.array(publicBuf).slice(12);
if (address[0] !== 0) { while (true) {
continue; secret = keccak256.array(secret);
const secretBuf = Buffer.from(secret);
if (secp256k1.privateKeyVerify(secretBuf)) {
// No compression, slice out last 64 bytes
const publicBuf = secp256k1.publicKeyCreate(secretBuf, false).slice(-64);
const address = keccak256.array(publicBuf).slice(12);
if (address[0] !== 0) {
continue;
}
const wallet = {
secret: bytesToHex(secretBuf),
public: bytesToHex(publicBuf),
address: bytesToHex(address)
};
return wallet;
} }
}
},
const wallet = { verifySecret (secret) {
secret: bytesToHex(secretBuf), const key = Buffer.from(secret.slice(2), 'hex');
public: bytesToHex(publicBuf),
address: bytesToHex(address)
};
return wallet; return secp256k1.privateKeyVerify(key);
},
createKeyObject ({ key, password }) {
key = Buffer.from(key);
password = Buffer.from(password);
const iv = keythereum.crypto.randomBytes(16);
const salt = keythereum.crypto.randomBytes(32);
const keyObject = keythereum.dump(password, key, salt, iv);
return JSON.stringify(keyObject);
},
decryptPrivateKey ({ keyObject, password }) {
password = Buffer.from(password);
try {
const key = keythereum.recover(password, keyObject);
// Convert to array to safely send from the worker
return Array.from(key);
} catch (e) {
return null;
} }
} }
} };
self.onmessage = function ({ data }) { self.onmessage = function ({ data }) {
const wallet = phraseToWallet(data); const result = route(data);
postMessage(wallet); postMessage(result);
close();
}; };
// Emulate a web worker in Node.js // Emulate a web worker in Node.js
@ -73,9 +119,9 @@ class KeyWorker {
postMessage (data) { postMessage (data) {
// Force async // Force async
setTimeout(() => { setTimeout(() => {
const wallet = phraseToWallet(data); const result = route(data);
this.onmessage({ data: wallet }); this.onmessage({ data: result });
}, 0); }, 0);
} }

View File

@ -0,0 +1,61 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
// Allow a web worker in the browser, with a fallback for Node.js
const hasWebWorkers = typeof Worker !== 'undefined';
const KeyWorker = hasWebWorkers ? require('worker-loader!./worker')
: require('./worker').KeyWorker;
class WorkerContainer {
busy = false;
_worker = new KeyWorker();
action (action, payload) {
if (this.busy) {
throw new Error('Cannot issue an action on a busy worker!');
}
this.busy = true;
return new Promise((resolve, reject) => {
this._worker.postMessage({ action, payload });
this._worker.onmessage = ({ data }) => {
this.busy = false;
resolve(data);
};
});
}
}
class WorkerPool {
pool = [];
getWorker () {
let container = this.pool.find((container) => !container.busy);
if (container) {
return container;
}
container = new WorkerContainer();
this.pool.push(container);
return container;
}
}
export default new WorkerPool();

View File

@ -19,7 +19,7 @@ import accounts from './accounts';
import transactions from './transactions'; import transactions from './transactions';
import { Middleware } from '../transport'; import { Middleware } from '../transport';
import { inNumber16 } from '../format/input'; import { inNumber16 } from '../format/input';
import { phraseToWallet, phraseToAddress } from './ethkey'; import { phraseToWallet, phraseToAddress, verifySecret } from './ethkey';
import { randomPhrase } from '@parity/wordlist'; import { randomPhrase } from '@parity/wordlist';
export default class LocalAccountsMiddleware extends Middleware { export default class LocalAccountsMiddleware extends Middleware {
@ -57,6 +57,22 @@ export default class LocalAccountsMiddleware extends Middleware {
}); });
}); });
register('parity_changePassword', ([address, oldPassword, newPassword]) => {
const account = accounts.get(address);
return account
.decryptPrivateKey(oldPassword)
.then((privateKey) => {
if (!privateKey) {
return false;
}
account.changePassword(privateKey, newPassword);
return true;
});
});
register('parity_checkRequest', ([id]) => { register('parity_checkRequest', ([id]) => {
return transactions.hash(id) || Promise.resolve(null); return transactions.hash(id) || Promise.resolve(null);
}); });
@ -84,6 +100,17 @@ export default class LocalAccountsMiddleware extends Middleware {
}); });
}); });
register('parity_newAccountFromSecret', ([secret, password]) => {
return verifySecret(secret)
.then((isValid) => {
if (!isValid) {
throw new Error('Invalid secret key');
}
return accounts.create(secret, password);
});
});
register('parity_setAccountMeta', ([address, meta]) => { register('parity_setAccountMeta', ([address, meta]) => {
accounts.get(address).meta = meta; accounts.get(address).meta = meta;
@ -127,6 +154,12 @@ export default class LocalAccountsMiddleware extends Middleware {
return accounts.remove(address, password); return accounts.remove(address, password);
}); });
register('parity_testPassword', ([address, password]) => {
const account = accounts.get(address);
return account.isValidPassword(password);
});
register('signer_confirmRequest', ([id, modify, password]) => { register('signer_confirmRequest', ([id, modify, password]) => {
const { const {
gasPrice, gasPrice,
@ -137,30 +170,33 @@ export default class LocalAccountsMiddleware extends Middleware {
data data
} = Object.assign(transactions.get(id), modify); } = Object.assign(transactions.get(id), modify);
return this const account = accounts.get(from);
.rpcRequest('parity_nextNonce', [from])
.then((nonce) => {
const tx = new EthereumTx({
nonce,
to,
data,
gasLimit: inNumber16(gasLimit),
gasPrice: inNumber16(gasPrice),
value: inNumber16(value)
});
const account = accounts.get(from);
tx.sign(account.decryptPrivateKey(password)); return Promise.all([
this.rpcRequest('parity_nextNonce', [from]),
const serializedTx = `0x${tx.serialize().toString('hex')}`; account.decryptPrivateKey(password)
])
return this.rpcRequest('eth_sendRawTransaction', [serializedTx]); .then(([nonce, privateKey]) => {
}) const tx = new EthereumTx({
.then((hash) => { nonce,
transactions.confirm(id, hash); to,
data,
return {}; gasLimit: inNumber16(gasLimit),
gasPrice: inNumber16(gasPrice),
value: inNumber16(value)
}); });
tx.sign(privateKey);
const serializedTx = `0x${tx.serialize().toString('hex')}`;
return this.rpcRequest('eth_sendRawTransaction', [serializedTx]);
})
.then((hash) => {
transactions.confirm(id, hash);
return {};
});
}); });
register('signer_rejectRequest', ([id]) => { register('signer_rejectRequest', ([id]) => {

View File

@ -80,12 +80,16 @@ export default class JsonRpcBase extends EventEmitter {
const res = middleware.handle(method, params); const res = middleware.handle(method, params);
if (res != null) { if (res != null) {
const result = this._wrapSuccessResult(res); // If `res` isn't a promise, we need to wrap it
const json = this.encode(method, params); return Promise.resolve(res)
.then((res) => {
const result = this._wrapSuccessResult(res);
const json = this.encode(method, params);
Logging.send(method, params, { json, result }); Logging.send(method, params, { json, result });
return res; return res;
});
} }
} }

View File

@ -17,7 +17,7 @@
import { range } from 'lodash'; import { range } from 'lodash';
export function bytesToHex (bytes) { export function bytesToHex (bytes) {
return '0x' + bytes.map((b) => ('0' + b.toString(16)).slice(-2)).join(''); return '0x' + Buffer.from(bytes).toString('hex');
} }
export function cleanupValue (value, type) { export function cleanupValue (value, type) {

View File

@ -23,6 +23,7 @@ import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import { Form, Input, IdentityIcon } from '~/ui'; import { Form, Input, IdentityIcon } from '~/ui';
import PasswordStrength from '~/ui/Form/PasswordStrength'; import PasswordStrength from '~/ui/Form/PasswordStrength';
import { RefreshIcon } from '~/ui/Icons'; import { RefreshIcon } from '~/ui/Icons';
import Loading from '~/ui/Loading';
import ChangeVault from '../ChangeVault'; import ChangeVault from '../ChangeVault';
import styles from '../createAccount.css'; import styles from '../createAccount.css';
@ -170,7 +171,9 @@ export default class CreateAccount extends Component {
const { accounts } = this.state; const { accounts } = this.state;
if (!accounts) { if (!accounts) {
return null; return (
<Loading className={ styles.selector } size={ 1 } />
);
} }
const identities = Object const identities = Object
@ -205,6 +208,14 @@ export default class CreateAccount extends Component {
createIdentities = () => { createIdentities = () => {
const { createStore } = this.props; const { createStore } = this.props;
this.setState({
accounts: null,
selectedAddress: ''
});
createStore.setAddress('');
createStore.setPhrase('');
return createStore return createStore
.createIdentities() .createIdentities()
.then((accounts) => { .then((accounts) => {

View File

@ -58,12 +58,12 @@ describe('modals/CreateAccount/NewAccount', () => {
return instance.componentWillMount(); return instance.componentWillMount();
}); });
it('creates initial accounts', () => { it('resets the accounts', () => {
expect(Object.keys(instance.state.accounts).length).to.equal(7); expect(instance.state.accounts).to.be.null;
}); });
it('sets the initial selected value', () => { it('resets the initial selected value', () => {
expect(instance.state.selectedAddress).to.equal(Object.keys(instance.state.accounts)[0]); expect(instance.state.selectedAddress).to.equal('');
}); });
}); });
}); });

View File

@ -69,7 +69,7 @@ export default class Store {
return !(this.nameError || this.walletFileError); return !(this.nameError || this.walletFileError);
case 'fromNew': case 'fromNew':
return !(this.nameError || this.passwordRepeatError); return !(this.nameError || this.passwordRepeatError) && this.hasAddress;
case 'fromPhrase': case 'fromPhrase':
return !(this.nameError || this.passwordRepeatError); return !(this.nameError || this.passwordRepeatError);
@ -85,6 +85,10 @@ export default class Store {
} }
} }
@computed get hasAddress () {
return !!(this.address);
}
@computed get passwordRepeatError () { @computed get passwordRepeatError () {
return this.password === this.passwordRepeat return this.password === this.passwordRepeat
? null ? null

View File

@ -329,6 +329,7 @@ describe('modals/CreateAccount/Store', () => {
describe('createType === fromNew', () => { describe('createType === fromNew', () => {
beforeEach(() => { beforeEach(() => {
store.setCreateType('fromNew'); store.setCreateType('fromNew');
store.setAddress('0x0000000000000000000000000000000000000000');
}); });
it('returns true on no errors', () => { it('returns true on no errors', () => {
@ -337,11 +338,13 @@ describe('modals/CreateAccount/Store', () => {
it('returns false on nameError', () => { it('returns false on nameError', () => {
store.setName(''); store.setName('');
expect(store.canCreate).to.be.false; expect(store.canCreate).to.be.false;
}); });
it('returns false on passwordRepeatError', () => { it('returns false on passwordRepeatError', () => {
store.setPassword('testing'); store.setPassword('testing');
expect(store.canCreate).to.be.false; expect(store.canCreate).to.be.false;
}); });
}); });