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

HDNodeWallet derivePath not working properly #4551

Closed
Jouzep opened this issue Jan 18, 2024 · 8 comments
Closed

HDNodeWallet derivePath not working properly #4551

Jouzep opened this issue Jan 18, 2024 · 8 comments
Assignees
Labels
bug Verified to be an issue. fixed/complete This Bug is fixed or Enhancement is complete and published. v6 Issues regarding v6

Comments

@Jouzep
Copy link

Jouzep commented Jan 18, 2024

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

@Jouzep Jouzep added investigate Under investigation and may be a bug. v6 Issues regarding v6 labels Jan 18, 2024
@lorcannrauzduel
Copy link

same problem

@niZmosis
Copy link
Sponsor

niZmosis commented Feb 3, 2024

Appears the deviation paths have been different since 6.0.0. We may have to revert to 5.7.2 if you want to derive accounts with the BIP44. As you can see from these tests, the base account from wallet is correct, but if you derive an account they are not correct, and the base account doesn't match derived account 0.

Test with 5.7.2:
`const { ethers } = require("ethers")

function test() {
const seedPhrase = 'escape joke bright reform stem industry cool announce hurt survey blossom wrap'

const wallet = ethers.Wallet.fromMnemonic(seedPhrase)

const path1 = m/44'/60'/0'/0/${0}
const derivedWallet1 = ethers.Wallet.fromMnemonic(seedPhrase, path1)
const path2 = m/44'/60'/0'/0/${1}
const derivedWallet2 = ethers.Wallet.fromMnemonic(seedPhrase, path2)

const metaMaskAccount1 = '0x904c2b17cdf69198eed7004E6b4C99e4C1DdB930'
const metaMaskAccount2 = '0x8Cdd9312b3D5Aa52d9ADccE816F9cfB90363A76b'

console.log('Base Wallet Address:', wallet.address)
console.log('Metamask 1:', metaMaskAccount1)
console.log('Derived 1:', derivedWallet1.address)
console.log('Metamask 2:', metaMaskAccount2)
console.log('Derived 2:', derivedWallet2.address)
}

test()`

Results:
Base Wallet Address: 0x904c2b17cdf69198eed7004E6b4C99e4C1DdB930 Metamask 1: 0x904c2b17cdf69198eed7004E6b4C99e4C1DdB930 Derived 1: 0x904c2b17cdf69198eed7004E6b4C99e4C1DdB930 Metamask 2: 0x8Cdd9312b3D5Aa52d9ADccE816F9cfB90363A76b Derived 2: 0x8Cdd9312b3D5Aa52d9ADccE816F9cfB90363A76b

Test with >=6.0.0
`const seedPhrase =
'escape joke bright reform stem industry cool announce hurt survey blossom wrap'

const wallet = Wallet.fromPhrase(seedPhrase)
const path1 = m/44'/60'/0'/0/${0}
const derivedWallet1 = wallet.derivePath(path1)
const path2 = m/44'/60'/0'/0/${1}
const derivedWallet2 = wallet.derivePath(path2)

const metaMaskAccount1 = '0x904c2b17cdf69198eed7004E6b4C99e4C1DdB930'
const metaMaskAccount2 = '0x8Cdd9312b3D5Aa52d9ADccE816F9cfB90363A76b'

console.log('Base Wallet Address:', wallet.address)
console.log('Metamask 1:', metaMaskAccount1)
console.log('Derived 1:', derivedWallet1.address)
console.log('Metamask 2:', metaMaskAccount2)
console.log('Derived 2:', derivedWallet2.address)`

Results:
Base Wallet Address: 0x904c2b17cdf69198eed7004E6b4C99e4C1DdB930 Metamask 1: 0x904c2b17cdf69198eed7004E6b4C99e4C1DdB930 Derived 1: 0xD3e7f7496373597bd382f8231B5C267952804058 Metamask 2: 0x8Cdd9312b3D5Aa52d9ADccE816F9cfB90363A76b Derived 2: 0x6eB2Da796aee744087F0DF30aFec8501282dfF22

@niZmosis
Copy link
Sponsor

niZmosis commented Feb 4, 2024

Alright I found out what is causing it, and found a temp work around as well. So when you call derivePath, it will call that wallet instances "deriveChild" function. Once a wallet is instantiated, it will hold on to its deviation path with the class prop called "path". So when you call derivePath(), the class will go call deriveChild for each part of the path. What happens is it appends that class prop "path" when making the deviated path which isn't right and which is why it ends up like "m/44'/60'/0'/0/0/44'/60'/0'/0/1". This is why it works fine when first making the wallet as "path" is an empty string. It goes deeper and not sure the full intention of the code so I will leave it at that, as this is more than adding a line of code to fix. But for a work around, do not use "deviatePath()", use the static functions.

