Skip to content
This repository has been archived by the owner on Apr 22, 2024. It is now read-only.

Implement binary search gasEstimation #119

Merged
merged 10 commits into from
Mar 2, 2017
76 changes: 55 additions & 21 deletions subproviders/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const Block = require('ethereumjs-block')
const FakeTransaction = require('ethereumjs-tx/fake.js')
const ethUtil = require('ethereumjs-util')
const createPayload = require('../util/create-payload.js')
const rpcHexEncoding = require('../util/rpc-hex-encoding.js')
const Subprovider = require('./subprovider.js')

module.exports = VmSubprovider
Expand All @@ -23,6 +24,7 @@ function VmSubprovider(opts){
self.methods = ['eth_call', 'eth_estimateGas']
// set initialization blocker
self._ready = new Stoplight()
self._blockGasLimit = null
}

// setup a block listener on 'setEngine'
Expand All @@ -31,6 +33,7 @@ VmSubprovider.prototype.setEngine = function(engine) {
Subprovider.prototype.setEngine.call(self, engine)
// unblock initialization after first block
engine.once('block', function(block) {
self._blockGasLimit = ethUtil.bufferToInt(block.gasLimit)
self._ready.go()
})
}
Expand All @@ -41,34 +44,65 @@ VmSubprovider.prototype.handleRequest = function(payload, next, end) {
}

const self = this
// console.log('VmSubprovider - runVm init', arguments)
self.runVm(payload, function(err, results){
// console.log('VmSubprovider - runVm return', arguments)
if (err) return end(err)
switch (payload.method) {

switch (payload.method) {

case 'eth_call':
case 'eth_call':
self.runVm(payload, function(err, results){
if (err) return end(err)
var result = '0x'
if (!results.error && results.vm.return) {
// console.log(results.vm.return.toString('hex'))
result = ethUtil.addHexPrefix(results.vm.return.toString('hex'))
}
return end(null, result)

case 'eth_estimateGas':
// since eth_estimateGas is just eth_call with
// a different part of the results,
// I considered transforming request to eth_call
// to reduce the cache area, but we'd need to store
// the full vm result somewhere, instead of just
// the return value. so instead we just run it again.
end(null, result)
})
return

var result = ethUtil.addHexPrefix(results.gasUsed.toString('hex'))
return end(null, result)
case 'eth_estimateGas':
self.estimateGas(payload, end)
return
}
}

}
})
VmSubprovider.prototype.estimateGas = function(payload, end) {
const self = this
var lo = 0
var hi = self._blockGasLimit

var minDiffBetweenIterations = 1200
var prevGasLimit = self._blockGasLimit
async.doWhilst(
function(callback) {
// Take a guess at the gas, and check transaction validity
var mid = (hi + lo) / 2
payload.params[0].gas = mid
self.runVm(payload, function(err, results) {
gasUsed = err ? self._blockGasLimit : ethUtil.bufferToInt(results.gasUsed)
if (err || gasUsed === 0) {
lo = mid
} else {
hi = mid
// Perf improvement: stop the binary search when the difference in gas between two iterations
// is less then `minDiffBetweenIterations`. Doing this cuts the number of iterations from 23
// to 12, with only a ~1000 gas loss in precision.
if (Math.abs(prevGasLimit - mid) < minDiffBetweenIterations) {
lo = hi
}
}
prevGasLimit = mid
callback()
})
},
function() { return lo+1 < hi },
function(err) {
if (err) {
end(err)
} else {
hi = Math.floor(hi)
var gasEstimateHex = rpcHexEncoding.intToQuantityHex(hi)
end(null, gasEstimateHex)
}
}
)
}

