mirror of
https://github.com/vacp2p/linea-monorepo.git
synced 2026-01-09 15:38:06 -05:00
Message anchoring v2 part6 (#910)
* coordinator: order MessageSentEvent.kt params for better reading * coordinator: fix MessageSentEvent.kt serialization * coordinator: WIP implementation of new Web3JL2MessageServiceSmartContractClient * coordinator: use new anchoring implementation * coordinator: use version from maven * coordinator: minor generics tweak * add .java-version to gitignore * coordinator: remove old anchoring implementation * coordinator: move Anchoring events to common interfaces packages * coordinator: add factory method * coordinator: use factory method * coordinator: clean unused method
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,6 +12,7 @@
|
||||
.externalToolBuilders/
|
||||
.gradle/
|
||||
.idea/
|
||||
.java-version
|
||||
.vscode
|
||||
.loadpath
|
||||
.metadata
|
||||
@@ -54,4 +55,4 @@ tsconfig.build.tsbuildinfo
|
||||
ts-libs/**/lib/**/*.so
|
||||
ts-libs/**/lib/**/*.dylib
|
||||
contracts/lib/forge-std/
|
||||
cache_forge
|
||||
cache_forge
|
||||
|
||||
@@ -209,10 +209,16 @@ max-pool-size=10
|
||||
keep-alive=true
|
||||
public-key="4a788ad6fa008beed58de6418369717d7492f37d173d70e2c26d9737e2c6eeae929452ef8602a19410844db3e200a0e73f5208fd76259a8766b73953fc3e7023"
|
||||
|
||||
[message-anchoring-service]
|
||||
disabled=false
|
||||
polling-interval="PT1S"
|
||||
max-messages-to-anchor=100
|
||||
[message-anchoring]
|
||||
disabled = false
|
||||
l1-highest-block-tag="LATEST"
|
||||
l2-highest-block-tag="LATEST"
|
||||
l1-event-polling-interval="PT1S"
|
||||
anchoring-tick-interval = "PT1S"
|
||||
[message-anchoring.l1-request-retries]
|
||||
failures-warning-threshold = 1
|
||||
[message-anchoring.l2-request-retries]
|
||||
failures-warning-threshold = 1
|
||||
|
||||
[l2-network-gas-pricing]
|
||||
disabled = false
|
||||
|
||||
@@ -35,6 +35,7 @@ dependencies {
|
||||
implementation project(':coordinator:ethereum:models-helper')
|
||||
implementation project(':coordinator:ethereum:blob-submitter')
|
||||
implementation project(':coordinator:ethereum:message-anchoring')
|
||||
implementation project(':coordinator:ethereum:message-anchoring-i2')
|
||||
implementation project(':coordinator:clients:web3signer-client')
|
||||
implementation project(':coordinator:persistence:blob')
|
||||
implementation project(':coordinator:persistence:aggregation')
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.sksamuel.hoplite.ConfigLoaderBuilder
|
||||
import com.sksamuel.hoplite.addPathSource
|
||||
import net.consensys.linea.traces.TracesCountersV1
|
||||
import net.consensys.linea.traces.TracesCountersV2
|
||||
import net.consensys.zkevm.coordinator.app.config.BlockParameterDecoder
|
||||
import net.consensys.zkevm.coordinator.app.config.CoordinatorConfig
|
||||
import net.consensys.zkevm.coordinator.app.config.CoordinatorConfigTomlDto
|
||||
import net.consensys.zkevm.coordinator.app.config.GasPriceCapTimeOfDayMultipliersConfig
|
||||
@@ -22,7 +23,10 @@ import java.nio.file.Path
|
||||
inline fun <reified T : Any> loadConfigsOrError(
|
||||
configFiles: List<Path>
|
||||
): Result<T, String> {
|
||||
val confBuilder: ConfigLoaderBuilder = ConfigLoaderBuilder.Companion.empty().addDefaults()
|
||||
val confBuilder: ConfigLoaderBuilder = ConfigLoaderBuilder.Companion
|
||||
.empty()
|
||||
.addDefaults()
|
||||
.addDecoder(BlockParameterDecoder())
|
||||
for (configFile in configFiles.reversed()) {
|
||||
// files must be added in reverse order for overriding
|
||||
confBuilder.addPathSource(configFile, false)
|
||||
|
||||
@@ -7,12 +7,8 @@ import linea.web3j.SmartContractErrors
|
||||
import linea.web3j.gas.EIP1559GasProvider
|
||||
import linea.web3j.transactionmanager.AsyncFriendlyTransactionManager
|
||||
import net.consensys.linea.contract.L2MessageService
|
||||
import net.consensys.linea.contract.LineaRollupAsyncFriendly
|
||||
import net.consensys.linea.contract.l1.Web3JLineaRollupSmartContractClient
|
||||
import net.consensys.linea.contract.l2.L2MessageServiceGasLimitEstimate
|
||||
import net.consensys.linea.ethereum.gaspricing.FeesCalculator
|
||||
import net.consensys.linea.ethereum.gaspricing.FeesFetcher
|
||||
import net.consensys.linea.ethereum.gaspricing.WMAGasProvider
|
||||
import net.consensys.linea.httprest.client.VertxHttpRestClient
|
||||
import net.consensys.zkevm.coordinator.app.config.L1Config
|
||||
import net.consensys.zkevm.coordinator.app.config.L2Config
|
||||
@@ -59,33 +55,6 @@ fun createTransactionManager(
|
||||
return AsyncFriendlyTransactionManager(client, transactionSignService, -1L)
|
||||
}
|
||||
|
||||
fun instantiateZkEvmContractClient(
|
||||
l1Config: L1Config,
|
||||
transactionManager: AsyncFriendlyTransactionManager,
|
||||
gasFetcher: FeesFetcher,
|
||||
priorityFeeCalculator: FeesCalculator,
|
||||
client: Web3j,
|
||||
smartContractErrors: SmartContractErrors
|
||||
): LineaRollupAsyncFriendly {
|
||||
return LineaRollupAsyncFriendly.load(
|
||||
l1Config.zkEvmContractAddress,
|
||||
client,
|
||||
transactionManager,
|
||||
WMAGasProvider(
|
||||
client.ethChainId().send().chainId.toLong(),
|
||||
gasFetcher,
|
||||
priorityFeeCalculator,
|
||||
WMAGasProvider.Config(
|
||||
gasLimit = l1Config.gasLimit,
|
||||
maxFeePerGasCap = l1Config.maxFeePerGasCap,
|
||||
maxPriorityFeePerGasCap = l1Config.maxPriorityFeePerGasCap,
|
||||
maxFeePerBlobGasCap = l1Config.maxFeePerBlobGasCap
|
||||
)
|
||||
),
|
||||
smartContractErrors
|
||||
)
|
||||
}
|
||||
|
||||
fun createLineaRollupContractClient(
|
||||
l1Config: L1Config,
|
||||
transactionManager: AsyncFriendlyTransactionManager,
|
||||
|
||||
@@ -4,14 +4,17 @@ import build.linea.clients.StateManagerClientV1
|
||||
import build.linea.clients.StateManagerV1JsonRpcClient
|
||||
import io.vertx.core.Vertx
|
||||
import kotlinx.datetime.Clock
|
||||
import linea.anchoring.MessageAnchoringApp
|
||||
import linea.contract.l1.LineaRollupSmartContractClientReadOnly
|
||||
import linea.contract.l1.Web3JLineaRollupSmartContractClientReadOnly
|
||||
import linea.contract.l2.Web3JL2MessageServiceSmartContractClient
|
||||
import linea.domain.BlockNumberAndHash
|
||||
import linea.encoding.BlockRLPEncoder
|
||||
import linea.web3j.ExtendedWeb3JImpl
|
||||
import linea.web3j.SmartContractErrors
|
||||
import linea.web3j.Web3jBlobExtended
|
||||
import linea.web3j.createWeb3jHttpClient
|
||||
import linea.web3j.ethapi.createEthApiClient
|
||||
import net.consensys.linea.blob.ShnarfCalculatorVersion
|
||||
import net.consensys.linea.contract.Web3JL2MessageService
|
||||
import net.consensys.linea.contract.Web3JL2MessageServiceLogsClient
|
||||
@@ -127,7 +130,7 @@ class L1DependentApp(
|
||||
private val log = LogManager.getLogger(this::class.java)
|
||||
|
||||
init {
|
||||
if (configs.messageAnchoringService.disabled) {
|
||||
if (configs.messageAnchoring.disabled) {
|
||||
log.warn("Message anchoring service is disabled")
|
||||
}
|
||||
if (configs.l2NetworkGasPricingService == null) {
|
||||
@@ -141,12 +144,6 @@ class L1DependentApp(
|
||||
l2Web3jClient
|
||||
)
|
||||
|
||||
private val l2MessageService = instantiateL2MessageServiceContractClient(
|
||||
configs.l2,
|
||||
l2TransactionManager,
|
||||
l2Web3jClient,
|
||||
smartContractErrors
|
||||
)
|
||||
private val l1Web3jClient = createWeb3jHttpClient(
|
||||
rpcUrl = configs.l1.rpcEndpoint.toString(),
|
||||
log = LogManager.getLogger("clients.l1.eth-api"),
|
||||
@@ -931,26 +928,41 @@ class L1DependentApp(
|
||||
return l1BasedLastFinalizedBlockProvider.getLastFinalizedBlock()
|
||||
}
|
||||
|
||||
private val messageAnchoringApp: L1toL2MessageAnchoringApp? =
|
||||
if (configs.messageAnchoringService.enabled) {
|
||||
L1toL2MessageAnchoringApp(
|
||||
vertx,
|
||||
L1toL2MessageAnchoringApp.Config(
|
||||
configs.l1,
|
||||
configs.l2,
|
||||
configs.finalizationSigner,
|
||||
configs.l2Signer,
|
||||
configs.messageAnchoringService
|
||||
),
|
||||
l1Web3jClient,
|
||||
l2Web3jClient,
|
||||
smartContractErrors,
|
||||
l2MessageService,
|
||||
l2TransactionManager
|
||||
private val messageAnchoringApp: LongRunningService = if (configs.messageAnchoring.enabled
|
||||
) {
|
||||
MessageAnchoringApp(
|
||||
vertx = vertx,
|
||||
config = MessageAnchoringApp.Config(
|
||||
l1RequestRetryConfig = configs.messageAnchoring.l1RequestRetryConfig,
|
||||
l1PollingInterval = configs.messageAnchoring.l1EventPollingInterval,
|
||||
l1SuccessBackoffDelay = configs.messageAnchoring.l1SuccessBackoffDelay,
|
||||
l1ContractAddress = configs.l1.zkEvmContractAddress,
|
||||
l1EventPollingTimeout = configs.messageAnchoring.l1EventPollingTimeout,
|
||||
l1EventSearchBlockChunk = configs.messageAnchoring.l1EventSearchBlockChunk,
|
||||
l2HighestBlockTag = configs.messageAnchoring.l2HighestBlockTag,
|
||||
anchoringTickInterval = configs.messageAnchoring.anchoringTickInterval,
|
||||
messageQueueCapacity = configs.messageAnchoring.messageQueueCapacity,
|
||||
maxMessagesToAnchorPerL2Transaction = configs.messageAnchoring.maxMessagesToAnchorPerL2Transaction
|
||||
),
|
||||
l1EthApiClient = createEthApiClient(
|
||||
web3jClient = l1Web3jClient,
|
||||
requestRetryConfig = null,
|
||||
vertx = vertx
|
||||
),
|
||||
l2MessageService = Web3JL2MessageServiceSmartContractClient.create(
|
||||
web3jClient = l2Web3jClient,
|
||||
contractAddress = configs.l2.messageServiceAddress,
|
||||
gasLimit = configs.l2.gasLimit,
|
||||
maxFeePerGasCap = configs.l2.maxFeePerGasCap,
|
||||
feeHistoryBlockCount = configs.l2.feeHistoryBlockCount,
|
||||
feeHistoryRewardPercentile = configs.l2.feeHistoryRewardPercentile,
|
||||
transactionManager = l2TransactionManager,
|
||||
smartContractErrors = smartContractErrors
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DisabledLongRunningService
|
||||
}
|
||||
|
||||
private val l2NetworkGasPricingService: L2NetworkGasPricingService? =
|
||||
if (configs.l2NetworkGasPricingService != null) {
|
||||
@@ -1053,7 +1065,7 @@ class L1DependentApp(
|
||||
.thenCompose { blobSubmissionCoordinator.start() }
|
||||
.thenCompose { aggregationFinalizationCoordinator.start() }
|
||||
.thenCompose { proofAggregationCoordinatorService.start() }
|
||||
.thenCompose { messageAnchoringApp?.start() ?: SafeFuture.completedFuture(Unit) }
|
||||
.thenCompose { messageAnchoringApp.start() }
|
||||
.thenCompose { l2NetworkGasPricingService?.start() ?: SafeFuture.completedFuture(Unit) }
|
||||
.thenCompose { l1FeeHistoryCachingService.start() }
|
||||
.thenCompose { deadlineConflationCalculatorRunnerOld.start() }
|
||||
@@ -1072,7 +1084,7 @@ class L1DependentApp(
|
||||
blobSubmissionCoordinator.stop(),
|
||||
aggregationFinalizationCoordinator.stop(),
|
||||
proofAggregationCoordinatorService.stop(),
|
||||
messageAnchoringApp?.stop() ?: SafeFuture.completedFuture(Unit),
|
||||
messageAnchoringApp.stop(),
|
||||
l2NetworkGasPricingService?.stop() ?: SafeFuture.completedFuture(Unit),
|
||||
l1FeeHistoryCachingService.stop(),
|
||||
blockCreationMonitor.stop(),
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package net.consensys.zkevm.coordinator.app
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import linea.web3j.SmartContractErrors
|
||||
import linea.web3j.gas.EIP1559GasProvider
|
||||
import linea.web3j.transactionmanager.AsyncFriendlyTransactionManager
|
||||
import net.consensys.linea.contract.L2MessageService
|
||||
import net.consensys.linea.contract.l1.Web3JLineaRollupSmartContractClient
|
||||
import net.consensys.zkevm.LongRunningService
|
||||
import net.consensys.zkevm.coordinator.app.config.L1Config
|
||||
import net.consensys.zkevm.coordinator.app.config.L2Config
|
||||
import net.consensys.zkevm.coordinator.app.config.MessageAnchoringServiceConfig
|
||||
import net.consensys.zkevm.coordinator.app.config.SignerConfig
|
||||
import net.consensys.zkevm.ethereum.coordination.messageanchoring.L1EventQuerier
|
||||
import net.consensys.zkevm.ethereum.coordination.messageanchoring.L1EventQuerierImpl
|
||||
import net.consensys.zkevm.ethereum.coordination.messageanchoring.L2MessageAnchorer
|
||||
import net.consensys.zkevm.ethereum.coordination.messageanchoring.L2MessageAnchorerImpl
|
||||
import net.consensys.zkevm.ethereum.coordination.messageanchoring.L2Querier
|
||||
import net.consensys.zkevm.ethereum.coordination.messageanchoring.L2QuerierImpl
|
||||
import net.consensys.zkevm.ethereum.coordination.messageanchoring.MessageAnchoringService
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.web3j.protocol.Web3j
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import kotlin.time.toKotlinDuration
|
||||
|
||||
class L1toL2MessageAnchoringApp(
|
||||
vertx: Vertx,
|
||||
configs: Config,
|
||||
l1Web3jClient: Web3j,
|
||||
l2Web3jClient: Web3j,
|
||||
smartContractErrors: SmartContractErrors,
|
||||
l2MessageService: L2MessageService,
|
||||
l2TransactionManager: AsyncFriendlyTransactionManager
|
||||
) : LongRunningService {
|
||||
private val log = LogManager.getLogger(this::class.java)
|
||||
|
||||
private var l1GasProvider = EIP1559GasProvider(
|
||||
l1Web3jClient,
|
||||
EIP1559GasProvider.Config(
|
||||
configs.l1.gasLimit,
|
||||
configs.l1.maxFeePerGasCap,
|
||||
configs.l1.feeHistoryBlockCount.toUInt(),
|
||||
configs.l1.feeHistoryRewardPercentile
|
||||
)
|
||||
)
|
||||
|
||||
data class Config(
|
||||
val l1: L1Config,
|
||||
val l2: L2Config,
|
||||
val l1Signer: SignerConfig,
|
||||
val l2Signer: SignerConfig,
|
||||
val messageAnchoringService: MessageAnchoringServiceConfig
|
||||
)
|
||||
|
||||
private val messageAnchoringService: MessageAnchoringService = run {
|
||||
val l1TransactionManager = createTransactionManager(
|
||||
vertx,
|
||||
configs.l1Signer,
|
||||
l1Web3jClient
|
||||
)
|
||||
|
||||
val l1EventQuerier: L1EventQuerier = L1EventQuerierImpl(
|
||||
vertx,
|
||||
L1EventQuerierImpl.Config(
|
||||
configs.l1.sendMessageEventPollingInterval.toKotlinDuration(),
|
||||
configs.l1.maxEventScrapingTime.toKotlinDuration(),
|
||||
configs.l1.earliestBlock,
|
||||
configs.l1.maxMessagesToCollect,
|
||||
configs.l1.zkEvmContractAddress,
|
||||
configs.l1.finalizedBlockTag,
|
||||
configs.l1.blockRangeLoopLimit
|
||||
),
|
||||
l1Web3jClient
|
||||
)
|
||||
|
||||
val l2Querier: L2Querier = L2QuerierImpl(
|
||||
l2Web3jClient,
|
||||
l2MessageService,
|
||||
L2QuerierImpl.Config(
|
||||
blocksToFinalizationL2 = configs.l2.blocksToFinalization,
|
||||
lastHashSearchWindow = configs.l2.lastHashSearchWindow,
|
||||
contractAddressToListen = l2MessageService.contractAddress
|
||||
),
|
||||
vertx
|
||||
)
|
||||
|
||||
val l2MessageAnchorer: L2MessageAnchorer = L2MessageAnchorerImpl(
|
||||
vertx,
|
||||
l2Web3jClient,
|
||||
l2MessageService,
|
||||
L2MessageAnchorerImpl.Config(
|
||||
configs.l2.anchoringReceiptPollingInterval.toKotlinDuration(),
|
||||
configs.l2.maxReceiptRetries,
|
||||
configs.l2.blocksToFinalization.toLong()
|
||||
)
|
||||
)
|
||||
|
||||
val anchoringConfig = MessageAnchoringService.Config(
|
||||
configs.messageAnchoringService.pollingInterval.toKotlinDuration(),
|
||||
configs.messageAnchoringService.maxMessagesToAnchor
|
||||
)
|
||||
|
||||
val lineaRollupSmartContractClient = Web3JLineaRollupSmartContractClient.load(
|
||||
contractAddress = configs.l1.zkEvmContractAddress,
|
||||
web3j = l1Web3jClient,
|
||||
transactionManager = l1TransactionManager,
|
||||
contractGasProvider = l1GasProvider,
|
||||
smartContractErrors = smartContractErrors
|
||||
)
|
||||
|
||||
MessageAnchoringService(
|
||||
anchoringConfig,
|
||||
vertx,
|
||||
l1EventQuerier,
|
||||
l2MessageAnchorer,
|
||||
l2Querier,
|
||||
lineaRollupSmartContractClient = lineaRollupSmartContractClient,
|
||||
l2MessageService,
|
||||
l2TransactionManager
|
||||
)
|
||||
}
|
||||
|
||||
override fun start(): SafeFuture<Unit> {
|
||||
return messageAnchoringService.start().thenPeek {
|
||||
log.info("L1toL2MessageAnchoringApp started")
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop(): SafeFuture<Unit> {
|
||||
return messageAnchoringService.stop().thenPeek {
|
||||
log.info("L1toL2MessageAnchoringApp stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.consensys.zkevm.coordinator.app.config
|
||||
|
||||
import com.sksamuel.hoplite.ConfigFailure
|
||||
import com.sksamuel.hoplite.ConfigResult
|
||||
import com.sksamuel.hoplite.DecoderContext
|
||||
import com.sksamuel.hoplite.LongNode
|
||||
import com.sksamuel.hoplite.Node
|
||||
import com.sksamuel.hoplite.StringNode
|
||||
import com.sksamuel.hoplite.decoder.Decoder
|
||||
import com.sksamuel.hoplite.fp.invalid
|
||||
import com.sksamuel.hoplite.fp.valid
|
||||
import linea.domain.BlockParameter
|
||||
import kotlin.reflect.KType
|
||||
|
||||
class BlockParameterDecoder : Decoder<BlockParameter> {
|
||||
override fun supports(type: KType): Boolean = type.classifier == BlockParameter::class
|
||||
override fun decode(node: Node, type: KType, context: DecoderContext): ConfigResult<BlockParameter> {
|
||||
return when (node) {
|
||||
is StringNode -> runCatching {
|
||||
BlockParameter.parse(node.value)
|
||||
}.fold(
|
||||
{ it.valid() },
|
||||
{ ConfigFailure.DecodeError(node, type).invalid() }
|
||||
)
|
||||
|
||||
is LongNode -> runCatching {
|
||||
BlockParameter.fromNumber(node.value)
|
||||
}.fold(
|
||||
{ it.valid() },
|
||||
{ ConfigFailure.DecodeError(node, type).invalid() }
|
||||
)
|
||||
|
||||
else -> ConfigFailure.DecodeError(node, type).invalid()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ interface RetryConfig {
|
||||
data class RequestRetryConfigTomlFriendly(
|
||||
override val maxRetries: Int? = null,
|
||||
override val timeout: Duration? = null,
|
||||
override val backoffDelay: Duration,
|
||||
override val backoffDelay: Duration = 1.milliseconds.toJavaDuration(),
|
||||
val failuresWarningThreshold: Int = 0
|
||||
) : RetryConfig {
|
||||
init {
|
||||
@@ -98,16 +98,39 @@ data class RequestRetryConfigTomlFriendly(
|
||||
require(maxRetries > 0) { "maxRetries must be greater than zero. value=$maxRetries" }
|
||||
}
|
||||
timeout?.also {
|
||||
require(timeout.toKotlinDuration() > 0.milliseconds) { "timeout must be >= 1ms. value=$timeout" }
|
||||
require(timeout.toKotlinDuration() > 1.milliseconds) { "timeout must be >= 1ms. value=$timeout" }
|
||||
}
|
||||
require(backoffDelay.toMillis() > 0) { "backoffDelay must be >= 1ms. value=$backoffDelay" }
|
||||
require(failuresWarningThreshold >= 0) {
|
||||
"failuresWarningThreshold must be greater than or equal to 0. value=$failuresWarningThreshold"
|
||||
}
|
||||
}
|
||||
|
||||
internal val asDomain = RequestRetryConfig(
|
||||
internal val asJsonRpcRetryConfig = RequestRetryConfig(
|
||||
maxRetries = maxRetries?.toUInt(),
|
||||
timeout = timeout?.toKotlinDuration(),
|
||||
backoffDelay = backoffDelay.toKotlinDuration(),
|
||||
failuresWarningThreshold = failuresWarningThreshold.toUInt()
|
||||
)
|
||||
|
||||
internal val asDomain: linea.domain.RetryConfig = linea.domain.RetryConfig(
|
||||
maxRetries = maxRetries?.toUInt(),
|
||||
timeout = timeout?.toKotlinDuration(),
|
||||
backoffDelay = backoffDelay.toKotlinDuration(),
|
||||
failuresWarningThreshold = failuresWarningThreshold.toUInt()
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun endlessRetry(
|
||||
backoffDelay: Duration,
|
||||
failuresWarningThreshold: Int
|
||||
) = RequestRetryConfigTomlFriendly(
|
||||
maxRetries = null,
|
||||
timeout = null,
|
||||
backoffDelay = backoffDelay,
|
||||
failuresWarningThreshold = failuresWarningThreshold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class PersistenceRetryConfig(
|
||||
@@ -119,7 +142,7 @@ data class PersistenceRetryConfig(
|
||||
internal interface RequestRetryConfigurable {
|
||||
val requestRetry: RequestRetryConfigTomlFriendly
|
||||
val requestRetryConfig: RequestRetryConfig
|
||||
get() = requestRetry.asDomain
|
||||
get() = requestRetry.asJsonRpcRetryConfig
|
||||
}
|
||||
|
||||
data class BlobCompressionConfig(
|
||||
@@ -356,16 +379,6 @@ interface FeatureToggleable {
|
||||
get() = !disabled
|
||||
}
|
||||
|
||||
data class MessageAnchoringServiceConfig(
|
||||
val pollingInterval: Duration,
|
||||
val maxMessagesToAnchor: UInt,
|
||||
override var disabled: Boolean = false
|
||||
) : FeatureToggleable {
|
||||
init {
|
||||
require(maxMessagesToAnchor > 0u) { "maxMessagesToAnchor must be greater than 0" }
|
||||
}
|
||||
}
|
||||
|
||||
data class L1DynamicGasPriceCapServiceConfig(
|
||||
val gasPriceCapCalculation: GasPriceCapCalculation,
|
||||
val feeHistoryFetcher: FeeHistoryFetcher,
|
||||
@@ -527,7 +540,7 @@ data class CoordinatorConfigTomlDto(
|
||||
val conflation: ConflationConfig,
|
||||
val api: ApiConfig,
|
||||
val l2Signer: SignerConfig,
|
||||
val messageAnchoringService: MessageAnchoringServiceConfig,
|
||||
val messageAnchoring: MessageAnchoringConfigTomlDto = MessageAnchoringConfigTomlDto(),
|
||||
val l2NetworkGasPricing: L2NetworkGasPricingTomlDto,
|
||||
val l1DynamicGasPriceCapService: L1DynamicGasPriceCapServiceConfig,
|
||||
val testL1Disabled: Boolean = false,
|
||||
@@ -551,7 +564,10 @@ data class CoordinatorConfigTomlDto(
|
||||
conflation = conflation,
|
||||
api = api,
|
||||
l2Signer = l2Signer,
|
||||
messageAnchoringService = messageAnchoringService,
|
||||
messageAnchoring = messageAnchoring.reified(
|
||||
l1DefaultEndpoint = l1.rpcEndpoint,
|
||||
l2DefaultEndpoint = l2.rpcEndpoint
|
||||
),
|
||||
l2NetworkGasPricingService =
|
||||
if (testL1Disabled || l2NetworkGasPricing.disabled) null else l2NetworkGasPricing.reified(),
|
||||
l1DynamicGasPriceCapService = l1DynamicGasPriceCapService,
|
||||
@@ -578,7 +594,7 @@ data class CoordinatorConfig(
|
||||
val conflation: ConflationConfig,
|
||||
val api: ApiConfig,
|
||||
val l2Signer: SignerConfig,
|
||||
val messageAnchoringService: MessageAnchoringServiceConfig,
|
||||
val messageAnchoring: MessageAnchoringConfig,
|
||||
val l2NetworkGasPricingService: L2NetworkGasPricingService.Config?,
|
||||
val l1DynamicGasPriceCapService: L1DynamicGasPriceCapServiceConfig,
|
||||
val testL1Disabled: Boolean = false,
|
||||
@@ -603,7 +619,7 @@ data class CoordinatorConfig(
|
||||
}
|
||||
|
||||
if (testL1Disabled) {
|
||||
messageAnchoringService.disabled = true
|
||||
messageAnchoring.disabled = true
|
||||
l1DynamicGasPriceCapService.disabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.consensys.zkevm.coordinator.app.config
|
||||
|
||||
import linea.domain.BlockParameter
|
||||
import java.net.URL
|
||||
import java.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.toJavaDuration
|
||||
import kotlin.time.toKotlinDuration
|
||||
|
||||
data class MessageAnchoringConfigTomlDto(
|
||||
var disabled: Boolean = false,
|
||||
val l1Endpoint: URL? = null, // shall default to L1 endpoint
|
||||
val l1HighestBlockTag: BlockParameter = BlockParameter.Tag.FINALIZED,
|
||||
val l1RequestRetries: RequestRetryConfigTomlFriendly = RequestRetryConfigTomlFriendly.endlessRetry(
|
||||
backoffDelay = 1.seconds.toJavaDuration(),
|
||||
failuresWarningThreshold = 3
|
||||
),
|
||||
val l1EventPollingInterval: Duration = 12.seconds.toJavaDuration(),
|
||||
val l1EventPollingTimeout: Duration = 6.seconds.toJavaDuration(),
|
||||
val l1SuccessBackoffDelay: Duration = 1.milliseconds.toJavaDuration(), // is configurable mostly for testing purposes
|
||||
val l1EventSearchBlockChunk: Int = 1000,
|
||||
val l2Endpoint: URL? = null,
|
||||
val l2HighestBlockTag: BlockParameter = BlockParameter.Tag.LATEST,
|
||||
val l2RequestRetries: RequestRetryConfigTomlFriendly = RequestRetryConfigTomlFriendly.endlessRetry(
|
||||
backoffDelay = 1.seconds.toJavaDuration(),
|
||||
failuresWarningThreshold = 3
|
||||
),
|
||||
val anchoringTickInterval: Duration = 2.seconds.toJavaDuration(),
|
||||
val messageQueueCapacity: Int = 10_000,
|
||||
val maxMessagesToAnchorPerL2Transaction: Int = 100
|
||||
) {
|
||||
init {
|
||||
require(messageQueueCapacity > 0) {
|
||||
"messageQueueCapacity must be greater than 0"
|
||||
}
|
||||
require(maxMessagesToAnchorPerL2Transaction >= 1) {
|
||||
"maxMessagesToAnchorPerL2Transaction=$maxMessagesToAnchorPerL2Transaction be equal or greater than 1"
|
||||
}
|
||||
require(l1EventPollingInterval.toMillis() >= 1) {
|
||||
"l1EventPollingInterval=$l1EventPollingInterval must be equal or greater than 1ms"
|
||||
}
|
||||
require(l1EventPollingTimeout.toMillis() >= 1) {
|
||||
"l1EventPollingTimeout=$l1EventPollingTimeout must be equal or greater than 1ms"
|
||||
}
|
||||
require(l1SuccessBackoffDelay.toMillis() >= 1) {
|
||||
"l1SuccessBackoffDelay=$l1SuccessBackoffDelay must be equal or greater than 1ms"
|
||||
}
|
||||
require(l1EventSearchBlockChunk >= 1) {
|
||||
"l1EventSearchBlockChunk=$l1EventSearchBlockChunk must be equal or greater than 1"
|
||||
}
|
||||
require(anchoringTickInterval.toMillis() >= 1) {
|
||||
"anchoringTickInterval must be equal or greater than 1ms"
|
||||
}
|
||||
}
|
||||
|
||||
fun reified(
|
||||
l1DefaultEndpoint: URL,
|
||||
l2DefaultEndpoint: URL
|
||||
): MessageAnchoringConfig {
|
||||
return MessageAnchoringConfig(
|
||||
disabled = disabled,
|
||||
l1Endpoint = l1Endpoint ?: l1DefaultEndpoint,
|
||||
l2Endpoint = l2Endpoint ?: l2DefaultEndpoint,
|
||||
l1HighestBlockTag = l1HighestBlockTag,
|
||||
l2HighestBlockTag = l2HighestBlockTag,
|
||||
l1RequestRetryConfig = l1RequestRetries.asDomain,
|
||||
l2RequestRetryConfig = l2RequestRetries.asDomain,
|
||||
l1EventPollingInterval = l1EventPollingInterval.toKotlinDuration(),
|
||||
l1EventPollingTimeout = l1EventPollingTimeout.toKotlinDuration(),
|
||||
l1SuccessBackoffDelay = l1SuccessBackoffDelay.toKotlinDuration(),
|
||||
l1EventSearchBlockChunk = l1EventSearchBlockChunk.toUInt(),
|
||||
anchoringTickInterval = anchoringTickInterval.toKotlinDuration(),
|
||||
messageQueueCapacity = messageQueueCapacity.toUInt(),
|
||||
maxMessagesToAnchorPerL2Transaction = maxMessagesToAnchorPerL2Transaction.toUInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class MessageAnchoringConfig(
|
||||
override var disabled: Boolean,
|
||||
val l1Endpoint: URL,
|
||||
val l2Endpoint: URL,
|
||||
val l1HighestBlockTag: BlockParameter,
|
||||
val l2HighestBlockTag: BlockParameter,
|
||||
val l1RequestRetryConfig: linea.domain.RetryConfig,
|
||||
val l2RequestRetryConfig: linea.domain.RetryConfig,
|
||||
val l1EventPollingInterval: kotlin.time.Duration,
|
||||
val l1EventPollingTimeout: kotlin.time.Duration,
|
||||
val l1SuccessBackoffDelay: kotlin.time.Duration,
|
||||
val l1EventSearchBlockChunk: UInt,
|
||||
val anchoringTickInterval: kotlin.time.Duration,
|
||||
val messageQueueCapacity: UInt,
|
||||
val maxMessagesToAnchorPerL2Transaction: UInt
|
||||
) : FeatureToggleable
|
||||
@@ -23,6 +23,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.mockito.kotlin.timeout
|
||||
import java.math.BigInteger
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
@@ -30,6 +31,7 @@ import java.nio.file.Paths
|
||||
import java.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
class CoordinatorConfigTest {
|
||||
companion object {
|
||||
@@ -262,11 +264,6 @@ class CoordinatorConfigTest {
|
||||
web3j = Web3jConfig(Masked("0x4d01ae6487860981699236a58b68f807ee5f17b12df5740b85cf4c4653be0f55"))
|
||||
)
|
||||
|
||||
private val messageAnchoringServiceConfig = MessageAnchoringServiceConfig(
|
||||
pollingInterval = Duration.parse("PT1S"),
|
||||
maxMessagesToAnchor = 100U
|
||||
)
|
||||
|
||||
private val l2NetworkGasPricingRequestRetryConfig = RequestRetryConfig(
|
||||
maxRetries = 3u,
|
||||
timeout = 6.seconds,
|
||||
@@ -367,7 +364,10 @@ class CoordinatorConfigTest {
|
||||
conflation = conflationConfig,
|
||||
api = apiConfig,
|
||||
l2Signer = l2SignerConfig,
|
||||
messageAnchoringService = messageAnchoringServiceConfig,
|
||||
messageAnchoring = MessageAnchoringConfigTomlDto().reified(
|
||||
l1DefaultEndpoint = l1Config.rpcEndpoint,
|
||||
l2DefaultEndpoint = l2Config.rpcEndpoint
|
||||
),
|
||||
l2NetworkGasPricingService = l2NetworkGasPricingServiceConfig,
|
||||
l1DynamicGasPriceCapService = l1DynamicGasPriceCapServiceConfig,
|
||||
proversConfig = proversConfig
|
||||
@@ -498,6 +498,20 @@ class CoordinatorConfigTest {
|
||||
responsesDirectory = Path.of("/data/prover/v3/aggregation/responses")
|
||||
)
|
||||
)
|
||||
),
|
||||
messageAnchoring = MessageAnchoringConfigTomlDto().copy(
|
||||
l1Endpoint = URI("http://l1-endpoint-for-anchoring:8545").toURL(),
|
||||
l2Endpoint = URI("http://l2-endpoint-for-anchoring:8545").toURL(),
|
||||
l1HighestBlockTag = BlockParameter.Tag.LATEST,
|
||||
l1EventPollingInterval = 1.seconds.toJavaDuration(),
|
||||
anchoringTickInterval = 1.seconds.toJavaDuration(),
|
||||
l1RequestRetries = RequestRetryConfigTomlFriendly(
|
||||
maxRetries = 10,
|
||||
failuresWarningThreshold = 1
|
||||
)
|
||||
).reified(
|
||||
l1DefaultEndpoint = l1Config.rpcEndpoint,
|
||||
l2DefaultEndpoint = l2Config.rpcEndpoint
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package net.consensys.zkevm.coordinator.app.config
|
||||
|
||||
import com.sksamuel.hoplite.ConfigLoaderBuilder
|
||||
import com.sksamuel.hoplite.toml.TomlPropertySource
|
||||
import linea.domain.BlockParameter
|
||||
import linea.domain.RetryConfig
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.net.URI
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MessageAnchoringConfigTest {
|
||||
private val l1DefaultEndpoint = URI("http://l1-default-rpc-endpoint:8545").toURL()
|
||||
private val l2DefaultEndpoint = URI("http://l2-default-rpc-endpoint:8545").toURL()
|
||||
data class Config(
|
||||
val messageAnchoring: MessageAnchoringConfigTomlDto = MessageAnchoringConfigTomlDto()
|
||||
)
|
||||
|
||||
private fun parseConfig(toml: String): MessageAnchoringConfig {
|
||||
return ConfigLoaderBuilder
|
||||
.default()
|
||||
.addDecoder(BlockParameterDecoder())
|
||||
.addSource(TomlPropertySource(toml))
|
||||
.build()
|
||||
.loadConfigOrThrow<Config>()
|
||||
.let {
|
||||
it.messageAnchoring.reified(
|
||||
l1DefaultEndpoint = l1DefaultEndpoint,
|
||||
l2DefaultEndpoint = l2DefaultEndpoint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should parse message anchoroing full config`() {
|
||||
val toml = """
|
||||
[message-anchoring]
|
||||
disabled = true
|
||||
l1-endpoint = "http://l1-rpc-endpoint:8545"
|
||||
l2-endpoint = "http://l2-rpc-endpoint:8545"
|
||||
l1-highest-block-tag="FINALIZED"
|
||||
l2-highest-block-tag="LATEST"
|
||||
l1-event-polling-interval="PT30S"
|
||||
l1-event-polling-timeout="PT6S"
|
||||
l1-success-backoff-delay="PT0.1s"
|
||||
l1-event-search-block-chunk=123
|
||||
message-anchoring-chunck-size=123
|
||||
anchoring-tick-interval="PT3S"
|
||||
message-queue-capacity=321
|
||||
maxMessagesToAnchorPerL2Transaction=54
|
||||
|
||||
[message-anchoring.l1-request-retries]
|
||||
max-retries = 10
|
||||
timeout = "PT100S"
|
||||
backoff-delay = "PT11S"
|
||||
failures-warning-threshold = 1
|
||||
[message-anchoring.l2-request-retries]
|
||||
max-retries = 20
|
||||
timeout = "PT200S"
|
||||
backoff-delay = "PT21S"
|
||||
failures-warning-threshold = 2
|
||||
""".trimIndent()
|
||||
|
||||
assertThat(parseConfig(toml))
|
||||
.isEqualTo(
|
||||
MessageAnchoringConfig(
|
||||
disabled = true,
|
||||
l1Endpoint = URI("http://l1-rpc-endpoint:8545").toURL(),
|
||||
l2Endpoint = URI("http://l2-rpc-endpoint:8545").toURL(),
|
||||
l1HighestBlockTag = BlockParameter.Tag.FINALIZED,
|
||||
l2HighestBlockTag = BlockParameter.Tag.LATEST,
|
||||
l1RequestRetryConfig = RetryConfig(
|
||||
maxRetries = 10u,
|
||||
timeout = 100.seconds,
|
||||
backoffDelay = 11.seconds,
|
||||
failuresWarningThreshold = 1u
|
||||
),
|
||||
l2RequestRetryConfig = RetryConfig(
|
||||
maxRetries = 20u,
|
||||
timeout = 200.seconds,
|
||||
backoffDelay = 21.seconds,
|
||||
failuresWarningThreshold = 2u
|
||||
),
|
||||
l1EventPollingInterval = 30.seconds,
|
||||
l1EventPollingTimeout = 6.seconds,
|
||||
l1SuccessBackoffDelay = 100.milliseconds,
|
||||
l1EventSearchBlockChunk = 123u,
|
||||
anchoringTickInterval = 3.seconds,
|
||||
messageQueueCapacity = 321u,
|
||||
maxMessagesToAnchorPerL2Transaction = 54u
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should parse message anchoroing with defaults`() {
|
||||
val toml = """
|
||||
# Nothing configured to return defaults
|
||||
""".trimIndent()
|
||||
|
||||
assertThat(parseConfig(toml))
|
||||
.isEqualTo(
|
||||
MessageAnchoringConfig(
|
||||
disabled = false,
|
||||
l1Endpoint = URI("http://l1-default-rpc-endpoint:8545").toURL(),
|
||||
l2Endpoint = URI("http://l2-default-rpc-endpoint:8545").toURL(),
|
||||
l1HighestBlockTag = BlockParameter.Tag.FINALIZED,
|
||||
l2HighestBlockTag = BlockParameter.Tag.LATEST,
|
||||
l1RequestRetryConfig = RetryConfig(
|
||||
maxRetries = null,
|
||||
timeout = null,
|
||||
backoffDelay = 1.seconds,
|
||||
failuresWarningThreshold = 3u
|
||||
),
|
||||
l2RequestRetryConfig = RetryConfig(
|
||||
maxRetries = null,
|
||||
timeout = null,
|
||||
backoffDelay = 1.seconds,
|
||||
failuresWarningThreshold = 3u
|
||||
),
|
||||
l1EventPollingInterval = 12.seconds,
|
||||
l1EventPollingTimeout = 6.seconds,
|
||||
l1SuccessBackoffDelay = 1.milliseconds,
|
||||
l1EventSearchBlockChunk = 1000u,
|
||||
anchoringTickInterval = 2.seconds,
|
||||
messageQueueCapacity = 10_000u,
|
||||
maxMessagesToAnchorPerL2Transaction = 100u
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -35,3 +35,14 @@ plain-transfer-cost-multiplier=1.0
|
||||
# Meaning 99.7% of transactions will be includable if priced using eth_gasPrice
|
||||
compressed-tx-size=350
|
||||
expected-gas=29400
|
||||
|
||||
[message-anchoring]
|
||||
disabled = false
|
||||
l1-endpoint = "http://l1-endpoint-for-anchoring:8545"
|
||||
l2-endpoint = "http://l2-endpoint-for-anchoring:8545"
|
||||
l1-highest-block-tag="LATEST"
|
||||
l1-event-polling-interval="PT1S"
|
||||
anchoring-tick-interval = "PT1S"
|
||||
[message-anchoring.l1-request-retries]
|
||||
max-retries = 10
|
||||
failures-warning-threshold = 1
|
||||
|
||||
@@ -198,11 +198,6 @@ max-pool-size=10
|
||||
keep-alive=true
|
||||
public-key="4a788ad6fa008beed58de6418369717d7492f37d173d70e2c26d9737e2c6eeae929452ef8602a19410844db3e200a0e73f5208fd76259a8766b73953fc3e7023"
|
||||
|
||||
[message-anchoring-service]
|
||||
disabled=false
|
||||
polling-interval="PT1S"
|
||||
max-messages-to-anchor=100
|
||||
|
||||
[l2-network-gas-pricing]
|
||||
disabled = false
|
||||
price-update-interval = "PT12S"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package net.consensys.linea.contract
|
||||
|
||||
import linea.web3j.padBlobForEip4844Submission
|
||||
import org.web3j.crypto.Blob
|
||||
import org.web3j.protocol.core.Response
|
||||
import org.web3j.protocol.exceptions.TransactionException
|
||||
|
||||
internal fun <T> throwExceptionIfJsonRpcErrorReturned(rpcMethod: String, response: Response<T>) {
|
||||
if (response.hasError()) {
|
||||
val rpcError = response.error
|
||||
var errorMessage =
|
||||
"$rpcMethod failed with JsonRpcError: code=${rpcError.code} message=${rpcError.message}"
|
||||
if (rpcError.data != null) {
|
||||
errorMessage += " data=${rpcError.data}"
|
||||
}
|
||||
|
||||
throw TransactionException(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<ByteArray>.toWeb3JTxBlob(): List<Blob> {
|
||||
return this.map { Blob(padBlobForEip4844Submission(it)) }
|
||||
}
|
||||
@@ -2,16 +2,15 @@ package net.consensys.linea.contract.l1
|
||||
|
||||
import build.linea.contract.LineaRollupV6
|
||||
import linea.contract.l1.Web3JLineaRollupSmartContractClientReadOnly
|
||||
import linea.domain.gas.GasPriceCaps
|
||||
import linea.kotlin.toULong
|
||||
import linea.web3j.SmartContractErrors
|
||||
import linea.web3j.transactionmanager.AsyncFriendlyTransactionManager
|
||||
import net.consensys.linea.contract.Web3JContractAsyncHelper
|
||||
import net.consensys.linea.contract.throwExceptionIfJsonRpcErrorReturned
|
||||
import net.consensys.zkevm.coordinator.clients.smartcontract.BlockAndNonce
|
||||
import net.consensys.zkevm.coordinator.clients.smartcontract.LineaRollupSmartContractClient
|
||||
import net.consensys.zkevm.domain.BlobRecord
|
||||
import net.consensys.zkevm.domain.ProofToFinalize
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCaps
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import org.web3j.crypto.Credentials
|
||||
@@ -133,7 +132,11 @@ class Web3JLineaRollupSmartContractClient internal constructor(
|
||||
return getVersion()
|
||||
.thenCompose { version ->
|
||||
val function = buildSubmitBlobsFunction(version, blobs)
|
||||
web3jContractHelper.executeBlobEthCall(function, blobs, gasPriceCaps)
|
||||
web3jContractHelper.executeBlobEthCall(
|
||||
function = function,
|
||||
blobs = blobs.map { it.blobCompressionProof!!.compressedData },
|
||||
gasPriceCaps = gasPriceCaps
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,11 +156,9 @@ class Web3JLineaRollupSmartContractClient internal constructor(
|
||||
parentL1RollingHash,
|
||||
parentL1RollingHashMessageNumber
|
||||
)
|
||||
web3jContractHelper.sendTransactionAsync(function, BigInteger.ZERO, gasPriceCaps)
|
||||
.thenApply { result ->
|
||||
throwExceptionIfJsonRpcErrorReturned("eth_sendRawTransaction", result)
|
||||
result.transactionHash
|
||||
}
|
||||
web3jContractHelper
|
||||
.sendTransactionAsync(function, BigInteger.ZERO, gasPriceCaps)
|
||||
.thenApply { result -> result.transactionHash }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,10 +198,7 @@ class Web3JLineaRollupSmartContractClient internal constructor(
|
||||
parentL1RollingHashMessageNumber
|
||||
)
|
||||
web3jContractHelper.sendTransactionAfterEthCallAsync(function, BigInteger.ZERO, gasPriceCaps)
|
||||
.thenApply { result ->
|
||||
throwExceptionIfJsonRpcErrorReturned("eth_sendRawTransaction", result)
|
||||
result.transactionHash
|
||||
}
|
||||
.thenApply { result -> result.transactionHash }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.consensys.zkevm.coordinator.clients.smartcontract
|
||||
|
||||
import linea.contract.l1.LineaRollupSmartContractClientReadOnly
|
||||
import linea.domain.gas.GasPriceCaps
|
||||
import net.consensys.zkevm.domain.BlobRecord
|
||||
import net.consensys.zkevm.domain.ProofToFinalize
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCaps
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
|
||||
data class BlockAndNonce(
|
||||
|
||||
@@ -1,26 +1,8 @@
|
||||
package net.consensys.zkevm.ethereum.gaspricing
|
||||
|
||||
import linea.kotlin.toGWei
|
||||
import linea.domain.gas.GasPriceCaps
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
|
||||
data class GasPriceCaps(
|
||||
val maxPriorityFeePerGasCap: ULong,
|
||||
val maxFeePerGasCap: ULong,
|
||||
val maxFeePerBlobGasCap: ULong,
|
||||
val maxBaseFeePerGasCap: ULong? = null
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "maxPriorityFeePerGasCap=${maxPriorityFeePerGasCap.toGWei()} GWei," +
|
||||
if (maxBaseFeePerGasCap != null) {
|
||||
" maxBaseFeePerGasCap=${maxBaseFeePerGasCap.toGWei()} GWei,"
|
||||
} else {
|
||||
""
|
||||
} +
|
||||
" maxFeePerGasCap=${maxFeePerGasCap.toGWei()} GWei," +
|
||||
" maxFeePerBlobGasCap=${maxFeePerBlobGasCap.toGWei()} GWei"
|
||||
}
|
||||
}
|
||||
|
||||
interface GasPriceCapProvider {
|
||||
fun getGasPriceCaps(targetL2BlockNumber: Long): SafeFuture<GasPriceCaps?>
|
||||
fun getGasPriceCapsWithCoefficient(targetL2BlockNumber: Long): SafeFuture<GasPriceCaps?>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.consensys.linea.ethereum.gaspricing.dynamiccap
|
||||
|
||||
import linea.domain.gas.GasPriceCaps
|
||||
import net.consensys.linea.metrics.LineaMetricsCategory
|
||||
import net.consensys.linea.metrics.MetricsFacade
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCapProvider
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCaps
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.consensys.linea.ethereum.gaspricing.dynamiccap
|
||||
|
||||
import linea.domain.gas.GasPriceCaps
|
||||
import net.consensys.linea.metrics.LineaMetricsCategory
|
||||
import net.consensys.linea.metrics.MetricsFacade
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCapProvider
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCaps
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import linea.domain.gas.GasPriceCaps
|
||||
import linea.kotlin.toGWei
|
||||
import linea.kotlin.toULong
|
||||
import linea.web3j.ExtendedWeb3J
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCapProvider
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCaps
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.consensys.linea.ethereum.gaspricing.dynamiccap
|
||||
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import linea.domain.gas.GasPriceCaps
|
||||
import net.consensys.linea.metrics.MetricsFacade
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCapProvider
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCaps
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package net.consensys.linea.ethereum.gaspricing.dynamiccap
|
||||
|
||||
import linea.domain.gas.GasPriceCaps
|
||||
import net.consensys.linea.metrics.MetricsFacade
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCapProvider
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCaps
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -3,8 +3,8 @@ package net.consensys.linea.ethereum.gaspricing.dynamiccap
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import linea.domain.gas.GasPriceCaps
|
||||
import linea.web3j.ExtendedWeb3J
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCaps
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package net.consensys.linea.ethereum.gaspricing
|
||||
|
||||
import linea.domain.gas.GasPriceCaps
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCapProvider
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCaps
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
|
||||
val defaultGasPriceCaps = GasPriceCaps(
|
||||
|
||||
@@ -3,7 +3,7 @@ package linea.anchoring
|
||||
import io.vertx.core.Vertx
|
||||
import linea.EthLogsSearcher
|
||||
import linea.anchoring.clients.L1MessageSentEventsPoller
|
||||
import linea.anchoring.events.MessageSentEvent
|
||||
import linea.contract.events.MessageSentEvent
|
||||
import linea.contract.l2.L2MessageServiceSmartContractClient
|
||||
import linea.domain.BlockParameter
|
||||
import linea.domain.RetryConfig
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package linea.anchoring
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import linea.anchoring.events.L1RollingHashUpdatedEvent
|
||||
import linea.anchoring.events.MessageSentEvent
|
||||
import linea.contract.events.L1RollingHashUpdatedEvent
|
||||
import linea.contract.events.MessageSentEvent
|
||||
import linea.contract.l2.L2MessageServiceSmartContractClient
|
||||
import linea.domain.BlockParameter
|
||||
import linea.domain.CommonDomainFunctions
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package linea.anchoring.clients
|
||||
|
||||
import linea.EthLogsSearcher
|
||||
import linea.anchoring.events.L1RollingHashUpdatedEvent
|
||||
import linea.anchoring.events.MessageSentEvent
|
||||
import linea.contract.events.L1RollingHashUpdatedEvent
|
||||
import linea.contract.events.MessageSentEvent
|
||||
import linea.domain.BlockParameter
|
||||
import linea.domain.BlockParameter.Companion.toBlockParameter
|
||||
import linea.domain.CommonDomainFunctions
|
||||
|
||||
@@ -2,7 +2,7 @@ package linea.anchoring.clients
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import linea.EthLogsSearcher
|
||||
import linea.anchoring.events.MessageSentEvent
|
||||
import linea.contract.events.MessageSentEvent
|
||||
import linea.contract.l2.L2MessageServiceSmartContractClient
|
||||
import linea.domain.BlockParameter
|
||||
import net.consensys.zkevm.PeriodicPollingService
|
||||
|
||||
@@ -2,10 +2,11 @@ package linea.anchoring
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import linea.anchoring.events.L1RollingHashUpdatedEvent
|
||||
import linea.anchoring.events.L2RollingHashUpdatedEvent
|
||||
import linea.anchoring.events.MessageSentEvent
|
||||
import linea.anchoring.fakes.FakeL2MessageService
|
||||
import linea.contract.events.L1RollingHashUpdatedEvent
|
||||
import linea.contract.events.L2RollingHashUpdatedEvent
|
||||
import linea.contract.events.MessageSentEvent
|
||||
import linea.contrat.events.L1MessageSentV1EthLogs
|
||||
import linea.domain.BlockParameter
|
||||
import linea.domain.RetryConfig
|
||||
import linea.ethapi.FakeEthApiClient
|
||||
@@ -255,7 +256,7 @@ class MessageAnchoringAppTest {
|
||||
l1BlocksWithMessages.forEach { blockNumber ->
|
||||
repeat(numberOfMessagesPerBlock) {
|
||||
ethLogs.add(
|
||||
createL1MessageSentV1Logs(
|
||||
linea.contrat.events.createL1MessageSentV1Logs(
|
||||
blockNumber = blockNumber,
|
||||
contractAddress = L1_CONTRACT_ADDRESS,
|
||||
messageNumber = messageNumber,
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
plugins {
|
||||
id 'net.consensys.zkevm.kotlin-library-conventions'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':jvm-libs:generic:extensions:futures')
|
||||
implementation project(':coordinator:core')
|
||||
implementation project(':coordinator:clients:smart-contract-client')
|
||||
implementation project(':coordinator:ethereum:common')
|
||||
|
||||
testImplementation project(':coordinator:ethereum:test-utils')
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output
|
||||
runtimeClasspath += main.output
|
||||
}
|
||||
java {
|
||||
compileClasspath += main.output
|
||||
runtimeClasspath += main.output
|
||||
}
|
||||
compileClasspath += sourceSets.main.output + sourceSets.main.compileClasspath + sourceSets.test.compileClasspath
|
||||
runtimeClasspath += sourceSets.main.output + sourceSets.main.runtimeClasspath + sourceSets.test.runtimeClasspath
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) { test ->
|
||||
description = "Runs integration tests."
|
||||
group = "verification"
|
||||
useJUnitPlatform()
|
||||
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
dependsOn(":localStackComposeUp")
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import java.math.BigInteger
|
||||
|
||||
fun createRandomSendMessageEvents(numberOfRandomHashes: ULong): List<SendMessageEvent> {
|
||||
return (0UL..numberOfRandomHashes)
|
||||
.map { n ->
|
||||
SendMessageEvent(
|
||||
Bytes32.random(),
|
||||
messageNumber = n + 1UL,
|
||||
blockNumber = n + 1UL
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class L1MessageToSend(
|
||||
val recipient: String,
|
||||
val fee: BigInteger,
|
||||
val calldata: ByteArray,
|
||||
val value: BigInteger
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as L1MessageToSend
|
||||
|
||||
if (recipient != other.recipient) return false
|
||||
if (fee != other.fee) return false
|
||||
if (!calldata.contentEquals(other.calldata)) return false
|
||||
return value == other.value
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = recipient.hashCode()
|
||||
result = 31 * result + fee.hashCode()
|
||||
result = 31 * result + calldata.contentHashCode()
|
||||
result = 31 * result + value.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import build.linea.contract.LineaRollupV6
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.junit5.Timeout
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import io.vertx.junit5.VertxTestContext
|
||||
import linea.contract.l1.LineaContractVersion
|
||||
import linea.kotlin.toBigInteger
|
||||
import linea.kotlin.toULong
|
||||
import net.consensys.linea.contract.LineaRollupAsyncFriendly
|
||||
import net.consensys.zkevm.ethereum.ContractsManager
|
||||
import net.consensys.zkevm.ethereum.Web3jClientManager
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.web3j.abi.EventEncoder
|
||||
import org.web3j.protocol.Web3j
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class L1EventQuerierIntegrationTest {
|
||||
private lateinit var testLineaRollupContractAddress: String
|
||||
private val l2RecipientAddress = "0x03dfa322A95039BB679771346Ee2dBfEa0e2B773"
|
||||
private val blockRangeLoopLimit = 5u
|
||||
private val maxMessagesToCollect = 100u
|
||||
private lateinit var web3Client: Web3j
|
||||
private lateinit var contract: LineaRollupAsyncFriendly
|
||||
private var l1ContractDeploymentBlockNumber: ULong = 0u
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
val deploymentResult = ContractsManager.get()
|
||||
.deployLineaRollup(contractVersion = LineaContractVersion.V6)
|
||||
.get()
|
||||
testLineaRollupContractAddress = deploymentResult.contractAddress
|
||||
web3Client = Web3jClientManager.l1Client
|
||||
@Suppress("DEPRECATION")
|
||||
contract = deploymentResult.rollupOperatorClientLegacy
|
||||
l1ContractDeploymentBlockNumber = deploymentResult.contractDeploymentBlockNumber.toULong()
|
||||
}
|
||||
|
||||
private fun createL1EventQuerier(
|
||||
vertx: Vertx,
|
||||
blockRangeLoopLimit: UInt
|
||||
): L1EventQuerierImpl {
|
||||
return L1EventQuerierImpl(
|
||||
vertx,
|
||||
L1EventQuerierImpl.Config(
|
||||
pollingInterval = 200.milliseconds,
|
||||
maxEventScrapingTime = 2.seconds,
|
||||
earliestL1Block = l1ContractDeploymentBlockNumber.toBigInteger(),
|
||||
maxMessagesToCollect = maxMessagesToCollect,
|
||||
l1MessageServiceAddress = testLineaRollupContractAddress,
|
||||
finalized = "latest",
|
||||
blockRangeLoopLimit = blockRangeLoopLimit
|
||||
),
|
||||
web3Client
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(45, timeUnit = TimeUnit.SECONDS)
|
||||
fun `l1Event querier returns events from the given hash`(vertx: Vertx, testContext: VertxTestContext) {
|
||||
val baseMessageToSend =
|
||||
L1MessageToSend(l2RecipientAddress, BigInteger.ZERO, ByteArray(0), BigInteger.valueOf(100001))
|
||||
val messagesToSend = listOf(
|
||||
baseMessageToSend,
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(100001)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(100002)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(100003)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(100005))
|
||||
)
|
||||
|
||||
val earlierEmittedParallelEvents = messagesToSend.map {
|
||||
contract.sendMessage(it.recipient, it.fee, it.calldata, it.value).sendAsync()
|
||||
.thenApply { receipt ->
|
||||
Pair(
|
||||
LineaRollupV6.getMessageSentEventFromLog(
|
||||
receipt.logs.first { log ->
|
||||
log.topics.contains(EventEncoder.encode(LineaRollupV6.MESSAGESENT_EVENT))
|
||||
}
|
||||
),
|
||||
receipt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val earlierEvents = earlierEmittedParallelEvents.map { it.get() }
|
||||
|
||||
val hashIndexToQueryFrom = 2
|
||||
val hashInTheMiddle = earlierEvents[hashIndexToQueryFrom].let {
|
||||
Bytes32.wrap(it.first._messageHash)
|
||||
}
|
||||
|
||||
val laterEmittedEvents = messagesToSend.map {
|
||||
contract.sendMessage(it.recipient, it.fee, it.calldata, it.value).sendAsync()
|
||||
.thenApply { receipt ->
|
||||
Pair(
|
||||
LineaRollupV6.getMessageSentEventFromLog(
|
||||
receipt.logs.first { log ->
|
||||
log.topics.contains(EventEncoder.encode(LineaRollupV6.MESSAGESENT_EVENT))
|
||||
}
|
||||
),
|
||||
receipt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val laterEvents = laterEmittedEvents.map { it.get() }
|
||||
val multiPostBlockLoopLimit = 20u
|
||||
val l1QuerierImpl = createL1EventQuerier(vertx, multiPostBlockLoopLimit)
|
||||
|
||||
// we should have events 4 and 5 (count = 5, less hashIndexToQueryFrom, less 1 (0 based))
|
||||
val allExpectedHashesInOrder: MutableList<SendMessageEvent> =
|
||||
earlierEvents.drop(hashIndexToQueryFrom + 1).take(maxMessagesToCollect.toInt()).map {
|
||||
SendMessageEvent(
|
||||
Bytes32.wrap(it.first._messageHash),
|
||||
it.first._nonce.toULong(),
|
||||
it.first.log.blockNumber.toULong()
|
||||
|
||||
)
|
||||
}.toMutableList()
|
||||
|
||||
l1QuerierImpl.getSendMessageEventsForAnchoredMessage(MessageHashAnchoredEvent(hashInTheMiddle)).thenApply {
|
||||
// we should have events 1,2,3,4 and 5
|
||||
val expectedHashes = laterEvents.map {
|
||||
SendMessageEvent(
|
||||
Bytes32.wrap(it.first._messageHash),
|
||||
it.first._nonce.toULong(),
|
||||
it.first.log.blockNumber.toULong()
|
||||
)
|
||||
}.toMutableList()
|
||||
|
||||
// we should have all 7
|
||||
allExpectedHashesInOrder.addAll(expectedHashes)
|
||||
|
||||
testContext.verify {
|
||||
assertThat(it).isEqualTo(allExpectedHashesInOrder)
|
||||
}.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Disabled
|
||||
@Timeout(1, timeUnit = TimeUnit.MINUTES)
|
||||
fun `l1Event querier returns events with more than 10000 messages`(vertx: Vertx, testContext: VertxTestContext) {
|
||||
val baseMessageToSend =
|
||||
L1MessageToSend(l2RecipientAddress, BigInteger.ZERO, ByteArray(0), BigInteger.valueOf(100001))
|
||||
|
||||
val messagesToSend = (1..10025).map { baseMessageToSend.copy(value = BigInteger.valueOf(100001)) }
|
||||
|
||||
val earlierEmittedParallelEvents = messagesToSend.map {
|
||||
contract.sendMessage(it.recipient, it.fee, it.calldata, it.value).sendAsync()
|
||||
.thenApply { receipt ->
|
||||
Pair(
|
||||
LineaRollupV6.staticExtractEventParameters(
|
||||
LineaRollupV6.MESSAGESENT_EVENT,
|
||||
receipt.logs.first { log ->
|
||||
log.topics.contains(EventEncoder.encode(LineaRollupV6.MESSAGESENT_EVENT))
|
||||
}
|
||||
),
|
||||
receipt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val events = earlierEmittedParallelEvents.map { it.get() }
|
||||
|
||||
val hashIndexToQueryFrom = 2
|
||||
val hashToTake = events[hashIndexToQueryFrom].let {
|
||||
Bytes32.wrap(it.first.indexedValues[2].value as ByteArray)
|
||||
}
|
||||
|
||||
val l1QuerierImpl = createL1EventQuerier(vertx, blockRangeLoopLimit)
|
||||
|
||||
val allExpectedHashesInOrder =
|
||||
events.drop(hashIndexToQueryFrom + 1).take(maxMessagesToCollect.toInt()).map {
|
||||
Bytes32.wrap(it.first.indexedValues[2].value as ByteArray)
|
||||
}
|
||||
|
||||
l1QuerierImpl.getSendMessageEventsForAnchoredMessage(MessageHashAnchoredEvent(hashToTake)).thenApply {
|
||||
val foundHashes = it.map { evt ->
|
||||
evt.messageHash
|
||||
}
|
||||
|
||||
// we should have all the hashes meeting the maxMessagesToCollect
|
||||
testContext.verify {
|
||||
assertThat(it.count()).isEqualTo(maxMessagesToCollect)
|
||||
assertThat(foundHashes).isEqualTo(allExpectedHashesInOrder)
|
||||
}.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.junit5.Timeout
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import io.vertx.junit5.VertxTestContext
|
||||
import linea.domain.BlockParameter
|
||||
import linea.kotlin.toBigInteger
|
||||
import linea.web3j.gas.EIP1559GasProvider
|
||||
import linea.web3j.transactionmanager.AsyncFriendlyTransactionManager
|
||||
import net.consensys.linea.async.toSafeFuture
|
||||
import net.consensys.linea.contract.L2MessageService
|
||||
import net.consensys.linea.contract.LineaRollupAsyncFriendly
|
||||
import net.consensys.zkevm.coordinator.clients.smartcontract.LineaRollupSmartContractClient
|
||||
import net.consensys.zkevm.ethereum.ContractsManager
|
||||
import net.consensys.zkevm.ethereum.Web3jClientManager
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.web3j.protocol.Web3j
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class L2MessageAnchorerIntegrationTest {
|
||||
private lateinit var testLineaRollupContractAddress: String
|
||||
private val l2RecipientAddress = "0x03dfa322A95039BB679771346Ee2dBfEa0e2B773"
|
||||
private val blockRangeLoopLimit = 100u
|
||||
|
||||
private val gasLimit = 2_500_000uL
|
||||
private val feeHistoryBlockCount = 4u
|
||||
private val feeHistoryRewardPercentile = 15.0
|
||||
private val maxFeePerGasCap = 10000uL
|
||||
private lateinit var l1Web3jClient: Web3j
|
||||
private lateinit var l2Web3jClient: Web3j
|
||||
private lateinit var l2TransactionManager: AsyncFriendlyTransactionManager
|
||||
private lateinit var l2Contract: L2MessageService
|
||||
private lateinit var l1ContractLegacyClient: LineaRollupAsyncFriendly
|
||||
private lateinit var l1ContractClient: LineaRollupSmartContractClient
|
||||
|
||||
private val messageAnchorerConfig = L2MessageAnchorerImpl.Config(
|
||||
receiptPollingInterval = 200.milliseconds,
|
||||
maxReceiptRetries = 100u,
|
||||
blocksToFinalisation = 0
|
||||
)
|
||||
private lateinit var l2MessageAnchorer: L2MessageAnchorerImpl
|
||||
private var l1ContractDeploymentBlockNumber: ULong = 0u
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach(
|
||||
vertx: Vertx
|
||||
) {
|
||||
val deploymentResult = ContractsManager.get().deployRollupAndL2MessageService().get()
|
||||
testLineaRollupContractAddress = deploymentResult.lineaRollup.contractAddress
|
||||
l1ContractDeploymentBlockNumber = deploymentResult.lineaRollup.contractDeploymentBlockNumber
|
||||
l1Web3jClient = Web3jClientManager.l1Client
|
||||
l2Web3jClient = Web3jClientManager.l2Client
|
||||
l2TransactionManager = deploymentResult.l2MessageService.anchorerOperator.txManager
|
||||
@Suppress("DEPRECATION")
|
||||
l1ContractLegacyClient = deploymentResult.lineaRollup.rollupOperatorClientLegacy
|
||||
l1ContractClient = deploymentResult.lineaRollup.rollupOperatorClient
|
||||
|
||||
val eip1559GasProvider = EIP1559GasProvider(
|
||||
l2Web3jClient,
|
||||
EIP1559GasProvider.Config(
|
||||
gasLimit = gasLimit,
|
||||
maxFeePerGasCap = maxFeePerGasCap,
|
||||
feeHistoryBlockCount = feeHistoryBlockCount,
|
||||
feeHistoryRewardPercentile = feeHistoryRewardPercentile
|
||||
)
|
||||
)
|
||||
|
||||
l2Contract = ContractsManager.get().connectL2MessageService(
|
||||
contractAddress = deploymentResult.l2MessageService.contractAddress,
|
||||
web3jClient = l2Web3jClient,
|
||||
transactionManager = deploymentResult.l2MessageService.anchorerOperator.txManager,
|
||||
gasProvider = eip1559GasProvider,
|
||||
smartContractErrors = mapOf("3b174434" to "MessageHashesListLengthHigherThanOneHundred")
|
||||
)
|
||||
|
||||
l2MessageAnchorer = L2MessageAnchorerImpl(
|
||||
vertx,
|
||||
l2Web3jClient,
|
||||
l2Contract,
|
||||
messageAnchorerConfig
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(1, timeUnit = TimeUnit.MINUTES)
|
||||
fun `all hashes found are anchored`(vertx: Vertx, testContext: VertxTestContext) {
|
||||
val baseMessageToSend =
|
||||
L1MessageToSend(
|
||||
l2RecipientAddress,
|
||||
BigInteger.TEN,
|
||||
ByteArray(0),
|
||||
BigInteger.valueOf(100001)
|
||||
)
|
||||
val messagesToSend = listOf(
|
||||
baseMessageToSend,
|
||||
baseMessageToSend.copy(fee = BigInteger.valueOf(11)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(100001))
|
||||
)
|
||||
SafeFuture.collectAll(
|
||||
messagesToSend.map { message ->
|
||||
l1ContractLegacyClient
|
||||
.sendMessage(message.recipient, message.fee, message.calldata, message.value).sendAsync()
|
||||
.toSafeFuture()
|
||||
}.stream()
|
||||
).get()
|
||||
|
||||
val l1QuerierImpl = L1EventQuerierImpl(
|
||||
vertx,
|
||||
L1EventQuerierImpl.Config(
|
||||
pollingInterval = 200.milliseconds,
|
||||
maxEventScrapingTime = 2.seconds,
|
||||
earliestL1Block = l1ContractDeploymentBlockNumber.toBigInteger(),
|
||||
maxMessagesToCollect = 100u,
|
||||
l1MessageServiceAddress = testLineaRollupContractAddress,
|
||||
finalized = "latest",
|
||||
blockRangeLoopLimit = blockRangeLoopLimit
|
||||
),
|
||||
l1Web3jClient = l1Web3jClient
|
||||
)
|
||||
|
||||
l1QuerierImpl.getSendMessageEventsForAnchoredMessage(messageHash = null)
|
||||
.thenApply { events ->
|
||||
val rollingHash = l1ContractClient.getMessageRollingHash(
|
||||
blockParameter = BlockParameter.Tag.LATEST,
|
||||
messageNumber = events.last().messageNumber.toLong()
|
||||
).get()
|
||||
l2MessageAnchorer.anchorMessages(events, rollingHash)
|
||||
.thenPeek {
|
||||
testContext.verify {
|
||||
val expectedLastAnchoredMessageNumber = events.last().messageNumber.toBigInteger()
|
||||
assertThat(l2Contract.lastAnchoredL1MessageNumber().send()).isEqualTo(expectedLastAnchoredMessageNumber)
|
||||
assertThat(l2Contract.l1RollingHashes(expectedLastAnchoredMessageNumber).send())
|
||||
.isEqualTo(rollingHash)
|
||||
}.completeNow()
|
||||
}
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(20, timeUnit = TimeUnit.SECONDS)
|
||||
fun `anchor messages gas estimation returns informative error`() {
|
||||
val exception = assertThrows<ExecutionException> {
|
||||
l2MessageAnchorer.anchorMessages(createRandomSendMessageEvents(101UL), Bytes32.random().toArray()).get()
|
||||
}
|
||||
assertThat(exception.message).contains(
|
||||
"3b174434",
|
||||
"MessageHashesListLengthHigherThanOneHundred",
|
||||
"Execution reverted"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import build.linea.contract.LineaRollupV6
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.junit5.Timeout
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import linea.contract.l1.LineaContractVersion
|
||||
import linea.kotlin.toBigInteger
|
||||
import linea.kotlin.toULong
|
||||
import linea.web3j.transactionmanager.AsyncFriendlyTransactionManager
|
||||
import net.consensys.linea.async.toSafeFuture
|
||||
import net.consensys.linea.contract.L2MessageService
|
||||
import net.consensys.linea.contract.LineaRollupAsyncFriendly
|
||||
import net.consensys.zkevm.coordinator.clients.smartcontract.LineaRollupSmartContractClient
|
||||
import net.consensys.zkevm.ethereum.ContractsManager
|
||||
import net.consensys.zkevm.ethereum.Web3jClientManager
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.awaitility.Awaitility.await
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.web3j.abi.EventEncoder
|
||||
import org.web3j.tx.gas.DefaultGasProvider
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class MessageServiceIntegrationTest {
|
||||
private val log: Logger = LogManager.getLogger(this::class.java)
|
||||
|
||||
private val l2RecipientAddress = "0x03dfa322A95039BB679771346Ee2dBfEa0e2B773"
|
||||
private val l1Web3Client = Web3jClientManager.l1Client
|
||||
private val l2Web3jClient = Web3jClientManager.l2Client
|
||||
private lateinit var l2TransactionManager: AsyncFriendlyTransactionManager
|
||||
|
||||
private val messagePollingInterval = 200.milliseconds
|
||||
private val maxScrapingTime = 2.seconds
|
||||
private val maxMessagesToAnchor = 5u
|
||||
private val blockRangeLoopLimit = 100u
|
||||
private val receiptPollingInterval = 500.milliseconds
|
||||
|
||||
private lateinit var l1ContractLegacyClient: LineaRollupAsyncFriendly
|
||||
private lateinit var l1ContractClient: LineaRollupSmartContractClient
|
||||
private lateinit var l2Contract: L2MessageService
|
||||
|
||||
private fun deployContracts() {
|
||||
val l1RollupDeploymentResult = ContractsManager.get()
|
||||
.deployLineaRollup(contractVersion = LineaContractVersion.V6)
|
||||
.get()
|
||||
@Suppress("DEPRECATION")
|
||||
l1ContractLegacyClient = l1RollupDeploymentResult.rollupOperatorClientLegacy
|
||||
l1ContractClient = l1RollupDeploymentResult.rollupOperatorClient
|
||||
val l2MessageServiceDeploymentResult = ContractsManager.get().deployL2MessageService().get()
|
||||
l2Contract = ContractsManager.get().connectL2MessageService(
|
||||
contractAddress = l2MessageServiceDeploymentResult.contractAddress,
|
||||
transactionManager = l2MessageServiceDeploymentResult.anchorerOperator.txManager
|
||||
)
|
||||
l2TransactionManager = l2MessageServiceDeploymentResult.anchorerOperator.txManager
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(90, timeUnit = TimeUnit.SECONDS)
|
||||
fun `test anchoring with RollingHash`(vertx: Vertx) {
|
||||
deployContracts()
|
||||
testAnchoredHashesAreReturnedCorrectly(
|
||||
l1ContractLegacyClient,
|
||||
l2Contract,
|
||||
vertx
|
||||
)
|
||||
}
|
||||
|
||||
fun testAnchoredHashesAreReturnedCorrectly(
|
||||
l1Contract: LineaRollupAsyncFriendly,
|
||||
l2Contract: L2MessageService,
|
||||
vertx: Vertx
|
||||
) {
|
||||
val sentMessages = sendMessages(l1Contract)
|
||||
val messageAnchoringService = initialiseServices(
|
||||
vertx,
|
||||
l1Contract.contractAddress,
|
||||
l2Contract.contractAddress,
|
||||
earliestL1Block = sentMessages.first().l1BlockNumber
|
||||
)
|
||||
|
||||
messageAnchoringService.start().get()
|
||||
await()
|
||||
.atMost(30, TimeUnit.SECONDS)
|
||||
.untilAsserted {
|
||||
val inboxStatusesFutures = sentMessages.map { message ->
|
||||
l2Contract.inboxL1L2MessageStatus(message.messageHash)
|
||||
.sendAsync()
|
||||
.toSafeFuture()
|
||||
}
|
||||
val anchoredStatuses = SafeFuture.collectAll(inboxStatusesFutures.stream()).get()
|
||||
assertThat(anchoredStatuses).allSatisfy { isAnchoredStatus(it) }
|
||||
}
|
||||
messageAnchoringService.stop().get()
|
||||
}
|
||||
|
||||
private fun isAnchoredStatus(status: BigInteger): Boolean {
|
||||
return status == BigInteger.valueOf(1)
|
||||
}
|
||||
|
||||
private fun initialiseServices(
|
||||
vertx: Vertx,
|
||||
l1ContractAddress: String,
|
||||
l2ContractAddress: String,
|
||||
earliestL1Block: ULong
|
||||
): MessageAnchoringService {
|
||||
val l1EventQuerier = L1EventQuerierImpl(
|
||||
vertx = vertx,
|
||||
config = L1EventQuerierImpl.Config(
|
||||
pollingInterval = messagePollingInterval,
|
||||
maxEventScrapingTime = maxScrapingTime,
|
||||
earliestL1Block = earliestL1Block.toBigInteger(),
|
||||
maxMessagesToCollect = maxMessagesToAnchor,
|
||||
l1MessageServiceAddress = l1ContractAddress,
|
||||
"latest",
|
||||
blockRangeLoopLimit = blockRangeLoopLimit
|
||||
),
|
||||
l1Web3jClient = l1Web3Client
|
||||
)
|
||||
|
||||
val l2Querier = L2QuerierImpl(
|
||||
l2Client = l2Web3jClient,
|
||||
messageService = l2Contract,
|
||||
config = L2QuerierImpl.Config(
|
||||
blocksToFinalizationL2 = 0u,
|
||||
lastHashSearchWindow = 5u,
|
||||
contractAddressToListen = l2ContractAddress
|
||||
),
|
||||
vertx = vertx
|
||||
)
|
||||
|
||||
val l2MessageAnchorer: L2MessageAnchorer = L2MessageAnchorerImpl(
|
||||
vertx = vertx,
|
||||
l2Web3j = l2Web3jClient,
|
||||
l2Client = l2Contract,
|
||||
config = L2MessageAnchorerImpl.Config(
|
||||
receiptPollingInterval = receiptPollingInterval,
|
||||
maxReceiptRetries = 10u,
|
||||
blocksToFinalisation = 0
|
||||
)
|
||||
)
|
||||
|
||||
return MessageAnchoringService(
|
||||
MessageAnchoringService.Config(
|
||||
pollingInterval = 500.milliseconds,
|
||||
maxMessagesToAnchor
|
||||
),
|
||||
vertx,
|
||||
l1EventQuerier,
|
||||
l2MessageAnchorer,
|
||||
l2Querier,
|
||||
l1ContractClient,
|
||||
L2MessageService.load(
|
||||
l2ContractAddress,
|
||||
l2Web3jClient,
|
||||
l2TransactionManager,
|
||||
DefaultGasProvider()
|
||||
),
|
||||
l2TransactionManager
|
||||
)
|
||||
}
|
||||
|
||||
data class MessageSentResult(
|
||||
val l1BlockNumber: ULong,
|
||||
val messageHash: ByteArray
|
||||
)
|
||||
|
||||
private fun sendMessages(contract: LineaRollupAsyncFriendly): List<MessageSentResult> {
|
||||
val baseMessageToSend = L1MessageToSend(
|
||||
recipient = l2RecipientAddress,
|
||||
fee = BigInteger.TEN,
|
||||
calldata = ByteArray(0),
|
||||
value = BigInteger.valueOf(200001)
|
||||
)
|
||||
val messagesToSend = listOf(
|
||||
baseMessageToSend,
|
||||
baseMessageToSend.copy(fee = BigInteger.valueOf(21)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(200001)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(200001)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(200001)),
|
||||
baseMessageToSend,
|
||||
baseMessageToSend.copy(fee = BigInteger.valueOf(21)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(200001)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(200001)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(200001)),
|
||||
baseMessageToSend,
|
||||
baseMessageToSend.copy(fee = BigInteger.valueOf(21)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(200001)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(200001)),
|
||||
baseMessageToSend.copy(value = BigInteger.valueOf(200001))
|
||||
)
|
||||
|
||||
val futures = messagesToSend.map {
|
||||
contract.sendMessage(it.recipient, it.fee, it.calldata, it.value).sendAsync()
|
||||
.toSafeFuture()
|
||||
.thenApply { transactionReceipt ->
|
||||
log.debug("Message has been sent in block {}", transactionReceipt.blockNumber)
|
||||
val eventValues = LineaRollupV6.staticExtractEventParameters(
|
||||
LineaRollupV6.MESSAGESENT_EVENT,
|
||||
transactionReceipt.logs.first { log ->
|
||||
log.topics.contains(EventEncoder.encode(LineaRollupV6.MESSAGESENT_EVENT))
|
||||
}
|
||||
)
|
||||
MessageSentResult(
|
||||
l1BlockNumber = transactionReceipt.blockNumber.toULong(),
|
||||
messageHash = eventValues.indexedValues[2].value as ByteArray
|
||||
)
|
||||
}
|
||||
}
|
||||
val emittedEvents: List<MessageSentResult> = SafeFuture.collectAll(futures.stream()).get()
|
||||
|
||||
return emittedEvents
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="warn">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="net.consensys.zkevm" level="trace" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
<Logger name="net.consensys.linea" level="trace" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
<!-- Set level to DEBUG to log Web3J request/responses -->
|
||||
<Logger name="org.web3j.protocol.http.HttpService" level="warn" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
<Root level="info" additivity="false">
|
||||
<appender-ref ref="console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
@@ -1,255 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import build.linea.contract.LineaRollupV6
|
||||
import io.vertx.core.Vertx
|
||||
import linea.kotlin.toULong
|
||||
import net.consensys.linea.async.toSafeFuture
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.web3j.abi.EventEncoder
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import org.web3j.protocol.core.methods.request.EthFilter
|
||||
import org.web3j.protocol.core.methods.response.EthLog
|
||||
import org.web3j.protocol.core.methods.response.Log
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.Callable
|
||||
import kotlin.time.Duration
|
||||
|
||||
class L1EventQuerierImpl(
|
||||
private val vertx: Vertx,
|
||||
private val config: Config,
|
||||
private val l1Web3jClient: Web3j
|
||||
) : L1EventQuerier {
|
||||
companion object {
|
||||
val encodedMessageSentEvent: String = EventEncoder.encode(LineaRollupV6.MESSAGESENT_EVENT)
|
||||
|
||||
fun parseMessageSentEventLogs(log: Log): SendMessageEvent {
|
||||
val messageSentEvent = LineaRollupV6.getMessageSentEventFromLog(log)
|
||||
return SendMessageEvent(
|
||||
Bytes32.wrap(messageSentEvent._messageHash),
|
||||
messageSentEvent._nonce.toULong(),
|
||||
messageSentEvent.log.blockNumber.toULong()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class QueryStateParameters(
|
||||
var startingBlock: BigInteger,
|
||||
var finalBlock: BigInteger,
|
||||
var startingEventLogIndex: BigInteger
|
||||
)
|
||||
|
||||
private val startingLogIndexToIncludeAllLogs = BigInteger.valueOf(-1)
|
||||
private val log: Logger = LogManager.getLogger(this::class.java)
|
||||
|
||||
class Config(
|
||||
val pollingInterval: Duration,
|
||||
val maxEventScrapingTime: Duration,
|
||||
val earliestL1Block: BigInteger,
|
||||
val maxMessagesToCollect: UInt,
|
||||
val l1MessageServiceAddress: String,
|
||||
val finalized: String,
|
||||
val blockRangeLoopLimit: UInt
|
||||
)
|
||||
|
||||
override fun getSendMessageEventsForAnchoredMessage(
|
||||
messageHash: MessageHashAnchoredEvent?
|
||||
): SafeFuture<List<SendMessageEvent>> {
|
||||
return vertx.executeBlocking(
|
||||
Callable {
|
||||
val finalBlock = getFinalBlock()
|
||||
val initialQueryStateParameters =
|
||||
if (messageHash != null) {
|
||||
// get startingBlock and index to start ignoring from for first round of data
|
||||
log.debug("Starting with message hash: {}", messageHash.messageHash)
|
||||
getBlockAtEventEmission(finalBlock, messageHash)
|
||||
} else {
|
||||
log.debug("Starting hash is null, using earliest block and latest block")
|
||||
QueryStateParameters(config.earliestL1Block, finalBlock, startingLogIndexToIncludeAllLogs)
|
||||
}
|
||||
|
||||
val collectedEvents = collectEvents(initialQueryStateParameters)
|
||||
log.debug(
|
||||
"Completing with events: ${collectedEvents.count()} with maxMessagesToAnchor:${config.maxMessagesToCollect}"
|
||||
)
|
||||
collectedEvents.take(config.maxMessagesToCollect.toInt())
|
||||
},
|
||||
true
|
||||
)
|
||||
.toSafeFuture()
|
||||
}
|
||||
|
||||
private fun collectEvents(queryStateParameters: QueryStateParameters): List<SendMessageEvent> {
|
||||
val collectedEvents: MutableList<SendMessageEvent> = mutableListOf()
|
||||
var eventCollectionQueryParameters = queryStateParameters
|
||||
val startTimestampMillis = System.currentTimeMillis()
|
||||
var elapsedTimeMillis: Long
|
||||
do {
|
||||
val finalBlock = getFinalBlock()
|
||||
|
||||
// NB! make sure we only use the latest finalized block or within limits
|
||||
eventCollectionQueryParameters.finalBlock = getFinalBlockForQueryingWithinLimits(
|
||||
eventCollectionQueryParameters.startingBlock,
|
||||
finalBlock
|
||||
)
|
||||
|
||||
log.trace("Querying for events with {}", eventCollectionQueryParameters)
|
||||
// get the mapped events and next block number to start from
|
||||
val (newEvents, nextQueryParameters) = getEventsFromLogIndexInRange(eventCollectionQueryParameters)
|
||||
eventCollectionQueryParameters = nextQueryParameters
|
||||
collectedEvents.addAll(newEvents)
|
||||
|
||||
// we may have enough messages, so we could end
|
||||
if (collectedEvents.count().toUInt() >= config.maxMessagesToCollect) {
|
||||
break
|
||||
}
|
||||
|
||||
Thread.sleep(config.pollingInterval.inWholeMilliseconds)
|
||||
elapsedTimeMillis = System.currentTimeMillis() - startTimestampMillis
|
||||
} while (elapsedTimeMillis < config.maxEventScrapingTime.inWholeMilliseconds)
|
||||
return collectedEvents
|
||||
}
|
||||
|
||||
private fun getFinalBlock(): BigInteger = l1Web3jClient
|
||||
.ethGetBlockByNumber(DefaultBlockParameter.valueOf(config.finalized), false)
|
||||
.send()
|
||||
.block
|
||||
.number
|
||||
|
||||
private fun getEventsFromLogIndexInRange(
|
||||
queryStateParameters: QueryStateParameters
|
||||
): Pair<List<SendMessageEvent>, QueryStateParameters> {
|
||||
val getLogsResponse = l1Web3jClient.ethGetLogs(
|
||||
buildEventFilter(
|
||||
queryStateParameters.startingBlock,
|
||||
queryStateParameters.finalBlock
|
||||
)
|
||||
).send()
|
||||
val tempLogs: MutableList<EthLog.LogResult<Any>>? = getLogsResponse.logs
|
||||
|
||||
log.trace("Getting events with {}", queryStateParameters)
|
||||
|
||||
val newLogs = tempLogs?.filter { logResult ->
|
||||
isEventAfterEventOnInitialBlock(
|
||||
logResult.get(),
|
||||
queryStateParameters.startingEventLogIndex,
|
||||
queryStateParameters.startingBlock
|
||||
)
|
||||
}
|
||||
|
||||
return when {
|
||||
tempLogs == null -> {
|
||||
log.debug("Logs request failed! Error: {}", getLogsResponse.error)
|
||||
Pair(emptyList(), queryStateParameters)
|
||||
}
|
||||
|
||||
!newLogs.isNullOrEmpty() -> {
|
||||
val lastLog = (newLogs.last().get() as Log)
|
||||
|
||||
val newStartingBlock = BigInteger.valueOf(lastLog.blockNumber.toLong())
|
||||
val newStartingIndex = BigInteger.valueOf(lastLog.logIndex.toLong())
|
||||
|
||||
val nextQueryStateParameters = QueryStateParameters(
|
||||
newStartingBlock,
|
||||
queryStateParameters.finalBlock,
|
||||
newStartingIndex
|
||||
)
|
||||
|
||||
val events = newLogs.map { mappingLog -> parseMessageSentEventLogs(mappingLog.get() as Log) }
|
||||
|
||||
Pair(events, nextQueryStateParameters)
|
||||
}
|
||||
|
||||
startAndFinalBlockAreSame(queryStateParameters) -> {
|
||||
Pair(
|
||||
listOf(),
|
||||
QueryStateParameters(
|
||||
queryStateParameters.finalBlock,
|
||||
queryStateParameters.finalBlock,
|
||||
queryStateParameters.startingEventLogIndex
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Pair(
|
||||
listOf(),
|
||||
QueryStateParameters(
|
||||
queryStateParameters.finalBlock,
|
||||
queryStateParameters.finalBlock,
|
||||
startingLogIndexToIncludeAllLogs
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAndFinalBlockAreSame(queryStateParameters: QueryStateParameters): Boolean {
|
||||
return queryStateParameters.startingBlock == queryStateParameters.finalBlock
|
||||
}
|
||||
|
||||
private fun isEventAfterEventOnInitialBlock(
|
||||
logResult: Any,
|
||||
logIndex: BigInteger,
|
||||
startingBlock: BigInteger
|
||||
): Boolean {
|
||||
val eventLog = (logResult as Log)
|
||||
return (eventLog.blockNumber == startingBlock && eventLog.logIndex > logIndex) ||
|
||||
(eventLog.blockNumber > startingBlock)
|
||||
}
|
||||
|
||||
private fun buildEventFilter(startingBlock: BigInteger, finalBlock: BigInteger): EthFilter {
|
||||
val sentMessagesFilter =
|
||||
EthFilter(
|
||||
DefaultBlockParameter.valueOf(startingBlock),
|
||||
DefaultBlockParameter.valueOf(finalBlock),
|
||||
config.l1MessageServiceAddress
|
||||
)
|
||||
|
||||
sentMessagesFilter.addSingleTopic(encodedMessageSentEvent)
|
||||
return sentMessagesFilter
|
||||
}
|
||||
|
||||
private fun getBlockAtEventEmission(
|
||||
finalBlock: BigInteger,
|
||||
messageHash: MessageHashAnchoredEvent
|
||||
): QueryStateParameters {
|
||||
val messageHashFilter =
|
||||
EthFilter(
|
||||
DefaultBlockParameter.valueOf(config.earliestL1Block),
|
||||
DefaultBlockParameter.valueOf(finalBlock),
|
||||
config.l1MessageServiceAddress
|
||||
)
|
||||
|
||||
messageHashFilter.addSingleTopic(encodedMessageSentEvent)
|
||||
messageHashFilter.addNullTopic()
|
||||
messageHashFilter.addNullTopic()
|
||||
messageHashFilter.addSingleTopic(messageHash.messageHash.toString())
|
||||
|
||||
log.trace("Trying to find the event in range [{} .. {}]", config.earliestL1Block, finalBlock)
|
||||
// get the block where the hash was found
|
||||
val logs = l1Web3jClient.ethGetLogs(messageHashFilter).send().logs
|
||||
|
||||
return if (!logs.isNullOrEmpty()) {
|
||||
val eventLog = logs.first().get() as Log
|
||||
val finalBlockWithinLimits = getFinalBlockForQueryingWithinLimits(eventLog.blockNumber, finalBlock)
|
||||
log.trace("Found event hash at block {}", eventLog.blockNumber)
|
||||
QueryStateParameters(eventLog.blockNumber, finalBlockWithinLimits, eventLog.logIndex)
|
||||
} else {
|
||||
val finalBlockWithinLimits = getFinalBlockForQueryingWithinLimits(config.earliestL1Block, finalBlock)
|
||||
QueryStateParameters(config.earliestL1Block, finalBlockWithinLimits, startingLogIndexToIncludeAllLogs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFinalBlockForQueryingWithinLimits(
|
||||
startingBlock: BigInteger,
|
||||
finalBlock: BigInteger
|
||||
): BigInteger {
|
||||
val loopLimit = BigInteger.valueOf(config.blockRangeLoopLimit.toLong())
|
||||
|
||||
return minOf(startingBlock + loopLimit, finalBlock)
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import linea.kotlin.toBigInteger
|
||||
import net.consensys.linea.async.AsyncRetryer
|
||||
import net.consensys.linea.async.toSafeFuture
|
||||
import net.consensys.linea.contract.L2MessageService
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.methods.response.EthBlock
|
||||
import org.web3j.protocol.core.methods.response.TransactionReceipt
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.Callable
|
||||
import kotlin.time.Duration
|
||||
|
||||
class L2MessageAnchorerImpl(
|
||||
private val vertx: Vertx,
|
||||
private val l2Web3j: Web3j,
|
||||
private val l2Client: L2MessageService,
|
||||
private val config: Config
|
||||
) : L2MessageAnchorer {
|
||||
|
||||
class Config(
|
||||
val receiptPollingInterval: Duration,
|
||||
val maxReceiptRetries: UInt,
|
||||
val blocksToFinalisation: Long
|
||||
)
|
||||
|
||||
private val log: Logger = LogManager.getLogger(this::class.java)
|
||||
|
||||
override fun anchorMessages(
|
||||
sendMessageEvents: List<SendMessageEvent>,
|
||||
finalRollingHash: ByteArray
|
||||
): SafeFuture<TransactionReceipt> {
|
||||
log.debug(
|
||||
"Anchoring using rolling hash {}, hashes={}",
|
||||
sendMessageEvents.count(),
|
||||
sendMessageEvents.map { it.messageHash.toHexString() }
|
||||
)
|
||||
|
||||
return vertx.executeBlocking(
|
||||
Callable {
|
||||
l2Client.anchorL1L2MessageHashes(
|
||||
sendMessageEvents.map { arr -> arr.messageHash.toArray() },
|
||||
sendMessageEvents.first().messageNumber.toBigInteger(),
|
||||
sendMessageEvents.last().messageNumber.toBigInteger(),
|
||||
finalRollingHash
|
||||
)
|
||||
},
|
||||
true
|
||||
).toSafeFuture().thenCompose { anchorMessageHashesCall ->
|
||||
anchorMessageHashesCall.sendAsync()
|
||||
.thenCompose { txReceipt ->
|
||||
val safeBlock = txReceipt.blockNumber.add(
|
||||
BigInteger.valueOf(config.blocksToFinalisation)
|
||||
)
|
||||
AsyncRetryer.retry(
|
||||
vertx,
|
||||
maxRetries = config.maxReceiptRetries.toInt(),
|
||||
backoffDelay = config.receiptPollingInterval,
|
||||
stopRetriesPredicate = transactionIsSafe(safeBlock)
|
||||
) {
|
||||
SafeFuture.of(l2Web3j.ethGetBlockByNumber({ "latest" }, false).sendAsync())
|
||||
}.exceptionally { _ -> null }
|
||||
.thenApply {
|
||||
txReceipt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun transactionIsSafe(safeBlockNumber: BigInteger) =
|
||||
{ result: EthBlock -> result.block.number >= safeBlockNumber }
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import net.consensys.linea.async.toSafeFuture
|
||||
import net.consensys.linea.contract.L2MessageService
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.web3j.abi.EventEncoder
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import org.web3j.protocol.core.methods.response.Log
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
class L2QuerierImpl(
|
||||
private val l2Client: Web3j,
|
||||
private val messageService: L2MessageService,
|
||||
private val config: Config,
|
||||
private val vertx: Vertx
|
||||
) : L2Querier {
|
||||
private val log: Logger = LogManager.getLogger(this::class.java)
|
||||
|
||||
data class Config(
|
||||
val blocksToFinalizationL2: UInt,
|
||||
val lastHashSearchWindow: UInt,
|
||||
val contractAddressToListen: String
|
||||
)
|
||||
|
||||
private fun finalizedBlockNumber(): SafeFuture<BigInteger> {
|
||||
return SafeFuture.of(
|
||||
l2Client.ethBlockNumber().sendAsync().thenApply {
|
||||
it.blockNumber.minus(BigInteger.valueOf(config.blocksToFinalizationL2.toLong()))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun findLastFinalizedAnchoredEvent(): SafeFuture<MessageHashAnchoredEvent?> {
|
||||
return vertx.executeBlocking(
|
||||
Callable {
|
||||
var finalizedBlockNumber = l2Client.ethBlockNumber().send().blockNumber
|
||||
if (finalizedBlockNumber > BigInteger.valueOf(config.blocksToFinalizationL2.toLong())) {
|
||||
finalizedBlockNumber = finalizedBlockNumber.minus(BigInteger.valueOf(config.blocksToFinalizationL2.toLong()))
|
||||
}
|
||||
|
||||
val bigIntSearchWindow = BigInteger.valueOf(config.lastHashSearchWindow.toLong())
|
||||
var startingBlock: BigInteger = BigInteger.valueOf(0)
|
||||
if (finalizedBlockNumber > bigIntSearchWindow) {
|
||||
startingBlock = finalizedBlockNumber.minus(bigIntSearchWindow)
|
||||
}
|
||||
var endingBlock = finalizedBlockNumber
|
||||
|
||||
log.debug(
|
||||
"Searching for event={} startingBlock={} endingBlock={}",
|
||||
L2MessageService.L1L2MESSAGEHASHESADDEDTOINBOX_EVENT.name,
|
||||
startingBlock,
|
||||
endingBlock
|
||||
)
|
||||
|
||||
messageService.setDefaultBlockParameter(DefaultBlockParameter.valueOf(finalizedBlockNumber))
|
||||
var event: MessageHashAnchoredEvent? = null
|
||||
while (startingBlock >= BigInteger.ZERO) {
|
||||
val messageHashFilter =
|
||||
org.web3j.protocol.core.methods.request.EthFilter(
|
||||
DefaultBlockParameter.valueOf(startingBlock),
|
||||
DefaultBlockParameter.valueOf(endingBlock),
|
||||
messageService.contractAddress
|
||||
)
|
||||
|
||||
messageHashFilter.addSingleTopic(EventEncoder.encode(L2MessageService.L1L2MESSAGEHASHESADDEDTOINBOX_EVENT))
|
||||
|
||||
val logs = l2Client.ethGetLogs(messageHashFilter).send().logs
|
||||
if (logs.isNotEmpty()) {
|
||||
val lastLog = logs.last().get() as Log
|
||||
val messageHash =
|
||||
L2MessageService.getL1L2MessageHashesAddedToInboxEventFromLog(lastLog).messageHashes.last()
|
||||
log.debug("Returning found hash={}", Bytes32.wrap(messageHash))
|
||||
event = MessageHashAnchoredEvent(Bytes32.wrap(messageHash))
|
||||
break
|
||||
} else {
|
||||
endingBlock = startingBlock
|
||||
startingBlock = startingBlock.minus(bigIntSearchWindow)
|
||||
}
|
||||
}
|
||||
event
|
||||
},
|
||||
true
|
||||
)
|
||||
.toSafeFuture()
|
||||
}
|
||||
|
||||
override fun getMessageHashStatus(messageHash: Bytes32): SafeFuture<BigInteger> {
|
||||
return SafeFuture.of(
|
||||
finalizedBlockNumber().thenApply {
|
||||
messageService.setDefaultBlockParameter(DefaultBlockParameter.valueOf(it))
|
||||
}.thenCompose {
|
||||
messageService.inboxL1L2MessageStatus(messageHash.toArray()).sendAsync()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import linea.contract.l1.LineaRollupSmartContractClientReadOnly
|
||||
import linea.domain.BlockParameter
|
||||
import linea.web3j.transactionmanager.AsyncFriendlyTransactionManager
|
||||
import net.consensys.linea.contract.L2MessageService
|
||||
import net.consensys.zkevm.PeriodicPollingService
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import kotlin.time.Duration
|
||||
|
||||
class MessageAnchoringService(
|
||||
private val config: Config,
|
||||
private val vertx: Vertx,
|
||||
private val l1EventQuerier: L1EventQuerier,
|
||||
private val l2MessageAnchorer: L2MessageAnchorer,
|
||||
private val l2Querier: L2Querier,
|
||||
private val lineaRollupSmartContractClient: LineaRollupSmartContractClientReadOnly,
|
||||
l2MessageService: L2MessageService,
|
||||
private val transactionManager: AsyncFriendlyTransactionManager,
|
||||
private val log: Logger = LogManager.getLogger(MessageAnchoringService::class.java)
|
||||
) : PeriodicPollingService(
|
||||
vertx = vertx,
|
||||
pollingIntervalMs = config.pollingInterval.inWholeMilliseconds,
|
||||
log = log
|
||||
) {
|
||||
class Config(
|
||||
val pollingInterval: Duration,
|
||||
val maxMessagesToAnchor: UInt
|
||||
)
|
||||
|
||||
private val inboxStatusUnknown: BigInteger = l2MessageService.INBOX_STATUS_UNKNOWN().send()
|
||||
|
||||
override fun action(): SafeFuture<Unit> {
|
||||
return l2Querier
|
||||
.findLastFinalizedAnchoredEvent()
|
||||
.thenCompose(l1EventQuerier::getSendMessageEventsForAnchoredMessage)
|
||||
.thenCompose { sentMessages ->
|
||||
SafeFuture.collectAll(
|
||||
sentMessages
|
||||
.map { sendMessageEvent ->
|
||||
l2Querier.getMessageHashStatus(sendMessageEvent.messageHash).thenApply { status
|
||||
->
|
||||
sendMessageEvent to status
|
||||
}
|
||||
}
|
||||
.stream()
|
||||
)
|
||||
}
|
||||
.thenApply { eventAndStatusPairs ->
|
||||
eventAndStatusPairs
|
||||
.filter { eventAndStatus -> eventAndStatus.second == inboxStatusUnknown }
|
||||
.map { it.first }
|
||||
}
|
||||
.thenCompose { eventsToAnchor ->
|
||||
if (eventsToAnchor.isNotEmpty()) {
|
||||
log.debug("Found {} un-anchored events", eventsToAnchor.count())
|
||||
|
||||
val messagesToAnchorWithinLimit = eventsToAnchor.take(config.maxMessagesToAnchor.toInt())
|
||||
|
||||
anchorMessagesUsingRollingHashProtocol(messagesToAnchorWithinLimit)
|
||||
} else {
|
||||
log.debug("Skipping anchoring as there are no hashes")
|
||||
SafeFuture.completedFuture(Unit)
|
||||
}
|
||||
}.exceptionally(::handleAnchoringError)
|
||||
}
|
||||
|
||||
private fun anchorMessagesUsingRollingHashProtocol(messagesEvents: List<SendMessageEvent>): SafeFuture<Unit> {
|
||||
return lineaRollupSmartContractClient.getMessageRollingHash(
|
||||
blockParameter = BlockParameter.Tag.LATEST,
|
||||
messageNumber = messagesEvents.last().messageNumber.toLong()
|
||||
).thenCompose { finalRollingHash ->
|
||||
transactionManager.resetNonce().thenCompose {
|
||||
l2MessageAnchorer.anchorMessages(
|
||||
messagesEvents,
|
||||
finalRollingHash
|
||||
)
|
||||
}.thenApply { anchoringResult ->
|
||||
log.info("Message anchoring using rolling hash transactionHash=${anchoringResult.transactionHash}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAnchoringError(error: Throwable) {
|
||||
when {
|
||||
(
|
||||
error.message != null && (
|
||||
error.message!!.contains("replacement transaction underpriced") ||
|
||||
error.message!!.contains("already known")
|
||||
)
|
||||
) ->
|
||||
log.debug("Anchoring transaction wasn't executed due to ${error.message}")
|
||||
|
||||
else ->
|
||||
// Since anchoring is stateless and will be retried on the next iteration of the loop, no error is considered
|
||||
// as unrecoverable, thus logged as warning, not ERROR. But we still want to see them, thus not DEBUG
|
||||
log.warn("Anchoring attempt failed! Anchoring will be re-attempted shortly.", error)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleError(error: Throwable) {
|
||||
log.error("Failed to anchor messages: errorMessage={}", error.message, error)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
|
||||
fun createRandomSendMessageEvents(numberOfRandomHashes: ULong): List<SendMessageEvent> {
|
||||
return (0UL..numberOfRandomHashes)
|
||||
.map { n ->
|
||||
SendMessageEvent(
|
||||
Bytes32.random(),
|
||||
messageNumber = n + 1UL,
|
||||
blockNumber = n + 1UL
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,646 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import build.linea.contract.LineaRollupV6
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.junit5.Timeout
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import io.vertx.junit5.VertxTestContext
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.web3j.abi.EventEncoder
|
||||
import org.web3j.abi.FunctionEncoder
|
||||
import org.web3j.abi.TypeEncoder
|
||||
import org.web3j.abi.datatypes.Address
|
||||
import org.web3j.abi.datatypes.generated.Uint256
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import org.web3j.protocol.core.methods.request.EthFilter
|
||||
import org.web3j.protocol.core.methods.response.EthBlock
|
||||
import org.web3j.protocol.core.methods.response.EthLog
|
||||
import org.web3j.protocol.core.methods.response.EthLog.LogResult
|
||||
import org.web3j.protocol.core.methods.response.Log
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextUInt
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class L1EventQuerierImplTest {
|
||||
private val testContractAddress = "0x6d976c9b8ceee705d4fe8699b44e5eb58242f484"
|
||||
private val testToAddress: Address = Address("0x087b027b0573D4f01345eF8D081E0E7d3B378d14")
|
||||
private val testFromAddress = Address("0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5")
|
||||
private val testFee = Uint256(12345)
|
||||
private val testValue = Uint256(123456789)
|
||||
private val testNonce = Uint256(1)
|
||||
private val callData: org.web3j.abi.datatypes.DynamicBytes =
|
||||
org.web3j.abi.datatypes.DynamicBytes("".encodeToByteArray())
|
||||
|
||||
private val logIndexStart = 1
|
||||
private val blockInitialEventIsOn = 19
|
||||
private val maxMessagesToAnchor = 100u
|
||||
private val pollingInterval = 10.milliseconds
|
||||
private val earliestL1Block = BigInteger.valueOf(0)
|
||||
private val maxEventScrapingTime: Duration = 1.seconds
|
||||
private val blockRangeLoopLimit = 100u
|
||||
|
||||
private lateinit var l1ClientMock: Web3j
|
||||
private lateinit var mockedEthBlock: EthBlock
|
||||
private lateinit var l1EventQuerier: L1EventQuerier
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach(vertx: Vertx) {
|
||||
l1ClientMock = mock<Web3j>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS)
|
||||
mockedEthBlock = mock<EthBlock>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS)
|
||||
l1EventQuerier =
|
||||
L1EventQuerierImpl(
|
||||
vertx,
|
||||
L1EventQuerierImpl.Config(
|
||||
pollingInterval,
|
||||
maxEventScrapingTime,
|
||||
earliestL1Block,
|
||||
maxMessagesToAnchor,
|
||||
testContractAddress,
|
||||
"latest",
|
||||
blockRangeLoopLimit
|
||||
),
|
||||
l1ClientMock
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun nullHashAndNoMessages_returnsNoEvents(testContext: VertxTestContext) {
|
||||
whenever(mockedEthBlock.block.number).thenReturn(BigInteger.valueOf(20))
|
||||
|
||||
whenever(l1ClientMock.ethGetBlockByNumber(any(), any()).send())
|
||||
.thenReturn(mockedEthBlock)
|
||||
|
||||
val emptyEvents: List<LogResult<Log>> = listOf()
|
||||
val mockLogs = mock<EthLog>()
|
||||
whenever(mockLogs.logs).thenReturn(emptyEvents)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogs)
|
||||
|
||||
l1EventQuerier.getSendMessageEventsForAnchoredMessage(null).thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it).isEmpty()
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(2, timeUnit = TimeUnit.SECONDS)
|
||||
fun nullHashAndMoreThan100Messages_returns100Events(testContext: VertxTestContext) {
|
||||
whenever(mockedEthBlock.block.number).thenReturn(BigInteger.valueOf(20))
|
||||
|
||||
whenever(l1ClientMock.ethGetBlockByNumber({ "latest" }, false).send())
|
||||
.thenReturn(mockedEthBlock)
|
||||
|
||||
val mockLogs = mock<EthLog>()
|
||||
val events = (blockInitialEventIsOn + 1..blockInitialEventIsOn + 106).map {
|
||||
createRandomSendEvent(
|
||||
it.toString(),
|
||||
Random.nextUInt().toString()
|
||||
)
|
||||
}
|
||||
whenever(mockLogs.logs).thenReturn(events)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogs)
|
||||
|
||||
l1EventQuerier.getSendMessageEventsForAnchoredMessage(null).thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it.count()).isEqualTo(100)
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun nullHashAndLessThan100Messages_returnsLessThan100Events(
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
whenever(mockedEthBlock.block.number)
|
||||
.thenReturn(BigInteger.valueOf(20))
|
||||
.thenReturn(BigInteger.valueOf(100))
|
||||
whenever(l1ClientMock.ethGetBlockByNumber({ "latest" }, false).send())
|
||||
.thenReturn(mockedEthBlock)
|
||||
|
||||
val mockLogs = mock<EthLog>()
|
||||
val events = (blockInitialEventIsOn + 1..blockInitialEventIsOn + 80).map {
|
||||
createRandomSendEvent(
|
||||
it.toString(),
|
||||
Random.nextUInt().toString()
|
||||
)
|
||||
}
|
||||
whenever(mockLogs.logs).thenReturn(events)
|
||||
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogs).thenReturn(mock<EthLog>())
|
||||
|
||||
l1EventQuerier.getSendMessageEventsForAnchoredMessage(null).thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it.count()).isEqualTo(80)
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun nullHashAndMoreThan100MessagesInMultipleQueries_returns100Events(
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
whenever(mockedEthBlock.block.number)
|
||||
.thenReturn(BigInteger.valueOf(20))
|
||||
.thenReturn(BigInteger.valueOf(140))
|
||||
whenever(l1ClientMock.ethGetBlockByNumber({ "latest" }, false).send())
|
||||
.thenReturn(mockedEthBlock)
|
||||
|
||||
val mockLogs = mock<EthLog>()
|
||||
val events = (blockInitialEventIsOn + 1..blockInitialEventIsOn + 80).map {
|
||||
createRandomSendEvent(
|
||||
it.toString(),
|
||||
Random.nextUInt().toString()
|
||||
)
|
||||
}
|
||||
whenever(mockLogs.logs).thenReturn(events)
|
||||
|
||||
val mockLogsRound2 = mock<EthLog>()
|
||||
val eventsRound2 = (blockInitialEventIsOn + 100..blockInitialEventIsOn + 120).map {
|
||||
createRandomSendEvent(
|
||||
it.toString(),
|
||||
Random.nextUInt().toString()
|
||||
)
|
||||
}
|
||||
whenever(mockLogsRound2.logs).thenReturn(eventsRound2)
|
||||
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogs).thenReturn(mockLogsRound2)
|
||||
|
||||
l1EventQuerier.getSendMessageEventsForAnchoredMessage(null).thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it.count()).isEqualTo(100)
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun existingHashNotFoundAndNoMessages_returnsNoEvents(
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
whenever(mockedEthBlock.block.number).thenReturn(BigInteger.valueOf(20))
|
||||
|
||||
whenever(l1ClientMock.ethGetBlockByNumber(any(), any()).send())
|
||||
.thenReturn(mockedEthBlock)
|
||||
|
||||
val emptyEvents: List<LogResult<Log>> = listOf()
|
||||
val mockLogs = mock<EthLog>()
|
||||
whenever(mockLogs.logs).thenReturn(emptyEvents)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogs)
|
||||
|
||||
l1EventQuerier
|
||||
.getSendMessageEventsForAnchoredMessage(
|
||||
MessageHashAnchoredEvent(messageHash = Bytes32.random())
|
||||
)
|
||||
.thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it).isEmpty()
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun existingHashFoundAndNoMessages_returnsNoEvents(testContext: VertxTestContext) {
|
||||
val messageHash = Bytes32.random()
|
||||
whenever(mockedEthBlock.block.number)
|
||||
.thenReturn(BigInteger.valueOf(20))
|
||||
.thenReturn(BigInteger.valueOf(30))
|
||||
|
||||
whenever(l1ClientMock.ethGetBlockByNumber({ "latest" }, false).send())
|
||||
.thenReturn(mockedEthBlock)
|
||||
|
||||
val mockLogs = mock<EthLog>()
|
||||
val emptyEvents: List<LogResult<Log>> = listOf()
|
||||
whenever(mockLogs.logs).thenReturn(emptyEvents)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogs)
|
||||
|
||||
val eventMockLogs = mock<EthLog>()
|
||||
val initialEvent = createRandomSendEvent(blockInitialEventIsOn.toString(), Random.nextUInt().toString())
|
||||
whenever(eventMockLogs.logs).thenReturn(listOf(initialEvent))
|
||||
whenever(l1ClientMock.ethGetLogs(buildMessageHashEventFilter(messageHash)).send()).thenReturn(eventMockLogs)
|
||||
.thenReturn(mockLogs)
|
||||
|
||||
l1EventQuerier
|
||||
.getSendMessageEventsForAnchoredMessage(MessageHashAnchoredEvent(messageHash))
|
||||
.thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it).isEmpty()
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun existingHashFoundMoreThan100Messages_returns100Events(
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
val messageHash = Bytes32.random()
|
||||
whenever(mockedEthBlock.block.number)
|
||||
.thenReturn(BigInteger.valueOf(20))
|
||||
.thenReturn(BigInteger.valueOf(30))
|
||||
|
||||
whenever(l1ClientMock.ethGetBlockByNumber({ "latest" }, false).send())
|
||||
.thenReturn(mockedEthBlock)
|
||||
|
||||
val mockLogs = mock<EthLog>()
|
||||
val newEvents = (blockInitialEventIsOn + 1..blockInitialEventIsOn + 100)
|
||||
.map { createRandomSendEvent(it.toString(), Random.nextUInt().toString()) }
|
||||
whenever(mockLogs.logs).thenReturn(newEvents)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogs)
|
||||
|
||||
val eventMockLogs = mock<EthLog>()
|
||||
val initialEvent = createRandomSendEvent(blockInitialEventIsOn.toString(), Random.nextUInt().toString())
|
||||
whenever(eventMockLogs.logs).thenReturn(listOf(initialEvent))
|
||||
whenever(l1ClientMock.ethGetLogs(buildMessageHashEventFilter(messageHash)).send()).thenReturn(eventMockLogs)
|
||||
.thenReturn(mockLogs)
|
||||
|
||||
l1EventQuerier
|
||||
.getSendMessageEventsForAnchoredMessage(MessageHashAnchoredEvent(messageHash))
|
||||
.thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it.count()).isEqualTo(100)
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun existingHashFoundLessThan100Messages_returnsLessThan100Events(
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
val messageHash = Bytes32.random()
|
||||
whenever(mockedEthBlock.block.number)
|
||||
.thenReturn(BigInteger.valueOf(20))
|
||||
.thenReturn(BigInteger.valueOf(30))
|
||||
|
||||
whenever(l1ClientMock.ethGetBlockByNumber({ "latest" }, false).send())
|
||||
.thenReturn(mockedEthBlock)
|
||||
|
||||
val mockLogs = mock<EthLog>()
|
||||
val newEvents = (blockInitialEventIsOn + 1..blockInitialEventIsOn + 80).map {
|
||||
createRandomSendEvent(
|
||||
it.toString(),
|
||||
Random.nextUInt().toString()
|
||||
)
|
||||
}
|
||||
whenever(mockLogs.logs).thenReturn(newEvents)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogs)
|
||||
|
||||
val emptyLogs = mock<EthLog>()
|
||||
val emptyEvents: List<LogResult<Log>> = listOf()
|
||||
whenever(emptyLogs.logs).thenReturn(emptyEvents)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(emptyLogs)
|
||||
|
||||
val eventMockLogs = mock<EthLog>()
|
||||
val initialEvent = createRandomSendEvent(blockInitialEventIsOn.toString(), Random.nextUInt().toString())
|
||||
whenever(eventMockLogs.logs).thenReturn(listOf(initialEvent))
|
||||
whenever(l1ClientMock.ethGetLogs(buildMessageHashEventFilter(messageHash)).send())
|
||||
.thenReturn(eventMockLogs)
|
||||
.thenReturn(mockLogs).thenReturn(emptyLogs)
|
||||
|
||||
l1EventQuerier
|
||||
.getSendMessageEventsForAnchoredMessage(MessageHashAnchoredEvent(messageHash))
|
||||
.thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it.count()).isEqualTo(80)
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun existingHashFound_DoesNotReturnDuplicateHashesWhenFinalBlockIsAlwaysTheSame(
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
val startingIndexForEvents = 1
|
||||
val expectedEventCount = 20
|
||||
val finalBlockThatDoesNotChange = blockInitialEventIsOn + 1
|
||||
val messageHash = Bytes32.random()
|
||||
|
||||
whenever(l1ClientMock.ethGetBlockByNumber(any(), any()).send().block.number)
|
||||
.thenReturn(BigInteger.valueOf(finalBlockThatDoesNotChange.toLong()))
|
||||
|
||||
// all expected returned events, incrementing the log index
|
||||
val mockLogs = mock<EthLog>()
|
||||
val newEvents = (startingIndexForEvents..expectedEventCount).map {
|
||||
createRandomSendEvent(
|
||||
finalBlockThatDoesNotChange.toString(),
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
whenever(mockLogs.logs).thenReturn(newEvents)
|
||||
|
||||
val foundEventLog = mock<EthLog>()
|
||||
val initialEvent = createRandomSendEvent(blockInitialEventIsOn.toString(), blockInitialEventIsOn.toString())
|
||||
whenever(foundEventLog.logs).thenReturn(listOf(initialEvent))
|
||||
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send())
|
||||
.thenReturn(foundEventLog).thenReturn(mockLogs)
|
||||
|
||||
l1EventQuerier
|
||||
.getSendMessageEventsForAnchoredMessage(MessageHashAnchoredEvent(messageHash))
|
||||
.thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it.count()).isEqualTo(expectedEventCount)
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun existingHashFound_DoesNotReturnDuplicateHashesWhenFinalBlockIsTheSameRepeatedlyAndThenChanges(
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
val expectedCountOnFirstFinalizedBlock = 20
|
||||
val expectedCountOnMovedOnFinalizedBlock = 20
|
||||
val finalBlockThatIsTheSameMultipleTimes = blockInitialEventIsOn + 1
|
||||
val movedOnFinalBlock = finalBlockThatIsTheSameMultipleTimes + 1
|
||||
val messageHash = Bytes32.random()
|
||||
val startingIndexForEvents = 1
|
||||
|
||||
// return the same block multiple times, then move on
|
||||
whenever(l1ClientMock.ethGetBlockByNumber(any(), any()).send().block.number)
|
||||
.thenReturn(BigInteger.valueOf(finalBlockThatIsTheSameMultipleTimes.toLong()))
|
||||
.thenReturn(BigInteger.valueOf(finalBlockThatIsTheSameMultipleTimes.toLong()))
|
||||
.thenReturn(BigInteger.valueOf(finalBlockThatIsTheSameMultipleTimes.toLong()))
|
||||
.thenReturn(BigInteger.valueOf(movedOnFinalBlock.toLong()))
|
||||
|
||||
// all expected returned events, incrementing the log index
|
||||
val initialFinalizedBlockLogs = mock<EthLog>()
|
||||
val newEvents = (startingIndexForEvents..expectedCountOnFirstFinalizedBlock).map {
|
||||
createRandomSendEvent(
|
||||
finalBlockThatIsTheSameMultipleTimes.toString(),
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
whenever(initialFinalizedBlockLogs.logs).thenReturn(newEvents)
|
||||
|
||||
val movedOnFinalizedLogs = mock<EthLog>()
|
||||
val movedOnEvents = (startingIndexForEvents..expectedCountOnMovedOnFinalizedBlock).map {
|
||||
createRandomSendEvent(
|
||||
movedOnFinalBlock.toString(),
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
whenever(movedOnFinalizedLogs.logs).thenReturn(movedOnEvents)
|
||||
|
||||
val foundEventLog = mock<EthLog>()
|
||||
val initialEvent = createRandomSendEvent(blockInitialEventIsOn.toString(), blockInitialEventIsOn.toString())
|
||||
whenever(foundEventLog.logs).thenReturn(listOf(initialEvent))
|
||||
|
||||
// return the same data for the same returned block multiple times, then move on
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send())
|
||||
.thenReturn(foundEventLog)
|
||||
.thenReturn(initialFinalizedBlockLogs)
|
||||
.thenReturn(initialFinalizedBlockLogs)
|
||||
.thenReturn(initialFinalizedBlockLogs)
|
||||
.thenReturn(movedOnFinalizedLogs)
|
||||
.thenReturn(movedOnFinalizedLogs)
|
||||
|
||||
l1EventQuerier
|
||||
.getSendMessageEventsForAnchoredMessage(MessageHashAnchoredEvent(messageHash))
|
||||
.thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it.count()).isEqualTo(expectedCountOnFirstFinalizedBlock + expectedCountOnMovedOnFinalizedBlock)
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun existingHashFound_returnsEventsOnLaterBlocksWithLowerLogIndex(
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
val messageHash = Bytes32.random()
|
||||
whenever(mockedEthBlock.block.number)
|
||||
.thenReturn(BigInteger.valueOf(20))
|
||||
.thenReturn(BigInteger.valueOf(100))
|
||||
|
||||
whenever(l1ClientMock.ethGetBlockByNumber({ "latest" }, false).send())
|
||||
.thenReturn(mockedEthBlock)
|
||||
|
||||
val mockLogs = mock<EthLog>()
|
||||
val newEvents = (blockInitialEventIsOn + 1..blockInitialEventIsOn + 80).map {
|
||||
createRandomSendEvent(
|
||||
it.toString(),
|
||||
it.toString() // enforcing a lower index
|
||||
)
|
||||
}
|
||||
whenever(mockLogs.logs).thenReturn(newEvents)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogs)
|
||||
|
||||
val emptyLogs = mock<EthLog>()
|
||||
val emptyEvents: List<LogResult<Log>> = listOf()
|
||||
whenever(emptyLogs.logs).thenReturn(emptyEvents)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(emptyLogs)
|
||||
|
||||
val eventMockLogs = mock<EthLog>()
|
||||
// Zenhub 770 - Enforcing a higher log index for the initial block to validate later blocks return results
|
||||
val initialEvent = createRandomSendEvent(blockInitialEventIsOn.toString(), "100") // all previous indexes are 20-99
|
||||
whenever(eventMockLogs.logs).thenReturn(listOf(initialEvent))
|
||||
whenever(l1ClientMock.ethGetLogs(buildMessageHashEventFilter(messageHash)).send())
|
||||
.thenReturn(eventMockLogs)
|
||||
.thenReturn(mockLogs).thenReturn(emptyLogs)
|
||||
|
||||
l1EventQuerier
|
||||
.getSendMessageEventsForAnchoredMessage(MessageHashAnchoredEvent(messageHash))
|
||||
.thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it.count()).isEqualTo(80)
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun existingHashFound_returnsEventsWithHigherLogIndexOnSameBlock(
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
val messageHash = Bytes32.random()
|
||||
whenever(mockedEthBlock.block.number)
|
||||
.thenReturn(BigInteger.valueOf(20))
|
||||
.thenReturn(BigInteger.valueOf(100))
|
||||
|
||||
whenever(l1ClientMock.ethGetBlockByNumber({ "latest" }, false).send())
|
||||
.thenReturn(mockedEthBlock)
|
||||
|
||||
val sameBlockNumber = Random.nextUInt()
|
||||
val mockLogs = mock<EthLog>()
|
||||
// have all the events in the same block
|
||||
val newEvents = (logIndexStart + 1..logIndexStart + 100).map {
|
||||
createRandomSendEvent(
|
||||
sameBlockNumber.toString(),
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
whenever(mockLogs.logs).thenReturn(newEvents)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogs)
|
||||
|
||||
val emptyLogs = mock<EthLog>()
|
||||
val emptyEvents: List<LogResult<Log>> = listOf()
|
||||
whenever(emptyLogs.logs).thenReturn(emptyEvents)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(emptyLogs)
|
||||
|
||||
val eventMockLogs = mock<EthLog>()
|
||||
// Forcing a lower index on the same block
|
||||
val initialEvent = createRandomSendEvent(sameBlockNumber.toString(), logIndexStart.toString())
|
||||
whenever(eventMockLogs.logs).thenReturn(listOf(initialEvent))
|
||||
whenever(l1ClientMock.ethGetLogs(buildMessageHashEventFilter(messageHash)).send())
|
||||
.thenReturn(eventMockLogs)
|
||||
.thenReturn(mockLogs).thenReturn(emptyLogs)
|
||||
|
||||
l1EventQuerier
|
||||
.getSendMessageEventsForAnchoredMessage(MessageHashAnchoredEvent(messageHash))
|
||||
.thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it.count()).isEqualTo(100)
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun foundHashAndMoreThan100MessagesInMultipleQueries_returns100Events(
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
val messageHash = Bytes32.random()
|
||||
whenever(mockedEthBlock.block.number)
|
||||
.thenReturn(BigInteger.valueOf(20))
|
||||
.thenReturn(BigInteger.valueOf(130))
|
||||
|
||||
whenever(l1ClientMock.ethGetBlockByNumber({ "latest" }, false).send())
|
||||
.thenReturn(mockedEthBlock)
|
||||
|
||||
val mockLogs = mock<EthLog>()
|
||||
val newEvents = (blockInitialEventIsOn + 1..blockInitialEventIsOn + 80).map {
|
||||
createRandomSendEvent(
|
||||
it.toString(),
|
||||
Random.nextUInt().toString()
|
||||
)
|
||||
}
|
||||
whenever(mockLogs.logs).thenReturn(newEvents)
|
||||
|
||||
val mockLogsRound2 = mock<EthLog>()
|
||||
val newEventsRound2 = (blockInitialEventIsOn + 100..blockInitialEventIsOn + 120).map {
|
||||
createRandomSendEvent(
|
||||
it.toString(),
|
||||
Random.nextUInt().toString()
|
||||
)
|
||||
}
|
||||
whenever(mockLogsRound2.logs).thenReturn(newEventsRound2)
|
||||
whenever(l1ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogsRound2)
|
||||
|
||||
val eventMockLogs = mock<EthLog>()
|
||||
val events = createRandomSendEvent(blockInitialEventIsOn.toString(), Random.nextUInt().toString())
|
||||
whenever(eventMockLogs.logs).thenReturn(listOf(events))
|
||||
whenever(l1ClientMock.ethGetLogs(buildMessageHashEventFilter(messageHash)).send()).thenReturn(eventMockLogs)
|
||||
.thenReturn(mockLogs).thenReturn(mockLogsRound2)
|
||||
|
||||
l1EventQuerier
|
||||
.getSendMessageEventsForAnchoredMessage(MessageHashAnchoredEvent(messageHash))
|
||||
.thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it.count()).isEqualTo(100)
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
private fun createRandomSendEvent(blockNumber: String, logIndex: String): LogResult<Log> {
|
||||
val log = Log()
|
||||
val eventSignature: String = EventEncoder.encode(LineaRollupV6.MESSAGESENT_EVENT)
|
||||
val messageHashValue = Bytes32.random()
|
||||
val messageHash = org.web3j.abi.datatypes.generated.Bytes32(messageHashValue.toArray())
|
||||
|
||||
log.topics =
|
||||
listOf(
|
||||
eventSignature,
|
||||
TypeEncoder.encode(testFromAddress),
|
||||
TypeEncoder.encode(testToAddress),
|
||||
TypeEncoder.encode(messageHash)
|
||||
)
|
||||
|
||||
log.data =
|
||||
FunctionEncoder.encodeConstructor(
|
||||
listOf(
|
||||
testFee,
|
||||
testValue,
|
||||
testNonce,
|
||||
callData
|
||||
)
|
||||
)
|
||||
|
||||
log.setBlockNumber(blockNumber)
|
||||
log.setLogIndex(logIndex)
|
||||
|
||||
return LogResult<Log> { log }
|
||||
}
|
||||
|
||||
private fun buildMessageHashEventFilter(messageHash: Bytes32): EthFilter {
|
||||
val messageHashFilter =
|
||||
EthFilter(
|
||||
DefaultBlockParameter.valueOf(earliestL1Block),
|
||||
DefaultBlockParameter.valueOf(BigInteger.valueOf(20)),
|
||||
testContractAddress
|
||||
)
|
||||
|
||||
messageHashFilter.addOptionalTopics(messageHash.toString())
|
||||
|
||||
return messageHashFilter
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.junit5.Timeout
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import io.vertx.junit5.VertxTestContext
|
||||
import linea.kotlin.toHexString
|
||||
import linea.kotlin.toULong
|
||||
import linea.web3j.gas.EIP1559GasProvider
|
||||
import linea.web3j.transactionmanager.AsyncFriendlyTransactionManager
|
||||
import net.consensys.linea.contract.l2.L2MessageServiceGasLimitEstimate
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.web3j.crypto.Hash
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import org.web3j.protocol.core.Request
|
||||
import org.web3j.protocol.core.methods.response.EthBlock
|
||||
import org.web3j.protocol.core.methods.response.EthEstimateGas
|
||||
import org.web3j.protocol.core.methods.response.EthFeeHistory
|
||||
import org.web3j.protocol.core.methods.response.EthGasPrice
|
||||
import org.web3j.protocol.core.methods.response.EthSendTransaction
|
||||
import org.web3j.protocol.core.methods.response.TransactionReceipt
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class L2MessageAnchorerGasLimitEstimationTest {
|
||||
|
||||
private val testContractAddress = "0x6d976c9b8ceee705d4fe8699b44e5eb58242f484"
|
||||
private val latestBlockNumber = 12345
|
||||
private val transactionBlockNumber = 12340
|
||||
private val pollingInterval = 10.milliseconds
|
||||
private val gasEstimationPercentile = 0.5
|
||||
private val gasLimit = 25_000_000uL
|
||||
private val feeHistoryBlockCount = 4u
|
||||
|
||||
// private val feeHistoryRewardPercentile = 15.0
|
||||
private val maxFeePerGasCap = 10000uL
|
||||
private val retryCount = 10u
|
||||
private val finalisedBlockDistance = latestBlockNumber.minus(transactionBlockNumber).toLong()
|
||||
|
||||
private val txHash = "0xfa41235fcc064e57ab2566d65732a25a24b36ff6edba3cdd5eb482071b435906"
|
||||
private val txGasLimitUsed = BigInteger.valueOf(2_000_000)
|
||||
|
||||
@Test
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun messageAnchoringUpdatesAndResetsGasLimitCap(vertx: Vertx, testContext: VertxTestContext) {
|
||||
val mockReceipt = mock<TransactionReceipt>()
|
||||
whenever(mockReceipt.isStatusOK).thenReturn(true)
|
||||
whenever(mockReceipt.transactionHash).thenReturn(txHash)
|
||||
whenever(mockReceipt.blockNumber).thenReturn(BigInteger.valueOf(transactionBlockNumber.toLong()))
|
||||
whenever(mockReceipt.gasUsed).thenReturn(txGasLimitUsed)
|
||||
|
||||
val l2ClientMock = createMockedWeb3jClient(mockReceipt, transactionBlockNumber, latestBlockNumber, 1337)
|
||||
val l2TransactionManager = createMockedTransactionManager(mockReceipt)
|
||||
val messageManager =
|
||||
createL2MessageServiceContractWithAsyncFriendlyTransactionManager(l2ClientMock, l2TransactionManager)
|
||||
|
||||
val testEvents = createRandomSendMessageEvents(11UL)
|
||||
|
||||
val l2MessageAnchorer = L2MessageAnchorerImpl(
|
||||
vertx,
|
||||
l2ClientMock,
|
||||
messageManager,
|
||||
L2MessageAnchorerImpl.Config(
|
||||
pollingInterval,
|
||||
retryCount,
|
||||
finalisedBlockDistance
|
||||
)
|
||||
)
|
||||
|
||||
l2MessageAnchorer.anchorMessages(
|
||||
testEvents,
|
||||
Bytes32.ZERO.toArray()
|
||||
)
|
||||
.thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
Assertions.assertThat(it).isNotNull
|
||||
Assertions.assertThat(it.gasUsed).isEqualTo(txGasLimitUsed)
|
||||
}
|
||||
.completeNow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createL2MessageServiceContractWithAsyncFriendlyTransactionManager(
|
||||
l2Web3jClient: Web3j,
|
||||
l2TransactionManager: AsyncFriendlyTransactionManager
|
||||
): L2MessageServiceGasLimitEstimate {
|
||||
return L2MessageServiceGasLimitEstimate.load(
|
||||
testContractAddress,
|
||||
l2Web3jClient,
|
||||
l2TransactionManager,
|
||||
EIP1559GasProvider(
|
||||
l2Web3jClient,
|
||||
EIP1559GasProvider.Config(gasLimit, maxFeePerGasCap, feeHistoryBlockCount, gasEstimationPercentile)
|
||||
),
|
||||
emptyMap()
|
||||
)
|
||||
}
|
||||
|
||||
private fun createRandomHashes(numberOfRandomHashes: Int): List<Bytes32> {
|
||||
return (0..numberOfRandomHashes)
|
||||
.map { Bytes32.random() }
|
||||
}
|
||||
|
||||
private fun createMockedWeb3jClient(
|
||||
expectedTransactionReceipt: TransactionReceipt,
|
||||
txBlockNumber: Int,
|
||||
currentBlockNumber: Int,
|
||||
chainId: Int
|
||||
): Web3j {
|
||||
val web3jClient = mock<Web3j>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS)
|
||||
val ethBlock = mock<EthBlock>()
|
||||
val block = mock<EthBlock.Block>()
|
||||
whenever(ethBlock.block).thenReturn(block)
|
||||
whenever(block.number).thenReturn(BigInteger.valueOf(txBlockNumber.toLong()))
|
||||
.thenReturn(BigInteger.valueOf(currentBlockNumber.toLong()))
|
||||
|
||||
whenever(web3jClient.ethGetBlockByNumber(any(), any()).sendAsync())
|
||||
.thenAnswer { SafeFuture.completedFuture(ethBlock) }
|
||||
|
||||
whenever(
|
||||
web3jClient
|
||||
.ethFeeHistory(
|
||||
ArgumentMatchers.eq(4),
|
||||
ArgumentMatchers.eq(DefaultBlockParameter.valueOf("latest")),
|
||||
ArgumentMatchers.eq(listOf(gasEstimationPercentile))
|
||||
)
|
||||
.sendAsync()
|
||||
)
|
||||
.thenAnswer {
|
||||
val feeHistoryResponse = EthFeeHistory()
|
||||
val feeHistory = EthFeeHistory.FeeHistory()
|
||||
feeHistory.setReward(mutableListOf(mutableListOf("0x1000")))
|
||||
feeHistory.setBaseFeePerGas(mutableListOf("0x100"))
|
||||
feeHistory.setOldestBlock(BigInteger.valueOf(currentBlockNumber.toLong() - 1).toULong().toHexString())
|
||||
feeHistory.gasUsedRatio = listOf(1.0)
|
||||
feeHistoryResponse.result = feeHistory
|
||||
SafeFuture.completedFuture(feeHistoryResponse)
|
||||
}
|
||||
whenever(web3jClient.ethGasPrice().sendAsync()).thenAnswer {
|
||||
val gasPriceResponse = EthGasPrice()
|
||||
gasPriceResponse.result = "0x100"
|
||||
SafeFuture.completedFuture(gasPriceResponse)
|
||||
}
|
||||
whenever(web3jClient.ethEstimateGas(any()).sendAsync()).thenAnswer {
|
||||
val estimateGasResponse = EthEstimateGas()
|
||||
estimateGasResponse.result = "0x1E8480"
|
||||
SafeFuture.completedFuture(estimateGasResponse)
|
||||
}
|
||||
|
||||
val sendTransactionResponse = EthSendTransaction()
|
||||
val expectedTransactionHash = txHash
|
||||
sendTransactionResponse.result = expectedTransactionHash
|
||||
whenever(web3jClient.ethSendTransaction(any())).thenAnswer {
|
||||
val hashToReturn = Hash.sha3(it.arguments[0] as String)
|
||||
sendTransactionResponse.result = hashToReturn
|
||||
val requestMock = mock<Request<*, EthSendTransaction>>()
|
||||
whenever(requestMock.send()).thenReturn(sendTransactionResponse)
|
||||
requestMock
|
||||
}
|
||||
whenever(web3jClient.ethSendRawTransaction(any())).thenAnswer {
|
||||
val hashToReturn = Hash.sha3(it.arguments[0] as String)
|
||||
sendTransactionResponse.result = hashToReturn
|
||||
val requestMock = mock<Request<*, EthSendTransaction>>()
|
||||
whenever(requestMock.send()).thenReturn(sendTransactionResponse)
|
||||
requestMock
|
||||
}
|
||||
whenever(web3jClient.ethGetTransactionReceipt(any()).send().transactionReceipt)
|
||||
.thenReturn(Optional.of(expectedTransactionReceipt))
|
||||
whenever(web3jClient.ethChainId().send().chainId)
|
||||
.thenReturn(BigInteger.valueOf(chainId.toLong()))
|
||||
|
||||
return web3jClient
|
||||
}
|
||||
|
||||
private fun createMockedTransactionManager(
|
||||
expectedTransactionReceipt: TransactionReceipt
|
||||
): AsyncFriendlyTransactionManager {
|
||||
val transactionManager = Mockito.mock(
|
||||
AsyncFriendlyTransactionManager::class.java
|
||||
) { invocation ->
|
||||
if ("executeTransactionEIP1559" == invocation?.method?.name) {
|
||||
expectedTransactionReceipt
|
||||
} else {
|
||||
Mockito.RETURNS_DEFAULTS.answer(invocation)
|
||||
}
|
||||
}
|
||||
whenever(transactionManager.fromAddress).thenReturn(testContractAddress)
|
||||
whenever(transactionManager.resetNonce()).thenReturn(SafeFuture.completedFuture(Unit))
|
||||
whenever(transactionManager.currentNonce()).thenReturn(BigInteger.ZERO)
|
||||
|
||||
return transactionManager
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.junit5.Timeout
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import io.vertx.junit5.VertxTestContext
|
||||
import linea.kotlin.toHexString
|
||||
import linea.kotlin.toULong
|
||||
import linea.web3j.gas.EIP1559GasProvider
|
||||
import net.consensys.linea.contract.L2MessageService
|
||||
import net.consensys.zkevm.ethereum.signing.ECKeypairSigner
|
||||
import net.consensys.zkevm.ethereum.signing.ECKeypairSignerAdapter
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.RepeatedTest
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.web3j.crypto.Credentials
|
||||
import org.web3j.crypto.Hash
|
||||
import org.web3j.crypto.Keys
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import org.web3j.protocol.core.Request
|
||||
import org.web3j.protocol.core.methods.response.EthBlock
|
||||
import org.web3j.protocol.core.methods.response.EthFeeHistory
|
||||
import org.web3j.protocol.core.methods.response.EthGasPrice
|
||||
import org.web3j.protocol.core.methods.response.EthSendTransaction
|
||||
import org.web3j.protocol.core.methods.response.TransactionReceipt
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class L2MessageAnchorerImplTest {
|
||||
private val testContractAddress = "0x6d976c9b8ceee705d4fe8699b44e5eb58242f484"
|
||||
private val latestBlockNumber = 12345
|
||||
private val transactionBlockNumber = 12340
|
||||
private val keyPair = Keys.createEcKeyPair()
|
||||
private val signer = ECKeypairSigner(keyPair)
|
||||
private val pollingInterval = 10.milliseconds
|
||||
private val gasEstimationPercentile = 0.5
|
||||
private val gasLimit = 100uL
|
||||
private val feeHistoryBlockCount = 4u
|
||||
private val maxFeePerGasCap = 10000uL
|
||||
private val retryCount = 10u
|
||||
private val finalisedBlockDistance = latestBlockNumber.minus(transactionBlockNumber).toLong()
|
||||
|
||||
private val txHash = "0xfa41235fcc064e57ab2566d65732a25a24b36ff6edba3cdd5eb482071b435906"
|
||||
|
||||
@RepeatedTest(10)
|
||||
@Timeout(10, timeUnit = TimeUnit.SECONDS)
|
||||
fun messageAnchoring_returnsTransactionReceipt(vertx: Vertx, testContext: VertxTestContext) {
|
||||
val mockReceipt = mock<TransactionReceipt>()
|
||||
whenever(mockReceipt.isStatusOK).thenReturn(true)
|
||||
whenever(mockReceipt.transactionHash).thenReturn(txHash)
|
||||
whenever(mockReceipt.blockNumber).thenReturn(BigInteger.valueOf(transactionBlockNumber.toLong()))
|
||||
|
||||
val l2ClientMock = createMockedWeb3jClient(mockReceipt, transactionBlockNumber, latestBlockNumber, 1337)
|
||||
val messageManager = createL2MessageServiceContractWithSimpleKeypairSigner(l2ClientMock)
|
||||
|
||||
val testEvents = createRandomSendMessageEvents(11UL)
|
||||
|
||||
val l2MessageAnchorerImpl =
|
||||
L2MessageAnchorerImpl(
|
||||
vertx,
|
||||
l2ClientMock,
|
||||
messageManager,
|
||||
L2MessageAnchorerImpl.Config(
|
||||
pollingInterval,
|
||||
retryCount,
|
||||
finalisedBlockDistance
|
||||
)
|
||||
)
|
||||
|
||||
l2MessageAnchorerImpl.anchorMessages(
|
||||
testEvents,
|
||||
Bytes32.ZERO.toArray()
|
||||
)
|
||||
.thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it.blockNumber).isEqualTo(mockReceipt.blockNumber)
|
||||
assertThat(it.transactionHash).isEqualTo(mockReceipt.transactionHash)
|
||||
}
|
||||
.completeNow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createL2MessageServiceContractWithSimpleKeypairSigner(
|
||||
l2Web3jClient: Web3j
|
||||
): L2MessageService {
|
||||
val signerAdapter = ECKeypairSignerAdapter(signer, keyPair.publicKey)
|
||||
val credentials = Credentials.create(signerAdapter)
|
||||
return L2MessageService.load(
|
||||
testContractAddress,
|
||||
l2Web3jClient,
|
||||
credentials,
|
||||
EIP1559GasProvider(
|
||||
l2Web3jClient,
|
||||
EIP1559GasProvider.Config(gasLimit, maxFeePerGasCap, feeHistoryBlockCount, gasEstimationPercentile)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMockedWeb3jClient(
|
||||
expectedTransactionReceipt: TransactionReceipt,
|
||||
txBlockNumber: Int,
|
||||
currentBlockNumber: Int,
|
||||
chainId: Int
|
||||
): Web3j {
|
||||
val web3jClient = mock<Web3j>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS)
|
||||
val ethBlock = mock<EthBlock>()
|
||||
val block = mock<EthBlock.Block>()
|
||||
whenever(ethBlock.block).thenReturn(block)
|
||||
whenever(block.number).thenReturn(BigInteger.valueOf(txBlockNumber.toLong()))
|
||||
.thenReturn(BigInteger.valueOf(currentBlockNumber.toLong()))
|
||||
|
||||
whenever(web3jClient.ethGetBlockByNumber(any(), any()).sendAsync())
|
||||
.thenAnswer { SafeFuture.completedFuture(ethBlock) }
|
||||
|
||||
whenever(
|
||||
web3jClient
|
||||
.ethFeeHistory(
|
||||
ArgumentMatchers.eq(4),
|
||||
ArgumentMatchers.eq(DefaultBlockParameter.valueOf("latest")),
|
||||
ArgumentMatchers.eq(listOf(gasEstimationPercentile))
|
||||
)
|
||||
.sendAsync()
|
||||
)
|
||||
.thenAnswer {
|
||||
val feeHistoryResponse = EthFeeHistory()
|
||||
val feeHistory = EthFeeHistory.FeeHistory()
|
||||
feeHistory.setReward(mutableListOf(mutableListOf("0x1000")))
|
||||
feeHistory.setBaseFeePerGas(mutableListOf("0x100"))
|
||||
feeHistory.setOldestBlock(BigInteger.valueOf(currentBlockNumber.toLong() - 1).toULong().toHexString())
|
||||
feeHistory.gasUsedRatio = listOf(1.0)
|
||||
feeHistoryResponse.result = feeHistory
|
||||
SafeFuture.completedFuture(feeHistoryResponse)
|
||||
}
|
||||
whenever(web3jClient.ethGasPrice().sendAsync()).thenAnswer {
|
||||
val gasPriceResponse = EthGasPrice()
|
||||
gasPriceResponse.result = "0x100"
|
||||
SafeFuture.completedFuture(gasPriceResponse)
|
||||
}
|
||||
val sendTransactionResponse = EthSendTransaction()
|
||||
val expectedTransactionHash = txHash
|
||||
sendTransactionResponse.result = expectedTransactionHash
|
||||
whenever(web3jClient.ethSendRawTransaction(any())).thenAnswer {
|
||||
val hashToReturn = Hash.sha3(it.arguments[0] as String)
|
||||
sendTransactionResponse.result = hashToReturn
|
||||
val requestMock = mock<Request<*, EthSendTransaction>>()
|
||||
whenever(requestMock.send()).thenReturn(sendTransactionResponse)
|
||||
requestMock
|
||||
}
|
||||
whenever(web3jClient.ethGetTransactionReceipt(any()).send().transactionReceipt)
|
||||
.thenReturn(Optional.of(expectedTransactionReceipt))
|
||||
whenever(web3jClient.ethChainId().send().chainId)
|
||||
.thenReturn(BigInteger.valueOf(chainId.toLong()))
|
||||
|
||||
return web3jClient
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.junit5.Timeout
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import io.vertx.junit5.VertxTestContext
|
||||
import net.consensys.linea.contract.L2MessageService
|
||||
import net.consensys.linea.contract.L2MessageService.L1L2MESSAGEHASHESADDEDTOINBOX_EVENT
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.RepeatedTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.web3j.abi.EventEncoder
|
||||
import org.web3j.abi.FunctionEncoder
|
||||
import org.web3j.abi.FunctionReturnDecoder
|
||||
import org.web3j.abi.datatypes.DynamicArray
|
||||
import org.web3j.crypto.Credentials
|
||||
import org.web3j.crypto.Keys
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.methods.response.EthBlockNumber
|
||||
import org.web3j.protocol.core.methods.response.EthCall
|
||||
import org.web3j.protocol.core.methods.response.EthLog
|
||||
import org.web3j.protocol.core.methods.response.Log
|
||||
import org.web3j.tx.gas.DefaultGasProvider
|
||||
import java.math.BigInteger
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.max
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class L2QuerierImplTest {
|
||||
private val testContractAddress = "0x6d976c9b8ceee705d4fe8699b44e5eb58242f484"
|
||||
private val blockNumber = 13
|
||||
private val keyPair = Keys.createEcKeyPair()
|
||||
|
||||
@RepeatedTest(10)
|
||||
@Timeout(5, timeUnit = TimeUnit.SECONDS)
|
||||
fun findLastFinalizedAnchoredEvent_returnsTheLastEvent(vertx: Vertx, testContext: VertxTestContext) {
|
||||
val l2ClientMock = mock<Web3j>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS)
|
||||
whenever(l2ClientMock.ethBlockNumber().send().blockNumber)
|
||||
.thenReturn(BigInteger.valueOf(blockNumber.toLong()))
|
||||
val randomEvents =
|
||||
listOf(
|
||||
createRandomEventWithHashes(1),
|
||||
createRandomEventWithHashes(2),
|
||||
createRandomEventWithHashes(3)
|
||||
)
|
||||
val lastEventData = randomEvents.last().data
|
||||
val expectedHash =
|
||||
lastEventData.substring(lastEventData.length - Bytes32.ZERO.toUnprefixedHexString().length)
|
||||
|
||||
val mockLogs = mock<EthLog>()
|
||||
val logResults: List<EthLog.LogResult<Log>> = randomEvents.map { EthLog.LogResult { it } }
|
||||
whenever(mockLogs.logs).thenReturn(logResults)
|
||||
whenever(l2ClientMock.ethGetLogs(any()).send()).thenReturn(mockLogs)
|
||||
|
||||
val credentials = Credentials.create(keyPair)
|
||||
val messageManager =
|
||||
L2MessageService.load(testContractAddress, l2ClientMock, credentials, DefaultGasProvider())
|
||||
val l2Querier =
|
||||
L2QuerierImpl(
|
||||
l2Client = l2ClientMock,
|
||||
messageService = messageManager,
|
||||
config = L2QuerierImpl.Config(
|
||||
blocksToFinalizationL2 = 1u,
|
||||
lastHashSearchWindow = 1u,
|
||||
contractAddressToListen = testContractAddress
|
||||
),
|
||||
vertx = vertx
|
||||
)
|
||||
l2Querier.findLastFinalizedAnchoredEvent().thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it!!.messageHash).isEqualTo(Bytes32.fromHexString(expectedHash))
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException { testContext.failNow(it) }
|
||||
}
|
||||
|
||||
@RepeatedTest(10)
|
||||
@Timeout(1, timeUnit = TimeUnit.SECONDS)
|
||||
fun findLastFinalizedAnchoredEvent_isAbleToFindEventsInThePast(
|
||||
vertx: Vertx,
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
val l2ClientMock = mock<Web3j>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS)
|
||||
whenever(l2ClientMock.ethBlockNumber().send().blockNumber)
|
||||
.thenReturn(BigInteger.valueOf(blockNumber.toLong()))
|
||||
|
||||
val randomEventsForRequests = createRandomEventBatches(6, 4, 6)
|
||||
val lastEventData = randomEventsForRequests.last().last().data
|
||||
|
||||
val expectedHash =
|
||||
lastEventData.substring(lastEventData.length - Bytes32.ZERO.toUnprefixedHexString().length)
|
||||
|
||||
val mockLogs = mock<EthLog>()
|
||||
val logResults: List<EthLog.LogResult<Log>> = randomEventsForRequests.last().map { EthLog.LogResult { it } }
|
||||
whenever(mockLogs.logs).thenReturn(logResults)
|
||||
|
||||
val emptyEvents: List<EthLog.LogResult<Log>> = listOf()
|
||||
val emptyMockLogs = mock<EthLog>()
|
||||
whenever(emptyMockLogs.logs).thenReturn(emptyEvents)
|
||||
|
||||
whenever(l2ClientMock.ethGetLogs(any()).send()).thenAnswer {
|
||||
emptyMockLogs
|
||||
}.thenAnswer {
|
||||
mockLogs
|
||||
}
|
||||
|
||||
val credentials = Credentials.create(keyPair)
|
||||
val messageManager =
|
||||
L2MessageService.load(testContractAddress, l2ClientMock, credentials, DefaultGasProvider())
|
||||
|
||||
val l2Querier =
|
||||
L2QuerierImpl(
|
||||
l2Client = l2ClientMock,
|
||||
messageService = messageManager,
|
||||
config = L2QuerierImpl.Config(
|
||||
blocksToFinalizationL2 = 1u,
|
||||
lastHashSearchWindow = 5u,
|
||||
contractAddressToListen = testContractAddress
|
||||
),
|
||||
vertx = vertx
|
||||
)
|
||||
l2Querier.findLastFinalizedAnchoredEvent().thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it!!.messageHash).isEqualTo(Bytes32.fromHexString(expectedHash))
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException { testContext.failNow(it) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(1, timeUnit = TimeUnit.SECONDS)
|
||||
fun getMessageHashStatus(
|
||||
vertx: Vertx,
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
val l2ClientMock = mock<Web3j>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS)
|
||||
val credentials = Credentials.create(keyPair)
|
||||
val messageManager =
|
||||
L2MessageService.load(testContractAddress, l2ClientMock, credentials, DefaultGasProvider())
|
||||
|
||||
val mockBlockNumberReturn = mock<EthBlockNumber>()
|
||||
whenever(mockBlockNumberReturn.blockNumber).thenReturn(BigInteger.valueOf(blockNumber.toLong()))
|
||||
whenever(l2ClientMock.ethBlockNumber().sendAsync())
|
||||
.thenReturn(CompletableFuture.completedFuture(mockBlockNumberReturn))
|
||||
|
||||
val l2Querier =
|
||||
L2QuerierImpl(
|
||||
l2Client = l2ClientMock,
|
||||
messageService = messageManager,
|
||||
config = L2QuerierImpl.Config(
|
||||
blocksToFinalizationL2 = 1u,
|
||||
lastHashSearchWindow = 1u,
|
||||
contractAddressToListen = testContractAddress
|
||||
),
|
||||
vertx = vertx
|
||||
)
|
||||
|
||||
val messageHash = Bytes32.random()
|
||||
val mockEthCall = mock<EthCall>()
|
||||
whenever(mockEthCall.value).thenReturn("0x0000000000000000000000000000000000000000000000000000000000000001")
|
||||
whenever(l2ClientMock.ethCall(any(), any()).send()).thenReturn(mockEthCall)
|
||||
|
||||
l2Querier.getMessageHashStatus(messageHash).thenApply {
|
||||
testContext
|
||||
.verify {
|
||||
assertThat(it).isNotNull
|
||||
assertThat(it!!).isEqualTo(BigInteger.valueOf(1))
|
||||
}
|
||||
.completeNow()
|
||||
}.whenException { testContext.failNow(it) }
|
||||
}
|
||||
|
||||
private fun createRandomEventBatches(
|
||||
numberOfBatches: Int,
|
||||
maxEventsPerBatch: Int,
|
||||
maxHashesPerEvent: Int
|
||||
): List<List<Log>> {
|
||||
return (1..numberOfBatches).map {
|
||||
val eventsToGenerate = max(Random().nextInt(maxEventsPerBatch), 1)
|
||||
(1..eventsToGenerate).map { createRandomEventWithHashes(maxHashesPerEvent) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRandomEventWithHashes(numberOfRandomHashes: Int): Log {
|
||||
val log = Log()
|
||||
val randomHashes =
|
||||
(0..numberOfRandomHashes)
|
||||
.map { Bytes32.random() }
|
||||
.map { org.web3j.abi.datatypes.generated.Bytes32(it.toArray()) }
|
||||
val eventSignature = EventEncoder.encode(L1L2MESSAGEHASHESADDEDTOINBOX_EVENT)
|
||||
|
||||
log.topics = listOf(eventSignature)
|
||||
val data = DynamicArray(org.web3j.abi.datatypes.generated.Bytes32::class.java, randomHashes)
|
||||
log.data = FunctionEncoder.encodeConstructor(listOf(data))
|
||||
FunctionReturnDecoder.decode(log.data, L1L2MESSAGEHASHESADDEDTOINBOX_EVENT.nonIndexedParameters)
|
||||
return log
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package net.consensys.zkevm.ethereum.coordination.messageanchoring
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.junit5.Timeout
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import io.vertx.junit5.VertxTestContext
|
||||
import linea.web3j.transactionmanager.AsyncFriendlyTransactionManager
|
||||
import net.consensys.linea.contract.L2MessageService
|
||||
import net.consensys.zkevm.coordinator.clients.smartcontract.LineaRollupSmartContractClient
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.mockito.Mockito.RETURNS_DEEP_STUBS
|
||||
import org.mockito.Mockito.atLeastOnce
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.never
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.web3j.protocol.core.methods.response.TransactionReceipt
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class MessageAnchoringServiceTest {
|
||||
private lateinit var mockTransactionManager: AsyncFriendlyTransactionManager
|
||||
private lateinit var mockL1Querier: L1EventQuerier
|
||||
private lateinit var mockL2MessageAnchorer: L2MessageAnchorer
|
||||
private lateinit var mockL2Querier: L2Querier
|
||||
private lateinit var l2MessageServiceContractClient: L2MessageService
|
||||
private lateinit var rollupSmartContractClient: LineaRollupSmartContractClient
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
mockTransactionManager = mock<AsyncFriendlyTransactionManager>(defaultAnswer = RETURNS_DEEP_STUBS)
|
||||
mockL1Querier = mock<L1EventQuerier>(defaultAnswer = RETURNS_DEEP_STUBS)
|
||||
mockL2MessageAnchorer = mock<L2MessageAnchorer>(defaultAnswer = RETURNS_DEEP_STUBS)
|
||||
mockL2Querier = mock<L2Querier>(defaultAnswer = RETURNS_DEEP_STUBS)
|
||||
l2MessageServiceContractClient = mock<L2MessageService>(defaultAnswer = RETURNS_DEEP_STUBS)
|
||||
rollupSmartContractClient = mock<LineaRollupSmartContractClient>()
|
||||
}
|
||||
|
||||
private fun createMessageAnchoringService(
|
||||
vertx: Vertx,
|
||||
maxMessagesToAnchor: UInt
|
||||
): MessageAnchoringService {
|
||||
return MessageAnchoringService(
|
||||
MessageAnchoringService.Config(
|
||||
pollingInterval = 10.milliseconds,
|
||||
maxMessagesToAnchor = maxMessagesToAnchor
|
||||
),
|
||||
vertx,
|
||||
mockL1Querier,
|
||||
mockL2MessageAnchorer,
|
||||
mockL2Querier,
|
||||
rollupSmartContractClient,
|
||||
l2MessageServiceContractClient,
|
||||
mockTransactionManager
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(4, timeUnit = TimeUnit.SECONDS)
|
||||
fun start_startsPollingProcessForMessagesUsingRollingHash(vertx: Vertx, testContext: VertxTestContext) {
|
||||
val maxMessagesToAnchor = 100u
|
||||
|
||||
whenever(l2MessageServiceContractClient.INBOX_STATUS_UNKNOWN().send()).thenReturn(BigInteger.valueOf(0))
|
||||
|
||||
val foundAnchoredHashEvent = MessageHashAnchoredEvent(Bytes32.random())
|
||||
val events = listOf(SendMessageEvent(Bytes32.random(), 10UL, 10UL))
|
||||
val mockTransactionReceipt = mock<TransactionReceipt>()
|
||||
|
||||
whenever(mockL2Querier.findLastFinalizedAnchoredEvent()).thenReturn(
|
||||
SafeFuture.completedFuture(foundAnchoredHashEvent)
|
||||
)
|
||||
whenever(mockL1Querier.getSendMessageEventsForAnchoredMessage(foundAnchoredHashEvent)).thenReturn(
|
||||
SafeFuture.completedFuture(events)
|
||||
)
|
||||
whenever(rollupSmartContractClient.getMessageRollingHash(any(), any())).thenReturn(
|
||||
SafeFuture.completedFuture(Bytes32.ZERO.toArray())
|
||||
)
|
||||
whenever(mockL2Querier.getMessageHashStatus(events.first().messageHash)).thenReturn(
|
||||
SafeFuture.completedFuture(BigInteger.valueOf(0))
|
||||
)
|
||||
whenever(
|
||||
mockL2MessageAnchorer.anchorMessages(any(), any())
|
||||
).thenReturn(
|
||||
SafeFuture.completedFuture(mockTransactionReceipt)
|
||||
)
|
||||
whenever(mockTransactionReceipt.transactionHash).thenReturn(
|
||||
Bytes32.random().toHexString()
|
||||
)
|
||||
|
||||
whenever(mockTransactionManager.resetNonce()).thenAnswer { SafeFuture.completedFuture(Unit) }
|
||||
|
||||
val monitor = createMessageAnchoringService(vertx, maxMessagesToAnchor)
|
||||
|
||||
monitor.start().thenApply {
|
||||
vertx.setTimer(
|
||||
100
|
||||
) {
|
||||
monitor.stop().thenApply {
|
||||
testContext.verify {
|
||||
verify(mockL2Querier, atLeastOnce()).findLastFinalizedAnchoredEvent()
|
||||
verify(mockL1Querier, atLeastOnce()).getSendMessageEventsForAnchoredMessage(foundAnchoredHashEvent)
|
||||
verify(mockL2Querier, atLeastOnce()).getMessageHashStatus(events.first().messageHash)
|
||||
verify(mockTransactionReceipt, atLeastOnce()).transactionHash
|
||||
verify(rollupSmartContractClient, atLeastOnce()).getMessageRollingHash(messageNumber = 10L)
|
||||
verify(mockL2MessageAnchorer, atLeastOnce()).anchorMessages(
|
||||
events,
|
||||
Bytes32.ZERO.toArray()
|
||||
)
|
||||
verify(mockTransactionManager, atLeastOnce()).resetNonce()
|
||||
}
|
||||
.completeNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(4, timeUnit = TimeUnit.SECONDS)
|
||||
fun start_startsPollingProcessForMessagesUsingRollingHashAndLimitsReturnedEvents(
|
||||
vertx: Vertx,
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
val maxMessagesToAnchor = 2u
|
||||
|
||||
whenever(l2MessageServiceContractClient.INBOX_STATUS_UNKNOWN().send()).thenReturn(BigInteger.valueOf(0))
|
||||
|
||||
val foundAnchoredHashEvent = MessageHashAnchoredEvent(Bytes32.random())
|
||||
val events = listOf(
|
||||
SendMessageEvent(Bytes32.random(), 1UL, 1UL),
|
||||
SendMessageEvent(Bytes32.random(), 2UL, 2UL),
|
||||
SendMessageEvent(Bytes32.random(), 3UL, 3UL),
|
||||
SendMessageEvent(Bytes32.random(), 4UL, 4UL)
|
||||
)
|
||||
|
||||
val mockTransactionReceipt = mock<TransactionReceipt>()
|
||||
|
||||
whenever(mockL2Querier.findLastFinalizedAnchoredEvent()).thenReturn(
|
||||
SafeFuture.completedFuture(foundAnchoredHashEvent)
|
||||
)
|
||||
whenever(mockL1Querier.getSendMessageEventsForAnchoredMessage(foundAnchoredHashEvent)).thenReturn(
|
||||
SafeFuture.completedFuture(events)
|
||||
)
|
||||
whenever(rollupSmartContractClient.getMessageRollingHash(any(), any())).thenReturn(
|
||||
SafeFuture.completedFuture(Bytes32.ZERO.toArray())
|
||||
)
|
||||
whenever(mockL2Querier.getMessageHashStatus(any())).thenReturn(
|
||||
SafeFuture.completedFuture(BigInteger.valueOf(0))
|
||||
)
|
||||
whenever(
|
||||
mockL2MessageAnchorer.anchorMessages(any(), any())
|
||||
).thenReturn(
|
||||
SafeFuture.completedFuture(mockTransactionReceipt)
|
||||
)
|
||||
whenever(mockTransactionReceipt.transactionHash).thenReturn(
|
||||
Bytes32.random().toHexString()
|
||||
)
|
||||
|
||||
whenever(mockTransactionManager.resetNonce()).thenAnswer { SafeFuture.completedFuture(Unit) }
|
||||
|
||||
val monitor = createMessageAnchoringService(vertx, maxMessagesToAnchor)
|
||||
|
||||
monitor.start().thenApply {
|
||||
vertx.setTimer(
|
||||
100
|
||||
) {
|
||||
monitor.stop().thenApply {
|
||||
testContext.verify {
|
||||
verify(mockL2MessageAnchorer, atLeastOnce()).anchorMessages(
|
||||
events.take(2),
|
||||
Bytes32.ZERO.toArray()
|
||||
)
|
||||
verify(mockL2MessageAnchorer, never()).anchorMessages(
|
||||
events.take(4),
|
||||
Bytes32.ZERO.toArray()
|
||||
)
|
||||
verify(mockL2Querier, atLeastOnce()).getMessageHashStatus(events.first().messageHash)
|
||||
verify(mockL2Querier, atLeastOnce()).getMessageHashStatus(events[1].messageHash)
|
||||
verify(mockL2Querier, atLeastOnce()).getMessageHashStatus(events[2].messageHash)
|
||||
verify(mockL2Querier, atLeastOnce()).getMessageHashStatus(events[3].messageHash)
|
||||
verify(mockL2Querier, atLeastOnce()).findLastFinalizedAnchoredEvent()
|
||||
verify(rollupSmartContractClient, atLeastOnce()).getMessageRollingHash(messageNumber = 2L)
|
||||
verify(mockL1Querier, atLeastOnce()).getSendMessageEventsForAnchoredMessage(foundAnchoredHashEvent)
|
||||
verify(mockTransactionReceipt, atLeastOnce()).transactionHash
|
||||
verify(mockTransactionManager, atLeastOnce()).resetNonce()
|
||||
}
|
||||
.completeNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package linea.anchoring.events
|
||||
package linea.contract.events
|
||||
|
||||
import linea.domain.EthLog
|
||||
import linea.domain.EthLogEvent
|
||||
@@ -1,4 +1,4 @@
|
||||
package linea.anchoring.events
|
||||
package linea.contract.events
|
||||
|
||||
import linea.domain.EthLog
|
||||
import linea.domain.EthLogEvent
|
||||
@@ -1,4 +1,4 @@
|
||||
package linea.anchoring.events
|
||||
package linea.contract.events
|
||||
|
||||
import linea.domain.EthLog
|
||||
import linea.domain.EthLogEvent
|
||||
@@ -8,26 +8,24 @@ import linea.kotlin.toULongFromLast8Bytes
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
* @notice Emitted when a message is sent.
|
||||
* @param _from The indexed sender address of the message (msg.sender).
|
||||
* @param _to The indexed intended recipient address of the message on the other layer.
|
||||
* @param _fee The fee being being paid to deliver the message to the recipient in Wei.
|
||||
* @param _value The value being sent to the recipient in Wei.
|
||||
* @param _nonce The unique message number.
|
||||
* @param _calldata The calldata being passed to the intended recipient when being called on claiming.
|
||||
* @param _messageHash The indexed hash of the message parameters.
|
||||
* @dev _calldata has the _ because calldata is a reserved word.
|
||||
* @dev We include the message hash to save hashing costs on the rollup.
|
||||
* Emitted when a message is sent.
|
||||
* @param messageNumber The unique message number.
|
||||
* @param from The indexed sender address of the message (msg.sender).
|
||||
* @param to The indexed intended recipient address of the message on the other layer.
|
||||
* @param fee fee being paid to deliver the message to the recipient in Wei.
|
||||
* @param value The value being sent to the recipient in Wei.
|
||||
* @param calldata The calldata being passed to the intended recipient when being called on claiming.
|
||||
* @param messageHash The indexed hash of the message parameters.
|
||||
* @dev This event is used on both L1 and L2.
|
||||
event MessageSent(
|
||||
address indexed _from,
|
||||
address indexed _to,
|
||||
uint256 _fee,
|
||||
uint256 _value,
|
||||
uint256 _nonce,
|
||||
bytes _calldata,
|
||||
bytes32 indexed _messageHash
|
||||
);
|
||||
* event MessageSent(
|
||||
* address indexed _from,
|
||||
* address indexed _to,
|
||||
* uint256 _fee,
|
||||
* uint256 _value, // messageNumber
|
||||
* uint256 _nonce,
|
||||
* bytes _calldata,
|
||||
* bytes32 indexed _messageHash
|
||||
* );
|
||||
*/
|
||||
data class MessageSentEvent(
|
||||
val messageNumber: ULong, // Unique message number
|
||||
@@ -92,6 +90,7 @@ data class MessageSentEvent(
|
||||
"to=${to.encodeHex()}, " +
|
||||
"fee=$fee, " +
|
||||
"value=$value, " +
|
||||
"nonce=$messageNumber, " +
|
||||
"calldata=${calldata.encodeHex()}, " +
|
||||
"messageHash=${messageHash.encodeHex()}" +
|
||||
")"
|
||||
@@ -1,10 +1,10 @@
|
||||
package linea.anchoring.events
|
||||
package linea.contract.events
|
||||
|
||||
import linea.domain.EthLog
|
||||
import linea.domain.EthLogEvent
|
||||
import linea.kotlin.decodeHex
|
||||
import linea.kotlin.toULongFromHex
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class L1RollingHashUpdatedEventTest {
|
||||
@@ -43,6 +43,6 @@ class L1RollingHashUpdatedEventTest {
|
||||
log = ethLog
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(expectedEthLogEvent)
|
||||
Assertions.assertThat(result).isEqualTo(expectedEthLogEvent)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package linea.anchoring.events
|
||||
package linea.contract.events
|
||||
|
||||
import linea.domain.EthLog
|
||||
import linea.domain.EthLogEvent
|
||||
import linea.kotlin.decodeHex
|
||||
import linea.kotlin.toULongFromHex
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class L2RollingHashUpdatedEventTest {
|
||||
@@ -40,6 +40,6 @@ class L2RollingHashUpdatedEventTest {
|
||||
log = ethLog
|
||||
)
|
||||
|
||||
assertThat(result).isEqualTo(expectedEthLogEvent)
|
||||
Assertions.assertThat(result).isEqualTo(expectedEthLogEvent)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
package linea.anchoring.events
|
||||
package linea.contract.events
|
||||
|
||||
import linea.domain.EthLog
|
||||
import linea.domain.EthLogEvent
|
||||
import linea.kotlin.decodeHex
|
||||
import linea.kotlin.toBigInteger
|
||||
import linea.kotlin.toULongFromHex
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigInteger
|
||||
import kotlin.text.compareTo
|
||||
|
||||
class MessageSentEventTest {
|
||||
|
||||
@@ -97,7 +99,7 @@ class MessageSentEventTest {
|
||||
val event1 = eventTemplate.copy(messageNumber = 1uL)
|
||||
val event2 = eventTemplate.copy(messageNumber = 2uL)
|
||||
|
||||
assertThat(event1.compareTo(event2)).isLessThan(0)
|
||||
Assertions.assertThat(event1.compareTo(event2)).isLessThan(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -105,7 +107,7 @@ class MessageSentEventTest {
|
||||
val event1 = eventTemplate.copy(messageNumber = 2uL)
|
||||
val event2 = eventTemplate.copy(messageNumber = 1uL)
|
||||
|
||||
assertThat(event1.compareTo(event2)).isGreaterThan(0)
|
||||
Assertions.assertThat(event1.compareTo(event2)).isGreaterThan(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -113,6 +115,6 @@ class MessageSentEventTest {
|
||||
val event1 = eventTemplate.copy(messageNumber = 2uL)
|
||||
val event2 = eventTemplate.copy(messageNumber = 2uL)
|
||||
|
||||
assertThat(event1.compareTo(event2)).isEqualTo(0)
|
||||
Assertions.assertThat(event1.compareTo(event2)).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package linea.anchoring
|
||||
package linea.contrat.events
|
||||
|
||||
import linea.anchoring.events.L1RollingHashUpdatedEvent
|
||||
import linea.anchoring.events.MessageSentEvent
|
||||
import linea.contract.events.L1RollingHashUpdatedEvent
|
||||
import linea.contract.events.MessageSentEvent
|
||||
import linea.domain.EthLog
|
||||
import linea.domain.EthLogEvent
|
||||
import linea.kotlin.decodeHex
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
description="Linea L1 smart contract client"
|
||||
description = "Linea L1 smart contract client"
|
||||
|
||||
dependencies {
|
||||
api project(':jvm-libs:linea:core:domain-models')
|
||||
@@ -11,9 +11,10 @@ dependencies {
|
||||
api project(':jvm-libs:generic:extensions:futures')
|
||||
api project(':jvm-libs:generic:extensions:kotlin')
|
||||
api 'build.linea:l1-rollup-contract-client:6.0.0-rc2'
|
||||
api 'build.linea:l2-message-service-contract-client:0.1.0'
|
||||
|
||||
implementation project(':jvm-libs:linea:web3j-extensions')
|
||||
api ("org.web3j:core:${libs.versions.web3j.get()}") {
|
||||
api("org.web3j:core:${libs.versions.web3j.get()}") {
|
||||
exclude group: 'org.slf4j', module: 'slf4j-nop'
|
||||
}
|
||||
implementation "io.vertx:vertx-core"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.consensys.linea.contract
|
||||
|
||||
import linea.domain.gas.GasPriceCaps
|
||||
import linea.kotlin.toBigInteger
|
||||
import linea.kotlin.toGWei
|
||||
import linea.kotlin.toULong
|
||||
@@ -11,10 +12,10 @@ import linea.web3j.gas.EIP4844GasFees
|
||||
import linea.web3j.gas.EIP4844GasProvider
|
||||
import linea.web3j.getRevertReason
|
||||
import linea.web3j.informativeEthCall
|
||||
import linea.web3j.requestAsync
|
||||
import linea.web3j.toWeb3jTxBlob
|
||||
import linea.web3j.transactionmanager.AsyncFriendlyTransactionManager
|
||||
import net.consensys.linea.async.toSafeFuture
|
||||
import net.consensys.zkevm.domain.BlobRecord
|
||||
import net.consensys.zkevm.ethereum.gaspricing.GasPriceCaps
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import org.apache.tuweni.bytes.Bytes
|
||||
@@ -32,6 +33,7 @@ import org.web3j.tx.gas.ContractGasProvider
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.collections.map
|
||||
|
||||
class Web3JContractAsyncHelper(
|
||||
val contractAddress: String,
|
||||
@@ -253,7 +255,9 @@ class Web3JContractAsyncHelper(
|
||||
)
|
||||
}
|
||||
val signedMessage = transactionManager.sign(transaction)
|
||||
return web3j.ethSendRawTransaction(signedMessage).sendAsync()
|
||||
return web3j
|
||||
.ethSendRawTransaction(signedMessage)
|
||||
.requestAsync { it }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -289,12 +293,8 @@ class Web3JContractAsyncHelper(
|
||||
gasPriceCaps: GasPriceCaps?
|
||||
): SafeFuture<String> {
|
||||
require(blobs.size in 0..6) { "Blobs size=${blobs.size} must be between 0 and 6." }
|
||||
return sendBlobCarryingTransaction(function, BigInteger.ZERO, blobs.toWeb3JTxBlob(), gasPriceCaps)
|
||||
.toSafeFuture()
|
||||
.thenApply { result ->
|
||||
throwExceptionIfJsonRpcErrorReturned("eth_sendRawTransaction", result)
|
||||
result.transactionHash
|
||||
}
|
||||
return sendBlobCarryingTransaction(function, BigInteger.ZERO, blobs.toWeb3jTxBlob(), gasPriceCaps)
|
||||
.thenApply { it.transactionHash }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -303,7 +303,7 @@ class Web3JContractAsyncHelper(
|
||||
weiValue: BigInteger,
|
||||
blobs: List<Blob>,
|
||||
gasPriceCaps: GasPriceCaps? = null
|
||||
): CompletableFuture<EthSendTransaction> {
|
||||
): SafeFuture<EthSendTransaction> {
|
||||
val blobVersionedHashes = blobs.map { BlobUtils.kzgToVersionedHash(BlobUtils.getCommitment(it)) }
|
||||
return getGasLimit(function, blobs, blobVersionedHashes)
|
||||
.thenCompose { gasLimit ->
|
||||
@@ -333,7 +333,8 @@ class Web3JContractAsyncHelper(
|
||||
maxFeePerBlobGas = gasPriceCaps?.maxFeePerBlobGasCap?.toBigInteger() ?: maxFeePerBlobGas.toBigInteger()
|
||||
)
|
||||
val signedMessage = transactionManager.sign(transaction)
|
||||
web3j.ethSendRawTransaction(signedMessage).sendAsync()
|
||||
web3j.ethSendRawTransaction(signedMessage)
|
||||
.requestAsync { it }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +354,11 @@ class Web3JContractAsyncHelper(
|
||||
): RemoteFunctionCall<TransactionReceipt> {
|
||||
return executeRemoteCallTransaction(function, BigInteger.ZERO)
|
||||
}
|
||||
fun executeEthCall(function: Function, overrideGasLimit: BigInteger? = null): SafeFuture<String?> {
|
||||
|
||||
fun executeEthCall(
|
||||
function: Function,
|
||||
overrideGasLimit: BigInteger? = null
|
||||
): SafeFuture<String?> {
|
||||
return (overrideGasLimit?.let { SafeFuture.completedFuture(overrideGasLimit) } ?: getGasLimit(function))
|
||||
.thenCompose { gasLimit ->
|
||||
Transaction.createFunctionCallTransaction(
|
||||
@@ -371,12 +376,12 @@ class Web3JContractAsyncHelper(
|
||||
|
||||
fun executeBlobEthCall(
|
||||
function: Function,
|
||||
blobs: List<BlobRecord>,
|
||||
blobs: List<ByteArray>,
|
||||
gasPriceCaps: GasPriceCaps?
|
||||
): SafeFuture<String?> {
|
||||
return createEip4844Transaction(
|
||||
function,
|
||||
blobs.map { it.blobCompressionProof!!.compressedData }.toWeb3JTxBlob(),
|
||||
blobs.toWeb3jTxBlob(),
|
||||
gasPriceCaps
|
||||
).thenCompose { tx ->
|
||||
web3j.informativeEthCall(tx, smartContractErrors)
|
||||
@@ -5,12 +5,12 @@ import linea.domain.BlockParameter
|
||||
import linea.kotlin.encodeHex
|
||||
import linea.kotlin.toBigInteger
|
||||
import linea.kotlin.toULong
|
||||
import linea.web3j.domain.toWeb3j
|
||||
import net.consensys.linea.async.toSafeFuture
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import org.web3j.crypto.Credentials
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.protocol.core.DefaultBlockParameter
|
||||
import org.web3j.tx.Contract
|
||||
import org.web3j.tx.exceptions.ContractCallException
|
||||
import org.web3j.tx.gas.StaticGasProvider
|
||||
@@ -20,13 +20,6 @@ import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
private val fakeCredentials = Credentials.create(ByteArray(32).encodeHex())
|
||||
|
||||
fun BlockParameter.toWeb3j(): DefaultBlockParameter {
|
||||
return when (this) {
|
||||
is BlockParameter.Tag -> DefaultBlockParameter.valueOf(this.getTag())
|
||||
is BlockParameter.BlockNumber -> DefaultBlockParameter.valueOf(this.getNumber().toBigInteger())
|
||||
}
|
||||
}
|
||||
|
||||
open class Web3JLineaRollupSmartContractClientReadOnly(
|
||||
val web3j: Web3j,
|
||||
val contractAddress: String,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package linea.contract.l2
|
||||
|
||||
import net.consensys.linea.contract.L2MessageService.FUNC_ANCHORL1L2MESSAGEHASHES
|
||||
import org.web3j.abi.TypeReference
|
||||
import org.web3j.abi.datatypes.DynamicArray
|
||||
import org.web3j.abi.datatypes.Function
|
||||
import org.web3j.abi.datatypes.Type
|
||||
import org.web3j.abi.datatypes.generated.Bytes32
|
||||
import org.web3j.abi.datatypes.generated.Uint256
|
||||
import java.math.BigInteger
|
||||
|
||||
internal fun buildAnchorL1L2MessageHashesV1(
|
||||
messageHashes: List<ByteArray>,
|
||||
startingMessageNumber: BigInteger,
|
||||
finalMessageNumber: BigInteger,
|
||||
finalRollingHash: ByteArray
|
||||
): Function {
|
||||
return Function(
|
||||
/* name = */ FUNC_ANCHORL1L2MESSAGEHASHES,
|
||||
/* inputParameters = */ listOf<Type<*>>(
|
||||
DynamicArray(
|
||||
Bytes32::class.java,
|
||||
messageHashes.map { Bytes32(it) }
|
||||
),
|
||||
Uint256(startingMessageNumber),
|
||||
Uint256(finalMessageNumber),
|
||||
Bytes32(finalRollingHash)
|
||||
),
|
||||
/* outputParameters = */ emptyList<TypeReference<*>>()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package linea.contract.l2
|
||||
|
||||
import linea.domain.BlockParameter
|
||||
import linea.kotlin.encodeHex
|
||||
import linea.kotlin.toBigInteger
|
||||
import linea.kotlin.toULong
|
||||
import linea.web3j.SmartContractErrors
|
||||
import linea.web3j.domain.toWeb3j
|
||||
import linea.web3j.gas.EIP1559GasProvider
|
||||
import linea.web3j.requestAsync
|
||||
import linea.web3j.transactionmanager.AsyncFriendlyTransactionManager
|
||||
import net.consensys.linea.async.toSafeFuture
|
||||
import net.consensys.linea.contract.L2MessageService
|
||||
import net.consensys.linea.contract.Web3JContractAsyncHelper
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import org.web3j.crypto.Credentials
|
||||
import org.web3j.protocol.Web3j
|
||||
import org.web3j.tx.Contract
|
||||
import org.web3j.tx.gas.StaticGasProvider
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class Web3JL2MessageServiceSmartContractClient(
|
||||
private val web3j: Web3j,
|
||||
private val contractAddress: String,
|
||||
private val web3jContractHelper: Web3JContractAsyncHelper,
|
||||
private val log: Logger = LogManager.getLogger(Web3JL2MessageServiceSmartContractClient::class.java)
|
||||
) : L2MessageServiceSmartContractClient {
|
||||
companion object {
|
||||
fun create(
|
||||
web3jClient: Web3j,
|
||||
contractAddress: String,
|
||||
gasLimit: ULong,
|
||||
maxFeePerGasCap: ULong,
|
||||
feeHistoryBlockCount: UInt,
|
||||
feeHistoryRewardPercentile: Double,
|
||||
transactionManager: AsyncFriendlyTransactionManager,
|
||||
smartContractErrors: SmartContractErrors
|
||||
): Web3JL2MessageServiceSmartContractClient {
|
||||
val gasProvider = EIP1559GasProvider(
|
||||
web3jClient = web3jClient,
|
||||
config = EIP1559GasProvider.Config(
|
||||
gasLimit = gasLimit,
|
||||
maxFeePerGasCap = maxFeePerGasCap,
|
||||
feeHistoryBlockCount = feeHistoryBlockCount,
|
||||
feeHistoryRewardPercentile = feeHistoryRewardPercentile
|
||||
)
|
||||
)
|
||||
val web3jContractHelper = Web3JContractAsyncHelper(
|
||||
contractAddress = contractAddress,
|
||||
web3j = web3jClient,
|
||||
contractGasProvider = gasProvider,
|
||||
transactionManager = transactionManager,
|
||||
smartContractErrors = smartContractErrors,
|
||||
useEthEstimateGas = true
|
||||
)
|
||||
return Web3JL2MessageServiceSmartContractClient(
|
||||
web3j = web3jClient,
|
||||
contractAddress = contractAddress,
|
||||
web3jContractHelper = web3jContractHelper
|
||||
)
|
||||
}
|
||||
}
|
||||
private val fakeCredentials = Credentials.create(ByteArray(32).encodeHex())
|
||||
private val smartContractVersionCache = AtomicReference<L2MessageServiceSmartContractVersion>(null)
|
||||
|
||||
private fun <T : Contract> contractClientAtBlock(blockParameter: BlockParameter, contract: Class<T>): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return when {
|
||||
L2MessageService::class.java.isAssignableFrom(contract) -> L2MessageService.load(
|
||||
contractAddress,
|
||||
web3j,
|
||||
fakeCredentials,
|
||||
StaticGasProvider(BigInteger.ZERO, BigInteger.ZERO)
|
||||
).apply {
|
||||
this.setDefaultBlockParameter(blockParameter.toWeb3j())
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unsupported contract type: ${contract::class.java}")
|
||||
} as T
|
||||
}
|
||||
|
||||
private fun getSmartContractVersion(): SafeFuture<L2MessageServiceSmartContractVersion> {
|
||||
return if (smartContractVersionCache.get() == L2MessageServiceSmartContractVersion.V1) {
|
||||
// once upgraded, it's not downgraded
|
||||
SafeFuture.completedFuture(L2MessageServiceSmartContractVersion.V1)
|
||||
} else {
|
||||
fetchSmartContractVersion()
|
||||
.thenPeek { contractLatestVersion ->
|
||||
if (smartContractVersionCache.get() != null &&
|
||||
contractLatestVersion != smartContractVersionCache.get()
|
||||
) {
|
||||
log.info(
|
||||
"L2 Message Service Smart contract upgraded: prevVersion={} upgradedVersion={}",
|
||||
smartContractVersionCache.get(),
|
||||
contractLatestVersion
|
||||
)
|
||||
}
|
||||
smartContractVersionCache.set(contractLatestVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchSmartContractVersion(): SafeFuture<L2MessageServiceSmartContractVersion> {
|
||||
return contractClientAtBlock(BlockParameter.Tag.LATEST, L2MessageService::class.java)
|
||||
.CONTRACT_VERSION()
|
||||
.requestAsync { version ->
|
||||
when {
|
||||
version.startsWith("1") -> L2MessageServiceSmartContractVersion.V1
|
||||
else -> throw IllegalStateException("Unsupported contract version: $version")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAddress(): String = contractAddress
|
||||
override fun getVersion(): SafeFuture<L2MessageServiceSmartContractVersion> = getSmartContractVersion()
|
||||
|
||||
override fun getLastAnchoredL1MessageNumber(block: BlockParameter): SafeFuture<ULong> {
|
||||
return contractClientAtBlock(block, L2MessageService::class.java)
|
||||
.lastAnchoredL1MessageNumber()
|
||||
.requestAsync { it.toULong() }
|
||||
}
|
||||
|
||||
override fun getRollingHashByL1MessageNumber(
|
||||
block: BlockParameter,
|
||||
l1MessageNumber: ULong
|
||||
): SafeFuture<ByteArray> {
|
||||
return contractClientAtBlock(block, L2MessageService::class.java)
|
||||
.l1RollingHashes(l1MessageNumber.toBigInteger())
|
||||
.requestAsync { it }
|
||||
}
|
||||
|
||||
override fun anchorL1L2MessageHashes(
|
||||
messageHashes: List<ByteArray>,
|
||||
startingMessageNumber: ULong,
|
||||
finalMessageNumber: ULong,
|
||||
finalRollingHash: ByteArray
|
||||
): SafeFuture<String> {
|
||||
return anchorL1L2MessageHashesV2(
|
||||
messageHashes = messageHashes,
|
||||
startingMessageNumber = startingMessageNumber.toBigInteger(),
|
||||
finalMessageNumber = finalMessageNumber.toBigInteger(),
|
||||
finalRollingHash = finalRollingHash
|
||||
)
|
||||
}
|
||||
|
||||
private fun anchorL1L2MessageHashesV2(
|
||||
messageHashes: List<ByteArray>,
|
||||
startingMessageNumber: BigInteger,
|
||||
finalMessageNumber: BigInteger,
|
||||
finalRollingHash: ByteArray
|
||||
): SafeFuture<String> {
|
||||
val function = buildAnchorL1L2MessageHashesV1(
|
||||
messageHashes = messageHashes,
|
||||
startingMessageNumber = startingMessageNumber,
|
||||
finalMessageNumber = finalMessageNumber,
|
||||
finalRollingHash = finalRollingHash
|
||||
)
|
||||
|
||||
return web3jContractHelper
|
||||
.sendTransactionAfterEthCallAsync(
|
||||
function = function,
|
||||
weiValue = BigInteger.ZERO,
|
||||
gasPriceCaps = null
|
||||
)
|
||||
.thenApply { response ->
|
||||
response.transactionHash
|
||||
}
|
||||
.toSafeFuture()
|
||||
}
|
||||
}
|
||||
@@ -28,5 +28,14 @@ data class RetryConfig(
|
||||
|
||||
companion object {
|
||||
val noRetries = RetryConfig(maxRetries = 0u)
|
||||
fun endlessRetry(
|
||||
backoffDelay: Duration,
|
||||
failuresWarningThreshold: UInt
|
||||
) = RetryConfig(
|
||||
maxRetries = null,
|
||||
timeout = null,
|
||||
backoffDelay = backoffDelay,
|
||||
failuresWarningThreshold = failuresWarningThreshold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package linea.domain.gas
|
||||
|
||||
import linea.kotlin.toGWei
|
||||
|
||||
data class GasPriceCaps(
|
||||
val maxPriorityFeePerGasCap: ULong,
|
||||
val maxFeePerGasCap: ULong,
|
||||
val maxFeePerBlobGasCap: ULong,
|
||||
val maxBaseFeePerGasCap: ULong? = null
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "maxPriorityFeePerGasCap=${maxPriorityFeePerGasCap.toGWei()} GWei," +
|
||||
if (maxBaseFeePerGasCap != null) {
|
||||
" maxBaseFeePerGasCap=${maxBaseFeePerGasCap.toGWei()} GWei,"
|
||||
} else {
|
||||
""
|
||||
} +
|
||||
" maxFeePerGasCap=${maxFeePerGasCap.toGWei()} GWei," +
|
||||
" maxFeePerBlobGasCap=${maxFeePerBlobGasCap.toGWei()} GWei"
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,10 @@ fun <Resp> rejectOnJsonRpcError(
|
||||
}
|
||||
}
|
||||
|
||||
fun <Resp, RespT, T> Request<*, Resp>.requestAsync(
|
||||
fun <Resp, T> Request<*, Resp>.requestAsync(
|
||||
mapperFn: (Resp) -> T
|
||||
): SafeFuture<T>
|
||||
where Resp : Response<RespT> {
|
||||
where Resp : Response<*> {
|
||||
return this.sendAsync()
|
||||
.thenCompose { response -> rejectOnJsonRpcError(this.method, response) }
|
||||
.toSafeFuture()
|
||||
|
||||
Reference in New Issue
Block a user