ethers.js: HDNodeWallet derivePath not working properly

Ethers Version

^6.9.2

Search Terms

HDNodeWallet, DerivePath

Describe the Problem

I am trying to derivePath from an HDNodeWallet, but the provided path is not the same path when the wallet is generated for example i am trying to generate a wallet using Mnemonic and a path input path=“m/44’/60’/0’/0/1” output path=“m/44’/60’/0’/0/1/44’/60’/0’/0/1”

Maybe i have a bad understanding of HDWallet but in my mind they should be the same path

Code Snippet

const ethers = require("ethers");
const path = "m/44'/60'/0'/0/1";

const phrase = "word word word word word word word word word word word word";

const mnemonic = ethers.Mnemonic.fromPhrase(phrase);
const wallet = ethers.HDNodeWallet.fromMnemonic(mnemonic, path);

console.log(wallet.path);
// output: m/44'/60'/0'/0/1
const wallet1 = wallet.derivePath(path);
console.log(wallet1.path);
// output: m/44'/60'/0'/0/1/44'/60'/0'/0/1

Contract ABI

No response

Errors

No response

Environment

node.js (v12 or newer)

Environment (Other)

No response

About this issue

  • Original URL
  • State: closed
  • Created 5 months ago
  • Comments: 20 (5 by maintainers)

Commits related to this issue

Most upvoted comments

So, the above code should have thrown an error. I’m adding that now: the "m/" part of the path asserts that the depth of the node is 0, i.e. that you are computing the child from the “master” or root node.

I’m adding a constraint that it ensures this is the case, so the code in the OP would throw.

As an aside, for those that wish to compute a large number of child nodes, this should be about 5 times faster, as it keeps a reference to an intermediate node, so those calculations do not need to be replicated:

const wallet = ethers.HDNodeWallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0");
const wallet1 = wallet.derivePath("0");
console.log(wallet1);
const wallet2 = wallet.derivePath("1");
console.log(wallet2);

// Or in a for loop:
for (let i = 0; i < 10; i++) {
  console.log(wallet.deriveChild(i));
}

@niZmosis It already exists, but is called .deriveChild, which accepts a number. 😃

@martines3000 Working previously was a bug. When you begin a path with m/ you are indicating you want to enforce the current not is the master node. Previously it was ignored, which is bad. Much of the time the higher nodes are not actually present in memory (a feature baked into the specification), so simply “jumping” to the master node isn’t possible. I could consider adding a feature in the future that would allow crawling back up to it, if enough state is in memory to reconstruct it, but I’d have to think about that more. To get the old address using the buggy version, you can simply leave the m/ off the second path to descend into the HD structure the same way. You should move those funds though, as they won’t be accessible using standard tools like ledger or MetaMask.

If you need any help with this, let me know. 😃

@Sean329 No that is not the intended behaviour. This problem should be corrected in the above change, which is to throw if an attempt is made to declare the current node is the root (i.e. master) node when it isn’t. Can you provide sample code that demonstrates the issue you are having?

@ricmoo Sure, I’m using v6.11.1 and running the code below, and plz look at the log:

const ethers = require("ethers");
const path0 = "44'/60'/0'/0/0";

const phrase = "word word word word word word word word word word word word";

const mnemonic = ethers.Mnemonic.fromPhrase(phrase);
const wallet = ethers.HDNodeWallet.fromMnemonic(mnemonic, path0);

console.log(wallet.path);  // output: m/44'/60'/0'/0/0

const path1 = "44'/60'/0'/0/1";
const wallet1 = wallet.derivePath(path1);

console.log(wallet1.path); // output: m/44'/60'/0'/0/0/44'/60'/0'/0/1

I expect the wallet1.path to be “m/44’/60’/0’/0/1” instead of being “m/44’/60’/0’/0/0/44’/60’/0’/0/1”. Am I misunderstanding some concepts in here? Thanks.

@Sean329 @martines3000

Here would be your updated code.

` const basePath = “44’/60’/0’/0”; // or “m/44’/60’/0’/0”

const phrase = “word word word word word word word word word word word word” const mnemonic = ethers.Mnemonic.fromPhrase(phrase); const baseWallet = ethers.HDNodeWallet.fromMnemonic(mnemonic, basePath);

const wallet1 = baseWallet.derivePath(‘0’); console.log(wallet1.path);

const wallet2 = baseWallet.derivePath(‘1’); console.log(wallet2.path); `

Notice our basePath leaves off the index, and then when deriving you use what ever index you’d like. Don’t use the baseWallet directly as the path isn’t complete. If you do provide a complete path when making the wallet, it will be valid. But if you go to derive a wallet from it, you will run into the path concatenation problem I showed earlier in the thread.

@ricmoo What do you think about renaming derivePath to deriveIndex and then instead of a string we pass it a number?

EDIT: ricmoo pointed out there is the deriveChild which takes in a number.

` const basePath = “44’/60’/0’/0”; // or “m/44’/60’/0’/0”

const phrase = “word word word word word word word word word word word word” const mnemonic = ethers.Mnemonic.fromPhrase(phrase); const baseWallet = ethers.HDNodeWallet.fromMnemonic(mnemonic, basePath);

const walletViaPath = baseWallet.derivePath(‘0’); console.log(walletViaPath.path);

const walletViaChild = baseWallet.deriveChild(0); console.log(walletViaChild.path); `

Thanks for the fast reply. Yes this indeed solved my issue.

I also double checked that I get the same addresses (as with the old approach) with a simple script and it looks good. 💯

EDIT: I didn’t test it correctly. If I try it with the following code, I get a different address.

const baseWalletOld =
  HDNodeWallet.fromMnemonic(mnemonic).derivePath(`m/44/1236/1/0/0`);

const baseWalletNew = HDNodeWallet.fromMnemonic(
  mnemonic,
  `m/44/1236/1/0`
).derivePath('0');

console.log(`Old: ${baseWalletOld.address}`);
console.log(`New: ${baseWalletNew.address}`);

Your baseWalletOld isn’t correct. When not providing the path to the fromMnemonic it will use the default path which is “export const defaultPath: string = “m/44’/60’/0’/0/0”;” at which point the base path of the wallet object is now to long to be able to use the derivePath. Because of that default, your “.derivePath(m/44/1236/1/0/0)” is now going to concatenate to the defaults. So you must provide the base path like before without the index, then use the derivePath with the index you want, not the full path. Your baseWalletNew is the correct way to use it.

@Sean329 No that is not the intended behaviour.

This problem should be corrected in the above change, which is to throw if an attempt is made to declare the current node is the root (i.e. master) node when it isn’t. Can you provide sample code that demonstrates the issue you are having?

@ricmoo Sure, I’m using v6.11.1 and running the code below, and plz look at the log:

const ethers = require("ethers");
const path0 = "44'/60'/0'/0/0";

const phrase = "word word word word word word word word word word word word";

const mnemonic = ethers.Mnemonic.fromPhrase(phrase);
const wallet = ethers.HDNodeWallet.fromMnemonic(mnemonic, path0);

console.log(wallet.path);  // output: m/44'/60'/0'/0/0

const path1 = "44'/60'/0'/0/1";
const wallet1 = wallet.derivePath(path1);

console.log(wallet1.path); // output: m/44'/60'/0'/0/0/44'/60'/0'/0/1

I expect the wallet1.path to be “m/44’/60’/0’/0/1” instead of being “m/44’/60’/0’/0/0/44’/60’/0’/0/1”. Am I misunderstanding some concepts in here? Thanks.