diff --git a/consensus-client-it/build.sbt b/consensus-client-it/build.sbt index 43ad5d97..95ac4481 100644 --- a/consensus-client-it/build.sbt +++ b/consensus-client-it/build.sbt @@ -7,7 +7,7 @@ import java.time.format.DateTimeFormatter description := "Consensus client integration tests" libraryDependencies ++= Seq( - "org.testcontainers" % "testcontainers" % "1.20.2", + "org.testcontainers" % "testcontainers" % "1.20.3", "org.web3j" % "core" % "4.9.8" ).map(_ % Test) diff --git a/consensus-client-it/src/test/resources/logback-test.xml b/consensus-client-it/src/test/resources/logback-test.xml index 1a2e690a..da302e6f 100644 --- a/consensus-client-it/src/test/resources/logback-test.xml +++ b/consensus-client-it/src/test/resources/logback-test.xml @@ -1,5 +1,7 @@ + + diff --git a/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingBackend.scala b/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingBackend.scala index 968823a4..72517c5a 100644 --- a/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingBackend.scala +++ b/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingBackend.scala @@ -11,7 +11,7 @@ class LoggingBackend[F[_], P](delegate: SttpBackend[F, P]) extends DelegateSttpB l.filter(_.logRequest).foreach { l => var logStr = s"${l.prefix} ${request.method} ${request.uri}" - if (l.logResponseBody) logStr += s": body=${request.body.show}" + if (l.logRequestBody) logStr += s": body=${request.body.show}" log.debug(logStr) } diff --git a/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala b/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala index 4e3db216..3c0575b4 100644 --- a/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala +++ b/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala @@ -26,6 +26,22 @@ import scala.util.chaining.scalaUtilChainingOps class NodeHttpApi(apiUri: Uri, backend: SttpBackend[Identity, ?], averageBlockDelay: FiniteDuration) extends HasRetry with ScorexLogging { protected override implicit val patienceConfig: PatienceConfig = PatienceConfig(timeout = averageBlockDelay, interval = 1.second) + def blockHeader(atHeight: Int): Option[BlockHeaderResponse] = { + val loggingOptions: LoggingOptions = LoggingOptions() + log.debug(s"${loggingOptions.prefix} blockHeader($atHeight)") + basicRequest + .get(uri"$apiUri/blocks/headers/at/$atHeight") + .response(asJson[BlockHeaderResponse]) + .tag(LoggingOptionsTag, loggingOptions) + .send(backend) + .body match { + case Left(HttpError(_, StatusCode.NotFound)) => none + case Left(HttpError(body, statusCode)) => throw new RuntimeException(s"Server returned error $body with status ${statusCode.code}") + case Left(DeserializationException(body, error)) => throw new RuntimeException(s"failed to parse response $body: $error") + case Right(r) => r.some + } + } + def waitForHeight(atLeast: Int): Height = { val loggingOptions: LoggingOptions = LoggingOptions() log.debug(s"${loggingOptions.prefix} waitForHeight($atLeast)") @@ -108,7 +124,7 @@ class NodeHttpApi(apiUri: Uri, backend: SttpBackend[Identity, ?], averageBlockDe case Right(r) => r.some } - def getDataByKey(address: Address, key: String)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Option[DataEntry[?]] = { + def dataByKey(address: Address, key: String)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Option[DataEntry[?]] = { log.debug(s"${loggingOptions.prefix} getDataByKey($address, $key)") basicRequest .get(uri"$apiUri/addresses/data/$address/$key") @@ -179,8 +195,24 @@ class NodeHttpApi(apiUri: Uri, backend: SttpBackend[Identity, ?], averageBlockDe case Right(r) => r.peers.length } + def evaluateExpr(address: Address, expr: String): JsObject = { + implicit val loggingOptions: LoggingOptions = LoggingOptions() + log.debug(s"${loggingOptions.prefix} evaluateExpr($address, '$expr')") + basicRequest + .post(uri"$apiUri/utils/script/evaluate/$address") + .body(Json.obj("expr" -> expr)) + .response(asJson[JsObject]) + .tag(LoggingOptionsTag, loggingOptions) + .send(backend) + .body match { + case Left(e) => throw new RuntimeException(e) + case Right(r) => r + } + } + def createWalletAddress(): Unit = { implicit val loggingOptions: LoggingOptions = LoggingOptions() + log.debug(s"${loggingOptions.prefix} createWalletAddress") basicRequest .post(uri"$apiUri/addresses") .header(`X-Api-Key`.name, ApiKeyValue) @@ -191,6 +223,7 @@ class NodeHttpApi(apiUri: Uri, backend: SttpBackend[Identity, ?], averageBlockDe def rollback(to: Height): Unit = { implicit val loggingOptions: LoggingOptions = LoggingOptions() + log.debug(s"${loggingOptions.prefix} rollback($to)") basicRequest .post(uri"$apiUri/debug/rollback") .header(`X-Api-Key`.name, ApiKeyValue) @@ -217,6 +250,11 @@ class NodeHttpApi(apiUri: Uri, backend: SttpBackend[Identity, ?], averageBlockDe object NodeHttpApi { val ApiKeyValue = "testapi" + case class BlockHeaderResponse(VRF: String) + object BlockHeaderResponse { + implicit val blockHeaderResponseFormat: OFormat[BlockHeaderResponse] = Json.format[BlockHeaderResponse] + } + case class HeightResponse(height: Height) object HeightResponse { implicit val heightResponseFormat: OFormat[HeightResponse] = Json.format[HeightResponse] diff --git a/consensus-client-it/src/test/scala/units/AlternativeChainTestSuite.scala b/consensus-client-it/src/test/scala/units/AlternativeChainTestSuite.scala new file mode 100644 index 00000000..61943682 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/AlternativeChainTestSuite.scala @@ -0,0 +1,51 @@ +package units + +import com.wavesplatform.account.KeyPair +import com.wavesplatform.common.state.ByteStr +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.docker.WavesNodeContainer + +class AlternativeChainTestSuite extends OneNodeTestSuite with OneNodeTestSuite.OneMiner { + "L2-383 Start an alternative chain after not getting an EL-block" in { + step("EL miner #2 join") + waves1.api.broadcastAndWait( + chainContract.join( + minerAccount = miner21Account, + elRewardAddress = miner21RewardAddress + ) + ) + + step("Wait miner #2 epoch") + waitMinerEpoch(miner21Account) + + step("Issue miner #2 block confirmation") + val lastContractBlock = waves1.chainContract.getLastBlockMeta(0).getOrElse(fail("Can't get last block")) + val lastWavesBlock = waves1.api.blockHeader(waves1.api.height).getOrElse(fail("Can't get current block header")) + waves1.api.broadcastAndWait( + chainContract.extendMainChain( + minerAccount = miner21Account, + blockHash = BlockHash("0x0000000000000000000000000000000000000000000000000000000000000001"), + parentBlockHash = lastContractBlock.hash, + e2cTransfersRootHashHex = EmptyE2CTransfersRootHashHex, + lastC2ETransferIndex = -1, + vrf = ByteStr.decodeBase58(lastWavesBlock.VRF).get + ) + ) + + step("Wait miner #1 epoch") + waitMinerEpoch(miner11Account) + + step("Checking an alternative chain started") + retry { + waves1.chainContract.getChainInfo(1L).getOrElse(fail("Can't get an alternative chain info")) + } + } + + private def waitMinerEpoch(minerAccount: KeyPair): Unit = { + val expectedGenerator = minerAccount.toAddress + retry { + val actualGenerator = waves1.chainContract.computedGenerator + if (actualGenerator != expectedGenerator) fail(s"Expected $expectedGenerator generator, got $actualGenerator") + }(patienceConfig.copy(timeout = WavesNodeContainer.AverageBlockDelay * 5)) + } +} diff --git a/consensus-client-it/src/test/scala/units/BaseItTestSuite.scala b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala similarity index 93% rename from consensus-client-it/src/test/scala/units/BaseItTestSuite.scala rename to consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala index ce9e673a..a2e6567f 100644 --- a/consensus-client-it/src/test/scala/units/BaseItTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala @@ -9,18 +9,18 @@ import monix.execution.atomic.AtomicBoolean import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should.Matchers import org.scalatest.{BeforeAndAfterAll, EitherValues, OptionValues} -import units.BaseItTestSuite.generateWavesGenesisConfig +import units.BaseDockerTestSuite.generateWavesGenesisConfig import units.client.contract.HasConsensusLayerDappTxHelpers -import units.docker.BaseContainer.{ConfigsDir, DefaultLogsDir} import units.docker.Networks import units.eth.Gwei +import units.test.TestEnvironment.* import units.test.{CustomMatchers, HasRetry} import java.io.PrintStream import java.nio.file.{Files, Path} import scala.concurrent.duration.DurationInt -trait BaseItTestSuite +trait BaseDockerTestSuite extends AnyFreeSpec with ScorexLogging with BeforeAndAfterAll @@ -44,7 +44,7 @@ trait BaseItTestSuite protected def setupNetwork(): Unit override def beforeAll(): Unit = { - BaseItTestSuite.init() + BaseDockerTestSuite.init() super.beforeAll() log.debug(s"Docker network name: ${network.getName}, id: ${network.getId}") // Force create network @@ -59,7 +59,7 @@ trait BaseItTestSuite } } -object BaseItTestSuite { +object BaseDockerTestSuite { private val initialized = AtomicBoolean(false) def init(): Unit = diff --git a/consensus-client-it/src/test/scala/units/OneNodeTestSuite.scala b/consensus-client-it/src/test/scala/units/OneNodeTestSuite.scala index 4f51c690..1a2d8a73 100644 --- a/consensus-client-it/src/test/scala/units/OneNodeTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/OneNodeTestSuite.scala @@ -4,7 +4,7 @@ import com.wavesplatform.common.utils.EitherExt2 import units.client.engine.model.BlockNumber import units.docker.{EcContainer, Networks, WavesNodeContainer} -trait OneNodeTestSuite extends BaseItTestSuite { +trait OneNodeTestSuite extends BaseDockerTestSuite { protected lazy val ec1: EcContainer = new EcContainer( network = network, number = 1, diff --git a/consensus-client-it/src/test/scala/units/ReportingTestName.scala b/consensus-client-it/src/test/scala/units/ReportingTestName.scala index b11246da..bef857c2 100644 --- a/consensus-client-it/src/test/scala/units/ReportingTestName.scala +++ b/consensus-client-it/src/test/scala/units/ReportingTestName.scala @@ -3,7 +3,7 @@ package units import org.scalatest.{Args, Status, SuiteMixin} trait ReportingTestName extends SuiteMixin { - self: BaseItTestSuite => + self: BaseDockerTestSuite => abstract override protected def runTest(testName: String, args: Args): Status = { testStep(s"Test '$testName' started") diff --git a/consensus-client-it/src/test/scala/units/TwoNodesTestSuite.scala b/consensus-client-it/src/test/scala/units/TwoNodesTestSuite.scala index e867ecfc..917629b8 100644 --- a/consensus-client-it/src/test/scala/units/TwoNodesTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/TwoNodesTestSuite.scala @@ -4,7 +4,7 @@ import com.wavesplatform.common.utils.EitherExt2 import units.client.engine.model.BlockNumber import units.docker.{EcContainer, Networks, WavesNodeContainer} -trait TwoNodesTestSuite extends BaseItTestSuite { +trait TwoNodesTestSuite extends BaseDockerTestSuite { protected lazy val ec1: EcContainer = new EcContainer( network = network, number = 1, diff --git a/consensus-client-it/src/test/scala/units/client/HttpChainContractClient.scala b/consensus-client-it/src/test/scala/units/client/HttpChainContractClient.scala index 46aa97ba..bd5e981d 100644 --- a/consensus-client-it/src/test/scala/units/client/HttpChainContractClient.scala +++ b/consensus-client-it/src/test/scala/units/client/HttpChainContractClient.scala @@ -12,10 +12,19 @@ import units.client.contract.{ChainContractClient, ContractBlock} import scala.annotation.tailrec class HttpChainContractClient(api: NodeHttpApi, override val contract: Address) extends ChainContractClient { - override def extractData(key: String): Option[DataEntry[?]] = api.getDataByKey(contract, key) + override def extractData(key: String): Option[DataEntry[?]] = api.dataByKey(contract, key) lazy val token: IssuedAsset = IssuedAsset(ByteStr.decodeBase58(getStringData("tokenId").getOrElse(fail("Call setup first"))).get) + def computedGenerator: Address = { + val rawResult = api.evaluateExpr(contract, "computedGenerator").result + val rawAddress = (rawResult \ "result" \ "value").as[String] + Address.fromString(rawAddress) match { + case Left(e) => fail(s"Can't parse computedGenerator address: $rawAddress. Reason: $e") + case Right(r) => r + } + } + def getEpochFirstBlock(epochNumber: Int): Option[ContractBlock] = getEpochMeta(epochNumber).flatMap { epochData => getEpochFirstBlock(epochData.lastBlockHash) diff --git a/consensus-client-it/src/test/scala/units/docker/BaseContainer.scala b/consensus-client-it/src/test/scala/units/docker/BaseContainer.scala index 6f2ab8f6..484f80e1 100644 --- a/consensus-client-it/src/test/scala/units/docker/BaseContainer.scala +++ b/consensus-client-it/src/test/scala/units/docker/BaseContainer.scala @@ -4,8 +4,6 @@ import com.wavesplatform.utils.LoggerFacade import org.slf4j.LoggerFactory import org.testcontainers.containers.wait.strategy.DockerHealthcheckWaitStrategy -import java.nio.file.{Files, Path} - abstract class BaseContainer(val hostName: String) { protected lazy val log = LoggerFacade(LoggerFactory.getLogger(s"${getClass.getSimpleName}.$hostName")) @@ -25,11 +23,3 @@ abstract class BaseContainer(val hostName: String) { def logPorts(): Unit } - -object BaseContainer { - val ConfigsDir: Path = Path.of(System.getProperty("cc.it.configs.dir")) - val DefaultLogsDir: Path = Path.of(System.getProperty("cc.it.logs.dir")) - Files.createDirectories(DefaultLogsDir) - - val WavesDockerImage: String = System.getProperty("cc.it.docker.image") -} diff --git a/consensus-client-it/src/test/scala/units/docker/DockerImages.scala b/consensus-client-it/src/test/scala/units/docker/DockerImages.scala new file mode 100644 index 00000000..5aaddb62 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/docker/DockerImages.scala @@ -0,0 +1,9 @@ +package units.docker + +import org.testcontainers.utility.DockerImageName.parse +import units.test.TestEnvironment.WavesDockerImage + +object DockerImages { + val WavesNode = parse(WavesDockerImage) + val ExecutionClient = parse("hyperledger/besu:latest") +} diff --git a/consensus-client-it/src/test/scala/units/docker/EcContainer.scala b/consensus-client-it/src/test/scala/units/docker/EcContainer.scala index ac5b69b0..d9c3a7ef 100644 --- a/consensus-client-it/src/test/scala/units/docker/EcContainer.scala +++ b/consensus-client-it/src/test/scala/units/docker/EcContainer.scala @@ -5,17 +5,16 @@ import com.typesafe.config.{ConfigFactory, ConfigValueFactory} import net.ceedubs.ficus.Ficus.toFicusConfig import org.testcontainers.containers.BindMode import org.testcontainers.containers.Network.NetworkImpl -import org.testcontainers.utility.DockerImageName import org.web3j.protocol.Web3j import org.web3j.protocol.http.HttpService import sttp.client3.HttpClientSyncBackend import units.ClientConfig import units.client.engine.{HttpEngineApiClient, LoggedEngineApiClient} -import units.docker.BaseContainer.{ConfigsDir, DefaultLogsDir} import units.docker.EcContainer.{EnginePort, RpcPort, mkConfig} import units.el.ElBridgeClient import units.eth.EthAddress import units.http.OkHttpLogger +import units.test.TestEnvironment.* import java.io.File import java.nio.charset.StandardCharsets @@ -25,7 +24,7 @@ class EcContainer(network: NetworkImpl, number: Int, ip: String) extends BaseCon private val logFile = new File(s"$DefaultLogsDir/besu-$number.log") Files.touch(logFile) - protected override val container = new GenericContainer(DockerImageName.parse("hyperledger/besu:latest")) + protected override val container = new GenericContainer(DockerImages.ExecutionClient) .withNetwork(network) .withExposedPorts(RpcPort, EnginePort) .withEnv(EcContainer.peersEnv, EcContainer.peersVal.mkString(",")) diff --git a/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala b/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala index 350d9100..52baaed5 100644 --- a/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala +++ b/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala @@ -9,11 +9,10 @@ import com.wavesplatform.common.utils.Base58 import com.wavesplatform.crypto import org.testcontainers.containers.BindMode import org.testcontainers.containers.Network.NetworkImpl -import org.testcontainers.utility.DockerImageName import sttp.client3.{HttpClientSyncBackend, UriContext} import units.client.HttpChainContractClient -import units.docker.BaseContainer.* import units.docker.WavesNodeContainer.* +import units.test.TestEnvironment.* import java.io.File import java.nio.charset.StandardCharsets @@ -34,7 +33,7 @@ class WavesNodeContainer( private val logFile = new File(s"$DefaultLogsDir/waves-$number.log") Files.touch(logFile) - protected override val container = new GenericContainer(DockerImageName.parse(WavesDockerImage)) + protected override val container = new GenericContainer(DockerImages.WavesNode) .withNetwork(network) .withExposedPorts(ApiPort) .withEnv( diff --git a/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala b/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala new file mode 100644 index 00000000..44bd8059 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala @@ -0,0 +1,11 @@ +package units.test + +import java.nio.file.{Files, Path} + +object TestEnvironment { + val ConfigsDir: Path = Path.of(System.getProperty("cc.it.configs.dir")) + val DefaultLogsDir: Path = Path.of(System.getProperty("cc.it.logs.dir")) + Files.createDirectories(DefaultLogsDir) + + val WavesDockerImage: String = System.getProperty("cc.it.docker.image") +} diff --git a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala index 7da7d9bd..c410e7e8 100644 --- a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala +++ b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala @@ -80,6 +80,28 @@ trait HasConsensusLayerDappTxHelpers { fee = extendMainChainFee ) + def extendMainChain( + minerAccount: KeyPair, + blockHash: BlockHash, + parentBlockHash: BlockHash, + e2cTransfersRootHashHex: String, + lastC2ETransferIndex: Long, + vrf: ByteStr + ): InvokeScriptTransaction = + TxHelpers.invoke( + invoker = minerAccount, + dApp = chainContractAddress, + func = "extendMainChain".some, + args = List( + Terms.CONST_STRING(blockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(parentBlockHash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(vrf).explicitGet(), + Terms.CONST_STRING(e2cTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(lastC2ETransferIndex) + ), + fee = extendMainChainFee + ) + def appendBlock( minerAccount: KeyPair, block: L2BlockLike,