Skip to content

Commit

Permalink
allow to disable congestion metrics, when using automated gas estimat…
Browse files Browse the repository at this point in the history
…ions; update readme
  • Loading branch information
Tofel committed Sep 3, 2024
1 parent 0ff98a1 commit c51c0b8
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 80 deletions.
19 changes: 8 additions & 11 deletions seth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ client, err := builder.
// EIP-1559 and gas estimations
WithEIP1559DynamicFees(true).
WithDynamicGasPrices(120_000_000_000, 44_000_000_000).
WithGasPriceEstimations(false, 10, seth.Priority_Fast).
WithGasPriceEstimations(true, 10, seth.Priority_Fast).
// gas bumping: retries, max gas price, bumping strategy function
WithGasBumping(5, 100_000_000_000, PriorityBasedGasBumpingStrategyFn).
Build()
Expand Down Expand Up @@ -400,20 +400,22 @@ For real networks, the estimation process differs for legacy transactions and th

1. **Initial Price**: Query the network node for the current suggested gas price.
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.
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.
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`.
4. **Buffering**: Add a buffer to the adjusted gas price to increase transaction reliability during high congestion.

##### EIP-1559 Transactions

1. **Tip Fee Query**: Ask the node for the current recommended tip fee.
2. **Fee History Analysis**: Gather the base fee and tip history from recent blocks to establish a fee baseline.
3. **Fee Selection**: Use the greater of the node's suggested tip or the historical average tip for upcoming calculations.
3. **Fee Selection**: Use the greatest of the node's suggested tip or the historical average tip for upcoming calculations.
4. **Priority and Adjustment**: Increase the base and tip fees based on transaction priority (`gas_price_estimation_tx_priority`), which influences how much you are willing to spend to expedite your transaction.
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.

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.

Finally, `gas_price_estimation_tx_priority` is also used, when deciding, which percentile to use for base fee and tip for historical fee data. Here's how that looks:

```go
Expand Down Expand Up @@ -445,7 +447,7 @@ For fast transactions we will increase gas price by 20%, for standard we will us

##### Buffer percents

We further adjust the gas price by adding a buffer to it, based on congestion rate:
If `gas_price_estimation_blocks` is higher than `0` we further adjust the gas price by adding a buffer to it, based on congestion rate:

```go
case Congestion_Low:
Expand All @@ -458,13 +460,8 @@ case Congestion_VeryHigh:
return 1.40, nil
```

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 fallback to hardcoded values.
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.

### DOT graphs

