@polygon-hermez/vm
Version:
An Ethereum VM implementation
417 lines • 17.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateTxReceipt = void 0;
/* eslint-disable no-undef */
const debug_1 = require("debug");
const ethereumjs_util_1 = require("ethereumjs-util");
const block_1 = require("@ethereumjs/block");
const common_1 = require("@ethereumjs/common");
const tx_1 = require("@ethereumjs/tx");
const bloom_1 = __importDefault(require("./bloom"));
const evm_1 = __importDefault(require("./evm/evm"));
const util_1 = require("./evm/opcodes/util");
const message_1 = __importDefault(require("./evm/message"));
const txContext_1 = __importDefault(require("./evm/txContext"));
const precompiles_1 = require("./evm/precompiles");
const debug = (0, debug_1.debug)('vm:tx');
const debugGas = (0, debug_1.debug)('vm:tx:gas');
/**
* @ignore
*/
async function runTx(opts) {
var _a;
// tx is required
if (!opts.tx) {
throw new Error('invalid input, tx is required');
}
// create a reasonable default if no block is given
opts.block = (_a = opts.block) !== null && _a !== void 0 ? _a : block_1.Block.fromBlockData({}, { common: opts.tx.common });
if (opts.skipBlockGasLimitValidation !== true &&
opts.block.header.gasLimit.lt(opts.tx.gasLimit)) {
const msg = _errorMsg('tx has a higher gas limit than the block', this, opts.block, opts.tx);
throw new Error(msg);
}
// Have to cast as `EIP2929StateManager` to access clearWarmedAccounts
const state = this.stateManager;
if (opts.reportAccessList && !('generateAccessList' in state)) {
const msg = _errorMsg('reportAccessList needs a StateManager implementing the generateAccessList() method', this, opts.block, opts.tx);
throw new Error(msg);
}
// Ensure we start with a clear warmed accounts Map
if (this._common.isActivatedEIP(2929)) {
state.clearWarmedAccounts();
}
await state.checkpoint();
if (this.DEBUG) {
debug('-'.repeat(100));
debug(`tx checkpoint`);
}
// Typed transaction specific setup tasks
if (opts.tx.supports(tx_1.Capability.EIP2718TypedTransaction) && this._common.isActivatedEIP(2718)) {
// Is it an Access List transaction?
if (!this._common.isActivatedEIP(2930)) {
await state.revert();
const msg = _errorMsg('Cannot run transaction: EIP 2930 is not activated.', this, opts.block, opts.tx);
throw new Error(msg);
}
if (opts.reportAccessList && !('generateAccessList' in state)) {
await state.revert();
const msg = _errorMsg('StateManager needs to implement generateAccessList() when running with reportAccessList option', this, opts.block, opts.tx);
throw new Error(msg);
}
if (opts.tx.supports(tx_1.Capability.EIP1559FeeMarket) && !this._common.isActivatedEIP(1559)) {
await state.revert();
const msg = _errorMsg('Cannot run transaction: EIP 1559 is not activated.', this, opts.block, opts.tx);
throw new Error(msg);
}
const castedTx = opts.tx;
castedTx.AccessListJSON.forEach((accessListItem) => {
const address = (0, ethereumjs_util_1.toBuffer)(accessListItem.address);
state.addWarmedAddress(address);
accessListItem.storageKeys.forEach((storageKey) => {
state.addWarmedStorage(address, (0, ethereumjs_util_1.toBuffer)(storageKey));
});
});
}
try {
const result = await _runTx.bind(this)(opts);
await state.commit();
if (this.DEBUG) {
debug(`tx checkpoint committed`);
}
if (this._common.isActivatedEIP(2929) && opts.reportAccessList) {
const { tx } = opts;
// Do not include sender address in access list
const removed = [tx.getSenderAddress()];
// Only include to address on present storage slot accesses
const onlyStorage = tx.to ? [tx.to] : [];
result.accessList = state.generateAccessList(removed, onlyStorage);
}
return result;
}
catch (e) {
await state.revert();
if (this.DEBUG) {
debug(`tx checkpoint reverted`);
}
throw e;
}
finally {
if (this._common.isActivatedEIP(2929)) {
state.clearWarmedAccounts();
}
}
}
exports.default = runTx;
async function _runTx(opts) {
var _a, _b, _c;
// Have to cast as `EIP2929StateManager` to access the EIP2929 methods
const state = this.stateManager;
this.vcm = opts.vcm;
let tx = opts.tx;
const { block } = opts;
if (!block) {
throw new Error('block required');
}
/**
* The `beforeTx` event
*
* @event Event: beforeTx
* @type {Object}
* @property {Transaction} tx emits the Transaction that is about to be processed
*/
await this._emit('beforeTx', tx);
const caller = tx.getSenderAddress();
if (this.DEBUG) {
debug(`New tx run hash=${opts.tx.isSigned() ? opts.tx.hash().toString('hex') : 'unsigned'} sender=${caller}`);
}
if (this._common.isActivatedEIP(2929)) {
// Add origin and precompiles to warm addresses
(0, precompiles_1.getActivePrecompiles)(this._common).forEach((address) => state.addWarmedAddress(address.buf));
state.addWarmedAddress(caller.buf);
if (tx.to) {
// Note: in case we create a contract, we do this in EVMs `_executeCreate` (this is also correct in inner calls, per the EIP)
state.addWarmedAddress(tx.to.buf);
}
}
// Validate gas limit against tx base fee (DataFee + TxFee + Creation Fee)
const txBaseFee = tx.getBaseFee();
const gasLimit = tx.gasLimit.clone();
if (gasLimit.lt(txBaseFee)) {
const msg = _errorMsg('base fee exceeds gas limit', this, block, tx);
throw new Error(msg);
}
gasLimit.isub(txBaseFee);
if (this.DEBUG) {
debugGas(`Subtracting base fee (${txBaseFee}) from gasLimit (-> ${gasLimit})`);
}
if (this._common.isActivatedEIP(1559)) {
// EIP-1559 spec:
// Ensure that the user was willing to at least pay the base fee
// assert transaction.max_fee_per_gas >= block.base_fee_per_gas
const maxFeePerGas = 'maxFeePerGas' in tx ? tx.maxFeePerGas : tx.gasPrice;
const baseFeePerGas = block.header.baseFeePerGas;
if (maxFeePerGas.lt(baseFeePerGas)) {
const msg = _errorMsg(`Transaction's maxFeePerGas (${maxFeePerGas}) is less than the block's baseFeePerGas (${baseFeePerGas})`, this, block, tx);
throw new Error(msg);
}
}
// Check from account's balance and nonce
let fromAccount = await state.getAccount(caller);
const { nonce, balance } = fromAccount;
// EIP-3607: Reject transactions from senders with deployed code
if (this._common.isActivatedEIP(3607) && !fromAccount.codeHash.equals(ethereumjs_util_1.KECCAK256_NULL)) {
const msg = _errorMsg('invalid sender address, address is not EOA (EIP-3607)', this, block, tx);
throw new Error(msg);
}
if (!opts.skipBalance) {
const cost = tx.getUpfrontCost(block.header.baseFeePerGas);
if (balance.lt(cost)) {
const msg = _errorMsg(`sender doesn't have enough funds to send tx. The upfront cost is: ${cost} and the sender's account (${caller}) only has: ${balance}`, this, block, tx);
throw new Error(msg);
}
if (tx.supports(tx_1.Capability.EIP1559FeeMarket)) {
// EIP-1559 spec:
// The signer must be able to afford the transaction
// `assert balance >= gas_limit * max_fee_per_gas`
const cost = tx.gasLimit.mul(tx.maxFeePerGas).add(tx.value);
if (balance.lt(cost)) {
const msg = _errorMsg(`sender doesn't have enough funds to send tx. The max cost is: ${cost} and the sender's account (${caller}) only has: ${balance}`, this, block, tx);
throw new Error(msg);
}
}
}
if (!opts.skipNonce) {
if (!nonce.eq(tx.nonce)) {
const msg = _errorMsg(`the tx doesn't have the correct nonce. account has nonce of: ${nonce} tx has nonce of: ${tx.nonce}`, this, block, tx);
throw new Error(msg);
}
}
let gasPrice;
let inclusionFeePerGas;
// EIP-1559 tx
if (tx.supports(tx_1.Capability.EIP1559FeeMarket)) {
const baseFee = block.header.baseFeePerGas;
inclusionFeePerGas = ethereumjs_util_1.BN.min(tx.maxPriorityFeePerGas, tx.maxFeePerGas.sub(baseFee));
gasPrice = inclusionFeePerGas.add(baseFee);
}
else {
// Have to cast as legacy tx since EIP1559 tx does not have gas price
// Compute effective gas price and update tx
if (typeof opts.effectivePercentage === 'undefined') {
opts.effectivePercentage = 255;
}
const effectivegasPrice = tx.gasPrice
.mul(new ethereumjs_util_1.BN(Number(opts.effectivePercentage) + 1))
.div(new ethereumjs_util_1.BN(256));
gasPrice = effectivegasPrice;
// Create new Transaction object with updated gas price
tx = new tx_1.Transaction(Object.assign(Object.assign({}, tx), { gasPrice }));
if (this._common.isActivatedEIP(1559)) {
const baseFee = block.header.baseFeePerGas;
inclusionFeePerGas = tx.gasPrice.sub(baseFee);
}
}
// Update from account's nonce and balance
fromAccount.nonce.iaddn(1);
const txCost = tx.gasLimit.mul(gasPrice);
fromAccount.balance.isub(txCost);
await state.putAccount(caller, fromAccount);
if (this.DEBUG) {
debug(`Update fromAccount (caller) nonce (-> ${fromAccount.nonce}) and balance(-> ${fromAccount.balance})`);
}
/*
* Execute message
*/
const txContext = new txContext_1.default(gasPrice, caller);
const { value, data, to } = tx;
const message = new message_1.default({
caller,
gasLimit,
to,
value,
data,
});
const evm = new evm_1.default(this, txContext, block);
if (this.DEBUG) {
debug(`Running tx=0x${tx.isSigned() ? tx.hash().toString('hex') : 'unsigned'} with caller=${caller} gasLimit=${gasLimit} to=${to ? to.toString() : ''} value=${value} data=0x${(0, util_1.short)(data)}`);
}
const results = (await evm.executeMessage(message));
if (this.DEBUG) {
debug('-'.repeat(100));
debug(`Received tx results gasUsed=${results.gasUsed} execResult: [ gasUsed=${results.gasUsed} exceptionError=${results.execResult.exceptionError ? results.execResult.exceptionError.error : ''} returnValue=${(0, util_1.short)(results.execResult.returnValue)} gasRefund=${results.execResult.gasRefund} ]`);
}
/*
* Parse results
*/
// Generate the bloom for the tx
results.bloom = txLogsBloom(results.execResult.logs);
if (this.DEBUG) {
debug(`Generated tx bloom with logs=${(_a = results.execResult.logs) === null || _a === void 0 ? void 0 : _a.length}`);
}
// Caculate the total gas used
results.gasUsed.iadd(txBaseFee);
if (this.DEBUG) {
debugGas(`tx add baseFee ${txBaseFee} to gasUsed (-> ${results.gasUsed})`);
}
// Process any gas refund
let gasRefund = (_b = results.execResult.gasRefund) !== null && _b !== void 0 ? _b : new ethereumjs_util_1.BN(0);
const maxRefundQuotient = this._common.param('gasConfig', 'maxRefundQuotient');
if (!gasRefund.isZero()) {
const maxRefund = results.gasUsed.divn(maxRefundQuotient);
gasRefund = ethereumjs_util_1.BN.min(gasRefund, maxRefund);
results.gasUsed.isub(gasRefund);
if (this.DEBUG) {
debug(`Subtract tx gasRefund (${gasRefund}) from gasUsed (-> ${results.gasUsed})`);
}
}
else {
if (this.DEBUG) {
debug(`No tx gasRefund`);
}
}
results.amountSpent = results.gasUsed.mul(gasPrice);
// Update sender's balance
fromAccount = await state.getAccount(caller);
const actualTxCost = results.gasUsed.mul(gasPrice);
const txCostDiff = txCost.sub(actualTxCost);
fromAccount.balance.iadd(txCostDiff);
await state.putAccount(caller, fromAccount);
if (this.DEBUG) {
debug(`Refunded txCostDiff (${txCostDiff}) to fromAccount (caller) balance (-> ${fromAccount.balance})`);
}
// Update miner's balance
let miner;
if (this._common.consensusType() === common_1.ConsensusType.ProofOfAuthority) {
// Backwards-compatibilty check
// TODO: can be removed along VM v6 release
if ('cliqueSigner' in block.header) {
miner = block.header.cliqueSigner();
}
else {
miner = ethereumjs_util_1.Address.zero();
}
}
else {
miner = block.header.coinbase;
}
const minerAccount = await state.getAccount(miner);
// add the amount spent on gas to the miner's account
if (this._common.isActivatedEIP(1559)) {
minerAccount.balance.iadd(results.gasUsed.mul(inclusionFeePerGas));
}
else {
minerAccount.balance.iadd(results.amountSpent);
}
// Put the miner account into the state. If the balance of the miner account remains zero, note that
// the state.putAccount function puts this into the "touched" accounts. This will thus be removed when
// we clean the touched accounts below in case we are in a fork >= SpuriousDragon
await state.putAccount(miner, minerAccount);
if (this.DEBUG) {
debug(`tx update miner account (${miner}) balance (-> ${minerAccount.balance})`);
}
/*
* Cleanup accounts
*/
if (results.execResult.selfdestruct) {
const keys = Object.keys(results.execResult.selfdestruct);
for (const k of keys) {
const address = new ethereumjs_util_1.Address(Buffer.from(k, 'hex'));
await state.deleteAccount(address);
if (this.DEBUG) {
debug(`tx selfdestruct on address=${address}`);
}
}
}
await state.cleanupTouchedAccounts();
state.clearOriginalStorageCache();
// Generate the tx receipt
const cumulativeGasUsed = ((_c = opts.blockGasUsed) !== null && _c !== void 0 ? _c : block.header.gasUsed).add(results.gasUsed);
results.receipt = await generateTxReceipt.bind(this)(tx, results, cumulativeGasUsed);
/**
* The `afterTx` event
*
* @event Event: afterTx
* @type {Object}
* @property {Object} result result of the transaction
*/
const event = Object.assign({ transaction: tx }, results);
await this._emit('afterTx', event);
if (this.DEBUG) {
debug(`tx run finished hash=${opts.tx.isSigned() ? opts.tx.hash().toString('hex') : 'unsigned'} sender=${caller}`);
}
return results;
}
/**
* @method txLogsBloom
* @private
*/
function txLogsBloom(logs) {
const bloom = new bloom_1.default();
if (logs) {
for (let i = 0; i < logs.length; i++) {
const log = logs[i];
// add the address
bloom.add(log[0]);
// add the topics
const topics = log[1];
for (let q = 0; q < topics.length; q++) {
bloom.add(topics[q]);
}
}
}
return bloom;
}
/**
* Returns the tx receipt.
* @param this The vm instance
* @param tx The transaction
* @param txResult The tx result
* @param cumulativeGasUsed The gas used in the block including this tx
*/
async function generateTxReceipt(tx, txResult, cumulativeGasUsed) {
var _a;
const baseReceipt = {
gasUsed: cumulativeGasUsed.toArrayLike(Buffer),
bitvector: txResult.bloom.bitvector,
logs: (_a = txResult.execResult.logs) !== null && _a !== void 0 ? _a : [],
};
let receipt;
if (this.DEBUG) {
debug(`Generate tx receipt transactionType=${tx.type} gasUsed=${cumulativeGasUsed} bitvector=${(0, util_1.short)(baseReceipt.bitvector)} (${baseReceipt.bitvector.length} bytes) logs=${baseReceipt.logs.length}`);
}
if (!tx.supports(tx_1.Capability.EIP2718TypedTransaction)) {
// Legacy transaction
if (this._common.gteHardfork('byzantium')) {
// Post-Byzantium
receipt = Object.assign({ status: txResult.execResult.exceptionError ? 0 : 1 }, baseReceipt);
}
else {
// Pre-Byzantium
const stateRoot = await this.stateManager.getStateRoot(true);
receipt = Object.assign({ stateRoot: stateRoot }, baseReceipt);
}
}
else {
// Typed EIP-2718 Transaction
receipt = Object.assign({ status: txResult.execResult.exceptionError ? 0 : 1 }, baseReceipt);
}
return receipt;
}
exports.generateTxReceipt = generateTxReceipt;
/**
* Internal helper function to create an annotated error message
*
* @param msg Base error message
* @hidden
*/
function _errorMsg(msg, vm, block, tx) {
const blockErrorStr = 'errorStr' in block ? block.errorStr() : 'block';
const txErrorStr = 'errorStr' in tx ? tx.errorStr() : 'tx';
const errorMsg = `${msg} (${vm.errorStr()} -> ${blockErrorStr} -> ${txErrorStr})`;
return errorMsg;
}
//# sourceMappingURL=runTx.js.map