VmSubprovider.prototype.runVm = function(payload, cb){
Expand Down
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ require('./solc')
require('./wallet')
require('./subproviders/sanitizer')
require('./subproviders/cache')
require('./subproviders/vm')
1 change: 1 addition & 0 deletions test/subproviders/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ require('./etherscan')
require('./ipc');
require('./sanitizer');
require('./cache');
require('./vm');
66 changes: 66 additions & 0 deletions test/subproviders/vm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const test = require('tape')
const async = require('async')
const ethUtil = require('ethereumjs-util')
const ProviderEngine = require('../../index.js')
const VmSubprovider = require('../../subproviders/vm')
const TestBlockProvider = require('../util/block.js')
const RpcSubprovider = require('../../subproviders/rpc')
const createPayload = require('../../util/create-payload.js')
const rpcHexEncoding = require('../../util/rpc-hex-encoding.js')

test('binary search eth_estimateGas implementation', function(t) {
var gasNeededScenarios = [
{
gasNeeded: 5,
gasEstimate: 1150,
numIterations: 12,
},
{
gasNeeded: 50000,
gasEstimate: 50046,
numIterations: 13,
},
{
gasNeeded: 4712387,
gasEstimate: 4712387,
numIterations: 23, // worst-case scenario
},
]

async.eachSeries(gasNeededScenarios, function(scenario, next) {
var engine = new ProviderEngine()
var vmSubprovider = new VmSubprovider()
var numIterations = 0
// Stub runVm so that it behaves as if it needs gasNeeded to run and increments numIterations
vmSubprovider.runVm = function(payload, cb) {
numIterations++
if (payload.params[0].gas < scenario.gasNeeded) {
cb(new Error('fake out of gas'))
} else {
cb(null, {
gasUsed: ethUtil.toBuffer(scenario.gasNeeded),
});
}
}
engine.addProvider(vmSubprovider)
engine.addProvider(new TestBlockProvider());
engine.start()
engine.sendAsync(createPayload({
method: 'eth_estimateGas',
params: [{}, 'latest'],
}), function(err, response) {
t.ifError(err, 'did not error')
t.ok(response, 'has response')

var gasEstimationInt = rpcHexEncoding.quantityHexToInt(response.result)
t.equal(gasEstimationInt, scenario.gasEstimate, 'properly calculates gas needed')
t.equal(numIterations, scenario.numIterations, 'ran expected number of iterations')

engine.stop()
next()
})
}, function(err) {
t.ifError(err, 'did not error')
t.end()
})
})
3 changes: 2 additions & 1 deletion test/util/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ function createBlock(blockParams, prevBlock, txs) {
blockParams = blockParams || {}
txs = txs || []
var defaultNumber = prevBlock ? incrementHex(prevBlock.number) : '0x01'
var defaultGasLimit = ethUtil.intToHex(4712388)
return extend({
// defaults
number: defaultNumber,
Expand All @@ -78,7 +79,7 @@ function createBlock(blockParams, prevBlock, txs) {
totalDifficulty: randomHash(),
size: randomHash(),
extraData: randomHash(),
gasLimit: randomHash(),
gasLimit: defaultGasLimit,
gasUsed: randomHash(),
timestamp: randomHash(),
transactions: txs,
Expand Down
7 changes: 7 additions & 0 deletions util/assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = assert

function assert(condition, message) {
if (!condition) {
throw message || "Assertion failed";
}
}
34 changes: 34 additions & 0 deletions util/rpc-hex-encoding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const ethUtil = require('ethereumjs-util')
const assert = require('./assert.js')

module.exports = {
intToQuantityHex: intToQuantityHex,
quantityHexToInt: quantityHexToInt,
}

/*
* As per https://github.com/ethereum/wiki/wiki/JSON-RPC#hex-value-encoding
* Quanities should be represented by the most compact hex representation possible
* This means that no leading zeroes are allowed. There helpers make it easy
* to convert to and from integers and their compact hex representation
*/

function intToQuantityHex(n){
assert(typeof n === 'number' && n === Math.floor(n), 'intToQuantityHex arg must be an integer')
var nHex = ethUtil.toBuffer(n).toString('hex')
if (nHex[0] === '0') {
nHex = nHex.substring(1)
}
return ethUtil.addHexPrefix(nHex)
}

function quantityHexToInt(prefixedQuantityHex) {
assert(typeof prefixedQuantityHex === 'string', 'arg to quantityHexToInt must be a string')
var quantityHex = ethUtil.stripHexPrefix(prefixedQuantityHex)
var isEven = quantityHex.length % 2 === 0
if (!isEven) {
quantityHex = '0' + quantityHex
}
var buf = new Buffer(quantityHex, 'hex')
return ethUtil.bufferToInt(buf)
}