-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #36 from keep-network/the-bid
See keep-network/keep-core#1803 For every submitted transaction, 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. The interval at which we check for transaction status, as well as the maximum gas price, can be configured.
- Loading branch information
Showing
10 changed files
with
506 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package ethutil | ||
|
||
import ( | ||
"context" | ||
"math/big" | ||
"time" | ||
|
||
"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 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 | ||
maxGasPrice *big.Int | ||
} | ||
|
||
// NewMiningWaiter creates a new MiningWaiter instance for the provided | ||
// 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, | ||
maxGasPrice *big.Int, | ||
) *MiningWaiter { | ||
return &MiningWaiter{ | ||
backend, | ||
checkInterval, | ||
maxGasPrice, | ||
} | ||
} | ||
|
||
// 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(context.Background(), timeout) | ||
defer cancel() | ||
|
||
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: | ||
} | ||
} | ||
} | ||
|
||
// 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, | ||
) { | ||
// 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) | ||
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 | ||
} | ||
|
||
// 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) | ||
|
||
// if we reached the maximum allowed gas price, submit one more time | ||
// with the maximum | ||
if gasPrice.Cmp(mw.maxGasPrice) > 0 { | ||
gasPrice = mw.maxGasPrice | ||
} | ||
|
||
// 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.Warningf("could not resubmit TX with a higher gas price: [%v]", err) | ||
return | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
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 := 5 | ||
expectedResubmissionGasPrices := []*big.Int{ | ||
big.NewInt(24000000000), // + 20% | ||
big.NewInt(28800000000), // + 20% | ||
big.NewInt(34560000000), // + 20% | ||
big.NewInt(41472000000), // + 20% | ||
big.NewInt(45000000000), // max allowed | ||
} | ||
|
||
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 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 | ||
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") | ||
} |
Oops, something went wrong.