From 2126ad8bc2076d52814914fb80dff3ff52382613 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Thu, 7 Mar 2024 12:19:14 -0800 Subject: [PATCH 1/9] update the default signing algorithm to ed25519. This causes breakage in downstream classes like Account --- packages/ripple-keypairs/src/index.ts | 2 +- packages/ripple-keypairs/test/api.test.ts | 16 ++++++++++++---- packages/secret-numbers/test/api.test.ts | 12 ++++++------ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/ripple-keypairs/src/index.ts b/packages/ripple-keypairs/src/index.ts index f41326e535..b99c38bcc6 100644 --- a/packages/ripple-keypairs/src/index.ts +++ b/packages/ripple-keypairs/src/index.ts @@ -38,7 +38,7 @@ function generateSeed( const entropy = options.entropy ? options.entropy.slice(0, 16) : randomBytes(16) - const type = options.algorithm === 'ed25519' ? 'ed25519' : 'secp256k1' + const type = options.algorithm === 'ecdsa-secp256k1' ? 'secp256k1' : 'ed25519' return encodeSeed(entropy, type) } diff --git a/packages/ripple-keypairs/test/api.test.ts b/packages/ripple-keypairs/test/api.test.ts index 91c19d6189..3b597932ca 100644 --- a/packages/ripple-keypairs/test/api.test.ts +++ b/packages/ripple-keypairs/test/api.test.ts @@ -15,15 +15,15 @@ const entropy = new Uint8Array([ ]) describe('api', () => { - it('generateSeed - secp256k1', () => { - expect(generateSeed({ entropy })).toEqual(fixtures.secp256k1.seed) + it('generateSeed - ed25519', () => { + expect(generateSeed({ entropy })).toEqual(fixtures.ed25519.seed) }) - it('generateSeed - secp256k1, random', () => { + it('generateSeed - ed25519, random', () => { const seed = generateSeed() expect(seed.startsWith('s')).toBeTruthy() const { type, bytes } = decodeSeed(seed) - expect(type).toEqual('secp256k1') + expect(type).toEqual('ed25519') expect(bytes.length).toEqual(16) }) @@ -41,6 +41,14 @@ describe('api', () => { expect(bytes.length).toEqual(16) }) + it('generateSeed, default algorithm used is ed25519', () => { + const seed = generateSeed() + expect(seed.startsWith('sEd')).toBeTruthy() + const { type, bytes } = decodeSeed(seed) + expect(type).toEqual('ed25519') + expect(bytes.length).toEqual(16) + }) + it('deriveKeypair - secp256k1', () => { const keypair = deriveKeypair(fixtures.secp256k1.seed) expect(keypair).toEqual(fixtures.secp256k1.keypair) diff --git a/packages/secret-numbers/test/api.test.ts b/packages/secret-numbers/test/api.test.ts index b9c4c7cb0b..b46aeb3169 100644 --- a/packages/secret-numbers/test/api.test.ts +++ b/packages/secret-numbers/test/api.test.ts @@ -22,10 +22,10 @@ describe('API: XRPL Secret Numbers', () => { const account = new Account(entropy) it('familySeed as expected', () => { - expect(account.getFamilySeed()).toEqual('sp5DmDCut79BpgumfHhvRzdxXYQyU') + expect(account.getFamilySeed()).toEqual('sEdSKUm3MuTvN745ezpSM94Xw45BsbA') }) it('address as expected', () => { - expect(account.getAddress()).toEqual('rMCcybKHfwCSkDHd3M36PAeUniEoygwjR3') + expect(account.getAddress()).toEqual('rMjDw1h3vQZUfYkQJV7PXeToajAA4JtkFJ') }) it('Account object to string as expected', () => { const accountAsStr = @@ -49,20 +49,20 @@ describe('API: XRPL Secret Numbers', () => { const account = new Account(secret) it('familySeed as expected', () => { - expect(account.getFamilySeed()).toEqual('sswpWwri7Y11dNCSmXdphgcoPZk3y') + expect(account.getFamilySeed()).toEqual('sEdSmrWh6iszywyGQCgguErD9DiuBY8') }) it('publicKey as expected', () => { const pubkey = - '020526A0EDC9123F7FBB7588402518B80FCD2C8D8AB4C45F5A68A2F220098EA06F' + 'EDBB1A131EA944C5D07D1DE39CAD2E128329CD1321F2F5759D2BB3EB94D5B8AB2F' expect(account.getKeypair().publicKey).toEqual(pubkey) }) it('privateKey as expected', () => { const privkey = - '005122B2127B4635FEE7D242FA6EC9B02B611C04494D0D7D49764374D90C8BC8D3' + 'EDB55E7518A732963CD444E6D1E682DCD6AD60DD53AA5743854D4C4AB52E2D6800' expect(account.getKeypair().privateKey).toEqual(privkey) }) it('address as expected', () => { - expect(account.getAddress()).toEqual('rfqJsRLLmr7wdWnEzW1mP6AVaPSdzmso9Z') + expect(account.getAddress()).toEqual('rJmyR83BfJdRpJabbkBH2ES8mkR168bNVJ') }) it('Account object to string as expected', () => { const accountAsStr = From f2c68ddf3f67380f86cefaed78431e94b705251e Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Thu, 7 Mar 2024 12:26:23 -0800 Subject: [PATCH 2/9] update HISTORY file --- packages/ripple-keypairs/HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ripple-keypairs/HISTORY.md b/packages/ripple-keypairs/HISTORY.md index 6a20d8af5f..832e502f12 100644 --- a/packages/ripple-keypairs/HISTORY.md +++ b/packages/ripple-keypairs/HISTORY.md @@ -1,6 +1,7 @@ # ripple-keypairs Release History ## Unreleased +- Update the default signing algorithm in `generateSeed` function to ed25519. This brings compatibility with the `fromSeed` function ## 2.0.0 (2024-02-01) From 52605250ed2ea36dbaf16b9331986938bd01a798 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Thu, 7 Mar 2024 16:45:53 -0800 Subject: [PATCH 3/9] include trouble shooting steps: broken dependencies --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 787117cb2a..d7ca85ed4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -276,3 +276,7 @@ We have a low-traffic mailing list for announcements of new `xrpl.js` releases. If you're using the XRP Ledger in production, you should run a [rippled server](https://github.com/ripple/rippled) and subscribe to the ripple-server mailing list as well. - [Subscribe to ripple-server](https://groups.google.com/g/ripple-server) + +## Troubleshooting steps + +If you encounter errors related to dependencies in the `npm run build` step, execute: `npm install`. If the error persists despite a successful execution of `npm i`, execute `npm run clean && npm install`. From 0a20edf8ea60c81c2d4834177332c9287dc0bb84 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Fri, 8 Mar 2024 16:40:06 -0800 Subject: [PATCH 4/9] update Wallet.fromMnemonic and Wallet.fromRFC1751Mnemonic to use ed25519 as the default signing algorithm --- packages/xrpl/src/Wallet/index.ts | 12 +++++------- packages/xrpl/test/wallet/index.test.ts | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/xrpl/src/Wallet/index.ts b/packages/xrpl/src/Wallet/index.ts index c5ce5baca5..10231612dc 100644 --- a/packages/xrpl/src/Wallet/index.ts +++ b/packages/xrpl/src/Wallet/index.ts @@ -223,8 +223,7 @@ export class Wallet { * @param opts.mnemonicEncoding - If set to 'rfc1751', this interprets the mnemonic as a rippled RFC1751 mnemonic like * `wallet_propose` generates in rippled. Otherwise the function defaults to bip39 decoding. * @param opts.algorithm - Only used if opts.mnemonicEncoding is 'rfc1751'. Allows the mnemonic to generate its - * secp256k1 seed, or its ed25519 seed. By default, it will generate the secp256k1 seed - * to match the rippled `wallet_propose` default algorithm. + * secp256k1 seed, or its ed25519 seed. By default, it will generate the ed25519 seed. * @returns A Wallet derived from a mnemonic. * @throws ValidationError if unable to derive private key from mnemonic input. */ @@ -240,7 +239,7 @@ export class Wallet { if (opts.mnemonicEncoding === 'rfc1751') { return Wallet.fromRFC1751Mnemonic(mnemonic, { masterAddress: opts.masterAddress, - algorithm: opts.algorithm, + algorithm: opts.algorithm ?? DEFAULT_ALGORITHM, }) } // Otherwise decode using bip39's mnemonic standard @@ -279,11 +278,10 @@ export class Wallet { ): Wallet { const seed = rfc1751MnemonicToKey(mnemonic) let encodeAlgorithm: 'ed25519' | 'secp256k1' - if (opts.algorithm === ECDSA.ed25519) { - encodeAlgorithm = 'ed25519' - } else { - // Defaults to secp256k1 since that's the default for `wallet_propose` + if (opts.algorithm === ECDSA.secp256k1) { encodeAlgorithm = 'secp256k1' + } else { + encodeAlgorithm = 'ed25519' } const encodedSeed = encodeSeed(seed, encodeAlgorithm) return Wallet.fromSeed(encodedSeed, { diff --git a/packages/xrpl/test/wallet/index.test.ts b/packages/xrpl/test/wallet/index.test.ts index bbb454108f..e2c255eb07 100644 --- a/packages/xrpl/test/wallet/index.test.ts +++ b/packages/xrpl/test/wallet/index.test.ts @@ -310,6 +310,26 @@ describe('Wallet', function () { assert.equal(wallet.privateKey, regularKeyPair.privateKey) assert.equal(wallet.classicAddress, masterAddress) }) + + it('derive a wallet using the default signing algorithm (ed25519) with RFC1751 mnemonic', function () { + const masterAddress = 'rUAi7pipxGpYfPNg3LtPcf2ApiS8aw9A93' + const regularKeyPair = { + mnemonic: 'I IRE BOND BOW TRIO LAID SEAT GOAL HEN IBIS IBIS DARE', + publicKey: + 'EDAAC3F98BB94F451804EF5993C847DAAA4E6154F455635659D88AA5C80F156303', + privateKey: + 'ED93D09224D09221B8845E7A9772E0D6259CD01029C557CD95978CC674E0192B25', + } + + const wallet = Wallet.fromMnemonic(regularKeyPair.mnemonic, { + masterAddress, + mnemonicEncoding: 'rfc1751', + }) + + assert.equal(wallet.publicKey, regularKeyPair.publicKey) + assert.equal(wallet.privateKey, regularKeyPair.privateKey) + assert.equal(wallet.classicAddress, masterAddress) + }) }) describe('fromSecretNumbers', function () { From a15c7c78bb74b5dbfd92e17feb1a578cdd88140f Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Fri, 8 Mar 2024 17:04:09 -0800 Subject: [PATCH 5/9] - add unit tests for Wallet.generateSeed() with secp256k1 signing algorithm - explicitly state the algorithm used with generateSeed(), instead of relying on the defaults --- packages/ripple-keypairs/README.md | 2 +- packages/ripple-keypairs/test/api.test.ts | 8 ++++++++ packages/secret-numbers/src/schema/Account.ts | 5 ++++- packages/secret-numbers/test/api.test.ts | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/ripple-keypairs/README.md b/packages/ripple-keypairs/README.md index a1f05ded62..b04b6cd336 100644 --- a/packages/ripple-keypairs/README.md +++ b/packages/ripple-keypairs/README.md @@ -11,7 +11,7 @@ eddsa deterministic signatures. ``` generateSeed({entropy?: Array, algorithm?: string}) -> string ``` -Generate a seed that can be used to generate keypairs. Entropy can be provided as an array of bytes expressed as integers in the range 0-255. If provided, it must be 16 bytes long (additional bytes are ignored). If not provided, entropy will be automatically generated. The "algorithm" defaults to "ecdsa-secp256k1", but can also be set to "ed25519". The result is a seed encoded in base58, starting with "s". +Generate a seed that can be used to generate keypairs. Entropy can be provided as an array of bytes expressed as integers in the range 0-255. If provided, it must be 16 bytes long (additional bytes are ignored). If not provided, entropy will be automatically generated. The "algorithm" defaults to "ed25519", but can also be set to "ecdsa-secp256k1". The result is a seed encoded in base58, starting with "s". ``` deriveKeypair(seed: string) -> {privateKey: string, publicKey: string} diff --git a/packages/ripple-keypairs/test/api.test.ts b/packages/ripple-keypairs/test/api.test.ts index 3b597932ca..eafc7487f1 100644 --- a/packages/ripple-keypairs/test/api.test.ts +++ b/packages/ripple-keypairs/test/api.test.ts @@ -41,6 +41,14 @@ describe('api', () => { expect(bytes.length).toEqual(16) }) + it('generateSeed - seckp256k1, random', () => { + const seed = generateSeed({ algorithm: 'ecdsa-secp256k1' }) + expect(seed.startsWith('s')).toBeTruthy() + const { type, bytes } = decodeSeed(seed) + expect(type).toEqual('secp256k1') + expect(bytes.length).toEqual(16) + }) + it('generateSeed, default algorithm used is ed25519', () => { const seed = generateSeed() expect(seed.startsWith('sEd')).toBeTruthy() diff --git a/packages/secret-numbers/src/schema/Account.ts b/packages/secret-numbers/src/schema/Account.ts index 03678d0716..17229e6b53 100644 --- a/packages/secret-numbers/src/schema/Account.ts +++ b/packages/secret-numbers/src/schema/Account.ts @@ -75,7 +75,10 @@ export class Account { private derive(): void { try { const entropy = secretToEntropy(this._secret) - this._account.familySeed = generateSeed({ entropy }) + this._account.familySeed = generateSeed({ + entropy, + algorithm: 'ed25519', + }) this._account.keypair = deriveKeypair(this._account.familySeed) this._account.address = deriveAddress(this._account.keypair.publicKey) } catch (error) { diff --git a/packages/secret-numbers/test/api.test.ts b/packages/secret-numbers/test/api.test.ts index b46aeb3169..bbaf10617f 100644 --- a/packages/secret-numbers/test/api.test.ts +++ b/packages/secret-numbers/test/api.test.ts @@ -9,7 +9,7 @@ describe('API: XRPL Secret Numbers', () => { it('Output sanity checks', () => { expect(account.getAddress()).toMatch(/^r[a-zA-Z0-9]{19,}$/u) const entropy = secretToEntropy(`${account.toString()}`.split(' ')) - const familySeed = generateSeed({ entropy }) + const familySeed = generateSeed({ entropy, algorithm: 'ed25519' }) const keypair = deriveKeypair(familySeed) const address = deriveAddress(keypair.publicKey) expect(address).toEqual(account.getAddress()) From a234fa9cc9a03231f9b84651e805eebcc8365eba Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Tue, 12 Mar 2024 10:51:12 -0700 Subject: [PATCH 6/9] include algorithm data member in the secret-numbers/Account class --- packages/secret-numbers/src/schema/Account.ts | 14 ++++- packages/secret-numbers/test/api.test.ts | 57 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/secret-numbers/src/schema/Account.ts b/packages/secret-numbers/src/schema/Account.ts index 17229e6b53..41522b99ff 100644 --- a/packages/secret-numbers/src/schema/Account.ts +++ b/packages/secret-numbers/src/schema/Account.ts @@ -7,6 +7,9 @@ import { secretToEntropy, } from '../utils' +// TODO: preferably import this type from ripple-keypairs +// import type { Algorithm } from 'ripple-keypairs' +type Algorithm = 'ecdsa-secp256k1' | 'ed25519' /* Types ==================================================================== */ export interface Keypair { @@ -33,7 +36,12 @@ export class Account { }, } - constructor(secretNumbers?: string[] | string | Uint8Array) { + private readonly _algorithm: Algorithm = 'ed25519' + + constructor( + secretNumbers?: string[] | string | Uint8Array, + algorithm?: Algorithm, + ) { if (typeof secretNumbers === 'string') { this._secret = parseSecretString(secretNumbers) } else if (Array.isArray(secretNumbers)) { @@ -44,6 +52,8 @@ export class Account { this._secret = randomSecret() } + if (algorithm) this._algorithm = algorithm + validateLengths(this._secret) this.derive() } @@ -77,7 +87,7 @@ export class Account { const entropy = secretToEntropy(this._secret) this._account.familySeed = generateSeed({ entropy, - algorithm: 'ed25519', + algorithm: this._algorithm, }) this._account.keypair = deriveKeypair(this._account.familySeed) this._account.address = deriveAddress(this._account.keypair.publicKey) diff --git a/packages/secret-numbers/test/api.test.ts b/packages/secret-numbers/test/api.test.ts index bbaf10617f..e045071cdc 100644 --- a/packages/secret-numbers/test/api.test.ts +++ b/packages/secret-numbers/test/api.test.ts @@ -71,6 +71,63 @@ describe('API: XRPL Secret Numbers', () => { }) }) + describe('Validate the default signing algorithm', () => { + const secret = [ + '084677', + '005323', + '580272', + '282388', + '626800', + '105300', + '560913', + '071783', + ] + + const account1 = new Account(secret) + const account2 = new Account(secret, 'ed25519') + + it('default signing algorithm is ed25519 in the Account class', () => { + expect(account1).toEqual(account2) + }) + }) + + describe('Account based on existing secret, explicitly specify secp256k1 algorithm', () => { + const secret = [ + '084677', + '005323', + '580272', + '282388', + '626800', + '105300', + '560913', + '071783', + ] + + const account = new Account(secret, 'ecdsa-secp256k1') + + it('familySeed as expected', () => { + expect(account.getFamilySeed()).toEqual('sswpWwri7Y11dNCSmXdphgcoPZk3y') + }) + it('publicKey as expected', () => { + const pubkey = + '020526A0EDC9123F7FBB7588402518B80FCD2C8D8AB4C45F5A68A2F220098EA06F' + expect(account.getKeypair().publicKey).toEqual(pubkey) + }) + it('privateKey as expected', () => { + const privkey = + '005122B2127B4635FEE7D242FA6EC9B02B611C04494D0D7D49764374D90C8BC8D3' + expect(account.getKeypair().privateKey).toEqual(privkey) + }) + it('address as expected', () => { + expect(account.getAddress()).toEqual('rfqJsRLLmr7wdWnEzW1mP6AVaPSdzmso9Z') + }) + it('Account object to string as expected', () => { + const accountAsStr = + '084677 005323 580272 282388 626800 105300 560913 071783' + expect(`${account.toString()}`).toEqual(accountAsStr) + }) + }) + describe('Checksum error', () => { const secret = [ '084677', From 3156306293b6aba934a08628d20873c382c900c6 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Tue, 12 Mar 2024 10:57:52 -0700 Subject: [PATCH 7/9] fix lint error --- packages/secret-numbers/src/schema/Account.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/secret-numbers/src/schema/Account.ts b/packages/secret-numbers/src/schema/Account.ts index 41522b99ff..280b84166f 100644 --- a/packages/secret-numbers/src/schema/Account.ts +++ b/packages/secret-numbers/src/schema/Account.ts @@ -52,7 +52,9 @@ export class Account { this._secret = randomSecret() } - if (algorithm) this._algorithm = algorithm + if (algorithm) { + this._algorithm = algorithm + } validateLengths(this._secret) this.derive() From c25d871467e57156209e174dfa2b36d3d0c3fd0b Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Wed, 13 Mar 2024 12:19:39 -0700 Subject: [PATCH 8/9] import Algorithm type from ripple-keypairs module --- packages/ripple-keypairs/src/index.ts | 1 + packages/secret-numbers/src/schema/Account.ts | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ripple-keypairs/src/index.ts b/packages/ripple-keypairs/src/index.ts index b99c38bcc6..d1e8734ca8 100644 --- a/packages/ripple-keypairs/src/index.ts +++ b/packages/ripple-keypairs/src/index.ts @@ -109,4 +109,5 @@ export { deriveAddress, deriveNodeAddress, decodeSeed, + Algorithm, } diff --git a/packages/secret-numbers/src/schema/Account.ts b/packages/secret-numbers/src/schema/Account.ts index 280b84166f..b0e553c44f 100644 --- a/packages/secret-numbers/src/schema/Account.ts +++ b/packages/secret-numbers/src/schema/Account.ts @@ -7,9 +7,7 @@ import { secretToEntropy, } from '../utils' -// TODO: preferably import this type from ripple-keypairs -// import type { Algorithm } from 'ripple-keypairs' -type Algorithm = 'ecdsa-secp256k1' | 'ed25519' +import type { Algorithm } from 'ripple-keypairs' /* Types ==================================================================== */ export interface Keypair { From 82e5c08a3f1f69a1241bf003db5ad93d5b26ae95 Mon Sep 17 00:00:00 2001 From: Chenna Keshava Date: Wed, 8 May 2024 14:09:16 -0700 Subject: [PATCH 9/9] fic lint errors: use an import alias --- packages/ripple-keypairs/src/index.ts | 3 ++- packages/secret-numbers/src/schema/Account.ts | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/ripple-keypairs/src/index.ts b/packages/ripple-keypairs/src/index.ts index d1e8734ca8..d39fe6e5aa 100644 --- a/packages/ripple-keypairs/src/index.ts +++ b/packages/ripple-keypairs/src/index.ts @@ -109,5 +109,6 @@ export { deriveAddress, deriveNodeAddress, decodeSeed, - Algorithm, } + +export type { Algorithm } diff --git a/packages/secret-numbers/src/schema/Account.ts b/packages/secret-numbers/src/schema/Account.ts index b0e553c44f..9a2166d7ca 100644 --- a/packages/secret-numbers/src/schema/Account.ts +++ b/packages/secret-numbers/src/schema/Account.ts @@ -1,4 +1,7 @@ import { deriveAddress, deriveKeypair, generateSeed } from 'ripple-keypairs' +// Use an import alias to avoid name-conflict with the Algorithm type +// defined in extensions/node_modules/typescript/lib/lib.dom.d.ts +import type { Algorithm as _Algorithm } from 'ripple-keypairs' import { entropyToSecret, @@ -7,7 +10,6 @@ import { secretToEntropy, } from '../utils' -import type { Algorithm } from 'ripple-keypairs' /* Types ==================================================================== */ export interface Keypair { @@ -34,11 +36,11 @@ export class Account { }, } - private readonly _algorithm: Algorithm = 'ed25519' + private readonly _algorithm: _Algorithm = 'ed25519' constructor( secretNumbers?: string[] | string | Uint8Array, - algorithm?: Algorithm, + algorithm?: _Algorithm, ) { if (typeof secretNumbers === 'string') { this._secret = parseSecretString(secretNumbers)