Repository: ethereum/eth-tester (language: python)

Issue:
Support HDPath for Mnemonic
* Version: 0.9.0b1
* Python: 3.10
* OS: linux


### What was wrong?

Currently when initializing the PyEVMBackend from a mnemonic, there is no option pass in an HDPath, which then uses the default HDPath which is incompatible with some other types of providers (such as Anvil)

https://github.com/ethereum/eth-tester/blob/master/eth_tester/backends/pyevm/main.py#L138

### How can it be fixed?

Allow passing in an HDPath to override the default


Discussion / hints:

Fix patch:
diff --git a/newsfragments/259.feature.rst b/newsfragments/259.feature.rst
new file mode 100644
index 00000000..e12fed7b
--- /dev/null
+++ b/newsfragments/259.feature.rst
@@ -0,0 +1,1 @@
+Add mnemonic hd_path parameter to create custom accounts in pyevm backend.
\ No newline at end of file


Test patch:
diff --git a/eth_tester/backends/pyevm/main.py b/eth_tester/backends/pyevm/main.py
index e32f32b7..26e0c1e5 100644
--- a/eth_tester/backends/pyevm/main.py
+++ b/eth_tester/backends/pyevm/main.py
@@ -131,13 +131,19 @@ def get_default_account_keys(quantity=None):
 
 
 @to_tuple
-def get_account_keys_from_mnemonic(mnemonic, quantity=None):
+def get_account_keys_from_mnemonic(mnemonic, quantity=None, hd_path=None):
     keys = KeyAPI()
     seed = seed_from_mnemonic(mnemonic, "")
     quantity = quantity or 10
+
+    if hd_path is None:
+        # default HD path
+        hd_path = "m/44'/60'/0'"
+
     for i in range(0, quantity):
-        hd_path = HDPath(f"m/44'/60'/0'/{i}")
-        private_key = keys.PrivateKey(hd_path.derive(seed))
+        # create unique HDPath to derive the private key for each account
+        key = HDPath(f"{hd_path}/{i}").derive(seed)
+        private_key = keys.PrivateKey(key)
         yield private_key
 
 
@@ -183,6 +189,7 @@ def setup_tester_chain(
     num_accounts=None,
     vm_configuration=None,
     mnemonic=None,
+    hd_path=None,
     genesis_is_post_merge=True,
 ):
     from eth.chains.base import MiningChain
@@ -231,7 +238,9 @@ def get_transaction_builder(self):
         num_accounts = len(genesis_state)
 
     if mnemonic:
-        account_keys = get_account_keys_from_mnemonic(mnemonic, quantity=num_accounts)
+        account_keys = get_account_keys_from_mnemonic(
+            mnemonic, quantity=num_accounts, hd_path=hd_path
+        )
     else:
         account_keys = get_default_account_keys(quantity=num_accounts)
 
