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:
Fluent Crafter
2025-04-29 11:55:08 +01:00
committed by GitHub
parent cfadda3cf9
commit f005044583
61 changed files with 711 additions and 3031 deletions

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package linea.anchoring.events
package linea.contract.events
import linea.domain.EthLog
import linea.domain.EthLogEvent

View File

@@ -1,4 +1,4 @@
package linea.anchoring.events
package linea.contract.events
import linea.domain.EthLog
import linea.domain.EthLogEvent

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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