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

feat(multi-srp): add id and typeIndex to hd keyrings #5071

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

PatrykLucka
Copy link

Explanation

References

Changelog

@metamask/package-a

  • : Your change here
  • : Your change here

@metamask/package-b

  • : Your change here
  • : Your change here

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've highlighted breaking changes using the "BREAKING" category above as appropriate
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes

@PatrykLucka PatrykLucka self-assigned this Dec 16, 2024
@PatrykLucka
Copy link
Author

@metamaskbot publish-preview

Copy link
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/accounts-controller": "20.0.2-preview-579728ba",
  "@metamask-previews/address-book-controller": "6.0.2-preview-579728ba",
  "@metamask-previews/announcement-controller": "7.0.2-preview-579728ba",
  "@metamask-previews/approval-controller": "7.1.1-preview-579728ba",
  "@metamask-previews/assets-controllers": "45.1.2-preview-579728ba",
  "@metamask-previews/base-controller": "7.0.2-preview-579728ba",
  "@metamask-previews/build-utils": "3.0.2-preview-579728ba",
  "@metamask-previews/chain-controller": "0.2.2-preview-579728ba",
  "@metamask-previews/composable-controller": "10.0.0-preview-579728ba",
  "@metamask-previews/controller-utils": "11.4.4-preview-579728ba",
  "@metamask-previews/ens-controller": "15.0.1-preview-579728ba",
  "@metamask-previews/eth-json-rpc-provider": "4.1.6-preview-579728ba",
  "@metamask-previews/gas-fee-controller": "22.0.2-preview-579728ba",
  "@metamask-previews/json-rpc-engine": "10.0.1-preview-579728ba",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.5-preview-579728ba",
  "@metamask-previews/keyring-controller": "19.0.2-preview-579728ba",
  "@metamask-previews/logging-controller": "6.0.3-preview-579728ba",
  "@metamask-previews/message-manager": "11.0.3-preview-579728ba",
  "@metamask-previews/multichain": "1.1.2-preview-579728ba",
  "@metamask-previews/name-controller": "8.0.2-preview-579728ba",
  "@metamask-previews/network-controller": "22.1.1-preview-579728ba",
  "@metamask-previews/notification-services-controller": "0.15.0-preview-579728ba",
  "@metamask-previews/permission-controller": "11.0.4-preview-579728ba",
  "@metamask-previews/permission-log-controller": "3.0.2-preview-579728ba",
  "@metamask-previews/phishing-controller": "12.3.1-preview-579728ba",
  "@metamask-previews/polling-controller": "12.0.2-preview-579728ba",
  "@metamask-previews/preferences-controller": "15.0.1-preview-579728ba",
  "@metamask-previews/profile-sync-controller": "3.1.1-preview-579728ba",
  "@metamask-previews/queued-request-controller": "8.0.2-preview-579728ba",
  "@metamask-previews/rate-limit-controller": "6.0.2-preview-579728ba",
  "@metamask-previews/remote-feature-flag-controller": "1.2.0-preview-579728ba",
  "@metamask-previews/selected-network-controller": "20.0.2-preview-579728ba",
  "@metamask-previews/signature-controller": "23.1.0-preview-579728ba",
  "@metamask-previews/transaction-controller": "42.0.0-preview-579728ba",
  "@metamask-previews/user-operation-controller": "21.0.0-preview-579728ba"
}

Copy link
Member

@mikesposito mikesposito left a comment

Choose a reason for hiding this comment

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

I feel like these changes should pass through the EthKeyring type before being handled here with casts as they were exceptions to the rule.

I do think that we should apply the changes to all keyrings, and to the type itself - but it would also be nice to see how we intend to use this changes on the clients to assess the actual necessity of them

Comment on lines +948 to +965
/**
* Returns the keyring with the given id.
*
* @param id - The id of the keyring to return.
* @returns The keyring with the given id.
*/
getKeyringById(id: string): unknown {
const keyring = this.#keyrings.find(
(item) =>
(item as EthKeyring<Json> & { opts: { id: string } }).opts.id === id,
);
if (!keyring) {
throw new Error(KeyringControllerError.KeyringNotFound);
}

return keyring;
}

Copy link
Member

Choose a reason for hiding this comment

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

This method is unsafe as clients (consumers) should not be able to access a keyring instance directly. That's the reason why getKeyringsBytype and getKeyringForAccount have been deprecated.

One suggestion could be to make this a # method so that consumers are forced to pass through withKeyring

