Skip to content

Commit

Permalink
Add gas param to TxOpts (#28)
Browse files Browse the repository at this point in the history
* add gas price

Signed-off-by: yoshidan <[email protected]>

* use eth_feeHistory

Signed-off-by: yoshidan <[email protected]>

* change protobuf

Signed-off-by: yoshidan <[email protected]>

* emulate gasTipCap as hardhat

Signed-off-by: yoshidan <[email protected]>

* tweak

Signed-off-by: yoshidan <[email protected]>

* add test

Signed-off-by: yoshidan <[email protected]>

* fix dynamic tx gas fee

Signed-off-by: yoshidan <[email protected]>

* support auto tx and gasLimit

Signed-off-by: yoshidan <[email protected]>

* add validation

Signed-off-by: yoshidan <[email protected]>

* retry feeHistory

Signed-off-by: yoshidan <[email protected]>

* refactor

Signed-off-by: yoshidan <[email protected]>

* refactor

Signed-off-by: yoshidan <[email protected]>

* add test

Signed-off-by: yoshidan <[email protected]>

---------

Signed-off-by: yoshidan <[email protected]>
  • Loading branch information
yoshidan authored Feb 5, 2024
1 parent d1da2f1 commit 1c384bf
Show file tree
Hide file tree
Showing 11 changed files with 1,080 additions and 49 deletions.
8 changes: 6 additions & 2 deletions pkg/relay/ethereum/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ func (chain *Chain) CallOpts(ctx context.Context, height int64) *bind.CallOpts {
return opts
}

func (chain *Chain) TxOpts(ctx context.Context) *bind.TransactOpts {
return &bind.TransactOpts{
func (chain *Chain) TxOpts(ctx context.Context) (*bind.TransactOpts, error) {
txOpts := &bind.TransactOpts{
From: chain.signer.Address(),
Signer: chain.signer.Sign,
}
if err := NewGasFeeCalculator(chain.client, &chain.config).Apply(ctx, txOpts); err != nil {
return nil, err
}
return txOpts, nil
}
83 changes: 83 additions & 0 deletions pkg/relay/ethereum/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/hex"
"errors"
"fmt"
"github.com/datachainlab/ethereum-ibc-relay-chain/pkg/utils"
"math/big"
"strings"

codectypes "github.com/cosmos/cosmos-sdk/codec/types"
Expand All @@ -16,6 +18,10 @@ var (
_ codectypes.UnpackInterfacesMessage = (*ChainConfig)(nil)
)

const TxTypeAuto = "auto"
const TxTypeLegacy = "legacy"
const TxTypeDynamic = "dynamic"

func (c ChainConfig) Build() (core.Chain, error) {
return NewChain(c)
}
Expand Down Expand Up @@ -60,9 +66,34 @@ func (c ChainConfig) Validate() error {
errs = append(errs, fmt.Errorf("config attribute \"allow_lc_functions\" is invalid: %v", err))
}
}
if c.TxType != TxTypeAuto && c.TxType != TxTypeLegacy && c.TxType != TxTypeDynamic {
errs = append(errs, fmt.Errorf("config attribute \"tx_type\" is invalid"))
}
if c.TxType == TxTypeDynamic {
if c.DynamicTxGasConfig == nil {
errs = append(errs, fmt.Errorf("config attribute \"dynamic_tx_gas_config\" is empty"))
} else {
if err := c.DynamicTxGasConfig.ValidateBasic(); err != nil {
errs = append(errs, fmt.Errorf("config attribute \"dynamic_tx_gas_config\" is invalid: %v", err))
}
}
}
return errors.Join(errs...)
}

func (f Fraction) Validate() error {
if f.Denominator == 0 {
return errors.New("zero is invalid fraction denominator")
}
return nil
}

// Mul multiplies `n` by `f` (this function mutates `n`)
func (f Fraction) Mul(n *big.Int) {
n.Mul(n, new(big.Int).SetUint64(f.Numerator))
n.Div(n, new(big.Int).SetUint64(f.Denominator))
}

func (c ChainConfig) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error {
if err := unpacker.UnpackAny(c.Signer, new(SignerConfig)); err != nil {
return fmt.Errorf("failed to unpack ChainConfig attribute \"signer\": %v", err)
Expand Down Expand Up @@ -131,3 +162,55 @@ func (lcf AllowLCFunctions) IsAllowed(address common.Address, selector [4]byte)
}
return false
}

func (gsc *DynamicTxGasConfig) ValidateBasic() error {
if gsc.LimitPriorityFeePerGas != "" {
if _, err := utils.ParseEtherAmount(gsc.LimitPriorityFeePerGas); err != nil {
return fmt.Errorf("config attribute \"limit_priority_fee_per_gas\" is invalid: %v", err)
}
}
if gsc.PriorityFeeRate == nil {
return fmt.Errorf("config attribute \"priority_fee_rate\" is nil")
}
if err := gsc.PriorityFeeRate.Validate(); err != nil {
return fmt.Errorf("config attribute \"priority_fee_rate\" is invalid: %v", err)
}
if gsc.LimitFeePerGas != "" {
if _, err := utils.ParseEtherAmount(gsc.LimitFeePerGas); err != nil {
return fmt.Errorf("config attribute \"limit_fee_per_gas\" is invalid: %v", err)
}
}
if gsc.BaseFeeRate == nil {
return fmt.Errorf("config attribute \"base_fee_rate\" is nil")
}
if err := gsc.BaseFeeRate.Validate(); err != nil {
return fmt.Errorf("config attribute \"base_fee_rate\" is invalid: %v", err)
}
if gsc.MaxRetryForFeeHistory == 0 {
return fmt.Errorf("config attribute \"max_retry_for_fee_history\" is zero")
}
if gsc.FeeHistoryRewardPercentile == 0 {
return fmt.Errorf("config attribute \"fee_history_reward_percentile\" is zero")
}
return nil
}

func (c *DynamicTxGasConfig) GetLimitPriorityFeePerGas() *big.Int {
if c.LimitPriorityFeePerGas == "" {
return new(big.Int)
} else if limit, err := utils.ParseEtherAmount(c.LimitPriorityFeePerGas); err != nil {
panic(err)
} else {
return limit
}
}

func (c *DynamicTxGasConfig) GetLimitFeePerGas() *big.Int {
if c.LimitFeePerGas == "" {
return new(big.Int)
} else if limit, err := utils.ParseEtherAmount(c.LimitFeePerGas); err != nil {
panic(err)
} else {
return limit
}
}
566 changes: 522 additions & 44 deletions pkg/relay/ethereum/config.pb.go

Large diffs are not rendered by default.

135 changes: 135 additions & 0 deletions pkg/relay/ethereum/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,138 @@ func genAddr(preimage string) common.Address {
copy(bz, h[:20])
return common.BytesToAddress(bz)
}

func TestDynamicTxConfig(t *testing.T) {
var cases = []struct {
input DynamicTxGasConfig
error bool
}{
{
input: DynamicTxGasConfig{
LimitFeePerGas: "1gwei",
LimitPriorityFeePerGas: "1gwei",
MaxRetryForFeeHistory: 1,
FeeHistoryRewardPercentile: 1,
PriorityFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
BaseFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
},
error: false,
},
{
input: DynamicTxGasConfig{
MaxRetryForFeeHistory: 1,
FeeHistoryRewardPercentile: 1,
PriorityFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
BaseFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
},
error: false,
},
{
input: DynamicTxGasConfig{
LimitFeePerGas: "1t",
MaxRetryForFeeHistory: 1,
FeeHistoryRewardPercentile: 1,
PriorityFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
BaseFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
},
error: true,
},
{
input: DynamicTxGasConfig{
LimitPriorityFeePerGas: "1t",
MaxRetryForFeeHistory: 1,
FeeHistoryRewardPercentile: 1,
PriorityFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
BaseFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
},
error: true,
},
{
input: DynamicTxGasConfig{
MaxRetryForFeeHistory: 0,
FeeHistoryRewardPercentile: 1,
PriorityFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
BaseFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
},
error: true,
},
{
input: DynamicTxGasConfig{
MaxRetryForFeeHistory: 1,
FeeHistoryRewardPercentile: 0,
PriorityFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
BaseFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
},
error: true,
},
{
input: DynamicTxGasConfig{
MaxRetryForFeeHistory: 1,
FeeHistoryRewardPercentile: 1,
BaseFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
},
error: true,
},
{
input: DynamicTxGasConfig{
MaxRetryForFeeHistory: 1,
FeeHistoryRewardPercentile: 0,
PriorityFeeRate: &Fraction{
Numerator: 1,
Denominator: 1,
},
},
error: true,
},
{
input: DynamicTxGasConfig{},
error: true,
},
}
for i, c := range cases {
t.Run(fmt.Sprint(i), func(t *testing.T) {
require := require.New(t)
err := c.input.ValidateBasic()
require.Equal(c.error, err != nil, err)
})
}
}
95 changes: 95 additions & 0 deletions pkg/relay/ethereum/gas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package ethereum

import (
"context"
"fmt"
"github.com/datachainlab/ethereum-ibc-relay-chain/pkg/client"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"math/big"
)

type GasFeeCalculator struct {
client *client.ETHClient
config *ChainConfig
}

func NewGasFeeCalculator(client *client.ETHClient, config *ChainConfig) *GasFeeCalculator {
return &GasFeeCalculator{
client: client,
config: config,
}
}

func (m *GasFeeCalculator) Apply(ctx context.Context, txOpts *bind.TransactOpts) error {
switch m.config.TxType {
case TxTypeLegacy:
gasPrice, err := m.client.SuggestGasPrice(ctx)
if err != nil {
return fmt.Errorf("failed to suggest gas price: %v", err)
}
txOpts.GasPrice = gasPrice
return nil
case TxTypeDynamic:
gasTipCap, gasFeeCap, err := m.feeHistory(ctx)
if err != nil {
return err
}
// GasTipCap = min(LimitPriorityFeePerGas, simulated_eth_maxPriorityFeePerGas * PriorityFeeRate)
m.config.DynamicTxGasConfig.PriorityFeeRate.Mul(gasTipCap)
if l := m.config.DynamicTxGasConfig.GetLimitPriorityFeePerGas(); l.Sign() > 0 && gasTipCap.Cmp(l) > 0 {
gasTipCap = l
}
// GasFeeCap = min(LimitFeePerGas, GasTipCap + BaseFee * BaseFeeRate)
m.config.DynamicTxGasConfig.BaseFeeRate.Mul(gasFeeCap)
gasFeeCap.Add(gasFeeCap, gasTipCap)
if l := m.config.DynamicTxGasConfig.GetLimitFeePerGas(); l.Sign() > 0 && gasFeeCap.Cmp(l) > 0 {
gasFeeCap = l
}

if gasFeeCap.Cmp(gasTipCap) < 0 {
return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap)
}
txOpts.GasFeeCap = gasFeeCap
txOpts.GasTipCap = gasTipCap
return nil
default:
return nil
}
}

