Skip to content

Commit c100119

Browse files
authored
[TT-1923] retry gas estimation in Seth (#1532)
1 parent 43a530d commit c100119

17 files changed

+278
-162
lines changed

.nancy-ignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ CVE-2024-32972 # CWE-400: Uncontrolled Resource Consumption ('Resource Exhaustio
1313
CVE-2023-42319 # CWE-noinfo: lol... go-ethereum v1.13.8 again
1414
CVE-2024-10086 # Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
1515
CVE-2024-51744 # CWE-755: Improper Handling of Exceptional Conditions
16-
CVE-2024-45338 # CWE-770: Allocation of Resources Without Limits or Throttling
16+
CVE-2024-45338 # CWE-770: Allocation of Resources Without Limits or Throttling
17+
CVE-2024-45337 # CWE-863: Incorrect Authorization in golang.org/x/[email protected]

book/src/libs/seth.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Reliable and debug-friendly Ethereum client
7070
- [ ] Tracing support (prestate)
7171
- [x] Tracing decoding
7272
- [x] Tracing tests
73-
- [ ] More tests for corner cases of decoding/tracing
73+
- [x] More tests for corner cases of decoding/tracing
7474
- [x] Saving of deployed contracts mapping (`address -> ABI_name`) for live networks
7575
- [x] Reading of deployed contracts mappings for live networks
7676
- [x] Automatic gas estimator (experimental)
@@ -233,7 +233,9 @@ client, err := NewClientBuilder().
233233
// EIP-1559 and gas estimations
234234
WithEIP1559DynamicFees(true).
235235
WithDynamicGasPrices(120_000_000_000, 44_000_000_000).
236-
WithGasPriceEstimations(true, 10, seth.Priority_Fast).
236+
// estimate gas prices based on the information from the RPC based on 10 last blocks
237+
// adjust the value to fast priority and 3 attempts to get the estimations
238+
WithGasPriceEstimations(true, 10, seth.Priority_Fast, 3).
237239
// gas bumping: retries, max gas price, bumping strategy function
238240
WithGasBumping(5, 100_000_000_000, PriorityBasedGasBumpingStrategyFn).
239241
Build()
@@ -441,6 +443,8 @@ For real networks, the estimation process differs for legacy transactions and th
441443
5. **Final Fee Calculation**: Sum the base fee and adjusted tip to set the `gas_fee_cap`.
442444
6. **Congestion Buffer**: Similar to legacy transactions, analyze congestion and apply a buffer to both the fee cap and the tip to secure transaction inclusion.
443445

446+
Regardless of transaction type, if fetching data from RPC or calculating prices fails due to any issue and `gas_price_estimation_attempt_count` is > 1 we will retry it N-1 number of times.
447+
444448
Understanding and setting these parameters correctly ensures that your transactions are processed efficiently and cost-effectively on the network.
445449

446450
When fetching historical base fee and tip data, we will use the last `gas_price_estimation_blocks` blocks. If it's set to `0` we will default to `100` last blocks. If the blockchain has less than `100` blocks we will use all of them.
@@ -492,6 +496,14 @@ case Congestion_VeryHigh:
492496
For low congestion rate we will increase gas price by 10%, for medium by 20%, for high by 30% and for very high by 40%. We cache block header data in an in-memory cache, so we don't have to fetch it every time we estimate gas. The cache has capacity equal to `gas_price_estimation_blocks` and every time we add a new element, we remove one that is least frequently used and oldest (with block number being a constant and chain always moving forward it makes no sense to keep old blocks). It's important to know that in order to use congestion metrics we need to fetch at least 80% of the requested blocks. If that fails, we will skip this part of the estimation and only adjust the gas price based on priority.
493497
For both transaction types if any of the steps fails, we fall back to hardcoded values.
494498

499+
##### Gas estimations attemps
500+
501+
If for any reason fetching gas price suggestions or fee history from the RPC fails, or subsequent calulation of percentiles fails, it can be retried. This behaviour is controlled by `gas_price_estimation_attempt_count`, which if empty or set to `0` will default to
502+
just one attempt, which means that if it fails, it won't be retried. Set it to `2` to allow a single retry, etc.
503+
504+
> [!NOTE]
505+
> To disable gas estimation set `gas_price_estimation_enabled` to `false`. Setting `gas_price_estimation_attempt_count` to `0` won't have such effect.
506+
495507
### DOT graphs
496508

497509
There are multiple ways of visualising DOT graphs:

seth/block_stats.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func (cs *BlockStats) CalculateBlockDurations(blocks []*types.Block) error {
125125
totalSize := uint64(0)
126126

127127
for i := 1; i < len(blocks); i++ {
128-
duration := time.Unix(int64(blocks[i].Time()), 0).Sub(time.Unix(int64(blocks[i-1].Time()), 0))
128+
duration := time.Unix(mustSafeInt64(blocks[i].Time()), 0).Sub(time.Unix(mustSafeInt64(blocks[i-1].Time()), 0))
129129
durations = append(durations, duration)
130130
totalDuration += duration
131131

@@ -155,7 +155,7 @@ func (cs *BlockStats) CalculateBlockDurations(blocks []*types.Block) error {
155155

156156
L.Debug().
157157
Uint64("BlockNumber", blocks[i].Number().Uint64()).
158-
Time("BlockTime", time.Unix(int64(blocks[i].Time()), 0)).
158+
Time("BlockTime", time.Unix(mustSafeInt64(blocks[i].Time()), 0)).
159159
Str("Duration", duration.String()).
160160
Float64("GasUsedPercentage", calculateRatioPercentage(blocks[i].GasUsed(), blocks[i].GasLimit())).
161161
Float64("TPS", tps).

seth/client.go

Lines changed: 11 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,12 @@ type Client struct {
8383
func NewClientWithConfig(cfg *Config) (*Client, error) {
8484
initDefaultLogging()
8585

86-
err := ValidateConfig(cfg)
87-
if err != nil {
88-
return nil, err
86+
if cfg == nil {
87+
return nil, errors.New("seth config cannot be nil")
88+
}
89+
90+
if cfgErr := cfg.Validate(); cfgErr != nil {
91+
return nil, cfgErr
8992
}
9093

9194
L.Debug().Msgf("Using tracing level: %s", cfg.TracingLevel)
@@ -155,73 +158,6 @@ func NewClientWithConfig(cfg *Config) (*Client, error) {
155158
)
156159
}
157160

158-
// ValidateConfig checks and validates the provided Config struct.
159-
// It ensures essential fields have valid values or default to appropriate values
160-
// when necessary. This function performs validation on gas price estimation,
161-
// gas limit, tracing level, trace outputs, network dial timeout, and pending nonce protection timeout.
162-
// If any configuration is invalid, it returns an error.
163-
func ValidateConfig(cfg *Config) error {
164-
if cfg.Network.GasPriceEstimationEnabled {
165-
if cfg.Network.GasPriceEstimationBlocks == 0 {
166-
L.Debug().Msg("Gas estimation is enabled, but block headers to use is set to 0. Will not use block congestion for gas estimation")
167-
}
168-
cfg.Network.GasPriceEstimationTxPriority = strings.ToLower(cfg.Network.GasPriceEstimationTxPriority)
169-
170-
if cfg.Network.GasPriceEstimationTxPriority == "" {
171-
cfg.Network.GasPriceEstimationTxPriority = Priority_Standard
172-
}
173-
174-
switch cfg.Network.GasPriceEstimationTxPriority {
175-
case Priority_Degen:
176-
case Priority_Fast:
177-
case Priority_Standard:
178-
case Priority_Slow:
179-
default:
180-
return errors.New("when automating gas estimation is enabled priority must be fast, standard or slow. fix it or disable gas estimation")
181-
}
182-
183-
}
184-
185-
if cfg.Network.GasLimit != 0 {
186-
L.Warn().
187-
Msg("Gas limit is set, this will override the gas limit set by the network. This option should be used **ONLY** if node is incapable of estimating gas limit itself, which happens only with very old versions")
188-
}
189-
190-
if cfg.TracingLevel == "" {
191-
cfg.TracingLevel = TracingLevel_Reverted
192-
}
193-
194-
cfg.TracingLevel = strings.ToUpper(cfg.TracingLevel)
195-
196-
switch cfg.TracingLevel {
197-
case TracingLevel_None:
198-
case TracingLevel_Reverted:
199-
case TracingLevel_All:
200-
default:
201-
return errors.New("tracing level must be one of: NONE, REVERTED, ALL")
202-
}
203-
204-
for _, output := range cfg.TraceOutputs {
205-
switch strings.ToLower(output) {
206-
case TraceOutput_Console:
207-
case TraceOutput_JSON:
208-
case TraceOutput_DOT:
209-
default:
210-
return errors.New("trace output must be one of: console, json, dot")
211-
}
212-
}
213-
214-
if cfg.Network.DialTimeout == nil {
215-
cfg.Network.DialTimeout = &Duration{D: DefaultDialTimeout}
216-
}
217-
218-
if cfg.PendingNonceProtectionTimeout == nil {
219-
cfg.PendingNonceProtectionTimeout = &Duration{D: DefaultPendingNonceProtectionTimeout}
220-
}
221-
222-
return nil
223-
}
224-
225161
// NewClient creates a new raw seth client with all deps setup from env vars
226162
func NewClient() (*Client, error) {
227163
cfg, err := ReadConfig()
@@ -277,7 +213,7 @@ func NewClientRaw(
277213
Addresses: addrs,
278214
PrivateKeys: pkeys,
279215
URL: cfg.FirstNetworkURL(),
280-
ChainID: int64(cfg.Network.ChainID),
216+
ChainID: mustSafeInt64(cfg.Network.ChainID),
281217
Context: ctx,
282218
CancelFunc: cancelFunc,
283219
}
@@ -481,7 +417,7 @@ func (m *Client) TransferETHFromKey(ctx context.Context, fromKeyNum int, to stri
481417
if err != nil {
482418
gasLimit = m.Cfg.Network.TransferGasFee
483419
} else {
484-
gasLimit = int64(gasLimitRaw)
420+
gasLimit = mustSafeInt64(gasLimitRaw)
485421
}
486422

487423
if gasPrice == nil {
@@ -492,7 +428,7 @@ func (m *Client) TransferETHFromKey(ctx context.Context, fromKeyNum int, to stri
492428
Nonce: m.NonceManager.NextNonce(m.Addresses[fromKeyNum]).Uint64(),
493429
To: &toAddr,
494430
Value: value,
495-
Gas: uint64(gasLimit),
431+
Gas: mustSafeUint64(gasLimit),
496432
GasPrice: gasPrice,
497433
}
498434
L.Debug().Interface("TransferTx", rawTx).Send()
@@ -607,7 +543,7 @@ func WithPending(pending bool) CallOpt {
607543
// WithBlockNumber sets blockNumber option for bind.CallOpts
608544
func WithBlockNumber(bn uint64) CallOpt {
609545
return func(o *bind.CallOpts) {
610-
o.BlockNumber = big.NewInt(int64(bn))
546+
o.BlockNumber = big.NewInt(mustSafeInt64(bn))
611547
}
612548
}
613549

@@ -1002,7 +938,7 @@ func (m *Client) configureTransactionOpts(
1002938
estimations GasEstimations,
1003939
o ...TransactOpt,
1004940
) *bind.TransactOpts {
1005-
opts.Nonce = big.NewInt(int64(nonce))
941+
opts.Nonce = big.NewInt(mustSafeInt64(nonce))
1006942
opts.GasPrice = estimations.GasPrice
1007943
opts.GasLimit = m.Cfg.Network.GasLimit
1008944

seth/client_api_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ func TestAPINonces(t *testing.T) {
124124
{
125125
name: "with nonce override",
126126
transactionOpts: []seth.TransactOpt{
127+
//nolint
127128
seth.WithNonce(big.NewInt(int64(pnonce))),
128129
},
129130
},

seth/client_builder.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,18 @@ type ClientBuilder struct {
2323
// NewClientBuilder creates a new ClientBuilder with reasonable default values. You only need to pass private key(s) and RPC URL to build a usable config.
2424
func NewClientBuilder() *ClientBuilder {
2525
network := &Network{
26-
Name: DefaultNetworkName,
27-
EIP1559DynamicFees: true,
28-
TxnTimeout: MustMakeDuration(5 * time.Minute),
29-
DialTimeout: MustMakeDuration(DefaultDialTimeout),
30-
TransferGasFee: DefaultTransferGasFee,
31-
GasPriceEstimationEnabled: true,
32-
GasPriceEstimationBlocks: 200,
33-
GasPriceEstimationTxPriority: Priority_Standard,
34-
GasPrice: DefaultGasPrice,
35-
GasFeeCap: DefaultGasFeeCap,
36-
GasTipCap: DefaultGasTipCap,
26+
Name: DefaultNetworkName,
27+
EIP1559DynamicFees: true,
28+
TxnTimeout: MustMakeDuration(5 * time.Minute),
29+
DialTimeout: MustMakeDuration(DefaultDialTimeout),
30+
TransferGasFee: DefaultTransferGasFee,
31+
GasPriceEstimationEnabled: true,
32+
GasPriceEstimationBlocks: 200,
33+
GasPriceEstimationTxPriority: Priority_Standard,
34+
GasPrice: DefaultGasPrice,
35+
GasFeeCap: DefaultGasFeeCap,
36+
GasTipCap: DefaultGasTipCap,
37+
GasPriceEstimationAttemptCount: DefaultGasPriceEstimationsAttemptCount,
3738
}
3839

3940
return &ClientBuilder{
@@ -176,22 +177,25 @@ func (c *ClientBuilder) WithNetworkChainId(chainId uint64) *ClientBuilder {
176177
// WithGasPriceEstimations enables or disables gas price estimations, sets the number of blocks to use for estimation or transaction priority.
177178
// Even with estimations enabled you should still either set legacy gas price with `WithLegacyGasPrice()` or EIP-1559 dynamic fees with `WithDynamicGasPrices()`
178179
// ss they will be used as fallback values, if the estimations fail.
180+
// To disable gas price estimations, set the enabled parameter to false. Setting attemptCount to 0 won't disable it, it will be treated as "no value" and default to 1.
179181
// Following priorities are supported: "slow", "standard" and "fast"
180-
// Default values are true for enabled, 200 blocks for estimation and "standard" for priority.
181-
func (c *ClientBuilder) WithGasPriceEstimations(enabled bool, estimationBlocks uint64, txPriority string) *ClientBuilder {
182+
// Default values are true for enabled, 200 blocks for estimation, "standard" for priority and 1 attempt.
183+
func (c *ClientBuilder) WithGasPriceEstimations(enabled bool, estimationBlocks uint64, txPriority string, attemptCount uint) *ClientBuilder {
182184
if !c.checkIfNetworkIsSet() {
183185
return c
184186
}
185187
c.config.Network.GasPriceEstimationEnabled = enabled
186188
c.config.Network.GasPriceEstimationBlocks = estimationBlocks
187189
c.config.Network.GasPriceEstimationTxPriority = txPriority
190+
c.config.Network.GasPriceEstimationAttemptCount = attemptCount
188191
// defensive programming
189192
if len(c.config.Networks) == 0 {
190193
c.config.Networks = append(c.config.Networks, c.config.Network)
191194
} else if net := c.config.findNetworkByName(c.config.Network.Name); net != nil {
192195
net.GasPriceEstimationEnabled = enabled
193196
net.GasPriceEstimationBlocks = estimationBlocks
194197
net.GasPriceEstimationTxPriority = txPriority
198+
net.GasPriceEstimationAttemptCount = attemptCount
195199
}
196200
return c
197201
}

seth/client_decode_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
func TestSmokeDebugReverts(t *testing.T) {
13-
c := newClient(t)
13+
c := newClientWithContractMapFromEnv(t)
1414

1515
type test struct {
1616
name string
@@ -63,7 +63,7 @@ func TestSmokeDebugReverts(t *testing.T) {
6363
}
6464

6565
func TestSmokeDebugData(t *testing.T) {
66-
c := newClient(t)
66+
c := newClientWithContractMapFromEnv(t)
6767
c.Cfg.TracingLevel = seth.TracingLevel_All
6868

6969
type test struct {
@@ -226,9 +226,10 @@ func TestSmokeDebugData(t *testing.T) {
226226
require.NoError(t, err)
227227
require.Equal(t, dtx.Input, tc.output.Input)
228228
require.Equal(t, dtx.Output, tc.output.Output)
229+
require.Equal(t, len(tc.output.Events), len(dtx.Events))
229230
for i, e := range tc.output.Events {
230231
require.NotNil(t, dtx.Events[i])
231-
require.Equal(t, dtx.Events[i].EventData, e.EventData)
232+
require.Equal(t, e.EventData, dtx.Events[i].EventData)
232233
}
233234
}
234235
})

0 commit comments

Comments
 (0)