From cb1672cb2004bfe31ac6585d5e6a7ff67a3c55d3 Mon Sep 17 00:00:00 2001 From: David Murdoch Date: Thu, 15 Sep 2022 01:27:10 -0400 Subject: [PATCH 1/2] add merge support --- .../ethereum/block/src/runtime-block.ts | 23 ++-- src/chains/ethereum/ethereum/package.json | 1 - src/chains/ethereum/ethereum/src/api.ts | 2 + .../ethereum/ethereum/src/blockchain.ts | 27 +++- .../src/data-managers/block-manager.ts | 2 + src/chains/ethereum/ethereum/src/wallet.ts | 11 +- .../ethereum/tests/api/eth/call.test.ts | 3 +- .../ethereum/tests/contracts/Merge.sol | 16 +++ .../ethereum/ethereum/tests/merge.test.ts | 124 ++++++++++++++++++ .../ethereum/options/src/chain-options.ts | 3 +- .../ethereum/options/src/miner-options.ts | 5 +- .../ethereum/transaction/src/hardfork.ts | 3 +- src/chains/ethereum/transaction/src/params.ts | 6 +- src/chains/ethereum/utils/src/things/tags.ts | 6 +- src/packages/ganache/README.md | 2 +- 15 files changed, 201 insertions(+), 33 deletions(-) create mode 100644 src/chains/ethereum/ethereum/tests/contracts/Merge.sol create mode 100644 src/chains/ethereum/ethereum/tests/merge.test.ts diff --git a/src/chains/ethereum/block/src/runtime-block.ts b/src/chains/ethereum/block/src/runtime-block.ts index 47f454872d..4ba865d08e 100644 --- a/src/chains/ethereum/block/src/runtime-block.ts +++ b/src/chains/ethereum/block/src/runtime-block.ts @@ -1,10 +1,4 @@ -import { - Data, - Quantity, - BUFFER_EMPTY, - BUFFER_32_ZERO, - BUFFER_8_ZERO -} from "@ganache/utils"; +import { Data, Quantity, BUFFER_EMPTY, BUFFER_8_ZERO } from "@ganache/utils"; import { KECCAK256_RLP_ARRAY } from "@ethereumjs/util"; import { EthereumRawBlockHeader, serialize } from "./serialize"; import { Address } from "@ganache/ethereum-address"; @@ -88,7 +82,11 @@ export class RuntimeBlock { gasLimit: bigint; gasUsed: bigint; timestamp: bigint; - baseFeePerGas?: bigint; + mixHash: Buffer; + // prevRandao is mixHash, but for the merge and it must be + // given to the VM this way + prevRandao: Buffer; + baseFeePerGas: bigint | undefined; }; constructor( @@ -100,7 +98,8 @@ export class RuntimeBlock { timestamp: Quantity, difficulty: Quantity, previousBlockTotalDifficulty: Quantity, - baseFeePerGas?: bigint + mixHash: Buffer, + baseFeePerGas: bigint | undefined ) { const ts = timestamp.toBuffer(); const coinbaseBuffer = coinbase.toBuffer(); @@ -115,7 +114,9 @@ export class RuntimeBlock { gasLimit: Quantity.toBigInt(gasLimit), gasUsed: Quantity.toBigInt(gasUsed), timestamp: Quantity.toBigInt(ts), - baseFeePerGas: baseFeePerGas === undefined ? 0n : baseFeePerGas + baseFeePerGas: baseFeePerGas === undefined ? 0n : baseFeePerGas, + mixHash, + prevRandao: mixHash }; // When forking we might get a block that doesn't have a baseFeePerGas value, // but EIP-1559 might be active on our chain. We need to keep track on if @@ -153,7 +154,7 @@ export class RuntimeBlock { gasUsed === 0n ? BUFFER_EMPTY : Quantity.toBuffer(gasUsed), Quantity.toBuffer(header.timestamp), extraData.toBuffer(), - BUFFER_32_ZERO, // mixHash + header.mixHash, BUFFER_8_ZERO // nonce ]; if (this.serializeBaseFeePerGas && header.baseFeePerGas !== undefined) { diff --git a/src/chains/ethereum/ethereum/package.json b/src/chains/ethereum/ethereum/package.json index 995cdf1d52..b449cab373 100644 --- a/src/chains/ethereum/ethereum/package.json +++ b/src/chains/ethereum/ethereum/package.json @@ -74,7 +74,6 @@ "env-paths": "2.2.1", "eth-sig-util": "2.5.3", "ethereumjs-abi": "0.6.8", - "keccak": "3.0.2", "level": "8.0.0", "lodash.clonedeep": "4.5.0", "lru-cache": "6.0.0", diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 5957d5593b..e89988c41a 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -954,6 +954,7 @@ export default class EthereumApi implements Api { parentHeader.timestamp, options.miner.difficulty, parentHeader.totalDifficulty, + blockchain.getMixHash(parentHeader.parentHash.toBuffer()), 0n // no baseFeePerGas for estimates ); const runArgs: EstimateGasRunArgs = { @@ -2879,6 +2880,7 @@ export default class EthereumApi implements Api { parentHeader.timestamp, options.miner.difficulty, parentHeader.totalDifficulty, + blockchain.getMixHash(parentHeader.parentHash.toBuffer()), baseFeePerGasBigInt ); diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 41dfb13107..b17c4b7738 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -38,7 +38,8 @@ import { BUFFER_32_ZERO, BUFFER_256_ZERO, findInsertPosition, - KNOWN_CHAINIDS + KNOWN_CHAINIDS, + keccak } from "@ganache/utils"; import AccountManager from "./data-managers/account-manager"; import BlockManager from "./data-managers/block-manager"; @@ -265,6 +266,8 @@ export default class Blockchain extends Emittery { ); } + this.isPostMerge = this.common.gteHardfork("merge"); + const blocks = (this.blocks = await BlockManager.initialize( this, common, @@ -596,12 +599,18 @@ export default class Blockchain extends Emittery { minerOptions.blockGasLimit.toBuffer(), BUFFER_ZERO, Quantity.from(timestamp), - minerOptions.difficulty, + this.isPostMerge ? Quantity.Zero : minerOptions.difficulty, previousHeader.totalDifficulty, + this.getMixHash(previousBlock.hash().toBuffer()), Block.calcNextBaseFee(previousBlock) ); }; + getMixHash(data: Buffer) { + // mixHash is used as an RNG post merge hardfork + return this.isPostMerge ? keccak(data) : BUFFER_32_ZERO; + } + isStarted = () => { return this.#state === Status.started; }; @@ -767,8 +776,9 @@ export default class Blockchain extends Emittery { blockGasLimit.toBuffer(), BUFFER_ZERO, Quantity.from(timestamp), - minerOptions.difficulty, + this.isPostMerge ? Quantity.Zero : minerOptions.difficulty, fallbackBlock.header.totalDifficulty, + this.getMixHash(fallbackBlock.hash().toBuffer()), baseFeePerGas ); @@ -806,6 +816,7 @@ export default class Blockchain extends Emittery { const baseFeePerGas = this.common.isActivatedEIP(1559) ? Block.INITIAL_BASE_FEE_PER_GAS : undefined; + const genesis = new RuntimeBlock( rawBlockNumber, Data.from(BUFFER_32_ZERO), @@ -813,8 +824,11 @@ export default class Blockchain extends Emittery { blockGasLimit.toBuffer(), BUFFER_ZERO, Quantity.from(timestamp), - this.#options.miner.difficulty, + this.isPostMerge ? Quantity.Zero : this.#options.miner.difficulty, Quantity.Zero, // we start the totalDifficulty at 0 + // we use the initial trie root as the genesis block's mixHash as it + // is deterministic based on initial wallet conditions + this.isPostMerge ? keccak(this.trie.root()) : BUFFER_32_ZERO, baseFeePerGas ); @@ -1447,6 +1461,8 @@ export default class Blockchain extends Emittery { }; }; + isPostMerge: boolean; + #prepareNextBlock = ( targetBlock: Block, parentBlock: Block, @@ -1464,8 +1480,9 @@ export default class Blockchain extends Emittery { BUFFER_ZERO, // make sure we use the same timestamp as the target block targetBlock.header.timestamp, - this.#options.miner.difficulty, + this.isPostMerge ? Quantity.Zero : this.#options.miner.difficulty, parentBlock.header.totalDifficulty, + this.getMixHash(parentBlock.hash().toBuffer()), Block.calcNextBaseFee(parentBlock) ) as RuntimeBlock & { uncleHeaders: []; diff --git a/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts b/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts index 8322f6c326..45df7d15fa 100644 --- a/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts +++ b/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts @@ -149,6 +149,8 @@ export default class BlockManager extends Manager { getBlockByTag(tag: Tag) { switch (tag) { case "latest": + case "finalized": + case "safe": return this.latest; case "pending": // TODO: build a real pending block! diff --git a/src/chains/ethereum/ethereum/src/wallet.ts b/src/chains/ethereum/ethereum/src/wallet.ts index 0eb8ae9015..8126e113eb 100644 --- a/src/chains/ethereum/ethereum/src/wallet.ts +++ b/src/chains/ethereum/ethereum/src/wallet.ts @@ -17,7 +17,6 @@ import secp256k1, { SECP256K1_N } from "@ganache/secp256k1"; import { mnemonicToSeedSync } from "bip39"; import { alea } from "seedrandom"; import crypto from "crypto"; -import createKeccakHash from "keccak"; import { writeFileSync } from "fs"; import { EthereumInternalOptions } from "@ganache/ethereum-options"; import { Address } from "@ganache/ethereum-address"; @@ -373,9 +372,7 @@ export default class Wallet { cipher.update(privateKey.toBuffer()), cipher.final() ]); - const mac = createKeccakHash("keccak256") - .update(Buffer.concat([derivedKey.slice(16, 32), ciphertext])) - .digest(); + const mac = keccak(Buffer.concat([derivedKey.slice(16, 32), ciphertext])); return { crypto: { cipher: CIPHER, @@ -420,9 +417,9 @@ export default class Wallet { kdfParams.dklen, { ...kdfParams, N: kdfParams.n } ); - localMac = createKeccakHash("keccak256") - .update(Buffer.concat([derivedKey.slice(16, 32), ciphertext])) - .digest(); + localMac = keccak( + Buffer.concat([derivedKey.slice(16, 32), ciphertext]) + ); } catch { localMac = null; } diff --git a/src/chains/ethereum/ethereum/tests/api/eth/call.test.ts b/src/chains/ethereum/ethereum/tests/api/eth/call.test.ts index f08bfc80ad..7afeb16525 100644 --- a/src/chains/ethereum/ethereum/tests/api/eth/call.test.ts +++ b/src/chains/ethereum/ethereum/tests/api/eth/call.test.ts @@ -4,7 +4,7 @@ import { EthereumProvider } from "../../../src/provider"; import getProvider from "../../helpers/getProvider"; import compile, { CompileOutput } from "../../helpers/compile"; import { join } from "path"; -import { BUFFER_EMPTY, Data, Quantity } from "@ganache/utils"; +import { BUFFER_32_ZERO, BUFFER_EMPTY, Data, Quantity } from "@ganache/utils"; import { CallError } from "@ganache/ethereum-utils"; import Blockchain from "../../../src/blockchain"; import Wallet from "../../../src/wallet"; @@ -884,6 +884,7 @@ describe("api", () => { parentHeader.timestamp, Quantity.One, // difficulty parentHeader.totalDifficulty, + BUFFER_32_ZERO, parentHeader.baseFeePerGas.toBigInt() ); simTx = { diff --git a/src/chains/ethereum/ethereum/tests/contracts/Merge.sol b/src/chains/ethereum/ethereum/tests/contracts/Merge.sol new file mode 100644 index 0000000000..0b88e28145 --- /dev/null +++ b/src/chains/ethereum/ethereum/tests/contracts/Merge.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.11; + +contract Merge { + uint256 public difficulty; + + constructor() { + require(block.difficulty > 1); + difficulty = block.difficulty; + } + + function getCurrentDifficulty() public view returns (uint256) { + require(block.difficulty > 1); + return uint256(block.difficulty); + } +} diff --git a/src/chains/ethereum/ethereum/tests/merge.test.ts b/src/chains/ethereum/ethereum/tests/merge.test.ts new file mode 100644 index 0000000000..0c0b48e28a --- /dev/null +++ b/src/chains/ethereum/ethereum/tests/merge.test.ts @@ -0,0 +1,124 @@ +import assert from "assert"; +import { join } from "path"; +import { EthereumProvider } from "../src/provider"; +import compile from "./helpers/compile"; +import getProvider from "./helpers/getProvider"; + +describe("merge", () => { + let provider: EthereumProvider; + let accounts: string[]; + beforeEach("get provider", async () => { + provider = await getProvider({ + wallet: { seed: "temet nosce" }, + chain: { hardfork: "merge" } + }); + accounts = await provider.send("eth_accounts"); + }); + describe("safe", () => { + it("returns the safe block", async () => { + const safe = await provider.send("eth_getBlockByNumber", ["safe", true]); + const latest = await provider.send("eth_getBlockByNumber", [ + "latest", + true + ]); + assert.deepStrictEqual(safe, latest); + }); + }); + describe("finalized", () => { + it("returns the finalized block", async () => { + const finalized = await provider.send("eth_getBlockByNumber", [ + "finalized", + true + ]); + const latest = await provider.send("eth_getBlockByNumber", [ + "latest", + true + ]); + assert.deepStrictEqual(finalized, latest); + }); + }); + + describe("difficulty", () => { + let contract: ReturnType; + let contractAddress: string; + beforeEach("deploy contract", async () => { + contract = compile(join(__dirname, "./contracts/Merge.sol")); + const transaction = { + from: accounts[0], + data: contract.code, + gasLimit: "0x2fefd8" + }; + const txHash = await provider.send("eth_sendTransaction", [transaction]); + const receipt = await provider.send("eth_getTransactionReceipt", [ + txHash + ]); + contractAddress = receipt.contractAddress; + }); + it("uses the block's mixhash value as the PREVRANDAO opcode", async () => { + const block = await provider.send("eth_getBlockByNumber", [ + "latest", + false + ]); + assert.strictEqual(block.difficulty, "0x0"); + + const constructorResult = await provider.send("eth_call", [ + { + from: accounts[0], + to: contractAddress, + data: `0x${contract.contract.evm.methodIdentifiers["difficulty()"]}` + }, + block.number + ]); + + const txHash = await provider.send("eth_sendTransaction", [ + { + from: accounts[0], + to: contractAddress, + gasLimit: "0x2fefd8", + data: `0x${contract.contract.evm.methodIdentifiers["getCurrentDifficulty()"]}` + } + ]); + const receipt = await provider.send("eth_getTransactionReceipt", [ + txHash + ]); + // make sure it didn't revert, which is will do if `difficulty` is not > `1` + assert.strictEqual(receipt.status, "0x1"); + + const viewResult1 = await provider.send("eth_call", [ + { + from: accounts[0], + to: contractAddress, + data: `0x${contract.contract.evm.methodIdentifiers["getCurrentDifficulty()"]}` + }, + block.number + ]); + + // it shouldn't be all 0s for the merge: + assert.notStrictEqual( + block.mixHash, + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + + // the results should match + assert.strictEqual(constructorResult, block.mixHash); + assert.strictEqual(viewResult1, block.mixHash); + + const lastblock = await provider.send("eth_getBlockByNumber", [ + receipt.blockNumber, + false + ]); + assert.strictEqual(lastblock.difficulty, "0x0"); + const viewResult2 = await provider.send("eth_call", [ + { + from: accounts[0], + to: contractAddress, + data: `0x${contract.contract.evm.methodIdentifiers["getCurrentDifficulty()"]}` + }, + "latest" + ]); + // it changed + assert.notStrictEqual(viewResult1, viewResult2); + assert.strictEqual(viewResult2, lastblock.mixHash); + }).timeout(0); + }); +}); diff --git a/src/chains/ethereum/options/src/chain-options.ts b/src/chains/ethereum/options/src/chain-options.ts index a7e169e6af..331da984ec 100644 --- a/src/chains/ethereum/options/src/chain-options.ts +++ b/src/chains/ethereum/options/src/chain-options.ts @@ -11,7 +11,8 @@ const HARDFORKS = [ "berlin", "london", "arrowGlacier", - "grayGlacier" + "grayGlacier", + "merge" ] as const; export type Hardfork = Writeable>; diff --git a/src/chains/ethereum/options/src/miner-options.ts b/src/chains/ethereum/options/src/miner-options.ts index 670cb0dff5..f6246929f3 100644 --- a/src/chains/ethereum/options/src/miner-options.ts +++ b/src/chains/ethereum/options/src/miner-options.ts @@ -61,7 +61,7 @@ export type MinerConfig = { }; /** - * Sets the block difficulty + * Sets the block difficulty; value is always 0 after the merge hardfork * * @defaultValue 1 */ @@ -275,7 +275,8 @@ export const MinerOptions: Definitions = { }, difficulty: { normalize: Quantity.from, - cliDescription: "Sets the block difficulty.", + cliDescription: + "Sets the block difficulty. Value is always 0 after the merge hardfork.", default: () => Quantity.One, cliType: "string", cliCoerce: toBigIntOrString diff --git a/src/chains/ethereum/transaction/src/hardfork.ts b/src/chains/ethereum/transaction/src/hardfork.ts index a87411b87c..fe38d6346e 100644 --- a/src/chains/ethereum/transaction/src/hardfork.ts +++ b/src/chains/ethereum/transaction/src/hardfork.ts @@ -7,4 +7,5 @@ export type Hardfork = | "berlin" | "london" | "arrowGlacier" - | "grayGlacier"; + | "grayGlacier" + | "merge"; diff --git a/src/chains/ethereum/transaction/src/params.ts b/src/chains/ethereum/transaction/src/params.ts index 47d447ed91..b160ead18b 100644 --- a/src/chains/ethereum/transaction/src/params.ts +++ b/src/chains/ethereum/transaction/src/params.ts @@ -27,7 +27,8 @@ export const Params = { | "berlin" | "london" | "arrowGlacier" - | "grayGlacier", + | "grayGlacier" + | "merge", bigint >([ ["chainstart", 68n], @@ -43,7 +44,8 @@ export const Params = { ["berlin", 16n], ["london", 16n], ["arrowGlacier", 16n], - ["grayGlacier", 16n] + ["grayGlacier", 16n], + ["merge", 16n] ]), /** diff --git a/src/chains/ethereum/utils/src/things/tags.ts b/src/chains/ethereum/utils/src/things/tags.ts index 8152310cb3..909fee3cd0 100644 --- a/src/chains/ethereum/utils/src/things/tags.ts +++ b/src/chains/ethereum/utils/src/things/tags.ts @@ -1,5 +1,7 @@ export enum InternalTag { earliest = "earliest", + finalized = "finalized", + safe = "safe", latest = "latest", pending = "pending" } @@ -7,7 +9,9 @@ export enum InternalTag { export type Tag = keyof typeof InternalTag; export namespace Tag { - export const latest = "latest"; export const earliest = "earliest"; + export const safe = "safe"; + export const finalized = "finalized"; + export const latest = "latest"; export const pending = "pending"; } diff --git a/src/packages/ganache/README.md b/src/packages/ganache/README.md index cbe2f552a8..d7dac79852 100644 --- a/src/packages/ganache/README.md +++ b/src/packages/ganache/README.md @@ -259,7 +259,7 @@ Chain: -k, --chain.hardfork Set the hardfork rules for the EVM. deprecated aliases: --hardfork [string] [choices: "constantinople", "byzantium", "petersburg", "istanbul", "muirGlacier", "berlin", - london", "arrowGlacier", "grayGlacier"] [default: london] + "london", "arrowGlacier", "grayGlacier", "merge"] [default: "london"] --chain.vmErrorsOnRPCResponse Whether to report runtime errors from EVM code as RPC errors. [boolean] [default: false] From 1bb7dd1667156187d97ec466fa11e6b1e1a71b25 Mon Sep 17 00:00:00 2001 From: David Murdoch Date: Fri, 23 Sep 2022 16:52:22 -0400 Subject: [PATCH 2/2] prevent filecoin from running in tests in node.js 14.0.0 --- src/chains/filecoin/filecoin/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chains/filecoin/filecoin/package.json b/src/chains/filecoin/filecoin/package.json index 884aab6591..831e6e6a57 100644 --- a/src/chains/filecoin/filecoin/package.json +++ b/src/chains/filecoin/filecoin/package.json @@ -9,7 +9,7 @@ "homepage": "https://github.com/trufflesuite/ganache/tree/develop/src/filecoin#readme", "license": "MIT", "engines": { - "node": ">=12.17.0 <17.0.0", + "node": ">=12.17.0 <14.0.0 || >=14.0.1 <17.0.0", "npm": ">=6.14.4" }, "main": "lib/index.js",