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

refactor: change how addresses are derived #133

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open

Conversation

meeh0w
Copy link
Member

@meeh0w meeh0w commented Feb 4, 2025

Description

Changes

  • Includes a schema migration for the secrets:

    • Instead of storing xpub and xpubXP, all primary wallets now store a collection of publicKeys: AddressPublicKey[].
      • Each AddressPublicKey includes the key itself (hex-encoded) and information on the derivation path that was used when it was derived (derivationPath and curve (either secp256k1 or ed25519)).
      • Some AddressPublicKeys can also include a btcWalletPolicyDetails (which did not go through any changes).
    • Some wallet types (i.e. Ledger and Keystone) will also store extendedPublicKeys collection.
  • Addresses are now computed using VM modules. To compute the addresses, VM Modules will build the required derivation paths and request public keys to be derived (so they can use them to compute the addresses):

  • Includes the refactoring needed to support the new schema + updated tests.

  • In the migrations code, I added a new type of migration - one that requires information from a different storage key.

    • This was needed because for some wallet types (i.e. Seed Phrase / Ledger / Keystone), I needed to know how many public keys need to be derived in order to keep the same amount of accounts after migration completes. Therefore I had to query the accounts storage space as a dependency of the migration.

Rationale

The previous approach was not very generic -- it relied on passing multiple parameters to VM Modules, many of which were ignored by some modules and required by others. For example:

  • EvmModule only relied on the extended public key (passed in as xpub)
  • BitcoinModule relied on the extended public key (xpub) and network.isTestnet.
  • AvalancheModule relied on the X/P extended public key (xpubXP) and multiple properties of the Network (isTestnet and isDevnet, which boiled down to the network's HRP)

Some implementations even relied on WalletType -- an implementation detail of the wallet that the modules should not care about at all.
Others did not care about most of the parameters they received. This made the interface for getAddress() method quite complex/bloated.

Notes

  • The existing PubKeyType { evm: string; xp?: string; ed25519?: string } is still alive in the code.
    • It is still used in the onboarding frontend, which I did not want to change at the moment as to not bloat the PR even further
    • It is also used by avalanche_getAccountPubKey, which I wanted to keep backwards-compatible (but we should update the response for that somewhat soon, as with the addition of ed25519 it became a bit confusing).

Testing

  • Full regression of the onboarding flow
  • Full regression of the Import with <XYZ> flows
  • Full regression of the Add new address flows
  • Full regression of the Export private key flow
  • Tests of the migration itself (I believe storage snapshots used by the QA team will be super helpful with that -- we just need to make sure we have snapshots for each wallet type, possibly even for combinations of those).

For all of the above, we need to make sure the wallet's state remains the same:

  • no accounts are missing
  • account addresses stay the same (with the exception of HVM addresses - those will change, as it's now using 9000 coin type instead of 60 (same as X/P chains, as opposed to EVM).
  • for Ledger, remember to check both BIP44 and Ledger Live derivation paths

Screenshots:

No visual changes

Checklist for the author

  • I've covered new/modified business logic with Jest test cases.
  • ⏳ I've tested the changes myself before sending it to code review and QA.
    • I'm still testing scenarios that come to my mind.

.nvmrc Outdated
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC it's the slip10 package that we use for deriving keys with ed25519 curve that needed this version of node (or newer).

Comment on lines +33 to +34
export const EVM_BASE_DERIVATION_PATH = "m/44'/60'/0'";
export const AVALANCHE_BASE_DERIVATION_PATH = "m/44'/9000'/0'";
Copy link
Member Author

@meeh0w meeh0w Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could probably use better names (those are paths for the legacy xpub and xpubXP).


case 'ed25519': {
const hdKey = slip10.fromMasterSeed(seed);
key = hex.encode(hdKey.derive(derivationPath).publicKey);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use .publicKeyRaw here -- publicKey is 0-prefixed (33 bytes total), we need the raw 32 bytes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d218327

csabbee
csabbee previously approved these changes Feb 13, 2025
Copy link
Contributor

@csabbee csabbee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kudos for this, really well done!
I'm not saying I did an all around testing but, I did played around with the dev build and didn't run into anything 🔥
-

Copy link
Contributor

@gergelylovas gergelylovas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice work. Especially like the documentation and the new clean secret storage model.

Comment on lines +79 to +94
// it('should throw error if derivation path is missing', async () => {
// moduleManager.modules = [
// {
// buildDerivationPath: jest.fn().mockResolvedValue({
// EVM: 'path1',
// }),
// } as unknown as Module,
// ];

// await expect(
// addressResolver.getDerivationPathsByVM(0, DerivationPath.BIP44, [
// 'AVM',
// ]),
// ).rejects.toThrow(SecretsError.DerivationPathMissing);
// });
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove

Comment on lines +26 to +28
init(moduleManager: ModuleManager) {
this.#moduleManager = moduleManager;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason why the module manager not injected like the Network and Secrets service?

Comment on lines +54 to +57
get modules(): Module[] {
return this.#modules;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use some more specific getter which doesn't assume all of the modules are always loaded?

@@ -1586,7 +1678,10 @@ describe('background/services/wallet/WalletService.ts', () => {

it('returns the correct list of addresses', async () => {
secretsService.getPrimaryAccountSecrets.mockResolvedValueOnce({
xpubXP,
extendedPublicKeys: [
// buildExtendedPublicKey('xpub', EVM_BASE_DERIVATION_PATH),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove if not needed

@@ -30,6 +32,8 @@ export class BackgroundRuntime {
this.lockService.activate();
this.onboardingService.activate();
this.moduleManager.activate();

this.addressResolver.init(this.moduleManager);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to define the addressResolver here?
Can it just inject the moduleManager via DI like it does with it's other dependencies

xpub: 'xpub',
xpubXP: 'xpubXP',
secretType: 'ledger',
btcWalletPolicyDetails: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have test scenarios where the optional properties are missing?


export default {
previousSchema: legacySecretsSchema,
dependencyKeys: [ACCOUNTS_STORAGE_KEY],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we ensure version consistency here?
Since the ACCOUNTS_STORAGE_KEY's migrations are running earlier than the wallet migrations (for now) it is possible that a future migration on the account storage will break this V5 migration. We should make sure the key's migration order stays the same.
Maybe we should use the types from the account service? Then at least a change in the types would trigger a TS error in here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants