UNPKG

@polygon-hermez/vm

Version:
622 lines 22.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const debug_1 = require("debug"); const ethereumjs_util_1 = require("ethereumjs-util"); const common_1 = require("@ethereumjs/common"); const exceptions_1 = require("../exceptions"); const message_1 = __importDefault(require("./message")); const ethers = require('ethers'); const ADDRESS_SYSTEM = '0x000000000000000000000000000000005ca1ab1e'; const LAST_TX_STORAGE_POS = '0x0000000000000000000000000000000000000000000000000000000000000000'; const STATE_ROOT_STORAGE_POS = 1; const debugGas = (0, debug_1.debug)('vm:eei:gas'); function trap(err) { throw new exceptions_1.VmError(err); } const MASK_160 = new ethereumjs_util_1.BN(1).shln(160).subn(1); function addressToBuffer(address) { if (Buffer.isBuffer(address)) return address; return address.and(MASK_160).toArrayLike(Buffer, 'be', 20); } /** * External interface made available to EVM bytecode. Modeled after * the ewasm EEI [spec](https://github.com/ewasm/design/blob/master/eth_interface.md). * It includes methods for accessing/modifying state, calling or creating contracts, access * to environment data among other things. * The EEI instance also keeps artifacts produced by the bytecode such as logs * and to-be-selfdestructed addresses. */ class EEI { constructor(env, state, evm, common, gasLeft) { this._env = env; this._state = state; this._evm = evm; this._lastReturned = Buffer.alloc(0); this._common = common; this._gasLeft = gasLeft; this._result = { logs: [], returnValue: undefined, selfdestruct: {}, }; } /** * Subtracts an amount from the gas counter. * @param amount - Amount of gas to consume * @param context - Usage context for debugging * @throws if out of gas */ useGas(amount, context) { this._gasLeft.isub(amount); if (this._evm._vm.DEBUG) { debugGas(`${context ? context + ': ' : ''}used ${amount} gas (-> ${this._gasLeft})`); } if (this._gasLeft.ltn(0)) { this._gasLeft = new ethereumjs_util_1.BN(0); trap(exceptions_1.ERROR.OUT_OF_GAS); } } /** * Adds a positive amount to the gas counter. * @param amount - Amount of gas refunded * @param context - Usage context for debugging */ refundGas(amount, context) { if (this._evm._vm.DEBUG) { debugGas(`${context ? context + ': ' : ''}refund ${amount} gas (-> ${this._evm._refund})`); } this._evm._refund.iadd(amount); } /** * Reduces amount of gas to be refunded by a positive value. * @param amount - Amount to subtract from gas refunds * @param context - Usage context for debugging */ subRefund(amount, context) { if (this._evm._vm.DEBUG) { debugGas(`${context ? context + ': ' : ''}sub gas refund ${amount} (-> ${this._evm._refund})`); } this._evm._refund.isub(amount); if (this._evm._refund.ltn(0)) { this._evm._refund = new ethereumjs_util_1.BN(0); trap(exceptions_1.ERROR.REFUND_EXHAUSTED); } } /** * Increments the internal gasLeft counter. Used for adding callStipend. * @param amount - Amount to add */ addStipend(amount) { if (this._evm._vm.DEBUG) { debugGas(`add stipend ${amount} (-> ${this._gasLeft})`); } this._gasLeft.iadd(amount); } /** * Returns address of currently executing account. */ getAddress() { return this._env.address; } /** * Returns balance of the given account. * @param address - Address of account */ async getExternalBalance(address) { // shortcut if current account if (address.equals(this._env.address)) { return this._env.contract.balance; } // otherwise load account then return balance const account = await this._state.getAccount(address); return account.balance; } /** * Returns balance of self. */ getSelfBalance() { return this._env.contract.balance; } /** * Returns caller address. This is the address of the account * that is directly responsible for this execution. */ getCaller() { return new ethereumjs_util_1.BN(this._env.caller.buf); } /** * Returns the deposited value by the instruction/transaction * responsible for this execution. */ getCallValue() { return new ethereumjs_util_1.BN(this._env.callValue); } /** * Returns input data in current environment. This pertains to the input * data passed with the message call instruction or transaction. */ getCallData() { return this._env.callData; } /** * Returns size of input data in current environment. This pertains to the * input data passed with the message call instruction or transaction. */ getCallDataSize() { return new ethereumjs_util_1.BN(this._env.callData.length); } /** * Returns the size of code running in current environment. */ getCodeSize() { return new ethereumjs_util_1.BN(this._env.code.length); } /** * Returns the code running in current environment. */ getCode() { return this._env.code; } /** * Returns true if the current call must be executed statically. */ isStatic() { return this._env.isStatic; } /** * Get size of an account’s code. * @param address - Address of account */ async getExternalCodeSize(address) { const addr = new ethereumjs_util_1.Address(addressToBuffer(address)); const code = await this._state.getContractCode(addr); return new ethereumjs_util_1.BN(code.length); } /** * Returns code of an account. * @param address - Address of account */ async getExternalCode(address) { const addr = new ethereumjs_util_1.Address(addressToBuffer(address)); return this._state.getContractCode(addr); } /** * Returns size of current return data buffer. This contains the return data * from the last executed call, callCode, callDelegate, callStatic or create. * Note: create only fills the return data buffer in case of a failure. */ getReturnDataSize() { return new ethereumjs_util_1.BN(this._lastReturned.length); } /** * Returns the current return data buffer. This contains the return data * from last executed call, callCode, callDelegate, callStatic or create. * Note: create only fills the return data buffer in case of a failure. */ getReturnData() { return this._lastReturned; } /** * Returns price of gas in current environment. */ getTxGasPrice() { return this._env.gasPrice; } /** * Returns the execution's origination address. This is the * sender of original transaction; it is never an account with * non-empty associated code. */ getTxOrigin() { return new ethereumjs_util_1.BN(this._env.origin.buf); } /** * Returns the block’s number. */ getBlockNumber() { return this._env.block.header.number; } /** * Returns the block's beneficiary address. */ getBlockCoinbase() { let coinbase; if (this._common.consensusAlgorithm() === common_1.ConsensusAlgorithm.Clique) { // Backwards-compatibilty check // TODO: can be removed along VM v5 release if ('cliqueSigner' in this._env.block.header) { coinbase = this._env.block.header.cliqueSigner(); } else { coinbase = ethereumjs_util_1.Address.zero(); } } else { coinbase = this._env.block.header.coinbase; } return new ethereumjs_util_1.BN(coinbase.toBuffer()); } /** * Returns the block's timestamp. */ getBlockTimestamp() { return this._env.block.header.timestamp; } /** * Returns the block's difficulty. */ getBlockDifficulty() { return this._env.block.header.difficulty; } /** * Returns the block's gas limit. */ getBlockGasLimit() { return this._env.block.header.gasLimit; } /** * Returns the chain ID for current chain. Introduced for the * CHAINID opcode proposed in [EIP-1344](https://eips.ethereum.org/EIPS/eip-1344). */ getChainId() { return this._common.chainIdBN(); } /** * Returns the Base Fee of the block as proposed in [EIP-3198](https;//eips.etheruem.org/EIPS/eip-3198) */ getBlockBaseFee() { const baseFee = this._env.block.header.baseFeePerGas; if (baseFee === undefined) { // Sanity check throw new Error('Block has no Base Fee'); } return baseFee; } /** * Returns Gets the hash of one of the 256 most recent complete blocks. * @param num - Number of block */ async getBlockHash(num) { const block = await this._env.blockchain.getBlock(num); return new ethereumjs_util_1.BN(block.hash()); } /** * Returns state root of the requested tx/block * @param num - Number of block */ async getBatchHash(num) { const stateRootPos = ethers.utils.solidityKeccak256(['uint256', 'uint256'], [Number(num), STATE_ROOT_STORAGE_POS]); const hash = await this._state.getContractStorage(new ethereumjs_util_1.Address((0, ethereumjs_util_1.toBuffer)(ADDRESS_SYSTEM)), (0, ethereumjs_util_1.toBuffer)(stateRootPos)); if (!hash || hash.length === 0) { return new ethereumjs_util_1.BN(0); } return new ethereumjs_util_1.BN(hash); } /** * Returns the current tx/block number * @param num - Number of block */ async getBlockNum() { const blockNum = await this._state.getContractStorage(new ethereumjs_util_1.Address((0, ethereumjs_util_1.toBuffer)(ADDRESS_SYSTEM)), (0, ethereumjs_util_1.toBuffer)(LAST_TX_STORAGE_POS)); return new ethereumjs_util_1.BN(blockNum); } /** * Store 256-bit a value in memory to persistent storage. */ async storageStore(key, value) { await this._state.putContractStorage(this._env.address, key, value); const account = await this._state.getAccount(this._env.address); this._env.contract = account; } /** * Loads a 256-bit value to memory from persistent storage. * @param key - Storage key * @param original - If true, return the original storage value (default: false) */ async storageLoad(key, original = false) { if (original) { return this._state.getOriginalContractStorage(this._env.address, key); } else { return this._state.getContractStorage(this._env.address, key); } } /** * Returns the current gasCounter. */ getGasLeft() { return this._gasLeft.clone(); } /** * Set the returning output data for the execution. * @param returnData - Output data to return */ finish(returnData) { this._result.returnValue = returnData; trap(exceptions_1.ERROR.STOP); } /** * Set the returning output data for the execution. This will halt the * execution immediately and set the execution result to "reverted". * @param returnData - Output data to return */ revert(returnData) { this._result.returnValue = returnData; trap(exceptions_1.ERROR.REVERT); } /** * Mark account for later deletion and give the remaining balance to the * specified beneficiary address. This will cause a trap and the * execution will be aborted immediately. * @param toAddress - Beneficiary address */ async selfDestruct(toAddress) { // return this._selfDestruct(toAddress) return this._sendall(toAddress); } async _sendall(toAddress) { // not move balances if receiver address is the same as beneficiary if (toAddress.equals(this._env.address)) { trap(exceptions_1.ERROR.STOP); } // Add to beneficiary balance const toAccount = await this._state.getAccount(toAddress); toAccount.balance.iadd(this._env.contract.balance); await this._state.putAccount(toAddress, toAccount); // Subtract from contract balance const account = await this._state.getAccount(this._env.address); account.balance = new ethereumjs_util_1.BN(0); await this._state.putAccount(this._env.address, account); trap(exceptions_1.ERROR.STOP); } async _customSelfDestruct(toAddress) { // only add to refund if this is the first selfdestruct for the address if (!this._result.selfdestruct[this._env.address.buf.toString('hex')]) { this.refundGas(new ethereumjs_util_1.BN(this._common.param('gasPrices', 'selfdestructRefund'))); } // clear contract bytecode await this._state.putContractCode(this._env.address, (0, ethereumjs_util_1.toBuffer)('0x')); // Add to beneficiary balance const toAccount = await this._state.getAccount(toAddress); toAccount.balance.iadd(this._env.contract.balance); await this._state.putAccount(toAddress, toAccount); // Subtract from contract balance const account = await this._state.getAccount(this._env.address); account.balance = new ethereumjs_util_1.BN(0); await this._state.putAccount(this._env.address, account); trap(exceptions_1.ERROR.STOP); } async _selfDestruct(toAddress) { // only add to refund if this is the first selfdestruct for the address if (!this._result.selfdestruct[this._env.address.buf.toString('hex')]) { this.refundGas(new ethereumjs_util_1.BN(this._common.param('gasPrices', 'selfdestructRefund'))); } this._result.selfdestruct[this._env.address.buf.toString('hex')] = toAddress.buf; // Add to beneficiary balance const toAccount = await this._state.getAccount(toAddress); toAccount.balance.iadd(this._env.contract.balance); await this._state.putAccount(toAddress, toAccount); // Subtract from contract balance const account = await this._state.getAccount(this._env.address); account.balance = new ethereumjs_util_1.BN(0); await this._state.putAccount(this._env.address, account); trap(exceptions_1.ERROR.STOP); } /** * Creates a new log in the current environment. */ log(data, numberOfTopics, topics) { if (numberOfTopics < 0 || numberOfTopics > 4) { trap(exceptions_1.ERROR.OUT_OF_RANGE); } if (topics.length !== numberOfTopics) { trap(exceptions_1.ERROR.INTERNAL_ERROR); } const log = [this._env.address.buf, topics, data]; this._result.logs.push(log); } /** * Sends a message with arbitrary data to a given address path. */ async call(gasLimit, address, value, data, outLength) { const msg = new message_1.default({ caller: this._env.address, gasLimit, to: address, value, data, isStatic: this._env.isStatic, depth: this._env.depth + 1, outLength, }); return this._baseCall(msg); } /** * Message-call into this account with an alternative account's code. */ async callCode(gasLimit, address, value, data, outLength) { const msg = new message_1.default({ caller: this._env.address, gasLimit, to: this._env.address, codeAddress: address, value, data, isStatic: this._env.isStatic, depth: this._env.depth + 1, outLength, }); return this._baseCall(msg); } /** * Sends a message with arbitrary data to a given address path, but disallow * state modifications. This includes log, create, selfdestruct and call with * a non-zero value. */ async callStatic(gasLimit, address, value, data, outLength) { const msg = new message_1.default({ caller: this._env.address, gasLimit, to: address, value, data, isStatic: true, depth: this._env.depth + 1, outLength, }); return this._baseCall(msg); } /** * Message-call into this account with an alternative account’s code, but * persisting the current values for sender and value. */ async callDelegate(gasLimit, address, value, data, outLength) { const msg = new message_1.default({ caller: this._env.caller, gasLimit, to: this._env.address, codeAddress: address, value, data, isStatic: this._env.isStatic, delegatecall: true, depth: this._env.depth + 1, outLength, }); return this._baseCall(msg); } async _baseCall(msg) { const selfdestruct = Object.assign({}, this._result.selfdestruct); msg.selfdestruct = selfdestruct; // empty the return data buffer this._lastReturned = Buffer.alloc(0); // Check if account has enough ether and max depth not exceeded if (this._env.depth >= this._common.param('vm', 'stackLimit') || (msg.delegatecall !== true && this._env.contract.balance.lt(msg.value))) { return { returnCode: new ethereumjs_util_1.BN(0), }; } const results = await this._evm.executeMessage(msg); if (results.execResult.logs) { this._result.logs = this._result.logs.concat(results.execResult.logs); } // this should always be safe this.useGas(results.gasUsed, 'CALL, STATICCALL, DELEGATECALL, CALLCODE'); // Set return value if (results.execResult.returnValue && (!results.execResult.exceptionError || results.execResult.exceptionError.error === exceptions_1.ERROR.REVERT)) { this._lastReturned = results.execResult.returnValue; } if (!results.execResult.exceptionError) { Object.assign(this._result.selfdestruct, selfdestruct); // update stateRoot on current contract const account = await this._state.getAccount(this._env.address); this._env.contract = account; } return { returnCode: this._getReturnCode(results), results, }; } /** * Creates a new contract with a given value. */ async create(gasLimit, value, data, salt = null) { const selfdestruct = Object.assign({}, this._result.selfdestruct); const msg = new message_1.default({ caller: this._env.address, gasLimit, value, data, salt, depth: this._env.depth + 1, selfdestruct, isCreate: true, }); // empty the return data buffer this._lastReturned = Buffer.alloc(0); // Check if account has enough ether and max depth not exceeded if (this._env.depth >= this._common.param('vm', 'stackLimit') || (msg.delegatecall !== true && this._env.contract.balance.lt(msg.value))) { return { returnCode: new ethereumjs_util_1.BN(0), }; } // EIP-2681 check if (this._env.contract.nonce.gte(ethereumjs_util_1.MAX_UINT64)) { return { returnCode: new ethereumjs_util_1.BN(0), }; } this._env.contract.nonce.iaddn(1); await this._state.putAccount(this._env.address, this._env.contract); const results = await this._evm.executeMessage(msg); if (results.execResult.logs) { this._result.logs = this._result.logs.concat(results.execResult.logs); } // this should always be safe this.useGas(results.gasUsed, 'CREATE'); // Set return buffer in case revert happened if (results.execResult.exceptionError && results.execResult.exceptionError.error === exceptions_1.ERROR.REVERT) { this._lastReturned = results.execResult.returnValue; } if (!results.execResult.exceptionError || results.execResult.exceptionError.error === exceptions_1.ERROR.CODESTORE_OUT_OF_GAS) { Object.assign(this._result.selfdestruct, selfdestruct); // update stateRoot on current contract const account = await this._state.getAccount(this._env.address); this._env.contract = account; if (results.createdAddress) { // push the created address to the stack return { returnCode: new ethereumjs_util_1.BN(results.createdAddress.buf), results, }; } } return { returnCode: this._getReturnCode(results), results, }; } /** * Creates a new contract with a given value. Generates * a deterministic address via CREATE2 rules. */ async create2(gasLimit, value, data, salt) { return this.create(gasLimit, value, data, salt); } /** * Returns true if account is empty or non-existent (according to EIP-161). * @param address - Address of account */ async isAccountEmpty(address) { return this._state.accountIsEmpty(address); } /** * Returns true if account exists in the state trie (it can be empty). Returns false if the account is `null`. * @param address - Address of account */ async accountExists(address) { return this._state.accountExists(address); } _getReturnCode(results) { // This preserves the previous logic, but seems to contradict the EEI spec // https://github.com/ewasm/design/blob/38eeded28765f3e193e12881ea72a6ab807a3371/eth_interface.md if (results.execResult.exceptionError) { return new ethereumjs_util_1.BN(0); } else { return new ethereumjs_util_1.BN(1); } } } exports.default = EEI; //# sourceMappingURL=eei.js.map