Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/cosmos/gogoproto v1.4.10
github.com/cosmos/ibc-go/v7 v7.2.0
github.com/ethereum/go-ethereum v1.12.0
github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c
github.com/hyperledger-labs/yui-relayer v0.4.19
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
Expand Down Expand Up @@ -112,7 +113,6 @@ require (
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c // indirect
github.com/huandu/skiplist v1.2.0 // indirect
github.com/improbable-eng/grpc-web v0.15.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand Down
19 changes: 19 additions & 0 deletions pkg/relay/ethereum/cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cmd

import (
"github.com/hyperledger-labs/yui-relayer/config"
"github.com/spf13/cobra"
)

func EthereumCmd(ctx *config.Context) *cobra.Command {
cmd := &cobra.Command{
Use: "ethereum",
Short: "manage ethereum configurations",
}

cmd.AddCommand(
pendingCmd(ctx),
)

return cmd
}
74 changes: 74 additions & 0 deletions pkg/relay/ethereum/cmd/pending.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package cmd

import (
"fmt"
"github.com/datachainlab/ethereum-ibc-relay-chain/pkg/relay/ethereum"
"github.com/datachainlab/ethereum-ibc-relay-chain/pkg/relay/ethereum/cmd/pending"
"github.com/hyperledger-labs/yui-relayer/config"
"github.com/spf13/cobra"
)

func pendingCmd(ctx *config.Context) *cobra.Command {
cmd := &cobra.Command{
Use: "pending",
Short: "Manage ethereum pending transactions",
}

cmd.AddCommand(
showPendingTxCmd(ctx),
replacePendingTxCmd(ctx),
)

return cmd
}

func showPendingTxCmd(ctx *config.Context) *cobra.Command {
cmd := &cobra.Command{
Use: "show [chain-id]",
Aliases: []string{"list"},
Short: "Show minimum nonce pending transactions sent by relayer",
RunE: func(cmd *cobra.Command, args []string) error {
chain, err := ctx.Config.GetChain(args[0])
if err != nil {
return err
}
ethChain := chain.Chain.(*ethereum.Chain)
logic := pending.NewLogic(ethChain)
tx, err := logic.ShowPendingTx(cmd.Context())
if err != nil {
return err
}
json, err := tx.MarshalJSON()
if err != nil {
return err
}
fmt.Println(string(json))
return nil
},
}
return cmd
}

func replacePendingTxCmd(ctx *config.Context) *cobra.Command {
cmd := &cobra.Command{
Use: "replace [chain-id]",
Aliases: []string{"replace"},
Short: "Replace minimum nonce pending transaction sent by relayer",
RunE: func(cmd *cobra.Command, args []string) error {
chain, err := ctx.Config.GetChain(args[0])
if err != nil {
return err
}
ethChain := chain.Chain.(*ethereum.Chain)
logic := pending.NewLogic(ethChain)
tx, err := logic.ShowPendingTx(cmd.Context())
if err != nil {
return err
}
ethereum.GetModuleLogger().Info("Pending transaction found", "txHash", tx.Hash())

return logic.ReplacePendingTx(cmd.Context(), tx.Hash())
},
}
return cmd
}
256 changes: 256 additions & 0 deletions pkg/relay/ethereum/cmd/pending/logic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package pending

import (
"context"
"fmt"
"github.com/datachainlab/ethereum-ibc-relay-chain/pkg/relay/ethereum"
"github.com/datachainlab/ethereum-ibc-relay-chain/pkg/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/holiman/uint256"
"math/big"
"time"
)

type pendingTransactions map[uint64]*types.Transaction

func (p pendingTransactions) GetMinimumNonceTransaction() *types.Transaction {
minNonce := uint64(0)
var minValue *types.Transaction = nil
first := true
for k, v := range p {
if first || minNonce > k {
minNonce = k
minValue = v
first = false
}
}
return minValue
}

type txPoolContent struct {
Pending pendingTransactions `json:"pending"`
}

type gasFees struct {
GasPriceInc *big.Int
MaxGasPrice *big.Int
GasTipCapInc *big.Int
MaxGasTipCap *big.Int
GasFeeCapInc *big.Int
MaxGasFeeCap *big.Int
}

type Logic struct {
ethChain *ethereum.Chain
}

func NewLogic(ethChain *ethereum.Chain) *Logic {
return &Logic{
ethChain: ethChain,
}
}

func (m *Logic) ShowPendingTx(ctx context.Context) (*types.Transaction, error) {
txs, err := m.listPendingTx(ctx)
if err != nil {
return nil, err
}
tx := txs.GetMinimumNonceTransaction()
if tx == nil {
return tx, fmt.Errorf("no pending transaction was found")
}
return tx, nil
}

func (m *Logic) ReplacePendingTx(ctx context.Context, txHash common.Hash) error {
replaceConfig := m.ethChain.Config().ReplaceTxConfig
timer := time.NewTimer(time.Duration(replaceConfig.CheckInterval) * time.Second)
defer timer.Stop()

logger := ethereum.GetModuleLogger()
start := time.Now()
for {
select {
case <-timer.C:
tx, isPending, err := m.ethChain.Client().Client.TransactionByHash(ctx, txHash)
if err != nil {
return err
}
if !isPending {
logger.Info("tx is not pending", "txHash", tx.Hash())
return nil
}
if time.Now().After(start.Add(time.Duration(replaceConfig.PendingDurationToReplace) * time.Second)) {
logger.Info("try to replace pending transaction", "txHash", tx.Hash())
return m.replacePendingTx(ctx, tx)
}
logger.Info("tx is still pending", "txHash", tx.Hash())
timer.Reset(time.Duration(replaceConfig.CheckInterval) * time.Second)
}
}
}

func (m *Logic) listPendingTx(ctx context.Context) (pendingTransactions, error) {
fromAddress := m.ethChain.CallOpts(ctx, 0).From
var value *txPoolContent
if err := m.ethChain.Client().Client.Client().Call(&value, "txpool_contentFrom", fromAddress); err != nil {
return nil, err
}
if value == nil {
return nil, nil
}
return value.Pending, nil
}

func (m *Logic) replacePendingTx(ctx context.Context, tx *types.Transaction) error {
client := m.ethChain.Client()
cfg := m.ethChain.Config().ReplaceTxConfig
if cfg != nil {
return fmt.Errorf("\"replace_tx_config\" in chain config is required to replace tx")
}

gasToReplace, err := parseGasFee(cfg)
if err != nil {
return err
}

txData, err := m.copyTxData(tx, gasToReplace)
if err != nil {
return err
}
txOpts, err := m.ethChain.TxOpts(ctx)
if err != nil {
return err
}
newTx, err := txOpts.Signer(txOpts.From, types.NewTx(txData))
if err != nil {
return err
}
if err = client.Client.SendTransaction(ctx, newTx); err != nil {
return err
}

logger := ethereum.GetModuleLogger()
if receipt, revertReason, err := client.WaitForReceiptAndGet(ctx, newTx.Hash(), m.ethChain.Config().EnableDebugTrace); err != nil {
return fmt.Errorf("replace tx error: txHash=%s, err=%v", newTx.Hash(), err)
} else if receipt.Status == types.ReceiptStatusFailed {
return fmt.Errorf("replace tx failed: txHash=%s, revertReason=%s", newTx.Hash(), revertReason)
}
logger.Info("replace tx success", "txHash", newTx.Hash())
return nil
}

func (m *Logic) copyTxData(src *types.Transaction, gas *gasFees) (types.TxData, error) {
switch src.Type() {
case types.AccessListTxType:
gasPrice := add(src.GasPrice(), gas.GasPriceInc)
if gasPrice.Cmp(gas.MaxGasPrice) > 0 {
return nil, fmt.Errorf("gasPrice > max : AccessListTx value=%v,max=%v", gasPrice, gas.MaxGasPrice)
}
return &types.AccessListTx{
Nonce: src.Nonce(),
GasPrice: gasPrice,
Gas: src.Gas(),
To: src.To(),
Value: src.Value(),
Data: src.Data(),
}, nil
case types.DynamicFeeTxType:
gasTipCap := add(src.GasTipCap(), gas.GasTipCapInc)
if gasTipCap.Cmp(gas.MaxGasTipCap) > 0 {
return nil, fmt.Errorf("gasTipCap > max : DynamicFeeTx value=%v,max=%v", gasTipCap, gas.MaxGasTipCap)
}
gasFeeCap := add(src.GasFeeCap(), gas.GasFeeCapInc)
if gasFeeCap.Cmp(gas.MaxGasFeeCap) > 0 {
return nil, fmt.Errorf("gasFeeCap > max : DynamicFeeTx value=%v,max=%v", gasFeeCap, gas.MaxGasFeeCap)
}
return &types.DynamicFeeTx{
ChainID: src.ChainId(),
Nonce: src.Nonce(),
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Gas: src.Gas(),
To: src.To(),
Value: src.Value(),
Data: src.Data(),
AccessList: src.AccessList(),
}, nil
case types.BlobTxType:
gasTipCap := add(src.GasTipCap(), gas.GasTipCapInc)
if gasTipCap.Cmp(gas.MaxGasTipCap) > 0 {
return nil, fmt.Errorf("gasTipCap > max : BlobTx value=%v,max=%v", gasTipCap, gas.MaxGasTipCap)
}
gasFeeCap := add(src.GasFeeCap(), gas.GasFeeCapInc)
if gasFeeCap.Cmp(gas.MaxGasFeeCap) > 0 {
return nil, fmt.Errorf("gasFeeCap > max : BlobTx value=%v,max=%v", gasFeeCap, gas.MaxGasFeeCap)
}
return &types.BlobTx{
ChainID: uint256.MustFromBig(src.ChainId()),
Nonce: src.Nonce(),
GasTipCap: uint256.MustFromBig(gasTipCap),
GasFeeCap: uint256.MustFromBig(gasFeeCap),
Gas: src.Gas(),
To: src.To(),
Value: uint256.MustFromBig(src.Value()),
Data: src.Data(),
AccessList: src.AccessList(),
BlobFeeCap: uint256.MustFromBig(src.BlobGasFeeCap()),
BlobHashes: src.BlobHashes(),
}, nil

default:
gasPrice := add(src.GasPrice(), gas.GasPriceInc)
if gasPrice.Cmp(gas.MaxGasPrice) > 0 {
return nil, fmt.Errorf("gasPrice > max : LegacyTx value=%v,max=%v", gasPrice, gas.MaxGasPrice)
}
return &types.LegacyTx{
Nonce: src.Nonce(),
GasPrice: gasPrice,
Gas: src.Gas(),
To: src.To(),
Value: src.Value(),
Data: src.Data(),
}, nil
}
}

func add(x *big.Int, y *big.Int) *big.Int {
return new(big.Int).Add(x, y)
}

func parseGasFee(cfg *ethereum.ReplaceTxConfig) (*gasFees, error) {
gasPriceInc, err := utils.ParseEtherAmount(cfg.GasPriceInc)
if err != nil {
return nil, err
}
maxGasPrice, err := utils.ParseEtherAmount(cfg.MaxGasPrice)
if err != nil {
return nil, err
}
gasTipCapInc, err := utils.ParseEtherAmount(cfg.GasTipCapInc)
if err != nil {
return nil, err
}
maxGasTipCap, err := utils.ParseEtherAmount(cfg.MaxGasTipCap)
if err != nil {
return nil, err
}
gasFeeCapInc, err := utils.ParseEtherAmount(cfg.GasFeeCapInc)
if err != nil {
return nil, err
}
maxGasFeeCap, err := utils.ParseEtherAmount(cfg.MaxGasFeeCap)
if err != nil {
return nil, err
}

return &gasFees{
GasPriceInc: gasPriceInc,
MaxGasPrice: maxGasPrice,
GasTipCapInc: gasTipCapInc,
MaxGasTipCap: maxGasTipCap,
GasFeeCapInc: gasFeeCapInc,
MaxGasFeeCap: maxGasFeeCap,
}, nil
}
Loading