From 164827f3dbbb90b8fd5879e23e407b66ee317528 Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Thu, 14 May 2020 18:50:27 +0200 Subject: [PATCH 01/12] MiningWaiter implementation MiningWaiter allows to block the execution until the given transaction is mined. We will use it to increase gas price after a certain timeout. --- pkg/chain/ethereum/ethutil/mine_waiter.go | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 pkg/chain/ethereum/ethutil/mine_waiter.go diff --git a/pkg/chain/ethereum/ethutil/mine_waiter.go b/pkg/chain/ethereum/ethutil/mine_waiter.go new file mode 100644 index 0000000..51988a2 --- /dev/null +++ b/pkg/chain/ethereum/ethutil/mine_waiter.go @@ -0,0 +1,33 @@ +package ethutil + +import ( + "context" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" +) + +// MiningWaiter allows to block the execution until the given transaction is +// mined. +type MiningWaiter struct { + backend bind.DeployBackend +} + +// NewMiningWaiter creates a new MiningWaiter instance for the provided +// client backend. +func NewMiningWaiter(backend bind.DeployBackend) *MiningWaiter { + return &MiningWaiter{backend} +} + +// WaitMined blocks the current execution until the transaction with the given +// hash is mined. Execution is blocked until the transaction is mined or until +// the given timeout passes. +func (mw *MiningWaiter) WaitMined( + timeout time.Duration, + tx *types.Transaction, +) (*types.Receipt, error) { + ctx, cancel := context.WithTimeout(timeout) + defer cancel() + + return bind.WaitMined(ctx, mw.backend, tx) +} \ No newline at end of file From a3c2935382d0cd8825c8edffb4d16f2ed3dda76e Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Thu, 14 May 2020 18:51:15 +0200 Subject: [PATCH 02/12] Accept MiningWaiter in all generated contracts --- tools/generators/ethereum/command.go.tmpl | 1 + tools/generators/ethereum/command_template_content.go | 1 + tools/generators/ethereum/contract.go.tmpl | 3 +++ tools/generators/ethereum/contract_template_content.go | 3 +++ 4 files changed, 8 insertions(+) diff --git a/tools/generators/ethereum/command.go.tmpl b/tools/generators/ethereum/command.go.tmpl index 3a835e0..66486a9 100644 --- a/tools/generators/ethereum/command.go.tmpl +++ b/tools/generators/ethereum/command.go.tmpl @@ -222,6 +222,7 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) { key, client, ethutil.NewNonceManager(key.Address, client), + ethutil.NewMiningWaiter(client), &sync.Mutex{}, ) } diff --git a/tools/generators/ethereum/command_template_content.go b/tools/generators/ethereum/command_template_content.go index e1e5fea..296b853 100644 --- a/tools/generators/ethereum/command_template_content.go +++ b/tools/generators/ethereum/command_template_content.go @@ -225,6 +225,7 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) { key, client, ethutil.NewNonceManager(key.Address, client), + ethutil.NewMiningWaiter(client), &sync.Mutex{}, ) } diff --git a/tools/generators/ethereum/contract.go.tmpl b/tools/generators/ethereum/contract.go.tmpl index fe1c36b..01c5ab8 100644 --- a/tools/generators/ethereum/contract.go.tmpl +++ b/tools/generators/ethereum/contract.go.tmpl @@ -35,6 +35,7 @@ type {{.Class}} struct { transactorOptions *bind.TransactOpts errorResolver *ethutil.ErrorResolver nonceManager *ethutil.NonceManager + miningWaiter *ethutil.MiningWaiter transactionMutex *sync.Mutex } @@ -44,6 +45,7 @@ func New{{.Class}}( accountKey *keystore.Key, backend bind.ContractBackend, nonceManager *ethutil.NonceManager, + miningWaiter *ethutil.MiningWaiter, transactionMutex *sync.Mutex, ) (*{{.Class}}, error) { callerOptions := &bind.CallOpts{ @@ -81,6 +83,7 @@ func New{{.Class}}( transactorOptions: transactorOptions, errorResolver: ethutil.NewErrorResolver(backend, &contractABI, &contractAddress), nonceManager: nonceManager, + miningWaiter: miningWaiter, transactionMutex: transactionMutex, }, nil } diff --git a/tools/generators/ethereum/contract_template_content.go b/tools/generators/ethereum/contract_template_content.go index 88377b7..39d2a59 100644 --- a/tools/generators/ethereum/contract_template_content.go +++ b/tools/generators/ethereum/contract_template_content.go @@ -38,6 +38,7 @@ type {{.Class}} struct { transactorOptions *bind.TransactOpts errorResolver *ethutil.ErrorResolver nonceManager *ethutil.NonceManager + miningWaiter *ethutil.MiningWaiter transactionMutex *sync.Mutex } @@ -47,6 +48,7 @@ func New{{.Class}}( accountKey *keystore.Key, backend bind.ContractBackend, nonceManager *ethutil.NonceManager, + miningWaiter *ethutil.MiningWaiter, transactionMutex *sync.Mutex, ) (*{{.Class}}, error) { callerOptions := &bind.CallOpts{ @@ -84,6 +86,7 @@ func New{{.Class}}( transactorOptions: transactorOptions, errorResolver: ethutil.NewErrorResolver(backend, &contractABI, &contractAddress), nonceManager: nonceManager, + miningWaiter: miningWaiter, transactionMutex: transactionMutex, }, nil } From 960a58fc5cab3c92fec6b2b4cf16ac9f460878cd Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Thu, 14 May 2020 18:56:28 +0200 Subject: [PATCH 03/12] Fixed compilation errors in MiningWaiter This is what happens when your IDE stucks... --- pkg/chain/ethereum/ethutil/mine_waiter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/chain/ethereum/ethutil/mine_waiter.go b/pkg/chain/ethereum/ethutil/mine_waiter.go index 51988a2..46f0d6c 100644 --- a/pkg/chain/ethereum/ethutil/mine_waiter.go +++ b/pkg/chain/ethereum/ethutil/mine_waiter.go @@ -2,6 +2,7 @@ package ethutil import ( "context" + "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/core/types" @@ -26,7 +27,7 @@ func (mw *MiningWaiter) WaitMined( timeout time.Duration, tx *types.Transaction, ) (*types.Receipt, error) { - ctx, cancel := context.WithTimeout(timeout) + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() return bind.WaitMined(ctx, mw.backend, tx) From 4b0032f7f37b0ceb1b31acd07c2efd3620abeab9 Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Fri, 15 May 2020 14:17:05 +0200 Subject: [PATCH 04/12] 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. --- pkg/chain/ethereum/config.go | 4 + pkg/chain/ethereum/ethutil/mine_waiter.go | 73 ++++++- .../ethereum/ethutil/mine_waiter_test.go | 200 ++++++++++++++++++ tools/generators/ethereum/command.go.tmpl | 13 +- .../ethereum/command_template_content.go | 13 +- .../contract_non_const_methods.go.tmpl | 28 +++ ...ract_non_const_methods_template_content.go | 28 +++ 7 files changed, 354 insertions(+), 5 deletions(-) create mode 100644 pkg/chain/ethereum/ethutil/mine_waiter_test.go diff --git a/pkg/chain/ethereum/config.go b/pkg/chain/ethereum/config.go index c34938a..cc50018 100644 --- a/pkg/chain/ethereum/config.go +++ b/pkg/chain/ethereum/config.go @@ -38,6 +38,10 @@ type Config struct { ContractAddresses map[string]string Account Account + + MiningCheckInterval int + + MaxGasPrice uint64 } // ContractAddress finds a given contract's address configuration and returns it diff --git a/pkg/chain/ethereum/ethutil/mine_waiter.go b/pkg/chain/ethereum/ethutil/mine_waiter.go index 46f0d6c..ca0a953 100644 --- a/pkg/chain/ethereum/ethutil/mine_waiter.go +++ b/pkg/chain/ethereum/ethutil/mine_waiter.go @@ -3,6 +3,7 @@ package ethutil import ( "context" "time" + "math/big" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/core/types" @@ -12,12 +13,22 @@ import ( // mined. type MiningWaiter struct { backend bind.DeployBackend + checkInterval time.Duration + maxGasPrice *big.Int } // NewMiningWaiter creates a new MiningWaiter instance for the provided // client backend. -func NewMiningWaiter(backend bind.DeployBackend) *MiningWaiter { - return &MiningWaiter{backend} +func NewMiningWaiter( + backend bind.DeployBackend, + checkInterval time.Duration, + maxGasPrice *big.Int, +) *MiningWaiter { + return &MiningWaiter{ + backend, + checkInterval, + maxGasPrice, + } } // WaitMined blocks the current execution until the transaction with the given @@ -29,6 +40,62 @@ func (mw *MiningWaiter) WaitMined( ) (*types.Receipt, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() + return bind.WaitMined(ctx, mw.backend, tx) +} + +type ResubmitTransactionFn func(gasPrice *big.Int) (*types.Transaction, error) + +func (mw MiningWaiter) ForceMining( + originalTransaction *types.Transaction, + resubmitFn ResubmitTransactionFn, +) { + transaction := originalTransaction + for { + receipt, err := mw.WaitMined(mw.checkInterval, transaction) + if err != nil { + logger.Infof( + "transaction [%v] not yet mined: [%v]", + transaction.Hash().TerminalString(), + err, + ) + } + + // transaction mined, we are good + if receipt != nil { + logger.Infof( + "transaction [%v] mined with status [%v] at block [%v]", + transaction.Hash().TerminalString(), + receipt.Status, + receipt.BlockNumber, + ) + return + } + + // add 20% to the previous gas price + gasPrice := transaction.GasPrice() + twentyPercent := new(big.Int).Div(gasPrice, big.NewInt(5)) + gasPrice = new(big.Int).Add(gasPrice, twentyPercent) + + // transaction not yet mined but we reached the maximum allowed gas + // price; giving up, we need to wait for the last submitted TX to be + // mined + if gasPrice.Cmp(mw.maxGasPrice) > 0 { + logger.Infof("reached the maximum allowed gas price; stopping resubmissions") + return + } - return bind.WaitMined(ctx, mw.backend, tx) + // transaction not yet mined and we can still increase gas price + // resubmitting transaction with 20% higher gas price + logger.Infof( + "resubmitting previous transaction [%v] with higher gas price [%v]", + transaction.Hash().TerminalString(), + gasPrice, + ) + + transaction, err = resubmitFn(gasPrice) + if err != nil { + logger.Errorf("failed resubmitting TX with higher gas price: [%v]", err) + return + } + } } \ No newline at end of file diff --git a/pkg/chain/ethereum/ethutil/mine_waiter_test.go b/pkg/chain/ethereum/ethutil/mine_waiter_test.go new file mode 100644 index 0000000..ff718f4 --- /dev/null +++ b/pkg/chain/ethereum/ethutil/mine_waiter_test.go @@ -0,0 +1,200 @@ +package ethutil + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +const checkInterval = 100 * time.Millisecond + +var maxGasPrice = big.NewInt(45000000000) // 45 Gwei + +func TestForceMining_FirstMined(t *testing.T) { + originalTransaction := createTransaction(big.NewInt(20000000000)) // 20 Gwei + + mockBackend := &mockDeployBackend{} + + var resubmissionGasPrices []*big.Int + + resubmitFn := func(gasPrice *big.Int) (*types.Transaction, error) { + resubmissionGasPrices = append(resubmissionGasPrices, gasPrice) + return createTransaction(gasPrice), nil + } + + // receipt is already there + mockBackend.receipt = &types.Receipt{} + + waiter := NewMiningWaiter(mockBackend, checkInterval, maxGasPrice) + waiter.ForceMining( + originalTransaction, + resubmitFn, + ) + + resubmissionCount := len(resubmissionGasPrices) + if resubmissionCount != 0 { + t.Fatalf("expected no resubmissions; has: [%v]", resubmissionCount) + } +} + +func TestForceMining_SecondMined(t *testing.T) { + originalTransaction := createTransaction(big.NewInt(20000000000)) // 20 Gwei + + mockBackend := &mockDeployBackend{} + + var resubmissionGasPrices []*big.Int + + resubmitFn := func(gasPrice *big.Int) (*types.Transaction, error) { + resubmissionGasPrices = append(resubmissionGasPrices, gasPrice) + // first resubmission succeeded + mockBackend.receipt = &types.Receipt{} + return createTransaction(gasPrice), nil + } + + waiter := NewMiningWaiter(mockBackend, checkInterval, maxGasPrice) + waiter.ForceMining( + originalTransaction, + resubmitFn, + ) + + resubmissionCount := len(resubmissionGasPrices) + if resubmissionCount != 1 { + t.Fatalf("expected one resubmission; has: [%v]", resubmissionCount) + } +} + +func TestForceMining_MultipleAttempts(t *testing.T) { + originalTransaction := createTransaction(big.NewInt(20000000000)) // 20 Gwei + + mockBackend := &mockDeployBackend{} + + var resubmissionGasPrices []*big.Int + + expectedAttempts := 3 + expectedResubmissionGasPrices := []*big.Int{ + big.NewInt(24000000000), // + 20% + big.NewInt(28800000000), // + 20% + big.NewInt(34560000000), // + 20% + } + + attemptsSoFar := 1 + resubmitFn := func(gasPrice *big.Int) (*types.Transaction, error) { + resubmissionGasPrices = append(resubmissionGasPrices, gasPrice) + if attemptsSoFar == expectedAttempts { + mockBackend.receipt = &types.Receipt{} + } else { + attemptsSoFar++ + } + return createTransaction(gasPrice), nil + } + + waiter := NewMiningWaiter(mockBackend, checkInterval, maxGasPrice) + waiter.ForceMining( + originalTransaction, + resubmitFn, + ) + + resubmissionCount := len(resubmissionGasPrices) + if resubmissionCount != expectedAttempts { + t.Fatalf( + "expected [%v] resubmission; has: [%v]", + expectedAttempts, + resubmissionCount, + ) + } + + for resubmission, price := range resubmissionGasPrices { + if price.Cmp(expectedResubmissionGasPrices[resubmission]) != 0 { + t.Fatalf( + "unexpected [%v] resubmission gas price\nexpected: [%v]\nactual: [%v]", + resubmission, + expectedResubmissionGasPrices[resubmission], + price, + ) + } + } +} + +func TestForceMining_MaxAllowedPriceReached(t *testing.T) { + originalTransaction := createTransaction(big.NewInt(20000000000)) // 20 Gwei + + mockBackend := &mockDeployBackend{} + + var resubmissionGasPrices []*big.Int + + expectedAttempts := 4 + expectedResubmissionGasPrices := []*big.Int{ + big.NewInt(24000000000), // + 20% + big.NewInt(28800000000), // + 20% + big.NewInt(34560000000), // + 20% + big.NewInt(41472000000), // + 20% + // the next one would be 49766400000 but since maxGasPrice = 45 Gwei + // resubmissions should stop here + } + + resubmitFn := func(gasPrice *big.Int) (*types.Transaction, error) { + resubmissionGasPrices = append(resubmissionGasPrices, gasPrice) + // not setting mockBackend.receipt, mining takes a very long time + return createTransaction(gasPrice), nil + } + + waiter := NewMiningWaiter(mockBackend, checkInterval, maxGasPrice) + waiter.ForceMining( + originalTransaction, + resubmitFn, + ) + + resubmissionCount := len(resubmissionGasPrices) + if resubmissionCount != expectedAttempts { + t.Fatalf( + "expected [%v] resubmission; has: [%v]", + expectedAttempts, + resubmissionCount, + ) + } + + for resubmission, price := range resubmissionGasPrices { + if price.Cmp(expectedResubmissionGasPrices[resubmission]) != 0 { + t.Fatalf( + "unexpected [%v] resubmission gas price\nexpected: [%v]\nactual: [%v]", + resubmission, + expectedResubmissionGasPrices[resubmission], + price, + ) + } + } +} + +func createTransaction(gasPrice *big.Int) *types.Transaction { + return types.NewTransaction( + 10, // nonce + common.HexToAddress("0x131D387731bBbC988B312206c74F77D004D6B84b"), // to + big.NewInt(0), // amount + 200000, // gas limit + gasPrice, // gas price + []byte{}, // data + ) +} + +type mockDeployBackend struct { + receipt *types.Receipt +} + +func (mdb *mockDeployBackend) TransactionReceipt( + ctx context.Context, + txHash common.Hash, +) (*types.Receipt, error) { + return mdb.receipt, nil +} + +func (mdb *mockDeployBackend) CodeAt( + ctx context.Context, + account common.Address, + blockNumber *big.Int, +) ([]byte, error) { + panic("not implemented") +} diff --git a/tools/generators/ethereum/command.go.tmpl b/tools/generators/ethereum/command.go.tmpl index 66486a9..9d28e9d 100644 --- a/tools/generators/ethereum/command.go.tmpl +++ b/tools/generators/ethereum/command.go.tmpl @@ -215,6 +215,17 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) { ) } + checkInterval := 30 * time.Second + maxGasPrice := big.NewInt(50000000000) // 50 Gwei + if config.MiningCheckInterval != 0 { + checkInterval = time.Duration(config.MiningCheckInterval) * time.Second + } + if config.MaxGasPrice != 0 { + maxGasPrice = new(big.Int).SetUint64(config.MaxGasPrice) + } + + miningWaiter := ethutil.NewMiningWaiter(client, checkInterval, maxGasPrice) + address := common.HexToAddress(config.ContractAddresses["{{.Class}}"]) return contract.New{{.Class}}( @@ -222,7 +233,7 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) { key, client, ethutil.NewNonceManager(key.Address, client), - ethutil.NewMiningWaiter(client), + miningWaiter, &sync.Mutex{}, ) } diff --git a/tools/generators/ethereum/command_template_content.go b/tools/generators/ethereum/command_template_content.go index 296b853..5cc3443 100644 --- a/tools/generators/ethereum/command_template_content.go +++ b/tools/generators/ethereum/command_template_content.go @@ -218,6 +218,17 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) { ) } + checkInterval := 30 * time.Second + maxGasPrice := big.NewInt(50000000000) // 50 Gwei + if config.MiningCheckInterval != 0 { + checkInterval = time.Duration(config.MiningCheckInterval) * time.Second + } + if config.MaxGasPrice != 0 { + maxGasPrice = new(big.Int).SetUint64(config.MaxGasPrice) + } + + miningWaiter := ethutil.NewMiningWaiter(client, checkInterval, maxGasPrice) + address := common.HexToAddress(config.ContractAddresses["{{.Class}}"]) return contract.New{{.Class}}( @@ -225,7 +236,7 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) { key, client, ethutil.NewNonceManager(key.Address, client), - ethutil.NewMiningWaiter(client), + miningWaiter, &sync.Mutex{}, ) } diff --git a/tools/generators/ethereum/contract_non_const_methods.go.tmpl b/tools/generators/ethereum/contract_non_const_methods.go.tmpl index e2e61ed..96b67c2 100644 --- a/tools/generators/ethereum/contract_non_const_methods.go.tmpl +++ b/tools/generators/ethereum/contract_non_const_methods.go.tmpl @@ -72,6 +72,34 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}( transaction.Hash().Hex(), ) + go {{$contract.ShortVar}}.miningWaiter.ForceMining( + transaction, + func(newGasPrice *big.Int) (*types.Transaction, error) { + transactorOptions.GasLimit = transaction.Gas() + transactorOptions.GasPrice = newGasPrice + + transaction, err := {{$contract.ShortVar}}.contract.{{$method.CapsName}}( + transactorOptions, + {{$method.Params}} + ) + if err != nil { + return transaction, {{$contract.ShortVar}}.errorResolver.ResolveError( + err, + {{$contract.ShortVar}}.transactorOptions.From, + {{if $method.Payable -}} + value + {{- else -}} + nil + {{- end -}}, + "{{$method.LowerName}}", + {{$method.Params}} + ) + } + + return transaction, nil + }, + ) + {{$contract.ShortVar}}.nonceManager.IncrementNonce() return transaction, err diff --git a/tools/generators/ethereum/contract_non_const_methods_template_content.go b/tools/generators/ethereum/contract_non_const_methods_template_content.go index 5c9d45b..3b6fe66 100644 --- a/tools/generators/ethereum/contract_non_const_methods_template_content.go +++ b/tools/generators/ethereum/contract_non_const_methods_template_content.go @@ -75,6 +75,34 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}( transaction.Hash().Hex(), ) + go {{$contract.ShortVar}}.miningWaiter.ForceMining( + transaction, + func(newGasPrice *big.Int) (*types.Transaction, error) { + transactorOptions.GasLimit = transaction.Gas() + transactorOptions.GasPrice = newGasPrice + + transaction, err := {{$contract.ShortVar}}.contract.{{$method.CapsName}}( + transactorOptions, + {{$method.Params}} + ) + if err != nil { + return transaction, {{$contract.ShortVar}}.errorResolver.ResolveError( + err, + {{$contract.ShortVar}}.transactorOptions.From, + {{if $method.Payable -}} + value + {{- else -}} + nil + {{- end -}}, + "{{$method.LowerName}}", + {{$method.Params}} + ) + } + + return transaction, nil + }, + ) + {{$contract.ShortVar}}.nonceManager.IncrementNonce() return transaction, err From db51d7ac3a91fe8dc6b7b06bf5b6744e020a1ea5 Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Fri, 15 May 2020 19:04:46 +0200 Subject: [PATCH 05/12] Use a separate context for TransactionReceipt call Using the same context leads to connection reset on context timeout and the same connection is used by subscriptions. --- pkg/chain/ethereum/ethutil/mine_waiter.go | 37 ++++++++++++++++------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/pkg/chain/ethereum/ethutil/mine_waiter.go b/pkg/chain/ethereum/ethutil/mine_waiter.go index ca0a953..ec6edba 100644 --- a/pkg/chain/ethereum/ethutil/mine_waiter.go +++ b/pkg/chain/ethereum/ethutil/mine_waiter.go @@ -2,8 +2,8 @@ package ethutil import ( "context" - "time" "math/big" + "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/core/types" @@ -12,9 +12,9 @@ import ( // MiningWaiter allows to block the execution until the given transaction is // mined. type MiningWaiter struct { - backend bind.DeployBackend + backend bind.DeployBackend checkInterval time.Duration - maxGasPrice *big.Int + maxGasPrice *big.Int } // NewMiningWaiter creates a new MiningWaiter instance for the provided @@ -35,12 +35,27 @@ func NewMiningWaiter( // hash is mined. Execution is blocked until the transaction is mined or until // the given timeout passes. func (mw *MiningWaiter) WaitMined( - timeout time.Duration, + timeout time.Duration, tx *types.Transaction, ) (*types.Receipt, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - return bind.WaitMined(ctx, mw.backend, tx) + + queryTicker := time.NewTicker(time.Second) + defer queryTicker.Stop() + + for { + receipt, _ := mw.backend.TransactionReceipt(context.TODO(), tx.Hash()) + if receipt != nil { + return receipt, nil + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-queryTicker.C: + } + } } type ResubmitTransactionFn func(gasPrice *big.Int) (*types.Transaction, error) @@ -59,7 +74,7 @@ func (mw MiningWaiter) ForceMining( err, ) } - + // transaction mined, we are good if receipt != nil { logger.Infof( @@ -75,8 +90,8 @@ func (mw MiningWaiter) ForceMining( gasPrice := transaction.GasPrice() twentyPercent := new(big.Int).Div(gasPrice, big.NewInt(5)) gasPrice = new(big.Int).Add(gasPrice, twentyPercent) - - // transaction not yet mined but we reached the maximum allowed gas + + // transaction not yet mined but we reached the maximum allowed gas // price; giving up, we need to wait for the last submitted TX to be // mined if gasPrice.Cmp(mw.maxGasPrice) > 0 { @@ -87,15 +102,15 @@ func (mw MiningWaiter) ForceMining( // transaction not yet mined and we can still increase gas price // resubmitting transaction with 20% higher gas price logger.Infof( - "resubmitting previous transaction [%v] with higher gas price [%v]", + "resubmitting previous transaction [%v] with a higher gas price [%v]", transaction.Hash().TerminalString(), gasPrice, ) - + transaction, err = resubmitFn(gasPrice) if err != nil { logger.Errorf("failed resubmitting TX with higher gas price: [%v]", err) return } } -} \ No newline at end of file +} From fd455956ba8f41ce4d6ce596cd1a83aa6819b339 Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Fri, 15 May 2020 19:10:41 +0200 Subject: [PATCH 06/12] Log submitted TX hash and nonce on info level This log should not be hidden on debug level, it's too important to be there. --- .../ethereum/contract_non_const_methods.go.tmpl | 11 +++++++++-- .../contract_non_const_methods_template_content.go | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/tools/generators/ethereum/contract_non_const_methods.go.tmpl b/tools/generators/ethereum/contract_non_const_methods.go.tmpl index 96b67c2..7f22b40 100644 --- a/tools/generators/ethereum/contract_non_const_methods.go.tmpl +++ b/tools/generators/ethereum/contract_non_const_methods.go.tmpl @@ -67,9 +67,10 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}( ) } - {{$logger}}.Debugf( - "submitted transaction {{$method.LowerName}} with id: [%v]", + {{$logger}}.Infof( + "submitted transaction {{$method.LowerName}} with id: [%v] and nonce [%v]", transaction.Hash().Hex(), + transaction.Nonce(), ) go {{$contract.ShortVar}}.miningWaiter.ForceMining( @@ -96,6 +97,12 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}( ) } + {{$logger}}.Infof( + "submitted transaction {{$method.LowerName}} with id: [%v] and nonce [%v]", + transaction.Hash().Hex(), + transaction.Nonce(), + ) + return transaction, nil }, ) diff --git a/tools/generators/ethereum/contract_non_const_methods_template_content.go b/tools/generators/ethereum/contract_non_const_methods_template_content.go index 3b6fe66..1bbc026 100644 --- a/tools/generators/ethereum/contract_non_const_methods_template_content.go +++ b/tools/generators/ethereum/contract_non_const_methods_template_content.go @@ -70,9 +70,10 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}( ) } - {{$logger}}.Debugf( - "submitted transaction {{$method.LowerName}} with id: [%v]", + {{$logger}}.Infof( + "submitted transaction {{$method.LowerName}} with id: [%v] and nonce [%v]", transaction.Hash().Hex(), + transaction.Nonce(), ) go {{$contract.ShortVar}}.miningWaiter.ForceMining( @@ -99,6 +100,12 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}( ) } + {{$logger}}.Infof( + "submitted transaction {{$method.LowerName}} with id: [%v] and nonce [%v]", + transaction.Hash().Hex(), + transaction.Nonce(), + ) + return transaction, nil }, ) From 207339f03f7dbd9a4eeb5a911e05b00c18f074c8 Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Fri, 15 May 2020 19:23:17 +0200 Subject: [PATCH 07/12] Improved mine waiter documentation --- pkg/chain/ethereum/ethutil/mine_waiter.go | 32 ++++++++++++++++++----- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/pkg/chain/ethereum/ethutil/mine_waiter.go b/pkg/chain/ethereum/ethutil/mine_waiter.go index ec6edba..cd43e3b 100644 --- a/pkg/chain/ethereum/ethutil/mine_waiter.go +++ b/pkg/chain/ethereum/ethutil/mine_waiter.go @@ -10,7 +10,8 @@ import ( ) // MiningWaiter allows to block the execution until the given transaction is -// mined. +// mined as well as monitor the transaction and bump up the gas price in case +// it is not mined in the given timeout. type MiningWaiter struct { backend bind.DeployBackend checkInterval time.Duration @@ -18,7 +19,17 @@ type MiningWaiter struct { } // NewMiningWaiter creates a new MiningWaiter instance for the provided -// client backend. +// client backend. It accepts two parameters setting up monitoring rules of the +// transaction mining status. +// +// Check interval is the time given for the transaction to be mined. If the +// transaction is not mined within that time, the gas price is increased by +// 20% and transaction is replaced with the one with a higher gas price. +// +// Max gas price specifies the maximum gas price the client is willing to pay +// for the transaction to be mined. The offered transaction gas price can not +// be higher than this value. If the maximum allowed gas price is reached, no +// further resubmission attempts are performed. func NewMiningWaiter( backend bind.DeployBackend, checkInterval time.Duration, @@ -58,8 +69,15 @@ func (mw *MiningWaiter) WaitMined( } } +// ResubmitTransactionFn implements the code for resubmitting the transaction +// with the higher gas price. It should guarantee the same nonce is used for +// transaction resubmission. type ResubmitTransactionFn func(gasPrice *big.Int) (*types.Transaction, error) +// ForceMining blocks until the transaction is mined and bumps up the gas price +// by 20% in the intervals defined by MiningWaiter in case the transaction has +// not been mined yet. It accepts the original transaction reference and the +// function responsible for executing transaction resubmission. func (mw MiningWaiter) ForceMining( originalTransaction *types.Transaction, resubmitFn ResubmitTransactionFn, @@ -86,7 +104,7 @@ func (mw MiningWaiter) ForceMining( return } - // add 20% to the previous gas price + // transaction not yet mined, add 20% to the previous gas price gasPrice := transaction.GasPrice() twentyPercent := new(big.Int).Div(gasPrice, big.NewInt(5)) gasPrice = new(big.Int).Add(gasPrice, twentyPercent) @@ -99,17 +117,17 @@ func (mw MiningWaiter) ForceMining( return } - // transaction not yet mined and we can still increase gas price - // resubmitting transaction with 20% higher gas price + // transaction not yet mined and we are still under the maximum allowed + // gas price; resubmitting transaction with 20% higher gas price + // evaluated earlier logger.Infof( "resubmitting previous transaction [%v] with a higher gas price [%v]", transaction.Hash().TerminalString(), gasPrice, ) - transaction, err = resubmitFn(gasPrice) if err != nil { - logger.Errorf("failed resubmitting TX with higher gas price: [%v]", err) + logger.Errorf("failed resubmitting TX with a higher gas price: [%v]", err) return } } From 7e3b0bc786623cd6dee6de017b98218145b3eb5e Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Fri, 15 May 2020 20:02:01 +0200 Subject: [PATCH 08/12] Extracted TX resubmission constants for commands to cmd.go --- pkg/cmd/cmd.go | 15 +++++++++++++++ tools/generators/ethereum/command.go.tmpl | 4 ++-- .../ethereum/command_template_content.go | 4 ++-- tools/generators/ethereum/contract.go.tmpl | 2 +- .../ethereum/contract_template_content.go | 2 +- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 5eec2b1..281cb07 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -4,6 +4,8 @@ package cmd import ( "fmt" + "math/big" + "time" "github.com/ethereum/go-ethereum/common" "github.com/keep-network/keep-common/pkg/cmd/flag" @@ -41,6 +43,19 @@ var ( // interaction. The value, if that flag is passed on the command line, is // stored in this variable. ValueFlagValue *flag.Uint256 = &flag.Uint256{} + + // DefaultMiningCheckInterval is the default interval in which transaction + // mining status is checked. If the transaction is not mined within this + // time, the gas price is increased and transaction is resubmitted. + // This value can be overwritten in the configuration file. + DefaultMiningCheckInterval = 60 * time.Second + + // DefaultMaxGasPrice specifies the default maximum gas price the client is + // willing to pay for the transaction to be mined. The offered transaction + // gas price can not be higher than the max gas price value. If the maximum + // allowed gas price is reached, no further resubmission attempts are + // performed. This value can be overwritten in the configuration file. + DefaultMaxGasPrice = big.NewInt(50000000000) // 50 Gwei ) // AvailableCommands is the exported list of generated commands that can be diff --git a/tools/generators/ethereum/command.go.tmpl b/tools/generators/ethereum/command.go.tmpl index 9d28e9d..c96de9f 100644 --- a/tools/generators/ethereum/command.go.tmpl +++ b/tools/generators/ethereum/command.go.tmpl @@ -215,8 +215,8 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) { ) } - checkInterval := 30 * time.Second - maxGasPrice := big.NewInt(50000000000) // 50 Gwei + checkInterval := cmd.DefaultMiningCheckInterval + maxGasPrice := cmd.DefaultMaxGasPrice if config.MiningCheckInterval != 0 { checkInterval = time.Duration(config.MiningCheckInterval) * time.Second } diff --git a/tools/generators/ethereum/command_template_content.go b/tools/generators/ethereum/command_template_content.go index 5cc3443..d04a69c 100644 --- a/tools/generators/ethereum/command_template_content.go +++ b/tools/generators/ethereum/command_template_content.go @@ -218,8 +218,8 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) { ) } - checkInterval := 30 * time.Second - maxGasPrice := big.NewInt(50000000000) // 50 Gwei + checkInterval := cmd.DefaultMiningCheckInterval + maxGasPrice := cmd.DefaultMaxGasPrice if config.MiningCheckInterval != 0 { checkInterval = time.Duration(config.MiningCheckInterval) * time.Second } diff --git a/tools/generators/ethereum/contract.go.tmpl b/tools/generators/ethereum/contract.go.tmpl index 01c5ab8..ab459c4 100644 --- a/tools/generators/ethereum/contract.go.tmpl +++ b/tools/generators/ethereum/contract.go.tmpl @@ -45,7 +45,7 @@ func New{{.Class}}( accountKey *keystore.Key, backend bind.ContractBackend, nonceManager *ethutil.NonceManager, - miningWaiter *ethutil.MiningWaiter, + miningWaiter *ethutil.MiningWaiter, transactionMutex *sync.Mutex, ) (*{{.Class}}, error) { callerOptions := &bind.CallOpts{ diff --git a/tools/generators/ethereum/contract_template_content.go b/tools/generators/ethereum/contract_template_content.go index 39d2a59..e29e0b8 100644 --- a/tools/generators/ethereum/contract_template_content.go +++ b/tools/generators/ethereum/contract_template_content.go @@ -48,7 +48,7 @@ func New{{.Class}}( accountKey *keystore.Key, backend bind.ContractBackend, nonceManager *ethutil.NonceManager, - miningWaiter *ethutil.MiningWaiter, + miningWaiter *ethutil.MiningWaiter, transactionMutex *sync.Mutex, ) (*{{.Class}}, error) { callerOptions := &bind.CallOpts{ From ac571524a54beb9b95f00c94e949862ab718603d Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Fri, 15 May 2020 20:06:55 +0200 Subject: [PATCH 09/12] Added docs to configuration options --- pkg/chain/ethereum/config.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/chain/ethereum/config.go b/pkg/chain/ethereum/config.go index cc50018..0dadaaf 100644 --- a/pkg/chain/ethereum/config.go +++ b/pkg/chain/ethereum/config.go @@ -39,8 +39,16 @@ type Config struct { Account Account + // MiningCheckInterval is the interval in which transaction + // mining status is checked. If the transaction is not mined within this + // time, the gas price is increased and transaction is resubmitted. MiningCheckInterval int + // MaxGasPrice specifies the maximum gas price the client is + // willing to pay for the transaction to be mined. The offered transaction + // gas price can not be higher than the max gas price value. If the maximum + // allowed gas price is reached, no further resubmission attempts are + // performed. MaxGasPrice uint64 } From 49ad28c0fadbb99d14ea035dc48640e41a9874b0 Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Fri, 15 May 2020 22:13:02 +0200 Subject: [PATCH 10/12] Resubmission failed message changed from error to warning It is possible the resubmission failed because the transaction has just been mined. It's not worth logging it as an ERROR especially that the first transaction was submitted (and that's always the case before we get to mining monitoring). --- pkg/chain/ethereum/ethutil/mine_waiter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/chain/ethereum/ethutil/mine_waiter.go b/pkg/chain/ethereum/ethutil/mine_waiter.go index cd43e3b..fd11bc1 100644 --- a/pkg/chain/ethereum/ethutil/mine_waiter.go +++ b/pkg/chain/ethereum/ethutil/mine_waiter.go @@ -127,7 +127,7 @@ func (mw MiningWaiter) ForceMining( ) transaction, err = resubmitFn(gasPrice) if err != nil { - logger.Errorf("failed resubmitting TX with a higher gas price: [%v]", err) + logger.Warningf("could not resubmit TX with a higher gas price: [%v]", err) return } } From 6b92fa268a810d0e278786c33f0773b772ff3a34 Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Fri, 15 May 2020 23:28:39 +0200 Subject: [PATCH 11/12] The last resubmission should be with the maximum allowed gas cost We were never hitting the maximum, stopping before the maximum was reached. --- pkg/chain/ethereum/ethutil/mine_waiter.go | 17 +++++++++++------ pkg/chain/ethereum/ethutil/mine_waiter_test.go | 5 ++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/pkg/chain/ethereum/ethutil/mine_waiter.go b/pkg/chain/ethereum/ethutil/mine_waiter.go index fd11bc1..914f6f5 100644 --- a/pkg/chain/ethereum/ethutil/mine_waiter.go +++ b/pkg/chain/ethereum/ethutil/mine_waiter.go @@ -104,17 +104,22 @@ func (mw MiningWaiter) ForceMining( return } - // transaction not yet mined, add 20% to the previous gas price + // transaction not yet mined, if the previous gas price was the maximum + // one, we no longer resubmit gasPrice := transaction.GasPrice() + if gasPrice.Cmp(mw.maxGasPrice) == 0 { + logger.Infof("reached the maximum allowed gas price; stopping resubmissions") + return + } + + // if we still have some margin, add 20% to the previous gas price twentyPercent := new(big.Int).Div(gasPrice, big.NewInt(5)) gasPrice = new(big.Int).Add(gasPrice, twentyPercent) - // transaction not yet mined but we reached the maximum allowed gas - // price; giving up, we need to wait for the last submitted TX to be - // mined + // if we reached the maximum allowed gas price, submit one more time + // with the maximum if gasPrice.Cmp(mw.maxGasPrice) > 0 { - logger.Infof("reached the maximum allowed gas price; stopping resubmissions") - return + gasPrice = mw.maxGasPrice } // transaction not yet mined and we are still under the maximum allowed diff --git a/pkg/chain/ethereum/ethutil/mine_waiter_test.go b/pkg/chain/ethereum/ethutil/mine_waiter_test.go index ff718f4..984c8a4 100644 --- a/pkg/chain/ethereum/ethutil/mine_waiter_test.go +++ b/pkg/chain/ethereum/ethutil/mine_waiter_test.go @@ -126,14 +126,13 @@ func TestForceMining_MaxAllowedPriceReached(t *testing.T) { var resubmissionGasPrices []*big.Int - expectedAttempts := 4 + expectedAttempts := 5 expectedResubmissionGasPrices := []*big.Int{ big.NewInt(24000000000), // + 20% big.NewInt(28800000000), // + 20% big.NewInt(34560000000), // + 20% big.NewInt(41472000000), // + 20% - // the next one would be 49766400000 but since maxGasPrice = 45 Gwei - // resubmissions should stop here + big.NewInt(45000000000), // max allowed } resubmitFn := func(gasPrice *big.Int) (*types.Transaction, error) { From 9036c7c35946f97b8e6c02cfc92bcd304b3d468a Mon Sep 17 00:00:00 2001 From: Piotr Dyraga Date: Fri, 15 May 2020 23:46:26 +0200 Subject: [PATCH 12/12] Do not resubmit TX if the original gas price was higher than allowed --- pkg/chain/ethereum/ethutil/mine_waiter.go | 10 +++++++ .../ethereum/ethutil/mine_waiter_test.go | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/pkg/chain/ethereum/ethutil/mine_waiter.go b/pkg/chain/ethereum/ethutil/mine_waiter.go index 914f6f5..59d8346 100644 --- a/pkg/chain/ethereum/ethutil/mine_waiter.go +++ b/pkg/chain/ethereum/ethutil/mine_waiter.go @@ -82,6 +82,16 @@ func (mw MiningWaiter) ForceMining( originalTransaction *types.Transaction, resubmitFn ResubmitTransactionFn, ) { + // if the original transaction's gas price was higher or equal the max + // allowed we do nothing; we need to wait for it to be mined + if originalTransaction.GasPrice().Cmp(mw.maxGasPrice) >= 0 { + logger.Infof( + "original transaction gas price is higher than the max allowed; " + + "skipping resubmissions", + ) + return + } + transaction := originalTransaction for { receipt, err := mw.WaitMined(mw.checkInterval, transaction) diff --git a/pkg/chain/ethereum/ethutil/mine_waiter_test.go b/pkg/chain/ethereum/ethutil/mine_waiter_test.go index 984c8a4..86792e5 100644 --- a/pkg/chain/ethereum/ethutil/mine_waiter_test.go +++ b/pkg/chain/ethereum/ethutil/mine_waiter_test.go @@ -168,6 +168,33 @@ func TestForceMining_MaxAllowedPriceReached(t *testing.T) { } } +func TestForceMining_OriginalPriceHigherThanMaxAllowed(t *testing.T) { + // original transaction was priced at 46 Gwei, the maximum allowed gas price + // is 45 Gwei + originalTransaction := createTransaction(big.NewInt(46000000000)) + + mockBackend := &mockDeployBackend{} + + var resubmissionGasPrices []*big.Int + + resubmitFn := func(gasPrice *big.Int) (*types.Transaction, error) { + resubmissionGasPrices = append(resubmissionGasPrices, gasPrice) + // not setting mockBackend.receipt, mining takes a very long time + return createTransaction(gasPrice), nil + } + + waiter := NewMiningWaiter(mockBackend, checkInterval, maxGasPrice) + waiter.ForceMining( + originalTransaction, + resubmitFn, + ) + + resubmissionCount := len(resubmissionGasPrices) + if resubmissionCount != 0 { + t.Fatalf("expected no resubmissions; has: [%v]", resubmissionCount) + } +} + func createTransaction(gasPrice *big.Int) *types.Transaction { return types.NewTransaction( 10, // nonce