Fix generateXAddress() and generateXAddress() with no entropy (#1211)

Fix #1209

Calling: Uint8Array.from(undefined)
Throws:
  TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
    at Function.from (<anonymous>)

* generateSeed: Pass only entropy and algorithm

* Update typescript and ripple-keypairs

* Improve unit tests

* Rename [Original Address] to [Classic Address] in test output
This commit is contained in:
Elliot Lee
2020-02-18 11:14:09 -08:00
committed by GitHub
parent 9caf077b58
commit 804094b1ce
6 changed files with 359 additions and 19 deletions

View File

@@ -29,7 +29,7 @@
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"ripple-address-codec": "^4.0.0", "ripple-address-codec": "^4.0.0",
"ripple-binary-codec": "^0.2.5", "ripple-binary-codec": "^0.2.5",
"ripple-keypairs": "^1.0.0-beta.6", "ripple-keypairs": "^1.0.0",
"ripple-lib-transactionparser": "0.8.2", "ripple-lib-transactionparser": "0.8.2",
"ws": "^7.2.0" "ws": "^7.2.0"
}, },
@@ -49,7 +49,7 @@
"nyc": "^15.0.0", "nyc": "^15.0.0",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"ts-node": "^8.4.1", "ts-node": "^8.4.1",
"typescript": "^3.6.4", "typescript": "^3.7.5",
"webpack": "^4.41.2", "webpack": "^4.41.2",
"webpack-bundle-analyzer": "^3.6.0", "webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.9" "webpack-cli": "^3.3.9"

View File

