Repository: celestiaorg/celestia-app

Issue:
Adjust the estimated gas price during congestion when it's equal to the minimum
If the mempool is full of transactions that are all using the minimum gas price, the current estimation mechanism will return the minimum gas price regardless of the priority. We can fix this by applying an adjustment to the estimation given the priority to increase the inclusion probability.

Hints from issue comments:

Solution patch:
diff --git a/app/grpc/gasestimation/gas_estimator.go b/app/grpc/gasestimation/gas_estimator.go
index ed481fbf..3a529815 100644
--- a/app/grpc/gasestimation/gas_estimator.go
+++ b/app/grpc/gasestimation/gas_estimator.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"math"
 	"sort"
 
 	tmclient "github.com/tendermint/tendermint/rpc/client"
@@ -126,6 +127,20 @@ func (s *gasEstimatorServer) estimateGasPrice(ctx context.Context, priority TxPr
 	return estimateGasPriceForTransactions(gasPrices, priority)
 }
 
+const (
+	// highPriorityGasAdjustmentRate is the percentage increase applied to the
+	// estimated gas price when the block is more than 70% full, i.e., gasPriceEstimationThreshold,
+	// but the gas prices are tightly clustered. This ensures that high-priority
+	// transactions still have a competitive fee to improve inclusion probability.
+	highPriorityGasAdjustmentRate = 1.3
+	// mediumPriorityGasAdjustmentRate similar to highPriorityGasAdjustmentRate but for
+	// the medium priority.
+	mediumPriorityGasAdjustmentRate = 1.1
+	// gasPriceAdjustmentThreshold the standard deviation threshold under which we
+	// apply the gas price adjustment.
+	gasPriceAdjustmentThreshold = 0.001
+)
+
 // estimateGasPriceForTransactions takes a list of transactions and priority
 // and returns a gas price estimation.
 // The priority sets the estimation as follows:
