diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 860c086db6..4a217acfcb 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`. diff --git a/packages/ripple-keypairs/HISTORY.md b/packages/ripple-keypairs/HISTORY.md index 7f4003b3a7..49dd7169f3 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) 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/src/index.ts b/packages/ripple-keypairs/src/index.ts index f41326e535..d39fe6e5aa 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) } @@ -110,3 +110,5 @@ export { deriveNodeAddress, decodeSeed, } + +export type { Algorithm } diff --git a/packages/ripple-keypairs/test/api.test.ts b/packages/ripple-keypairs/test/api.test.ts index 91c19d6189..eafc7487f1 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,22 @@ 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() + 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/src/schema/Account.ts b/packages/secret-numbers/src/schema/Account.ts index 03678d0716..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, @@ -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,10 @@ export class Account { this._secret = randomSecret() } + if (algorithm) { + this._algorithm = algorithm + } + validateLengths(this._secret) this.derive() } @@ -75,7 +87,10 @@ export class Account { private derive(): void { try { const entropy = secretToEntropy(this._secret) - this._account.familySeed = generateSeed({ entropy }) + this._account.familySeed = generateSeed({ + entropy, + algorithm: this._algorithm, + }) 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 b9c4c7cb0b..e045071cdc 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()) @@ -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 = @@ -48,6 +48,63 @@ describe('API: XRPL Secret Numbers', () => { const account = new Account(secret) + it('familySeed as expected', () => { + expect(account.getFamilySeed()).toEqual('sEdSmrWh6iszywyGQCgguErD9DiuBY8') + }) + it('publicKey as expected', () => { + const pubkey = + 'EDBB1A131EA944C5D07D1DE39CAD2E128329CD1321F2F5759D2BB3EB94D5B8AB2F' + expect(account.getKeypair().publicKey).toEqual(pubkey) + }) + it('privateKey as expected', () => { + const privkey = + 'EDB55E7518A732963CD444E6D1E682DCD6AD60DD53AA5743854D4C4AB52E2D6800' + expect(account.getKeypair().privateKey).toEqual(privkey) + }) + it('address as expected', () => { + expect(account.getAddress()).toEqual('rJmyR83BfJdRpJabbkBH2ES8mkR168bNVJ') + }) + it('Account object to string as expected', () => { + const accountAsStr = + '084677 005323 580272 282388 626800 105300 560913 071783' + expect(`${account.toString()}`).toEqual(accountAsStr) + }) + }) + + 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') }) 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 () {