Skip to content

Commit 4b0032f

Browse files
committed
Try to outbid transaction gas price when it's not yet mined
We check in the predefined intervals if the transaction has been mined already and if not, we increase the gas price by 20% and try to submit again.
1 parent 960a58f commit 4b0032f

7 files changed

Lines changed: 354 additions & 5 deletions

File tree

pkg/chain/ethereum/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ type Config struct {
3838
ContractAddresses map[string]string
3939

4040
Account Account
41+
42+
MiningCheckInterval int
43+
44+
MaxGasPrice uint64
4145
}
4246

4347
// ContractAddress finds a given contract's address configuration and returns it

pkg/chain/ethereum/ethutil/mine_waiter.go

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ethutil
33
import (
44
"context"
55
"time"
6+
"math/big"
67

78
"github.com/ethereum/go-ethereum/accounts/abi/bind"
89
"github.com/ethereum/go-ethereum/core/types"
@@ -12,12 +13,22 @@ import (
1213
// mined.
1314
type MiningWaiter struct {
1415
backend bind.DeployBackend
16+
checkInterval time.Duration
17+
maxGasPrice *big.Int
1518
}
1619

1720
// NewMiningWaiter creates a new MiningWaiter instance for the provided
1821
// client backend.
19-
func NewMiningWaiter(backend bind.DeployBackend) *MiningWaiter {
20-
return &MiningWaiter{backend}
22+
func NewMiningWaiter(
23+
backend bind.DeployBackend,
24+
checkInterval time.Duration,
25+
maxGasPrice *big.Int,
26+
) *MiningWaiter {
27+
return &MiningWaiter{
28+
backend,
29+
checkInterval,
30+
maxGasPrice,
31+
}
2132
}
2233

2334
// WaitMined blocks the current execution until the transaction with the given
@@ -29,6 +40,62 @@ func (mw *MiningWaiter) WaitMined(
2940
) (*types.Receipt, error) {
3041
ctx, cancel := context.WithTimeout(context.Background(), timeout)
3142
defer cancel()
43+
return bind.WaitMined(ctx, mw.backend, tx)
44+
}
45+
46+
type ResubmitTransactionFn func(gasPrice *big.Int) (*types.Transaction, error)
47+
48+
func (mw MiningWaiter) ForceMining(
49+
originalTransaction *types.Transaction,
50+
resubmitFn ResubmitTransactionFn,
51+
) {
52+
transaction := originalTransaction
53+
for {
54+
receipt, err := mw.WaitMined(mw.checkInterval, transaction)
55+
if err != nil {
56+
logger.Infof(
57+
"transaction [%v] not yet mined: [%v]",
58+
transaction.Hash().TerminalString(),
59+
err,
60+
)
61+
}
62+
63+
// transaction mined, we are good
64+
if receipt != nil {
65+
logger.Infof(
66+
"transaction [%v] mined with status [%v] at block [%v]",
67+
transaction.Hash().TerminalString(),
68+
receipt.Status,
69+
receipt.BlockNumber,
70+
)
71+
return
72+
}
73+
74+
// add 20% to the previous gas price
75+
gasPrice := transaction.GasPrice()
76+
twentyPercent := new(big.Int).Div(gasPrice, big.NewInt(5))
77+
gasPrice = new(big.Int).Add(gasPrice, twentyPercent)
78+
79+
// transaction not yet mined but we reached the maximum allowed gas
80+
// price; giving up, we need to wait for the last submitted TX to be
81+
// mined
82+
if gasPrice.Cmp(mw.maxGasPrice) > 0 {
83+
logger.Infof("reached the maximum allowed gas price; stopping resubmissions")
84+
return
85+
}
3286

33-
return bind.WaitMined(ctx, mw.backend, tx)
87+
// transaction not yet mined and we can still increase gas price
88+
// resubmitting transaction with 20% higher gas price
89+
logger.Infof(
90+
"resubmitting previous transaction [%v] with higher gas price [%v]",
91+
transaction.Hash().TerminalString(),
92+
gasPrice,
93+
)
94+
95+
transaction, err = resubmitFn(gasPrice)
96+
if err != nil {
97+
logger.Errorf("failed resubmitting TX with higher gas price: [%v]", err)
98+
return
99+
}
100+
}
34101
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package ethutil
2+
3+
import (
4+
"context"
5+
"math/big"
6+
"testing"
7+
"time"
8+
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/ethereum/go-ethereum/core/types"
11+
)
12+
13+
const checkInterval = 100 * time.Millisecond
14+
15+
var maxGasPrice = big.NewInt(45000000000) // 45 Gwei
16+
17+
func TestForceMining_FirstMined(t *testing.T) {
18+
originalTransaction := createTransaction(big.NewInt(20000000000)) // 20 Gwei
19+
20+
mockBackend := &mockDeployBackend{}
21+
22+
var resubmissionGasPrices []*big.Int
23+
24+
resubmitFn := func(gasPrice *big.Int) (*types.Transaction, error) {
25+
resubmissionGasPrices = append(resubmissionGasPrices, gasPrice)
26+
return createTransaction(gasPrice), nil
27+
}
28+
29+
// receipt is already there
30+
mockBackend.receipt = &types.Receipt{}
31+
32+
waiter := NewMiningWaiter(mockBackend, checkInterval, maxGasPrice)
33+
waiter.ForceMining(
34+
originalTransaction,
35+
resubmitFn,
36+
)
37+
38+
resubmissionCount := len(resubmissionGasPrices)
39+
if resubmissionCount != 0 {
40+
t.Fatalf("expected no resubmissions; has: [%v]", resubmissionCount)
41+
}
42+
}
43+
44+
func TestForceMining_SecondMined(t *testing.T) {
45+
originalTransaction := createTransaction(big.NewInt(20000000000)) // 20 Gwei
46+
47+
mockBackend := &mockDeployBackend{}
48+
49+
var resubmissionGasPrices []*big.Int
50+
51+
resubmitFn := func(gasPrice *big.Int) (*types.Transaction, error) {
52+
resubmissionGasPrices = append(resubmissionGasPrices, gasPrice)
53+
// first resubmission succeeded
54+
mockBackend.receipt = &types.Receipt{}
55+
return createTransaction(gasPrice), nil
56+
}
57+
58+
waiter := NewMiningWaiter(mockBackend, checkInterval, maxGasPrice)
59+
waiter.ForceMining(
60+
originalTransaction,
61+
resubmitFn,
62+
)
63+
64+
resubmissionCount := len(resubmissionGasPrices)
65+
if resubmissionCount != 1 {
66+
t.Fatalf("expected one resubmission; has: [%v]", resubmissionCount)
67+
}
68+
}
69+
70+
func TestForceMining_MultipleAttempts(t *testing.T) {
71+
originalTransaction := createTransaction(big.NewInt(20000000000)) // 20 Gwei
72+
73+
mockBackend := &mockDeployBackend{}
74+
75+
var resubmissionGasPrices []*big.Int
76+
77+
expectedAttempts := 3
78+
expectedResubmissionGasPrices := []*big.Int{
79+
big.NewInt(24000000000), // + 20%
80+
big.NewInt(28800000000), // + 20%
81+
big.NewInt(34560000000), // + 20%
82+
}
83+
84+
attemptsSoFar := 1
85+
resubmitFn := func(gasPrice *big.Int) (*types.Transaction, error) {
86+
resubmissionGasPrices = append(resubmissionGasPrices, gasPrice)
87+
if attemptsSoFar == expectedAttempts {
88+
mockBackend.receipt = &types.Receipt{}
89+
} else {
90+
attemptsSoFar++
91+
}
92+
return createTransaction(gasPrice), nil
93+
}
94+
95+
waiter := NewMiningWaiter(mockBackend, checkInterval, maxGasPrice)
96+
waiter.ForceMining(
97+
originalTransaction,
98+
resubmitFn,
99+
)
100+
101+
resubmissionCount := len(resubmissionGasPrices)
102+
if resubmissionCount != expectedAttempts {
103+
t.Fatalf(
104+
"expected [%v] resubmission; has: [%v]",
105+
expectedAttempts,
106+
resubmissionCount,
107+
)
108+
}
109+
110+
for resubmission, price := range resubmissionGasPrices {
111+
if price.Cmp(expectedResubmissionGasPrices[resubmission]) != 0 {
112+
t.Fatalf(
113+
"unexpected [%v] resubmission gas price\nexpected: [%v]\nactual: [%v]",
114+
resubmission,
115+
expectedResubmissionGasPrices[resubmission],
116+
price,
117+
)
118+
}
119+
}
120+
}
121+
122+
func TestForceMining_MaxAllowedPriceReached(t *testing.T) {
123+
originalTransaction := createTransaction(big.NewInt(20000000000)) // 20 Gwei
124+
125+
mockBackend := &mockDeployBackend{}
126+
127+
var resubmissionGasPrices []*big.Int
128+
129+
expectedAttempts := 4
130+
expectedResubmissionGasPrices := []*big.Int{
131+
big.NewInt(24000000000), // + 20%
132+
big.NewInt(28800000000), // + 20%
133+
big.NewInt(34560000000), // + 20%
134+
big.NewInt(41472000000), // + 20%
135+
// the next one would be 49766400000 but since maxGasPrice = 45 Gwei
136+
// resubmissions should stop here
137+
}
138+
139+
resubmitFn := func(gasPrice *big.Int) (*types.Transaction, error) {
140+
resubmissionGasPrices = append(resubmissionGasPrices, gasPrice)
141+
// not setting mockBackend.receipt, mining takes a very long time
142+
return createTransaction(gasPrice), nil
143+
}
144+
145+
waiter := NewMiningWaiter(mockBackend, checkInterval, maxGasPrice)
146+
waiter.ForceMining(
147+
originalTransaction,
148+
resubmitFn,
149+
)
150+
151+
resubmissionCount := len(resubmissionGasPrices)
152+
if resubmissionCount != expectedAttempts {
153+
t.Fatalf(
154+
"expected [%v] resubmission; has: [%v]",
155+
expectedAttempts,
156+
resubmissionCount,
157+
)
158+
}
159+
160+
for resubmission, price := range resubmissionGasPrices {
161+
if price.Cmp(expectedResubmissionGasPrices[resubmission]) != 0 {
162+
t.Fatalf(
163+
"unexpected [%v] resubmission gas price\nexpected: [%v]\nactual: [%v]",
164+
resubmission,
165+
expectedResubmissionGasPrices[resubmission],
166+
price,
167+
)
168+
}
169+
}
170+
}
171+
172+
func createTransaction(gasPrice *big.Int) *types.Transaction {
173+
return types.NewTransaction(
174+
10, // nonce
175+
common.HexToAddress("0x131D387731bBbC988B312206c74F77D004D6B84b"), // to
176+
big.NewInt(0), // amount
177+
200000, // gas limit
178+
gasPrice, // gas price
179+
[]byte{}, // data
180+
)
181+
}
182+
183+
type mockDeployBackend struct {
184+
receipt *types.Receipt
185+
}
186+
187+
func (mdb *mockDeployBackend) TransactionReceipt(
188+
ctx context.Context,
189+
txHash common.Hash,
190+
) (*types.Receipt, error) {
191+
return mdb.receipt, nil
192+
}
193+
194+
func (mdb *mockDeployBackend) CodeAt(
195+
ctx context.Context,
196+
account common.Address,
197+
blockNumber *big.Int,
198+
) ([]byte, error) {
199+
panic("not implemented")
200+
}

tools/generators/ethereum/command.go.tmpl

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,25 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) {
215215
)
216216
}
217217

218+
checkInterval := 30 * time.Second
219+
maxGasPrice := big.NewInt(50000000000) // 50 Gwei
220+
if config.MiningCheckInterval != 0 {
221+
checkInterval = time.Duration(config.MiningCheckInterval) * time.Second
222+
}
223+
if config.MaxGasPrice != 0 {
224+
maxGasPrice = new(big.Int).SetUint64(config.MaxGasPrice)
225+
}
226+
227+
miningWaiter := ethutil.NewMiningWaiter(client, checkInterval, maxGasPrice)
228+
218229
address := common.HexToAddress(config.ContractAddresses["{{.Class}}"])
219230

220231
return contract.New{{.Class}}(
221232
address,
222233
key,
223234
client,
224235
ethutil.NewNonceManager(key.Address, client),
225-
ethutil.NewMiningWaiter(client),
236+
miningWaiter,
226237
&sync.Mutex{},
227238
)
228239
}

tools/generators/ethereum/command_template_content.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,25 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) {
218218
)
219219
}
220220
221+
checkInterval := 30 * time.Second
222+
maxGasPrice := big.NewInt(50000000000) // 50 Gwei
223+
if config.MiningCheckInterval != 0 {
224+
checkInterval = time.Duration(config.MiningCheckInterval) * time.Second
225+
}
226+
if config.MaxGasPrice != 0 {
227+
maxGasPrice = new(big.Int).SetUint64(config.MaxGasPrice)
228+
}
229+
230+
miningWaiter := ethutil.NewMiningWaiter(client, checkInterval, maxGasPrice)
231+
221232
address := common.HexToAddress(config.ContractAddresses["{{.Class}}"])
222233
223234
return contract.New{{.Class}}(
224235
address,
225236
key,
226237
client,
227238
ethutil.NewNonceManager(key.Address, client),
228-
ethutil.NewMiningWaiter(client),
239+
miningWaiter,
229240
&sync.Mutex{},
230241
)
231242
}

tools/generators/ethereum/contract_non_const_methods.go.tmpl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,34 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}(
7272
transaction.Hash().Hex(),
7373
)
7474

75+
go {{$contract.ShortVar}}.miningWaiter.ForceMining(
76+
transaction,
77+
func(newGasPrice *big.Int) (*types.Transaction, error) {
78+
transactorOptions.GasLimit = transaction.Gas()
79+
transactorOptions.GasPrice = newGasPrice
80+
81+
transaction, err := {{$contract.ShortVar}}.contract.{{$method.CapsName}}(
82+
transactorOptions,
83+
{{$method.Params}}
84+
)
85+
if err != nil {
86+
return transaction, {{$contract.ShortVar}}.errorResolver.ResolveError(
87+
err,
88+
{{$contract.ShortVar}}.transactorOptions.From,
89+
{{if $method.Payable -}}
90+
value
91+
{{- else -}}
92+
nil
93+
{{- end -}},
94+
"{{$method.LowerName}}",
95+
{{$method.Params}}
96+
)
97+
}
98+
99+
return transaction, nil
100+
},
101+
)
102+
75103
{{$contract.ShortVar}}.nonceManager.IncrementNonce()
76104

77105
return transaction, err

0 commit comments

Comments
 (0)