Skip to content

Commit 701416a

Browse files
ahsan-javaidjanniks
authored andcommitted
fix: offload bip39 from wallet-sdk
1 parent 591b0fb commit 701416a

File tree

5 files changed

+104
-23
lines changed

5 files changed

+104
-23
lines changed

packages/wallet-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"@stacks/profile": "^4.0.0",
5454
"@stacks/storage": "^4.0.0",
5555
"@stacks/transactions": "^4.0.0",
56-
"bip39": "^3.0.2",
56+
"@scure/bip39": "^1.0.0",
5757
"bitcoinjs-lib": "^5.2.0",
5858
"bn.js": "^5.2.0",
5959
"c32check": "^1.1.3",

packages/wallet-sdk/src/generate.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
import { generateMnemonic, mnemonicToSeed } from 'bip39';
1+
// https://github.com/paulmillr/scure-bip39
2+
// Secure, audited & minimal implementation of BIP39 mnemonic phrases.
3+
import { generateMnemonic, mnemonicToSeed } from '@scure/bip39';
4+
// Word lists not imported by default as that would increase bundle sizes too much as in case of bitcoinjs/bip39
5+
// Use default english world list similiar to bitcoinjs/bip39
6+
// Backward compatible with bitcoinjs/bip39 dependency
7+
// Very small in size as compared to bitcoinjs/bip39 wordlist
8+
// Reference: https://github.com/paulmillr/scure-bip39
9+
import { wordlist } from '@scure/bip39/wordlists/english';
10+
211
// https://github.com/paulmillr/scure-bip32
312
// Secure, audited & minimal implementation of BIP32 hierarchical deterministic (HD) wallets.
413
import { HDKey } from '@scure/bip32';
5-
import { randomBytes } from '@stacks/encryption';
614
import { Wallet, getRootNode } from './models/common';
715
import { encrypt } from './encryption';
816
import { deriveAccount, deriveWalletKeys } from './derive';
@@ -11,7 +19,7 @@ import { DerivationType } from '.';
1119
export type AllowedKeyEntropyBits = 128 | 256;
1220

1321
export const generateSecretKey = (entropy: AllowedKeyEntropyBits = 256) => {
14-
const secretKey = generateMnemonic(entropy, randomBytes);
22+
const secretKey = generateMnemonic(wordlist, entropy);
1523
return secretKey;
1624
};
1725

packages/wallet-sdk/tests/derive-keychain.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
selectStxDerivation,
88
fetchUsernameForAccountByDerivationType,
99
} from '../src';
10-
import { mnemonicToSeed } from 'bip39';
10+
// https://github.com/paulmillr/scure-bip39
11+
// Secure, audited & minimal implementation of BIP39 mnemonic phrases.
12+
import { mnemonicToSeed } from '@scure/bip39';
1113
import { BIP32Interface, fromBase58 } from 'bip32';
1214
import { HDKey } from '@scure/bip32';
1315
import { TransactionVersion, bytesToHex } from '@stacks/transactions';

packages/wallet-sdk/tests/derive.test.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
selectStxDerivation,
88
fetchUsernameForAccountByDerivationType,
99
} from '../src';
10-
import { mnemonicToSeed } from 'bip39';
10+
// https://github.com/paulmillr/scure-bip39
11+
// Secure, audited & minimal implementation of BIP39 mnemonic phrases.
12+
import { mnemonicToSeed } from '@scure/bip39';
1113
import { fromBase58, fromSeed } from 'bip32';
1214
import { TransactionVersion } from '@stacks/transactions';
1315
import { StacksMainnet } from '@stacks/network';
@@ -22,7 +24,7 @@ const DATA_ADDRESS = 'SP30RZ44NTH2D95M1HSWVMM8VVHSAFY71VF3XQZ0K';
2224

