@polygon-hermez/vm
Version:
An Ethereum VM implementation
803 lines (709 loc) • 21.9 kB
text/typescript
import { debug as createDebugLogger } from 'debug'
import { Account, Address, BN, MAX_UINT64, toBuffer } from 'ethereumjs-util'
import { Block } from '@ethereumjs/block'
import Blockchain from '@ethereumjs/blockchain'
import Common, { ConsensusAlgorithm } from '@ethereumjs/common'
import { StateManager } from '../state/index'
import { VmError, ERROR } from '../exceptions'
import Message from './message'
import EVM, { EVMResult } from './evm'
import { Log } from './types'
const ethers = require('ethers')
const ADDRESS_SYSTEM = '0x000000000000000000000000000000005ca1ab1e'
const LAST_TX_STORAGE_POS = '0x0000000000000000000000000000000000000000000000000000000000000000'
const STATE_ROOT_STORAGE_POS = 1
const debugGas = createDebugLogger('vm:eei:gas')
function trap(err: ERROR) {
throw new VmError(err)
}
const MASK_160 = new BN(1).shln(160).subn(1)
function addressToBuffer(address: BN) {
if (Buffer.isBuffer(address)) return address
return address.and(MASK_160).toArrayLike(Buffer, 'be', 20)
}
/**
* Environment data which is made available to EVM bytecode.
*/
export interface Env {
blockchain: Blockchain
address: Address
caller: Address
callData: Buffer
callValue: BN
code: Buffer
isStatic: boolean
depth: number
gasPrice: BN
origin: Address
block: Block
contract: Account
// Different than address for DELEGATECALL and CALLCODE
codeAddress: Address
isCreate: boolean
isDeploy: boolean
}
/**
* Immediate (unprocessed) result of running an EVM bytecode.
*/
export interface RunResult {
logs: Log[]
returnValue?: Buffer
/**
* A map from the accounts that have self-destructed to the addresses to send their funds to
*/
selfdestruct: { [k: string]: Buffer }
}
/**
* Result for object of a CALL
*/
export interface BaseCallResult {
returnCode: BN
results?: EVMResult
}
/**
* 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.
*/
export default class EEI {
_env: Env
_result: RunResult
_state: StateManager
_evm: EVM
_lastReturned: Buffer
_common: Common
_gasLeft: BN
constructor(env: Env, state: StateManager, evm: EVM, common: Common, gasLeft: BN) {
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: BN, context?: string): void {
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 BN(0)
trap(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: BN, context?: string): void {
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: BN, context?: string): void {
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 BN(0)
trap(ERROR.REFUND_EXHAUSTED)
}
}
/**
* Increments the internal gasLeft counter. Used for adding callStipend.
* @param amount - Amount to add
*/
addStipend(amount: BN): void {
if (this._evm._vm.DEBUG) {
debugGas(`add stipend ${amount} (-> ${this._gasLeft})`)
}
this._gasLeft.iadd(amount)
}
/**
* Returns address of currently executing account.
*/
getAddress(): Address {
return this._env.address
}
/**
* Returns balance of the given account.
* @param address - Address of account
*/
async getExternalBalance(address: Address): Promise<BN> {
// 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(): BN {
return this._env.contract.balance
}
/**
* Returns caller address. This is the address of the account
* that is directly responsible for this execution.
*/
getCaller(): BN {
return new BN(this._env.caller.buf)
}
/**
* Returns the deposited value by the instruction/transaction
* responsible for this execution.
*/
getCallValue(): BN {
return new 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(): Buffer {
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(): BN {
return new BN(this._env.callData.length)
}
/**
* Returns the size of code running in current environment.
*/
getCodeSize(): BN {
return new BN(this._env.code.length)
}
/**
* Returns the code running in current environment.
*/
getCode(): Buffer {
return this._env.code
}
/**
* Returns true if the current call must be executed statically.
*/
isStatic(): boolean {
return this._env.isStatic
}
/**
* Get size of an account’s code.
* @param address - Address of account
*/
async getExternalCodeSize(address: BN): Promise<BN> {
const addr = new Address(addressToBuffer(address))
const code = await this._state.getContractCode(addr)
return new BN(code.length)
}
/**
* Returns code of an account.
* @param address - Address of account
*/
async getExternalCode(address: BN): Promise<Buffer> {
const addr = new 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(): BN {
return new 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(): Buffer {
return this._lastReturned
}
/**
* Returns price of gas in current environment.
*/
getTxGasPrice(): BN {
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(): BN {
return new BN(this._env.origin.buf)
}
/**
* Returns the block’s number.
*/
getBlockNumber(): BN {
return this._env.block.header.number
}
/**
* Returns the block's beneficiary address.
*/
getBlockCoinbase(): BN {
let coinbase: Address
if (this._common.consensusAlgorithm() === 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 = Address.zero()
}
} else {
coinbase = this._env.block.header.coinbase
}
return new BN(coinbase.toBuffer())
}
/**
* Returns the block's timestamp.
*/
getBlockTimestamp(): BN {
return this._env.block.header.timestamp
}
/**
* Returns the block's difficulty.
*/
getBlockDifficulty(): BN {
return this._env.block.header.difficulty
}
/**
* Returns the block's gas limit.
*/
getBlockGasLimit(): BN {
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(): BN {
return this._common.chainIdBN()
}
/**
* Returns the Base Fee of the block as proposed in [EIP-3198](https;//eips.etheruem.org/EIPS/eip-3198)
*/
getBlockBaseFee(): BN {
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: BN): Promise<BN> {
const block = await this._env.blockchain.getBlock(num)
return new BN(block.hash())
}
/**
* Returns state root of the requested tx/block
* @param num - Number of block
*/
async getBatchHash(num: BN): Promise<BN> {
const stateRootPos = ethers.utils.solidityKeccak256(
['uint256', 'uint256'],
[Number(num), STATE_ROOT_STORAGE_POS]
)
const hash = await this._state.getContractStorage(
new Address(toBuffer(ADDRESS_SYSTEM)),
toBuffer(stateRootPos)
)
if (!hash || hash.length === 0) {
return new BN(0)
}
return new BN(hash)
}
/**
* Returns the current tx/block number
* @param num - Number of block
*/
async getBlockNum(): Promise<BN> {
const blockNum = await this._state.getContractStorage(
new Address(toBuffer(ADDRESS_SYSTEM)),
toBuffer(LAST_TX_STORAGE_POS)
)
return new BN(blockNum)
}
/**
* Store 256-bit a value in memory to persistent storage.
*/
async storageStore(key: Buffer, value: Buffer): Promise<void> {
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: Buffer, original = false): Promise<Buffer> {
if (original) {
return this._state.getOriginalContractStorage(this._env.address, key)
} else {
return this._state.getContractStorage(this._env.address, key)
}
}
/**
* Returns the current gasCounter.
*/
getGasLeft(): BN {
return this._gasLeft.clone()
}
/**
* Set the returning output data for the execution.
* @param returnData - Output data to return
*/
finish(returnData: Buffer): void {
this._result.returnValue = returnData
trap(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: Buffer): void {
this._result.returnValue = returnData
trap(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: Address): Promise<void> {
// return this._selfDestruct(toAddress)
return this._sendall(toAddress)
}
async _sendall(toAddress: Address): Promise<void> {
// not move balances if receiver address is the same as beneficiary
if (toAddress.equals(this._env.address)) {
trap(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 BN(0)
await this._state.putAccount(this._env.address, account)
trap(ERROR.STOP)
}
async _customSelfDestruct(toAddress: Address): Promise<void> {
// 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 BN(this._common.param('gasPrices', 'selfdestructRefund')))
}
// clear contract bytecode
await this._state.putContractCode(this._env.address, 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 BN(0)
await this._state.putAccount(this._env.address, account)
trap(ERROR.STOP)
}
async _selfDestruct(toAddress: Address): Promise<void> {
// 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 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 BN(0)
await this._state.putAccount(this._env.address, account)
trap(ERROR.STOP)
}
/**
* Creates a new log in the current environment.
*/
log(data: Buffer, numberOfTopics: number, topics: Buffer[]): void {
if (numberOfTopics < 0 || numberOfTopics > 4) {
trap(ERROR.OUT_OF_RANGE)
}
if (topics.length !== numberOfTopics) {
trap(ERROR.INTERNAL_ERROR)
}
const log: 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: BN,
address: Address,
value: BN,
data: Buffer,
outLength: BN
): Promise<BaseCallResult> {
const msg = new Message({
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: BN,
address: Address,
value: BN,
data: Buffer,
outLength: BN
): Promise<BaseCallResult> {
const msg = new Message({
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: BN,
address: Address,
value: BN,
data: Buffer,
outLength: BN
): Promise<BaseCallResult> {
const msg = new Message({
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: BN,
address: Address,
value: BN,
data: Buffer,
outLength: BN
): Promise<BaseCallResult> {
const msg = new Message({
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: Message): Promise<BaseCallResult> {
const selfdestruct = { ...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 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 === 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: BN,
value: BN,
data: Buffer,
salt: Buffer | null = null
): Promise<BaseCallResult> {
const selfdestruct = { ...this._result.selfdestruct }
const msg = new Message({
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 BN(0),
}
}
// EIP-2681 check
if (this._env.contract.nonce.gte(MAX_UINT64)) {
return {
returnCode: new 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 === ERROR.REVERT
) {
this._lastReturned = results.execResult.returnValue
}
if (
!results.execResult.exceptionError ||
results.execResult.exceptionError.error === 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 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: BN, value: BN, data: Buffer, salt: Buffer): Promise<BaseCallResult> {
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: Address): Promise<boolean> {
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: Address): Promise<boolean> {
return this._state.accountExists(address)
}
private _getReturnCode(results: EVMResult) {
// 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 BN(0)
} else {
return new BN(1)
}
}
}