@@ -28,10 +28,13 @@ export interface GenerateAddressOptions {
function generateAddressAPI(options: GenerateAddressOptions): GeneratedAddress { function generateAddressAPI(options: GenerateAddressOptions): GeneratedAddress {
validate.generateAddress({options}) validate.generateAddress({options})
try { try {
const secret = keypairs.generateSeed({ const generateSeedOptions: { entropy?: Uint8Array; algorithm?: "ecdsa-secp256k1" | "ed25519"; } = {
entropy: Uint8Array.from(options.entropy),
algorithm: options.algorithm algorithm: options.algorithm
}) }
if (options.entropy) {
generateSeedOptions.entropy = Uint8Array.from(options.entropy)
}
const secret = keypairs.generateSeed(generateSeedOptions)
const keypair = keypairs.deriveKeypair(secret) const keypair = keypairs.deriveKeypair(secret)
const classicAddress = keypairs.deriveAddress(keypair.publicKey) const classicAddress = keypairs.deriveAddress(keypair.publicKey)
const returnValue: any = { const returnValue: any = {

View File

@@ -1,6 +1,7 @@
import assert from 'assert-diff' import assert from 'assert-diff'
import responses from '../../fixtures/responses' import responses from '../../fixtures/responses'
import {TestSuite} from '../../utils' import {TestSuite} from '../../utils'
import { GenerateAddressOptions } from '../../../src/offline/generate-address'
const {generateAddress: RESPONSE_FIXTURES} = responses const {generateAddress: RESPONSE_FIXTURES} = responses
/** /**
@@ -9,22 +10,192 @@ const {generateAddress: RESPONSE_FIXTURES} = responses
* - Check out "test/api/index.ts" for more information about the test runner. * - Check out "test/api/index.ts" for more information about the test runner.
*/ */
export default <TestSuite>{ export default <TestSuite>{
'generateAddress': async (api, address) => { 'generateAddress': async (api) => {
function random(): number[] { // GIVEN entropy of all zeros
function random() {
return new Array(16).fill(0) return new Array(16).fill(0)
} }
assert.deepEqual( assert.deepEqual(
// WHEN generating an address
api.generateAddress({entropy: random()}), api.generateAddress({entropy: random()}),
// THEN we get the expected return value
RESPONSE_FIXTURES RESPONSE_FIXTURES
) )
}, },
'generateAddress invalid': async (api, address) => { 'generateAddress invalid entropy': async (api) => {
assert.throws(() => { assert.throws(() => {
// GIVEN entropy of 1 byte
function random() { function random() {
return new Array(1).fill(0) return new Array(1).fill(0)
} }
// WHEN generating an address
api.generateAddress({entropy: random()}) api.generateAddress({entropy: random()})
// THEN an UnexpectedError is thrown
// because 16 bytes of entropy are required
}, api.errors.UnexpectedError) }, api.errors.UnexpectedError)
},
'generateAddress with no options object': async (api) => {
// GIVEN no options
// WHEN generating an address
const account = api.generateAddress()
// THEN we get an object with an address starting with 'r' and a secret starting with 's'
assert(account.address.startsWith('r'), 'Address must start with `r`')
assert(account.secret.startsWith('s'), 'Secret must start with `s`')
},
'generateAddress with empty options object': async (api) => {
// GIVEN an empty options object
const options = {}
// WHEN generating an address
const account = api.generateAddress(options)
// THEN we get an object with an address starting with 'r' and a secret starting with 's'
assert(account.address.startsWith('r'), 'Address must start with `r`')
assert(account.secret.startsWith('s'), 'Secret must start with `s`')
},
'generateAddress with algorithm `ecdsa-secp256k1`': async (api) => {
// GIVEN we want to use 'ecdsa-secp256k1'
const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1'}
// WHEN generating an address
const account = api.generateAddress(options)
// THEN we get an object with an address starting with 'r' and a secret starting with 's' (not 'sEd')
assert(account.address.startsWith('r'), 'Address must start with `r`')
assert.deepEqual(account.secret.slice(0, 1), 's', `Secret ${account.secret} must start with 's'`)
assert.notStrictEqual(account.secret.slice(0, 3), 'sEd', `secp256k1 secret ${account.secret} must not start with 'sEd'`)
},
'generateAddress with algorithm `ed25519`': async (api) => {
// GIVEN we want to use 'ed25519'
const options: GenerateAddressOptions = {algorithm: 'ed25519'}
// WHEN generating an address
const account = api.generateAddress(options)
// THEN we get an object with an address starting with 'r' and a secret starting with 'sEd'
assert(account.address.startsWith('r'), 'Address must start with `r`')
assert.deepEqual(account.secret.slice(0, 3), 'sEd', `Ed25519 secret ${account.secret} must start with 'sEd'`)
},
'generateAddress with algorithm `ecdsa-secp256k1` and given entropy': async (api) => {
// GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0)}
// WHEN generating an address
const account = api.generateAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, responses.generateAddress)
},
'generateAddress with algorithm `ed25519` and given entropy': async (api) => {
// GIVEN we want to use 'ed25519' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0)}
// WHEN generating an address
const account = api.generateAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, {
// generateAddress return value always includes xAddress to encourage X-address adoption
xAddress: 'X7xq1YJ4xmLSGGLhuakFQB9CebWYthQkgsvFC4LGFH871HB',
classicAddress: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7",
address: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7",
secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE'
})
},
'generateAddress with algorithm `ecdsa-secp256k1` and given entropy; include classic address': async (api) => {
// GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0), includeClassicAddress: true}
// WHEN generating an address
const account = api.generateAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, responses.generateAddress)
},
'generateAddress with algorithm `ed25519` and given entropy; include classic address': async (api) => {
// GIVEN we want to use 'ed25519' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0), includeClassicAddress: true}
// WHEN generating an address
const account = api.generateAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, {
// generateAddress return value always includes xAddress to encourage X-address adoption
xAddress: 'X7xq1YJ4xmLSGGLhuakFQB9CebWYthQkgsvFC4LGFH871HB',
secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE',
classicAddress: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7",
address: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7"
})
},
'generateAddress with algorithm `ecdsa-secp256k1` and given entropy; include classic address; for test network use': async (api) => {
// GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0), includeClassicAddress: true, test: true}
// WHEN generating an address
const account = api.generateAddress(options)
// THEN we get the expected return value
const response = Object.assign({}, responses.generateAddress, {
// generateAddress return value always includes xAddress to encourage X-address adoption
xAddress: 'TVG3TcCD58BD6MZqsNuTihdrhZwR8SzvYS8U87zvHsAcNw4'
})
assert.deepEqual(account, response)
},
'generateAddress with algorithm `ed25519` and given entropy; include classic address; for test network use': async (api) => {
// GIVEN we want to use 'ed25519' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0), includeClassicAddress: true, test: true}
// WHEN generating an address
const account = api.generateAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, {
// generateAddress return value always includes xAddress to encourage X-address adoption
xAddress: 'T7t4HeTMF5tT68agwuVbJwu23ssMPeh8dDtGysZoQiij1oo',
secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE',
classicAddress: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7",
address: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7"
})
},
'generateAddress for test network use': async (api) => {
// GIVEN we want an address for test network use
const options: GenerateAddressOptions = {test: true}
// WHEN generating an address
const account = api.generateAddress(options)
// THEN we get an object with xAddress starting with 'T' and a secret starting with 's'
// generateAddress return value always includes xAddress to encourage X-address adoption
assert.deepEqual(account.xAddress.slice(0, 1), 'T', 'Test addresses start with T')
assert.deepEqual(account.secret.slice(0, 1), 's', `Secret ${account.secret} must start with 's'`)
} }
} }

View File

@@ -1,6 +1,7 @@
import assert from 'assert-diff' import assert from 'assert-diff'
import responses from '../../fixtures/responses' import responses from '../../fixtures/responses'
import {TestSuite} from '../../utils' import {TestSuite} from '../../utils'
import { GenerateAddressOptions } from '../../../src/offline/generate-address'
/** /**
* Every test suite exports their tests in the default object. * Every test suite exports their tests in the default object.
@@ -8,22 +9,175 @@ import {TestSuite} from '../../utils'
* - Check out "test/api/index.ts" for more information about the test runner. * - Check out "test/api/index.ts" for more information about the test runner.
*/ */
export default <TestSuite>{ export default <TestSuite>{
'generateXAddress': async (api, address) => { 'generateXAddress': async (api) => {
// GIVEN entropy of all zeros
function random() { function random() {
return new Array(16).fill(0) return new Array(16).fill(0)
} }
assert.deepEqual( assert.deepEqual(
// WHEN generating an X-address
api.generateXAddress({entropy: random()}), api.generateXAddress({entropy: random()}),
// THEN we get the expected return value
responses.generateXAddress responses.generateXAddress
) )
}, },
'generateXAddress invalid': async (api, address) => { 'generateXAddress invalid entropy': async (api) => {
assert.throws(() => { assert.throws(() => {
// GIVEN entropy of 1 byte
function random() { function random() {
return new Array(1).fill(0) return new Array(1).fill(0)
} }
// WHEN generating an X-address
api.generateXAddress({entropy: random()}) api.generateXAddress({entropy: random()})
// THEN an UnexpectedError is thrown
// because 16 bytes of entropy are required
}, api.errors.UnexpectedError) }, api.errors.UnexpectedError)
},
'generateXAddress with no options object': async (api) => {
// GIVEN no options
// WHEN generating an X-address
const account = api.generateXAddress()
// THEN we get an object with an xAddress starting with 'X' and a secret starting with 's'
assert(account.xAddress.startsWith('X'), 'By default X-addresses start with X')
assert(account.secret.startsWith('s'), 'Secrets start with s')
},
'generateXAddress with empty options object': async (api) => {
// GIVEN an empty options object
const options = {}
// WHEN generating an X-address
const account = api.generateXAddress(options)
// THEN we get an object with an xAddress starting with 'X' and a secret starting with 's'
assert(account.xAddress.startsWith('X'), 'By default X-addresses start with X')
assert(account.secret.startsWith('s'), 'Secrets start with s')
},
'generateXAddress with algorithm `ecdsa-secp256k1`': async (api) => {
// GIVEN we want to use 'ecdsa-secp256k1'
const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1'}
// WHEN generating an X-address
const account = api.generateXAddress(options)
// THEN we get an object with an xAddress starting with 'X' and a secret starting with 's'
assert(account.xAddress.startsWith('X'), 'By default X-addresses start with X')
assert.deepEqual(account.secret.slice(0, 1), 's', `Secret ${account.secret} must start with 's'`)
assert.notStrictEqual(account.secret.slice(0, 3), 'sEd', `secp256k1 secret ${account.secret} must not start with 'sEd'`)
},
'generateXAddress with algorithm `ed25519`': async (api) => {
// GIVEN we want to use 'ed25519'
const options: GenerateAddressOptions = {algorithm: 'ed25519'}
// WHEN generating an X-address
const account = api.generateXAddress(options)
// THEN we get an object with an xAddress starting with 'X' and a secret starting with 'sEd'
assert(account.xAddress.startsWith('X'), 'By default X-addresses start with X')
assert.deepEqual(account.secret.slice(0, 3), 'sEd', `Ed25519 secret ${account.secret} must start with 'sEd'`)
},
'generateXAddress with algorithm `ecdsa-secp256k1` and given entropy': async (api) => {
// GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0)}
// WHEN generating an X-address
const account = api.generateXAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, responses.generateXAddress)
},
'generateXAddress with algorithm `ed25519` and given entropy': async (api) => {
// GIVEN we want to use 'ed25519' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0)}
// WHEN generating an X-address
const account = api.generateXAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, {
xAddress: 'X7xq1YJ4xmLSGGLhuakFQB9CebWYthQkgsvFC4LGFH871HB',
secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE'
})
},
'generateXAddress with algorithm `ecdsa-secp256k1` and given entropy; include classic address': async (api) => {
// GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0), includeClassicAddress: true}
// WHEN generating an X-address
const account = api.generateXAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, responses.generateAddress)
},
'generateXAddress with algorithm `ed25519` and given entropy; include classic address': async (api) => {
// GIVEN we want to use 'ed25519' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0), includeClassicAddress: true}
// WHEN generating an X-address
const account = api.generateXAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, {
xAddress: 'X7xq1YJ4xmLSGGLhuakFQB9CebWYthQkgsvFC4LGFH871HB',
secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE',
classicAddress: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7",
address: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7"
})
},
'generateXAddress with algorithm `ecdsa-secp256k1` and given entropy; include classic address; for test network use': async (api) => {
// GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ecdsa-secp256k1', entropy: new Array(16).fill(0), includeClassicAddress: true, test: true}
// WHEN generating an X-address
const account = api.generateXAddress(options)
// THEN we get the expected return value
const response = Object.assign({}, responses.generateAddress, {
xAddress: 'TVG3TcCD58BD6MZqsNuTihdrhZwR8SzvYS8U87zvHsAcNw4'
})
assert.deepEqual(account, response)
},
'generateXAddress with algorithm `ed25519` and given entropy; include classic address; for test network use': async (api) => {
// GIVEN we want to use 'ed25519' with entropy of zero
const options: GenerateAddressOptions = {algorithm: 'ed25519', entropy: new Array(16).fill(0), includeClassicAddress: true, test: true}
// WHEN generating an X-address
const account = api.generateXAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, {
xAddress: 'T7t4HeTMF5tT68agwuVbJwu23ssMPeh8dDtGysZoQiij1oo',
secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE',
classicAddress: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7",
address: "r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7"
})
},
'generateXAddress for test network use': async (api) => {
// GIVEN we want an X-address for test network use
const options: GenerateAddressOptions = {test: true}
// WHEN generating an X-address
const account = api.generateXAddress(options)
// THEN we get an object with xAddress starting with 'T' and a secret starting with 's'
assert.deepEqual(account.xAddress.slice(0, 1), 'T', 'Test X-addresses start with T')
assert.deepEqual(account.secret.slice(0, 1), 's', `Secret ${account.secret} must start with 's'`)
} }
} }

