@polygon-hermez/vm
Version:
An Ethereum VM implementation
505 lines (455 loc) • 16.4 kB
text/typescript
import {
addressToBuffer,
divCeil,
maxCallGas,
setLengthLeftStorage,
subMemUsage,
trap,
updateSstoreGas,
} from '.'
import { Address, BN } from 'ethereumjs-util'
import { ERROR } from '../../exceptions'
import { RunState } from '../interpreter'
import Common from '@ethereumjs/common'
import { updateSstoreGasEIP1283 } from './EIP1283'
import { updateSstoreGasEIP2200 } from './EIP2200'
import { accessAddressEIP2929, accessStorageEIP2929 } from './EIP2929'
/**
* This file returns the dynamic parts of opcodes which have dynamic gas
* These are not pure functions: some edit the size of the memory
* These functions are therefore not read-only
*/
// The dynamic gas handler methods take a runState and a gas BN
// The gas BN is necessary, since the base fee needs to be included,
// to calculate the max call gas for the call opcodes correctly.
export interface AsyncDynamicGasHandler {
(runState: RunState, gas: BN, common: Common): Promise<void>
}
export const dynamicGasHandlers: Map<number, AsyncDynamicGasHandler> = new Map<
number,
AsyncDynamicGasHandler
>([
[
/* SHA3 */
0x20,
async function (runState, gas, common): Promise<void> {
const [offset, length] = runState.stack.peek(2)
gas.iadd(subMemUsage(runState, offset, length, common))
gas.iadd(new BN(common.param('gasPrices', 'sha3Word')).imul(divCeil(length, new BN(32))))
},
],
[
/* BALANCE */
0x31,
async function (runState, gas, common): Promise<void> {
if (common.isActivatedEIP(2929)) {
const addressBN = runState.stack.peek()[0]
const address = new Address(addressToBuffer(addressBN))
gas.iadd(accessAddressEIP2929(runState, address, common))
}
},
],
[
/* CALLDATACOPY */
0x37,
async function (runState, gas, common): Promise<void> {
const [memOffset, _dataOffset, dataLength] = runState.stack.peek(3)
gas.iadd(subMemUsage(runState, memOffset, dataLength, common))
if (!dataLength.eqn(0)) {
gas.iadd(new BN(common.param('gasPrices', 'copy')).imul(divCeil(dataLength, new BN(32))))
}
},
],
[
/* CODECOPY */
0x39,
async function (runState, gas, common): Promise<void> {
const [memOffset, _codeOffset, dataLength] = runState.stack.peek(3)
gas.iadd(subMemUsage(runState, memOffset, dataLength, common))
if (!dataLength.eqn(0)) {
gas.iadd(new BN(common.param('gasPrices', 'copy')).imul(divCeil(dataLength, new BN(32))))
}
},
],
[
/* EXTCODESIZE */
0x3b,
async function (runState, gas, common): Promise<void> {
if (common.isActivatedEIP(2929)) {
const addressBN = runState.stack.peek()[0]
const address = new Address(addressToBuffer(addressBN))
gas.iadd(accessAddressEIP2929(runState, address, common))
}
},
],
[
/* EXTCODECOPY */
0x3c,
async function (runState, gas, common): Promise<void> {
const [addressBN, memOffset, _codeOffset, dataLength] = runState.stack.peek(4)
gas.iadd(subMemUsage(runState, memOffset, dataLength, common))
if (common.isActivatedEIP(2929)) {
const address = new Address(addressToBuffer(addressBN))
gas.iadd(accessAddressEIP2929(runState, address, common))
}
if (!dataLength.eqn(0)) {
gas.iadd(new BN(common.param('gasPrices', 'copy')).imul(divCeil(dataLength, new BN(32))))
}
},
],
[
/* RETURNDATACOPY */
0x3e,
async function (runState, gas, common): Promise<void> {
const [memOffset, returnDataOffset, dataLength] = runState.stack.peek(3)
if (returnDataOffset.add(dataLength).gt(runState.eei.getReturnDataSize())) {
trap(ERROR.OUT_OF_GAS)
}
gas.iadd(subMemUsage(runState, memOffset, dataLength, common))
if (!dataLength.eqn(0)) {
gas.iadd(new BN(common.param('gasPrices', 'copy')).mul(divCeil(dataLength, new BN(32))))
}
},
],
[
/* EXTCODEHASH */
0x3f,
async function (runState, gas, common): Promise<void> {
if (common.isActivatedEIP(2929)) {
const addressBN = runState.stack.peek()[0]
const address = new Address(addressToBuffer(addressBN))
gas.iadd(accessAddressEIP2929(runState, address, common))
}
},
],
[
/* MLOAD */
0x51,
async function (runState, gas, common): Promise<void> {
const pos = runState.stack.peek()[0]
gas.iadd(subMemUsage(runState, pos, new BN(32), common))
},
],
[
/* MSTORE */
0x52,
async function (runState, gas, common): Promise<void> {
const offset = runState.stack.peek()[0]
gas.iadd(subMemUsage(runState, offset, new BN(32), common))
},
],
[
/* MSTORE8 */
0x53,
async function (runState, gas, common): Promise<void> {
const offset = runState.stack.peek()[0]
gas.iadd(subMemUsage(runState, offset, new BN(1), common))
},
],
[
/* SLOAD */
0x54,
async function (runState, gas, common): Promise<void> {
const key = runState.stack.peek()[0]
const keyBuf = key.toArrayLike(Buffer, 'be', 32)
if (common.isActivatedEIP(2929)) {
gas.iadd(accessStorageEIP2929(runState, keyBuf, false, common))
}
},
],
[
/* SSTORE */
0x55,
async function (runState, gas, common): Promise<void> {
if (runState.eei.isStatic()) {
trap(ERROR.STATIC_STATE_CHANGE)
}
const [key, val] = runState.stack.peek(2)
const keyBuf = key.toArrayLike(Buffer, 'be', 32)
// NOTE: this should be the shortest representation
let value
if (val.isZero()) {
value = Buffer.from([])
} else {
value = val.toArrayLike(Buffer, 'be')
}
// TODO: Replace getContractStorage with EEI method
const currentStorage = setLengthLeftStorage(await runState.eei.storageLoad(keyBuf))
const originalStorage = setLengthLeftStorage(await runState.eei.storageLoad(keyBuf, true))
if (common.hardfork() === 'constantinople') {
gas.iadd(
updateSstoreGasEIP1283(
runState,
currentStorage,
originalStorage,
setLengthLeftStorage(value),
common
)
)
} else if (common.gteHardfork('istanbul')) {
gas.iadd(
updateSstoreGasEIP2200(
runState,
currentStorage,
originalStorage,
setLengthLeftStorage(value),
keyBuf,
common
)
)
} else {
gas.iadd(updateSstoreGas(runState, currentStorage, setLengthLeftStorage(value), common))
}
if (common.isActivatedEIP(2929)) {
// We have to do this after the Istanbul (EIP2200) checks.
// Otherwise, we might run out of gas, due to "sentry check" of 2300 gas,
// if we deduct extra gas first.
gas.iadd(accessStorageEIP2929(runState, keyBuf, true, common))
}
},
],
[
/* LOG */
0xa0,
async function (runState, gas, common): Promise<void> {
if (runState.eei.isStatic()) {
trap(ERROR.STATIC_STATE_CHANGE)
}
const [memOffset, memLength] = runState.stack.peek(2)
const topicsCount = runState.opCode - 0xa0
if (topicsCount < 0 || topicsCount > 4) {
trap(ERROR.OUT_OF_RANGE)
}
gas.iadd(subMemUsage(runState, memOffset, memLength, common))
gas.iadd(
new BN(common.param('gasPrices', 'logTopic'))
.imuln(topicsCount)
.iadd(memLength.muln(common.param('gasPrices', 'logData')))
)
},
],
[
/* CREATE */
0xf0,
async function (runState, gas, common): Promise<void> {
if (runState.eei.isStatic()) {
trap(ERROR.STATIC_STATE_CHANGE)
}
const [_value, offset, length] = runState.stack.peek(3)
if (common.isActivatedEIP(2929)) {
gas.iadd(accessAddressEIP2929(runState, runState.eei.getAddress(), common, false))
}
gas.iadd(subMemUsage(runState, offset, length, common))
let gasLimit = new BN(runState.eei.getGasLeft().isub(gas))
gasLimit = maxCallGas(gasLimit.clone(), gasLimit.clone(), runState, common)
runState.messageGasLimit = gasLimit
},
],
[
/* CALL */
0xf1,
async function (runState, gas, common): Promise<void> {
const [currentGasLimit, toAddr, value, inOffset, inLength, outOffset, outLength] =
runState.stack.peek(7)
const toAddress = new Address(addressToBuffer(toAddr))
if (runState.eei.isStatic() && !value.isZero()) {
trap(ERROR.STATIC_STATE_CHANGE)
}
gas.iadd(subMemUsage(runState, inOffset, inLength, common))
gas.iadd(subMemUsage(runState, outOffset, outLength, common))
if (common.isActivatedEIP(2929)) {
gas.iadd(accessAddressEIP2929(runState, toAddress, common))
}
if (!value.isZero()) {
gas.iadd(new BN(common.param('gasPrices', 'callValueTransfer')))
}
if (common.gteHardfork('spuriousDragon')) {
// We are at or after Spurious Dragon
// Call new account gas: account is DEAD and we transfer nonzero value
if ((await runState.eei.isAccountEmpty(toAddress)) && !value.isZero()) {
gas.iadd(new BN(common.param('gasPrices', 'callNewAccount')))
}
} else if (!(await runState.eei.accountExists(toAddress))) {
// We are before Spurious Dragon and the account does not exist.
// Call new account gas: account does not exist (it is not in the state trie, not even as an "empty" account)
gas.iadd(new BN(common.param('gasPrices', 'callNewAccount')))
}
const gasLimit = maxCallGas(
currentGasLimit.clone(),
runState.eei.getGasLeft().isub(gas),
runState,
common
)
// note that TangerineWhistle or later this cannot happen
// (it could have ran out of gas prior to getting here though)
if (gasLimit.gt(runState.eei.getGasLeft().isub(gas))) {
trap(ERROR.OUT_OF_GAS)
}
if (gas.gt(runState.eei.getGasLeft())) {
trap(ERROR.OUT_OF_GAS)
}
if (!value.isZero()) {
// TODO: Don't use private attr directly
runState.eei._gasLeft.iaddn(common.param('gasPrices', 'callStipend'))
gasLimit.iaddn(common.param('gasPrices', 'callStipend'))
}
runState.messageGasLimit = gasLimit
},
],
[
/* CALLCODE */
0xf2,
async function (runState, gas, common): Promise<void> {
const [currentGasLimit, toAddr, value, inOffset, inLength, outOffset, outLength] =
runState.stack.peek(7)
gas.iadd(subMemUsage(runState, inOffset, inLength, common))
gas.iadd(subMemUsage(runState, outOffset, outLength, common))
if (common.isActivatedEIP(2929)) {
const toAddress = new Address(addressToBuffer(toAddr))
gas.iadd(accessAddressEIP2929(runState, toAddress, common))
}
if (!value.isZero()) {
gas.iadd(new BN(common.param('gasPrices', 'callValueTransfer')))
}
const gasLimit = maxCallGas(
currentGasLimit.clone(),
runState.eei.getGasLeft().isub(gas),
runState,
common
)
// note that TangerineWhistle or later this cannot happen
// (it could have ran out of gas prior to getting here though)
if (gasLimit.gt(runState.eei.getGasLeft().isub(gas))) {
trap(ERROR.OUT_OF_GAS)
}
if (!value.isZero()) {
// TODO: Don't use private attr directly
runState.eei._gasLeft.iaddn(common.param('gasPrices', 'callStipend'))
gasLimit.iaddn(common.param('gasPrices', 'callStipend'))
}
runState.messageGasLimit = gasLimit
},
],
[
/* RETURN */
0xf3,
async function (runState, gas, common): Promise<void> {
const [offset, length] = runState.stack.peek(2)
gas.iadd(subMemUsage(runState, offset, length, common))
},
],
[
/* DELEGATECALL */
0xf4,
async function (runState, gas, common): Promise<void> {
const [currentGasLimit, toAddr, inOffset, inLength, outOffset, outLength] =
runState.stack.peek(6)
gas.iadd(subMemUsage(runState, inOffset, inLength, common))
gas.iadd(subMemUsage(runState, outOffset, outLength, common))
if (common.isActivatedEIP(2929)) {
const toAddress = new Address(addressToBuffer(toAddr))
gas.iadd(accessAddressEIP2929(runState, toAddress, common))
}
const gasLimit = maxCallGas(
currentGasLimit.clone(),
runState.eei.getGasLeft().isub(gas),
runState,
common
)
// note that TangerineWhistle or later this cannot happen
// (it could have ran out of gas prior to getting here though)
if (gasLimit.gt(runState.eei.getGasLeft().isub(gas))) {
trap(ERROR.OUT_OF_GAS)
}
runState.messageGasLimit = gasLimit
},
],
[
/* CREATE2 */
0xf5,
async function (runState, gas, common): Promise<void> {
if (runState.eei.isStatic()) {
trap(ERROR.STATIC_STATE_CHANGE)
}
const [_value, offset, length, _salt] = runState.stack.peek(4)
gas.iadd(subMemUsage(runState, offset, length, common))
if (common.isActivatedEIP(2929)) {
gas.iadd(accessAddressEIP2929(runState, runState.eei.getAddress(), common, false))
}
gas.iadd(new BN(common.param('gasPrices', 'sha3Word')).imul(divCeil(length, new BN(32))))
let gasLimit = new BN(runState.eei.getGasLeft().isub(gas))
gasLimit = maxCallGas(gasLimit.clone(), gasLimit.clone(), runState, common) // CREATE2 is only available after TangerineWhistle (Constantinople introduced this opcode)
runState.messageGasLimit = gasLimit
},
],
[
/* STATICCALL */
0xfa,
async function (runState, gas, common): Promise<void> {
const [currentGasLimit, toAddr, inOffset, inLength, outOffset, outLength] =
runState.stack.peek(6)
gas.iadd(subMemUsage(runState, inOffset, inLength, common))
gas.iadd(subMemUsage(runState, outOffset, outLength, common))
if (common.isActivatedEIP(2929)) {
const toAddress = new Address(addressToBuffer(toAddr))
gas.iadd(accessAddressEIP2929(runState, toAddress, common))
}
const gasLimit = maxCallGas(
currentGasLimit.clone(),
runState.eei.getGasLeft().isub(gas),
runState,
common
) // we set TangerineWhistle or later to true here, as STATICCALL was available from Byzantium (which is after TangerineWhistle)
runState.messageGasLimit = gasLimit
},
],
[
/* REVERT */
0xfd,
async function (runState, gas, common): Promise<void> {
const [offset, length] = runState.stack.peek(2)
gas.iadd(subMemUsage(runState, offset, length, common))
},
],
[
/* SELFDESTRUCT */
0xff,
async function (runState, gas, common): Promise<void> {
if (runState.eei.isStatic()) {
trap(ERROR.STATIC_STATE_CHANGE)
}
const selfdestructToAddressBN = runState.stack.peek()[0]
const selfdestructToAddress = new Address(addressToBuffer(selfdestructToAddressBN))
let deductGas = false
if (common.gteHardfork('spuriousDragon')) {
// EIP-161: State Trie Clearing
const balance = await runState.eei.getExternalBalance(runState.eei.getAddress())
if (balance.gtn(0)) {
// This technically checks if account is empty or non-existent
// TODO: improve on the API here (EEI and StateManager)
const empty = await runState.eei.isAccountEmpty(selfdestructToAddress)
if (empty) {
deductGas = true
}
}
} else if (common.gteHardfork('tangerineWhistle')) {
// EIP-150 (Tangerine Whistle) gas semantics
const exists = await runState.stateManager.accountExists(selfdestructToAddress)
if (!exists) {
deductGas = true
}
}
if (deductGas) {
gas.iadd(new BN(common.param('gasPrices', 'callNewAccount')))
}
if (common.isActivatedEIP(2929)) {
gas.iadd(accessAddressEIP2929(runState, selfdestructToAddress, common, true, true))
}
},
],
])
// Set the range [0xa0, 0xa4] to the LOG handler
const logDynamicFunc = dynamicGasHandlers.get(0xa0)!
for (let i = 0xa1; i <= 0xa4; i++) {
dynamicGasHandlers.set(i, logDynamicFunc)
}