From 519a07646d4bd892c2427548fb79d6fa259d99b6 Mon Sep 17 00:00:00 2001 From: Sina Mahmoodi <1591639+s1na@users.noreply.github.com> Date: Wed, 10 Apr 2019 09:38:38 +0200 Subject: [PATCH] Promisify runTx internals (#493) * Promisify runTx internals * Fix linting error --- lib/evm/loop.js | 7 +- lib/index.js | 5 + lib/runTx.js | 315 +++++++++++++++++---------------------------- tests/api/runTx.js | 5 +- 4 files changed, 129 insertions(+), 203 deletions(-) diff --git a/lib/evm/loop.js b/lib/evm/loop.js index e83a47fda4..fd94a507f5 100644 --- a/lib/evm/loop.js +++ b/lib/evm/loop.js @@ -1,4 +1,3 @@ -const promisify = require('util.promisify') const BN = require('bn.js') const Block = require('ethereumjs-block') const utils = require('ethereumjs-util') @@ -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. @@ -234,8 +233,4 @@ module.exports = class Loop { return jumps } - - async _emit (k, v) { - return promisify(this._vm.emit.bind(this._vm))(k, v) - } } diff --git a/lib/index.js b/lib/index.js index 8290966e23..c6b0d4fa5d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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 @@ -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) + } } diff --git a/lib/runTx.js b/lib/runTx.js index 5e316a202c..3cc1cb4674 100644 --- a/lib/runTx.js +++ b/lib/runTx.js @@ -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') @@ -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. @@ -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 } /** diff --git a/tests/api/runTx.js b/tests/api/runTx.js index d7dca5e24d..48269f2895 100644 --- a/tests/api/runTx.js +++ b/tests/api/runTx.js @@ -11,7 +11,8 @@ function setup (vm = null) { if (vm === null) { vm = { stateManager: new StateManager({ }), - emit: (e, val, cb) => { cb() } + emit: (e, val, cb) => { cb() }, + _emit: (e, val) => new Promise((resolve, reject) => resolve()) } } @@ -40,7 +41,7 @@ tape('runTx', (t) => { }) t.test('should fail to run without signature', async (st) => { - const tx = getTransaction() + const tx = getTransaction(false, true) shouldFail(st, suite.runTx({ tx }), (e) => st.ok(e.message.toLowerCase().includes('signature'), 'should fail with appropriate error') )