func (m *GasFeeCalculator) feeHistory(ctx context.Context) (*big.Int, *big.Int, error) {
rewardPercentile := float64(m.config.DynamicTxGasConfig.FeeHistoryRewardPercentile)
maxRetry := m.config.DynamicTxGasConfig.MaxRetryForFeeHistory

latest, hErr := m.client.HeaderByNumber(ctx, nil)
if hErr != nil {
return nil, nil, fmt.Errorf("failed to get latest header: %v", hErr)
}
for i := uint32(0); i < maxRetry+1; i++ {
block := big.NewInt(0).Sub(latest.Number, big.NewInt(int64(i)))
history, err := m.client.FeeHistory(ctx, 1, block, []float64{rewardPercentile})
if err != nil {
return nil, nil, fmt.Errorf("failed to get feeHistory: %v", err)
}
if gasTipCap, baseFee, ok := getFeeInfo(history); ok {
return gasTipCap, baseFee, nil
}
}
return nil, nil, fmt.Errorf("no fee was found: latest=%v, maxRetry=%d", latest, maxRetry)
}

func getFeeInfo(v *ethereum.FeeHistory) (*big.Int, *big.Int, bool) {
if len(v.Reward) == 0 || len(v.Reward[0]) == 0 || v.Reward[0][0].Cmp(big.NewInt(0)) == 0 {
return nil, nil, false
}
gasTipCap := v.Reward[0][0]

if len(v.BaseFee) < 1 {
return nil, nil, false
}
// history.BaseFee[0] is baseFee (same as chain.Client().HeaderByNumber(ctx, nil).BaseFee)
// history.BaseFee[1] is nextBaseFee
baseFee := v.BaseFee[0]
return gasTipCap, baseFee, true
}
Loading

0 comments on commit 1c384bf

Please sign in to comment.