From af98663a330293abe24f913e0d89076fac82a973 Mon Sep 17 00:00:00 2001 From: Ben Burns Date: Mon, 29 Oct 2018 15:29:52 +0000 Subject: [PATCH] refactor vm & state trie so they are short-lived objs --- lib/blockchain_double.js | 443 ++++++++++++---------- lib/statemanager.js | 17 +- lib/utils/forkedblockchain.js | 125 +++--- npm-shrinkwrap.json | 20 + test/forking.js | 692 ++++++++++++---------------------- 5 files changed, 561 insertions(+), 736 deletions(-) diff --git a/lib/blockchain_double.js b/lib/blockchain_double.js index c360670a5b..1ab363efae 100644 --- a/lib/blockchain_double.js +++ b/lib/blockchain_double.js @@ -3,13 +3,13 @@ var Account = require("ethereumjs-account"); var Block = require("ethereumjs-block"); var Log = require("./utils/log"); var Receipt = require("./utils/receipt"); +var Database = require("./database"); +var Trie = require("merkle-patricia-tree"); var VM = require("ethereumjs-vm"); var RuntimeError = require("./utils/runtimeerror"); -var Trie = require("merkle-patricia-tree"); var utils = require("ethereumjs-util"); var async = require("async"); var Heap = require("heap"); -var Database = require("./database"); var EventEmitter = require("events"); var _ = require("lodash"); @@ -17,18 +17,17 @@ function BlockchainDouble(options) { var self = this; EventEmitter.apply(self); + this.database = new Database(options); + this.options = options = this._applyDefaultOptions(options || {}); this.logger = options.logger || console; - this.data = new Database(options); - - if (options.trie != null && options.db_path != null) { - throw new Error("Can't initialize a TestRPC with a db and a custom trie."); - } - this.pending_transactions = []; + // tracks latest state root as latest block changes + this._latestStateRoot = undefined; + // updated periodically to keep up with the times this.blockGasLimit = options.gasLimit; this.defaultTransactionGasLimit = options.defaultTransactionGasLimit; @@ -53,10 +52,68 @@ BlockchainDouble.prototype._applyDefaultOptions = function(options) { return _.merge(options, defaultOptions, Object.assign({}, options)); }; +BlockchainDouble.prototype.getVM = function getVM(root) { + const self = this; + + let vm = new VM({ + state: this.getStateTrie(root), + blockchain: { + // EthereumJS VM needs a blockchain object in order to get block information. + // When calling getBlock() it will pass a number that's of a Buffer type. + // Unfortunately, it uses a 64-character buffer (when converted to hex) to + // represent block numbers as well as block hashes. Since it's very unlikely + // any block number will get higher than the maximum safe Javascript integer, + // we can convert this buffer to a number ahead of time before calling our + // own getBlock(). If the conversion succeeds, we have a block number. + // If it doesn't, we have a block hash. (Note: Our implementation accepts both.) + getBlock: function(number, done) { + try { + number = to.number(number); + } catch (e) { + // Do nothing; must be a block hash. + } + + self.getBlock(number, done); + } + }, + enableHomestead: true, + activatePrecompiles: true, + allowUnlimitedContractSize: this.options.allowUnlimitedContractSize + }); + + if (this.options.debug === true) { + // log executed opcodes, including args as hex + vm.on("step", function(info) { + var name = info.opcode.name; + var argsNum = info.opcode.in; + if (argsNum) { + var args = info.stack + .slice(-argsNum) + .map((arg) => to.hex(arg)) + .join(" "); + + self.logger.log(`${name} ${args}`); + } else { + self.logger.log(name); + } + }); + } + + return vm; +}; + +BlockchainDouble.prototype.getLatestStateRoot = function getLatestStateRoot(callback) { + return this._latestStateRoot; +}; + +BlockchainDouble.prototype.getStateTrie = function getStateTrie(root) { + return new Trie(this.database.trie_db, root); +}; + BlockchainDouble.prototype.initialize = function(accounts, callback) { var self = this; - this.data.initialize(function(err) { + self.database.initialize(function(err) { if (err) { return callback(err); } @@ -68,62 +125,6 @@ BlockchainDouble.prototype.initialize = function(accounts, callback) { var options = self.options; - var root = null; - - if (block) { - root = block.header.stateRoot; - } - - // I haven't yet found a good way to do this. Getting the trie from the - // forked blockchain without going through the other setup is a little gross. - self.stateTrie = self.createStateTrie(self.data.trie_db, root); - - self.vm = - options.vm || - new VM({ - state: self.stateTrie, - blockchain: { - // EthereumJS VM needs a blockchain object in order to get block information. - // When calling getBlock() it will pass a number that's of a Buffer type. - // Unfortunately, it uses a 64-character buffer (when converted to hex) to - // represent block numbers as well as block hashes. Since it's very unlikely - // any block number will get higher than the maximum safe Javascript integer, - // we can convert this buffer to a number ahead of time before calling our - // own getBlock(). If the conversion succeeds, we have a block number. - // If it doesn't, we have a block hash. (Note: Our implementation accepts both.) - getBlock: function(number, done) { - try { - number = to.number(number); - } catch (e) { - // Do nothing; must be a block hash. - } - - self.getBlock(number, done); - } - }, - enableHomestead: true, - activatePrecompiles: true, - allowUnlimitedContractSize: options.allowUnlimitedContractSize - }); - - if (options.debug === true) { - // log executed opcodes, including args as hex - self.vm.on("step", function(info) { - var name = info.opcode.name; - var argsNum = info.opcode.in; - if (argsNum) { - var args = info.stack - .slice(-argsNum) - .map((arg) => to.hex(arg)) - .join(" "); - - self.logger.log(`${name} ${args}`); - } else { - self.logger.log(name); - } - }); - } - if (options.time) { self.setTime(options.time); } @@ -135,6 +136,8 @@ BlockchainDouble.prototype.initialize = function(accounts, callback) { return callback(); } + const vm = self.getVM(); + self.createGenesisBlock(function(err, block) { if (err) { return callback(err); @@ -145,13 +148,15 @@ BlockchainDouble.prototype.initialize = function(accounts, callback) { async.eachSeries( accounts, function(accountData, finished) { - self.putAccount(accountData.account, accountData.address, finished); + self.putAccount(accountData.account, accountData.address, vm, finished); }, function(err) { if (err) { return callback(err); } + block.header.stateRoot = vm.stateManager.trie.root; + // Create first block self.putBlock(block, [], [], callback); } @@ -161,17 +166,13 @@ BlockchainDouble.prototype.initialize = function(accounts, callback) { }); }; -BlockchainDouble.prototype.createStateTrie = function(db, root) { - return new Trie(this.data.trie_db, root); -}; - // Overrideable so other implementations (forking) can edit it. BlockchainDouble.prototype.createGenesisBlock = function(callback) { this.createBlock(callback); }; BlockchainDouble.prototype.latestBlock = function(callback) { - this.data.blocks.last(function(err, last) { + this.database.blocks.last(function(err, last) { if (err) { return callback(err); } @@ -212,21 +213,21 @@ BlockchainDouble.prototype.getBlock = function(number, callback) { // block hash if (hash.length > 40) { - this.data.blockHashes.get(to.hex(hash), function(err, blockIndex) { + this.database.blockHashes.get(to.hex(hash), function(err, blockIndex) { if (err) { return callback(err); } - return self.data.blocks.get(blockIndex, callback); + return self.database.blocks.get(blockIndex, callback); }); } else { // Block number - return this.data.blocks.get(to.number(hash), callback); + return this.database.blocks.get(to.number(hash), callback); } } else { if (number === "latest" || number === "pending") { return this.latestBlock(callback); } else if (number === "earliest") { - return this.data.blocks.first(callback); + return this.database.blocks.first(callback); } } }; @@ -234,32 +235,42 @@ BlockchainDouble.prototype.getBlock = function(number, callback) { BlockchainDouble.prototype.putBlock = function(block, logs, receipts, callback) { var self = this; - // Lock in the state root for this block. - block.header.stateRoot = this.stateTrie.root; - - this.data.blocks.length(function(err, length) { + this.database.blocks.length(function(err, length) { if (err) { return callback(err); } var requests = [ - self.data.blocks.push.bind(self.data.blocks, block), - self.data.blockLogs.push.bind(self.data.blockLogs, logs), - self.data.blockHashes.set.bind(self.data.blockHashes, to.hex(block.hash()), length) + self.database.blocks.push.bind(self.database.blocks, block), + self.database.blockLogs.push.bind(self.database.blockLogs, logs), + self.database.blockHashes.set.bind( + self.database.blockHashes, + to.hex(block.hash()), + length + ) ]; block.transactions.forEach(function(tx, index) { var txHash = to.hex(tx.hash()); requests.push( - self.data.transactions.set.bind(self.data.transactions, txHash, tx), - self.data.transactionReceipts.set.bind(self.data.transactionReceipts, txHash, receipts[index]) + self.database.transactions.set.bind( + self.database.transactions, + txHash, + tx + ), + self.database.transactionReceipts.set.bind( + self.database.transactionReceipts, + txHash, + receipts[index] + ) ); }); async.parallel(requests, (err, result) => { if (!err) { self.emit("block", block); + self._latestStateRoot = block.header.stateRoot; } callback(err, result); }); @@ -269,45 +280,61 @@ BlockchainDouble.prototype.putBlock = function(block, logs, receipts, callback) BlockchainDouble.prototype.popBlock = function(callback) { var self = this; - this.data.blocks.last(function(err, block) { + this.database.blocks.last(function(err, block) { if (err) { return callback(err); } + if (block == null) { return callback(null, null); } - var requests = []; - var blockHash = to.hex(block.hash()); + // update latest stateRoot before actually deleting to avoid race conditions + self.getBlock(block.header.parentHash, function(err, parent) { + if (err) { + return callback(err); + } - block.transactions.forEach(function(tx) { - var txHash = to.hex(tx.hash()); + if (parent == null) { + return callback(new Error("Tried to pop genesis block, not allowed.")); + } + + var requests = []; + var blockHash = to.hex(block.hash()); + + block.transactions.forEach(function(tx) { + var txHash = to.hex(tx.hash()); + + requests.push( + self.database.transactions.del.bind( + self.database.transactions, + txHash + ), + self.database.transactionReceipts.del.bind( + self.database.transactionReceipts, + txHash + ) + ); + }); requests.push( - self.data.transactions.del.bind(self.data.transactions, txHash), - self.data.transactionReceipts.del.bind(self.data.transactionReceipts, txHash) + self.database.blockLogs.pop.bind( + self.database.blockLogs + ), + self.database.blockHashes.del.bind( + self.database.blockHashes, + blockHash + ), + self.database.blocks.pop.bind( + self.database.blocks + ) // Do this one last in case anything relies on it. ); - }); - requests.push( - self.data.blockLogs.pop.bind(self.data.blockLogs), - self.data.blockHashes.del.bind(self.data.blockHashes, blockHash), - self.data.blocks.pop.bind(self.data.blocks) // Do this one last in case anything relies on it. - ); - - async.series(requests, function(err) { - if (err) { - return callback(err); - } - - // Set the root to the last available, which will "roll back" to the previous - // moment in time. Note that all the old data is still in the db, but it's now just junk data. - self.data.blocks.last(function(err, newLastBlock) { + async.series(requests, function(err) { if (err) { return callback(err); } - self.stateTrie.root = newLastBlock.header.stateRoot; - // Remember: Return block we popped off. + callback(null, block); }); }); @@ -318,18 +345,32 @@ BlockchainDouble.prototype.clearPendingTransactions = function() { this.pending_transactions = []; }; -BlockchainDouble.prototype.putAccount = function(account, address, callback) { +BlockchainDouble.prototype.putAccount = function(account, address, vm, callback) { var self = this; address = utils.toBuffer(address); + if (typeof vm === "function") { + callback = vm; + vm = null; + } - this.vm.stateManager.putAccount(address, account, function(err) { - if (err) { - return callback(err); - } + function _putAccount(vm) { + vm.stateManager.putAccount(address, account, function(err) { + if (err) { + return callback(err); + } - self.vm.stateManager.cache.flush(callback); - }); + vm.stateManager.cache.flush(callback); + }); + } + + if (vm) { + _putAccount(vm); + } else { + let stateRoot = this.getLatestStateRoot(); + vm = self.getVM(stateRoot); + _putAccount(vm); + } }; /** @@ -379,11 +420,18 @@ BlockchainDouble.prototype.createBlock = function(parent, callback) { block.header.parentHash = to.hex(parent.hash()); } + // temporarily set the new block's state root to the parent's + // will be updated by processBlock + if (self._latestStateRoot) { + block.header.stateRoot = self._latestStateRoot; + } + callback(null, block); }); }; BlockchainDouble.prototype.getQueuedNonce = function(address, callback) { + const self = this; var nonce = null; this.pending_transactions.forEach(function(tx) { @@ -407,7 +455,9 @@ BlockchainDouble.prototype.getQueuedNonce = function(address, callback) { return callback(null, Buffer.from([nonce + 1])); } - this.stateTrie.get(address, function(err, val) { + let stateRoot = this.getLatestStateRoot(); + const stateTrie = self.getStateTrie(stateRoot); + stateTrie.get(address, function(err, val) { if (err) { return callback(err); } @@ -472,28 +522,26 @@ BlockchainDouble.prototype.sortByPriceAndNonce = function() { BlockchainDouble.prototype.processCall = function(tx, blockNumber, callback) { var self = this; - var startingStateRoot; + let vm; - var cleanUpAndReturn = function(err, result, changeRoot) { - self.vm.stateManager.revert(function(e) { - // For defaultBlock, undo state root changes - if (changeRoot) { - self.stateTrie.root = startingStateRoot; + var cleanUpAndReturn = function(err, result) { + vm.stateManager.revert(function(revertErr) { + if (err) { + return callback(err); } - callback(err || e, result); + if (revertErr) { + return callback(revertErr); + } + callback(null, result); }); }; - var runCall = function(tx, changeRoot, err, block) { + var runCall = function(tx, err, block) { if (err) { return callback(err); } - // For defaultBlock, use that block's root - if (changeRoot) { - startingStateRoot = self.stateTrie.root; - self.stateTrie.root = block.header.stateRoot; - } + vm = self.getVM(block.header.stateRoot); // create a fake block with this fake transaction self.createBlock(block, function(err, block) { @@ -504,7 +552,7 @@ BlockchainDouble.prototype.processCall = function(tx, blockNumber, callback) { // We checkpoint here for speed. We want all state trie reads/writes to happen in memory, // and the final output be flushed to the database at the end of transaction processing. - self.vm.stateManager.checkpoint(); + vm.stateManager.checkpoint(); var runArgs = { tx: tx, @@ -513,7 +561,7 @@ BlockchainDouble.prototype.processCall = function(tx, blockNumber, callback) { skipNonce: true }; - self.vm.runTx(runArgs, function(vmerr, result) { + vm.runTx(runArgs, function(vmerr, result) { // This is a check that has been in there for awhile. I'm unsure if it's required, but it can't hurt. if (vmerr && vmerr instanceof Error === false) { vmerr = new Error("VM error: " + vmerr); @@ -527,15 +575,15 @@ BlockchainDouble.prototype.processCall = function(tx, blockNumber, callback) { // If no error, check for a runtime error. This can return null if no runtime error. vmerr = RuntimeError.fromResults([tx], { results: [result] }); - cleanUpAndReturn(vmerr, result, changeRoot); + cleanUpAndReturn(vmerr, result); }); }); }; // Delegate block selection blockNumber === "latest" - ? self.latestBlock(runCall.bind(null, tx, false)) - : self.getBlock(blockNumber, runCall.bind(null, tx, true)); + ? self.latestBlock(runCall.bind(null, tx)) + : self.getBlock(blockNumber, runCall.bind(null, tx)); }; /** @@ -546,28 +594,27 @@ BlockchainDouble.prototype.processCall = function(tx, blockNumber, callback) { * @param {Block} block block to process * @param {Boolean} commit Whether or not changes should be committed to the state * trie and the block appended to the end of the chain. + * @param {VM} vm Optional VM instance to use to process the block. If null, processBlock + * gets its own VM instance from the stateManager. * @param {Function} callback Callback function when transaction processing is completed. * @return [type] [description] */ -BlockchainDouble.prototype.processBlock = function(block, commit, callback) { +BlockchainDouble.prototype.processBlock = function(block, commit, vm, callback) { var self = this; - if (typeof commit === "function") { - callback = commit; - commit = true; - } + vm = vm || self.getVM(block.header.stateRoot); // We checkpoint here for speed. We want all state trie reads/writes to happen in memory, // and the final output be flushed to the database at the end of transaction processing. - self.vm.stateManager.checkpoint(); + vm.stateManager.checkpoint(); var cleanup = function(err) { - self.vm.stateManager.revert(function(e) { + vm.stateManager.revert(function(e) { callback(err || e); }); }; - self.vm.runBlock( + vm.runBlock( { block: block, generate: true @@ -651,16 +698,18 @@ BlockchainDouble.prototype.processBlock = function(block, commit, callback) { function commmitIfNeeded(cb) { if (commit === true) { - self.vm.stateManager.commit(function(e) { + vm.stateManager.commit(function(e) { if (e) { return cleanup(e); } + block.header.stateRoot = vm.stateManager.trie.root; + // Put that block on the end the chain self.putBlock(block, logs, receipts, cb); }); } else { - self.vm.stateManager.revert(cb); + vm.stateManager.revert(cb); } } @@ -731,7 +780,7 @@ BlockchainDouble.prototype.processNextBlock = function(timestamp, callback) { // Overwrite block timestamp if (timestamp) { - self.data.blocks.last(function(err, last) { + self.database.blocks.last(function(err, last) { if (err) { return callback(err); } @@ -748,7 +797,7 @@ BlockchainDouble.prototype.processNextBlock = function(timestamp, callback) { Array.prototype.push.apply(block.transactions, currentTransactions); // Process the block, committing the block to the chain - self.processBlock(block, true, callback); + self.processBlock(block, true, null, callback); }); }; @@ -761,10 +810,8 @@ BlockchainDouble.prototype.processNextBlock = function(timestamp, callback) { * Strategy: * * 1. Find block where transaction occurred - * 2. Set state root of that block - * 3. Rerun every transaction in that block prior to and including the requested transaction - * 4. Reset state root back to original - * 5. Send trace results back. + * 2. Rerun every transaction in that block prior to and including the requested transaction + * 3. Send trace results back. * * @param {[type]} tx [description] * @param {Function} callback [description] @@ -775,6 +822,7 @@ BlockchainDouble.prototype.processTransactionTrace = function(hash, params, call var targetHash = to.hex(hash); var txHashCurrentlyProcessing = ""; var txCurrentlyProcessing = null; + var vm; var storageStack = { currentDepth: -1, @@ -837,7 +885,7 @@ BlockchainDouble.prototype.processTransactionTrace = function(hash, params, call returnVal.structLogs.push(structLog); next(); } else { - structLog = self.processStorageTrace(structLog, storageStack, event, function(err, structLog) { + structLog = self.processStorageTrace(structLog, storageStack, event, vm, function(err, structLog) { if (err) { return next(err); } @@ -852,24 +900,19 @@ BlockchainDouble.prototype.processTransactionTrace = function(hash, params, call txHashCurrentlyProcessing = to.hex(tx.hash()); if (txHashCurrentlyProcessing === targetHash) { - self.vm.on("step", stepListener); + vm.on("step", stepListener); } } // afterTxListener cleans up everything. function afterTxListener() { if (txHashCurrentlyProcessing === targetHash) { - self.vm.removeListener("step", stepListener); - self.vm.removeListener("beforeTx", beforeTxListener); - self.vm.removeListener("afterTx", afterTxListener); + vm.removeListener("step", stepListener); + vm.removeListener("beforeTx", beforeTxListener); + vm.removeListener("afterTx", afterTxListener); } } - // Listen to beforeTx and afterTx so we know when our target transaction - // is processing. These events will add the vent listener for getting the trace data. - self.vm.on("beforeTx", beforeTxListener); - self.vm.on("afterTx", afterTxListener); - // #1 - get block via transaction receipt this.getTransactionReceipt(targetHash, function(err, receipt) { if (err) { @@ -883,15 +926,17 @@ BlockchainDouble.prototype.processTransactionTrace = function(hash, params, call var targetBlock = receipt.block; // Get the parent of the target block - self.getBlock(targetBlock.header.parentHash, function(err, parent) { + self.getBlock(to.rpcDataHexString(targetBlock.header.parentHash), function(err, parent) { if (err) { return callback(err); } - var startingStateRoot = self.stateTrie.root; - - // #2 - Set state root of original block - self.stateTrie.root = parent.header.stateRoot; + // #2 - get VM at original block's stateRoot + vm = self.getVM(parent.header.stateRoot); + // Listen to beforeTx and afterTx so we know when our target transaction + // is processing. These events will add the vent listener for getting the trace data. + vm.on("beforeTx", beforeTxListener); + vm.on("afterTx", afterTxListener); // Prepare the "next" block with necessary transactions self.createBlock(parent, function(err, block) { @@ -910,19 +955,16 @@ BlockchainDouble.prototype.processTransactionTrace = function(hash, params, call } // #3 - Process the block without committing the data. - self.processBlock(block, false, function(err) { + self.processBlock(block, false, vm, function(err) { // Ignore runtime errors, or else erroneous transactions can't be traced. if (err && err.message.indexOf("VM Exception") === 0) { err = null; } - // #4 - reset the state root. - self.stateTrie.root = startingStateRoot; - // Just to be safe - self.vm.removeListener("beforeTx", beforeTxListener); - self.vm.removeListener("afterTx", afterTxListener); - self.vm.removeListener("step", stepListener); + vm.removeListener("beforeTx", beforeTxListener); + vm.removeListener("afterTx", afterTxListener); + vm.removeListener("step", stepListener); // #5 - send state results back callback(err, returnVal); @@ -932,8 +974,7 @@ BlockchainDouble.prototype.processTransactionTrace = function(hash, params, call }); }; -BlockchainDouble.prototype.processStorageTrace = function(structLog, storageStack, event, callback) { - var self = this; +BlockchainDouble.prototype.processStorageTrace = function(structLog, storageStack, event, vm, callback) { var name = event.opcode.name; var argsNum = event.opcode.in; @@ -966,7 +1007,7 @@ BlockchainDouble.prototype.processStorageTrace = function(structLog, storageStac // this one's more fun, we need to get the value the contract is loading from current storage key = to.rpcDataHexString(args[0], 64).replace("0x", ""); - self.vm.stateManager.getContractStorage(event.address, "0x" + key, function(err, result) { + vm.stateManager.getContractStorage(event.address, "0x" + key, function(err, result) { if (err) { return callback(err); } @@ -985,24 +1026,26 @@ BlockchainDouble.prototype.processStorageTrace = function(structLog, storageStac } }; -BlockchainDouble.prototype.getAccount = function(address, number, callback) { +BlockchainDouble.prototype.getAccount = function(trie, address, number, callback) { var self = this; + if (typeof number === "function") { + callback = number; + number = address; + address = trie; + trie = null; + } + this.getBlock(number, function(err, block) { if (err) { return callback(err); } - var trie = self.stateTrie; - - // Manipulate the state root in place to maintain checkpoints - var currentStateRoot = trie.root; - self.stateTrie.root = block.header.stateRoot; + if (!trie) { + trie = self.getStateTrie(block.header.stateRoot); + } trie.get(utils.toBuffer(address), function(err, data) { - // Finally, put the stateRoot back for good - trie.root = currentStateRoot; - if (err) { return callback(err); } @@ -1044,16 +1087,10 @@ BlockchainDouble.prototype.getStorage = function(address, position, number, call return callback(err); } - var trie = self.stateTrie; - - // Manipulate the state root in place to maintain checkpoints - var currentStateRoot = trie.root; - self.stateTrie.root = block.header.stateRoot; + var trie = self.getStateTrie(block.header.stateRoot); trie.get(utils.toBuffer(address), function(err, data) { if (err != null) { - // Put the stateRoot back if there's an error - trie.root = currentStateRoot; return callback(err); } @@ -1062,9 +1099,6 @@ BlockchainDouble.prototype.getStorage = function(address, position, number, call trie.root = account.stateRoot; trie.get(utils.setLengthLeft(utils.toBuffer(position), 32), function(err, value) { - // Finally, put the stateRoot back for good - trie.root = currentStateRoot; - if (err != null) { return callback(err); } @@ -1083,25 +1117,16 @@ BlockchainDouble.prototype.getCode = function(address, number, callback) { return callback(err); } - var trie = self.stateTrie; - - // Manipulate the state root in place to maintain checkpoints - var currentStateRoot = trie.root; - self.stateTrie.root = block.header.stateRoot; + var trie = self.getStateTrie(block.header.stateRoot); trie.get(utils.toBuffer(address), function(err, data) { if (err != null) { - // Put the stateRoot back if there's an error - trie.root = currentStateRoot; return callback(err); } var account = new Account(data); account.getCode(trie, function(err, code) { - // Finally, put the stateRoot back for good - trie.root = currentStateRoot; - if (err) { return callback(err); } @@ -1115,7 +1140,7 @@ BlockchainDouble.prototype.getCode = function(address, number, callback) { BlockchainDouble.prototype.getTransaction = function(hash, callback) { hash = to.hex(hash); - this.data.transactions.get(hash, function(err, tx) { + this.database.transactions.get(hash, function(err, tx) { if (err) { if (err.notFound) { return callback(null, null); @@ -1130,7 +1155,7 @@ BlockchainDouble.prototype.getTransaction = function(hash, callback) { BlockchainDouble.prototype.getTransactionReceipt = function(hash, callback) { hash = to.hex(hash); - this.data.transactionReceipts.get(hash, function(err, receipt) { + this.database.transactionReceipts.get(hash, function(err, receipt) { if (err) { if (err.notFound) { return callback(null, null); @@ -1149,12 +1174,12 @@ BlockchainDouble.prototype.getBlockLogs = function(number, callback) { if (err) { return callback(err); } - self.data.blockLogs.get(effective, callback); + self.database.blockLogs.get(effective, callback); }); }; BlockchainDouble.prototype.getHeight = function(callback) { - this.data.blocks.length(function(err, length) { + this.database.blocks.length(function(err, length) { if (err) { return callback(err); } @@ -1181,7 +1206,7 @@ BlockchainDouble.prototype.setTime = function(date) { }; BlockchainDouble.prototype.close = function(callback) { - this.data.close(callback); + this.database.close(callback); }; module.exports = BlockchainDouble; diff --git a/lib/statemanager.js b/lib/statemanager.js index 69b75056e0..de25400d44 100644 --- a/lib/statemanager.js +++ b/lib/statemanager.js @@ -26,9 +26,6 @@ function StateManager(options, provider) { this.blockchain = new BlockchainDouble(options); } - this.vm = this.blockchain.vm; - this.stateTrie = this.blockchain.stateTrie; - this.accounts = {}; this.secure = !!options.secure; this.account_passwords = {}; @@ -59,7 +56,7 @@ function StateManager(options, provider) { this.mining_interval_timeout = null; this._provider = provider; -} +}; const defaultOptions = { total_accounts: 10, @@ -156,7 +153,7 @@ StateManager.prototype.initialize = function(callback) { // If the user didn't pass a specific version id in, then use the // forked blockchain's version (if it exists) or create our own. if (!self.net_version) { - self.net_version = self.blockchain.fork_version; + self.net_version = self.blockchain.forkVersion; } if (self.is_mining_on_interval) { @@ -883,16 +880,6 @@ StateManager.prototype.revert = function(snapshotId, callback) { ); }; -StateManager.prototype.hasContractCode = function(address, callback) { - this.vm.stateManager.getContractCode(address, function(err, result) { - if (err != null) { - callback(err, false); - } else { - callback(null, true); - } - }); -}; - StateManager.prototype.startMining = function(callback) { this.is_mining = true; diff --git a/lib/utils/forkedblockchain.js b/lib/utils/forkedblockchain.js index 86e8d0d3f0..abc66d3bf1 100644 --- a/lib/utils/forkedblockchain.js +++ b/lib/utils/forkedblockchain.js @@ -59,19 +59,25 @@ ForkedBlockchain.prototype.initialize = function(accounts, callback) { return callback(err); } - // Unfortunately forking requires a bit of monkey patching, but it gets the job done. - self.vm.stateManager._lookupStorageTrie = self.lookupStorageTrie.bind(self); - self.vm.stateManager.cache._lookupAccount = self.getAccount.bind(self); - self.vm.stateManager.getContractCode = self.getCode.bind(self); - self.vm.stateManager.putContractCode = self.putCode.bind(self); - callback(); }); }); }; -ForkedBlockchain.prototype.createStateTrie = function(db, root) { - return new ForkedStorageTrie(db, root, { +ForkedBlockchain.prototype.getVM = function getVM(root) { + let vm = BlockchainDouble.prototype.getVM.call(this, root); + + // Unfortunately forking requires a bit of monkey patching, but it gets the job done. + vm.stateManager._lookupStorageTrie = this.lookupStorageTrie.bind(this, vm.stateManager.trie); + vm.stateManager.cache._lookupAccount = this._getAccount.bind(this, vm.stateManager.trie); + vm.stateManager.getContractCode = this._getCode.bind(this, vm.stateManager.trie); + vm.stateManager.putContractCode = this._putCode.bind(this, vm); + + return vm; +}; + +ForkedBlockchain.prototype.getStateTrie = function(root) { + return new ForkedStorageTrie(this.database.trie_db, root, { fork: this.fork, forkBlockNumber: this.forkBlockNumber, blockchain: this @@ -99,7 +105,6 @@ ForkedBlockchain.prototype.createGenesisBlock = function(callback) { // Update the relevant block numbers self.forkBlockNumber = self.options.forkBlockNumber = blockNumber; - self.stateTrie.forkBlockNumber = blockNumber; self.createBlock(function(err, block) { if (err) { @@ -114,12 +119,12 @@ ForkedBlockchain.prototype.createGenesisBlock = function(callback) { }); }; -ForkedBlockchain.prototype.createForkedStorageTrie = function(address) { +ForkedBlockchain.prototype.createForkedStorageTrie = function(stateTrie, address) { address = to.hex(address); - var trie = new ForkedStorageTrie(this.data.trie_db, null, { + var trie = new ForkedStorageTrie(this.database.trie_db, null, { address: address, - stateTrie: this.stateTrie, + stateTrie, blockchain: this, fork: this.fork, forkBlockNumber: this.forkBlockNumber @@ -130,14 +135,14 @@ ForkedBlockchain.prototype.createForkedStorageTrie = function(address) { return trie; }; -ForkedBlockchain.prototype.lookupStorageTrie = function(address, callback) { +ForkedBlockchain.prototype.lookupStorageTrie = function(stateTrie, address, callback) { address = to.hex(address); if (this.storageTrieCache[address] != null) { return callback(null, this.storageTrieCache[address]); } - callback(null, this.createForkedStorageTrie(address)); + callback(null, this.createForkedStorageTrie(stateTrie, address)); }; ForkedBlockchain.prototype.isFallbackBlock = function(value, callback) { @@ -153,7 +158,8 @@ ForkedBlockchain.prototype.isFallbackBlock = function(value, callback) { }; ForkedBlockchain.prototype.isBlockHash = function(value) { - return typeof value === "string" && value.indexOf("0x") === 0 && value.length > 42; + return (typeof value === "string" && value.indexOf("0x") === 0 && value.length > 42) || + (Buffer.isBuffer(value) && value.length >= 32); }; ForkedBlockchain.prototype.isFallbackBlockHash = function(value, callback) { @@ -163,7 +169,7 @@ ForkedBlockchain.prototype.isFallbackBlockHash = function(value, callback) { return callback(null, false); } - self.data.blockHashes.get(value, function(err, blockIndex) { + self.database.blockHashes.get(value, function(err, blockIndex) { if (err) { if (err.notFound) { // If the block isn't found in our database, then it must be a fallback block. @@ -226,16 +232,20 @@ ForkedBlockchain.prototype.getFallbackBlock = function(numberOrHash, cb) { }; ForkedBlockchain.prototype.getBlock = function(number, callback) { - var self = this; - - this.isFallbackBlockHash(number, function(err, isFallbackBlockHash) { - if (err) { - return callback(err); - } - if (isFallbackBlockHash) { - return self.getFallbackBlock(number, callback); - } + const self = this; + if (self.isBlockHash(number)) { + self.isFallbackBlockHash(number, function(err, isFallbackBlockHash) { + if (err) { + return callback(err); + } + if (isFallbackBlockHash) { + return self.getFallbackBlock(number, callback); + } else { + return BlockchainDouble.prototype.getBlock.call(self, number, callback); + } + }); + } else { self.isFallbackBlock(number, function(err, isFallbackBlock) { if (err) { return callback(err); @@ -243,31 +253,25 @@ ForkedBlockchain.prototype.getBlock = function(number, callback) { if (isFallbackBlock) { return self.getFallbackBlock(number, callback); - } + } else { + // If we don't have string-based a block hash, turn what we do have into a number + // before sending it to getBlock. + self.getRelativeBlockNumber(number, function(err, blockReference) { + if (err) { + return callback(err); + } - // If we don't have string-based a block hash, turn what we do have into a number - // before sending it to getBlock. - function getBlockReference(value, callback) { - if (!self.isBlockHash(value)) { - self.getRelativeBlockNumber(value, callback); - } else { - callback(null, value); - } + BlockchainDouble.prototype.getBlock.call(self, blockReference, callback); + }); } - - getBlockReference(number, function(err, blockReference) { - if (err) { - return callback(err); - } - - BlockchainDouble.prototype.getBlock.call(self, blockReference, callback); - }); }); - }); + } }; ForkedBlockchain.prototype.getStorage = function(address, key, number, callback) { - this.lookupStorageTrie(address, function(err, trie) { + // TODO - get by block + let stateTrie = this.getStateTrie(this.getLatestStateRoot()); + this.lookupStorageTrie(stateTrie, address, function(err, trie) { if (err) { return callback(err); } @@ -276,6 +280,11 @@ ForkedBlockchain.prototype.getStorage = function(address, key, number, callback) }; ForkedBlockchain.prototype.getCode = function(address, number, callback) { + let stateTrie = this.getStateTrie(this.getLatestStateRoot()); + this._getCode(stateTrie, address, number, callback); +}; + +ForkedBlockchain.prototype._getCode = function(stateTrie, address, number, callback) { var self = this; if (typeof number === "function") { @@ -293,7 +302,7 @@ ForkedBlockchain.prototype.getCode = function(address, number, callback) { } number = effective; - self.stateTrie.keyExists(address, function(err, exists) { + stateTrie.keyExists(address, function(err, exists) { if (err) { return callback(err); } @@ -319,34 +328,33 @@ ForkedBlockchain.prototype.getCode = function(address, number, callback) { }); }; -ForkedBlockchain.prototype.putCode = function(address, value, callback) { +ForkedBlockchain.prototype._putCode = function(vm, address, value, callback) { // This is a bit of a hack. We need to bypass the vm's // _lookupAccount call that vm.stateManager.putContractCode() uses. // This means we have to do some things ourself. The last call // to self.stateTrie.put() at the bottom is important because // we can't just be satisfied putting it in the cache. - var self = this; - self.vm.stateManager.cache.flush(() => { + vm.stateManager.cache.flush(() => { address = utils.toBuffer(address); - this.stateTrie.get(address, function(err, data) { + vm.stateManager.trie.get(address, function(err, data) { if (err) { return callback(err); } var account = new Account(data); - account.setCode(self.stateTrie, value, function(err, result) { + account.setCode(vm.stateManager.trie, value, function(err, result) { if (err) { return callback(err); } - self.stateTrie.put(address, account.serialize(), function(err) { + vm.stateManager.trie.put(address, account.serialize(), function(err) { if (err) { return callback(err); } // Ensure the cache updates as well. - self.vm.stateManager.putAccount(address, account, callback); + vm.stateManager.putAccount(address, account, callback); }); }); }); @@ -354,6 +362,11 @@ ForkedBlockchain.prototype.putCode = function(address, value, callback) { }; ForkedBlockchain.prototype.getAccount = function(address, number, callback) { + let stateTrie = this.getStateTrie(this.getLatestStateRoot()); + this._getAccount(stateTrie, address, number, callback); +}; + +ForkedBlockchain.prototype._getAccount = function(stateTrie, address, number, callback) { var self = this; if (typeof number === "function") { @@ -368,13 +381,13 @@ ForkedBlockchain.prototype.getAccount = function(address, number, callback) { number = effective; // If the account doesn't exist in our state trie, get it off the wire. - self.stateTrie.keyExists(address, function(err, exists) { + stateTrie.keyExists(address, function(err, exists) { if (err) { return callback(err); } if (exists && number > to.number(self.forkBlockNumber)) { - BlockchainDouble.prototype.getAccount.call(self, address, number, function(err, acc) { + BlockchainDouble.prototype.getAccount.call(self, stateTrie, address, number, function(err, acc) { if (err) { return callback(err); } @@ -487,9 +500,11 @@ ForkedBlockchain.prototype.fetchAccountFromFallback = function(address, blockNum account.exists = code !== "0x" || balance !== "0x0" || nonce !== "0x0"; + let stateTrie = self.getStateTrie(self.getLatestStateRoot()); + // This puts the code on the trie, keyed by the hash of the code. // It does not actually link an account to code in the trie. - account.setCode(self.stateTrie, utils.toBuffer(code), function(err) { + account.setCode(stateTrie, utils.toBuffer(code), function(err) { if (err) { return callback(err); } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 88412746e5..ccb56442f3 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -3202,6 +3202,16 @@ "requires": { "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "ethereumjs-util": "^5.1.1" + }, + "dependencies": { + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "requires": { + "bn.js": "^4.10.0", + "ethereumjs-util": "^5.0.0" + } + } } }, "ethereum-common": { @@ -9269,6 +9279,16 @@ "requires": { "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", "ethereumjs-util": "^5.1.1" + }, + "dependencies": { + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "requires": { + "bn.js": "^4.10.0", + "ethereumjs-util": "^5.0.0" + } + } } }, "ethereumjs-abi": { diff --git a/test/forking.js b/test/forking.js index d48154ca5f..0fca5e6d7c 100644 --- a/test/forking.js +++ b/test/forking.js @@ -7,7 +7,6 @@ var Ganache = require(process.env.TEST_BUILD var fs = require("fs"); var solc = require("solc"); var to = require("../lib/utils/to.js"); -var async = require("async"); // Thanks solc. At least this works! // This removes solc's overzealous uncaughtException event handler. @@ -96,112 +95,68 @@ describe("Forking", function() { done(); }); - before("Gather forked accounts", function(done) { + before("Gather forked accounts", async function() { this.timeout(5000); - forkedWeb3.eth.getAccounts(function(err, f) { - if (err) { - return done(err); - } - forkedAccounts = f; - done(); - }); + forkedAccounts = await forkedWeb3.eth.getAccounts(); }); - before("Deploy initial contracts", function(done) { - forkedWeb3.eth.sendTransaction( - { - from: forkedAccounts[0], - data: contract.binary, - gas: 3141592 - }, - function(err, tx) { - if (err) { - return done(err); - } + before("Deploy initial contracts", async function() { + let receipt = await forkedWeb3.eth.sendTransaction({ + from: forkedAccounts[0], + data: contract.binary, + gas: 3141592 + }); - // Save this for a later test. - initialDeployTransactionHash = tx; + // Save this for a later test. + initialDeployTransactionHash = receipt.transactionHash; - forkedWeb3.eth.getTransactionReceipt(tx, function(err, receipt) { - if (err) { - return done(err); - } + contractAddress = receipt.contractAddress; - contractAddress = receipt.contractAddress; - - forkedWeb3.eth.getCode(contractAddress, function(err, code) { - if (err) { - return done(err); - } - - // Ensure there's *something* there. - assert.notStrictEqual(code, null); - assert.notStrictEqual(code, "0x"); - assert.notStrictEqual(code, "0x0"); - - // Deploy a second one, which we won't use often. - forkedWeb3.eth.sendTransaction( - { - from: forkedAccounts[0], - data: contract.binary, - gas: 3141592 - }, - function(err, tx) { - if (err) { - return done(err); - } - forkedWeb3.eth.getTransactionReceipt(tx, function(err, receipt) { - if (err) { - return done(err); - } - - secondContractAddress = receipt.contractAddress; - done(); - }); - } - ); - }); - }); - } - ); + let code = await forkedWeb3.eth.getCode(contractAddress); + + // Ensure there's *something* there. + assert.notStrictEqual(code, null); + assert.notStrictEqual(code, "0x"); + assert.notStrictEqual(code, "0x0"); + + // Deploy a second one, which we won't use often. + forkedWeb3.eth.sendTransaction({ + from: forkedAccounts[0], + data: contract.binary, + gas: 3141592 + }); + + secondContractAddress = receipt.contractAddress; }); - before("Make a transaction on the forked chain that produces a log", function(done) { + before("Make a transaction on the forked chain that produces a log", async function() { this.timeout(10000); var forkedExample = new forkedWeb3.eth.Contract(JSON.parse(contract.abi), contractAddress); var event = forkedExample.events.ValueSet({}); - event.once("data", function(logs) { - done(); + let p = new Promise(async function(resolve, reject) { + event.once("data", function(logs) { + resolve(); + }); }); - forkedExample.methods.setValue(7).send({ from: forkedAccounts[0] }, function(err, tx) { - if (err) { - return done(err); - } - }); + await forkedExample.methods.setValue(7).send({ from: forkedAccounts[0] }); + await p; }); - before("Get initial balance and nonce", function(done) { - async.parallel( - { - balance: forkedWeb3.eth.getBalance.bind(forkedWeb3.eth, forkedAccounts[0]), - nonce: forkedWeb3.eth.getTransactionCount.bind(forkedWeb3.eth, forkedAccounts[0]) - }, - function(err, result) { - if (err) { - return done(err); - } - initialFallbackAccountState = result; - initialFallbackAccountState.nonce = to.number(initialFallbackAccountState.nonce); - done(); - } - ); + before("Get initial balance and nonce", async function() { + let balance = await forkedWeb3.eth.getBalance(forkedAccounts[0]); + let nonce = await forkedWeb3.eth.getTransactionCount(forkedAccounts[0]); + + initialFallbackAccountState = { + balance, + nonce: to.number(nonce) + }; }); - before("Set main web3 provider, forking from forked chain at this point", function(done) { + before("Set main web3 provider, forking from forked chain at this point", async function() { mainWeb3.setProvider( Ganache.provider({ fork: forkedTargetUrl.replace("ws", "http"), @@ -211,131 +166,72 @@ describe("Forking", function() { }) ); - forkedWeb3.eth.getBlockNumber(function(err, number) { - if (err) { - return done(err); - } - forkBlockNumber = number; - done(); - }); + forkBlockNumber = await forkedWeb3.eth.getBlockNumber(); }); - before("Gather main accounts", function(done) { + before("Gather main accounts", async function() { this.timeout(5000); - mainWeb3.eth.getAccounts(function(err, m) { - if (err) { - return done(err); - } - mainAccounts = m; - done(); - }); + mainAccounts = await mainWeb3.eth.getAccounts(); }); - it("should fetch a contract from the forked provider via the main provider", function(done) { - mainWeb3.eth.getCode(contractAddress, function(err, mainCode) { - if (err) { - return done(err); - } + it("should fetch a contract from the forked provider via the main provider", async function() { + let mainCode = await mainWeb3.eth.getCode(contractAddress); + // Ensure there's *something* there. + assert.notStrictEqual(mainCode, null); + assert.notStrictEqual(mainCode, "0x"); + assert.notStrictEqual(mainCode, "0x0"); - // Ensure there's *something* there. - assert.notStrictEqual(mainCode, null); - assert.notStrictEqual(mainCode, "0x"); - assert.notStrictEqual(mainCode, "0x0"); - - // Now make sure it matches exactly. - forkedWeb3.eth.getCode(contractAddress, function(err, forkedCode) { - if (err) { - return done(err); - } + // Now make sure it matches exactly. + let forkedCode = await forkedWeb3.eth.getCode(contractAddress); - assert.strictEqual(mainCode, forkedCode); - done(); - }); - }); + assert.strictEqual(mainCode, forkedCode); }); - it("should get the balance of an address in the forked provider via the main provider", function(done) { + it("should get the balance of an address in the forked provider via the main provider", async function() { // Assert preconditions var firstForkedAccount = forkedAccounts[0]; assert(mainAccounts.indexOf(firstForkedAccount) < 0); // Now for the real test: Get the balance of a forked account through the main provider. - mainWeb3.eth.getBalance(firstForkedAccount, function(err, balance) { - if (err) { - return done(err); - } + let balance = await mainWeb3.eth.getBalance(firstForkedAccount); - // We don't assert the exact balance as transactions cost eth - assert(balance > 999999); - done(); - }); + // We don't assert the exact balance as transactions cost eth + assert(balance > 999999); }); - it("should get storage values on the forked provider via the main provider", function(done) { - mainWeb3.eth.getStorageAt(contractAddress, contract.position_of_value, function(err, result) { - if (err) { - return done(err); - } - assert.strictEqual(mainWeb3.utils.hexToNumber(result), 7); - done(); - }); + it("should get storage values on the forked provider via the main provider", async function() { + let result = await mainWeb3.eth.getStorageAt(contractAddress, contract.position_of_value); + assert.strictEqual(mainWeb3.utils.hexToNumber(result), 7); }); - it("should execute calls against a contract on the forked provider via the main provider", function(done) { + it("should execute calls against a contract on the forked provider via the main provider", async function() { var example = new mainWeb3.eth.Contract(JSON.parse(contract.abi), contractAddress); - example.methods.value().call({ from: mainAccounts[0] }, function(err, result) { - if (err) { - return done(err); - } - assert.strictEqual(mainWeb3.utils.hexToNumber(result), 7); + let result = await example.methods.value().call({ from: mainAccounts[0] }); + assert.strictEqual(mainWeb3.utils.hexToNumber(result), 7); - // Make the call again to ensure caches updated and the call still works. - example.methods.value().call({ from: mainAccounts[0] }, function(err, result) { - if (err) { - return done(err); - } - assert.strictEqual(mainWeb3.utils.hexToNumber(result), 7); - done(err); - }); - }); + // Make the call again to ensure caches updated and the call still works. + result = await example.methods.value().call({ from: mainAccounts[0] }); + assert.strictEqual(mainWeb3.utils.hexToNumber(result), 7); }); - it("should make a transaction on the main provider while not transacting on the forked provider", (done) => { + it("should make a transaction on the main provider while not transacting on the forked provider", async function() { var example = new mainWeb3.eth.Contract(JSON.parse(contract.abi), contractAddress); var forkedExample = new forkedWeb3.eth.Contract(JSON.parse(contract.abi), contractAddress); - // TODO: ugly workaround - not sure why this is necessary. - if (!forkedExample._requestManager.provider) { - forkedExample._requestManager.setProvider(forkedWeb3.eth._provider); - } - - example.methods.setValue(25).send({ from: mainAccounts[0] }, function(err) { - if (err) { - return done(err); - } + await example.methods.setValue(25).send({ from: mainAccounts[0] }); - // It insta-mines, so we can make a call directly after. - example.methods.value().call({ from: mainAccounts[0] }, function(err, result) { - if (err) { - return done(err); - } - assert.strictEqual(mainWeb3.utils.hexToNumber(result), 25); + // It insta-mines, so we can make a call directly after. + let result = await example.methods.value().call({ from: mainAccounts[0] }); + assert.strictEqual(mainWeb3.utils.hexToNumber(result), 25); - // Now call back to the forked to ensure it's value stayed 5 - forkedExample.methods.value().call({ from: forkedAccounts[0] }, function(err, result) { - if (err) { - return done(err); - } - assert.strictEqual(forkedWeb3.utils.hexToNumber(result), 7); - done(); - }); - }); - }); + // Now call back to the forked to ensure it's value stayed 5 + result = await forkedExample.methods.value().call({ from: forkedAccounts[0] }); + assert.strictEqual(forkedWeb3.utils.hexToNumber(result), 7); }); - it("should ignore continued transactions on the forked blockchain by pegging the forked block number", (done) => { + it("should ignore transactions on the forked chain after forked block", async function() { // In this test, we're going to use the second contract address that we haven't // used previously. This ensures the data hasn't been cached on the main web3 trie // yet, and it will require it forked to the forked provider at a specific block. @@ -345,38 +241,20 @@ describe("Forking", function() { var forkedExample = new forkedWeb3.eth.Contract(JSON.parse(contract.abi), secondContractAddress); - // TODO: ugly workaround - not sure why this is necessary. - if (!forkedExample._requestManager.provider) { - forkedExample._requestManager.setProvider(forkedWeb3.eth._provider); - } - // This transaction happens entirely on the forked chain after forking. // It should be ignored by the main chain. - forkedExample.methods.setValue(800).send({ from: forkedAccounts[0] }, function(err, result) { - if (err) { - return done(err); - } - // Let's assert the value was set correctly. - forkedExample.methods.value().call({ from: forkedAccounts[0] }, function(err, result) { - if (err) { - return done(err); - } - assert.strictEqual(forkedWeb3.utils.hexToNumber(result), 800); + await forkedExample.methods.setValue(800).send({ from: forkedAccounts[0] }); + // Let's assert the value was set correctly. + let result = await forkedExample.methods.value().call({ from: forkedAccounts[0] }); + assert.strictEqual(to.number(result), 800); - // Now lets check the value on the main chain. It shouldn't be 800. - example.methods.value().call({ from: mainAccounts[0] }, function(err, result) { - if (err) { - return done(err); - } + // Now lets check the value on the main chain. It shouldn't be 800. + result = await example.methods.value().call({ from: mainAccounts[0] }); - assert.strictEqual(mainWeb3.utils.hexToNumber(result), 5); - done(); - }); - }); - }); + assert.notStrictEqual(mainWeb3.utils.hexToNumber(result), 800); }); - it("should maintain a block number that includes new blocks PLUS the existing chain", function(done) { + it("should maintain a block number that includes new blocks PLUS the existing chain", async function() { // Note: The main provider should be at block 5 at this test. Reasoning: // - The forked chain has an initial block, which is block 0. // - The forked chain performed a transaction that produced a log, resulting in block 1. @@ -384,120 +262,74 @@ describe("Forking", function() { // - The main chain forked from there, creating its own initial block, block 4. // - Then the main chain performed a transaction, putting it at block 5. - mainWeb3.eth.getBlockNumber(function(err, result) { - if (err) { - return done(err); - } + let result = await mainWeb3.eth.getBlockNumber(); - assert.strictEqual(mainWeb3.utils.hexToNumber(result), 5); + assert.strictEqual(mainWeb3.utils.hexToNumber(result), 5); - // Now lets get a block that exists on the forked chain. - mainWeb3.eth.getBlock(0, function(err, mainBlock) { - if (err) { - return done(err); - } + // Now lets get a block that exists on the forked chain. + let mainBlock = await mainWeb3.eth.getBlock(0); - // And compare it to the forked chain's block - forkedWeb3.eth.getBlock(0, function(err, forkedBlock) { - if (err) { - return done(err); - } + // And compare it to the forked chain's block + let forkedBlock = await forkedWeb3.eth.getBlock(0); - // Block hashes should be the same. - assert.strictEqual(mainBlock.hash, forkedBlock.hash); + // Block hashes should be the same. + assert.strictEqual(mainBlock.hash, forkedBlock.hash); - // Now make sure we can get the block by hash as well. - mainWeb3.eth.getBlock(mainBlock.hash, function(err, mainBlockByHash) { - if (err) { - return done(err); - } + // Now make sure we can get the block by hash as well. + let mainBlockByHash = await mainWeb3.eth.getBlock(mainBlock.hash); - assert.strictEqual(mainBlock.hash, mainBlockByHash.hash); - done(); - }); - }); - }); - }); + assert.strictEqual(mainBlock.hash, mainBlockByHash.hash); }); - it("should have a genesis block whose parent is the last block from the forked provider", function(done) { - forkedWeb3.eth.getBlock(forkBlockNumber, function(err, forkedBlock) { - if (err) { - return done(err); - } + it("should have a genesis block whose parent is the last block from the forked provider", async function() { + let forkedBlock = await forkedWeb3.eth.getBlock(forkBlockNumber); + let parentHash = forkedBlock.hash; - var parentHash = forkedBlock.hash; + let mainGenesisNumber = mainWeb3.utils.hexToNumber(forkBlockNumber) + 1; + let mainGenesis = await mainWeb3.eth.getBlock(mainGenesisNumber); - var mainGenesisNumber = mainWeb3.utils.hexToNumber(forkBlockNumber) + 1; - mainWeb3.eth.getBlock(mainGenesisNumber, function(err, mainGenesis) { - if (err) { - return done(err); - } - - assert.strictEqual(mainGenesis.parentHash, parentHash); - done(); - }); - }); + assert.strictEqual(mainGenesis.parentHash, parentHash); }); // Note: This test also puts a new contract on the forked chain, which is a good test. it( "should represent the block number correctly in the Oracle contract (oracle.blockhash0)," + - " providing forked block hash and number", - function() { + " providing forked block hash and number", + async function() { this.timeout(10000); - var oracleSol = fs.readFileSync("./test/Oracle.sol", { encoding: "utf8" }); - var solcResult = solc.compile(oracleSol); - var oracleOutput = solcResult.contracts[":Oracle"]; - - return new mainWeb3.eth.Contract(JSON.parse(oracleOutput.interface)) - .deploy({ data: oracleOutput.bytecode }) - .send({ from: mainAccounts[0], gas: 3141592 }) - .then(function(oracle) { - // TODO: ugly workaround - not sure why this is necessary. - if (!oracle._requestManager.provider) { - oracle._requestManager.setProvider(mainWeb3.eth._provider); - } - return mainWeb3.eth - .getBlock(0) - .then(function(block) { - return oracle.methods - .blockhash0() - .call() - .then(function(blockhash) { - assert.strictEqual(blockhash, block.hash); - // Now check the block number. - return mainWeb3.eth.getBlockNumber(); - }); - }) - .then(function(expectedNumber) { - return oracle.methods - .currentBlock() - .call() - .then(function(number) { - assert.strictEqual(to.number(number), expectedNumber + 1); - return oracle.methods.setCurrentBlock().send({ from: mainAccounts[0], gas: 3141592 }); - }) - .then(function(tx) { - return oracle.methods.lastBlock().call({ from: mainAccounts[0] }); - }) - .then(function(val) { - assert.strictEqual(to.number(val), expectedNumber + 1); - }); - }); - }); + const oracleSol = fs.readFileSync("./test/Oracle.sol", { encoding: "utf8" }); + const solcResult = solc.compile(oracleSol); + const oracleOutput = solcResult.contracts[":Oracle"]; + + const contract = new mainWeb3.eth.Contract(JSON.parse(oracleOutput.interface)); + const deployTxn = contract.deploy({ data: oracleOutput.bytecode }); + const oracle = await deployTxn.send({ from: mainAccounts[0], gas: 3141592 }); + + const block = await mainWeb3.eth.getBlock(0); + const blockhash = await oracle.methods.blockhash0().call(); + assert.strictEqual(blockhash, block.hash); + + const expectedNumber = await mainWeb3.eth.getBlockNumber(); + + const number = await oracle.methods.currentBlock().call(); + assert.strictEqual(to.number(number), expectedNumber + 1); + + await oracle.methods.setCurrentBlock().send({ from: mainAccounts[0], gas: 3141592 }); + const val = await oracle.methods.lastBlock().call({ from: mainAccounts[0] }); + assert.strictEqual(to.number(val), expectedNumber + 1); } ); - // TODO + // TODO: refactor this to not use web3 it("should be able to get logs across the fork boundary", function(done) { this.timeout(30000); - var example = new mainWeb3.eth.Contract(JSON.parse(contract.abi), contractAddress); + let example = new mainWeb3.eth.Contract(JSON.parse(contract.abi), contractAddress); + + let event = example.events.ValueSet({ fromBlock: 0, toBlock: "latest" }); - var event = example.events.ValueSet({ fromBlock: 0, toBlock: "latest" }); + let callcount = 0; - var callcount = 0; event.on("data", function(log) { callcount++; if (callcount === 2) { @@ -507,192 +339,138 @@ describe("Forking", function() { }); }); - it("should return the correct nonce based on block number", function(done) { + it("should return the correct nonce based on block number", async function() { // Note for the first two requests, we choose the block numbers 1 before and after the fork to // ensure we're pulling data off the correct provider in both cases. - async.parallel( - { - nonceBeforeFork: mainWeb3.eth.getTransactionCount.bind(mainWeb3.eth, forkedAccounts[0], forkBlockNumber - 1), - nonceAtFork: mainWeb3.eth.getTransactionCount.bind(mainWeb3.eth, forkedAccounts[0], forkBlockNumber + 1), - nonceLatestMain: mainWeb3.eth.getTransactionCount.bind(mainWeb3.eth, forkedAccounts[0], "latest"), - nonceLatestFallback: forkedWeb3.eth.getTransactionCount.bind(forkedWeb3.eth, forkedAccounts[0], "latest") - }, - function(err, results) { - if (err) { - return done(err); - } - - var nonceBeforeFork = results.nonceBeforeFork; - var nonceAtFork = results.nonceAtFork; - var nonceLatestMain = results.nonceLatestMain; - var nonceLatestFallback = results.nonceLatestFallback; - - // First ensure our nonces for the block before the fork - // Note that we're asking for the block *before* the forked block, - // which automatically means we sacrifice a transaction (i.e., one nonce value) - assert.strictEqual(nonceBeforeFork, initialFallbackAccountState.nonce - 1); - - // Now check at the fork. We should expect our initial state. - assert.strictEqual(nonceAtFork, initialFallbackAccountState.nonce); - - // Make sure the main web3 provider didn't alter the state of the forked account. - // This means the nonce should stay the same. - assert.strictEqual(nonceLatestMain, initialFallbackAccountState.nonce); - - // And since we made one additional transaction with this account on the forked - // provider AFTER the fork, it's nonce should be one ahead, and the main provider's - // nonce for that address shouldn't acknowledge it. - assert.strictEqual(nonceLatestFallback, nonceLatestMain + 1); - - done(); - } - ); + let nonceBeforeFork = await mainWeb3.eth.getTransactionCount(forkedAccounts[0], forkBlockNumber - 1); + let nonceAtFork = await mainWeb3.eth.getTransactionCount(forkedAccounts[0], forkBlockNumber + 1); + let nonceLatestMain = await mainWeb3.eth.getTransactionCount(forkedAccounts[0], "latest"); + let nonceLatestFallback = await forkedWeb3.eth.getTransactionCount(forkedAccounts[0], "latest"); + + // First ensure our nonces for the block before the fork + // Note that we're asking for the block *before* the forked block, + // which automatically means we sacrifice a transaction (i.e., one nonce value) + assert.strictEqual(nonceBeforeFork, initialFallbackAccountState.nonce - 1); + + // Now check at the fork. We should expect our initial state. + assert.strictEqual(nonceAtFork, initialFallbackAccountState.nonce); + + // Make sure the main web3 provider didn't alter the state of the forked account. + // This means the nonce should stay the same. + assert.strictEqual(nonceLatestMain, initialFallbackAccountState.nonce); + + // And since we made one additional transaction with this account on the forked + // provider AFTER the fork, it's nonce should be one ahead, and the main provider's + // nonce for that address shouldn't acknowledge it. + assert.strictEqual(nonceLatestFallback, nonceLatestMain + 1); }); - it("should return the correct balance based on block number", function(done) { + it("should return the correct balance based on block number", async function() { // Note for the first two requests, we choose the block numbers 1 before and after the fork to // ensure we're pulling data off the correct provider in both cases. - async.parallel( - { - balanceBeforeFork: mainWeb3.eth.getBalance.bind(mainWeb3.eth, forkedAccounts[0], forkBlockNumber - 1), - balanceAfterFork: mainWeb3.eth.getBalance.bind(mainWeb3.eth, forkedAccounts[0], forkBlockNumber + 1), - balanceLatestMain: mainWeb3.eth.getBalance.bind(mainWeb3.eth, forkedAccounts[0], "latest"), - balanceLatestFallback: forkedWeb3.eth.getBalance.bind(forkedWeb3.eth, forkedAccounts[0], "latest") - }, - function(err, results) { - if (err) { - return done(err); - } - - var balanceBeforeFork = mainWeb3.utils.toBN(results.balanceBeforeFork); - var balanceAfterFork = mainWeb3.utils.toBN(results.balanceAfterFork); - var balanceLatestMain = mainWeb3.utils.toBN(results.balanceLatestMain); - var balanceLatestFallback = mainWeb3.utils.toBN(results.balanceLatestFallback); - - // First ensure our balances for the block before the fork - // We do this by simply ensuring the balance has decreased since exact values - // are hard to assert in this case. - assert(balanceBeforeFork.gt(balanceAfterFork)); - - // Make sure it's not substantially larger. it should only be larger by a small - // amount (<2%). This assertion was added since forked balances were previously - // incorrectly being converted between decimal and hex - assert(balanceBeforeFork.muln(0.95).lt(balanceAfterFork)); - - // Since the forked provider had once extra transaction for this account, - // it should have a lower balance, and the main provider shouldn't acknowledge - // that transaction. - assert(balanceLatestMain.gt(balanceLatestFallback)); - - // Make sure it's not substantially larger. it should only be larger by a small - // amount (<2%). This assertion was added since forked balances were previously - // incorrectly being converted between decimal and hex - assert(balanceLatestMain.muln(0.95).lt(balanceLatestFallback)); - - done(); - } + let balanceBeforeFork = new mainWeb3.utils.BN( + await mainWeb3.eth.getBalance(forkedAccounts[0], forkBlockNumber - 1) + ); + let balanceAfterFork = new mainWeb3.utils.BN( + await mainWeb3.eth.getBalance(forkedAccounts[0], forkBlockNumber + 1) ); + let balanceLatestMain = new mainWeb3.utils.BN( + await mainWeb3.eth.getBalance(forkedAccounts[0], "latest") + ); + let balanceLatestFallback = new mainWeb3.utils.BN( + await forkedWeb3.eth.getBalance(forkedAccounts[0], "latest") + ); + + // First ensure our balances for the block before the fork + // We do this by simply ensuring the balance has decreased since exact values + // are hard to assert in this case. + assert(balanceBeforeFork.gt(balanceAfterFork)); + + // Make sure it's not substantially larger. it should only be larger by a small + // amount (<2%). This assertion was added since forked balances were previously + // incorrectly being converted between decimal and hex + assert(balanceBeforeFork.muln(0.95).lt(balanceAfterFork)); + + // Since the forked provider had once extra transaction for this account, + // it should have a lower balance, and the main provider shouldn't acknowledge + // that transaction. + assert(balanceLatestMain.gt(balanceLatestFallback)); + + // Make sure it's not substantially larger. it should only be larger by a small + // amount (<2%). This assertion was added since forked balances were previously + // incorrectly being converted between decimal and hex + assert(balanceLatestMain.muln(0.95).lt(balanceLatestFallback)); }); - it("should return the correct code based on block number", function(done) { + it("should return the correct code based on block number", async function() { // This one is simpler than the previous two. Either the code exists or doesn't. - async.parallel( - { - codeEarliest: mainWeb3.eth.getCode.bind(mainWeb3.eth, contractAddress, "earliest"), - codeAfterFork: mainWeb3.eth.getCode.bind(mainWeb3.eth, contractAddress, forkBlockNumber + 1), - codeLatest: mainWeb3.eth.getCode.bind(mainWeb3.eth, contractAddress, "latest") - }, - function(err, results) { - if (err) { - return done(err); - } - - var codeEarliest = results.codeEarliest; - var codeAfterFork = results.codeAfterFork; - var codeLatest = results.codeLatest; + let codeEarliest = await mainWeb3.eth.getCode(contractAddress, "earliest"); + let codeAfterFork = await mainWeb3.eth.getCode(contractAddress, forkBlockNumber + 1); + let codeLatest = await mainWeb3.eth.getCode(contractAddress, "latest"); - // There should be no code initially. - assert.strictEqual(codeEarliest, "0x"); + // There should be no code initially. + assert.strictEqual(codeEarliest, "0x"); - // Arbitrary length check since we can't assert the exact value - assert(codeAfterFork.length > 20); - assert(codeLatest.length > 20); + // Arbitrary length check since we can't assert the exact value + assert(codeAfterFork.length > 20); + assert(codeLatest.length > 20); - // These should be the same since code can't change. - assert.strictEqual(codeAfterFork, codeLatest); - - done(); - } - ); + // These should be the same since code can't change. + assert.strictEqual(codeAfterFork, codeLatest); }); - it("should return transactions for blocks requested before the fork", function(done) { - forkedWeb3.eth.getTransactionReceipt(initialDeployTransactionHash, function(err, receipt) { - if (err) { - return done(err); - } - - forkedWeb3.eth.getBlock(receipt.blockNumber, true, function(err, referenceBlock) { - if (err) { - return done(err); - } - - mainWeb3.eth.getBlock(receipt.blockNumber, true, function(err, forkedBlock) { - if (err) { - return done(err); - } + it("should return transactions for blocks requested before the fork", async function() { + let receipt = await forkedWeb3.eth.getTransactionReceipt(initialDeployTransactionHash); + let referenceBlock = await forkedWeb3.eth.getBlock(receipt.blockNumber, true); + let forkedBlock = await mainWeb3.eth.getBlock(receipt.blockNumber, true); + assert.strictEqual(forkedBlock.transactions.length, referenceBlock.transactions.length); + assert.deepStrictEqual(forkedBlock.transactions, referenceBlock.transactions); + }); - assert.strictEqual(forkedBlock.transactions.length, referenceBlock.transactions.length); - assert.deepStrictEqual(forkedBlock.transactions, referenceBlock.transactions); - done(); - }); - }); - }); + it("should return a transaction for transactions made before the fork", async function() { + let referenceTransaction = await forkedWeb3.eth.getTransaction(initialDeployTransactionHash); + let forkedTransaction = await mainWeb3.eth.getTransaction(initialDeployTransactionHash); + assert.deepStrictEqual(referenceTransaction, forkedTransaction); }); - it("should return a transaction for transactions made before the fork", function(done) { - forkedWeb3.eth.getTransaction(initialDeployTransactionHash, function(err, referenceTransaction) { - if (err) { - return done(err); - } + it("should return a transaction receipt for transactions made before the fork", async function() { + let referenceReceipt = await forkedWeb3.eth.getTransactionReceipt(initialDeployTransactionHash); + assert.deepStrictEqual(referenceReceipt.transactionHash, initialDeployTransactionHash); - mainWeb3.eth.getTransaction(initialDeployTransactionHash, function(err, forkedTransaction) { - if (err) { - return done(err); - } + let forkedReceipt = await mainWeb3.eth.getTransactionReceipt(initialDeployTransactionHash); - assert.deepStrictEqual(referenceTransaction, forkedTransaction); - done(); - }); - }); + assert.deepStrictEqual(forkedReceipt.transactionHash, initialDeployTransactionHash); + assert.deepStrictEqual(referenceReceipt, forkedReceipt); }); - it("should return a transaction receipt for transactions made before the fork", function(done) { - forkedWeb3.eth.getTransactionReceipt(initialDeployTransactionHash, function(err, referenceReceipt) { - if (err) { - return done(err); - } - assert.deepStrictEqual(referenceReceipt.transactionHash, initialDeployTransactionHash); - - mainWeb3.eth.getTransactionReceipt(initialDeployTransactionHash, function(err, forkedReceipt) { - if (err) { - return done(err); - } - - assert.deepStrictEqual(forkedReceipt.transactionHash, initialDeployTransactionHash); - assert.deepStrictEqual(referenceReceipt, forkedReceipt); - done(); - }); - }); + it("should return the same network version as the chain it forked from", async function() { + let forkedNetwork = await forkedWeb3.eth.net.getId(); + let mainNetwork = await mainWeb3.eth.net.getId(); + assert.strictEqual(mainNetwork, forkedNetwork); }); - it("should return the same network version as the chain it forked from", function(done) { - forkedWeb3.eth.net.getId(function(_, forkedNetwork) { - mainWeb3.eth.net.getId(function(_, mainNetwork) { - assert.strictEqual(mainNetwork, forkedNetwork); - }); + it("should trace a successful transaction", async function() { + let block = await mainWeb3.eth.getBlock("latest"); + let hash = block.transactions[0]; + + await new Promise((resolve, reject) => { + mainWeb3._provider.send( + { + jsonrpc: "2.0", + method: "debug_traceTransaction", + params: [hash, []], + id: new Date().getTime() + }, + function(err, response) { + if (err) { + reject(err); + } + if (response.error) { + reject(response.error); + } + resolve(); + }); }); - done(); }); after("Shutdown server", function(done) {