@@ -133,14 +148,24 @@ func (s *gasEstimatorServer) estimateGasPrice(ctx context.Context, priority TxPr
 // - Medium Priority: The gas price is the median price of the all gas prices in the mempool.
 // - Low Priority: The gas price is the median price of the bottom 10% of gas prices in the mempool.
 // - Unspecified Priority (default): This is equivalent to the Medium priority, using the median price of all gas prices in the mempool.
+// If the list of gas prices has a standard deviation < gasPriceAdjustmentThreshold, meaning the gas price values are tightly clustered,
+// an increase of 30% and 10% will be added for high and medium priority respectively.
 // More information can be found in ADR-023.
 func estimateGasPriceForTransactions(gasPrices []float64, priority TxPriority) (float64, error) {
 	if len(gasPrices) == 0 {
 		return 0, errors.New("empty gas prices list")
 	}
+	stDev := StandardDeviation(Mean(gasPrices), gasPrices)
 	switch priority {
-	case TxPriority_TX_PRIORITY_UNSPECIFIED:
-		return Median(gasPrices)
+	case TxPriority_TX_PRIORITY_MEDIUM, TxPriority_TX_PRIORITY_UNSPECIFIED:
+		estimation, err := Median(gasPrices)
+		if err != nil {
+			return 0, err
+		}
+		if stDev < gasPriceAdjustmentThreshold {
+			return estimation * mediumPriorityGasAdjustmentRate, nil
+		}
+		return estimation, nil
 	case TxPriority_TX_PRIORITY_LOW:
 		bottom10PercentIndex := len(gasPrices) * 10 / 100
 		if bottom10PercentIndex == 0 {
@@ -148,10 +173,15 @@ func estimateGasPriceForTransactions(gasPrices []float64, priority TxPriority) (
 			bottom10PercentIndex = 1
 		}
 		return Median(gasPrices[:bottom10PercentIndex])
-	case TxPriority_TX_PRIORITY_MEDIUM:
-		return Median(gasPrices)
 	case TxPriority_TX_PRIORITY_HIGH:
-		return Median(gasPrices[len(gasPrices)*90/100:])
+		estimation, err := Median(gasPrices[len(gasPrices)*90/100:])
+		if err != nil {
+			return 0, err
+		}
+		if stDev < gasPriceAdjustmentThreshold {
+			return estimation * highPriorityGasAdjustmentRate, nil
+		}
+		return estimation, nil
 	default:
 		return 0, fmt.Errorf("unknown priority: %d", priority)
 	}
@@ -221,3 +251,29 @@ func Median(gasPrices []float64) (float64, error) {
 	mid2 := gasPrices[n/2]
 	return (mid1 + mid2) / 2.0, nil
 }
+
+// Mean calculates the mean value of the provided gas prices.
+func Mean(gasPrices []float64) float64 {
+	if len(gasPrices) == 0 {
+		return 0
+	}
+	sum := 0.0
+	for _, gasPrice := range gasPrices {
+		sum += gasPrice
+	}
+	return sum / float64(len(gasPrices))
+}
+
+// StandardDeviation calculates the standard deviation of the provided gas prices.
+func StandardDeviation(meanGasPrice float64, gasPrices []float64) float64 {
+	if len(gasPrices) < 2 {
+		return 0
+	}
+	var variance float64
+	for _, gasPrice := range gasPrices {
+		diff := gasPrice - meanGasPrice
+		variance += diff * diff
+	}
+	variance /= float64(len(gasPrices))
+	return math.Sqrt(variance)
+}
diff --git a/docs/architecture/adr-023-gas-used-and-gas-price-estimation.md b/docs/architecture/adr-023-gas-used-and-gas-price-estimation.md
index f3cdb600..f468b9a8 100644
--- a/docs/architecture/adr-023-gas-used-and-gas-price-estimation.md
+++ b/docs/architecture/adr-023-gas-used-and-gas-price-estimation.md
@@ -48,6 +48,8 @@ There are multiple ways to have a gas price estimation for a priority level. Thi
 
 If the mempool has more transactions that it can fit in the next block, the estimation will be based on the top gas prices that can fit in a full block. Otherwise, if the mempool transactions can't fill more than 70% of the max block size, the minimum gas price will be returned.
 
+If the gas prices used for the estimation are tightly clustered, an increase of 30% and 10% will be added for high and medium priority respectively. Tightly clustered values are defined by the standard deviation of the elements being below the `gasPriceAdjustmentThreshold`. 
+
 The following is a basic implementation of the median that we can use:
 
 ```go
diff --git a/go.mod b/go.mod
index 8f86e842..b46857fd 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/cosmos/gogoproto v1.7.0
 	github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v6 v6.1.2
 	github.com/cosmos/ibc-go/v6 v6.2.2
-	github.com/ethereum/go-ethereum v1.15.5
+	github.com/ethereum/go-ethereum v1.15.6
 	github.com/gogo/protobuf v1.3.3
 	github.com/golang/mock v1.6.0
 	github.com/golang/protobuf v1.5.4
diff --git a/go.sum b/go.sum
index a3256b83..95c6d5a5 100644
--- a/go.sum
+++ b/go.sum
@@ -466,6 +466,7 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
 github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
 github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
+github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
 github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
 github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I=
 github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE=
@@ -522,8 +523,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
 github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
 github.com/ethereum/go-ethereum v1.10.17/go.mod h1:Lt5WzjM07XlXc95YzrhosmR4J9Ahd6X2wyEV2SvGhk0=
-github.com/ethereum/go-ethereum v1.15.5 h1:Fo2TbBWC61lWVkFw9tsMoHCNX1ndpuaQBRJ8H6xLUPo=
-github.com/ethereum/go-ethereum v1.15.5/go.mod h1:1LG2LnMOx2yPRHR/S+xuipXH29vPr6BIH6GElD8N/fo=
+github.com/ethereum/go-ethereum v1.15.6 h1:jgLoUM6/pNjp0uEnXyWcWikDwa4j1wZlcqkX8Pm8A+I=
+github.com/ethereum/go-ethereum v1.15.6/go.mod h1:+S9k+jFzlyVTNcYGvqFhzN/SFhI6vA+aOY4T5tLSPL0=
 github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
 github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
 github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
@@ -538,6 +539,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/ferranbt/fastssz v0.1.2 h1:Dky6dXlngF6Qjc+EfDipAkE83N5I5DE68bY6O0VLNPk=
+github.com/ferranbt/fastssz v0.1.2/go.mod h1:X5UPrE2u1UJjxHA8X54u04SBwdAQjG2sFtWs39YxyWs=
 github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
 github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
 github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
@@ -552,6 +555,7 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
 github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI=
 github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
 github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
 github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
@@ -781,6 +785,7 @@ github.com/grafana/pyroscope-go v1.1.2 h1:7vCfdORYQMCxIzI3NlYAs3FcBP760+gWuYWOyi
 github.com/grafana/pyroscope-go v1.1.2/go.mod h1:HSSmHo2KRn6FasBA4vK7BMiQqyQq8KSuBKvrhkXxYPU=
 github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
 github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
+github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0=
 github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI=
@@ -865,11 +870,15 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY=
 github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI=
+github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k=
 github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
+github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs=
+github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/influxdata/influxql v1.1.1-0.20200828144457-65d3ef77d385/go.mod h1:gHp9y86a/pxhjJ+zMjNXiQAA197Xk9wLxaz+fGG+kWk=
 github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE=
 github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
+github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 h1:vilfsDSy7TDxedi9gyBkMvAirat/oRcL0lFdJBf6tdM=
 github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
 github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19ybifQhZoQNF5D8=
 github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE=
@@ -1004,6 +1013,8 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
 github.com/minio/minio-go/v7 v7.0.74 h1:fTo/XlPBTSpo3BAMshlwKL5RspXRv9us5UeHEGYCFe0=
 github.com/minio/minio-go/v7 v7.0.74/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8=
+github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
+github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -1091,6 +1102,8 @@ github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKw
 github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
 github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
 github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
 github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
@@ -1108,6 +1121,7 @@ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNH
 github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
 github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
+github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM=
 github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
 github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
 github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 h1:hDSdbBuw3Lefr6R18ax0tZ2BJeNB3NehB3trOwYBsdU=



Test patch:
diff --git a/app/grpc/gasestimation/gas_estimator_test.go b/app/grpc/gasestimation/gas_estimator_test.go
index c6876dc7..ee3d603a 100644
--- a/app/grpc/gasestimation/gas_estimator_test.go
+++ b/app/grpc/gasestimation/gas_estimator_test.go
@@ -1,8 +1,12 @@
 package gasestimation
 
 import (
+	"math"
 	"testing"
 
+	"github.com/celestiaorg/celestia-app/v3/pkg/appconsts"
+	"github.com/tendermint/tendermint/libs/rand"
+
 	"github.com/stretchr/testify/require"
 
 	"github.com/stretchr/testify/assert"
@@ -105,6 +109,64 @@ func TestEstimateGasPriceForTransactions(t *testing.T) {
 	}
 }
 
+func TestEstimateClusteredGasPrices(t *testing.T) {
+	minGasPrice := appconsts.DefaultMinGasPrice * 2
+	maxGasPrice := minGasPrice + gasPriceAdjustmentThreshold
+	gasPrices := make([]float64, 20)
+	for i := range gasPrices {
+		randomGasPrice := minGasPrice + rand.Float64()*(maxGasPrice-minGasPrice)
+		gasPrices[i] = randomGasPrice
+	}
+	medianGasPrice, err := Median(gasPrices)
+	require.NoError(t, err)
+	medianGasPrice *= mediumPriorityGasAdjustmentRate
+	bottomMedian, err := Median(gasPrices[:len(gasPrices)*10/100])
+	require.NoError(t, err)
+	topMedian, err := Median(gasPrices[len(gasPrices)*90/100:])
+	require.NoError(t, err)
+	topMedian *= highPriorityGasAdjustmentRate
+
+	tests := []struct {
+		name     string
+		priority TxPriority
+		want     float64
+		wantErr  bool
+	}{
+		{
+			name:     "NONE -> same as MEDIUM (median)",
+			priority: TxPriority_TX_PRIORITY_UNSPECIFIED,
+			want:     medianGasPrice,
+		},
+		{
+			name:     "LOW -> bottom 10% median",
+			priority: TxPriority_TX_PRIORITY_LOW,
+			want:     bottomMedian,
+		},
+		{
+			name:     "MEDIUM -> median",
+			priority: TxPriority_TX_PRIORITY_MEDIUM,
+			want:     medianGasPrice,
+		},
+		{
+			name:     "HIGH -> top 10% median",
+			priority: TxPriority_TX_PRIORITY_HIGH,
+			want:     topMedian,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := estimateGasPriceForTransactions(gasPrices, tt.priority)
+			if tt.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.want, got)
+			}
+		})
+	}
+}
+
 func TestEstimateGasPriceSmallList(t *testing.T) {
 	gasPrices := []float64{10, 20, 30, 40, 50, 60}
 	bottomMedian, err := Median(gasPrices[:1])
@@ -114,3 +176,82 @@ func TestEstimateGasPriceSmallList(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, got, bottomMedian)
 }
+
+func TestMean(t *testing.T) {
+	tests := []struct {
+		name      string
+		gasPrices []float64
+		want      float64
+	}{
+		{
+			name:      "Empty slice",
+			gasPrices: []float64{},
+			want:      0,
+		},
+		{
+			name:      "Single element",
+			gasPrices: []float64{10},
+			want:      10,
+		},
+		{
+			name:      "Multiple elements",
+			gasPrices: []float64{1, 2, 3, 4, 5},
+			want:      3,
+		},
+		{
+			name:      "Mixed floats",
+			gasPrices: []float64{1.5, 2.5, 3.5},
+			want:      2.5,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := Mean(tt.gasPrices)
+			if got != tt.want {
+				t.Errorf("mean(%v) = %v, want %v", tt.gasPrices, got, tt.want)
+			}
+		})
+	}
+}
+
+func TestStandardDeviation(t *testing.T) {
+	tests := []struct {
+		name      string
+		gasPrices []float64
+		want      float64
+	}{
+		{
+			name:      "Empty slice",
+			gasPrices: []float64{},
+			want:      0,
+		},
+		{
+			name:      "Single element",
+			gasPrices: []float64{10},
+			want:      0,
+		},
+		{
+			name:      "Multiple elements",
+			gasPrices: []float64{10, 20, 30, 40, 50},
+			// Variance = 200, stdev ~ 14.142135...
+			want: 14.142135623730951,
+		},
+		{
+			name:      "Identical elements",
+			gasPrices: []float64{5, 5, 5, 5},
+			want:      0,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			meanVal := Mean(tt.gasPrices)
+			got := StandardDeviation(meanVal, tt.gasPrices)
+			// We'll do a tolerance check for floating-point comparisons.
+			if math.Abs(got-tt.want) > 1e-9 {
+				t.Errorf("stdDev(%v) = %v, want %v", tt.gasPrices, got, tt.want)
+			}
+		})
+	}
+}