@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
JavaScript
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
;