Skip to content

Commit f7d1719

Browse files
committed
fix: gnokey add -derivation-path flag
Signed-off-by: D4ryl00 <d4ryl00@gmail.com>
1 parent 33412c2 commit f7d1719

File tree

2 files changed

+233
-54
lines changed

2 files changed

+233
-54
lines changed

tm2/pkg/crypto/keys/client/add.go

Lines changed: 152 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -143,87 +143,178 @@ func execAdd(cfg *AddCfg, args []string, io commands.IO) error {
143143
return fmt.Errorf("unable to read keybase, %w", err)
144144
}
145145

146-
// Check if the key exists
147-
exists, err := kb.HasByName(name)
148-
if err != nil {
149-
return fmt.Errorf("unable to fetch key, %w", err)
146+
getMnemonic := func() (string, error) {
147+
switch {
148+
case cfg.Recover:
149+
bip39Message := "Enter your bip39 mnemonic"
150+
var mnemonic string
151+
var err error
152+
if cfg.Masked {
153+
mnemonic, err = io.GetPassword(bip39Message, false)
154+
} else {
155+
mnemonic, err = io.GetString(bip39Message)
156+
}
157+
if err != nil {
158+
return "", fmt.Errorf("unable to parse mnemonic, %w", err)
159+
}
160+
161+
// Make sure it's valid
162+
if !bip39.IsMnemonicValid(mnemonic) {
163+
return "", errInvalidMnemonic
164+
}
165+
166+
return mnemonic, nil
167+
case cfg.Entropy:
168+
// Generate mnemonic using custom entropy
169+
mnemonic, err := GenerateMnemonicWithCustomEntropy(io, cfg.Masked)
170+
if err != nil {
171+
return "", fmt.Errorf("unable to generate mnemonic with custom entropy, %w", err)
172+
}
173+
174+
return mnemonic, nil
175+
default:
176+
// Generate mnemonic using computer PRNG
177+
mnemonic, err := GenerateMnemonic(mnemonicEntropySize)
178+
if err != nil {
179+
return "", fmt.Errorf("unable to generate mnemonic, %w", err)
180+
}
181+
182+
return mnemonic, nil
183+
}
150184
}
151185

152-
// Get overwrite confirmation, if any
153-
if exists {
154-
overwrite, err := io.GetConfirmation(fmt.Sprintf("Override the existing name %s", name))
186+
var (
187+
infos []keys.Info
188+
mnemonic string
189+
)
190+
191+
if len(cfg.DerivationPath) == 0 {
192+
// Check if the key exists
193+
exists, err := kb.HasByName(name)
155194
if err != nil {
156-
return fmt.Errorf("unable to get confirmation, %w", err)
195+
return fmt.Errorf("unable to fetch key, %w", err)
157196
}
158197

159-
if !overwrite {
160-
return errOverwriteAborted
161-
}
162-
}
198+
// Get overwrite confirmation, if any
199+
if exists {
200+
overwrite, err := io.GetConfirmation(fmt.Sprintf("Override the existing name %s", name))
201+
if err != nil {
202+
return fmt.Errorf("unable to get confirmation, %w", err)
203+
}
163204

164-
// Ask for a password when generating a local key
165-
pw, err := promptPassphrase(io, cfg.RootCfg.InsecurePasswordStdin)
166-
if err != nil {
167-
return err
168-
}
205+
if !overwrite {
206+
return errOverwriteAborted
207+
}
208+
}
169209

170-
var mnemonic string
210+
// Ask for a password when generating a local key
211+
pw, err := promptPassphrase(io, cfg.RootCfg.InsecurePasswordStdin)
212+
if err != nil {
213+
return err
214+
}
171215

172-
switch {
173-
case cfg.Recover:
174-
bip39Message := "Enter your bip39 mnemonic"
175-
if cfg.Masked {
176-
mnemonic, err = io.GetPassword(bip39Message, false)
177-
} else {
178-
mnemonic, err = io.GetString(bip39Message)
216+
mnemonic, err = getMnemonic()
217+
if err != nil {
218+
return err
179219
}
220+
221+
// Save the account
222+
info, err := kb.CreateAccount(
223+
name,
224+
mnemonic,
225+
"",
226+
pw,
227+
uint32(cfg.Account),
228+
uint32(cfg.Index),
229+
)
180230
if err != nil {
181-
return fmt.Errorf("unable to parse mnemonic, %w", err)
231+
return fmt.Errorf("unable to save account to keybase, %w", err)
182232
}
183233

184-
// Make sure it's valid
185-
if !bip39.IsMnemonicValid(mnemonic) {
186-
return errInvalidMnemonic
234+
infos = []keys.Info{info}
235+
} else {
236+
type deriveEntry struct {
237+
name string
238+
params *hd.BIP44Params
187239
}
188-
case cfg.Entropy:
189-
// Generate mnemonic using custom entropy
190-
mnemonic, err = GenerateMnemonicWithCustomEntropy(io, cfg.Masked)
191-
if err != nil {
192-
return fmt.Errorf("unable to generate mnemonic with custom entropy, %w", err)
240+
241+
entries := make([]deriveEntry, 0, len(cfg.DerivationPath))
242+
243+
for _, path := range cfg.DerivationPath {
244+
params, err := hd.NewParamsFromPath(path)
245+
if err != nil {
246+
return fmt.Errorf("unable to parse derivation path, %w", err)
247+
}
248+
249+
derivedName := deriveKeyName(name, params, len(cfg.DerivationPath))
250+
251+
exists, err := kb.HasByName(derivedName)
252+
if err != nil {
253+
return fmt.Errorf("unable to fetch key, %w", err)
254+
}
255+
256+
if exists {
257+
overwrite, err := io.GetConfirmation(fmt.Sprintf("Override the existing name %s", derivedName))
258+
if err != nil {
259+
return fmt.Errorf("unable to get confirmation, %w", err)
260+
}
261+
262+
if !overwrite {
263+
return errOverwriteAborted
264+
}
265+
}
266+
267+
entries = append(entries, deriveEntry{
268+
name: derivedName,
269+
params: params,
270+
})
193271
}
194-
default:
195-
// Generate mnemonic using computer PRNG
196-
mnemonic, err = GenerateMnemonic(mnemonicEntropySize)
272+
273+
mnemonic, err = getMnemonic()
197274
if err != nil {
198-
return fmt.Errorf("unable to generate mnemonic, %w", err)
275+
return err
199276
}
200-
}
201277

202-
// Save the account
203-
info, err := kb.CreateAccount(
204-
name,
205-
mnemonic,
206-
"",
207-
pw,
208-
uint32(cfg.Account),
209-
uint32(cfg.Index),
210-
)
211-
if err != nil {
212-
return fmt.Errorf("unable to save account to keybase, %w", err)
278+
infos = make([]keys.Info, 0, len(entries))
279+
280+
for _, entry := range entries {
281+
// Ask for a password when generating a local key
282+
pw, err := promptPassphrase(io, cfg.RootCfg.InsecurePasswordStdin)
283+
if err != nil {
284+
return err
285+
}
286+
287+
info, err := kb.CreateAccountBip44(
288+
entry.name,
289+
mnemonic,
290+
"",
291+
pw,
292+
*entry.params,
293+
)
294+
if err != nil {
295+
return fmt.Errorf("unable to save account to keybase, %w", err)
296+
}
297+
298+
infos = append(infos, info)
299+
}
213300
}
214301

215302
// Print the derived address info
216303
printDerive(mnemonic, cfg.DerivationPath, io)
217304

218305
// Recover key from seed passphrase
219306
if cfg.Recover {
220-
printCreate(info, false, "", io)
307+
for _, info := range infos {
308+
printCreate(info, false, "", io)
309+
}
221310

222311
return nil
223312
}
224313

225-
// Print the key create info
226-
printCreate(info, !cfg.NoBackup, mnemonic, io)
314+
// Print the key create info (mnemonic only once)
315+
for i, info := range infos {
316+
printCreate(info, !cfg.NoBackup && i == 0, mnemonic, io)
317+
}
227318

228319
return nil
229320
}
@@ -310,6 +401,14 @@ func printDerive(
310401
}
311402
}
312403

404+
func deriveKeyName(base string, params *hd.BIP44Params, totalPaths int) string {
405+
if totalPaths == 1 {
406+
return base
407+
}
408+
409+
return fmt.Sprintf("%s-%d-%d", base, params.Account, params.AddressIndex)
410+
}
411+
313412
// generateAccounts the accounts using the provided mnemonics
314413
func generateAccounts(mnemonic string, paths []string) []crypto.Address {
315414
addresses := make([]crypto.Address, len(paths))

tm2/pkg/crypto/keys/client/add_test.go

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/gnolang/gno/tm2/pkg/commands"
12+
"github.com/gnolang/gno/tm2/pkg/crypto/hd"
1213
"github.com/gnolang/gno/tm2/pkg/crypto/keys"
1314
"github.com/stretchr/testify/assert"
1415
"github.com/stretchr/testify/require"
@@ -356,7 +357,16 @@ func TestAdd_Derive(t *testing.T) {
356357
mockOut := bytes.NewBufferString("")
357358

358359
io := commands.NewTestIO()
359-
io.SetIn(strings.NewReader(dummyPass + "\n" + dummyPass + "\n" + mnemonic + "\n"))
360+
var sb strings.Builder
361+
sb.WriteString(mnemonic)
362+
sb.WriteString("\n")
363+
for range paths {
364+
sb.WriteString(dummyPass)
365+
sb.WriteString("\n")
366+
sb.WriteString(dummyPass)
367+
sb.WriteString("\n")
368+
}
369+
io.SetIn(strings.NewReader(sb.String()))
360370
io.SetOut(commands.WriteNopCloser(mockOut))
361371

362372
// Create the command
@@ -396,6 +406,76 @@ func TestAdd_Derive(t *testing.T) {
396406
}
397407
})
398408

409+
t.Run("derivation paths create keybase entries", func(t *testing.T) {
410+
t.Parallel()
411+
412+
var (
413+
kbHome = t.TempDir()
414+
mnemonic = generateTestMnemonic(t)
415+
paths = generateDerivationPaths(3)
416+
417+
baseOptions = BaseOptions{
418+
InsecurePasswordStdin: true,
419+
Home: kbHome,
420+
}
421+
422+
dummyPass = "dummy-pass"
423+
keyName = "example-key"
424+
)
425+
426+
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
427+
defer cancelFn()
428+
429+
io := commands.NewTestIO()
430+
var sb strings.Builder
431+
sb.WriteString(mnemonic)
432+
sb.WriteString("\n")
433+
for range paths {
434+
sb.WriteString(dummyPass)
435+
sb.WriteString("\n")
436+
sb.WriteString(dummyPass)
437+
sb.WriteString("\n")
438+
}
439+
io.SetIn(strings.NewReader(sb.String()))
440+
441+
// Create the command
442+
cmd := NewRootCmdWithBaseConfig(io, baseOptions)
443+
444+
args := []string{
445+
"add",
446+
"--insecure-password-stdin",
447+
"--home",
448+
kbHome,
449+
"--recover",
450+
keyName,
451+
}
452+
453+
for _, path := range paths {
454+
args = append(
455+
args, []string{
456+
"--derivation-path",
457+
path,
458+
}...,
459+
)
460+
}
461+
462+
require.NoError(t, cmd.ParseAndRun(ctx, args))
463+
464+
kb, err := keys.NewKeyBaseFromDir(kbHome)
465+
require.NoError(t, err)
466+
467+
for _, path := range paths {
468+
params, err := hd.NewParamsFromPath(path)
469+
require.NoError(t, err)
470+
471+
derivedName := deriveKeyName(keyName, params, len(paths))
472+
473+
has, err := kb.HasByName(derivedName)
474+
require.NoError(t, err)
475+
require.True(t, has)
476+
}
477+
})
478+
399479
t.Run("malformed derivation path", func(t *testing.T) {
400480
t.Parallel()
401481

0 commit comments

Comments
 (0)