Expand Down
2 changes: 1 addition & 1 deletion seth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func NewClientWithConfig(cfg *Config) (*Client, error) {
func ValidateConfig(cfg *Config) error {
if cfg.Network.GasPriceEstimationEnabled {
if cfg.Network.GasPriceEstimationBlocks == 0 {
return errors.New("when automating gas estimation is enabled blocks must be greater than 0. fix it or disable gas estimation")
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)

Expand Down
2 changes: 1 addition & 1 deletion seth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const (
DefaultPendingNonceProtectionTimeout = 1 * time.Minute

DefaultTransferGasFee = 21_000
DefaultGasPrice = 1_000_000_000 // 1 Gwei
DefaultGasPrice = 100_000_000_000 // 100 Gwei
DefaultGasFeeCap = 100_000_000_000 // 100 Gwei
DefaultGasTipCap = 50_000_000_000 // 50 Gwei
)
Expand Down
2 changes: 1 addition & 1 deletion seth/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,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(false, 10, seth.Priority_Fast).
WithGasPriceEstimations(true, 10, seth.Priority_Fast).
Build()

require.NoError(t, err, "failed to build client")
Expand Down
7 changes: 7 additions & 0 deletions seth/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ func (m *GasEstimator) Stats(fromNumber uint64, priorityPerc float64) (GasSugges
if err != nil {
return GasSuggestions{}, err
}
if fromNumber == 0 {
if bn > 100 {
fromNumber = bn - 100
} else {
fromNumber = 1
}
}
hist, err := m.Client.Client.FeeHistory(context.Background(), fromNumber, big.NewInt(int64(bn)), []float64{priorityPerc})
if err != nil {
return GasSuggestions{}, err
Expand Down
138 changes: 72 additions & 66 deletions seth/gas_adjuster.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,44 +265,47 @@ func (m *Client) GetSuggestedEIP1559Fees(ctx context.Context, priority string) (

initialFeeCap := new(big.Int).Add(big.NewInt(int64(baseFee64)), currentGasTip)

// between 0 and 1 (empty blocks - full blocks)
var congestionMetric float64
//nolint
congestionMetric, err = m.CalculateNetworkCongestionMetric(m.Cfg.Network.GasPriceEstimationBlocks, CongestionStrategy_NewestFirst)
if err == nil {
congestionClassification := classifyCongestion(congestionMetric)

L.Debug().
Str("CongestionMetric", fmt.Sprintf("%.4f", congestionMetric)).
Str("CongestionClassification", congestionClassification).
Float64("AdjustmentFactor", adjustmentFactor).
Str("Priority", priority).
Msg("Adjustment factors")
// skip if we do not want to calculate congestion metrics
if m.Cfg.Network.GasPriceEstimationBlocks > 0 {
// between 0 and 1 (empty blocks - full blocks)
var congestionMetric float64
//nolint
congestionMetric, err = m.CalculateNetworkCongestionMetric(m.Cfg.Network.GasPriceEstimationBlocks, CongestionStrategy_NewestFirst)
if err == nil {
congestionClassification := classifyCongestion(congestionMetric)

L.Debug().
Str("CongestionMetric", fmt.Sprintf("%.4f", congestionMetric)).
Str("CongestionClassification", congestionClassification).
Float64("AdjustmentFactor", adjustmentFactor).
Str("Priority", priority).
Msg("Adjustment factors")

// between 1.1 and 1.4
var bufferAdjustment float64
bufferAdjustment, err = getCongestionFactor(congestionClassification)
if err != nil {
return
}

// between 1.1 and 1.4
var bufferAdjustment float64
bufferAdjustment, err = getCongestionFactor(congestionClassification)
if err != nil {
// Calculate base fee buffer
bufferedBaseFeeFloat := new(big.Float).Mul(new(big.Float).SetInt(adjustedBaseFee), big.NewFloat(bufferAdjustment))
adjustedBaseFee, _ = bufferedBaseFeeFloat.Int(nil)

// Apply buffer also to the tip
bufferedTipCapFloat := new(big.Float).Mul(new(big.Float).SetInt(adjustedTipCap), big.NewFloat(bufferAdjustment))
adjustedTipCap, _ = bufferedTipCapFloat.Int(nil)
} else if !strings.Contains(err.Error(), BlockFetchingErr) {
return
} else {
L.Warn().
Err(err).
Msg("Failed to calculate congestion metric. Skipping congestion buffer adjustment")

// set error to nil, as we can still calculate the fees, but without congestion buffer
// we don't want to return an error in this case
err = nil
}

// Calculate base fee buffer
bufferedBaseFeeFloat := new(big.Float).Mul(new(big.Float).SetInt(adjustedBaseFee), big.NewFloat(bufferAdjustment))
adjustedBaseFee, _ = bufferedBaseFeeFloat.Int(nil)

// Apply buffer also to the tip
bufferedTipCapFloat := new(big.Float).Mul(new(big.Float).SetInt(adjustedTipCap), big.NewFloat(bufferAdjustment))
adjustedTipCap, _ = bufferedTipCapFloat.Int(nil)
} else if !strings.Contains(err.Error(), BlockFetchingErr) {
return
} else {
L.Warn().
Err(err).
Msg("Failed to calculate congestion metric. Skipping congestion buffer adjustment")

// set error to nil, as we can still calculate the fees, but without congestion buffer
// we don't want to return an error in this case
err = nil
}

maxFeeCap = new(big.Int).Add(adjustedBaseFee, adjustedTipCap)
Expand Down Expand Up @@ -366,40 +369,43 @@ func (m *Client) GetSuggestedLegacyFees(ctx context.Context, priority string) (a
adjustedGasPriceFloat := new(big.Float).Mul(big.NewFloat(adjustmentFactor), new(big.Float).SetFloat64(float64(suggestedGasPrice.Int64())))
adjustedGasPrice, _ = adjustedGasPriceFloat.Int(nil)

// between 0 and 1 (empty blocks - full blocks)
var congestionMetric float64
//nolint
congestionMetric, err = m.CalculateNetworkCongestionMetric(m.Cfg.Network.GasPriceEstimationBlocks, CongestionStrategy_NewestFirst)
if err == nil {
congestionClassification := classifyCongestion(congestionMetric)

L.Debug().
Str("CongestionMetric", fmt.Sprintf("%.4f", congestionMetric)).
Str("CongestionClassification", congestionClassification).
Float64("AdjustmentFactor", adjustmentFactor).
Str("Priority", priority).
Msg("Suggested Legacy fees")
// skip if we do not want to calculate congestion metrics
if m.Cfg.Network.GasPriceEstimationBlocks > 0 {
// between 0 and 1 (empty blocks - full blocks)
var congestionMetric float64
//nolint
congestionMetric, err = m.CalculateNetworkCongestionMetric(m.Cfg.Network.GasPriceEstimationBlocks, CongestionStrategy_NewestFirst)
if err == nil {
congestionClassification := classifyCongestion(congestionMetric)

L.Debug().
Str("CongestionMetric", fmt.Sprintf("%.4f", congestionMetric)).
Str("CongestionClassification", congestionClassification).
Float64("AdjustmentFactor", adjustmentFactor).
Str("Priority", priority).
Msg("Suggested Legacy fees")

// between 1.1 and 1.4
var bufferAdjustment float64
bufferAdjustment, err = getCongestionFactor(congestionClassification)
if err != nil {
return
}

// between 1.1 and 1.4
var bufferAdjustment float64
bufferAdjustment, err = getCongestionFactor(congestionClassification)
if err != nil {
// Calculate and apply the buffer.
bufferedGasPriceFloat := new(big.Float).Mul(new(big.Float).SetInt(adjustedGasPrice), big.NewFloat(bufferAdjustment))
adjustedGasPrice, _ = bufferedGasPriceFloat.Int(nil)
} else if !strings.Contains(err.Error(), BlockFetchingErr) {
return
} else {
L.Warn().
Err(err).
Msg("Failed to calculate congestion metric. Skipping congestion buffer adjustment")

// set error to nil, as we can still calculate the fees, but without congestion buffer
// we don't want to return an error in this case
err = nil
}

// Calculate and apply the buffer.
bufferedGasPriceFloat := new(big.Float).Mul(new(big.Float).SetInt(adjustedGasPrice), big.NewFloat(bufferAdjustment))
adjustedGasPrice, _ = bufferedGasPriceFloat.Int(nil)
} else if !strings.Contains(err.Error(), BlockFetchingErr) {
return
} else {
L.Warn().
Err(err).
Msg("Failed to calculate congestion metric. Skipping congestion buffer adjustment")

// set error to nil, as we can still calculate the fees, but without congestion buffer
// we don't want to return an error in this case
err = nil
}

L.Debug().
Expand Down

0 comments on commit c51c0b8

Please sign in to comment.