From eb46abff1cc88fa47ddb84004eb36474d3031319 Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Wed, 7 Apr 2021 18:48:59 -0300 Subject: [PATCH 1/2] Add support for EIP-2930 txs to LocalAccountsProvider --- .../src/internal/core/providers/accounts.ts | 33 ++++++-- .../test/internal/core/providers/accounts.ts | 84 ++++++++++++++++++- 2 files changed, 109 insertions(+), 8 deletions(-) diff --git a/packages/hardhat-core/src/internal/core/providers/accounts.ts b/packages/hardhat-core/src/internal/core/providers/accounts.ts index 7bd851759c..d56fb7d9f6 100644 --- a/packages/hardhat-core/src/internal/core/providers/accounts.ts +++ b/packages/hardhat-core/src/internal/core/providers/accounts.ts @@ -1,4 +1,3 @@ -import { Transaction as TransactionT } from "@ethereumjs/tx"; import { BN } from "ethereumjs-util"; import * as t from "io-ts"; @@ -188,9 +187,11 @@ export class LocalAccountsProvider extends ProviderWrapperWithChainId { chainId: number, privateKey: Buffer ): Promise { - const chains = await import("@ethereumjs/common/dist/chains"); + const { chains } = await import("@ethereumjs/common/dist/chains"); - const { Transaction } = await import("@ethereumjs/tx"); + const { AccessListEIP2930Transaction, Transaction } = await import( + "@ethereumjs/tx" + ); const { default: Common } = await import("@ethereumjs/common"); @@ -200,18 +201,36 @@ export class LocalAccountsProvider extends ProviderWrapperWithChainId { }; const common = - chains.chains.names[chainId] !== undefined - ? new Common({ chain: chainId }) + chains.names[chainId] !== undefined + ? new Common({ chain: chainId, hardfork: "berlin" }) : Common.forCustomChain( "mainnet", { chainId, networkId: chainId, }, - "istanbul" + "berlin" ); - const transaction = Transaction.fromTxData(txData, { common }); + let transaction; + if (txData.accessList !== undefined) { + // we convert the access list to the type + // that AccessListEIP2930Transaction expects + const accessList = txData.accessList.map( + ({ address, storageKeys }) => + [address, storageKeys] as [Buffer, Buffer[]] + ); + + transaction = AccessListEIP2930Transaction.fromTxData( + { + ...txData, + accessList, + }, + { common } + ); + } else { + transaction = Transaction.fromTxData(txData, { common }); + } const signedTransaction = transaction.sign(privateKey); diff --git a/packages/hardhat-core/test/internal/core/providers/accounts.ts b/packages/hardhat-core/test/internal/core/providers/accounts.ts index 30d6bd8f47..189fd02b54 100644 --- a/packages/hardhat-core/test/internal/core/providers/accounts.ts +++ b/packages/hardhat-core/test/internal/core/providers/accounts.ts @@ -23,6 +23,8 @@ function privateKeyToAddress(privateKey: string): string { return bufferToHex(privateToAddress(toBuffer(privateKey))).toLowerCase(); } +const MOCK_PROVIDER_CHAIN_ID = 123; + describe("Local accounts provider", () => { let mock: MockedProvider; let wrapper: EIP1193Provider; @@ -35,7 +37,10 @@ describe("Local accounts provider", () => { beforeEach(() => { mock = new MockedProvider(); - mock.setReturnValue("net_version", numberToRpcQuantity(123)); + mock.setReturnValue( + "net_version", + numberToRpcQuantity(MOCK_PROVIDER_CHAIN_ID) + ); mock.setReturnValue("eth_getTransactionCount", numberToRpcQuantity(0x8)); mock.setReturnValue("eth_accounts", []); @@ -166,6 +171,83 @@ describe("Local accounts provider", () => { assert.equal(mock.getNumberOfCalls("eth_getTransactionCount"), 1); }); + it("should send access list transactions", async () => { + await wrapper.request({ + method: "eth_sendTransaction", + params: [ + { + from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToRpcQuantity(30000), + gasPrice: numberToRpcQuantity(1), + nonce: numberToRpcQuantity(0), + value: numberToRpcQuantity(1), + chainId: numberToRpcQuantity(MOCK_PROVIDER_CHAIN_ID), + accessList: [ + { + address: "0x57d7aD4D3F0C74e3766874CF06fA1DC23C21f7E8", + storageKeys: [ + "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", + ], + }, + ], + }, + ], + }); + + const rawTransaction = mock.getLatestParams("eth_sendRawTransaction")[0]; + + // this is a valid raw EIP_2930 tx + // checked in a local hardhat node, where the sender account + // had funds and the chain id was 123 + const expectedRaw = + "0x01f89a7b800182753094b5bc06d4548a3ac17d72b372ae1e416bf65b8e" + + "ad0180f838f79457d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8e1a0a5" + + "0e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394" + + "ec80a02b2fca5e2cf3569d29693e965f045529efa6a54bf0ab11104dd4ea" + + "8b2ca3daf7a06025c30f36a179a09b9952e025632a65f220ec385eccd23a" + + "1fb952976eace481"; + + assert.equal(rawTransaction, expectedRaw); + }); + + it("should add the chainId value if it's missing", async () => { + await wrapper.request({ + method: "eth_sendTransaction", + params: [ + { + from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToRpcQuantity(30000), + gasPrice: numberToRpcQuantity(1), + nonce: numberToRpcQuantity(0), + value: numberToRpcQuantity(1), + accessList: [ + { + address: "0x57d7aD4D3F0C74e3766874CF06fA1DC23C21f7E8", + storageKeys: [ + "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", + ], + }, + ], + }, + ], + }); + + const rawTransaction = mock.getLatestParams("eth_sendRawTransaction")[0]; + + // see previous test + const expectedRaw = + "0x01f89a7b800182753094b5bc06d4548a3ac17d72b372ae1e416bf65b8e" + + "ad0180f838f79457d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8e1a0a5" + + "0e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394" + + "ec80a02b2fca5e2cf3569d29693e965f045529efa6a54bf0ab11104dd4ea" + + "8b2ca3daf7a06025c30f36a179a09b9952e025632a65f220ec385eccd23a" + + "1fb952976eace481"; + + assert.equal(rawTransaction, expectedRaw); + }); + describe("eth_sign", () => { it("Should be compatible with parity's implementation", async () => { // This test was created by using Parity Ethereum From 4753312ae84577ce724f8420baba0ca2f43265dd Mon Sep 17 00:00:00 2001 From: Franco Victorio Date: Thu, 8 Apr 2021 11:04:19 -0300 Subject: [PATCH 2/2] Check that the raw tx matches the sent tx values --- .../test/internal/core/providers/accounts.ts | 103 ++++++++++++------ 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/packages/hardhat-core/test/internal/core/providers/accounts.ts b/packages/hardhat-core/test/internal/core/providers/accounts.ts index 189fd02b54..68c660308f 100644 --- a/packages/hardhat-core/test/internal/core/providers/accounts.ts +++ b/packages/hardhat-core/test/internal/core/providers/accounts.ts @@ -1,3 +1,5 @@ +import Common from "@ethereumjs/common"; +import { AccessListEIP2930Transaction } from "@ethereumjs/tx"; import { assert } from "chai"; import { bufferToHex, privateToAddress, toBuffer } from "ethereumjs-util"; @@ -172,27 +174,26 @@ describe("Local accounts provider", () => { }); it("should send access list transactions", async () => { - await wrapper.request({ - method: "eth_sendTransaction", - params: [ + const tx = { + from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToRpcQuantity(30000), + gasPrice: numberToRpcQuantity(1), + nonce: numberToRpcQuantity(0), + value: numberToRpcQuantity(1), + chainId: numberToRpcQuantity(MOCK_PROVIDER_CHAIN_ID), + accessList: [ { - from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToRpcQuantity(30000), - gasPrice: numberToRpcQuantity(1), - nonce: numberToRpcQuantity(0), - value: numberToRpcQuantity(1), - chainId: numberToRpcQuantity(MOCK_PROVIDER_CHAIN_ID), - accessList: [ - { - address: "0x57d7aD4D3F0C74e3766874CF06fA1DC23C21f7E8", - storageKeys: [ - "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", - ], - }, + address: "0x57d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8", + storageKeys: [ + "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", ], }, ], + }; + await wrapper.request({ + method: "eth_sendTransaction", + params: [tx], }); const rawTransaction = mock.getLatestParams("eth_sendRawTransaction")[0]; @@ -209,29 +210,30 @@ describe("Local accounts provider", () => { "1fb952976eace481"; assert.equal(rawTransaction, expectedRaw); + + validateRawEIP2930Transaction(expectedRaw, tx); }); it("should add the chainId value if it's missing", async () => { - await wrapper.request({ - method: "eth_sendTransaction", - params: [ + const tx = { + from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", + gas: numberToRpcQuantity(30000), + gasPrice: numberToRpcQuantity(1), + nonce: numberToRpcQuantity(0), + value: numberToRpcQuantity(1), + accessList: [ { - from: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - to: "0xb5bc06d4548a3ac17d72b372ae1e416bf65b8ead", - gas: numberToRpcQuantity(30000), - gasPrice: numberToRpcQuantity(1), - nonce: numberToRpcQuantity(0), - value: numberToRpcQuantity(1), - accessList: [ - { - address: "0x57d7aD4D3F0C74e3766874CF06fA1DC23C21f7E8", - storageKeys: [ - "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", - ], - }, + address: "0x57d7ad4d3f0c74e3766874cf06fa1dc23c21f7e8", + storageKeys: [ + "0xa50e92910457911e0e22d6dd1672f440a37b590b231d8309101255290f5394ec", ], }, ], + }; + await wrapper.request({ + method: "eth_sendTransaction", + params: [tx], }); const rawTransaction = mock.getLatestParams("eth_sendRawTransaction")[0]; @@ -246,6 +248,8 @@ describe("Local accounts provider", () => { "1fb952976eace481"; assert.equal(rawTransaction, expectedRaw); + + validateRawEIP2930Transaction(expectedRaw, tx); }); describe("eth_sign", () => { @@ -518,3 +522,36 @@ describe("Sender providers", () => { }); }); }); + +/** + * Validate that `rawTx` is an EIP-2930 transaction that has + * the same values as `tx` + */ +function validateRawEIP2930Transaction(rawTx: string, tx: any) { + const common = Common.forCustomChain( + "mainnet", + { chainId: MOCK_PROVIDER_CHAIN_ID }, + "berlin" + ); + + const sentTx = AccessListEIP2930Transaction.fromSerializedTx( + toBuffer(rawTx), + { common } + ); + + const accessList = sentTx.accessList.map(([address, storageKeys]) => { + return { + address: bufferToHex(address), + storageKeys: storageKeys.map(bufferToHex), + }; + }); + + assert.equal(sentTx.getSenderAddress().toString(), tx.from); + assert.equal(sentTx.to?.toString(), tx.to); + + assert.equal(numberToRpcQuantity(sentTx.gasLimit), tx.gas); + assert.equal(numberToRpcQuantity(sentTx.gasPrice), tx.gasPrice); + assert.equal(numberToRpcQuantity(sentTx.nonce), tx.nonce); + assert.equal(numberToRpcQuantity(sentTx.value), tx.value); + assert.deepEqual(accessList, tx.accessList); +}