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

Wrapped Keys #513

Conversation

MaximusHaximus
Copy link
Contributor

@MaximusHaximus MaximusHaximus commented Jun 26, 2024

Description

  • Updates wrapped keys to support multiple networks; Production for Habanero (and Datil in the future), TestNetworks for Cayenne and Manzano
  • Adds new keyType and publicKey keys to the persisted metadata, and requires they are provided when consumers call the methods that require them
  • Organizes types, constants, and logic by domain -- lit-actions-client contains all code specific to interacting with the lit actions, service-client contains all of the same for interactions with the wrapped keys backend, and api contains definitions of all top-level API methods that will be exposed to the consumer of the package
  • Explicitly exporting types and methods from the top-level index.ts rather than blanket exporting everything in the package with * exports. The goal here is to facilitate us being able to modify the LIT action APIs without making breaking changes to the top-level API methods that consumers are using as we need to.
  • Added a bunch of JSDocs and explicit types for parameters for API-exposed methods and their return values
  • Removed a lot of duplicated JSDoc comments by using @typedef and @extends
  • Removed the customXXXX methods, in favour of us instead add a basic documentation blurb to the JSDocs for prospective integrators that shows them how easy it is to get the private key metadata from the wrapped keys service, and then submit it to any custom LIT action, using the LitNodeClient instance that they already have in scope. The custom behaviour in the SDK exposes internals of LIT actions to our consumer API, and would cause us to get tickets against the wrapped-keys SDK that are actually issues with custom LIT Action implementations on top of making the API worse for consumers of the package who want to use 'supported' networks.

e.g.:

import { getPrivateKeyMetadata } from '@lit-protocol/wrapped-keys'

import { litNodeClient } from '@lit-protocol/lit-node-client'

await litNodeClient.connect({litNetwork: 'cayenne'});
const sessionSigs = await litNodeClient.getSessionSigs(...);
const keyMetadata = await getPrivateKeyMetadata({ litNodeClient, sessionSigs})
const customLitActionWithWrappedKeyResponse = await litNodeClient.executeJs({
  sessionSigs,
  ipfsId: litActionIpfsCid, // OR
  code: litActionCode, 
  jsParams: {
    accessControlConditions,
    keyMetadata,
  },
});

// Do whatever you want to with `customLitActionWithWrappedKeyResponse

Type of change

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

  • Ran all local tests against Cayenne. The only ones that fail say they intentionally fail (?!?!?!)

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

… and add `AcssParams` to `AccessControlConditions` type for accuracy
…t implementation

- Responsible for communicating with off-chain storage that is authenticated with a valid LIT session signature
- Methods are responsible for performing generate, signMessage, signTransaction operations using LIT actions
- `evm` and `solana` type `network` use our pre-defined actions IPFS CIDs
- `custom` type `network` requires the caller provide either LIT action source code or an IPFS CID where the source can be found
- Interfaces for executing the LIT actions directly are not exported from the package, so we can change our LIT actions APIs without making breaking changes to the top-level client APIs
- Consistent error and response handling is executed for all LIT action executions using methods in `utils` in `lit-actions-client`.
- Update constants for multiple target network support
- Move interfaces to more standard-named `types` file
- Rename tests to `utils.spec.ts` since it only tests util functions
- Update util functions imperative style to use declarative methods for better readability
- Update getPkpAddressFromSessionSig() util function to permit more than 3 capabilities to be present without throwing an error
- Update `getPkpAccessControlCondition()` to actually return _one condition_, not an array with one condition in it.  Type updated accordingly.
- Glues together service-client and lit-action-client appropriately with public API surface area to be exposed to consumers of the package
- Updated JSDocs on exported functions to reference types instead of repeating mostly the same strings on each function's JSDoc
- Added `getEncryptedKeyMetadata` method to facilitate arbitrary usage of the encrypted key without needing to use our built-in logic/assumptions around LIT actions, and to allow callers to fetch the `address` and other metadata that is being persisted to the backend
- Remove legacy `wrapped-keys.ts` file as its logic has been migrated into `api`
…cit.

- Namespaced API methods and constants separately from exported types
- Only exported types, interfaces and constants that are part of the API layer that end consumers will interact with
…e -- it breaks a ton of code in the `canonicalFormatter`

- Replaced type on `getPkpAccessControlCondition()` with `AccsDefaultParams`, a narrower type instead.
- Added `EthereumLitTransaction` type to exports
…they don't play nicely with string union types.
…-keys API

