Merge pull request #5390 from paritytech/mh-publicnode-perf
Public node: perf and fixes
This commit is contained in:
commit
237bac4500
@ -176,7 +176,7 @@
|
||||
"geopattern": "1.2.3",
|
||||
"isomorphic-fetch": "2.2.1",
|
||||
"js-sha3": "0.5.5",
|
||||
"keythereum": "0.4.3",
|
||||
"keythereum": "0.4.6",
|
||||
"lodash": "4.17.2",
|
||||
"loglevel": "1.4.1",
|
||||
"marked": "0.3.6",
|
||||
|
@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { keythereum } from '../ethkey';
|
||||
import { createKeyObject, decryptPrivateKey } from '../ethkey';
|
||||
|
||||
export default class Account {
|
||||
constructor (persist, data) {
|
||||
@ -31,12 +31,14 @@ export default class Account {
|
||||
}
|
||||
|
||||
isValidPassword (password) {
|
||||
try {
|
||||
keythereum.recover(Buffer.from(password), this._keyObject);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return decryptPrivateKey(this._keyObject, password)
|
||||
.then((privateKey) => {
|
||||
if (!privateKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
get address () {
|
||||
@ -68,21 +70,23 @@ export default class Account {
|
||||
}
|
||||
|
||||
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) {
|
||||
const iv = keythereum.crypto.randomBytes(16);
|
||||
const salt = keythereum.crypto.randomBytes(32);
|
||||
return createKeyObject(key, password).then((keyObject) => {
|
||||
const account = new Account(persist, { keyObject });
|
||||
|
||||
// Keythereum will fail if `password` is an empty string
|
||||
password = Buffer.from(password);
|
||||
|
||||
const keyObject = keythereum.dump(password, key, salt, iv);
|
||||
|
||||
const account = new Account(persist, { keyObject });
|
||||
|
||||
return account;
|
||||
return account;
|
||||
});
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
|
@ -38,14 +38,23 @@ export default class Accounts {
|
||||
|
||||
create (secret, password) {
|
||||
const privateKey = Buffer.from(secret.slice(2), 'hex');
|
||||
const account = Account.fromPrivateKey(this.persist, privateKey, password);
|
||||
|
||||
this._store.push(account);
|
||||
this.lastAddress = account.address;
|
||||
return Account
|
||||
.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) {
|
||||
@ -73,28 +82,41 @@ export default class Accounts {
|
||||
remove (address, password) {
|
||||
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);
|
||||
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const account = this._store[index];
|
||||
|
||||
if (!account.isValidPassword(password)) {
|
||||
console.log('invalid password');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (address === this.lastAddress) {
|
||||
this.lastAddress = NULL_ADDRESS;
|
||||
return;
|
||||
}
|
||||
|
||||
this._store.splice(index, 1);
|
||||
|
||||
this.persist();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
mapArray (mapper) {
|
||||
|
@ -14,31 +14,35 @@
|
||||
// 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;
|
||||
import workerPool from './workerPool';
|
||||
|
||||
// Local accounts should never be used outside of the browser
|
||||
export let keythereum = null;
|
||||
export function createKeyObject (key, password) {
|
||||
return workerPool.getWorker().action('createKeyObject', { key, password })
|
||||
.then((obj) => JSON.parse(obj));
|
||||
}
|
||||
|
||||
if (hasWebWorkers) {
|
||||
require('keythereum/dist/keythereum');
|
||||
export function decryptPrivateKey (keyObject, password) {
|
||||
return workerPool
|
||||
.getWorker()
|
||||
.action('decryptPrivateKey', { keyObject, password })
|
||||
.then((privateKey) => {
|
||||
if (privateKey) {
|
||||
return Buffer.from(privateKey);
|
||||
}
|
||||
|
||||
keythereum = window.keythereum;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
export function phraseToAddress (phrase) {
|
||||
return phraseToWallet(phrase).then((wallet) => wallet.address);
|
||||
return phraseToWallet(phrase)
|
||||
.then((wallet) => wallet.address);
|
||||
}
|
||||
|
||||
export function phraseToWallet (phrase) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new KeyWorker();
|
||||
|
||||
worker.postMessage(phrase);
|
||||
worker.onmessage = ({ data }) => {
|
||||
resolve(data);
|
||||
};
|
||||
});
|
||||
return workerPool.getWorker().action('phraseToWallet', phrase);
|
||||
}
|
||||
|
||||
export function verifySecret (secret) {
|
||||
return workerPool.getWorker().action('verifySecret', secret);
|
||||
}
|
||||
|
@ -14,58 +14,104 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// 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 { keccak_256 as keccak256 } from 'js-sha3';
|
||||
import { bytesToHex } from '~/api/util/format';
|
||||
|
||||
const isWorker = typeof self !== 'undefined';
|
||||
|
||||
// Stay compatible between environments
|
||||
if (typeof self !== 'object') {
|
||||
if (!isWorker) {
|
||||
const scope = typeof global === 'undefined' ? window : global;
|
||||
|
||||
scope.self = scope;
|
||||
}
|
||||
|
||||
function bytesToHex (bytes) {
|
||||
return '0x' + Array.from(bytes).map(n => ('0' + n.toString(16)).slice(-2)).join('');
|
||||
// keythereum should never be used outside of the browser
|
||||
let keythereum = null;
|
||||
|
||||
if (isWorker) {
|
||||
require('keythereum/dist/keythereum');
|
||||
|
||||
keythereum = self.keythereum;
|
||||
}
|
||||
|
||||
// Logic ported from /ethkey/src/brain.rs
|
||||
function phraseToWallet (phrase) {
|
||||
let secret = keccak256.array(phrase);
|
||||
|
||||
for (let i = 0; i < 16384; i++) {
|
||||
secret = keccak256.array(secret);
|
||||
function route ({ action, payload }) {
|
||||
if (action in actions) {
|
||||
return actions[action](payload);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
secret = keccak256.array(secret);
|
||||
return null;
|
||||
}
|
||||
|
||||
const secretBuf = Buffer.from(secret);
|
||||
const actions = {
|
||||
phraseToWallet (phrase) {
|
||||
let secret = keccak256.array(phrase);
|
||||
|
||||
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);
|
||||
for (let i = 0; i < 16384; i++) {
|
||||
secret = keccak256.array(secret);
|
||||
}
|
||||
|
||||
if (address[0] !== 0) {
|
||||
continue;
|
||||
while (true) {
|
||||
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 = {
|
||||
secret: bytesToHex(secretBuf),
|
||||
public: bytesToHex(publicBuf),
|
||||
address: bytesToHex(address)
|
||||
};
|
||||
verifySecret (secret) {
|
||||
const key = Buffer.from(secret.slice(2), 'hex');
|
||||
|
||||
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 }) {
|
||||
const wallet = phraseToWallet(data);
|
||||
const result = route(data);
|
||||
|
||||
postMessage(wallet);
|
||||
close();
|
||||
postMessage(result);
|
||||
};
|
||||
|
||||
// Emulate a web worker in Node.js
|
||||
@ -73,9 +119,9 @@ class KeyWorker {
|
||||
postMessage (data) {
|
||||
// Force async
|
||||
setTimeout(() => {
|
||||
const wallet = phraseToWallet(data);
|
||||
const result = route(data);
|
||||
|
||||
this.onmessage({ data: wallet });
|
||||
this.onmessage({ data: result });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
61
js/src/api/local/ethkey/workerPool.js
Normal file
61
js/src/api/local/ethkey/workerPool.js
Normal 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();
|
@ -19,7 +19,7 @@ import accounts from './accounts';
|
||||
import transactions from './transactions';
|
||||
import { Middleware } from '../transport';
|
||||
import { inNumber16 } from '../format/input';
|
||||
import { phraseToWallet, phraseToAddress } from './ethkey';
|
||||
import { phraseToWallet, phraseToAddress, verifySecret } from './ethkey';
|
||||
import { randomPhrase } from '@parity/wordlist';
|
||||
|
||||
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]) => {
|
||||
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]) => {
|
||||
accounts.get(address).meta = meta;
|
||||
|
||||
@ -127,6 +154,12 @@ export default class LocalAccountsMiddleware extends Middleware {
|
||||
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]) => {
|
||||
const {
|
||||
gasPrice,
|
||||
@ -137,30 +170,33 @@ export default class LocalAccountsMiddleware extends Middleware {
|
||||
data
|
||||
} = Object.assign(transactions.get(id), modify);
|
||||
|
||||
return this
|
||||
.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);
|
||||
const account = accounts.get(from);
|
||||
|
||||
tx.sign(account.decryptPrivateKey(password));
|
||||
|
||||
const serializedTx = `0x${tx.serialize().toString('hex')}`;
|
||||
|
||||
return this.rpcRequest('eth_sendRawTransaction', [serializedTx]);
|
||||
})
|
||||
.then((hash) => {
|
||||
transactions.confirm(id, hash);
|
||||
|
||||
return {};
|
||||
return Promise.all([
|
||||
this.rpcRequest('parity_nextNonce', [from]),
|
||||
account.decryptPrivateKey(password)
|
||||
])
|
||||
.then(([nonce, privateKey]) => {
|
||||
const tx = new EthereumTx({
|
||||
nonce,
|
||||
to,
|
||||
data,
|
||||
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]) => {
|
||||
|
@ -80,12 +80,16 @@ export default class JsonRpcBase extends EventEmitter {
|
||||
const res = middleware.handle(method, params);
|
||||
|
||||
if (res != null) {
|
||||
const result = this._wrapSuccessResult(res);
|
||||
const json = this.encode(method, params);
|
||||
// If `res` isn't a promise, we need to wrap it
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { range } from 'lodash';
|
||||
|
||||
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) {
|
||||
|
@ -23,6 +23,7 @@ import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||
import { Form, Input, IdentityIcon } from '~/ui';
|
||||
import PasswordStrength from '~/ui/Form/PasswordStrength';
|
||||
import { RefreshIcon } from '~/ui/Icons';
|
||||
import Loading from '~/ui/Loading';
|
||||
|
||||
import ChangeVault from '../ChangeVault';
|
||||
import styles from '../createAccount.css';
|
||||
@ -170,7 +171,9 @@ export default class CreateAccount extends Component {
|
||||
const { accounts } = this.state;
|
||||
|
||||
if (!accounts) {
|
||||
return null;
|
||||
return (
|
||||
<Loading className={ styles.selector } size={ 1 } />
|
||||
);
|
||||
}
|
||||
|
||||
const identities = Object
|
||||
@ -205,6 +208,14 @@ export default class CreateAccount extends Component {
|
||||
createIdentities = () => {
|
||||
const { createStore } = this.props;
|
||||
|
||||
this.setState({
|
||||
accounts: null,
|
||||
selectedAddress: ''
|
||||
});
|
||||
|
||||
createStore.setAddress('');
|
||||
createStore.setPhrase('');
|
||||
|
||||
return createStore
|
||||
.createIdentities()
|
||||
.then((accounts) => {
|
||||
|
@ -58,12 +58,12 @@ describe('modals/CreateAccount/NewAccount', () => {
|
||||
return instance.componentWillMount();
|
||||
});
|
||||
|
||||
it('creates initial accounts', () => {
|
||||
expect(Object.keys(instance.state.accounts).length).to.equal(7);
|
||||
it('resets the accounts', () => {
|
||||
expect(instance.state.accounts).to.be.null;
|
||||
});
|
||||
|
||||
it('sets the initial selected value', () => {
|
||||
expect(instance.state.selectedAddress).to.equal(Object.keys(instance.state.accounts)[0]);
|
||||
it('resets the initial selected value', () => {
|
||||
expect(instance.state.selectedAddress).to.equal('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -69,7 +69,7 @@ export default class Store {
|
||||
return !(this.nameError || this.walletFileError);
|
||||
|
||||
case 'fromNew':
|
||||
return !(this.nameError || this.passwordRepeatError);
|
||||
return !(this.nameError || this.passwordRepeatError) && this.hasAddress;
|
||||
|
||||
case 'fromPhrase':
|
||||
return !(this.nameError || this.passwordRepeatError);
|
||||
@ -85,6 +85,10 @@ export default class Store {
|
||||
}
|
||||
}
|
||||
|
||||
@computed get hasAddress () {
|
||||
return !!(this.address);
|
||||
}
|
||||
|
||||
@computed get passwordRepeatError () {
|
||||
return this.password === this.passwordRepeat
|
||||
? null
|
||||
|
@ -329,6 +329,7 @@ describe('modals/CreateAccount/Store', () => {
|
||||
describe('createType === fromNew', () => {
|
||||
beforeEach(() => {
|
||||
store.setCreateType('fromNew');
|
||||
store.setAddress('0x0000000000000000000000000000000000000000');
|
||||
});
|
||||
|
||||
it('returns true on no errors', () => {
|
||||
@ -337,11 +338,13 @@ describe('modals/CreateAccount/Store', () => {
|
||||
|
||||
it('returns false on nameError', () => {
|
||||
store.setName('');
|
||||
|
||||
expect(store.canCreate).to.be.false;
|
||||
});
|
||||
|
||||
it('returns false on passwordRepeatError', () => {
|
||||
store.setPassword('testing');
|
||||
|
||||
expect(store.canCreate).to.be.false;
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user