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:
jonesho
2025-06-21 01:29:11 +08:00
committed by GitHub
parent d9d1474782
commit b31a330d40
16 changed files with 622 additions and 9 deletions

View File

@@ -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"

View File

@@ -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(

View File

@@ -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(),

View File

@@ -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"),
)
}
}

View File

@@ -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,

View File

@@ -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(

View File

@@ -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(

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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,
)
}
}
}

View File

@@ -82,7 +82,7 @@ class ExtraDataV1PricerServiceTest {
pollingInterval = pollingInterval,
vertx = vertx,
feesFetcher = mockFeesFetcher,
minerExtraDataCalculatorImpl = boundableFeeCalculator,
minerExtraDataCalculator = boundableFeeCalculator,
extraDataUpdater = mockExtraDataUpdater,
)

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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!"))
}
}
}
}