diff --git a/config/coordinator/coordinator-config-v2.toml b/config/coordinator/coordinator-config-v2.toml index 4d8c79fe..7c8801cc 100644 --- a/config/coordinator/coordinator-config-v2.toml +++ b/config/coordinator/coordinator-config-v2.toml @@ -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" diff --git a/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/L2NetworkGasPricingConfig.kt b/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/L2NetworkGasPricingConfig.kt index 225d8c5b..d408346e 100644 --- a/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/L2NetworkGasPricingConfig.kt +++ b/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/L2NetworkGasPricingConfig.kt @@ -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( diff --git a/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/toml/CoordinatorConfigFilesToml.kt b/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/toml/CoordinatorConfigFilesToml.kt index 191221f8..6496bbec 100644 --- a/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/toml/CoordinatorConfigFilesToml.kt +++ b/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/toml/CoordinatorConfigFilesToml.kt @@ -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(), diff --git a/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/toml/L2NetworkGasPricingConfigToml.kt b/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/toml/L2NetworkGasPricingConfigToml.kt index 10b7118b..8cbd8d80 100644 --- a/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/toml/L2NetworkGasPricingConfigToml.kt +++ b/coordinator/app/src/main/kotlin/linea/coordinator/config/v2/toml/L2NetworkGasPricingConfigToml.kt @@ -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"), ) } } diff --git a/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/app/L1DependentApp.kt b/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/app/L1DependentApp.kt index 1eb9506b..4de07e3d 100644 --- a/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/app/L1DependentApp.kt +++ b/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/app/L1DependentApp.kt @@ -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, diff --git a/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/app/L2NetworkGasPricingService.kt b/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/app/L2NetworkGasPricingService.kt index dcb4abf3..a533941c 100644 --- a/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/app/L2NetworkGasPricingService.kt +++ b/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/app/L2NetworkGasPricingService.kt @@ -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( diff --git a/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/blockcreation/BlockCreationMonitor.kt b/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/blockcreation/BlockCreationMonitor.kt index 828e85c1..b6d60024 100644 --- a/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/blockcreation/BlockCreationMonitor.kt +++ b/coordinator/app/src/main/kotlin/net/consensys/zkevm/coordinator/blockcreation/BlockCreationMonitor.kt @@ -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( diff --git a/coordinator/app/src/test/kotlin/linea/coordinator/config/v2/L2NetWorkingGasPricingConfigParsingTest.kt b/coordinator/app/src/test/kotlin/linea/coordinator/config/v2/L2NetWorkingGasPricingConfigParsingTest.kt index d34bff13..f233ead8 100644 --- a/coordinator/app/src/test/kotlin/linea/coordinator/config/v2/L2NetWorkingGasPricingConfigParsingTest.kt +++ b/coordinator/app/src/test/kotlin/linea/coordinator/config/v2/L2NetWorkingGasPricingConfigParsingTest.kt @@ -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 diff --git a/coordinator/ethereum/gas-pricing/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/DynamicGasPrice.kt b/coordinator/ethereum/gas-pricing/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/DynamicGasPrice.kt index 059c4a61..ff147520 100644 --- a/coordinator/ethereum/gas-pricing/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/DynamicGasPrice.kt +++ b/coordinator/ethereum/gas-pricing/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/DynamicGasPrice.kt @@ -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 } +interface L2CalldataSizeAccumulator { + fun getSumOfL2CalldataSize(): SafeFuture +} + fun interface FeesCalculator { fun calculateFees(feeHistory: FeeHistory): Double } diff --git a/coordinator/ethereum/gas-pricing/static-cap/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/ExtraDataV1PricerService.kt b/coordinator/ethereum/gas-pricing/static-cap/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/ExtraDataV1PricerService.kt index 4edea8a9..15b65970 100644 --- a/coordinator/ethereum/gas-pricing/static-cap/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/ExtraDataV1PricerService.kt +++ b/coordinator/ethereum/gas-pricing/static-cap/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/ExtraDataV1PricerService.kt @@ -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 diff --git a/coordinator/ethereum/gas-pricing/static-cap/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataBasedVariableFeesCalculator.kt b/coordinator/ethereum/gas-pricing/static-cap/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataBasedVariableFeesCalculator.kt new file mode 100644 index 00000000..bb0ed338 --- /dev/null +++ b/coordinator/ethereum/gas-pricing/static-cap/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataBasedVariableFeesCalculator.kt @@ -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 = 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 + } +} diff --git a/coordinator/ethereum/gas-pricing/static-cap/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataSizeAccumulatorImpl.kt b/coordinator/ethereum/gas-pricing/static-cap/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataSizeAccumulatorImpl.kt new file mode 100644 index 00000000..8836fd66 --- /dev/null +++ b/coordinator/ethereum/gas-pricing/static-cap/src/main/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataSizeAccumulatorImpl.kt @@ -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 { + 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 { + return getRecentL2CalldataSize() + .whenException { th -> + log.error( + "Get the sum of L2 calldata size from the last {} blocks failure: {}", + config.calldataSizeBlockCount, + th.message, + th, + ) + } + } +} diff --git a/coordinator/ethereum/gas-pricing/static-cap/src/test/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/ExtraDataV1PricerServiceTest.kt b/coordinator/ethereum/gas-pricing/static-cap/src/test/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/ExtraDataV1PricerServiceTest.kt index 2d365d8c..49ef7760 100644 --- a/coordinator/ethereum/gas-pricing/static-cap/src/test/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/ExtraDataV1PricerServiceTest.kt +++ b/coordinator/ethereum/gas-pricing/static-cap/src/test/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/ExtraDataV1PricerServiceTest.kt @@ -82,7 +82,7 @@ class ExtraDataV1PricerServiceTest { pollingInterval = pollingInterval, vertx = vertx, feesFetcher = mockFeesFetcher, - minerExtraDataCalculatorImpl = boundableFeeCalculator, + minerExtraDataCalculator = boundableFeeCalculator, extraDataUpdater = mockExtraDataUpdater, ) diff --git a/coordinator/ethereum/gas-pricing/static-cap/src/test/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataBasedVariableFeesCalculatorTest.kt b/coordinator/ethereum/gas-pricing/static-cap/src/test/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataBasedVariableFeesCalculatorTest.kt new file mode 100644 index 00000000..a4790839 --- /dev/null +++ b/coordinator/ethereum/gas-pricing/static-cap/src/test/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataBasedVariableFeesCalculatorTest.kt @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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() + whenever(mockVariableFeesCalculator.calculateFees(eq(feeHistory))) + .thenReturn(variableFee, 0.0) + + val mockl2CalldataSizeAccumulator = mock { + 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 { + 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) + } +} diff --git a/coordinator/ethereum/gas-pricing/static-cap/src/test/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataSizeAccumulatorImplTest.kt b/coordinator/ethereum/gas-pricing/static-cap/src/test/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataSizeAccumulatorImplTest.kt new file mode 100644 index 00000000..730c79fb --- /dev/null +++ b/coordinator/ethereum/gas-pricing/static-cap/src/test/kotlin/net/consensys/linea/ethereum/gaspricing/staticcap/L2CalldataSizeAccumulatorImplTest.kt @@ -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 { + 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 { + 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 { + on { ethBlockNumber() } doReturn SafeFuture.failedFuture(RuntimeException("Failed for testing")) + } + val l2CalldataSizeAccumulator = L2CalldataSizeAccumulatorImpl( + config = config, + web3jClient = mockWeb3jClient, + ) + + assertThrows { + l2CalldataSizeAccumulator.getSumOfL2CalldataSize().get() + } + } + + @Test + fun test_getSumOfL2CalldataSize_when_ethBlockNumber_is_less_than_calldataSizeBlockCount() { + val mockWeb3jClient = mock { + 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 { + 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) + } +} diff --git a/jvm-libs/linea/web3j-extensions/src/main/kotlin/linea/web3j/ExtendedWeb3J.kt b/jvm-libs/linea/web3j-extensions/src/main/kotlin/linea/web3j/ExtendedWeb3J.kt index 83542100..97d92e9e 100644 --- a/jvm-libs/linea/web3j-extensions/src/main/kotlin/linea/web3j/ExtendedWeb3J.kt +++ b/jvm-libs/linea/web3j-extensions/src/main/kotlin/linea/web3j/ExtendedWeb3J.kt @@ -19,6 +19,7 @@ interface ExtendedWeb3J { fun ethBlockNumber(): SafeFuture fun ethGetBlock(blockParameter: BlockParameter): SafeFuture fun ethGetBlockTimestampByNumber(blockNumber: Long): SafeFuture + fun ethGetBlockSizeByNumber(blockNumber: Long): SafeFuture } class ExtendedWeb3JImpl(override val web3jClient: Web3j) : ExtendedWeb3J { @@ -76,4 +77,26 @@ class ExtendedWeb3JImpl(override val web3jClient: Web3j) : ExtendedWeb3J { } } } + + override fun ethGetBlockSizeByNumber( + blockNumber: Long, + ): SafeFuture { + 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!")) + } + } + } }