Skip to content

Commit b5b830a

Browse files
authored
[DX-2127] add support for auto priority to Seth (#2199)
1 parent a9e72b8 commit b5b830a

File tree

12 files changed

+347
-127
lines changed

12 files changed

+347
-127
lines changed

book/src/libs/seth.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,13 +568,17 @@ For real networks, the estimation process differs for legacy transactions and th
568568

569569
##### Legacy Transactions
570570

571+
Unless priority is set to `auto`, when we will defer to what the RPC node suggests, following logic is used:
572+
571573
1. **Initial Price**: Query the network node for the current suggested gas price.
572574
2. **Priority Adjustment**: Modify the initial price based on `gas_price_estimation_tx_priority`. Higher priority increases the price to ensure faster inclusion in a block.
573575
3. **Congestion Analysis**: Examine the last X blocks (as specified by `gas_price_estimation_blocks`) to determine network congestion, calculating the usage rate of gas in each block and giving recent blocks more weight. Disabled if `gas_price_estimation_blocks` equals `0`.
574576
4. **Buffering**: Add a buffer to the adjusted gas price to increase transaction reliability during high congestion.
575577

576578
##### EIP-1559 Transactions
577579

580+
Unless priority is set to `auto`, when we will defer to what the RPC node suggests, following logic is used:
581+
578582
1. **Tip Fee Query**: Ask the node for the current recommended tip fee.
579583
2. **Fee History Analysis**: Gather the base fee and tip history from recent blocks to establish a fee baseline.
580584
3. **Fee Selection**: Use the greatest of the node's suggested tip or the historical average tip for upcoming calculations.

seth/.changeset/v1.51.3.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Added "auto" priority which defers all gas related estimations to the RPC node (by setting corresponding values to `nil`)

seth/client.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,10 @@ func NewClientRaw(
320320
if cfg.ReadOnly {
321321
return nil, errors.New(ErrReadOnlyEphemeralKeys)
322322
}
323-
gasPrice, err := c.GetSuggestedLegacyFees(context.Background(), Priority_Standard)
323+
ctx, cancel := context.WithTimeout(context.Background(), c.Cfg.Network.TxnTimeout.D)
324+
defer cancel()
325+
326+
gasPrice, err := c.GetSuggestedLegacyFees(ctx, c.Cfg.Network.GasPriceEstimationTxPriority)
324327
if err != nil {
325328
gasPrice = big.NewInt(c.Cfg.Network.GasPrice)
326329
}
@@ -331,8 +334,6 @@ func NewClientRaw(
331334
}
332335
L.Warn().Msg("Ephemeral mode, all funds will be lost!")
333336

334-
ctx, cancel := context.WithCancel(context.Background())
335-
defer cancel()
336337
eg, egCtx := errgroup.WithContext(ctx)
337338
// root key is element 0 in ephemeral
338339
for _, addr := range c.Addresses[1:] {
@@ -408,7 +409,7 @@ func (m *Client) checkRPCHealth() error {
408409
ctx, cancel := context.WithTimeout(context.Background(), m.Cfg.Network.TxnTimeout.Duration())
409410
defer cancel()
410411

411-
gasPrice, err := m.GetSuggestedLegacyFees(context.Background(), Priority_Standard)
412+
gasPrice, err := m.GetSuggestedLegacyFees(context.Background(), m.Cfg.Network.GasPriceEstimationTxPriority)
412413
if err != nil {
413414
gasPrice = big.NewInt(m.Cfg.Network.GasPrice)
414415
}
@@ -772,8 +773,11 @@ func (m *Client) AnySyncedKey() int {
772773
}
773774

774775
type GasEstimations struct {
775-
GasPrice *big.Int
776+
// GasPrice for legacy transactions. If nil, RPC node will auto-estimate.
777+
GasPrice *big.Int
778+
// GasTipCap for EIP-1559 transactions. If nil, RPC node will auto-estimate.
776779
GasTipCap *big.Int
780+
// GasFeeCap for EIP-1559 transactions. If nil, RPC node will auto-estimate.
777781
GasFeeCap *big.Int
778782
}
779783

@@ -825,9 +829,8 @@ func (m *Client) getProposedTransactionOptions(keyNum int) (*bind.TransactOpts,
825829
pending nonce for key %d is higher than last nonce, there are %d pending transactions.
826830
827831
This issue is caused by one of two things:
828-
1. You are using the same keyNum in multiple goroutines, which is not supported. Each goroutine should use an unique keyNum.
829-
2. You have stuck transaction(s). Speed them up by sending replacement transactions with higher gas price before continuing, otherwise future transactions most probably will also get stuck.
830-
`
832+
1. You are using the same keyNum in multiple goroutines, which is not supported. Each goroutine should use an unique keyNum
833+
2. You have stuck transaction(s). Speed them up by sending replacement transactions with higher gas price before continuing, otherwise future transactions most probably will also get stuck`
831834
err := fmt.Errorf(errMsg, keyNum, nonceStatus.PendingNonce-nonceStatus.LastNonce)
832835
m.Errors = append(m.Errors, err)
833836
// can't return nil, otherwise RPC wrapper will panic, and we might lose funds on testnets/mainnets, that's why
@@ -890,6 +893,15 @@ func (m *Client) NewDefaultGasEstimationRequest() GasEstimationRequest {
890893
func (m *Client) CalculateGasEstimations(request GasEstimationRequest) GasEstimations {
891894
estimations := GasEstimations{}
892895

896+
if request.Priority == Priority_Auto {
897+
// Return empty estimations with nil gas prices and fees.
898+
// This signals to the RPC node to auto-estimate gas prices.
899+
// The nil values will be passed to bind.TransactOpts, which treats
900+
// nil gas fields as a request for automatic estimation.
901+
L.Debug().Msg("Auto priority selected, skipping gas estimations")
902+
return estimations
903+
}
904+
893905
if m.Cfg.IsSimulatedNetwork() || !request.GasEstimationEnabled {
894906
estimations.GasPrice = big.NewInt(request.FallbackGasPrice)
895907
estimations.GasFeeCap = big.NewInt(request.FallbackGasFeeCap)
@@ -978,7 +990,15 @@ func (m *Client) configureTransactionOpts(
978990
opts.GasPrice = nil
979991
opts.GasTipCap = estimations.GasTipCap
980992
opts.GasFeeCap = estimations.GasFeeCap
993+
994+
// Log when using auto-estimated gas (all nil values)
995+
if opts.GasTipCap == nil && opts.GasFeeCap == nil {
996+
L.Debug().Msg("Using RPC node's automatic gas estimation (EIP-1559)")
997+
}
998+
} else if opts.GasPrice == nil {
999+
L.Debug().Msg("Using RPC node's automatic gas estimation (Legacy)")
9811000
}
1001+
9821002
for _, f := range o {
9831003
f(opts)
9841004
}

seth/client_builder_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,60 @@ func TestConfig_SimulatedBackend(t *testing.T) {
434434
require.IsType(t, backend.Client(), client.Client, "expected simulated client")
435435
}
436436

437+
func TestConfig_SimulatedBackend_Priority_Auto(t *testing.T) {
438+
backend, cancelFn := StartSimulatedBackend([]common.Address{common.HexToAddress("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")})
439+
t.Cleanup(func() {
440+
cancelFn()
441+
})
442+
443+
builder := seth.NewClientBuilder()
444+
445+
client, err := builder.
446+
WithNetworkName("simulated").
447+
WithEthClient(backend.Client()).
448+
WithPrivateKeys([]string{"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"}).
449+
WithGasPriceEstimations(true, 10, seth.Priority_Auto, 1).
450+
Build()
451+
452+
require.NoError(t, err, "failed to build client")
453+
require.Equal(t, 1, len(client.PrivateKeys), "expected 1 private key")
454+
require.Equal(t, 1, len(client.Addresses), "expected 1 addresse")
455+
require.IsType(t, backend.Client(), client.Client, "expected simulated client")
456+
457+
linkAbi, err := link_token.LinkTokenMetaData.GetAbi()
458+
require.NoError(t, err, "failed to get LINK ABI")
459+
460+
_, err = client.DeployContract(client.NewTXOpts(), "LinkToken", *linkAbi, common.FromHex(link_token.LinkTokenMetaData.Bin))
461+
require.NoError(t, err, "failed to deploy LINK contract")
462+
}
463+
464+
func TestConfig_SimulatedBackend_No_Historical_Fees(t *testing.T) {
465+
backend, cancelFn := StartSimulatedBackend([]common.Address{common.HexToAddress("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")})
466+
t.Cleanup(func() {
467+
cancelFn()
468+
})
469+
470+
builder := seth.NewClientBuilder()
471+
472+
client, err := builder.
473+
WithNetworkName("simulated").
474+
WithEthClient(backend.Client()).
475+
WithPrivateKeys([]string{"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"}).
476+
WithGasPriceEstimations(true, 0, seth.Priority_Standard, 1).
477+
Build()
478+
479+
require.NoError(t, err, "failed to build client")
480+
require.Equal(t, 1, len(client.PrivateKeys), "expected 1 private key")
481+
require.Equal(t, 1, len(client.Addresses), "expected 1 addresse")
482+
require.IsType(t, backend.Client(), client.Client, "expected simulated client")
483+
484+
linkAbi, err := link_token.LinkTokenMetaData.GetAbi()
485+
require.NoError(t, err, "failed to get LINK ABI")
486+
487+
_, err = client.DeployContract(client.NewTXOpts(), "LinkToken", *linkAbi, common.FromHex(link_token.LinkTokenMetaData.Bin))
488+
require.NoError(t, err, "failed to deploy LINK contract")
489+
}
490+
437491
func TestConfig_SimulatedBackend_ContractDeploymentHooks(t *testing.T) {
438492
backend, cancelFn := StartSimulatedBackend([]common.Address{common.HexToAddress("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")})
439493
t.Cleanup(func() {

seth/cmd/seth.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,23 +118,23 @@ func RunCLI(args []string) error {
118118
ge := seth.NewGasEstimator(C)
119119
blocks := cCtx.Uint64("blocks")
120120
tipPerc := cCtx.Float64("tipPercentile")
121-
stats, err := ge.Stats(blocks, tipPerc)
121+
stats, err := ge.Stats(cCtx.Context, blocks, tipPerc)
122122
if err != nil {
123123
return err
124124
}
125125
seth.L.Info().
126-
Interface("Max", stats.GasPrice.Max).
127-
Interface("99", stats.GasPrice.Perc99).
128-
Interface("75", stats.GasPrice.Perc75).
129-
Interface("50", stats.GasPrice.Perc50).
130-
Interface("25", stats.GasPrice.Perc25).
126+
Interface("Max", stats.BaseFeePerc.Max).
127+
Interface("99", stats.BaseFeePerc.Perc99).
128+
Interface("75", stats.BaseFeePerc.Perc75).
129+
Interface("50", stats.BaseFeePerc.Perc50).
130+
Interface("25", stats.BaseFeePerc.Perc25).
131131
Msg("Base fee (Wei)")
132132
seth.L.Info().
133-
Interface("Max", stats.TipCap.Max).
134-
Interface("99", stats.TipCap.Perc99).
135-
Interface("75", stats.TipCap.Perc75).
136-
Interface("50", stats.TipCap.Perc50).
137-
Interface("25", stats.TipCap.Perc25).
133+
Interface("Max", stats.TipCapPerc.Max).
134+
Interface("99", stats.TipCapPerc.Perc99).
135+
Interface("75", stats.TipCapPerc.Perc75).
136+
Interface("50", stats.TipCapPerc.Perc50).
137+
Interface("25", stats.TipCapPerc.Perc25).
138138
Msg("Priority fee (Wei)")
139139
seth.L.Info().
140140
Interface("GasPrice", stats.SuggestedGasPrice).

seth/config.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,15 @@ func (c *Config) Validate() error {
223223
case Priority_Fast:
224224
case Priority_Standard:
225225
case Priority_Slow:
226+
case Priority_Auto:
226227
default:
227-
return errors.New("when automating gas estimation is enabled priority must be fast, standard or slow. fix it or disable gas estimation")
228+
return errors.New("when automating gas estimation is enabled priority must be auto, fast, standard or slow. fix it or disable gas estimation")
228229
}
229230

231+
if c.GasBump != nil && c.GasBump.Retries > 0 &&
232+
c.Network.GasPriceEstimationTxPriority == Priority_Auto {
233+
return errors.New("gas bumping is not compatible with auto priority gas estimation")
234+
}
230235
}
231236

232237
if c.Network.GasLimit != 0 {

seth/gas.go

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package seth
22

33
import (
44
"context"
5+
"fmt"
56
"math/big"
67

78
"github.com/montanaflynn/stats"
9+
"github.com/pkg/errors"
810
)
911

1012
// GasEstimator estimates gas prices
@@ -21,22 +23,32 @@ func NewGasEstimator(c *Client) *GasEstimator {
2123

2224
// Stats calculates gas price and tip cap suggestions based on historical fee data over a specified number of blocks.
2325
// It computes quantiles for base fees and tip caps and provides suggested gas price and tip cap values.
24-
func (m *GasEstimator) Stats(fromNumber uint64, priorityPerc float64) (GasSuggestions, error) {
25-
bn, err := m.Client.Client.BlockNumber(context.Background())
26+
func (m *GasEstimator) Stats(ctx context.Context, blockCount uint64, priorityPerc float64) (GasSuggestions, error) {
27+
estimations := GasSuggestions{}
28+
29+
if blockCount == 0 {
30+
return estimations, errors.New("block count must be greater than zero")
31+
}
32+
33+
currentBlock, err := m.Client.Client.BlockNumber(ctx)
2634
if err != nil {
27-
return GasSuggestions{}, err
35+
return GasSuggestions{}, fmt.Errorf("failed to get current block number: %w", err)
2836
}
29-
if fromNumber == 0 {
30-
if bn > 100 {
31-
fromNumber = bn - 100
32-
} else {
33-
fromNumber = 1
34-
}
37+
if currentBlock == 0 {
38+
return GasSuggestions{}, errors.New("current block number is zero. No fee history available")
3539
}
36-
hist, err := m.Client.Client.FeeHistory(context.Background(), fromNumber, big.NewInt(int64(bn)), []float64{priorityPerc})
40+
if blockCount >= currentBlock {
41+
blockCount = currentBlock - 1
42+
}
43+
44+
hist, err := m.Client.Client.FeeHistory(ctx, blockCount, big.NewInt(mustSafeInt64(currentBlock)), []float64{priorityPerc})
3745
if err != nil {
38-
return GasSuggestions{}, err
46+
return GasSuggestions{}, fmt.Errorf("failed to get fee history: %w", err)
3947
}
48+
L.Trace().
49+
Interface("History", hist).
50+
Msg("Fee history")
51+
4052
baseFees := make([]float64, 0)
4153
for _, bf := range hist.BaseFee {
4254
if bf == nil {
@@ -48,8 +60,14 @@ func (m *GasEstimator) Stats(fromNumber uint64, priorityPerc float64) (GasSugges
4860
}
4961
gasPercs, err := quantilesFromFloatArray(baseFees)
5062
if err != nil {
51-
return GasSuggestions{}, err
63+
return GasSuggestions{}, fmt.Errorf("failed to calculate quantiles from fee history for base fee: %w", err)
5264
}
65+
estimations.BaseFeePerc = gasPercs
66+
67+
L.Trace().
68+
Interface("Gas percentiles ", gasPercs).
69+
Msg("Base fees")
70+
5371
tips := make([]float64, 0)
5472
for _, bf := range hist.Reward {
5573
if len(bf) == 0 {
@@ -64,25 +82,33 @@ func (m *GasEstimator) Stats(fromNumber uint64, priorityPerc float64) (GasSugges
6482
}
6583
tipPercs, err := quantilesFromFloatArray(tips)
6684
if err != nil {
67-
return GasSuggestions{}, err
85+
return GasSuggestions{}, fmt.Errorf("failed to calculate quantiles from fee history for tip cap: %w", err)
6886
}
69-
suggestedGasPrice, err := m.Client.Client.SuggestGasPrice(context.Background())
87+
estimations.TipCapPerc = tipPercs
88+
L.Trace().
89+
Interface("Gas percentiles ", tipPercs).
90+
Msg("Tip caps")
91+
92+
suggestedGasPrice, err := m.Client.Client.SuggestGasPrice(ctx)
7093
if err != nil {
71-
return GasSuggestions{}, err
94+
return GasSuggestions{}, fmt.Errorf("failed to get suggested gas price: %w", err)
7295
}
73-
suggestedGasTipCap, err := m.Client.Client.SuggestGasTipCap(context.Background())
96+
estimations.SuggestedGasPrice = suggestedGasPrice
97+
98+
suggestedGasTipCap, err := m.Client.Client.SuggestGasTipCap(ctx)
7499
if err != nil {
75-
return GasSuggestions{}, err
100+
return GasSuggestions{}, fmt.Errorf("failed to get suggested gas tip cap: %w", err)
76101
}
77-
L.Trace().
78-
Interface("History", hist).
79-
Msg("Fee history")
80-
return GasSuggestions{
81-
GasPrice: gasPercs,
82-
TipCap: tipPercs,
83-
SuggestedGasPrice: suggestedGasPrice,
84-
SuggestedGasTipCap: suggestedGasTipCap,
85-
}, nil
102+
103+
estimations.SuggestedGasTipCap = suggestedGasTipCap
104+
105+
header, err := m.Client.Client.HeaderByNumber(ctx, nil)
106+
if err != nil {
107+
return GasSuggestions{}, fmt.Errorf("failed to get latest block header: %w", err)
108+
}
109+
estimations.LastBaseFee = header.BaseFee
110+
111+
return estimations, nil
86112
}
87113

88114
// GasPercentiles contains gas percentiles
@@ -95,8 +121,9 @@ type GasPercentiles struct {
95121
}
96122

97123
type GasSuggestions struct {
98-
GasPrice *GasPercentiles
99-
TipCap *GasPercentiles
124+
BaseFeePerc *GasPercentiles
125+
TipCapPerc *GasPercentiles
126+
LastBaseFee *big.Int
100127
SuggestedGasPrice *big.Int
101128
SuggestedGasTipCap *big.Int
102129
}

0 commit comments

Comments
 (0)