Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update the default signing algorithm to ed25519 #2658

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
1 change: 1 addition & 0 deletions packages/ripple-keypairs/HISTORY.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
2 changes: 1 addition & 1 deletion packages/ripple-keypairs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ eddsa deterministic signatures.
```
generateSeed({entropy?: Array<integer>, 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}
Expand Down
4 changes: 3 additions & 1 deletion packages/ripple-keypairs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be tested against the enum?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 24 should use the enum too for the key

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've lost some of the context here. Which enum are you referring to?
In line 24, I'm using secp256k1 imported from './signing-schemes/secp256k1'

Are you referring to that import?

return encodeSeed(entropy, type)
}

Expand Down Expand Up @@ -110,3 +110,5 @@ export {
deriveNodeAddress,
decodeSeed,
}

export type { Algorithm }
24 changes: 20 additions & 4 deletions packages/ripple-keypairs/test/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand All @@ -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)
Expand Down
19 changes: 17 additions & 2 deletions packages/secret-numbers/src/schema/Account.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)) {
Expand All @@ -44,6 +52,10 @@ export class Account {
this._secret = randomSecret()
}

if (algorithm) {
this._algorithm = algorithm
}

validateLengths(this._secret)
this.derive()
}
Expand Down Expand Up @@ -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) {
Expand Down
63 changes: 60 additions & 3 deletions packages/secret-numbers/test/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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 =
Expand All @@ -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')
})
Expand Down
12 changes: 5 additions & 7 deletions packages/xrpl/src/Wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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
Expand Down Expand Up @@ -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, {
Expand Down
20 changes: 20 additions & 0 deletions packages/xrpl/test/wallet/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
Loading