mirror of
https://github.com/vacp2p/linea-monorepo.git
synced 2026-01-09 04:08:01 -05:00
coordinator: support for smart contract v6 (#240)
* coordinator: support for smart contract v6
This commit is contained in:
15
Makefile
15
Makefile
@@ -80,6 +80,21 @@ deploy-linea-rollup:
|
||||
LINEA_ROLLUP_GENESIS_TIMESTAMP=1683325137 \
|
||||
npx hardhat deploy --no-compile --network zkevm_dev --tags PlonkVerifier,LineaRollupV5
|
||||
|
||||
deploy-linea-rollup-v6:
|
||||
# WARNING: FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE
|
||||
cd contracts/; \
|
||||
PRIVATE_KEY=$${DEPLOYMENT_PRIVATE_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80} \
|
||||
BLOCKCHAIN_NODE=http:\\localhost:8445/ \
|
||||
PLONKVERIFIER_NAME=IntegrationTestTrueVerifier \
|
||||
LINEA_ROLLUP_INITIAL_STATE_ROOT_HASH=0x072ead6777750dc20232d1cee8dc9a395c2d350df4bbaa5096c6f59b214dcecd \
|
||||
LINEA_ROLLUP_INITIAL_L2_BLOCK_NUMBER=0 \
|
||||
LINEA_ROLLUP_SECURITY_COUNCIL=0x90F79bf6EB2c4f870365E785982E1f101E93b906 \
|
||||
LINEA_ROLLUP_OPERATORS=$${LINEA_ROLLUP_OPERATORS:-0x70997970C51812dc3A010C7d01b50e0d17dc79C8,0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC} \
|
||||
LINEA_ROLLUP_RATE_LIMIT_PERIOD=86400 \
|
||||
LINEA_ROLLUP_RATE_LIMIT_AMOUNT=1000000000000000000000 \
|
||||
LINEA_ROLLUP_GENESIS_TIMESTAMP=1683325137 \
|
||||
npx hardhat deploy --no-compile --network zkevm_dev --tags PlonkVerifier,LineaRollup
|
||||
|
||||
deploy-l2messageservice:
|
||||
# WARNING: FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE
|
||||
cd contracts/; \
|
||||
|
||||
@@ -7,6 +7,7 @@ dependencies {
|
||||
implementation project(':jvm-libs:linea:web3j-extensions')
|
||||
api 'build.linea:l1-rollup-contract-client:0.0.1'
|
||||
api 'build.linea:l2-message-service-contract-client:0.0.1'
|
||||
implementation(project(':jvm-libs:linea:linea-contracts:l1-rollup'))
|
||||
|
||||
api ("org.web3j:core:${libs.versions.web3j.get()}") {
|
||||
exclude group: 'org.slf4j', module: 'slf4j-nop'
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package net.consensys.linea.contract.l1
|
||||
|
||||
import build.linea.contract.LineaRollupV6
|
||||
import net.consensys.toBigInteger
|
||||
import net.consensys.zkevm.coordinator.clients.smartcontract.LineaContractVersion
|
||||
import net.consensys.zkevm.domain.BlobRecord
|
||||
import net.consensys.zkevm.domain.ProofToFinalize
|
||||
import org.web3j.abi.TypeReference
|
||||
import org.web3j.abi.datatypes.DynamicArray
|
||||
import org.web3j.abi.datatypes.DynamicBytes
|
||||
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
|
||||
import java.util.Arrays
|
||||
|
||||
internal fun buildSubmitBlobsFunction(
|
||||
version: LineaContractVersion,
|
||||
blobs: List<BlobRecord>
|
||||
): Function {
|
||||
return when (version) {
|
||||
LineaContractVersion.V5 -> buildSubmitBlobsFunction(blobs)
|
||||
LineaContractVersion.V6 -> buildSubmitBlobsFunctionV6(blobs)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildSubmitBlobsFunctionV6(
|
||||
blobs: List<BlobRecord>
|
||||
): Function {
|
||||
val blobsSubmissionData = blobs.map { blob ->
|
||||
val blobCompressionProof = blob.blobCompressionProof!!
|
||||
// BlobSubmission(BigInteger dataEvaluationClaim, byte[] kzgCommitment, byte[] kzgProof,
|
||||
// byte[] finalStateRootHash, byte[] snarkHash)
|
||||
LineaRollupV6.BlobSubmission(
|
||||
/*dataEvaluationClaim*/ BigInteger(blobCompressionProof.expectedY),
|
||||
/*kzgCommitment*/ blobCompressionProof.commitment,
|
||||
/*kzgProof*/ blobCompressionProof.kzgProofContract,
|
||||
/*finalStateRootHash*/ blobCompressionProof.finalStateRootHash,
|
||||
/*snarkHash*/ blobCompressionProof.snarkHash
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
function submitBlobs(
|
||||
BlobSubmission[] calldata _blobSubmissions,
|
||||
bytes32 _parentShnarf,
|
||||
bytes32 _finalBlobShnarf
|
||||
)
|
||||
*/
|
||||
return Function(
|
||||
LineaRollupV6.FUNC_SUBMITBLOBS,
|
||||
Arrays.asList<Type<*>>(
|
||||
DynamicArray(LineaRollupV6.BlobSubmission::class.java, blobsSubmissionData),
|
||||
Bytes32(blobs.first().blobCompressionProof!!.prevShnarf),
|
||||
Bytes32(blobs.last().blobCompressionProof!!.expectedShnarf)
|
||||
),
|
||||
emptyList<TypeReference<*>>()
|
||||
)
|
||||
}
|
||||
|
||||
fun buildFinalizeBlocksFunction(
|
||||
version: LineaContractVersion,
|
||||
aggregationProof: ProofToFinalize,
|
||||
aggregationLastBlob: BlobRecord,
|
||||
parentShnarf: ByteArray,
|
||||
parentL1RollingHash: ByteArray,
|
||||
parentL1RollingHashMessageNumber: Long
|
||||
): Function {
|
||||
when (version) {
|
||||
LineaContractVersion.V5 -> {
|
||||
return buildFinalizeBlobsFunction(
|
||||
aggregationProof,
|
||||
aggregationLastBlob,
|
||||
parentShnarf,
|
||||
parentL1RollingHash,
|
||||
parentL1RollingHashMessageNumber
|
||||
)
|
||||
}
|
||||
|
||||
LineaContractVersion.V6 -> {
|
||||
return buildFinalizeBlockFunctionV6(
|
||||
aggregationProof,
|
||||
aggregationLastBlob,
|
||||
parentL1RollingHash,
|
||||
parentL1RollingHashMessageNumber
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildFinalizeBlockFunctionV6(
|
||||
aggregationProof: ProofToFinalize,
|
||||
aggregationLastBlob: BlobRecord,
|
||||
parentL1RollingHash: ByteArray,
|
||||
parentL1RollingHashMessageNumber: Long
|
||||
): Function {
|
||||
val aggregationEndBlobInfo = LineaRollupV6.ShnarfData(
|
||||
/*parentShnarf*/ aggregationLastBlob.blobCompressionProof!!.prevShnarf,
|
||||
/*snarkHash*/ aggregationLastBlob.blobCompressionProof!!.snarkHash,
|
||||
/*finalStateRootHash*/ aggregationLastBlob.blobCompressionProof!!.finalStateRootHash,
|
||||
/*dataEvaluationPoint*/ aggregationLastBlob.blobCompressionProof!!.expectedX,
|
||||
/*dataEvaluationClaim*/ aggregationLastBlob.blobCompressionProof!!.expectedY
|
||||
)
|
||||
|
||||
// FinalizationDataV3(
|
||||
// byte[] parentStateRootHash,
|
||||
// BigInteger endBlockNumber,
|
||||
// ShnarfData shnarfData,
|
||||
// BigInteger lastFinalizedTimestamp,
|
||||
// BigInteger finalTimestamp,
|
||||
// byte[] lastFinalizedL1RollingHash,
|
||||
// byte[] l1RollingHash,
|
||||
// BigInteger lastFinalizedL1RollingHashMessageNumber,
|
||||
// BigInteger l1RollingHashMessageNumber,
|
||||
// BigInteger l2MerkleTreesDepth,
|
||||
// List<byte[]> l2MerkleRoots,
|
||||
// byte[] l2MessagingBlocksOffsets
|
||||
// )
|
||||
|
||||
val finalizationData = LineaRollupV6.FinalizationDataV3(
|
||||
/*parentStateRootHash*/ aggregationProof.parentStateRootHash,
|
||||
/*endBlockNumber*/ aggregationProof.endBlockNumber.toBigInteger(),
|
||||
/*shnarfData*/ aggregationEndBlobInfo,
|
||||
/*lastFinalizedTimestamp*/ aggregationProof.parentAggregationLastBlockTimestamp.epochSeconds.toBigInteger(),
|
||||
/*finalTimestamp*/ aggregationProof.finalTimestamp.epochSeconds.toBigInteger(),
|
||||
/*lastFinalizedL1RollingHash*/ parentL1RollingHash,
|
||||
/*l1RollingHash*/ aggregationProof.l1RollingHash,
|
||||
/*lastFinalizedL1RollingHashMessageNumber*/ parentL1RollingHashMessageNumber.toBigInteger(),
|
||||
/*l1RollingHashMessageNumber*/ aggregationProof.l1RollingHashMessageNumber.toBigInteger(),
|
||||
/*l2MerkleTreesDepth*/ aggregationProof.l2MerkleTreesDepth.toBigInteger(),
|
||||
/*l2MerkleRoots*/ aggregationProof.l2MerkleRoots,
|
||||
/*l2MessagingBlocksOffsets*/ aggregationProof.l2MessagingBlocksOffsets
|
||||
)
|
||||
|
||||
/**
|
||||
* function finalizeBlocks(
|
||||
* bytes calldata _aggregatedProof,
|
||||
* uint256 _proofType,
|
||||
* FinalizationDataV3 calldata _finalizationData
|
||||
* )
|
||||
*/
|
||||
val function = Function(
|
||||
LineaRollupV6.FUNC_FINALIZEBLOCKS,
|
||||
Arrays.asList<Type<*>>(
|
||||
DynamicBytes(aggregationProof.aggregatedProof),
|
||||
Uint256(aggregationProof.aggregatedVerifierIndex.toLong()),
|
||||
finalizationData
|
||||
),
|
||||
emptyList<TypeReference<*>>()
|
||||
)
|
||||
return function
|
||||
}
|
||||
@@ -105,53 +105,39 @@ class Web3JLineaRollupSmartContractClient internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun submitBlobs(
|
||||
blobs: List<BlobRecord>,
|
||||
gasPriceCaps: GasPriceCaps?
|
||||
): SafeFuture<String> {
|
||||
return submitBlobsV5(blobs, gasPriceCaps)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends EIP4844 blob carrying transaction to the smart contract.
|
||||
* Uses SMC `submitBlobs` function that supports multiple blobs per call.
|
||||
*/
|
||||
private fun submitBlobsV5(
|
||||
override fun submitBlobs(
|
||||
blobs: List<BlobRecord>,
|
||||
gasPriceCaps: GasPriceCaps?
|
||||
): SafeFuture<String> {
|
||||
require(blobs.size in 1..6) { "Blobs size=${blobs.size} must be between 1 and 6." }
|
||||
val function = buildSubmitBlobsFunction(
|
||||
blobs
|
||||
)
|
||||
|
||||
return helper.sendBlobCarryingTransactionAndGetTxHash(
|
||||
function = function,
|
||||
blobs = blobs.map { it.blobCompressionProof!!.compressedData },
|
||||
gasPriceCaps = gasPriceCaps
|
||||
)
|
||||
return getVersion()
|
||||
.thenCompose { version ->
|
||||
val function = buildSubmitBlobsFunction(version, blobs)
|
||||
helper.sendBlobCarryingTransactionAndGetTxHash(
|
||||
function = function,
|
||||
blobs = blobs.map { it.blobCompressionProof!!.compressedData },
|
||||
gasPriceCaps = gasPriceCaps
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun submitBlobsEthCall(
|
||||
blobs: List<BlobRecord>,
|
||||
gasPriceCaps: GasPriceCaps?
|
||||
): SafeFuture<String?> {
|
||||
return submitBlobsEthCallImpl(blobs, gasPriceCaps)
|
||||
}
|
||||
|
||||
private fun submitBlobsEthCallImpl(
|
||||
blobs: List<BlobRecord>,
|
||||
gasPriceCaps: GasPriceCaps? = null
|
||||
): SafeFuture<String?> {
|
||||
val function = buildSubmitBlobsFunction(blobs)
|
||||
|
||||
val transaction = helper.createEip4844Transaction(
|
||||
function,
|
||||
blobs.map { it.blobCompressionProof!!.compressedData }.toWeb3JTxBlob(),
|
||||
gasPriceCaps
|
||||
)
|
||||
|
||||
return web3j.informativeEthCall(transaction, smartContractErrors)
|
||||
return getVersion()
|
||||
.thenCompose { version ->
|
||||
val function = buildSubmitBlobsFunction(version, blobs)
|
||||
val transaction = helper.createEip4844Transaction(
|
||||
function,
|
||||
blobs.map { it.blobCompressionProof!!.compressedData }.toWeb3JTxBlob(),
|
||||
gasPriceCaps
|
||||
)
|
||||
web3j.informativeEthCall(transaction, smartContractErrors)
|
||||
}
|
||||
}
|
||||
|
||||
override fun finalizeBlocks(
|
||||
@@ -162,38 +148,22 @@ class Web3JLineaRollupSmartContractClient internal constructor(
|
||||
parentL1RollingHashMessageNumber: Long,
|
||||
gasPriceCaps: GasPriceCaps?
|
||||
): SafeFuture<String> {
|
||||
return finalizeBlocksV5(
|
||||
aggregation,
|
||||
aggregationLastBlob,
|
||||
parentShnarf,
|
||||
parentL1RollingHash,
|
||||
parentL1RollingHashMessageNumber,
|
||||
gasPriceCaps
|
||||
)
|
||||
}
|
||||
|
||||
private fun finalizeBlocksV5(
|
||||
aggregation: ProofToFinalize,
|
||||
aggregationLastBlob: BlobRecord,
|
||||
parentShnarf: ByteArray,
|
||||
parentL1RollingHash: ByteArray,
|
||||
parentL1RollingHashMessageNumber: Long,
|
||||
gasPriceCaps: GasPriceCaps?
|
||||
): SafeFuture<String> {
|
||||
val function = buildFinalizeBlobsFunction(
|
||||
aggregation,
|
||||
aggregationLastBlob,
|
||||
parentShnarf,
|
||||
parentL1RollingHash,
|
||||
parentL1RollingHashMessageNumber
|
||||
)
|
||||
|
||||
return SafeFuture.of(
|
||||
helper.sendTransactionAsync(function, BigInteger.ZERO, gasPriceCaps)
|
||||
).thenApply { result ->
|
||||
throwExceptionIfJsonRpcErrorReturned("eth_sendRawTransaction", result)
|
||||
result.transactionHash
|
||||
}
|
||||
return getVersion()
|
||||
.thenCompose { version ->
|
||||
val function = buildFinalizeBlocksFunction(
|
||||
version,
|
||||
aggregation,
|
||||
aggregationLastBlob,
|
||||
parentShnarf,
|
||||
parentL1RollingHash,
|
||||
parentL1RollingHashMessageNumber
|
||||
)
|
||||
helper.sendTransactionAsync(function, BigInteger.ZERO, gasPriceCaps)
|
||||
.thenApply { result ->
|
||||
throwExceptionIfJsonRpcErrorReturned("eth_sendRawTransaction", result)
|
||||
result.transactionHash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun finalizeBlocksEthCall(
|
||||
@@ -203,30 +173,17 @@ class Web3JLineaRollupSmartContractClient internal constructor(
|
||||
parentL1RollingHash: ByteArray,
|
||||
parentL1RollingHashMessageNumber: Long
|
||||
): SafeFuture<String?> {
|
||||
return finalizeBlocksEthCallV5(
|
||||
aggregation,
|
||||
aggregationLastBlob,
|
||||
parentShnarf,
|
||||
parentL1RollingHash,
|
||||
parentL1RollingHashMessageNumber
|
||||
)
|
||||
}
|
||||
|
||||
private fun finalizeBlocksEthCallV5(
|
||||
aggregation: ProofToFinalize,
|
||||
aggregationLastBlob: BlobRecord,
|
||||
parentShnarf: ByteArray,
|
||||
parentL1RollingHash: ByteArray,
|
||||
parentL1RollingHashMessageNumber: Long
|
||||
): SafeFuture<String?> {
|
||||
val function = buildFinalizeBlobsFunction(
|
||||
aggregation,
|
||||
aggregationLastBlob,
|
||||
parentShnarf,
|
||||
parentL1RollingHash,
|
||||
parentL1RollingHashMessageNumber
|
||||
)
|
||||
|
||||
return helper.executeEthCall(function)
|
||||
return getVersion()
|
||||
.thenCompose { version ->
|
||||
val function = buildFinalizeBlocksFunction(
|
||||
version,
|
||||
aggregation,
|
||||
aggregationLastBlob,
|
||||
parentShnarf,
|
||||
parentL1RollingHash,
|
||||
parentL1RollingHashMessageNumber
|
||||
)
|
||||
helper.executeEthCall(function)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.consensys.linea.contract.l1
|
||||
|
||||
import build.linea.contract.LineaRollupV6
|
||||
import net.consensys.encodeHex
|
||||
import net.consensys.linea.BlockParameter
|
||||
import net.consensys.linea.async.toSafeFuture
|
||||
@@ -13,6 +14,8 @@ 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
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.math.BigInteger
|
||||
@@ -34,41 +37,76 @@ open class Web3JLineaRollupSmartContractClientReadOnly(
|
||||
) : LineaRollupSmartContractClientReadOnly {
|
||||
|
||||
protected fun contractClientAtBlock(blockParameter: BlockParameter): LineaRollup {
|
||||
return LineaRollup.load(
|
||||
contractAddress,
|
||||
web3j,
|
||||
fakeCredentials,
|
||||
StaticGasProvider(BigInteger.ZERO, BigInteger.ZERO)
|
||||
).apply {
|
||||
this.setDefaultBlockParameter(blockParameter.toWeb3j())
|
||||
}
|
||||
return contractClientAtBlock(blockParameter, LineaRollup::class.java)
|
||||
}
|
||||
|
||||
protected fun <T : Contract> contractClientAtBlock(blockParameter: BlockParameter, contract: Class<T>): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return when {
|
||||
LineaRollupV6::class.java.isAssignableFrom(contract) -> LineaRollupV6.load(
|
||||
contractAddress,
|
||||
web3j,
|
||||
fakeCredentials,
|
||||
StaticGasProvider(BigInteger.ZERO, BigInteger.ZERO)
|
||||
).apply {
|
||||
this.setDefaultBlockParameter(blockParameter.toWeb3j())
|
||||
}
|
||||
|
||||
LineaRollup::class.java.isAssignableFrom(contract) -> LineaRollup.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
|
||||
}
|
||||
|
||||
protected val smartContractVersionCache: AtomicReference<LineaContractVersion> =
|
||||
AtomicReference(fetchSmartContractVersion().get())
|
||||
|
||||
private fun getSmartContractVersion(): SafeFuture<LineaContractVersion> {
|
||||
return if (smartContractVersionCache.get() == LineaContractVersion.V5) {
|
||||
return if (smartContractVersionCache.get() == LineaContractVersion.V6) {
|
||||
// once upgraded, it's not downgraded
|
||||
SafeFuture.completedFuture(LineaContractVersion.V5)
|
||||
SafeFuture.completedFuture(LineaContractVersion.V6)
|
||||
} else {
|
||||
fetchSmartContractVersion().thenPeek { contractLatestVersion ->
|
||||
if (contractLatestVersion != smartContractVersionCache.get()) {
|
||||
log.info(
|
||||
"Smart contract upgraded: prevVersion={} upgradedVersion={}",
|
||||
smartContractVersionCache.get(),
|
||||
contractLatestVersion
|
||||
)
|
||||
fetchSmartContractVersion()
|
||||
.thenPeek { contractLatestVersion ->
|
||||
if (contractLatestVersion != smartContractVersionCache.get()) {
|
||||
log.info(
|
||||
"Smart contract upgraded: prevVersion={} upgradedVersion={}",
|
||||
smartContractVersionCache.get(),
|
||||
contractLatestVersion
|
||||
)
|
||||
}
|
||||
smartContractVersionCache.set(contractLatestVersion)
|
||||
}
|
||||
smartContractVersionCache.set(contractLatestVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchSmartContractVersion(): SafeFuture<LineaContractVersion> {
|
||||
// FIXME: this is a temporary solution to determine the smart contract version.
|
||||
// It should rely on events
|
||||
return SafeFuture.completedFuture(LineaContractVersion.V5)
|
||||
return contractClientAtBlock(BlockParameter.Tag.LATEST, LineaRollupV6::class.java)
|
||||
.CONTRACT_VERSION()
|
||||
.sendAsync()
|
||||
.toSafeFuture()
|
||||
.thenApply { version ->
|
||||
when {
|
||||
version.startsWith("6") -> LineaContractVersion.V6
|
||||
else -> throw IllegalStateException("Unsupported contract version: $version")
|
||||
}
|
||||
}
|
||||
.exceptionallyCompose { error ->
|
||||
if (error.cause is ContractCallException) {
|
||||
// means that contract does not have CONTRACT_VERSION method available yet
|
||||
// so it is still V5, so defaulting to V5
|
||||
SafeFuture.completedFuture(LineaContractVersion.V5)
|
||||
} else {
|
||||
SafeFuture.failedFuture(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAddress(): String = contractAddress
|
||||
@@ -94,11 +132,18 @@ open class Web3JLineaRollupSmartContractClientReadOnly(
|
||||
return contractClientAtBlock(blockParameter).rollingHashes(messageNumber.toBigInteger()).sendAsync().toSafeFuture()
|
||||
}
|
||||
|
||||
override fun findBlobFinalBlockNumberByShnarf(blockParameter: BlockParameter, shnarf: ByteArray): SafeFuture<ULong?> {
|
||||
return contractClientAtBlock(blockParameter)
|
||||
.shnarfFinalBlockNumbers(shnarf).sendAsync()
|
||||
.thenApply { if (it == BigInteger.ZERO) null else it.toULong() }
|
||||
.toSafeFuture()
|
||||
override fun isBlobShnarfPresent(blockParameter: BlockParameter, shnarf: ByteArray): SafeFuture<Boolean> {
|
||||
return getVersion()
|
||||
.thenCompose { version ->
|
||||
if (version == LineaContractVersion.V5) {
|
||||
contractClientAtBlock(blockParameter, LineaRollup::class.java).shnarfFinalBlockNumbers(shnarf)
|
||||
} else {
|
||||
contractClientAtBlock(blockParameter, LineaRollupV6::class.java).blobShnarfExists(shnarf)
|
||||
}
|
||||
.sendAsync()
|
||||
.thenApply { it != BigInteger.ZERO }
|
||||
.toSafeFuture()
|
||||
}
|
||||
}
|
||||
|
||||
override fun blockStateRootHash(blockParameter: BlockParameter, lineaL2BlockNumber: ULong): SafeFuture<ByteArray> {
|
||||
|
||||
@@ -7,7 +7,8 @@ import net.consensys.zkevm.ethereum.gaspricing.GasPriceCaps
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
|
||||
enum class LineaContractVersion : Comparable<LineaContractVersion> {
|
||||
V5 // "EIP4844 multiple blobs per tx support - version in all networks",
|
||||
V5, // "EIP4844 multiple blobs per tx support - version in all networks"
|
||||
V6 // more efficient data submission and new events for state recovery
|
||||
}
|
||||
|
||||
interface LineaRollupSmartContractClientReadOnly : ContractVersionProvider<LineaContractVersion> {
|
||||
@@ -30,12 +31,14 @@ interface LineaRollupSmartContractClientReadOnly : ContractVersionProvider<Linea
|
||||
): SafeFuture<ByteArray>
|
||||
|
||||
/**
|
||||
* Get the final block number of a shnarf
|
||||
* Checks if a blob's shnarf is already present in the smart contract
|
||||
* It meant blob was sent to l1 and accepted by the smart contract.
|
||||
* Note: snarf in the future may be cleanned up after finalization.
|
||||
*/
|
||||
fun findBlobFinalBlockNumberByShnarf(
|
||||
fun isBlobShnarfPresent(
|
||||
blockParameter: BlockParameter = BlockParameter.Tag.LATEST,
|
||||
shnarf: ByteArray
|
||||
): SafeFuture<ULong?>
|
||||
): SafeFuture<Boolean>
|
||||
|
||||
/**
|
||||
* Gets Type 2 StateRootHash for Linea Block
|
||||
|
||||
@@ -61,12 +61,12 @@ class BlobAndAggregationFinalizationIntTest : CleanDbTestSuiteParallel() {
|
||||
vertx: Vertx,
|
||||
smartContractVersion: LineaContractVersion
|
||||
) {
|
||||
if (smartContractVersion != LineaContractVersion.V5) {
|
||||
if (listOf(LineaContractVersion.V5, LineaContractVersion.V6).contains(smartContractVersion).not()) {
|
||||
// V6 with prover V3 is soon comming, so we will need to update/extend this test setup
|
||||
throw IllegalArgumentException("Only V5 contract version is supported")
|
||||
throw IllegalArgumentException("unsupported contract version=$smartContractVersion!")
|
||||
}
|
||||
val rollupDeploymentFuture = ContractsManager.get()
|
||||
.deployLineaRollup(numberOfOperators = 2, contractVersion = LineaContractVersion.V5)
|
||||
.deployLineaRollup(numberOfOperators = 2, contractVersion = smartContractVersion)
|
||||
// load files from FS while smc deploy
|
||||
loadBlobsAndAggregations(
|
||||
blobsResponsesDir = "$testDataDir/compression/responses",
|
||||
@@ -90,10 +90,10 @@ class BlobAndAggregationFinalizationIntTest : CleanDbTestSuiteParallel() {
|
||||
)
|
||||
aggregationsRepository = AggregationsRepositoryImpl(PostgresAggregationsDao(sqlClient, fakeClock))
|
||||
|
||||
val lineaRollupContractForDataSubmissionV4 = rollupDeploymentResult.rollupOperatorClient
|
||||
val lineaRollupContractForDataSubmissionV5 = rollupDeploymentResult.rollupOperatorClient
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val alreadySubmittedBlobFilter = L1ShnarfBasedAlreadySubmittedBlobsFilter(lineaRollupContractForDataSubmissionV4)
|
||||
val alreadySubmittedBlobFilter = L1ShnarfBasedAlreadySubmittedBlobsFilter(lineaRollupContractForDataSubmissionV5)
|
||||
|
||||
blobSubmissionCoordinator = run {
|
||||
BlobSubmissionCoordinator.create(
|
||||
@@ -105,7 +105,7 @@ class BlobAndAggregationFinalizationIntTest : CleanDbTestSuiteParallel() {
|
||||
),
|
||||
blobsRepository = blobsRepository,
|
||||
aggregationsRepository = aggregationsRepository,
|
||||
lineaSmartContractClient = lineaRollupContractForDataSubmissionV4,
|
||||
lineaSmartContractClient = lineaRollupContractForDataSubmissionV5,
|
||||
alreadySubmittedBlobsFilter = alreadySubmittedBlobFilter,
|
||||
gasPriceCapProvider = FakeGasPriceCapProvider(),
|
||||
vertx = vertx,
|
||||
@@ -115,9 +115,10 @@ class BlobAndAggregationFinalizationIntTest : CleanDbTestSuiteParallel() {
|
||||
|
||||
aggregationFinalizationCoordinator = run {
|
||||
lineaRollupContractForAggregationSubmission = MakeFileDelegatedContractsManager
|
||||
.connectToLineaRollupContractV5(
|
||||
.connectToLineaRollupContract(
|
||||
rollupDeploymentResult.contractAddress,
|
||||
rollupDeploymentResult.rollupOperators[1].txManager
|
||||
|
||||
)
|
||||
|
||||
val aggregationSubmitter = AggregationSubmitterImpl(
|
||||
@@ -141,15 +142,6 @@ class BlobAndAggregationFinalizationIntTest : CleanDbTestSuiteParallel() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(3, timeUnit = TimeUnit.MINUTES)
|
||||
fun `submission works with contract V5`(
|
||||
vertx: Vertx,
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
testSubmission(vertx, testContext, LineaContractVersion.V5)
|
||||
}
|
||||
|
||||
private fun testSubmission(
|
||||
vertx: Vertx,
|
||||
testContext: VertxTestContext,
|
||||
@@ -180,4 +172,22 @@ class BlobAndAggregationFinalizationIntTest : CleanDbTestSuiteParallel() {
|
||||
testContext.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(3, timeUnit = TimeUnit.MINUTES)
|
||||
fun `submission works with contract V5`(
|
||||
vertx: Vertx,
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
testSubmission(vertx, testContext, LineaContractVersion.V5)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(3, timeUnit = TimeUnit.MINUTES)
|
||||
fun `submission works with contract V6`(
|
||||
vertx: Vertx,
|
||||
testContext: VertxTestContext
|
||||
) {
|
||||
testSubmission(vertx, testContext, LineaContractVersion.V6)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,15 @@ class L1ShnarfBasedAlreadySubmittedBlobsFilter(
|
||||
blobRecords: List<BlobRecord>
|
||||
): SafeFuture<List<BlobRecord>> {
|
||||
val blockByShnarfQueryFutures = blobRecords.map { blobRecord ->
|
||||
lineaRollup.findBlobFinalBlockNumberByShnarf(shnarf = blobRecord.expectedShnarf)
|
||||
lineaRollup
|
||||
.isBlobShnarfPresent(shnarf = blobRecord.expectedShnarf)
|
||||
.thenApply { isShnarfPresent ->
|
||||
if (isShnarfPresent) {
|
||||
blobRecord.endBlockNumber
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SafeFuture.collectAll(blockByShnarfQueryFutures.stream())
|
||||
|
||||
@@ -26,13 +26,13 @@ class L1ShnarfBasedAlreadySubmittedBlobsFilterTest {
|
||||
val blobs = listOf(blob1, blob2, blob3, blob4, blob5, blob6, blob7)
|
||||
|
||||
val l1SmcClient = mock<LineaRollupSmartContractClient>()
|
||||
whenever(l1SmcClient.findBlobFinalBlockNumberByShnarf(any(), any()))
|
||||
whenever(l1SmcClient.isBlobShnarfPresent(any(), any()))
|
||||
.thenAnswer { invocation ->
|
||||
val shnarfQueried = invocation.getArgument<ByteArray>(1)
|
||||
val endBlockNumber = when {
|
||||
shnarfQueried.contentEquals(blob3.expectedShnarf) -> blob3.endBlockNumber
|
||||
shnarfQueried.contentEquals(blob5.expectedShnarf) -> blob5.endBlockNumber
|
||||
else -> null
|
||||
shnarfQueried.contentEquals(blob3.expectedShnarf) -> true
|
||||
shnarfQueried.contentEquals(blob5.expectedShnarf) -> true
|
||||
else -> false
|
||||
}
|
||||
SafeFuture.completedFuture(endBlockNumber)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import net.consensys.linea.contract.LineaRollup
|
||||
import net.consensys.linea.contract.LineaRollupAsyncFriendly
|
||||
import net.consensys.toBigInteger
|
||||
import net.consensys.toULong
|
||||
import net.consensys.zkevm.coordinator.clients.smartcontract.LineaContractVersion
|
||||
import net.consensys.zkevm.ethereum.ContractsManager
|
||||
import net.consensys.zkevm.ethereum.Web3jClientManager
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
@@ -35,7 +36,9 @@ class L1EventQuerierIntegrationTest {
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
val deploymentResult = ContractsManager.get().deployLineaRollup().get()
|
||||
val deploymentResult = ContractsManager.get()
|
||||
.deployLineaRollup(contractVersion = LineaContractVersion.V5)
|
||||
.get()
|
||||
testLineaRollupContractAddress = deploymentResult.contractAddress
|
||||
web3Client = Web3jClientManager.l1Client
|
||||
@Suppress("DEPRECATION")
|
||||
|
||||
@@ -10,6 +10,7 @@ import net.consensys.linea.contract.LineaRollup
|
||||
import net.consensys.linea.contract.LineaRollupAsyncFriendly
|
||||
import net.consensys.toBigInteger
|
||||
import net.consensys.toULong
|
||||
import net.consensys.zkevm.coordinator.clients.smartcontract.LineaContractVersion
|
||||
import net.consensys.zkevm.coordinator.clients.smartcontract.LineaRollupSmartContractClient
|
||||
import net.consensys.zkevm.ethereum.ContractsManager
|
||||
import net.consensys.zkevm.ethereum.Web3jClientManager
|
||||
@@ -47,7 +48,9 @@ class MessageServiceIntegrationTest {
|
||||
private lateinit var l2Contract: L2MessageService
|
||||
|
||||
private fun deployContracts() {
|
||||
val l1RollupDeploymentResult = ContractsManager.get().deployLineaRollup().get()
|
||||
val l1RollupDeploymentResult = ContractsManager.get()
|
||||
.deployLineaRollup(contractVersion = LineaContractVersion.V5)
|
||||
.get()
|
||||
@Suppress("DEPRECATION")
|
||||
l1ContractLegacyClient = l1RollupDeploymentResult.rollupOperatorClientLegacy
|
||||
l1ContractClient = l1RollupDeploymentResult.rollupOperatorClient
|
||||
|
||||
@@ -14,6 +14,8 @@ dependencies {
|
||||
implementation("org.web3j:core:${libs.versions.web3j.get()}") {
|
||||
exclude group: 'org.slf4j', module: 'slf4j-nop'
|
||||
}
|
||||
implementation "com.sksamuel.hoplite:hoplite-core:${libs.versions.hoplite.get()}"
|
||||
implementation "com.sksamuel.hoplite:hoplite-toml:${libs.versions.hoplite.get()}"
|
||||
implementation "com.fasterxml.jackson.core:jackson-annotations:${libs.versions.jackson.get()}"
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:${libs.versions.jackson.get()}"
|
||||
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:${libs.versions.jackson.get()}"
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package net.consensys.zkevm.ethereum
|
||||
|
||||
import com.sksamuel.hoplite.ConfigLoaderBuilder
|
||||
import com.sksamuel.hoplite.addFileSource
|
||||
import net.consensys.linea.contract.AsyncFriendlyTransactionManager
|
||||
import net.consensys.linea.contract.EIP1559GasProvider
|
||||
import net.consensys.linea.contract.LineaRollupAsyncFriendly
|
||||
import net.consensys.linea.contract.StaticGasProvider
|
||||
import net.consensys.linea.contract.l1.Web3JLineaRollupSmartContractClient
|
||||
import net.consensys.linea.contract.l2.L2MessageServiceGasLimitEstimate
|
||||
import net.consensys.linea.testing.filesystem.findPathTo
|
||||
import net.consensys.linea.web3j.SmartContractErrors
|
||||
import net.consensys.zkevm.coordinator.clients.smartcontract.LineaContractVersion
|
||||
import net.consensys.zkevm.coordinator.clients.smartcontract.LineaRollupSmartContractClient
|
||||
@@ -45,17 +48,17 @@ interface ContractsManager {
|
||||
*/
|
||||
fun deployLineaRollup(
|
||||
numberOfOperators: Int = 1,
|
||||
contractVersion: LineaContractVersion = LineaContractVersion.V5
|
||||
contractVersion: LineaContractVersion
|
||||
): SafeFuture<LineaRollupDeploymentResult>
|
||||
|
||||
fun deployL2MessageService(): SafeFuture<L2MessageServiceDeploymentResult>
|
||||
|
||||
fun deployRollupAndL2MessageService(
|
||||
dataCompressionAndProofAggregationMigrationBlock: ULong = 1000UL,
|
||||
numberOfOperators: Int = 1
|
||||
numberOfOperators: Int = 1,
|
||||
l1ContractVersion: LineaContractVersion = LineaContractVersion.V5
|
||||
): SafeFuture<ContactsDeploymentResult>
|
||||
|
||||
@Deprecated("Use connectToLineaRollupContractV5 instead")
|
||||
fun connectToLineaRollupContract(
|
||||
contractAddress: String,
|
||||
transactionManager: AsyncFriendlyTransactionManager,
|
||||
@@ -64,18 +67,8 @@ interface ContractsManager {
|
||||
maxFeePerGas = 11_000uL,
|
||||
maxPriorityFeePerGas = 10_000uL,
|
||||
gasLimit = 1_000_000uL
|
||||
)
|
||||
): LineaRollupAsyncFriendly
|
||||
|
||||
fun connectToLineaRollupContractV5(
|
||||
contractAddress: String,
|
||||
transactionManager: AsyncFriendlyTransactionManager,
|
||||
gasProvider: ContractEIP1559GasProvider = StaticGasProvider(
|
||||
L1AccountManager.chainId,
|
||||
maxFeePerGas = 11_000uL,
|
||||
maxPriorityFeePerGas = 10_000uL,
|
||||
gasLimit = 1_000_000uL
|
||||
)
|
||||
),
|
||||
smartContractErrors: SmartContractErrors? = null
|
||||
): LineaRollupSmartContractClient
|
||||
|
||||
fun connectL2MessageService(
|
||||
@@ -94,14 +87,35 @@ interface ContractsManager {
|
||||
smartContractErrors: SmartContractErrors = emptyMap()
|
||||
): L2MessageServiceGasLimitEstimate
|
||||
|
||||
@Deprecated("Use connectToLineaRollupContract instead")
|
||||
fun connectToLineaRollupContractLegacy(
|
||||
contractAddress: String,
|
||||
transactionManager: AsyncFriendlyTransactionManager,
|
||||
gasProvider: ContractEIP1559GasProvider = StaticGasProvider(
|
||||
L1AccountManager.chainId,
|
||||
maxFeePerGas = 11_000uL,
|
||||
maxPriorityFeePerGas = 10_000uL,
|
||||
gasLimit = 1_000_000uL
|
||||
)
|
||||
): LineaRollupAsyncFriendly
|
||||
|
||||
companion object {
|
||||
// TODO: think of better get the Instance
|
||||
fun get(): ContractsManager = MakeFileDelegatedContractsManager
|
||||
}
|
||||
}
|
||||
|
||||
object MakeFileDelegatedContractsManager : ContractsManager {
|
||||
val log = LoggerFactory.getLogger(MakeFileDelegatedContractsManager::class.java)
|
||||
val lineaRollupContractErrors = findPathTo("config")!!
|
||||
.resolve("common/smart-contract-errors.toml")
|
||||
.let { filePath ->
|
||||
data class ErrorsFile(val smartContractErrors: Map<String, String>)
|
||||
ConfigLoaderBuilder.default()
|
||||
.addFileSource(filePath.toAbsolutePath().toString())
|
||||
.build()
|
||||
.loadConfigOrThrow<ErrorsFile>()
|
||||
.smartContractErrors
|
||||
}
|
||||
|
||||
override fun deployLineaRollup(
|
||||
numberOfOperators: Int,
|
||||
@@ -133,12 +147,14 @@ object MakeFileDelegatedContractsManager : ContractsManager {
|
||||
AccountTransactionManager(it, L1AccountManager.getTransactionManager(it))
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val rollupOperatorClient = connectToLineaRollupContract(
|
||||
deploymentResult.address,
|
||||
accountsTxManagers.first().txManager
|
||||
accountsTxManagers.first().txManager,
|
||||
smartContractErrors = lineaRollupContractErrors
|
||||
)
|
||||
val rollupOperatorClientV4 = connectToLineaRollupContractV5(
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val rollupOperatorClientLegacy = connectToLineaRollupContractLegacy(
|
||||
deploymentResult.address,
|
||||
accountsTxManagers.first().txManager
|
||||
)
|
||||
@@ -147,8 +163,8 @@ object MakeFileDelegatedContractsManager : ContractsManager {
|
||||
contractDeploymentAccount = contractDeploymentAccount,
|
||||
contractDeploymentBlockNumber = deploymentResult.blockNumber.toULong(),
|
||||
rollupOperators = accountsTxManagers,
|
||||
rollupOperatorClientLegacy = rollupOperatorClient,
|
||||
rollupOperatorClient = rollupOperatorClientV4
|
||||
rollupOperatorClientLegacy = rollupOperatorClientLegacy,
|
||||
rollupOperatorClient = rollupOperatorClient
|
||||
)
|
||||
}
|
||||
return future
|
||||
@@ -174,9 +190,10 @@ object MakeFileDelegatedContractsManager : ContractsManager {
|
||||
|
||||
override fun deployRollupAndL2MessageService(
|
||||
dataCompressionAndProofAggregationMigrationBlock: ULong,
|
||||
numberOfOperators: Int
|
||||
numberOfOperators: Int,
|
||||
l1ContractVersion: LineaContractVersion
|
||||
): SafeFuture<ContactsDeploymentResult> {
|
||||
return deployLineaRollup(numberOfOperators)
|
||||
return deployLineaRollup(numberOfOperators, l1ContractVersion)
|
||||
.thenCombine(deployL2MessageService()) { lineaRollupDeploymentResult, l2MessageServiceDeploymentResult ->
|
||||
ContactsDeploymentResult(
|
||||
lineaRollup = lineaRollupDeploymentResult,
|
||||
@@ -185,32 +202,18 @@ object MakeFileDelegatedContractsManager : ContractsManager {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use connectToLineaRollupContractV5 instead")
|
||||
override fun connectToLineaRollupContract(
|
||||
contractAddress: String,
|
||||
transactionManager: AsyncFriendlyTransactionManager,
|
||||
gasProvider: ContractEIP1559GasProvider
|
||||
): LineaRollupAsyncFriendly {
|
||||
return LineaRollupAsyncFriendly.load(
|
||||
contractAddress,
|
||||
Web3jClientManager.l1Client,
|
||||
transactionManager,
|
||||
gasProvider,
|
||||
emptyMap()
|
||||
)
|
||||
}
|
||||
|
||||
override fun connectToLineaRollupContractV5(
|
||||
contractAddress: String,
|
||||
transactionManager: AsyncFriendlyTransactionManager,
|
||||
gasProvider: ContractEIP1559GasProvider
|
||||
gasProvider: ContractEIP1559GasProvider,
|
||||
smartContractErrors: SmartContractErrors?
|
||||
): LineaRollupSmartContractClient {
|
||||
return Web3JLineaRollupSmartContractClient.load(
|
||||
contractAddress,
|
||||
Web3jClientManager.l1Client,
|
||||
transactionManager,
|
||||
gasProvider,
|
||||
emptyMap()
|
||||
smartContractErrors ?: lineaRollupContractErrors
|
||||
)
|
||||
}
|
||||
|
||||
@@ -229,4 +232,33 @@ object MakeFileDelegatedContractsManager : ContractsManager {
|
||||
smartContractErrors
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use connectToLineaRollupContract instead")
|
||||
override fun connectToLineaRollupContractLegacy(
|
||||
contractAddress: String,
|
||||
transactionManager: AsyncFriendlyTransactionManager,
|
||||
gasProvider: ContractEIP1559GasProvider
|
||||
): LineaRollupAsyncFriendly {
|
||||
return LineaRollupAsyncFriendly.load(
|
||||
contractAddress,
|
||||
Web3jClientManager.l1Client,
|
||||
transactionManager,
|
||||
gasProvider,
|
||||
emptyMap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
data class SmartContractErrors(val smartContractErrors: Map<String, String>)
|
||||
|
||||
val lineaRollupContractErrors = findPathTo("config")!!
|
||||
.resolve("common/smart-contract-errors.toml")
|
||||
.let { filePath ->
|
||||
ConfigLoaderBuilder.default()
|
||||
.addFileSource(filePath.toAbsolutePath().toString())
|
||||
.build()
|
||||
.loadConfigOrThrow<SmartContractErrors>()
|
||||
}
|
||||
println(lineaRollupContractErrors)
|
||||
}
|
||||
|
||||
@@ -143,10 +143,10 @@ fun makeDeployLineaRollup(
|
||||
// "HARDHAT_DISABLE_CACHE" to "true"
|
||||
)
|
||||
deploymentPrivateKey?.let { env["DEPLOYMENT_PRIVATE_KEY"] = it }
|
||||
val command = if (contractVersion == LineaContractVersion.V5) {
|
||||
"make deploy-linea-rollup"
|
||||
} else {
|
||||
throw IllegalArgumentException("Unsupported contract version: $contractVersion")
|
||||
val command = when (contractVersion) {
|
||||
LineaContractVersion.V5 -> "make deploy-linea-rollup"
|
||||
LineaContractVersion.V6 -> "make deploy-linea-rollup-v6"
|
||||
else -> throw IllegalArgumentException("Unsupported contract version: $contractVersion")
|
||||
}
|
||||
|
||||
return deployContract(
|
||||
|
||||
@@ -16,8 +16,8 @@ dependencies {
|
||||
|
||||
web3jContractWrappers {
|
||||
def contractAbi = layout.buildDirectory.dir("${rootProject.projectDir}/contracts/abi").get()
|
||||
.file("LineaRollupV5.0.abi").asFile.absolutePath
|
||||
.file("LineaRollupV6.0.abi").asFile.absolutePath
|
||||
|
||||
contractsPackage = "net.consensys.linea.contract"
|
||||
contracts = ["$contractAbi": "LineaRollup"]
|
||||
contractsPackage = "build.linea.contract"
|
||||
contracts = ["$contractAbi": "LineaRollupV6"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user