Skip to content

Commit

Permalink
Promisify runTx internals (#493)
Browse files Browse the repository at this point in the history
* Promisify runTx internals

* Fix linting error
  • Loading branch information
s1na authored Apr 10, 2019
1 parent ac60927 commit 519a076
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 203 deletions.
7 changes: 1 addition & 6 deletions lib/evm/loop.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const promisify = require('util.promisify')
const BN = require('bn.js')
const Block = require('ethereumjs-block')
const utils = require('ethereumjs-util')
Expand Down Expand Up @@ -212,7 +211,7 @@ module.exports = class Loop {
* @property {BN} memoryWordCount current size of memory in words
* @property {StateManager} stateManager a [`StateManager`](stateManager.md) instance (Beta API)
*/
return this._emit('step', eventObj)
return this._vm._emit('step', eventObj)
}

// Returns all valid jump destinations.
Expand All @@ -234,8 +233,4 @@ module.exports = class Loop {

return jumps
}

async _emit (k, v) {
return promisify(this._vm.emit.bind(this._vm))(k, v)
}
}
5 changes: 5 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const Buffer = require('safe-buffer').Buffer
const promisify = require('util.promisify')
const ethUtil = require('ethereumjs-util')
const { StateManager } = require('./state')
const Common = require('ethereumjs-common').default
Expand Down Expand Up @@ -64,4 +65,8 @@ module.exports = class VM extends AsyncEventEmitter {
copy () {
return new VM({ stateManager: this.stateManager.copy(), blockchain: this.blockchain })
}

async _emit (topic, data) {
return promisify(this.emit.bind(this))(topic, data)
}
}
315 changes: 120 additions & 195 deletions lib/runTx.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const Buffer = require('safe-buffer').Buffer
const async = require('async')
const utils = require('ethereumjs-util')
const BN = utils.BN
const Bloom = require('./bloom')
Expand All @@ -9,6 +7,7 @@ const Interpreter = require('./evm/interpreter')
const Message = require('./evm/message')
const TxContext = require('./evm/txContext')
const { StorageReader } = require('./state')
const PStateManager = require('./state/promisified')

/**
* Process a transaction. Run the vm. Transfers eth. Checks balances.
Expand Down Expand Up @@ -37,218 +36,144 @@ module.exports = function (opts, cb) {
return cb(new Error('invalid input, opts must be provided'))
}

var self = this
var block = opts.block
var tx = opts.tx
var gasLimit
var results
var basefee
var storageReader = new StorageReader(self.stateManager)

// tx is required
if (!tx) {
if (!opts.tx) {
return cb(new Error('invalid input, tx is required'))
}

// create a reasonable default if no block is given
if (!block) {
block = new Block()
if (!opts.block) {
opts.block = new Block()
}

if (new BN(block.header.gasLimit).lt(new BN(tx.gasLimit))) {
cb(new Error('tx has a higher gas limit than the block'))
return
if (new BN(opts.block.header.gasLimit).lt(new BN(opts.tx.gasLimit))) {
return cb(new Error('tx has a higher gas limit than the block'))
}

// run everything
async.series([
checkpointState,
runTxHook,
updateFromAccount,
runCall,
runAfterTxHook
], function (err) {
if (err) {
self.stateManager.revert(function () {
cb(err, results)
})
} else {
self.stateManager.commit(function (err) {
cb(err, results)
this.stateManager.checkpoint(() => {
_runTx.bind(this)(opts)
.then((results) => {
this.stateManager.commit(function (err) {
cb(err, results)
})
}).catch((err) => {
this.stateManager.revert(function () {
cb(err, null)
})
})
}
})
}

function checkpointState (cb) {
self.stateManager.checkpoint(cb)
async function _runTx (opts) {
const block = opts.block
const tx = opts.tx
const state = new PStateManager(this.stateManager)

/**
* 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)

// Validate gas limit against base fee
const basefee = tx.getBaseFee()
const gasLimit = new BN(tx.gasLimit)
if (gasLimit.lt(basefee)) {
throw new Error('base fee exceeds gas limit')
}

// run the transaction hook
function runTxHook (cb) {
/**
* The `beforeTx` event
*
* @event Event: beforeTx
* @type {Object}
* @property {Transaction} tx emits the Transaction that is about to be processed
*/
self.emit('beforeTx', tx, cb)
gasLimit.isub(basefee)

