Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade ethereumjs and use the new BlockBuilder API #1370

Merged
merged 4 commits into from
Apr 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/hardhat-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@
"@ethereumjs/block": "^3.2.0",
"@ethereumjs/blockchain": "^5.2.1",
"@ethereumjs/common": "^2.2.0",
"@ethereumjs/tx": "^3.1.1",
"@ethereumjs/vm": "^5.2.0",
"@ethereumjs/tx": "^3.1.2",
"@ethereumjs/vm": "^5.3.0",
"@sentry/node": "^5.18.1",
"@solidity-parser/parser": "^0.11.0",
"@types/bn.js": "^5.1.0",
Expand All @@ -96,7 +96,7 @@
"eth-sig-util": "^2.5.2",
"ethereum-cryptography": "^0.1.2",
"ethereumjs-abi": "^0.6.8",
"ethereumjs-util": "^7.0.9",
"ethereumjs-util": "^7.0.10",
"find-up": "^2.1.0",
"fp-ts": "1.19.3",
"fs-extra": "^7.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,9 @@ function isRpcHashString(u: unknown): u is string {
}

function isRpcAddressString(u: unknown): u is string {
return typeof u === "string" && safeIsValidAddress(u);
return typeof u === "string" && isValidAddress(u);
}

function isInteger(num: unknown): num is number {
return Number.isInteger(num);
}

function safeIsValidAddress(u: string) {
let isValid = false;
// This try catch should be here until this is released: https://github.com/ethereumjs/ethereumjs-monorepo/pull/1174
try {
isValid = isValidAddress(u);
} catch (e) {}
return isValid;
}
243 changes: 87 additions & 156 deletions packages/hardhat-core/src/internal/hardhat-network/provider/node.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Block, BlockHeader } from "@ethereumjs/block";
import { Block } from "@ethereumjs/block";
import Common from "@ethereumjs/common";
import { Transaction } from "@ethereumjs/tx";
import VM from "@ethereumjs/vm";
import Bloom from "@ethereumjs/vm/dist/bloom";
import { EVMResult, ExecResult } from "@ethereumjs/vm/dist/evm/evm";
import { ERROR } from "@ethereumjs/vm/dist/exceptions";
import {
generateTxReceipt,
PostByzantiumTxReceipt,
PreByzantiumTxReceipt,
RunBlockResult,
} from "@ethereumjs/vm/dist/runBlock";
import { RunTxResult } from "@ethereumjs/vm/dist/runTx";
import { DefaultStateManager, StateManager } from "@ethereumjs/vm/dist/state";
import chalk from "chalk";
import debug from "debug";
Expand All @@ -22,11 +22,9 @@ import {
ecsign,
hashPersonalMessage,
privateToAddress,
rlp,
toBuffer,
} from "ethereumjs-util";
import EventEmitter from "events";
import { BaseTrie as Trie } from "merkle-patricia-tree";

import { CompilerInput, CompilerOutput } from "../../../types";
import { HARDHAT_NETWORK_DEFAULT_GAS_PRICE } from "../../core/config/default-config";
Expand Down Expand Up @@ -299,10 +297,7 @@ export class HardhatNode extends EventEmitter {
return this._mineTransaction(tx);
}