*/
export type KeyringObject = {
accounts: string[];
type: string;
typeIndex: number;
Copy link
Member

Choose a reason for hiding this comment

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

I think that storing the this typeIndex value would make things unnecessarily complicated. Can't we just use the ID to get its index programmatically?

Given that we may have multiple HDKeyrings that are restored with the same order every time the wallet is unlocked (the keyring instances are recreated), if one in the middle of the array would get removed we would have to reassign typeIndex to all other keyrings of the same type, which would make the cost / benefit of this unclear to me

Comment on lines +550 to +551
typeIndex: opts?.typeIndex,
id: opts?.id,
Copy link
Member

Choose a reason for hiding this comment

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

This may be undefined but the types changed in the rest of the controller suggest that these are always defined. This could create unexpected scenarios

Comment on lines +681 to +694
async addNewAccount(
accountCount?: number,
keyringId?: string,
): Promise<string> {
return this.#persistOrRollback(async () => {
const primaryKeyring = this.getKeyringsByType('HD Key Tree')[0] as
| EthKeyring<Json>
| undefined;
if (!primaryKeyring) {
let selectedKeyring: EthKeyring<Json> | undefined;
if (keyringId) {
selectedKeyring = this.getKeyringById(keyringId) as EthKeyring<Json>;
} else {
selectedKeyring = this.getKeyringsByType(
'HD Key Tree',
)[0] as EthKeyring<Json>;
}
if (!selectedKeyring) {
Copy link
Member

Choose a reason for hiding this comment

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

Calling this.withKeyring internally here would avoid extra complexity and would delegate to withKeyring the following responsibilities:

  1. Keyring selection
  2. Safeguarding the operation with controller locks
  3. Handle the case where the keyring is inexistent

@@ -758,6 +783,15 @@ export class KeyringController extends BaseController<
});
}

async createKeyringFromMnemonic(mnemonic: string): Promise<string> {
Copy link
Member

Choose a reason for hiding this comment

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

Mnemonics are usually handled as UInt8Array for security reasons. Can we do the same here?

Comment on lines +2247 to +2256
if (type === KeyringTypes.hd) {
await keyring.deserialize({
...(data ?? {}),
typeIndex: lastIndexOfType + 1,
id: ulid(),
});
} else {
// @ts-expect-error Enforce data type after updating clients
await keyring.deserialize(data);
}
Copy link
Member

Choose a reason for hiding this comment

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

If in methods like addNewAccount we allow a consumer to reference any kind of keyring by its ID, why we do this for the HD keyring only here?

@@ -758,6 +783,15 @@ export class KeyringController extends BaseController<
});
}

async createKeyringFromMnemonic(mnemonic: string): Promise<string> {
Copy link
Member

Choose a reason for hiding this comment

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

Why do clients need this function if they can use addNewKeyring? It would be nice to avoid new keyring-specific methods on KeyringController since we are already trying to get rid of the ones that are already in place (e.g. QR-related ones)

Comment on lines +681 to +684
async addNewAccount(
accountCount?: number,
keyringId?: string,
): Promise<string> {
Copy link
Member

@mikesposito mikesposito Dec 18, 2024

Choose a reason for hiding this comment

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

There seems to be no restriction now on what kind of Keyring this operation can be executed on. However, this would not make sense for almost all keyrings we have besides the HD one.

What are the benefits of using an ID, instead of a type and an index?

Comment on lines +893 to +899
async getAccounts(keyringId?: string): Promise<string[]> {
return this.state.keyrings
.filter((keyring) => (keyringId ? keyring.id === keyringId : true))
.reduce<string[]>(
(accounts, keyring) => accounts.concat(keyring.accounts),
[],
);
Copy link
Member

Choose a reason for hiding this comment

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

Clients could autonomously execute this operation by filtering KeyringController's state already

@mikesposito
Copy link
Member

mikesposito commented Dec 18, 2024

it would also be nice to see how we intend to use this changes on the clients to assess the actual necessity of them

I did take a look at the client's PRs, but I'm still not convinced that we strictly need these changes to allow clients to operate on multiple HD keyrings. Most of the usages that I spotted were to give clients access to specific keyring instances directly, which is something that we want to avoid since it would bypass KeyringController safeguards.

I may have missed some, but addNewAccount, withKeyring, getAccounts and the fact that keyrings are always restored in the same order could have allowed the client to do what we need already via type and index which we already have.

I was mainly worried about the removal operation of one of the HD keyrings (e.g. one in the middle) which would change the order and indexes of keyrings, but I don't see this feature implemented, and even if it was, I believe the clients would still be able to reference every single keyring in KeyringController and execute operations on them.

The only benefit of this that I see is that we would be able to assign pet names to HD Keyrings which could then be shown to users to identify them, but that's something different than an ID used internally to route operations

Comment on lines +713 to 714
const [addedAccountAddress] = await selectedKeyring.addAccounts(1);
await this.verifySeedPhrase();
Copy link
Member

Choose a reason for hiding this comment

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

this verifySeedPhrase call will only verify the first HD keyring, while now we probably want to verify them all

Comment on lines +1541 to +1542
} else if ('id' in selector) {
keyring = this.getKeyringById(selector.id) as SelectedKeyring;
Copy link
Member

Choose a reason for hiding this comment

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

If a client passes a selector like

{
  id: undefined
}

this function would execute the operation with the first keyring found with no id

Comment on lines +77 to +94
/**
* Strip the `id` property from each keyring in the state as it is generated
* by the `ulid()` function and is not deterministic.
*
* @param state - The state to strip the `id` property from.
* @returns The state with the `id` property stripped from each keyring.
*/
function stripKeyringIds(state: KeyringControllerState): Omit<
KeyringControllerState,
'keyrings'
> & {
keyrings: Omit<(typeof state.keyrings)[number], 'id'>[];
} {
return {
...state,
keyrings: state.keyrings.map(({ id, ...rest }) => rest),
};
}
Copy link
Member

Choose a reason for hiding this comment

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

We could, alternatively, mock the ulid package

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.

2 participants