diff --git a/.nancy-ignore b/.nancy-ignore index e43ddaec9..31f5456d3 100644 --- a/.nancy-ignore +++ b/.nancy-ignore @@ -13,4 +13,5 @@ CVE-2024-32972 # CWE-400: Uncontrolled Resource Consumption ('Resource Exhaustio CVE-2023-42319 # CWE-noinfo: lol... go-ethereum v1.13.8 again CVE-2024-10086 # Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') CVE-2024-51744 # CWE-755: Improper Handling of Exceptional Conditions -CVE-2024-45338 # CWE-770: Allocation of Resources Without Limits or Throttling \ No newline at end of file +CVE-2024-45338 # CWE-770: Allocation of Resources Without Limits or Throttling +CVE-2024-45337 # CWE-863: Incorrect Authorization in golang.org/x/crypto@v0.28.0 \ No newline at end of file diff --git a/book/src/libs/seth.md b/book/src/libs/seth.md index ff3d5b344..033f7ca10 100644 --- a/book/src/libs/seth.md +++ b/book/src/libs/seth.md @@ -70,7 +70,7 @@ Reliable and debug-friendly Ethereum client - [ ] Tracing support (prestate) - [x] Tracing decoding - [x] Tracing tests -- [ ] More tests for corner cases of decoding/tracing +- [x] More tests for corner cases of decoding/tracing - [x] Saving of deployed contracts mapping (`address -> ABI_name`) for live networks - [x] Reading of deployed contracts mappings for live networks - [x] Automatic gas estimator (experimental) @@ -233,7 +233,9 @@ client, err := NewClientBuilder(). // EIP-1559 and gas estimations WithEIP1559DynamicFees(true). WithDynamicGasPrices(120_000_000_000, 44_000_000_000). - WithGasPriceEstimations(true, 10, seth.Priority_Fast). + // estimate gas prices based on the information from the RPC based on 10 last blocks + // adjust the value to fast priority and 3 attempts to get the estimations + WithGasPriceEstimations(true, 10, seth.Priority_Fast, 3). // gas bumping: retries, max gas price, bumping strategy function WithGasBumping(5, 100_000_000_000, PriorityBasedGasBumpingStrategyFn). Build() @@ -441,6 +443,8 @@ For real networks, the estimation process differs for legacy transactions and th 5. **Final Fee Calculation**: Sum the base fee and adjusted tip to set the `gas_fee_cap`. 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. +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. + Understanding and setting these parameters correctly ensures that your transactions are processed efficiently and cost-effectively on the network. 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: 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. For both transaction types if any of the steps fails, we fall back to hardcoded values. +##### Gas estimations attemps + +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 +just one attempt, which means that if it fails, it won't be retried. Set it to `2` to allow a single retry, etc. + +> [!NOTE] +> To disable gas estimation set `gas_price_estimation_enabled` to `false`. Setting `gas_price_estimation_attempt_count` to `0` won't have such effect. + ### DOT graphs There are multiple ways of visualising DOT graphs: diff --git a/seth/block_stats.go b/seth/block_stats.go index 17cfeca05..db883331c 100644 --- a/seth/block_stats.go +++ b/seth/block_stats.go @@ -125,7 +125,7 @@ func (cs *BlockStats) CalculateBlockDurations(blocks []*types.Block) error { totalSize := uint64(0) for i := 1; i < len(blocks); i++ { - duration := time.Unix(int64(blocks[i].Time()), 0).Sub(time.Unix(int64(blocks[i-1].Time()), 0)) + duration := time.Unix(mustSafeInt64(blocks[i].Time()), 0).Sub(time.Unix(mustSafeInt64(blocks[i-1].Time()), 0)) durations = append(durations, duration) totalDuration += duration @@ -155,7 +155,7 @@ func (cs *BlockStats) CalculateBlockDurations(blocks []*types.Block) error { L.Debug(). Uint64("BlockNumber", blocks[i].Number().Uint64()). - Time("BlockTime", time.Unix(int64(blocks[i].Time()), 0)). + Time("BlockTime", time.Unix(mustSafeInt64(blocks[i].Time()), 0)). Str("Duration", duration.String()). Float64("GasUsedPercentage", calculateRatioPercentage(blocks[i].GasUsed(), blocks[i].GasLimit())). Float64("TPS", tps). diff --git a/seth/client.go b/seth/client.go index 1d35b4d4c..43a8a0af1 100644 --- a/seth/client.go +++ b/seth/client.go @@ -83,9 +83,12 @@ type Client struct { func NewClientWithConfig(cfg *Config) (*Client, error) { initDefaultLogging() - err := ValidateConfig(cfg) - if err != nil { - return nil, err + if cfg == nil { + return nil, errors.New("seth config cannot be nil") + } + + if cfgErr := cfg.Validate(); cfgErr != nil { + return nil, cfgErr } L.Debug().Msgf("Using tracing level: %s", cfg.TracingLevel) @@ -155,73 +158,6 @@ func NewClientWithConfig(cfg *Config) (*Client, error) { ) } -// ValidateConfig checks and validates the provided Config struct. -// It ensures essential fields have valid values or default to appropriate values -// when necessary. This function performs validation on gas price estimation, -// gas limit, tracing level, trace outputs, network dial timeout, and pending nonce protection timeout. -// If any configuration is invalid, it returns an error. -func ValidateConfig(cfg *Config) error { - if cfg.Network.GasPriceEstimationEnabled { - if cfg.Network.GasPriceEstimationBlocks == 0 { - L.Debug().Msg("Gas estimation is enabled, but block headers to use is set to 0. Will not use block congestion for gas estimation") - } - cfg.Network.GasPriceEstimationTxPriority = strings.ToLower(cfg.Network.GasPriceEstimationTxPriority) - - if cfg.Network.GasPriceEstimationTxPriority == "" { - cfg.Network.GasPriceEstimationTxPriority = Priority_Standard - } - - switch cfg.Network.GasPriceEstimationTxPriority { - case Priority_Degen: - case Priority_Fast: - case Priority_Standard: - case Priority_Slow: - default: - return errors.New("when automating gas estimation is enabled priority must be fast, standard or slow. fix it or disable gas estimation") - } - - } - - if cfg.Network.GasLimit != 0 { - L.Warn(). - 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") - } - - if cfg.TracingLevel == "" { - cfg.TracingLevel = TracingLevel_Reverted - } - - cfg.TracingLevel = strings.ToUpper(cfg.TracingLevel) - - switch cfg.TracingLevel { - case TracingLevel_None: - case TracingLevel_Reverted: - case TracingLevel_All: - default: - return errors.New("tracing level must be one of: NONE, REVERTED, ALL") - } - - for _, output := range cfg.TraceOutputs { - switch strings.ToLower(output) { - case TraceOutput_Console: - case TraceOutput_JSON: - case TraceOutput_DOT: - default: - return errors.New("trace output must be one of: console, json, dot") - } - } - - if cfg.Network.DialTimeout == nil { - cfg.Network.DialTimeout = &Duration{D: DefaultDialTimeout} - } - - if cfg.PendingNonceProtectionTimeout == nil { - cfg.PendingNonceProtectionTimeout = &Duration{D: DefaultPendingNonceProtectionTimeout} - } - - return nil -} - // NewClient creates a new raw seth client with all deps setup from env vars func NewClient() (*Client, error) { cfg, err := ReadConfig() @@ -277,7 +213,7 @@ func NewClientRaw( Addresses: addrs, PrivateKeys: pkeys, URL: cfg.FirstNetworkURL(), - ChainID: int64(cfg.Network.ChainID), + ChainID: mustSafeInt64(cfg.Network.ChainID), Context: ctx, CancelFunc: cancelFunc, } @@ -481,7 +417,7 @@ func (m *Client) TransferETHFromKey(ctx context.Context, fromKeyNum int, to stri if err != nil { gasLimit = m.Cfg.Network.TransferGasFee } else { - gasLimit = int64(gasLimitRaw) + gasLimit = mustSafeInt64(gasLimitRaw) } if gasPrice == nil { @@ -492,7 +428,7 @@ func (m *Client) TransferETHFromKey(ctx context.Context, fromKeyNum int, to stri Nonce: m.NonceManager.NextNonce(m.Addresses[fromKeyNum]).Uint64(), To: &toAddr, Value: value, - Gas: uint64(gasLimit), + Gas: mustSafeUint64(gasLimit), GasPrice: gasPrice, } L.Debug().Interface("TransferTx", rawTx).Send() @@ -607,7 +543,7 @@ func WithPending(pending bool) CallOpt { // WithBlockNumber sets blockNumber option for bind.CallOpts func WithBlockNumber(bn uint64) CallOpt { return func(o *bind.CallOpts) { - o.BlockNumber = big.NewInt(int64(bn)) + o.BlockNumber = big.NewInt(mustSafeInt64(bn)) } } @@ -1002,7 +938,7 @@ func (m *Client) configureTransactionOpts( estimations GasEstimations, o ...TransactOpt, ) *bind.TransactOpts { - opts.Nonce = big.NewInt(int64(nonce)) + opts.Nonce = big.NewInt(mustSafeInt64(nonce)) opts.GasPrice = estimations.GasPrice opts.GasLimit = m.Cfg.Network.GasLimit diff --git a/seth/client_api_test.go b/seth/client_api_test.go index 4c03afe06..8f7721fdb 100644 --- a/seth/client_api_test.go +++ b/seth/client_api_test.go @@ -124,6 +124,7 @@ func TestAPINonces(t *testing.T) { { name: "with nonce override", transactionOpts: []seth.TransactOpt{ + //nolint seth.WithNonce(big.NewInt(int64(pnonce))), }, }, diff --git a/seth/client_builder.go b/seth/client_builder.go index 52b8b79c5..acf885784 100644 --- a/seth/client_builder.go +++ b/seth/client_builder.go @@ -23,17 +23,18 @@ type ClientBuilder struct { // 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. func NewClientBuilder() *ClientBuilder { network := &Network{ - Name: DefaultNetworkName, - EIP1559DynamicFees: true, - TxnTimeout: MustMakeDuration(5 * time.Minute), - DialTimeout: MustMakeDuration(DefaultDialTimeout), - TransferGasFee: DefaultTransferGasFee, - GasPriceEstimationEnabled: true, - GasPriceEstimationBlocks: 200, - GasPriceEstimationTxPriority: Priority_Standard, - GasPrice: DefaultGasPrice, - GasFeeCap: DefaultGasFeeCap, - GasTipCap: DefaultGasTipCap, + Name: DefaultNetworkName, + EIP1559DynamicFees: true, + TxnTimeout: MustMakeDuration(5 * time.Minute), + DialTimeout: MustMakeDuration(DefaultDialTimeout), + TransferGasFee: DefaultTransferGasFee, + GasPriceEstimationEnabled: true, + GasPriceEstimationBlocks: 200, + GasPriceEstimationTxPriority: Priority_Standard, + GasPrice: DefaultGasPrice, + GasFeeCap: DefaultGasFeeCap, + GasTipCap: DefaultGasTipCap, + GasPriceEstimationAttemptCount: DefaultGasPriceEstimationsAttemptCount, } return &ClientBuilder{ @@ -176,15 +177,17 @@ func (c *ClientBuilder) WithNetworkChainId(chainId uint64) *ClientBuilder { // WithGasPriceEstimations enables or disables gas price estimations, sets the number of blocks to use for estimation or transaction priority. // Even with estimations enabled you should still either set legacy gas price with `WithLegacyGasPrice()` or EIP-1559 dynamic fees with `WithDynamicGasPrices()` // ss they will be used as fallback values, if the estimations fail. +// 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. // Following priorities are supported: "slow", "standard" and "fast" -// Default values are true for enabled, 200 blocks for estimation and "standard" for priority. -func (c *ClientBuilder) WithGasPriceEstimations(enabled bool, estimationBlocks uint64, txPriority string) *ClientBuilder { +// Default values are true for enabled, 200 blocks for estimation, "standard" for priority and 1 attempt. +func (c *ClientBuilder) WithGasPriceEstimations(enabled bool, estimationBlocks uint64, txPriority string, attemptCount uint) *ClientBuilder { if !c.checkIfNetworkIsSet() { return c } c.config.Network.GasPriceEstimationEnabled = enabled c.config.Network.GasPriceEstimationBlocks = estimationBlocks c.config.Network.GasPriceEstimationTxPriority = txPriority + c.config.Network.GasPriceEstimationAttemptCount = attemptCount // defensive programming if len(c.config.Networks) == 0 { c.config.Networks = append(c.config.Networks, c.config.Network) @@ -192,6 +195,7 @@ func (c *ClientBuilder) WithGasPriceEstimations(enabled bool, estimationBlocks u net.GasPriceEstimationEnabled = enabled net.GasPriceEstimationBlocks = estimationBlocks net.GasPriceEstimationTxPriority = txPriority + net.GasPriceEstimationAttemptCount = attemptCount } return c } diff --git a/seth/client_decode_test.go b/seth/client_decode_test.go index d0e4adf54..d55b287d5 100644 --- a/seth/client_decode_test.go +++ b/seth/client_decode_test.go @@ -10,7 +10,7 @@ import ( ) func TestSmokeDebugReverts(t *testing.T) { - c := newClient(t) + c := newClientWithContractMapFromEnv(t) type test struct { name string @@ -63,7 +63,7 @@ func TestSmokeDebugReverts(t *testing.T) { } func TestSmokeDebugData(t *testing.T) { - c := newClient(t) + c := newClientWithContractMapFromEnv(t) c.Cfg.TracingLevel = seth.TracingLevel_All type test struct { @@ -226,9 +226,10 @@ func TestSmokeDebugData(t *testing.T) { require.NoError(t, err) require.Equal(t, dtx.Input, tc.output.Input) require.Equal(t, dtx.Output, tc.output.Output) + require.Equal(t, len(tc.output.Events), len(dtx.Events)) for i, e := range tc.output.Events { require.NotNil(t, dtx.Events[i]) - require.Equal(t, dtx.Events[i].EventData, e.EventData) + require.Equal(t, e.EventData, dtx.Events[i].EventData) } } }) diff --git a/seth/config.go b/seth/config.go index f992bbee5..9941a4f2a 100644 --- a/seth/config.go +++ b/seth/config.go @@ -33,10 +33,11 @@ const ( DefaultDialTimeout = 1 * time.Minute DefaultPendingNonceProtectionTimeout = 1 * time.Minute - DefaultTransferGasFee = 21_000 - DefaultGasPrice = 100_000_000_000 // 100 Gwei - DefaultGasFeeCap = 100_000_000_000 // 100 Gwei - DefaultGasTipCap = 50_000_000_000 // 50 Gwei + DefaultTransferGasFee = 21_000 + DefaultGasPrice = 100_000_000_000 // 100 Gwei + DefaultGasFeeCap = 100_000_000_000 // 100 Gwei + DefaultGasTipCap = 50_000_000_000 // 50 Gwei + DefaultGasPriceEstimationsAttemptCount = 1 // this actually means no retries, due to how the retry-go library works ) type Config struct { @@ -99,21 +100,22 @@ type NonceManagerCfg struct { } type Network struct { - Name string `toml:"name"` - URLs []string `toml:"urls_secret"` - ChainID uint64 `toml:"chain_id"` - EIP1559DynamicFees bool `toml:"eip_1559_dynamic_fees"` - GasPrice int64 `toml:"gas_price"` - GasFeeCap int64 `toml:"gas_fee_cap"` - GasTipCap int64 `toml:"gas_tip_cap"` - GasLimit uint64 `toml:"gas_limit"` - TxnTimeout *Duration `toml:"transaction_timeout"` - DialTimeout *Duration `toml:"dial_timeout"` - TransferGasFee int64 `toml:"transfer_gas_fee"` - PrivateKeys []string `toml:"private_keys_secret"` - GasPriceEstimationEnabled bool `toml:"gas_price_estimation_enabled"` - GasPriceEstimationBlocks uint64 `toml:"gas_price_estimation_blocks"` - GasPriceEstimationTxPriority string `toml:"gas_price_estimation_tx_priority"` + Name string `toml:"name"` + URLs []string `toml:"urls_secret"` + ChainID uint64 `toml:"chain_id"` + EIP1559DynamicFees bool `toml:"eip_1559_dynamic_fees"` + GasPrice int64 `toml:"gas_price"` + GasFeeCap int64 `toml:"gas_fee_cap"` + GasTipCap int64 `toml:"gas_tip_cap"` + GasLimit uint64 `toml:"gas_limit"` + TxnTimeout *Duration `toml:"transaction_timeout"` + DialTimeout *Duration `toml:"dial_timeout"` + TransferGasFee int64 `toml:"transfer_gas_fee"` + PrivateKeys []string `toml:"private_keys_secret"` + GasPriceEstimationEnabled bool `toml:"gas_price_estimation_enabled"` + GasPriceEstimationBlocks uint64 `toml:"gas_price_estimation_blocks"` + GasPriceEstimationTxPriority string `toml:"gas_price_estimation_tx_priority"` + GasPriceEstimationAttemptCount uint `toml:"gas_price_estimation_attempt_count"` } // DefaultClient returns a Client with reasonable default config with the specified RPC URL and private keys. You should pass at least 1 private key. @@ -193,6 +195,77 @@ func ReadConfig() (*Config, error) { return cfg, nil } +// Validate checks and validates the provided Config struct. +// It ensures essential fields have valid values or default to appropriate values +// when necessary. This function performs validation on gas price estimation, +// gas limit, tracing level, trace outputs, network dial timeout, and pending nonce protection timeout. +// If any configuration is invalid, it returns an error. +func (c *Config) Validate() error { + if c.Network.GasPriceEstimationEnabled { + if c.Network.GasPriceEstimationBlocks == 0 { + L.Debug().Msg("Gas estimation is enabled, but block headers to use is set to 0. Will not use block congestion for gas estimation") + } + c.Network.GasPriceEstimationTxPriority = strings.ToLower(c.Network.GasPriceEstimationTxPriority) + + if c.Network.GasPriceEstimationTxPriority == "" { + c.Network.GasPriceEstimationTxPriority = Priority_Standard + } + + switch c.Network.GasPriceEstimationTxPriority { + case Priority_Degen: + case Priority_Fast: + case Priority_Standard: + case Priority_Slow: + default: + return errors.New("when automating gas estimation is enabled priority must be fast, standard or slow. fix it or disable gas estimation") + } + + } + + if c.Network.GasLimit != 0 { + L.Warn(). + 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") + } + + if c.Network.GasPriceEstimationAttemptCount == 0 { + c.Network.GasPriceEstimationAttemptCount = DefaultGasPriceEstimationsAttemptCount + } + + if c.TracingLevel == "" { + c.TracingLevel = TracingLevel_Reverted + } + + c.TracingLevel = strings.ToUpper(c.TracingLevel) + + switch c.TracingLevel { + case TracingLevel_None: + case TracingLevel_Reverted: + case TracingLevel_All: + default: + return errors.New("tracing level must be one of: NONE, REVERTED, ALL") + } + + for _, output := range c.TraceOutputs { + switch strings.ToLower(output) { + case TraceOutput_Console: + case TraceOutput_JSON: + case TraceOutput_DOT: + default: + return errors.New("trace output must be one of: console, json, dot") + } + } + + if c.Network.DialTimeout == nil { + c.Network.DialTimeout = &Duration{D: DefaultDialTimeout} + } + + if c.PendingNonceProtectionTimeout == nil { + c.PendingNonceProtectionTimeout = &Duration{D: DefaultPendingNonceProtectionTimeout} + } + + return nil +} + // FirstNetworkURL returns first network URL func (c *Config) FirstNetworkURL() string { return c.Network.URLs[0] diff --git a/seth/config_test.go b/seth/config_test.go index b329745be..dea9dc28b 100644 --- a/seth/config_test.go +++ b/seth/config_test.go @@ -192,7 +192,7 @@ func TestConfig_LegacyGas_No_Estimations(t *testing.T) { WithPrivateKeys([]string{"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"}). // Gas price and estimations WithLegacyGasPrice(710_000_000). - WithGasPriceEstimations(false, 0, ""). + WithGasPriceEstimations(false, 0, "", 0). Build() require.NoError(t, err, "failed to build client") require.Equal(t, 1, len(client.PrivateKeys), "expected 1 private key") @@ -215,7 +215,7 @@ func TestConfig_Eip1559Gas_With_Estimations(t *testing.T) { // Gas price and estimations WithEIP1559DynamicFees(true). WithDynamicGasPrices(120_000_000_000, 44_000_000_000). - WithGasPriceEstimations(true, 10, seth.Priority_Fast). + WithGasPriceEstimations(true, 10, seth.Priority_Fast, seth.DefaultGasPriceEstimationsAttemptCount). Build() require.NoError(t, err, "failed to build client") @@ -238,7 +238,7 @@ func TestConfig_NoPrivateKeys_RpcHealthEnabled(t *testing.T) { // Gas price and estimations WithEIP1559DynamicFees(true). WithDynamicGasPrices(120_000_000_000, 44_000_000_000). - WithGasPriceEstimations(false, 10, seth.Priority_Fast). + WithGasPriceEstimations(false, 10, seth.Priority_Fast, seth.DefaultGasPriceEstimationsAttemptCount). Build() require.Error(t, err, "succeeded in building the client") @@ -255,7 +255,7 @@ func TestConfig_NoPrivateKeys_PendingNonce(t *testing.T) { // Gas price and estimations WithEIP1559DynamicFees(true). WithDynamicGasPrices(120_000_000_000, 44_000_000_000). - WithGasPriceEstimations(false, 10, seth.Priority_Fast). + WithGasPriceEstimations(false, 10, seth.Priority_Fast, 2). WithProtections(true, false, seth.MustMakeDuration(1*time.Minute)). Build() @@ -274,7 +274,7 @@ func TestConfig_NoPrivateKeys_EphemeralKeys(t *testing.T) { // Gas price and estimations WithEIP1559DynamicFees(true). WithDynamicGasPrices(120_000_000_000, 44_000_000_000). - WithGasPriceEstimations(false, 10, seth.Priority_Fast). + WithGasPriceEstimations(false, 10, seth.Priority_Fast, 2). WithProtections(false, false, seth.MustMakeDuration(1*time.Minute)). Build() @@ -288,7 +288,7 @@ func TestConfig_NoPrivateKeys_GasEstimations(t *testing.T) { _, err := builder. WithNetworkName("my network"). WithRpcUrl("ws://localhost:8546"). - WithGasPriceEstimations(true, 10, seth.Priority_Fast). + WithGasPriceEstimations(true, 10, seth.Priority_Fast, 2). WithProtections(false, false, seth.MustMakeDuration(1*time.Minute)). Build() @@ -306,7 +306,7 @@ func TestConfig_NoPrivateKeys_TxOpts(t *testing.T) { // Gas price and estimations WithEIP1559DynamicFees(true). WithDynamicGasPrices(120_000_000_000, 44_000_000_000). - WithGasPriceEstimations(false, 10, seth.Priority_Fast). + WithGasPriceEstimations(false, 10, seth.Priority_Fast, 2). WithProtections(false, false, seth.MustMakeDuration(1*time.Minute)). Build() @@ -326,7 +326,7 @@ func TestConfig_NoPrivateKeys_Tracing(t *testing.T) { WithRpcUrl("ws://localhost:8546"). WithEIP1559DynamicFees(true). WithDynamicGasPrices(120_000_000_000, 44_000_000_000). - WithGasPriceEstimations(false, 10, seth.Priority_Fast). + WithGasPriceEstimations(false, 10, seth.Priority_Fast, 2). WithProtections(false, false, seth.MustMakeDuration(1*time.Minute)). WithGethWrappersFolders([]string{"./contracts/bind"}). Build() @@ -375,7 +375,7 @@ func TestConfig_ReadOnlyMode(t *testing.T) { WithRpcUrl("ws://localhost:8546"). WithEphemeralAddresses(10, 1000). WithPrivateKeys([]string{"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"}). - WithGasPriceEstimations(true, 10, seth.Priority_Fast). + WithGasPriceEstimations(true, 10, seth.Priority_Fast, 2). WithReadOnlyMode(). Build() diff --git a/seth/gas_adjuster.go b/seth/gas_adjuster.go index 997972a59..1b30970d5 100644 --- a/seth/gas_adjuster.go +++ b/seth/gas_adjuster.go @@ -10,7 +10,9 @@ import ( "sync" "time" + "github.com/avast/retry-go" "github.com/ethereum/go-ethereum/core/types" + "github.com/pkg/errors" ) const ( @@ -59,7 +61,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy timeout = 6 } - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(mustSafeInt64(timeout))*time.Second) defer cancel() header, err := m.Client.HeaderByNumber(ctx, bn) if err != nil { @@ -79,7 +81,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy L.Trace().Msgf("Block range for gas calculation: %d - %d", lastBlockNumber-blocksNumber, lastBlockNumber) - lastBlock, err := getHeaderData(big.NewInt(int64(lastBlockNumber))) + lastBlock, err := getHeaderData(big.NewInt(mustSafeInt64(lastBlockNumber))) if err != nil { return 0, err } @@ -114,7 +116,7 @@ func (m *Client) CalculateNetworkCongestionMetric(blocksNumber uint64, strategy return } dataCh <- header - }(big.NewInt(int64(i))) + }(big.NewInt(mustSafeInt64(i))) } wg.Wait() @@ -147,7 +149,12 @@ func calculateSimpleNetworkCongestionMetric(headers []*types.Header) float64 { func calculateNewestFirstNetworkCongestionMetric(headers []*types.Header) float64 { // sort blocks so that we are sure they are in ascending order slices.SortFunc(headers, func(i, j *types.Header) int { - return int(i.Number.Uint64() - j.Number.Uint64()) + if i.Number.Uint64() < j.Number.Uint64() { + return -1 + } else if i.Number.Uint64() > j.Number.Uint64() { + return 1 + } + return 0 }) var weightedSum, totalWeight float64 @@ -175,20 +182,37 @@ func calculateNewestFirstNetworkCongestionMetric(headers []*types.Header) float6 func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) (maxFeeCap *big.Int, adjustedTipCap *big.Int, err error) { L.Info().Msg("Calculating suggested EIP-1559 fees") var suggestedGasTip *big.Int - suggestedGasTip, err = m.Client.SuggestGasTipCap(ctx) - if err != nil { - return - } + var baseFee64, historicalSuggestedTip64 float64 + attempts := getSafeGasEstimationsAttemptCount(m.Cfg) - L.Debug(). - Str("CurrentGasTip", fmt.Sprintf("%s wei / %s ether", suggestedGasTip.String(), WeiToEther(suggestedGasTip).Text('f', -1))). - Msg("Current suggested gas tip") + retryErr := retry.Do(func() error { + var tipErr error + suggestedGasTip, tipErr = m.Client.SuggestGasTipCap(ctx) + if tipErr != nil { + return tipErr + } - // Fetch the baseline historical base fee and tip for the selected priority - var baseFee64, historicalSuggestedTip64 float64 - //nolint - baseFee64, historicalSuggestedTip64, err = m.HistoricalFeeData(priority) - if err != nil { + L.Debug(). + Str("CurrentGasTip", fmt.Sprintf("%s wei / %s ether", suggestedGasTip.String(), WeiToEther(suggestedGasTip).Text('f', -1))). + Msg("Current suggested gas tip") + + // Fetch the baseline historical base fee and tip for the selected priority + var historyErr error + //nolint + baseFee64, historicalSuggestedTip64, historyErr = m.HistoricalFeeData(priority) + return historyErr + }, + retry.Attempts(attempts), + retry.Delay(1*time.Second), + retry.LastErrorOnly(true), + retry.DelayType(retry.FixedDelay), + retry.OnRetry(func(i uint, retryErr error) { + L.Debug(). + Msgf("Retrying fetching of EIP1559 suggested fees due to: %s. Attempt %d/%d", retryErr.Error(), (i + 1), attempts) + })) + + if retryErr != nil { + err = retryErr return } @@ -346,15 +370,32 @@ func (m *Client) GetSuggestedLegacyFees(ctx context.Context, priority string) (a Msg("Calculating suggested Legacy fees") var suggestedGasPrice *big.Int - suggestedGasPrice, err = m.Client.SuggestGasPrice(ctx) - if err != nil { - return - } + attempts := getSafeGasEstimationsAttemptCount(m.Cfg) - if suggestedGasPrice.Int64() == 0 { - err = fmt.Errorf("suggested gas price is 0") - L.Debug(). - Msg("Incorrect gas data received from node: suggested gas price was 0. Skipping automation gas estimation") + retryErr := retry.Do(func() error { + var priceErr error + suggestedGasPrice, priceErr = m.Client.SuggestGasPrice(ctx) + if priceErr != nil { + return priceErr + } + + if suggestedGasPrice.Int64() == 0 { + return errors.New("suggested gas price is 0") + } + + return nil + }, + retry.Attempts(attempts), + retry.Delay(1*time.Second), + retry.LastErrorOnly(true), + retry.DelayType(retry.FixedDelay), + retry.OnRetry(func(i uint, retryErr error) { + L.Debug(). + Msgf("Retrying fetching of legacy suggested gas price due to: %s. Attempt %d/%d", retryErr.Error(), (i + 1), attempts) + })) + + if retryErr != nil { + err = retryErr return } @@ -467,7 +508,7 @@ func (m *Client) HistoricalFeeData(priority string) (baseFee float64, historical stats, err := estimator.Stats(m.Cfg.Network.GasPriceEstimationBlocks, 99) if err != nil { L.Debug(). - Msgf("Failed to get fee history due to: %s. Skipping automation gas estimation", err.Error()) + Msgf("Failed to get fee history due to: %s", err.Error()) return } @@ -489,7 +530,7 @@ func (m *Client) HistoricalFeeData(priority string) (baseFee float64, historical err = fmt.Errorf("unknown priority: %s", priority) L.Debug(). Str("Priority", priority). - Msgf("Unknown priority: %s. Skipping automation gas estimation", err.Error()) + Msgf("Unknown priority: %s", err.Error()) return @@ -543,3 +584,10 @@ func calculateMagnitudeDifference(first, second *big.Float) (int, string) { intDiff := int(math.Ceil(diff)) return intDiff, fmt.Sprintf("%d orders of magnitude larger", intDiff) } + +func getSafeGasEstimationsAttemptCount(cfg *Config) uint { + if cfg.Network.GasPriceEstimationAttemptCount == 0 { + return DefaultGasPriceEstimationsAttemptCount + } + return cfg.Network.GasPriceEstimationAttemptCount +} diff --git a/seth/gas_bump_test.go b/seth/gas_bump_test.go index fd5a7939a..fa9fb812f 100644 --- a/seth/gas_bump_test.go +++ b/seth/gas_bump_test.go @@ -184,7 +184,7 @@ func TestGasBumping_Contract_Deployment_Legacy_CustomBumpingFunction(t *testing. client, err := seth.NewClientBuilder(). WithRpcUrl(c.Cfg.Network.URLs[0]). WithPrivateKeys([]string{newPk}). - WithGasPriceEstimations(false, 0, ""). + WithGasPriceEstimations(false, 0, "", 0). WithEIP1559DynamicFees(false). WithLegacyGasPrice(1). WithTransactionTimeout(10*time.Second). diff --git a/seth/keyfile.go b/seth/keyfile.go index 515e81838..042a74d10 100644 --- a/seth/keyfile.go +++ b/seth/keyfile.go @@ -71,7 +71,7 @@ func ReturnFunds(c *Client, toAddr string) error { if err != nil { gasLimit = c.Cfg.Network.TransferGasFee } else { - gasLimit = int64(gasLimitRaw) + gasLimit = mustSafeInt64(gasLimitRaw) } networkTransferFee := gasPrice.Int64() * gasLimit diff --git a/seth/nonce.go b/seth/nonce.go index 6c2e2131d..0d0f66246 100644 --- a/seth/nonce.go +++ b/seth/nonce.go @@ -66,14 +66,14 @@ func (m *NonceManager) UpdateNonces() error { if err != nil { return err } - m.Nonces[addr] = int64(nonce) + m.Nonces[addr] = mustSafeInt64(nonce) } L.Debug().Interface("Nonces", m.Nonces).Msg("Updated nonces for addresses") m.SyncedKeys = make(chan *KeyNonce, len(m.Addresses)) for keyNum, addr := range m.Addresses[1:] { m.SyncedKeys <- &KeyNonce{ KeyNum: keyNum + 1, - Nonce: uint64(m.Nonces[addr]), + Nonce: mustSafeUint64(m.Nonces[addr]), } } return nil @@ -134,7 +134,7 @@ func (m *NonceManager) anySyncedKey() int { L.Trace(). Interface("KeyNum", keyData.KeyNum). Uint64("Nonce", nonce). - Int("Expected nonce", int(keyData.Nonce+1)). + Int("Expected nonce", mustSafeInt(keyData.Nonce+1)). Interface("Address", m.Addresses[keyData.KeyNum]). Msg("Key NOT synced") diff --git a/seth/seth.toml b/seth/seth.toml index becaf03fe..4c41b0b61 100644 --- a/seth/seth.toml +++ b/seth/seth.toml @@ -78,6 +78,9 @@ key_sync_timeout = "20s" key_sync_retry_delay = "1s" key_sync_retries = 10 +[block_stats] +rpc_requests_per_second_limit = 15 + [[networks]] name = "Anvil" dial_timeout = "1m" @@ -116,6 +119,8 @@ eip_1559_dynamic_fees = true gas_price_estimation_enabled = true gas_price_estimation_blocks = 100 gas_price_estimation_tx_priority = "standard" +# how many times to try fetching & calculating gas price data in case of errors, defaults to 1 if empty +gas_price_estimation_attempt_count = 5 # fallback values transfer_gas_fee = 21_000 @@ -123,6 +128,7 @@ gas_price = 150_000_000_000 #150 gwei gas_fee_cap = 150_000_000_000 #150 gwei gas_tip_cap = 50_000_000_000 #50 gwei + [[networks]] name = "Fuji" dial_timeout = "1m" @@ -133,6 +139,8 @@ eip_1559_dynamic_fees = true #gas_price_estimation_enabled = true #gas_price_estimation_blocks = 100 #gas_price_estimation_tx_priority = "standard" +# how many times to try fetching & calculating gas price data in case of errors, defaults to 1 if empty +#gas_price_estimation_attempt_count = 5 # gas limits #transfer_gas_fee = 21_000 @@ -146,6 +154,7 @@ eip_1559_dynamic_fees = true #gas_fee_cap = 30_000_000_000 #gas_tip_cap = 1_800_000_000 + [[networks]] name = "Sepolia" dial_timeout = "1m" @@ -156,6 +165,8 @@ eip_1559_dynamic_fees = true gas_price_estimation_enabled = true gas_price_estimation_blocks = 100 gas_price_estimation_tx_priority = "standard" +# how many times to try fetching & calculating gas price data in case of errors, defaults to 1 if empty +gas_price_estimation_attempt_count = 5 # gas limits transfer_gas_fee = 21_000 @@ -183,6 +194,8 @@ gas_price_estimation_enabled = true gas_price_estimation_blocks = 100 # transaction priority, which determines adjustment factor multiplier applied to suggested values (fast - 1.2x, standard - 1x, slow - 0.8x) gas_price_estimation_tx_priority = "standard" +# how many times to try fetching & calculating gas price data in case of errors, defaults to 1 if empty +gas_price_estimation_attempt_count = 5 # gas limits transfer_gas_fee = 21_000 @@ -207,6 +220,8 @@ eip_1559_dynamic_fees = false gas_price_estimation_enabled = true gas_price_estimation_blocks = 100 gas_price_estimation_tx_priority = "standard" +# how many times to try fetching & calculating gas price data in case of errors, defaults to 1 if empty +gas_price_estimation_attempt_count = 5 # gas limits transfer_gas_fee = 21_000 @@ -221,6 +236,7 @@ gas_price = 50_000_000 gas_fee_cap = 1_800_000_000 gas_tip_cap = 1_800_000_000 + [[networks]] name = "ARBITRUM_SEPOLIA" dial_timeout = "1m" @@ -239,6 +255,5 @@ gas_price_estimation_enabled = true gas_price_estimation_blocks = 100 # priority of the transaction, can be "fast", "standard" or "slow" (the higher the priority, the higher adjustment factor will be used for gas estimation) [default: "standard"] gas_price_estimation_tx_priority = "standard" - -[block_stats] -rpc_requests_per_second_limit = 15 +# how many times to try fetching & calculating gas price data in case of errors, defaults to 1 if empty +gas_price_estimation_attempt_count = 5 \ No newline at end of file diff --git a/seth/tracing.go b/seth/tracing.go index 03f762a09..f75c37823 100644 --- a/seth/tracing.go +++ b/seth/tracing.go @@ -406,7 +406,7 @@ func (t *Tracer) decodeCall(byteSignature []byte, rawCall Call) (*DecodedCall, e Str("Gas", rawCall.Gas). Msg("Failed to parse value") } else { - defaultCall.GasLimit = uint64(decimalValue) + defaultCall.GasLimit = mustSafeUint64(decimalValue) } } @@ -418,7 +418,7 @@ func (t *Tracer) decodeCall(byteSignature []byte, rawCall Call) (*DecodedCall, e Str("GasUsed", rawCall.GasUsed). Msg("Failed to parse value") } else { - defaultCall.GasUsed = uint64(decimalValue) + defaultCall.GasUsed = mustSafeUint64(decimalValue) } } diff --git a/seth/util.go b/seth/util.go index e8e29a56f..5f9e8f33d 100644 --- a/seth/util.go +++ b/seth/util.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "math" "math/big" "os" "path/filepath" @@ -65,7 +66,7 @@ func (m *Client) CalculateSubKeyFunding(addrs, gasPrice, rooKeyBuffer int64) (*F if err == nil { gasLimitRaw, err := m.EstimateGasLimitForFundTransfer(m.Addresses[0], common.HexToAddress(newAddress), big.NewInt(0).Quo(balance, big.NewInt(addrs))) if err == nil { - gasLimit = int64(gasLimitRaw) + gasLimit = mustSafeInt64(gasLimitRaw) } } @@ -386,7 +387,7 @@ func DecodePragmaVersion(bytecode string) (Pragma, error) { } // each byte is represented by 2 characters in hex - metadataLengthInt := int(metadataByteLengthUint) * 2 + metadataLengthInt := mustSafeInt(metadataByteLengthUint) * 2 // if we get nonsensical metadata length, it means that metadata section is not present and last 2 bytes do not represent metadata length if metadataLengthInt > len(bytecode) { @@ -457,3 +458,24 @@ This error could be caused by several issues. Please try these steps to resolve Original error:` return fmt.Errorf("%s\n%s", message, err.Error()) } + +func mustSafeInt64(input uint64) int64 { + if input > math.MaxInt64 { + panic(fmt.Errorf("uint64 %d exceeds int64 max value", input)) + } + return int64(input) +} + +func mustSafeInt(input uint64) int { + if input > math.MaxInt { + panic(fmt.Errorf("uint64 %d exceeds int max value", input)) + } + return int(input) +} + +func mustSafeUint64(input int64) uint64 { + if input < 0 { + panic(fmt.Errorf("int64 %d is below uint64 min value", input)) + } + return uint64(input) +} diff --git a/seth/util_test.go b/seth/util_test.go index 111c6c213..0e3ff338c 100644 --- a/seth/util_test.go +++ b/seth/util_test.go @@ -183,6 +183,7 @@ func TestUtilPendingNonce(t *testing.T) { seth.L.Debug().Msgf("Starting tx %d", index) opts := c.NewTXOpts() + //nolint nonceToUse := int64(getNonceAndIncrement()) opts.Nonce = big.NewInt(nonceToUse) defer seth.L.Debug().Msgf("Finished tx %d with nonce %d", index, nonceToUse) @@ -193,6 +194,7 @@ func TestUtilPendingNonce(t *testing.T) { if index == 50 { started <- struct{}{} } + //nolint nonceCh <- uint64(nonceToUse) }(i) } @@ -205,6 +207,7 @@ func TestUtilPendingNonce(t *testing.T) { pendingNonce, err := c.Client.PendingNonceAt(context.Background(), c.Addresses[testCase.keyNum]) require.NoError(t, err, "Error getting pending nonce") + //nolint require.Greater(t, int64(pendingNonce), int64(lastNonce), "Pending nonce should be greater than last nonce") if testCase.keyNum == 0 {