public async mineBlock(
timestamp?: BN,
sentTxHash?: string
): Promise<MineBlockResult> {
public async mineBlock(timestamp?: BN): Promise<MineBlockResult> {
const [
blockTimestamp,
offsetShouldChange,
Expand All @@ -315,14 +310,12 @@ export class HardhatNode extends EventEmitter {
blockTimestamp.iaddn(1);
}

const previousRoot = await this._stateManager.getStateRoot();
let result: MineBlockResult;
try {
result = await this._mineBlockWithPendingTxs(blockTimestamp, sentTxHash);
result = await this._mineBlockWithPendingTxs(blockTimestamp);
} catch (err) {
await this._stateManager.setStateRoot(previousRoot);
if (err?.message.includes("sender doesn't have enough funds")) {
throw new InvalidInputError(err.message);
throw new InvalidInputError(err.message, err);
}

// Some network errors are HardhatErrors, and can end up here when forking
Expand Down Expand Up @@ -884,8 +877,8 @@ export class HardhatNode extends EventEmitter {
}

private async _mineTransaction(tx: Transaction): Promise<MineBlockResult> {
const txHash = await this._addPendingTransaction(tx);
return this.mineBlock(undefined, txHash);
await this._addPendingTransaction(tx);
return this.mineBlock();
}

private async _mineTransactionAndPending(
Expand Down Expand Up @@ -917,12 +910,12 @@ export class HardhatNode extends EventEmitter {
"Failed to mine transaction for unknown reason, this should never happen"
);
}
results.push(await this.mineBlock(undefined, txHash));
results.push(await this.mineBlock());
txReceipt = await this.getTransactionReceipt(txHash);
} while (txReceipt === undefined);

while (this._txPool.hasPendingTransactions()) {
results.push(await this.mineBlock(undefined, txHash));
results.push(await this.mineBlock());
}

return results;
Expand Down Expand Up @@ -973,141 +966,100 @@ export class HardhatNode extends EventEmitter {
}
}

/**
* Mines a new block with as many pending txs as possible, adding it to
* the VM's blockchain.
*
* This method reverts any modification to the state manager if it throws.
*/
private async _mineBlockWithPendingTxs(
blockTimestamp: BN,
sentTxHash?: string
blockTimestamp: BN
): Promise<MineBlockResult> {
let block = await this._getNextBlockTemplate(blockTimestamp);

const bloom = new Bloom();
const results: RunTxResult[] = [];
const receipts: TxReceipt[] = [];
const traces: GatherTracesResult[] = [];
const receiptTrie = new Trie();

const blockGasLimit = this.getBlockGasLimit();
const minTxFee = this._getMinimalTransactionFee();
const gasLeft = blockGasLimit.clone();
const pendingTxs = this._txPool.getPendingTransactions();
const txHeap = new TxPriorityHeap(pendingTxs);

let tx = txHeap.peek();

let cumulativeGasUsed = new BN(0);
while (gasLeft.gte(minTxFee) && tx !== undefined) {
const shouldThrow = sentTxHash === bufferToHex(tx.hash());

const txResult = await this._runTx(tx, block, gasLeft, shouldThrow);
if (txResult !== null) {
bloom.or(txResult.bloom);
results.push(txResult);

cumulativeGasUsed = cumulativeGasUsed.add(txResult.gasUsed);

const receipt = this._createReceipt(txResult, cumulativeGasUsed);
receipts.push(receipt);
await receiptTrie.put(
rlp.encode(receipts.length - 1),
rlp.encode(Object.values(receipt))
);

traces.push(await this._gatherTraces(txResult.execResult));
block.transactions.push(tx);
const parentBlock = await this.getLatestBlock();

gasLeft.isub(txResult.gasUsed);
txHeap.shift();
} else {
txHeap.pop();
}
tx = txHeap.peek();
}
const headerData = {
gasLimit: this.getBlockGasLimit(),
coinbase: this.getCoinbaseAddress(),
nonce: "0x0000000000000042",
timestamp: blockTimestamp,
};

await this._txPool.updatePendingAndQueued();
await this._assignBlockReward();
const blockBuilder = await this._vm.buildBlock({
parentBlock,
headerData,
blockOpts: { calcDifficultyFromHeader: parentBlock.header },
});

// The BlockHeader properties are readonly so
// the block has to be recreated here...
await block.genTxTrie();
try {
const traces: GatherTracesResult[] = [];

const blockGasLimit = this.getBlockGasLimit();
const minTxFee = this._getMinimalTransactionFee();
const pendingTxs = this._txPool.getPendingTransactions();
const txHeap = new TxPriorityHeap(pendingTxs);

let tx = txHeap.peek();

const results = [];
const receipts = [];

while (
blockGasLimit.sub(blockBuilder.gasUsed).gte(minTxFee) &&
tx !== undefined
) {
if (tx.gasLimit.gt(blockGasLimit.sub(blockBuilder.gasUsed))) {
txHeap.pop();
} else {
const txResult = await blockBuilder.addTransaction(tx);
const { txReceipt } = await generateTxReceipt.bind(this._vm)(
tx,
txResult,
blockBuilder.gasUsed
);

const header = BlockHeader.fromHeaderData({
...block.toJSON().header,
gasUsed: toBuffer(blockGasLimit.sub(gasLeft)),
stateRoot: await this._stateManager.getStateRoot(),
bloom: bloom.bitvector,
receiptTrie: receiptTrie.root,
transactionsTrie: block.txTrie.root,
});
traces.push(await this._gatherTraces(txResult.execResult));
results.push(txResult);
receipts.push(txReceipt);

block = new Block(header, block.transactions, block.uncleHeaders, {
common: this._vm._common,
});
txHeap.shift();
}

return {
block,
blockResult: {
results,
receipts,
stateRoot: block.header.stateRoot,
logsBloom: block.header.bloom,
receiptRoot: block.header.receiptTrie,
gasUsed: block.header.gasUsed,
},
traces,
};
}
tx = txHeap.peek();
}

private async _runTx(
tx: Transaction,
block: Block,
gasLeft: BN,
shouldThrow: boolean
): Promise<RunTxResult | null> {
const preRunStateRoot = await this._stateManager.getStateRoot();
try {
const txGasLimit = new BN(tx.gasLimit);
if (txGasLimit.gt(gasLeft)) {
await this._stateManager.setStateRoot(preRunStateRoot);
return null;
// This workarounds a bug in BlockBuilder#build
// tslint:disable-next-line:no-string-literal
if (blockBuilder["checkpointed"] !== true) {
await this._vm.stateManager.checkpoint();
}

return this._vm.runTx({ tx, block, skipBlockGasLimitValidation: true });
const block = await blockBuilder.build();

await this._txPool.updatePendingAndQueued();

return {
block,
blockResult: {
results,
receipts,
stateRoot: block.header.stateRoot,
logsBloom: block.header.bloom,
receiptRoot: block.header.receiptTrie,
gasUsed: block.header.gasUsed,
},
traces,
};
} catch (err) {
if (shouldThrow) {
throw err;
}
return null;
await blockBuilder.revert();
throw err;
}
}

private async _assignBlockReward() {
const minerAddress = this.getCoinbaseAddress();
const miner = await this._stateManager.getAccount(minerAddress);
const blockReward = this._getBlockReward();
miner.balance = miner.balance.add(blockReward);
await this._stateManager.putAccount(minerAddress, miner);
}

private _getMinimalTransactionFee(): BN {
// Typically 21_000 gas
return new BN(this._vm._common.param("gasPrices", "tx"));
}

private _getBlockReward(): BN {
return new BN(this._vm._common.param("pow", "minerReward"));
}

private _createReceipt(
txResult: RunTxResult,
cumulativeGasUsed: BN
): TxReceipt {
return {
status: txResult.execResult.exceptionError === undefined ? 1 : 0, // Receipts have a 0 as status on error
gasUsed: toBuffer(cumulativeGasUsed),
bitvector: txResult.bloom.bitvector,
logs: txResult.execResult.logs ?? [],
};
}

private async _getFakeTransaction(
txParams: TransactionParams
): Promise<Transaction> {
Expand Down Expand Up @@ -1288,30 +1240,6 @@ export class HardhatNode extends EventEmitter {
return [blockTimestamp, offsetShouldChange, newOffset];
}

private async _getNextBlockTemplate(timestamp: BN): Promise<Block> {
const latestBlock = await this.getLatestBlock();

const headerData = {
gasLimit: this.getBlockGasLimit(),
number: latestBlock.header.number.addn(1),
parentHash: latestBlock.hash(),
coinbase: this.getCoinbaseAddress(),
nonce: "0x0000000000000042",
timestamp,
};

const block = Block.fromBlockData(
{
header: {
...headerData,
},
},
{ common: this._vm._common, calcDifficultyFromHeader: latestBlock.header }
);

return block;
}

private async _resetNextBlockTimestamp() {
this.setNextBlockTimestamp(new BN(0));
}
Expand Down Expand Up @@ -1339,13 +1267,16 @@ export class HardhatNode extends EventEmitter {
return this._localAccounts.get(senderAddress)!;
}

/**
* Saves a block as successfully run. This method requires that the block
* was added to the blockchain.
*/
private async _saveBlockAsSuccessfullyRun(
block: Block,
runBlockResult: RunBlockResult
) {
const receipts = getRpcReceipts(block, runBlockResult);

await this._blockchain.putBlock(block);
this._blockchain.addTransactionReceipts(receipts);

const td = await this.getBlockTotalDifficulty(block);
Expand Down
Loading