iancoleman / bip39

A web tool for converting BIP39 mnemonic codes
https://iancoleman.io/bip39/
MIT License
3.41k stars 1.42k forks source link

Mismatching Ethereum Xpubs #559

Open glethuillier opened 2 years ago

glethuillier commented 2 years ago

From the following mnemonic, I have generated its corresponding Ethereum xpub using the current online version of the Mnemonic Code Converter (derivation path: m/44'/60'/0'/0/0).

Mnemonic: myth like bonus scare over problem client lizard pioneer submit female collect (for testing purposes, generated by Ganache)

Mnemonic Code Converter generates—using configuration: ETH - Ethereum, BIP44, default derivation path—the following data:

In parallel, I have generated the xpub using the following script:

import * as ethers from "ethers";
import * as bip39 from "bip39";
import HDKey from "hdkey";
import * as assert from "assert";

const mnemonic =
  "myth like bonus scare over problem client lizard pioneer submit female collect";

const derivationPath = "m/44'/60'/0'/0/0";

// Generate the xpub using two different libraries

const mnemonicToXpubMethod1 = (mnemonic: string, derivationPath: string) => {
  const HDNode = ethers.utils.HDNode;
  const masterNode = HDNode.fromMnemonic(mnemonic);
  const standardEthereum = masterNode.derivePath(derivationPath);

  const xpub = standardEthereum.neuter().extendedKey;

  console.log("xpub generated using the `ethers` library:", xpub);

  return xpub;
};

const mnemonicToXpubMethod2 = async (
  mnemonic: string,
  derivationPath: string
) => {
  const seed = await bip39.mnemonicToSeed(mnemonic);

  const master = HDKey.fromMasterSeed(seed);
  const child = master.derive(derivationPath);

  const xpub = child.publicExtendedKey;

  console.log("xpub generated using the `bip39` library: ", xpub);

  return xpub;
};

const run = async () => {
  console.log("Mnemonic:", mnemonic);
  console.log("Correct derivation path:", derivationPath);

  const xpubA = mnemonicToXpubMethod1(mnemonic, derivationPath);
  const xpubB = await mnemonicToXpubMethod2(mnemonic, derivationPath);

  // both xpubs should be strictly identical
  assert.strictEqual(xpubA, xpubB);
};

run();

The xpub generated by this script (using two different libraries) is: xpub6GkCbW4FDnz8k9zhroVhZefi9fXhFFuTmmcQqfQKPDBrQjtSu4pdLpQ1Tje1BAzUw6PbJ47MMtNZ4RYM42VzRNxwaYUFnm3R44Lejvh9nHE. It differs from the xpubs generated by Mnemonic Code Converter

Interestingly, your tool correctly derives the expected addresses from the mnemonic (0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1, etc.).

Do you perhaps know why the xpubs generated by Mnemonic Code Converter and the one generated by my script differ?

Thank you in advance for your assistance.

petjal commented 2 years ago

I don't know anything about ethereum, but:

Does ethereum have test vectors, or do you have any other "knowns", that you can force into the different steps in your script?

Try adding a debug mode that prints out forced-ascii (like bash cat -A) or hashes or lengths or similar of every input and output of every command line. (Could be something as dumb as unexpected whitespace hiding in a bad copy/paste of a the mnemonic.)

Does the order of the "import" statements matter?

glethuillier commented 2 years ago

Thank you @petjal.

  1. I am not aware of reliable test vectors (that is: mnemonic → xpub, as far as Ethereum is concerned).
  2. The xpub generated by my script seems to be correct. Indeed, the expected address is derived from this xpub (using two different libraries). See the full source code below. Therefore, I was wondering if it may be an issue with the Mnemonic Code Converter (hence my initial question). In other words, I have reasons to believe that:

myth like bonus scare over problem client lizard pioneer submit female collectxpub6GkCbW4FDnz8k9zhroVhZefi9fXhFFuTmmcQqfQKPDBrQjtSu4pdLpQ1Tje1BAzUw6PbJ47MMtNZ4RYM42VzRNxwaYUFnm3R44Lejvh9nHE (xpub generated by the script but not by the Mnemonic Code Converter)0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1 (address derived by both script and Mnemonic Code Converter)

Source code of the script generating the xpub and the address from the mnemonic:

import * as ethers from "ethers";
import * as bip39 from "bip39";
import HDKey from "hdkey";
import { HDNode } from "@findeth/hdnode";
import Wallet from "ethereumjs-wallet";
import * as assert from "assert";

const mnemonic =
  "myth like bonus scare over problem client lizard pioneer submit female collect";

const derivationPath = "m/44'/60'/0'/0/0";

// Generate xpub using two different libraries

const mnemonicToXpubMethod1 = (mnemonic: string, derivationPath: string) => {
  const HDNode = ethers.utils.HDNode;
  const masterNode = HDNode.fromMnemonic(mnemonic);
  const standardEthereum = masterNode.derivePath(derivationPath);

  const xpub = standardEthereum.neuter().extendedKey;

  console.log("xpub generated using the `ethers` library:", xpub);

  return xpub;
};

const mnemonicToXpubMethod2 = async (
  mnemonic: string,
  derivationPath: string
) => {
  const seed = await bip39.mnemonicToSeed(mnemonic);

  const master = HDKey.fromMasterSeed(seed);
  const child = master.derive(derivationPath);

  const xpub = child.publicExtendedKey;

  console.log("xpub generated using the `bip39` library: ", xpub);

  return xpub;
};

// Derive address from xpub using two different libraries

const deriveAddressMethod1 = (xpub: string) => {
  const address = HDNode.fromExtendedKey(xpub).address;
  console.log(
    "Address derived using the `@findeth/hdnode` library:\t  ",
    address
  );

  return address;
};

const deriveAddressMethod2 = (xpub: string) => {
  const address = Wallet.fromExtendedPublicKey(xpub).getAddressString();
  console.log(
    "Address derived using the `ethereumjs-wallet` library:\t  ",
    address
  );

  return address;
};

const run = async () => {
  console.log("Mnemonic:", mnemonic);
  console.log("Derivation path:", derivationPath);

  const xpubA = mnemonicToXpubMethod1(mnemonic, derivationPath);
  const xpubB = await mnemonicToXpubMethod2(mnemonic, derivationPath);

  // both xpubs should be strictly identical
  assert.strictEqual(xpubA, xpubB);

  const addressA = deriveAddressMethod1(xpubA);
  const addressB = deriveAddressMethod2(xpubB);

  // both addresses should be identical (case insensitive)
  assert.notStrictEqual(addressA, addressB);
};

run();

Output:

Mnemonic: myth like bonus scare over problem client lizard pioneer submit female collect
Derivation path: m/44'/60'/0'/0/0
xpub generated using the `ethers` library: xpub6GkCbW4FDnz8k9zhroVhZefi9fXhFFuTmmcQqfQKPDBrQjtSu4pdLpQ1Tje1BAzUw6PbJ47MMtNZ4RYM42VzRNxwaYUFnm3R44Lejvh9nHE
xpub generated using the `bip39` library:  xpub6GkCbW4FDnz8k9zhroVhZefi9fXhFFuTmmcQqfQKPDBrQjtSu4pdLpQ1Tje1BAzUw6PbJ47MMtNZ4RYM42VzRNxwaYUFnm3R44Lejvh9nHE
Address derived using the `@findeth/hdnode` library:       0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
Address derived using the `ethereumjs-wallet` library:     0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1