Skip to content

Commit fd2bf15

Browse files
authored
tx retry with higher gas price (#96)
* tx retry with higher gas price
1 parent 6d45bae commit fd2bf15

File tree

7 files changed

+286
-124
lines changed

7 files changed

+286
-124
lines changed

cmd/upload.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ type uploadArgument struct {
5656
routines int
5757

5858
fragmentSize int64
59+
maxGasPrice uint
60+
nRetries int
61+
step int64
5962

6063
timeout time.Duration
6164
}
@@ -79,6 +82,9 @@ func bindUploadFlags(cmd *cobra.Command, args *uploadArgument) {
7982
cmd.Flags().Int64Var(&args.fragmentSize, "fragment-size", 1024*1024*1024*4, "the size of fragment to split into when file is too large")
8083

8184
cmd.Flags().IntVar(&args.routines, "routines", runtime.GOMAXPROCS(0), "number of go routines for uploading simutanously")
85+
cmd.Flags().UintVar(&args.maxGasPrice, "max-gas-price", 0, "max gas price to send transaction")
86+
cmd.Flags().IntVar(&args.nRetries, "n-retries", 0, "number of retries for uploading when it's not gas price issue")
87+
cmd.Flags().Int64Var(&args.step, "step", 15, "step of gas price increasing, step / 10 (for 15, the new gas price is 1.5 * last gas price)")
8288

8389
cmd.Flags().DurationVar(&args.timeout, "timeout", 0, "cli task timeout, 0 for no timeout")
8490
}
@@ -124,6 +130,11 @@ func upload(*cobra.Command, []string) {
124130
if uploadArgs.finalityRequired {
125131
finalityRequired = transfer.FileFinalized
126132
}
133+
134+
var maxGasPrice *big.Int
135+
if uploadArgs.maxGasPrice > 0 {
136+
maxGasPrice = big.NewInt(int64(uploadArgs.maxGasPrice))
137+
}
127138
opt := transfer.UploadOption{
128139
Tags: hexutil.MustDecode(uploadArgs.tags),
129140
FinalityRequired: finalityRequired,
@@ -132,6 +143,9 @@ func upload(*cobra.Command, []string) {
132143
SkipTx: uploadArgs.skipTx,
133144
Fee: fee,
134145
Nonce: nonce,
146+
MaxGasPrice: maxGasPrice,
147+
NRetries: uploadArgs.nRetries,
148+
Step: uploadArgs.step,
135149
}
136150

137151
file, err := core.Open(uploadArgs.file)

common/blockchain/rpc.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
var Web3LogEnabled bool
2121

2222
type RetryOption struct {
23+
NRetries int
2324
Interval time.Duration
2425
logger *logrus.Logger
2526
}
@@ -70,12 +71,18 @@ func WaitForReceipt(ctx context.Context, client *web3go.Client, txHash common.Ha
7071
opt.Interval = time.Second * 3
7172
}
7273

74+
if opt.Interval == 0 {
75+
opt.Interval = time.Second * 3
76+
}
77+
7378
reminder := util.NewReminder(opt.logger, time.Minute)
7479
for receipt == nil {
7580
if receipt, err = client.WithContext(ctx).Eth.TransactionReceipt(txHash); err != nil {
7681
return nil, err
7782
}
7883

84+
logrus.WithField("txHash", txHash).WithField("receipt", receipt).Info("Transaction receipt")
85+
7986
// remind
8087
if receipt == nil {
8188
reminder.RemindWith("Transaction not executed yet", "hash", txHash)

contract/contract.go

Lines changed: 149 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import (
1010
"github.com/0glabs/0g-storage-client/common/blockchain"
1111
"github.com/ethereum/go-ethereum/accounts/abi/bind"
1212
"github.com/ethereum/go-ethereum/common"
13-
"github.com/ethereum/go-ethereum/core/types"
1413
"github.com/ethereum/go-ethereum/crypto"
1514
"github.com/openweb3/web3go"
15+
"github.com/openweb3/web3go/types"
16+
"github.com/pkg/errors"
1617
"github.com/sirupsen/logrus"
1718
)
1819

@@ -26,11 +27,13 @@ type TxRetryOption struct {
2627
Timeout time.Duration
2728
MaxNonGasRetries int
2829
MaxGasPrice *big.Int
30+
Step int64
2931
}
3032

3133
var SpecifiedBlockError = "Specified block header does not exist"
32-
var DefaultTimeout = 30 * time.Second
33-
var DefaultMaxNonGasRetries = 10
34+
var DefaultTimeout = 15 * time.Minute
35+
var DefaultMaxNonGasRetries = 20
36+
var DefaultStep = int64(15)
3437

3538
func IsRetriableSubmitLogEntryError(msg string) bool {
3639
return strings.Contains(msg, SpecifiedBlockError) || strings.Contains(msg, "mempool") || strings.Contains(msg, "timeout")
@@ -52,6 +55,22 @@ func NewFlowContract(flowAddress common.Address, clientWithSigner *web3go.Client
5255
return &FlowContract{contract, flow, clientWithSigner}, nil
5356
}
5457

58+
func (f *FlowContract) GetNonce(ctx context.Context) (*big.Int, error) {
59+
sm, err := f.clientWithSigner.GetSignerManager()
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
addr := sm.List()[0].Address()
65+
66+
nonce, err := f.clientWithSigner.Eth.TransactionCount(addr, nil)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
return nonce, nil
72+
}
73+
5574
func (f *FlowContract) GetGasPrice() (*big.Int, error) {
5675
gasPrice, err := f.clientWithSigner.Eth.GasPrice()
5776
if err != nil {
@@ -110,68 +129,161 @@ func TransactWithGasAdjustment(
110129
opts *bind.TransactOpts,
111130
retryOpts *TxRetryOption,
112131
params ...interface{},
113-
) (*types.Transaction, error) {
132+
) (*types.Receipt, error) {
114133
// Set timeout and max non-gas retries from retryOpts if provided.
115134
if retryOpts == nil {
116135
retryOpts = &TxRetryOption{
117-
Timeout: DefaultTimeout,
118136
MaxNonGasRetries: DefaultMaxNonGasRetries,
119137
}
120138
}
121139

140+
if retryOpts.MaxNonGasRetries == 0 {
141+
retryOpts.MaxNonGasRetries = DefaultMaxNonGasRetries
142+
}
143+
144+
if retryOpts.Step == 0 {
145+
retryOpts.Step = DefaultStep
146+
}
147+
148+
if t, ok := opts.Context.Deadline(); ok {
149+
retryOpts.Timeout = time.Until(t)
150+
}
151+
152+
logrus.WithField("timeout", retryOpts.Timeout).WithField("maxNonGasRetries", retryOpts.MaxNonGasRetries).Debug("Set retry options")
153+
154+
if opts.Nonce == nil {
155+
// Get the current nonce if not set.
156+
nonce, err := contract.GetNonce(opts.Context)
157+
if err != nil {
158+
return nil, err
159+
}
160+
// add one to the nonce
161+
opts.Nonce = nonce
162+
}
163+
164+
logrus.WithField("nonce", opts.Nonce).Info("Set nonce")
165+
122166
if opts.GasPrice == nil {
123167
// Get the current gas price if not set.
124168
gasPrice, err := contract.GetGasPrice()
125169
if err != nil {
126-
return nil, fmt.Errorf("failed to get gas price: %w", err)
170+
return nil, errors.WithMessage(err, "failed to get gas price")
127171
}
128172
opts.GasPrice = gasPrice
129173
logrus.WithField("gasPrice", opts.GasPrice).Debug("Receive current gas price from chain node")
130174
}
131175

132176
logrus.WithField("gasPrice", opts.GasPrice).Info("Set gas price")
133177

134-
nRetries := 0
135-
for {
136-
// Create a fresh context per iteration.
137-
ctx, cancel := context.WithTimeout(context.Background(), retryOpts.Timeout)
138-
opts.Context = ctx
139-
tx, err := contract.FlowTransactor.contract.Transact(opts, method, params...)
140-
cancel() // cancel this iteration's context
141-
if err == nil {
142-
return tx, nil
143-
}
178+
receiptCh := make(chan *types.Receipt, 1)
179+
errCh := make(chan error, 1)
180+
failCh := make(chan error, 1)
144181

145-
errStr := strings.ToLower(err.Error())
182+
var ctx context.Context
183+
var cancel context.CancelFunc
184+
if retryOpts.Timeout > 0 {
185+
ctx, cancel = context.WithTimeout(context.Background(), retryOpts.Timeout)
186+
} else {
187+
ctx, cancel = context.WithCancel(context.Background())
188+
}
146189

147-
if !IsRetriableSubmitLogEntryError(errStr) {
148-
return nil, fmt.Errorf("failed to send transaction: %w", err)
190+
// calculate number of gas retry by dividing max gas price by current gas price and the ration
191+
nGasRetry := 0
192+
if retryOpts.MaxGasPrice != nil {
193+
gasPrice := opts.GasPrice
194+
for gasPrice.Cmp(retryOpts.MaxGasPrice) <= 0 {
195+
gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(retryOpts.Step))
196+
gasPrice.Div(gasPrice, big.NewInt(10))
197+
nGasRetry++
149198
}
199+
}
150200

151-
if strings.Contains(errStr, "mempool") || strings.Contains(errStr, "timeout") {
152-
if retryOpts.MaxGasPrice == nil {
153-
return nil, fmt.Errorf("mempool full and no max gas price is set, failed to send transaction: %w", err)
154-
} else {
155-
newGasPrice := new(big.Int).Mul(opts.GasPrice, big.NewInt(11))
156-
newGasPrice.Div(newGasPrice, big.NewInt(10))
157-
if newGasPrice.Cmp(retryOpts.MaxGasPrice) > 0 {
158-
opts.GasPrice = new(big.Int).Set(retryOpts.MaxGasPrice)
159-
} else {
160-
opts.GasPrice = newGasPrice
201+
go func() {
202+
nRetries := 0
203+
for {
204+
select {
205+
case <-ctx.Done():
206+
// main or another goroutine canceled the context
207+
logrus.Info("Context canceled; stopping outer loop")
208+
return
209+
default:
210+
}
211+
tx, err := contract.FlowTransactor.contract.Transact(opts, method, params...)
212+
213+
var receipt *types.Receipt
214+
if err == nil {
215+
// Wait for successful execution
216+
go func() {
217+
receipt, err = contract.WaitForReceipt(ctx, tx.Hash(), true, blockchain.RetryOption{NRetries: retryOpts.MaxNonGasRetries})
218+
if err == nil {
219+
receiptCh <- receipt
220+
return
221+
}
222+
errCh <- err
223+
}()
224+
// even if the receipt is received, this loop will continue until the context is canceled
225+
time.Sleep(30 * time.Second)
226+
err = fmt.Errorf("timeout")
227+
}
228+
229+
logrus.WithError(err).Error("Failed to send transaction")
230+
231+
errStr := strings.ToLower(err.Error())
232+
233+
if !IsRetriableSubmitLogEntryError(errStr) {
234+
if strings.Contains(errStr, "invalid nonce") {
235+
return
161236
}
162-
logrus.WithError(err).Infof("Increasing gas price to %v due to mempool/timeout error", opts.GasPrice)
237+
failCh <- errors.WithMessage(err, "failed to send transaction")
238+
return
163239
}
164-
} else {
165-
nRetries++
166-
if nRetries >= retryOpts.MaxNonGasRetries {
167-
return nil, fmt.Errorf("failed to send transaction after %d retries: %w", nRetries, err)
240+
241+
// If the error is due to mempool full or timeout, retry with a higher gas price
242+
if strings.Contains(errStr, "mempool") || strings.Contains(errStr, "timeout") {
243+
if retryOpts.MaxGasPrice == nil {
244+
failCh <- errors.WithMessage(err, "mempool full and no max gas price is set, failed to send transaction")
245+
return
246+
} else if opts.GasPrice.Cmp(retryOpts.MaxGasPrice) >= 0 {
247+
return
248+
} else {
249+
newGasPrice := new(big.Int).Mul(opts.GasPrice, big.NewInt(retryOpts.Step))
250+
newGasPrice.Div(newGasPrice, big.NewInt(10))
251+
if newGasPrice.Cmp(retryOpts.MaxGasPrice) > 0 {
252+
opts.GasPrice = new(big.Int).Set(retryOpts.MaxGasPrice)
253+
} else {
254+
opts.GasPrice = newGasPrice
255+
}
256+
logrus.WithError(err).Infof("Increasing gas price to %v due to mempool/timeout error", opts.GasPrice)
257+
}
258+
} else {
259+
nRetries++
260+
if nRetries >= retryOpts.MaxNonGasRetries {
261+
failCh <- errors.WithMessage(err, "failed to send transaction")
262+
return
263+
}
264+
logrus.WithError(err).Infof("Retrying with same gas price %v, attempt %d", opts.GasPrice, nRetries)
168265
}
169-
logrus.WithError(err).Infof("Retrying with same gas price %v, attempt %d", opts.GasPrice, nRetries)
170266
}
267+
}()
171268

172-
time.Sleep(10 * time.Second)
269+
nErr := 0
270+
for {
271+
select {
272+
case receipt := <-receiptCh:
273+
cancel()
274+
return receipt, nil
275+
case err := <-errCh:
276+
nErr++
277+
if nErr >= nGasRetry {
278+
failCh <- errors.WithMessage(err, "All gas price retries failed")
279+
cancel()
280+
return nil, err
281+
}
282+
case err := <-failCh:
283+
cancel()
284+
return nil, err
285+
}
173286
}
174-
175287
}
176288

177289
func (submission Submission) Fee(pricePerSector *big.Int) *big.Int {

tests/go_tests/segment_upload_test/main.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,12 @@ func runTest() error {
9898
if err != nil {
9999
return errors.WithMessage(err, "failed to initialize uploader")
100100
}
101-
_, _, err = uploader.SubmitLogEntry(ctx, []core.IterableData{data}, make([][]byte, 1), nil, nil)
101+
_, _, err = uploader.SubmitLogEntry(ctx, []core.IterableData{data}, make([][]byte, 1), transfer.SubmitLogEntryOption{
102+
NRetries: 5,
103+
Step: 15,
104+
})
102105
if err != nil {
103-
return errors.WithMessage(err, "failed to sub log entry")
106+
return errors.WithMessage(err, "failed to submit log entry")
104107
}
105108
// wait for log entry
106109
var info *node.FileInfo

0 commit comments

Comments
 (0)