From 2211f3e97a7ada5b98c369eb31741f9bd6e588ec Mon Sep 17 00:00:00 2001 From: maskpp Date: Sun, 18 Feb 2024 15:54:16 +0800 Subject: [PATCH 01/25] add sender --- internal/sender/sender.go | 170 +++++++++++++++++++++++++++++++++ internal/sender/sender_test.go | 1 + 2 files changed, 171 insertions(+) create mode 100644 internal/sender/sender.go create mode 100644 internal/sender/sender_test.go diff --git a/internal/sender/sender.go b/internal/sender/sender.go new file mode 100644 index 000000000..4ebc8d9f8 --- /dev/null +++ b/internal/sender/sender.go @@ -0,0 +1,170 @@ +package sender + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + "strings" + "sync" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + + "github.com/taikoxyz/taiko-client/pkg/rpc" +) + +type Config struct { + // The gap number between a block be confirmed and the latest block. + Confirmations uint64 + // The maximum gas price can be used to send transaction. + MaxGasPrice uint64 +} + +type TxConfirm struct { + TxID uint64 + Confirm uint64 + Tx *types.Transaction + Receipt *types.Receipt +} + +type Sender struct { + ctx context.Context + cfg *Config + + chainID *big.Int + client *rpc.EthClient + + Opts *bind.TransactOpts + + unconfirmedTxs map[uint64]*types.Transaction + txConfirmCh map[uint64]chan<- *TxConfirm + + wg sync.WaitGroup + stopCh chan struct{} +} + +// NewSender returns a new instance of Sender. +func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ecdsa.PrivateKey) (*Sender, error) { + // Get the chain ID + chainID, err := client.ChainID(ctx) + if err != nil { + return nil, err + } + // Create a new transactor + opts, err := bind.NewKeyedTransactorWithChainID(priv, chainID) + if err != nil { + return nil, err + } + opts.NoSend = true + + sender := &Sender{ + ctx: ctx, + cfg: cfg, + chainID: chainID, + client: client, + Opts: opts, + } + + sender.wg.Add(1) + go sender.loop() + + return sender, nil +} + +func (s *Sender) Stop() { + close(s.stopCh) + s.wg.Wait() +} + +func (s *Sender) WaitTxConfirm(txID uint64) <-chan *TxConfirm { + s.txConfirmCh[txID] <- s.updateGasTipGasFee[txID] + return ch +} + +// SendTransaction sends a transaction to the target address. +func (s *Sender) SendTransaction(tx *types.Transaction) error { + baseTx := &types.DynamicFeeTx{ + ChainID: s.chainID, + To: tx.To(), + Nonce: s.Opts.Nonce.Uint64(), + GasFeeCap: new(big.Int).Set(s.Opts.GasFeeCap), + GasTipCap: new(big.Int).Set(s.Opts.GasTipCap), + Gas: tx.Gas(), + Value: tx.Value(), + Data: tx.Data(), + } + rawTx, err := s.Opts.Signer(s.Opts.From, types.NewTx(baseTx)) + if err != nil { + return err + } + // Send the transaction + err = s.client.SendTransaction(s.ctx, rawTx) + // Check if the error is nonce too low or nonce too high + if err == nil || + strings.Contains(err.Error(), "nonce too low") || + strings.Contains(err.Error(), "nonce too high") { + + return nil + } + + return err +} + +func (s *Sender) setNonce() error { + // Get the nonce + nonce, err := s.client.PendingNonceAt(s.ctx, s.Opts.From) + if err != nil { + return err + } + s.Opts.Nonce = new(big.Int).SetUint64(nonce) + + return nil +} + +func (s *Sender) updateGasTipGasFee(head *types.Header) error { + // Get the gas tip cap + gasTipCap, err := s.client.SuggestGasTipCap(s.ctx) + if err != nil { + return err + } + s.Opts.GasTipCap = gasTipCap + + // Get the gas fee cap + gasFeeCap := new(big.Int).Add(gasTipCap, new(big.Int).Mul(head.BaseFee, big.NewInt(2))) + // Check if the gas fee cap is less than the gas tip cap + if gasFeeCap.Cmp(gasTipCap) < 0 { + return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap) + } + s.Opts.GasFeeCap = gasFeeCap + + return nil +} + +func (s *Sender) loop() { + defer s.wg.Done() + + headCh := make(chan *types.Header, 3) + // Subscribe new head + sub, err := s.client.SubscribeNewHead(s.ctx, headCh) + if err != nil { + log.Crit("failed to subscribe new head", "err", err) + } + defer sub.Unsubscribe() + + for { + select { + case head := <-headCh: + // Update the gas tip and gas fee + err = s.updateGasTipGasFee(head) + if err != nil { + log.Warn("failed to update gas tip and gas fee", "err", err) + } + case <-s.ctx.Done(): + return + case <-s.stopCh: + return + } + } +} diff --git a/internal/sender/sender_test.go b/internal/sender/sender_test.go new file mode 100644 index 000000000..0e314aed0 --- /dev/null +++ b/internal/sender/sender_test.go @@ -0,0 +1 @@ +package sender From 1ffb231779e0dcb711e49bbd1957ebccb32825c2 Mon Sep 17 00:00:00 2001 From: maskpp Date: Tue, 20 Feb 2024 12:11:28 +0800 Subject: [PATCH 02/25] update sender --- go.mod | 3 +- go.sum | 2 + internal/sender/sender.go | 146 ++++++++++++++++++++++----------- internal/sender/sender_test.go | 70 +++++++++++++++- 4 files changed, 169 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index eb724bae7..5884e989f 100644 --- a/go.mod +++ b/go.mod @@ -12,12 +12,14 @@ require ( github.com/joho/godotenv v1.5.1 github.com/labstack/echo/v4 v4.11.1 github.com/modern-go/reflect2 v1.0.2 + github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/prysmaticlabs/prysm/v4 v4.2.0 github.com/stretchr/testify v1.8.4 github.com/swaggo/swag v1.16.2 github.com/urfave/cli/v2 v2.25.7 golang.org/x/sync v0.5.0 + modernc.org/mathutil v1.6.0 ) require ( @@ -217,7 +219,6 @@ require ( k8s.io/klog/v2 v2.80.0 // indirect k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect lukechampine.com/blake3 v1.2.1 // indirect - modernc.org/mathutil v1.6.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.0.2 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/go.sum b/go.sum index 1ac517dde..286174734 100644 --- a/go.sum +++ b/go.sum @@ -797,6 +797,8 @@ github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTm github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= +github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= diff --git a/internal/sender/sender.go b/internal/sender/sender.go index 74b1a1579..abc6f43ca 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -10,9 +10,12 @@ import ( "sync/atomic" "time" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + cmap "github.com/orcaman/concurrent-map/v2" "modernc.org/mathutil" "github.com/taikoxyz/taiko-client/pkg/rpc" @@ -23,18 +26,20 @@ type Config struct { Confirmations uint64 // The maximum gas price can be used to send transaction. MaxGasPrice uint64 - gasRate uint64 + // The gas rate to increase the gas price. + GasRate uint64 // The maximum number of pending transactions. - maxPendTxs int - - retryTimes uint64 + MaxPendTxs int + // The maximum retry times to send transaction. + RetryTimes uint64 } type TxConfirm struct { retryTimes uint64 confirms uint64 - TxID uint64 + TxID string + baseTx *types.DynamicFeeTx Tx *types.Transaction Receipt *types.Receipt @@ -50,12 +55,13 @@ type Sender struct { header *types.Header client *rpc.EthClient - Opts *bind.TransactOpts + opts *bind.TransactOpts globalTxID uint64 - unconfirmedTxs map[uint64]*TxConfirm - txConfirmCh map[uint64]chan *TxConfirm + unconfirmedTxs cmap.ConcurrentMap[string, *TxConfirm] //uint64]*TxConfirm + txConfirmCh cmap.ConcurrentMap[string, chan *TxConfirm] + mu sync.Mutex wg sync.WaitGroup stopCh chan struct{} } @@ -86,11 +92,18 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec chainID: chainID, header: header, client: client, - Opts: opts, - unconfirmedTxs: make(map[uint64]*TxConfirm, cfg.maxPendTxs), - txConfirmCh: make(map[uint64]chan *TxConfirm, cfg.maxPendTxs), + opts: opts, + unconfirmedTxs: cmap.New[*TxConfirm](), + txConfirmCh: cmap.New[chan *TxConfirm](), stopCh: make(chan struct{}), } + // Set the nonce + sender.setNonce() + // Update the gas tip and gas fee. + err = sender.updateGasTipGasFee(header) + if err != nil { + return nil, err + } sender.wg.Add(1) go sender.loop() @@ -104,17 +117,41 @@ func (s *Sender) Stop() { } // WaitTxConfirm returns a channel to receive the transaction confirmation. -func (s *Sender) WaitTxConfirm(txID uint64) (bool, <-chan *TxConfirm) { - confirmCh, ok := s.txConfirmCh[txID] - return ok, confirmCh +func (s *Sender) WaitTxConfirm(txID string) (<-chan *TxConfirm, bool) { + confirmCh, ok := s.txConfirmCh.Get(txID) + return confirmCh, ok +} + +// SendRaw sends a transaction to the target address. +func (s *Sender) SendRaw(target *common.Address, value *big.Int, data []byte) (string, error) { + gasLimit, err := s.client.EstimateGas(s.ctx, ethereum.CallMsg{ + From: s.opts.From, + To: target, + Value: value, + Data: data, + GasFeeCap: s.opts.GasFeeCap, + GasTipCap: s.opts.GasTipCap, + }) + if err != nil { + return "", err + } + return s.SendTransaction(types.NewTx(&types.DynamicFeeTx{ + ChainID: s.chainID, + To: target, + GasFeeCap: s.opts.GasFeeCap, + GasTipCap: s.opts.GasTipCap, + Gas: gasLimit, + Value: value, + Data: data, + })) } // SendTransaction sends a transaction to the target address. -func (s *Sender) SendTransaction(tx *types.Transaction) (uint64, error) { - if len(s.unconfirmedTxs) >= s.maxPendTxs { - return 0, fmt.Errorf("too many pending transactions") +func (s *Sender) SendTransaction(tx *types.Transaction) (string, error) { + if s.unconfirmedTxs.Count() >= s.MaxPendTxs { + return "", fmt.Errorf("too many pending transactions") } - txID := atomic.AddUint64(&s.globalTxID, 1) + txID := fmt.Sprint(atomic.AddUint64(&s.globalTxID, 1)) confirmTx := &TxConfirm{ TxID: txID, baseTx: &types.DynamicFeeTx{ @@ -130,16 +167,20 @@ func (s *Sender) SendTransaction(tx *types.Transaction) (uint64, error) { } err := s.sendTx(confirmTx) if err != nil { - return 0, err + log.Error("failed to send transaction", "tx_id", txID, "tx_hash", tx.Hash().String(), "err", err) + return "", err } // Add the transaction to the unconfirmed transactions - s.unconfirmedTxs[txID] = confirmTx - s.txConfirmCh[txID] = make(chan *TxConfirm, 1) + s.unconfirmedTxs.Set(txID, confirmTx) + s.txConfirmCh.Set(txID, make(chan *TxConfirm, 1)) return txID, nil } func (s *Sender) sendTx(confirmTx *TxConfirm) error { + s.mu.Lock() + defer s.mu.Unlock() + confirmTx.retryTimes++ tx := confirmTx.baseTx baseTx := &types.DynamicFeeTx{ @@ -151,31 +192,28 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { Value: tx.Value, Data: tx.Data, } - var ( - rawTx *types.Transaction - ) - // Try 3 retryTimes if nonce is not correct. + // Try 3 RetryTimes if nonce is not correct. for i := 0; i < 3; i++ { - nonce, err := s.client.NonceAt(s.ctx, s.Opts.From, nil) + baseTx.Nonce = s.opts.Nonce.Uint64() + rawTx, err := s.opts.Signer(s.opts.From, types.NewTx(baseTx)) if err != nil { return err } - baseTx.Nonce = nonce - rawTx, err = s.Opts.Signer(s.Opts.From, types.NewTx(baseTx)) - if err != nil { - return err - } - confirmTx.Error = s.client.SendTransaction(s.ctx, rawTx) + err = s.client.SendTransaction(s.ctx, rawTx) + confirmTx.Error = err // Check if the error is nonce too low or nonce too high - if strings.Contains(err.Error(), "nonce too low") || - strings.Contains(err.Error(), "nonce too high") { - log.Warn("nonce is not correct, retry to send transaction", "tx_hash", rawTx.Hash().String(), "nonce", nonce, "err", err) - time.Sleep(time.Millisecond * 500) - continue - } else if err != nil { + if err != nil { + if strings.Contains(err.Error(), "nonce too low") || + strings.Contains(err.Error(), "nonce too high") { + s.setNonce() + log.Warn("nonce is not correct, retry to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) + time.Sleep(time.Millisecond * 500) + continue + } log.Error("failed to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) return err } + s.opts.Nonce = big.NewInt(s.opts.Nonce.Int64() + 1) confirmTx.baseTx = baseTx confirmTx.Tx = rawTx @@ -186,24 +224,34 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { } func (s *Sender) resendTx(confirmTx *TxConfirm) { - if confirmTx.retryTimes >= s.retryTimes { - // TODO: add the transaction to the failed transactions + if confirmTx.retryTimes >= s.RetryTimes { + s.unconfirmedTxs.Remove(confirmTx.TxID) + s.txConfirmCh.Remove(confirmTx.TxID) + return } // Increase the gas price. gas := confirmTx.baseTx.Gas - confirmTx.baseTx.Gas = mathutil.MinUint64(s.MaxGasPrice, gas+gas/s.gasRate) + confirmTx.baseTx.Gas = mathutil.MinUint64(s.MaxGasPrice, gas+gas/s.GasRate) confirmTx.Error = s.sendTx(confirmTx) } +func (s *Sender) setNonce() { + nonce, err := s.client.PendingNonceAt(s.ctx, s.opts.From) + if err != nil { + log.Warn("failed to get the nonce", "from", s.opts.From, "err", err) + return + } + s.opts.Nonce = new(big.Int).SetUint64(nonce) +} + func (s *Sender) updateGasTipGasFee(head *types.Header) error { // Get the gas tip cap gasTipCap, err := s.client.SuggestGasTipCap(s.ctx) if err != nil { return err } - s.Opts.GasTipCap = gasTipCap // Get the gas fee cap gasFeeCap := new(big.Int).Add(gasTipCap, new(big.Int).Mul(head.BaseFee, big.NewInt(2))) @@ -211,7 +259,8 @@ func (s *Sender) updateGasTipGasFee(head *types.Header) error { if gasFeeCap.Cmp(gasTipCap) < 0 { return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap) } - s.Opts.GasFeeCap = gasFeeCap + s.opts.GasTipCap = gasTipCap + s.opts.GasFeeCap = gasFeeCap return nil } @@ -225,7 +274,7 @@ func (s *Sender) loop() { for { select { case <-tick.C: - if len(s.unconfirmedTxs) == 0 { + if s.unconfirmedTxs.Count() == 0 { continue } head, err := s.client.HeaderByNumber(s.ctx, nil) @@ -255,7 +304,7 @@ func (s *Sender) loop() { } func (s *Sender) checkPendingTransactions() { - for txID, txConfirm := range s.unconfirmedTxs { + for txID, txConfirm := range s.unconfirmedTxs.Items() { if txConfirm.Error != nil { s.resendTx(txConfirm) continue @@ -273,13 +322,14 @@ func (s *Sender) checkPendingTransactions() { txConfirm.confirms = s.header.Number.Uint64() - txConfirm.Receipt.BlockNumber.Uint64() // Check if the transaction is confirmed if s.header.Number.Uint64()-txConfirm.confirms >= s.Confirmations { + confirmCh, _ := s.txConfirmCh.Get(txID) select { - case s.txConfirmCh[txID] <- txConfirm: + case confirmCh <- txConfirm: default: } // Remove the transaction from the unconfirmed transactions - delete(s.unconfirmedTxs, txID) - delete(s.txConfirmCh, txID) + s.unconfirmedTxs.Remove(txID) + s.txConfirmCh.Remove(txID) } } } diff --git a/internal/sender/sender_test.go b/internal/sender/sender_test.go index 1ee118fb1..37713dab8 100644 --- a/internal/sender/sender_test.go +++ b/internal/sender/sender_test.go @@ -1,8 +1,72 @@ package sender_test -import "testing" +import ( + "context" + "math/big" + "os" + "runtime" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "golang.org/x/sync/errgroup" + + "github.com/taikoxyz/taiko-client/internal/sender" + "github.com/taikoxyz/taiko-client/internal/utils" + "github.com/taikoxyz/taiko-client/pkg/rpc" +) func TestSender(t *testing.T) { - t.Parallel() - t.Skip("Not implemented.") + utils.LoadEnv() + + ctx := context.Background() + + client, err := rpc.NewEthClient(ctx, os.Getenv("L1_NODE_WS_ENDPOINT"), time.Second*10) + assert.NoError(t, err) + + priv, err := crypto.ToECDSA(common.FromHex(os.Getenv("L1_PROPOSER_PRIVATE_KEY"))) + assert.NoError(t, err) + + send, err := sender.NewSender(ctx, &sender.Config{ + Confirmations: 1, + MaxGasPrice: 1000000000000, + GasRate: 10, + MaxPendTxs: 10, + RetryTimes: 3, + }, client, priv) + assert.NoError(t, err) + defer send.Stop() + + var ( + batchSize = 10 + eg errgroup.Group + confirmsCh = make([]<-chan *sender.TxConfirm, 0, batchSize) + ) + eg.SetLimit(runtime.NumCPU()) + for i := 0; i < batchSize; i++ { + i := i + eg.Go(func() error { + addr := common.BigToAddress(big.NewInt(int64(i))) + txID, err := send.SendRaw(&addr, big.NewInt(1), nil) + if err == nil { + confirmCh, _ := send.WaitTxConfirm(txID) + confirmsCh = append(confirmsCh, confirmCh) + } + return err + }) + } + err = eg.Wait() + assert.NoError(t, err) + + for len(confirmsCh) > 0 { + confirmCh := confirmsCh[0] + select { + case confirm := <-confirmCh: + assert.NoError(t, confirm.Error) + confirmsCh = confirmsCh[1:] + default: + } + } } From a122b25761ce935adf4baf3d6870617012b41acf Mon Sep 17 00:00:00 2001 From: maskpp Date: Wed, 21 Feb 2024 11:19:09 +0800 Subject: [PATCH 03/25] update sender --- internal/sender/sender.go | 212 ++++++++++++++++++--------------- internal/sender/sender_test.go | 148 +++++++++++++++++++---- 2 files changed, 243 insertions(+), 117 deletions(-) diff --git a/internal/sender/sender.go b/internal/sender/sender.go index abc6f43ca..dbf110004 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -10,13 +10,11 @@ import ( "sync/atomic" "time" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" cmap "github.com/orcaman/concurrent-map/v2" - "modernc.org/mathutil" "github.com/taikoxyz/taiko-client/pkg/rpc" ) @@ -25,7 +23,7 @@ type Config struct { // The gap number between a block be confirmed and the latest block. Confirmations uint64 // The maximum gas price can be used to send transaction. - MaxGasPrice uint64 + MaxGasPrice *big.Int // The gas rate to increase the gas price. GasRate uint64 // The maximum number of pending transactions. @@ -35,7 +33,7 @@ type Config struct { } type TxConfirm struct { - retryTimes uint64 + RetryTimes uint64 confirms uint64 TxID string @@ -51,11 +49,11 @@ type Sender struct { ctx context.Context *Config - chainID *big.Int - header *types.Header - client *rpc.EthClient + header *types.Header + client *rpc.EthClient - opts *bind.TransactOpts + ChainID *big.Int + Opts *bind.TransactOpts globalTxID uint64 unconfirmedTxs cmap.ConcurrentMap[string, *TxConfirm] //uint64]*TxConfirm @@ -89,16 +87,16 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec sender := &Sender{ ctx: ctx, Config: cfg, - chainID: chainID, + ChainID: chainID, header: header, client: client, - opts: opts, + Opts: opts, unconfirmedTxs: cmap.New[*TxConfirm](), txConfirmCh: cmap.New[chan *TxConfirm](), stopCh: make(chan struct{}), } // Set the nonce - sender.setNonce() + sender.adjustNonce(nil) // Update the gas tip and gas fee. err = sender.updateGasTipGasFee(header) if err != nil { @@ -123,24 +121,14 @@ func (s *Sender) WaitTxConfirm(txID string) (<-chan *TxConfirm, bool) { } // SendRaw sends a transaction to the target address. -func (s *Sender) SendRaw(target *common.Address, value *big.Int, data []byte) (string, error) { - gasLimit, err := s.client.EstimateGas(s.ctx, ethereum.CallMsg{ - From: s.opts.From, - To: target, - Value: value, - Data: data, - GasFeeCap: s.opts.GasFeeCap, - GasTipCap: s.opts.GasTipCap, - }) - if err != nil { - return "", err - } +func (s *Sender) SendRaw(nonce uint64, target *common.Address, value *big.Int, data []byte) (string, error) { return s.SendTransaction(types.NewTx(&types.DynamicFeeTx{ - ChainID: s.chainID, + ChainID: s.ChainID, To: target, - GasFeeCap: s.opts.GasFeeCap, - GasTipCap: s.opts.GasTipCap, - Gas: gasLimit, + Nonce: nonce, + GasFeeCap: s.Opts.GasFeeCap, + GasTipCap: s.Opts.GasTipCap, + Gas: 1000000, Value: value, Data: data, })) @@ -155,10 +143,11 @@ func (s *Sender) SendTransaction(tx *types.Transaction) (string, error) { confirmTx := &TxConfirm{ TxID: txID, baseTx: &types.DynamicFeeTx{ - ChainID: s.chainID, + ChainID: s.ChainID, To: tx.To(), - GasFeeCap: tx.GasFeeCap(), - GasTipCap: tx.GasTipCap(), + Nonce: tx.Nonce(), + GasFeeCap: s.Opts.GasFeeCap, + GasTipCap: s.Opts.GasTipCap, Gas: tx.Gas(), Value: tx.Value(), Data: tx.Data(), @@ -166,7 +155,7 @@ func (s *Sender) SendTransaction(tx *types.Transaction) (string, error) { Tx: tx, } err := s.sendTx(confirmTx) - if err != nil { + if err != nil && !strings.Contains(err.Error(), "replacement transaction") { log.Error("failed to send transaction", "tx_id", txID, "tx_hash", tx.Hash().String(), "err", err) return "", err } @@ -181,69 +170,61 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { s.mu.Lock() defer s.mu.Unlock() - confirmTx.retryTimes++ - tx := confirmTx.baseTx - baseTx := &types.DynamicFeeTx{ - ChainID: s.chainID, - To: tx.To, - GasFeeCap: tx.GasFeeCap, - GasTipCap: tx.GasTipCap, - Gas: tx.Gas, - Value: tx.Value, - Data: tx.Data, - } + baseTx := confirmTx.baseTx + // Try 3 RetryTimes if nonce is not correct. - for i := 0; i < 3; i++ { - baseTx.Nonce = s.opts.Nonce.Uint64() - rawTx, err := s.opts.Signer(s.opts.From, types.NewTx(baseTx)) - if err != nil { - return err + rawTx, err := s.Opts.Signer(s.Opts.From, types.NewTx(baseTx)) + if err != nil { + return err + } + confirmTx.Tx = rawTx + err = s.client.SendTransaction(s.ctx, rawTx) + confirmTx.Error = err + // Check if the error is nonce too low. + if err != nil { + if strings.Contains(err.Error(), "nonce too low") { + s.adjustNonce(baseTx) + log.Warn("nonce is not correct, retry to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) + return nil } - err = s.client.SendTransaction(s.ctx, rawTx) - confirmTx.Error = err - // Check if the error is nonce too low or nonce too high - if err != nil { - if strings.Contains(err.Error(), "nonce too low") || - strings.Contains(err.Error(), "nonce too high") { - s.setNonce() - log.Warn("nonce is not correct, retry to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) - time.Sleep(time.Millisecond * 500) - continue - } - log.Error("failed to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) - return err + if err.Error() == "replacement transaction underpriced" { + s.adjustGas(baseTx) + log.Warn("replacement transaction underpriced", "tx_hash", rawTx.Hash().String(), "err", err) + return nil } - s.opts.Nonce = big.NewInt(s.opts.Nonce.Int64() + 1) - - confirmTx.baseTx = baseTx - confirmTx.Tx = rawTx - break + log.Error("failed to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) + return err } + s.Opts.Nonce = big.NewInt(s.Opts.Nonce.Int64() + 1) return nil } -func (s *Sender) resendTx(confirmTx *TxConfirm) { - if confirmTx.retryTimes >= s.RetryTimes { - s.unconfirmedTxs.Remove(confirmTx.TxID) - s.txConfirmCh.Remove(confirmTx.TxID) - return +func (s *Sender) adjustGas(baseTx *types.DynamicFeeTx) { + rate := big.NewInt(int64(100 + s.GasRate)) + baseTx.GasFeeCap = new(big.Int).Mul(baseTx.GasFeeCap, rate) + baseTx.GasFeeCap.Div(baseTx.GasFeeCap, big.NewInt(100)) + if s.MaxGasPrice.Cmp(baseTx.GasFeeCap) < 0 { + baseTx.GasFeeCap = new(big.Int).Set(s.MaxGasPrice) } - // Increase the gas price. - gas := confirmTx.baseTx.Gas - confirmTx.baseTx.Gas = mathutil.MinUint64(s.MaxGasPrice, gas+gas/s.GasRate) - - confirmTx.Error = s.sendTx(confirmTx) + baseTx.GasTipCap = new(big.Int).Mul(baseTx.GasTipCap, rate) + baseTx.GasTipCap.Div(baseTx.GasTipCap, big.NewInt(100)) + if baseTx.GasTipCap.Cmp(baseTx.GasFeeCap) > 0 { + baseTx.GasTipCap = new(big.Int).Set(baseTx.GasFeeCap) + } } -func (s *Sender) setNonce() { - nonce, err := s.client.PendingNonceAt(s.ctx, s.opts.From) +func (s *Sender) adjustNonce(baseTx *types.DynamicFeeTx) { + nonce, err := s.client.NonceAt(s.ctx, s.Opts.From, nil) if err != nil { - log.Warn("failed to get the nonce", "from", s.opts.From, "err", err) + log.Warn("failed to get the nonce", "from", s.Opts.From, "err", err) return } - s.opts.Nonce = new(big.Int).SetUint64(nonce) + s.Opts.Nonce = new(big.Int).SetUint64(nonce) + if baseTx != nil { + baseTx.Nonce = nonce + } } func (s *Sender) updateGasTipGasFee(head *types.Header) error { @@ -259,8 +240,13 @@ func (s *Sender) updateGasTipGasFee(head *types.Header) error { if gasFeeCap.Cmp(gasTipCap) < 0 { return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap) } - s.opts.GasTipCap = gasTipCap - s.opts.GasFeeCap = gasFeeCap + if gasFeeCap.Cmp(s.MaxGasPrice) > 0 { + gasFeeCap = new(big.Int).Set(s.MaxGasPrice) + gasTipCap = new(big.Int).Set(s.MaxGasPrice) + } + + s.Opts.GasTipCap = gasTipCap + s.Opts.GasFeeCap = gasFeeCap return nil } @@ -268,15 +254,22 @@ func (s *Sender) updateGasTipGasFee(head *types.Header) error { func (s *Sender) loop() { defer s.wg.Done() - tick := time.NewTicker(time.Second * 3) - defer tick.Stop() + tickHead := time.NewTicker(time.Second * 3) + defer tickHead.Stop() + + tickResend := time.NewTicker(time.Second * 2) + defer tickResend.Stop() for { select { - case <-tick.C: + case <-tickResend.C: if s.unconfirmedTxs.Count() == 0 { continue } + s.resendTransaction() + // Check the unconfirmed transactions + s.checkPendingTransactions() + case <-tickHead.C: head, err := s.client.HeaderByNumber(s.ctx, nil) if err != nil { log.Warn("failed to get the latest header", "err", err) @@ -292,9 +285,6 @@ func (s *Sender) loop() { if err != nil { log.Warn("failed to update gas tip and gas fee", "err", err) } - // Check the unconfirmed transactions - s.checkPendingTransactions() - case <-s.ctx.Done(): return case <-s.stopCh: @@ -303,16 +293,39 @@ func (s *Sender) loop() { } } +func (s *Sender) resendTransaction() { + for txID, txConfirm := range s.unconfirmedTxs.Items() { + if txConfirm.Error == nil { + continue + } + txConfirm.RetryTimes++ + if s.RetryTimes != 0 && txConfirm.RetryTimes >= s.RetryTimes { + s.unconfirmedTxs.Remove(txID) + s.txConfirmCh.Remove(txID) + continue + } + _ = s.sendTx(txConfirm) + } +} + func (s *Sender) checkPendingTransactions() { for txID, txConfirm := range s.unconfirmedTxs.Items() { if txConfirm.Error != nil { - s.resendTx(txConfirm) continue } if txConfirm.Receipt == nil { - // Get the transaction receipt + // Ignore the transaction if it is pending. + _, isPending, err := s.client.TransactionByHash(s.ctx, txConfirm.Tx.Hash()) + if err != nil || isPending { + continue + } + // Get the transaction receipt. receipt, err := s.client.TransactionReceipt(s.ctx, txConfirm.Tx.Hash()) if err != nil { + if err.Error() == "not found" { + txConfirm.Error = err + s.releaseConfirmCh(txID) + } log.Warn("failed to get the transaction receipt", "tx_hash", txConfirm.Tx.Hash().String(), "err", err) continue } @@ -322,14 +335,19 @@ func (s *Sender) checkPendingTransactions() { txConfirm.confirms = s.header.Number.Uint64() - txConfirm.Receipt.BlockNumber.Uint64() // Check if the transaction is confirmed if s.header.Number.Uint64()-txConfirm.confirms >= s.Confirmations { - confirmCh, _ := s.txConfirmCh.Get(txID) - select { - case confirmCh <- txConfirm: - default: - } - // Remove the transaction from the unconfirmed transactions - s.unconfirmedTxs.Remove(txID) - s.txConfirmCh.Remove(txID) + s.releaseConfirmCh(txID) } } } + +func (s *Sender) releaseConfirmCh(txID string) { + txConfirm, _ := s.unconfirmedTxs.Get(txID) + confirmCh, _ := s.txConfirmCh.Get(txID) + select { + case confirmCh <- txConfirm: + default: + } + // Remove the transaction from the unconfirmed transactions + s.unconfirmedTxs.Remove(txID) + s.txConfirmCh.Remove(txID) +} diff --git a/internal/sender/sender_test.go b/internal/sender/sender_test.go index 37713dab8..291eaad94 100644 --- a/internal/sender/sender_test.go +++ b/internal/sender/sender_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" "golang.org/x/sync/errgroup" @@ -18,29 +19,38 @@ import ( "github.com/taikoxyz/taiko-client/pkg/rpc" ) -func TestSender(t *testing.T) { - utils.LoadEnv() - +func setSender(cfg *sender.Config) (*rpc.EthClient, *sender.Sender, error) { ctx := context.Background() client, err := rpc.NewEthClient(ctx, os.Getenv("L1_NODE_WS_ENDPOINT"), time.Second*10) - assert.NoError(t, err) + if err != nil { + return nil, nil, err + } priv, err := crypto.ToECDSA(common.FromHex(os.Getenv("L1_PROPOSER_PRIVATE_KEY"))) - assert.NoError(t, err) + if err != nil { + return nil, nil, err + } - send, err := sender.NewSender(ctx, &sender.Config{ - Confirmations: 1, - MaxGasPrice: 1000000000000, - GasRate: 10, + send, err := sender.NewSender(ctx, cfg, client, priv) + + return client, send, err +} + +func TestNormalSender(t *testing.T) { + utils.LoadEnv() + _, send, err := setSender(&sender.Config{ + Confirmations: 0, + MaxGasPrice: big.NewInt(20000000000), + GasRate: 50, MaxPendTxs: 10, - RetryTimes: 3, - }, client, priv) + RetryTimes: 0, + }) assert.NoError(t, err) defer send.Stop() var ( - batchSize = 10 + batchSize = 5 eg errgroup.Group confirmsCh = make([]<-chan *sender.TxConfirm, 0, batchSize) ) @@ -49,7 +59,7 @@ func TestSender(t *testing.T) { i := i eg.Go(func() error { addr := common.BigToAddress(big.NewInt(int64(i))) - txID, err := send.SendRaw(&addr, big.NewInt(1), nil) + txID, err := send.SendRaw(send.Opts.Nonce.Uint64(), &addr, big.NewInt(1), nil) if err == nil { confirmCh, _ := send.WaitTxConfirm(txID) confirmsCh = append(confirmsCh, confirmCh) @@ -60,13 +70,111 @@ func TestSender(t *testing.T) { err = eg.Wait() assert.NoError(t, err) - for len(confirmsCh) > 0 { - confirmCh := confirmsCh[0] - select { - case confirm := <-confirmCh: - assert.NoError(t, confirm.Error) - confirmsCh = confirmsCh[1:] - default: + for ; len(confirmsCh) > 0; confirmsCh = confirmsCh[1:] { + confirm := <-confirmsCh[0] + assert.NoError(t, confirm.Error) + } +} + +// Test touch max gas price and replacement. +func TestReplacement(t *testing.T) { + utils.LoadEnv() + + client, send, err := setSender(&sender.Config{ + Confirmations: 0, + MaxGasPrice: big.NewInt(20000000000), + GasRate: 50, + MaxPendTxs: 10, + RetryTimes: 0, + }) + assert.NoError(t, err) + defer send.Stop() + + // Let max gas price be 2 times of the gas fee cap. + send.MaxGasPrice = new(big.Int).Mul(send.Opts.GasFeeCap, big.NewInt(2)) + + nonce, err := client.NonceAt(context.Background(), send.Opts.From, nil) + assert.NoError(t, err) + + pendingNonce, err := client.PendingNonceAt(context.Background(), send.Opts.From) + assert.NoError(t, err) + // Run test only if mempool has no pending transactions. + if pendingNonce > nonce { + return + } + + nonce++ + baseTx := &types.DynamicFeeTx{ + ChainID: send.ChainID, + To: &common.Address{}, + GasFeeCap: send.MaxGasPrice, + GasTipCap: send.MaxGasPrice, + Nonce: nonce, + Gas: 21000, + Value: big.NewInt(1), + Data: nil, + } + rawTx, err := send.Opts.Signer(send.Opts.From, types.NewTx(baseTx)) + assert.NoError(t, err) + err = client.SendTransaction(context.Background(), rawTx) + assert.NoError(t, err) + + confirmsCh := make([]<-chan *sender.TxConfirm, 0, 5) + + // Replace the transaction with a higher nonce. + txID, err := send.SendRaw(nonce, &common.Address{}, big.NewInt(1), nil) + assert.NoError(t, err) + confirmCh, _ := send.WaitTxConfirm(txID) + confirmsCh = append(confirmsCh, confirmCh) + + time.Sleep(time.Second * 6) + // Send a transaction with a next nonce and let all the transactions be confirmed. + txID, err = send.SendRaw(nonce-1, &common.Address{}, big.NewInt(1), nil) + assert.NoError(t, err) + confirmCh, _ = send.WaitTxConfirm(txID) + confirmsCh = append(confirmsCh, confirmCh) + + for ; len(confirmsCh) > 0; confirmsCh = confirmsCh[1:] { + confirm := <-confirmsCh[0] + // Check the replaced transaction's gasFeeTap touch the max gas price. + if confirm.Tx.Nonce() == nonce { + assert.Equal(t, send.MaxGasPrice, confirm.Tx.GasFeeCap()) } + assert.NoError(t, confirm.Error) + t.Log(confirm.Receipt.BlockNumber.String()) + } + + _, err = client.TransactionReceipt(context.Background(), rawTx.Hash()) + assert.Equal(t, "not found", err.Error()) +} + +// Test nonce too low. +func TestNonceTooLow(t *testing.T) { + utils.LoadEnv() + + client, send, err := setSender(&sender.Config{ + Confirmations: 0, + MaxGasPrice: big.NewInt(20000000000), + GasRate: 50, + MaxPendTxs: 10, + RetryTimes: 0, + }) + assert.NoError(t, err) + defer send.Stop() + + nonce, err := client.NonceAt(context.Background(), send.Opts.From, nil) + assert.NoError(t, err) + pendingNonce, err := client.PendingNonceAt(context.Background(), send.Opts.From) + assert.NoError(t, err) + // Run test only if mempool has no pending transactions. + if pendingNonce > nonce { + return } + + txID, err := send.SendRaw(nonce-3, &common.Address{}, big.NewInt(1), nil) + assert.NoError(t, err) + confirmCh, _ := send.WaitTxConfirm(txID) + confirm := <-confirmCh + assert.NoError(t, confirm.Error) + assert.Equal(t, nonce, confirm.Tx.Nonce()) } From 29b2f822879dc44a1a0c69c3b9b8aca48124447e Mon Sep 17 00:00:00 2001 From: maskpp Date: Wed, 21 Feb 2024 16:40:34 +0800 Subject: [PATCH 04/25] use sender in proposer module --- internal/sender/sender.go | 36 ++++-- internal/sender/sender_test.go | 6 +- internal/testutils/helper.go | 4 +- internal/testutils/interfaces.go | 1 - proposer/proposer.go | 189 ++++++++----------------------- proposer/proposer_test.go | 14 +-- 6 files changed, 79 insertions(+), 171 deletions(-) diff --git a/internal/sender/sender.go b/internal/sender/sender.go index dbf110004..817bcb9be 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -19,6 +19,10 @@ import ( "github.com/taikoxyz/taiko-client/pkg/rpc" ) +var ( + rootSender = map[common.Address]*Sender{} +) + type Config struct { // The gap number between a block be confirmed and the latest block. Confirmations uint64 @@ -30,6 +34,7 @@ type Config struct { MaxPendTxs int // The maximum retry times to send transaction. RetryTimes uint64 + GasLimit uint64 } type TxConfirm struct { @@ -42,7 +47,7 @@ type TxConfirm struct { Tx *types.Transaction Receipt *types.Receipt - Error error + Err error } type Sender struct { @@ -102,6 +107,11 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec if err != nil { return nil, err } + // Add the sender to the root sender. + if rootSender[opts.From] != nil { + return nil, fmt.Errorf("sender already exists") + } + rootSender[opts.From] = sender sender.wg.Add(1) go sender.loop() @@ -128,7 +138,7 @@ func (s *Sender) SendRaw(nonce uint64, target *common.Address, value *big.Int, d Nonce: nonce, GasFeeCap: s.Opts.GasFeeCap, GasTipCap: s.Opts.GasTipCap, - Gas: 1000000, + Gas: s.GasLimit, Value: value, Data: data, })) @@ -179,7 +189,7 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { } confirmTx.Tx = rawTx err = s.client.SendTransaction(s.ctx, rawTx) - confirmTx.Error = err + confirmTx.Err = err // Check if the error is nonce too low. if err != nil { if strings.Contains(err.Error(), "nonce too low") { @@ -295,13 +305,12 @@ func (s *Sender) loop() { func (s *Sender) resendTransaction() { for txID, txConfirm := range s.unconfirmedTxs.Items() { - if txConfirm.Error == nil { + if txConfirm.Err == nil { continue } txConfirm.RetryTimes++ if s.RetryTimes != 0 && txConfirm.RetryTimes >= s.RetryTimes { - s.unconfirmedTxs.Remove(txID) - s.txConfirmCh.Remove(txID) + s.releaseConfirm(txID) continue } _ = s.sendTx(txConfirm) @@ -310,7 +319,7 @@ func (s *Sender) resendTransaction() { func (s *Sender) checkPendingTransactions() { for txID, txConfirm := range s.unconfirmedTxs.Items() { - if txConfirm.Error != nil { + if txConfirm.Err != nil { continue } if txConfirm.Receipt == nil { @@ -323,24 +332,29 @@ func (s *Sender) checkPendingTransactions() { receipt, err := s.client.TransactionReceipt(s.ctx, txConfirm.Tx.Hash()) if err != nil { if err.Error() == "not found" { - txConfirm.Error = err - s.releaseConfirmCh(txID) + txConfirm.Err = err + s.releaseConfirm(txID) } log.Warn("failed to get the transaction receipt", "tx_hash", txConfirm.Tx.Hash().String(), "err", err) continue } txConfirm.Receipt = receipt + if receipt.Status != types.ReceiptStatusSuccessful { + txConfirm.Err = fmt.Errorf("transaction reverted, hash: %s", receipt.TxHash.String()) + s.releaseConfirm(txID) + continue + } } txConfirm.confirms = s.header.Number.Uint64() - txConfirm.Receipt.BlockNumber.Uint64() // Check if the transaction is confirmed if s.header.Number.Uint64()-txConfirm.confirms >= s.Confirmations { - s.releaseConfirmCh(txID) + s.releaseConfirm(txID) } } } -func (s *Sender) releaseConfirmCh(txID string) { +func (s *Sender) releaseConfirm(txID string) { txConfirm, _ := s.unconfirmedTxs.Get(txID) confirmCh, _ := s.txConfirmCh.Get(txID) select { diff --git a/internal/sender/sender_test.go b/internal/sender/sender_test.go index 291eaad94..da45781c0 100644 --- a/internal/sender/sender_test.go +++ b/internal/sender/sender_test.go @@ -72,7 +72,7 @@ func TestNormalSender(t *testing.T) { for ; len(confirmsCh) > 0; confirmsCh = confirmsCh[1:] { confirm := <-confirmsCh[0] - assert.NoError(t, confirm.Error) + assert.NoError(t, confirm.Err) } } @@ -140,7 +140,7 @@ func TestReplacement(t *testing.T) { if confirm.Tx.Nonce() == nonce { assert.Equal(t, send.MaxGasPrice, confirm.Tx.GasFeeCap()) } - assert.NoError(t, confirm.Error) + assert.NoError(t, confirm.Err) t.Log(confirm.Receipt.BlockNumber.String()) } @@ -175,6 +175,6 @@ func TestNonceTooLow(t *testing.T) { assert.NoError(t, err) confirmCh, _ := send.WaitTxConfirm(txID) confirm := <-confirmCh - assert.NoError(t, confirm.Error) + assert.NoError(t, confirm.Err) assert.Equal(t, nonce, confirm.Tx.Nonce()) } diff --git a/internal/testutils/helper.go b/internal/testutils/helper.go index 495946da1..306362c4e 100644 --- a/internal/testutils/helper.go +++ b/internal/testutils/helper.go @@ -27,7 +27,7 @@ import ( func ProposeInvalidTxListBytes(s *ClientTestSuite, proposer Proposer) { invalidTxListBytes := RandomBytes(256) - s.Nil(proposer.ProposeTxList(context.Background(), invalidTxListBytes, 1, nil)) + s.Nil(proposer.ProposeTxList(context.Background(), invalidTxListBytes, 1)) } func ProposeAndInsertEmptyBlocks( @@ -54,7 +54,7 @@ func ProposeAndInsertEmptyBlocks( encoded, err := rlp.EncodeToBytes(emptyTxs) s.Nil(err) - s.Nil(proposer.ProposeTxList(context.Background(), encoded, 0, nil)) + s.Nil(proposer.ProposeTxList(context.Background(), encoded, 0)) ProposeInvalidTxListBytes(s, proposer) diff --git a/internal/testutils/interfaces.go b/internal/testutils/interfaces.go index f53c234a1..a6be5ef48 100644 --- a/internal/testutils/interfaces.go +++ b/internal/testutils/interfaces.go @@ -20,6 +20,5 @@ type Proposer interface { ctx context.Context, txListBytes []byte, txNum uint, - nonce *uint64, ) error } diff --git a/proposer/proposer.go b/proposer/proposer.go index c07c1b8d3..8704161c1 100644 --- a/proposer/proposer.go +++ b/proposer/proposer.go @@ -3,20 +3,16 @@ package proposer import ( "bytes" "context" - "crypto/ecdsa" "errors" "fmt" "math/big" "math/rand" - "strings" "sync" "time" "github.com/cenkalti/backoff/v4" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" @@ -27,6 +23,7 @@ import ( "github.com/taikoxyz/taiko-client/bindings" "github.com/taikoxyz/taiko-client/bindings/encoding" "github.com/taikoxyz/taiko-client/internal/metrics" + "github.com/taikoxyz/taiko-client/internal/sender" "github.com/taikoxyz/taiko-client/pkg/rpc" selector "github.com/taikoxyz/taiko-client/proposer/prover_selector" ) @@ -63,6 +60,8 @@ type Proposer struct { CustomProposeOpHook func() error AfterCommitHook func() error + sender *sender.Sender + ctx context.Context wg sync.WaitGroup } @@ -105,6 +104,17 @@ func (p *Proposer) InitFromConfig(ctx context.Context, cfg *Config) (err error) return err } + if p.sender, err = sender.NewSender(ctx, &sender.Config{ + Confirmations: 0, + MaxGasPrice: big.NewInt(20000000000), + GasRate: 10, + MaxPendTxs: 100, + RetryTimes: 0, + GasLimit: cfg.ProposeBlockTxGasLimit, + }, p.rpc.L1, cfg.L1ProposerPrivKey); err != nil { + return err + } + if p.proverSelector, err = selector.NewETHFeeEOASelector( &protocolConfigs, p.rpc, @@ -255,22 +265,6 @@ func (p *Proposer) ProposeOp(ctx context.Context) error { if len(txLists) == 0 { return errNoNewTxs } - - head, err := p.rpc.L1.BlockNumber(ctx) - if err != nil { - return err - } - nonce, err := p.rpc.L1.NonceAt( - ctx, - crypto.PubkeyToAddress(p.L1ProposerPrivKey.PublicKey), - new(big.Int).SetUint64(head), - ) - if err != nil { - return err - } - - log.Info("Proposer account information", "chainHead", head, "nonce", nonce) - g := new(errgroup.Group) for i, txs := range txLists { func(i int, txs types.Transactions) { @@ -284,8 +278,7 @@ func (p *Proposer) ProposeOp(ctx context.Context) error { return fmt.Errorf("failed to encode transactions: %w", err) } - txNonce := nonce + uint64(i) - if err := p.ProposeTxList(ctx, txListBytes, uint(txs.Len()), &txNonce); err != nil { + if err := p.ProposeTxList(ctx, txListBytes, uint(txs.Len())); err != nil { return fmt.Errorf("failed to propose transactions: %w", err) } @@ -307,11 +300,9 @@ func (p *Proposer) ProposeOp(ctx context.Context) error { return nil } -func (p *Proposer) sendProposeBlockTxWithBlobHash( +func (p *Proposer) makeProposeBlockTxWithBlobHash( ctx context.Context, txListBytes []byte, - nonce *uint64, - isReplacement bool, ) (*types.Transaction, error) { // Make sidecar in order to get blob hash. sideCar, err := rpc.MakeSidecar(txListBytes) @@ -328,28 +319,6 @@ func (p *Proposer) sendProposeBlockTxWithBlobHash( return nil, err } - // Propose the transactions list - opts, err := getTxOpts(ctx, p.rpc.L1, p.L1ProposerPrivKey, p.rpc.L1ChainID, maxFee) - if err != nil { - return nil, err - } - if nonce != nil { - opts.Nonce = new(big.Int).SetUint64(*nonce) - } - opts.GasLimit = p.ProposeBlockTxGasLimit - if isReplacement { - if opts, err = rpc.IncreaseGasTipCap( - ctx, - p.rpc, - opts, - p.proposerAddress, - new(big.Int).SetUint64(p.ProposeBlockTxReplacementMultiplier), - p.ProposeBlockTxGasTipCap, - ); err != nil { - return nil, err - } - } - var parentMetaHash = [32]byte{} if p.IncludeParentMetaHash { state, err := p.rpc.TaikoL1.State(&bind.CallOpts{Context: ctx}) @@ -365,8 +334,6 @@ func (p *Proposer) sendProposeBlockTxWithBlobHash( parentMetaHash = parent.Blk.MetaHash } - hookCalls := make([]encoding.HookCall, 0) - // Initially just use the AssignmentHook default. hookInputData, err := encoding.EncodeAssignmentHookInput(&encoding.AssignmentHookInput{ Assignment: assignment, @@ -376,10 +343,6 @@ func (p *Proposer) sendProposeBlockTxWithBlobHash( return nil, err } - hookCalls = append(hookCalls, encoding.HookCall{ - Hook: p.AssignmentHookAddress, - Data: hookInputData, - }) encodedParams, err := encoding.EncodeBlockParams(&encoding.BlockParams{ AssignedProver: assignedProver, ExtraData: rpc.StringToBytes32(p.ExtraData), @@ -388,13 +351,17 @@ func (p *Proposer) sendProposeBlockTxWithBlobHash( BlobHash: [32]byte{}, CacheBlobForReuse: false, ParentMetaHash: parentMetaHash, - HookCalls: hookCalls, + HookCalls: []encoding.HookCall{{ + Hook: p.AssignmentHookAddress, + Data: hookInputData, + }}, }) if err != nil { return nil, err } - opts.NoSend = true + opts := p.sender.Opts + opts.Value = maxFee rawTx, err := p.rpc.TaikoL1.ProposeBlock( opts, encodedParams, @@ -404,8 +371,6 @@ func (p *Proposer) sendProposeBlockTxWithBlobHash( return nil, encoding.TryParsingCustomError(err) } - // Create blob tx and send it. - opts.NoSend = false proposeTx, err := p.rpc.L1.TransactBlobTx(opts, &p.TaikoL1Address, rawTx.Data(), sideCar) if err != nil { return nil, err @@ -416,12 +381,10 @@ func (p *Proposer) sendProposeBlockTxWithBlobHash( return proposeTx, nil } -// sendProposeBlockTx tries to send a TaikoL1.proposeBlock transaction. -func (p *Proposer) sendProposeBlockTx( +// makeProposeBlockTx tries to send a TaikoL1.proposeBlock transaction. +func (p *Proposer) makeProposeBlockTx( ctx context.Context, txListBytes []byte, - nonce *uint64, - isReplacement bool, ) (*types.Transaction, error) { assignment, assignedProver, maxFee, err := p.proverSelector.AssignProver( ctx, @@ -432,27 +395,8 @@ func (p *Proposer) sendProposeBlockTx( return nil, err } - // Propose the transactions list - opts, err := getTxOpts(ctx, p.rpc.L1, p.L1ProposerPrivKey, p.rpc.L1ChainID, maxFee) - if err != nil { - return nil, err - } - if nonce != nil { - opts.Nonce = new(big.Int).SetUint64(*nonce) - } - opts.GasLimit = p.ProposeBlockTxGasLimit - if isReplacement { - if opts, err = rpc.IncreaseGasTipCap( - ctx, - p.rpc, - opts, - p.proposerAddress, - new(big.Int).SetUint64(p.ProposeBlockTxReplacementMultiplier), - p.ProposeBlockTxGasTipCap, - ); err != nil { - return nil, err - } - } + opts := p.sender.Opts + opts.Value = maxFee var parentMetaHash = [32]byte{} if p.IncludeParentMetaHash { @@ -517,48 +461,39 @@ func (p *Proposer) ProposeTxList( ctx context.Context, txListBytes []byte, txNum uint, - nonce *uint64, ) error { - var ( - isReplacement bool - tx *types.Transaction - err error - ) - if err = backoff.Retry( + var txID string + if err := backoff.Retry( func() error { if ctx.Err() != nil { return nil } + var ( + tx *types.Transaction + err error + ) // Send tx list by blob tx. if p.BlobAllowed { - tx, err = p.sendProposeBlockTxWithBlobHash( + tx, err = p.makeProposeBlockTxWithBlobHash( ctx, txListBytes, - nonce, - isReplacement, ) } else { - tx, err = p.sendProposeBlockTx( + tx, err = p.makeProposeBlockTx( ctx, txListBytes, - nonce, - isReplacement, ) } + if err != nil { + log.Warn("Failed to make taikoL1.proposeBlock transaction", "error", encoding.TryParsingCustomError(err)) + return err + } + txID, err = p.sender.SendTransaction(tx) if err != nil { log.Warn("Failed to send taikoL1.proposeBlock transaction", "error", encoding.TryParsingCustomError(err)) - if strings.Contains(err.Error(), core.ErrNonceTooLow.Error()) { - return nil - } - if strings.Contains(err.Error(), txpool.ErrReplaceUnderpriced.Error()) { - isReplacement = true - } else { - isReplacement = false - } return err } - - return nil + return err }, backoff.WithMaxRetries( backoff.NewConstantBackOff(retryInterval), @@ -570,16 +505,12 @@ func (p *Proposer) ProposeTxList( if ctx.Err() != nil { return ctx.Err() } - if err != nil { - return err - } - ctxWithTimeout, cancel := context.WithTimeout(ctx, p.WaitReceiptTimeout) - defer cancel() - if tx != nil { - if _, err = rpc.WaitReceipt(ctxWithTimeout, p.rpc.L1, tx); err != nil { - return err - } + // Waiting for the transaction to be confirmed. + confirmCh, _ := p.sender.WaitTxConfirm(txID) + confirm := <-confirmCh + if confirm.Err != nil { + return confirm.Err } log.Info("📝 Propose transactions succeeded", "txs", txNum) @@ -596,7 +527,7 @@ func (p *Proposer) ProposeEmptyBlockOp(ctx context.Context) error { if err != nil { return err } - return p.ProposeTxList(ctx, emptyTxListBytes, 0, nil) + return p.ProposeTxList(ctx, emptyTxListBytes, 0) } // updateProposingTicker updates the internal proposing timer. @@ -654,31 +585,3 @@ func (p *Proposer) initTierFees() error { return nil } - -// getTxOpts creates a bind.TransactOpts instance using the given private key. -func getTxOpts( - ctx context.Context, - cli *rpc.EthClient, - privKey *ecdsa.PrivateKey, - chainID *big.Int, - fee *big.Int, -) (*bind.TransactOpts, error) { - opts, err := bind.NewKeyedTransactorWithChainID(privKey, chainID) - if err != nil { - return nil, fmt.Errorf("failed to generate prepareBlock transaction options: %w", err) - } - - gasTipCap, err := cli.SuggestGasTipCap(ctx) - if err != nil { - if rpc.IsMaxPriorityFeePerGasNotFoundError(err) { - gasTipCap = rpc.FallbackGasTipCap - } else { - return nil, err - } - } - - opts.GasTipCap = gasTipCap - opts.Value = fee - - return opts, nil -} diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index 3c276fb09..78d11959e 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -156,14 +156,8 @@ func (s *ProposerTestSuite) TestCustomProposeOpHook() { func (s *ProposerTestSuite) TestSendProposeBlockTx() { fee := big.NewInt(10000) - opts, err := getTxOpts( - context.Background(), - s.p.rpc.L1, - s.p.L1ProposerPrivKey, - s.RPCClient.L1ChainID, - fee, - ) - s.Nil(err) + opts := s.p.sender.Opts + opts.GasTipCap = fee s.Greater(opts.GasTipCap.Uint64(), uint64(0)) nonce, err := s.RPCClient.L1.PendingNonceAt(context.Background(), s.p.proposerAddress) @@ -189,11 +183,9 @@ func (s *ProposerTestSuite) TestSendProposeBlockTx() { encoded, err := rlp.EncodeToBytes(emptyTxs) s.Nil(err) - newTx, err := s.p.sendProposeBlockTx( + newTx, err := s.p.makeProposeBlockTx( context.Background(), encoded, - &nonce, - true, ) s.Nil(err) s.Greater(newTx.GasTipCap().Uint64(), tx.GasTipCap().Uint64()) From 297465bff7d5bfa0f0df245f5381edafe86fed12 Mon Sep 17 00:00:00 2001 From: maskpp Date: Wed, 21 Feb 2024 16:42:16 +0800 Subject: [PATCH 05/25] use sender in proposer module --- driver/chain_syncer/chain_syncer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver/chain_syncer/chain_syncer_test.go b/driver/chain_syncer/chain_syncer_test.go index ce63a2dac..53373779f 100644 --- a/driver/chain_syncer/chain_syncer_test.go +++ b/driver/chain_syncer/chain_syncer_test.go @@ -107,7 +107,7 @@ func (s *ChainSyncerTestSuite) TestAheadOfProtocolVerifiedHead2() { s.Nil(err) s.Equal("test", string(bytes.TrimRight(l2Head.Extra, "\x00"))) log.Info("L1HeaderByNumber head", "number", head.Number) - // (equiv to s.state.GetL2Head().number) + // (equiv to s.state.GetL2Head().Number) log.Info("L2HeaderByNumber head", "number", l2Head.Number) // increase evm time to make blocks verifiable. From c873b75d1bdf0439e20cc302e289013d5759c4cd Mon Sep 17 00:00:00 2001 From: maskpp Date: Wed, 21 Feb 2024 16:50:02 +0800 Subject: [PATCH 06/25] fix bug --- proposer/proposer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index 78d11959e..17e3eacc5 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -157,7 +157,7 @@ func (s *ProposerTestSuite) TestCustomProposeOpHook() { func (s *ProposerTestSuite) TestSendProposeBlockTx() { fee := big.NewInt(10000) opts := s.p.sender.Opts - opts.GasTipCap = fee + opts.Value = fee s.Greater(opts.GasTipCap.Uint64(), uint64(0)) nonce, err := s.RPCClient.L1.PendingNonceAt(context.Background(), s.p.proposerAddress) From e2b702fbf567e13062d6c0e905d6d3f78290941a Mon Sep 17 00:00:00 2001 From: maskpp Date: Wed, 21 Feb 2024 17:48:58 +0800 Subject: [PATCH 07/25] Add max transaction waiting time --- internal/sender/sender.go | 44 +++++++++++++++++++++------------- internal/sender/sender_test.go | 36 ++++++++++++++++------------ proposer/proposer.go | 2 +- 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/internal/sender/sender.go b/internal/sender/sender.go index 817bcb9be..86420f88f 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -28,13 +28,16 @@ type Config struct { Confirmations uint64 // The maximum gas price can be used to send transaction. MaxGasPrice *big.Int - // The gas rate to increase the gas price. - GasRate uint64 + // The gas rate to increase the gas price, 20 means 20% gas growth rate. + GasGrowthRate uint64 // The maximum number of pending transactions. MaxPendTxs int // The maximum retry times to send transaction. RetryTimes uint64 - GasLimit uint64 + // The gas limit for raw transaction. + GasLimit uint64 + // The maximum waiting time for transaction in mempool. + MaxWaitingTime time.Duration } type TxConfirm struct { @@ -89,6 +92,11 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec // Do not automatically send transactions opts.NoSend = true + // Set default MaxWaitingTime. + if cfg.MaxWaitingTime == 0 { + cfg.MaxWaitingTime = time.Minute * 5 + } + sender := &Sender{ ctx: ctx, Config: cfg, @@ -211,7 +219,7 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { } func (s *Sender) adjustGas(baseTx *types.DynamicFeeTx) { - rate := big.NewInt(int64(100 + s.GasRate)) + rate := big.NewInt(int64(100 + s.GasGrowthRate)) baseTx.GasFeeCap = new(big.Int).Mul(baseTx.GasFeeCap, rate) baseTx.GasFeeCap.Div(baseTx.GasFeeCap, big.NewInt(100)) if s.MaxGasPrice.Cmp(baseTx.GasFeeCap) < 0 { @@ -273,28 +281,24 @@ func (s *Sender) loop() { for { select { case <-tickResend.C: - if s.unconfirmedTxs.Count() == 0 { - continue - } s.resendTransaction() - // Check the unconfirmed transactions - s.checkPendingTransactions() case <-tickHead.C: - head, err := s.client.HeaderByNumber(s.ctx, nil) + header, err := s.client.HeaderByNumber(s.ctx, nil) if err != nil { log.Warn("failed to get the latest header", "err", err) continue } - if s.header.Hash() == head.Hash() { + if s.header.Hash() == header.Hash() { continue } - s.header = head - + s.header = header // Update the gas tip and gas fee - err = s.updateGasTipGasFee(head) + err = s.updateGasTipGasFee(header) if err != nil { log.Warn("failed to update gas tip and gas fee", "err", err) } + // Check the unconfirmed transactions + s.checkPendingTransactions(header) case <-s.ctx.Done(): return case <-s.stopCh: @@ -317,17 +321,23 @@ func (s *Sender) resendTransaction() { } } -func (s *Sender) checkPendingTransactions() { +func (s *Sender) checkPendingTransactions(header *types.Header) { + curTime := time.Now() for txID, txConfirm := range s.unconfirmedTxs.Items() { if txConfirm.Err != nil { continue } if txConfirm.Receipt == nil { // Ignore the transaction if it is pending. - _, isPending, err := s.client.TransactionByHash(s.ctx, txConfirm.Tx.Hash()) + tx, isPending, err := s.client.TransactionByHash(s.ctx, txConfirm.Tx.Hash()) if err != nil || isPending { continue } + // If the transaction is in mempool for too long, replace it. + if waitTime := curTime.Sub(tx.Time()); waitTime > s.MaxWaitingTime { + txConfirm.Err = fmt.Errorf("transaction in mempool for too long") + continue + } // Get the transaction receipt. receipt, err := s.client.TransactionReceipt(s.ctx, txConfirm.Tx.Hash()) if err != nil { @@ -346,7 +356,7 @@ func (s *Sender) checkPendingTransactions() { } } - txConfirm.confirms = s.header.Number.Uint64() - txConfirm.Receipt.BlockNumber.Uint64() + txConfirm.confirms = header.Number.Uint64() - txConfirm.Receipt.BlockNumber.Uint64() // Check if the transaction is confirmed if s.header.Number.Uint64()-txConfirm.confirms >= s.Confirmations { s.releaseConfirm(txID) diff --git a/internal/sender/sender_test.go b/internal/sender/sender_test.go index da45781c0..c0defc5e4 100644 --- a/internal/sender/sender_test.go +++ b/internal/sender/sender_test.go @@ -40,11 +40,13 @@ func setSender(cfg *sender.Config) (*rpc.EthClient, *sender.Sender, error) { func TestNormalSender(t *testing.T) { utils.LoadEnv() _, send, err := setSender(&sender.Config{ - Confirmations: 0, - MaxGasPrice: big.NewInt(20000000000), - GasRate: 50, - MaxPendTxs: 10, - RetryTimes: 0, + Confirmations: 0, + MaxGasPrice: big.NewInt(20000000000), + GasGrowthRate: 50, + MaxPendTxs: 10, + RetryTimes: 0, + GasLimit: 2000000, + MaxWaitingTime: time.Second * 10, }) assert.NoError(t, err) defer send.Stop() @@ -81,11 +83,13 @@ func TestReplacement(t *testing.T) { utils.LoadEnv() client, send, err := setSender(&sender.Config{ - Confirmations: 0, - MaxGasPrice: big.NewInt(20000000000), - GasRate: 50, - MaxPendTxs: 10, - RetryTimes: 0, + Confirmations: 0, + MaxGasPrice: big.NewInt(20000000000), + GasGrowthRate: 50, + MaxPendTxs: 10, + RetryTimes: 0, + GasLimit: 2000000, + MaxWaitingTime: time.Second * 10, }) assert.NoError(t, err) defer send.Stop() @@ -153,11 +157,13 @@ func TestNonceTooLow(t *testing.T) { utils.LoadEnv() client, send, err := setSender(&sender.Config{ - Confirmations: 0, - MaxGasPrice: big.NewInt(20000000000), - GasRate: 50, - MaxPendTxs: 10, - RetryTimes: 0, + Confirmations: 0, + MaxGasPrice: big.NewInt(20000000000), + GasGrowthRate: 50, + MaxPendTxs: 10, + RetryTimes: 0, + GasLimit: 2000000, + MaxWaitingTime: time.Second * 10, }) assert.NoError(t, err) defer send.Stop() diff --git a/proposer/proposer.go b/proposer/proposer.go index 8704161c1..6f4853283 100644 --- a/proposer/proposer.go +++ b/proposer/proposer.go @@ -107,7 +107,7 @@ func (p *Proposer) InitFromConfig(ctx context.Context, cfg *Config) (err error) if p.sender, err = sender.NewSender(ctx, &sender.Config{ Confirmations: 0, MaxGasPrice: big.NewInt(20000000000), - GasRate: 10, + GasGrowthRate: 10, MaxPendTxs: 100, RetryTimes: 0, GasLimit: cfg.ProposeBlockTxGasLimit, From aadd533fe11907e913ae89e4a3447273c163e41e Mon Sep 17 00:00:00 2001 From: maskpp Date: Wed, 21 Feb 2024 21:09:05 +0800 Subject: [PATCH 08/25] add blob tx operation --- internal/sender/sender.go | 91 +++++++++---------------------- internal/sender/sender_test.go | 27 ++++------ internal/sender/transaction.go | 97 ++++++++++++++++++++++++++++++++++ proposer/proposer.go | 5 +- 4 files changed, 135 insertions(+), 85 deletions(-) create mode 100644 internal/sender/transaction.go diff --git a/internal/sender/sender.go b/internal/sender/sender.go index 86420f88f..190435094 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -24,29 +24,26 @@ var ( ) type Config struct { - // The gap number between a block be confirmed and the latest block. - Confirmations uint64 - // The maximum gas price can be used to send transaction. - MaxGasPrice *big.Int - // The gas rate to increase the gas price, 20 means 20% gas growth rate. - GasGrowthRate uint64 - // The maximum number of pending transactions. - MaxPendTxs int // The maximum retry times to send transaction. RetryTimes uint64 - // The gas limit for raw transaction. - GasLimit uint64 // The maximum waiting time for transaction in mempool. MaxWaitingTime time.Duration + + // The gas limit for raw transaction. + GasLimit uint64 + // The gas rate to increase the gas price, 20 means 20% gas growth rate. + GasGrowthRate uint64 + // The maximum gas price can be used to send transaction. + MaxGasFee uint64 + MaxBlobFee uint64 } type TxConfirm struct { RetryTimes uint64 - confirms uint64 TxID string - baseTx *types.DynamicFeeTx + baseTx types.TxData Tx *types.Transaction Receipt *types.Receipt @@ -127,7 +124,7 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec return sender, nil } -func (s *Sender) Stop() { +func (s *Sender) Close() { close(s.stopCh) s.wg.Wait() } @@ -154,25 +151,20 @@ func (s *Sender) SendRaw(nonce uint64, target *common.Address, value *big.Int, d // SendTransaction sends a transaction to the target address. func (s *Sender) SendTransaction(tx *types.Transaction) (string, error) { - if s.unconfirmedTxs.Count() >= s.MaxPendTxs { + if s.unconfirmedTxs.Count() >= 100 { return "", fmt.Errorf("too many pending transactions") } + txData, err := s.makeTxData(tx) + if err != nil { + return "", err + } txID := fmt.Sprint(atomic.AddUint64(&s.globalTxID, 1)) confirmTx := &TxConfirm{ - TxID: txID, - baseTx: &types.DynamicFeeTx{ - ChainID: s.ChainID, - To: tx.To(), - Nonce: tx.Nonce(), - GasFeeCap: s.Opts.GasFeeCap, - GasTipCap: s.Opts.GasTipCap, - Gas: tx.Gas(), - Value: tx.Value(), - Data: tx.Data(), - }, - Tx: tx, + TxID: txID, + baseTx: txData, + Tx: tx, } - err := s.sendTx(confirmTx) + err = s.sendTx(confirmTx) if err != nil && !strings.Contains(err.Error(), "replacement transaction") { log.Error("failed to send transaction", "tx_id", txID, "tx_hash", tx.Hash().String(), "err", err) return "", err @@ -218,33 +210,6 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { return nil } -func (s *Sender) adjustGas(baseTx *types.DynamicFeeTx) { - rate := big.NewInt(int64(100 + s.GasGrowthRate)) - baseTx.GasFeeCap = new(big.Int).Mul(baseTx.GasFeeCap, rate) - baseTx.GasFeeCap.Div(baseTx.GasFeeCap, big.NewInt(100)) - if s.MaxGasPrice.Cmp(baseTx.GasFeeCap) < 0 { - baseTx.GasFeeCap = new(big.Int).Set(s.MaxGasPrice) - } - - baseTx.GasTipCap = new(big.Int).Mul(baseTx.GasTipCap, rate) - baseTx.GasTipCap.Div(baseTx.GasTipCap, big.NewInt(100)) - if baseTx.GasTipCap.Cmp(baseTx.GasFeeCap) > 0 { - baseTx.GasTipCap = new(big.Int).Set(baseTx.GasFeeCap) - } -} - -func (s *Sender) adjustNonce(baseTx *types.DynamicFeeTx) { - nonce, err := s.client.NonceAt(s.ctx, s.Opts.From, nil) - if err != nil { - log.Warn("failed to get the nonce", "from", s.Opts.From, "err", err) - return - } - s.Opts.Nonce = new(big.Int).SetUint64(nonce) - if baseTx != nil { - baseTx.Nonce = nonce - } -} - func (s *Sender) updateGasTipGasFee(head *types.Header) error { // Get the gas tip cap gasTipCap, err := s.client.SuggestGasTipCap(s.ctx) @@ -258,9 +223,10 @@ func (s *Sender) updateGasTipGasFee(head *types.Header) error { if gasFeeCap.Cmp(gasTipCap) < 0 { return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap) } - if gasFeeCap.Cmp(s.MaxGasPrice) > 0 { - gasFeeCap = new(big.Int).Set(s.MaxGasPrice) - gasTipCap = new(big.Int).Set(s.MaxGasPrice) + maxGasFee := big.NewInt(int64(s.MaxGasFee)) + if gasFeeCap.Cmp(maxGasFee) > 0 { + gasFeeCap = new(big.Int).Set(maxGasFee) + gasTipCap = new(big.Int).Set(maxGasFee) } s.Opts.GasTipCap = gasTipCap @@ -298,7 +264,7 @@ func (s *Sender) loop() { log.Warn("failed to update gas tip and gas fee", "err", err) } // Check the unconfirmed transactions - s.checkPendingTransactions(header) + s.checkPendingTransactions() case <-s.ctx.Done(): return case <-s.stopCh: @@ -321,7 +287,7 @@ func (s *Sender) resendTransaction() { } } -func (s *Sender) checkPendingTransactions(header *types.Header) { +func (s *Sender) checkPendingTransactions() { curTime := time.Now() for txID, txConfirm := range s.unconfirmedTxs.Items() { if txConfirm.Err != nil { @@ -355,12 +321,7 @@ func (s *Sender) checkPendingTransactions(header *types.Header) { continue } } - - txConfirm.confirms = header.Number.Uint64() - txConfirm.Receipt.BlockNumber.Uint64() - // Check if the transaction is confirmed - if s.header.Number.Uint64()-txConfirm.confirms >= s.Confirmations { - s.releaseConfirm(txID) - } + s.releaseConfirm(txID) } } diff --git a/internal/sender/sender_test.go b/internal/sender/sender_test.go index c0defc5e4..3be676678 100644 --- a/internal/sender/sender_test.go +++ b/internal/sender/sender_test.go @@ -40,16 +40,14 @@ func setSender(cfg *sender.Config) (*rpc.EthClient, *sender.Sender, error) { func TestNormalSender(t *testing.T) { utils.LoadEnv() _, send, err := setSender(&sender.Config{ - Confirmations: 0, - MaxGasPrice: big.NewInt(20000000000), + MaxGasFee: 20000000000, GasGrowthRate: 50, - MaxPendTxs: 10, RetryTimes: 0, GasLimit: 2000000, MaxWaitingTime: time.Second * 10, }) assert.NoError(t, err) - defer send.Stop() + defer send.Close() var ( batchSize = 5 @@ -83,19 +81,17 @@ func TestReplacement(t *testing.T) { utils.LoadEnv() client, send, err := setSender(&sender.Config{ - Confirmations: 0, - MaxGasPrice: big.NewInt(20000000000), + MaxGasFee: 20000000000, GasGrowthRate: 50, - MaxPendTxs: 10, RetryTimes: 0, GasLimit: 2000000, MaxWaitingTime: time.Second * 10, }) assert.NoError(t, err) - defer send.Stop() + defer send.Close() // Let max gas price be 2 times of the gas fee cap. - send.MaxGasPrice = new(big.Int).Mul(send.Opts.GasFeeCap, big.NewInt(2)) + send.MaxGasFee = send.Opts.GasFeeCap.Uint64() * 2 nonce, err := client.NonceAt(context.Background(), send.Opts.From, nil) assert.NoError(t, err) @@ -111,8 +107,8 @@ func TestReplacement(t *testing.T) { baseTx := &types.DynamicFeeTx{ ChainID: send.ChainID, To: &common.Address{}, - GasFeeCap: send.MaxGasPrice, - GasTipCap: send.MaxGasPrice, + GasFeeCap: big.NewInt(int64(send.MaxGasFee - 1)), + GasTipCap: big.NewInt(int64(send.MaxGasFee - 1)), Nonce: nonce, Gas: 21000, Value: big.NewInt(1), @@ -142,10 +138,9 @@ func TestReplacement(t *testing.T) { confirm := <-confirmsCh[0] // Check the replaced transaction's gasFeeTap touch the max gas price. if confirm.Tx.Nonce() == nonce { - assert.Equal(t, send.MaxGasPrice, confirm.Tx.GasFeeCap()) + assert.Equal(t, send.MaxGasFee, confirm.Tx.GasFeeCap().Uint64()) } assert.NoError(t, confirm.Err) - t.Log(confirm.Receipt.BlockNumber.String()) } _, err = client.TransactionReceipt(context.Background(), rawTx.Hash()) @@ -157,16 +152,14 @@ func TestNonceTooLow(t *testing.T) { utils.LoadEnv() client, send, err := setSender(&sender.Config{ - Confirmations: 0, - MaxGasPrice: big.NewInt(20000000000), + MaxGasFee: 20000000000, GasGrowthRate: 50, - MaxPendTxs: 10, RetryTimes: 0, GasLimit: 2000000, MaxWaitingTime: time.Second * 10, }) assert.NoError(t, err) - defer send.Stop() + defer send.Close() nonce, err := client.NonceAt(context.Background(), send.Opts.From, nil) assert.NoError(t, err) diff --git a/internal/sender/transaction.go b/internal/sender/transaction.go new file mode 100644 index 000000000..35083ba9d --- /dev/null +++ b/internal/sender/transaction.go @@ -0,0 +1,97 @@ +package sender + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/holiman/uint256" + "modernc.org/mathutil" +) + +func (s *Sender) adjustGas(txData types.TxData) { + rate := s.GasGrowthRate + 100 + switch baseTx := txData.(type) { + case *types.DynamicFeeTx: + gasFeeCap := baseTx.GasFeeCap.Int64() + gasFeeCap = gasFeeCap / 100 * int64(rate) + gasFeeCap = mathutil.MinInt64(gasFeeCap, int64(s.MaxGasFee)) + baseTx.GasFeeCap = big.NewInt(gasFeeCap) + + gasTipCap := baseTx.GasTipCap.Int64() + gasTipCap = gasTipCap / 100 * int64(rate) + gasTipCap = mathutil.MinInt64(gasFeeCap, mathutil.MinInt64(gasTipCap, int64(s.MaxGasFee))) + baseTx.GasTipCap = big.NewInt(gasTipCap) + case *types.BlobTx: + gasFeeCap := baseTx.GasFeeCap.Uint64() + gasFeeCap = gasFeeCap / 100 * rate + gasFeeCap = mathutil.MinUint64(gasFeeCap, s.MaxGasFee) + baseTx.GasFeeCap = uint256.NewInt(gasFeeCap) + + gasTipCap := baseTx.GasTipCap.Uint64() + gasTipCap = gasTipCap / 100 * rate + gasTipCap = mathutil.MinUint64(gasFeeCap, mathutil.MinUint64(gasTipCap, s.MaxGasFee)) + baseTx.GasTipCap = uint256.NewInt(gasTipCap) + + blobFeeCap := baseTx.BlobFeeCap.Uint64() + blobFeeCap = blobFeeCap / 100 * rate + blobFeeCap = mathutil.MinUint64(blobFeeCap, s.MaxBlobFee) + baseTx.BlobFeeCap = uint256.NewInt(blobFeeCap) + } +} + +func (s *Sender) adjustNonce(txData types.TxData) { + nonce, err := s.client.NonceAt(s.ctx, s.Opts.From, nil) + if err != nil { + log.Warn("failed to get the nonce", "from", s.Opts.From, "err", err) + return + } + s.Opts.Nonce = new(big.Int).SetUint64(nonce) + + switch baseTx := txData.(type) { + case *types.DynamicFeeTx: + baseTx.Nonce = nonce + case *types.BlobTx: + baseTx.Nonce = nonce + } +} + +func (s *Sender) makeTxData(tx *types.Transaction) (types.TxData, error) { + switch tx.Type() { + case types.DynamicFeeTxType: + return &types.DynamicFeeTx{ + ChainID: s.ChainID, + To: tx.To(), + Nonce: tx.Nonce(), + GasFeeCap: s.Opts.GasFeeCap, + GasTipCap: s.Opts.GasTipCap, + Gas: tx.Gas(), + Value: tx.Value(), + Data: tx.Data(), + AccessList: tx.AccessList(), + }, nil + case types.BlobTxType: + var to common.Address + if tx.To() != nil { + to = *tx.To() + } + return &types.BlobTx{ + ChainID: uint256.MustFromBig(s.ChainID), + To: to, + Nonce: tx.Nonce(), + GasFeeCap: uint256.MustFromBig(s.Opts.GasFeeCap), + GasTipCap: uint256.MustFromBig(s.Opts.GasTipCap), + Gas: tx.Gas(), + Value: uint256.MustFromBig(tx.Value()), + Data: tx.Data(), + AccessList: tx.AccessList(), + BlobFeeCap: uint256.MustFromBig(tx.BlobGasFeeCap()), + BlobHashes: tx.BlobHashes(), + Sidecar: tx.BlobTxSidecar(), + }, nil + default: + return nil, fmt.Errorf("unsupported transaction type: %v", tx.Type()) + } +} diff --git a/proposer/proposer.go b/proposer/proposer.go index 6f4853283..4f1eb53c7 100644 --- a/proposer/proposer.go +++ b/proposer/proposer.go @@ -105,10 +105,8 @@ func (p *Proposer) InitFromConfig(ctx context.Context, cfg *Config) (err error) } if p.sender, err = sender.NewSender(ctx, &sender.Config{ - Confirmations: 0, - MaxGasPrice: big.NewInt(20000000000), + MaxGasFee: 20000000000, GasGrowthRate: 10, - MaxPendTxs: 100, RetryTimes: 0, GasLimit: cfg.ProposeBlockTxGasLimit, }, p.rpc.L1, cfg.L1ProposerPrivKey); err != nil { @@ -186,6 +184,7 @@ func (p *Proposer) eventLoop() { // Close closes the proposer instance. func (p *Proposer) Close(ctx context.Context) { + p.sender.Close() p.wg.Wait() } From 9ba862907964777ed33f43e82e6ffc224e9b26d9 Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 09:24:02 +0800 Subject: [PATCH 09/25] update bindings --- bindings/.githead | 2 +- bindings/gen_lib_verifying.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bindings/.githead b/bindings/.githead index bb3428663..55e9fb838 100644 --- a/bindings/.githead +++ b/bindings/.githead @@ -1 +1 @@ -2607b5b027882d68cb7f6d423dc459d8083bdd3a +f53130cae8ed108c2c36547130d6e7199db496ee diff --git a/bindings/gen_lib_verifying.go b/bindings/gen_lib_verifying.go index 77f88912e..944705b56 100644 --- a/bindings/gen_lib_verifying.go +++ b/bindings/gen_lib_verifying.go @@ -31,7 +31,7 @@ var ( // LibVerifyingMetaData contains all meta data concerning the LibVerifying contract. var LibVerifyingMetaData = &bind.MetaData{ - ABI: "[{\"type\":\"function\",\"name\":\"isConfigValid\",\"inputs\":[{\"name\":\"config\",\"type\":\"tuple\",\"internalType\":\"structTaikoData.Config\",\"components\":[{\"name\":\"chainId\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"blockMaxProposals\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"blockRingBufferSize\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"maxBlocksToVerifyPerProposal\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"blockMaxGasLimit\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"blockMaxTxListBytes\",\"type\":\"uint24\",\"internalType\":\"uint24\"},{\"name\":\"blobExpiry\",\"type\":\"uint24\",\"internalType\":\"uint24\"},{\"name\":\"blobAllowedForDA\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"blobReuseEnabled\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"livenessBond\",\"type\":\"uint96\",\"internalType\":\"uint96\"},{\"name\":\"ethDepositRingBufferSize\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"ethDepositMinCountPerBlock\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"ethDepositMaxCountPerBlock\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"ethDepositMinAmount\",\"type\":\"uint96\",\"internalType\":\"uint96\"},{\"name\":\"ethDepositMaxAmount\",\"type\":\"uint96\",\"internalType\":\"uint96\"},{\"name\":\"ethDepositGas\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"ethDepositMaxFee\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"blockSyncThreshold\",\"type\":\"uint8\",\"internalType\":\"uint8\"}]}],\"outputs\":[{\"name\":\"isValid\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"pure\"},{\"type\":\"event\",\"name\":\"BlockVerified\",\"inputs\":[{\"name\":\"blockId\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"},{\"name\":\"assignedProver\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"prover\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"blockHash\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"},{\"name\":\"stateRoot\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"},{\"name\":\"tier\",\"type\":\"uint16\",\"indexed\":false,\"internalType\":\"uint16\"},{\"name\":\"contestations\",\"type\":\"uint8\",\"indexed\":false,\"internalType\":\"uint8\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"L1_BLOCK_MISMATCH\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"L1_INVALID_CONFIG\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"L1_TRANSITION_ID_ZERO\",\"inputs\":[]}]", + ABI: "[{\"type\":\"function\",\"name\":\"isConfigValid\",\"inputs\":[{\"name\":\"config\",\"type\":\"tuple\",\"internalType\":\"structTaikoData.Config\",\"components\":[{\"name\":\"chainId\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"blockMaxProposals\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"blockRingBufferSize\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"maxBlocksToVerifyPerProposal\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"blockMaxGasLimit\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"blockMaxTxListBytes\",\"type\":\"uint24\",\"internalType\":\"uint24\"},{\"name\":\"blobExpiry\",\"type\":\"uint24\",\"internalType\":\"uint24\"},{\"name\":\"blobAllowedForDA\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"blobReuseEnabled\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"livenessBond\",\"type\":\"uint96\",\"internalType\":\"uint96\"},{\"name\":\"ethDepositRingBufferSize\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"ethDepositMinCountPerBlock\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"ethDepositMaxCountPerBlock\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"ethDepositMinAmount\",\"type\":\"uint96\",\"internalType\":\"uint96\"},{\"name\":\"ethDepositMaxAmount\",\"type\":\"uint96\",\"internalType\":\"uint96\"},{\"name\":\"ethDepositGas\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"ethDepositMaxFee\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"blockSyncThreshold\",\"type\":\"uint8\",\"internalType\":\"uint8\"}]}],\"outputs\":[{\"name\":\"isValid\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"BlockVerified\",\"inputs\":[{\"name\":\"blockId\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"},{\"name\":\"assignedProver\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"prover\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"blockHash\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"},{\"name\":\"stateRoot\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"},{\"name\":\"tier\",\"type\":\"uint16\",\"indexed\":false,\"internalType\":\"uint16\"},{\"name\":\"contestations\",\"type\":\"uint8\",\"indexed\":false,\"internalType\":\"uint8\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"L1_BLOCK_MISMATCH\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"L1_INVALID_CONFIG\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"L1_TRANSITION_ID_ZERO\",\"inputs\":[]}]", } // LibVerifyingABI is the input ABI used to generate the binding from. @@ -182,7 +182,7 @@ func (_LibVerifying *LibVerifyingTransactorRaw) Transact(opts *bind.TransactOpts // IsConfigValid is a free data retrieval call binding the contract method 0x7e10e12f. // -// Solidity: function isConfigValid((uint64,uint64,uint64,uint64,uint32,uint24,uint24,bool,bool,uint96,uint256,uint64,uint64,uint96,uint96,uint256,uint256,uint8) config) pure returns(bool isValid) +// Solidity: function isConfigValid((uint64,uint64,uint64,uint64,uint32,uint24,uint24,bool,bool,uint96,uint256,uint64,uint64,uint96,uint96,uint256,uint256,uint8) config) view returns(bool isValid) func (_LibVerifying *LibVerifyingCaller) IsConfigValid(opts *bind.CallOpts, config TaikoDataConfig) (bool, error) { var out []interface{} err := _LibVerifying.contract.Call(opts, &out, "isConfigValid", config) @@ -199,14 +199,14 @@ func (_LibVerifying *LibVerifyingCaller) IsConfigValid(opts *bind.CallOpts, conf // IsConfigValid is a free data retrieval call binding the contract method 0x7e10e12f. // -// Solidity: function isConfigValid((uint64,uint64,uint64,uint64,uint32,uint24,uint24,bool,bool,uint96,uint256,uint64,uint64,uint96,uint96,uint256,uint256,uint8) config) pure returns(bool isValid) +// Solidity: function isConfigValid((uint64,uint64,uint64,uint64,uint32,uint24,uint24,bool,bool,uint96,uint256,uint64,uint64,uint96,uint96,uint256,uint256,uint8) config) view returns(bool isValid) func (_LibVerifying *LibVerifyingSession) IsConfigValid(config TaikoDataConfig) (bool, error) { return _LibVerifying.Contract.IsConfigValid(&_LibVerifying.CallOpts, config) } // IsConfigValid is a free data retrieval call binding the contract method 0x7e10e12f. // -// Solidity: function isConfigValid((uint64,uint64,uint64,uint64,uint32,uint24,uint24,bool,bool,uint96,uint256,uint64,uint64,uint96,uint96,uint256,uint256,uint8) config) pure returns(bool isValid) +// Solidity: function isConfigValid((uint64,uint64,uint64,uint64,uint32,uint24,uint24,bool,bool,uint96,uint256,uint64,uint64,uint96,uint96,uint256,uint256,uint8) config) view returns(bool isValid) func (_LibVerifying *LibVerifyingCallerSession) IsConfigValid(config TaikoDataConfig) (bool, error) { return _LibVerifying.Contract.IsConfigValid(&_LibVerifying.CallOpts, config) } From d6f6ad55f05cb8b9590c9edab1726a34842280ff Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 14:29:56 +0800 Subject: [PATCH 10/25] update sender --- driver/driver_test.go | 4 +++ internal/sender/sender.go | 52 +++++++++++----------------------- internal/sender/transaction.go | 27 +++++++++++++++++- proposer/proposer.go | 22 +++++++------- proposer/proposer_test.go | 52 +++++++++++++++++++++------------- 5 files changed, 90 insertions(+), 67 deletions(-) diff --git a/driver/driver_test.go b/driver/driver_test.go index 3cdb47efa..6b884fa18 100644 --- a/driver/driver_test.go +++ b/driver/driver_test.go @@ -168,6 +168,8 @@ func (s *DriverTestSuite) TestCheckL1ReorgToHigherFork() { s.Equal(l1Head3.Number.Uint64(), l1Head1.Number.Uint64()) s.Equal(l1Head3.Hash(), l1Head1.Hash()) + // Because of evm_revert operation, the nonce of the proposer need to be adjusted. + s.p.Sender.AdjustNonce(nil) // Propose ten blocks on another fork for i := 0; i < 10; i++ { testutils.ProposeInvalidTxListBytes(&s.ClientTestSuite, s.p) @@ -231,6 +233,7 @@ func (s *DriverTestSuite) TestCheckL1ReorgToLowerFork() { s.Equal(l1Head3.Number.Uint64(), l1Head1.Number.Uint64()) s.Equal(l1Head3.Hash(), l1Head1.Hash()) + s.p.Sender.AdjustNonce(nil) // Propose one blocks on another fork testutils.ProposeInvalidTxListBytes(&s.ClientTestSuite, s.p) @@ -291,6 +294,7 @@ func (s *DriverTestSuite) TestCheckL1ReorgToSameHeightFork() { s.Equal(l1Head3.Number.Uint64(), l1Head1.Number.Uint64()) s.Equal(l1Head3.Hash(), l1Head1.Hash()) + s.p.Sender.AdjustNonce(nil) // Propose two blocks on another fork testutils.ProposeInvalidTxListBytes(&s.ClientTestSuite, s.p) time.Sleep(3 * time.Second) diff --git a/internal/sender/sender.go b/internal/sender/sender.go index 190435094..759e81acc 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "fmt" "math/big" + "os" "strings" "sync" "sync/atomic" @@ -93,6 +94,9 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec if cfg.MaxWaitingTime == 0 { cfg.MaxWaitingTime = time.Minute * 5 } + if cfg.GasLimit == 0 { + cfg.GasLimit = 21000 + } sender := &Sender{ ctx: ctx, @@ -106,7 +110,7 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec stopCh: make(chan struct{}), } // Set the nonce - sender.adjustNonce(nil) + sender.AdjustNonce(nil) // Update the gas tip and gas fee. err = sender.updateGasTipGasFee(header) if err != nil { @@ -116,7 +120,9 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec if rootSender[opts.From] != nil { return nil, fmt.Errorf("sender already exists") } - rootSender[opts.From] = sender + if os.Getenv("RUN_TESTS") == "" { + rootSender[opts.From] = sender + } sender.wg.Add(1) go sender.loop() @@ -193,7 +199,7 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { // Check if the error is nonce too low. if err != nil { if strings.Contains(err.Error(), "nonce too low") { - s.adjustNonce(baseTx) + s.AdjustNonce(baseTx) log.Warn("nonce is not correct, retry to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) return nil } @@ -210,31 +216,6 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { return nil } -func (s *Sender) updateGasTipGasFee(head *types.Header) error { - // Get the gas tip cap - gasTipCap, err := s.client.SuggestGasTipCap(s.ctx) - if err != nil { - return err - } - - // Get the gas fee cap - gasFeeCap := new(big.Int).Add(gasTipCap, new(big.Int).Mul(head.BaseFee, big.NewInt(2))) - // Check if the gas fee cap is less than the gas tip cap - if gasFeeCap.Cmp(gasTipCap) < 0 { - return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap) - } - maxGasFee := big.NewInt(int64(s.MaxGasFee)) - if gasFeeCap.Cmp(maxGasFee) > 0 { - gasFeeCap = new(big.Int).Set(maxGasFee) - gasTipCap = new(big.Int).Set(maxGasFee) - } - - s.Opts.GasTipCap = gasTipCap - s.Opts.GasFeeCap = gasFeeCap - - return nil -} - func (s *Sender) loop() { defer s.wg.Done() @@ -248,6 +229,8 @@ func (s *Sender) loop() { select { case <-tickResend.C: s.resendTransaction() + // Check the unconfirmed transactions + s.checkPendingTransactions() case <-tickHead.C: header, err := s.client.HeaderByNumber(s.ctx, nil) if err != nil { @@ -263,8 +246,6 @@ func (s *Sender) loop() { if err != nil { log.Warn("failed to update gas tip and gas fee", "err", err) } - // Check the unconfirmed transactions - s.checkPendingTransactions() case <-s.ctx.Done(): return case <-s.stopCh: @@ -288,7 +269,6 @@ func (s *Sender) resendTransaction() { } func (s *Sender) checkPendingTransactions() { - curTime := time.Now() for txID, txConfirm := range s.unconfirmedTxs.Items() { if txConfirm.Err != nil { continue @@ -296,12 +276,14 @@ func (s *Sender) checkPendingTransactions() { if txConfirm.Receipt == nil { // Ignore the transaction if it is pending. tx, isPending, err := s.client.TransactionByHash(s.ctx, txConfirm.Tx.Hash()) - if err != nil || isPending { + if err != nil { continue } - // If the transaction is in mempool for too long, replace it. - if waitTime := curTime.Sub(tx.Time()); waitTime > s.MaxWaitingTime { - txConfirm.Err = fmt.Errorf("transaction in mempool for too long") + if isPending { + // If the transaction is in mempool for too long, replace it. + if waitTime := time.Since(tx.Time()); waitTime > s.MaxWaitingTime { + txConfirm.Err = fmt.Errorf("transaction in mempool for too long") + } continue } // Get the transaction receipt. diff --git a/internal/sender/transaction.go b/internal/sender/transaction.go index 35083ba9d..b3fac1f75 100644 --- a/internal/sender/transaction.go +++ b/internal/sender/transaction.go @@ -42,7 +42,7 @@ func (s *Sender) adjustGas(txData types.TxData) { } } -func (s *Sender) adjustNonce(txData types.TxData) { +func (s *Sender) AdjustNonce(txData types.TxData) { nonce, err := s.client.NonceAt(s.ctx, s.Opts.From, nil) if err != nil { log.Warn("failed to get the nonce", "from", s.Opts.From, "err", err) @@ -58,6 +58,31 @@ func (s *Sender) adjustNonce(txData types.TxData) { } } +func (s *Sender) updateGasTipGasFee(head *types.Header) error { + // Get the gas tip cap + gasTipCap, err := s.client.SuggestGasTipCap(s.ctx) + if err != nil { + return err + } + + // Get the gas fee cap + gasFeeCap := new(big.Int).Add(gasTipCap, new(big.Int).Mul(head.BaseFee, big.NewInt(2))) + // Check if the gas fee cap is less than the gas tip cap + if gasFeeCap.Cmp(gasTipCap) < 0 { + return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap) + } + maxGasFee := big.NewInt(int64(s.MaxGasFee)) + if gasFeeCap.Cmp(maxGasFee) > 0 { + gasFeeCap = new(big.Int).Set(maxGasFee) + gasTipCap = new(big.Int).Set(maxGasFee) + } + + s.Opts.GasTipCap = gasTipCap + s.Opts.GasFeeCap = gasFeeCap + + return nil +} + func (s *Sender) makeTxData(tx *types.Transaction) (types.TxData, error) { switch tx.Type() { case types.DynamicFeeTxType: diff --git a/proposer/proposer.go b/proposer/proposer.go index 4f1eb53c7..82d737cb8 100644 --- a/proposer/proposer.go +++ b/proposer/proposer.go @@ -60,7 +60,7 @@ type Proposer struct { CustomProposeOpHook func() error AfterCommitHook func() error - sender *sender.Sender + Sender *sender.Sender ctx context.Context wg sync.WaitGroup @@ -104,11 +104,11 @@ func (p *Proposer) InitFromConfig(ctx context.Context, cfg *Config) (err error) return err } - if p.sender, err = sender.NewSender(ctx, &sender.Config{ - MaxGasFee: 20000000000, - GasGrowthRate: 10, - RetryTimes: 0, - GasLimit: cfg.ProposeBlockTxGasLimit, + if p.Sender, err = sender.NewSender(ctx, &sender.Config{ + MaxGasFee: 20000000000, + GasGrowthRate: 20, + GasLimit: cfg.ProposeBlockTxGasLimit, + MaxWaitingTime: time.Second * 30, }, p.rpc.L1, cfg.L1ProposerPrivKey); err != nil { return err } @@ -184,7 +184,7 @@ func (p *Proposer) eventLoop() { // Close closes the proposer instance. func (p *Proposer) Close(ctx context.Context) { - p.sender.Close() + p.Sender.Close() p.wg.Wait() } @@ -359,7 +359,7 @@ func (p *Proposer) makeProposeBlockTxWithBlobHash( return nil, err } - opts := p.sender.Opts + opts := p.Sender.Opts opts.Value = maxFee rawTx, err := p.rpc.TaikoL1.ProposeBlock( opts, @@ -394,7 +394,7 @@ func (p *Proposer) makeProposeBlockTx( return nil, err } - opts := p.sender.Opts + opts := p.Sender.Opts opts.Value = maxFee var parentMetaHash = [32]byte{} @@ -487,7 +487,7 @@ func (p *Proposer) ProposeTxList( log.Warn("Failed to make taikoL1.proposeBlock transaction", "error", encoding.TryParsingCustomError(err)) return err } - txID, err = p.sender.SendTransaction(tx) + txID, err = p.Sender.SendTransaction(tx) if err != nil { log.Warn("Failed to send taikoL1.proposeBlock transaction", "error", encoding.TryParsingCustomError(err)) return err @@ -506,7 +506,7 @@ func (p *Proposer) ProposeTxList( } // Waiting for the transaction to be confirmed. - confirmCh, _ := p.sender.WaitTxConfirm(txID) + confirmCh, _ := p.Sender.WaitTxConfirm(txID) confirm := <-confirmCh if confirm.Err != nil { return confirm.Err diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index 17e3eacc5..de3eabb40 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -2,6 +2,7 @@ package proposer import ( "context" + "fmt" "math/big" "os" "testing" @@ -15,6 +16,7 @@ import ( "github.com/taikoxyz/taiko-client/bindings" "github.com/taikoxyz/taiko-client/internal/testutils" + "github.com/taikoxyz/taiko-client/internal/utils" "github.com/taikoxyz/taiko-client/pkg/rpc" ) @@ -155,39 +157,48 @@ func (s *ProposerTestSuite) TestCustomProposeOpHook() { } func (s *ProposerTestSuite) TestSendProposeBlockTx() { + s.SetL1Automine(false) + defer s.SetL1Automine(true) + + s.p.Sender.AdjustNonce(nil) + fee := big.NewInt(10000) - opts := s.p.sender.Opts + opts := s.p.Sender.Opts opts.Value = fee s.Greater(opts.GasTipCap.Uint64(), uint64(0)) nonce, err := s.RPCClient.L1.PendingNonceAt(context.Background(), s.p.proposerAddress) s.Nil(err) - tx := types.NewTransaction( - nonce, - common.BytesToAddress([]byte{}), - common.Big1, - 100000, - opts.GasTipCap, - []byte{}, - ) - - s.SetL1Automine(false) - defer s.SetL1Automine(true) + var txID string + txID, err = s.p.Sender.SendRaw(nonce, &common.Address{}, common.Big1, nil) + confirmCh, _ := s.p.Sender.WaitTxConfirm(txID) + confirm := <-confirmCh + tx := confirm.Tx - signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(s.RPCClient.L1ChainID), s.p.L1ProposerPrivKey) + pendingNonce, err := s.RPCClient.L1.PendingNonceAt(context.Background(), s.p.proposerAddress) s.Nil(err) - s.Nil(s.RPCClient.L1.SendTransaction(context.Background(), signedTx)) + fmt.Println(pendingNonce) - var emptyTxs []types.Transaction - encoded, err := rlp.EncodeToBytes(emptyTxs) + encoded, err := rlp.EncodeToBytes([]types.Transaction{}) + s.Nil(err) + var newTx *types.Transaction + if s.p.BlobAllowed { + newTx, err = s.p.makeProposeBlockTxWithBlobHash(context.Background(), encoded) + } else { + newTx, err = s.p.makeProposeBlockTx( + context.Background(), + encoded, + ) + } s.Nil(err) - newTx, err := s.p.makeProposeBlockTx( - context.Background(), - encoded, - ) + txID, err = s.p.Sender.SendTransaction(newTx) s.Nil(err) + confirmCh, _ = s.p.Sender.WaitTxConfirm(txID) + confirm = <-confirmCh + s.Nil(confirm.Err) + s.Greater(newTx.GasTipCap().Uint64(), tx.GasTipCap().Uint64()) } @@ -217,5 +228,6 @@ func (s *ProposerTestSuite) TestStartClose() { } func TestProposerTestSuite(t *testing.T) { + utils.LoadEnv() suite.Run(t, new(ProposerTestSuite)) } From 968344a841cab38c91aaa1e739ca6f69a7bafdc4 Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 14:43:14 +0800 Subject: [PATCH 11/25] use uuid in sender --- go.mod | 2 +- go.sum | 2 ++ internal/sender/sender.go | 5 ++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 5884e989f..b4e2786b0 100644 --- a/go.mod +++ b/go.mod @@ -81,7 +81,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.1 // indirect diff --git a/go.sum b/go.sum index 286174734..5f9e4abf3 100644 --- a/go.sum +++ b/go.sum @@ -444,6 +444,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= diff --git a/internal/sender/sender.go b/internal/sender/sender.go index 759e81acc..b737538a1 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -8,7 +8,6 @@ import ( "os" "strings" "sync" - "sync/atomic" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -16,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" cmap "github.com/orcaman/concurrent-map/v2" + "github.com/pborman/uuid" "github.com/taikoxyz/taiko-client/pkg/rpc" ) @@ -61,7 +61,6 @@ type Sender struct { ChainID *big.Int Opts *bind.TransactOpts - globalTxID uint64 unconfirmedTxs cmap.ConcurrentMap[string, *TxConfirm] //uint64]*TxConfirm txConfirmCh cmap.ConcurrentMap[string, chan *TxConfirm] @@ -164,7 +163,7 @@ func (s *Sender) SendTransaction(tx *types.Transaction) (string, error) { if err != nil { return "", err } - txID := fmt.Sprint(atomic.AddUint64(&s.globalTxID, 1)) + txID := uuid.New() confirmTx := &TxConfirm{ TxID: txID, baseTx: txData, From e234883d604c4538980e808173f2d38258bc07aa Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 14:45:06 +0800 Subject: [PATCH 12/25] use uuid in sender --- go.mod | 1 + go.sum | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index b4e2786b0..60285d4a9 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/labstack/echo/v4 v4.11.1 github.com/modern-go/reflect2 v1.0.2 github.com/orcaman/concurrent-map/v2 v2.0.1 + github.com/pborman/uuid v1.2.1 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/prysmaticlabs/prysm/v4 v4.2.0 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 5f9e4abf3..bd2399a37 100644 --- a/go.sum +++ b/go.sum @@ -442,8 +442,6 @@ github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= @@ -808,6 +806,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTK github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= From 3c5a5312702dcde5dcbfe4b8ade15c867c7a48d1 Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 14:52:19 +0800 Subject: [PATCH 13/25] add GetUnconfirmedTx api --- internal/sender/sender.go | 9 +++++++++ proposer/proposer_test.go | 8 ++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/internal/sender/sender.go b/internal/sender/sender.go index b737538a1..446c66c93 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -140,6 +140,15 @@ func (s *Sender) WaitTxConfirm(txID string) (<-chan *TxConfirm, bool) { return confirmCh, ok } +// GetUnconfirmedTx returns the unconfirmed transaction by the transaction ID. +func (s *Sender) GetUnconfirmedTx(txID string) *types.Transaction { + txConfirm, ok := s.unconfirmedTxs.Get(txID) + if !ok { + return nil + } + return txConfirm.Tx +} + // SendRaw sends a transaction to the target address. func (s *Sender) SendRaw(nonce uint64, target *common.Address, value *big.Int, data []byte) (string, error) { return s.SendTransaction(types.NewTx(&types.DynamicFeeTx{ diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index de3eabb40..ae503e7f8 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -172,9 +172,7 @@ func (s *ProposerTestSuite) TestSendProposeBlockTx() { var txID string txID, err = s.p.Sender.SendRaw(nonce, &common.Address{}, common.Big1, nil) - confirmCh, _ := s.p.Sender.WaitTxConfirm(txID) - confirm := <-confirmCh - tx := confirm.Tx + tx := s.p.Sender.GetUnconfirmedTx(txID) pendingNonce, err := s.RPCClient.L1.PendingNonceAt(context.Background(), s.p.proposerAddress) s.Nil(err) @@ -195,9 +193,7 @@ func (s *ProposerTestSuite) TestSendProposeBlockTx() { txID, err = s.p.Sender.SendTransaction(newTx) s.Nil(err) - confirmCh, _ = s.p.Sender.WaitTxConfirm(txID) - confirm = <-confirmCh - s.Nil(confirm.Err) + newTx = s.p.Sender.GetUnconfirmedTx(txID) s.Greater(newTx.GasTipCap().Uint64(), tx.GasTipCap().Uint64()) } From 4d13ff7bbee74d97b2dea7721c8912c6258d40be Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 15:03:24 +0800 Subject: [PATCH 14/25] update gastipcap --- internal/sender/sender.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/sender/sender.go b/internal/sender/sender.go index 446c66c93..202cda6be 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -168,6 +168,12 @@ func (s *Sender) SendTransaction(tx *types.Transaction) (string, error) { if s.unconfirmedTxs.Count() >= 100 { return "", fmt.Errorf("too many pending transactions") } + + // Update the gas tip and gas fee. + if err := s.updateGasTipGasFee(s.header); err != nil { + return "", err + } + txData, err := s.makeTxData(tx) if err != nil { return "", err From 8461019803c7d8f4719423daecc07e8d01c0c207 Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 15:21:08 +0800 Subject: [PATCH 15/25] fix bug --- internal/sender/sender.go | 51 ++++++++++++++++++--------------------- proposer/proposer_test.go | 1 + 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/internal/sender/sender.go b/internal/sender/sender.go index 202cda6be..8a1b7d89d 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -169,11 +169,6 @@ func (s *Sender) SendTransaction(tx *types.Transaction) (string, error) { return "", fmt.Errorf("too many pending transactions") } - // Update the gas tip and gas fee. - if err := s.updateGasTipGasFee(s.header); err != nil { - return "", err - } - txData, err := s.makeTxData(tx) if err != nil { return "", err @@ -202,31 +197,33 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { baseTx := confirmTx.baseTx - // Try 3 RetryTimes if nonce is not correct. - rawTx, err := s.Opts.Signer(s.Opts.From, types.NewTx(baseTx)) - if err != nil { - return err - } - confirmTx.Tx = rawTx - err = s.client.SendTransaction(s.ctx, rawTx) - confirmTx.Err = err - // Check if the error is nonce too low. - if err != nil { - if strings.Contains(err.Error(), "nonce too low") { - s.AdjustNonce(baseTx) - log.Warn("nonce is not correct, retry to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) - return nil + for i := 0; i < 3; i++ { + // Try 3 RetryTimes if nonce is not correct. + rawTx, err := s.Opts.Signer(s.Opts.From, types.NewTx(baseTx)) + if err != nil { + return err } - if err.Error() == "replacement transaction underpriced" { - s.adjustGas(baseTx) - log.Warn("replacement transaction underpriced", "tx_hash", rawTx.Hash().String(), "err", err) - return nil + confirmTx.Tx = rawTx + err = s.client.SendTransaction(s.ctx, rawTx) + confirmTx.Err = err + // Check if the error is nonce too low. + if err != nil { + if strings.Contains(err.Error(), "nonce too low") { + s.AdjustNonce(baseTx) + log.Warn("nonce is not correct, retry to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) + continue + } + if err.Error() == "replacement transaction underpriced" { + s.adjustGas(baseTx) + log.Warn("replacement transaction underpriced", "tx_hash", rawTx.Hash().String(), "err", err) + continue + } + log.Error("failed to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) + return err } - log.Error("failed to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) - return err + s.Opts.Nonce = big.NewInt(s.Opts.Nonce.Int64() + 1) + break } - s.Opts.Nonce = big.NewInt(s.Opts.Nonce.Int64() + 1) - return nil } diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index ae503e7f8..36f142dfd 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -172,6 +172,7 @@ func (s *ProposerTestSuite) TestSendProposeBlockTx() { var txID string txID, err = s.p.Sender.SendRaw(nonce, &common.Address{}, common.Big1, nil) + s.Nil(err) tx := s.p.Sender.GetUnconfirmedTx(txID) pendingNonce, err := s.RPCClient.L1.PendingNonceAt(context.Background(), s.p.proposerAddress) From f4e40cbdbf8a04601a6ecfb5a4621354ef5971a4 Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 15:53:55 +0800 Subject: [PATCH 16/25] fix bug --- proposer/proposer_test.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index 36f142dfd..07aced40c 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -2,7 +2,6 @@ package proposer import ( "context" - "fmt" "math/big" "os" "testing" @@ -16,7 +15,6 @@ import ( "github.com/taikoxyz/taiko-client/bindings" "github.com/taikoxyz/taiko-client/internal/testutils" - "github.com/taikoxyz/taiko-client/internal/utils" "github.com/taikoxyz/taiko-client/pkg/rpc" ) @@ -170,15 +168,10 @@ func (s *ProposerTestSuite) TestSendProposeBlockTx() { nonce, err := s.RPCClient.L1.PendingNonceAt(context.Background(), s.p.proposerAddress) s.Nil(err) - var txID string - txID, err = s.p.Sender.SendRaw(nonce, &common.Address{}, common.Big1, nil) + txID, err := s.p.Sender.SendRaw(nonce, &common.Address{}, common.Big1, nil) s.Nil(err) tx := s.p.Sender.GetUnconfirmedTx(txID) - pendingNonce, err := s.RPCClient.L1.PendingNonceAt(context.Background(), s.p.proposerAddress) - s.Nil(err) - fmt.Println(pendingNonce) - encoded, err := rlp.EncodeToBytes([]types.Transaction{}) s.Nil(err) var newTx *types.Transaction @@ -192,7 +185,7 @@ func (s *ProposerTestSuite) TestSendProposeBlockTx() { } s.Nil(err) - txID, err = s.p.Sender.SendTransaction(newTx) + txID, err = s.p.Sender.SendRaw(nonce, newTx.To(), newTx.Value(), newTx.Data()) s.Nil(err) newTx = s.p.Sender.GetUnconfirmedTx(txID) @@ -225,6 +218,5 @@ func (s *ProposerTestSuite) TestStartClose() { } func TestProposerTestSuite(t *testing.T) { - utils.LoadEnv() suite.Run(t, new(ProposerTestSuite)) } From 33ec6032a8e933b8027e1c8e6e3bf721fda2c4f5 Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 16:27:34 +0800 Subject: [PATCH 17/25] upgrade sender test --- internal/sender/sender_test.go | 136 +++++++++++++++------------------ 1 file changed, 62 insertions(+), 74 deletions(-) diff --git a/internal/sender/sender_test.go b/internal/sender/sender_test.go index 3be676678..a35b9bda3 100644 --- a/internal/sender/sender_test.go +++ b/internal/sender/sender_test.go @@ -2,6 +2,7 @@ package sender_test import ( "context" + "github.com/taikoxyz/taiko-client/internal/utils" "math/big" "os" "runtime" @@ -11,44 +12,20 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" "golang.org/x/sync/errgroup" "github.com/taikoxyz/taiko-client/internal/sender" - "github.com/taikoxyz/taiko-client/internal/utils" "github.com/taikoxyz/taiko-client/pkg/rpc" ) -func setSender(cfg *sender.Config) (*rpc.EthClient, *sender.Sender, error) { - ctx := context.Background() - - client, err := rpc.NewEthClient(ctx, os.Getenv("L1_NODE_WS_ENDPOINT"), time.Second*10) - if err != nil { - return nil, nil, err - } - - priv, err := crypto.ToECDSA(common.FromHex(os.Getenv("L1_PROPOSER_PRIVATE_KEY"))) - if err != nil { - return nil, nil, err - } - - send, err := sender.NewSender(ctx, cfg, client, priv) - - return client, send, err +type SenderTestSuite struct { + suite.Suite + client *rpc.EthClient + sender *sender.Sender } -func TestNormalSender(t *testing.T) { - utils.LoadEnv() - _, send, err := setSender(&sender.Config{ - MaxGasFee: 20000000000, - GasGrowthRate: 50, - RetryTimes: 0, - GasLimit: 2000000, - MaxWaitingTime: time.Second * 10, - }) - assert.NoError(t, err) - defer send.Close() - +func (s *SenderTestSuite) TestNormalSender() { var ( batchSize = 5 eg errgroup.Group @@ -59,45 +36,35 @@ func TestNormalSender(t *testing.T) { i := i eg.Go(func() error { addr := common.BigToAddress(big.NewInt(int64(i))) - txID, err := send.SendRaw(send.Opts.Nonce.Uint64(), &addr, big.NewInt(1), nil) + txID, err := s.sender.SendRaw(s.sender.Opts.Nonce.Uint64(), &addr, big.NewInt(1), nil) if err == nil { - confirmCh, _ := send.WaitTxConfirm(txID) + confirmCh, _ := s.sender.WaitTxConfirm(txID) confirmsCh = append(confirmsCh, confirmCh) } return err }) } - err = eg.Wait() - assert.NoError(t, err) + s.Nil(eg.Wait()) for ; len(confirmsCh) > 0; confirmsCh = confirmsCh[1:] { confirm := <-confirmsCh[0] - assert.NoError(t, confirm.Err) + s.Nil(confirm.Err) } } // Test touch max gas price and replacement. -func TestReplacement(t *testing.T) { - utils.LoadEnv() - - client, send, err := setSender(&sender.Config{ - MaxGasFee: 20000000000, - GasGrowthRate: 50, - RetryTimes: 0, - GasLimit: 2000000, - MaxWaitingTime: time.Second * 10, - }) - assert.NoError(t, err) - defer send.Close() +func (s *SenderTestSuite) TestReplacement() { + send := s.sender + client := s.client // Let max gas price be 2 times of the gas fee cap. send.MaxGasFee = send.Opts.GasFeeCap.Uint64() * 2 - nonce, err := client.NonceAt(context.Background(), send.Opts.From, nil) - assert.NoError(t, err) + nonce, err := s.client.NonceAt(context.Background(), send.Opts.From, nil) + s.Nil(err) pendingNonce, err := client.PendingNonceAt(context.Background(), send.Opts.From) - assert.NoError(t, err) + s.Nil(err) // Run test only if mempool has no pending transactions. if pendingNonce > nonce { return @@ -115,22 +82,22 @@ func TestReplacement(t *testing.T) { Data: nil, } rawTx, err := send.Opts.Signer(send.Opts.From, types.NewTx(baseTx)) - assert.NoError(t, err) + s.Nil(err) err = client.SendTransaction(context.Background(), rawTx) - assert.NoError(t, err) + s.Nil(err) confirmsCh := make([]<-chan *sender.TxConfirm, 0, 5) // Replace the transaction with a higher nonce. txID, err := send.SendRaw(nonce, &common.Address{}, big.NewInt(1), nil) - assert.NoError(t, err) + s.Nil(err) confirmCh, _ := send.WaitTxConfirm(txID) confirmsCh = append(confirmsCh, confirmCh) time.Sleep(time.Second * 6) // Send a transaction with a next nonce and let all the transactions be confirmed. txID, err = send.SendRaw(nonce-1, &common.Address{}, big.NewInt(1), nil) - assert.NoError(t, err) + s.Nil(err) confirmCh, _ = send.WaitTxConfirm(txID) confirmsCh = append(confirmsCh, confirmCh) @@ -138,42 +105,63 @@ func TestReplacement(t *testing.T) { confirm := <-confirmsCh[0] // Check the replaced transaction's gasFeeTap touch the max gas price. if confirm.Tx.Nonce() == nonce { - assert.Equal(t, send.MaxGasFee, confirm.Tx.GasFeeCap().Uint64()) + s.Equal(send.MaxGasFee, confirm.Tx.GasFeeCap().Uint64()) } - assert.NoError(t, confirm.Err) + s.Nil(confirm.Err) } _, err = client.TransactionReceipt(context.Background(), rawTx.Hash()) - assert.Equal(t, "not found", err.Error()) + s.Equal("not found", err.Error()) } // Test nonce too low. -func TestNonceTooLow(t *testing.T) { - utils.LoadEnv() - - client, send, err := setSender(&sender.Config{ - MaxGasFee: 20000000000, - GasGrowthRate: 50, - RetryTimes: 0, - GasLimit: 2000000, - MaxWaitingTime: time.Second * 10, - }) - assert.NoError(t, err) - defer send.Close() +func (s *SenderTestSuite) TestNonceTooLow() { + client := s.client + send := s.sender nonce, err := client.NonceAt(context.Background(), send.Opts.From, nil) - assert.NoError(t, err) + s.Nil(err) pendingNonce, err := client.PendingNonceAt(context.Background(), send.Opts.From) - assert.NoError(t, err) + s.Nil(err) // Run test only if mempool has no pending transactions. if pendingNonce > nonce { return } txID, err := send.SendRaw(nonce-3, &common.Address{}, big.NewInt(1), nil) - assert.NoError(t, err) + s.Nil(err) confirmCh, _ := send.WaitTxConfirm(txID) confirm := <-confirmCh - assert.NoError(t, confirm.Err) - assert.Equal(t, nonce, confirm.Tx.Nonce()) + s.Nil(confirm.Err) + s.Equal(nonce, confirm.Tx.Nonce()) +} + +func (s *SenderTestSuite) SetupTest() { + utils.LoadEnv() + + ctx := context.Background() + var err error + s.client, err = rpc.NewEthClient(ctx, os.Getenv("L1_NODE_WS_ENDPOINT"), time.Second*10) + s.Nil(err) + + priv, err := crypto.ToECDSA(common.FromHex(os.Getenv("L1_PROPOSER_PRIVATE_KEY"))) + s.Nil(err) + + s.sender, err = sender.NewSender(ctx, &sender.Config{ + MaxGasFee: 20000000000, + GasGrowthRate: 50, + RetryTimes: 0, + GasLimit: 2000000, + MaxWaitingTime: time.Second * 10, + }, s.client, priv) + s.Nil(err) +} + +func (s *SenderTestSuite) TestStartClose() { + s.sender.Close() + s.client.Close() +} + +func TestSenderTestSuite(t *testing.T) { + suite.Run(t, new(SenderTestSuite)) } From bf22201a504ce72bc52cf4c276566d969955bcd0 Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 16:31:48 +0800 Subject: [PATCH 18/25] order import --- internal/sender/sender_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/sender/sender_test.go b/internal/sender/sender_test.go index a35b9bda3..176aefb73 100644 --- a/internal/sender/sender_test.go +++ b/internal/sender/sender_test.go @@ -2,7 +2,6 @@ package sender_test import ( "context" - "github.com/taikoxyz/taiko-client/internal/utils" "math/big" "os" "runtime" @@ -16,6 +15,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/taikoxyz/taiko-client/internal/sender" + "github.com/taikoxyz/taiko-client/internal/utils" "github.com/taikoxyz/taiko-client/pkg/rpc" ) From 18f29a1284c7f99119fd8a92accd24a41c5c2a5f Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 18:22:13 +0800 Subject: [PATCH 19/25] upgrade sender --- internal/sender/sender.go | 56 ++++++++++++++++++++++------------ internal/sender/sender_test.go | 32 ++++++------------- internal/sender/transaction.go | 40 ++++++++++++++++++++++++ pkg/rpc/utils.go | 6 ++-- proposer/proposer.go | 3 +- 5 files changed, 90 insertions(+), 47 deletions(-) diff --git a/internal/sender/sender.go b/internal/sender/sender.go index 8a1b7d89d..ad1aa6d3c 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -25,6 +25,8 @@ var ( ) type Config struct { + // The minimum confirmations to consider the transaction is confirmed. + Confirm uint64 // The maximum retry times to send transaction. RetryTimes uint64 // The maximum waiting time for transaction in mempool. @@ -41,6 +43,7 @@ type Config struct { type TxConfirm struct { RetryTimes uint64 + confirm uint64 TxID string @@ -134,10 +137,22 @@ func (s *Sender) Close() { s.wg.Wait() } -// WaitTxConfirm returns a channel to receive the transaction confirmation. -func (s *Sender) WaitTxConfirm(txID string) (<-chan *TxConfirm, bool) { +// ConfirmChannel returns a channel to receive the transaction confirmation. +func (s *Sender) ConfirmChannel(txID string) <-chan *TxConfirm { confirmCh, ok := s.txConfirmCh.Get(txID) - return confirmCh, ok + if !ok { + log.Warn("transaction not found", "tx_id", txID) + } + return confirmCh +} + +// ConfirmChannels returns all the transaction confirmation channels. +func (s *Sender) ConfirmChannels() map[string]<-chan *TxConfirm { + channels := map[string]<-chan *TxConfirm{} + for txID, confirmCh := range s.txConfirmCh.Items() { + channels[txID] = confirmCh + } + return channels } // GetUnconfirmedTx returns the unconfirmed transaction by the transaction ID. @@ -230,33 +245,31 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { func (s *Sender) loop() { defer s.wg.Done() - tickHead := time.NewTicker(time.Second * 3) - defer tickHead.Stop() + headCh := make(chan *types.Header, 2) + sub, err := s.client.SubscribeNewHead(s.ctx, headCh) + if err != nil { + panic(err) + } + defer sub.Unsubscribe() - tickResend := time.NewTicker(time.Second * 2) - defer tickResend.Stop() + tick := time.NewTicker(time.Second * 2) + defer tick.Stop() for { select { - case <-tickResend.C: + case <-tick.C: s.resendTransaction() - // Check the unconfirmed transactions - s.checkPendingTransactions() - case <-tickHead.C: - header, err := s.client.HeaderByNumber(s.ctx, nil) - if err != nil { - log.Warn("failed to get the latest header", "err", err) - continue - } - if s.header.Hash() == header.Hash() { - continue - } + case header := <-headCh: + // If chain appear reorg then handle mempool transactions. + // TODO: handle reorg transactions s.header = header // Update the gas tip and gas fee err = s.updateGasTipGasFee(header) if err != nil { log.Warn("failed to update gas tip and gas fee", "err", err) } + // Check the unconfirmed transactions + s.checkPendingTransactions() case <-s.ctx.Done(): return case <-s.stopCh: @@ -314,7 +327,10 @@ func (s *Sender) checkPendingTransactions() { continue } } - s.releaseConfirm(txID) + txConfirm.confirm = s.header.Number.Uint64() - txConfirm.Receipt.BlockNumber.Uint64() + if txConfirm.confirm >= s.Confirm { + s.releaseConfirm(txID) + } } } diff --git a/internal/sender/sender_test.go b/internal/sender/sender_test.go index 176aefb73..5754c982f 100644 --- a/internal/sender/sender_test.go +++ b/internal/sender/sender_test.go @@ -27,27 +27,22 @@ type SenderTestSuite struct { func (s *SenderTestSuite) TestNormalSender() { var ( - batchSize = 5 - eg errgroup.Group - confirmsCh = make([]<-chan *sender.TxConfirm, 0, batchSize) + batchSize = 5 + eg errgroup.Group ) eg.SetLimit(runtime.NumCPU()) for i := 0; i < batchSize; i++ { i := i eg.Go(func() error { addr := common.BigToAddress(big.NewInt(int64(i))) - txID, err := s.sender.SendRaw(s.sender.Opts.Nonce.Uint64(), &addr, big.NewInt(1), nil) - if err == nil { - confirmCh, _ := s.sender.WaitTxConfirm(txID) - confirmsCh = append(confirmsCh, confirmCh) - } + _, err := s.sender.SendRaw(s.sender.Opts.Nonce.Uint64(), &addr, big.NewInt(1), nil) return err }) } s.Nil(eg.Wait()) - for ; len(confirmsCh) > 0; confirmsCh = confirmsCh[1:] { - confirm := <-confirmsCh[0] + for _, confirmCh := range s.sender.ConfirmChannels() { + confirm := <-confirmCh s.Nil(confirm.Err) } } @@ -86,23 +81,17 @@ func (s *SenderTestSuite) TestReplacement() { err = client.SendTransaction(context.Background(), rawTx) s.Nil(err) - confirmsCh := make([]<-chan *sender.TxConfirm, 0, 5) - // Replace the transaction with a higher nonce. - txID, err := send.SendRaw(nonce, &common.Address{}, big.NewInt(1), nil) + _, err = send.SendRaw(nonce, &common.Address{}, big.NewInt(1), nil) s.Nil(err) - confirmCh, _ := send.WaitTxConfirm(txID) - confirmsCh = append(confirmsCh, confirmCh) time.Sleep(time.Second * 6) // Send a transaction with a next nonce and let all the transactions be confirmed. - txID, err = send.SendRaw(nonce-1, &common.Address{}, big.NewInt(1), nil) + _, err = send.SendRaw(nonce-1, &common.Address{}, big.NewInt(1), nil) s.Nil(err) - confirmCh, _ = send.WaitTxConfirm(txID) - confirmsCh = append(confirmsCh, confirmCh) - for ; len(confirmsCh) > 0; confirmsCh = confirmsCh[1:] { - confirm := <-confirmsCh[0] + for _, confirmCh := range send.ConfirmChannels() { + confirm := <-confirmCh // Check the replaced transaction's gasFeeTap touch the max gas price. if confirm.Tx.Nonce() == nonce { s.Equal(send.MaxGasFee, confirm.Tx.GasFeeCap().Uint64()) @@ -130,8 +119,7 @@ func (s *SenderTestSuite) TestNonceTooLow() { txID, err := send.SendRaw(nonce-3, &common.Address{}, big.NewInt(1), nil) s.Nil(err) - confirmCh, _ := send.WaitTxConfirm(txID) - confirm := <-confirmCh + confirm := <-send.ConfirmChannel(txID) s.Nil(confirm.Err) s.Equal(nonce, confirm.Tx.Nonce()) } diff --git a/internal/sender/transaction.go b/internal/sender/transaction.go index b3fac1f75..988913ca8 100644 --- a/internal/sender/transaction.go +++ b/internal/sender/transaction.go @@ -8,7 +8,10 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/holiman/uint256" + "github.com/pborman/uuid" "modernc.org/mathutil" + + "github.com/taikoxyz/taiko-client/pkg/rpc" ) func (s *Sender) adjustGas(txData types.TxData) { @@ -120,3 +123,40 @@ func (s *Sender) makeTxData(tx *types.Transaction) (types.TxData, error) { return nil, fmt.Errorf("unsupported transaction type: %v", tx.Type()) } } + +func (s *Sender) handleReorgTransactions() { // nolint: unused + content, err := rpc.ContentFrom(s.ctx, s.client, s.Opts.From) + if err != nil { + log.Warn("failed to get the unconfirmed transactions", "address", s.Opts.From.String(), "err", err) + return + } + if len(content) == 0 { + return + } + + txs := map[common.Hash]*types.Transaction{} + for _, txMap := range content { + for _, tx := range txMap { + txs[tx.Hash()] = tx + } + } + for _, confirm := range s.unconfirmedTxs.Items() { + delete(txs, confirm.Tx.Hash()) + } + for _, tx := range txs { + baseTx, err := s.makeTxData(tx) + if err != nil { + log.Warn("failed to make the transaction data when handle reorg txs", "tx_hash", tx.Hash().String(), "err", err) + return + } + txID := uuid.New() + confirm := &TxConfirm{ + TxID: txID, + Tx: tx, + baseTx: baseTx, + } + s.unconfirmedTxs.Set(txID, confirm) + s.txConfirmCh.Set(txID, make(chan *TxConfirm, 1)) + log.Info("handle reorg tx", "tx_hash", tx.Hash().String(), "tx_id", txID) + } +} diff --git a/pkg/rpc/utils.go b/pkg/rpc/utils.go index 7c0032faf..d75a14a9b 100644 --- a/pkg/rpc/utils.go +++ b/pkg/rpc/utils.go @@ -284,7 +284,7 @@ func IncreaseGasTipCap( log.Info("Try replacing a transaction with same nonce", "sender", address, "nonce", opts.Nonce) - originalTx, err := GetPendingTxByNonce(ctxWithTimeout, cli, address, opts.Nonce.Uint64()) + originalTx, err := GetPendingTxByNonce(ctxWithTimeout, cli.L1, address, opts.Nonce.Uint64()) if err != nil || originalTx == nil { log.Warn( "Original transaction not found", @@ -322,14 +322,14 @@ func IncreaseGasTipCap( // GetPendingTxByNonce tries to retrieve a pending transaction with a given nonce in a node's mempool. func GetPendingTxByNonce( ctx context.Context, - cli *Client, + cli *EthClient, address common.Address, nonce uint64, ) (*types.Transaction, error) { ctxWithTimeout, cancel := ctxWithTimeoutOrDefault(ctx, defaultTimeout) defer cancel() - content, err := ContentFrom(ctxWithTimeout, cli.L1, address) + content, err := ContentFrom(ctxWithTimeout, cli, address) if err != nil { return nil, err } diff --git a/proposer/proposer.go b/proposer/proposer.go index 82d737cb8..db455a3dd 100644 --- a/proposer/proposer.go +++ b/proposer/proposer.go @@ -506,8 +506,7 @@ func (p *Proposer) ProposeTxList( } // Waiting for the transaction to be confirmed. - confirmCh, _ := p.Sender.WaitTxConfirm(txID) - confirm := <-confirmCh + confirm := <-p.Sender.ConfirmChannel(txID) if confirm.Err != nil { return confirm.Err } From d244ddc77be592054333d3a3df8fc35a0351843d Mon Sep 17 00:00:00 2001 From: maskpp Date: Thu, 22 Feb 2024 20:44:10 +0800 Subject: [PATCH 20/25] fix bug --- internal/sender/sender.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/internal/sender/sender.go b/internal/sender/sender.go index ad1aa6d3c..f44cfb778 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -245,12 +245,8 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { func (s *Sender) loop() { defer s.wg.Done() - headCh := make(chan *types.Header, 2) - sub, err := s.client.SubscribeNewHead(s.ctx, headCh) - if err != nil { - panic(err) - } - defer sub.Unsubscribe() + hTick := time.NewTicker(time.Second * 3) + defer hTick.Stop() tick := time.NewTicker(time.Second * 2) defer tick.Stop() @@ -259,9 +255,19 @@ func (s *Sender) loop() { select { case <-tick.C: s.resendTransaction() - case header := <-headCh: + case <-hTick.C: + header, err := s.client.HeaderByNumber(s.ctx, nil) + if err != nil { + log.Error("failed to get the latest header", "err", err) + return + } + // If chain appear reorg then handle mempool transactions. // TODO: handle reorg transactions + + if s.header.Hash() == header.Hash() { + continue + } s.header = header // Update the gas tip and gas fee err = s.updateGasTipGasFee(header) From 1b20ab44e7448764004646333a61c9fb9a7bfe5f Mon Sep 17 00:00:00 2001 From: maskpp Date: Fri, 23 Feb 2024 11:57:32 +0800 Subject: [PATCH 21/25] expose GetSender --- driver/driver_test.go | 21 +++++++++++++++------ internal/testutils/interfaces.go | 2 ++ proposer/proposer.go | 19 ++++++++++++------- proposer/proposer_test.go | 13 +++++++------ 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/driver/driver_test.go b/driver/driver_test.go index 6b884fa18..691670768 100644 --- a/driver/driver_test.go +++ b/driver/driver_test.go @@ -129,7 +129,10 @@ func (s *DriverTestSuite) TestProcessL1Blocks() { } func (s *DriverTestSuite) TestCheckL1ReorgToHigherFork() { - var testnetL1SnapshotID string + var ( + testnetL1SnapshotID string + sender = s.p.GetSender() + ) s.Nil(s.RPCClient.L1.CallContext(context.Background(), &testnetL1SnapshotID, "evm_snapshot")) s.NotEmpty(testnetL1SnapshotID) @@ -169,7 +172,7 @@ func (s *DriverTestSuite) TestCheckL1ReorgToHigherFork() { s.Equal(l1Head3.Hash(), l1Head1.Hash()) // Because of evm_revert operation, the nonce of the proposer need to be adjusted. - s.p.Sender.AdjustNonce(nil) + sender.AdjustNonce(nil) // Propose ten blocks on another fork for i := 0; i < 10; i++ { testutils.ProposeInvalidTxListBytes(&s.ClientTestSuite, s.p) @@ -194,7 +197,10 @@ func (s *DriverTestSuite) TestCheckL1ReorgToHigherFork() { } func (s *DriverTestSuite) TestCheckL1ReorgToLowerFork() { - var testnetL1SnapshotID string + var ( + testnetL1SnapshotID string + sender = s.p.GetSender() + ) s.Nil(s.RPCClient.L1.CallContext(context.Background(), &testnetL1SnapshotID, "evm_snapshot")) s.NotEmpty(testnetL1SnapshotID) @@ -233,7 +239,7 @@ func (s *DriverTestSuite) TestCheckL1ReorgToLowerFork() { s.Equal(l1Head3.Number.Uint64(), l1Head1.Number.Uint64()) s.Equal(l1Head3.Hash(), l1Head1.Hash()) - s.p.Sender.AdjustNonce(nil) + sender.AdjustNonce(nil) // Propose one blocks on another fork testutils.ProposeInvalidTxListBytes(&s.ClientTestSuite, s.p) @@ -255,7 +261,10 @@ func (s *DriverTestSuite) TestCheckL1ReorgToLowerFork() { } func (s *DriverTestSuite) TestCheckL1ReorgToSameHeightFork() { - var testnetL1SnapshotID string + var ( + testnetL1SnapshotID string + sender = s.p.GetSender() + ) s.Nil(s.RPCClient.L1.CallContext(context.Background(), &testnetL1SnapshotID, "evm_snapshot")) s.NotEmpty(testnetL1SnapshotID) @@ -294,7 +303,7 @@ func (s *DriverTestSuite) TestCheckL1ReorgToSameHeightFork() { s.Equal(l1Head3.Number.Uint64(), l1Head1.Number.Uint64()) s.Equal(l1Head3.Hash(), l1Head1.Hash()) - s.p.Sender.AdjustNonce(nil) + sender.AdjustNonce(nil) // Propose two blocks on another fork testutils.ProposeInvalidTxListBytes(&s.ClientTestSuite, s.p) time.Sleep(3 * time.Second) diff --git a/internal/testutils/interfaces.go b/internal/testutils/interfaces.go index a6be5ef48..487d83bf9 100644 --- a/internal/testutils/interfaces.go +++ b/internal/testutils/interfaces.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/taikoxyz/taiko-client/cmd/utils" + "github.com/taikoxyz/taiko-client/internal/sender" ) type CalldataSyncer interface { @@ -21,4 +22,5 @@ type Proposer interface { txListBytes []byte, txNum uint, ) error + GetSender() *sender.Sender } diff --git a/proposer/proposer.go b/proposer/proposer.go index db455a3dd..396ce2d64 100644 --- a/proposer/proposer.go +++ b/proposer/proposer.go @@ -60,7 +60,7 @@ type Proposer struct { CustomProposeOpHook func() error AfterCommitHook func() error - Sender *sender.Sender + sender *sender.Sender ctx context.Context wg sync.WaitGroup @@ -104,7 +104,7 @@ func (p *Proposer) InitFromConfig(ctx context.Context, cfg *Config) (err error) return err } - if p.Sender, err = sender.NewSender(ctx, &sender.Config{ + if p.sender, err = sender.NewSender(ctx, &sender.Config{ MaxGasFee: 20000000000, GasGrowthRate: 20, GasLimit: cfg.ProposeBlockTxGasLimit, @@ -184,7 +184,7 @@ func (p *Proposer) eventLoop() { // Close closes the proposer instance. func (p *Proposer) Close(ctx context.Context) { - p.Sender.Close() + p.sender.Close() p.wg.Wait() } @@ -359,7 +359,7 @@ func (p *Proposer) makeProposeBlockTxWithBlobHash( return nil, err } - opts := p.Sender.Opts + opts := p.sender.Opts opts.Value = maxFee rawTx, err := p.rpc.TaikoL1.ProposeBlock( opts, @@ -394,7 +394,7 @@ func (p *Proposer) makeProposeBlockTx( return nil, err } - opts := p.Sender.Opts + opts := p.sender.Opts opts.Value = maxFee var parentMetaHash = [32]byte{} @@ -487,7 +487,7 @@ func (p *Proposer) ProposeTxList( log.Warn("Failed to make taikoL1.proposeBlock transaction", "error", encoding.TryParsingCustomError(err)) return err } - txID, err = p.Sender.SendTransaction(tx) + txID, err = p.sender.SendTransaction(tx) if err != nil { log.Warn("Failed to send taikoL1.proposeBlock transaction", "error", encoding.TryParsingCustomError(err)) return err @@ -506,7 +506,7 @@ func (p *Proposer) ProposeTxList( } // Waiting for the transaction to be confirmed. - confirm := <-p.Sender.ConfirmChannel(txID) + confirm := <-p.sender.ConfirmChannel(txID) if confirm.Err != nil { return confirm.Err } @@ -551,6 +551,11 @@ func (p *Proposer) Name() string { return "proposer" } +// GetSender returns the sender instance. +func (p *Proposer) GetSender() *sender.Sender { + return p.sender +} + // initTierFees initializes the proving fees for every proof tier configured in the protocol for the proposer. func (p *Proposer) initTierFees() error { for _, tier := range p.tiers { diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index 07aced40c..fb3f121d9 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -155,22 +155,23 @@ func (s *ProposerTestSuite) TestCustomProposeOpHook() { } func (s *ProposerTestSuite) TestSendProposeBlockTx() { + sender := s.p.GetSender() s.SetL1Automine(false) defer s.SetL1Automine(true) - s.p.Sender.AdjustNonce(nil) + sender.AdjustNonce(nil) fee := big.NewInt(10000) - opts := s.p.Sender.Opts + opts := sender.Opts opts.Value = fee s.Greater(opts.GasTipCap.Uint64(), uint64(0)) nonce, err := s.RPCClient.L1.PendingNonceAt(context.Background(), s.p.proposerAddress) s.Nil(err) - txID, err := s.p.Sender.SendRaw(nonce, &common.Address{}, common.Big1, nil) + txID, err := sender.SendRaw(nonce, &common.Address{}, common.Big1, nil) s.Nil(err) - tx := s.p.Sender.GetUnconfirmedTx(txID) + tx := sender.GetUnconfirmedTx(txID) encoded, err := rlp.EncodeToBytes([]types.Transaction{}) s.Nil(err) @@ -185,9 +186,9 @@ func (s *ProposerTestSuite) TestSendProposeBlockTx() { } s.Nil(err) - txID, err = s.p.Sender.SendRaw(nonce, newTx.To(), newTx.Value(), newTx.Data()) + txID, err = sender.SendRaw(nonce, newTx.To(), newTx.Value(), newTx.Data()) s.Nil(err) - newTx = s.p.Sender.GetUnconfirmedTx(txID) + newTx = sender.GetUnconfirmedTx(txID) s.Greater(newTx.GasTipCap().Uint64(), tx.GasTipCap().Uint64()) } From dc42922a1389823b6c00d344e9108bd5e616b771 Mon Sep 17 00:00:00 2001 From: maskpp Date: Fri, 23 Feb 2024 14:25:16 +0800 Subject: [PATCH 22/25] upgrade sender --- internal/sender/{transaction.go => common.go} | 23 ++++++++++++ internal/sender/sender.go | 37 +++++++++++-------- 2 files changed, 45 insertions(+), 15 deletions(-) rename internal/sender/{transaction.go => common.go} (86%) diff --git a/internal/sender/transaction.go b/internal/sender/common.go similarity index 86% rename from internal/sender/transaction.go rename to internal/sender/common.go index 988913ca8..1c192439e 100644 --- a/internal/sender/transaction.go +++ b/internal/sender/common.go @@ -3,6 +3,7 @@ package sender import ( "fmt" "math/big" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -160,3 +161,25 @@ func (s *Sender) handleReorgTransactions() { // nolint: unused log.Info("handle reorg tx", "tx_hash", tx.Hash().String(), "tx_id", txID) } } + +func setDefault[T uint64 | time.Duration](src, dest T) T { + if src == 0 { + return dest + } + return src +} + +func setConfig(config *Config) *Config { + if config == nil { + return DefaultConfig + } + return &Config{ + Confirm: setDefault(config.Confirm, DefaultConfig.Confirm), + RetryTimes: setDefault(config.RetryTimes, DefaultConfig.RetryTimes), + MaxWaitingTime: setDefault(config.MaxWaitingTime, DefaultConfig.MaxWaitingTime), + GasLimit: setDefault(config.GasLimit, DefaultConfig.GasLimit), + GasGrowthRate: setDefault(config.GasGrowthRate, DefaultConfig.GasGrowthRate), + MaxGasFee: setDefault(config.MaxGasFee, DefaultConfig.MaxGasFee), + MaxBlobFee: setDefault(config.MaxBlobFee, DefaultConfig.MaxBlobFee), + } +} diff --git a/internal/sender/sender.go b/internal/sender/sender.go index ed4ed177c..f7eef8bbe 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -21,13 +21,22 @@ import ( ) var ( - rootSender = map[common.Address]*Sender{} + rootSender = map[uint64]map[common.Address]*Sender{} + DefaultConfig = &Config{ + Confirm: 0, + RetryTimes: 0, + MaxWaitingTime: time.Minute * 5, + GasLimit: 21000, + GasGrowthRate: 50, + MaxGasFee: 20000000000, + MaxBlobFee: 1000000000, + } ) type Config struct { // The minimum confirmations to consider the transaction is confirmed. Confirm uint64 - // The maximum retry times to send transaction. + // The maximum retry times to send transaction, 0 means always retry. RetryTimes uint64 // The maximum waiting time for transaction in mempool. MaxWaitingTime time.Duration @@ -74,6 +83,7 @@ type Sender struct { // NewSender returns a new instance of Sender. func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ecdsa.PrivateKey) (*Sender, error) { + cfg = setConfig(cfg) // Get the chain ID header, err := client.HeaderByNumber(ctx, nil) if err != nil { @@ -88,14 +98,6 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec // Do not automatically send transactions opts.NoSend = true - // Set default MaxWaitingTime. - if cfg.MaxWaitingTime == 0 { - cfg.MaxWaitingTime = time.Minute * 5 - } - if cfg.GasLimit == 0 { - cfg.GasLimit = 21000 - } - sender := &Sender{ ctx: ctx, Config: cfg, @@ -114,12 +116,17 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec if err != nil { return nil, err } + // Add the sender to the root sender. - if rootSender[opts.From] != nil { - return nil, fmt.Errorf("sender already exists") - } - if os.Getenv("RUN_TESTS") == "" { - rootSender[opts.From] = sender + if root := rootSender[client.ChainID.Uint64()]; root == nil { + rootSender[client.ChainID.Uint64()] = map[common.Address]*Sender{} + } else { + if root[opts.From] != nil { + return nil, fmt.Errorf("sender already exists") + } + if os.Getenv("RUN_TESTS") == "" { + root[opts.From] = sender + } } sender.wg.Add(1) From 0eabc3ca9360661c0255e71a1dfd630f0df6a770 Mon Sep 17 00:00:00 2001 From: maskpp Date: Fri, 23 Feb 2024 22:11:55 +0800 Subject: [PATCH 23/25] upgrade sender --- internal/sender/common.go | 19 ++++++--- internal/sender/sender.go | 71 +++++++++++++++++----------------- internal/sender/sender_test.go | 33 ++++++---------- internal/testutils/suite.go | 8 ++++ internal/utils/utils.go | 6 +-- pkg/rpc/utils.go | 16 ++++++-- 6 files changed, 84 insertions(+), 69 deletions(-) diff --git a/internal/sender/common.go b/internal/sender/common.go index 1c192439e..b0fbead89 100644 --- a/internal/sender/common.go +++ b/internal/sender/common.go @@ -91,7 +91,7 @@ func (s *Sender) makeTxData(tx *types.Transaction) (types.TxData, error) { switch tx.Type() { case types.DynamicFeeTxType: return &types.DynamicFeeTx{ - ChainID: s.ChainID, + ChainID: s.client.ChainID, To: tx.To(), Nonce: tx.Nonce(), GasFeeCap: s.Opts.GasFeeCap, @@ -107,7 +107,7 @@ func (s *Sender) makeTxData(tx *types.Transaction) (types.TxData, error) { to = *tx.To() } return &types.BlobTx{ - ChainID: uint256.MustFromBig(s.ChainID), + ChainID: uint256.MustFromBig(s.client.ChainID), To: to, Nonce: tx.Nonce(), GasFeeCap: uint256.MustFromBig(s.Opts.GasFeeCap), @@ -126,7 +126,7 @@ func (s *Sender) makeTxData(tx *types.Transaction) (types.TxData, error) { } func (s *Sender) handleReorgTransactions() { // nolint: unused - content, err := rpc.ContentFrom(s.ctx, s.client, s.Opts.From) + content, err := rpc.Content(s.ctx, s.client) if err != nil { log.Warn("failed to get the unconfirmed transactions", "address", s.Opts.From.String(), "err", err) return @@ -136,11 +136,18 @@ func (s *Sender) handleReorgTransactions() { // nolint: unused } txs := map[common.Hash]*types.Transaction{} - for _, txMap := range content { - for _, tx := range txMap { - txs[tx.Hash()] = tx + for _, txMapStatus := range content { + for key, txMapNonce := range txMapStatus { + addr := common.HexToAddress(key) + if addr != s.Opts.From { + continue + } + for _, tx := range txMapNonce { + txs[tx.Hash()] = tx + } } } + // Remove the already handled transactions. for _, confirm := range s.unconfirmedTxs.Items() { delete(txs, confirm.Tx.Hash()) } diff --git a/internal/sender/sender.go b/internal/sender/sender.go index f7eef8bbe..2923cc2a1 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -21,7 +21,7 @@ import ( ) var ( - rootSender = map[uint64]map[common.Address]*Sender{} + rootSenders = map[uint64]map[common.Address]*Sender{} DefaultConfig = &Config{ Confirm: 0, RetryTimes: 0, @@ -36,7 +36,7 @@ var ( type Config struct { // The minimum confirmations to consider the transaction is confirmed. Confirm uint64 - // The maximum retry times to send transaction, 0 means always retry. + // The maximum retry times to send transaction, 0 means retry until succeed. RetryTimes uint64 // The maximum waiting time for transaction in mempool. MaxWaitingTime time.Duration @@ -70,8 +70,7 @@ type Sender struct { header *types.Header client *rpc.EthClient - ChainID *big.Int - Opts *bind.TransactOpts + Opts *bind.TransactOpts unconfirmedTxs cmap.ConcurrentMap[string, *TxConfirm] //uint64]*TxConfirm txConfirmCh cmap.ConcurrentMap[string, chan *TxConfirm] @@ -84,11 +83,6 @@ type Sender struct { // NewSender returns a new instance of Sender. func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ecdsa.PrivateKey) (*Sender, error) { cfg = setConfig(cfg) - // Get the chain ID - header, err := client.HeaderByNumber(ctx, nil) - if err != nil { - return nil, err - } // Create a new transactor opts, err := bind.NewKeyedTransactorWithChainID(priv, client.ChainID) @@ -98,10 +92,24 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec // Do not automatically send transactions opts.NoSend = true + // Add the sender to the root sender. + if root := rootSenders[client.ChainID.Uint64()]; root == nil { + rootSenders[client.ChainID.Uint64()] = map[common.Address]*Sender{} + } else { + if root[opts.From] != nil { + return nil, fmt.Errorf("sender already exists") + } + } + + // Get the chain ID + header, err := client.HeaderByNumber(ctx, nil) + if err != nil { + return nil, err + } + sender := &Sender{ ctx: ctx, Config: cfg, - ChainID: client.ChainID, header: header, client: client, Opts: opts, @@ -116,17 +124,8 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec if err != nil { return nil, err } - - // Add the sender to the root sender. - if root := rootSender[client.ChainID.Uint64()]; root == nil { - rootSender[client.ChainID.Uint64()] = map[common.Address]*Sender{} - } else { - if root[opts.From] != nil { - return nil, fmt.Errorf("sender already exists") - } - if os.Getenv("RUN_TESTS") == "" { - root[opts.From] = sender - } + if os.Getenv("RUN_TESTS") == "" { + rootSenders[client.ChainID.Uint64()][opts.From] = sender } sender.wg.Add(1) @@ -170,7 +169,7 @@ func (s *Sender) GetUnconfirmedTx(txID string) *types.Transaction { // SendRaw sends a transaction to the target address. func (s *Sender) SendRaw(nonce uint64, target *common.Address, value *big.Int, data []byte) (string, error) { return s.SendTransaction(types.NewTx(&types.DynamicFeeTx{ - ChainID: s.ChainID, + ChainID: s.client.ChainID, To: target, Nonce: nonce, GasFeeCap: s.Opts.GasFeeCap, @@ -248,8 +247,14 @@ func (s *Sender) sendTx(confirmTx *TxConfirm) error { func (s *Sender) loop() { defer s.wg.Done() - hTick := time.NewTicker(time.Second * 3) - defer hTick.Stop() + // Subscribe new head. + headCh := make(chan *types.Header, 3) + sub, err := s.client.SubscribeNewHead(s.ctx, headCh) + if err != nil { + log.Error("failed to subscribe new head", "err", err) + return + } + defer sub.Unsubscribe() tick := time.NewTicker(time.Second * 2) defer tick.Stop() @@ -258,19 +263,13 @@ func (s *Sender) loop() { select { case <-tick.C: s.resendTransaction() - case <-hTick.C: - header, err := s.client.HeaderByNumber(s.ctx, nil) - if err != nil { - log.Error("failed to get the latest header", "err", err) - return - } - + case header := <-headCh: // If chain appear reorg then handle mempool transactions. - // TODO: handle reorg transactions - - if s.header.Hash() == header.Hash() { - continue - } + // Handle reorg transactions + //if s.header.Hash() != header.ParentHash { + // TODO handle reorg transactions + //s.handleReorgTransactions() + //} s.header = header // Update the gas tip and gas fee err = s.updateGasTipGasFee(header) diff --git a/internal/sender/sender_test.go b/internal/sender/sender_test.go index 5754c982f..9b2e79095 100644 --- a/internal/sender/sender_test.go +++ b/internal/sender/sender_test.go @@ -15,23 +15,18 @@ import ( "golang.org/x/sync/errgroup" "github.com/taikoxyz/taiko-client/internal/sender" - "github.com/taikoxyz/taiko-client/internal/utils" - "github.com/taikoxyz/taiko-client/pkg/rpc" + "github.com/taikoxyz/taiko-client/internal/testutils" ) type SenderTestSuite struct { - suite.Suite - client *rpc.EthClient + testutils.ClientTestSuite sender *sender.Sender } func (s *SenderTestSuite) TestNormalSender() { - var ( - batchSize = 5 - eg errgroup.Group - ) + var eg errgroup.Group eg.SetLimit(runtime.NumCPU()) - for i := 0; i < batchSize; i++ { + for i := 0; i < 5; i++ { i := i eg.Go(func() error { addr := common.BigToAddress(big.NewInt(int64(i))) @@ -50,12 +45,12 @@ func (s *SenderTestSuite) TestNormalSender() { // Test touch max gas price and replacement. func (s *SenderTestSuite) TestReplacement() { send := s.sender - client := s.client + client := s.RPCClient.L1 // Let max gas price be 2 times of the gas fee cap. send.MaxGasFee = send.Opts.GasFeeCap.Uint64() * 2 - nonce, err := s.client.NonceAt(context.Background(), send.Opts.From, nil) + nonce, err := client.NonceAt(context.Background(), send.Opts.From, nil) s.Nil(err) pendingNonce, err := client.PendingNonceAt(context.Background(), send.Opts.From) @@ -67,7 +62,7 @@ func (s *SenderTestSuite) TestReplacement() { nonce++ baseTx := &types.DynamicFeeTx{ - ChainID: send.ChainID, + ChainID: client.ChainID, To: &common.Address{}, GasFeeCap: big.NewInt(int64(send.MaxGasFee - 1)), GasTipCap: big.NewInt(int64(send.MaxGasFee - 1)), @@ -105,7 +100,7 @@ func (s *SenderTestSuite) TestReplacement() { // Test nonce too low. func (s *SenderTestSuite) TestNonceTooLow() { - client := s.client + client := s.RPCClient.L1 send := s.sender nonce, err := client.NonceAt(context.Background(), send.Opts.From, nil) @@ -125,13 +120,9 @@ func (s *SenderTestSuite) TestNonceTooLow() { } func (s *SenderTestSuite) SetupTest() { - utils.LoadEnv() + s.ClientTestSuite.SetupTest() ctx := context.Background() - var err error - s.client, err = rpc.NewEthClient(ctx, os.Getenv("L1_NODE_WS_ENDPOINT"), time.Second*10) - s.Nil(err) - priv, err := crypto.ToECDSA(common.FromHex(os.Getenv("L1_PROPOSER_PRIVATE_KEY"))) s.Nil(err) @@ -141,13 +132,13 @@ func (s *SenderTestSuite) SetupTest() { RetryTimes: 0, GasLimit: 2000000, MaxWaitingTime: time.Second * 10, - }, s.client, priv) + }, s.RPCClient.L1, priv) s.Nil(err) } -func (s *SenderTestSuite) TestStartClose() { +func (s *SenderTestSuite) TearDownTest() { s.sender.Close() - s.client.Close() + s.ClientTestSuite.TearDownTest() } func TestSenderTestSuite(t *testing.T) { diff --git a/internal/testutils/suite.go b/internal/testutils/suite.go index 13c0774d1..b5164f08c 100644 --- a/internal/testutils/suite.go +++ b/internal/testutils/suite.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/taikoxyz/taiko-client/bindings" + "github.com/taikoxyz/taiko-client/internal/utils" "github.com/taikoxyz/taiko-client/pkg/jwt" "github.com/taikoxyz/taiko-client/pkg/rpc" "github.com/taikoxyz/taiko-client/prover/server" @@ -32,6 +33,7 @@ type ClientTestSuite struct { } func (s *ClientTestSuite) SetupTest() { + utils.LoadEnv() // Default logger glogger := log.NewGlogHandler(log.NewTerminalHandlerWithLevel(os.Stdout, log.LevelInfo, true)) log.SetDefault(log.NewLogger(glogger)) @@ -151,3 +153,9 @@ func (s *ClientTestSuite) RevertL1Snapshot(snapshotID string) { s.Nil(s.RPCClient.L1.CallContext(context.Background(), &revertRes, "evm_revert", snapshotID)) s.True(revertRes) } + +func (s *ClientTestSuite) MineL1Block() { + var blockID string + s.Nil(s.RPCClient.L1.CallContext(context.Background(), &blockID, "evm_mine")) + s.NotEmpty(blockID) +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 12497d7e7..1260ecce3 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -17,15 +17,15 @@ func LoadEnv() { // load test environment variables. currentPath, err := os.Getwd() if err != nil { - log.Warn("get current path failed", "err", err) + log.Debug("get current path failed", "err", err) } path := strings.Split(currentPath, "/taiko-client") if len(path) == 0 { - log.Warn("not a taiko-client repo") + log.Debug("not a taiko-client repo") } err = godotenv.Load(fmt.Sprintf("%s/taiko-client/integration_test/.env", path[0])) if err != nil { - log.Warn("failed to load test env", "current path", currentPath, "err", err) + log.Debug("failed to load test env", "current path", currentPath, "err", err) } } diff --git a/pkg/rpc/utils.go b/pkg/rpc/utils.go index d75a14a9b..e4de73293 100644 --- a/pkg/rpc/utils.go +++ b/pkg/rpc/utils.go @@ -250,18 +250,28 @@ func GetBlockProofStatus( }, nil } -type AccountPoolContent map[string]map[string]*types.Transaction +type AccountPoolContent map[string]map[string]map[string]*types.Transaction +type AccountPoolContentFrom map[string]map[string]*types.Transaction + +// Content GetPendingTxs fetches the pending transactions from tx pool. +func Content(ctx context.Context, client *EthClient) (AccountPoolContent, error) { + ctxWithTimeout, cancel := ctxWithTimeoutOrDefault(ctx, defaultTimeout) + defer cancel() + + var result AccountPoolContent + return result, client.CallContext(ctxWithTimeout, &result, "txpool_content") +} // ContentFrom fetches a given account's transactions list from a node's transactions pool. func ContentFrom( ctx context.Context, rawRPC *EthClient, address common.Address, -) (AccountPoolContent, error) { +) (AccountPoolContentFrom, error) { ctxWithTimeout, cancel := ctxWithTimeoutOrDefault(ctx, defaultTimeout) defer cancel() - var result AccountPoolContent + var result AccountPoolContentFrom return result, rawRPC.CallContext( ctxWithTimeout, &result, From f7768c91d722e1cddfc932e5aec11b0f3fe863a0 Mon Sep 17 00:00:00 2001 From: maskpp Date: Fri, 23 Feb 2024 22:39:28 +0800 Subject: [PATCH 24/25] update some English wordings in transaction sender, no logic change --- internal/sender/common.go | 52 ++++--- internal/sender/sender.go | 266 ++++++++++++++++++--------------- internal/sender/sender_test.go | 22 +-- proposer/proposer.go | 2 +- proposer/proposer_test.go | 4 +- 5 files changed, 188 insertions(+), 158 deletions(-) diff --git a/internal/sender/common.go b/internal/sender/common.go index b0fbead89..87ab346b1 100644 --- a/internal/sender/common.go +++ b/internal/sender/common.go @@ -15,6 +15,8 @@ import ( "github.com/taikoxyz/taiko-client/pkg/rpc" ) +// adjustGas adjusts the gas fee cap and gas tip cap of the given transaction with the configured +// growth rate. func (s *Sender) adjustGas(txData types.TxData) { rate := s.GasGrowthRate + 100 switch baseTx := txData.(type) { @@ -46,22 +48,26 @@ func (s *Sender) adjustGas(txData types.TxData) { } } +// AdjustNonce adjusts the nonce of the given transaction with the current nonce of the sender. func (s *Sender) AdjustNonce(txData types.TxData) { nonce, err := s.client.NonceAt(s.ctx, s.Opts.From, nil) if err != nil { - log.Warn("failed to get the nonce", "from", s.Opts.From, "err", err) + log.Warn("Failed to get the nonce", "from", s.Opts.From, "err", err) return } s.Opts.Nonce = new(big.Int).SetUint64(nonce) - switch baseTx := txData.(type) { + switch tx := txData.(type) { case *types.DynamicFeeTx: - baseTx.Nonce = nonce + tx.Nonce = nonce case *types.BlobTx: - baseTx.Nonce = nonce + tx.Nonce = nonce + default: + log.Warn("Unsupported transaction type", "from", s.Opts.From) } } +// updateGasTipGasFee updates the gas tip cap and gas fee cap of the sender with the given chain head info. func (s *Sender) updateGasTipGasFee(head *types.Header) error { // Get the gas tip cap gasTipCap, err := s.client.SuggestGasTipCap(s.ctx) @@ -75,7 +81,7 @@ func (s *Sender) updateGasTipGasFee(head *types.Header) error { if gasFeeCap.Cmp(gasTipCap) < 0 { return fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap) } - maxGasFee := big.NewInt(int64(s.MaxGasFee)) + maxGasFee := new(big.Int).SetUint64(s.MaxGasFee) if gasFeeCap.Cmp(maxGasFee) > 0 { gasFeeCap = new(big.Int).Set(maxGasFee) gasTipCap = new(big.Int).Set(maxGasFee) @@ -87,7 +93,8 @@ func (s *Sender) updateGasTipGasFee(head *types.Header) error { return nil } -func (s *Sender) makeTxData(tx *types.Transaction) (types.TxData, error) { +// buildTxData assembles the transaction data from the given transaction. +func (s *Sender) buildTxData(tx *types.Transaction) (types.TxData, error) { switch tx.Type() { case types.DynamicFeeTxType: return &types.DynamicFeeTx{ @@ -125,6 +132,7 @@ func (s *Sender) makeTxData(tx *types.Transaction) (types.TxData, error) { } } +// handleReorgTransactions handles the transactions which are backed to the mempool due to reorg. func (s *Sender) handleReorgTransactions() { // nolint: unused content, err := rpc.Content(s.ctx, s.client) if err != nil { @@ -149,26 +157,27 @@ func (s *Sender) handleReorgTransactions() { // nolint: unused } // Remove the already handled transactions. for _, confirm := range s.unconfirmedTxs.Items() { - delete(txs, confirm.Tx.Hash()) + delete(txs, confirm.CurrentTx.Hash()) } for _, tx := range txs { - baseTx, err := s.makeTxData(tx) + baseTx, err := s.buildTxData(tx) if err != nil { log.Warn("failed to make the transaction data when handle reorg txs", "tx_hash", tx.Hash().String(), "err", err) return } txID := uuid.New() - confirm := &TxConfirm{ - TxID: txID, - Tx: tx, - baseTx: baseTx, + confirm := &TxToConfirm{ + ID: txID, + CurrentTx: tx, + originalTx: baseTx, } s.unconfirmedTxs.Set(txID, confirm) - s.txConfirmCh.Set(txID, make(chan *TxConfirm, 1)) + s.txToConfirmCh.Set(txID, make(chan *TxToConfirm, 1)) log.Info("handle reorg tx", "tx_hash", tx.Hash().String(), "tx_id", txID) } } +// setDefault sets the default value if the given value is 0. func setDefault[T uint64 | time.Duration](src, dest T) T { if src == 0 { return dest @@ -176,17 +185,18 @@ func setDefault[T uint64 | time.Duration](src, dest T) T { return src } -func setConfig(config *Config) *Config { +// setConfigWithDefaultValues sets the config with default values if the given config is nil. +func setConfigWithDefaultValues(config *Config) *Config { if config == nil { return DefaultConfig } return &Config{ - Confirm: setDefault(config.Confirm, DefaultConfig.Confirm), - RetryTimes: setDefault(config.RetryTimes, DefaultConfig.RetryTimes), - MaxWaitingTime: setDefault(config.MaxWaitingTime, DefaultConfig.MaxWaitingTime), - GasLimit: setDefault(config.GasLimit, DefaultConfig.GasLimit), - GasGrowthRate: setDefault(config.GasGrowthRate, DefaultConfig.GasGrowthRate), - MaxGasFee: setDefault(config.MaxGasFee, DefaultConfig.MaxGasFee), - MaxBlobFee: setDefault(config.MaxBlobFee, DefaultConfig.MaxBlobFee), + ConfirmationDepth: setDefault(config.ConfirmationDepth, DefaultConfig.ConfirmationDepth), + MaxRetrys: setDefault(config.MaxRetrys, DefaultConfig.MaxRetrys), + MaxWaitingTime: setDefault(config.MaxWaitingTime, DefaultConfig.MaxWaitingTime), + GasLimit: setDefault(config.GasLimit, DefaultConfig.GasLimit), + GasGrowthRate: setDefault(config.GasGrowthRate, DefaultConfig.GasGrowthRate), + MaxGasFee: setDefault(config.MaxGasFee, DefaultConfig.MaxGasFee), + MaxBlobFee: setDefault(config.MaxBlobFee, DefaultConfig.MaxBlobFee), } } diff --git a/internal/sender/sender.go b/internal/sender/sender.go index 2923cc2a1..765c76104 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" cmap "github.com/orcaman/concurrent-map/v2" "github.com/pborman/uuid" @@ -21,68 +22,75 @@ import ( ) var ( - rootSenders = map[uint64]map[common.Address]*Sender{} - DefaultConfig = &Config{ - Confirm: 0, - RetryTimes: 0, - MaxWaitingTime: time.Minute * 5, - GasLimit: 21000, - GasGrowthRate: 50, - MaxGasFee: 20000000000, - MaxBlobFee: 1000000000, + sendersMap = map[uint64]map[common.Address]*Sender{} + unconfirmedTxsCap = 100 + nonceIncorrectRetrys = 3 + unconfirmedTxsCheckInternal = 2 * time.Second + chainHeadFetchInterval = 3 * time.Second // nolint:unused + errTimeoutInMempool = fmt.Errorf("transaction in mempool for too long") + DefaultConfig = &Config{ + ConfirmationDepth: 0, + MaxRetrys: 0, + MaxWaitingTime: 5 * time.Minute, + GasLimit: params.TxGas, + GasGrowthRate: 50, + MaxGasFee: 20_000_000_000, + MaxBlobFee: 1_000_000_000, } ) +// Config represents the configuration of the transaction sender. type Config struct { - // The minimum confirmations to consider the transaction is confirmed. - Confirm uint64 - // The maximum retry times to send transaction, 0 means retry until succeed. - RetryTimes uint64 - // The maximum waiting time for transaction in mempool. + // The minimum block confirmations to wait to confirm a transaction. + ConfirmationDepth uint64 + // The maximum retry times when sending transactions. + MaxRetrys uint64 + // The maximum waiting time for the inclusion of transactions. MaxWaitingTime time.Duration - // The gas limit for raw transaction. + // The gas limit for transactions. GasLimit uint64 // The gas rate to increase the gas price, 20 means 20% gas growth rate. GasGrowthRate uint64 - // The maximum gas price can be used to send transaction. + // The maximum gas fee can be used when sending transactions. MaxGasFee uint64 MaxBlobFee uint64 } -type TxConfirm struct { - RetryTimes uint64 - confirm uint64 +// TxToConfirm represents a transaction which is waiting for its confirmation. +type TxToConfirm struct { + confirmations uint64 + originalTx types.TxData - TxID string - - baseTx types.TxData - Tx *types.Transaction - Receipt *types.Receipt + ID string + Retrys uint64 + CurrentTx *types.Transaction + Receipt *types.Receipt Err error } +// Sender represents a global transaction sender. type Sender struct { ctx context.Context *Config - header *types.Header + head *types.Header client *rpc.EthClient Opts *bind.TransactOpts - unconfirmedTxs cmap.ConcurrentMap[string, *TxConfirm] //uint64]*TxConfirm - txConfirmCh cmap.ConcurrentMap[string, chan *TxConfirm] + unconfirmedTxs cmap.ConcurrentMap[string, *TxToConfirm] + txToConfirmCh cmap.ConcurrentMap[string, chan *TxToConfirm] mu sync.Mutex wg sync.WaitGroup stopCh chan struct{} } -// NewSender returns a new instance of Sender. +// NewSender creates a new instance of Sender. func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ecdsa.PrivateKey) (*Sender, error) { - cfg = setConfig(cfg) + cfg = setConfigWithDefaultValues(cfg) // Create a new transactor opts, err := bind.NewKeyedTransactorWithChainID(priv, client.ChainID) @@ -93,8 +101,8 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec opts.NoSend = true // Add the sender to the root sender. - if root := rootSenders[client.ChainID.Uint64()]; root == nil { - rootSenders[client.ChainID.Uint64()] = map[common.Address]*Sender{} + if root := sendersMap[client.ChainID.Uint64()]; root == nil { + sendersMap[client.ChainID.Uint64()] = map[common.Address]*Sender{} } else { if root[opts.From] != nil { return nil, fmt.Errorf("sender already exists") @@ -102,7 +110,7 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec } // Get the chain ID - header, err := client.HeaderByNumber(ctx, nil) + head, err := client.HeaderByNumber(ctx, nil) if err != nil { return nil, err } @@ -110,22 +118,22 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec sender := &Sender{ ctx: ctx, Config: cfg, - header: header, + head: head, client: client, Opts: opts, - unconfirmedTxs: cmap.New[*TxConfirm](), - txConfirmCh: cmap.New[chan *TxConfirm](), + unconfirmedTxs: cmap.New[*TxToConfirm](), + txToConfirmCh: cmap.New[chan *TxToConfirm](), stopCh: make(chan struct{}), } - // Set the nonce + // Initialize the nonce sender.AdjustNonce(nil) - // Update the gas tip and gas fee. - err = sender.updateGasTipGasFee(header) - if err != nil { + + // Initialize the gas fee related fields + if sender.updateGasTipGasFee(head) != nil { return nil, err } if os.Getenv("RUN_TESTS") == "" { - rootSenders[client.ChainID.Uint64()][opts.From] = sender + sendersMap[client.ChainID.Uint64()][opts.From] = sender } sender.wg.Add(1) @@ -139,19 +147,19 @@ func (s *Sender) Close() { s.wg.Wait() } -// ConfirmChannel returns a channel to receive the transaction confirmation. -func (s *Sender) ConfirmChannel(txID string) <-chan *TxConfirm { - confirmCh, ok := s.txConfirmCh.Get(txID) +// TxToConfirmChannel returns a channel to wait the given transaction's confirmation. +func (s *Sender) TxToConfirmChannel(txID string) <-chan *TxToConfirm { + ch, ok := s.txToConfirmCh.Get(txID) if !ok { - log.Warn("transaction not found", "tx_id", txID) + log.Warn("Transaction not found", "id", txID) } - return confirmCh + return ch } -// ConfirmChannels returns all the transaction confirmation channels. -func (s *Sender) ConfirmChannels() map[string]<-chan *TxConfirm { - channels := map[string]<-chan *TxConfirm{} - for txID, confirmCh := range s.txConfirmCh.Items() { +// TxToConfirmChannels returns channels to wait the given transactions confirmation. +func (s *Sender) TxToConfirmChannels() map[string]<-chan *TxToConfirm { + channels := map[string]<-chan *TxToConfirm{} + for txID, confirmCh := range s.txToConfirmCh.Items() { channels[txID] = confirmCh } return channels @@ -159,15 +167,15 @@ func (s *Sender) ConfirmChannels() map[string]<-chan *TxConfirm { // GetUnconfirmedTx returns the unconfirmed transaction by the transaction ID. func (s *Sender) GetUnconfirmedTx(txID string) *types.Transaction { - txConfirm, ok := s.unconfirmedTxs.Get(txID) + txToConfirm, ok := s.unconfirmedTxs.Get(txID) if !ok { return nil } - return txConfirm.Tx + return txToConfirm.CurrentTx } -// SendRaw sends a transaction to the target address. -func (s *Sender) SendRaw(nonce uint64, target *common.Address, value *big.Int, data []byte) (string, error) { +// SendRawTransaction sends a transaction to the given Ethereum node. +func (s *Sender) SendRawTransaction(nonce uint64, target *common.Address, value *big.Int, data []byte) (string, error) { return s.SendTransaction(types.NewTx(&types.DynamicFeeTx{ ChainID: s.client.ChainID, To: target, @@ -180,70 +188,73 @@ func (s *Sender) SendRaw(nonce uint64, target *common.Address, value *big.Int, d })) } -// SendTransaction sends a transaction to the target address. +// SendTransaction sends a transaction to the given Ethereum node. func (s *Sender) SendTransaction(tx *types.Transaction) (string, error) { - if s.unconfirmedTxs.Count() >= 100 { + if s.unconfirmedTxs.Count() >= unconfirmedTxsCap { return "", fmt.Errorf("too many pending transactions") } - txData, err := s.makeTxData(tx) + txData, err := s.buildTxData(tx) if err != nil { return "", err } + txID := uuid.New() - confirmTx := &TxConfirm{ - TxID: txID, - baseTx: txData, - Tx: tx, + txToConfirm := &TxToConfirm{ + ID: txID, + originalTx: txData, + CurrentTx: tx, } - err = s.sendTx(confirmTx) - if err != nil && !strings.Contains(err.Error(), "replacement transaction") { - log.Error("failed to send transaction", "tx_id", txID, "tx_hash", tx.Hash().String(), "err", err) + + if err := s.send(txToConfirm); err != nil && !strings.Contains(err.Error(), "replacement transaction") { + log.Error("Failed to send transaction", "id", txID, "hash", tx.Hash(), "err", err) return "", err } + // Add the transaction to the unconfirmed transactions - s.unconfirmedTxs.Set(txID, confirmTx) - s.txConfirmCh.Set(txID, make(chan *TxConfirm, 1)) + s.unconfirmedTxs.Set(txID, txToConfirm) + s.txToConfirmCh.Set(txID, make(chan *TxToConfirm, 1)) return txID, nil } -func (s *Sender) sendTx(confirmTx *TxConfirm) error { +// send is the internal method to send the given transaction. +func (s *Sender) send(tx *TxToConfirm) error { s.mu.Lock() defer s.mu.Unlock() - baseTx := confirmTx.baseTx + originalTx := tx.originalTx - for i := 0; i < 3; i++ { - // Try 3 RetryTimes if nonce is not correct. - rawTx, err := s.Opts.Signer(s.Opts.From, types.NewTx(baseTx)) + for i := 0; i < nonceIncorrectRetrys; i++ { + // Retry when nonce is incorrect + rawTx, err := s.Opts.Signer(s.Opts.From, types.NewTx(originalTx)) if err != nil { return err } - confirmTx.Tx = rawTx - err = s.client.SendTransaction(s.ctx, rawTx) - confirmTx.Err = err - // Check if the error is nonce too low. + tx.CurrentTx = rawTx + tx.Err = s.client.SendTransaction(s.ctx, rawTx) + // Check if the error is nonce too low if err != nil { if strings.Contains(err.Error(), "nonce too low") { - s.AdjustNonce(baseTx) - log.Warn("nonce is not correct, retry to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) + s.AdjustNonce(originalTx) + log.Warn("Nonce is incorrect, retry sending the transaction with new nonce", "hash", rawTx.Hash(), "err", err) continue } if err.Error() == "replacement transaction underpriced" { - s.adjustGas(baseTx) - log.Warn("replacement transaction underpriced", "tx_hash", rawTx.Hash().String(), "err", err) + s.adjustGas(originalTx) + log.Warn("Replacement transaction underpriced", "hash", rawTx.Hash(), "err", err) continue } - log.Error("failed to send transaction", "tx_hash", rawTx.Hash().String(), "err", err) + log.Error("Failed to send transaction", "hash", rawTx.Hash(), "err", err) return err } - s.Opts.Nonce = big.NewInt(s.Opts.Nonce.Int64() + 1) + s.Opts.Nonce = new(big.Int).Add(s.Opts.Nonce, common.Big1) break } return nil } +// loop is the main event loop of the transaction sender. func (s *Sender) loop() { defer s.wg.Done() @@ -256,100 +267,109 @@ func (s *Sender) loop() { } defer sub.Unsubscribe() - tick := time.NewTicker(time.Second * 2) - defer tick.Stop() + unconfirmedTxsCheckTicker := time.NewTicker(unconfirmedTxsCheckInternal) + defer unconfirmedTxsCheckTicker.Stop() for { select { - case <-tick.C: - s.resendTransaction() - case header := <-headCh: + case <-s.ctx.Done(): + return + case <-s.stopCh: + return + case <-unconfirmedTxsCheckTicker.C: + s.resendUnconfirmedTxs() + case newHead := <-headCh: // If chain appear reorg then handle mempool transactions. - // Handle reorg transactions + // TODO(Huan): handle reorg transactions //if s.header.Hash() != header.ParentHash { - // TODO handle reorg transactions //s.handleReorgTransactions() //} - s.header = header + s.head = newHead // Update the gas tip and gas fee - err = s.updateGasTipGasFee(header) - if err != nil { - log.Warn("failed to update gas tip and gas fee", "err", err) + if err = s.updateGasTipGasFee(newHead); err != nil { + log.Warn("Failed to update gas tip and gas fee", "err", err) } // Check the unconfirmed transactions - s.checkPendingTransactions() - case <-s.ctx.Done(): - return - case <-s.stopCh: - return + s.checkPendingTransactionsConfirmation() } } } -func (s *Sender) resendTransaction() { - for txID, txConfirm := range s.unconfirmedTxs.Items() { - if txConfirm.Err == nil { +// resendUnconfirmedTxs resends all unconfirmed transactions. +func (s *Sender) resendUnconfirmedTxs() { + for id, unconfirmedTx := range s.unconfirmedTxs.Items() { + if unconfirmedTx.Err == nil { continue } - txConfirm.RetryTimes++ - if s.RetryTimes != 0 && txConfirm.RetryTimes >= s.RetryTimes { - s.releaseConfirm(txID) + unconfirmedTx.Retrys++ + if s.MaxRetrys != 0 && unconfirmedTx.Retrys >= s.MaxRetrys { + s.releaseUnconfirmedTx(id) continue } - _ = s.sendTx(txConfirm) + if err := s.send(unconfirmedTx); err != nil { + log.Warn( + "Failed to resend the transaction", + "id", id, + "retrys", unconfirmedTx.Retrys, + "err", err, + ) + } } } -func (s *Sender) checkPendingTransactions() { - for txID, txConfirm := range s.unconfirmedTxs.Items() { - if txConfirm.Err != nil { +// checkPendingTransactionsConfirmation checks the confirmation of the pending transactions. +func (s *Sender) checkPendingTransactionsConfirmation() { + for id, pendingTx := range s.unconfirmedTxs.Items() { + if pendingTx.Err != nil { continue } - if txConfirm.Receipt == nil { + if pendingTx.Receipt == nil { // Ignore the transaction if it is pending. - tx, isPending, err := s.client.TransactionByHash(s.ctx, txConfirm.Tx.Hash()) + tx, isPending, err := s.client.TransactionByHash(s.ctx, pendingTx.CurrentTx.Hash()) if err != nil { + log.Warn("Failed to fetch transaction", "hash", pendingTx.CurrentTx.Hash(), "err", err) continue } if isPending { // If the transaction is in mempool for too long, replace it. - if waitTime := time.Since(tx.Time()); waitTime > s.MaxWaitingTime { - txConfirm.Err = fmt.Errorf("transaction in mempool for too long") + if time.Since(tx.Time()) > s.MaxWaitingTime { + pendingTx.Err = errTimeoutInMempool } continue } // Get the transaction receipt. - receipt, err := s.client.TransactionReceipt(s.ctx, txConfirm.Tx.Hash()) + receipt, err := s.client.TransactionReceipt(s.ctx, pendingTx.CurrentTx.Hash()) if err != nil { if err.Error() == "not found" { - txConfirm.Err = err - s.releaseConfirm(txID) + pendingTx.Err = err + s.releaseUnconfirmedTx(id) } - log.Warn("failed to get the transaction receipt", "tx_hash", txConfirm.Tx.Hash().String(), "err", err) + log.Warn("Failed to get the transaction receipt", "hash", pendingTx.CurrentTx.Hash(), "err", err) continue } - txConfirm.Receipt = receipt + pendingTx.Receipt = receipt if receipt.Status != types.ReceiptStatusSuccessful { - txConfirm.Err = fmt.Errorf("transaction reverted, hash: %s", receipt.TxHash.String()) - s.releaseConfirm(txID) + pendingTx.Err = fmt.Errorf("transaction reverted, hash: %s", receipt.TxHash) + s.releaseUnconfirmedTx(id) continue } } - txConfirm.confirm = s.header.Number.Uint64() - txConfirm.Receipt.BlockNumber.Uint64() - if txConfirm.confirm >= s.Confirm { - s.releaseConfirm(txID) + pendingTx.confirmations = s.head.Number.Uint64() - pendingTx.Receipt.BlockNumber.Uint64() + if pendingTx.confirmations >= s.ConfirmationDepth { + s.releaseUnconfirmedTx(id) } } } -func (s *Sender) releaseConfirm(txID string) { +// releaseUnconfirmedTx releases the unconfirmed transaction by the transaction ID. +func (s *Sender) releaseUnconfirmedTx(txID string) { txConfirm, _ := s.unconfirmedTxs.Get(txID) - confirmCh, _ := s.txConfirmCh.Get(txID) + confirmCh, _ := s.txToConfirmCh.Get(txID) select { case confirmCh <- txConfirm: default: } // Remove the transaction from the unconfirmed transactions s.unconfirmedTxs.Remove(txID) - s.txConfirmCh.Remove(txID) + s.txToConfirmCh.Remove(txID) } diff --git a/internal/sender/sender_test.go b/internal/sender/sender_test.go index 9b2e79095..80e8722ca 100644 --- a/internal/sender/sender_test.go +++ b/internal/sender/sender_test.go @@ -30,13 +30,13 @@ func (s *SenderTestSuite) TestNormalSender() { i := i eg.Go(func() error { addr := common.BigToAddress(big.NewInt(int64(i))) - _, err := s.sender.SendRaw(s.sender.Opts.Nonce.Uint64(), &addr, big.NewInt(1), nil) + _, err := s.sender.SendRawTransaction(s.sender.Opts.Nonce.Uint64(), &addr, big.NewInt(1), nil) return err }) } s.Nil(eg.Wait()) - for _, confirmCh := range s.sender.ConfirmChannels() { + for _, confirmCh := range s.sender.TxToConfirmChannels() { confirm := <-confirmCh s.Nil(confirm.Err) } @@ -77,19 +77,19 @@ func (s *SenderTestSuite) TestReplacement() { s.Nil(err) // Replace the transaction with a higher nonce. - _, err = send.SendRaw(nonce, &common.Address{}, big.NewInt(1), nil) + _, err = send.SendRawTransaction(nonce, &common.Address{}, big.NewInt(1), nil) s.Nil(err) time.Sleep(time.Second * 6) // Send a transaction with a next nonce and let all the transactions be confirmed. - _, err = send.SendRaw(nonce-1, &common.Address{}, big.NewInt(1), nil) + _, err = send.SendRawTransaction(nonce-1, &common.Address{}, big.NewInt(1), nil) s.Nil(err) - for _, confirmCh := range send.ConfirmChannels() { + for _, confirmCh := range send.TxToConfirmChannels() { confirm := <-confirmCh // Check the replaced transaction's gasFeeTap touch the max gas price. - if confirm.Tx.Nonce() == nonce { - s.Equal(send.MaxGasFee, confirm.Tx.GasFeeCap().Uint64()) + if confirm.CurrentTx.Nonce() == nonce { + s.Equal(send.MaxGasFee, confirm.CurrentTx.GasFeeCap().Uint64()) } s.Nil(confirm.Err) } @@ -112,11 +112,11 @@ func (s *SenderTestSuite) TestNonceTooLow() { return } - txID, err := send.SendRaw(nonce-3, &common.Address{}, big.NewInt(1), nil) + txID, err := send.SendRawTransaction(nonce-3, &common.Address{}, big.NewInt(1), nil) s.Nil(err) - confirm := <-send.ConfirmChannel(txID) + confirm := <-send.TxToConfirmChannel(txID) s.Nil(confirm.Err) - s.Equal(nonce, confirm.Tx.Nonce()) + s.Equal(nonce, confirm.CurrentTx.Nonce()) } func (s *SenderTestSuite) SetupTest() { @@ -129,7 +129,7 @@ func (s *SenderTestSuite) SetupTest() { s.sender, err = sender.NewSender(ctx, &sender.Config{ MaxGasFee: 20000000000, GasGrowthRate: 50, - RetryTimes: 0, + MaxRetrys: 0, GasLimit: 2000000, MaxWaitingTime: time.Second * 10, }, s.RPCClient.L1, priv) diff --git a/proposer/proposer.go b/proposer/proposer.go index ed9aaa42f..0bd52cb4d 100644 --- a/proposer/proposer.go +++ b/proposer/proposer.go @@ -489,7 +489,7 @@ func (p *Proposer) ProposeTxList( } // Waiting for the transaction to be confirmed. - confirm := <-p.sender.ConfirmChannel(txID) + confirm := <-p.sender.TxToConfirmChannel(txID) if confirm.Err != nil { return confirm.Err } diff --git a/proposer/proposer_test.go b/proposer/proposer_test.go index 78851e08d..e5a1e23b1 100644 --- a/proposer/proposer_test.go +++ b/proposer/proposer_test.go @@ -169,7 +169,7 @@ func (s *ProposerTestSuite) TestSendProposeBlockTx() { nonce, err := s.RPCClient.L1.PendingNonceAt(context.Background(), s.p.proposerAddress) s.Nil(err) - txID, err := sender.SendRaw(nonce, &common.Address{}, common.Big1, nil) + txID, err := sender.SendRawTransaction(nonce, &common.Address{}, common.Big1, nil) s.Nil(err) tx := sender.GetUnconfirmedTx(txID) @@ -186,7 +186,7 @@ func (s *ProposerTestSuite) TestSendProposeBlockTx() { } s.Nil(err) - txID, err = sender.SendRaw(nonce, newTx.To(), newTx.Value(), newTx.Data()) + txID, err = sender.SendRawTransaction(nonce, newTx.To(), newTx.Value(), newTx.Data()) s.Nil(err) newTx = sender.GetUnconfirmedTx(txID) From 6c5c7ee0301add332f384a699ebcc3acd1c8b40a Mon Sep 17 00:00:00 2001 From: maskpp Date: Fri, 23 Feb 2024 22:56:01 +0800 Subject: [PATCH 25/25] fix mistake changes --- internal/sender/sender.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/sender/sender.go b/internal/sender/sender.go index 765c76104..3007b571d 100644 --- a/internal/sender/sender.go +++ b/internal/sender/sender.go @@ -129,7 +129,7 @@ func NewSender(ctx context.Context, cfg *Config, client *rpc.EthClient, priv *ec sender.AdjustNonce(nil) // Initialize the gas fee related fields - if sender.updateGasTipGasFee(head) != nil { + if err = sender.updateGasTipGasFee(head); err != nil { return nil, err } if os.Getenv("RUN_TESTS") == "" { @@ -232,7 +232,8 @@ func (s *Sender) send(tx *TxToConfirm) error { return err } tx.CurrentTx = rawTx - tx.Err = s.client.SendTransaction(s.ctx, rawTx) + err = s.client.SendTransaction(s.ctx, rawTx) + tx.Err = err // Check if the error is nonce too low if err != nil { if strings.Contains(err.Error(), "nonce too low") {