diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2aa3589726..1d94a7f337 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,3 +134,40 @@ jobs: - name: Runs node tests run: sbt -Denv=test clean ++${{ matrix.scala }} test + test_it: + name: Run it node tests + strategy: + matrix: + os: [ ubuntu-latest ] + scala: [ 2.12.18 ] + java: [ adopt@1.8 ] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v2 + with: + path: it + fetch-depth: 0 + + - name: Setup Java and Scala + uses: olafurpg/setup-scala@v10 + with: + java-version: ${{ matrix.java }} + + - name: Cache sbt + uses: actions/cache@v2 + with: + path: | + ~/.sbt + ~/.ivy2/cache + ~/.coursier/cache/v1 + ~/.cache/coursier/v1 + ~/AppData/Local/Coursier/Cache/v1 + ~/Library/Caches/Coursier/v1 + key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} + + - name: Runs it node tests + run: | + cd it + mkdir tmp + TMPDIR=$(pwd)/tmp sbt -Denv=test clean ++${{ matrix.scala }} it:test \ No newline at end of file diff --git a/README.md b/README.md index 53d2e5a64e..9efcc702f2 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ This repository has modular structure, so only parts which are needed for an app * [ergo-core](ergo-core/README.md) - functionality needed for an SPV client (P2P messages, block section stuctures, PoW, NiPoPoW) * ergo-wallet - Java and Scala functionalities to sign and verify transactions +Using IntelliJ IDEA be sure to set Build Tools / sbt -> `sbt shell` / `use for` / builds, to avoid compilation errors + ## Contributing to Ergo Ergo is an open-source project and we welcome contributions from developers and testers! Join the discussion over [Ergo Discord](https://discord.gg/kj7s7nb) in #development channel, or Telegram: https://t.me/ErgoDevelopers. Please also check out our [Contributing documentation](https://docs.ergoplatform.com/contribute/). diff --git a/build.sbt b/build.sbt index 26497538ba..ecda37a558 100644 --- a/build.sbt +++ b/build.sbt @@ -52,7 +52,7 @@ libraryDependencies ++= Seq( "ch.qos.logback" % "logback-classic" % "1.3.5", // test dependencies - "org.scala-lang.modules" %% "scala-async" % "0.9.7" % "test", + "org.scala-lang.modules" %% "scala-async" % "1.0.1" % "test", "org.scalactic" %% "scalactic" % "3.0.3" % "test", "org.scalatest" %% "scalatest" % "3.2.10" % "test,it", "org.scalacheck" %% "scalacheck" % "1.14.+" % "test", @@ -63,7 +63,9 @@ libraryDependencies ++= Seq( "org.asynchttpclient" % "async-http-client" % "2.6.+" % "test", "com.fasterxml.jackson.dataformat" % "jackson-dataformat-properties" % "2.9.2" % "test", - "com.spotify" % "docker-client" % "8.14.5" % "test" classifier "shaded" + "com.github.docker-java" % "docker-java-core" % "3.3.4" % Test, + "com.github.docker-java" % "docker-java-transport-httpclient5" % "3.3.4" % Test, + ) updateOptions := updateOptions.value.withLatestSnapshots(false) @@ -167,6 +169,7 @@ configs(IntegrationTest extend Test) inConfig(IntegrationTest)(Seq( parallelExecution := false, test := (test dependsOn docker).value, + scalacOptions ++= Seq("-Xasync") )) dockerfile in docker := { @@ -175,7 +178,7 @@ dockerfile in docker := { val configMainNet = (resourceDirectory in IntegrationTest).value / "mainnetTemplate.conf" new Dockerfile { - from("openjdk:9-jre-slim") + from("openjdk:11-jre-slim") label("ergo-integration-tests", "ergo-integration-tests") add(assembly.value, "/opt/ergo/ergo.jar") add(Seq(configDevNet), "/opt/ergo") @@ -280,6 +283,7 @@ configs(It2Test) inConfig(It2Test)(Defaults.testSettings ++ Seq( parallelExecution := false, test := (test dependsOn docker).value, + scalacOptions ++= Seq("-Xasync") )) lazy val ergo = (project in file(".")) diff --git a/project/plugins.sbt b/project/plugins.sbt index d1afea04d8..65d0533f02 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,7 +2,7 @@ logLevel := Level.Warn addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") -addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.4.1") +addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.11.0") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6") diff --git a/src/it/resources/devnetTemplate.conf b/src/it/resources/devnetTemplate.conf index 40bd85b140..0db87620b9 100644 --- a/src/it/resources/devnetTemplate.conf +++ b/src/it/resources/devnetTemplate.conf @@ -53,6 +53,9 @@ scorex { } restApi { bindAddress = "0.0.0.0:9051" - apiKeyHash = null + # Hex-encoded Blake2b256 hash of an API key. Should be 64-chars long Base16 string. + # Below is the hash of "hello" string. + # Change it! + apiKeyHash = "324dcf027dd4a30a932c441f365a25e86b173defa4b8e58948253471b81b72cf" } } \ No newline at end of file diff --git a/src/it/scala/org/ergoplatform/it/DeepRollBackSpec.scala b/src/it/scala/org/ergoplatform/it/DeepRollBackSpec.scala index b1ea045021..f09e709fc6 100644 --- a/src/it/scala/org/ergoplatform/it/DeepRollBackSpec.scala +++ b/src/it/scala/org/ergoplatform/it/DeepRollBackSpec.scala @@ -5,7 +5,6 @@ import com.typesafe.config.Config import org.ergoplatform.it.container.{IntegrationSuite, Node} import org.ergoplatform.nodeView.history.ErgoHistoryUtils import org.scalatest.freespec.AnyFreeSpec - import scala.async.Async import scala.concurrent.{Await, Future} import scala.concurrent.duration._ @@ -13,8 +12,8 @@ import scala.concurrent.duration._ class DeepRollBackSpec extends AnyFreeSpec with IntegrationSuite { val keepVersions = 350 - val chainLength = 150 - val delta = 50 + val chainLength = 50 + val delta = 150 val localVolumeA = s"$localDataDir/node-rollback-spec/nodeA/data" val localVolumeB = s"$localDataDir/node-rollback-spec/nodeB/data" @@ -28,40 +27,64 @@ class DeepRollBackSpec extends AnyFreeSpec with IntegrationSuite { .withFallback(shortInternalMinerPollingInterval) .withFallback(keepVersionsConfig(keepVersions)) .withFallback(nodeSeedConfigs.head) + .withFallback(localOnlyConfig) val minerBConfig: Config = specialDataDirConfig(remoteVolumeB) .withFallback(shortInternalMinerPollingInterval) .withFallback(keepVersionsConfig(keepVersions)) .withFallback(nodeSeedConfigs.last) + .withFallback(localOnlyConfig) val minerAConfigNonGen: Config = minerAConfig .withFallback(nonGeneratingPeerConfig) + .withFallback(localOnlyConfig) val minerBConfigNonGen: Config = minerBConfig .withFallback(nonGeneratingPeerConfig) + .withFallback(localOnlyConfig) - "Deep rollback handling" ignore { + "Deep rollback handling" in { val result: Future[Unit] = Async.async { + // 1. Let nodeA mine and sync nodeB + + val minerAGen: Node = docker.startDevNetNode(minerAConfig, + specialVolumeOpt = Some((localVolumeA, remoteVolumeA))).get + + val minerBGen: Node = docker.startDevNetNode(minerBConfigNonGen, + specialVolumeOpt = Some((localVolumeB, remoteVolumeB))).get + + Async.await(minerAGen.waitForHeight(1)) + Async.await(minerBGen.waitForHeight(1)) + + val genesisAGen = Async.await(minerAGen.headerIdsByHeight(ErgoHistoryUtils.GenesisHeight)).head + val genesisBGen = Async.await(minerBGen.headerIdsByHeight(ErgoHistoryUtils.GenesisHeight)).head + + val minerAGenBestHeight = Async.await(minerAGen.fullHeight) + val minerBGenBestHeight = Async.await(minerBGen.fullHeight) + + log.info("heightA: " + minerAGenBestHeight) + log.info("heightB: " + minerBGenBestHeight) + + genesisAGen shouldBe genesisBGen + + // 2. Stop all nodes + docker.stopNode(minerAGen.containerId) + docker.stopNode(minerBGen.containerId) + val minerAIsolated: Node = docker.startDevNetNode(minerAConfig, isolatedPeersConfig, specialVolumeOpt = Some((localVolumeA, remoteVolumeA))).get - // 1. Let the first node mine `chainLength + delta` blocks + // 1. Let nodeA mine `chainLength + delta` blocks in isolation Async.await(minerAIsolated.waitForHeight(chainLength + delta)) - val genesisA = Async.await(minerAIsolated.headerIdsByHeight(ErgoHistoryUtils.GenesisHeight)).head - val minerBIsolated: Node = docker.startDevNetNode(minerBConfig, isolatedPeersConfig, specialVolumeOpt = Some((localVolumeB, remoteVolumeB))).get - // 2. Let another node mine `chainLength` blocks + // 2. Let nodeB mine `chainLength` blocks in isolation Async.await(minerBIsolated.waitForHeight(chainLength)) - val genesisB = Async.await(minerBIsolated.headerIdsByHeight(ErgoHistoryUtils.GenesisHeight)).head - - genesisA should not equal genesisB - log.info("Mining phase done") val minerABestHeight = Async.await(minerAIsolated.fullHeight) @@ -75,7 +98,7 @@ class DeepRollBackSpec extends AnyFreeSpec with IntegrationSuite { (minerABestHeight > minerBBestHeight) shouldBe true - // 3. Restart node A and node B (having shorter chain) with disabled mining + // 3. Restart nodeA and nodeB (having shorter chain) with disabled mining val minerA: Node = docker.startDevNetNode(minerAConfigNonGen, specialVolumeOpt = Some((localVolumeA, remoteVolumeA))).get @@ -102,7 +125,7 @@ class DeepRollBackSpec extends AnyFreeSpec with IntegrationSuite { minerBBestBlock shouldEqual minerABestBlock } - Await.result(result, 25.minutes) + Await.result(result, 15.minutes) } } diff --git a/src/it/scala/org/ergoplatform/it/ForkResolutionSpec.scala b/src/it/scala/org/ergoplatform/it/ForkResolutionSpec.scala index a66c470f8b..7e74d13d0c 100644 --- a/src/it/scala/org/ergoplatform/it/ForkResolutionSpec.scala +++ b/src/it/scala/org/ergoplatform/it/ForkResolutionSpec.scala @@ -30,10 +30,12 @@ class ForkResolutionSpec extends AnyFlatSpec with Matchers with IntegrationSuite val dirs: Seq[File] = localVolumes.map(vol => new File(vol)) dirs.foreach(_.mkdirs()) - val minerConfig: Config = nodeSeedConfigs.head - val onlineMiningNodesConfig: List[Config] = nodeSeedConfigs.slice(1, nodesQty) + val nodeConfigs: List[Config] = nodeSeedConfigs.take(4).map(_.withFallback(localOnlyConfig)) + + val minerConfig: Config = nodeConfigs.head + val onlineMiningNodesConfig: List[Config] = nodeConfigs.slice(1, nodesQty) .map(_.withFallback(onlineGeneratingPeerConfig)) - val offlineMiningNodesConfig: List[Config] = nodeSeedConfigs.slice(1, nodesQty) + val offlineMiningNodesConfig: List[Config] = nodeConfigs.slice(1, nodesQty) def localVolume(n: Int): String = s"$localDataDir/fork-resolution-spec/node-$n/data" @@ -48,7 +50,7 @@ class ForkResolutionSpec extends AnyFlatSpec with Matchers with IntegrationSuite implicit val patienceConfig: PatienceConfig = PatienceConfig((nodeConfigs.size * 2).seconds, 3.second) blocking(Thread.sleep(nodeConfigs.size * 2000)) eventually { - Await.result(Future.traverse(nodes.get)(_.waitForStartup), nodeConfigs.size.seconds) + Await.result(Future.traverse(nodes.get)(_.waitForStartup), 180.seconds) } } @@ -60,6 +62,9 @@ class ForkResolutionSpec extends AnyFlatSpec with Matchers with IntegrationSuite // 5. Check that nodes reached consensus on created forks; it should "Fork resolution after isolated mining" in { + log.info(minerConfig.toString) + onlineMiningNodesConfig.foreach(x => log.info(x.toString)) + val nodes: List[Node] = startNodesWithBinds(minerConfig +: onlineMiningNodesConfig) val result = Async.async { diff --git a/src/it/scala/org/ergoplatform/it/KnownNodesSpec.scala b/src/it/scala/org/ergoplatform/it/KnownNodesSpec.scala index ad4c5b1a9d..6c166bd099 100644 --- a/src/it/scala/org/ergoplatform/it/KnownNodesSpec.scala +++ b/src/it/scala/org/ergoplatform/it/KnownNodesSpec.scala @@ -9,11 +9,16 @@ import scala.concurrent.duration._ class KnownNodesSpec extends AnyFlatSpec with IntegrationSuite { - val nodeConfigs: List[Config] = nodeSeedConfigs.take(3).map(nonGeneratingPeerConfig.withFallback) + val nodeConfigs: List[Config] = nodeSeedConfigs.take(3) + .map(conf => nonGeneratingPeerConfig + .withFallback(conf) + .withFallback(localOnlyConfig) + ) + val nodes: List[Node] = docker.startDevNetNodes(nodeConfigs, sequentialTopologyConfig).get - // todo: https://github.com/ergoplatform/ergo/issues/653 - it should s"The third node knows first node" ignore { + // All nodes should propagate sequentially, so any node knows each other + it should s"The third node knows first node" in { val node03 = nodes.find(_.nodeName == "node03").value val targetPeersCount = nodes.length - 1 /* self */ diff --git a/src/it/scala/org/ergoplatform/it/LongChainSyncSpec.scala b/src/it/scala/org/ergoplatform/it/LongChainSyncSpec.scala index 4d2686d4f0..184293b884 100644 --- a/src/it/scala/org/ergoplatform/it/LongChainSyncSpec.scala +++ b/src/it/scala/org/ergoplatform/it/LongChainSyncSpec.scala @@ -11,14 +11,19 @@ class LongChainSyncSpec extends AnyFlatSpec with IntegrationSuite { val chainLength = 300 - val minerConfig: Config = shortInternalMinerPollingInterval.withFallback(nodeSeedConfigs.head) - val nonGeneratingConfig: Config = nonGeneratingPeerConfig.withFallback(nodeSeedConfigs(1)) + val minerConfig: Config = shortInternalMinerPollingInterval + .withFallback(nodeSeedConfigs.head) + .withFallback(localOnlyConfig) + + val nonGeneratingConfig: Config = + nonGeneratingPeerConfig.withFallback(nodeSeedConfigs(1)).withFallback(localOnlyConfig) val miner: Node = docker.startDevNetNode(minerConfig).get it should s"Long chain ($chainLength blocks) synchronization" in { - val result: Future[Int] = miner.waitForHeight(chainLength) + val result: Future[Int] = miner + .waitForHeight(chainLength) .flatMap { _ => val follower = docker.startDevNetNode(nonGeneratingConfig).get follower.waitForHeight(chainLength) @@ -27,4 +32,3 @@ class LongChainSyncSpec extends AnyFlatSpec with IntegrationSuite { Await.result(result, 10.minutes) } } - diff --git a/src/it/scala/org/ergoplatform/it/NodeRecoverySpec.scala b/src/it/scala/org/ergoplatform/it/NodeRecoverySpec.scala index 92a28f18bd..624f3d6779 100644 --- a/src/it/scala/org/ergoplatform/it/NodeRecoverySpec.scala +++ b/src/it/scala/org/ergoplatform/it/NodeRecoverySpec.scala @@ -1,7 +1,6 @@ package org.ergoplatform.it import java.io.File - import akka.japi.Option.Some import com.typesafe.config.Config import org.ergoplatform.it.container.{IntegrationSuite, Node} @@ -27,6 +26,7 @@ class NodeRecoverySpec val offlineGeneratingPeer: Config = specialDataDirConfig(remoteVolume) .withFallback(offlineGeneratingPeerConfig) .withFallback(nodeSeedConfigs.head) + .withFallback(localOnlyConfig) val node: Node = docker.startDevNetNode(offlineGeneratingPeer, specialVolumeOpt = Some((localVolume, remoteVolume))).get diff --git a/src/it/scala/org/ergoplatform/it/OpenApiSpec.scala b/src/it/scala/org/ergoplatform/it/OpenApiSpec.scala index 3ea6607633..aee14e9e75 100644 --- a/src/it/scala/org/ergoplatform/it/OpenApiSpec.scala +++ b/src/it/scala/org/ergoplatform/it/OpenApiSpec.scala @@ -1,9 +1,14 @@ package org.ergoplatform.it -import java.io.{PrintWriter, File} +import java.io.{File, PrintWriter} import com.typesafe.config.Config -import org.ergoplatform.it.container.{IntegrationSuite, ApiChecker, ApiCheckerConfig, Node} +import org.ergoplatform.it.container.{ + ApiChecker, + ApiCheckerConfig, + IntegrationSuite, + Node +} import org.scalatest.flatspec.AnyFlatSpec import scala.concurrent.duration._ @@ -12,42 +17,54 @@ import scala.io.Source class OpenApiSpec extends AnyFlatSpec with IntegrationSuite { - val expectedHeight: Int = 2 - val paramsFilePath: String = "/tmp/parameters.yaml" + val expectedHeight: Int = 2 + val paramsFilePath: String = "/tmp/parameters.yaml" val paramsTemplatePath: String = "src/it/resources/parameters-template.txt" - val offlineGeneratingPeer: Config = offlineGeneratingPeerConfig.withFallback(nodeSeedConfigs.head) + val offlineGeneratingPeer: Config = offlineGeneratingPeerConfig + .withFallback(nodeSeedConfigs.head) + .withFallback(localOnlyConfig) val node: Node = docker.startDevNetNode(offlineGeneratingPeer).get - def renderTemplate(template: String, varMapping: Map[String, String]): String = varMapping - .foldLeft(template) { case (s, (k, v)) => s.replaceAll(s"@$k", v) } + def renderTemplate(template: String, varMapping: Map[String, String]): String = + varMapping + .foldLeft(template) { case (s, (k, v)) => s.replaceAll(s"@$k", v) } def createParamsFile(params: Map[String, String]): Unit = { - val template: String = Source.fromFile(paramsTemplatePath).getLines.map(_ + "\n").mkString + val template: String = + Source.fromFile(paramsTemplatePath).getLines.map(_ + "\n").mkString val writer: PrintWriter = new PrintWriter(new File(paramsFilePath)) writer.write(renderTemplate(template, params)) writer.close() } - it should "OpenApi specification check" in { - val result: Future[Unit] = node.waitForHeight(expectedHeight) - .flatMap { _ => node.headerIdsByHeight(expectedHeight) } + it should "OpenApi specification check" ignore { + val result: Future[Unit] = node + .waitForHeight(expectedHeight) + .flatMap { _ => + node.headerIdsByHeight(expectedHeight) + } .map { headerIds => createParamsFile( Map( - "blockHeight" -> expectedHeight.toString, + "blockHeight" -> expectedHeight.toString, "lastHeadersCount" -> expectedHeight.toString, - "headerId" -> headerIds.head) + "headerId" -> headerIds.head + ) ) - val apiAddressToCheck: String = s"${node.nodeInfo.networkIpAddress}:${node.nodeInfo.containerApiPort}" - val specFilePath: String = new File("src/main/resources/api/openapi.yaml").getAbsolutePath - val checker: ApiChecker = docker.startOpenApiChecker( - ApiCheckerConfig(apiAddressToCheck, specFilePath, paramsFilePath) - ).get + val apiAddressToCheck: String = + s"${node.nodeInfo.networkIpAddress}:${node.nodeInfo.containerApiPort}" + val specFilePath: String = + new File("src/main/resources/api/openapi.yaml").getAbsolutePath + val checker: ApiChecker = docker + .startOpenApiChecker( + ApiCheckerConfig(apiAddressToCheck, specFilePath, paramsFilePath) + ) + .get - docker.waitContainer(checker.containerId).statusCode shouldBe 0 + docker.waitContainer(checker.containerId).awaitStatusCode() shouldBe 0 } Await.result(result, 2.minutes) diff --git a/src/it/scala/org/ergoplatform/it/PrunedDigestNodeSync2Spec.scala b/src/it/scala/org/ergoplatform/it/PrunedDigestNodeSync2Spec.scala index e35596c70b..e38f8ca870 100644 --- a/src/it/scala/org/ergoplatform/it/PrunedDigestNodeSync2Spec.scala +++ b/src/it/scala/org/ergoplatform/it/PrunedDigestNodeSync2Spec.scala @@ -1,7 +1,6 @@ package org.ergoplatform.it import java.io.File - import akka.japi.Option.Some import com.typesafe.config.Config import org.ergoplatform.it.container.{IntegrationSuite, Node} @@ -25,13 +24,18 @@ class PrunedDigestNodeSync2Spec extends AnyFlatSpec with IntegrationSuite { val minerConfig: Config = nodeSeedConfigs.head .withFallback(internalMinerPollingIntervalConfig(1000)) .withFallback(specialDataDirConfig(remoteVolume)) + .withFallback(localOnlyConfig) + val nodeForSyncingConfig: Config = minerConfig .withFallback(nonGeneratingPeerConfig) + .withFallback(localOnlyConfig) + val digestConfig: Config = digestStatePeerConfig .withFallback(blockIntervalConfig(600)) .withFallback(prunedHistoryConfig(blocksToKeep)) .withFallback(nonGeneratingPeerConfig) .withFallback(nodeSeedConfigs(1)) + .withFallback(localOnlyConfig) // Testing scenario: // 1. Start up mining node and let it mine chain of length ~ {approxTargetHeight}; diff --git a/src/it/scala/org/ergoplatform/it/PrunedDigestNodeSyncSpec.scala b/src/it/scala/org/ergoplatform/it/PrunedDigestNodeSyncSpec.scala index a5a397a7ce..7861fd4388 100644 --- a/src/it/scala/org/ergoplatform/it/PrunedDigestNodeSyncSpec.scala +++ b/src/it/scala/org/ergoplatform/it/PrunedDigestNodeSyncSpec.scala @@ -1,7 +1,6 @@ package org.ergoplatform.it import java.io.File - import akka.japi.Option.Some import com.typesafe.config.Config import org.asynchttpclient.util.HttpConstants @@ -26,13 +25,18 @@ class PrunedDigestNodeSyncSpec extends AnyFlatSpec with IntegrationSuite { val minerConfig: Config = nodeSeedConfigs.head .withFallback(internalMinerPollingIntervalConfig(10000)) .withFallback(specialDataDirConfig(remoteVolume)) + .withFallback(localOnlyConfig) + val nodeForSyncingConfig: Config = minerConfig .withFallback(nonGeneratingPeerConfig) + .withFallback(localOnlyConfig) + val digestConfig: Config = digestStatePeerConfig .withFallback(blockIntervalConfig(500)) .withFallback(prunedHistoryConfig(blocksToKeep)) .withFallback(nonGeneratingPeerConfig) .withFallback(nodeSeedConfigs(1)) + .withFallback(localOnlyConfig) // Testing scenario: // 1. Start up mining node and let it mine chain of length ~ {approxTargetHeight}; diff --git a/src/it/scala/org/ergoplatform/it/StateRecoveryDigestNodeSpec.scala b/src/it/scala/org/ergoplatform/it/StateRecoveryDigestNodeSpec.scala index afa7692bd4..9b2a796aaa 100644 --- a/src/it/scala/org/ergoplatform/it/StateRecoveryDigestNodeSpec.scala +++ b/src/it/scala/org/ergoplatform/it/StateRecoveryDigestNodeSpec.scala @@ -1,7 +1,6 @@ package org.ergoplatform.it import java.io.File - import akka.japi.Option.Some import com.typesafe.config.Config import org.apache.commons.io.FileUtils @@ -27,11 +26,14 @@ class StateRecoveryDigestNodeSpec extends AnyFlatSpec with IntegrationSuite { val minerConfig: Config = nodeSeedConfigs.head .withFallback(internalMinerPollingIntervalConfig(10000)) .withFallback(specialDataDirConfig(remoteVolume)) + .withFallback(localOnlyConfig) + val followerConfig: Config = digestStatePeerConfig .withFallback(blockIntervalConfig(10000)) .withFallback(nonGeneratingPeerConfig) .withFallback(nodeSeedConfigs(1)) .withFallback(specialDataDirConfig(remoteVolume)) + .withFallback(localOnlyConfig) // Testing scenario: // 1. Start up one node and let it mine {approxMinerTargetHeight} blocks; diff --git a/src/it/scala/org/ergoplatform/it/UtxoStateNodesSyncSpec.scala b/src/it/scala/org/ergoplatform/it/UtxoStateNodesSyncSpec.scala index b47bbf6cf1..2d6f848b99 100644 --- a/src/it/scala/org/ergoplatform/it/UtxoStateNodesSyncSpec.scala +++ b/src/it/scala/org/ergoplatform/it/UtxoStateNodesSyncSpec.scala @@ -11,28 +11,37 @@ class UtxoStateNodesSyncSpec extends AnyFlatSpec with IntegrationSuite { val blocksQty = 5 - val forkDepth: Int = blocksQty + val forkDepth: Int = blocksQty val minerConfig: Config = nodeSeedConfigs.head - val nonGeneratingConfig: Config = nonGeneratingPeerConfig.withFallback(nodeSeedConfigs(1)) - val onlineGeneratingConfigs: List[Config] = nodeSeedConfigs.slice(2, 4).map(onlineGeneratingPeerConfig.withFallback) - val nodeConfigs: List[Config] = minerConfig +: nonGeneratingConfig +: onlineGeneratingConfigs + val nonGeneratingConfig: Config = + nonGeneratingPeerConfig.withFallback(nodeSeedConfigs(1)) + + val onlineGeneratingConfigs: List[Config] = + nodeSeedConfigs.slice(2, 4).map(onlineGeneratingPeerConfig.withFallback) + + val nodeConfigs: List[Config] = + (minerConfig +: nonGeneratingConfig +: onlineGeneratingConfigs) + .map(_.withFallback(localOnlyConfig)) val nodes: List[Node] = docker.startDevNetNodes(nodeConfigs).get it should s"Utxo state nodes synchronisation ($blocksQty blocks)" in { val result = for { initHeight <- Future.traverse(nodes)(_.fullHeight).map(x => math.max(x.max, 1)) - _ <- Future.traverse(nodes)(_.waitForHeight(initHeight + blocksQty)) - headers <- Future.traverse(nodes)(_.headerIdsByHeight(initHeight + blocksQty - forkDepth)) + _ <- Future.traverse(nodes)(_.waitForHeight(initHeight + blocksQty)) + headers <- Future.traverse(nodes)( + _.headerIdsByHeight(initHeight + blocksQty - forkDepth) + ) } yield { - log.info(s"Headers at height ${initHeight + blocksQty - forkDepth}: ${headers.mkString(",")}") + log.info( + s"Headers at height ${initHeight + blocksQty - forkDepth}: ${headers.mkString(",")}" + ) val headerIdsAtSameHeight = headers.flatten - val sample = headerIdsAtSameHeight.head + val sample = headerIdsAtSameHeight.head headerIdsAtSameHeight should contain only sample } Await.result(result, 15.minutes) } } - diff --git a/src/it/scala/org/ergoplatform/it/WalletSpec.scala b/src/it/scala/org/ergoplatform/it/WalletSpec.scala index fd041daaf7..344b165e49 100644 --- a/src/it/scala/org/ergoplatform/it/WalletSpec.scala +++ b/src/it/scala/org/ergoplatform/it/WalletSpec.scala @@ -10,7 +10,11 @@ import org.ergoplatform.it.api.NodeApi.UnexpectedStatusCodeException import org.ergoplatform.it.container.{IntegrationSuite, Node} import org.ergoplatform.it.util.RichEither import org.ergoplatform.modifiers.mempool.UnsignedErgoTransaction -import org.ergoplatform.nodeView.wallet.requests.{PaymentRequest, PaymentRequestEncoder, RequestsHolder, RequestsHolderEncoder} +import org.ergoplatform.nodeView.wallet.requests.{ + PaymentRequest, + RequestsHolder, + RequestsHolderEncoder +} import org.ergoplatform.nodeView.wallet.{AugWalletTransaction, ErgoWalletServiceImpl} import org.ergoplatform.settings.{Args, ErgoSettings, ErgoSettingsReader} import org.ergoplatform.utils.{ErgoTestHelpers, WalletTestOps} @@ -25,26 +29,48 @@ import sigmastate.Values.{ErgoTree, TrueLeaf} import scala.concurrent.ExecutionContext -class WalletSpec extends AsyncWordSpec with IntegrationSuite with WalletTestOps with ApiCodecs { +class WalletSpec + extends AsyncWordSpec + with IntegrationSuite + with WalletTestOps + with ApiCodecs { + import org.ergoplatform.utils.ErgoNodeTestConstants._ + import org.ergoplatform.utils.ErgoCoreTestConstants._ - override implicit def executionContext: ExecutionContext = ErgoTestHelpers.defaultExecutionContext + implicit override def executionContext: ExecutionContext = + ErgoTestHelpers.defaultExecutionContext val ergoSettings: ErgoSettings = ErgoSettingsReader.read( - Args(userConfigPathOpt = Some("src/test/resources/application.conf"), networkTypeOpt = None)) + Args( + userConfigPathOpt = Some("src/test/resources/application.conf"), + networkTypeOpt = None + ) + ) + + private val nodeConfig: Config = nonGeneratingPeerConfig + .withFallback(nodeSeedConfigs.head) + .withFallback(localOnlyConfig) + + private val node: Node = + docker.startDevNetNode(nodeConfig, sequentialTopologyConfig).get - private val nodeConfig: Config = nonGeneratingPeerConfig.withFallback(nodeSeedConfigs.head) - private val node: Node = docker.startDevNetNode(nodeConfig, sequentialTopologyConfig).get - implicit val requestsHolderEncoder: RequestsHolderEncoder = new RequestsHolderEncoder(ergoSettings) + implicit val requestsHolderEncoder: RequestsHolderEncoder = new RequestsHolderEncoder( + ergoSettings + ) "it should be initialized with testMnemonic" in { - node.waitForStartup.flatMap { node: Node => - node.getWihApiKey("/wallet/status") - }.map { response => - val body = parse(response.getResponseBody) - body.flatMap(_.hcursor.downField("isInitialized").as[Boolean]) shouldBe Right(true) - body.flatMap(_.hcursor.downField("isUnlocked").as[Boolean]) shouldBe Right(true) - body.flatMap(_.hcursor.downField("walletHeight").as[Int]) shouldBe Right(0) - } + node.waitForStartup + .flatMap { node: Node => + node.getWihApiKey("/wallet/status") + } + .map { response => + val body = parse(response.getResponseBody) + body.flatMap(_.hcursor.downField("isInitialized").as[Boolean]) shouldBe Right( + true + ) + body.flatMap(_.hcursor.downField("isUnlocked").as[Boolean]) shouldBe Right(true) + body.flatMap(_.hcursor.downField("walletHeight").as[Int]) shouldBe Right(0) + } } "initializing already initialized wallet should fail" in { @@ -61,7 +87,15 @@ class WalletSpec extends AsyncWordSpec with IntegrationSuite with WalletTestOps "restoring initialized wallet should fail" in { node.waitForStartup.flatMap { node: Node => recoverToExceptionIf[UnexpectedStatusCodeException] { - node.postJson("/wallet/restore", Json.obj("pass" -> "foo".asJson, "mnemonic" -> "bar".asJson)) + node.postJson( + "/wallet/restore", + Json.obj( + "pass" -> "foo".asJson, + "usePre1627KeyDerivation" -> false.asJson, + "mnemonic" -> "bar".asJson, + "mnemonicPass" -> "barz".asJson + ) + ) }.map { ex => ex.response.getStatusCode shouldBe 400 ex.response.getResponseBody should include("Wallet is already initialized") @@ -71,32 +105,62 @@ class WalletSpec extends AsyncWordSpec with IntegrationSuite with WalletTestOps "it should generate unsigned transaction" in { import sigmastate.eval._ - val mnemonic = SecretString.create(walletAutoInitConfig.getString("ergo.wallet.testMnemonic")) - val prover = new ErgoWalletServiceImpl(settings).buildProverFromMnemonic(mnemonic, None, parameters) - val pk = prover.hdPubKeys.head.key - val ergoTree = ErgoTree.fromProposition(TrueLeaf) + val mnemonic = + SecretString.create(walletAutoInitConfig.getString("ergo.wallet.testMnemonic")) + val prover = new ErgoWalletServiceImpl(settings) + .buildProverFromMnemonic(mnemonic, None, parameters) + val pk = prover.hdPubKeys.head.key + val ergoTree = ErgoTree.fromProposition(TrueLeaf) val transactionId = ModifierId @@ Base16.encode(Array.fill(32)(5: Byte)) - val input = new ErgoBox(60000000, ergoTree, Colls.emptyColl[(TokenId, Long)], Map.empty, transactionId, 0, 1) + val input = new ErgoBox( + 60000000, + ergoTree, + Colls.emptyColl[(TokenId, Long)], + Map.empty, + transactionId, + 0, + 1 + ) val encodedBox = Base16.encode(ErgoBoxSerializer.toBytes(input)) - val paymentRequest = PaymentRequest(P2PKAddress(pk)(addressEncoder), 50000000, Seq.empty, Map.empty) - val requestsHolder = RequestsHolder(Seq(paymentRequest), feeOpt = Some(100000L), Seq(encodedBox), dataInputsRaw = Seq.empty, minerRewardDelay = 720)(addressEncoder) + val paymentRequest = PaymentRequest( + P2PKAddress(pk)(settings.addressEncoder), + 50000000, + Seq.empty, + Map.empty + ) + val requestsHolder = RequestsHolder( + Seq(paymentRequest), + feeOpt = Some(100000L), + Seq(encodedBox), + dataInputsRaw = Seq.empty, + minerRewardDelay = 720 + )(settings.addressEncoder) - node.waitForStartup.flatMap { node: Node => - for { - _ <- node.postJson("/wallet/payment/send", Json.arr(new PaymentRequestEncoder(settings)(paymentRequest))) - generateTxResp <- node.postJson("/wallet/transaction/generateUnsigned", requestsHolder.asJson) - txs <- node.getWihApiKey("/wallet/transactions") - } yield (txs, generateTxResp) - }.map { case (txs, generateTxResp) => - decode[Seq[AugWalletTransaction]](txs.getResponseBody).left.map(_.getMessage).get shouldBe empty - - val generatedTx = decode[UnsignedErgoTransaction](generateTxResp.getResponseBody).left.map(_.getMessage).get - generatedTx.inputs.size shouldBe 1 - generatedTx.outputs.size shouldBe 3 - generatedTx.outputCandidates.size shouldBe 3 - } + node.waitForStartup + .flatMap { node: Node => + for { + generateTxResp <- node.postJson( + "/wallet/transaction/generateUnsigned", + requestsHolder.asJson + ) + txs <- node.getWihApiKey("/wallet/transactions") + } yield (txs, generateTxResp) + } + .map { + case (txs, generateTxResp) => + decode[Seq[AugWalletTransaction]](txs.getResponseBody).left + .map(_.getMessage) + .get shouldBe empty + + val generatedTx = decode[UnsignedErgoTransaction]( + generateTxResp.getResponseBody + ).left.map(_.getMessage).get + generatedTx.inputs.size shouldBe 1 + generatedTx.outputs.size shouldBe 3 + generatedTx.outputCandidates.size shouldBe 3 + } } } diff --git a/src/it/scala/org/ergoplatform/it/container/Docker.scala b/src/it/scala/org/ergoplatform/it/container/Docker.scala index 5e71e45426..3d9e48caca 100644 --- a/src/it/scala/org/ergoplatform/it/container/Docker.scala +++ b/src/it/scala/org/ergoplatform/it/container/Docker.scala @@ -4,17 +4,19 @@ import java.io.{File, FileOutputStream} import java.net.InetAddress import java.nio.file.{Files, Path, Paths} import java.util.concurrent.atomic.AtomicBoolean -import java.util.{Collections, Properties, UUID, List => JList, Map => JMap} +import java.util.{Properties, UUID} import cats.implicits._ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper -import com.google.common.collect.ImmutableMap +import com.github.dockerjava.api.DockerClient +import com.github.dockerjava.api.command.{CreateContainerCmd, WaitContainerResultCallback} +import com.github.dockerjava.api.exception.NotFoundException +import com.github.dockerjava.api.model.{Bind, ContainerNetwork, ExposedPort, HostConfig, Network, PortBinding, Ports, Volume} +import com.github.dockerjava.api.model.Network.Ipam import com.google.common.primitives.Ints -import com.spotify.docker.client.DockerClient._ -import com.spotify.docker.client.exceptions.ImageNotFoundException -import com.spotify.docker.client.messages.EndpointConfig.EndpointIpamConfig -import com.spotify.docker.client.messages._ -import com.spotify.docker.client.{DefaultDockerClient, DockerClient} +import com.github.dockerjava.core.{DefaultDockerClientConfig, DockerClientImpl} +import com.github.dockerjava.httpclient5.ApacheDockerHttpClient +import com.github.dockerjava.transport.DockerHttpClient import com.typesafe.config.{Config, ConfigFactory, ConfigRenderOptions} import net.ceedubs.ficus.Ficus._ import org.apache.commons.io.FileUtils @@ -30,34 +32,52 @@ import scala.concurrent.{Await, ExecutionContext, Future, blocking} import scala.util.control.NonFatal import scala.util.{Failure, Random, Try} -// scalastyle:off number.of.methods -class Docker(suiteConfig: Config = ConfigFactory.empty, - tag: String = "ergo_integration_test", - localDataVolumeOpt: Option[String] = None) - (implicit ec: ExecutionContext) extends AutoCloseable with ScorexLogging { +class Docker( + suiteConfig: Config = ConfigFactory.empty, + tag: String = "ergo_integration_test", + localDataVolumeOpt: Option[String] = None +)(implicit ec: ExecutionContext) + extends AutoCloseable + with ScorexLogging { import Docker._ - private val http = asyncHttpClient(config() - .setMaxConnections(50) - .setMaxConnectionsPerHost(10) - .setMaxRequestRetry(1) - .setReadTimeout(10000) - .setRequestTimeout(10000)) - - private val client = DefaultDockerClient.fromEnv().build() - private var nodeRepository = Seq.empty[Node] + private val http = asyncHttpClient( + config() + .setMaxConnections(50) + .setMaxConnectionsPerHost(10) + .setMaxRequestRetry(1) + .setReadTimeout(10000) + .setRequestTimeout(10000) + ) + + private val configStandart = + DefaultDockerClientConfig.createDefaultConfigBuilder().build() + + private val httpDockerClient: DockerHttpClient = new ApacheDockerHttpClient.Builder() + .dockerHost(configStandart.getDockerHost) + .sslConfig(configStandart.getSSLConfig) + .maxConnections(100) + .connectionTimeout(java.time.Duration.ofSeconds(30)) + .responseTimeout(java.time.Duration.ofSeconds(45)) + .build() + + private val client: DockerClient = + DockerClientImpl.getInstance(configStandart, httpDockerClient) + private var nodeRepository = Seq.empty[Node] private var apiCheckerOpt: Option[ApiChecker] = None - private val isStopped = new AtomicBoolean(false) + private val isStopped = new AtomicBoolean(false) // This should be called after client is ready but before network created. // This allows resource cleanup for the network if we are running out of them initBeforeStart() private def uuidShort: String = UUID.randomUUID().hashCode().toHexString - private val networkName = Docker.networkNamePrefix + uuidShort - private val networkSeed = Random.nextInt(0x100000) << 4 | 0x0A000000 - private val networkPrefix = s"${InetAddress.getByAddress(Ints.toByteArray(networkSeed)).getHostAddress}/28" + private val networkName = Docker.networkNamePrefix + uuidShort + private val networkSeed = Random.nextInt(0x100000) << 4 | 0x0A000000 + + private val networkPrefix = + s"${InetAddress.getByAddress(Ints.toByteArray(networkSeed)).getHostAddress}/28" private val innerNetwork: Network = createNetwork(3) def nodes: Seq[Node] = nodeRepository @@ -69,17 +89,22 @@ class Docker(suiteConfig: Config = ConfigFactory.empty, } } - private def startNodes(networkType: NetworkType, - nodeConfigs: List[Config], - configEnrich: ExtraConfig) = { + private def startNodes( + networkType: NetworkType, + nodeConfigs: List[Config], + configEnrich: ExtraConfig + ) = { log.trace(s"Starting ${nodeConfigs.size} containers") - nodeConfigs.map(cfg => startNode(networkType, cfg, configEnrich)) + nodeConfigs + .map(cfg => startNode(networkType, cfg, configEnrich)) .sequence .map(waitForStartupBlocking) } - def startDevNetNodes(nodeConfigs: List[Config], - configEnrich: ExtraConfig = noExtraConfig): Try[List[Node]] = + def startDevNetNodes( + nodeConfigs: List[Config], + configEnrich: ExtraConfig = noExtraConfig + ): Try[List[Node]] = startNodes(DevNet, nodeConfigs, configEnrich) def waitForStartupBlocking(nodes: List[Node]): List[Node] = { @@ -91,15 +116,13 @@ class Docker(suiteConfig: Config = ConfigFactory.empty, Future.sequence(nodes map { _.waitForStartup }) } - def waitContainer(id: String): ContainerExit = client.waitContainer(id) + def waitContainer(id: String): WaitContainerResultCallback = client.waitContainerCmd(id).start() def startOpenApiChecker(checkerInfo: ApiCheckerConfig): Try[ApiChecker] = Try { - client.pull(ApiCheckerImageStable) - - val ip: String = ipForNode(999, networkSeed) - val containerId: String = client.createContainer(buildApiCheckerContainerConfig(checkerInfo, ip)).id + val ip: String = ipForNode(999, networkSeed) + val containerId: String = buildApiCheckerContainerCmd(checkerInfo, ip).exec().getId connectToNetwork(containerId, ip) - client.startContainer(containerId) + client.startContainerCmd(containerId).exec() log.info(s"Started ApiChecker: $containerId") @@ -108,39 +131,43 @@ class Docker(suiteConfig: Config = ConfigFactory.empty, checker } - private def startNode(networkType: NetworkType, - nodeSpecificConfig: Config, - extraConfig: ExtraConfig, - specialVolumeOpt: Option[(String, String)] = None) = { - val initialSettings = buildErgoSettings(networkType, nodeSpecificConfig) + private def startNode( + networkType: NetworkType, + nodeSpecificConfig: Config, + extraConfig: ExtraConfig, + specialVolumeOpt: Option[(String, String)] = None + ) = { + val initialSettings = buildErgoSettings(networkType, nodeSpecificConfig) val configuredNodeName = initialSettings.scorexSettings.network.nodeName - val nodeNumber = configuredNodeName.replace("node", "").toInt - val ip = ipForNode(nodeNumber, networkSeed) - val restApiPort = initialSettings.scorexSettings.restApi.bindAddress.getPort - val networkPort = initialSettings.scorexSettings.network.bindAddress.getPort + val nodeNumber = configuredNodeName.replace("node", "").toInt + val ip = ipForNode(nodeNumber, networkSeed) + val restApiPort = initialSettings.scorexSettings.restApi.bindAddress.getPort + val networkPort = initialSettings.scorexSettings.network.bindAddress.getPort - val nodeConfig: Config = enrichNodeConfig(networkType, nodeSpecificConfig, extraConfig, ip, networkPort) + val nodeConfig: Config = + enrichNodeConfig(networkType, nodeSpecificConfig, extraConfig, ip, networkPort) val settings: ErgoSettings = buildErgoSettings(networkType, nodeConfig) - val containerConfig: ContainerConfig = buildPeerContainerConfig(networkType, nodeConfig, settings, - ip, specialVolumeOpt) + val containerBuilder: CreateContainerCmd = + buildPeerContainerCmd(networkType, nodeConfig, settings, ip, specialVolumeOpt) val containerName = networkName + "-" + configuredNodeName + "-" + uuidShort Try { - val containerId = client.createContainer(containerConfig, containerName).id + val containerId = containerBuilder.withName(containerName).exec().getId val attachedNetwork = connectToNetwork(containerId, ip) - client.startContainer(containerId) + client.startContainerCmd(containerId).exec() - val containerInfo = client.inspectContainer(containerId) - val ports = containerInfo.networkSettings().ports() + val containerInfo = client.inspectContainerCmd(containerId).exec() + val ports = containerInfo.getNetworkSettings.getPorts val nodeInfo = NodeInfo( - hostRestApiPort = extractHostPort(ports, restApiPort), - hostNetworkPort = extractHostPort(ports, networkPort), + hostRestApiPort = extractHostPort(ports, restApiPort), + hostNetworkPort = extractHostPort(ports, networkPort), containerNetworkPort = networkPort, - containerApiPort = restApiPort, - apiIpAddress = containerInfo.networkSettings().ipAddress(), - networkIpAddress = attachedNetwork.ipAddress(), - containerId = containerId) + containerApiPort = restApiPort, + apiIpAddress = containerInfo.getNetworkSettings.getIpAddress, + networkIpAddress = attachedNetwork.getIpAddress, + containerId = containerId + ) log.info(s"Started node: $nodeInfo") @@ -148,24 +175,35 @@ class Docker(suiteConfig: Config = ConfigFactory.empty, nodeRepository = nodeRepository :+ node node } recoverWith { - case e: ImageNotFoundException => - Failure(new Exception(s"Error: docker image is missing. Run 'sbt it:test' to generate it.", e)) + case e: NotFoundException => + Failure( + new Exception( + s"Error: docker image is missing. Run 'sbt it:test' to generate it.", + e + ) + ) } } - def startDevNetNode(nodeSpecificConfig: Config, - extraConfig: ExtraConfig = noExtraConfig, - specialVolumeOpt: Option[(String, String)] = None): Try[Node] = + def startDevNetNode( + nodeSpecificConfig: Config, + extraConfig: ExtraConfig = noExtraConfig, + specialVolumeOpt: Option[(String, String)] = None + ): Try[Node] = startNode(DevNet, nodeSpecificConfig, extraConfig, specialVolumeOpt) - def startTestNetNode(nodeSpecificConfig: Config, - extraConfig: ExtraConfig = noExtraConfig, - specialVolumeOpt: Option[(String, String)] = None): Try[Node] = + def startTestNetNode( + nodeSpecificConfig: Config, + extraConfig: ExtraConfig = noExtraConfig, + specialVolumeOpt: Option[(String, String)] = None + ): Try[Node] = startNode(TestNet, nodeSpecificConfig, extraConfig, specialVolumeOpt) - def startMainNetNodeYesImSure(nodeSpecificConfig: Config, - extraConfig: ExtraConfig = noExtraConfig, - specialVolumeOpt: Option[(String, String)] = None): Try[Node] = + def startMainNetNodeYesImSure( + nodeSpecificConfig: Config, + extraConfig: ExtraConfig = noExtraConfig, + specialVolumeOpt: Option[(String, String)] = None + ): Try[Node] = startNode(MainNet, nodeSpecificConfig, extraConfig, specialVolumeOpt) private def buildErgoSettings(networkType: NetworkType, nodeConfig: Config) = { @@ -178,147 +216,169 @@ class Docker(suiteConfig: Config = ConfigFactory.empty, ErgoSettingsReader.fromConfig(actualConfig) } - private def enrichNodeConfig(networkType: NetworkType, - nodeConfig: Config, - extraConfig: ExtraConfig, - ip: String, - port: Int) = { - val publicPeerConfig = nodeConfig//.withFallback(declaredAddressConfig(ip, port)) + private def enrichNodeConfig( + networkType: NetworkType, + nodeConfig: Config, + extraConfig: ExtraConfig, + ip: String, + port: Int + ) = { + val publicPeerConfig = nodeConfig //.withFallback(declaredAddressConfig(ip, port)) val withPeerConfig = nodeRepository.headOption.fold(publicPeerConfig) { node => knownPeersConfig(Seq(node.nodeInfo)).withFallback(publicPeerConfig) } - val enrichedConfig = extraConfig(this, nodeConfig).fold(withPeerConfig)(_.withFallback(withPeerConfig)) - val actualConfig = enrichedConfig.withFallback(suiteConfig) + val enrichedConfig = + extraConfig(this, nodeConfig).fold(withPeerConfig)(_.withFallback(withPeerConfig)) + val actualConfig = enrichedConfig + .withFallback(suiteConfig) .withFallback(defaultConfigTemplate(networkType)) + log.info(actualConfig.toString) actualConfig } - private def buildApiCheckerContainerConfig(checkerInfo: ApiCheckerConfig, ip: String): ContainerConfig = { - val hostConfig: HostConfig = HostConfig.builder() - .appendBinds(s"${checkerInfo.specFilePath}:/app/openapi.yaml", s"${checkerInfo.paramsFilePath}:/app/parameters.yaml") - .build() - - val networkingConfig: ContainerConfig.NetworkingConfig = ContainerConfig.NetworkingConfig - .create(Map(networkName -> endpointConfigFor(ip)).asJava) + private def buildApiCheckerContainerCmd( + checkerInfo: ApiCheckerConfig, + ip: String + ): CreateContainerCmd = { + val hostConfig: HostConfig = new HostConfig() + .withBinds( + new Bind(checkerInfo.specFilePath, new Volume("/opt/ergo/openapi.yaml")), + new Bind(checkerInfo.paramsFilePath, new Volume("/opt/ergo/parameters.yaml")) + ) - ContainerConfig.builder() - .image(ApiCheckerImageStable) - .cmd("openapi.yaml", "--api", s"http://${checkerInfo.apiAddressToCheck}", "--parameters", "parameters.yaml") - .networkingConfig(networkingConfig) - .hostConfig(hostConfig) - .build() + client + .createContainerCmd(ApiCheckerImageStable) + .withCmd( + "openapi.yaml", + "--api", + s"http://${checkerInfo.apiAddressToCheck}", + "--parameters", + "parameters.yaml" + ) + .withHostConfig(hostConfig) + .withHostName(networkName) + .withIpv4Address(ip) } - private def buildPeerContainerConfig(networkType: NetworkType, - nodeConfig: Config, - settings: ErgoSettings, - ip: String, - specialVolumeOpt: Option[(String, String)]): ContainerConfig = { + private def buildPeerContainerCmd( + networkType: NetworkType, + nodeConfig: Config, + settings: ErgoSettings, + ip: String, + specialVolumeOpt: Option[(String, String)] + ) = { val restApiPort = settings.scorexSettings.restApi.bindAddress.getPort val networkPort = settings.scorexSettings.network.bindAddress.getPort - val portBindings = new ImmutableMap.Builder[String, JList[PortBinding]]() - .put(restApiPort.toString, Collections.singletonList(PortBinding.randomPort("0.0.0.0"))) - .put(networkPort.toString, Collections.singletonList(PortBinding.randomPort("0.0.0.0"))) - .build() + val portBindings = List( + new PortBinding(new Ports.Binding("0.0.0.0", null), new ExposedPort(restApiPort)), + new PortBinding(new Ports.Binding("0.0.0.0", null), new ExposedPort(networkPort)) + ) val oneGB: Long = 1024 * 1024 * 1024 val memoryLimit = networkType match { case MainNet => 3 * oneGB - case _ => oneGB + case _ => oneGB } - val hostConfig = specialVolumeOpt - .map { case (lv, rv) => - HostConfig.builder() - .appendBinds(s"$lv:$rv") + val hostConfig: HostConfig = specialVolumeOpt + .map { + case (lv, rv) => + new HostConfig().withBinds( + new Bind(lv, new Volume(rv)) + ) } - .getOrElse(HostConfig.builder()) - .portBindings(portBindings) - .memory(memoryLimit) - .build() - - val networkingConfig = ContainerConfig.NetworkingConfig - .create(Map(networkName -> endpointConfigFor(ip)).asJava) + .getOrElse(new HostConfig()) + .withPortBindings(portBindings.asJava) + .withMemory(memoryLimit) val configCommandLine = renderProperties(asProperties(nodeConfig)) val networkTypeCmdOption = networkType match { case MainNet => "--mainnet" case TestNet => "--testnet" - case DevNet => "" + case DevNet => "" } val miscCmdOptions = networkType match { case MainNet => "-Xmx2G" - case _ => "" + case _ => "" } - val shellCmd = "echo Options: $OPTS; java $OPTS -jar " + + val shellCmd = "echo Options: $OPTS; java $OPTS -Dlibrary.leveldbjni.path=/opt/ergo -jar " + s"$miscCmdOptions /opt/ergo/ergo.jar $networkTypeCmdOption -c /opt/ergo/${networkType.verboseName}Template.conf" - ContainerConfig.builder() - .image(ErgoImageLatest) - .exposedPorts(restApiPort.toString, networkPort.toString) - .networkingConfig(networkingConfig) - .hostConfig(hostConfig) - .env(s"OPTS=$configCommandLine") - .entrypoint("sh", "-c", shellCmd) - .build() + client + .createContainerCmd(ErgoImageLatest) + .withExposedPorts( + List(ExposedPort.tcp(restApiPort), ExposedPort.tcp(networkPort)).asJava + ) + .withHostConfig(hostConfig) + .withHostName(networkName) + .withIpv4Address(ip) + .withEnv(s"OPTS=$configCommandLine") + .withEntrypoint("sh", "-c", shellCmd) } - private def createNetwork(maxRetry: Int): Network = try { - val params = DockerClient.ListNetworksParam.byNetworkName(networkName) - val networkOpt = client.listNetworks(params).asScala.headOption - networkOpt match { - case Some(network) => - log.info(s"Network ${network.name()} (id: ${network.id()}) is created for $tag, " + - s"ipam: ${ipamToString(network)}") - network - case None => - log.debug(s"Creating network $networkName for $tag") - // Specify the network manually because of race conditions: https://github.com/moby/moby/issues/20648 - val r = client.createNetwork(buildNetworkConfig()) - Option(r.warnings()).foreach(log.warn(_)) - createNetwork(maxRetry - 1) + private def createNetwork(maxRetry: Int): Network = + try { + val networkOpt = + client.listNetworksCmd().withNameFilter(networkName).exec().asScala.headOption + networkOpt match { + case Some(network) => + log.info( + s"Network ${network.getName} (id: ${network.getId}) is created for $tag, " + + s"ipam: ${ipamToString(network)}" + ) + network + case None => + log.debug(s"Creating network $networkName for $tag") + val r = client + .createNetworkCmd() + .withName(networkName) + .withAttachable(true) + .withIpam(buildNetworkConfigIpam()) + .withCheckDuplicate(true) + .exec() + Option(r.getWarnings).foreach(_.foreach(log.warn(_))) + createNetwork(maxRetry - 1) //proceed to check if created + } + } catch { + case NonFatal(e) => + log.warn(s"Can not create a network for $tag", e) + if (maxRetry == 0) throw e else createNetwork(maxRetry - 1) } - } catch { - case NonFatal(e) => - log.warn(s"Can not create a network for $tag", e) - if (maxRetry == 0) throw e else createNetwork(maxRetry - 1) - } - private def buildNetworkConfig(): NetworkConfig = { - val config = IpamConfig.create(networkPrefix, networkPrefix, ipForNode(0xE, networkSeed)) - val ipam = Ipam.builder() - .driver("default") - .config(Seq(config).asJava) - .build() - - NetworkConfig.builder() - .name(networkName) - .ipam(ipam) - .checkDuplicate(true) - .build() + private def buildNetworkConfigIpam(): Ipam = { + val config = new Ipam.Config() + .withSubnet(networkPrefix) + .withIpRange(networkPrefix) + .withGateway(ipForNode(0xE, networkSeed)) + new Ipam() + .withDriver("default") + .withConfig(config) } - private def connectToNetwork(containerId: String, ip: String): AttachedNetwork = { - client.connectToNetwork( - innerNetwork.id(), - NetworkConnection - .builder() - .containerId(containerId) - .endpointConfig(endpointConfigFor(ip)) - .build() - ) + private def connectToNetwork(containerId: String, ip: String): ContainerNetwork = { + client + .connectToNetworkCmd() + .withNetworkId(innerNetwork.getId) + .withContainerId(containerId) + .withContainerNetwork(new ContainerNetwork().withIpv4Address(ip)) + .exec() + waitForNetwork(containerId) } - @tailrec private def waitForNetwork(containerId: String, maxTry: Int = 5): AttachedNetwork = { - def errMsg = s"Container $containerId has not connected to the network ${innerNetwork.name()}" - val containerInfo = client.inspectContainer(containerId) - val networks = containerInfo.networkSettings().networks().asScala - if (networks.contains(innerNetwork.name())) { - networks(innerNetwork.name()) + @tailrec private def waitForNetwork( + containerId: String, + maxTry: Int = 5 + ): ContainerNetwork = { + def errMsg = + s"Container $containerId has not connected to the network ${innerNetwork.getName}" + val containerInfo = client.inspectContainerCmd(containerId).exec() + val networks = containerInfo.getNetworkSettings.getNetworks.asScala + if (networks.contains(innerNetwork.getName)) { + networks(innerNetwork.getName) } else if (maxTry > 0) { blocking(Thread.sleep(1000)) log.debug(s"$errMsg, retrying. Max tries = $maxTry") @@ -328,16 +388,17 @@ class Docker(suiteConfig: Config = ConfigFactory.empty, } } - def stopNode(node: Node, secondsToWait: Int): Unit = stopNode(node.containerId, secondsToWait) + def stopNode(node: Node, secondsToWait: Int): Unit = + stopNode(node.containerId, secondsToWait) def stopNode(containerId: String, secondsToWait: Int = 5): Unit = { nodeRepository = nodeRepository.filterNot(_.containerId == containerId) - client.stopContainer(containerId, secondsToWait) + client.stopContainerCmd(containerId).withTimeout(secondsToWait).exec() } def forceStopNode(containerId: String): Unit = { nodeRepository = nodeRepository.filterNot(_.containerId == containerId) - client.removeContainer(containerId, RemoveContainerParam.forceKill()) + client.removeContainerCmd(containerId).withForce(true).exec() } override def close(): Unit = { @@ -345,7 +406,7 @@ class Docker(suiteConfig: Config = ConfigFactory.empty, log.info("Stopping containers") nodeRepository foreach { node => node.close() - client.stopContainer(node.containerId, 0) + client.stopContainerCmd(node.containerId).withTimeout(0).exec() } http.close() @@ -353,18 +414,18 @@ class Docker(suiteConfig: Config = ConfigFactory.empty, apiCheckerOpt.foreach { checker => saveLogs(checker.containerId, "openapi-checker") - client.removeContainer(checker.containerId, RemoveContainerParam.forceKill()) + client.removeContainerCmd(checker.containerId).withForce(true).exec() } nodeRepository foreach { node => - client.removeContainer(node.containerId, RemoveContainerParam.forceKill()) + client.removeContainerCmd(node.containerId).withForce(true).exec() } - client.removeNetwork(innerNetwork.id()) + client.removeNetworkCmd(innerNetwork.getId).exec() client.close() localDataVolumeOpt.foreach { path => val dataVolume = new File(path) - FileUtils.deleteDirectory(dataVolume) + FileUtils.forceDeleteOnExit(dataVolume) } } } @@ -374,18 +435,18 @@ class Docker(suiteConfig: Config = ConfigFactory.empty, Files.createDirectories(logDir) val fileName: String = s"$tag-$containerId" - val logFile: File = logDir.resolve(s"$fileName.log").toFile + val logFile: File = logDir.resolve(s"$fileName.log").toFile log.info(s"Writing logs of $tag-$containerId to ${logFile.getAbsolutePath}") val fileStream: FileOutputStream = new FileOutputStream(logFile, false) - client.logs( - containerId, - DockerClient.LogsParam.timestamps(), - DockerClient.LogsParam.follow(), - DockerClient.LogsParam.stdout(), - DockerClient.LogsParam.stderr() - ) - .attach(fileStream, fileStream) + client + .logContainerCmd(containerId) + .withTimestamps(true) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .start() + .onStart(fileStream) } private def saveNodeLogs(): Unit = { @@ -395,88 +456,99 @@ class Docker(suiteConfig: Config = ConfigFactory.empty, import node.nodeInfo.containerId val fileName = if (tag.isEmpty) containerId else s"$tag-$containerId" - val logFile = logDir.resolve(s"$fileName.log").toFile + val logFile = logDir.resolve(s"$fileName.log").toFile log.info(s"Writing logs of $containerId to ${logFile.getAbsolutePath}") val fileStream = new FileOutputStream(logFile, false) - client.logs( - containerId, - DockerClient.LogsParam.timestamps(), - DockerClient.LogsParam.follow(), - DockerClient.LogsParam.stdout(), - DockerClient.LogsParam.stderr() - ) - .attach(fileStream, fileStream) + client + .logContainerCmd(containerId) + .withTimestamps(true) + .withFollowStream(true) + .withStdOut(true) + .withStdErr(true) + .start() + .onStart(fileStream) + } } - def disconnectFromNetwork(containerId: String): Unit = client.disconnectFromNetwork(containerId, innerNetwork.id()) + def disconnectFromNetwork(containerId: String): Unit = + client + .disconnectFromNetworkCmd() + .withContainerId(containerId) + .withNetworkId(innerNetwork.getId) + .exec() - def disconnectFromNetwork(node: Node): Unit = disconnectFromNetwork(node.nodeInfo.containerId) + def disconnectFromNetwork(node: Node): Unit = + disconnectFromNetwork(node.nodeInfo.containerId) - def connectToNetwork(node: Node): Unit = connectToNetwork(node.nodeInfo.containerId, node.nodeInfo.networkIpAddress) + def connectToNetwork(node: Node): Unit = + connectToNetwork(node.nodeInfo.containerId, node.nodeInfo.networkIpAddress) def cleanupDanglingResources(): Unit = { log.debug("Cleaning up Docker resources") // remove containers - client.listContainers(ListContainersParam.allContainers()).asScala - .filter(_.names.asScala.head.startsWith("/" + networkNamePrefix)) - .foreach(c => client.removeContainer(c.id, RemoveContainerParam.forceKill)) + client + .listContainersCmd() + .exec() + .asScala + .filter(_.getNames.head.startsWith("/" + networkNamePrefix)) + .foreach(c => client.removeContainerCmd(c.getId).withForce(true).exec()) // removes networks - client.listNetworks(ListNetworksParam.customNetworks).asScala - .filter(_.name().startsWith(networkNamePrefix)) - .foreach(n => client.removeNetwork(n.id)) + client + .listNetworksCmd() + .exec() + .asScala + .filter(_.getName.startsWith(networkNamePrefix)) + .foreach(n => client.removeNetworkCmd(n.getId).exec()) //remove images - client.listImages(ListImagesParam.danglingImages()).asScala - .filter(img => Option(img.labels()).exists(_.containsKey(dockerImageLabel))) - .foreach(img => client.removeImage(img.id())) + client + .listImagesCmd() + .withDanglingFilter(true) + .exec() + .asScala + .filter(img => Option(img.getLabels).exists(_.containsKey(dockerImageLabel))) + .foreach(img => client.removeImageCmd(img.getId).exec()) } def cleanupDanglingIfNeeded(): Unit = { - val shouldCleanup = nodesJointConfig.getOrElse[Boolean]("testing.integration.cleanupDocker", false) + val shouldCleanup = + nodesJointConfig.getOrElse[Boolean]("testing.integration.cleanupDocker", false) if (shouldCleanup) { cleanupDanglingResources() } } } -// scalastyle:on number.of.methods object Docker extends IntegrationTestConstants { - val ErgoImageLatest: String = "org.ergoplatform/ergo:latest" - val ApiCheckerImageLatest: String = "andyceo/openapi-checker:latest" - val ApiCheckerImageStable: String = "andyceo/openapi-checker:0.1.0-openapi-core-0.5.0" + val ErgoImageLatest: String = "org.ergoplatform/ergo" + val ApiCheckerImageLatest: String = "andyceo/openapi-checker" + val ApiCheckerImageStable: String = "andyceo/openapi-checker:0.1.0-openapi-core-0.5.0" // not present in docker anymore - val dockerImageLabel = "ergo-integration-tests" + val dockerImageLabel = "ergo-integration-tests" val networkNamePrefix: String = "ergo-itest-" type ExtraConfig = (Docker, Config) => Option[Config] def noExtraConfig: ExtraConfig = (_, _) => None - - private val jsonMapper = new ObjectMapper + private val jsonMapper = new ObjectMapper private val propsMapper = new JavaPropsMapper - def endpointConfigFor(ip: String): EndpointConfig = - EndpointConfig.builder() - .ipAddress(ip) - .ipamConfig(EndpointIpamConfig.builder().ipv4Address(ip).build()) - .build() - def ipForNode(nodeNumber: Int, networkSeed: Int): String = { val addressBytes = Ints.toByteArray(nodeNumber & 0xF | networkSeed) InetAddress.getByAddress(addressBytes).getHostAddress } def ipamToString(network: Network): String = - network - .ipam() - .config().asScala - .map { n => s"subnet=${n.subnet()}, ip range=${n.ipRange()}" } + network.getIpam.getConfig.asScala + .map { n => + s"subnet=${n.getSubnet}, ip range=${n.getIpRange}" + } .mkString(", ") def asProperties(config: Config): Properties = { @@ -487,9 +559,12 @@ object Docker extends IntegrationTestConstants { def renderProperties(props: Properties): String = props.asScala.map { case (k, v) if v.split(" ").length > 1 => s"-D$k=${v.split(" ").mkString("_")}" - case (k, v) => s"-D$k=$v" + case (k, v) => s"-D$k=$v" } mkString " " - def extractHostPort(portBindingMap: JMap[String, JList[PortBinding]], containerPort: Int): Int = - portBindingMap.get(s"$containerPort/tcp").get(0).hostPort().toInt + def extractHostPort(ports: Ports, containerPort: Int): Int = + ports.getBindings + .get(ExposedPort.tcp(containerPort))(0) + .getHostPortSpec + .toInt } diff --git a/src/it/scala/org/ergoplatform/it/container/IntegrationSuite.scala b/src/it/scala/org/ergoplatform/it/container/IntegrationSuite.scala index 7be018c96a..607e472934 100644 --- a/src/it/scala/org/ergoplatform/it/container/IntegrationSuite.scala +++ b/src/it/scala/org/ergoplatform/it/container/IntegrationSuite.scala @@ -2,6 +2,7 @@ package org.ergoplatform.it.container import org.ergoplatform.utils.ErgoTestHelpers import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures} +import org.scalatest.matchers.should.Matchers import org.scalatest.{BeforeAndAfterAll, Suite} import scorex.util.ScorexLogging @@ -9,16 +10,19 @@ import scala.concurrent.ExecutionContext import scala.util.Random trait IntegrationSuite - extends BeforeAndAfterAll - with IntegrationTestConstants - with ErgoTestHelpers - with ScalaFutures - with IntegrationPatience - with ScorexLogging { this: Suite => + extends BeforeAndAfterAll + with IntegrationTestConstants + with ErgoTestHelpers + with ScalaFutures + with IntegrationPatience + with Matchers + with ScorexLogging { this: Suite => implicit def executionContext: ExecutionContext = ErgoTestHelpers.defaultExecutionContext - protected val localDataDir: String = s"/tmp/ergo-${Random.nextInt(Int.MaxValue)}" + val tempDir: String = System.getenv("TMPDIR") + + protected val localDataDir: String = s"$tempDir/ergo-${Random.nextInt(Int.MaxValue)}" protected val docker: Docker = new Docker(tag = getClass.getSimpleName, localDataVolumeOpt = Some(localDataDir)) diff --git a/src/it/scala/org/ergoplatform/it/container/IntegrationTestConstants.scala b/src/it/scala/org/ergoplatform/it/container/IntegrationTestConstants.scala index 5e10e0d09c..b7588b882b 100644 --- a/src/it/scala/org/ergoplatform/it/container/IntegrationTestConstants.scala +++ b/src/it/scala/org/ergoplatform/it/container/IntegrationTestConstants.scala @@ -4,11 +4,10 @@ import com.typesafe.config.{Config, ConfigFactory} import net.ceedubs.ficus.Ficus._ import org.ergoplatform.it.container.Docker.ExtraConfig import org.ergoplatform.settings.NetworkType -import org.ergoplatform.utils.ErgoTestConstants import scala.collection.JavaConverters._ -trait IntegrationTestConstants extends ErgoTestConstants { +trait IntegrationTestConstants { val walletAutoInitConfig: Config = ConfigFactory.parseString( s""" @@ -121,4 +120,9 @@ trait IntegrationTestConstants extends ErgoTestConstants { """.stripMargin ) + val localOnlyConfig: Config = ConfigFactory.parseString( + """ + |scorex.network.localOnly = true + """.stripMargin + ) } diff --git a/src/it/scala/org/ergoplatform/it/container/Node.scala b/src/it/scala/org/ergoplatform/it/container/Node.scala index 45e13aa731..8fb29695ab 100644 --- a/src/it/scala/org/ergoplatform/it/container/Node.scala +++ b/src/it/scala/org/ergoplatform/it/container/Node.scala @@ -24,9 +24,9 @@ class Node(val settings: ErgoSettings, val nodeInfo: NodeInfo, override val clie def containerId: String = nodeInfo.containerId override val chainId: Char = 'I' override val networkNodeName: String = s"it-test-client-to-${nodeInfo.networkIpAddress}" - override val restAddress: String = "localhost" + override val restAddress: String = nodeInfo.apiIpAddress override val networkAddress: String = "localhost" - override val nodeRestPort: Int = nodeInfo.hostRestApiPort + override val nodeRestPort: Int = nodeInfo.containerApiPort override val networkPort: Int = nodeInfo.hostNetworkPort override val blockDelay: FiniteDuration = settings.chainSettings.blockInterval