From 217d7f6adb45982e88272bd7346fed01647e84f9 Mon Sep 17 00:00:00 2001 From: anson Date: Fri, 7 Nov 2025 16:30:33 +0000 Subject: [PATCH 1/3] tests(signing): cover hashing + all pkp schemes --- .../sdk/auth-context-consumption/pkp-sign.mdx | 19 ++++- .../e2e/src/tickets/signing-schemes.spec.ts | 3 + .../e2e/src/tickets/signing-schemes.suite.ts | 83 +++++++++++++++++++ .../pkpSign/pkpSign.InputSchema.ts | 1 + .../vNaga/shared/schemas/LitMessageSchema.ts | 31 +++++-- 5 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 packages/e2e/src/tickets/signing-schemes.spec.ts create mode 100644 packages/e2e/src/tickets/signing-schemes.suite.ts diff --git a/docs/sdk/auth-context-consumption/pkp-sign.mdx b/docs/sdk/auth-context-consumption/pkp-sign.mdx index 859a158d21..9858012ee1 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 network hashes ECDSA payloads for you using the canonical function for each chain (Ethereum → keccak256, Bitcoin/Cosmos → SHA-256/SHA-384) before the nodes sign anything. 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); } From d2ff969413dad4cb9b3c7ee9b5796b5dbffa20fb Mon Sep 17 00:00:00 2001 From: anson Date: Fri, 7 Nov 2025 16:32:34 +0000 Subject: [PATCH 2/3] chore(release): add changeset --- .changeset/gold-ducks-repeat.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/gold-ducks-repeat.md 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. From beebcdfaa98ac4e86074990778941ea7995cf2c6 Mon Sep 17 00:00:00 2001 From: Anson Date: Fri, 7 Nov 2025 16:51:47 +0000 Subject: [PATCH 3/3] Update docs/sdk/auth-context-consumption/pkp-sign.mdx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Anson --- docs/sdk/auth-context-consumption/pkp-sign.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdk/auth-context-consumption/pkp-sign.mdx b/docs/sdk/auth-context-consumption/pkp-sign.mdx index 9858012ee1..523fa78f65 100644 --- a/docs/sdk/auth-context-consumption/pkp-sign.mdx +++ b/docs/sdk/auth-context-consumption/pkp-sign.mdx @@ -33,7 +33,7 @@ const signatures = await litClient.chain.raw.pkpSign({ ### Hashing defaults and bypass -By default the network hashes ECDSA payloads for you using the canonical function for each chain (Ethereum → keccak256, Bitcoin/Cosmos → SHA-256/SHA-384) before the nodes sign anything. 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`: +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));