Here is where the problem is in hdwallet.ts

` /**

  • Return the child for %%index%%.
    */
    deriveChild(_index: Numeric): HDNodeWallet {
    const index = getNumber(_index, "index");
    assertArgument(index <= 0xffffffff, "invalid index", "index", index);
// Base path
let path = this.path; // This will already have 'm/44'/60'/0'/0/0' when you call derivePath()
if (path) {
  path += "/" + (index & ~HardenedBit);
  if (index & HardenedBit) { path += "'"; }
}

const { IR, IL } = ser_I(index, this.chainCode, this.publicKey, this.privateKey);
const ki = new SigningKey(toBeHex((toBigInt(IL) + BigInt(this.privateKey)) % N, 32));

return new HDNodeWallet(_guard, ki, this.fingerprint, hexlify(IR),
  path, index, this.depth + 1, this.mnemonic, this.provider);

}`

Workaround:

`const { ethers } = require("ethers")

function test() {
const seedPhrase =
'escape joke bright reform stem industry cool announce hurt survey blossom wrap'

const wallet = ethers.HDNodeWallet.fromPhrase(seedPhrase)
const mnemonic = wallet.mnemonic

const path1 = m/44'/60'/0'/0/${0}
const derivedWallet1 = ethers.HDNodeWallet.fromMnemonic(mnemonic, path1)
// const derivedWallet1 = ethers.HDNodeWallet.fromPhrase(seedPhrase, '', path1)
const path2 = m/44'/60'/0'/0/${1}
const derivedWallet2 = ethers.HDNodeWallet.fromPhrase(seedPhrase, '', path2)

const metaMaskAccount1 = '0x904c2b17cdf69198eed7004E6b4C99e4C1DdB930'
const metaMaskAccount2 = '0x8Cdd9312b3D5Aa52d9ADccE816F9cfB90363A76b'

console.log('Wallet Address:', wallet.address)
console.log('Metamask 1:', metaMaskAccount1)
console.log('Derived 1:', derivedWallet1.address)
console.log('Metamask 2:', metaMaskAccount2)
console.log('Derived 2:', derivedWallet2.address)
}

test()`

Results:

Wallet Address: 0x904c2b17cdf69198eed7004E6b4C99e4C1DdB930 Metamask 1: 0x904c2b17cdf69198eed7004E6b4C99e4C1DdB930 Derived 1: 0x904c2b17cdf69198eed7004E6b4C99e4C1DdB930 Metamask 2: 0x8Cdd9312b3D5Aa52d9ADccE816F9cfB90363A76b Derived 2: 0x8Cdd9312b3D5Aa52d9ADccE816F9cfB90363A76b

@Jouzep
Copy link
Author

Jouzep commented Feb 4, 2024

Hello thank you mate very good explanation

@Jouzep Jouzep closed this as completed Feb 4, 2024
@niZmosis
Copy link
Sponsor

We found a work around but the issue you brought up is still a problem if you can reopen it.

@ricmoo
Copy link
Member

ricmoo commented Feb 13, 2024

I want to keep this open to further investigate. I’ll close this again if it is deemed not an issue.

There are also two functions getAccountPath and getIndexedAccountPath, depending if you want to match Ledger or if you want to match MetaMask. Each wallet will have chosen their own standard, so it will take some testing to figure out which to use. I see the MetaMask path followed more often though, but you can also use something like Ledger Live; when you import a mnemonic, if checks the first few accounts on both the Ledger and the MetaMask paths, and if it finds MetaMask transactions, switched to MetaMask mode and tags them with a little icon.

@ricmoo ricmoo reopened this Feb 13, 2024
@ricmoo
Copy link
Member

ricmoo commented Feb 14, 2024

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));
}

@ricmoo
Copy link
Member

ricmoo commented Feb 14, 2024

These changes were published in v6.11.1.

Thanks! :)

@ricmoo ricmoo closed this as completed Feb 14, 2024
@ricmoo ricmoo added bug Verified to be an issue. fixed/complete This Bug is fixed or Enhancement is complete and published. and removed investigate Under investigation and may be a bug. labels Feb 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Verified to be an issue. fixed/complete This Bug is fixed or Enhancement is complete and published. v6 Issues regarding v6
Projects
None yet
Development

No branches or pull requests

4 participants