diff --git a/packages/ripple-keypairs/README.md b/packages/ripple-keypairs/README.md index e8f044e0..9c7498d3 100644 --- a/packages/ripple-keypairs/README.md +++ b/packages/ripple-keypairs/README.md @@ -18,6 +18,22 @@ publicKey: 'ED5F5AC8B98974A3CA843326D9B88CEBD0560177B973EE0B149F782CFAA06DC66A' }') ``` +## Generate random validator keys +```js +> var generateValidatorKeys = require('ripple-keypairs').generateValidatorKeys; +> generateValidatorKeys(); +{ seed: 'ssC7Y9LMKhuzFMVueaj2fnTuGLftA', + publicKey: 'n9MU2RsULUayZnWeLssjbMzVRPeVUUMgiPYTwe8eMgpdGDWp5t8C' } +``` + +## Derive validator keys from a seed +```js +> var validatorKeysFromSeed = require('ripple-keypairs').validatorKeysFromSeed; +> validatorKeysFromSeed('ssC7Y9LMKhuzFMVueaj2fnTuGLftA'); +{ seed: 'ssC7Y9LMKhuzFMVueaj2fnTuGLftA', + publicKey: 'n9MU2RsULUayZnWeLssjbMzVRPeVUUMgiPYTwe8eMgpdGDWp5t8C' } +``` + ## Sign a transaction see [examples/sign-transaction.js](examples/sign-transaction.js) ```js diff --git a/packages/ripple-keypairs/src/index.js b/packages/ripple-keypairs/src/index.js index 23507f98..9e52d502 100644 --- a/packages/ripple-keypairs/src/index.js +++ b/packages/ripple-keypairs/src/index.js @@ -12,6 +12,7 @@ const rand = require('brorand'); // elliptic const secp256k1 = elliptic.ec('secp256k1'); const Ed25519 = elliptic.eddsa('ed25519'); +const {utils: {parseBytes}} = elliptic; const { bytesToHex, @@ -57,7 +58,7 @@ function findk256Key(bytes, discrim) { /** * @param {Object} [options] - * @param {Number} [options.accountIndex=0] - the account number to generate -* @param {Boolean} [options.root=false] - generate root key-pair, +* @param {Boolean} [options.validator=false] - generate root key-pair, * as used by validators. * @return {new bn.js} - * @@ -65,7 +66,7 @@ function findk256Key(bytes, discrim) { /* eslint-enable valid-jsdoc */ function derivek256Secret(seed, opts={}) { - const root = opts.root; + const root = opts.validator; const order = secp256k1.curve.n; // This private generator represents the `root` private key, and is what's @@ -100,7 +101,10 @@ function deriveEdKeyPairSeed(seed) { /* --------------------------------- KEYPAIR -------------------------------- */ -function KeyPair() {} +function KeyPair({seedBytes, pubBytes}) { + this.seedBytes = seedBytes; + this.pubKeyCanonicalBytes__ = pubBytes; +} /* @param {Array} message @@ -145,34 +149,48 @@ KeyPair.prototype.signHex = function(message) { * @private * @param {Object} - key */ -function Ed25519Pair(key) { +function Ed25519Pair() { KeyPair.apply(this, arguments); - this.key = key; this.type = KeyType.ed25519; } util.inherits(Ed25519Pair, KeyPair); /** -* @param {Seed} seed - A 128 bit seed +* @param {Array} seedBytes - A 128 bit seed * @return {Ed25519Pair} key pair */ -Ed25519Pair.fromSeed = function(seed) { - const seed256 = deriveEdKeyPairSeed(seed); - const derived = Ed25519.keyFromSecret(seed256); - return new Ed25519Pair(derived); +Ed25519Pair.fromSeed = function(seedBytes) { + return new Ed25519Pair({seedBytes}); }; +/** +* @param {Seed} publicKey - public key in canonical form (0xED + 32 bytes) +* @return {Ed25519Pair} key pair +*/ +Ed25519Pair.fromPublic = function (publicKey) { + return new Ed25519Pair({pubBytes: parseBytes(publicKey)}); +}; + +hasCachedProperty(Ed25519Pair, 'key', function() { + if (this.seedBytes) { + const seed256 = deriveEdKeyPairSeed(this.seedBytes); + return Ed25519.keyFromSecret(seed256); + } else { + return Ed25519.keyFromPublic(this.pubKeyCanonicalBytes().slice(1)); + } +}); + hasCachedProperty(Ed25519Pair, 'pubKeyCanonicalBytes', function() { - return [0xED].concat(this.key.pubBytes()); + return [0xED].concat(this.key().pubBytes()); }); Ed25519Pair.prototype.sign = function(message) { - return this.key.sign(message).toBytes(); + return this.key().sign(message).toBytes(); }; Ed25519Pair.prototype.verify = function(message, signature) { - return this.key.verify(message, signature); + return this.key().verify(message, signature); }; /* ---------------------------- SECP256K1 KEYPAIR --------------------------- */ @@ -181,20 +199,29 @@ Ed25519Pair.prototype.verify = function(message, signature) { * @class * @private */ -function K256Pair(key) { +function K256Pair({validator}) { KeyPair.apply(this, arguments); this.type = KeyType.secp256k1; - this.key = key; + this.validator = validator; } util.inherits(K256Pair, KeyPair); -K256Pair.fromSeed = function(seed) { - return new K256Pair(secp256k1.keyFromPrivate(derivek256Secret(seed))); +K256Pair.fromSeed = function(seedBytes, opts={}) { + return new K256Pair({seedBytes, validator: opts.validator}); }; +hasCachedProperty(K256Pair, 'key', function() { + if (this.seedBytes) { + const options = {validator: this.validator}; + return secp256k1.keyFromPrivate(derivek256Secret(this.seedBytes, options)); + } else { + return secp256k1.keyFromPublic(this.pubKeyCanonicalBytes()); + } +}); + hasCachedProperty(K256Pair, 'pubKeyCanonicalBytes', function() { - return this.key.getPublic(/*compact*/ true, /*enc*/ 'bytes'); + return this.key().getPublic().encodeCompressed(); }); /* @@ -206,7 +233,7 @@ K256Pair.prototype.sign = function(message) { K256Pair.prototype._createSignature = function(message) { // The key.sign message silently discards options - return secp256k1.sign(this.hashMessage(message), this.key, {canonical: true}); + return this.key().sign(this.hashMessage(message), {canonical: true}); }; /* @@ -223,16 +250,16 @@ K256Pair.prototype.hashMessage = function(message) { */ K256Pair.prototype.verify = function(message, signature) { try { - return this.key.verify(this.hashMessage(message), signature); + return this.key().verify(this.hashMessage(message), signature); } catch (e) { return false; } }; -function keyPairFromSeed(seedString) { +function keyPairFromSeed(seedString, options) { const decoded = codec.decodeSeed(seedString); - const pair = decoded.type === 'ed25519' ? Ed25519Pair : K256Pair; - return pair.fromSeed(decoded.bytes); + const Pair = decoded.type === 'ed25519' ? Ed25519Pair : K256Pair; + return Pair.fromSeed(decoded.bytes, options); } function deriveWallet(type, seedBytes) { @@ -256,7 +283,8 @@ function deriveWallet(type, seedBytes) { }; } -function generateWallet({type='secp256k1', randGen=rand}) { +function generateWallet(opts={}) { + const {type='secp256k1', randGen=rand} = opts; const seedBytes = randGen(16); return deriveWallet(type, seedBytes); } @@ -266,6 +294,25 @@ function walletFromSeed(seed) { return deriveWallet(type, bytes); } +function deriveValidator(seedBytes) { + const pair = K256Pair.fromSeed(seedBytes, {validator: true}); + return { + seed: codec.encodeK256Seed(seedBytes), + publicKey: codec.encodeNodePublic(pair.pubKeyCanonicalBytes()) + }; +} + +function generateValidatorKeys(opts={}) { + const {randGen=rand} = opts; + return deriveValidator(randGen(16)); +} + +function validatorKeysFromSeed(seed) { + const {type, bytes} = codec.decodeSeed(seed); + assert(type == KeyType.secp256k1); + return deriveValidator(bytes); +} + module.exports = { KeyPair, K256Pair, @@ -275,5 +322,7 @@ module.exports = { createAccountID, keyPairFromSeed, generateWallet, - walletFromSeed + generateValidatorKeys, + walletFromSeed, + validatorKeysFromSeed }; diff --git a/packages/ripple-keypairs/test/keypairs-test.js b/packages/ripple-keypairs/test/keypairs-test.js index f8e81a67..2fdcb213 100644 --- a/packages/ripple-keypairs/test/keypairs-test.js +++ b/packages/ripple-keypairs/test/keypairs-test.js @@ -12,7 +12,9 @@ const { Ed25519Pair, keyPairFromSeed, generateWallet, - walletFromSeed + walletFromSeed, + generateValidatorKeys, + validatorKeysFromSeed } = keypairs; const {SerializedObject} = require('ripple-lib'); @@ -45,6 +47,13 @@ describe('ED25519Pair', function() { pair = Ed25519Pair.fromSeed(seedFromPhrase('niq')); }); + it('can be constructed from a pulbic key to verify a txn', function() { + const sig = pair.sign(FIXTURES.message); + const key = Ed25519Pair.fromPublic(pair.pubKeyCanonicalBytes()); + assert(key.verify(FIXTURES.message, sig)); + assert(!key.verify(FIXTURES.message.concat(0), sig)); + }); + it('has a String member `type` equal to KeyPair.ed25519 constant', function() { assert.equal(pair.type, KeyType.ed25519); @@ -121,6 +130,32 @@ describe('generateWallet', function() { }); }); +describe('generateValidatorKeys', function() { + function randGen(len) { + return _.fill(Array(len), 0); + } + it('can generate secp256k1 validator keys', function() { + /* + rippled validation_create 00000000000000000000000000000000 + { + "result" : { + "status" : "success", + "validation_key" : "A A A A A A A A A A A A", + "validation_public_key" : "n9LPxYzbDpWBZ1bC3J3Fdkgqoa3FEhVKCnS8yKp7RFQFwuvd8Q2c", + "validation_seed" : "sp6JS7f14BuwFY8Mw6bTtLKWauoUs" + } + } + */ + const expected = { + seed: 'sp6JS7f14BuwFY8Mw6bTtLKWauoUs', + publicKey: 'n9LPxYzbDpWBZ1bC3J3Fdkgqoa3FEhVKCnS8yKp7RFQFwuvd8Q2c' + }; + const actual = generateValidatorKeys({randGen}); + assert.deepEqual(actual, expected); + assert.deepEqual(validatorKeysFromSeed(actual.seed), expected); + }); +}); + describe('K256Pair', function() { describe('generated tests', function() { /*eslint-disable max-len*/