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

View File

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

View File

@@ -1,6 +1,7 @@
import assert from 'assert-diff'
import responses from '../../fixtures/responses'
import {TestSuite} from '../../utils'
import { GenerateAddressOptions } from '../../../src/offline/generate-address'
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.
*/
export default <TestSuite>{
'generateAddress': async (api, address) => {
function random(): number[] {
'generateAddress': async (api) => {
// GIVEN entropy of all zeros
function random() {
return new Array(16).fill(0)
}
assert.deepEqual(
// WHEN generating an address
api.generateAddress({entropy: random()}),
// THEN we get the expected return value
RESPONSE_FIXTURES
)
},
'generateAddress invalid': async (api, address) => {
'generateAddress invalid entropy': async (api) => {
assert.throws(() => {
// GIVEN entropy of 1 byte
function random() {
return new Array(1).fill(0)
}
// WHEN generating an address
api.generateAddress({entropy: random()})
// THEN an UnexpectedError is thrown
// because 16 bytes of entropy are required
}, 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 responses from '../../fixtures/responses'
import {TestSuite} from '../../utils'
import { GenerateAddressOptions } from '../../../src/offline/generate-address'
/**
* 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.
*/
export default <TestSuite>{
'generateXAddress': async (api, address) => {
'generateXAddress': async (api) => {
// GIVEN entropy of all zeros
function random() {
return new Array(16).fill(0)
}
assert.deepEqual(
// WHEN generating an X-address
api.generateXAddress({entropy: random()}),
// THEN we get the expected return value
responses.generateXAddress
)
},
'generateXAddress invalid': async (api, address) => {
'generateXAddress invalid entropy': async (api) => {
assert.throws(() => {
// GIVEN entropy of 1 byte
function random() {
return new Array(1).fill(0)
}
// WHEN generating an X-address
api.generateXAddress({entropy: random()})
// THEN an UnexpectedError is thrown
// because 16 bytes of entropy are required
}, 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:
for (const {name: methodName, tests, config} of allTestSuites) {
describe(`api.${methodName}`, () => {
// Run each test with the original-style address.
describe(`[Original Address]`, () => {
for (const [testName, fn] of tests) {
// Run each test that does not use an address.
for (const [testName, fn] of tests) {
if (fn.length === 1) {
it(testName, function() {
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) {
describe(`[X-address]`, () => {
for (const [testName, fn] of tests) {
it(testName, function() {
return fn(this.api, addresses.ACCOUNT_X)
})
if (fn.length === 2) {
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"
ripple-address-codec "^4.0.0"
ripple-keypairs@^1.0.0-beta.6:
ripple-keypairs@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ripple-keypairs/-/ripple-keypairs-1.0.0.tgz#8f1c604f89daeac5e61b7eebbbca2da99da2bacf"
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"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^3.6.4:
typescript@^3.7.5:
version "3.7.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==