From 4739e0b3cecaeb69f3d9d506c940c7d276249497 Mon Sep 17 00:00:00 2001 From: Anson Date: Thu, 27 Feb 2025 15:55:42 +0000 Subject: [PATCH 1/2] feat: experiemental EIP7702 --- .../handlers/signEip7702Auth.ts | 51 +++++ .../primitive/signAuthorization.ts | 210 ++++++++++++++++++ my-app/main.ts | 7 +- my-lit-action/lit-action.ts | 99 +++++---- 4 files changed, 323 insertions(+), 44 deletions(-) create mode 100644 la-utils/la-transactions/handlers/signEip7702Auth.ts create mode 100644 la-utils/la-transactions/primitive/signAuthorization.ts diff --git a/la-utils/la-transactions/handlers/signEip7702Auth.ts b/la-utils/la-transactions/handlers/signEip7702Auth.ts new file mode 100644 index 0000000..fbe70df --- /dev/null +++ b/la-utils/la-transactions/handlers/signEip7702Auth.ts @@ -0,0 +1,51 @@ +import { signAuthorization } from "../primitive/signAuthorization"; +import { toEthAddress } from "../../la-pkp/toEthAddress"; + +/** + * Handler function for EIP-7702 authorization signing + * This function provides a high-level interface for generating EIP-7702 compliant authorization tuples + * + * @param {Object} params - The parameters object + * @param {string} params.pkpPublicKey - The PKP's public key + * @param {string} params.targetAddress - The address being authorized + * @param {string|number} params.chainId - The chain ID (0 for universal deployment) + * @param {string|number} params.nonce - Optional nonce value (defaults to 0 for new accounts) + * @returns {Promise<{ + * chainId: number, + * address: string, + * nonce: number, + * yParity: number, + * r: string, + * s: string, + * signer: string + * }>} The authorization tuple components and signer address + */ +export const signEip7702Auth = async ({ + pkpPublicKey, + targetAddress, + chainId = 0, + nonce = 0, +}: { + pkpPublicKey: string; + targetAddress: string; + chainId?: string | number; + nonce?: string | number; +}) => { + // Get the signer's address from PKP + const signerAddress = toEthAddress(pkpPublicKey); + + // Generate the authorization tuple + const authTuple = await signAuthorization({ + sigName: "eip7702-auth", + pkpPublicKey, + chainId, + target: targetAddress, + nonce, + }); + + // Return authorization tuple with signer address + return { + ...authTuple, + signer: signerAddress + }; +}; \ No newline at end of file diff --git a/la-utils/la-transactions/primitive/signAuthorization.ts b/la-utils/la-transactions/primitive/signAuthorization.ts new file mode 100644 index 0000000..a1c3c95 --- /dev/null +++ b/la-utils/la-transactions/primitive/signAuthorization.ts @@ -0,0 +1,210 @@ +// Validate inputs according to EIP-7702 spec +/** Maximum value for 256-bit unsigned integers (2^256 - 1) + * Used to validate: + * - signature components (r,s) + * - chainId + */ +const MAX_UINT256 = BigInt( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +); + +/** Maximum value for 64-bit unsigned integers (2^64 - 1) + * Used to validate: + * - nonce value + */ +const MAX_UINT64 = BigInt("0xffffffffffffffff"); + +/** Maximum value for 8-bit unsigned integers (2^8 - 1) + * Used to validate: + * - signature yParity (v) value + */ +const MAX_UINT8 = BigInt("0xff"); + +/** + * Signs an EIP-7702 authorization tuple + * This function implements the authorization signing as specified in EIP-7702 + * + * @param {Object} params - The parameters object + * @param {string} params.pkpPublicKey - The PKP's public key + * @param {string|number} params.chainId - The chain ID (0 for universal deployment) + * @param {string} params.target - The target address that will be authorized + * @param {string|number} params.nonce - The nonce value (must be < 2^64) + * @param {string} params.sigName - The name of the signature for tracking + * @returns {Promise<{chainId: number, address: string, nonce: number, yParity: number, r: string, s: string}>} + * The authorization tuple components as specified in EIP-7702 + */ +export const signAuthorization = async ({ + sigName, + pkpPublicKey, + chainId, + target, + nonce, +}: { + sigName: string; + pkpPublicKey: string; + chainId: string | number; + target: string; + nonce: string | number; +}) => { + console.log("=== Debug: Input Parameters ==="); + console.log("pkpPublicKey:", pkpPublicKey); + console.log("chainId:", chainId); + console.log("target:", target); + console.log("nonce:", nonce); + + console.log("\n=== Debug: Max Values ==="); + console.log("MAX_UINT256 (hex):", MAX_UINT256.toString(16)); + console.log("MAX_UINT64 (hex):", MAX_UINT64.toString(16)); + console.log("MAX_UINT8 (hex):", MAX_UINT8.toString(16)); + + // Validate chainId + const numericChainId = BigInt(chainId); + console.log("numericChainId:", numericChainId); + if (numericChainId > MAX_UINT256) { + throw new Error("Chain ID must be less than 2^256"); + } + + // Validate nonce + const numericNonce = BigInt(nonce); + console.log("numericNonce:", numericNonce); + if (numericNonce > MAX_UINT64) { + throw new Error("Nonce must be less than 2^64"); + } + + // Validate target address + if (!target.startsWith("0x")) { + throw new Error("Target address must start with 0x"); + } + const addressWithoutPrefix = target.slice(2); + console.log("addressWithoutPrefix:", addressWithoutPrefix); + if (addressWithoutPrefix.length !== 40) { + // 20 bytes = 40 hex chars + throw new Error("Target address must be 20 bytes long"); + } + if (!/^[0-9a-fA-F]+$/.test(addressWithoutPrefix)) { + throw new Error("Target address must be a valid hex string"); + } + + // Format the PKP public key if needed + const pkForLit = pkpPublicKey.startsWith("0x") + ? pkpPublicKey.slice(2) + : pkpPublicKey; + + // Create the authorization message according to EIP-7702 spec + // MAGIC (0x05) || rlp([chain_id, address, nonce]) + const MAGIC = "0x05"; + const rlpEncoded = ethers.utils.RLP.encode([ + ethers.utils.hexlify(chainId), + target, + ethers.utils.hexlify(nonce), + ]); + + console.log("\n=== Debug: Message Construction ==="); + console.log("MAGIC:", MAGIC); + console.log("RLP encoded:", rlpEncoded); + + const messageToSign = ethers.utils.concat([ + ethers.utils.hexlify(MAGIC), + rlpEncoded, + ]); + + console.log("Message to sign:", messageToSign); + + // Hash the message + const messageHash = ethers.utils.keccak256(messageToSign); + console.log("Message hash:", messageHash); + + // Sign the hash using PKP + const sig = await Lit.Actions.signAndCombineEcdsa({ + toSign: ethers.utils.arrayify(messageHash), + publicKey: pkForLit, + sigName, + }); + + console.log("\n=== Debug: Raw Signature ==="); + console.log("Raw signature:", sig); + + // Parse signature components + const parsedSig = JSON.parse(sig); + console.log("\n=== Debug: Parsed Signature Components ==="); + console.log("v:", parsedSig.v); + console.log("r:", parsedSig.r); + console.log("s:", parsedSig.s); + + // Validate signature components + const yParity = BigInt(parsedSig.v); + console.log("\n=== Debug: yParity Validation ==="); + console.log("yParity:", yParity.toString()); + console.log("MAX_UINT8:", MAX_UINT8.toString(16)); + + if (yParity > MAX_UINT8) { + throw new Error("y_parity must be less than 2^8"); + } + + // Add hex prefix for BigInt conversion and ensure exactly 64 characters (32 bytes) + const rHexRaw = parsedSig.r.replace(/^0+/, ""); + const sHexRaw = parsedSig.s.replace(/^0+/, ""); + + // Take the last 64 characters to ensure correct length + const rHex = "0x" + rHexRaw.slice(-64); + const sHex = "0x" + sHexRaw.slice(-64); + + console.log("\n=== Debug: r/s Hex Values ==="); + console.log("Original r:", parsedSig.r); + console.log("Truncated rHex:", rHex); + console.log("Original s:", parsedSig.s); + console.log("Truncated sHex:", sHex); + + try { + const r = BigInt(rHex); + console.log("\n=== Debug: r Value Detailed Comparison ==="); + console.log("r decimal:", r.toString()); + console.log("r hex:", r.toString(16)); + console.log("r length in hex:", r.toString(16).length); + console.log("MAX_UINT256 decimal:", MAX_UINT256.toString()); + console.log("MAX_UINT256 hex:", MAX_UINT256.toString(16)); + console.log("MAX_UINT256 length in hex:", MAX_UINT256.toString(16).length); + console.log("Is r > MAX_UINT256?", r > MAX_UINT256); + console.log("Difference:", (r - MAX_UINT256).toString()); + + if (r > MAX_UINT256) { + throw new Error("r must be less than 2^256"); + } + + const s = BigInt(sHex); + console.log("\n=== Debug: s Value ==="); + console.log("s decimal:", s.toString()); + console.log("s hex:", s.toString(16)); + + if (s > MAX_UINT256) { + throw new Error("s must be less than 2^256"); + } + + // Additional EIP-2 validation for s value + const secp256k1n = BigInt( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ); + console.log("\n=== Debug: secp256k1n Validation ==="); + console.log("secp256k1n/2:", (secp256k1n / BigInt(2)).toString(16)); + + if (s > secp256k1n / BigInt(2)) { + throw new Error( + "s value must be less than or equal to secp256k1n/2 as per EIP-2" + ); + } + + // Return the authorization tuple components + return { + chainId: Number(chainId), + address: target, + nonce: Number(nonce), + yParity: Number(yParity), + r: rHex, + s: sHex, + }; + } catch (error) { + console.error("\n=== Debug: Error Details ==="); + console.error("Error converting or validating r/s values:", error); + throw error; + } +}; diff --git a/my-app/main.ts b/my-app/main.ts index 701c6a1..1c35e1e 100644 --- a/my-app/main.ts +++ b/my-app/main.ts @@ -4,12 +4,15 @@ import { readPKP } from "../scripts/utils/io"; (async () => { const lit = await createLitService(); + const pkp = readPKP(); - console.log("🏃‍♂️ Running Lit Action..."); + console.log("🏃‍♂️ Running EIP-7702 Authorization Example..."); const res = await lit.executeJs({ code: litActionCodeString, params: { - pkpPublicKey: readPKP().pkpInfo.publicKey, + pkpPublicKey: pkp.pkpInfo.publicKey, + targetAddress: "0x1234567890123456789012345678901234567890", // Example target address + chainId: 0, // 0 for universal deployment }, }); diff --git a/my-lit-action/lit-action.ts b/my-lit-action/lit-action.ts index b512184..575c6ee 100644 --- a/my-lit-action/lit-action.ts +++ b/my-lit-action/lit-action.ts @@ -2,6 +2,7 @@ import { getYellowstoneProvider } from "../la-utils/la-chain/yellowstone/getYell import { toEthAddress } from "../la-utils/la-pkp/toEthAddress"; import { contractCall } from "../la-utils/la-transactions/handlers/contractCall"; import { nativeSend } from "../la-utils/la-transactions/handlers/nativeSend"; +import { signEip7702Auth } from "../la-utils/la-transactions/handlers/signEip7702Auth"; import { contractExample } from "./contract-example"; import { composeTxUrl } from "./utils"; @@ -13,48 +14,62 @@ declare global { } (async () => { - console.log("👋 Hello via Lit Action!"); + console.log("🔐 EIP-7702 Authorization Example"); + + try { + // Generate the EIP-7702 authorization tuple + const authTuple = await signEip7702Auth({ + pkpPublicKey: params.pkpPublicKey, + targetAddress: "0x341E5273E2E2ea3c4aDa4101F008b1261E58510D", + chainId: 1, + }); + + console.log("✅ Authorization signed successfully!"); + console.log("authTuple:", authTuple); + } catch (error) { + console.error("❌ Failed to sign authorization:", error); + } // Access your jsParams here - console.log("PKP Public Key:", params.pkpPublicKey); - - // using a helper function - const pkpEthAddress = toEthAddress(params.pkpPublicKey); - console.log("PKP ETH Address:", pkpEthAddress); - - // Get the provider - const provider = await getYellowstoneProvider(); - - // Example 1: Send transaction using the nativeSend handler, which is a wrapper around the primitive functions - const txHash = await nativeSend({ - provider, - pkpPublicKey: params.pkpPublicKey, - to: pkpEthAddress, - amount: "0.0001", - }); - - // Example 2: Call a contract function - const txHash2 = await contractCall({ - provider, - pkpPublicKey: params.pkpPublicKey, - callerAddress: pkpEthAddress, - abi: [contractExample.methods.mintNextAndAddAuthMethods], - contractAddress: contractExample.address, - functionName: "mintNextAndAddAuthMethods", - args: [ - 2, - [2], - ["0x170d13600caea2933912f39a0334eca3d22e472be203f937c4bad0213d92ed71"], - ["0x0000000000000000000000000000000000000000000000000000000000000000"], - [[1]], - true, - true, - ], - overrides: { - value: 1n, - }, - }); - - console.log(`🎉 [nativeSend] Transaction sent: ${composeTxUrl(txHash)}`); - console.log(`🎉 [contractCall] Transaction sent: ${composeTxUrl(txHash2)}`); + // console.log("PKP Public Key:", params.pkpPublicKey); + + // // using a helper function + // const pkpEthAddress = toEthAddress(params.pkpPublicKey); + // console.log("PKP ETH Address:", pkpEthAddress); + + // // Get the provider + // const provider = await getYellowstoneProvider(); + + // // Example 1: Send transaction using the nativeSend handler, which is a wrapper around the primitive functions + // const txHash = await nativeSend({ + // provider, + // pkpPublicKey: params.pkpPublicKey, + // to: pkpEthAddress, + // amount: "0.0001", + // }); + + // // Example 2: Call a contract function + // const txHash2 = await contractCall({ + // provider, + // pkpPublicKey: params.pkpPublicKey, + // callerAddress: pkpEthAddress, + // abi: [contractExample.methods.mintNextAndAddAuthMethods], + // contractAddress: contractExample.address, + // functionName: "mintNextAndAddAuthMethods", + // args: [ + // 2, + // [2], + // ["0x170d13600caea2933912f39a0334eca3d22e472be203f937c4bad0213d92ed71"], + // ["0x0000000000000000000000000000000000000000000000000000000000000000"], + // [[1]], + // true, + // true, + // ], + // overrides: { + // value: 1n, + // }, + // }); + + // console.log(`🎉 [nativeSend] Transaction sent: ${composeTxUrl(txHash)}`); + // console.log(`🎉 [contractCall] Transaction sent: ${composeTxUrl(txHash2)}`); })(); From 7774bc0c7f372165a0f265178a55cf5f5f7df01e Mon Sep 17 00:00:00 2001 From: Anson Date: Thu, 27 Feb 2025 16:29:44 +0000 Subject: [PATCH 2/2] feat: add example of using Viem private key --- .../handlers/signEip7702Auth.ts | 10 +-- .../handlers/signEip7702AuthViem.ts | 68 ++++++++++++++++++ my-lit-action/lit-action.ts | 71 +++++++------------ package.json | 1 + scripts/postbuild.ts | 17 +++-- 5 files changed, 109 insertions(+), 58 deletions(-) create mode 100644 la-utils/la-transactions/handlers/signEip7702AuthViem.ts diff --git a/la-utils/la-transactions/handlers/signEip7702Auth.ts b/la-utils/la-transactions/handlers/signEip7702Auth.ts index fbe70df..93e1109 100644 --- a/la-utils/la-transactions/handlers/signEip7702Auth.ts +++ b/la-utils/la-transactions/handlers/signEip7702Auth.ts @@ -2,9 +2,9 @@ import { signAuthorization } from "../primitive/signAuthorization"; import { toEthAddress } from "../../la-pkp/toEthAddress"; /** - * Handler function for EIP-7702 authorization signing - * This function provides a high-level interface for generating EIP-7702 compliant authorization tuples - * + * Handler function for EIP-7702 authorization signing using PKP + * This function provides a high-level interface for generating EIP-7702 compliant authorization tuples using PKP + * * @param {Object} params - The parameters object * @param {string} params.pkpPublicKey - The PKP's public key * @param {string} params.targetAddress - The address being authorized @@ -46,6 +46,6 @@ export const signEip7702Auth = async ({ // Return authorization tuple with signer address return { ...authTuple, - signer: signerAddress + signer: signerAddress, }; -}; \ No newline at end of file +}; diff --git a/la-utils/la-transactions/handlers/signEip7702AuthViem.ts b/la-utils/la-transactions/handlers/signEip7702AuthViem.ts new file mode 100644 index 0000000..06d8521 --- /dev/null +++ b/la-utils/la-transactions/handlers/signEip7702AuthViem.ts @@ -0,0 +1,68 @@ +import { concatHex, encodeAbiParameters, type Hex } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; + +/** + * Signs an EIP-7702 authorization using a private key via Viem + * This function provides a direct interface to sign EIP-7702 authorizations using Viem's primitives and a private key + * + * @param {Object} params - The parameters object + * @param {Hex} params.privateKey - The private key to sign with (must be in 0x format) + * @param {string} params.targetAddress - The address being authorized + * @param {bigint} params.chainId - The chain ID (defaults to 1n for Ethereum mainnet) + * @param {bigint} params.nonce - The nonce value (defaults to 0n) + * @returns {Promise<{ + * chainId: number, + * address: string, + * nonce: number, + * yParity: number, + * r: string, + * s: string, + * signer: string + * }>} The authorization tuple components and signer address + */ +export const signEip7702AuthViem = async ({ + privateKey, + targetAddress, + chainId = 1n, + nonce = 0n, +}: { + privateKey: Hex; + targetAddress: string; + chainId?: bigint; + nonce?: bigint; +}) => { + const wallet = privateKeyToAccount(privateKey); + + const message = { + chainId, + target: targetAddress as Hex, + nonce, + }; + + // Sign using Viem's signMessage with EIP-7702 format + const signature = await wallet.signMessage({ + message: concatHex([ + "0x05", // MAGIC prefix for EIP-7702 + encodeAbiParameters( + [{ type: "uint256" }, { type: "address" }, { type: "uint64" }], + [message.chainId, message.target, message.nonce] + ), + ]), + }); + + // Extract r, s, v components from the signature + const r = `0x${signature.slice(2, 66)}`; + const s = `0x${signature.slice(66, 130)}`; + const v = parseInt(signature.slice(130, 132), 16); + + // Return in the same format as PKP auth tuple + return { + chainId: Number(chainId), + address: targetAddress, + nonce: Number(nonce), + yParity: v - 27, // Convert v to yParity + r, + s, + signer: wallet.address + }; +}; \ No newline at end of file diff --git a/my-lit-action/lit-action.ts b/my-lit-action/lit-action.ts index 575c6ee..9edbb67 100644 --- a/my-lit-action/lit-action.ts +++ b/my-lit-action/lit-action.ts @@ -3,8 +3,10 @@ import { toEthAddress } from "../la-utils/la-pkp/toEthAddress"; import { contractCall } from "../la-utils/la-transactions/handlers/contractCall"; import { nativeSend } from "../la-utils/la-transactions/handlers/nativeSend"; import { signEip7702Auth } from "../la-utils/la-transactions/handlers/signEip7702Auth"; +import { signEip7702AuthViem } from "../la-utils/la-transactions/handlers/signEip7702AuthViem"; import { contractExample } from "./contract-example"; import { composeTxUrl } from "./utils"; +import { type Hex } from "viem"; // Define your jsParams here. It's sending from ./my-app/main.ts declare global { @@ -13,63 +15,40 @@ declare global { }; } +const TARGET_ADDRESS = "0x341E5273E2E2ea3c4aDa4101F008b1261E58510D"; + (async () => { - console.log("🔐 EIP-7702 Authorization Example"); + console.log("🔐 EIP-7702 Authorization Examples"); + // Example 1: Using PKP to sign EIP-7702 authorization try { - // Generate the EIP-7702 authorization tuple + // Generate the EIP-7702 authorization tuple using PKP const authTuple = await signEip7702Auth({ pkpPublicKey: params.pkpPublicKey, - targetAddress: "0x341E5273E2E2ea3c4aDa4101F008b1261E58510D", + targetAddress: TARGET_ADDRESS, chainId: 1, }); - console.log("✅ Authorization signed successfully!"); + console.log("✅ PKP Authorization tuple generated successfully!"); console.log("authTuple:", authTuple); } catch (error) { - console.error("❌ Failed to sign authorization:", error); + console.error("❌ Failed to sign PKP authorization:", error); } - // Access your jsParams here - // console.log("PKP Public Key:", params.pkpPublicKey); - - // // using a helper function - // const pkpEthAddress = toEthAddress(params.pkpPublicKey); - // console.log("PKP ETH Address:", pkpEthAddress); - - // // Get the provider - // const provider = await getYellowstoneProvider(); - - // // Example 1: Send transaction using the nativeSend handler, which is a wrapper around the primitive functions - // const txHash = await nativeSend({ - // provider, - // pkpPublicKey: params.pkpPublicKey, - // to: pkpEthAddress, - // amount: "0.0001", - // }); - - // // Example 2: Call a contract function - // const txHash2 = await contractCall({ - // provider, - // pkpPublicKey: params.pkpPublicKey, - // callerAddress: pkpEthAddress, - // abi: [contractExample.methods.mintNextAndAddAuthMethods], - // contractAddress: contractExample.address, - // functionName: "mintNextAndAddAuthMethods", - // args: [ - // 2, - // [2], - // ["0x170d13600caea2933912f39a0334eca3d22e472be203f937c4bad0213d92ed71"], - // ["0x0000000000000000000000000000000000000000000000000000000000000000"], - // [[1]], - // true, - // true, - // ], - // overrides: { - // value: 1n, - // }, - // }); + // Example 2: Using Viem with private key to sign EIP-7702 authorization + try { + const privateKey = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex; // Example private key + + const signature = await signEip7702AuthViem({ + privateKey, + targetAddress: TARGET_ADDRESS, + chainId: 1n, + nonce: 0n, + }); - // console.log(`🎉 [nativeSend] Transaction sent: ${composeTxUrl(txHash)}`); - // console.log(`🎉 [contractCall] Transaction sent: ${composeTxUrl(txHash2)}`); + console.log("✅ Private key Authorization tuple generated:", signature); + } catch (error) { + console.error("❌ Failed to sign with Viem:", error); + } })(); diff --git a/package.json b/package.json index 9d51faa..80cd686 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "inquirer": "^12.4.2", "json-with-bigint": "^2.4.2", "pino-caller": "^3.4.0", + "viem": "^2.23.5", "zod": "^3.24.2" } } diff --git a/scripts/postbuild.ts b/scripts/postbuild.ts index 7a78291..419e36f 100644 --- a/scripts/postbuild.ts +++ b/scripts/postbuild.ts @@ -1,6 +1,6 @@ /** * Postbuild script to process the lit-action.js file - * Purpose: Reads the compiled lit-action.js, escapes backticks, and wraps it in a module export + * Purpose: Reads the compiled lit-action.js, removes Ajv code, and converts it to a string export * Usage: Run after build process to prepare the lit-action code for distribution */ @@ -8,9 +8,12 @@ import fs from "fs"; console.log("- postbuilding..."); const actionCode = fs.readFileSync("./dist/lit-action.js", "utf-8"); -// Escape both backticks and template literal expressions -const escapedActionCode = actionCode - .replace(/`/g, "\\`") - .replace(/\${/g, "\\${"); -const code = `export const litActionCodeString = \`${escapedActionCode}\`;`; -fs.writeFileSync("./dist/lit-action.js", code); + +// Create a JavaScript string literal with the code properly escaped +// using JSON.stringify to handle all escaping correctly +const codeAsString = JSON.stringify(actionCode); + +// Create a regular string concatenation without template literals +const outputCode = "export const litActionCodeString = " + codeAsString + ";"; + +fs.writeFileSync("./dist/lit-action.js", outputCode); \ No newline at end of file