UNPKG

@nomiclabs/buidler

Version:

Buidler is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

1,650 lines (1,365 loc) 46.4 kB
import VM from "@nomiclabs/ethereumjs-vm"; import Bloom from "@nomiclabs/ethereumjs-vm/dist/bloom"; import { EVMResult, ExecResult } from "@nomiclabs/ethereumjs-vm/dist/evm/evm"; import { ERROR } from "@nomiclabs/ethereumjs-vm/dist/exceptions"; import { RunBlockResult } from "@nomiclabs/ethereumjs-vm/dist/runBlock"; import { StateManager } from "@nomiclabs/ethereumjs-vm/dist/state"; import PStateManager from "@nomiclabs/ethereumjs-vm/dist/state/promisified"; import chalk from "chalk"; import debug from "debug"; import Account from "ethereumjs-account"; import Block from "ethereumjs-block"; import Common from "ethereumjs-common"; import { FakeTransaction, Transaction } from "ethereumjs-tx"; import { BN, bufferToHex, ECDSASignature, ecsign, hashPersonalMessage, privateToAddress, toBuffer, } from "ethereumjs-util"; import EventEmitter from "events"; import Trie from "merkle-patricia-tree/secure"; import { promisify } from "util"; import { BUIDLEREVM_DEFAULT_GAS_PRICE } from "../../core/config/default-config"; import { getUserConfigPath } from "../../core/project-structure"; import { Reporter } from "../../sentry/reporter"; import { dateToTimestampSeconds, getDifferenceInSeconds, } from "../../util/date"; import { createModelsAndDecodeBytecodes } from "../stack-traces/compiler-to-model"; import { CompilerInput, CompilerOutput } from "../stack-traces/compiler-types"; import { ConsoleLogger } from "../stack-traces/consoleLogger"; import { ContractsIdentifier } from "../stack-traces/contracts-identifier"; import { MessageTrace } from "../stack-traces/message-trace"; import { decodeRevertReason } from "../stack-traces/revert-reasons"; import { encodeSolidityStackTrace, SolidityError, } from "../stack-traces/solidity-errors"; import { SolidityStackTrace, StackTraceEntryType, } from "../stack-traces/solidity-stack-trace"; import { SolidityTracer } from "../stack-traces/solidityTracer"; import { VmTraceDecoder } from "../stack-traces/vm-trace-decoder"; import { VMTracer } from "../stack-traces/vm-tracer"; import { Blockchain } from "./blockchain"; import { InternalError, InvalidInputError, TransactionExecutionError, } from "./errors"; import { bloomFilter, Filter, filterLogs, LATEST_BLOCK, Type } from "./filter"; import { getRpcBlock, getRpcLog, RpcLogOutput } from "./output"; import { getCurrentTimestamp } from "./utils"; const log = debug("buidler:core:buidler-evm:node"); // This library's types are wrong, they don't type check // tslint:disable-next-line no-var-requires const ethSigUtil = require("eth-sig-util"); export type Block = any; export interface GenesisAccount { privateKey: string; balance: string | number | BN; } export const COINBASE_ADDRESS = toBuffer( "0xc014ba5ec014ba5ec014ba5ec014ba5ec014ba5e" ); export interface CallParams { to: Buffer; from: Buffer; gasLimit: BN; gasPrice: BN; value: BN; data: Buffer; } export interface TransactionParams { to: Buffer; from: Buffer; gasLimit: BN; gasPrice: BN; value: BN; data: Buffer; nonce: BN; } export interface FilterParams { fromBlock: BN; toBlock: BN; addresses: Buffer[]; normalizedTopics: Array<Array<Buffer | null> | null>; } export interface TxReceipt { status: 0 | 1; gasUsed: Buffer; bitvector: Buffer; logs: RpcLogOutput[]; } export interface TxBlockResult { receipt: TxReceipt; createAddresses: Buffer | undefined; bloomBitvector: Buffer; } // tslint:disable only-buidler-error export interface SolidityTracerOptions { solidityVersion: string; compilerInput: CompilerInput; compilerOutput: CompilerOutput; } interface Snapshot { id: number; date: Date; latestBlock: Block; stateRoot: Buffer; blockTimeOffsetSeconds: BN; nextBlockTimestamp: BN; transactionByHash: Map<string, Transaction>; transactionHashToBlockHash: Map<string, string>; blockHashToTxBlockResults: Map<string, TxBlockResult[]>; blockHashToTotalDifficulty: Map<string, BN>; } export class BuidlerNode extends EventEmitter { public static async create( hardfork: string, networkName: string, chainId: number, networkId: number, blockGasLimit: number, genesisAccounts: GenesisAccount[] = [], solidityVersion?: string, allowUnlimitedContractSize?: boolean, initialDate?: Date, compilerInput?: CompilerInput, compilerOutput?: CompilerOutput ): Promise<[Common, BuidlerNode]> { const stateTrie = new Trie(); const putIntoStateTrie = promisify(stateTrie.put.bind(stateTrie)); for (const acc of genesisAccounts) { let balance: BN; if ( typeof acc.balance === "string" && acc.balance.toLowerCase().startsWith("0x") ) { balance = new BN(toBuffer(acc.balance)); } else { balance = new BN(acc.balance); } const account = new Account({ balance }); const pk = toBuffer(acc.privateKey); const address = privateToAddress(pk); await putIntoStateTrie(address, account.serialize()); } // Mimic precompiles activation for (let i = 1; i <= 8; i++) { await putIntoStateTrie( new BN(i).toArrayLike(Buffer, "be", 20), new Account().serialize() ); } const initialBlockTimestamp = initialDate !== undefined ? dateToTimestampSeconds(initialDate) : getCurrentTimestamp(); const common = Common.forCustomChain( "mainnet", { chainId, networkId, name: networkName, genesis: { timestamp: `0x${initialBlockTimestamp.toString(16)}`, hash: "0x", gasLimit: blockGasLimit, difficulty: 1, nonce: "0x42", extraData: "0x1234", stateRoot: bufferToHex(stateTrie.root), }, }, hardfork ); const stateManager = new StateManager({ common: common as any, // TS error because of a version mismatch trie: stateTrie, }); const blockchain = new Blockchain(); const vm = new VM({ common: common as any, // TS error because of a version mismatch activatePrecompiles: true, stateManager, blockchain: blockchain as any, allowUnlimitedContractSize, }); const genesisBlock = new Block(null, { common }); genesisBlock.setGenesisParams(); await new Promise((resolve) => { blockchain.putBlock(genesisBlock, () => resolve()); }); const node = new BuidlerNode( vm, blockchain, genesisAccounts.map((acc) => toBuffer(acc.privateKey)), new BN(blockGasLimit), genesisBlock, solidityVersion, initialDate, compilerInput, compilerOutput ); return [common, node]; } private readonly _common: Common; private readonly _stateManager: PStateManager; private readonly _accountPrivateKeys: Map<string, Buffer> = new Map(); private _blockTimeOffsetSeconds: BN = new BN(0); private _nextBlockTimestamp: BN = new BN(0); private _transactionByHash: Map<string, Transaction> = new Map(); private _transactionHashToBlockHash: Map<string, string> = new Map(); private _blockHashToTxBlockResults: Map<string, TxBlockResult[]> = new Map(); private _blockHashToTotalDifficulty: Map<string, BN> = new Map(); private _lastFilterId = new BN(0); private _filters: Map<string, Filter> = new Map(); private _nextSnapshotId = 1; // We start in 1 to mimic Ganache private readonly _snapshots: Snapshot[] = []; private readonly _vmTracer: VMTracer; private readonly _vmTraceDecoder: VmTraceDecoder; private readonly _solidityTracer: SolidityTracer; private readonly _consoleLogger: ConsoleLogger = new ConsoleLogger(); private _failedStackTraces = 0; private readonly _getLatestBlock: () => Promise<Block>; private readonly _getBlock: (hashOrNumber: Buffer | BN) => Promise<Block>; private constructor( private readonly _vm: VM, private readonly _blockchain: Blockchain, localAccounts: Buffer[], private readonly _blockGasLimit: BN, genesisBlock: Block, solidityVersion?: string, initialDate?: Date, compilerInput?: CompilerInput, compilerOutput?: CompilerOutput ) { super(); const config = getUserConfigPath(); this._stateManager = new PStateManager(this._vm.stateManager); this._common = this._vm._common as any; // TODO: There's a version mismatch, that's why we cast this._initLocalAccounts(localAccounts); this._blockHashToTotalDifficulty.set( bufferToHex(genesisBlock.hash()), this._computeTotalDifficulty(genesisBlock) ); this._getLatestBlock = promisify( this._vm.blockchain.getLatestBlock.bind(this._vm.blockchain) ); this._getBlock = promisify( this._vm.blockchain.getBlock.bind(this._vm.blockchain) ); this._vmTracer = new VMTracer(this._vm, true); this._vmTracer.enableTracing(); if (initialDate !== undefined) { this._blockTimeOffsetSeconds = new BN( getDifferenceInSeconds(initialDate, new Date()) ); } const contractsIdentifier = new ContractsIdentifier(); this._vmTraceDecoder = new VmTraceDecoder(contractsIdentifier); this._solidityTracer = new SolidityTracer(); if ( solidityVersion === undefined || compilerInput === undefined || compilerOutput === undefined ) { return; } try { const bytecodes = createModelsAndDecodeBytecodes( solidityVersion, compilerInput, compilerOutput ); for (const bytecode of bytecodes) { this._vmTraceDecoder.addBytecode(bytecode); } } catch (error) { console.warn( chalk.yellow( "The Buidler EVM tracing engine could not be initialized. Run Buidler with --verbose to learn more." ) ); log( "Buidler EVM tracing disabled: ContractsIdentifier failed to be initialized. Please report this to help us improve Buidler.\n", error ); Reporter.reportError(error); } } public async getSignedTransaction( txParams: TransactionParams ): Promise<Transaction> { const tx = new Transaction(txParams, { common: this._common }); const pk = await this._getLocalAccountPrivateKey(txParams.from); tx.sign(pk); return tx; } public async _getFakeTransaction( txParams: TransactionParams ): Promise<Transaction> { return new FakeTransaction(txParams, { common: this._common }); } public async runTransactionInNewBlock( tx: Transaction ): Promise<{ trace: MessageTrace; block: Block; blockResult: RunBlockResult; error?: Error; consoleLogMessages: string[]; }> { await this._validateTransaction(tx); await this._saveTransactionAsReceived(tx); const [ blockTimestamp, offsetShouldChange, newOffset, ] = this._calculateTimestampAndOffset(); const block = await this._getNextBlockTemplate(blockTimestamp); const needsTimestampIncrease = await this._timestampClashesWithPreviousBlockOne( block ); if (needsTimestampIncrease) { await this._increaseBlockTimestamp(block); } await this._addTransactionToBlock(block, tx); const result = await this._vm.runBlock({ block, generate: true, skipBlockValidation: true, }); if (needsTimestampIncrease) { await this.increaseTime(new BN(1)); } await this._saveBlockAsSuccessfullyRun(block, result); await this._saveTransactionAsSuccessfullyRun(tx, block); let vmTrace = this._vmTracer.getLastTopLevelMessageTrace(); const vmTracerError = this._vmTracer.getLastError(); this._vmTracer.clearLastError(); vmTrace = this._vmTraceDecoder.tryToDecodeMessageTrace(vmTrace); const consoleLogMessages = await this._getConsoleLogMessages( vmTrace, vmTracerError ); const error = await this._manageErrors( result.results[0].execResult, vmTrace, vmTracerError ); if (offsetShouldChange) { await this.increaseTime(newOffset.sub(await this.getTimeIncrement())); } await this._resetNextBlockTimestamp(); return { trace: vmTrace, block, blockResult: result, error, consoleLogMessages, }; } public async mineEmptyBlock(timestamp: BN) { // need to check if timestamp is specified or nextBlockTimestamp is set // if it is, time offset must be set to timestamp|nextBlockTimestamp - Date.now // if it is not, time offset remain the same const [ blockTimestamp, offsetShouldChange, newOffset, ] = this._calculateTimestampAndOffset(timestamp); const block = await this._getNextBlockTemplate(blockTimestamp); const needsTimestampIncrease = await this._timestampClashesWithPreviousBlockOne( block ); if (needsTimestampIncrease) { await this._increaseBlockTimestamp(block); } await promisify(block.genTxTrie.bind(block))(); block.header.transactionsTrie = block.txTrie.root; const previousRoot = await this._stateManager.getStateRoot(); let result: RunBlockResult; try { result = await this._vm.runBlock({ block, generate: true, skipBlockValidation: true, }); if (needsTimestampIncrease) { await this.increaseTime(new BN(1)); } await this._saveBlockAsSuccessfullyRun(block, result); if (offsetShouldChange) { await this.increaseTime(newOffset.sub(await this.getTimeIncrement())); } await this._resetNextBlockTimestamp(); return result; } catch (error) { // We set the state root to the previous one. This is equivalent to a // rollback of this block. await this._stateManager.setStateRoot(previousRoot); throw new TransactionExecutionError(error); } } public async runCall( call: CallParams, runOnNewBlock: boolean ): Promise<{ result: Buffer; trace: MessageTrace; error?: Error; consoleLogMessages: string[]; }> { const tx = await this._getFakeTransaction({ ...call, nonce: await this.getAccountNonce(call.from), }); const result = await this._runTxAndRevertMutations(tx, runOnNewBlock); let vmTrace = this._vmTracer.getLastTopLevelMessageTrace(); const vmTracerError = this._vmTracer.getLastError(); this._vmTracer.clearLastError(); vmTrace = this._vmTraceDecoder.tryToDecodeMessageTrace(vmTrace); const consoleLogMessages = await this._getConsoleLogMessages( vmTrace, vmTracerError ); const error = await this._manageErrors( result.execResult, vmTrace, vmTracerError ); return { result: result.execResult.returnValue, trace: vmTrace, error, consoleLogMessages, }; } public async getAccountBalance(address: Buffer): Promise<BN> { const account = await this._stateManager.getAccount(address); return new BN(account.balance); } public async getAccountNonce(address: Buffer): Promise<BN> { const account = await this._stateManager.getAccount(address); return new BN(account.nonce); } public async getAccountNonceInPreviousBlock(address: Buffer): Promise<BN> { const account = await this._stateManager.getAccount(address); const latestBlock = await this._getLatestBlock(); const latestBlockTxsFromAccount = latestBlock.transactions.filter( (tx: Transaction) => tx.getSenderAddress().equals(address) ); return new BN(account.nonce).subn(latestBlockTxsFromAccount.length); } public async getLatestBlock(): Promise<Block> { return this._getLatestBlock(); } public async getLatestBlockNumber(): Promise<BN> { return new BN((await this._getLatestBlock()).header.number); } public async getLocalAccountAddresses(): Promise<string[]> { return [...this._accountPrivateKeys.keys()]; } public async getBlockGasLimit(): Promise<BN> { return this._blockGasLimit; } public async estimateGas( txParams: TransactionParams ): Promise<{ estimation: BN; trace: MessageTrace; error?: Error; consoleLogMessages: string[]; }> { const tx = await this._getFakeTransaction({ ...txParams, gasLimit: await this.getBlockGasLimit(), }); const result = await this._runTxAndRevertMutations(tx); let vmTrace = this._vmTracer.getLastTopLevelMessageTrace(); const vmTracerError = this._vmTracer.getLastError(); this._vmTracer.clearLastError(); vmTrace = this._vmTraceDecoder.tryToDecodeMessageTrace(vmTrace); const consoleLogMessages = await this._getConsoleLogMessages( vmTrace, vmTracerError ); // This is only considered if the call to _runTxAndRevertMutations doesn't // manage errors if (result.execResult.exceptionError !== undefined) { return { estimation: await this.getBlockGasLimit(), trace: vmTrace, error: await this._manageErrors( result.execResult, vmTrace, vmTracerError ), consoleLogMessages, }; } const initialEstimation = result.gasUsed; return { estimation: await this._correctInitialEstimation( txParams, initialEstimation ), trace: vmTrace, consoleLogMessages, }; } public async getGasPrice(): Promise<BN> { return new BN(BUIDLEREVM_DEFAULT_GAS_PRICE); } public async getCoinbaseAddress(): Promise<Buffer> { return COINBASE_ADDRESS; } public async getStorageAt(address: Buffer, slot: BN): Promise<Buffer> { const key = slot.toArrayLike(Buffer, "be", 32); const data = await this._stateManager.getContractStorage(address, key); // TODO: The state manager returns the data as it was saved, it doesn't // pad it. Technically, the storage consists of 32-byte slots, so we should // always return 32 bytes. The problem is that Ganache doesn't handle them // this way. We compromise a little here to ease the migration into // BuidlerEVM :( // const EXPECTED_DATA_SIZE = 32; // if (data.length < EXPECTED_DATA_SIZE) { // return Buffer.concat( // [Buffer.alloc(EXPECTED_DATA_SIZE - data.length, 0), data], // EXPECTED_DATA_SIZE // ); // } return data; } public async getBlockByNumber(blockNumber: BN): Promise<Block | undefined> { if (blockNumber.gten(this._blockHashToTotalDifficulty.size)) { return undefined; } return this._getBlock(blockNumber); } public async getBlockByHash(hash: Buffer): Promise<Block | undefined> { if (!(await this._hasBlockWithHash(hash))) { return undefined; } return this._getBlock(hash); } public async getBlockByTransactionHash( hash: Buffer ): Promise<Block | undefined> { const blockHash = this._transactionHashToBlockHash.get(bufferToHex(hash)); if (blockHash === undefined) { return undefined; } return this.getBlockByHash(toBuffer(blockHash)); } public async getBlockTotalDifficulty(block: Block): Promise<BN> { const blockHash = bufferToHex(block.hash()); const td = this._blockHashToTotalDifficulty.get(blockHash); if (td !== undefined) { return td; } return this._computeTotalDifficulty(block); } public async getCode(address: Buffer): Promise<Buffer> { return this._stateManager.getContractCode(address); } public async setNextBlockTimestamp(timestamp: BN) { this._nextBlockTimestamp = new BN(timestamp); } public async increaseTime(increment: BN) { this._blockTimeOffsetSeconds = this._blockTimeOffsetSeconds.add(increment); } public async getTimeIncrement(): Promise<BN> { return this._blockTimeOffsetSeconds; } public async getNextBlockTimestamp(): Promise<BN> { return this._nextBlockTimestamp; } public async getSuccessfulTransactionByHash( hash: Buffer ): Promise<Transaction | undefined> { const tx = this._transactionByHash.get(bufferToHex(hash)); if (tx !== undefined && (await this._transactionWasSuccessful(tx))) { return tx; } return undefined; } public async getTxBlockResults( block: Block ): Promise<TxBlockResult[] | undefined> { return this._blockHashToTxBlockResults.get(bufferToHex(block.hash())); } public async getPendingTransactions(): Promise<Transaction[]> { return []; } public async signPersonalMessage( address: Buffer, data: Buffer ): Promise<ECDSASignature> { const messageHash = hashPersonalMessage(data); const privateKey = await this._getLocalAccountPrivateKey(address); return ecsign(messageHash, privateKey); } public async signTypedData(address: Buffer, typedData: any): Promise<string> { const privateKey = await this._getLocalAccountPrivateKey(address); return ethSigUtil.signTypedData_v4(privateKey, { data: typedData, }); } public async getStackTraceFailuresCount(): Promise<number> { return this._failedStackTraces; } public async takeSnapshot(): Promise<number> { const id = this._nextSnapshotId; // We copy all the maps here, as they may be modified const snapshot: Snapshot = { id, date: new Date(), latestBlock: await this.getLatestBlock(), stateRoot: await this._stateManager.getStateRoot(), blockTimeOffsetSeconds: new BN(this._blockTimeOffsetSeconds), nextBlockTimestamp: new BN(this._nextBlockTimestamp), transactionByHash: new Map(this._transactionByHash.entries()), transactionHashToBlockHash: new Map( this._transactionHashToBlockHash.entries() ), blockHashToTxBlockResults: new Map( this._blockHashToTxBlockResults.entries() ), blockHashToTotalDifficulty: new Map( this._blockHashToTotalDifficulty.entries() ), }; this._snapshots.push(snapshot); this._nextSnapshotId += 1; return id; } public async revertToSnapshot(id: number): Promise<boolean> { const snapshotIndex = this._getSnapshotIndex(id); if (snapshotIndex === undefined) { return false; } const snapshot = this._snapshots[snapshotIndex]; // We compute a new offset such that // now + new_offset === snapshot_date + old_offset const now = new Date(); const offsetToSnapshotInMillis = snapshot.date.valueOf() - now.valueOf(); const offsetToSnapshotInSecs = Math.ceil(offsetToSnapshotInMillis / 1000); const newOffset = snapshot.blockTimeOffsetSeconds.addn( offsetToSnapshotInSecs ); // We delete all following blocks, changes the state root, and all the // relevant Node fields. // // Note: There's no need to copy the maps here, as snapshots can only be // used once this._blockchain.deleteAllFollowingBlocks(snapshot.latestBlock); await this._stateManager.setStateRoot(snapshot.stateRoot); this._blockTimeOffsetSeconds = newOffset; this._nextBlockTimestamp = snapshot.nextBlockTimestamp; this._transactionByHash = snapshot.transactionByHash; this._transactionHashToBlockHash = snapshot.transactionHashToBlockHash; this._blockHashToTxBlockResults = snapshot.blockHashToTxBlockResults; this._blockHashToTotalDifficulty = snapshot.blockHashToTotalDifficulty; // We delete this and the following snapshots, as they can only be used // once in Ganache this._snapshots.splice(snapshotIndex); return true; } public async newFilter( filterParams: FilterParams, isSubscription: boolean ): Promise<BN> { filterParams = await this._computeFilterParams(filterParams, true); const filterId = this._getNextFilterId(); this._filters.set(this._filterIdToFiltersKey(filterId), { id: filterId, type: Type.LOGS_SUBSCRIPTION, criteria: { fromBlock: filterParams.fromBlock, toBlock: filterParams.toBlock, addresses: filterParams.addresses, normalizedTopics: filterParams.normalizedTopics, }, deadline: this._newDeadline(), hashes: [], logs: await this.getLogs(filterParams), subscription: isSubscription, }); return filterId; } public async newBlockFilter(isSubscription: boolean): Promise<BN> { const block = await this.getLatestBlock(); const filterId = this._getNextFilterId(); this._filters.set(this._filterIdToFiltersKey(filterId), { id: filterId, type: Type.BLOCK_SUBSCRIPTION, deadline: this._newDeadline(), hashes: [bufferToHex(block.header.hash())], logs: [], subscription: isSubscription, }); return filterId; } public async newPendingTransactionFilter( isSubscription: boolean ): Promise<BN> { const filterId = this._getNextFilterId(); this._filters.set(this._filterIdToFiltersKey(filterId), { id: filterId, type: Type.PENDING_TRANSACTION_SUBSCRIPTION, deadline: this._newDeadline(), hashes: [], logs: [], subscription: isSubscription, }); return filterId; } public async uninstallFilter( filterId: BN, subscription: boolean ): Promise<boolean> { const key = this._filterIdToFiltersKey(filterId); const filter = this._filters.get(key); if (filter === undefined) { return false; } if ( (filter.subscription && !subscription) || (!filter.subscription && subscription) ) { return false; } this._filters.delete(key); return true; } public async getFilterChanges( filterId: BN ): Promise<string[] | RpcLogOutput[] | undefined> { const key = this._filterIdToFiltersKey(filterId); const filter = this._filters.get(key); if (filter === undefined) { return undefined; } filter.deadline = this._newDeadline(); switch (filter.type) { case Type.BLOCK_SUBSCRIPTION: case Type.PENDING_TRANSACTION_SUBSCRIPTION: const hashes = filter.hashes; filter.hashes = []; return hashes; case Type.LOGS_SUBSCRIPTION: const logs = filter.logs; filter.logs = []; return logs; } return undefined; } public async getFilterLogs( filterId: BN ): Promise<RpcLogOutput[] | undefined> { const key = this._filterIdToFiltersKey(filterId); const filter = this._filters.get(key); if (filter === undefined) { return undefined; } const logs = filter.logs; filter.logs = []; filter.deadline = this._newDeadline(); return logs; } public async getLogs(filterParams: FilterParams): Promise<RpcLogOutput[]> { filterParams = await this._computeFilterParams(filterParams, false); const logs: RpcLogOutput[] = []; for ( let i = filterParams.fromBlock; i.lte(filterParams.toBlock); i = i.addn(1) ) { const block = await this._getBlock(new BN(i)); const blockResults = this._blockHashToTxBlockResults.get( bufferToHex(block.hash()) ); if (blockResults === undefined) { continue; } if ( !bloomFilter( new Bloom(block.header.bloom), filterParams.addresses, filterParams.normalizedTopics ) ) { continue; } for (const tx of blockResults) { logs.push( ...filterLogs(tx.receipt.logs, { fromBlock: filterParams.fromBlock, toBlock: filterParams.toBlock, addresses: filterParams.addresses, normalizedTopics: filterParams.normalizedTopics, }) ); } } return logs; } public async addCompilationResult( compilerVersion: string, compilerInput: CompilerInput, compilerOutput: CompilerOutput ): Promise<boolean> { let bytecodes; try { bytecodes = createModelsAndDecodeBytecodes( compilerVersion, compilerInput, compilerOutput ); } catch (error) { console.warn( chalk.yellow( "The Buidler EVM tracing engine could not be updated. Run Buidler with --verbose to learn more." ) ); log( "ContractsIdentifier failed to be updated. Please report this to help us improve Buidler.\n", error ); return false; } for (const bytecode of bytecodes) { this._vmTraceDecoder.addBytecode(bytecode); } return true; } private _getSnapshotIndex(id: number): number | undefined { for (const [i, snapshot] of this._snapshots.entries()) { if (snapshot.id === id) { return i; } // We already removed the snapshot we are looking for if (snapshot.id > id) { return undefined; } } return undefined; } private _initLocalAccounts(localAccounts: Buffer[]) { for (const pk of localAccounts) { this._accountPrivateKeys.set(bufferToHex(privateToAddress(pk)), pk); } } private async _getConsoleLogMessages( vmTrace: MessageTrace, vmTracerError: Error | undefined ): Promise<string[]> { if (vmTracerError !== undefined) { log( "Could not print console log. Please report this to help us improve Buidler.\n", vmTracerError ); return []; } return this._consoleLogger.getLogMessages(vmTrace); } private async _manageErrors( vmResult: ExecResult, vmTrace: MessageTrace, vmTracerError?: Error ): Promise<SolidityError | TransactionExecutionError | undefined> { if (vmResult.exceptionError === undefined) { return undefined; } let stackTrace: SolidityStackTrace | undefined; try { if (vmTracerError !== undefined) { throw vmTracerError; } stackTrace = this._solidityTracer.getStackTrace(vmTrace); } catch (error) { this._failedStackTraces += 1; log( "Could not generate stack trace. Please report this to help us improve Buidler.\n", error ); } const error = vmResult.exceptionError; if (error.error === ERROR.OUT_OF_GAS) { if (this._isContractTooLargeStackTrace(stackTrace)) { return encodeSolidityStackTrace( "Transaction run out of gas", stackTrace! ); } return new TransactionExecutionError("Transaction run out of gas"); } if (error.error === ERROR.REVERT) { if (vmResult.returnValue.length === 0) { if (stackTrace !== undefined) { return encodeSolidityStackTrace( "Transaction reverted without a reason", stackTrace ); } return new TransactionExecutionError( "Transaction reverted without a reason" ); } if (stackTrace !== undefined) { return encodeSolidityStackTrace( `VM Exception while processing transaction: revert ${decodeRevertReason( vmResult.returnValue )}`, stackTrace ); } return new TransactionExecutionError( `VM Exception while processing transaction: revert ${decodeRevertReason( vmResult.returnValue )}` ); } if (stackTrace !== undefined) { return encodeSolidityStackTrace("Transaction failed: revert", stackTrace); } return new TransactionExecutionError("Transaction failed: revert"); } private _isContractTooLargeStackTrace( stackTrace: SolidityStackTrace | undefined ) { return ( stackTrace !== undefined && stackTrace.length > 0 && stackTrace[stackTrace.length - 1].type === StackTraceEntryType.CONTRACT_TOO_LARGE_ERROR ); } private _calculateTimestampAndOffset(timestamp?: BN): [BN, boolean, BN] { let blockTimestamp: BN; let offsetShouldChange: boolean; let newOffset: BN = new BN(0); // if timestamp is not provided, we check nextBlockTimestamp, if it is // set, we use it as the timestamp instead. If it is not set, we use // time offset + real time as the timestamp. if (timestamp === undefined || timestamp.eq(new BN(0))) { if (this._nextBlockTimestamp.eq(new BN(0))) { blockTimestamp = new BN(getCurrentTimestamp()).add( this._blockTimeOffsetSeconds ); offsetShouldChange = false; } else { blockTimestamp = new BN(this._nextBlockTimestamp); offsetShouldChange = true; } } else { offsetShouldChange = true; blockTimestamp = timestamp; } if (offsetShouldChange) { newOffset = blockTimestamp.sub(new BN(getCurrentTimestamp())); } return [blockTimestamp, offsetShouldChange, newOffset]; } private async _getNextBlockTemplate(timestamp: BN): Promise<Block> { const block = new Block( { header: { gasLimit: this._blockGasLimit, nonce: "0x42", timestamp, }, }, { common: this._common } ); block.validate = (blockchain: any, cb: any) => cb(null); const latestBlock = await this.getLatestBlock(); block.header.number = toBuffer(new BN(latestBlock.header.number).addn(1)); block.header.parentHash = latestBlock.hash(); block.header.difficulty = block.header.canonicalDifficulty(latestBlock); block.header.coinbase = await this.getCoinbaseAddress(); return block; } private async _resetNextBlockTimestamp() { this._nextBlockTimestamp = new BN(0); } private async _saveTransactionAsReceived(tx: Transaction) { this._transactionByHash.set(bufferToHex(tx.hash(true)), tx); this._filters.forEach((filter) => { if (filter.type === Type.PENDING_TRANSACTION_SUBSCRIPTION) { const hash = bufferToHex(tx.hash(true)); if (filter.subscription) { this._emitEthEvent(filter.id, hash); return; } filter.hashes.push(hash); } }); } private async _getLocalAccountPrivateKey(sender: Buffer): Promise<Buffer> { const senderAddress = bufferToHex(sender); if (!this._accountPrivateKeys.has(senderAddress)) { throw new InvalidInputError(`unknown account ${senderAddress}`); } return this._accountPrivateKeys.get(senderAddress)!; } private async _addTransactionToBlock(block: Block, tx: Transaction) { block.transactions.push(tx); await promisify(block.genTxTrie.bind(block))(); block.header.transactionsTrie = block.txTrie.root; } private async _saveBlockAsSuccessfullyRun( block: Block, runBlockResult: RunBlockResult ) { await this._putBlock(block); const txBlockResults: TxBlockResult[] = []; for (let i = 0; i < runBlockResult.results.length; i += 1) { const result = runBlockResult.results[i]; const receipt = runBlockResult.receipts[i]; const logs = receipt.logs.map( (rcpLog, logIndex) => (runBlockResult.receipts[i].logs[logIndex] = getRpcLog( rcpLog, block.transactions[i], block, i, logIndex )) ); txBlockResults.push({ bloomBitvector: result.bloom.bitvector, createAddresses: result.createdAddress, receipt: { status: receipt.status, gasUsed: receipt.gasUsed, bitvector: receipt.bitvector, logs, }, }); } const blockHash = bufferToHex(block.hash()); this._blockHashToTxBlockResults.set(blockHash, txBlockResults); const td = this._computeTotalDifficulty(block); this._blockHashToTotalDifficulty.set(blockHash, td); const rpcLogs: RpcLogOutput[] = []; for (const receipt of runBlockResult.receipts) { rpcLogs.push(...receipt.logs); } this._filters.forEach((filter, key) => { if (filter.deadline.valueOf() < new Date().valueOf()) { this._filters.delete(key); } switch (filter.type) { case Type.BLOCK_SUBSCRIPTION: const hash = block.hash(); if (filter.subscription) { this._emitEthEvent(filter.id, getRpcBlock(block, td, false)); return; } filter.hashes.push(bufferToHex(hash)); break; case Type.LOGS_SUBSCRIPTION: if ( bloomFilter( new Bloom(block.header.bloom), filter.criteria!.addresses, filter.criteria!.normalizedTopics ) ) { const logs = filterLogs(rpcLogs, filter.criteria!); if (logs.length === 0) { return; } if (filter.subscription) { logs.forEach((rpcLog) => { this._emitEthEvent(filter.id, rpcLog); }); return; } filter.logs.push(...logs); } break; } }); } private async _putBlock(block: Block): Promise<void> { return new Promise((resolve, reject) => { this._vm.blockchain.putBlock(block, (err?: any) => { if (err !== undefined && err !== null) { reject(err); return; } resolve(); }); }); } private async _hasBlockWithHash(blockHash: Buffer): Promise<boolean> { if (this._blockHashToTotalDifficulty.has(bufferToHex(blockHash))) { return true; } const block = await this.getBlockByNumber(new BN(0)); return block.hash().equals(blockHash); } private async _saveTransactionAsSuccessfullyRun( tx: Transaction, block: Block ) { this._transactionHashToBlockHash.set( bufferToHex(tx.hash(true)), bufferToHex(block.hash()) ); } private async _transactionWasSuccessful(tx: Transaction): Promise<boolean> { return this._transactionHashToBlockHash.has(bufferToHex(tx.hash(true))); } private async _timestampClashesWithPreviousBlockOne( block: Block ): Promise<boolean> { const blockTimestamp = new BN(block.header.timestamp); const latestBlock = await this.getLatestBlock(); const latestBlockTimestamp = new BN(latestBlock.header.timestamp); return latestBlockTimestamp.eq(blockTimestamp); } private async _increaseBlockTimestamp(block: Block) { block.header.timestamp = new BN(block.header.timestamp).addn(1); } private async _setBlockTimestamp(block: Block, timestamp: BN) { block.header.timestamp = new BN(timestamp); } private async _validateTransaction(tx: Transaction) { // Geth throws this error if a tx is sent twice if (await this._transactionWasSuccessful(tx)) { throw new InvalidInputError( `known transaction: ${bufferToHex(tx.hash(true)).toString()}` ); } if (!tx.verifySignature()) { throw new InvalidInputError("Invalid transaction signature"); } // Geth returns this error if trying to create a contract and no data is provided if (tx.to.length === 0 && tx.data.length === 0) { throw new InvalidInputError( "contract creation without any data provided" ); } const expectedNonce = await this.getAccountNonce(tx.getSenderAddress()); const actualNonce = new BN(tx.nonce); if (!expectedNonce.eq(actualNonce)) { throw new InvalidInputError( `Invalid nonce. Expected ${expectedNonce} but got ${actualNonce}. If you are running a script or test, you may be sending transactions in parallel. Using JavaScript? You probably forgot an await. If you are using a wallet or dapp, try resetting your wallet's accounts.` ); } const baseFee = tx.getBaseFee(); const gasLimit = new BN(tx.gasLimit); if (baseFee.gt(gasLimit)) { throw new InvalidInputError( `Transaction requires at least ${baseFee} gas but got ${gasLimit}` ); } if (gasLimit.gt(this._blockGasLimit)) { throw new InvalidInputError( `Transaction gas limit is ${gasLimit} and exceeds block gas limit of ${this._blockGasLimit}` ); } } private _computeTotalDifficulty(block: Block): BN { const difficulty = new BN(block.header.difficulty); const parentHash = bufferToHex(block.header.parentHash); if ( parentHash === "0x0000000000000000000000000000000000000000000000000000000000000000" ) { return difficulty; } const parentTd = this._blockHashToTotalDifficulty.get(parentHash); if (parentTd === undefined) { throw new InternalError(`Unrecognized parent block ${parentHash}`); } return parentTd.add(difficulty); } private async _correctInitialEstimation( txParams: TransactionParams, initialEstimation: BN ): Promise<BN> { let tx = await this._getFakeTransaction({ ...txParams, gasLimit: initialEstimation, }); if (tx.getBaseFee().gte(initialEstimation)) { initialEstimation = tx.getBaseFee().addn(1); tx = await this._getFakeTransaction({ ...txParams, gasLimit: initialEstimation, }); } const result = await this._runTxAndRevertMutations(tx); if (result.execResult.exceptionError === undefined) { return initialEstimation; } return this._binarySearchEstimation( txParams, initialEstimation, await this.getBlockGasLimit() ); } private async _binarySearchEstimation( txParams: TransactionParams, highestFailingEstimation: BN, lowestSuccessfulEstimation: BN, roundNumber = 0 ): Promise<BN> { if (lowestSuccessfulEstimation.lte(highestFailingEstimation)) { // This shouldn't happen, but we don't wan't to go into an infinite loop // if it ever happens return lowestSuccessfulEstimation; } const MAX_GAS_ESTIMATION_IMPROVEMENT_ROUNDS = 20; const diff = lowestSuccessfulEstimation.sub(highestFailingEstimation); const minDiff = highestFailingEstimation.gten(4_000_000) ? 50_000 : highestFailingEstimation.gten(1_000_000) ? 10_000 : highestFailingEstimation.gten(100_000) ? 1_000 : highestFailingEstimation.gten(50_000) ? 500 : highestFailingEstimation.gten(30_000) ? 300 : 200; if (diff.lten(minDiff)) { return lowestSuccessfulEstimation; } if (roundNumber > MAX_GAS_ESTIMATION_IMPROVEMENT_ROUNDS) { return lowestSuccessfulEstimation; } const binSearchNewEstimation = highestFailingEstimation.add(diff.divn(2)); const optimizedEstimation = roundNumber === 0 ? highestFailingEstimation.muln(3) : binSearchNewEstimation; const newEstimation = optimizedEstimation.gt(binSearchNewEstimation) ? binSearchNewEstimation : optimizedEstimation; // Let other things execute await new Promise((resolve) => setImmediate(resolve)); const tx = await this._getFakeTransaction({ ...txParams, gasLimit: newEstimation, }); const result = await this._runTxAndRevertMutations(tx); if (result.execResult.exceptionError === undefined) { return this._binarySearchEstimation( txParams, highestFailingEstimation, newEstimation, roundNumber + 1 ); } return this._binarySearchEstimation( txParams, newEstimation, lowestSuccessfulEstimation, roundNumber + 1 ); } /** * This function runs a transaction and reverts all the modifications that it * makes. * * If throwOnError is true, errors are managed locally and thrown on * failure. If it's false, the tx's RunTxResult is returned, and the vmTracer * inspected/resetted. */ private async _runTxAndRevertMutations( tx: Transaction, runOnNewBlock: boolean = true ): Promise<EVMResult> { const initialStateRoot = await this._stateManager.getStateRoot(); try { let blockContext; // if the context is to estimate gas or run calls in pending block if (runOnNewBlock) { const [ blockTimestamp, offsetShouldChange, newOffset, ] = this._calculateTimestampAndOffset(); blockContext = await this._getNextBlockTemplate(blockTimestamp); const needsTimestampIncrease = await this._timestampClashesWithPreviousBlockOne( blockContext ); if (needsTimestampIncrease) { await this._increaseBlockTimestamp(blockContext); } // in the context of running estimateGas call, we have to do binary // search for the gas and run the call multiple times. Since it is // an approximate approach to calculate the gas, it is important to // run the call in a block that is as close to the real one as // possible, hence putting the tx to the block is good to have here. await this._addTransactionToBlock(blockContext, tx); } else { // if the context is to run calls with the latest block blockContext = await this.getLatestBlock(); } return await this._vm.runTx({ block: blockContext, tx, skipNonce: true, skipBalance: true, }); } finally { await this._stateManager.setStateRoot(initialStateRoot); } } private async _computeFilterParams( filterParams: FilterParams, isFilter: boolean ): Promise<FilterParams> { const latestBlockNumber = await this.getLatestBlockNumber(); const newFilterParams = { ...filterParams }; if (newFilterParams.fromBlock === LATEST_BLOCK) { newFilterParams.fromBlock = latestBlockNumber; } if (!isFilter && newFilterParams.toBlock === LATEST_BLOCK) { newFilterParams.toBlock = latestBlockNumber; } if (newFilterParams.toBlock.gt(latestBlockNumber)) { newFilterParams.toBlock = latestBlockNumber; } if (newFilterParams.fromBlock.gt(latestBlockNumber)) { newFilterParams.fromBlock = latestBlockNumber; } return newFilterParams; } private _newDeadline(): Date { const dt = new Date(); dt.setMinutes(dt.getMinutes() + 5); // This will not overflow return dt; } private _getNextFilterId(): BN { this._lastFilterId = this._lastFilterId.addn(1); return this._lastFilterId; } private _filterIdToFiltersKey(filterId: BN): string { return filterId.toString(); } private _emitEthEvent(filterId: BN, result: any) { this.emit("ethEvent", { result, filterId, }); } }