- Fixed missing logic in `generatePrivateKey` for handling `custom` case
- Added some code to testCustomSignTransactionWrappedKey to bring it closer to doing what its title implies it does. It fails.
- Replace try/catch at top-level `tinny-operations` so that failed tests don't...fail.  But they still do ?!
@MaximusHaximus MaximusHaximus requested review from DashKash54, Ansonhkg and joshLong145 and removed request for Ansonhkg and joshLong145 June 26, 2024 01:54
@spacesailor24
Copy link
Contributor

The local-tests/tests/testImportWrappedKey.ts test is importing a Solana key but says the keyType is K256

This test also isn't really testing if the key was imported correctly, but I think we'd have to use either getEncryptedKeyMetadata or the sign methods to verify it was

Comment on lines +40 to +48
export const api = {
exportPrivateKey,
generatePrivateKey,
getEncryptedKeyMetadata,
importPrivateKey,
signMessageWithEncryptedKey,
signTransactionWithEncryptedKey,
storeEncryptedKeyMetadata,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious why we're wrapping this in an object, does this make it not tree shakeable if a user is only using one or a few of these methods?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@MaximusHaximus MaximusHaximus Jun 28, 2024

Choose a reason for hiding this comment

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

It's still tree-shakeable; bundlers will only include the values from the exported object that the consumer actually uses, and these are all specific functions, rather than classes with mixed dependencies.

I actually had considered adding a createClient({litNodeClient, pkpSessionSigs}) method to the module exports, which would eliminate those 2 arguments from all of our API functions, simplify all of our typescript types and make our function signature more concise and easy to understand -- but decided not to because the return value of createClient() would not have been tree-shakeable and would have always included all of the functions.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If both are tree-shakable then what's the reason behind export const api = {..} instead of directly exporting the individual functions? @MaximusHaximus

SignMessageParams;

interface BaseLitTransaction {
chain: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice if we could restrict this to the supported EVM networks for EthereumLitTransactions and mainnet-beta | testnet | devnet for Solana txs

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sure for later, can you cut a ticket for tracking this @spacesailor24 and comment here

@@ -82,7 +82,7 @@ const {
clusterApiUrl(unsignedTransaction.chain),
'confirmed'
);
await solanaConnection.sendRawTransaction(transaction.serialize());
await solanaConnection.sendRawTransaction(transaction.serialize()); // FIXME: Shouldn't this return the tx hash for consistency with the evm action?
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes it should, however, in my testing waiting for the transaction hash always resulted in the Lit Action timing out. I'm going to add the following code to the docs to show users how to obtain the tx hash/receipt outside of the Lit Action:

// Assuming signature is in base64, convert it back to byte array for querying
const byteSignature = Buffer.from(signature, 'base64');

// Wait for confirmation and fetch the transaction details
const confirmation = await solanaConnection.confirmTransaction(byteSignature);
console.log('Transaction confirmation status:', confirmation.value);

// Get the transaction receipt
const receipt = await solanaConnection.getTransaction(byteSignature.toString('base64'), { commitment: 'confirmed' });
console.log('Transaction receipt:', receipt);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we can't keep the return values and behaviours of the functions consistent, it might make more sense to split them into separately named methods at the API layer. It's already a bit funky that we have a union type for the transaction -- if the return values and behaviour of the actual LIT actions differ as well, keeping them under a single method name in our API is probably hurting more than helping consumers of the SDK.

Can you look into this approach and see if it will work for us? @spacesailor24
https://solana.stackexchange.com/a/24

@spacesailor24
Copy link
Contributor

Are there tests for getEncryptedKeyMetadata and storeEncryptedKeyMetadata?

@CLAassistant
Copy link

CLAassistant commented Jun 28, 2024

CLA assistant check
All committers have signed the CLA.

Copy link
Collaborator

@DashKash54 DashKash54 left a comment

Choose a reason for hiding this comment

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

Left a few comments

@@ -31,7 +28,7 @@ export const testEthereumBroadcastTransactionGeneratedKey = async (

const { pkpAddress, generatedPublicKey } = await generatePrivateKey({
pkpSessionSigs,
network: NETWORK_EVM,
network: 'evm',
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Why remove the constant?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So once we started restricting network to be specific values using a string union type, I discovered that if someone used the const to call our method, the typescript checker would throw an error TS2322: Type string is not assignable to type Network -- because the type of the constant is string, decides that it is too generate -- it's not a specific string literal.

So then I tried defining these as:

export const NETWORK_EVM: Network = 'evm';
export const NETWORK_SOLANA: Network = 'solana';

That did fix the consumers code's typechecking, but then, inside the SDK I started getting TS checker errors in the lit-actions-client/constants.ts file where we define the LIT_ACTION_CID_REPOSITORY:

TS2739: Type Readonly<{ [x: string]: 'QmdYUhPCCK5hpDWMK1NiDLNLG6RZQy61QE4J7dBm1Y2nbA' | 'QmSi9GL2weCFEP1SMAUw5PDpZRr436Zt3tLUNrSECPA5dT'; }> is missing the following properties from type Readonly<Record<Network, string>>: evm, solana

Even though the entry clearly has the correct keys in it, defined by their constants :(

import { LitCidRepository } from './types';
import { NETWORK_EVM, NETWORK_SOLANA } from '../constants';

import { Network } from '../types';

export type LitActionType =
  | 'signTransaction'
  | 'signMessage'
  | 'generateEncryptedKey';

export type LitCidRepositoryEntry = Readonly<Record<Network, string>>;

export type LitCidRepository = Readonly<
  Record<LitActionType, LitCidRepositoryEntry>
>;

const LIT_ACTION_CID_REPOSITORY: LitCidRepository = {
  signTransaction: Object.freeze({
    [NETWORK_EVM]: 'QmdYUhPCCK5hpDWMK1NiDLNLG6RZQy61QE4J7dBm1Y2nbA',
    [NETWORK_SOLANA]: 'QmSi9GL2weCFEP1SMAUw5PDpZRr436Zt3tLUNrSECPA5dT',
  }),
  signMessage: Object.freeze({
    [NETWORK_EVM]: 'QmTMGcyp77NeppGaqF2DmE1F8GXTSxQYzXCrbE7hNudUWx',
    [NETWORK_SOLANA]: 'QmYQC6cd4EMvyB4XPkfEEAwNXJupRZWU5JsTCUrjey4ovp',
  }),
  generateEncryptedKey: Object.freeze({
    [NETWORK_EVM]: 'QmaoPMSqcze3NW3KSA75ecWSkcmWT1J7kVr8LyJPCKRvHd',
    [NETWORK_SOLANA]: 'QmdRBXYLYvcNHrChmsZ2jFDY8dA99CcSdqHo3p1ES3UThL',
  }),
};

export { LIT_ACTION_CID_REPOSITORY };

In the end, I discovered that I had to use string literals in the LIT_ACTION_CID_REPOSITORY to make TS happy; I couldn't use our consts as keys. Then I was faced with a conundrum; now external consumers would be using constants that we didn't use ourselves. Since TS can't use runtime data as types, I went the opposite direction and exported the string union type. I did some digging around the net to try and identify a solution that would let me use the same consts that consumers used to define the keys on our repository, but couldn't find a solution and it was taking quite a lot of time at that point.

Given that these are user-facing arguments, maybe it'd be fine for our internal files to use string constants when defining actual records, but external consumers still have constants to use if they want to? I've pushed a commit that makes that change -- take a look and LMK what you think. I also added exported constants for keyType using the same pattern.

packages/types/src/lib/interfaces.ts Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why have we changed these to interfaces do we plan to extend it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nothing changed -- our ESLint rule configuration (and SDK-wide convention) enforces that we always use interfaces unless we are using features that require a type. I ran eslint --fix on the file and it fixed the entries that should have been interfaces :). See first commit in the pr here: 21cfaf3

Copy link
Collaborator

Choose a reason for hiding this comment

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

Then EsLint is not being run in the CI? @MaximusHaximus @Ansonhkg ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's right, there is no mutative CI that changes files, and not everyone has their editors set to run eslint --fix on save, so these things happen ™️. It's been on the roadmap to implement for a few months.

packages/wrapped-keys/src/lib/lit-actions-client/utils.ts Outdated Show resolved Hide resolved
packages/wrapped-keys/src/lib/service-client/constants.ts Outdated Show resolved Hide resolved
@MaximusHaximus
Copy link
Contributor Author

Are there tests for getEncryptedKeyMetadata and storeEncryptedKeyMetadata?

Not directly; they are used by all of the other tests, so are extensively tested alreeady

@MaximusHaximus
Copy link
Contributor Author

Addressed feedback and responded to some questions with questions @DashKash54 :)

Copy link
Collaborator

@DashKash54 DashKash54 left a comment

Choose a reason for hiding this comment

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

This looks good

Copy link
Collaborator

@DashKash54 DashKash54 left a comment

Choose a reason for hiding this comment

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

LGTM!

@DashKash54 DashKash54 merged commit 4be606e into feature/lit-3125-sdk-for-wrapping-up-walletx-2 Jun 29, 2024
3 of 4 checks passed
@DashKash54 DashKash54 deleted the wrapped-keys-multinetwork-address-algo branch June 29, 2024 21:24
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.

4 participants