From 2ed531c1dd81b4f7274b6f0ec1b66ed89bba921a Mon Sep 17 00:00:00 2001 From: Madhur Shrimal Date: Thu, 29 Aug 2024 17:44:52 -0700 Subject: [PATCH 1/3] feat: add remote sign for attestation --- .env.local | 8 ++++- operator/index.ts | 81 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/.env.local b/.env.local index ba248cb8..27db2075 100644 --- a/.env.local +++ b/.env.local @@ -1,3 +1,6 @@ +# local or remote +SIGNER_TYPE=remote + PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 RPC_URL=http://127.0.0.1:8545 CONTRACT_ADDRESS=0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB @@ -13,4 +16,7 @@ HOLESKY_CONTRACT_ADDRESS=0x3361953F4a9628672dCBcDb29e91735fb1985390 HOLESKY_DELEGATION_MANAGER_ADDRESS=0xA44151489861Fe9e3055d95adC98FbD462B948e7 HOLESKY_STAKE_REGISTRY_ADDRESS=0xBDACD5998989Eec814ac7A0f0f6596088AA2a270 HOLESKY_AVS_DIRECTORY_ADDRESS=0x055733000064333CaDDbC92763c58BF0192fFeBf -HOLESKY_WS_RPC_URL=wss://holesky.drpc.org \ No newline at end of file +HOLESKY_WS_RPC_URL=wss://holesky.drpc.org + +REMOTE_SIGNER_URL="fill me in if you want to use remote signer" +OPERATOR_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \ No newline at end of file diff --git a/operator/index.ts b/operator/index.ts index 55554fab..7d14b3f5 100644 --- a/operator/index.ts +++ b/operator/index.ts @@ -14,6 +14,11 @@ const contractAddress = process.env.CONTRACT_ADDRESS!; const stakeRegistryAddress = process.env.STAKE_REGISTRY_ADDRESS!; const avsDirectoryAddress = process.env.AVS_DIRECTORY_ADDRESS!; +const remoteSignerUrl = process.env.REMOTE_SIGNER_URL!; +const operatorAddress = process.env.OPERATOR_ADDRESS!; + +const signerType = process.env.SIGNER_TYPE!; + const delegationManager = new ethers.Contract(delegationManagerAddress, delegationABI, wallet); const contract = new ethers.Contract(contractAddress, contractABI, wallet); const registryContract = new ethers.Contract(stakeRegistryAddress, registryABI, wallet); @@ -22,8 +27,20 @@ const avsDirectory = new ethers.Contract(avsDirectoryAddress, avsDirectoryABI, w const signAndRespondToTask = async (taskIndex: number, taskCreatedBlock: number, taskName: string) => { const message = `Hello, ${taskName}`; const messageHash = ethers.utils.solidityKeccak256(["string"], [message]); - const messageBytes = ethers.utils.arrayify(messageHash); - const signature = await wallet.signMessage(messageBytes); + + let signature = ""; + if (signerType === "local") { + console.log("Using local private key to sign message") + const messageBytes = ethers.utils.arrayify(messageHash); + signature = await wallet.signMessage(messageBytes); + } else if (signerType === "remote") { + console.log("Using remote signer to sign message") + signature = await callJsonRpcEndpoint( + remoteSignerUrl, + "eth_sign", + [operatorAddress, messageHash] + ); + } console.log( `Signing and responding to task ${taskIndex}` @@ -60,16 +77,16 @@ const registerOperator = async () => { // Calculate the digest hash using the avsDirectory's method const digestHash = await avsDirectory.calculateOperatorAVSRegistrationDigestHash( - wallet.address, - contract.address, - salt, + wallet.address, + contract.address, + salt, expiry ); // // Sign the digest hash with the operator's private key const signingKey = new ethers.utils.SigningKey(process.env.PRIVATE_KEY!); const signature = signingKey.signDigest(digestHash); - + // // Encode the signature in the required format operatorSignature.signature = ethers.utils.joinSignature(signature); @@ -101,4 +118,54 @@ const main = async () => { main().catch((error) => { console.error("Error in main function:", error); -}); \ No newline at end of file +}); + +interface JsonRpcRequest { + jsonrpc: string; + method: string; + params: any[]; + id: number; +} + +interface JsonRpcResponse { + jsonrpc: string; + result?: any; + error?: { + code: number; + message: string; + }; + id: number; +} + +async function callJsonRpcEndpoint( + url: string, + method: string, + params: any[] = [] +): Promise { + const request: JsonRpcRequest = { + jsonrpc: "2.0", + method, + params, + id: 1, // You might want to generate a unique id for each request + }; + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const jsonResponse: JsonRpcResponse = await response.json(); + + if (jsonResponse.error) { + throw new Error(`JSON-RPC error: ${jsonResponse.error.message}`); + } + + return jsonResponse.result; +} \ No newline at end of file From 4848b75776d5a45cecabcc979dc25fbd48e54cdc Mon Sep 17 00:00:00 2001 From: Madhur Shrimal Date: Thu, 29 Aug 2024 17:53:37 -0700 Subject: [PATCH 2/3] revert default config to local --- .env.local | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.local b/.env.local index 27db2075..0d9f0173 100644 --- a/.env.local +++ b/.env.local @@ -1,5 +1,5 @@ # local or remote -SIGNER_TYPE=remote +SIGNER_TYPE=local PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 RPC_URL=http://127.0.0.1:8545 From 99542ec58e6dd696d2dba74fe22870e514a5feb0 Mon Sep 17 00:00:00 2001 From: Madhur Shrimal Date: Tue, 3 Sep 2024 11:00:00 -0700 Subject: [PATCH 3/3] smart contract tx with remote signer --- operator/index.ts | 43 ++++++++++----- operator/remoteSigner.ts | 115 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 operator/remoteSigner.ts diff --git a/operator/index.ts b/operator/index.ts index 7d14b3f5..a5086588 100644 --- a/operator/index.ts +++ b/operator/index.ts @@ -4,25 +4,27 @@ import { delegationABI } from "./abis/delegationABI"; import { contractABI } from './abis/contractABI'; import { registryABI } from './abis/registryABI'; import { avsDirectoryABI } from './abis/avsDirectoryABI'; +import { RemoteSigner } from "./remoteSigner"; dotenv.config(); +const remoteSignerUrl = process.env.REMOTE_SIGNER_URL!; +const operatorAddress = process.env.OPERATOR_ADDRESS!; const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); +const signer = new RemoteSigner(operatorAddress, provider, remoteSignerUrl); +let address = ""; const delegationManagerAddress = process.env.DELEGATION_MANAGER_ADDRESS!; const contractAddress = process.env.CONTRACT_ADDRESS!; const stakeRegistryAddress = process.env.STAKE_REGISTRY_ADDRESS!; const avsDirectoryAddress = process.env.AVS_DIRECTORY_ADDRESS!; -const remoteSignerUrl = process.env.REMOTE_SIGNER_URL!; -const operatorAddress = process.env.OPERATOR_ADDRESS!; - const signerType = process.env.SIGNER_TYPE!; -const delegationManager = new ethers.Contract(delegationManagerAddress, delegationABI, wallet); -const contract = new ethers.Contract(contractAddress, contractABI, wallet); -const registryContract = new ethers.Contract(stakeRegistryAddress, registryABI, wallet); -const avsDirectory = new ethers.Contract(avsDirectoryAddress, avsDirectoryABI, wallet); +let delegationManager: ethers.Contract; +let contract: ethers.Contract; +let registryContract: ethers.Contract; +let avsDirectory: ethers.Contract; const signAndRespondToTask = async (taskIndex: number, taskCreatedBlock: number, taskName: string) => { const message = `Hello, ${taskName}`; @@ -35,11 +37,7 @@ const signAndRespondToTask = async (taskIndex: number, taskCreatedBlock: number, signature = await wallet.signMessage(messageBytes); } else if (signerType === "remote") { console.log("Using remote signer to sign message") - signature = await callJsonRpcEndpoint( - remoteSignerUrl, - "eth_sign", - [operatorAddress, messageHash] - ); + signature = await signer.signMessage(messageHash); } console.log( @@ -58,7 +56,7 @@ const signAndRespondToTask = async (taskIndex: number, taskCreatedBlock: number, const registerOperator = async () => { console.log("check") const tx1 = await delegationManager.registerAsOperator({ - earningsReceiver: await wallet.address, + earningsReceiver: address, delegationApprover: "0x0000000000000000000000000000000000000000", stakerOptOutWindowBlocks: 0 }, ""); @@ -77,13 +75,15 @@ const registerOperator = async () => { // Calculate the digest hash using the avsDirectory's method const digestHash = await avsDirectory.calculateOperatorAVSRegistrationDigestHash( - wallet.address, + address, contract.address, salt, expiry ); // // Sign the digest hash with the operator's private key + // TODO(shrimalmadhur): I am not completely sure about how to make this work with remote signer + // as the signDigest function is not available on the remote signer. const signingKey = new ethers.utils.SigningKey(process.env.PRIVATE_KEY!); const signature = signingKey.signDigest(digestHash); @@ -92,7 +92,7 @@ const registerOperator = async () => { const tx2 = await registryContract.registerOperatorWithSignature( operatorSignature, - wallet.address + address ); await tx2.wait(); console.log("Operator registered on AVS successfully"); @@ -110,6 +110,19 @@ const monitorNewTasks = async () => { }; const main = async () => { + if (signerType === "local") { + address = wallet.address; + delegationManager = new ethers.Contract(delegationManagerAddress, delegationABI, wallet); + contract = new ethers.Contract(contractAddress, contractABI, wallet); + registryContract = new ethers.Contract(stakeRegistryAddress, registryABI, wallet); + avsDirectory = new ethers.Contract(avsDirectoryAddress, avsDirectoryABI, wallet); + } else { + address = await signer.getAddress(); + delegationManager = new ethers.Contract(delegationManagerAddress, delegationABI, signer); + contract = new ethers.Contract(contractAddress, contractABI, signer); + registryContract = new ethers.Contract(stakeRegistryAddress, registryABI, signer); + avsDirectory = new ethers.Contract(avsDirectoryAddress, avsDirectoryABI, signer); + } await registerOperator(); monitorNewTasks().catch((error) => { console.error("Error monitoring tasks:", error); diff --git a/operator/remoteSigner.ts b/operator/remoteSigner.ts new file mode 100644 index 00000000..e79135dd --- /dev/null +++ b/operator/remoteSigner.ts @@ -0,0 +1,115 @@ +import { Signer, providers, utils } from 'ethers'; + +export class RemoteSigner extends Signer { + private readonly address: string; + readonly provider: providers.Provider; + private readonly remoteSigningEndpoint: string; + + constructor(address: string, provider: providers.Provider, remoteSigningEndpoint: string) { + super(); + this.address = address; + this.provider = provider; + this.remoteSigningEndpoint = remoteSigningEndpoint; + } + + async getAddress(): Promise { + return this.address; + } + + async signMessage(message: string | utils.Bytes): Promise { + if (typeof(message) === "string") { + return this.signMessageHash(message); + } else { + const messageHash = utils.solidityKeccak256(["string"], [message]) + return this.signMessageHash(messageHash); + } + } + + async signTransaction(transaction: utils.Deferrable): Promise { + // Implement the logic to send the transaction to your remote signing service + // and return the signed transaction + const tx = { + from: transaction.from, + to: transaction.to, + value: transaction.value, + gas: transaction.gasLimit?.toString(), + maxPriorityFeePerGas: transaction.maxPriorityFeePerGas?.toString(), + maxFeePerGas: transaction.maxFeePerGas?.toString(), + nonce: transaction.nonce, + data: transaction.data, + } + const signedTransaction = await callJsonRpcEndpoint( + this.remoteSigningEndpoint, + "eth_signTransaction", + [tx] + ); + + return signedTransaction; + } + + connect(provider: providers.Provider): Signer { + return new RemoteSigner(this.address, provider, this.remoteSigningEndpoint); + } + + private async signMessageHash(messageHash: string): Promise { + // Implement the logic to send the message hash to your remote signing service + // and return the signature + const signature = await callJsonRpcEndpoint( + this.remoteSigningEndpoint, + "eth_sign", + [this.address, messageHash] + ); + + return signature; + } +} + +interface JsonRpcRequest { + jsonrpc: string; + method: string; + params: any[]; + id: number; +} + +interface JsonRpcResponse { + jsonrpc: string; + result?: any; + error?: { + code: number; + message: string; + }; + id: number; +} + +async function callJsonRpcEndpoint( + url: string, + method: string, + params: any[] = [] +): Promise { + const request: JsonRpcRequest = { + jsonrpc: "2.0", + method, + params, + id: 1, // You might want to generate a unique id for each request + }; + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const jsonResponse: JsonRpcResponse = await response.json(); + + if (jsonResponse.error) { + throw new Error(`JSON-RPC error: ${jsonResponse.error.message}`); + } + + return jsonResponse.result; +} \ No newline at end of file