diff --git a/lib/opFns.js b/lib/opFns.js index 980d659f5d..05999b9e64 100644 --- a/lib/opFns.js +++ b/lib/opFns.js @@ -374,19 +374,10 @@ module.exports = { value = val.toArrayLike(Buffer, 'be') } - stateManager.getContractStorage(runState.address, key, function (err, found) { + getContractStorage(runState, address, key, function (err, found) { if (err) return cb(err) try { - if (value.length === 0 && !found.length) { - subGas(runState, new BN(runState._common.param('gasPrices', 'sstoreReset'))) - } else if (value.length === 0 && found.length) { - subGas(runState, new BN(runState._common.param('gasPrices', 'sstoreReset'))) - runState.gasRefund.iaddn(runState._common.param('gasPrices', 'sstoreRefund')) - } else if (value.length !== 0 && !found.length) { - subGas(runState, new BN(runState._common.param('gasPrices', 'sstoreSet'))) - } else if (value.length !== 0 && found.length) { - subGas(runState, new BN(runState._common.param('gasPrices', 'sstoreReset'))) - } + updateSstoreGas(runState, found, value) } catch (e) { cb(e.error) return @@ -1024,3 +1015,73 @@ function makeCall (runState, callOptions, localOpts, cb) { } } } + +function getContractStorage (runState, address, key, cb) { + if (runState._common.gteHardfork('constantinople')) { + async.parallel({ + original: function (cb) { runState.originalState.getContractStorage(address, key, cb) }, + current: function (cb) { runState.stateManager.getContractStorage(address, key, cb) } + }, cb) + } else { + runState.stateManager.getContractStorage(address, key, cb) + } +} + +function updateSstoreGas (runState, found, value) { + if (runState._common.gteHardfork('constantinople')) { + var original = found.original + var current = found.current + if (current.equals(value)) { + // If current value equals new value (this is a no-op), 200 gas is deducted. + subGas(runState, new BN(runState._common.param('gasPrices', 'netSstoreNoopGas'))) + return + } + // If current value does not equal new value + if (original.equals(current)) { + // If original value equals current value (this storage slot has not been changed by the current execution context) + if (original.length === 0) { + // If original value is 0, 20000 gas is deducted. + return subGas(runState, new BN(runState._common.param('gasPrices', 'netSstoreInitGas'))) + } + if (value.length === 0) { + // If new value is 0, add 15000 gas to refund counter. + runState.gasRefund = runState.gasRefund.addn(runState._common.param('gasPrices', 'netSstoreClearRefund')) + } + // Otherwise, 5000 gas is deducted. + return subGas(runState, new BN(runState._common.param('gasPrices', 'netSstoreCleanGas'))) + } + // If original value does not equal current value (this storage slot is dirty), 200 gas is deducted. Apply both of the following clauses. + if (original.length !== 0) { + // If original value is not 0 + if (current.length === 0) { + // If current value is 0 (also means that new value is not 0), remove 15000 gas from refund counter. We can prove that refund counter will never go below 0. + runState.gasRefund = runState.gasRefund.subn(runState._common.param('gasPrices', 'netSstoreClearRefund')) + } else if (value.length === 0) { + // If new value is 0 (also means that current value is not 0), add 15000 gas to refund counter. + runState.gasRefund = runState.gasRefund.addn(runState._common.param('gasPrices', 'netSstoreClearRefund')) + } + } + if (original.equals(value)) { + // If original value equals new value (this storage slot is reset) + if (original.length === 0) { + // If original value is 0, add 19800 gas to refund counter. + runState.gasRefund = runState.gasRefund.addn(runState._common.param('gasPrices', 'netSstoreResetClearRefund')) + } else { + // Otherwise, add 4800 gas to refund counter. + runState.gasRefund = runState.gasRefund.addn(runState._common.param('gasPrices', 'netSstoreResetRefund')) + } + } + return subGas(runState, new BN(runState._common.param('gasPrices', 'netSstoreDirtyGas'))) + } else { + if (value.length === 0 && !found.length) { + subGas(runState, new BN(runState._common.param('gasPrices', 'sstoreReset'))) + } else if (value.length === 0 && found.length) { + subGas(runState, new BN(runState._common.param('gasPrices', 'sstoreReset'))) + runState.gasRefund.iaddn(runState._common.param('gasPrices', 'sstoreRefund')) + } else if (value.length !== 0 && !found.length) { + subGas(runState, new BN(runState._common.param('gasPrices', 'sstoreSet'))) + } else if (value.length !== 0 && found.length) { + subGas(runState, new BN(runState._common.param('gasPrices', 'sstoreReset'))) + } + } +} diff --git a/lib/runCode.js b/lib/runCode.js index ba70b7d07c..2e1b36268c 100644 --- a/lib/runCode.js +++ b/lib/runCode.js @@ -48,6 +48,7 @@ module.exports = function (opts, cb) { var runState = { blockchain: self.blockchain, stateManager: stateManager, + originalState: stateManager.copy(), returnValue: false, stopped: false, vmError: false, diff --git a/package.json b/package.json index ce9c0d7e19..79c6c4e2c8 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "async-eventemitter": "^0.2.2", "ethereumjs-account": "^2.0.3", "ethereumjs-block": "~2.0.1", - "ethereumjs-common": "~0.4.0", + "ethereumjs-common": "^0.6.0", "ethereumjs-util": "^6.0.0", "fake-merkle-patricia-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1", diff --git a/tests/constantinopleSstoreTest.js b/tests/constantinopleSstoreTest.js new file mode 100644 index 0000000000..f4e57f6676 --- /dev/null +++ b/tests/constantinopleSstoreTest.js @@ -0,0 +1,113 @@ +const tape = require('tape') +const async = require('async') +const VM = require('../') +const Account = require('ethereumjs-account') +const testUtil = require('./util') +const Trie = require('merkle-patricia-tree/secure') +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN + +var testCases = [ + { code: '0x60006000556000600055', usedGas: 412, refund: 0, original: '0x' }, + { code: '0x60006000556001600055', usedGas: 20212, refund: 0, original: '0x' }, + { code: '0x60016000556000600055', usedGas: 20212, refund: 19800, original: '0x' }, + { code: '0x60016000556002600055', usedGas: 20212, refund: 0, original: '0x' }, + { code: '0x60016000556001600055', usedGas: 20212, refund: 0, original: '0x' }, + { code: '0x60006000556000600055', usedGas: 5212, refund: 15000, original: '0x01' }, + { code: '0x60006000556001600055', usedGas: 5212, refund: 4800, original: '0x01' }, + { code: '0x60006000556002600055', usedGas: 5212, refund: 0, original: '0x01' }, + { code: '0x60026000556000600055', usedGas: 5212, refund: 15000, original: '0x01' }, + { code: '0x60026000556003600055', usedGas: 5212, refund: 0, original: '0x01' }, + { code: '0x60026000556001600055', usedGas: 5212, refund: 4800, original: '0x01' }, + { code: '0x60026000556002600055', usedGas: 5212, refund: 0, original: '0x01' }, + { code: '0x60016000556000600055', usedGas: 5212, refund: 15000, original: '0x01' }, + { code: '0x60016000556002600055', usedGas: 5212, refund: 0, original: '0x01' }, + { code: '0x60016000556001600055', usedGas: 412, refund: 0, original: '0x01' }, + { code: '0x600160005560006000556001600055', usedGas: 40218, refund: 19800, original: '0x' }, + { code: '0x600060005560016000556000600055', usedGas: 10218, refund: 19800, original: '0x01' } +] + +var testData = { + 'env': { + 'currentCoinbase': '0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba', + 'currentDifficulty': '0x0100', + 'currentGasLimit': '0x0f4240', + 'currentNumber': '0x00', + 'currentTimestamp': '0x01' + }, + 'exec': { + 'address': '0x01', + 'caller': '0xcd1722f3947def4cf144679da39c4c32bdc35681', + 'code': '0x60006000556000600055', + 'data': '0x', + 'gas': '0', + 'gasPrice': '0x5af3107a4000', + 'origin': '0xcd1722f3947def4cf144679da39c4c32bdc35681', + 'value': '0x0de0b6b3a7640000' + }, + 'gas': '0', + 'pre': { + '0x01': { + 'balance': '0x152d02c7e14af6800000', + 'code': '0x', + 'nonce': '0x00', + 'storage': { + '0x': '0' + } + } + } +} + +tape('test constantinople SSTORE (eip-1283)', function (t) { + testCases.forEach(function (params, i) { + t.test('should correctly run eip-1283 test #' + i, function (st) { + let state = new Trie() + let results + let account + + testData.exec.code = params.code + testData.exec.gas = params.usedGas + testData.pre['0x01'].storage['0x'] = params.original + + async.series([ + function (done) { + let acctData = testData.pre[testData.exec.address] + account = new Account() + account.nonce = testUtil.format(acctData.nonce) + account.balance = testUtil.format(acctData.balance) + testUtil.setupPreConditions(state, testData, done) + }, + function (done) { + state.get(Buffer.from(testData.exec.address, 'hex'), function (err, data) { + let a = new Account(data) + account.stateRoot = a.stateRoot + done(err) + }) + }, + function (done) { + let block = testUtil.makeBlockFromEnv(testData.env) + let vm = new VM({state: state, hardfork: 'constantinople'}) + let runCodeData = testUtil.makeRunCodeData(testData.exec, account, block) + vm.runCode(runCodeData, function (err, r) { + if (r) { + results = r + } + done(err) + }) + }, + function (done) { + if (testData.gas) { + let actualGas = results.gas.toString() + let expectedGas = new BN(testUtil.format(testData.gas)).toString() + t.equal(actualGas, expectedGas, 'valid gas usage') + t.equals(results.gasRefund.toNumber(), params.refund, 'valid gas refund') + } + done() + } + ], function (err) { + t.assert(!err) + st.end() + }) + }) + }) +}) diff --git a/tests/tester.js b/tests/tester.js index 8c80d24ecb..5bfbc96d0d 100755 --- a/tests/tester.js +++ b/tests/tester.js @@ -250,6 +250,7 @@ function runAll () { require('./tester.js') require('./cacheTest.js') require('./genesishashes.js') + require('./constantinopleSstoreTest.js') async.series([ // runTests.bind(this, 'VMTests', {}), // VM tests disabled since we don't support Frontier gas costs runTests.bind(this, 'GeneralStateTests', {}),