Skip to content

Commit e737742

Browse files
committed
Rework gas estimation logic to find the lower gas limit at which a transaction executes successfully
1 parent eb05840 commit e737742

File tree

5 files changed

+124
-54
lines changed

5 files changed

+124
-54
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: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ const minFlowBalance = 2
4949
const coaFundingBalance = minFlowBalance - 1
5050
const blockGasLimit = 120_000_000
5151

52+
// estimateGasErrorRatio is the amount of overestimation eth_estimateGas
53+
// is allowed to produce in order to speed up calculations.
54+
const estimateGasErrorRatio = 0.015
55+
5256
type Requester interface {
5357
// SendRawTransaction will submit signed transaction data to the network.
5458
// The submitted EVM transaction hash is returned.
@@ -62,7 +66,7 @@ type Requester interface {
6266
// Note, this function doesn't make and changes in the state/blockchain and is
6367
// useful to execute and retrieve values.
6468
Call(
65-
tx *types.LegacyTx,
69+
tx *types.DynamicFeeTx,
6670
from common.Address,
6771
height uint64,
6872
stateOverrides *ethTypes.StateOverride,
@@ -72,7 +76,7 @@ type Requester interface {
7276
// Note, this function doesn't make any changes in the state/blockchain and is
7377
// useful to executed and retrieve the gas consumption and possible failures.
7478
EstimateGas(
75-
tx *types.LegacyTx,
79+
tx *types.DynamicFeeTx,
7680
from common.Address,
7781
height uint64,
7882
stateOverrides *ethTypes.StateOverride,
@@ -348,7 +352,7 @@ func (e *EVM) GetStorageAt(
348352
}
349353

350354
func (e *EVM) Call(
351-
tx *types.LegacyTx,
355+
tx *types.DynamicFeeTx,
352356
from common.Address,
353357
height uint64,
354358
stateOverrides *ethTypes.StateOverride,
@@ -358,42 +362,109 @@ func (e *EVM) Call(
358362
return nil, err
359363
}
360364

361-
return result.ReturnedData, err
365+
resultSummary := result.ResultSummary()
366+
if resultSummary.ErrorCode != 0 {
367+
if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted {
368+
return nil, errs.NewRevertError(resultSummary.ReturnedData)
369+
}
370+
return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage)
371+
}
372+
373+
return result.ReturnedData, nil
362374
}
363375

364376
func (e *EVM) EstimateGas(
365-
tx *types.LegacyTx,
377+
tx *types.DynamicFeeTx,
366378
from common.Address,
367379
height uint64,
368380
stateOverrides *ethTypes.StateOverride,
369381
) (uint64, error) {
382+
// Binary search the gas limit, as it may need to be higher than the amount used
383+
var (
384+
failingGasLimit uint64 // lowest-known gas limit where tx execution fails
385+
passingGasLimit uint64 // lowest-known gas limit where tx execution succeeds
386+
)
387+
// Determine the highest gas limit that can be used during the estimation.
388+
passingGasLimit = blockGasLimit
389+
if tx.Gas >= gethParams.TxGas {
390+
passingGasLimit = tx.Gas
391+
}
392+
tx.Gas = passingGasLimit
393+
// We first execute the transaction at the highest allowable gas limit,
394+
// since if this fails we can return error immediately.
370395
result, err := e.dryRunTx(tx, from, height, stateOverrides)
371396
if err != nil {
372397
return 0, err
373398
}
399+
resultSummary := result.ResultSummary()
400+
if resultSummary.ErrorCode != 0 {
401+
if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted {
402+
return 0, errs.NewRevertError(resultSummary.ReturnedData)
403+
}
404+
}
374405

375-
if result.Successful() {
376-
// As mentioned in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md#specification
377-
// Define "all but one 64th" of N as N - floor(N / 64).
378-
// If a call asks for more gas than the maximum allowed amount
379-
// (i.e. the total amount of gas remaining in the parent after subtracting
380-
// the gas cost of the call and memory expansion), do not return an OOG error;
381-
// instead, if a call asks for more gas than all but one 64th of the maximum
382-
// allowed amount, call with all but one 64th of the maximum allowed amount of
383-
// gas (this is equivalent to a version of EIP-901 plus EIP-1142).
384-
// CREATE only provides all but one 64th of the parent gas to the child call.
385-
result.GasConsumed = AddOne64th(result.GasConsumed)
406+
// For almost any transaction, the gas consumed by the unconstrained execution
407+
// above lower-bounds the gas limit required for it to succeed. One exception
408+
// is those that explicitly check gas remaining in order to execute within a
409+
// given limit, but we probably don't want to return the lowest possible gas
410+
// limit for these cases anyway.
411+
failingGasLimit = result.GasConsumed - 1
412+
413+
// There's a fairly high chance for the transaction to execute successfully
414+
// with gasLimit set to the first execution's GasConsumed + GasRefund.
415+
// Explicitly check that gas amount and use as a limit for the binary search.
416+
optimisticGasLimit := (result.GasConsumed + result.GasRefund + gethParams.CallStipend) * 64 / 63
417+
if optimisticGasLimit < passingGasLimit {
418+
tx.Gas = optimisticGasLimit
419+
result, err = e.dryRunTx(tx, from, height, stateOverrides)
420+
if err != nil {
421+
// This should not happen under normal conditions since if we make it this far the
422+
// transaction had run without error at least once before.
423+
return 0, err
424+
}
425+
resultSummary := result.ResultSummary()
426+
if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas {
427+
failingGasLimit = optimisticGasLimit
428+
} else {
429+
passingGasLimit = optimisticGasLimit
430+
}
431+
}
386432

387-
// Adding `gethParams.SstoreSentryGasEIP2200` is needed for this condition:
388-
// https://github.com/onflow/go-ethereum/blob/master/core/vm/operations_acl.go#L29-L32
389-
result.GasConsumed += gethParams.SstoreSentryGasEIP2200
433+
// Binary search for the smallest gas limit that allows the tx to execute successfully.
434+
for failingGasLimit+1 < passingGasLimit {
435+
// It is a bit pointless to return a perfect estimation, as changing
436+
// network conditions require the caller to bump it up anyway. Since
437+
// wallets tend to use 20-25% bump, allowing a small approximation
438+
// error is fine (as long as it's upwards).
439+
if float64(passingGasLimit-failingGasLimit)/float64(passingGasLimit) < estimateGasErrorRatio {
440+
break
441+
}
442+
mid := (passingGasLimit + failingGasLimit) / 2
443+
if mid > failingGasLimit*2 {
444+
// Most txs don't need much higher gas limit than their gas used, and most txs don't
445+
// require near the full block limit of gas, so the selection of where to bisect the
446+
// range here is skewed to favor the low side.
447+
mid = failingGasLimit * 2
448+
}
449+
tx.Gas = mid
450+
result, err = e.dryRunTx(tx, from, height, stateOverrides)
451+
if err != nil {
452+
return 0, err
453+
}
454+
resultSummary := result.ResultSummary()
455+
if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeOutOfGas {
456+
failingGasLimit = mid
457+
} else {
458+
passingGasLimit = mid
459+
}
460+
}
390461

391-
// Take into account any gas refunds, which are calculated only after
392-
// transaction execution.
393-
result.GasConsumed += result.GasRefund
462+
if tx.AccessList != nil {
463+
passingGasLimit += uint64(len(tx.AccessList)) * gethParams.TxAccessListAddressGas
464+
passingGasLimit += uint64(tx.AccessList.StorageKeys()) * gethParams.TxAccessListStorageKeyGas
394465
}
395466

396-
return result.GasConsumed, err
467+
return passingGasLimit, nil
397468
}
398469

399470
func (e *EVM) GetCode(
@@ -485,7 +556,7 @@ func (e *EVM) evmToCadenceHeight(height uint64) (uint64, error) {
485556
}
486557

487558
func (e *EVM) dryRunTx(
488-
tx *types.LegacyTx,
559+
tx *types.DynamicFeeTx,
489560
from common.Address,
490561
height uint64,
491562
stateOverrides *ethTypes.StateOverride,
@@ -545,14 +616,6 @@ func (e *EVM) dryRunTx(
545616
return nil, err
546617
}
547618

548-
resultSummary := result.ResultSummary()
549-
if resultSummary.ErrorCode != 0 {
550-
if resultSummary.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted {
551-
return nil, errs.NewRevertError(resultSummary.ReturnedData)
552-
}
553-
return nil, errs.NewFailedTransactionError(resultSummary.ErrorMessage)
554-
}
555-
556619
return result, nil
557620
}
558621

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)