From d9b288b8f5fc5306070086e7247b9c879b9182d5 Mon Sep 17 00:00:00 2001 From: chengzhinei Date: Mon, 24 Jun 2024 11:11:30 +0800 Subject: [PATCH] add EstimateGasOpt --- jsonrpc/apollo_xlayer.go | 2 + jsonrpc/config.go | 3 +- jsonrpc/endpoints_eth.go | 8 +- jsonrpc/endpoints_eth_xlayer.go | 13 +++ jsonrpc/mocks/mock_state.go | 5 ++ jsonrpc/types/interfaces.go | 1 + state/transaction.go | 153 ++++++++++++++++++++++++++++++++ 7 files changed, 183 insertions(+), 2 deletions(-) diff --git a/jsonrpc/apollo_xlayer.go b/jsonrpc/apollo_xlayer.go index ecb6dee4f6..156625f98e 100644 --- a/jsonrpc/apollo_xlayer.go +++ b/jsonrpc/apollo_xlayer.go @@ -12,6 +12,7 @@ type ApolloConfig struct { BatchRequestsEnabled bool BatchRequestsLimit uint GasLimitFactor float64 + EnableEstimateGasOpt bool DisableAPIs []string RateLimit RateLimitConfig DynamicGP DynamicGPConfig @@ -63,6 +64,7 @@ func UpdateConfig(apolloConfig Config) { getApolloConfig().BatchRequestsEnabled = apolloConfig.BatchRequestsEnabled getApolloConfig().BatchRequestsLimit = apolloConfig.BatchRequestsLimit getApolloConfig().GasLimitFactor = apolloConfig.GasLimitFactor + getApolloConfig().EnableEstimateGasOpt = apolloConfig.EnableEstimateGasOpt getApolloConfig().setDisableAPIs(apolloConfig.DisableAPIs) setRateLimit(apolloConfig.RateLimit) setApiAuth(apolloConfig.ApiAuthentication) diff --git a/jsonrpc/config.go b/jsonrpc/config.go index fe9f2b202f..53ec1e6053 100644 --- a/jsonrpc/config.go +++ b/jsonrpc/config.go @@ -87,7 +87,8 @@ type Config struct { // suggested gas limit: 100 // GasLimitFactor: 1.1 // gas limit = 110 - GasLimitFactor float64 `mapstructure:"GasLimitFactor"` + GasLimitFactor float64 `mapstructure:"GasLimitFactor"` + EnableEstimateGasOpt bool `mapstructure:"EnableEstimateGasOpt"` // DisableAPIs disable some API DisableAPIs []string `mapstructure:"DisableAPIs"` diff --git a/jsonrpc/endpoints_eth.go b/jsonrpc/endpoints_eth.go index 9b67934cdf..fa3f359ef5 100644 --- a/jsonrpc/endpoints_eth.go +++ b/jsonrpc/endpoints_eth.go @@ -207,7 +207,13 @@ func (e *EthEndpoints) EstimateGas(arg *types.TxArgs, blockArg *types.BlockNumbe t2 := time.Now() toTxTime := t2.Sub(t1) - gasEstimation, returnValue, err := e.state.EstimateGas(tx, sender, isGasFreeSender, blockToProcess, dbTx) + var gasEstimation uint64 + var returnValue []byte + if e.enableEstimateGasOpt() { + gasEstimation, returnValue, err = e.state.EstimateGasOpt(tx, sender, isGasFreeSender, blockToProcess, dbTx) + } else { + gasEstimation, returnValue, err = e.state.EstimateGas(tx, sender, isGasFreeSender, blockToProcess, dbTx) + } if errors.Is(err, runtime.ErrExecutionReverted) { data := make([]byte, len(returnValue)) copy(data, returnValue) diff --git a/jsonrpc/endpoints_eth_xlayer.go b/jsonrpc/endpoints_eth_xlayer.go index 2ab478108d..3676f7717c 100644 --- a/jsonrpc/endpoints_eth_xlayer.go +++ b/jsonrpc/endpoints_eth_xlayer.go @@ -281,6 +281,19 @@ func (e *EthEndpoints) getGasEstimationWithFactorXLayer(gasEstimation uint64) ui return gasEstimationWithFactor } +func (e *EthEndpoints) enableEstimateGasOpt() bool { + res := false + if getApolloConfig().Enable() { + getApolloConfig().RLock() + res = getApolloConfig().EnableEstimateGasOpt + getApolloConfig().RUnlock() + } else { + res = e.cfg.EnableEstimateGasOpt + } + + return res +} + // internal func (e *EthEndpoints) newPendingTransactionFilterXLayer(wsConn *concurrentWsConn) (interface{}, types.Error) { //XLayer handle diff --git a/jsonrpc/mocks/mock_state.go b/jsonrpc/mocks/mock_state.go index bc9070e6a6..5c93ab7272 100644 --- a/jsonrpc/mocks/mock_state.go +++ b/jsonrpc/mocks/mock_state.go @@ -151,6 +151,11 @@ func (_m *StateMock) EstimateGas(transaction *coretypes.Transaction, senderAddre return r0, r1, r2 } +// EstimateGasOpt provides a mock function with given fields: transaction, senderAddress, l2BlockNumber, dbTx +func (_m *StateMock) EstimateGasOpt(transaction *coretypes.Transaction, senderAddress common.Address, isGasFreeSender bool, l2BlockNumber *uint64, dbTx pgx.Tx) (uint64, []byte, error) { + return _m.EstimateGas(transaction, senderAddress, isGasFreeSender, l2BlockNumber, dbTx) +} + // GetBalance provides a mock function with given fields: ctx, address, root func (_m *StateMock) GetBalance(ctx context.Context, address common.Address, root common.Hash) (*big.Int, error) { ret := _m.Called(ctx, address, root) diff --git a/jsonrpc/types/interfaces.go b/jsonrpc/types/interfaces.go index c09dcdc0ce..f733df5eb2 100644 --- a/jsonrpc/types/interfaces.go +++ b/jsonrpc/types/interfaces.go @@ -39,6 +39,7 @@ type StateInterface interface { BeginStateTransaction(ctx context.Context) (pgx.Tx, error) DebugTransaction(ctx context.Context, transactionHash common.Hash, traceConfig state.TraceConfig, dbTx pgx.Tx) (*runtime.ExecutionResult, error) EstimateGas(transaction *types.Transaction, senderAddress common.Address, isGasFreeSender bool, l2BlockNumber *uint64, dbTx pgx.Tx) (uint64, []byte, error) + EstimateGasOpt(transaction *types.Transaction, senderAddress common.Address, isGasFreeSender bool, l2BlockNumber *uint64, dbTx pgx.Tx) (uint64, []byte, error) GetBalance(ctx context.Context, address common.Address, root common.Hash) (*big.Int, error) GetCode(ctx context.Context, address common.Address, root common.Hash) ([]byte, error) GetL2BlockByHash(ctx context.Context, hash common.Hash, dbTx pgx.Tx) (*state.L2Block, error) diff --git a/state/transaction.go b/state/transaction.go index ef62cc14fe..62960ad052 100644 --- a/state/transaction.go +++ b/state/transaction.go @@ -863,6 +863,7 @@ func (s *State) EstimateGas(transaction *types.Transaction, senderAddress common if lowEnd < estimationResult.gasUsed { lowEnd = estimationResult.gasUsed } + firstGasued := estimationResult.gasUsed optimisticGasLimit := (estimationResult.gasUsed + estimationResult.gasRefund + params.CallStipend) * 64 / 63 // nolint:gomnd if optimisticGasLimit < highEnd { @@ -927,10 +928,162 @@ func (s *State) EstimateGas(transaction *types.Transaction, senderAddress common } log.Infof("state-EstimateGas time. getBlock:%vms, getBatch:%vms, getForkID:%vms, getNonce:%vms, getEnd:%vms, internalGas:%vms, exec:%vms", getBlockTime.Milliseconds(), getBatchTime.Milliseconds(), getForkIDTime.Milliseconds(), getNonceTime.Milliseconds(), getEndTime.Milliseconds(), internalGasTime.Milliseconds(), time.Since(t6).Milliseconds()) + log.Infof("state-EstimateGas value. first gasUsed:%d, final gas", firstGasued, highEnd) return highEnd, nil, nil } +// EstimateGasOpt for a transaction +func (s *State) EstimateGasOpt(transaction *types.Transaction, senderAddress common.Address, isGasFreeSender bool, l2BlockNumber *uint64, dbTx pgx.Tx) (uint64, []byte, error) { + const ethTransferGas = 21000 + + ctx := context.Background() + + t0 := time.Now() + + var l2Block *L2Block + var err error + if l2BlockNumber == nil { + l2Block, err = s.GetLastL2Block(ctx, dbTx) + } else { + l2Block, err = s.GetL2BlockByNumber(ctx, *l2BlockNumber, dbTx) + } + if err != nil { + return 0, nil, err + } + + t1 := time.Now() + getBlockTime := t1.Sub(t0) + + batch, err := s.GetBatchByL2BlockNumber(ctx, l2Block.NumberU64(), dbTx) + if err != nil { + return 0, nil, err + } + + t2 := time.Now() + getBatchTime := t2.Sub(t1) + + forkID := s.GetForkIDByBatchNumber(batch.BatchNumber) + latestL2BlockNumber, err := s.GetLastL2BlockNumber(ctx, dbTx) + if err != nil { + return 0, nil, err + } + + t3 := time.Now() + getForkIDTime := t3.Sub(t2) + + loadedNonce, err := s.tree.GetNonce(ctx, senderAddress, l2Block.Root().Bytes()) + if err != nil { + return 0, nil, err + } + nonce := loadedNonce.Uint64() + + t4 := time.Now() + getNonceTime := t4.Sub(t3) + + highEnd := MaxTxGasLimit + + // if gas price is set, set the highEnd to the max amount + // of the account afford + isGasPriceSet := !isGasFreeSender && transaction.GasPrice().BitLen() != 0 + if isGasPriceSet { + senderBalance, err := s.tree.GetBalance(ctx, senderAddress, l2Block.Root().Bytes()) + if errors.Is(err, ErrNotFound) { + senderBalance = big.NewInt(0) + } else if err != nil { + return 0, nil, err + } + + availableBalance := new(big.Int).Set(senderBalance) + // check if the account has funds to pay the transfer value + if transaction.Value() != nil { + if transaction.Value().Cmp(availableBalance) > 0 { + return 0, nil, ErrInsufficientFundsForTransfer + } + + // deduct the value from the available balance + availableBalance.Sub(availableBalance, transaction.Value()) + } + + // Check the gas allowance for this account, make sure high end is capped to it + gasAllowance := new(big.Int).Div(availableBalance, transaction.GasPrice()) + if gasAllowance.IsUint64() && highEnd > gasAllowance.Uint64() { + log.Debugf("Gas estimation high-end capped by allowance [%d]", gasAllowance.Uint64()) + highEnd = gasAllowance.Uint64() + } + } + + // if the tx gas is set and it is smaller than the highEnd, + // limit the highEnd to the maximum allowed by the tx gas + if transaction.Gas() != 0 && transaction.Gas() < highEnd { + highEnd = transaction.Gas() + } + + // set start values for lowEnd and highEnd: + lowEnd, err := core.IntrinsicGas(transaction.Data(), transaction.AccessList(), s.isContractCreation(transaction), true, false, false) + if err != nil { + return 0, nil, err + } + + // if the intrinsic gas is the same as the constant value for eth transfer + // and the transaction has a receiver address + if lowEnd == ethTransferGas && transaction.To() != nil { + receiver := *transaction.To() + // check if the receiver address is not a smart contract + code, err := s.tree.GetCode(ctx, receiver, l2Block.Root().Bytes()) + if err != nil { + log.Warnf("error while getting code for address %v: %v", receiver.String(), err) + } else if len(code) == 0 { + // in case it is just an account, we can avoid the execution and return + // the transfer constant amount + return lowEnd, nil, nil + } + } + + t5 := time.Now() + getEndTime := t5.Sub(t4) + + // testTransaction runs the transaction with the specified gas value. + // it returns a status indicating if the transaction has failed, if it + // was reverted and the accompanying error + + // Check if the highEnd is a good value to make the transaction pass, if it fails we + // can return immediately. + log.Debugf("Estimate gas. Trying to execute TX with %v gas", highEnd) + var estimationResult *testGasEstimationResult + if forkID < FORKID_ETROG { + estimationResult, err = s.internalTestGasEstimationTransactionV1(ctx, batch, l2Block, latestL2BlockNumber, transaction, forkID, senderAddress, isGasFreeSender, highEnd, nonce, false) + } else { + estimationResult, err = s.internalTestGasEstimationTransactionV2(ctx, batch, l2Block, latestL2BlockNumber, transaction, forkID, senderAddress, isGasFreeSender, highEnd, nonce, false) + } + if err != nil { + return 0, nil, err + } + if estimationResult.failed { + if estimationResult.reverted { + return 0, estimationResult.returnValue, estimationResult.executionError + } + + if estimationResult.ooc { + return 0, nil, estimationResult.executionError + } + + // The transaction shouldn't fail, for whatever reason, at highEnd + return 0, nil, fmt.Errorf( + "gas required exceeds allowance (%d)", + highEnd, + ) + } + + t6 := time.Now() + internalGasTime := t6.Sub(t5) + + log.Infof("state-EstimateGas time. getBlock:%vms, getBatch:%vms, getForkID:%vms, getNonce:%vms, getEnd:%vms, internalGas:%vms", + getBlockTime.Milliseconds(), getBatchTime.Milliseconds(), getForkIDTime.Milliseconds(), getNonceTime.Milliseconds(), getEndTime.Milliseconds(), internalGasTime.Milliseconds()) + + return estimationResult.gasUsed, nil, nil +} + // internalTestGasEstimationTransactionV1 is used by the EstimateGas to test the tx execution // during the binary search process to define the gas estimation of a given tx for l2 blocks // before ETROG