mirror of
https://github.com/vacp2p/linea-monorepo.git
synced 2026-01-06 22:23:55 -05:00
feat: add calldata based pricing for variable cost (#1189)
* feat: add calldata based pricing for variable cost * feat: revised log and removed comment * feat: revised tests and checks beased on PR reviews
This commit is contained in:
@@ -116,8 +116,6 @@ failures-warning-threshold = 2
|
||||
l1-polling-interval = "PT1S"
|
||||
l1-query-block-tag="LATEST"
|
||||
|
||||
[l1-submission]
|
||||
disabled = true
|
||||
[l1-submission.dynamic-gas-price-cap]
|
||||
disabled = false
|
||||
[l1-submission.dynamic-gas-price-cap.gas-price-cap-calculation]
|
||||
@@ -265,6 +263,11 @@ blob-submission-expected-execution-gas = 213000
|
||||
variable-cost-upper-bound = 10000000001 # ~10 GWEI
|
||||
variable-cost-lower-bound = 90000001 # ~0.09 GWEI
|
||||
margin = 4.0
|
||||
[l2-network-gas-pricing.dynamic-gas-pricing.calldata-based-pricing]
|
||||
calldata-sum-size-block-count = 5 # disabled if zero
|
||||
fee-change-denominator = 32
|
||||
calldata-sum-size-target = 109000
|
||||
block-size-non-calldata-overhead = 540
|
||||
|
||||
[database]
|
||||
hostname = "postgres"
|
||||
|
||||
@@ -15,6 +15,7 @@ data class L2NetworkGasPricingConfig(
|
||||
val extraDataUpdateEndpoint: URL,
|
||||
val extraDataUpdateRequestRetries: RetryConfig,
|
||||
val l1Endpoint: URL,
|
||||
val l2Endpoint: URL,
|
||||
) : FeatureToggle {
|
||||
data class DynamicGasPricing(
|
||||
val l1BlobGas: ULong,
|
||||
@@ -22,6 +23,14 @@ data class L2NetworkGasPricingConfig(
|
||||
val variableCostUpperBound: ULong,
|
||||
val variableCostLowerBound: ULong,
|
||||
val margin: Double,
|
||||
val calldataBasedPricing: CalldataBasedPricing?,
|
||||
)
|
||||
|
||||
data class CalldataBasedPricing(
|
||||
val calldataSumSizeBlockCount: UInt = 5u,
|
||||
val feeChangeDenominator: UInt = 32u,
|
||||
val calldataSumSizeTarget: ULong = 109000uL,
|
||||
val blockSizeNonCalldataOverhead: UInt = 540u,
|
||||
)
|
||||
|
||||
data class FlatRateGasPricing(
|
||||
|
||||
@@ -64,6 +64,7 @@ data class CoordinatorConfigToml(
|
||||
),
|
||||
l2NetworkGasPricing = this.configs.l2NetworkGasPricing.reified(
|
||||
l1DefaultEndpoint = this.configs.defaults.l1Endpoint,
|
||||
l2DefaultEndpoint = this.configs.defaults.l2Endpoint,
|
||||
),
|
||||
database = this.configs.database.reified(),
|
||||
api = this.configs.api.reified(),
|
||||
|
||||
@@ -8,6 +8,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
data class L2NetworkGasPricingConfigToml(
|
||||
val disabled: Boolean = false,
|
||||
val l1Endpoint: URL? = null,
|
||||
val l2Endpoint: URL? = null,
|
||||
val priceUpdateInterval: Duration = 12.seconds,
|
||||
val feeHistoryBlockCount: UInt = 1000u,
|
||||
val feeHistoryRewardPercentile: UInt = 15u,
|
||||
@@ -34,6 +35,7 @@ data class L2NetworkGasPricingConfigToml(
|
||||
val variableCostUpperBound: ULong,
|
||||
val variableCostLowerBound: ULong,
|
||||
val margin: Double,
|
||||
val calldataBasedPricing: CalldataBasedPricingToml? = null,
|
||||
) {
|
||||
fun reified(): L2NetworkGasPricingConfig.DynamicGasPricing {
|
||||
return L2NetworkGasPricingConfig.DynamicGasPricing(
|
||||
@@ -42,6 +44,23 @@ data class L2NetworkGasPricingConfigToml(
|
||||
variableCostUpperBound = this.variableCostUpperBound,
|
||||
variableCostLowerBound = this.variableCostLowerBound,
|
||||
margin = this.margin,
|
||||
calldataBasedPricing = this.calldataBasedPricing?.reified(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class CalldataBasedPricingToml(
|
||||
val calldataSumSizeBlockCount: UInt = 5U,
|
||||
val feeChangeDenominator: UInt = 32U,
|
||||
val calldataSumSizeTarget: ULong = 109000UL,
|
||||
val blockSizeNonCalldataOverhead: UInt = 540U,
|
||||
) {
|
||||
fun reified(): L2NetworkGasPricingConfig.CalldataBasedPricing {
|
||||
return L2NetworkGasPricingConfig.CalldataBasedPricing(
|
||||
calldataSumSizeBlockCount = this.calldataSumSizeBlockCount,
|
||||
feeChangeDenominator = this.feeChangeDenominator,
|
||||
calldataSumSizeTarget = this.calldataSumSizeTarget,
|
||||
blockSizeNonCalldataOverhead = this.blockSizeNonCalldataOverhead,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -66,6 +85,7 @@ data class L2NetworkGasPricingConfigToml(
|
||||
|
||||
fun reified(
|
||||
l1DefaultEndpoint: URL?,
|
||||
l2DefaultEndpoint: URL?,
|
||||
): L2NetworkGasPricingConfig {
|
||||
return L2NetworkGasPricingConfig(
|
||||
disabled = disabled,
|
||||
@@ -78,6 +98,7 @@ data class L2NetworkGasPricingConfigToml(
|
||||
extraDataUpdateEndpoint = this.extraDataUpdateEndpoint,
|
||||
extraDataUpdateRequestRetries = this.extraDataUpdateRequestRetries.asDomain,
|
||||
l1Endpoint = this.l1Endpoint ?: l1DefaultEndpoint ?: throw AssertionError("l1Endpoint must be set"),
|
||||
l2Endpoint = this.l2Endpoint ?: l2DefaultEndpoint ?: throw AssertionError("l2Endpoint must be set"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import linea.contract.l1.LineaRollupSmartContractClientReadOnly
|
||||
import linea.contract.l1.Web3JLineaRollupSmartContractClientReadOnly
|
||||
import linea.coordinator.config.toJsonRpcRetry
|
||||
import linea.coordinator.config.v2.CoordinatorConfig
|
||||
import linea.coordinator.config.v2.Type2StateProofManagerConfig
|
||||
import linea.coordinator.config.v2.isDisabled
|
||||
import linea.coordinator.config.v2.isEnabled
|
||||
import linea.domain.BlockNumberAndHash
|
||||
@@ -32,6 +33,8 @@ import net.consensys.linea.ethereum.gaspricing.dynamiccap.GasPriceCapProviderFor
|
||||
import net.consensys.linea.ethereum.gaspricing.dynamiccap.GasPriceCapProviderImpl
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.ExtraDataV1UpdaterImpl
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.FeeHistoryFetcherImpl
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.L2CalldataBasedVariableFeesCalculator
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.L2CalldataSizeAccumulatorImpl
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.MinerExtraDataV1CalculatorImpl
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.TransactionCostCalculator
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.VariableFeesCalculator
|
||||
@@ -543,11 +546,29 @@ class L1DependentApp(
|
||||
sequencerEndpoint = configs.l2NetworkGasPricing.extraDataUpdateEndpoint,
|
||||
retryConfig = configs.l2NetworkGasPricing.extraDataUpdateRequestRetries.toJsonRpcRetry(),
|
||||
),
|
||||
l2CalldataSizeAccumulatorConfig = configs.l2NetworkGasPricing.dynamicGasPricing.calldataBasedPricing?.let {
|
||||
L2CalldataSizeAccumulatorImpl.Config(
|
||||
blockSizeNonCalldataOverhead = it.blockSizeNonCalldataOverhead,
|
||||
calldataSizeBlockCount = it.calldataSumSizeBlockCount,
|
||||
)
|
||||
},
|
||||
l2CalldataBasedVariableFeesCalculatorConfig =
|
||||
configs.l2NetworkGasPricing.dynamicGasPricing.calldataBasedPricing?.let {
|
||||
L2CalldataBasedVariableFeesCalculator.Config(
|
||||
feeChangeDenominator = it.feeChangeDenominator,
|
||||
calldataSizeBlockCount = it.calldataSumSizeBlockCount,
|
||||
maxBlockCalldataSize = it.calldataSumSizeTarget.toUInt(),
|
||||
)
|
||||
},
|
||||
)
|
||||
val l1Web3jClient = createWeb3jHttpClient(
|
||||
rpcUrl = configs.l2NetworkGasPricing.l1Endpoint.toString(),
|
||||
log = LogManager.getLogger("clients.l1.eth.l2pricing"),
|
||||
)
|
||||
val l2Web3jClient = createWeb3jHttpClient(
|
||||
rpcUrl = configs.l2NetworkGasPricing.l2Endpoint.toString(),
|
||||
log = LogManager.getLogger("clients.l2.eth.l2pricing"),
|
||||
)
|
||||
L2NetworkGasPricingService(
|
||||
vertx = vertx,
|
||||
httpJsonRpcClientFactory = httpJsonRpcClientFactory,
|
||||
@@ -558,6 +579,7 @@ class L1DependentApp(
|
||||
log = LogManager.getLogger("clients.l1.eth.l2pricing"),
|
||||
),
|
||||
),
|
||||
l2Web3jClient = ExtendedWeb3JImpl(l2Web3jClient),
|
||||
config = config,
|
||||
)
|
||||
} else {
|
||||
@@ -674,7 +696,7 @@ class L1DependentApp(
|
||||
|
||||
companion object {
|
||||
fun setupL1FinalizationMonitorForShomeiFrontend(
|
||||
type2StateProofProviderConfig: linea.coordinator.config.v2.Type2StateProofManagerConfig,
|
||||
type2StateProofProviderConfig: Type2StateProofManagerConfig,
|
||||
httpJsonRpcClientFactory: VertxHttpJsonRpcClientFactory,
|
||||
lineaRollupClient: LineaRollupSmartContractClientReadOnly,
|
||||
l2Web3jClient: Web3j,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.consensys.zkevm.coordinator.app
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import linea.web3j.ExtendedWeb3J
|
||||
import linea.web3j.Web3jBlobExtended
|
||||
import net.consensys.linea.ethereum.gaspricing.BoundableFeeCalculator
|
||||
import net.consensys.linea.ethereum.gaspricing.FeesCalculator
|
||||
@@ -11,6 +12,8 @@ import net.consensys.linea.ethereum.gaspricing.staticcap.ExtraDataV1UpdaterImpl
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.FeeHistoryFetcherImpl
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.GasPriceUpdaterImpl
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.GasUsageRatioWeightedAverageFeesCalculator
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.L2CalldataBasedVariableFeesCalculator
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.L2CalldataSizeAccumulatorImpl
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.MinMineableFeesPricerService
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.MinerExtraDataV1CalculatorImpl
|
||||
import net.consensys.linea.ethereum.gaspricing.staticcap.TransactionCostCalculator
|
||||
@@ -28,6 +31,7 @@ class L2NetworkGasPricingService(
|
||||
httpJsonRpcClientFactory: VertxHttpJsonRpcClientFactory,
|
||||
l1Web3jClient: Web3j,
|
||||
l1Web3jService: Web3jBlobExtended,
|
||||
l2Web3jClient: ExtendedWeb3J,
|
||||
config: Config,
|
||||
) : LongRunningService {
|
||||
data class LegacyGasPricingCalculatorConfig(
|
||||
@@ -47,6 +51,8 @@ class L2NetworkGasPricingService(
|
||||
val variableFeesCalculatorBounds: BoundableFeeCalculator.Config,
|
||||
val extraDataCalculatorConfig: MinerExtraDataV1CalculatorImpl.Config,
|
||||
val extraDataUpdaterConfig: ExtraDataV1UpdaterImpl.Config,
|
||||
val l2CalldataSizeAccumulatorConfig: L2CalldataSizeAccumulatorImpl.Config?,
|
||||
val l2CalldataBasedVariableFeesCalculatorConfig: L2CalldataBasedVariableFeesCalculator.Config?,
|
||||
)
|
||||
private val log = LogManager.getLogger(this::class.java)
|
||||
|
||||
@@ -98,14 +104,31 @@ class L2NetworkGasPricingService(
|
||||
null
|
||||
}
|
||||
|
||||
private fun isL2CalldataBasedVariableFeesEnabled(config: Config): Boolean {
|
||||
return config.l2CalldataBasedVariableFeesCalculatorConfig != null &&
|
||||
config.l2CalldataSizeAccumulatorConfig != null &&
|
||||
config.l2CalldataBasedVariableFeesCalculatorConfig.calldataSizeBlockCount > 0u
|
||||
}
|
||||
|
||||
private val extraDataPricerService: ExtraDataV1PricerService? = if (config.extraDataPricingPropagationEnabled) {
|
||||
ExtraDataV1PricerService(
|
||||
pollingInterval = config.extraDataUpdateInterval,
|
||||
vertx = vertx,
|
||||
feesFetcher = gasPricingFeesFetcher,
|
||||
minerExtraDataCalculatorImpl = MinerExtraDataV1CalculatorImpl(
|
||||
minerExtraDataCalculator = MinerExtraDataV1CalculatorImpl(
|
||||
config = config.extraDataCalculatorConfig,
|
||||
variableFeesCalculator = boundedVariableCostCalculator,
|
||||
variableFeesCalculator = if (isL2CalldataBasedVariableFeesEnabled(config)) {
|
||||
L2CalldataBasedVariableFeesCalculator(
|
||||
variableFeesCalculator = boundedVariableCostCalculator,
|
||||
l2CalldataSizeAccumulator = L2CalldataSizeAccumulatorImpl(
|
||||
web3jClient = l2Web3jClient,
|
||||
config = config.l2CalldataSizeAccumulatorConfig!!,
|
||||
),
|
||||
config = config.l2CalldataBasedVariableFeesCalculatorConfig!!,
|
||||
)
|
||||
} else {
|
||||
boundedVariableCostCalculator
|
||||
},
|
||||
legacyFeesCalculator = legacyGasPricingCalculator,
|
||||
),
|
||||
extraDataUpdater = ExtraDataV1UpdaterImpl(
|
||||
|
||||
@@ -188,7 +188,7 @@ class BlockCreationMonitor(
|
||||
val blockNumber = _nexBlockNumberToFetch.get()
|
||||
web3j.ethGetBlock(blockNumber.toBlockParameter())
|
||||
.thenPeek { block ->
|
||||
log.trace("requestedBock={} responselock={}", blockNumber, block?.number)
|
||||
log.trace("requestedBlock={} responseBlock={}", blockNumber, block?.number)
|
||||
}
|
||||
.whenException {
|
||||
log.warn(
|
||||
|
||||
@@ -37,6 +37,11 @@ class L2NetWorkingGasPricingConfigParsingTest {
|
||||
variable-cost-upper-bound = 10000000001 # ~10 GWEI
|
||||
variable-cost-lower-bound = 90000001 # ~0.09 GWEI
|
||||
margin = 4.0
|
||||
[l2-network-gas-pricing.dynamic-gas-pricing.calldata-based-pricing]
|
||||
calldata-sum-size-block-count = 5
|
||||
fee-change-denominator = 32
|
||||
calldata-sum-size-target = 109000
|
||||
block-size-non-calldata-overhead = 540
|
||||
""".trimIndent()
|
||||
|
||||
val config = L2NetworkGasPricingConfigToml(
|
||||
@@ -59,6 +64,12 @@ class L2NetWorkingGasPricingConfigParsingTest {
|
||||
variableCostUpperBound = 10_000_000_001UL, // ~10 GWEI
|
||||
variableCostLowerBound = 90_000_001UL, // ~0.09 GWEI
|
||||
margin = 4.0,
|
||||
calldataBasedPricing = L2NetworkGasPricingConfigToml.CalldataBasedPricingToml(
|
||||
calldataSumSizeBlockCount = 5u,
|
||||
feeChangeDenominator = 32u,
|
||||
calldataSumSizeTarget = 109000uL,
|
||||
blockSizeNonCalldataOverhead = 540u,
|
||||
),
|
||||
),
|
||||
flatRateGasPricing = L2NetworkGasPricingConfigToml.FlatRateGasPricingToml(
|
||||
gasPriceUpperBound = 10_000_000_000UL, // 10 GWEI
|
||||
|
||||
@@ -4,12 +4,17 @@ import linea.domain.FeeHistory
|
||||
import linea.kotlin.decodeHex
|
||||
import linea.kotlin.encodeHex
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
interface FeesFetcher {
|
||||
fun getL1EthGasPriceData(): SafeFuture<FeeHistory>
|
||||
}
|
||||
|
||||
interface L2CalldataSizeAccumulator {
|
||||
fun getSumOfL2CalldataSize(): SafeFuture<BigInteger>
|
||||
}
|
||||
|
||||
fun interface FeesCalculator {
|
||||
fun calculateFees(feeHistory: FeeHistory): Double
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.vertx.core.Vertx
|
||||
import linea.kotlin.toIntervalString
|
||||
import net.consensys.linea.ethereum.gaspricing.ExtraDataUpdater
|
||||
import net.consensys.linea.ethereum.gaspricing.FeesFetcher
|
||||
import net.consensys.linea.ethereum.gaspricing.MinerExtraDataCalculator
|
||||
import net.consensys.linea.ethereum.gaspricing.MinerExtraDataV1
|
||||
import net.consensys.zkevm.PeriodicPollingService
|
||||
import org.apache.logging.log4j.LogManager
|
||||
@@ -15,7 +16,7 @@ class ExtraDataV1PricerService(
|
||||
pollingInterval: Duration,
|
||||
vertx: Vertx,
|
||||
private val feesFetcher: FeesFetcher,
|
||||
private val minerExtraDataCalculatorImpl: MinerExtraDataV1CalculatorImpl,
|
||||
private val minerExtraDataCalculator: MinerExtraDataCalculator,
|
||||
private val extraDataUpdater: ExtraDataUpdater,
|
||||
private val log: Logger = LogManager.getLogger(ExtraDataV1PricerService::class.java),
|
||||
) : PeriodicPollingService(
|
||||
@@ -30,7 +31,7 @@ class ExtraDataV1PricerService(
|
||||
.getL1EthGasPriceData()
|
||||
.thenCompose { feeHistory ->
|
||||
val blockRange = feeHistory.blocksRange()
|
||||
val newExtraData = minerExtraDataCalculatorImpl.calculateMinerExtraData(feeHistory)
|
||||
val newExtraData = minerExtraDataCalculator.calculateMinerExtraData(feeHistory)
|
||||
if (lastExtraData != newExtraData) {
|
||||
// this is just to avoid log noise.
|
||||
lastExtraData = newExtraData
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package net.consensys.linea.ethereum.gaspricing.staticcap
|
||||
|
||||
import linea.domain.FeeHistory
|
||||
import net.consensys.linea.ethereum.gaspricing.FeesCalculator
|
||||
import net.consensys.linea.ethereum.gaspricing.L2CalldataSizeAccumulator
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/*
|
||||
CALLDATA_BASED_FEE_CHANGE_DENOMINATOR = 32
|
||||
CALLDATA_BASED_FEE_BLOCK_COUNT = 5
|
||||
MAX_BLOCK_CALLDATA_SIZE = 109000
|
||||
|
||||
variable_cost = as documented in VARIABLE_COST (https://docs.linea.build/get-started/how-to/gas-fees#gas-pricing)
|
||||
calldata_target = CALLDATA_BASED_FEE_BLOCK_COUNT * MAX_BLOCK_CALLDATA_SIZE / 2
|
||||
# delta fluctuates between [-1 and 1]
|
||||
delta = (sum(block_size over CALLDATA_BASED_FEE_BLOCK_COUNT) - calldata_target) / calldata_target
|
||||
variable_cost = max(variable_cost, previous_variable_cost * ( 1 + delta / CALLDATA_BASED_FEE_CHANGE_DENOMINATOR )
|
||||
*/
|
||||
class L2CalldataBasedVariableFeesCalculator(
|
||||
val config: Config,
|
||||
val variableFeesCalculator: FeesCalculator,
|
||||
val l2CalldataSizeAccumulator: L2CalldataSizeAccumulator,
|
||||
) : FeesCalculator {
|
||||
data class Config(
|
||||
val feeChangeDenominator: UInt,
|
||||
val calldataSizeBlockCount: UInt,
|
||||
val maxBlockCalldataSize: UInt,
|
||||
) {
|
||||
init {
|
||||
require(feeChangeDenominator > 0u) { "feeChangeDenominator=$feeChangeDenominator must be greater than 0" }
|
||||
require(maxBlockCalldataSize > 0u) { "maxBlockCalldataSize=$maxBlockCalldataSize must be greater than 0" }
|
||||
}
|
||||
}
|
||||
|
||||
private val log = LogManager.getLogger(this::class.java)
|
||||
private var lastVariableCost: AtomicReference<Double> = AtomicReference(0.0)
|
||||
|
||||
override fun calculateFees(feeHistory: FeeHistory): Double {
|
||||
val variableFee = variableFeesCalculator.calculateFees(feeHistory)
|
||||
|
||||
if (config.calldataSizeBlockCount == 0u) {
|
||||
log.debug(
|
||||
"Calldata-based variable fee is disabled as calldataSizeBlockCount is set as 0: variableFee={} wei",
|
||||
variableFee,
|
||||
)
|
||||
return variableFee
|
||||
}
|
||||
|
||||
val callDataTargetSize = config.maxBlockCalldataSize
|
||||
.times(config.calldataSizeBlockCount)
|
||||
.toDouble().div(2.0)
|
||||
|
||||
val delta = (
|
||||
l2CalldataSizeAccumulator
|
||||
.getSumOfL2CalldataSize().get().toDouble()
|
||||
.minus(callDataTargetSize)
|
||||
)
|
||||
.div(callDataTargetSize)
|
||||
.coerceAtLeast(-1.0)
|
||||
.coerceAtMost(1.0)
|
||||
|
||||
val calldataBasedVariableFee =
|
||||
lastVariableCost.get().times(1.0 + (delta.div(config.feeChangeDenominator.toDouble())))
|
||||
|
||||
val finalVariableFee = variableFee.coerceAtLeast(calldataBasedVariableFee)
|
||||
|
||||
lastVariableCost.set(finalVariableFee)
|
||||
|
||||
log.debug(
|
||||
"Calculated calldataBasedVariableFee={} wei variableFee={} wei finalVariableFee={} wei " +
|
||||
"delta={} maxBlockCalldataSize={} calldataSizeBlockCount={} feeChangeDenominator={}",
|
||||
calldataBasedVariableFee,
|
||||
variableFee,
|
||||
finalVariableFee,
|
||||
delta,
|
||||
config.maxBlockCalldataSize,
|
||||
config.calldataSizeBlockCount,
|
||||
config.feeChangeDenominator,
|
||||
)
|
||||
|
||||
return finalVariableFee
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package net.consensys.linea.ethereum.gaspricing.staticcap
|
||||
|
||||
import linea.kotlin.toBigInteger
|
||||
import linea.kotlin.toUInt
|
||||
import linea.web3j.ExtendedWeb3J
|
||||
import net.consensys.linea.ethereum.gaspricing.L2CalldataSizeAccumulator
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
|
||||
class L2CalldataSizeAccumulatorImpl(
|
||||
private val web3jClient: ExtendedWeb3J,
|
||||
private val config: Config,
|
||||
) : L2CalldataSizeAccumulator {
|
||||
private val log: Logger = LogManager.getLogger(this::class.java)
|
||||
|
||||
data class Config(
|
||||
val blockSizeNonCalldataOverhead: UInt,
|
||||
val calldataSizeBlockCount: UInt,
|
||||
) {
|
||||
init {
|
||||
require(calldataSizeBlockCount <= 60u) {
|
||||
"calldataSizeBlockCount must be less than 60 to avoid excessive " +
|
||||
"eth_getBlockByNumber calls to the web3j client. Value=$calldataSizeBlockCount"
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun getRecentL2CalldataSize(): SafeFuture<BigInteger> {
|
||||
return web3jClient.ethBlockNumber()
|
||||
.thenCompose { currentBlockNumber ->
|
||||
if (config.calldataSizeBlockCount > 0u && currentBlockNumber.toUInt() >= config.calldataSizeBlockCount) {
|
||||
val futures =
|
||||
((currentBlockNumber.toUInt() - config.calldataSizeBlockCount + 1U)..currentBlockNumber.toUInt())
|
||||
.map { blockNumber ->
|
||||
web3jClient.ethGetBlockSizeByNumber(blockNumber.toLong())
|
||||
}
|
||||
SafeFuture.collectAll(futures.stream())
|
||||
.thenApply { blockSizes ->
|
||||
blockSizes.sumOf {
|
||||
it.minus(config.blockSizeNonCalldataOverhead.toULong().toBigInteger())
|
||||
.coerceAtLeast(BigInteger.ZERO)
|
||||
}.also {
|
||||
log.debug(
|
||||
"sumOfBlockSizes={} blockSizes={} blockSizeNonCalldataOverhead={}",
|
||||
it,
|
||||
blockSizes,
|
||||
config.blockSizeNonCalldataOverhead,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SafeFuture.completedFuture(BigInteger.ZERO)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSumOfL2CalldataSize(): SafeFuture<BigInteger> {
|
||||
return getRecentL2CalldataSize()
|
||||
.whenException { th ->
|
||||
log.error(
|
||||
"Get the sum of L2 calldata size from the last {} blocks failure: {}",
|
||||
config.calldataSizeBlockCount,
|
||||
th.message,
|
||||
th,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ class ExtraDataV1PricerServiceTest {
|
||||
pollingInterval = pollingInterval,
|
||||
vertx = vertx,
|
||||
feesFetcher = mockFeesFetcher,
|
||||
minerExtraDataCalculatorImpl = boundableFeeCalculator,
|
||||
minerExtraDataCalculator = boundableFeeCalculator,
|
||||
extraDataUpdater = mockExtraDataUpdater,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package net.consensys.linea.ethereum.gaspricing.staticcap
|
||||
|
||||
import linea.domain.FeeHistory
|
||||
import net.consensys.linea.ethereum.gaspricing.FeesCalculator
|
||||
import net.consensys.linea.ethereum.gaspricing.L2CalldataSizeAccumulator
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
|
||||
class L2CalldataBasedVariableFeesCalculatorTest {
|
||||
private val config = L2CalldataBasedVariableFeesCalculator.Config(
|
||||
feeChangeDenominator = 32u,
|
||||
calldataSizeBlockCount = 5u,
|
||||
maxBlockCalldataSize = 109000u,
|
||||
)
|
||||
private val feeHistory = FeeHistory(
|
||||
oldestBlock = 100uL,
|
||||
baseFeePerGas = listOf(100UL),
|
||||
reward = listOf(listOf(1000UL)),
|
||||
gasUsedRatio = listOf(0.25),
|
||||
baseFeePerBlobGas = listOf(100UL),
|
||||
blobGasUsedRatio = listOf(0.25),
|
||||
)
|
||||
private val variableFee = 15000.0
|
||||
val mockVariableFeesCalculator = mock<FeesCalculator> {
|
||||
on { calculateFees(eq(feeHistory)) } doReturn variableFee
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_calculateFees_past_blocks_calldata_at_max_target() {
|
||||
val sumOfCalldataSize = (109000 * 5).toBigInteger() // maxBlockCalldataSize * calldataSizeBlockCount
|
||||
val mockl2CalldataSizeAccumulator = mock<L2CalldataSizeAccumulator> {
|
||||
on { getSumOfL2CalldataSize() } doReturn SafeFuture.completedFuture(sumOfCalldataSize)
|
||||
}
|
||||
// delta would be 1.0
|
||||
val delta = 1.0
|
||||
val expectedVariableFees = 15000.0 * (1.0 + delta / 32.0)
|
||||
|
||||
val feesCalculator = L2CalldataBasedVariableFeesCalculator(
|
||||
config = config,
|
||||
variableFeesCalculator = mockVariableFeesCalculator,
|
||||
l2CalldataSizeAccumulator = mockl2CalldataSizeAccumulator,
|
||||
)
|
||||
|
||||
// call calculateFees first to instantiate the lastVariableCost
|
||||
feesCalculator.calculateFees(feeHistory)
|
||||
|
||||
assertThat(feesCalculator.calculateFees(feeHistory))
|
||||
.isEqualTo(expectedVariableFees)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_calculateFees_past_blocks_calldata_exceed_max_target() {
|
||||
// This could happen as the calldata from L2CalldataSizeAccumulator is just approximation
|
||||
val sumOfCalldataSize = (200000 * 5).toBigInteger()
|
||||
val mockl2CalldataSizeAccumulator = mock<L2CalldataSizeAccumulator> {
|
||||
on { getSumOfL2CalldataSize() } doReturn SafeFuture.completedFuture(sumOfCalldataSize)
|
||||
}
|
||||
// delta would be 1.0
|
||||
val delta = 1.0
|
||||
val expectedVariableFees = 15000.0 * (1.0 + delta / 32.0)
|
||||
|
||||
val feesCalculator = L2CalldataBasedVariableFeesCalculator(
|
||||
config = config,
|
||||
variableFeesCalculator = mockVariableFeesCalculator,
|
||||
l2CalldataSizeAccumulator = mockl2CalldataSizeAccumulator,
|
||||
)
|
||||
|
||||
// call calculateFees first to instantiate the lastVariableCost
|
||||
feesCalculator.calculateFees(feeHistory)
|
||||
|
||||
assertThat(feesCalculator.calculateFees(feeHistory))
|
||||
.isEqualTo(expectedVariableFees)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_calculateFees_past_blocks_calldata_size_at_zero() {
|
||||
val mockl2CalldataSizeAccumulator = mock<L2CalldataSizeAccumulator> {
|
||||
on { getSumOfL2CalldataSize() } doReturn SafeFuture.completedFuture(BigInteger.ZERO)
|
||||
}
|
||||
|
||||
val feesCalculator = L2CalldataBasedVariableFeesCalculator(
|
||||
config = config,
|
||||
variableFeesCalculator = mockVariableFeesCalculator,
|
||||
l2CalldataSizeAccumulator = mockl2CalldataSizeAccumulator,
|
||||
)
|
||||
|
||||
// call calculateFees first to instantiate the lastVariableCost
|
||||
feesCalculator.calculateFees(feeHistory)
|
||||
|
||||
assertThat(feesCalculator.calculateFees(feeHistory))
|
||||
.isEqualTo(15000.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_calculateFees_past_blocks_calldata_above_half_max() {
|
||||
val sumOfCalldataSize = (81750 * 5).toBigInteger()
|
||||
val mockl2CalldataSizeAccumulator = mock<L2CalldataSizeAccumulator> {
|
||||
on { getSumOfL2CalldataSize() } doReturn SafeFuture.completedFuture(sumOfCalldataSize)
|
||||
}
|
||||
// delta would be 0.5
|
||||
val delta = 0.5
|
||||
val expectedVariableFees = 15000.0 * (1.0 + delta / 32.0)
|
||||
|
||||
val feesCalculator = L2CalldataBasedVariableFeesCalculator(
|
||||
config = config,
|
||||
variableFeesCalculator = mockVariableFeesCalculator,
|
||||
l2CalldataSizeAccumulator = mockl2CalldataSizeAccumulator,
|
||||
)
|
||||
|
||||
// call calculateFees first to instantiate the lastVariableCost
|
||||
feesCalculator.calculateFees(feeHistory)
|
||||
|
||||
assertThat(feesCalculator.calculateFees(feeHistory))
|
||||
.isEqualTo(expectedVariableFees)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_calculateFees_past_blocks_calldata_below_half_max() {
|
||||
val sumOfCalldataSize = (27250 * 5).toBigInteger()
|
||||
val mockl2CalldataSizeAccumulator = mock<L2CalldataSizeAccumulator> {
|
||||
on { getSumOfL2CalldataSize() } doReturn SafeFuture.completedFuture(sumOfCalldataSize)
|
||||
}
|
||||
|
||||
val feesCalculator = L2CalldataBasedVariableFeesCalculator(
|
||||
config = config,
|
||||
variableFeesCalculator = mockVariableFeesCalculator,
|
||||
l2CalldataSizeAccumulator = mockl2CalldataSizeAccumulator,
|
||||
)
|
||||
|
||||
// call calculateFees first to instantiate the lastVariableCost
|
||||
feesCalculator.calculateFees(feeHistory)
|
||||
|
||||
assertThat(feesCalculator.calculateFees(feeHistory))
|
||||
.isEqualTo(15000.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_calculateFees_increase_to_more_than_double_when_past_blocks_calldata_at_max_target() {
|
||||
val sumOfCalldataSize = (109000 * 5).toBigInteger() // maxBlockCalldataSize * calldataSizeBlockCount
|
||||
val mockl2CalldataSizeAccumulator = mock<L2CalldataSizeAccumulator> {
|
||||
on { getSumOfL2CalldataSize() } doReturn SafeFuture.completedFuture(sumOfCalldataSize)
|
||||
}
|
||||
|
||||
val feesCalculator = L2CalldataBasedVariableFeesCalculator(
|
||||
config = config,
|
||||
variableFeesCalculator = mockVariableFeesCalculator,
|
||||
l2CalldataSizeAccumulator = mockl2CalldataSizeAccumulator,
|
||||
)
|
||||
|
||||
// call calculateFees first to instantiate the lastVariableCost
|
||||
feesCalculator.calculateFees(feeHistory)
|
||||
|
||||
// With (1 + 1/32) as the rate, after 23 calls
|
||||
// the expectedVariableFees should be increased to more than double
|
||||
var calculatedFee = 0.0
|
||||
(0..22).forEach { _ ->
|
||||
calculatedFee = feesCalculator.calculateFees(feeHistory)
|
||||
}
|
||||
|
||||
assertThat(calculatedFee).isGreaterThan(15000.0 * 2.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_calculateFees_decrease_to_less_than_half_when_past_blocks_calldata_at_zero() {
|
||||
val mockVariableFeesCalculator = mock<FeesCalculator>()
|
||||
whenever(mockVariableFeesCalculator.calculateFees(eq(feeHistory)))
|
||||
.thenReturn(variableFee, 0.0)
|
||||
|
||||
val mockl2CalldataSizeAccumulator = mock<L2CalldataSizeAccumulator> {
|
||||
on { getSumOfL2CalldataSize() } doReturn SafeFuture.completedFuture(BigInteger.ZERO)
|
||||
}
|
||||
|
||||
val feesCalculator = L2CalldataBasedVariableFeesCalculator(
|
||||
config = config,
|
||||
variableFeesCalculator = mockVariableFeesCalculator,
|
||||
l2CalldataSizeAccumulator = mockl2CalldataSizeAccumulator,
|
||||
)
|
||||
|
||||
// call calculateFees first to instantiate the lastVariableCost
|
||||
feesCalculator.calculateFees(feeHistory)
|
||||
|
||||
// With (1 - 1/32) as the rate, after 22 calls
|
||||
// the expectedVariableFees should be decreased to less than half
|
||||
var calculatedFee = 0.0
|
||||
(0..21).forEach { _ ->
|
||||
calculatedFee = feesCalculator.calculateFees(feeHistory)
|
||||
}
|
||||
|
||||
assertThat(calculatedFee).isLessThan(15000.0 / 2.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_calculateFees_when_block_count_is_zero() {
|
||||
val sumOfCalldataSize = (109000 * 5).toBigInteger() // maxBlockCalldataSize * calldataSizeBlockCount
|
||||
val mockl2CalldataSizeAccumulator = mock<L2CalldataSizeAccumulator> {
|
||||
on { getSumOfL2CalldataSize() } doReturn SafeFuture.completedFuture(sumOfCalldataSize)
|
||||
}
|
||||
|
||||
val feesCalculator = L2CalldataBasedVariableFeesCalculator(
|
||||
config = L2CalldataBasedVariableFeesCalculator.Config(
|
||||
feeChangeDenominator = 32u,
|
||||
calldataSizeBlockCount = 0u, // set zero to disable calldata-based variable fees
|
||||
maxBlockCalldataSize = 109000u,
|
||||
),
|
||||
variableFeesCalculator = mockVariableFeesCalculator,
|
||||
l2CalldataSizeAccumulator = mockl2CalldataSizeAccumulator,
|
||||
)
|
||||
|
||||
// call calculateFees first to instantiate the lastVariableCost
|
||||
feesCalculator.calculateFees(feeHistory)
|
||||
|
||||
// The returned variable fees should always be 15000.0
|
||||
// as calldata-based variable fees is disabled
|
||||
assertThat(feesCalculator.calculateFees(feeHistory))
|
||||
.isEqualTo(15000.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package net.consensys.linea.ethereum.gaspricing.staticcap
|
||||
|
||||
import linea.web3j.ExtendedWeb3J
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verify
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
|
||||
class L2CalldataSizeAccumulatorImplTest {
|
||||
private val config = L2CalldataSizeAccumulatorImpl.Config(
|
||||
blockSizeNonCalldataOverhead = 540u,
|
||||
calldataSizeBlockCount = 5u,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun test_getSumOfL2CalldataSize() {
|
||||
val mockWeb3jClient = mock<ExtendedWeb3J> {
|
||||
on { ethBlockNumber() } doReturn SafeFuture.completedFuture(100.toBigInteger())
|
||||
on { ethGetBlockSizeByNumber(any()) } doReturn SafeFuture.completedFuture(10540.toBigInteger())
|
||||
}
|
||||
val l2CalldataSizeAccumulator = L2CalldataSizeAccumulatorImpl(
|
||||
config = config,
|
||||
web3jClient = mockWeb3jClient,
|
||||
)
|
||||
|
||||
val sumOfL2CalldataSize = l2CalldataSizeAccumulator.getSumOfL2CalldataSize().get()
|
||||
|
||||
(0..4).forEach {
|
||||
verify(mockWeb3jClient, times(1))
|
||||
.ethGetBlockSizeByNumber(eq(100L - it))
|
||||
}
|
||||
|
||||
val expectedCalldataSize = (10540 - 540) * 5
|
||||
assertThat(sumOfL2CalldataSize).isEqualTo(expectedCalldataSize.toBigInteger())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getSumOfL2CalldataSize_for_each_calldata_size_at_zero() {
|
||||
val mockWeb3jClient = mock<ExtendedWeb3J> {
|
||||
on { ethBlockNumber() } doReturn SafeFuture.completedFuture(100.toBigInteger())
|
||||
on { ethGetBlockSizeByNumber(any()) } doReturn SafeFuture.completedFuture(BigInteger.ZERO)
|
||||
}
|
||||
val l2CalldataSizeAccumulator = L2CalldataSizeAccumulatorImpl(
|
||||
config = config,
|
||||
web3jClient = mockWeb3jClient,
|
||||
)
|
||||
|
||||
val sumOfL2CalldataSize = l2CalldataSizeAccumulator.getSumOfL2CalldataSize().get()
|
||||
|
||||
(0..4).forEach {
|
||||
verify(mockWeb3jClient, times(1))
|
||||
.ethGetBlockSizeByNumber(eq(100L - it))
|
||||
}
|
||||
|
||||
assertThat(sumOfL2CalldataSize).isEqualTo(BigInteger.ZERO)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getSumOfL2CalldataSize_for_exception() {
|
||||
val mockWeb3jClient = mock<ExtendedWeb3J> {
|
||||
on { ethBlockNumber() } doReturn SafeFuture.failedFuture(RuntimeException("Failed for testing"))
|
||||
}
|
||||
val l2CalldataSizeAccumulator = L2CalldataSizeAccumulatorImpl(
|
||||
config = config,
|
||||
web3jClient = mockWeb3jClient,
|
||||
)
|
||||
|
||||
assertThrows<Exception> {
|
||||
l2CalldataSizeAccumulator.getSumOfL2CalldataSize().get()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getSumOfL2CalldataSize_when_ethBlockNumber_is_less_than_calldataSizeBlockCount() {
|
||||
val mockWeb3jClient = mock<ExtendedWeb3J> {
|
||||
on { ethBlockNumber() } doReturn SafeFuture.completedFuture(BigInteger.ONE)
|
||||
on { ethGetBlockSizeByNumber(any()) } doReturn SafeFuture.completedFuture(10540.toBigInteger())
|
||||
}
|
||||
val l2CalldataSizeAccumulator = L2CalldataSizeAccumulatorImpl(
|
||||
config = config,
|
||||
web3jClient = mockWeb3jClient,
|
||||
)
|
||||
|
||||
val sumOfL2CalldataSize = l2CalldataSizeAccumulator.getSumOfL2CalldataSize().get()
|
||||
|
||||
verify(mockWeb3jClient, times(0)).ethGetBlockSizeByNumber(any())
|
||||
|
||||
assertThat(sumOfL2CalldataSize).isEqualTo(BigInteger.ZERO)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getSumOfL2CalldataSize_if_calldataSizeBlockCount_is_zero() {
|
||||
val mockWeb3jClient = mock<ExtendedWeb3J> {
|
||||
on { ethBlockNumber() } doReturn SafeFuture.completedFuture(BigInteger.ONE)
|
||||
on { ethGetBlockSizeByNumber(any()) } doReturn SafeFuture.completedFuture(10540.toBigInteger())
|
||||
}
|
||||
val l2CalldataSizeAccumulator = L2CalldataSizeAccumulatorImpl(
|
||||
config = L2CalldataSizeAccumulatorImpl.Config(
|
||||
blockSizeNonCalldataOverhead = 540u,
|
||||
calldataSizeBlockCount = 0u,
|
||||
),
|
||||
web3jClient = mockWeb3jClient,
|
||||
)
|
||||
|
||||
val sumOfL2CalldataSize = l2CalldataSizeAccumulator.getSumOfL2CalldataSize().get()
|
||||
|
||||
verify(mockWeb3jClient, times(0)).ethGetBlockSizeByNumber(any())
|
||||
|
||||
assertThat(sumOfL2CalldataSize).isEqualTo(BigInteger.ZERO)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ interface ExtendedWeb3J {
|
||||
fun ethBlockNumber(): SafeFuture<BigInteger>
|
||||
fun ethGetBlock(blockParameter: BlockParameter): SafeFuture<Block?>
|
||||
fun ethGetBlockTimestampByNumber(blockNumber: Long): SafeFuture<BigInteger>
|
||||
fun ethGetBlockSizeByNumber(blockNumber: Long): SafeFuture<BigInteger>
|
||||
}
|
||||
|
||||
class ExtendedWeb3JImpl(override val web3jClient: Web3j) : ExtendedWeb3J {
|
||||
@@ -76,4 +77,26 @@ class ExtendedWeb3JImpl(override val web3jClient: Web3j) : ExtendedWeb3J {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun ethGetBlockSizeByNumber(
|
||||
blockNumber: Long,
|
||||
): SafeFuture<BigInteger> {
|
||||
return SafeFuture.of(
|
||||
web3jClient
|
||||
.ethGetBlockByNumber(
|
||||
DefaultBlockParameter.valueOf(BigInteger.valueOf(blockNumber)),
|
||||
false,
|
||||
)
|
||||
.sendAsync(),
|
||||
)
|
||||
.thenCompose { response ->
|
||||
if (response.hasError()) {
|
||||
SafeFuture.failedFuture(buildException(response.error))
|
||||
} else {
|
||||
response.block?.let {
|
||||
SafeFuture.completedFuture(response.block.size)
|
||||
} ?: SafeFuture.failedFuture(Exception("Block $blockNumber not found!"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user