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.

995 lines (993 loc) 46.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const ethereumjs_vm_1 = __importDefault(require("@nomiclabs/ethereumjs-vm")); const bloom_1 = __importDefault(require("@nomiclabs/ethereumjs-vm/dist/bloom")); const exceptions_1 = require("@nomiclabs/ethereumjs-vm/dist/exceptions"); const state_1 = require("@nomiclabs/ethereumjs-vm/dist/state"); const promisified_1 = __importDefault(require("@nomiclabs/ethereumjs-vm/dist/state/promisified")); const chalk_1 = __importDefault(require("chalk")); const debug_1 = __importDefault(require("debug")); const ethereumjs_account_1 = __importDefault(require("ethereumjs-account")); const ethereumjs_block_1 = __importDefault(require("ethereumjs-block")); const ethereumjs_common_1 = __importDefault(require("ethereumjs-common")); const ethereumjs_tx_1 = require("ethereumjs-tx"); const ethereumjs_util_1 = require("ethereumjs-util"); const events_1 = __importDefault(require("events")); const secure_1 = __importDefault(require("merkle-patricia-tree/secure")); const util_1 = require("util"); const default_config_1 = require("../../core/config/default-config"); const reporter_1 = require("../../sentry/reporter"); const date_1 = require("../../util/date"); const compiler_to_model_1 = require("../stack-traces/compiler-to-model"); const consoleLogger_1 = require("../stack-traces/consoleLogger"); const contracts_identifier_1 = require("../stack-traces/contracts-identifier"); const revert_reasons_1 = require("../stack-traces/revert-reasons"); const solidity_errors_1 = require("../stack-traces/solidity-errors"); const solidity_stack_trace_1 = require("../stack-traces/solidity-stack-trace"); const solidityTracer_1 = require("../stack-traces/solidityTracer"); const vm_trace_decoder_1 = require("../stack-traces/vm-trace-decoder"); const vm_tracer_1 = require("../stack-traces/vm-tracer"); const blockchain_1 = require("./blockchain"); const errors_1 = require("./errors"); const filter_1 = require("./filter"); const output_1 = require("./output"); const utils_1 = require("./utils"); const log = debug_1.default("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"); exports.COINBASE_ADDRESS = ethereumjs_util_1.toBuffer("0xc014ba5ec014ba5ec014ba5ec014ba5ec014ba5e"); class BuidlerNode extends events_1.default { constructor(_vm, _blockchain, localAccounts, _blockGasLimit, genesisBlock, solidityVersion, initialDate, compilerInput, compilerOutput) { super(); this._vm = _vm; this._blockchain = _blockchain; this._blockGasLimit = _blockGasLimit; this._accountPrivateKeys = new Map(); this._blockTimeOffsetSeconds = new ethereumjs_util_1.BN(0); this._nextBlockTimestamp = new ethereumjs_util_1.BN(0); this._transactionByHash = new Map(); this._transactionHashToBlockHash = new Map(); this._blockHashToTxBlockResults = new Map(); this._blockHashToTotalDifficulty = new Map(); this._lastFilterId = new ethereumjs_util_1.BN(0); this._filters = new Map(); this._nextSnapshotId = 1; // We start in 1 to mimic Ganache this._snapshots = []; this._consoleLogger = new consoleLogger_1.ConsoleLogger(); this._failedStackTraces = 0; this._stateManager = new promisified_1.default(this._vm.stateManager); this._common = this._vm._common; // TODO: There's a version mismatch, that's why we cast this._initLocalAccounts(localAccounts); this._blockHashToTotalDifficulty.set(ethereumjs_util_1.bufferToHex(genesisBlock.hash()), this._computeTotalDifficulty(genesisBlock)); this._getLatestBlock = util_1.promisify(this._vm.blockchain.getLatestBlock.bind(this._vm.blockchain)); this._getBlock = util_1.promisify(this._vm.blockchain.getBlock.bind(this._vm.blockchain)); this._vmTracer = new vm_tracer_1.VMTracer(this._vm, true); this._vmTracer.enableTracing(); if (initialDate !== undefined) { this._blockTimeOffsetSeconds = new ethereumjs_util_1.BN(date_1.getDifferenceInSeconds(initialDate, new Date())); } const contractsIdentifier = new contracts_identifier_1.ContractsIdentifier(); this._vmTraceDecoder = new vm_trace_decoder_1.VmTraceDecoder(contractsIdentifier); this._solidityTracer = new solidityTracer_1.SolidityTracer(); if (solidityVersion === undefined || compilerInput === undefined || compilerOutput === undefined) { return; } try { const bytecodes = compiler_to_model_1.createModelsAndDecodeBytecodes(solidityVersion, compilerInput, compilerOutput); for (const bytecode of bytecodes) { this._vmTraceDecoder.addBytecode(bytecode); } } catch (error) { console.warn(chalk_1.default.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_1.Reporter.reportError(error); } } static async create(hardfork, networkName, chainId, networkId, blockGasLimit, genesisAccounts = [], solidityVersion, allowUnlimitedContractSize, initialDate, compilerInput, compilerOutput) { const stateTrie = new secure_1.default(); const putIntoStateTrie = util_1.promisify(stateTrie.put.bind(stateTrie)); for (const acc of genesisAccounts) { let balance; if (typeof acc.balance === "string" && acc.balance.toLowerCase().startsWith("0x")) { balance = new ethereumjs_util_1.BN(ethereumjs_util_1.toBuffer(acc.balance)); } else { balance = new ethereumjs_util_1.BN(acc.balance); } const account = new ethereumjs_account_1.default({ balance }); const pk = ethereumjs_util_1.toBuffer(acc.privateKey); const address = ethereumjs_util_1.privateToAddress(pk); await putIntoStateTrie(address, account.serialize()); } // Mimic precompiles activation for (let i = 1; i <= 8; i++) { await putIntoStateTrie(new ethereumjs_util_1.BN(i).toArrayLike(Buffer, "be", 20), new ethereumjs_account_1.default().serialize()); } const initialBlockTimestamp = initialDate !== undefined ? date_1.dateToTimestampSeconds(initialDate) : utils_1.getCurrentTimestamp(); const common = ethereumjs_common_1.default.forCustomChain("mainnet", { chainId, networkId, name: networkName, genesis: { timestamp: `0x${initialBlockTimestamp.toString(16)}`, hash: "0x", gasLimit: blockGasLimit, difficulty: 1, nonce: "0x42", extraData: "0x1234", stateRoot: ethereumjs_util_1.bufferToHex(stateTrie.root), }, }, hardfork); const stateManager = new state_1.StateManager({ common: common, trie: stateTrie, }); const blockchain = new blockchain_1.Blockchain(); const vm = new ethereumjs_vm_1.default({ common: common, activatePrecompiles: true, stateManager, blockchain: blockchain, allowUnlimitedContractSize, }); const genesisBlock = new ethereumjs_block_1.default(null, { common }); genesisBlock.setGenesisParams(); await new Promise((resolve) => { blockchain.putBlock(genesisBlock, () => resolve()); }); const node = new BuidlerNode(vm, blockchain, genesisAccounts.map((acc) => ethereumjs_util_1.toBuffer(acc.privateKey)), new ethereumjs_util_1.BN(blockGasLimit), genesisBlock, solidityVersion, initialDate, compilerInput, compilerOutput); return [common, node]; } async getSignedTransaction(txParams) { const tx = new ethereumjs_tx_1.Transaction(txParams, { common: this._common }); const pk = await this._getLocalAccountPrivateKey(txParams.from); tx.sign(pk); return tx; } async _getFakeTransaction(txParams) { return new ethereumjs_tx_1.FakeTransaction(txParams, { common: this._common }); } async runTransactionInNewBlock(tx) { 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 ethereumjs_util_1.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, }; } async mineEmptyBlock(timestamp) { // 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 util_1.promisify(block.genTxTrie.bind(block))(); block.header.transactionsTrie = block.txTrie.root; const previousRoot = await this._stateManager.getStateRoot(); let result; try { result = await this._vm.runBlock({ block, generate: true, skipBlockValidation: true, }); if (needsTimestampIncrease) { await this.increaseTime(new ethereumjs_util_1.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 errors_1.TransactionExecutionError(error); } } async runCall(call, blockNumber) { const tx = await this._getFakeTransaction(Object.assign(Object.assign({}, call), { nonce: await this.getAccountNonce(call.from, null) })); const result = await this._runOnBlockContext(blockNumber, () => this._runTxAndRevertMutations(tx, blockNumber === null)); 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, }; } async getAccountBalance(address, blockNumber) { const account = await this._runOnBlockContext(blockNumber, () => this._stateManager.getAccount(address)); return new ethereumjs_util_1.BN(account.balance); } async getAccountNonce(address, blockNumber) { const account = await this._runOnBlockContext(blockNumber, () => this._stateManager.getAccount(address)); return new ethereumjs_util_1.BN(account.nonce); } async getAccountNonceInPreviousBlock(address) { const account = await this._stateManager.getAccount(address); const latestBlock = await this._getLatestBlock(); const latestBlockTxsFromAccount = latestBlock.transactions.filter((tx) => tx.getSenderAddress().equals(address)); return new ethereumjs_util_1.BN(account.nonce).subn(latestBlockTxsFromAccount.length); } async getLatestBlock() { return this._getLatestBlock(); } async getLatestBlockNumber() { return new ethereumjs_util_1.BN((await this._getLatestBlock()).header.number); } async getLocalAccountAddresses() { return [...this._accountPrivateKeys.keys()]; } async getBlockGasLimit() { return this._blockGasLimit; } async estimateGas(txParams, blockNumber) { const tx = await this._getFakeTransaction(Object.assign(Object.assign({}, txParams), { gasLimit: await this.getBlockGasLimit() })); const result = await this._runOnBlockContext(blockNumber, () => 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, }; } async getGasPrice() { return new ethereumjs_util_1.BN(default_config_1.BUIDLEREVM_DEFAULT_GAS_PRICE); } async getCoinbaseAddress() { return exports.COINBASE_ADDRESS; } async getStorageAt(address, slot, blockNumber) { const key = slot.toArrayLike(Buffer, "be", 32); let data; data = this._runOnBlockContext(blockNumber, () => 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; } async getBlockByNumber(blockNumber) { if (blockNumber.gten(this._blockHashToTotalDifficulty.size)) { return undefined; } return this._getBlock(blockNumber); } async getBlockByHash(hash) { if (!(await this._hasBlockWithHash(hash))) { return undefined; } return this._getBlock(hash); } async getBlockByTransactionHash(hash) { const blockHash = this._transactionHashToBlockHash.get(ethereumjs_util_1.bufferToHex(hash)); if (blockHash === undefined) { return undefined; } return this.getBlockByHash(ethereumjs_util_1.toBuffer(blockHash)); } async getBlockTotalDifficulty(block) { const blockHash = ethereumjs_util_1.bufferToHex(block.hash()); const td = this._blockHashToTotalDifficulty.get(blockHash); if (td !== undefined) { return td; } return this._computeTotalDifficulty(block); } async getCode(address, blockNumber) { return this._runOnBlockContext(blockNumber, () => this._stateManager.getContractCode(address)); } async setNextBlockTimestamp(timestamp) { this._nextBlockTimestamp = new ethereumjs_util_1.BN(timestamp); } async increaseTime(increment) { this._blockTimeOffsetSeconds = this._blockTimeOffsetSeconds.add(increment); } async getTimeIncrement() { return this._blockTimeOffsetSeconds; } async getNextBlockTimestamp() { return this._nextBlockTimestamp; } async getSuccessfulTransactionByHash(hash) { const tx = this._transactionByHash.get(ethereumjs_util_1.bufferToHex(hash)); if (tx !== undefined && (await this._transactionWasSuccessful(tx))) { return tx; } return undefined; } async getTxBlockResults(block) { return this._blockHashToTxBlockResults.get(ethereumjs_util_1.bufferToHex(block.hash())); } async getPendingTransactions() { return []; } async signPersonalMessage(address, data) { const messageHash = ethereumjs_util_1.hashPersonalMessage(data); const privateKey = await this._getLocalAccountPrivateKey(address); return ethereumjs_util_1.ecsign(messageHash, privateKey); } async signTypedData(address, typedData) { const privateKey = await this._getLocalAccountPrivateKey(address); return ethSigUtil.signTypedData_v4(privateKey, { data: typedData, }); } async getStackTraceFailuresCount() { return this._failedStackTraces; } async takeSnapshot() { const id = this._nextSnapshotId; // We copy all the maps here, as they may be modified const snapshot = { id, date: new Date(), latestBlock: await this.getLatestBlock(), stateRoot: await this._stateManager.getStateRoot(), blockTimeOffsetSeconds: new ethereumjs_util_1.BN(this._blockTimeOffsetSeconds), nextBlockTimestamp: new ethereumjs_util_1.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; } async revertToSnapshot(id) { 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; } async newFilter(filterParams, isSubscription) { filterParams = await this._computeFilterParams(filterParams, true); const filterId = this._getNextFilterId(); this._filters.set(this._filterIdToFiltersKey(filterId), { id: filterId, type: filter_1.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; } async newBlockFilter(isSubscription) { const block = await this.getLatestBlock(); const filterId = this._getNextFilterId(); this._filters.set(this._filterIdToFiltersKey(filterId), { id: filterId, type: filter_1.Type.BLOCK_SUBSCRIPTION, deadline: this._newDeadline(), hashes: [ethereumjs_util_1.bufferToHex(block.header.hash())], logs: [], subscription: isSubscription, }); return filterId; } async newPendingTransactionFilter(isSubscription) { const filterId = this._getNextFilterId(); this._filters.set(this._filterIdToFiltersKey(filterId), { id: filterId, type: filter_1.Type.PENDING_TRANSACTION_SUBSCRIPTION, deadline: this._newDeadline(), hashes: [], logs: [], subscription: isSubscription, }); return filterId; } async uninstallFilter(filterId, subscription) { 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; } async getFilterChanges(filterId) { const key = this._filterIdToFiltersKey(filterId); const filter = this._filters.get(key); if (filter === undefined) { return undefined; } filter.deadline = this._newDeadline(); switch (filter.type) { case filter_1.Type.BLOCK_SUBSCRIPTION: case filter_1.Type.PENDING_TRANSACTION_SUBSCRIPTION: const hashes = filter.hashes; filter.hashes = []; return hashes; case filter_1.Type.LOGS_SUBSCRIPTION: const logs = filter.logs; filter.logs = []; return logs; } return undefined; } async getFilterLogs(filterId) { 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; } async getLogs(filterParams) { filterParams = await this._computeFilterParams(filterParams, false); const logs = []; for (let i = filterParams.fromBlock; i.lte(filterParams.toBlock); i = i.addn(1)) { const block = await this._getBlock(new ethereumjs_util_1.BN(i)); const blockResults = this._blockHashToTxBlockResults.get(ethereumjs_util_1.bufferToHex(block.hash())); if (blockResults === undefined) { continue; } if (!filter_1.bloomFilter(new bloom_1.default(block.header.bloom), filterParams.addresses, filterParams.normalizedTopics)) { continue; } for (const tx of blockResults) { logs.push(...filter_1.filterLogs(tx.receipt.logs, { fromBlock: filterParams.fromBlock, toBlock: filterParams.toBlock, addresses: filterParams.addresses, normalizedTopics: filterParams.normalizedTopics, })); } } return logs; } async addCompilationResult(compilerVersion, compilerInput, compilerOutput) { let bytecodes; try { bytecodes = compiler_to_model_1.createModelsAndDecodeBytecodes(compilerVersion, compilerInput, compilerOutput); } catch (error) { console.warn(chalk_1.default.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; } _getSnapshotIndex(id) { 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; } _initLocalAccounts(localAccounts) { for (const pk of localAccounts) { this._accountPrivateKeys.set(ethereumjs_util_1.bufferToHex(ethereumjs_util_1.privateToAddress(pk)), pk); } } async _getConsoleLogMessages(vmTrace, vmTracerError) { 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); } async _manageErrors(vmResult, vmTrace, vmTracerError) { if (vmResult.exceptionError === undefined) { return undefined; } let stackTrace; 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 === exceptions_1.ERROR.OUT_OF_GAS) { if (this._isContractTooLargeStackTrace(stackTrace)) { return solidity_errors_1.encodeSolidityStackTrace("Transaction run out of gas", stackTrace); } return new errors_1.TransactionExecutionError("Transaction run out of gas"); } if (error.error === exceptions_1.ERROR.REVERT) { if (vmResult.returnValue.length === 0) { if (stackTrace !== undefined) { return solidity_errors_1.encodeSolidityStackTrace("Transaction reverted without a reason", stackTrace); } return new errors_1.TransactionExecutionError("Transaction reverted without a reason"); } if (stackTrace !== undefined) { return solidity_errors_1.encodeSolidityStackTrace(`VM Exception while processing transaction: revert ${revert_reasons_1.decodeRevertReason(vmResult.returnValue)}`, stackTrace); } return new errors_1.TransactionExecutionError(`VM Exception while processing transaction: revert ${revert_reasons_1.decodeRevertReason(vmResult.returnValue)}`); } if (stackTrace !== undefined) { return solidity_errors_1.encodeSolidityStackTrace("Transaction failed: revert", stackTrace); } return new errors_1.TransactionExecutionError("Transaction failed: revert"); } _isContractTooLargeStackTrace(stackTrace) { return (stackTrace !== undefined && stackTrace.length > 0 && stackTrace[stackTrace.length - 1].type === solidity_stack_trace_1.StackTraceEntryType.CONTRACT_TOO_LARGE_ERROR); } _calculateTimestampAndOffset(timestamp) { let blockTimestamp; let offsetShouldChange; let newOffset = new ethereumjs_util_1.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 ethereumjs_util_1.BN(0))) { if (this._nextBlockTimestamp.eq(new ethereumjs_util_1.BN(0))) { blockTimestamp = new ethereumjs_util_1.BN(utils_1.getCurrentTimestamp()).add(this._blockTimeOffsetSeconds); offsetShouldChange = false; } else { blockTimestamp = new ethereumjs_util_1.BN(this._nextBlockTimestamp); offsetShouldChange = true; } } else { offsetShouldChange = true; blockTimestamp = timestamp; } if (offsetShouldChange) { newOffset = blockTimestamp.sub(new ethereumjs_util_1.BN(utils_1.getCurrentTimestamp())); } return [blockTimestamp, offsetShouldChange, newOffset]; } async _getNextBlockTemplate(timestamp) { const block = new ethereumjs_block_1.default({ header: { gasLimit: this._blockGasLimit, nonce: "0x42", timestamp, }, }, { common: this._common }); block.validate = (blockchain, cb) => cb(null); const latestBlock = await this.getLatestBlock(); block.header.number = ethereumjs_util_1.toBuffer(new ethereumjs_util_1.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; } async _resetNextBlockTimestamp() { this._nextBlockTimestamp = new ethereumjs_util_1.BN(0); } async _saveTransactionAsReceived(tx) { this._transactionByHash.set(ethereumjs_util_1.bufferToHex(tx.hash(true)), tx); this._filters.forEach((filter) => { if (filter.type === filter_1.Type.PENDING_TRANSACTION_SUBSCRIPTION) { const hash = ethereumjs_util_1.bufferToHex(tx.hash(true)); if (filter.subscription) { this._emitEthEvent(filter.id, hash); return; } filter.hashes.push(hash); } }); } async _getLocalAccountPrivateKey(sender) { const senderAddress = ethereumjs_util_1.bufferToHex(sender); if (!this._accountPrivateKeys.has(senderAddress)) { throw new errors_1.InvalidInputError(`unknown account ${senderAddress}`); } return this._accountPrivateKeys.get(senderAddress); } async _addTransactionToBlock(block, tx) { block.transactions.push(tx); await util_1.promisify(block.genTxTrie.bind(block))(); block.header.transactionsTrie = block.txTrie.root; } async _saveBlockAsSuccessfullyRun(block, runBlockResult) { await this._putBlock(block); const txBlockResults = []; 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] = output_1.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 = ethereumjs_util_1.bufferToHex(block.hash()); this._blockHashToTxBlockResults.set(blockHash, txBlockResults); const td = this._computeTotalDifficulty(block); this._blockHashToTotalDifficulty.set(blockHash, td); const rpcLogs = []; 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 filter_1.Type.BLOCK_SUBSCRIPTION: const hash = block.hash(); if (filter.subscription) { this._emitEthEvent(filter.id, output_1.getRpcBlock(block, td, false)); return; } filter.hashes.push(ethereumjs_util_1.bufferToHex(hash)); break; case filter_1.Type.LOGS_SUBSCRIPTION: if (filter_1.bloomFilter(new bloom_1.default(block.header.bloom), filter.criteria.addresses, filter.criteria.normalizedTopics)) { const logs = filter_1.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; } }); } async _putBlock(block) { return new Promise((resolve, reject) => { this._vm.blockchain.putBlock(block, (err) => { if (err !== undefined && err !== null) { reject(err); return; } resolve(); }); }); } async _hasBlockWithHash(blockHash) { if (this._blockHashToTotalDifficulty.has(ethereumjs_util_1.bufferToHex(blockHash))) { return true; } const block = await this.getBlockByNumber(new ethereumjs_util_1.BN(0)); return block.hash().equals(blockHash); } async _saveTransactionAsSuccessfullyRun(tx, block) { this._transactionHashToBlockHash.set(ethereumjs_util_1.bufferToHex(tx.hash(true)), ethereumjs_util_1.bufferToHex(block.hash())); } async _transactionWasSuccessful(tx) { return this._transactionHashToBlockHash.has(ethereumjs_util_1.bufferToHex(tx.hash(true))); } async _timestampClashesWithPreviousBlockOne(block) { const blockTimestamp = new ethereumjs_util_1.BN(block.header.timestamp); const latestBlock = await this.getLatestBlock(); const latestBlockTimestamp = new ethereumjs_util_1.BN(latestBlock.header.timestamp); return latestBlockTimestamp.eq(blockTimestamp); } async _increaseBlockTimestamp(block) { block.header.timestamp = new ethereumjs_util_1.BN(block.header.timestamp).addn(1); } async _setBlockTimestamp(block, timestamp) { block.header.timestamp = new ethereumjs_util_1.BN(timestamp); } async _validateTransaction(tx) { // Geth throws this error if a tx is sent twice if (await this._transactionWasSuccessful(tx)) { throw new errors_1.InvalidInputError(`known transaction: ${ethereumjs_util_1.bufferToHex(tx.hash(true)).toString()}`); } if (!tx.verifySignature()) { throw new errors_1.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 errors_1.InvalidInputError("contract creation without any data provided"); } const expectedNonce = await this.getAccountNonce(tx.getSenderAddress(), null); const actualNonce = new ethereumjs_util_1.BN(tx.nonce); if (!expectedNonce.eq(actualNonce)) { throw new errors_1.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 ethereumjs_util_1.BN(tx.gasLimit); if (baseFee.gt(gasLimit)) { throw new errors_1.InvalidInputError(`Transaction requires at least ${baseFee} gas but got ${gasLimit}`); } if (gasLimit.gt(this._blockGasLimit)) { throw new errors_1.InvalidInputError(`Transaction gas limit is ${gasLimit} and exceeds block gas limit of ${this._blockGasLimit}`); } } _computeTotalDifficulty(block) { const difficulty = new ethereumjs_util_1.BN(block.header.difficulty); const parentHash = ethereumjs_util_1.bufferToHex(block.header.parentHash); if (parentHash === "0x0000000000000000000000000000000000000000000000000000000000000000") { return difficulty; } const parentTd = this._blockHashToTotalDifficulty.get(parentHash); if (parentTd === undefined) { throw new errors_1.InternalError(`Unrecognized parent block ${parentHash}`); } return parentTd.add(difficulty); } async _runOnBlockContext(blockNumber, action) { let block; if (blockNumber == null) { block = await this.getLatestBlock(); } else { block = await this.getBlockByNumber(blockNumber); } const currentStateRoot = await this._stateManager.getStateRoot(); await this._stateManager.setStateRoot(block.header.stateRoot); try { return await action(); } finally { await this._stateManager.setStateRoot(currentStateRoot); } } async _correctInitialEstimation(txParams, initialEstimation) { let tx = await this._getFakeTransaction(Object.assign(Object.assign({}, txParams), { gasLimit: initialEstimation })); if (tx.getBaseFee().gte(initialEstimation)) { initialEstimation = tx.getBaseFee().addn(1); tx = await this._getFakeTransaction(Object.assign(Object.assign({}, txParams), { gasLimit: initialEstimation })); } const result = await this._runTxAndRevertMutations(tx); if (result.execResult.exceptionError === undefined) { return initialEstimation; } return this._binarySearchEstimation(txParams, initialEstimation, await this.getBlockGasLimit()); } async _binarySearchEstimation(txParams, highestFailingEstimation, lowestSuccessfulEstimation, roundNumber = 0) { 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(4000000) ? 50000 : highestFailingEstimation.gten(1000000) ? 10000 : highestFailingEstimation.gten(100000) ? 1000 : highestFailingEstimation.gten(50000) ? 500 : highestFailingEstimation.gten(30000) ? 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(Object.assign(Object.assign({}, 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. */ async _runTxAndRevertMutations(tx, runOnNewBlock = true) { 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); } } async _computeFilterParams(filterParams, isFilter) { const latestBlockNumber = await this.getLatestBlockNumber(); const newFilterParams = Object.assign({}, filterParams); if (newFilterParams.fromBlock === filter_1.LATEST_BLOCK) { newFilterParams.fromBlock = latestBlockNumber; } if (!isFilter && newFilterParams.toBlock === filter_1.LATEST_BLOCK) { newFilterParams.toBlock = latestBlockNumber; } if (newFilterParams.toBlock.gt(latestBlockNumber)) { newFilterParams.toBlock = latestBlockNumber; } if (newFilterParams.fromBlock.gt(latestBlockNumber)) { newFilterParams.fromBlock = latestBlockNumber; } return newFilterParams; } _newDeadline() { const dt = new Date(); dt.setMinutes(dt.getMinutes() + 5); // This will not overflow return dt; } _getNextFilterId() { this._lastFilterId = this._lastFilterId.addn(1); return this._lastFilterId; } _filterIdToFiltersKey(filterId) { return filterId.toString(); } _emitEthEvent(filterId, result) { this.emit("ethEvent", { result, filterId, }); } } exports.BuidlerNode = BuidlerNode; //# sourceMappingURL=node.js.map