diff --git a/.changeset/chilled-cooks-listen.md b/.changeset/chilled-cooks-listen.md new file mode 100644 index 0000000000..e35572dcd9 --- /dev/null +++ b/.changeset/chilled-cooks-listen.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Add support for the `personal_sign RPC method to Hardhat Network. diff --git a/packages/hardhat-core/src/internal/core/errors-list.ts b/packages/hardhat-core/src/internal/core/errors-list.ts index 93ce6b8c62..15bfe4474a 100644 --- a/packages/hardhat-core/src/internal/core/errors-list.ts +++ b/packages/hardhat-core/src/internal/core/errors-list.ts @@ -360,6 +360,15 @@ Please double check your transactions' parameters.`, Please double check your transactions' parameters.`, shouldBeReported: false, }, + PERSONALSIGN_MISSING_ADDRESS_PARAM: { + number: 116, + message: 'Missing "address" param when calling personal_sign.', + title: "Missing `address` param when calling personal_sign.", + description: `You called \`personal_sign\` with incorrect parameters. + +Please check that you are sending an \`address\` parameter.`, + shouldBeReported: false, + }, }, TASK_DEFINITIONS: { PARAM_AFTER_VARIADIC: { diff --git a/packages/hardhat-core/src/internal/core/providers/accounts.ts b/packages/hardhat-core/src/internal/core/providers/accounts.ts index f97daebdd6..287bdb4465 100644 --- a/packages/hardhat-core/src/internal/core/providers/accounts.ts +++ b/packages/hardhat-core/src/internal/core/providers/accounts.ts @@ -74,6 +74,25 @@ export class LocalAccountsProvider extends ProviderWrapperWithChainId { } } + if (args.method === "personal_sign") { + if (params.length > 0) { + const [data, address] = validateParams(params, rpcData, rpcAddress); + + if (data !== undefined) { + if (address === undefined) { + throw new HardhatError( + ERRORS.NETWORK.PERSONALSIGN_MISSING_ADDRESS_PARAM + ); + } + + const privateKey = this._getPrivateKeyForAddress(address); + const messageHash = hashPersonalMessage(toBuffer(data)); + const signature = ecsign(messageHash, privateKey); + return toRpcSig(signature.v, signature.r, signature.s); + } + } + } + if (args.method === "eth_signTypedData_v4") { const [address, data] = validateParams(params, rpcAddress, t.any); diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/modules/personal.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/modules/personal.ts new file mode 100644 index 0000000000..753f68414d --- /dev/null +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/modules/personal.ts @@ -0,0 +1,39 @@ +import { Address, toRpcSig } from "ethereumjs-util"; +import { rpcAddress, rpcData } from "../../../core/jsonrpc/types/base-types"; +import { validateParams } from "../../../core/jsonrpc/types/input/validation"; +import { MethodNotFoundError } from "../../../core/providers/errors"; +import { HardhatNode } from "../node"; + +/* eslint-disable @nomiclabs/hardhat-internal-rules/only-hardhat-error */ + +export class PersonalModule { + constructor(private readonly _node: HardhatNode) {} + + public async processRequest( + method: string, + params: any[] = [] + ): Promise { + switch (method) { + case "personal_sign": { + return this._signAction(...this._signParams(params)); + } + } + + throw new MethodNotFoundError(`Method ${method} not found`); + } + + // personal_sign + + private _signParams(params: any[]): [Buffer, Buffer] { + return validateParams(params, rpcData, rpcAddress); + } + + private async _signAction(data: Buffer, address: Buffer): Promise { + const signature = await this._node.signPersonalMessage( + new Address(address), + data + ); + + return toRpcSig(signature.v, signature.r, signature.s); + } +} diff --git a/packages/hardhat-core/src/internal/hardhat-network/provider/provider.ts b/packages/hardhat-core/src/internal/hardhat-network/provider/provider.ts index 17b08b640e..ca5e493477 100644 --- a/packages/hardhat-core/src/internal/hardhat-network/provider/provider.ts +++ b/packages/hardhat-core/src/internal/hardhat-network/provider/provider.ts @@ -32,6 +32,7 @@ import { EthModule } from "./modules/eth"; import { EvmModule } from "./modules/evm"; import { HardhatModule } from "./modules/hardhat"; import { ModulesLogger } from "./modules/logger"; +import { PersonalModule } from "./modules/personal"; import { NetModule } from "./modules/net"; import { Web3Module } from "./modules/web3"; import { HardhatNode } from "./node"; @@ -65,6 +66,7 @@ export class HardhatNetworkProvider private _evmModule?: EvmModule; private _hardhatModule?: HardhatModule; private _debugModule?: DebugModule; + private _personalModule?: PersonalModule; private readonly _mutex = new Mutex(); constructor( @@ -205,6 +207,10 @@ export class HardhatNetworkProvider return this._debugModule!.processRequest(method, params); } + if (method.startsWith("personal_")) { + return this._personalModule!.processRequest(method, params); + } + throw new MethodNotFoundError(`Method ${method} not found`); } @@ -265,6 +271,7 @@ export class HardhatNetworkProvider this._experimentalHardhatNetworkMessageTraceHooks ); this._debugModule = new DebugModule(node); + this._personalModule = new PersonalModule(node); this._forwardNodeEvents(node); } diff --git a/packages/hardhat-core/test/internal/core/providers/accounts.ts b/packages/hardhat-core/test/internal/core/providers/accounts.ts index f1f5d388ec..fd4c816baf 100644 --- a/packages/hardhat-core/test/internal/core/providers/accounts.ts +++ b/packages/hardhat-core/test/internal/core/providers/accounts.ts @@ -588,6 +588,50 @@ describe("Local accounts provider", () => { ]); }); }); + + describe("personal_sign", () => { + it("Should be compatible with geth's implementation", async () => { + // This test was created by using Geth 1.10.12-unstable and calling personal_sign + + const provider = new LocalAccountsProvider(mock, [ + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + ]); + + const result = await provider.request({ + method: "personal_sign", + params: [ + "0x5417aa2a18a44da0675524453ff108c545382f0d7e26605c56bba47c21b5e979", + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + ], + }); + + assert.equal( + result, + "0x9c73dd4937a37eecab3abb54b74b6ec8e500080431d36afedb1726624587ee6710296e10c1194dded7376f13ff03ef6c9e797eb86bae16c20c57776fc69344271c" + ); + }); + + it("Should be compatible with metamask's implementation", async () => { + // This test was created by using Metamask 10.3.0 + + const provider = new LocalAccountsProvider(mock, [ + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + ]); + + const result = (await provider.request({ + method: "personal_sign", + params: [ + "0x7699f568ecd7753e6ddf75a42fa4c2cc86cbbdc704c9eb1a6b6d4b9d8b8d1519", + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + ], + })) as string; + + assert.equal( + result, + "0x2875e4206c9fe3b229291c81f95cc4f421e2f4d3e023f5b4041daa56ab4000977010b47a3c01036ec8a6a0872aec2ab285150f003d01b0d8da60c1cceb9154181c" + ); + }); + }); }); describe("hdwallet provider", () => { diff --git a/packages/hardhat-core/test/internal/hardhat-network/helpers/providers.ts b/packages/hardhat-core/test/internal/hardhat-network/helpers/providers.ts index ab3f5cf0b5..e12887ac84 100644 --- a/packages/hardhat-core/test/internal/hardhat-network/helpers/providers.ts +++ b/packages/hardhat-core/test/internal/hardhat-network/helpers/providers.ts @@ -35,6 +35,11 @@ export const DEFAULT_ACCOUNTS = [ "0xe331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd10b", balance: new BN(10).pow(new BN(21)), }, + { + privateKey: + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + balance: new BN(10).pow(new BN(21)), + }, ]; export const DEFAULT_ACCOUNTS_ADDRESSES = DEFAULT_ACCOUNTS.map((account) => bufferToHex(privateToAddress(toBuffer(account.privateKey))).toLowerCase() diff --git a/packages/hardhat-core/test/internal/hardhat-network/provider/modules/personal.ts b/packages/hardhat-core/test/internal/hardhat-network/provider/modules/personal.ts new file mode 100644 index 0000000000..4720d81c08 --- /dev/null +++ b/packages/hardhat-core/test/internal/hardhat-network/provider/modules/personal.ts @@ -0,0 +1,55 @@ +import { assert } from "chai"; + +import { workaroundWindowsCiFailures } from "../../../../utils/workaround-windows-ci-failures"; +import { setCWD } from "../../helpers/cwd"; +import { PROVIDERS } from "../../helpers/providers"; + +describe("Personal module", function () { + PROVIDERS.forEach(({ name, useProvider, isFork }) => { + if (isFork) { + this.timeout(50000); + } + + workaroundWindowsCiFailures.call(this, { isFork }); + + describe(`${name} provider`, function () { + setCWD(); + useProvider(); + + describe("personal_sign", async function () { + it("Should be compatible with geth's implementation", async function () { + // This test was created by using Geth 1.10.12-unstable and calling personal_sign + const result = await this.provider.request({ + method: "personal_sign", + params: [ + "0x5417aa2a18a44da0675524453ff108c545382f0d7e26605c56bba47c21b5e979", + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + ], + }); + + assert.equal( + result, + "0x9c73dd4937a37eecab3abb54b74b6ec8e500080431d36afedb1726624587ee6710296e10c1194dded7376f13ff03ef6c9e797eb86bae16c20c57776fc69344271c" + ); + }); + + it("Should be compatible with metamask's implementation", async function () { + // This test was created by using Metamask 10.3.0 + + const result = (await this.provider.request({ + method: "personal_sign", + params: [ + "0x7699f568ecd7753e6ddf75a42fa4c2cc86cbbdc704c9eb1a6b6d4b9d8b8d1519", + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + ], + })) as string; + + assert.equal( + result, + "0x2875e4206c9fe3b229291c81f95cc4f421e2f4d3e023f5b4041daa56ab4000977010b47a3c01036ec8a6a0872aec2ab285150f003d01b0d8da60c1cceb9154181c" + ); + }); + }); + }); + }); +});