View File

@@ -32,21 +32,33 @@ describe('RippleAPI [Test Runner]', function() {
// Run all the tests: // Run all the tests:
for (const {name: methodName, tests, config} of allTestSuites) { for (const {name: methodName, tests, config} of allTestSuites) {
describe(`api.${methodName}`, () => { describe(`api.${methodName}`, () => {
// Run each test with the original-style address. // Run each test that does not use an address.
describe(`[Original Address]`, () => { for (const [testName, fn] of tests) {
for (const [testName, fn] of tests) { if (fn.length === 1) {
it(testName, function() { it(testName, function() {
return fn(this.api, addresses.ACCOUNT) return fn(this.api, addresses.ACCOUNT)
}) })
} }
}
// Run each test with a classic address.
describe(`[Classic Address]`, () => {
for (const [testName, fn] of tests) {
if (fn.length === 2) {
it(testName, function() {
return fn(this.api, addresses.ACCOUNT)
})
}
}
}) })
// Run each test with the newer, x-address style. // Run each test with an X-address.
if (!config.skipXAddress) { if (!config.skipXAddress) {
describe(`[X-address]`, () => { describe(`[X-address]`, () => {
for (const [testName, fn] of tests) { for (const [testName, fn] of tests) {
it(testName, function() { if (fn.length === 2) {
return fn(this.api, addresses.ACCOUNT_X) it(testName, function() {
}) return fn(this.api, addresses.ACCOUNT_X)
})
}
} }
}) })
} }

View File

@@ -4121,7 +4121,7 @@ ripple-binary-codec@^0.2.5:
lodash "^4.17.15" lodash "^4.17.15"
ripple-address-codec "^4.0.0" ripple-address-codec "^4.0.0"
ripple-keypairs@^1.0.0-beta.6: ripple-keypairs@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/ripple-keypairs/-/ripple-keypairs-1.0.0.tgz#8f1c604f89daeac5e61b7eebbbca2da99da2bacf" resolved "https://registry.yarnpkg.com/ripple-keypairs/-/ripple-keypairs-1.0.0.tgz#8f1c604f89daeac5e61b7eebbbca2da99da2bacf"
integrity sha512-MQ3d6fU3D+Cqu5ma4dfkfa+KakN2sKpVVVN0FeJyAYPVIGXu8Rcvd1g028TdwYAZcSYk0tGn5UhHxd0gUG3T8g== integrity sha512-MQ3d6fU3D+Cqu5ma4dfkfa+KakN2sKpVVVN0FeJyAYPVIGXu8Rcvd1g028TdwYAZcSYk0tGn5UhHxd0gUG3T8g==
@@ -4801,7 +4801,7 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^3.6.4: typescript@^3.7.5:
version "3.7.5" version "3.7.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==