From 5b5c05a96f65a275841fa88c13858029bce98fd7 Mon Sep 17 00:00:00 2001 From: Vyatcheslav Suharnikov Date: Wed, 20 Nov 2024 19:02:50 +0400 Subject: [PATCH] Support a chain registry: - Consensus Client works only if it is approved on a chain registry; - Approvals and rejections are detected during work; - Migrated settings from ficus to pureconfig; - New tests. --- build.sbt | 17 +++-- .../src/test/scala/units/Accounts.scala | 1 + .../scala/units/BaseDockerTestSuite.scala | 3 + .../scala/units/RegistryDockerTestSuite.scala | 30 +++++++++ docker/Dockerfile | 2 +- .../configs/wavesnode/genesis-template.conf | 9 +++ local-network/configs/wavesnode/waves.conf | 3 + local-network/deploy/deploy.py | 6 ++ local-network/deploy/local/network.py | 6 +- src/main/scala/units/ClientConfig.scala | 7 -- src/main/scala/units/ConsensusClient.scala | 13 +++- src/main/scala/units/ELUpdater.scala | 57 +++++++++------- .../scala/units/client/JsonRpcClient.scala | 7 +- .../units/BaseIntegrationTestSuite.scala | 12 +++- .../units/BlockBriefValidationTestSuite.scala | 2 +- .../units/BlockFullValidationTestSuite.scala | 2 +- .../units/BlockIssuesForgingTestSuite.scala | 4 +- .../scala/units/C2ETransfersTestSuite.scala | 2 +- .../scala/units/E2CTransfersTestSuite.scala | 4 +- src/test/scala/units/ExtensionDomain.scala | 6 +- src/test/scala/units/RegistryTestSuite.scala | 67 +++++++++++++++++++ src/test/scala/units/TestSettings.scala | 20 ++++-- .../scala/units/client/TestEcClients.scala | 7 +- .../HasConsensusLayerDappTxHelpers.scala | 22 +++++- 24 files changed, 241 insertions(+), 68 deletions(-) create mode 100644 consensus-client-it/src/test/scala/units/RegistryDockerTestSuite.scala create mode 100644 src/test/scala/units/RegistryTestSuite.scala diff --git a/build.sbt b/build.sbt index 1060985f..d8dae008 100644 --- a/build.sbt +++ b/build.sbt @@ -30,13 +30,16 @@ inScope(Global)( name := "consensus-client" maintainer := "Units Network Team" -libraryDependencies ++= Seq( - "com.wavesplatform" % "node-testkit" % "1.5.8" % Test, - "com.wavesplatform" % "node" % "1.5.8" % Provided, - "com.softwaremill.sttp.client3" % "core_2.13" % "3.10.1", - "com.softwaremill.sttp.client3" %% "play-json" % "3.10.1", - "com.github.jwt-scala" %% "jwt-play-json" % "10.0.1" -) +libraryDependencies ++= { + val nodeVersion = "1.5-3977-SNAPSHOT" + Seq( + "com.wavesplatform" % "node-testkit" % nodeVersion % Test, + "com.wavesplatform" % "node" % nodeVersion % Provided, + "com.softwaremill.sttp.client3" % "core_2.13" % "3.10.1", + "com.softwaremill.sttp.client3" %% "play-json" % "3.10.1", + "com.github.jwt-scala" %% "jwt-play-json" % "10.0.1" + ) +} Compile / packageDoc / publishArtifact := false diff --git a/consensus-client-it/src/test/scala/units/Accounts.scala b/consensus-client-it/src/test/scala/units/Accounts.scala index 227e60d0..8abdc383 100644 --- a/consensus-client-it/src/test/scala/units/Accounts.scala +++ b/consensus-client-it/src/test/scala/units/Accounts.scala @@ -9,6 +9,7 @@ import units.eth.EthAddress import java.nio.charset.StandardCharsets trait Accounts { + val chainRegistryAccount: KeyPair = mkKeyPair("devnet registry", 0) val chainContractAccount: KeyPair = mkKeyPair("devnet cc", 0) val miner11Account = mkKeyPair("devnet-1", 0) diff --git a/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala index 2cce8fb5..cc857c46 100644 --- a/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala @@ -78,6 +78,9 @@ trait BaseDockerTestSuite } protected def setupChain(): Unit = { + log.info("Approve chain on registry") + waves1.api.broadcast(ChainRegistry.approve()) + log.info("Set script") waves1.api.broadcastAndWait(ChainContract.setScript()) diff --git a/consensus-client-it/src/test/scala/units/RegistryDockerTestSuite.scala b/consensus-client-it/src/test/scala/units/RegistryDockerTestSuite.scala new file mode 100644 index 00000000..81d709ad --- /dev/null +++ b/consensus-client-it/src/test/scala/units/RegistryDockerTestSuite.scala @@ -0,0 +1,30 @@ +package units + +class RegistryDockerTestSuite extends BaseDockerTestSuite { + "Approved, rejected, approved - mining works when approved" in { + step(s"Wait miner 1 (${miner11Account.toAddress}) forge at least one block") + chainContract.waitForHeight(1L) + + step("Broadcast a reject transaction") + val rejectTxnHeight = waves1.api.broadcastAndWait(ChainRegistry.reject()).height + + step("Wait a rejection height") + waves1.api.waitForHeight(rejectTxnHeight + 1) + val lastElBlock1 = chainContract.getLastBlockMeta(0L).value + + step("Expect no mining") + waves1.api.waitForHeight(rejectTxnHeight + 2) + + val lastElBlock2 = chainContract.getLastBlockMeta(0L).value + withClue("Same block - no mining: ") { + lastElBlock2.hash shouldBe lastElBlock1.hash + } + + val approveTxnHeight = waves1.api.broadcastAndWait(ChainRegistry.approve()).height + step("Wait an approval height") + waves1.api.waitForHeight(approveTxnHeight + 1) + + step("Mining working") + chainContract.waitForHeight(lastElBlock2.height + 1) + } +} diff --git a/docker/Dockerfile b/docker/Dockerfile index 8d6d3938..b1c29f21 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG baseImage=ghcr.io/wavesplatform/waves:1.5.8-1 +ARG baseImage=ghcr.io/wavesplatform/waves:units-registry FROM $baseImage COPY target /tmp/ RUN tar zxvf /tmp/consensus-client.tgz -C $WAVES_INSTALL_PATH --strip-components=1 diff --git a/local-network/configs/wavesnode/genesis-template.conf b/local-network/configs/wavesnode/genesis-template.conf index 99724a5c..91aaa3ce 100644 --- a/local-network/configs/wavesnode/genesis-template.conf +++ b/local-network/configs/wavesnode/genesis-template.conf @@ -62,6 +62,15 @@ genesis-generator { # Address: 3FVp6fUSmVehs7Q3uJBXe5kWtpz57f7azzA { seed-text = "devnet dao", nonce = 0, amount = 100000000 } + # Chain registry + # Seed text: devnet registry + # Nonce: 0 + # Seed: 3oujRTe9jVvnMHcKe9vGG + # Private key: 3kqzGQiNy5qTKEGDzCxh6Kk8LLXW5Lh22pLmZvFNqDm1 + # Public key: HRHrDistEQkJJ7dBxBBzxfSsbN8oEF8xt3FeUHLNBYfX + # Address: 3FibjK5ZAXFfJf9J3gDsheia88Vz2itLPu1 + { seed-text = "devnet registry", nonce = 0, amount = 100000000 } + # Chain contract # Seed text: devnet cc # Nonce: 0 diff --git a/local-network/configs/wavesnode/waves.conf b/local-network/configs/wavesnode/waves.conf index 4acda82f..663b5e1e 100644 --- a/local-network/configs/wavesnode/waves.conf +++ b/local-network/configs/wavesnode/waves.conf @@ -41,7 +41,9 @@ waves { dao-address = "3FVGizGxjdi8m4e8WKjbwtzg4rdiiUGkLCu" xtn-buyback-address = "3FZQMZsyypqDUk2r5katP1AUChe7Uzc9dC4" xtn-buyback-reward-period = 20 + units-registry-address = "3FibjK5ZAXFfJf9J3gDsheia88Vz2itLPu1" } + rewards { term = 100000 term-after-capped-reward-feature = 5 @@ -49,6 +51,7 @@ waves { min-increment = 50000000 voting-interval = 1 } + include "genesis.conf" include "/etc/it/genesis.conf" } diff --git a/local-network/deploy/deploy.py b/local-network/deploy/deploy.py index 90eef034..49a94de8 100644 --- a/local-network/deploy/deploy.py +++ b/local-network/deploy/deploy.py @@ -23,8 +23,14 @@ log.info(f"Wait for {min_peers} peers, now: {r}") sleep(2) +log.info(f"Registry address: {network.cl_registry.oracleAddress}") log.info(f"Chain contract address: {network.cl_chain_contract.oracleAddress}") +log.info("Approve chain contract on registry") +network.cl_registry.storeData( + f"unit_{network.cl_chain_contract.oracleAddress}_approved", "boolean", True +) + script_info = network.cl_chain_contract.oracleAcc.scriptInfo() if script_info["script"] is None: log.info("Set chain contract script") diff --git a/local-network/deploy/local/network.py b/local-network/deploy/local/network.py index ac808767..68723222 100644 --- a/local-network/deploy/local/network.py +++ b/local-network/deploy/local/network.py @@ -28,9 +28,13 @@ class Miner: class ExtendedNetwork(Network): @cached_property - def cl_dao(self) -> ChainContract: + def cl_dao(self) -> pw.Address: return pw.Address(seed="devnet dao", nonce=0) + @cached_property + def cl_registry(self) -> pw.Oracle: + return pw.Oracle(seed="devnet registry") + @cached_property def cl_chain_contract(self) -> ChainContract: return ChainContract(seed="devnet cc", nonce=0) diff --git a/src/main/scala/units/ClientConfig.scala b/src/main/scala/units/ClientConfig.scala index b35e5473..89889984 100644 --- a/src/main/scala/units/ClientConfig.scala +++ b/src/main/scala/units/ClientConfig.scala @@ -3,9 +3,6 @@ package units import com.wavesplatform.account.Address import com.wavesplatform.common.utils.EitherExt2 import com.wavesplatform.settings.* -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.arbitraryTypeValueReader -import net.ceedubs.ficus.readers.{Generated, ValueReader} import units.client.JsonRpcClient import scala.concurrent.duration.FiniteDuration @@ -29,7 +26,3 @@ case class ClientConfig( apiRequestRetryWaitTime = apiRequestRetryWaitTime ) } - -object ClientConfig { - implicit val valueReader: Generated[ValueReader[ClientConfig]] = arbitraryTypeValueReader -} diff --git a/src/main/scala/units/ConsensusClient.scala b/src/main/scala/units/ConsensusClient.scala index d8b24dcb..5781a75c 100644 --- a/src/main/scala/units/ConsensusClient.scala +++ b/src/main/scala/units/ConsensusClient.scala @@ -1,14 +1,17 @@ package units +import com.typesafe.config.Config import com.typesafe.scalalogging.StrictLogging import com.wavesplatform.block.{Block, MicroBlock} import com.wavesplatform.common.state.ByteStr +import com.wavesplatform.common.utils.EitherExt2 import com.wavesplatform.events.BlockchainUpdateTriggers import com.wavesplatform.extensions.{Extension, Context as ExtensionContext} import com.wavesplatform.state.{Blockchain, StateSnapshot} import io.netty.channel.group.DefaultChannelGroup import monix.execution.{CancelableFuture, Scheduler} -import net.ceedubs.ficus.Ficus.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import units.ConsensusClient.ChainHandler import units.client.engine.EngineApiClient import units.network.* @@ -28,15 +31,18 @@ class ConsensusClient(context: ExtensionContext) extends StrictLogging with Exte private val chainHandlers: Seq[ChainHandler] = { val defaultConfig = context.settings.config.getConfig("units.defaults") + def load(cfg: Config): ClientConfig = + ConfigSource.fromConfig(cfg.withFallback(defaultConfig).resolve()).loadOrThrow[ClientConfig] + val legacyChainConfig = - Try(context.settings.config.getConfig("waves.l2")).toOption.map(_.withFallback(defaultConfig).as[ClientConfig]).tapEach { _ => + Try(context.settings.config.getConfig("waves.l2")).toOption.map(load).tapEach { _ => logger.info("Consensus client settings at waves.l2 path have been deprecated, please update your config file") } val newChainConfigs = context.settings.config .getConfigList("units.chains") .asScala - .map(cfg => cfg.withFallback(defaultConfig).resolve().as[ClientConfig]) + .map(load) val allChainConfigs = legacyChainConfig ++ newChainConfigs @@ -93,6 +99,7 @@ object ConsensusClient { config, context.time, context.wallet, + context.settings.blockchainSettings.functionalitySettings.unitsRegistryAddressParsed.explicitGet(), blockObserver.loadBlock, context.broadcastTransaction, eluScheduler, diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala index e829acab..f16e130d 100644 --- a/src/main/scala/units/ELUpdater.scala +++ b/src/main/scala/units/ELUpdater.scala @@ -10,9 +10,9 @@ import com.wavesplatform.crypto import com.wavesplatform.lang.ValidationError import com.wavesplatform.lang.v1.compiler.Terms.FUNCTION_CALL import com.wavesplatform.network.ChannelGroupExt -import com.wavesplatform.state.Blockchain import com.wavesplatform.state.diffs.FeeValidation.{FeeConstants, FeeUnit, ScriptExtraFee} import com.wavesplatform.state.diffs.TransactionDiffer.TransactionValidationError +import com.wavesplatform.state.{Blockchain, BooleanDataEntry} import com.wavesplatform.transaction.TxValidationError.InvokeRejectError import com.wavesplatform.transaction.smart.InvokeScriptTransaction import com.wavesplatform.transaction.smart.script.trace.TracedResult @@ -50,6 +50,7 @@ class ELUpdater( config: ClientConfig, time: Time, wallet: Wallet, + registryAddress: Option[Address], requestBlockFromPeers: BlockHash => CancelableFuture[BlockWithChannel], broadcastTx: Transaction => TracedResult[ValidationError, Boolean], scheduler: Scheduler, @@ -147,23 +148,6 @@ class ELUpdater( } } - private def cleanPriorityPool(): Unit = { - // A transaction moves to priority pool when a new key block references one of the previous micro blocks. - // When we add a new fresh transaction (extendMainChain) to UTX, it is validated against a stale transaction changes. - // Removing here, because we have these transactions in PP after the onProcessBlock trigger - utx.getPriorityPool.foreach { pp => - val staleTxs = pp.priorityTransactions.filter { - case tx: InvokeScriptTransaction => tx.dApp == contractAddress - case _ => false - } - - if (staleTxs.nonEmpty) { - logger.debug(s"Removing stale transactions: ${staleTxs.map(_.id()).mkString(", ")}") - utx.removeAll(staleTxs) - } - } - } - private def callContract(fc: FUNCTION_CALL, blockData: EcBlock, invoker: KeyPair): JobResult[Unit] = { val extraFee = if (blockchain.hasPaidVerifier(invoker.toAddress)) ScriptExtraFee else 0 @@ -180,7 +164,6 @@ class ELUpdater( blockchain.settings.addressSchemeCharacter.toByte ).signWith(invoker.privateKey) logger.info(s"Invoking $contractAddress '${fc.function.funcName}' for block ${blockData.hash}->${blockData.parentHash}, txId=${tx.id()}") - cleanPriorityPool() broadcastTx(tx).resultE match { case Right(true) => Either.unit @@ -250,7 +233,7 @@ class ELUpdater( ) } case Working(_, _, _, _, _, _: Mining | _: FollowingChain, _, _) => - // a new epoch started and we trying to apply a previous epoch payload: + // a new epoch started, and we're trying to apply a previous epoch payload: // Mining - we mine again // FollowingChain - we validate case other => logger.debug(s"Unexpected state $other attempting to finish building $payloadId") @@ -485,10 +468,32 @@ class ELUpdater( } private def handleConsensusLayerChanged(): Unit = { - state match { - case Starting => updateStartingState() - case w: Working[ChainStatus] => updateWorkingState(w) - case other => logger.debug(s"Unprocessed state: $other") + def stopMining(): Unit = setState("26", Starting) + + isChainEnabled match { + case Left(e) => + logger.warn(s"$contractAddress chain is disabled: $e") + stopMining() + + case Right(false) => + logger.warn(s"$contractAddress chain is disabled") + stopMining() + + case Right(true) => + state match { + case Starting => updateStartingState() + case w: Working[ChainStatus] => updateWorkingState(w) + case other => logger.debug(s"Unprocessed state: $other") + } + } + } + + private def isChainEnabled: Either[String, Boolean] = registryAddress.fold(true.asRight[String]) { registryAddress => + val key = registryKey(contractAddress) + blockchain.accountData(registryAddress, key) match { + case Some(BooleanDataEntry(_, isEnabled)) => isEnabled.asRight + case None => false.asRight + case Some(x) => s"Expected '$key' to be a boolean, got: $x".asLeft } } @@ -1572,7 +1577,7 @@ object ELUpdater { case class ChainSwitchInfo(prevChainId: Long, referenceBlock: ContractBlock) - /** We haven't received a EC-block {@link missedBlock} of a previous epoch when started a mining on a new epoch. We can return to the main chain, if + /** We haven't received an EC-block [[missedBlock]] of a previous epoch when started a mining on a new epoch. We can return to the main chain, if we * get a missed EC-block. */ case class ReturnToMainChainInfo(missedBlock: ContractBlock, missedBlockParent: EcBlock, chainId: Long) @@ -1599,4 +1604,6 @@ object ELUpdater { val msg = hitSource.arr ++ HexBytesConverter.toBytes(parentHash) HexBytesConverter.toHex(crypto.secureHash(msg)) } + + def registryKey(chainContract: Address): String = s"unit_${chainContract}_approved" } diff --git a/src/main/scala/units/client/JsonRpcClient.scala b/src/main/scala/units/client/JsonRpcClient.scala index d046a9b8..3251d056 100644 --- a/src/main/scala/units/client/JsonRpcClient.scala +++ b/src/main/scala/units/client/JsonRpcClient.scala @@ -2,10 +2,9 @@ package units.client import cats.Id import cats.syntax.either.* -import net.ceedubs.ficus.Ficus.* -import net.ceedubs.ficus.readers.ArbitraryTypeReader.arbitraryTypeValueReader -import net.ceedubs.ficus.readers.{Generated, ValueReader} import play.api.libs.json.{JsError, JsValue, Reads, Writes} +import pureconfig.* +import pureconfig.generic.semiauto.* import sttp.client3.* import sttp.client3.playJson.* import units.ClientError @@ -79,7 +78,7 @@ object JsonRpcClient { } object Config { - implicit val configValueReader: Generated[ValueReader[Config]] = arbitraryTypeValueReader + implicit val configValueReader: ConfigReader[Config] = deriveReader } def newRequestId: Int = ThreadLocalRandom.current().nextInt(10000, 100000) diff --git a/src/test/scala/units/BaseIntegrationTestSuite.scala b/src/test/scala/units/BaseIntegrationTestSuite.scala index 4acfd31a..b12468a4 100644 --- a/src/test/scala/units/BaseIntegrationTestSuite.scala +++ b/src/test/scala/units/BaseIntegrationTestSuite.scala @@ -1,5 +1,6 @@ package units +import com.wavesplatform.account.KeyPair import com.wavesplatform.database.{RDB, loadActiveLeases} import com.wavesplatform.db.WithDomain import com.wavesplatform.db.WithState.AddrWithBalance @@ -16,6 +17,7 @@ import units.eth.{EthAddress, Gwei} import units.test.CustomMatchers import units.util.HexBytesConverter +import java.nio.charset.StandardCharsets import java.util.concurrent.ThreadLocalRandom trait BaseIntegrationTestSuite @@ -27,15 +29,19 @@ trait BaseIntegrationTestSuite with EitherValues with OptionValues with CustomMatchers { - protected def defaultSettings = TestSettings.Default + protected val chainRegistryAccount: KeyPair = KeyPair("chain-registry".getBytes(StandardCharsets.UTF_8)) + protected val elMinerDefaultReward = Gwei.ofRawGwei(2_000_000_000L) protected val elBridgeAddress = EthAddress.unsafeFrom("0x0000000000000000000000000000000000006a7e") + protected def defaultSettings = TestSettings().withChainRegistry(chainRegistryAccount.toAddress) + protected def withExtensionDomain[R](settings: TestSettings = defaultSettings)(f: ExtensionDomain => R): R = withExtensionDomainUninitialized(settings) { d => log.debug("EL init") val txs = List( + d.ChainRegistry.approve(), d.ChainContract.setScript(), d.ChainContract.setup( d.ecGenesisBlock, @@ -51,7 +57,7 @@ trait BaseIntegrationTestSuite f(d) } - private def withExtensionDomainUninitialized[R](settings: TestSettings)(test: ExtensionDomain => R): R = + protected def withExtensionDomainUninitialized[R](settings: TestSettings = defaultSettings)(test: ExtensionDomain => R): R = withRocksDBWriter(settings.wavesSettings) { blockchain => var d: ExtensionDomain = null val bcu = new BlockchainUpdaterImpl( @@ -68,6 +74,7 @@ trait BaseIntegrationTestSuite blockchainUpdater = bcu, rocksDBWriter = blockchain, settings = settings.wavesSettings, + chainRegistryAccount = chainRegistryAccount, elBridgeAddress = elBridgeAddress, elMinerDefaultReward = elMinerDefaultReward ) @@ -76,6 +83,7 @@ trait BaseIntegrationTestSuite val balances = List( AddrWithBalance(TxHelpers.defaultAddress, 1_000_000.waves), + AddrWithBalance(d.chainRegistryAddress, 10.waves), AddrWithBalance(d.chainContractAddress, 10.waves) ) ++ settings.finalAdditionalBalances diff --git a/src/test/scala/units/BlockBriefValidationTestSuite.scala b/src/test/scala/units/BlockBriefValidationTestSuite.scala index ada4ebf5..b2dbc291 100644 --- a/src/test/scala/units/BlockBriefValidationTestSuite.scala +++ b/src/test/scala/units/BlockBriefValidationTestSuite.scala @@ -7,7 +7,7 @@ import units.eth.EthAddress class BlockBriefValidationTestSuite extends BaseIntegrationTestSuite { private val miner = ElMinerSettings(TxHelpers.signer(1)) - override protected val defaultSettings: TestSettings = TestSettings.Default.copy( + override protected val defaultSettings: TestSettings = super.defaultSettings.copy( initialMiners = List(miner) ) diff --git a/src/test/scala/units/BlockFullValidationTestSuite.scala b/src/test/scala/units/BlockFullValidationTestSuite.scala index 99c9a941..d589804e 100644 --- a/src/test/scala/units/BlockFullValidationTestSuite.scala +++ b/src/test/scala/units/BlockFullValidationTestSuite.scala @@ -16,7 +16,7 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite { private val reliable = ElMinerSettings(TxHelpers.signer(1)) private val malfunction = ElMinerSettings(TxHelpers.signer(2)) // Prevents a block finalization - override protected val defaultSettings: TestSettings = TestSettings.Default.copy( + override protected val defaultSettings: TestSettings = super.defaultSettings.copy( initialMiners = List(reliable, malfunction) ) diff --git a/src/test/scala/units/BlockIssuesForgingTestSuite.scala b/src/test/scala/units/BlockIssuesForgingTestSuite.scala index d4322398..848218cd 100644 --- a/src/test/scala/units/BlockIssuesForgingTestSuite.scala +++ b/src/test/scala/units/BlockIssuesForgingTestSuite.scala @@ -13,11 +13,11 @@ import scala.concurrent.duration.DurationInt class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { private val transferReceiver = TxHelpers.secondSigner - private val thisMiner = ElMinerSettings(Wallet.generateNewAccount(TestSettings.Default.walletSeed, 0)) + private val thisMiner = ElMinerSettings(Wallet.generateNewAccount(super.defaultSettings.walletSeed, 0)) private val otherMiner1 = ElMinerSettings(TxHelpers.signer(2)) private val otherMiner2 = ElMinerSettings(TxHelpers.signer(3)) - override protected val defaultSettings: TestSettings = TestSettings.Default + override protected val defaultSettings: TestSettings = super.defaultSettings .copy( initialMiners = List(thisMiner, otherMiner1, otherMiner2), additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, DefaultFees.ChainContract.withdrawFee)) diff --git a/src/test/scala/units/C2ETransfersTestSuite.scala b/src/test/scala/units/C2ETransfersTestSuite.scala index 5840f43e..514579d0 100644 --- a/src/test/scala/units/C2ETransfersTestSuite.scala +++ b/src/test/scala/units/C2ETransfersTestSuite.scala @@ -11,7 +11,7 @@ class C2ETransfersTestSuite extends BaseIntegrationTestSuite { private val validTransferRecipient = "1111111111111111111111111111111111111111" private val unrelatedAsset = TxHelpers.issue(issuer = transferSenderAccount) - override protected val defaultSettings: TestSettings = TestSettings.Default.copy( + override protected val defaultSettings: TestSettings = super.defaultSettings.copy( additionalBalances = List(AddrWithBalance(transferSenderAccount.toAddress)) ) diff --git a/src/test/scala/units/E2CTransfersTestSuite.scala b/src/test/scala/units/E2CTransfersTestSuite.scala index cefac22f..5f270369 100644 --- a/src/test/scala/units/E2CTransfersTestSuite.scala +++ b/src/test/scala/units/E2CTransfersTestSuite.scala @@ -25,10 +25,10 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { private val e2CTransfersRootHashHex = HexBytesConverter.toHex(Bridge.mkTransfersHash(ecBlockLogs).explicitGet()) private val transferProofs = Bridge.mkTransferProofs(List(transfer), 0).reverse // Contract requires from bottom to top - private val reliable = ElMinerSettings(Wallet.generateNewAccount(TestSettings.Default.walletSeed, 0)) + private val reliable = ElMinerSettings(Wallet.generateNewAccount(super.defaultSettings.walletSeed, 0)) private val malfunction = ElMinerSettings(TxHelpers.signer(2)) // Prevents block finalization - override protected val defaultSettings: TestSettings = TestSettings.Default.copy( + override protected val defaultSettings: TestSettings = super.defaultSettings.copy( initialMiners = List(reliable), additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, DefaultFees.ChainContract.withdrawFee)) ) diff --git a/src/test/scala/units/ExtensionDomain.scala b/src/test/scala/units/ExtensionDomain.scala index 3c9eeeed..f78b8687 100644 --- a/src/test/scala/units/ExtensionDomain.scala +++ b/src/test/scala/units/ExtensionDomain.scala @@ -33,10 +33,11 @@ import monix.execution.ExecutionModel import monix.execution.schedulers.TestScheduler import monix.reactive.Observable import monix.reactive.subjects.PublishSubject -import net.ceedubs.ficus.Ficus.* import org.scalatest.exceptions.TestFailedException import org.web3j.abi.datatypes.generated.Uint256 import play.api.libs.json.* +import pureconfig.ConfigSource +import pureconfig.generic.auto.* import units.ELUpdater.* import units.ELUpdater.State.{ChainStatus, Working} import units.ExtensionDomain.* @@ -58,6 +59,7 @@ class ExtensionDomain( blockchainUpdater: BlockchainUpdaterImpl, rocksDBWriter: RocksDBWriter, settings: WavesSettings, + override val chainRegistryAccount: KeyPair, elBridgeAddress: EthAddress, elMinerDefaultReward: Gwei ) extends Domain(rdb, blockchainUpdater, rocksDBWriter, settings) @@ -67,7 +69,7 @@ class ExtensionDomain( with ScorexLogging { self => override val chainContractAccount: KeyPair = KeyPair("chain-contract".getBytes(StandardCharsets.UTF_8)) - val l2Config = settings.config.as[ClientConfig]("units.defaults") + val l2Config = ConfigSource.fromConfig(settings.config).at("units.defaults").loadOrThrow[ClientConfig] require(l2Config.chainContractAddress == chainContractAddress, "Check settings") val ecGenesisBlock = EcBlock( diff --git a/src/test/scala/units/RegistryTestSuite.scala b/src/test/scala/units/RegistryTestSuite.scala new file mode 100644 index 00000000..9d5fa8ff --- /dev/null +++ b/src/test/scala/units/RegistryTestSuite.scala @@ -0,0 +1,67 @@ +package units + +import com.wavesplatform.state.StringDataEntry +import com.wavesplatform.transaction.{DataTransaction, TxHelpers} +import com.wavesplatform.wallet.Wallet + +class RegistryTestSuite extends BaseIntegrationTestSuite { + private val miner = ElMinerSettings(Wallet.generateNewAccount(super.defaultSettings.walletSeed, 0)) + private val irrelevantChainAddress = TxHelpers.signer(10).toAddress + + override protected val defaultSettings: TestSettings = super.defaultSettings + .copy(initialMiners = List(miner)) + .withEnabledElMining + + "Mining doesn't start when chains registry" - { + def noMiningTest(registryTxn: ExtensionDomain => List[DataTransaction]): Unit = run(0)(registryTxn(_)) + + "doesn't have a key" in noMiningTest(_ => Nil) + + "has only an irrelevant chain" in noMiningTest { d => + d.ChainRegistry.approve(irrelevantChainAddress) :: Nil + } + + "has an invalid key" in noMiningTest { d => + val wrongTypeData = StringDataEntry(ELUpdater.registryKey(d.chainContractAddress), "true") + d.ChainRegistry.setStatus(wrongTypeData) :: Nil + } + + "has a rejected chain" in noMiningTest(_.ChainRegistry.reject() :: Nil) + } + + "Mining starts when chain registry has an approved chain" in run(1) { d => + List( + d.ChainRegistry.approve(irrelevantChainAddress), + d.ChainRegistry.approve() + ) + } + + private def run(miningAttempts: Int)(registryTxn: ExtensionDomain => List[DataTransaction]): Unit = { + val settings = defaultSettings + withExtensionDomainUninitialized(settings) { d => + log.debug("EL init") + val txs = { + registryTxn(d) ++ + List( + d.ChainContract.setScript(), + d.ChainContract.setup( + d.ecGenesisBlock, + elMinerDefaultReward.amount.longValue(), + defaultSettings.daoRewardAccount.map(_.toAddress), + defaultSettings.daoRewardAmount + ) + ) ++ + settings.initialMiners.map { x => d.ChainContract.join(x.account, x.elRewardAddress) } + } + + d.appendBlock(txs*) + d.advanceConsensusLayerChanged() + + step("Start new epoch for ecBlock") + d.advanceNewBlocks(miner.address) + d.advanceConsensusLayerChanged() + + d.ecClients.miningAttempts shouldBe miningAttempts + } + } +} diff --git a/src/test/scala/units/TestSettings.scala b/src/test/scala/units/TestSettings.scala index 6aa28955..f165691f 100644 --- a/src/test/scala/units/TestSettings.scala +++ b/src/test/scala/units/TestSettings.scala @@ -1,7 +1,7 @@ package units import com.typesafe.config.ConfigFactory -import com.wavesplatform.account.{KeyPair, SeedKeyPair} +import com.wavesplatform.account.{Address, KeyPair, SeedKeyPair} import com.wavesplatform.db.WithState.AddrWithBalance import com.wavesplatform.settings.WavesSettings import com.wavesplatform.test.{DomainPresets, NumericExt} @@ -21,15 +21,23 @@ case class TestSettings( def walletSeed: Array[Byte] = wavesSettings.walletSettings.seed.getOrElse(throw new RuntimeException("No wallet seed")).arr - def withEnabledElMining: TestSettings = copy(wavesSettings = Waves.WithMining) + def withEnabledElMining: TestSettings = copy(wavesSettings = + wavesSettings.copy(config = ConfigFactory.parseString("units.defaults.mining-enable = true").withFallback(wavesSettings.config)) + ) + + def withChainRegistry(address: Address): TestSettings = copy(wavesSettings = Waves.withChainRegistry(wavesSettings, Some(address))) } object TestSettings { - val Default = TestSettings() - private object Waves { - val Default = DomainPresets.TransactionStateSnapshot - val WithMining = Default.copy(config = ConfigFactory.parseString("units.defaults.mining-enable = true").withFallback(Default.config)) + val Default = withChainRegistry(DomainPresets.TransactionStateSnapshot, None) + + def withChainRegistry(settings: WavesSettings, address: Option[Address]): WavesSettings = + settings.copy(blockchainSettings = + settings.blockchainSettings.copy(functionalitySettings = + settings.blockchainSettings.functionalitySettings.copy(unitsRegistryAddress = address.map(_.toString)) + ) + ) } } diff --git a/src/test/scala/units/client/TestEcClients.scala b/src/test/scala/units/client/TestEcClients.scala index 0225f1b6..2128dd27 100644 --- a/src/test/scala/units/client/TestEcClients.scala +++ b/src/test/scala/units/client/TestEcClients.scala @@ -62,6 +62,9 @@ class TestEcClients private ( */ def fullValidatedBlocks: Set[BlockHash] = getLogsCalls.get() + private val forkChoiceUpdateWithPayloadIdCalls = AtomicInt(0) + def miningAttempts: Int = forkChoiceUpdateWithPayloadIdCalls.get() + val engineApi = new LoggedEngineApiClient( new EngineApiClient { override def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash, requestId: Int): JobResult[PayloadStatus] = { @@ -87,7 +90,8 @@ class TestEcClients private ( prevRandao: String, withdrawals: Vector[Withdrawal], requestId: Int - ): JobResult[PayloadId] = + ): JobResult[PayloadId] = { + forkChoiceUpdateWithPayloadIdCalls.increment() forgingBlocks .get() .collectFirst { case fb if fb.testBlock.parentHash == lastBlockHash => fb } match { @@ -98,6 +102,7 @@ class TestEcClients private ( case Some(fb) => fb.payloadId.asRight } + } override def getPayload(payloadId: PayloadId, requestId: Int): JobResult[JsObject] = forgingBlocks.transformAndExtract(_.withoutFirst { fb => fb.payloadId == payloadId }) match { diff --git a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala index c0c67fc7..4153c5b5 100644 --- a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala +++ b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala @@ -6,11 +6,12 @@ import com.wavesplatform.common.merkle.Digest import com.wavesplatform.common.state.ByteStr import com.wavesplatform.common.utils.EitherExt2 import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.state.{BooleanDataEntry, DataEntry} import com.wavesplatform.test.NumericExt import com.wavesplatform.transaction.TxHelpers.defaultSigner import com.wavesplatform.transaction.smart.{InvokeScriptTransaction, SetScriptTransaction} -import com.wavesplatform.transaction.{Asset, TxHelpers} -import units.BlockHash +import com.wavesplatform.transaction.{Asset, DataTransaction, TxHelpers} +import units.{BlockHash, ELUpdater} import units.client.L2BlockLike import units.client.contract.HasConsensusLayerDappTxHelpers.* import units.client.contract.HasConsensusLayerDappTxHelpers.DefaultFees.ChainContract.* @@ -22,6 +23,23 @@ trait HasConsensusLayerDappTxHelpers { def chainContractAccount: KeyPair lazy val chainContractAddress: Address = chainContractAccount.toAddress + def chainRegistryAccount: KeyPair + lazy val chainRegistryAddress: Address = chainRegistryAccount.toAddress + + object ChainRegistry { + def approve(chainContract: Address = chainContractAddress): DataTransaction = + setStatus(BooleanDataEntry(ELUpdater.registryKey(chainContract), value = true)) + + def reject(chainContract: Address = chainContractAddress): DataTransaction = + setStatus(BooleanDataEntry(ELUpdater.registryKey(chainContract), value = false)) + + def setStatus(status: DataEntry[?]): DataTransaction = + TxHelpers.data( + account = chainRegistryAccount, + entries = List(status) + ) + } + object ChainContract { def setScript(): SetScriptTransaction = TxHelpers.setScript(chainContractAccount, CompiledChainContract.script, fee = setScriptFee)