// Check from account's balance and nonce
let fromAccount = await state.getAccount(tx.from)
if (!opts.skipBalance && new BN(fromAccount.balance).lt(tx.getUpfrontCost())) {
throw new Error(
`sender doesn't have enough funds to send tx. The upfront cost is: ${tx.getUpfrontCost().toString()}\
and the sender's account only has: ${new BN(fromAccount.balance).toString()}`
)
} else if (!opts.skipNonce && !(new BN(fromAccount.nonce).eq(new BN(tx.nonce)))) {
throw new Error(
`the tx doesn't have the correct nonce. account has nonce of: ${new BN(fromAccount.nonce).toString()}\
tx has nonce of: $new BN(tx.nonce).toString()}`
)
}

// run the transaction hook
function runAfterTxHook (cb) {
/**
* The `afterTx` event
*
* @event Event: afterTx
* @type {Object}
* @property {Object} result result of the transaction
*/
self.emit('afterTx', results, cb)
// Update from account's nonce and balance
fromAccount.nonce = new BN(fromAccount.nonce).addn(1)
fromAccount.balance = new BN(fromAccount.balance).sub(new BN(tx.gasLimit).mul(new BN(tx.gasPrice)))
await state.putAccount(tx.from, fromAccount)

/*
* Execute message
*/
const txContext = new TxContext(tx.gasPrice, tx.from)
const message = new Message({
caller: tx.from,
gasLimit: gasLimit,
to: tx.to.toString('hex') !== '' ? tx.to : undefined,
value: tx.value,
data: tx.data
})
const storageReader = new StorageReader(this.stateManager)
const interpreter = new Interpreter(this, txContext, block, storageReader)
const results = await interpreter.executeMessage(message)

/*
* Parse results
*/
// Generate the bloom for the tx
results.bloom = txLogsBloom(results.vm.logs)
// Caculate the total gas used
results.gasUsed = results.gasUsed.add(basefee)
// Process any gas refund
results.gasRefund = results.vm.gasRefund
if (results.gasRefund) {
if (results.gasRefund.lt(results.gasUsed.divn(2))) {
results.gasUsed.isub(results.gasRefund)
} else {
results.gasUsed.isub(results.gasUsed.divn(2))
}
}

function updateFromAccount (cb) {
self.stateManager.getAccount(tx.from, function (err, fromAccount) {
if (err) {
cb(err)
return
}

var message
if (!opts.skipBalance && new BN(fromAccount.balance).lt(tx.getUpfrontCost())) {
message = "sender doesn't have enough funds to send tx. The upfront cost is: " + tx.getUpfrontCost().toString() + ' and the sender\'s account only has: ' + new BN(fromAccount.balance).toString()
cb(new Error(message))
return
} else if (!opts.skipNonce && !(new BN(fromAccount.nonce).eq(new BN(tx.nonce)))) {
message = "the tx doesn't have the correct nonce. account has nonce of: " + new BN(fromAccount.nonce).toString() + ' tx has nonce of: ' + new BN(tx.nonce).toString()
cb(new Error(message))
return
}

// increment the nonce
fromAccount.nonce = new BN(fromAccount.nonce).addn(1)

basefee = tx.getBaseFee()
gasLimit = new BN(tx.gasLimit)
if (gasLimit.lt(basefee)) {
return cb(new Error('base fee exceeds gas limit'))
}
gasLimit.isub(basefee)

fromAccount.balance = new BN(fromAccount.balance).sub(new BN(tx.gasLimit).mul(new BN(tx.gasPrice)))
self.stateManager.putAccount(tx.from, fromAccount, cb)
})
results.amountSpent = results.gasUsed.mul(new BN(tx.gasPrice))

// Update sender's balance
fromAccount = await state.getAccount(tx.from)
const finalFromBalance = new BN(tx.gasLimit).sub(results.gasUsed)
.mul(new BN(tx.gasPrice))
.add(new BN(fromAccount.balance))
fromAccount.balance = finalFromBalance
await state.putAccount(utils.toBuffer(tx.from), fromAccount)

// Update miner's balance
let minerAccount = await state.getAccount(block.header.coinbase)
// add the amount spent on gas to the miner's account
minerAccount.balance = new BN(minerAccount.balance).add(results.amountSpent)
if (!(new BN(minerAccount.balance).isZero())) {
await state.putAccount(block.header.coinbase, minerAccount)
}

// sets up the environment and runs a `call`
function runCall (cb) {
const txContext = new TxContext(tx.gasPrice, tx.from)
const message = new Message({
caller: tx.from,
gasLimit: gasLimit,
to: tx.to.toString('hex') !== '' ? tx.to : undefined,
value: tx.value,
data: tx.data
})

const interpreter = new Interpreter(self, txContext, block, storageReader)
interpreter.executeMessage(message)
.then((results) => parseResults(null, results))
.catch((err) => parseResults(err, null))

function parseResults (err, _results) {
if (err) return cb(err)
results = _results

// generate the bloom for the tx
results.bloom = txLogsBloom(results.vm.logs)

// caculate the total gas used
results.gasUsed = results.gasUsed.add(basefee)

// process any gas refund
results.gasRefund = results.vm.gasRefund
if (results.gasRefund) {
if (results.gasRefund.lt(results.gasUsed.divn(2))) {
results.gasUsed.isub(results.gasRefund)
} else {
results.gasUsed.isub(results.gasUsed.divn(2))
}
}

results.amountSpent = results.gasUsed.mul(new BN(tx.gasPrice))

async.series([
loadFromAccount,
updateFromAccount,
loadMinerAccount,
updateMinerAccount,
cleanupAccounts
], cb)

var fromAccount
function loadFromAccount (next) {
self.stateManager.getAccount(tx.from, function (err, account) {
fromAccount = account
next(err)
})
}

function updateFromAccount (next) {
// refund the leftover gas amount
var finalFromBalance = new BN(tx.gasLimit).sub(results.gasUsed)
.mul(new BN(tx.gasPrice))
.add(new BN(fromAccount.balance))
fromAccount.balance = finalFromBalance

self.stateManager.putAccount(utils.toBuffer(tx.from), fromAccount, next)
}

var minerAccount
function loadMinerAccount (next) {
self.stateManager.getAccount(block.header.coinbase, function (err, account) {
minerAccount = account
next(err)
})
}

function updateMinerAccount (next) {
// add the amount spent on gas to the miner's account
minerAccount.balance = new BN(minerAccount.balance)
.add(results.amountSpent)

// save the miner's account
if (!(new BN(minerAccount.balance).isZero())) {
self.stateManager.putAccount(block.header.coinbase, minerAccount, next)
} else {
next()
}
}

function cleanupAccounts (next) {
if (!results.vm.selfdestruct) {
results.vm.selfdestruct = {}
}

var keys = Object.keys(results.vm.selfdestruct)

async.series([
deleteSelfDestructs,
cleanTouched
], next)

function deleteSelfDestructs (done) {
async.each(keys, function (s, cb) {
self.stateManager.putAccount(Buffer.from(s, 'hex'), new Account(), cb)
}, done)
}

function cleanTouched (done) {
self.stateManager.cleanupTouchedAccounts(done)
}
}
/*
* Cleanup accounts
*/
if (results.vm.selfdestruct) {
const keys = Object.keys(results.vm.selfdestruct)
for (let k of keys) {
await state.putAccount(Buffer.from(k, 'hex'), new Account())
}
}
await state.cleanupTouchedAccounts()

/**
* The `afterTx` event
*
* @event Event: afterTx
* @type {Object}
* @property {Object} result result of the transaction
*/
await this._emit('afterTx', results)

return results
}

/**
Expand Down
Loading

0 comments on commit 519a076

Please sign in to comment.