2325
test('keys are serialized, and can be deserialized properly using wallet private key for stx', async () => {
2426
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
25-
const rootNode1 = fromSeed(rootPrivateKey);
27+
const rootNode1 = fromSeed(Buffer.from(rootPrivateKey));
2628
const derived = await deriveWalletKeys(rootNode1);
2729
const rootNode = fromBase58(derived.rootKey);
2830
const account = deriveAccount({
@@ -38,7 +40,7 @@ test('keys are serialized, and can be deserialized properly using wallet private
3840

3941
test('keys are serialized, and can be deserialized properly using data private key for stx', async () => {
4042
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
41-
const rootNode1 = fromSeed(rootPrivateKey);
43+
const rootNode1 = fromSeed(Buffer.from(rootPrivateKey));
4244
const derived = await deriveWalletKeys(rootNode1);
4345
const rootNode = fromBase58(derived.rootKey);
4446
const account = deriveAccount({
@@ -54,14 +56,14 @@ test('keys are serialized, and can be deserialized properly using data private k
5456

5557
test('backwards compatible legacy config private key derivation', async () => {
5658
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
57-
const rootNode = fromSeed(rootPrivateKey);
59+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
5860
const legacyKey = deriveLegacyConfigPrivateKey(rootNode);
5961
expect(legacyKey).toEqual('767b51d866d068b02ce126afe3737896f4d0c486263d9b932f2822109565a3c6');
6062
});
6163

6264
test('derive derivation path without username', async () => {
6365
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
64-
const rootNode = fromSeed(rootPrivateKey);
66+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
6567
const network = new StacksMainnet();
6668
const { username, stxDerivationType } = await selectStxDerivation({
6769
username: undefined,
@@ -75,7 +77,7 @@ test('derive derivation path without username', async () => {
7577

7678
test('derive derivation path with username owned by address of stx derivation path', async () => {
7779
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
78-
const rootNode = fromSeed(rootPrivateKey);
80+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
7981
const network = new StacksMainnet();
8082

8183
fetchMock.once(JSON.stringify({ address: DATA_ADDRESS }));
@@ -92,7 +94,7 @@ test('derive derivation path with username owned by address of stx derivation pa
9294

9395
test('derive derivation path with username owned by address of unknown derivation path', async () => {
9496
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
95-
const rootNode = fromSeed(rootPrivateKey);
97+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
9698
const network = new StacksMainnet();
9799

98100
fetchMock.once(JSON.stringify({ address: 'SP000000000000000000002Q6VF78' }));
@@ -109,7 +111,7 @@ test('derive derivation path with username owned by address of unknown derivatio
109111

110112
test('derive derivation path with username owned by address of data derivation path', async () => {
111113
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
112-
const rootNode = fromSeed(rootPrivateKey);
114+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
113115
const network = new StacksMainnet();
114116

115117
fetchMock.once(JSON.stringify({ address: 'SP30RZ44NTH2D95M1HSWVMM8VVHSAFY71VF3XQZ0K' }));
@@ -126,7 +128,7 @@ test('derive derivation path with username owned by address of data derivation p
126128

127129
test('derive derivation path with new username owned by address of stx derivation path', async () => {
128130
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
129-
const rootNode = fromSeed(rootPrivateKey);
131+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
130132
const network = new StacksMainnet();
131133

132134
fetchMock.once(JSON.stringify({ names: ['public_profile_for_testing.id.blockstack'] }));
@@ -146,7 +148,7 @@ test('derive derivation path with new username owned by address of stx derivatio
146148

147149
test('derive derivation path with new username owned by address of data derivation path', async () => {
148150
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
149-
const rootNode = fromSeed(rootPrivateKey);
151+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
150152
const network = new StacksMainnet();
151153

152154
fetchMock
@@ -171,7 +173,7 @@ test('derive derivation path with new username owned by address of data derivati
171173

172174
test('derive derivation path with username and without network', async () => {
173175
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
174-
const rootNode = fromSeed(rootPrivateKey);
176+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
175177

176178
const { username, stxDerivationType } = await selectStxDerivation({
177179
username: 'public_profile_for_testing.id.blockstack',
@@ -184,7 +186,7 @@ test('derive derivation path with username and without network', async () => {
184186

185187
test('derive derivation path without username and without network', async () => {
186188
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
187-
const rootNode = fromSeed(rootPrivateKey);
189+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
188190

189191
const { username, stxDerivationType } = await selectStxDerivation({
190192
username: undefined,
@@ -197,7 +199,7 @@ test('derive derivation path without username and without network', async () =>
197199

198200
test('fetch username owned by derivation type', async () => {
199201
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
200-
const rootNode = fromSeed(rootPrivateKey);
202+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
201203

202204
fetchMock.once(JSON.stringify({ names: ['public_profile_for_testing.id.blockstack'] }));
203205

@@ -212,7 +214,7 @@ test('fetch username owned by derivation type', async () => {
212214

213215
test('fetch username owned by different derivation type', async () => {
214216
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
215-
const rootNode = fromSeed(rootPrivateKey);
217+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
216218

217219
fetchMock.once(JSON.stringify({ names: [] }));
218220

@@ -227,7 +229,7 @@ test('fetch username owned by different derivation type', async () => {
227229

228230
test('fetch username defaults to mainnet', async () => {
229231
const rootPrivateKey = await mnemonicToSeed(SECRET_KEY);
230-
const rootNode = fromSeed(rootPrivateKey);
232+
const rootNode = fromSeed(Buffer.from(rootPrivateKey));
231233

232234
fetchMock.once(JSON.stringify({ names: ['public_profile_for_testing.id.blockstack'] }));
233235

packages/wallet-sdk/tests/generate.test.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { validateMnemonic } from 'bip39';
1+
// https://github.com/paulmillr/scure-bip39
2+
// Secure, audited & minimal implementation of BIP39 mnemonic phrases.
3+
import {entropyToMnemonic, mnemonicToSeed, mnemonicToEntropy, validateMnemonic} from '@scure/bip39';
4+
// Word lists not imported by default as that would increase bundle sizes too much as in case of bitcoinjs/bip39
5+
// Use default english world list similiar to bitcoinjs/bip39
6+
// Backward compatible with bitcoinjs/bip39 dependency
7+
// Very small in size as compared to bitcoinjs/bip39 wordlist
8+
// Reference: https://github.com/paulmillr/scure-bip39
9+
import { wordlist } from '@scure/bip39/wordlists/english';
210
import {
311
generateSecretKey,
412
generateWallet,
@@ -21,8 +29,8 @@ describe(generateSecretKey, () => {
2129
});
2230

2331
test('generates a valid mnemonic', () => {
24-
expect(validateMnemonic(generateSecretKey())).toBeTruthy();
25-
expect(validateMnemonic(generateSecretKey(128))).toBeTruthy();
32+
expect(validateMnemonic(generateSecretKey(), wordlist)).toBeTruthy();
33+
expect(validateMnemonic(generateSecretKey(128), wordlist)).toBeTruthy();
2634
});
2735
});
2836

@@ -70,3 +78,64 @@ describe(generateWallet, () => {
7078
);
7179
});
7280
});
81+
82+
describe('Compatibility verification @scure/bip39 vs bitcoinjs/bip39', () => {
83+
test('Verify compatibility @scure/bip39 <=> bitcoinjs/bip39', () => {
84+
// Consider an entropy
85+
const entropy = '00000000000000000000000000000000';
86+
// Consider same entropy in array format
87+
const entropyUint8Array = new Uint8Array(entropy.split('').map(Number));
88+
89+
// Use vectors to verify result with bitcoinjs/bip39 instead of importing bitcoinjs/bip39
90+
const bitcoinjsBip39 = { // Consider it equivalent to bitcoinjs/bip39 (offloaded now)
91+
// Using this map of required functions from bitcoinjs/bip39 and mocking the output for considered entropy
92+
entropyToMnemonicBip39: (_: string) => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
93+
validateMnemonicBip39: (_: string) => true,
94+
mnemonicToEntropyBip39: (_: string) => '00000000000000000000000000000000',
95+
};
96+
97+
// entropyToMnemonicBip39 imported from bitcoinjs/bip39
98+
const bip39Mnemonic = bitcoinjsBip39.entropyToMnemonicBip39(entropy);
99+
// entropyToMnemonic imported from @scure/bip39
100+
const mnemonic = entropyToMnemonic(entropyUint8Array, wordlist);
101+
102+
//Phase 1: Cross verify mnemonic validity: @scure/bip39 <=> bitcoinjs/bip39
103+
104+
// validateMnemonic imported from @scure/bip39
105+
expect(validateMnemonic(bip39Mnemonic, wordlist)).toEqual(true);
106+
// validateMnemonicBip39 imported from bitcoinjs/bip39
107+
expect(bitcoinjsBip39.validateMnemonicBip39(mnemonic)).toEqual(true);
108+
109+
// validateMnemonic imported from @scure/bip39
110+
expect(validateMnemonic(mnemonic, wordlist)).toEqual(true);
111+
// validateMnemonicBip39 imported from bitcoinjs/bip39
112+
expect(bitcoinjsBip39.validateMnemonicBip39(bip39Mnemonic)).toEqual(true);
113+
114+
//Phase 2: Get back entropy from mnemonic and verify @scure/bip39 <=> bitcoinjs/bip39
115+
116+
// mnemonicToEntropy imported from @scure/bip39
117+
expect(mnemonicToEntropy(mnemonic, wordlist)).toEqual(entropyUint8Array);
118+
// mnemonicToEntropyBip39 imported from bitcoinjs/bip39
119+
expect(bitcoinjsBip39.mnemonicToEntropyBip39(bip39Mnemonic)).toEqual(entropy);
120+
// mnemonicToEntropy imported from @scure/bip39
121+
expect(Buffer.from(mnemonicToEntropy(bip39Mnemonic, wordlist)).toString('hex')).toEqual(entropy);
122+
// mnemonicToEntropyBip39 imported from bitcoinjs/bip39
123+
const entropyString = bitcoinjsBip39.mnemonicToEntropyBip39(mnemonic);
124+
// Convert entropy to bytes
125+
const entropyInBytes = new Uint8Array(entropyString.split('').map(Number))
126+
// entropy should match with entropyUint8Array
127+
expect(entropyInBytes).toEqual(entropyUint8Array);
128+
});
129+
130+
test('Seed verification @scure/bip39 <=> bitcoinjs/bip39', async () => {
131+
// Consider an entropy as actually generated by calling generateMnemonic(wordlist)
132+
const mnemonic = 'limb basket cactus metal come display chicken brief execute version attract journey';
133+
const seed = await mnemonicToSeed(mnemonic);
134+
// Use vectors to verify result with bitcoinjs/bip39 instead of importing bitcoinjs/bip39
135+
const bitcoinjsBip39 = { // Consider it equivalent to bitcoinjs/bip39 (offloaded now)
136+
// Using this map of required functions from bitcoinjs/bip39 and mocking the output for considered entropy
137+
mnemonicToSeedBip39: (_: string) => '8f157914f06a56abf3a188c9a96faa74100e34d30aff7a6bafe8af33d5c398ef703759e30654f536a2241dc88a5fd3d963b743153b450c91dcfc0ab9f3d90256'
138+
};
139+
expect(Buffer.from(seed).toString('hex')).toEqual(bitcoinjsBip39.mnemonicToSeedBip39(mnemonic));
140+
});
141+
});

0 commit comments

Comments
 (0)