Skip to content

Commit 6d11b34

Browse files
authored
Merge pull request #671 from onflow/mpeter/improve-gas-estimation-logic
Improve gas estimation logic for `eth_estimateGas`
2 parents b57e3e0 + 227f57e commit 6d11b34

File tree

5 files changed

+129
-55
lines changed

5 files changed

+129
-55
lines changed

api/utils.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func handleError[T any](err error, log zerolog.Logger, collector metrics.Collect
136136
// `EVM.dryRun` inside Cadence scripts, meaning that no state change
137137
// will occur.
138138
// This is only useful for `eth_estimateGas` and `eth_call` endpoints.
139-
func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.LegacyTx, error) {
139+
func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.DynamicFeeTx, error) {
140140
var data []byte
141141
if args.Data != nil {
142142
data = *args.Data
@@ -156,12 +156,19 @@ func encodeTxFromArgs(args ethTypes.TransactionArgs) (*types.LegacyTx, error) {
156156
value = args.Value.ToInt()
157157
}
158158

159-
return &types.LegacyTx{
160-
Nonce: 0,
161-
To: args.To,
162-
Value: value,
163-
Gas: gasLimit,
164-
GasPrice: big.NewInt(0),
165-
Data: data,
159+
accessList := types.AccessList{}
160+
if args.AccessList != nil {
161+
accessList = *args.AccessList
162+
}
163+
164+
return &types.DynamicFeeTx{
165+
Nonce: 0,
166+
To: args.To,
167+
Value: value,
168+
Gas: gasLimit,
169+
Data: data,
170+
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
171+
GasFeeCap: (*big.Int)(args.MaxFeePerGas),
172+
AccessList: accessList,
166173
}, nil
167174
}

services/requester/requester.go

Lines changed: 100 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ var (
4545
const minFlowBalance = 2
4646
const blockGasLimit = 120_000_000
4747

48+
// estimateGasErrorRatio is the amount of overestimation eth_estimateGas
49+
// is allowed to produce in order to speed up calculations.
50+
const estimateGasErrorRatio = 0.015
51+
4852
type Requester interface {
4953
// SendRawTransaction will submit signed transaction data to the network.
5054
// The submitted EVM transaction hash is returned.
@@ -58,7 +62,7 @@ type Requester interface {
5862
// Note, this function doesn't make and changes in the state/blockchain and is
5963
// useful to execute and retrieve values.
6064
Call(
61-
tx *types.LegacyTx,
65+
tx *types.DynamicFeeTx,
6266
from common.Address,
6367
height uint64,
6468
stateOverrides *ethTypes.StateOverride,
@@ -68,7 +72,7 @@ type Requester interface {
6872
// Note, this function doesn't make any changes in the state/blockchain and is
6973
// useful to executed and retrieve the gas consumption and possible failures.
7074
EstimateGas(
71-
tx *types.LegacyTx,
75+
tx *types.DynamicFeeTx,
7276
from common.Address,
7377
height uint64,
7478
stateOverrides *ethTypes.StateOverride,
@@ -324,7 +328,7 @@ func (e *EVM) GetStorageAt(
324328
}
325329

326330
func (e *EVM) Call(
327-
tx *types.LegacyTx,
331+
tx *types.DynamicFeeTx,
328332
from common.Address,
329333
height uint64,
330334
stateOverrides *ethTypes.StateOverride,
@@ -334,42 +338,113 @@ func (e *EVM) Call(
334338
return nil, err
335339
}
336340

337-
return result.ReturnedData, err
341+
resultSummary := result.ResultSummary()
342+
if resultSummary.ErrorCode != 0 {
343+
if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted {
344+
return nil, errs.NewRevertError(resultSummary.ReturnedData)
345+
}
346+
return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage)
347+
}
348+
349+
return result.ReturnedData, nil
338350
}
339351

340352
func (e *EVM) EstimateGas(
341-
tx *types.LegacyTx,
353+
tx *types.DynamicFeeTx,
342354
from common.Address,
343355
height uint64,
344356
stateOverrides *ethTypes.StateOverride,
345357
) (uint64, error) {
358+
// Note: The following algorithm, is largely inspired from
359+
// https://github.com/onflow/go-ethereum/blob/master/eth/gasestimator/gasestimator.go#L49-L192,
360+
// and adapted to fit our use-case.
361+
// Binary search the gas limit, as it may need to be higher than the amount used
362+
var (
363+
failingGasLimit uint64 // lowest-known gas limit where tx execution fails
364+
passingGasLimit uint64 // lowest-known gas limit where tx execution succeeds
365+
)
366+
// Determine the highest gas limit that can be used during the estimation.
367+
passingGasLimit = blockGasLimit
368+
if tx.Gas >= gethParams.TxGas {
369+
passingGasLimit = tx.Gas
370+
}
371+
tx.Gas = passingGasLimit
372+
// We first execute the transaction at the highest allowable gas limit,
373+
// since if this fails we can return error immediately.
346374
result, err := e.dryRunTx(tx, from, height, stateOverrides)
347375
if err != nil {
348376
return 0, err
349377
}
378+
resultSummary := result.ResultSummary()
379+
if resultSummary.ErrorCode != 0 {
380+
if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted {
381+
return 0, errs.NewRevertError(resultSummary.ReturnedData)
382+
}
383+
return 0, errs.NewFailedTransactionError(resultSummary.ErrorMessage)
384+
}
385+
386+
// For almost any transaction, the gas consumed by the unconstrained execution
387+
// above lower-bounds the gas limit required for it to succeed. One exception
388+
// is those that explicitly check gas remaining in order to execute within a
389+
// given limit, but we probably don't want to return the lowest possible gas
390+
// limit for these cases anyway.
391+
failingGasLimit = result.GasConsumed - 1
392+
393+
// There's a fairly high chance for the transaction to execute successfully
394+
// with gasLimit set to the first execution's GasConsumed + GasRefund.
395+
// Explicitly check that gas amount and use as a limit for the binary search.
396+
optimisticGasLimit := (result.GasConsumed + result.GasRefund + gethParams.CallStipend) * 64 / 63
397+
if optimisticGasLimit < passingGasLimit {
398+
tx.Gas = optimisticGasLimit
399+
result, err = e.dryRunTx(tx, from, height, stateOverrides)
400+
if err != nil {
401+
// This should not happen under normal conditions since if we make it this far the
402+
// transaction had run without error at least once before.
403+
return 0, err
404+
}
405+
resultSummary := result.ResultSummary()
406+
if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas {
407+
failingGasLimit = optimisticGasLimit
408+
} else {
409+
passingGasLimit = optimisticGasLimit
410+
}
411+
}
350412

351-
if result.Successful() {
352-
// As mentioned in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md#specification
353-
// Define "all but one 64th" of N as N - floor(N / 64).
354-
// If a call asks for more gas than the maximum allowed amount
355-
// (i.e. the total amount of gas remaining in the parent after subtracting
356-
// the gas cost of the call and memory expansion), do not return an OOG error;
357-
// instead, if a call asks for more gas than all but one 64th of the maximum
358-
// allowed amount, call with all but one 64th of the maximum allowed amount of
359-
// gas (this is equivalent to a version of EIP-901 plus EIP-1142).
360-
// CREATE only provides all but one 64th of the parent gas to the child call.
361-
result.GasConsumed = AddOne64th(result.GasConsumed)
362-
363-
// Adding `gethParams.SstoreSentryGasEIP2200` is needed for this condition:
364-
// https://github.com/onflow/go-ethereum/blob/master/core/vm/operations_acl.go#L29-L32
365-
result.GasConsumed += gethParams.SstoreSentryGasEIP2200
413+
// Binary search for the smallest gas limit that allows the tx to execute successfully.
414+
for failingGasLimit+1 < passingGasLimit {
415+
// It is a bit pointless to return a perfect estimation, as changing
416+
// network conditions require the caller to bump it up anyway. Since
417+
// wallets tend to use 20-25% bump, allowing a small approximation
418+
// error is fine (as long as it's upwards).
419+
if float64(passingGasLimit-failingGasLimit)/float64(passingGasLimit) < estimateGasErrorRatio {
420+
break
421+
}
422+
mid := (passingGasLimit + failingGasLimit) / 2
423+
if mid > failingGasLimit*2 {
424+
// Most txs don't need much higher gas limit than their gas used, and most txs don't
425+
// require near the full block limit of gas, so the selection of where to bisect the
426+
// range here is skewed to favor the low side.
427+
mid = failingGasLimit * 2
428+
}
429+
tx.Gas = mid
430+
result, err = e.dryRunTx(tx, from, height, stateOverrides)
431+
if err != nil {
432+
return 0, err
433+
}
434+
resultSummary := result.ResultSummary()
435+
if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas {
436+
failingGasLimit = mid
437+
} else {
438+
passingGasLimit = mid
439+
}
440+
}
366441

367-
// Take into account any gas refunds, which are calculated only after
368-
// transaction execution.
369-
result.GasConsumed += result.GasRefund
442+
if tx.AccessList != nil {
443+
passingGasLimit += uint64(len(tx.AccessList)) * gethParams.TxAccessListAddressGas
444+
passingGasLimit += uint64(tx.AccessList.StorageKeys()) * gethParams.TxAccessListStorageKeyGas
370445
}
371446

372-
return result.GasConsumed, err
447+
return passingGasLimit, nil
373448
}
374449

375450
func (e *EVM) GetCode(
@@ -461,7 +536,7 @@ func (e *EVM) evmToCadenceHeight(height uint64) (uint64, error) {
461536
}
462537

463538
func (e *EVM) dryRunTx(
464-
tx *types.LegacyTx,
539+
tx *types.DynamicFeeTx,
465540
from common.Address,
466541
height uint64,
467542
stateOverrides *ethTypes.StateOverride,
@@ -521,14 +596,6 @@ func (e *EVM) dryRunTx(
521596
return nil, err
522597
}
523598

524-
resultSummary := result.ResultSummary()
525-
if resultSummary.ErrorCode != 0 {
526-
if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted {
527-
return nil, errs.NewRevertError(resultSummary.ReturnedData)
528-
}
529-
return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage)
530-
}
531-
532599
return result, nil
533600
}
534601

tests/web3js/build_evm_state_test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ it('should handle a large number of EVM interactions', async () => {
156156
gas: 55_000,
157157
gasPrice: conf.minGasPrice
158158
}, 82n)
159-
assert.equal(estimatedGas, 23823n)
159+
assert.equal(estimatedGas, 21358n)
160160

161161
estimatedGas = await web3.eth.estimateGas({
162162
from: conf.eoa.address,
@@ -165,7 +165,7 @@ it('should handle a large number of EVM interactions', async () => {
165165
gas: 55_000,
166166
gasPrice: conf.minGasPrice
167167
}, latest)
168-
assert.equal(estimatedGas, 29292n)
168+
assert.equal(estimatedGas, 26811n)
169169

170170
// Add calls to verify correctness of eth_getCode on historical heights
171171
let code = await web3.eth.getCode(contractAddress, 82n)

tests/web3js/debug_traces_test.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ it('should retrieve transaction traces', async () => {
3535
// Assert proper response for `callTracer`
3636
let txTrace = response.body.result
3737
assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e')
38-
assert.equal(txTrace.gas, '0x118e0c')
38+
assert.equal(txTrace.gas, '0x1167ac')
3939
assert.equal(txTrace.gasUsed, '0x114010')
4040
assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab')
4141
assert.lengthOf(txTrace.input, 9856n)
@@ -92,7 +92,7 @@ it('should retrieve transaction traces', async () => {
9292
// Assert proper response for `callTracer`
9393
txTrace = response.body.result
9494
assert.equal(txTrace.from, '0xfacf71692421039876a5bb4f10ef7a439d8ef61e')
95-
assert.equal(txTrace.gas, '0x72c3')
95+
assert.equal(txTrace.gas, '0x697f')
9696
assert.equal(txTrace.gasUsed, '0x6827')
9797
assert.equal(txTrace.to, '0x99a64c993965f8d69f985b5171bc20065cc32fab')
9898
assert.equal(
@@ -161,10 +161,10 @@ it('should retrieve transaction traces', async () => {
161161
txTraces,
162162
[
163163
{
164-
txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd',
164+
txHash: '0xc34f49f9c6b56ebd88095054e2ad42d6854ba818a9657caf3f8500161a5e4ef7',
165165
result: {
166166
from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e',
167-
gas: '0x72c3',
167+
gas: '0x697f',
168168
gasUsed: '0x6827',
169169
to: '0x99a64c993965f8d69f985b5171bc20065cc32fab',
170170
input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064',
@@ -200,10 +200,10 @@ it('should retrieve transaction traces', async () => {
200200
txTraces,
201201
[
202202
{
203-
txHash: '0x87449feedc004c75c0e8b12d01656f2e28366c7d73b1b5336beae20aaa5033dd',
203+
txHash: '0xc34f49f9c6b56ebd88095054e2ad42d6854ba818a9657caf3f8500161a5e4ef7',
204204
result: {
205205
from: '0xfacf71692421039876a5bb4f10ef7a439d8ef61e',
206-
gas: '0x72c3',
206+
gas: '0x697f',
207207
gasUsed: '0x6827',
208208
to: '0x99a64c993965f8d69f985b5171bc20065cc32fab',
209209
input: '0x6057361d0000000000000000000000000000000000000000000000000000000000000064',
@@ -257,15 +257,15 @@ it('should retrieve transaction traces', async () => {
257257
txTrace,
258258
{
259259
from: conf.eoa.address.toLowerCase(),
260-
gas: '0xc9c7',
260+
gas: '0xbf57',
261261
gasUsed: '0x6147',
262262
to: contractAddress.toLowerCase(),
263263
input: '0xc550f90f',
264264
output: '0x0000000000000000000000000000000000000000000000000000000000000006',
265265
calls: [
266266
{
267267
from: contractAddress.toLowerCase(),
268-
gas: '0x6948',
268+
gas: '0x5f01',
269269
gasUsed: '0x2',
270270
to: '0x0000000000000000000000010000000000000001',
271271
input: '0x53e87d66',

tests/web3js/eth_deploy_contract_and_interact_test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ it('deploy contract and interact', async () => {
221221
},
222222
'0x1'
223223
)
224-
assert.equal(gasEstimate, 23977n)
224+
assert.equal(gasEstimate, 21510n)
225225

226226
gasEstimate = await web3.eth.estimateGas(
227227
{
@@ -233,7 +233,7 @@ it('deploy contract and interact', async () => {
233233
},
234234
'latest'
235235
)
236-
assert.equal(gasEstimate, 27398n)
236+
assert.equal(gasEstimate, 25052n)
237237

238238
// check that `eth_call` can handle state overrides
239239
let stateOverrides = {
@@ -274,7 +274,7 @@ it('deploy contract and interact', async () => {
274274
assert.isDefined(response.body)
275275

276276
result = response.body.result
277-
assert.equal(result, '0x72c3')
277+
assert.equal(result, '0x697f')
278278

279279
stateOverrides = {
280280
[contractAddress]: {
@@ -295,5 +295,5 @@ it('deploy contract and interact', async () => {
295295
// setting a storage slot from a zero-value, to a non-zero value has an
296296
// increase of about 20,000 gas. Which is quite different to `0x72c3`.
297297
result = response.body.result
298-
assert.equal(result, '0xb69a')
298+
assert.equal(result, '0xac6d')
299299
})

0 commit comments

Comments
 (0)