diff --git a/.changeset/gold-ducks-repeat.md b/.changeset/gold-ducks-repeat.md new file mode 100644 index 0000000000..f2fc0c9242 --- /dev/null +++ b/.changeset/gold-ducks-repeat.md @@ -0,0 +1,6 @@ +--- +'@lit-protocol/networks': patch +'@lit-protocol/e2e': patch +--- + +PKP signing now auto-hashes Cosmos payloads, exposes a documented bypassAutoHashing option, and ships with a new e2e suite plus docs so builders can rely on every listed curve working out of the box. diff --git a/docs/sdk/auth-context-consumption/pkp-sign.mdx b/docs/sdk/auth-context-consumption/pkp-sign.mdx index 859a158d21..523fa78f65 100644 --- a/docs/sdk/auth-context-consumption/pkp-sign.mdx +++ b/docs/sdk/auth-context-consumption/pkp-sign.mdx @@ -31,6 +31,23 @@ const signatures = await litClient.chain.raw.pkpSign({ }); ``` +### Hashing defaults and bypass + +By default the SDK hashes ECDSA payloads for you using the canonical function for each chain (Ethereum → keccak256, Bitcoin/Cosmos → SHA-256/SHA-384) before sending to the nodes for signing. Schnorr/EdDSA schemes receive the raw bytes exactly as you provided them. If you already computed a digest (for example when signing EIP-712 typed data) you can pass it directly and opt out of the SDK hashing step by setting `bypassAutoHashing: true`: + +```ts +const digestBytes = hexToBytes(hashTypedData(typedData)); + +const signature = await litClient.chain.raw.pkpSign({ + chain: 'ethereum', + signingScheme: 'EcdsaK256Sha256', + pubKey: pkpInfo.pubkey, + authContext, + toSign: digestBytes, + bypassAutoHashing: true, +}); +``` + --- # Available signing schemes @@ -66,4 +83,4 @@ const signatures = await litClient.chain.raw.pkpSign({ | `SchnorrRistretto25519Sha512` | Ristretto25519 | | `SchnorrRedJubjubBlake2b512` | Jubjub | | `SchnorrRedDecaf377Blake2b512` | Decaf377 | -| `SchnorrkelSubstrate` | sr25519 | \ No newline at end of file +| `SchnorrkelSubstrate` | sr25519 | diff --git a/packages/e2e/src/tickets/signing-schemes.spec.ts b/packages/e2e/src/tickets/signing-schemes.spec.ts new file mode 100644 index 0000000000..aa69130c71 --- /dev/null +++ b/packages/e2e/src/tickets/signing-schemes.spec.ts @@ -0,0 +1,3 @@ +import { registerSigningSchemesTicketSuite } from './signing-schemes.suite'; + +registerSigningSchemesTicketSuite(); diff --git a/packages/e2e/src/tickets/signing-schemes.suite.ts b/packages/e2e/src/tickets/signing-schemes.suite.ts new file mode 100644 index 0000000000..ba8664db8f --- /dev/null +++ b/packages/e2e/src/tickets/signing-schemes.suite.ts @@ -0,0 +1,83 @@ +import { LitCurve } from '@lit-protocol/constants'; +import { SigningChainSchema } from '@lit-protocol/schemas'; +import { z } from 'zod'; +import { createEnvVars } from '../helper/createEnvVars'; +import { createTestAccount } from '../helper/createTestAccount'; +import { createTestEnv } from '../helper/createTestEnv'; + +type SigningChain = z.infer; + +type SchemeUnderTest = { + scheme: LitCurve; + chain: SigningChain; +}; + +const SIGNING_MATRIX: SchemeUnderTest[] = [ + // ECDSA variants + { scheme: 'EcdsaK256Sha256', chain: 'ethereum' }, + { scheme: 'EcdsaP256Sha256', chain: 'ethereum' }, + { scheme: 'EcdsaP384Sha384', chain: 'ethereum' }, + // Schnorr over secp256k1 (Bitcoin / Taproot) + { scheme: 'SchnorrK256Sha256', chain: 'bitcoin' }, + { scheme: 'SchnorrK256Taproot', chain: 'bitcoin' }, + // Schnorr over NIST curves + { scheme: 'SchnorrP256Sha256', chain: 'cosmos' }, + { scheme: 'SchnorrP384Sha384', chain: 'cosmos' }, + // EdDSA-style curves + { scheme: 'SchnorrEd25519Sha512', chain: 'solana' }, + { scheme: 'SchnorrEd448Shake256', chain: 'solana' }, + // ZK / privacy-focused curves + { scheme: 'SchnorrRistretto25519Sha512', chain: 'solana' }, + { scheme: 'SchnorrRedJubjubBlake2b512', chain: 'solana' }, + { scheme: 'SchnorrRedDecaf377Blake2b512', chain: 'solana' }, + { scheme: 'SchnorrkelSubstrate', chain: 'solana' }, +]; + +export function registerSigningSchemesTicketSuite() { + describe('pkp signing schemes', () => { + let testEnv: Awaited>; + let signerAccount: Awaited>; + + beforeAll(async () => { + const envVars = createEnvVars(); + testEnv = await createTestEnv(envVars); + signerAccount = await createTestAccount(testEnv, { + label: 'Signing Schemes', + fundAccount: true, + fundLedger: true, + hasEoaAuthContext: true, + hasPKP: true, + fundPKP: true, + fundPKPLedger: true, + }); + }); + + it.each(SIGNING_MATRIX)( + 'should sign using %s', + async ({ scheme, chain }) => { + if (!signerAccount.pkp?.pubkey) { + throw new Error('Signer PKP was not initialized'); + } + if (!signerAccount.eoaAuthContext) { + throw new Error('Signer account is missing an EOA auth context'); + } + + const toSign = new TextEncoder().encode( + `Lit signing e2e test using ${scheme}` + ); + + const signature = await testEnv.litClient.chain.raw.pkpSign({ + authContext: signerAccount.eoaAuthContext, + pubKey: signerAccount.pkp.pubkey, + signingScheme: scheme, + chain, + toSign, + userMaxPrice: 100_000_000_000_000_000n, // 0.1 ETH in wei to clear threshold comfortably + }); + + expect(signature.signature).toBeTruthy(); + expect(signature.sigType).toBe(scheme); + } + ); + }); +} diff --git a/packages/networks/src/networks/vNaga/shared/managers/api-manager/pkpSign/pkpSign.InputSchema.ts b/packages/networks/src/networks/vNaga/shared/managers/api-manager/pkpSign/pkpSign.InputSchema.ts index f0a47da466..4db267ba51 100644 --- a/packages/networks/src/networks/vNaga/shared/managers/api-manager/pkpSign/pkpSign.InputSchema.ts +++ b/packages/networks/src/networks/vNaga/shared/managers/api-manager/pkpSign/pkpSign.InputSchema.ts @@ -21,6 +21,7 @@ export const PKPSignInputSchema = z.object({ toSign: z.any(), authContext: z.union([PKPAuthContextSchema, EoaAuthContextSchema]), userMaxPrice: z.bigint().optional(), + bypassAutoHashing: z.boolean().optional(), }); export const EthereumPKPSignInputSchema = PKPSignInputSchema.omit({ diff --git a/packages/networks/src/networks/vNaga/shared/schemas/LitMessageSchema.ts b/packages/networks/src/networks/vNaga/shared/schemas/LitMessageSchema.ts index 42267b9da3..d7554ce378 100644 --- a/packages/networks/src/networks/vNaga/shared/schemas/LitMessageSchema.ts +++ b/packages/networks/src/networks/vNaga/shared/schemas/LitMessageSchema.ts @@ -70,10 +70,15 @@ export const chainHashMapper: ChainHashMapper = { EcdsaP384Sha384: sha384, }, - // @ts-ignore TODO: add support for this - cosmos: undefined, + cosmos: { + EcdsaK256Sha256: sha256, + EcdsaP256Sha256: sha256, + EcdsaP384Sha384: sha384, + }, - // @ts-ignore TODO: add support for this + // Solana signatures use Ed25519 (handled by the FROST branch), + // so we intentionally omit it from the ECDSA mapper. + // @ts-ignore solana: undefined, }; @@ -89,9 +94,23 @@ export const LitMessageSchema = z } if (CURVE_GROUP_BY_CURVE_TYPE[signingScheme] === 'ECDSA') { - const hashedMessage = chainHashMapper[chain][ - signingScheme as DesiredEcdsaSchemes - ](new Uint8Array(toSign)); + const chainHasher = chainHashMapper[chain]; + + if (!chainHasher) { + throw new Error( + `Chain "${chain}" does not support ECDSA signing with Lit yet.` + ); + } + + const hashFn = chainHasher[signingScheme as DesiredEcdsaSchemes]; + + if (!hashFn) { + throw new Error( + `Signing scheme "${signingScheme}" is not enabled for chain "${chain}".` + ); + } + + const hashedMessage = hashFn(new Uint8Array(toSign)); return BytesArraySchema.parse(hashedMessage); }