@@ -321,6 +330,7 @@ def __init__(
         genesis_state=None,
         vm_configuration=None,
         mnemonic=None,
+        hd_path=None,
     ):
         """
         :param genesis_parameters: A dict of chain parameters for overriding default
@@ -342,7 +352,12 @@ def __init__(
         self.account_keys = None  # set below
         accounts = len(genesis_state) if genesis_state else None
         self.reset_to_genesis(
-            genesis_parameters, genesis_state, accounts, vm_configuration, mnemonic
+            genesis_parameters,
+            genesis_state,
+            accounts,
+            vm_configuration,
+            mnemonic,
+            hd_path,
         )
 
     @classmethod
@@ -353,11 +368,18 @@ def from_mnemonic(
         num_accounts=None,
         genesis_parameters=None,
         vm_configuration=None,
+        hd_path=None,
     ):
+        """
+        Create a genesis state pre-populated with accounts. A number of accounts can be
+        initialized with a mnemonic phrase and heirarchical deterministic path. If no
+        overrides are provided, a default set of accounts will be used.
+        """
         genesis_state = PyEVMBackend.generate_genesis_state(
             mnemonic=mnemonic,
             overrides=genesis_state_overrides or {},
             num_accounts=num_accounts,
+            hd_path=hd_path,
         )
 
         return cls(
@@ -365,6 +387,7 @@ def from_mnemonic(
             genesis_state=genesis_state,
             vm_configuration=vm_configuration,
             mnemonic=mnemonic,
+            hd_path=hd_path,
         )
 
     #
@@ -380,16 +403,23 @@ def _generate_genesis_params(overrides=None):
         return get_default_genesis_params(overrides=overrides)
 
     @classmethod
-    def generate_genesis_state(cls, overrides=None, num_accounts=None, mnemonic=None):
+    def generate_genesis_state(
+        cls, overrides=None, num_accounts=None, mnemonic=None, hd_path=None
+    ):
         return cls._generate_genesis_state(
-            overrides=overrides, num_accounts=num_accounts, mnemonic=mnemonic
+            overrides=overrides,
+            num_accounts=num_accounts,
+            mnemonic=mnemonic,
+            hd_path=hd_path,
         )
 
     @staticmethod
-    def _generate_genesis_state(overrides=None, num_accounts=None, mnemonic=None):
+    def _generate_genesis_state(
+        overrides=None, num_accounts=None, mnemonic=None, hd_path=None
+    ):
         if mnemonic:
             account_keys = get_account_keys_from_mnemonic(
-                mnemonic, quantity=num_accounts
+                mnemonic, quantity=num_accounts, hd_path=hd_path
             )
         else:
             account_keys = get_default_account_keys(quantity=num_accounts)
@@ -405,6 +435,7 @@ def reset_to_genesis(
         num_accounts=None,
         vm_configuration=None,
         mnemonic=None,
+        hd_path=None,
     ):
         self.account_keys, self.chain = setup_tester_chain(
             genesis_params,
@@ -412,6 +443,7 @@ def reset_to_genesis(
             num_accounts,
             vm_configuration,
             mnemonic,
+            hd_path,
         )
 
     #
diff --git a/tests/backends/test_pyevm.py b/tests/backends/test_pyevm.py
index 0140740a..6ce3ff5f 100644
--- a/tests/backends/test_pyevm.py
+++ b/tests/backends/test_pyevm.py
@@ -46,6 +46,15 @@ def eth_tester():
     return EthereumTester(backend=backend)
 
 
+@pytest.fixture
+def accounts_from_mnemonic():
+    return [
+        "0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C",
+        "0xc89D42189f0450C2b2c3c61f58Ec5d628176A1E7",
+        "0x318b469BBa396AEc2C60342F9441be36A1945174",
+    ]
+
+
 def test_custom_virtual_machines():
     if not is_supported_pyevm_version_available():
         pytest.skip("PyEVM is not available")
@@ -231,7 +240,7 @@ def test_override_genesis_state(self):
             balance = tester.get_balance(account=account)
             assert balance == state_overrides["balance"]
 
-    def test_from_mnemonic(self):
+    def test_from_mnemonic(self, accounts_from_mnemonic):
         # Initialize PyEVM backend using MNEMONIC, num_accounts,
         # and state overrides (balance)
         num_accounts = 3
@@ -242,25 +251,46 @@ def test_from_mnemonic(self):
             genesis_state_overrides={"balance": balance},
         )
 
-        # Each of these accounts stems from the MNEMONIC
-        expected_accounts = [
-            "0x1e59ce931b4cfea3fe4b875411e280e173cb7a9c",
-            "0xc89d42189f0450c2b2c3c61f58ec5d628176a1e7",
-            "0x318b469bba396aec2c60342f9441be36a1945174",
-        ]
-
         # Test integration with EthereumTester
         tester = EthereumTester(backend=pyevm_backend)
 
         actual_accounts = tester.get_accounts()
         assert len(actual_accounts) == num_accounts
 
+        assert all(acct in accounts_from_mnemonic for acct in actual_accounts)
+
         for i in range(0, num_accounts):
             actual = actual_accounts[i]
-            expected = expected_accounts[i]
+            expected = accounts_from_mnemonic[i]
             assert actual.lower() == expected.lower()
             assert tester.get_balance(account=actual) == balance
 
+    def test_from_mnemonic_override_hd_path(self, accounts_from_mnemonic):
+        # Initialize PyEVM backend using MNEMONIC, num_accounts,
+        # and custom hd_path
+        num_accounts = 3
+        pyevm_backend = PyEVMBackend.from_mnemonic(
+            MNEMONIC,
+            num_accounts=num_accounts,
+            hd_path="m/44'/60'/7'",
+        )
+
+        # Each of these accounts stems from the MNEMONIC, but with a different hd_path
+        expected_accounts = [
+            "0x9aEFA413550e6Ae8690642994310d13dDA248b6b",
+            "0xcBA8AFA62949343128FE341C3C7F6b119dF78249",
+            "0x2C7DdecbF4555dd2220eF92e21B2912342655845",
+        ]
+
+        # Test integration with EthereumTester
+        tester = EthereumTester(backend=pyevm_backend)
+
+        actual_accounts = tester.get_accounts()
+        assert len(actual_accounts) == num_accounts
+
+        assert not any(acct in accounts_from_mnemonic for acct in actual_accounts)
+        assert all(acct in expected_accounts for acct in actual_accounts)
+
     def test_generate_custom_genesis_parameters(self):
 
         # Establish parameter overrides, for example a custom genesis gas limit