diff --git a/lib/utils/forkedblockchain.js b/lib/utils/forkedblockchain.js index 7700435848..9caff703c2 100644 --- a/lib/utils/forkedblockchain.js +++ b/lib/utils/forkedblockchain.js @@ -60,22 +60,15 @@ ForkedBlockchain.prototype.initialize = function(accounts, callback) { self.forkVersion = version; - BlockchainDouble.prototype.initialize.call(self, accounts, function(err) { - if (err) { - return callback(err); - } - - self.patchVM(self.vm); - - callback(); - }); + BlockchainDouble.prototype.initialize.call(self, accounts, callback); }); }; ForkedBlockchain.prototype.patchVM = function(vm) { const trie = vm.stateManager._trie; + const lookupAccount = this.getLookupAccount(trie); // Unfortunately forking requires a bit of monkey patching, but it gets the job done. - vm.stateManager._lookupStorageTrie = this.lookupStorageTrie.bind(this); - vm.stateManager._cache._lookupAccount = this.getLookupAccount(trie); + vm.stateManager._cache._lookupAccount = lookupAccount; + vm.stateManager._lookupStorageTrie = this.getLookupStorageTrie(trie, lookupAccount); vm.stateManager.getContractCode = this.getCode.bind(this); }; @@ -108,7 +101,7 @@ ForkedBlockchain.prototype.createGenesisBlock = function(callback) { // Update the relevant block numbers self.forkBlockNumber = self.options.fork_block_number = blockNumber; - self.stateTrie.forkBlockNumber = blockNumber; + self.stateTrie.forkBlockNumber = self.stateTrie.options.forkBlockNumber = blockNumber; self.createBlock(function(err, block) { if (err) { @@ -123,30 +116,20 @@ ForkedBlockchain.prototype.createGenesisBlock = function(callback) { }); }; -ForkedBlockchain.prototype.createForkedStorageTrie = function(address) { - address = to.hex(address); - - var trie = new ForkedStorageTrie(this.data.trie_db, null, { - address: address, - stateTrie: this.stateTrie, - blockchain: this, - fork: this.fork, - forkBlockNumber: this.forkBlockNumber - }); - - this.storageTrieCache[address] = trie; - - return trie; -}; - -ForkedBlockchain.prototype.lookupStorageTrie = function(address, callback) { - address = to.hex(address); - - if (this.storageTrieCache[address] != null) { - return callback(null, this.storageTrieCache[address]); - } +ForkedBlockchain.prototype.getLookupStorageTrie = function(stateTrie, lookupAccount) { + lookupAccount = lookupAccount || this.getLookupAccount(stateTrie); + return (address, callback) => { + const storageTrie = stateTrie.copy(); + storageTrie.address = address; + lookupAccount(address, (err, account) => { + if (err) { + return callback(err); + } - callback(null, this.createForkedStorageTrie(address)); + storageTrie.root = account.stateRoot; + callback(null, storageTrie); + }); + }; }; ForkedBlockchain.prototype.isFallbackBlock = function(value, callback) { @@ -173,6 +156,10 @@ ForkedBlockchain.prototype.isFallbackBlockHash = function(value, callback) { return callback(null, false); } + if (Buffer.isBuffer(value)) { + value = to.hex(value); + } + self.data.blockHashes.get(value, function(err, blockIndex) { if (err) { if (err.notFound) { @@ -229,52 +216,46 @@ ForkedBlockchain.prototype.getFallbackBlock = function(numberOrHash, cb) { }; ForkedBlockchain.prototype.getBlock = function(number, callback) { - var self = this; - - if (Buffer.isBuffer(number)) { - number = to.hex(number); - } - - if (this.isBlockHash(number)) { - this.isFallbackBlockHash(number, handle); + let checkFn; + const isBlockHash = this.isBlockHash(number); + if (isBlockHash) { + checkFn = this.isFallbackBlockHash; } else { - self.isFallbackBlock(number, handle); + checkFn = this.isFallbackBlock; } - - function handle(err, isFallback) { + checkFn.call(this, number, (err, isFallback) => { if (err) { return callback(err); } if (isFallback) { - return self.getFallbackBlock(number, callback); + return this.getFallbackBlock(number, callback); } - getBlockReference(number, function(err, blockReference) { - if (err) { - return callback(err); - } - - BlockchainDouble.prototype.getBlock.call(self, blockReference, callback); - }); - } - - // 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); + const getBlock = BlockchainDouble.prototype.getBlock.bind(this); + if (isBlockHash) { + getBlock(number, callback); } else { - callback(null, value); + this.getRelativeBlockNumber(number, (err, number) => { + if (err) { + return callback(err); + } + getBlock(number, callback); + }); } - } + }); }; ForkedBlockchain.prototype.getStorage = function(address, key, number, callback) { - this.lookupStorageTrie(address, function(err, trie) { + this.getLookupStorageTrie(this.stateTrie)(address, (err, trie) => { if (err) { return callback(err); } - trie.get(key, callback); + this.getEffectiveBlockNumber(number, (err, number) => { + if (err) { + return callback(err); + } + trie.get(key, number, callback); + }); }); }; @@ -657,33 +638,4 @@ ForkedBlockchain.prototype.getBlockLogs = function(number, callback) { }); }; -ForkedBlockchain.prototype._checkpointTrie = function() { - var self = this; - - BlockchainDouble.prototype._checkpointTrie.call(this); - - Object.keys(this.storageTrieCache).forEach(function(address) { - var trie = self.storageTrieCache[address]; - trie.customCheckpoint(); - }); -}; - -ForkedBlockchain.prototype._revertTrie = function() { - var self = this; - - BlockchainDouble.prototype._revertTrie.call(this); - - Object.keys(this.storageTrieCache).forEach(function(address) { - var trie = self.storageTrieCache[address]; - - // We're trying to revert to a point before this trie was created. - // Let's just remove the trie. - if (trie.checkpoints.length === 0) { - delete self.storageTrieCache[address]; - } else { - trie.customRevert(); - } - }); -}; - module.exports = ForkedBlockchain; diff --git a/lib/utils/forkedstoragetrie.js b/lib/utils/forkedstoragetrie.js index 775dbe5119..b89837196e 100644 --- a/lib/utils/forkedstoragetrie.js +++ b/lib/utils/forkedstoragetrie.js @@ -1,38 +1,26 @@ -var MerklePatriciaTree = require("merkle-patricia-tree"); +const MerklePatriciaTree = require("merkle-patricia-tree"); +const BaseTrie = require("merkle-patricia-tree/baseTrie"); +const checkpointInterface = require("merkle-patricia-tree/checkpoint-interface"); var utils = require("ethereumjs-util"); var inherits = require("util").inherits; var Web3 = require("web3"); var to = require("./to.js"); -inherits(ForkedStorageTrie, MerklePatriciaTree); +inherits(ForkedStorageBaseTrie, BaseTrie); -function ForkedStorageTrie(db, root, options) { - MerklePatriciaTree.call(this, db, root); +function ForkedStorageBaseTrie(db, root, options) { + BaseTrie.call(this, db, root); + this.options = options; this.address = options.address; - - this.fork = options.fork; this.forkBlockNumber = options.forkBlockNumber; - this.blockchain = options.blockchain; - - this.web3 = new Web3(); - this.web3.setProvider(this.fork); - - this.checkpoints = []; + this.fork = options.fork; + this.web3 = new Web3(this.fork); } -ForkedStorageTrie.prototype.keyExists = function(key, callback) { - key = utils.toBuffer(key); - - this.findPath(key, function(err, node, remainder, stack) { - const exists = node && remainder.length === 0; - callback(err, exists); - }); -}; - // Note: This overrides a standard method whereas the other methods do not. -ForkedStorageTrie.prototype.get = function(key, blockNumber, callback) { +ForkedStorageBaseTrie.prototype.get = function(key, blockNumber, callback) { var self = this; // Allow an optional blockNumber @@ -53,6 +41,9 @@ ForkedStorageTrie.prototype.get = function(key, blockNumber, callback) { } if (exists) { + // TODO: just because we have the key doesn't mean we're at the right + // block number/root to send it. We need to check the block number + // before using the data in our own trie. MerklePatriciaTree.prototype.get.call(self, key, function(err, r) { callback(err, r); }); @@ -67,12 +58,14 @@ ForkedStorageTrie.prototype.get = function(key, blockNumber, callback) { callback(null, account.serialize()); }); } else { + if (to.number(blockNumber) > to.number(self.forkBlockNumber)) { + blockNumber = self.forkBlockNumber; + } self.web3.eth.getStorageAt(to.hex(self.address), to.hex(key), blockNumber, function(err, value) { if (err) { return callback(err); } - value = utils.toBuffer(value); value = utils.rlp.encode(value); callback(null, value); @@ -82,16 +75,27 @@ ForkedStorageTrie.prototype.get = function(key, blockNumber, callback) { }); }; -// I don't want checkpoints to be removed by commits. -// Note: For some reason, naming this function checkpoint() -// -- overriding the default function -- prevents it from -// being called. -ForkedStorageTrie.prototype.customCheckpoint = function() { - this.checkpoints.push(this.root); +ForkedStorageBaseTrie.prototype.keyExists = function(key, callback) { + key = utils.toBuffer(key); + + this.findPath(key, function(err, node, remainder, stack) { + const exists = node && remainder.length === 0; + callback(err, exists); + }); }; -ForkedStorageTrie.prototype.customRevert = function() { - this.root = this.checkpoints.pop(); +ForkedStorageBaseTrie.prototype.copy = function() { + return new ForkedStorageBaseTrie(this.db, this.root, this.options); }; +inherits(ForkedStorageTrie, ForkedStorageBaseTrie); + +function ForkedStorageTrie(db, root, options) { + ForkedStorageBaseTrie.call(this, db, root, options); + checkpointInterface(this); +} + +ForkedStorageTrie.prove = MerklePatriciaTree.prove; +ForkedStorageTrie.verifyProof = MerklePatriciaTree.verifyProof; + module.exports = ForkedStorageTrie; diff --git a/test/debug/debug.js b/test/debug/debug.js index 8d12572e07..af72ea9906 100644 --- a/test/debug/debug.js +++ b/test/debug/debug.js @@ -1,17 +1,50 @@ const assert = require("assert"); const bootstrap = require("../helpers/contract/bootstrap"); +const { promisify } = require("util"); +var Ganache = require(process.env.TEST_BUILD + ? "../../build/ganache.core." + process.env.TEST_BUILD + ".js" + : "../../index.js"); // Thanks solc. At least this works! // This removes solc's overzealous uncaughtException event handler. process.removeAllListeners("uncaughtException"); -describe("Debug", function() { +function test(forked) { let options = {}; let context; + const mnemonic = "sweet candy treat"; const gas = 3141592; let hashToTrace = null; let multipleCallsHashToTrace = null; const expectedValueBeforeTrace = "1234"; + const val = "26"; + const forkedTargetUrl = "ws://localhost:21345"; + + // steps: + + /* + setValue(26) + this sets .value to 26 and otherValue to 31 (26 + 5) + then setValue(1234) + this sets .value to 1234 and otherValue to 1265 (31 + 1234) + then trace the first tx + we want to make sure the data set by the traced transaction: + 26 and 31. + and that it didn't modify the original data + */ + + if (forked) { + let forkedServer; + before("init forkedServer", async function() { + forkedServer = Ganache.server({ + mnemonic + }); + await promisify(forkedServer.listen)(21345); + }); + after("shutdown forkedServer", () => { + forkedServer.close(); + }); + } before("set up web3 and contract", async function() { this.timeout(10000); @@ -19,24 +52,28 @@ describe("Debug", function() { contractFiles: ["DebugContract"], contractSubdirectory: "debug" }; - context = await bootstrap(contractRef); + const options = forked ? { fork: forkedTargetUrl.replace("ws", "http"), mnemonic } : { mnemonic }; + context = await bootstrap(contractRef, options); }); describe("Trace a successful transaction", function() { before("set up transaction that should be traced", async() => { const { accounts, instance } = context; options = { from: accounts[0], gas }; - const tx = await instance.methods.setValue(26).send(options); - - // check the value is what we expect it to be: 26 - const value = await instance.methods.value().call(options); - assert.strictEqual(value, "26"); + const tx = await instance.methods.setValue(val).send(options); // set hashToTrace to the tx we made, so we know preconditions are correctly set hashToTrace = tx.transactionHash; }); - before("change state of contract to ensure trace doesn't overwrite data", async() => { + it("sets the value to 26", async() => { + const { instance } = context; + // check the value is what we expect it to be: 26 + const value = await instance.methods.value().call(options); + assert.strictEqual(value, val); + }); + + it("changes state of contract to ensure trace doesn't overwrite data", async() => { const { accounts, instance } = context; options = { from: accounts[0], gas }; await instance.methods.setValue(expectedValueBeforeTrace).send(options); @@ -48,59 +85,47 @@ describe("Debug", function() { it("should trace a successful transaction without changing state", async function() { // We want to trace the transaction that sets the value to 26 - const { accounts, instance, provider } = context; - - await new Promise((resolve, reject) => { - provider.send( - { - jsonrpc: "2.0", - method: "debug_traceTransaction", - params: [hashToTrace, []], - id: new Date().getTime() - }, - function(err, response) { - if (err) { - reject(err); - } - if (response.error) { - reject(response.error); - } + const { accounts, instance, send } = context; - const structLogs = response.result.structLogs; + const response = await send("debug_traceTransaction", hashToTrace, []); - // To at least assert SOMETHING, let's assert the last opcode - assert(structLogs.length > 0); + if (response.error) { + assert.fail(response.error); + } - for (let op of structLogs) { - if (op.stack.length > 0) { - // check formatting of stack - it was broken when updating to ethereumjs-vm v2.3.3 - assert.strictEqual(op.stack[0].length, 64); - assert.notStrictEqual(op.stack[0].substr(0, 2), "0x"); - break; - } - } + const structLogs = response.result.structLogs; - const lastop = structLogs[structLogs.length - 1]; - - assert.strictEqual(lastop.op, "STOP"); - assert.strictEqual(lastop.gasCost, 1); - assert.strictEqual(lastop.pc, 209); - assert.strictEqual( - lastop.storage["0000000000000000000000000000000000000000000000000000000000000000"], - "000000000000000000000000000000000000000000000000000000000000001a" - ); - assert.strictEqual( - lastop.storage["0000000000000000000000000000000000000000000000000000000000000001"], - "000000000000000000000000000000000000000000000000000000000000001f" - ); - - resolve(); - } - ); - }); + // To at least assert SOMETHING, let's assert the last opcode + assert(structLogs.length > 0); + + for (let op of structLogs) { + if (op.stack.length > 0) { + // check formatting of stack - it was broken when updating to ethereumjs-vm v2.3.3 + assert.strictEqual(op.stack[0].length, 64); + assert.notStrictEqual(op.stack[0].substr(0, 2), "0x"); + break; + } + } + + const lastop = structLogs[structLogs.length - 1]; + assert.strictEqual(lastop.op, "STOP"); + assert.strictEqual(lastop.gasCost, 1); + assert.strictEqual(lastop.pc, 209); + assert.strictEqual( + lastop.storage["0000000000000000000000000000000000000000000000000000000000000000"], + "000000000000000000000000000000000000000000000000000000000000001a" + ); + assert.strictEqual( + lastop.storage["0000000000000000000000000000000000000000000000000000000000000001"], + "000000000000000000000000000000000000000000000000000000000000001f" + ); + console.log("--------------------------------------------------"); const value = await instance.methods.value().call({ from: accounts[0], gas }); assert.strictEqual(value, expectedValueBeforeTrace); + + const otherValue = await instance.methods.otherValue().call({ from: accounts[0], gas }); + assert.strictEqual(otherValue, "1265"); }); }); @@ -121,7 +146,7 @@ describe("Debug", function() { assert.strictEqual(updatedValue, "1268"); }); - it("should trace a transaction with multiple calls to the same contract", async function() { + it("should trace a transaction with multiple calls to the same contract", function(done) { const { web3 } = context; const provider = web3.currentProvider; let arrayOfStorageKeyValues = []; @@ -164,8 +189,20 @@ describe("Debug", function() { arrayOfStorageKeyValues[3]["0000000000000000000000000000000000000000000000000000000000000001"], "00000000000000000000000000000000000000000000000000000000000004f4" ); + + done(); } ); }); }); +} + +describe("Debug", function() { + describe("Direct", function() { + test(); + }); + + describe("Forked", function() { + test(true); + }); }); diff --git a/test/forking.js b/test/forking.js index 7b282c4479..a045edbabb 100644 --- a/test/forking.js +++ b/test/forking.js @@ -193,6 +193,11 @@ describe("Forking", function() { assert.strictEqual(mainWeb3.utils.hexToNumber(result), 7); }); + it("should get storage values on the forked provider via the main provider at a block number", async() => { + const result = await mainWeb3.eth.getStorageAt(contractAddress, contract.position_of_value, 1); + assert.strictEqual(mainWeb3.utils.hexToNumber(result), 5); + }); + it("should execute calls against a contract on the forked provider via the main provider", async() => { var example = new mainWeb3.eth.Contract(JSON.parse(contract.abi), contractAddress);