vaporyjs-vm
Version:
an vapory VM implementation
201 lines (170 loc) • 5.54 kB
JavaScript
const async = require('async')
const utils = require('vaporyjs-util')
const BN = utils.BN
const Bloom = require('./bloom.js')
const Block = require('vaporyjs-block')
/**
* Process a transaction. Run the vm. Transfers vap. checks balaces
* @method processTx
* @param opts
* @param opts.tx {Transaciton} - a transaction
* @param opts.skipNonce - skips the nonce check
* @param opts.skipBalance - skips the balance check
* @param opts.block {Block} needed to process the transaction, if no block is given a default one is created
* @param cb {Function} - the callback
*/
module.exports = function (opts, cb) {
var self = this
var block = opts.block
var tx = opts.tx
var gasLimit
var results
var basefee
// create a reasonable default if no block is given
if (!block) {
block = new Block()
}
if (new BN(block.header.gasLimit).cmp(new BN(tx.gasLimit)) === -1) {
cb(new Error('tx has a higher gas limit than the block'))
return
}
if (opts.populateCache === undefined) {
opts.populateCache = true
}
// run everything
async.series([
populateCache,
runTxHook,
runCall,
saveTries,
runAfterTxHook,
function (cb) {
self.stateManager.cache.flush(function () {
if (opts.populateCache) {
self.stateManager.cache.clear()
}
cb()
})
}
], function (err) {
cb(err, results)
})
// run the transaction hook
function runTxHook (cb) {
self.emit('beforeTx', tx, cb)
}
// run the transaction hook
function runAfterTxHook (cb) {
self.emit('afterTx', results, cb)
}
/**
* populates the cache with the two and from of the tx
*/
function populateCache (cb) {
var accounts = new Set()
accounts.add(tx.from.toString('hex'))
accounts.add(tx.to.toString('hex'))
accounts.add(block.header.coinbase.toString('hex'))
if (opts.populateCache === false) {
return cb()
}
self.stateManager.warmCache(accounts, cb)
}
// sets up the envorment and runs a `call`
function runCall (cb) {
// check to the sender's account to make sure it has enought wei and the
// correct nonce
var fromAccount = self.stateManager.cache.get(tx.from)
var message
if (!opts.skipBalance && new BN(fromAccount.balance).cmp(tx.getUpfrontCost()) === -1) {
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).cmp(new BN(tx.nonce)) !== 0) {
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).sub(basefee)
fromAccount.balance = new BN(fromAccount.balance).sub(new BN(tx.gasLimit).mul(new BN(tx.gasPrice)))
self.stateManager.cache.put(tx.from, fromAccount)
var options = {
caller: tx.from,
gasLimit: gasLimit,
gasPrice: tx.gasPrice,
to: tx.to,
value: tx.value,
data: tx.data,
block: block,
populateCache: false
}
if (tx.to.toString('hex') === '') {
delete options.to
}
// run call
self.runCall(options, parseResults)
function parseResults (err, _results) {
if (err) return cb(err)
results = _results
// generate the bloom for the tx
results.bloom = txLogsBloom(results.vm.logs)
fromAccount = self.stateManager.cache.get(tx.from)
// caculate the totall gas used
results.gasUsed = results.gasUsed.add(basefee)
// refund the accoun.stateManagert
var gasRefund = results.vm.gasRefund
if (gasRefund) {
if (gasRefund.cmp(results.gasUsed.divn(2)) === -1) {
results.gasUsed.isub(gasRefund)
} else {
results.gasUsed.isub(results.gasUsed.divn(2))
}
}
results.amountSpent = results.gasUsed.mul(new BN(tx.gasPrice))
// refund the left over gas amount
fromAccount.balance = new BN(tx.gasLimit).sub(results.gasUsed)
.mul(new BN(tx.gasPrice))
.add(new BN(fromAccount.balance))
self.stateManager.cache.put(tx.from, fromAccount)
var minerAccount = self.stateManager.cache.get(block.header.coinbase)
// 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
self.stateManager.cache.put(block.header.coinbase, minerAccount)
if (!results.vm.suicides) {
results.vm.suicides = {}
}
var keys = Object.keys(results.vm.suicides)
keys.forEach(function (s) {
self.stateManager.cache.del(Buffer.from(s, 'hex'))
})
cb()
}
}
function saveTries (cb) {
self.stateManager.commitContracts(cb)
}
}
/**
* @method txLogsBloom
*/
function txLogsBloom (logs) {
var bloom = new Bloom()
if (logs) {
for (var i = 0; i < logs.length; i++) {
var log = logs[i]
// add the address
bloom.add(log[0])
// add the topics
var topics = log[1]
for (var q = 0; q < topics.length; q++) {
bloom.add(topics[q])
}
}
}
return bloom
}