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

Commit

Permalink
Merge 5b9f2f3 into 710bea1
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasjpaterno committed Sep 26, 2019
2 parents 710bea1 + 5b9f2f3 commit 20dc823
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 56 deletions.
117 changes: 62 additions & 55 deletions lib/utils/gasEstimation.js
@@ -1,4 +1,13 @@
const STIPEND = 2300;
const { BN } = require("ethereumjs-util");
const bn = (val = 0) => new BN(val);
const STIPEND = bn(2300);

const check = (set) => (opname) => set.has(opname);
const isCall = check(new Set(["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE"]));
const isCallOrCallcode = check(new Set(["CALL", "CALLCODE"]));
const isCreate = check(new Set(["CREATE", "CREATE2"]));
const isTerminator = check(new Set(["STOP", "RETURN", "REVERT", "INVALID", "SELFDESTRUCT"]));

module.exports = async(vm, runArgs, callback) => {
const steps = stepTracker();
vm.on("step", steps.collect);
Expand All @@ -7,23 +16,24 @@ module.exports = async(vm, runArgs, callback) => {
const base = index === 0;
let start = index;
let stop = 0;
let cost = 0;
let sixtyFloorths = 0;
let cost = bn();
let sixtyFloorths = bn();
const op = steps.ops[index];
const next = steps.ops[index + 1];
const intermediateCost = op.gasLeft - next.gasLeft;
let callingFee = fee || 0;
const intermediateCost = op.gasLeft.sub(next.gasLeft);
const callingFee = fee || bn();
let compositeContext = false;

function addGas(val) {
if (sixtyFloorths) {
if (val >= sixtyFloorths) {
sixtyFloorths = 0;
// Add to our current context, but accounted for in sixtyfloorths
if (sixtyFloorths.gtn(0)) {
if (val.gte(sixtyFloorths)) {
sixtyFloorths = bn();
} else {
sixtyFloorths -= val;
sixtyFloorths.isub(val);
}
}
cost += val;
cost.iadd(val);
}

return {
Expand All @@ -40,30 +50,39 @@ module.exports = async(vm, runArgs, callback) => {
transfer: (ctx) => {
const values = ctx.getCost();
addGas(values.cost);
sixtyFloorths += values.sixtyFloorths;
sixtyFloorths.iadd(values.sixtyFloorths);
},
addSixtyFloorth: (sixtyFloorth) => {
sixtyFloorths += sixtyFloorth;
sixtyFloorths.iadd(sixtyFloorth);
},
addRange: (fee = 0) => {
addRange: (fee = bn()) => {
// only occurs on stack increasing ops
addGas(steps.ops[base || compositeContext ? start : start + 1].gasLeft - steps.ops[stop].gasLeft + fee);
addGas(steps.ops[base || compositeContext ? start : start + 1].gasLeft.sub(steps.ops[stop].gasLeft).add(fee));
},
finalizeRange: () => {
let range;
// if we have a composite context and we are NOT at the final terminator
if (compositeContext && stop !== steps.ops.length - 1) {
range = steps.ops[start].gasLeft - steps.ops[stop - 1].gasLeft;
range = steps.ops[start].gasLeft.sub(steps.ops[stop - 1].gasLeft);
addGas(range);
const tail = steps.ops[stop - 1].gasLeft - steps.ops[stop].gasLeft;
range = tail + intermediateCost;
const tail = steps.ops[stop - 1].gasLeft.sub(steps.ops[stop].gasLeft);
range = tail.add(intermediateCost);
} else {
range = steps.ops[start].gasLeft - steps.ops[stop].gasLeft;
range = steps.ops[start].gasLeft.sub(steps.ops[stop].gasLeft);
}
range -= callingFee;
range.isub(callingFee);
addGas(range);
if (stop !== steps.ops.length - 1) {
cost += sixtyFloorths;
sixtyFloorths = Math.floor(cost / 63);
if (isCallOrCallcode(op.opcode.name) && !op.stack[op.stack.length - 3].isZero()) {
cost.iadd(sixtyFloorths);
const innerCost = next.gasLeft.sub(steps.ops[stop - 1].gasLeft);
if (innerCost.gt(STIPEND)) {
sixtyFloorths = cost.divn(63);
} else if (innerCost.lte(STIPEND)) {
sixtyFloorths = STIPEND.sub(innerCost);
}
} else if (stop !== steps.ops.length - 1) {
cost.iadd(sixtyFloorths);
sixtyFloorths = cost.divn(63);
}
}
};
Expand All @@ -81,16 +100,16 @@ module.exports = async(vm, runArgs, callback) => {
const current = ops[currentIndex];
const name = current.opcode.name;
if (isCall(name) || isCreate(name)) {
if (steps.isSVT(currentIndex)) {
if (steps.isPrecompile(currentIndex)) {
context.setStop(currentIndex + 1);
context.addRange();
context.setStart(currentIndex + 1);
context.addSixtyFloorth(STIPEND);
} else {
context.setStop(currentIndex);
context.addRange(current.opcode.fee);
context.addRange(bn(current.opcode.fee));
stack.push(context);
context = Context(currentIndex, current.opcode.fee); // setup next context
context = Context(currentIndex, bn(current.opcode.fee)); // setup next context
}
} else if (isTerminator(name)) {
// only on the last operation
Expand All @@ -109,8 +128,8 @@ module.exports = async(vm, runArgs, callback) => {
}
cursor++;
}
let gas = context.getCost();
return gas.cost + gas.sixtyFloorths;
const gas = context.getCost();
return gas.cost.add(gas.sixtyFloorths);
};

const result = await vm.runTx(runArgs).catch((vmerr) => ({ vmerr }));
Expand All @@ -120,57 +139,45 @@ module.exports = async(vm, runArgs, callback) => {
} else if (result.execResult.exceptionError) {
return callback(new Error(`execution error: ${result.execResult.exceptionError.error}`));
} else if (steps.done()) {
let estimate = result.gasUsed;
const estimate = result.gasUsed;
result.gasEstimate = estimate;
} else {
const actualUsed = steps.ops[0].gasLeft.sub(steps.ops[steps.ops.length - 1].gasLeft).toNumber();
const total = getTotal();
const sixtyFloorths = total - actualUsed;
result.gasEstimate = result.gasUsed.addn(sixtyFloorths);
const actualUsed = steps.ops[0].gasLeft.sub(steps.ops[steps.ops.length - 1].gasLeft);
const sixtyFloorths = getTotal().sub(actualUsed);
result.gasEstimate = result.gasUsed.add(sixtyFloorths);
}
callback(vmerr, result);
};

const check = (arr) => (opname) => arr.includes(opname);
const isCall = check(["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE"]);
const isCreate = check(["CREATE", "CREATE2"]);
const isTerminator = check(["STOP", "RETURN", "REVERT", "INVALID", "SELFDESTRUCT"]);

const stepTracker = () => {
const sysOps = [];
const allOps = [];
const svt = [];
let simpleCallCheck = false;
let simpleCallDepth = 0;
const preCompile = new Set();
let preCompileCheck = false;
let precompileCallDepth = 0;
return {
collect: (info) => {
if (simpleCallCheck) {
// This is checking for a CALL operation with no subsequent STOP/RETURN
// and where the 'call depth' never increases.
// It's usually as a result of a .call or .transfer in solidity to an
// external account or a contract with no payable.
// simpleCallCheck acts as a boolean flag checking whether the previous
// operation was a CALL. The flag is set during the 'isCall' conditional
// as well as the simpleCallDepth so its always 'up-to-date'.
if (info.depth === simpleCallDepth) {
// If the current depth (info.depth) equals the depth of a simpleCall
if (preCompileCheck) {
if (info.depth === precompileCallDepth) {
// If the current depth is unchanged.
// we record its position.
svt.push(allOps.length - 1);
preCompile.add(allOps.length - 1);
}
// Reset the flag immediately here
simpleCallCheck = false;
preCompileCheck = false;
}
if (isCall(info.opcode.name)) {
simpleCallCheck = true;
simpleCallDepth = info.depth;
info.stack = info.stack.map((val) => val.clone());
preCompileCheck = true;
precompileCallDepth = info.depth;
sysOps.push({ index: allOps.length, depth: info.depth, name: info.opcode.name });
} else if (isCreate(info.opcode.name) || isTerminator(info.opcode.name)) {
sysOps.push({ index: allOps.length, depth: info.depth, name: info.opcode.name });
}
// This goes last so we can use the length for the index ^
allOps.push(info);
},
isSVT: (index) => svt.includes(index),
isPrecompile: (index) => preCompile.has(index),
done: () => !allOps.length || sysOps.length < 2 || !isTerminator(allOps[allOps.length - 1].opcode.name),
ops: allOps,
systemOps: sysOps
Expand Down
19 changes: 19 additions & 0 deletions test/contracts/gas/NonZero.sol
@@ -0,0 +1,19 @@
pragma solidity ^0.5.0;
contract Target {
function () external payable { }
}
contract NonZero {
Target private theInstance;
constructor() public {
theInstance = new Target();
}
function doCall() external payable {
address(theInstance).call.value(msg.value).gas(123456)("");
}
function doTransfer() external payable {
address(theInstance).transfer(msg.value);
}
function doSend() external payable {
address(theInstance).send(msg.value);
}
}
33 changes: 32 additions & 1 deletion test/gas/gas.js
Expand Up @@ -46,6 +46,7 @@ describe("Gas", function() {
let Fib;
let SendContract;
let Create2;
let NonZero;
before("Setting up EIP150 contracts", async function() {
this.timeout(10000);

Expand All @@ -56,13 +57,15 @@ describe("Gas", function() {
const donation = Object.assign({ contractFiles: ["Donation"] }, subDirectory);
const fib = Object.assign({ contractFiles: ["Fib"] }, subDirectory);
const sendContract = Object.assign({ contractFiles: ["SendContract"] }, subDirectory);
const nonZero = Object.assign({ contractFiles: ["NonZero"] }, subDirectory);

const ganacheProviderOptions = { seed, hardfork };

ContractFactory = await bootstrap(factory, ganacheProviderOptions, hardfork);
TestDepth = await bootstrap(testDepth, ganacheProviderOptions, hardfork);
Donation = await bootstrap(donation, ganacheProviderOptions, hardfork);
Fib = await bootstrap(fib, ganacheProviderOptions, hardfork);
NonZero = await bootstrap(nonZero, ganacheProviderOptions, hardfork);
SendContract = await bootstrap(
sendContract,
Object.assign(
Expand Down Expand Up @@ -194,7 +197,7 @@ describe("Gas", function() {
});
});
await Promise.all(promises);
}).timeout(3000);
}).timeout(5000);

it("Should estimate gas perfectly with EIP150 - CREATE2", async() => {
const { accounts, instance, web3 } = Create2;
Expand Down Expand Up @@ -285,6 +288,34 @@ describe("Gas", function() {
assert.strictEqual(newContractBalance, "0", "balance is not 0");
}).timeout(10000);
}

it("should correctly handle non-zero value child messages", async() => {
const {
accounts: [from],
instance: { _address: to, methods },
send
} = NonZero;
const fns = [methods.doSend, methods.doTransfer, methods.doCall];
for (let i = 0, l = fns; i < l.length; i++) {
const tx = {
from,
to,
value: "1000000000000000000",
data: fns[i]().encodeABI()
};
const { result: gasLimit } = await send("eth_estimateGas", tx);
tx.gasLimit = "0x" + (parseInt(gasLimit) - 1).toString(16);
await assert.rejects(() => send("eth_sendTransaction", tx), {
message: "VM Exception while processing transaction: out of gas"
});
tx.gasLimit = gasLimit;
await assert.doesNotReject(
() => send("eth_sendTransaction", tx),
undefined,
`SANITY CHECK. Still not enough gas? ${gasLimit} Our estimate is still too low`
);
}
});
});

describe("Refunds", function() {
Expand Down

0 comments on commit 20dc823

Please sign in to comment.