From e51a32066846d4d0e1237450a74ecb46317d51bf Mon Sep 17 00:00:00 2001 From: Vyatcheslav Suharnikov Date: Fri, 22 Nov 2024 15:44:24 +0400 Subject: [PATCH] Docker tests (#22) --- .github/workflows/check-pr.yml | 26 +- .gitignore | 3 + README.md | 20 ++ build.sbt | 70 +++-- consensus-client-it/build.sbt | 67 ++++ .../java/units/bridge/BridgeContract.java | 286 +++++++++++++++++ .../src/test/resources/logback-test.xml | 56 ++++ .../wavesplatform/api/LoggingBackend.scala | 49 +++ .../com/wavesplatform/api/LoggingUtil.scala | 7 + .../com/wavesplatform/api/NodeHttpApi.scala | 290 ++++++++++++++++++ .../scalatest/enablers/ConstantRetrying.scala | 56 ++++ .../src/test/scala/units/Accounts.scala | 33 ++ .../units/AlternativeChainTestSuite.scala | 60 ++++ .../scala/units/BaseDockerTestSuite.scala | 142 +++++++++ .../test/scala/units/BridgeC2ETestSuite.scala | 79 +++++ .../test/scala/units/BridgeE2CTestSuite.scala | 127 ++++++++ .../test/scala/units/ReportingTestName.scala | 18 ++ .../test/scala/units/RewardTestSuite.scala | 44 +++ .../test/scala/units/SyncingTestSuite.scala | 86 ++++++ .../src/test/scala/units/UnitsConvert.scala | 17 + .../client/HttpChainContractClient.scala | 90 ++++++ .../scala/units/docker/BaseContainer.scala | 25 ++ .../scala/units/docker/BesuContainer.scala | 52 ++++ .../scala/units/docker/DockerImages.scala | 10 + .../test/scala/units/docker/EcContainer.scala | 42 +++ .../scala/units/docker/GenericContainer.scala | 7 + .../scala/units/docker/GethContainer.scala | 69 +++++ .../test/scala/units/docker/Networks.scala | 28 ++ .../units/docker/WavesNodeContainer.scala | 104 +++++++ .../test/scala/units/el/ElBridgeClient.scala | 46 +++ .../test/scala/units/http/OkHttpLogger.scala | 40 +++ .../test/IntegrationTestEventually.scala | 12 + .../scala/units/test/TestEnvironment.scala | 12 + local-network/README.md | 6 +- local-network/_debug/besu/Dockerfile | 10 - local-network/configs/besu/besu.yml | 2 +- local-network/configs/besu/log4j2.xml | 24 +- local-network/configs/besu/run-besu.sh | 4 + local-network/configs/ec-common/genesis.json | 4 +- .../configs/wavesnode/genesis-template.conf | 69 +++-- local-network/configs/wavesnode/units.conf | 23 ++ local-network/configs/wavesnode/waves.conf | 28 +- local-network/consensus_client-image-build.sh | 3 +- local-network/deploy/README.md | 12 + local-network/deploy/deploy.py | 2 +- local-network/deploy/local/network.py | 10 +- local-network/docker-compose.yml | 2 +- project/plugins.sbt | 7 +- src/main/scala/units/Bridge.scala | 3 + src/main/scala/units/ClientConfig.scala | 8 +- .../units/ConsensusClientDependencies.scala | 2 +- src/main/scala/units/ELUpdater.scala | 11 +- .../scala/units/client/JsonRpcClient.scala | 46 ++- .../client/contract/ChainContractClient.scala | 12 +- .../units/client/engine/EngineApiClient.scala | 26 +- .../client/engine/HttpEngineApiClient.scala | 74 +++-- .../client/engine/LoggedEngineApiClient.scala | 65 ++-- .../model/ForkChoiceUpdatedRequest.scala | 6 +- .../engine/model/GetBlockByHashRequest.scala | 4 +- .../model/GetBlockByNumberRequest.scala | 4 +- .../client/engine/model/GetLogsRequest.scala | 4 +- .../engine/model/GetLogsResponseEntry.scala | 5 +- .../engine/model/GetPayloadBodyByHash.scala | 6 +- .../engine/model/GetPayloadRequest.scala | 6 +- .../engine/model/NewPayloadRequest.scala | 4 +- src/main/scala/units/client/package.scala | 6 - src/test/resources/application.conf | 13 +- .../units/BaseIntegrationTestSuite.scala | 8 +- .../units/BlockFullValidationTestSuite.scala | 8 +- .../units/BlockIssuesForgingTestSuite.scala | 30 +- .../scala/units/C2ETransfersTestSuite.scala | 2 +- .../scala/units/E2CTransfersTestSuite.scala | 80 +++-- src/test/scala/units/ExtensionDomain.scala | 2 +- src/test/scala/units/TestSettings.scala | 2 +- .../scala/units/client/TestEcClients.scala | 42 ++- .../HasConsensusLayerDappTxHelpers.scala | 53 +++- 76 files changed, 2473 insertions(+), 338 deletions(-) create mode 100644 README.md create mode 100644 consensus-client-it/build.sbt create mode 100644 consensus-client-it/src/test/java/units/bridge/BridgeContract.java create mode 100644 consensus-client-it/src/test/resources/logback-test.xml create mode 100644 consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingBackend.scala create mode 100644 consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingUtil.scala create mode 100644 consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala create mode 100644 consensus-client-it/src/test/scala/org/scalatest/enablers/ConstantRetrying.scala create mode 100644 consensus-client-it/src/test/scala/units/Accounts.scala create mode 100644 consensus-client-it/src/test/scala/units/AlternativeChainTestSuite.scala create mode 100644 consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala create mode 100644 consensus-client-it/src/test/scala/units/BridgeC2ETestSuite.scala create mode 100644 consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala create mode 100644 consensus-client-it/src/test/scala/units/ReportingTestName.scala create mode 100644 consensus-client-it/src/test/scala/units/RewardTestSuite.scala create mode 100644 consensus-client-it/src/test/scala/units/SyncingTestSuite.scala create mode 100644 consensus-client-it/src/test/scala/units/UnitsConvert.scala create mode 100644 consensus-client-it/src/test/scala/units/client/HttpChainContractClient.scala create mode 100644 consensus-client-it/src/test/scala/units/docker/BaseContainer.scala create mode 100644 consensus-client-it/src/test/scala/units/docker/BesuContainer.scala create mode 100644 consensus-client-it/src/test/scala/units/docker/DockerImages.scala create mode 100644 consensus-client-it/src/test/scala/units/docker/EcContainer.scala create mode 100644 consensus-client-it/src/test/scala/units/docker/GenericContainer.scala create mode 100644 consensus-client-it/src/test/scala/units/docker/GethContainer.scala create mode 100644 consensus-client-it/src/test/scala/units/docker/Networks.scala create mode 100644 consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala create mode 100644 consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala create mode 100644 consensus-client-it/src/test/scala/units/http/OkHttpLogger.scala create mode 100644 consensus-client-it/src/test/scala/units/test/IntegrationTestEventually.scala create mode 100644 consensus-client-it/src/test/scala/units/test/TestEnvironment.scala delete mode 100644 local-network/_debug/besu/Dockerfile create mode 100644 local-network/configs/wavesnode/units.conf delete mode 100644 src/main/scala/units/client/package.scala diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index 3c3b2246..87dd0d39 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -6,6 +6,18 @@ jobs: check-pr: name: Check PR runs-on: ubuntu-latest + env: + JAVA_OPTS: -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + SBT_IT_TEST_THREADS: 2 + services: + docker: + image: docker:latest + options: --privileged # Required for Docker-in-Docker + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ports: + - 2375:2375 steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 @@ -13,5 +25,15 @@ jobs: distribution: 'temurin' java-version: '11' cache: 'sbt' - - name: Check PR - run: sbt --batch "compile;test" + - uses: sbt/setup-sbt@v1 + - name: Run tests + run: | + sbt --batch "test;docker;consensus-client-it/test" + - name: Archive logs + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-logs_${{ env.BRANCH_NAME }}_${{ github.run_id }} + path: consensus-client-it/target/test-logs + if-no-files-found: warn + retention-days: 14 diff --git a/.gitignore b/.gitignore index 80467331..fcbe3df0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .DS_Store target +.bloop .bsp +.metals +metals.sbt .idea docker/data docker/logs diff --git a/README.md b/README.md new file mode 100644 index 00000000..528a10d0 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# consensus-client + +## 👨‍💻 Development + +1. Run unit tests: + ```bash + sbt test + ``` + +2. Run integration tests: + 1. Build the Docker image: + ```bash + sbt docker + ``` + Note: Build the Docker image whenever the consensus client code is updated, including after pulling from the repository. + 2. Run the integration tests: + ```bash + sbt "consensus-client-it/test" + ``` + 3. See logs in `consensus-client-it/target/test-logs`. diff --git a/build.sbt b/build.sbt index e8f12f33..1060985f 100644 --- a/build.sbt +++ b/build.sbt @@ -1,37 +1,43 @@ -Global / onChangedBuildSource := ReloadOnSourceChanges +import com.github.sbt.git.SbtGit.GitKeys.gitCurrentBranch -enablePlugins(UniversalDeployPlugin, GitVersioning) +enablePlugins(UniversalDeployPlugin, GitVersioning, sbtdocker.DockerPlugin) git.useGitDescribe := true git.baseVersion := "1.0.0" git.uncommittedSignifier := Some("DIRTY") -scalaVersion := "2.13.15" -organization := "network.units" -organizationName := "Units Network" -name := "consensus-client" -maintainer := "Units Network Team" -resolvers ++= Resolver.sonatypeOssRepos("releases") ++ Resolver.sonatypeOssRepos("snapshots") ++ Seq(Resolver.mavenLocal) +inScope(Global)( + Seq( + onChangedBuildSource := ReloadOnSourceChanges, + scalaVersion := "2.13.15", + organization := "network.units", + organizationName := "Units Network", + resolvers ++= Resolver.sonatypeOssRepos("releases") ++ Resolver.sonatypeOssRepos("snapshots") ++ Seq(Resolver.mavenLocal), + scalacOptions ++= Seq( + "-Xsource:3", + "-feature", + "-deprecation", + "-unchecked", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-Ywarn-unused:-implicits", + "-Xlint" + ) + ) +) + +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.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" ) -scalacOptions ++= Seq( - "-Xsource:3", - "-feature", - "-deprecation", - "-unchecked", - "-language:higherKinds", - "-language:implicitConversions", - "-language:postfixOps", - "-Ywarn-unused:-implicits", - "-Xlint" -) - Compile / packageDoc / publishArtifact := false def makeJarName( @@ -81,3 +87,23 @@ buildTarballsForDocker := { baseDirectory.value / "docker" / "target" / "consensus-client.tgz" ) } + +inTask(docker)( + Seq( + imageNames := Seq( + ImageName(s"consensus-client:${gitCurrentBranch.value}"), // Integration tests + ImageName("consensus-client:local") // local-network + ), + dockerfile := NativeDockerfile(baseDirectory.value / "docker" / "Dockerfile"), + buildOptions := BuildOptions(cache = true) + ) +) + +docker := docker.dependsOn(LocalRootProject / buildTarballsForDocker).value + +lazy val `consensus-client` = project.in(file(".")) + +lazy val `consensus-client-it` = project + .dependsOn( + `consensus-client` % "compile;test->test" + ) diff --git a/consensus-client-it/build.sbt b/consensus-client-it/build.sbt new file mode 100644 index 00000000..ed32665e --- /dev/null +++ b/consensus-client-it/build.sbt @@ -0,0 +1,67 @@ +import com.github.sbt.git.SbtGit.git.gitCurrentBranch +import sbt.Tests.Group + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +description := "Consensus client integration tests" + +libraryDependencies ++= Seq( + "org.testcontainers" % "testcontainers" % "1.20.3", + "org.web3j" % "core" % "4.9.8" +).map(_ % Test) + +val logsDirectory = taskKey[File]("The directory for logs") // Task to evaluate and recreate the logs directory every time + +Global / concurrentRestrictions := { + val threadNumber = Option(System.getenv("SBT_IT_TEST_THREADS")).fold(1)(_.toInt) + Seq(Tags.limit(Tags.ForkedTestGroup, threadNumber)) +} + +inConfig(Test)( + Seq( + logsDirectory := { + val runId: String = Option(System.getenv("RUN_ID")).getOrElse(DateTimeFormatter.ofPattern("MM-dd--HH_mm_ss").format(LocalDateTime.now)) + val r = target.value / "test-logs" / runId + r.mkdirs() + r + }, + javaOptions ++= Seq( + s"-Dlogback.configurationFile=${(Test / resourceDirectory).value}/logback-test.xml", // Fixes a logback blaming for multiple configs + s"-Dcc.it.configs.dir=${baseDirectory.value.getParent}/local-network/configs", + s"-Dcc.it.docker.image=consensus-client:${gitCurrentBranch.value}" + ), + testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-fFWD", ((Test / logsDirectory).value / "summary.log").toString), + fork := true, + testForkedParallel := true, + testGrouping := { + val javaHomeVal = (test / javaHome).value + val baseLogDirVal = (Test / logsDirectory).value + val envVarsVal = (Test / envVars).value + val javaOptionsVal = (Test / javaOptions).value + + val tests = (Test / definedTests).value + + tests.zipWithIndex.map { case (suite, i) => + val suiteLogDir = baseLogDirVal / suite.name.replaceAll("""(\w)\w*\.""", "$1.") // foo.bar.Baz -> f.b.Baz + Group( + suite.name, + Seq(suite), + Tests.SubProcess( + ForkOptions( + javaHome = javaHomeVal, + outputStrategy = (Test / outputStrategy).value, + bootJars = Vector.empty[java.io.File], + workingDirectory = Option((Test / baseDirectory).value), + runJVMOptions = Vector( + s"-Dcc.it.logs.dir=$suiteLogDir" + ) ++ javaOptionsVal, + connectInput = false, + envVars = envVarsVal + ) + ) + ) + } + } + ) +) diff --git a/consensus-client-it/src/test/java/units/bridge/BridgeContract.java b/consensus-client-it/src/test/java/units/bridge/BridgeContract.java new file mode 100644 index 00000000..f4944b65 --- /dev/null +++ b/consensus-client-it/src/test/java/units/bridge/BridgeContract.java @@ -0,0 +1,286 @@ +package units.bridge; + +import io.reactivex.Flowable; +import org.web3j.abi.EventEncoder; +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.Event; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.generated.Bytes20; +import org.web3j.abi.datatypes.generated.Uint256; +import org.web3j.abi.datatypes.primitive.Int; +import org.web3j.abi.datatypes.primitive.Long; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameter; +import org.web3j.protocol.core.RemoteCall; +import org.web3j.protocol.core.RemoteFunctionCall; +import org.web3j.protocol.core.methods.request.EthFilter; +import org.web3j.protocol.core.methods.response.BaseEventResponse; +import org.web3j.protocol.core.methods.response.Log; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.tx.Contract; +import org.web3j.tx.TransactionManager; +import org.web3j.tx.gas.ContractGasProvider; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + *

Auto generated code. + *

Do not modify! + *

Please use the web3j command line tools, + * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the + * codegen module to update. + * + *

Generated with web3j version 1.6.1. + */ +@SuppressWarnings("rawtypes") +public class BridgeContract extends Contract { + public static final String BINARY = "6080604052348015600e575f80fd5b506108b68061001c5f395ff3fe60806040526004361061006e575f3560e01c806396f396c31161004c57806396f396c3146100e3578063c4a4326d14610105578063e984df0e1461011d578063fccc281314610131575f80fd5b806339dd5d1b146100725780637157405a146100b957806378338413146100ce575b5f80fd5b34801561007d575f80fd5b506100a161008c36600461059e565b5f6020819052908152604090205461ffff1681565b60405161ffff90911681526020015b60405180910390f35b3480156100c4575f80fd5b506100a161040081565b6100e16100dc3660046105b5565b61015c565b005b3480156100ee575f80fd5b506100f761044e565b6040519081526020016100b0565b348015610110575f80fd5b506100f76402540be40081565b348015610128575f80fd5b506100f7610468565b34801561013c575f80fd5b506101445f81565b6040516001600160a01b0390911681526020016100b0565b61016c6402540be40060016105fc565b34101561017834610478565b61019061018b6402540be40060016105fc565b610478565b6040516020016101a1929190610630565b604051602081830303815290604052906101d75760405162461bcd60e51b81526004016101ce9190610688565b60405180910390fd5b506101ef6402540be400677fffffffffffffff6105fc565b3411156101fb34610478565b61021561018b6402540be400677fffffffffffffff6105fc565b6040516020016102269291906106bd565b604051602081830303815290604052906102535760405162461bcd60e51b81526004016101ce9190610688565b50435f8181526020819052604090205461ffff166104009081119061027790610478565b604051602001610287919061070c565b604051602081830303815290604052906102b45760405162461bcd60e51b81526004016101ce9190610688565b505f818152602081905260408120805461ffff16916102d283610786565b91906101000a81548161ffff021916908361ffff160217905550505f6402540be400346102ff91906107a6565b9050346103116402540be400836105fc565b1461031b34610478565b6103296402540be400610478565b60405160200161033a9291906107c5565b604051602081830303815290604052906103675760405162461bcd60e51b81526004016101ce9190610688565b506040515f90819034908281818185825af1925050503d805f81146103a7576040519150601f19603f3d011682016040523d82523d5f602084013e6103ac565b606091505b50509050806103fd5760405162461bcd60e51b815260206004820152601e60248201527f4661696c656420746f2073656e6420746f206275726e2061646472657373000060448201526064016101ce565b604080516bffffffffffffffffffffffff1986168152600784900b60208201527ffeadaf04de8d7c2594453835b9a93b747e20e7a09a7fdb9280579a6dbaf131a8910160405180910390a150505050565b6104656402540be400677fffffffffffffff6105fc565b81565b6104656402540be40060016105fc565b6060815f0361049e5750506040805180820190915260018152600360fc1b602082015290565b815f5b81156104c757806104b181610814565b91506104c09050600a836107a6565b91506104a1565b5f8167ffffffffffffffff8111156104e1576104e161082c565b6040519080825280601f01601f19166020018201604052801561050b576020820181803683370190505b509050815b851561059557610521600182610840565b90505f61052f600a886107a6565b61053a90600a6105fc565b6105449088610840565b61054f906030610853565b90505f8160f81b90508084848151811061056b5761056b61086c565b60200101906001600160f81b03191690815f1a90535061058c600a896107a6565b97505050610510565b50949350505050565b5f602082840312156105ae575f80fd5b5035919050565b5f602082840312156105c5575f80fd5b81356bffffffffffffffffffffffff19811681146105e1575f80fd5b9392505050565b634e487b7160e01b5f52601160045260245ffd5b8082028115828204841417610613576106136105e8565b92915050565b5f81518060208401855e5f93019283525090919050565b6a029b2b73a103b30b63ab2960ad1b81525f61064f600b830185610619565b7f206d7573742062652067726561746572206f7220657175616c20746f20000000815261067f601d820185610619565b95945050505050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b6a029b2b73a103b30b63ab2960ad1b81525f6106dc600b830185610619565b7f206d757374206265206c657373206f7220657175616c20746f20000000000000815261067f601a820185610619565b7f4d6178207472616e7366657273206c696d6974206f662000000000000000000081525f61073d6017830184610619565b7f207265616368656420696e207468697320626c6f636b2e2054727920746f207381527232b732103a3930b739b332b9399030b3b0b4b760691b60208201526033019392505050565b5f61ffff821661ffff810361079d5761079d6105e8565b60010192915050565b5f826107c057634e487b7160e01b5f52601260045260245ffd5b500490565b6a029b2b73a103b30b63ab2960ad1b81525f6107e4600b830185610619565b7f206d7573742062652061206d756c7469706c65206f6620000000000000000000815261067f6017820185610619565b5f60018201610825576108256105e8565b5060010190565b634e487b7160e01b5f52604160045260245ffd5b81810381811115610613576106136105e8565b60ff8181168382160190811115610613576106136105e8565b634e487b7160e01b5f52603260045260245ffdfea2646970667358221220106399f534da089226c14e2f183f8421d059a924c65c97d7e4f3e931c54fe1bb64736f6c634300081a0033"; + + private static String librariesLinkedBinary; + + public static final String FUNC_BURN_ADDRESS = "BURN_ADDRESS"; + + public static final String FUNC_EL_TO_CL_RATIO = "EL_TO_CL_RATIO"; + + public static final String FUNC_MAX_AMOUNT_IN_WEI = "MAX_AMOUNT_IN_WEI"; + + public static final String FUNC_MAX_TRANSFERS_IN_BLOCK = "MAX_TRANSFERS_IN_BLOCK"; + + public static final String FUNC_MIN_AMOUNT_IN_WEI = "MIN_AMOUNT_IN_WEI"; + + public static final String FUNC_SENDNATIVE = "sendNative"; + + public static final String FUNC_TRANSFERSPERBLOCK = "transfersPerBlock"; + + public static final Event SENTNATIVE_EVENT = new Event("SentNative", + Arrays.>asList(new TypeReference() { + }, new TypeReference() { + })); + ; + + @Deprecated + protected BridgeContract(String contractAddress, Web3j web3j, Credentials credentials, + BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + protected BridgeContract(String contractAddress, Web3j web3j, Credentials credentials, + ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, credentials, contractGasProvider); + } + + @Deprecated + protected BridgeContract(String contractAddress, Web3j web3j, + TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + protected BridgeContract(String contractAddress, Web3j web3j, + TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static List getSentNativeEvents( + TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(SENTNATIVE_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (Contract.EventValuesWithLog eventValues : valueList) { + SentNativeEventResponse typedResponse = new SentNativeEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.wavesRecipient = (byte[]) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.amount = (java.lang.Long) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static SentNativeEventResponse getSentNativeEventFromLog(Log log) { + Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(SENTNATIVE_EVENT, log); + SentNativeEventResponse typedResponse = new SentNativeEventResponse(); + typedResponse.log = log; + typedResponse.wavesRecipient = (byte[]) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.amount = (java.lang.Long) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + + public Flowable sentNativeEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getSentNativeEventFromLog(log)); + } + + public Flowable sentNativeEventFlowable( + DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(SENTNATIVE_EVENT)); + return sentNativeEventFlowable(filter); + } + + public RemoteFunctionCall call_BURN_ADDRESS() { + final Function function = new Function(FUNC_BURN_ADDRESS, + Arrays.asList(), + Arrays.>asList(new TypeReference

() { + })); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall send_BURN_ADDRESS() { + final Function function = new Function( + FUNC_BURN_ADDRESS, + Arrays.asList(), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall call_EL_TO_CL_RATIO() { + final Function function = new Function(FUNC_EL_TO_CL_RATIO, + Arrays.asList(), + Arrays.>asList(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteFunctionCall send_EL_TO_CL_RATIO() { + final Function function = new Function( + FUNC_EL_TO_CL_RATIO, + Arrays.asList(), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall call_MAX_AMOUNT_IN_WEI() { + final Function function = new Function(FUNC_MAX_AMOUNT_IN_WEI, + Arrays.asList(), + Arrays.>asList(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteFunctionCall send_MAX_AMOUNT_IN_WEI() { + final Function function = new Function( + FUNC_MAX_AMOUNT_IN_WEI, + Arrays.asList(), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall call_MAX_TRANSFERS_IN_BLOCK() { + final Function function = new Function(FUNC_MAX_TRANSFERS_IN_BLOCK, + Arrays.asList(), + Arrays.>asList(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, Integer.class); + } + + public RemoteFunctionCall send_MAX_TRANSFERS_IN_BLOCK() { + final Function function = new Function( + FUNC_MAX_TRANSFERS_IN_BLOCK, + Arrays.asList(), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall call_MIN_AMOUNT_IN_WEI() { + final Function function = new Function(FUNC_MIN_AMOUNT_IN_WEI, + Arrays.asList(), + Arrays.>asList(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteFunctionCall send_MIN_AMOUNT_IN_WEI() { + final Function function = new Function( + FUNC_MIN_AMOUNT_IN_WEI, + Arrays.asList(), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall send_sendNative(byte[] wavesRecipient, + BigInteger weiValue) { + final Function function = new Function( + FUNC_SENDNATIVE, + Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes20(wavesRecipient)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function, weiValue); + } + + public RemoteFunctionCall call_transfersPerBlock(BigInteger param0) { + final Function function = new Function(FUNC_TRANSFERSPERBLOCK, + Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(param0)), + Arrays.>asList(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, Integer.class); + } + + public RemoteFunctionCall send_transfersPerBlock(BigInteger param0) { + final Function function = new Function( + FUNC_TRANSFERSPERBLOCK, + Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(param0)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + @Deprecated + public static BridgeContract load(String contractAddress, Web3j web3j, Credentials credentials, + BigInteger gasPrice, BigInteger gasLimit) { + return new BridgeContract(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static BridgeContract load(String contractAddress, Web3j web3j, + TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new BridgeContract(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static BridgeContract load(String contractAddress, Web3j web3j, Credentials credentials, + ContractGasProvider contractGasProvider) { + return new BridgeContract(contractAddress, web3j, credentials, contractGasProvider); + } + + public static BridgeContract load(String contractAddress, Web3j web3j, + TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new BridgeContract(contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static RemoteCall deploy(Web3j web3j, Credentials credentials, + ContractGasProvider contractGasProvider) { + return deployRemoteCall(BridgeContract.class, web3j, credentials, contractGasProvider, getDeploymentBinary(), ""); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, Credentials credentials, + BigInteger gasPrice, BigInteger gasLimit) { + return deployRemoteCall(BridgeContract.class, web3j, credentials, gasPrice, gasLimit, getDeploymentBinary(), ""); + } + + public static RemoteCall deploy(Web3j web3j, + TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return deployRemoteCall(BridgeContract.class, web3j, transactionManager, contractGasProvider, getDeploymentBinary(), ""); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, + TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return deployRemoteCall(BridgeContract.class, web3j, transactionManager, gasPrice, gasLimit, getDeploymentBinary(), ""); + } + + private static String getDeploymentBinary() { + if (librariesLinkedBinary != null) { + return librariesLinkedBinary; + } else { + return BINARY; + } + } + + public static class SentNativeEventResponse extends BaseEventResponse { + public byte[] wavesRecipient; + + public java.lang.Long amount; + } +} diff --git a/consensus-client-it/src/test/resources/logback-test.xml b/consensus-client-it/src/test/resources/logback-test.xml new file mode 100644 index 00000000..fc8fb696 --- /dev/null +++ b/consensus-client-it/src/test/resources/logback-test.xml @@ -0,0 +1,56 @@ + + + + + + + + + DEBUG + + + ${pattern} + + + + + + TRACE + + ${cc.it.logs.dir:-target/logs}/test.log + false + + ${pattern} + + + + + + TRACE + + ${cc.it.logs.dir:-target/logs}/test-http.log + false + + ${pattern} + + + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 00000000..0d41bccf --- /dev/null +++ b/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingBackend.scala @@ -0,0 +1,49 @@ +package com.wavesplatform.api + +import com.wavesplatform.api.LoggingBackend.{LoggingOptions, LoggingOptionsTag} +import com.wavesplatform.utils.ScorexLogging +import sttp.capabilities.Effect +import sttp.client3.* + +class LoggingBackend[F[_], P](delegate: SttpBackend[F, P]) extends DelegateSttpBackend[F, P](delegate) with ScorexLogging { + override def send[T, R >: P & Effect[F]](request: Request[T, R]): F[Response[T]] = { + val l = request.tag(LoggingOptionsTag).collect { case l: LoggingOptions => l } + + l.filter(_.logRequest).foreach { l => + var logStr = s"${l.prefix} ${request.method} ${request.uri}" + if (l.logRequestBody) logStr += s": body=${request.body.show}" + log.debug(logStr) + } + + val requestWithRawJson = request.response(asBothOption(request.response, asStringAlways)) + val withErrorLog = responseMonad.handleError(requestWithRawJson.send(delegate)) { x => + l.foreach { l => log.debug(s"${l.prefix} Error: ${x.getMessage}") } + responseMonad.error(x) + } + + responseMonad.flatMap(withErrorLog) { response => + l.foreach { l => + var logStr = s"${l.prefix} HTTP ${response.code}" + if (l.logResponseBody) logStr += s": body=${response.body._2}" + log.debug(logStr) + } + + responseMonad.unit(response.copy(body = response.body._1)) + } + } +} + +object LoggingBackend { + val LoggingOptionsTag = "logging" + + case class LoggingOptions( + logCall: Boolean = true, + logResult: Boolean = true, + logRequest: Boolean = true, + logRequestBody: Boolean = true, + logResponseBody: Boolean = true, + requestId: Int = LoggingUtil.currRequestId + ) { + val prefix = s"[$requestId]" + } +} diff --git a/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingUtil.scala b/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingUtil.scala new file mode 100644 index 00000000..f0f69972 --- /dev/null +++ b/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingUtil.scala @@ -0,0 +1,7 @@ +package com.wavesplatform.api + +import java.util.concurrent.ThreadLocalRandom + +object LoggingUtil { + def currRequestId: Int = ThreadLocalRandom.current().nextInt(10000, 100000) +} 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 new file mode 100644 index 00000000..bbae09a6 --- /dev/null +++ b/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala @@ -0,0 +1,290 @@ +package com.wavesplatform.api + +import cats.syntax.either.* +import cats.syntax.option.* +import com.wavesplatform.account.Address +import com.wavesplatform.api.LoggingBackend.{LoggingOptions, LoggingOptionsTag} +import com.wavesplatform.api.NodeHttpApi.* +import com.wavesplatform.api.http.ApiMarshallers.TransactionJsonWrites +import com.wavesplatform.api.http.TransactionsApiRoute.ApplicationStatus +import com.wavesplatform.api.http.`X-Api-Key` +import com.wavesplatform.common.state.ByteStr +import com.wavesplatform.state.DataEntry.Format +import com.wavesplatform.state.{DataEntry, EmptyDataEntry, Height} +import com.wavesplatform.transaction.Asset.IssuedAsset +import com.wavesplatform.transaction.Transaction +import com.wavesplatform.utils.ScorexLogging +import org.scalatest.matchers.should.Matchers +import play.api.libs.json.* +import sttp.client3.* +import sttp.client3.playJson.* +import sttp.model.{StatusCode, Uri} +import units.docker.WavesNodeContainer.MaxBlockDelay +import units.test.IntegrationTestEventually + +class NodeHttpApi(apiUri: Uri, backend: SttpBackend[Identity, ?], apiKeyValue: String = DefaultApiKeyValue) + extends IntegrationTestEventually + with Matchers + with ScorexLogging { + def blockHeader(atHeight: Int)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Option[BlockHeaderResponse] = { + if (loggingOptions.logRequest) 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)) => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} None") + none + case Left(HttpError(body, statusCode)) => fail(s"Server returned error $body with status ${statusCode.code}") + case Left(DeserializationException(body, error)) => fail(s"Failed to parse response $body: $error") + case Right(r) => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} $r") + r.some + } + } + + def waitForHeight(atLeast: Int)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Height = { + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} waitForHeight($atLeast)") + val subsequentLoggingOptions = loggingOptions.copy(logCall = false, logResult = false, logRequest = false) + val currHeight = height()(subsequentLoggingOptions) + if (currHeight >= atLeast) currHeight + else { + Thread.sleep(patienceConfig.interval.toMillis) + val waitBlocks = (atLeast - currHeight).min(1) + eventually(timeout(MaxBlockDelay * waitBlocks)) { + val h = height()(subsequentLoggingOptions) + h should be >= atLeast + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} $h") + h + } + } + } + + def height()(implicit loggingOptions: LoggingOptions = LoggingOptions()): Height = { + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} height") + basicRequest + .get(uri"$apiUri/blocks/height") + .response(asJson[HeightResponse]) + .tag(LoggingOptionsTag, loggingOptions) + .send(backend) + .body match { + case Left(e) => fail(e) + case Right(r) => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} ${r.height}") + r.height + } + } + + def broadcastAndWait( + txn: Transaction + )(implicit loggingOptions: LoggingOptions = LoggingOptions(logResponseBody = false)): TransactionInfoResponse = { + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} broadcastAndWait") + broadcast(txn)(loggingOptions.copy(logRequest = false)).left.foreach { e => + fail(s"Can't broadcast ${txn.id()}: code=${e.error}, message=${e.message}") + } + waitForSucceeded(txn.id()) + } + + def broadcast[T <: Transaction](txn: T)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Either[ErrorResponse, T] = { + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} broadcast($txn)") + basicRequest + .post(uri"$apiUri/transactions/broadcast") + .body(txn: Transaction) + .response(asJsonEither[ErrorResponse, BroadcastResponse]) + .tag(LoggingOptionsTag, loggingOptions) + .send(backend) + .body match { + case Left(HttpError(e, _)) => e.asLeft + case Left(e) => fail(e) + case _ => txn.asRight + } + } + + def waitForSucceeded(txnId: ByteStr)(implicit loggingOptions: LoggingOptions = LoggingOptions()): TransactionInfoResponse = { + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} waitFor($txnId)") + var attempt = 0 + eventually { + attempt += 1 + val subsequentLoggingOptions = loggingOptions.copy(logCall = false, logResult = false, logRequest = attempt == 1) + transactionInfo(txnId)(subsequentLoggingOptions) match { + case Some(r) if r.applicationStatus == ApplicationStatus.Succeeded => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} $r") + r + + case r => fail(s"Expected ${ApplicationStatus.Succeeded} status, got: ${r.map(_.applicationStatus)}") + } + } + } + + def transactionInfo(txnId: ByteStr)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Option[TransactionInfoResponse] = { + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} transactionInfo($txnId)") + basicRequest + .get(uri"$apiUri/transactions/info/$txnId") + .response(asJson[TransactionInfoResponse]) + .tag(LoggingOptionsTag, loggingOptions) + .send(backend) + .body match { + case Left(HttpError(_, StatusCode.NotFound)) => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} None") + none + case Left(HttpError(body, statusCode)) => fail(s"Server returned error $body with status ${statusCode.code}") + case Left(DeserializationException(body, error)) => fail(s"Failed to parse response $body: $error") + case Right(r) => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} $r") + r.some + } + } + + def dataByKey(address: Address, key: String)(implicit loggingOptions: LoggingOptions = LoggingOptions(logRequest = false)): Option[DataEntry[?]] = { + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} dataByKey($address, $key)") + basicRequest + .get(uri"$apiUri/addresses/data/$address/$key") + .response(asJson[DataEntry[?]]) + .tag(LoggingOptionsTag, loggingOptions) + .send(backend) + .body match { + case Left(HttpError(_, StatusCode.NotFound)) => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} None") + none + case Left(HttpError(body, statusCode)) => fail(s"Server returned error $body with status ${statusCode.code}") + case Left(DeserializationException(body, error)) => fail(s"Failed to parse response $body: $error") + case Right(r) => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} $r") + r match { + case _: EmptyDataEntry => none + case _ => r.some + } + } + } + + def balance(address: Address, asset: IssuedAsset)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Long = { + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} balance($address, $asset)") + basicRequest + .get(uri"$apiUri/assets/balance/$address/$asset") + .response(asJson[AssetBalanceResponse]) + .tag(LoggingOptionsTag, loggingOptions) + .send(backend) + .body match { + case Left(HttpError(_, StatusCode.NotFound)) => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} 0") + 0L + case Left(HttpError(body, statusCode)) => fail(s"Server returned error $body with status ${statusCode.code}") + case Left(DeserializationException(body, error)) => fail(s"Failed to parse response $body: $error") + case Right(r) => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} ${r.balance}") + r.balance + } + } + + def assetQuantity(asset: IssuedAsset)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Long = { + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} assetQuantity($asset)") + basicRequest + .get(uri"$apiUri/assets/details/$asset?full=false") + .response(asJson[AssetDetailsResponse]) + .tag(LoggingOptionsTag, loggingOptions) + .send(backend) + .body match { + case Left(HttpError(body, statusCode)) => fail(s"Server returned error $body with status ${statusCode.code}") + case Left(DeserializationException(body, error)) => fail(s"Failed to parse response $body: $error") + case Right(r) => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} ${r.quantity}") + r.quantity + } + } + + def evaluateExpr(address: Address, expr: String)(implicit loggingOptions: LoggingOptions = LoggingOptions(logRequest = false)): JsObject = { + if (loggingOptions.logCall) 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) => fail(e) + case Right(r) => + if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} ${(r \ "result").getOrElse(r)}") + r + } + } + + def createWalletAddress()(implicit loggingOptions: LoggingOptions = LoggingOptions()): Unit = { + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} createWalletAddress") + basicRequest + .post(uri"$apiUri/addresses") + .header(`X-Api-Key`.name, apiKeyValue) + .response(asString) + .tag(LoggingOptionsTag, loggingOptions) + .send(backend) + } + + def rollback(to: Height)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Unit = { + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} rollback($to)") + basicRequest + .post(uri"$apiUri/debug/rollback") + .header(`X-Api-Key`.name, apiKeyValue) + .body( + Json.obj( + "rollbackTo" -> to, + "returnTransactionsToUtx" -> false + ) + ) + .response(asString) + .tag(LoggingOptionsTag, loggingOptions) + .send(backend) + } + + def print(message: String): Unit = + basicRequest + .post(uri"$apiUri/debug/print") + .header(`X-Api-Key`.name, apiKeyValue) + .body(Json.obj("message" -> message)) + .response(ignore) + .send(backend) +} + +object NodeHttpApi { + val DefaultApiKeyValue = "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] + } + + case class BroadcastResponse(id: String) + object BroadcastResponse { + implicit val broadcastResponseFormat: OFormat[BroadcastResponse] = Json.format[BroadcastResponse] + } + + case class TransactionInfoResponse(height: Height, applicationStatus: String) + object TransactionInfoResponse { + implicit val transactionInfoResponseFormat: OFormat[TransactionInfoResponse] = Json.format[TransactionInfoResponse] + } + + case class AssetBalanceResponse(balance: Long) + object AssetBalanceResponse { + implicit val assetBalanceResponseFormat: OFormat[AssetBalanceResponse] = Json.format[AssetBalanceResponse] + } + + case class AssetDetailsResponse(quantity: Long) + object AssetDetailsResponse { + implicit val assetDetailsResponseFormat: OFormat[AssetDetailsResponse] = Json.format[AssetDetailsResponse] + } + + case class ConnectedPeersResponse(peers: List[JsObject]) + object ConnectedPeersResponse { + implicit val connectedPeersResponseFormat: OFormat[ConnectedPeersResponse] = Json.format[ConnectedPeersResponse] + } + + case class ErrorResponse(error: Int, message: String) + object ErrorResponse { + implicit val errorResponseFormat: OFormat[ErrorResponse] = Json.format[ErrorResponse] + } +} diff --git a/consensus-client-it/src/test/scala/org/scalatest/enablers/ConstantRetrying.scala b/consensus-client-it/src/test/scala/org/scalatest/enablers/ConstantRetrying.scala new file mode 100644 index 00000000..a8235129 --- /dev/null +++ b/consensus-client-it/src/test/scala/org/scalatest/enablers/ConstantRetrying.scala @@ -0,0 +1,56 @@ +package org.scalatest.enablers + +import org.scalactic.source +import org.scalatest.Resources +import org.scalatest.Suite.anExceptionThatShouldCauseAnAbort +import org.scalatest.exceptions.{StackDepthException, TestFailedDueToTimeoutException, TestPendingException} +import org.scalatest.time.{Nanosecond, Span} + +import scala.annotation.tailrec + +// Retrying with a constant intervals. Copy-paste from Retrying.retryingNatureOfT without excess logic +object ConstantRetrying { + def create[T]: Retrying[T] = new Retrying[T] { + override def retry(timeout: Span, interval: Span, pos: source.Position)(fun: => T): T = { + val startNanos = System.nanoTime + def makeAValiantAttempt(): Either[Throwable, T] = { + try { + Right(fun) + } catch { + case tpe: TestPendingException => throw tpe + case e: Throwable if !anExceptionThatShouldCauseAnAbort(e) => Left(e) + } + } + + @tailrec + def tryTryAgain(attempt: Int): T = { + makeAValiantAttempt() match { + case Right(result) => result + case Left(e) => + val duration = System.nanoTime - startNanos + if (duration < timeout.totalNanos) { + Thread.sleep(interval.millisPart, interval.nanosPart) + } else { + val durationSpan = Span(1, Nanosecond).scaledBy(duration.toDouble) // Use scaledBy to get pretty units + throw new TestFailedDueToTimeoutException( + (_: StackDepthException) => + Some( + if (e.getMessage == null) + Resources.didNotEventuallySucceed(attempt.toString, durationSpan.prettyString) + else + Resources.didNotEventuallySucceedBecause(attempt.toString, durationSpan.prettyString, e.getMessage) + ), + Some(e), + Left(pos), + None, + timeout + ) + } + + tryTryAgain(attempt + 1) + } + } + tryTryAgain(1) + } + } +} diff --git a/consensus-client-it/src/test/scala/units/Accounts.scala b/consensus-client-it/src/test/scala/units/Accounts.scala new file mode 100644 index 00000000..227e60d0 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/Accounts.scala @@ -0,0 +1,33 @@ +package units + +import com.google.common.primitives.{Bytes, Ints} +import com.wavesplatform.account.{KeyPair, SeedKeyPair} +import com.wavesplatform.crypto +import org.web3j.crypto.Credentials +import units.eth.EthAddress + +import java.nio.charset.StandardCharsets + +trait Accounts { + val chainContractAccount: KeyPair = mkKeyPair("devnet cc", 0) + + val miner11Account = mkKeyPair("devnet-1", 0) + val miner11RewardAddress = EthAddress.unsafeFrom("0x7dbcf9c6c3583b76669100f9be3caf6d722bc9f9") + + val miner12Account = mkKeyPair("devnet-1", 1) + val miner12RewardAddress = EthAddress.unsafeFrom("0x7dbcf9c6c3583b76669100f9be3caf6d722bc9f0") + + val miner21Account = mkKeyPair("devnet-2", 0) + val miner21RewardAddress = EthAddress.unsafeFrom("0xcf0b9e13fdd593f4ca26d36afcaa44dd3fdccbed") + + val clRichAccount1 = mkKeyPair("devnet rich", 0) + val clRichAccount2 = mkKeyPair("devnet rich", 1) + + val elBridgeAddress = EthAddress.unsafeFrom("0x0000000000000000000000000000000000006a7e") + + val elRichAccount1 = Credentials.create("8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63") + val elRichAccount2 = Credentials.create("ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f") + + protected def mkKeyPair(seed: String, nonce: Int): SeedKeyPair = + SeedKeyPair(crypto.secureHash(Bytes.concat(Ints.toByteArray(nonce), seed.getBytes(StandardCharsets.UTF_8)))) +} 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..835f0124 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/AlternativeChainTestSuite.scala @@ -0,0 +1,60 @@ +package units + +import com.wavesplatform.account.KeyPair +import com.wavesplatform.api.LoggingBackend.LoggingOptions +import com.wavesplatform.api.http.ApiError.ScriptExecutionError +import com.wavesplatform.common.state.ByteStr +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex + +import scala.annotation.tailrec + +class AlternativeChainTestSuite extends BaseDockerTestSuite { + "L2-383 Start an alternative chain after not getting an EL-block" in { + step(s"Wait miner 1 (${miner11Account.toAddress}) forge at least one block") + chainContract.waitForHeight(1L) + + step(s"EL miner 2 (${miner21Account.toAddress}) join") + val heightBeforeJoin = waves1.api.height() + waves1.api.broadcastAndWait( + ChainContract.join( + minerAccount = miner21Account, + elRewardAddress = miner21RewardAddress + ) + ) + + step(s"Wait miner 2 (${miner21Account.toAddress}) epoch and issue a block confirmation") + waves1.api.waitForHeight(heightBeforeJoin + 1) + broadcastElBlockConfirmation(miner21Account) + + step(s"Wait miner 1 (${miner11Account.toAddress}) epoch") + chainContract.waitForMinerEpoch(miner11Account) + + step("Checking an alternative chain started") + chainContract.waitForChainId(1L) + } + + @tailrec private def broadcastElBlockConfirmation(minerAccount: KeyPair, maxAttempts: Int = 5)(implicit + loggingOptions: LoggingOptions = LoggingOptions(logRequest = false) + ): Unit = { + if (maxAttempts == 0) fail("Can't broadcast an EL-block confirmation: all attempts are out") + + chainContract.waitForMinerEpoch(minerAccount) + val lastContractBlock = chainContract.getLastBlockMeta(0).value + val lastWavesBlock = waves1.api.blockHeader(waves1.api.height()).value + val txn = ChainContract.extendMainChain( + minerAccount = minerAccount, + blockHash = BlockHash("0x0000000000000000000000000000000000000000000000000000000000000001"), + parentBlockHash = lastContractBlock.hash, + e2cTransfersRootHashHex = EmptyE2CTransfersRootHashHex, + lastC2ETransferIndex = -1, + vrf = ByteStr.decodeBase58(lastWavesBlock.VRF).get + ) + waves1.api.broadcast(txn) match { + case Left(e) if e.error == ScriptExecutionError.Id => + log.debug(s"Failed to send an EL-block confirmation: $e") + broadcastElBlockConfirmation(minerAccount, maxAttempts - 1) + case Left(e) => fail(s"Can't broadcast an EL-block confirmation: $e") + case _ => waves1.api.waitForSucceeded(txn.id()) + } + } +} diff --git a/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala new file mode 100644 index 00000000..2cce8fb5 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala @@ -0,0 +1,142 @@ +package units + +import com.wavesplatform.account.AddressScheme +import com.wavesplatform.api.LoggingBackend +import com.wavesplatform.common.state.ByteStr +import com.wavesplatform.common.utils.EitherExt2 +import com.wavesplatform.utils.ScorexLogging +import monix.execution.atomic.AtomicBoolean +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should.Matchers +import org.scalatest.{BeforeAndAfterAll, EitherValues, OptionValues} +import sttp.client3.{HttpClientSyncBackend, Identity, SttpBackend} +import units.client.HttpChainContractClient +import units.client.contract.HasConsensusLayerDappTxHelpers +import units.client.engine.model.BlockNumber +import units.docker.* +import units.docker.WavesNodeContainer.generateWavesGenesisConfig +import units.el.ElBridgeClient +import units.eth.Gwei +import units.test.{CustomMatchers, IntegrationTestEventually, TestEnvironment} + +trait BaseDockerTestSuite + extends AnyFreeSpec + with ScorexLogging + with BeforeAndAfterAll + with Matchers + with CustomMatchers + with EitherValues + with OptionValues + with ReportingTestName + with IntegrationTestEventually + with Accounts + with HasConsensusLayerDappTxHelpers { + override val currentHitSource: ByteStr = ByteStr.empty + protected val rewardAmount: Gwei = Gwei.ofRawGwei(2_000_000_000L) + + protected lazy val network = Networks.network + + protected lazy val wavesGenesisConfigPath = generateWavesGenesisConfig() + + private implicit val httpClientBackend: SttpBackend[Identity, Any] = new LoggingBackend(HttpClientSyncBackend()) + + protected lazy val ec1: EcContainer = { + val constructor = TestEnvironment.ExecutionClient match { + case "besu" => new BesuContainer(_, _, _) + case "geth" => new GethContainer(_, _, _) + case x => throw new RuntimeException(s"Unknown execution client: $x. Only 'geth' or 'besu' supported") + } + + constructor(network, 1, Networks.ipForNode(2) /* ipForNode(1) is assigned to Ryuk */ ) + } + + protected lazy val waves1: WavesNodeContainer = new WavesNodeContainer( + network = network, + number = 1, + ip = Networks.ipForNode(3), + baseSeed = "devnet-1", + chainContractAddress = chainContractAddress, + ecEngineApiUrl = ec1.engineApiDockerUrl, + genesisConfigPath = wavesGenesisConfigPath + ) + + protected lazy val chainContract = new HttpChainContractClient(waves1.api, chainContractAddress) + protected lazy val elBridge = new ElBridgeClient(ec1.web3j, elBridgeAddress) + + protected def startNodes(): Unit = { + ec1.start() + ec1.logPorts() + + waves1.start() + waves1.waitReady() + waves1.logPorts() + } + + protected def stopNodes(): Unit = { + waves1.stop() + ec1.stop() + } + + protected def setupChain(): Unit = { + log.info("Set script") + waves1.api.broadcastAndWait(ChainContract.setScript()) + + log.info("Setup chain contract") + val genesisBlock = ec1.engineApi.getBlockByNumber(BlockNumber.Number(0)).explicitGet().getOrElse(fail("No EL genesis block")) + waves1.api.broadcastAndWait( + ChainContract.setup( + genesisBlock = genesisBlock, + elMinerReward = rewardAmount.amount.longValue(), + daoAddress = None, + daoReward = 0, + invoker = chainContractAccount + ) + ) + log.info(s"Token id: ${chainContract.token}") + + log.info("EL miner #1 join") + val joinMiner1Result = waves1.api.broadcastAndWait( + ChainContract.join( + minerAccount = miner11Account, + elRewardAddress = miner11RewardAddress + ) + ) + + val epoch1Number = joinMiner1Result.height + 1 + log.info(s"Wait for #$epoch1Number epoch") + waves1.api.waitForHeight(epoch1Number) + } + + override protected def step(text: String): Unit = { + super.step(text) + waves1.api.print(text) + // ec1.web3j // Hove no idea how to do this + } + + override def beforeAll(): Unit = { + BaseDockerTestSuite.init() + super.beforeAll() + log.debug(s"Docker network name: ${network.getName}, id: ${network.getId}") // Force create network + + startNodes() + setupChain() + } + + override protected def afterAll(): Unit = { + httpClientBackend.close() + + stopNodes() + network.close() + super.afterAll() + } +} + +object BaseDockerTestSuite { + private val initialized = AtomicBoolean(false) + + def init(): Unit = + if (initialized.compareAndSet(expect = false, update = true)) + AddressScheme.current = new AddressScheme { + override val chainId: Byte = 'D'.toByte + } +} diff --git a/consensus-client-it/src/test/scala/units/BridgeC2ETestSuite.scala b/consensus-client-it/src/test/scala/units/BridgeC2ETestSuite.scala new file mode 100644 index 00000000..018bb955 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BridgeC2ETestSuite.scala @@ -0,0 +1,79 @@ +package units + +import com.wavesplatform.transaction.TxHelpers +import units.client.engine.model.BlockNumber +import units.eth.EthAddress + +class BridgeC2ETestSuite extends BaseDockerTestSuite { + private val clSender = clRichAccount1 + private val elReceiver = elRichAccount1 + private val elReceiverAddress = EthAddress.unsafeFrom(elReceiver.getAddress) + private val userAmount = 1 + private val wavesAmount = UnitsConvert.toWavesAmount(userAmount) + private val gweiAmount = UnitsConvert.toGwei(userAmount) + + "L2-380 Checking balances in CL->EL transfers" in { + def clAssetQuantity: Long = waves1.api.assetQuantity(chainContract.token) + def chainContractBalance: Long = waves1.api.balance(chainContractAddress, chainContract.token) + + val clAssetQuantityBefore = clAssetQuantity + val chainContractBalanceBefore = chainContractBalance + + val elCurrHeight = ec1.web3j.ethBlockNumber().send().getBlockNumber.intValueExact() + + waves1.api.broadcastAndWait( + ChainContract.transfer( + sender = clSender, + destElAddress = elReceiverAddress, + asset = chainContract.token, + amount = wavesAmount + ) + ) + + val chainContractBalanceAfter = chainContractBalance + withClue("1. Chain contract balance unchanged: ") { + chainContractBalanceAfter shouldBe chainContractBalanceBefore + } + + val clAssetQuantityAfter = clAssetQuantity + withClue("1. Tokens burned: ") { + clAssetQuantityAfter shouldBe (clAssetQuantityBefore - wavesAmount) + } + + val withdrawal = Iterator + .from(elCurrHeight + 1) + .map { h => + eventually { + ec1.engineApi.getBlockByNumber(BlockNumber.Number(h)).toOption.flatten.value + } + } + .flatMap(_.withdrawals) + .find(_.address == elReceiverAddress) + .head + + withClue("2. Expected amount: ") { + withdrawal.amount shouldBe gweiAmount + } + } + + override def beforeAll(): Unit = { + super.beforeAll() + + step("Prepare: issue tokens on chain contract and transfer to a user") + waves1.api.broadcastAndWait( + TxHelpers.reissue( + asset = chainContract.token, + sender = chainContractAccount, + amount = wavesAmount + ) + ) + waves1.api.broadcastAndWait( + TxHelpers.transfer( + from = chainContractAccount, + to = clSender.toAddress, + amount = wavesAmount, + asset = chainContract.token + ) + ) + } +} diff --git a/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala b/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala new file mode 100644 index 00000000..c0221452 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala @@ -0,0 +1,127 @@ +package units + +import com.wavesplatform.common.utils.EitherExt2 +import com.wavesplatform.utils.EthEncoding +import org.web3j.protocol.core.DefaultBlockParameterName +import org.web3j.protocol.core.methods.response.TransactionReceipt +import org.web3j.protocol.exceptions.TransactionException +import org.web3j.utils.Convert +import units.el.ElBridgeClient + +class BridgeE2CTestSuite extends BaseDockerTestSuite { + private val elSender = elRichAccount1 + private val clRecipient = clRichAccount1 + private val userAmount = 1 + private val wavesAmount = UnitsConvert.toWavesAmount(userAmount) + + private def sendNative(amount: BigInt = UnitsConvert.toWei(userAmount)): TransactionReceipt = + elBridge.sendNative(elSender, clRecipient.toAddress, amount) + + private val tenGwei = BigInt(Convert.toWei("10", Convert.Unit.GWEI).toBigIntegerExact) + + "Negative" - { + def sendNativeInvalid(amount: BigInt): TransactionException = + try { + sendNative(amount) + fail(s"Expected sendNative($amount) to fail") + } catch { + case e: TransactionException => e + } + + "L2-264 Amount should % 10 Gwei" in { + val e = sendNativeInvalid(tenGwei + 1) + val encodedRevertReason = e.getTransactionReceipt.get().getRevertReason + val revertReason = ElBridgeClient.decodeRevertReason(encodedRevertReason) + revertReason shouldBe "Sent value 10000000001 must be a multiple of 10000000000" + } + + "L2-265 Amount should be between 10 and MAX_AMOUNT_IN_WEI Gwei" in { + withClue("1. Less than 10 Gwei: ") { + val e = sendNativeInvalid(1) + val encodedRevertReason = e.getTransactionReceipt.get().getRevertReason + val revertReason = ElBridgeClient.decodeRevertReason(encodedRevertReason) + revertReason shouldBe "Sent value 1 must be greater or equal to 10000000000" + } + + withClue("2. More than MAX_AMOUNT_IN_WEI: ") { + val maxAmountInWei = BigInt(Long.MaxValue) * tenGwei + val biggerAmount = (maxAmountInWei / tenGwei + 1) * tenGwei + val e = sendNativeInvalid(biggerAmount) + val encodedRevertReason = e.getTransactionReceipt.get().getRevertReason + val revertReason = ElBridgeClient.decodeRevertReason(encodedRevertReason) + revertReason shouldBe s"Sent value $biggerAmount must be less or equal to $maxAmountInWei" + } + } + } + + "L2-325 Sent tokens burned" in { + def burnedTokens = ec1.web3j.ethGetBalance(ElBridgeClient.BurnAddress.hex, DefaultBlockParameterName.LATEST).send().getBalance + val burnedTokensBefore = BigInt(burnedTokens) + + val transferAmount = tenGwei + sendNative(transferAmount) + val burnedTokensAfter = BigInt(burnedTokens) + + burnedTokensAfter shouldBe (transferAmount + burnedTokensBefore) + } + + "L2-379 Checking balances in EL->CL transfers" in { + step("Broadcast Bridge.sendNative transaction") + def bridgeBalance = ec1.web3j.ethGetBalance(elBridgeAddress.hex, DefaultBlockParameterName.LATEST).send().getBalance + val bridgeBalanceBefore = bridgeBalance + val sendTxnReceipt = sendNative() + + withClue("1. The balance of Bridge contract wasn't changed: ") { + val bridgeBalanceAfter = bridgeBalance + bridgeBalanceAfter shouldBe bridgeBalanceBefore + } + + val blockHash = BlockHash(sendTxnReceipt.getBlockHash) + step(s"Block with transaction: $blockHash") + + val logsInBlock = ec1.engineApi.getLogs(blockHash, elBridgeAddress, Bridge.ElSentNativeEventTopic).explicitGet() + + val transferEvents = logsInBlock.map { x => + Bridge.ElSentNativeEvent.decodeArgs(x.data).explicitGet() + } + step(s"Transfer events: ${transferEvents.mkString(", ")}") + + val sendTxnLogIndex = logsInBlock.indexWhere(_.transactionHash == sendTxnReceipt.getTransactionHash) + val transferProofs = Bridge.mkTransferProofs(transferEvents, sendTxnLogIndex).reverse + + step(s"Wait block $blockHash on contract") + val blockConfirmationHeight = eventually { + chainContract.getBlock(blockHash).value.height + } + + step(s"Wait block $blockHash ($blockConfirmationHeight) finalization") + eventually { + val currFinalizedHeight = chainContract.getFinalizedBlock.height + step(s"Current finalized height: $currFinalizedHeight") + currFinalizedHeight should be >= blockConfirmationHeight + } + + withClue("3. Tokens received: ") { + step( + s"Broadcast withdraw transaction: transferIndexInBlock=$sendTxnLogIndex, amount=$wavesAmount, " + + s"merkleProof={${transferProofs.map(EthEncoding.toHexString).mkString(",")}}" + ) + + def receiverBalance: Long = waves1.api.balance(clRecipient.toAddress, chainContract.token) + val receiverBalanceBefore = receiverBalance + + waves1.api.broadcastAndWait( + ChainContract.withdraw( + sender = clRecipient, + blockHash = BlockHash(sendTxnReceipt.getBlockHash), + merkleProof = transferProofs, + transferIndexInBlock = sendTxnLogIndex, + amount = wavesAmount + ) + ) + + val balanceAfter = receiverBalance + balanceAfter shouldBe (receiverBalanceBefore + wavesAmount) + } + } +} diff --git a/consensus-client-it/src/test/scala/units/ReportingTestName.scala b/consensus-client-it/src/test/scala/units/ReportingTestName.scala new file mode 100644 index 00000000..bef857c2 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/ReportingTestName.scala @@ -0,0 +1,18 @@ +package units + +import org.scalatest.{Args, Status, SuiteMixin} + +trait ReportingTestName extends SuiteMixin { + self: BaseDockerTestSuite => + + abstract override protected def runTest(testName: String, args: Args): Status = { + testStep(s"Test '$testName' started") + val r = super.runTest(testName, args) + testStep(s"Test '$testName' ${if (r.succeeds()) "SUCCEEDED" else "FAILED"}") + r + } + + private def testStep(text: String): Unit = step(s"---------- $text ----------") + + protected def step(text: String): Unit = log.debug(text) +} diff --git a/consensus-client-it/src/test/scala/units/RewardTestSuite.scala b/consensus-client-it/src/test/scala/units/RewardTestSuite.scala new file mode 100644 index 00000000..6a857298 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/RewardTestSuite.scala @@ -0,0 +1,44 @@ +package units + +import units.client.engine.model.BlockNumber + +class RewardTestSuite extends BaseDockerTestSuite { + "L2-234 The reward for a previous epoch is in the first block withdrawals" in { + val epoch1FirstEcBlock = eventually { + ec1.engineApi.getBlockByNumber(BlockNumber.Number(1)).value.value + } + + withClue("No reward for genesis block: ") { + epoch1FirstEcBlock.withdrawals shouldBe empty + } + + val epoch1FirstContractBlock = eventually { + chainContract.getBlock(epoch1FirstEcBlock.hash).value + } + + val epoch1Number = epoch1FirstContractBlock.epoch + val epoch2Number = epoch1Number + 1 + + waves1.api.waitForHeight(epoch2Number) + + step(s"Wait for epoch #$epoch2Number data on chain contract") + val epoch2FirstContractBlock = eventually { + chainContract.getEpochFirstBlock(epoch2Number).value + } + + val epoch2FirstEcBlock = ec1.engineApi + .getBlockByHash(epoch2FirstContractBlock.hash) + .value + .value + + epoch2FirstEcBlock.withdrawals should have length 1 + + withClue("Expected reward amount: ") { + epoch2FirstEcBlock.withdrawals(0).amount shouldBe rewardAmount + } + + withClue("Expected reward receiver: ") { + epoch2FirstEcBlock.withdrawals(0).address shouldBe miner11RewardAddress + } + } +} diff --git a/consensus-client-it/src/test/scala/units/SyncingTestSuite.scala b/consensus-client-it/src/test/scala/units/SyncingTestSuite.scala new file mode 100644 index 00000000..311130b3 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/SyncingTestSuite.scala @@ -0,0 +1,86 @@ +package units + +import com.wavesplatform.utils.EthEncoding +import org.web3j.crypto.{RawTransaction, TransactionEncoder} +import org.web3j.protocol.core.DefaultBlockParameter +import org.web3j.protocol.core.methods.response.{EthSendTransaction, TransactionReceipt} +import org.web3j.tx.gas.DefaultGasProvider +import org.web3j.utils.Convert +import units.docker.EcContainer + +import java.math.BigInteger +import scala.jdk.OptionConverters.RichOptional + +class SyncingTestSuite extends BaseDockerTestSuite { + private val elSender = elRichAccount1 + private val amount = Convert.toWei("1", Convert.Unit.ETHER).toBigInteger + + // TODO: Will be removed after fixing geth issues + "L2-381 EL transactions appear after rollback" ignore { + step("Send transaction 1") + val txn1Result = sendTxn(0) + waitForTxn(txn1Result) + + val height1 = waves1.api.height() + + step("Wait for next epoch") + waves1.api.waitForHeight(height1 + 1) + + step("Send transactions 2 and 3") + val txn2Result = sendTxn(1) + val txn3Result = sendTxn(2) + + val txn2Receipt = waitForTxn(txn2Result) + val txn3Receipt = waitForTxn(txn3Result) + + val blocksWithTxns = List(txn2Receipt, txn3Receipt).map(x => x.getBlockNumber -> x.getBlockHash).toMap + step(s"Waiting blocks ${blocksWithTxns.mkString(", ")} on contract") + blocksWithTxns.foreach { case (_, blockHash) => + eventually { + chainContract.getBlock(BlockHash(blockHash)).value + } + } + + step("Rollback CL") + waves1.api.rollback(height1) + + step("Wait for EL mining") + waves1.api.waitForHeight(height1 + 2) + + step(s"Waiting blocks ${blocksWithTxns.mkString(", ")} disappear") + blocksWithTxns.foreach { case (_, blockHash) => + eventually { + chainContract.getBlock(BlockHash(blockHash)) shouldBe empty + } + } + + blocksWithTxns.foreach { case (height, blockHash) => + eventually { + val block = ec1.web3j.ethGetBlockByNumber(DefaultBlockParameter.valueOf(height), false).send().getBlock + block.getHash shouldNot be(blockHash) + } + } + + step("Waiting transactions 2 and 3 on EL") + waitForTxn(txn2Result) + waitForTxn(txn3Result) + } + + private def sendTxn(nonce: Long): EthSendTransaction = { + val rawTransaction = RawTransaction.createEtherTransaction( + EcContainer.ChainId, + BigInteger.valueOf(nonce), + DefaultGasProvider.GAS_LIMIT, + "0x0000000000000000000000000000000000000000", + amount, + BigInteger.ZERO, + DefaultGasProvider.GAS_PRICE + ) + val signedTransaction = EthEncoding.toHexString(TransactionEncoder.signMessage(rawTransaction, elSender)) + ec1.web3j.ethSendRawTransaction(signedTransaction).send() + } + + private def waitForTxn(txnResult: EthSendTransaction): TransactionReceipt = eventually { + ec1.web3j.ethGetTransactionReceipt(txnResult.getTransactionHash).send().getTransactionReceipt.toScala.value + } +} diff --git a/consensus-client-it/src/test/scala/units/UnitsConvert.scala b/consensus-client-it/src/test/scala/units/UnitsConvert.scala new file mode 100644 index 00000000..367a41ce --- /dev/null +++ b/consensus-client-it/src/test/scala/units/UnitsConvert.scala @@ -0,0 +1,17 @@ +package units + +import com.wavesplatform.settings.Constants +import org.web3j.utils.Convert +import units.eth.Gwei + +import java.math.BigInteger + +object UnitsConvert { + def toWavesAmount(userAmount: BigDecimal): Long = (userAmount * Constants.UnitsInWave).toLongExact + def toWei(userAmount: BigDecimal): BigInteger = Convert.toWei(userAmount.bigDecimal, Convert.Unit.ETHER).toBigIntegerExact + def toGwei(userAmount: BigDecimal): Gwei = { + val rawWei = Convert.toWei(userAmount.bigDecimal, Convert.Unit.ETHER) + val rawGwei = Convert.fromWei(rawWei, Convert.Unit.GWEI).longValue() + Gwei.ofRawGwei(rawGwei) + } +} diff --git a/consensus-client-it/src/test/scala/units/client/HttpChainContractClient.scala b/consensus-client-it/src/test/scala/units/client/HttpChainContractClient.scala new file mode 100644 index 00000000..8bea9d06 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/client/HttpChainContractClient.scala @@ -0,0 +1,90 @@ +package units.client + +import cats.syntax.option.* +import com.wavesplatform.account.{Address, KeyPair} +import com.wavesplatform.api.LoggingBackend.LoggingOptions +import com.wavesplatform.api.NodeHttpApi +import com.wavesplatform.common.state.ByteStr +import com.wavesplatform.state.DataEntry +import com.wavesplatform.transaction.Asset.IssuedAsset +import com.wavesplatform.utils.ScorexLogging +import org.scalatest.OptionValues +import org.scalatest.matchers.should.Matchers +import units.BlockHash +import units.client.contract.ChainContractClient.DefaultMainChainId +import units.client.contract.{ChainContractClient, ContractBlock} +import units.docker.WavesNodeContainer +import units.test.IntegrationTestEventually + +import scala.annotation.tailrec + +class HttpChainContractClient(api: NodeHttpApi, override val contract: Address) + extends ChainContractClient + with IntegrationTestEventually + with Matchers + with OptionValues + with ScorexLogging { + override def extractData(key: String): Option[DataEntry[?]] = api.dataByKey(contract, key)(LoggingOptions(logRequest = false)) + + private val fiveBlocks = WavesNodeContainer.MaxBlockDelay * 5 + lazy val token: IssuedAsset = IssuedAsset(ByteStr.decodeBase58(getStringData("tokenId").getOrElse(fail("Call setup first"))).get) + + def getEpochFirstBlock(epochNumber: Int): Option[ContractBlock] = + getEpochMeta(epochNumber).flatMap { epochData => + getEpochFirstBlock(epochData.lastBlockHash) + } + + def getEpochFirstBlock(blockHashInEpoch: BlockHash): Option[ContractBlock] = { + @tailrec + def loop(lastBlock: ContractBlock): Option[ContractBlock] = + getBlock(lastBlock.parentHash) match { + case Some(blockData) => + if (blockData.height == 0) blockData.some + else if (blockData.epoch == lastBlock.epoch) loop(blockData) + else lastBlock.some + + case None => none + } + + getBlock(blockHashInEpoch).flatMap { blockData => + if (blockData.height == 0) blockData.some + else loop(blockData) + } + } + + def waitForMinerEpoch(minerAccount: KeyPair)(implicit loggingOptions: LoggingOptions = LoggingOptions(logRequest = false)): Unit = { + val expectedGenerator = minerAccount.toAddress + if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} waitMinerEpoch($expectedGenerator)") + + val subsequentLoggingOptions = loggingOptions.copy(logCall = false) + eventually(timeout(fiveBlocks)) { + val actualGenerator = computedGenerator()(subsequentLoggingOptions) + actualGenerator shouldBe expectedGenerator + } + } + + def waitForHeight(atLeast: Long, chainId: Long = DefaultMainChainId): Unit = { + log.debug(s"waitForHeight($atLeast)") + eventually { + val lastBlock = getLastBlockMeta(chainId).value + lastBlock.height should be >= atLeast + } + } + + def waitForChainId(chainId: Long): Unit = { + log.debug(s"waitForChainId($chainId)") + eventually { + getFirstBlockHash(chainId) shouldBe defined + } + } + + def computedGenerator()(implicit loggingOptions: LoggingOptions): Address = { + if (loggingOptions.logCall) log.debug("computedGenerator") + 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 + } + } +} diff --git a/consensus-client-it/src/test/scala/units/docker/BaseContainer.scala b/consensus-client-it/src/test/scala/units/docker/BaseContainer.scala new file mode 100644 index 00000000..484f80e1 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/docker/BaseContainer.scala @@ -0,0 +1,25 @@ +package units.docker + +import com.wavesplatform.utils.LoggerFacade +import org.slf4j.LoggerFactory +import org.testcontainers.containers.wait.strategy.DockerHealthcheckWaitStrategy + +abstract class BaseContainer(val hostName: String) { + protected lazy val log = LoggerFacade(LoggerFactory.getLogger(s"${getClass.getSimpleName}.$hostName")) + + protected val container: GenericContainer + + def start(): Unit = { + container.start() + } + + def waitReady(): Unit = { + container.waitingFor(new DockerHealthcheckWaitStrategy) + } + + def stop(): Unit = { + container.stop() + } + + def logPorts(): Unit +} diff --git a/consensus-client-it/src/test/scala/units/docker/BesuContainer.scala b/consensus-client-it/src/test/scala/units/docker/BesuContainer.scala new file mode 100644 index 00000000..61022170 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/docker/BesuContainer.scala @@ -0,0 +1,52 @@ +package units.docker + +import org.testcontainers.containers.BindMode +import org.testcontainers.containers.Network.NetworkImpl +import org.web3j.protocol.Web3j +import org.web3j.protocol.http.HttpService +import sttp.client3.{Identity, SttpBackend} +import units.client.JsonRpcClient +import units.client.engine.{EngineApiClient, HttpEngineApiClient, LoggedEngineApiClient} +import units.docker.EcContainer.{EnginePort, RpcPort} +import units.http.OkHttpLogger +import units.test.TestEnvironment.ConfigsDir + +import scala.concurrent.duration.DurationInt + +class BesuContainer(network: NetworkImpl, number: Int, ip: String)(implicit httpClientBackend: SttpBackend[Identity, Any]) + extends EcContainer(number) { + protected override val container = new GenericContainer(DockerImages.BesuExecutionClient) + .withNetwork(network) + .withExposedPorts(RpcPort, EnginePort) + .withEnv("LOG4J_CONFIGURATION_FILE", "/config/log4j2.xml") + .withEnv("ROOT_LOG_FILE_LEVEL", "TRACE") + .withFileSystemBind(s"$ConfigsDir/ec-common/genesis.json", "/genesis.json", BindMode.READ_ONLY) + .withFileSystemBind(s"$ConfigsDir/besu", "/config", BindMode.READ_ONLY) + .withFileSystemBind(s"$ConfigsDir/besu/run-besu.sh", "/tmp/run.sh", BindMode.READ_ONLY) + .withFileSystemBind(s"$ConfigsDir/ec-common/p2p-key-$number.hex", "/etc/secrets/p2p-key", BindMode.READ_ONLY) + .withFileSystemBind(s"$logFile", "/opt/besu/logs/besu.log", BindMode.READ_WRITE) + .withCreateContainerCmdModifier { cmd => + cmd + .withName(s"${network.getName}-$hostName") + .withHostName(hostName) + .withIpv4Address(ip) + .withEntrypoint("/tmp/run.sh") + .withStopTimeout(5) + } + + override lazy val engineApi: EngineApiClient = new LoggedEngineApiClient( + new HttpEngineApiClient( + JsonRpcClient.Config(apiUrl = s"http://${container.getHost}:$enginePort", apiRequestRetries = 5, apiRequestRetryWaitTime = 1.second), + httpClientBackend + ) + ) + + override lazy val web3j = Web3j.build( + new HttpService( + s"http://${container.getHost}:$rpcPort", + HttpService.getOkHttpClientBuilder + .addInterceptor(OkHttpLogger) + .build() + ) + ) +} 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..3806cfa5 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/docker/DockerImages.scala @@ -0,0 +1,10 @@ +package units.docker + +import org.testcontainers.utility.DockerImageName.parse +import units.test.TestEnvironment.WavesDockerImage + +object DockerImages { + val WavesNode = parse(WavesDockerImage) + val BesuExecutionClient = parse("hyperledger/besu:latest") + val GethExecutionClient = parse("ethereum/client-go:stable") +} diff --git a/consensus-client-it/src/test/scala/units/docker/EcContainer.scala b/consensus-client-it/src/test/scala/units/docker/EcContainer.scala new file mode 100644 index 00000000..46b97368 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/docker/EcContainer.scala @@ -0,0 +1,42 @@ +package units.docker + +import com.google.common.io.Files +import org.web3j.protocol.Web3j +import units.client.JsonRpcClient +import units.client.engine.EngineApiClient +import units.docker.EcContainer.{EnginePort, RpcPort} +import units.test.TestEnvironment.* + +import java.io.File +import scala.concurrent.duration.DurationInt + +abstract class EcContainer(number: Int) extends BaseContainer(s"ec-$number") { + protected val logFile = new File(s"$DefaultLogsDir/ec-$number.log") + Files.touch(logFile) + + lazy val rpcPort = container.getMappedPort(RpcPort) + lazy val enginePort = container.getMappedPort(EnginePort) + + lazy val engineApiDockerUrl = s"http://$hostName:${EcContainer.EnginePort}" + lazy val engineApiConfig = JsonRpcClient.Config( + apiUrl = s"http://${container.getHost}:$enginePort", + apiRequestRetries = 5, + apiRequestRetryWaitTime = 1.second + ) + + def engineApi: EngineApiClient + def web3j: Web3j + + override def stop(): Unit = { + web3j.shutdown() + super.stop() + } + + override def logPorts(): Unit = log.debug(s"External host: ${container.getHost}, rpc: $rpcPort, engine: $enginePort") +} + +object EcContainer { + val RpcPort = 8545 + val EnginePort = 8551 + val ChainId = 1337L // from genesis.json +} diff --git a/consensus-client-it/src/test/scala/units/docker/GenericContainer.scala b/consensus-client-it/src/test/scala/units/docker/GenericContainer.scala new file mode 100644 index 00000000..7704cf02 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/docker/GenericContainer.scala @@ -0,0 +1,7 @@ +package units.docker + +import org.testcontainers.containers.GenericContainer as JGenericContrainer +import org.testcontainers.utility.DockerImageName + +// Fixed autocompletion in IntelliJ IDEA +class GenericContainer(dockerImageName: DockerImageName) extends JGenericContrainer[GenericContainer](dockerImageName) diff --git a/consensus-client-it/src/test/scala/units/docker/GethContainer.scala b/consensus-client-it/src/test/scala/units/docker/GethContainer.scala new file mode 100644 index 00000000..88735ee8 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/docker/GethContainer.scala @@ -0,0 +1,69 @@ +package units.docker + +import okhttp3.Interceptor +import org.testcontainers.containers.BindMode +import org.testcontainers.containers.Network.NetworkImpl +import org.web3j.protocol.Web3j +import org.web3j.protocol.http.HttpService +import pdi.jwt.{JwtAlgorithm, JwtClaim, JwtJson} +import sttp.client3.{Identity, SttpBackend} +import units.client.JwtAuthenticationBackend +import units.client.engine.{EngineApiClient, HttpEngineApiClient, LoggedEngineApiClient} +import units.docker.EcContainer.{EnginePort, RpcPort} +import units.http.OkHttpLogger +import units.test.TestEnvironment.ConfigsDir + +import java.time.Clock +import scala.io.Source + +class GethContainer(network: NetworkImpl, number: Int, ip: String)(implicit httpClientBackend: SttpBackend[Identity, Any]) + extends EcContainer(number) { + protected override val container = new GenericContainer(DockerImages.GethExecutionClient) + .withNetwork(network) + .withExposedPorts(RpcPort, EnginePort) + .withFileSystemBind(s"$ConfigsDir/ec-common/genesis.json", "/tmp/genesis.json", BindMode.READ_ONLY) + .withFileSystemBind(s"$ConfigsDir/geth/run-geth.sh", "/tmp/run.sh", BindMode.READ_ONLY) + .withFileSystemBind(s"$ConfigsDir/ec-common/p2p-key-$number.hex", "/etc/secrets/p2p-key", BindMode.READ_ONLY) + .withFileSystemBind(s"$ConfigsDir/ec-common/jwt-secret-$number.hex", "/etc/secrets/jwtsecret", BindMode.READ_ONLY) + .withFileSystemBind(s"$logFile", "/root/logs/log", BindMode.READ_WRITE) + .withCreateContainerCmdModifier { cmd => + cmd + .withName(s"${network.getName}-$hostName") + .withHostName(hostName) + .withIpv4Address(ip) + .withEntrypoint("/tmp/run.sh") + .withStopTimeout(5) + } + + lazy val jwtSecretKey = { + val src = Source.fromFile(s"$ConfigsDir/ec-common/jwt-secret-$number.hex") + try src.getLines().next() + finally src.close() + } + + override lazy val engineApi: EngineApiClient = new LoggedEngineApiClient( + new HttpEngineApiClient( + engineApiConfig, + new JwtAuthenticationBackend(jwtSecretKey, httpClientBackend) + ) + ) + + override lazy val web3j = Web3j.build( + new HttpService( + s"http://${container.getHost}:$rpcPort", + HttpService.getOkHttpClientBuilder + .addInterceptor { (chain: Interceptor.Chain) => + val orig = chain.request() + val jwtToken = JwtJson.encode(JwtClaim().issuedNow(Clock.systemUTC), jwtSecretKey, JwtAlgorithm.HS256) + val request = orig + .newBuilder() + .header("Authorization", s"Bearer $jwtToken") + .build() + + chain.proceed(request) + } + .addInterceptor(OkHttpLogger) + .build() + ) + ) +} diff --git a/consensus-client-it/src/test/scala/units/docker/Networks.scala b/consensus-client-it/src/test/scala/units/docker/Networks.scala new file mode 100644 index 00000000..841e0931 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/docker/Networks.scala @@ -0,0 +1,28 @@ +package units.docker + +import com.github.dockerjava.api.command.CreateNetworkCmd +import com.github.dockerjava.api.model.Network.Ipam +import com.google.common.primitives.Ints.toByteArray +import org.testcontainers.containers.Network + +import java.net.InetAddress +import scala.util.Random + +object Networks { + // A random network in 10.x.x.x range + val networkSeed = Random.nextInt(0x100000) << 4 | 0x0a000000 + + // 10.x.x.x/28 network will accommodate up to 13 nodes + val networkPrefix = s"${InetAddress.getByAddress(toByteArray(networkSeed)).getHostAddress}/28" + + def network: Network.NetworkImpl = Network + .builder() + .driver("bridge") + .enableIpv6(false) + .createNetworkCmdModifier((n: CreateNetworkCmd) => + n.withIpam(new Ipam().withConfig(new Ipam.Config().withSubnet(networkPrefix).withIpRange(networkPrefix))) + ) + .build() + + def ipForNode(nodeId: Int): String = InetAddress.getByAddress(toByteArray(nodeId & 0xf | networkSeed)).getHostAddress +} diff --git a/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala b/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala new file mode 100644 index 00000000..f6af360e --- /dev/null +++ b/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala @@ -0,0 +1,104 @@ +package units.docker + +import com.google.common.io.Files as GFiles +import com.google.common.primitives.{Bytes, Ints} +import com.typesafe.config.ConfigFactory +import com.wavesplatform.account.{Address, SeedKeyPair} +import com.wavesplatform.api.NodeHttpApi +import com.wavesplatform.common.utils.Base58 +import com.wavesplatform.{GenesisBlockGenerator, crypto} +import org.testcontainers.containers.BindMode +import org.testcontainers.containers.Network.NetworkImpl +import sttp.client3.{Identity, SttpBackend, UriContext} +import units.docker.WavesNodeContainer.* +import units.test.TestEnvironment.* + +import java.io.{File, PrintStream} +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Path} +import scala.concurrent.duration.DurationInt +import scala.jdk.CollectionConverters.MapHasAsJava +import scala.jdk.DurationConverters.JavaDurationOps + +class WavesNodeContainer( + network: NetworkImpl, + number: Int, + ip: String, + baseSeed: String, + chainContractAddress: Address, + ecEngineApiUrl: String, + genesisConfigPath: Path +)(implicit httpClientBackend: SttpBackend[Identity, Any]) + extends BaseContainer(s"wavesnode-$number") { + private val logFile = new File(s"$DefaultLogsDir/waves-$number.log") + GFiles.touch(logFile) + + protected override val container = new GenericContainer(DockerImages.WavesNode) + .withNetwork(network) + .withExposedPorts(ApiPort) + .withEnv( + Map( + "NODE_NUMBER" -> s"$number", + "WAVES_WALLET_SEED" -> Base58.encode(baseSeed.getBytes(StandardCharsets.UTF_8)), + "JAVA_OPTS" -> List( + s"-Dunits.defaults.chain-contract=$chainContractAddress", + s"-Dunits.defaults.execution-client-address=$ecEngineApiUrl", + "-Dlogback.file.level=TRACE", + "-Dfile.encoding=UTF-8" + ).mkString(" "), + "WAVES_LOG_LEVEL" -> "TRACE", // STDOUT logs + "WAVES_HEAP_SIZE" -> "1g" + ).asJava + ) + .withFileSystemBind(s"$ConfigsDir/wavesnode", "/etc/waves", BindMode.READ_ONLY) + .withFileSystemBind(s"$ConfigsDir/ec-common", "/etc/secrets", BindMode.READ_ONLY) + .withFileSystemBind(s"$genesisConfigPath", "/etc/it/genesis.conf", BindMode.READ_ONLY) + .withFileSystemBind(s"$logFile", "/var/log/waves/waves.log", BindMode.READ_WRITE) + .withCreateContainerCmdModifier { cmd => + cmd + .withName(s"${network.getName}-$hostName") + .withHostName(hostName) + .withIpv4Address(ip) + .withStopTimeout(5) // Otherwise we don't have logs in the end + } + + lazy val apiPort = container.getMappedPort(ApiPort) + + lazy val api = new NodeHttpApi(uri"http://${container.getHost}:$apiPort", httpClientBackend) + + override def logPorts(): Unit = log.debug(s"External host: ${container.getHost}, api: $apiPort") +} + +object WavesNodeContainer { + val ApiPort = 6869 + + val GenesisTemplateFile = new File(s"$ConfigsDir/wavesnode/genesis-template.conf") + val GenesisTemplate = ConfigFactory.parseFile(GenesisTemplateFile) + val MaxBlockDelay = GenesisTemplate.getDuration("genesis-generator.average-block-delay").toScala * 2 + + def mkKeyPair(seed: String, nonce: Int): SeedKeyPair = + SeedKeyPair(crypto.secureHash(Bytes.concat(Ints.toByteArray(nonce), seed.getBytes(StandardCharsets.UTF_8)))) + + def generateWavesGenesisConfig(): Path = { + val templateFile = ConfigsDir.resolve("wavesnode/genesis-template.conf").toAbsolutePath + + val origConfig = ConfigFactory.parseFile(templateFile.toFile) + val gap = 25.seconds // To force node mining at start, otherwise it schedules + val overrides = ConfigFactory.parseString( + s"""genesis-generator { + | timestamp = ${System.currentTimeMillis() - gap.toMillis} + |}""".stripMargin + ) + + val genesisSettings = GenesisBlockGenerator.parseSettings(overrides.withFallback(origConfig)) + + val origOut = System.out + System.setOut(new PrintStream({ (_: Int) => })) // We don't use System.out in tests, so it should not be an issue + val config = GenesisBlockGenerator.createConfig(genesisSettings) + System.setOut(origOut) + + val dest = DefaultLogsDir.resolve("genesis.conf").toAbsolutePath + Files.writeString(dest, config) + dest + } +} diff --git a/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala b/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala new file mode 100644 index 00000000..a75be3b3 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala @@ -0,0 +1,46 @@ +package units.el + +import com.wavesplatform.account.Address +import com.wavesplatform.utils.ScorexLogging +import org.web3j.abi.datatypes.{AbiTypes, Type} +import org.web3j.abi.{FunctionReturnDecoder, TypeReference} +import org.web3j.crypto.Credentials +import org.web3j.protocol.Web3j +import org.web3j.protocol.core.methods.response.TransactionReceipt +import org.web3j.tx.gas.DefaultGasProvider +import units.bridge.BridgeContract +import units.eth.EthAddress + +import java.util.Collections + +class ElBridgeClient(web3j: Web3j, address: EthAddress) extends ScorexLogging { + def sendNative( + sender: Credentials, + recipient: Address, + amountInEther: BigInt + ): TransactionReceipt = { + val senderAddress = sender.getAddress + log.debug(s"sendNative($senderAddress->$recipient: $amountInEther Wei)") + val bridgeContract = BridgeContract.load(address.hex, web3j, sender, new DefaultGasProvider) + bridgeContract.send_sendNative(recipient.publicKeyHash, amountInEther.bigInteger).send() + } +} + +object ElBridgeClient { + val BurnAddress = EthAddress.unsafeFrom("0x0000000000000000000000000000000000000000") + + def decodeRevertReason(hexRevert: String): String = { + val cleanHex = if (hexRevert.startsWith("0x")) hexRevert.drop(2) else hexRevert + val errorSignature = "08c379a0" // Error(string) + + if (!cleanHex.startsWith(errorSignature)) throw new RuntimeException(s"Not a revert reason: $hexRevert") + + val strType = TypeReference.create(AbiTypes.getType("string").asInstanceOf[Class[Type[?]]]) + val revertReasonTypes = Collections.singletonList(strType) + + val encodedReason = "0x" + cleanHex.drop(8) + val decoded = FunctionReturnDecoder.decode(encodedReason, revertReasonTypes) + if (decoded.isEmpty) throw new RuntimeException(s"Unknown revert reason: $hexRevert") + else decoded.get(0).getValue.asInstanceOf[String] + } +} diff --git a/consensus-client-it/src/test/scala/units/http/OkHttpLogger.scala b/consensus-client-it/src/test/scala/units/http/OkHttpLogger.scala new file mode 100644 index 00000000..8a2f4cc1 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/http/OkHttpLogger.scala @@ -0,0 +1,40 @@ +package units.http + +import com.wavesplatform.api.LoggingUtil +import com.wavesplatform.utils.ScorexLogging +import okhttp3.{Interceptor, Request, Response} +import play.api.libs.json.Json + +import scala.util.Try + +object OkHttpLogger extends Interceptor with ScorexLogging { + override def intercept(chain: Interceptor.Chain): Response = { + val req = chain.request() + val bodyStr = readRequestBody(req) + + val currRequestId = (Json.parse(bodyStr) \ "id").asOpt[Long].getOrElse(LoggingUtil.currRequestId.toLong) + log.debug(s"[$currRequestId] ${req.method()} ${req.url()}: body=$bodyStr") + val res = chain.proceed(req) + log.debug(s"[$currRequestId] HTTP ${res.code()}: body=${readResponseBody(res)}") + res + } + + private def readRequestBody(request: Request): String = request.body() match { + case null => "null" + case body => + val buffer = new okio.Buffer() + Try { + body.writeTo(buffer) + buffer.readUtf8() + }.getOrElse("Could not read body") + } + + private def readResponseBody(response: Response): String = response.body() match { + case null => "null" + case body => + val source = body.source() + source.request(Long.MaxValue) // Buffer the entire body. + val buffer = source.getBuffer.clone() + buffer.readUtf8().trim + } +} diff --git a/consensus-client-it/src/test/scala/units/test/IntegrationTestEventually.scala b/consensus-client-it/src/test/scala/units/test/IntegrationTestEventually.scala new file mode 100644 index 00000000..942496e8 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/test/IntegrationTestEventually.scala @@ -0,0 +1,12 @@ +package units.test + +import org.scalatest.concurrent.Eventually +import org.scalatest.enablers.{ConstantRetrying, Retrying} +import units.docker.WavesNodeContainer.MaxBlockDelay + +import scala.concurrent.duration.DurationInt + +trait IntegrationTestEventually extends Eventually { + implicit def retrying[T]: Retrying[T] = ConstantRetrying.create[T] + implicit override def patienceConfig: PatienceConfig = PatienceConfig(timeout = MaxBlockDelay * 2, interval = 1.second) +} 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..f5ef1377 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala @@ -0,0 +1,12 @@ +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") + val ExecutionClient: String = System.getProperty("cc.it.ec", "besu") // | geth +} diff --git a/local-network/README.md b/local-network/README.md index 8f506d9a..8095dd44 100644 --- a/local-network/README.md +++ b/local-network/README.md @@ -37,11 +37,7 @@ See [./deploy](./deploy/). # Keys * Node HTTP API Key: `testapi` -* CL accounts (see [genesis-template.conf](configs/wavesnode/genesis-template.conf) for more information): - * Node wallet seed: - * wavesnode-1: `devnet-1` - * wavesnode-2: `devnet-2` - * Chain contract: `3FdaanzgX4roVgHevhq8L8q42E7EZL9XTQr` +* CL accounts: see [genesis-template.conf](configs/wavesnode/genesis-template.conf) * EL mining reward accounts: * Reward account for **Miner 1** (`wavesnode-1`, `besu-1`): * Address: `0x7dBcf9c6C3583b76669100f9BE3CaF6d722bc9f9` diff --git a/local-network/_debug/besu/Dockerfile b/local-network/_debug/besu/Dockerfile deleted file mode 100644 index c3c297ab..00000000 --- a/local-network/_debug/besu/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM hyperledger/besu:develop - -USER root - -RUN apt-get update && apt-get install -y \ - curl \ - netcat \ - iproute2 - -USER besu diff --git a/local-network/configs/besu/besu.yml b/local-network/configs/besu/besu.yml index 95b693d8..e683f057 100644 --- a/local-network/configs/besu/besu.yml +++ b/local-network/configs/besu/besu.yml @@ -1,6 +1,6 @@ services: besu: - image: hyperledger/besu:latest # Debug version: besu-debug:latest , see _debug/ + image: hyperledger/besu:latest volumes: - ../ec-common/genesis.json:/genesis.json:ro - .:/config:ro diff --git a/local-network/configs/besu/log4j2.xml b/local-network/configs/besu/log4j2.xml index 6309ddbb..17b1a158 100644 --- a/local-network/configs/besu/log4j2.xml +++ b/local-network/configs/besu/log4j2.xml @@ -3,18 +3,20 @@ %date %-5level [%-25.25thread] %35.35c{1.} - %msg%n%throwable + DEBUG - + - - + + - - + + @@ -23,25 +25,25 @@ - + - + - + - + - - + + diff --git a/local-network/configs/besu/run-besu.sh b/local-network/configs/besu/run-besu.sh index 09c6009e..36c69169 100755 --- a/local-network/configs/besu/run-besu.sh +++ b/local-network/configs/besu/run-besu.sh @@ -2,6 +2,10 @@ IP=$(hostname -I) +tee /opt/besu/logs/besu.log < share # the sum of shares should be <= initial-balance distributions = [ - # CL miner on wavesnode-1 + # Miner on wavesnode-1 # Seed text: devnet-1 # Nonce: 0 # Seed: HnyGuCEnV1A @@ -17,25 +17,16 @@ genesis-generator { # Address: 3FNraPMYcfuGREcxorNSEBHgNLjmYtaHy9e { seed-text = "devnet-1", nonce = 0, amount = 998036000000000 } - # Chain contract on wavesnode-1 + # Miner on wavesnode-1 # Seed text: devnet-1 - # Nonce: 2 + # Nonce: 1 # Seed: HnyGuCEnV1A - # Private key: 7TzN4c2XgvSN28gEbvW5wmUecfa4SrPRLXjgr5DRfQMG - # Public key: 5yxSxDhB2ZL5agYnU6WRtEqPg3T3FfUY3zouKrMpnTX6 - # Address: 3FdaanzgX4roVgHevhq8L8q42E7EZL9XTQr - { seed-text = "devnet-1", nonce = 2, amount = 20000000000 } + # Private key: 5r7KpS3MNXxRcz9jMbpjZKhaZVxDxF74H4VLW6ydi4aT + # Public key: 2JYMTjUK7tC8NQi6TD6oWgy41YbrnXuoLzZydrFKTKt6 + # Address: 3FSgXpgbT6m1speWgVx3cVxAZKmdr4barHU + { seed-text = "devnet-1", nonce = 1, amount = 998036000000000 } - # DAO Address - # Seed text: devnet dao 1 - # Nonce: 0 - # Seed: 2ttKSSq2o7VU6kFwN - # Private key: EXC4wafzVus3tmesKEouuewZHGNzEG5qxNHMN66XAiG4 - # Public key: 7S2iCGvRcx3bJA3y8R9k6sJdVATg3ss8r4RuW1S8VriA - # Address: 3FYp54DX9Np9uouxBtnnCFPYWzz7K3kvSD2 - { seed-text = "devnet dao 1", nonce = 0, amount = 100000000 } - - # CL miner on wavesnode-2 + # Miner on wavesnode-2 # Seed text: devnet-2 # Nonce: 0 # Seed: HnyGuCEnV1B @@ -46,20 +37,38 @@ genesis-generator { # Additional addresses - # Seed text: devnet-0 + # Seed text: devnet rich # Nonce: 0 - # Seed: HnyGuCEnV19 - # Private key: 8aYKVCJVYfcSQeSH1kM4D2e4Fcj5cAQzMh8uZ5hW2tB9 - # Public key: GRKksNikCNXYT7HqxCnqzjP5pf4NctgtTnmCvCMDEhAT - # Address: 3Ffcu52v7ETMAFhNAqQvHsv4JuQjEVP9q6E - { seed-text = "devnet-0", nonce = 0, amount = 2305842994213693951 } + # Seed: RtxP5VZ8GyepM8T + # Private key: 5Kn5nRo6piCWg4cwoHWj41L8YnASfYSRt9RkwXxfCjQx + # Public key: HWVpTRztoFNwbCsJ1PxtneNSVywkwD8PzMwagDM1YoE6 + # Address: 3FiaGiTaJ57qLpw5zmdxSejnUYCK2R9E2DV + { seed-text = "devnet rich", nonce = 0, amount = 2305842994213693951 } - # Seed text: devnet-0 + # Seed text: devnet rich # Nonce: 1 - # Seed: HnyGuCEnV19 - # Private key: 5tE8jwZ3B4eLTvdz7LqEBMcmspoqXD9nPCEfcZgh2BeV - # Public key: BArfnBGQ9gkNHC2EftXf42uJuBioSffZiJoyYCTtwxof - # Address: 3FQFFWxMCDmyZhkRzKA7QiAZL4RKMhJvFnu - { seed-text = "devnet-0", nonce = 1, amount = 2305842994213693952 } + # Seed: RtxP5VZ8GyepM8T + # Private key: HkHtYtTVyUy2hcjNLowuEBTrjvCcuq4vGSaWac6Q5gLi + # Public key: AhbKXvrNikp2iynszNxAAzoE7hgzaaZpV9jyVpi5D1jS + # Address: 3FfXrt2RZCBQm7AKjeRhQjR9vGwu8Cb3RjN + { seed-text = "devnet rich", nonce = 1, amount = 2305842994213693952 } + + # DAO Address + # Seed text: devnet dao + # Nonce: 0 + # Seed: 6e9UCtnLmrTWeS + # Private key: 81JkCduSWu47R9ZsZE6TNdEcaLKPJzxHJAACzKc3qTrV + # Public key: 8DTQLSTZ8UcqVRmk7rBeA8vpeHRhcZ2SwtxEuTRRad1x + # Address: 3FVp6fUSmVehs7Q3uJBXe5kWtpz57f7azzA + { seed-text = "devnet dao", nonce = 0, amount = 100000000 } + + # Chain contract + # Seed text: devnet cc + # Nonce: 0 + # Seed: 2H7uPBNbqgJG2 + # Private key: Ah9hXYB5bPs25iepkAUoR2RGSRB5B7pU4JNM5X6coH1j + # Public key: csSdfq8widAC231mR8VQRKcMWWHozZ827kAJWnrLqHR + # Address: 3FXDd4LoxxqVLfMk8M25f8CQvfCtGMyiXV1 + { seed-text = "devnet cc", nonce = 0, amount = 20000000000 } ] } diff --git a/local-network/configs/wavesnode/units.conf b/local-network/configs/wavesnode/units.conf new file mode 100644 index 00000000..0bcbe1e0 --- /dev/null +++ b/local-network/configs/wavesnode/units.conf @@ -0,0 +1,23 @@ +units { + defaults { + chain-contract = "3FXDd4LoxxqVLfMk8M25f8CQvfCtGMyiXV1" + execution-client-address = "http://ec-"${NODE_NUMBER}":8551" + network { + port = 6865 + bind-address = "0.0.0.0" + known-peers = [ + "wavesnode-1:6865" + "wavesnode-2:6865" + # Uncomment only if have a waves node. Otherwise, nodes won't connect + # "wavesnode-3:6865" + # "wavesnode-4:6865" + ] + + enable-peers-exchange = no + } + + jwt-secret-file = "/etc/secrets/jwt-secret-"${NODE_NUMBER}".hex" + } + + chains = [ {} ] # Enable one network +} diff --git a/local-network/configs/wavesnode/waves.conf b/local-network/configs/wavesnode/waves.conf index 6473e406..4acda82f 100644 --- a/local-network/configs/wavesnode/waves.conf +++ b/local-network/configs/wavesnode/waves.conf @@ -50,6 +50,7 @@ waves { voting-interval = 1 } include "genesis.conf" + include "/etc/it/genesis.conf" } } @@ -78,9 +79,11 @@ waves { known-peers = [ "wavesnode-1:6863" "wavesnode-2:6863" + # Uncomment only if have a waves node. Otherwise, nodes won't connect + # "wavesnode-3:6863" + # "wavesnode-4:6863" ] - max-single-host-connections = 6 enable-peers-exchange = no enable-blacklisting = no @@ -93,27 +96,6 @@ waves { } } - # P2P Network settings - l2 { - chain-contract = "3FdaanzgX4roVgHevhq8L8q42E7EZL9XTQr" - - execution-client-address = "http://ec-"${NODE_NUMBER}":8551" - jwt-secret-file = "/etc/secrets/jwt-secret-"${NODE_NUMBER}".hex" - - network { - bind-address = "0.0.0.0" - port = 6865 - known-peers = [ - "wavesnode-1:6865" - "wavesnode-2:6865" - ] - enable-peers-exchange = off - peers-broadcast-interval = 1d - } - - mining-enable = yes - } - rest-api { # Enable/disable node's REST API enable = yes @@ -148,3 +130,5 @@ waves { password = "" } } + +include required("units.conf") diff --git a/local-network/consensus_client-image-build.sh b/local-network/consensus_client-image-build.sh index 6b52f2db..d79e9518 100755 --- a/local-network/consensus_client-image-build.sh +++ b/local-network/consensus_client-image-build.sh @@ -3,5 +3,4 @@ DIR="$(cd "$(dirname "$0")" && pwd)" cd "${DIR}/.." || exit -sbt -J-Xmx4G -J-Xss4m -Dfile.encoding=UTF-8 -Dsbt.supershell=false buildTarballsForDocker -docker build -t consensus-client:local docker +sbt -J-Xmx4G -J-Xss4m -Dfile.encoding=UTF-8 -Dsbt.supershell=false docker diff --git a/local-network/deploy/README.md b/local-network/deploy/README.md index eca81c57..36dfd701 100644 --- a/local-network/deploy/README.md +++ b/local-network/deploy/README.md @@ -7,3 +7,15 @@ To run tests on the host machine, from this directory: 1. If you're on macOS with Apple Silicon: install `gcc`. 2. Create the virtual environment and install dependencies: `./dev-setup.sh` 3. Run test, e.g.: `./tests/transfer-c2e.py` + +To generate `Bridge.java`, run: +```bash +web3j generate solidity \ + --abiFile=setup/el/compiled/Bridge.abi \ + --binFile=setup/el/compiled/Bridge.bin \ + --generateBoth \ + --outputDir=../../consensus-client-it/src/test/java/ \ + -p units.bridge \ + --primitiveTypes \ + -c BridgeContract +``` diff --git a/local-network/deploy/deploy.py b/local-network/deploy/deploy.py index 4e10f9b6..90eef034 100644 --- a/local-network/deploy/deploy.py +++ b/local-network/deploy/deploy.py @@ -45,7 +45,7 @@ r = network.cl_chain_contract.setup( el_genesis_block_hash, - daoAddress = '3FYp54DX9Np9uouxBtnnCFPYWzz7K3kvSD2' + daoAddress = network.cl_dao.address ) waves.force_success(log, r, "Can not setup the chain contract") diff --git a/local-network/deploy/local/network.py b/local-network/deploy/local/network.py index c66e1412..ac808767 100644 --- a/local-network/deploy/local/network.py +++ b/local-network/deploy/local/network.py @@ -27,9 +27,13 @@ class Miner: class ExtendedNetwork(Network): + @cached_property + def cl_dao(self) -> ChainContract: + return pw.Address(seed="devnet dao", nonce=0) + @cached_property def cl_chain_contract(self) -> ChainContract: - return ChainContract(seed="devnet-1", nonce=2) + return ChainContract(seed="devnet cc", nonce=0) @cached_property def cl_miners(self) -> List[Miner]: @@ -56,7 +60,7 @@ def cl_miners(self) -> List[Miner]: @cached_property def cl_rich_accounts(self) -> List[pw.Address]: - return [pw.Address(seed="devnet-0", nonce=n) for n in range(0, 2)] + return [pw.Address(seed="devnet rich", nonce=n) for n in range(0, 2)] @cached_property def el_rich_accounts(self) -> List[LocalAccount]: @@ -75,7 +79,7 @@ def el_rich_accounts(self) -> List[LocalAccount]: chain_id_str="D", cl_node_api_url=get_waves_api_url(1), el_node_api_url=get_ec_api_url(1), - chain_contract_address="3FdaanzgX4roVgHevhq8L8q42E7EZL9XTQr", + chain_contract_address="3FXDd4LoxxqVLfMk8M25f8CQvfCtGMyiXV1", ) diff --git a/local-network/docker-compose.yml b/local-network/docker-compose.yml index deeca46d..ca880fbe 100644 --- a/local-network/docker-compose.yml +++ b/local-network/docker-compose.yml @@ -24,7 +24,7 @@ services: service: geth ports: - "127.0.0.1:28551:8551" # Engine port - - "127.0.0.1:28545:8545" # RPC port, useful because doesn't require an auth token + - "127.0.0.1:28545:8545" # RPC port volumes: - ./configs/ec-common/p2p-key-2.hex:/etc/secrets/p2p-key:ro - ./configs/ec-common/jwt-secret-2.hex:/etc/secrets/jwtsecret:ro diff --git a/project/plugins.sbt b/project/plugins.sbt index fb7e7d99..804c55ee 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,5 +3,8 @@ resolvers ++= Seq( Resolver.sbtPluginRepo("releases") ) -addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.0") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") +Seq( + "com.github.sbt" % "sbt-native-packager" % "1.10.0", + "com.github.sbt" % "sbt-ci-release" % "1.5.12", + "se.marcuslonnberg" % "sbt-docker" % "1.10.0" +).map(addSbtPlugin) diff --git a/src/main/scala/units/Bridge.scala b/src/main/scala/units/Bridge.scala index 5760258c..2b395a53 100644 --- a/src/main/scala/units/Bridge.scala +++ b/src/main/scala/units/Bridge.scala @@ -27,6 +27,9 @@ object Bridge { val ElSentNativeEventTopic = org.web3j.abi.EventEncoder.encode(ElSentNativeEventDef) + /** @param amount + * In waves units, see bridge.sol + */ case class ElSentNativeEvent(wavesAddress: Address, amount: Long) object ElSentNativeEvent { diff --git a/src/main/scala/units/ClientConfig.scala b/src/main/scala/units/ClientConfig.scala index 82290b7f..b35e5473 100644 --- a/src/main/scala/units/ClientConfig.scala +++ b/src/main/scala/units/ClientConfig.scala @@ -6,7 +6,7 @@ import com.wavesplatform.settings.* import net.ceedubs.ficus.Ficus.* import net.ceedubs.ficus.readers.ArbitraryTypeReader.arbitraryTypeValueReader import net.ceedubs.ficus.readers.{Generated, ValueReader} -import net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase +import units.client.JsonRpcClient import scala.concurrent.duration.FiniteDuration @@ -22,6 +22,12 @@ case class ClientConfig( jwtSecretFile: Option[String] ) { lazy val chainContractAddress: Address = Address.fromString(chainContract).explicitGet() + + val jsonRpcClient = JsonRpcClient.Config( + apiUrl = executionClientAddress, + apiRequestRetries = apiRequestRetries, + apiRequestRetryWaitTime = apiRequestRetryWaitTime + ) } object ClientConfig { diff --git a/src/main/scala/units/ConsensusClientDependencies.scala b/src/main/scala/units/ConsensusClientDependencies.scala index 1b2a51cf..292c2057 100644 --- a/src/main/scala/units/ConsensusClientDependencies.scala +++ b/src/main/scala/units/ConsensusClientDependencies.scala @@ -37,7 +37,7 @@ class ConsensusClientDependencies(val config: ClientConfig) extends AutoCloseabl httpClientBackend } - val engineApiClient = new LoggedEngineApiClient(new HttpEngineApiClient(config, maybeAuthenticatedBackend)) + val engineApiClient = new LoggedEngineApiClient(new HttpEngineApiClient(config.jsonRpcClient, maybeAuthenticatedBackend)) val allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE) val peerDatabase = new PeerDatabaseImpl(config.network) diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala index 714adf57..e829acab 100644 --- a/src/main/scala/units/ELUpdater.scala +++ b/src/main/scala/units/ELUpdater.scala @@ -259,13 +259,14 @@ class ELUpdater( private def rollbackTo(prevState: Working[ChainStatus], target: L2BlockLike, finalizedBlock: ContractBlock): JobResult[Working[ChainStatus]] = { val targetHash = target.hash + logger.info(s"Starting rollback to $targetHash") for { rollbackBlock <- mkRollbackBlock(targetHash) - _ = logger.info(s"Starting rollback to $targetHash using rollback block ${rollbackBlock.hash}") + _ = logger.info(s"Intermediate rollback block: ${rollbackBlock.hash}") fixedFinalizedBlock = if (finalizedBlock.height > rollbackBlock.parentBlock.height) rollbackBlock.parentBlock else finalizedBlock _ <- confirmBlock(rollbackBlock.hash, fixedFinalizedBlock.hash) _ <- confirmBlock(target, fixedFinalizedBlock) - lastEcBlock <- engineApiClient.getLastExecutionBlock + lastEcBlock <- engineApiClient.getLastExecutionBlock() _ <- Either.cond( targetHash == lastEcBlock.hash, (), @@ -286,7 +287,7 @@ class ELUpdater( setState("10", newState) newState } - } + }.left.map(e => ClientError(s"Error during rollback: ${e.message}")) private def startBuildingPayload( epochInfo: EpochInfo, @@ -454,7 +455,7 @@ class ELUpdater( (for { newEpochInfo <- calculateEpochInfo mainChainInfo <- chainContractClient.getMainChainInfo.toRight("Can't get main chain info") - lastEcBlock <- engineApiClient.getLastExecutionBlock.leftMap(_.message) + lastEcBlock <- engineApiClient.getLastExecutionBlock().leftMap(_.message) } yield { logger.trace(s"Following main chain ${mainChainInfo.id}") val fullValidationStatus = FullValidationStatus( @@ -890,7 +891,7 @@ class ELUpdater( private def waitForSyncCompletion(target: ContractBlock): Unit = scheduler.scheduleOnce(5.seconds)(state match { case SyncingToFinalizedBlock(finalizedBlockHash) if finalizedBlockHash == target.hash => logger.debug(s"Checking if EL has synced to ${target.hash} on height ${target.height}") - engineApiClient.getLastExecutionBlock match { + engineApiClient.getLastExecutionBlock() match { case Left(error) => logger.error(s"Sync to ${target.hash} was not completed, error=${error.message}") setState("23", Starting) diff --git a/src/main/scala/units/client/JsonRpcClient.scala b/src/main/scala/units/client/JsonRpcClient.scala index dcc1f99c..d046a9b8 100644 --- a/src/main/scala/units/client/JsonRpcClient.scala +++ b/src/main/scala/units/client/JsonRpcClient.scala @@ -2,47 +2,49 @@ package units.client import cats.Id import cats.syntax.either.* -import units.client.JsonRpcClient.DefaultTimeout -import units.{ClientConfig, ClientError} +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 sttp.client3.* import sttp.client3.playJson.* -import sttp.model.Uri +import units.ClientError +import units.client.JsonRpcClient.* import java.util.concurrent.ThreadLocalRandom -import scala.concurrent.duration.{DurationInt, FiniteDuration} +import scala.concurrent.duration.FiniteDuration import scala.util.{Failure, Success, Try} trait JsonRpcClient { private type RpcRequest[B] = Request[Either[ResponseException[String, JsError], JsonRpcResponse[B]], Any] - def config: ClientConfig + def config: Config def backend: SttpBackend[Id, ?] - def apiUrl: Uri - protected def sendRequest[RQ: Writes, RP: Reads](requestBody: RQ, timeout: FiniteDuration = DefaultTimeout): Either[String, Option[RP]] = - sendRequest(mkRequest(requestBody, timeout), config.apiRequestRetries) + protected def sendRequest[RQ: Writes, RP: Reads]( + requestBody: RQ, + timeout: FiniteDuration, + requestId: Int + ): Either[String, Option[RP]] = + sendRequest(requestId, mkRequest(requestBody, timeout), config.apiRequestRetries) protected def parseJson[A: Reads](jsValue: JsValue): Either[ClientError, A] = Try(jsValue.as[A]).toEither.leftMap(err => ClientError(s"Response parse error: ${err.getMessage}")) - private def mkRequest[A: Writes, B: Reads](requestBody: A, timeout: FiniteDuration): RpcRequest[B] = { - val currRequestId = ThreadLocalRandom.current().nextInt(10000, 100000).toString + private def mkRequest[A: Writes, B: Reads](requestBody: A, timeout: FiniteDuration): RpcRequest[B] = basicRequest .body(requestBody) - .post(apiUrl) + .post(config.sttpApiUri) .response(asJson[JsonRpcResponse[B]]) .readTimeout(timeout) - .tag(RequestIdTag, currRequestId) - } - private def sendRequest[RQ: Writes, RS: Reads](request: RpcRequest[RS], retriesLeft: Int): Either[String, Option[RS]] = { + private def sendRequest[RQ: Writes, RS: Reads](requestId: Int, request: RpcRequest[RS], retriesLeft: Int): Either[String, Option[RS]] = { def retryIf(cond: Boolean, elseError: String): Either[String, Option[RS]] = if (cond) { val retries = retriesLeft - 1 - // TODO: make non-blocking waiting Thread.sleep(config.apiRequestRetryWaitTime.toMillis) - sendRequest(request.tag(RetriesLeftTag, retries), retries) + onRetry(requestId) + sendRequest(requestId, request, retries) } else Left(elseError) Try { @@ -67,8 +69,18 @@ trait JsonRpcClient { // geth: // https://github.com/ethereum/go-ethereum/blob/master/rpc/errors.go#L71 lcErrorMessage.contains("timed out") } + + def onRetry(requestId: Int): Unit } object JsonRpcClient { - val DefaultTimeout: FiniteDuration = 1.minute + case class Config(apiUrl: String, apiRequestRetries: Int, apiRequestRetryWaitTime: FiniteDuration) { + val sttpApiUri = uri"$apiUrl" + } + + object Config { + implicit val configValueReader: Generated[ValueReader[Config]] = arbitraryTypeValueReader + } + + def newRequestId: Int = ThreadLocalRandom.current().nextInt(10000, 100000) } diff --git a/src/main/scala/units/client/contract/ChainContractClient.scala b/src/main/scala/units/client/contract/ChainContractClient.scala index 8c328191..95f0b307 100644 --- a/src/main/scala/units/client/contract/ChainContractClient.scala +++ b/src/main/scala/units/client/contract/ChainContractClient.scala @@ -61,7 +61,7 @@ trait ChainContractClient { val chainHeight = bb.getLong() val epoch = bb.getLong().toInt // blockMeta is set up in a chain contract and RIDE numbers are Longs val parentHash = BlockHash(bb.getByteArray(BlockHashBytesSize)) - val chainId = if (bb.remaining() >= 8) bb.getLong() else 0L + val chainId = if (bb.remaining() >= 8) bb.getLong() else DefaultMainChainId val e2cTransfersRootHash = if (bb.remaining() >= ContractBlock.E2CTransfersRootHashLength) bb.getByteArray(ContractBlock.E2CTransfersRootHashLength) @@ -228,16 +228,16 @@ trait ChainContractClient { private def getLastBlockHash(chainId: Long): Option[BlockHash] = getChainMeta(chainId).map(_._2) - private def getFirstBlockHash(chainId: Long): Option[BlockHash] = + protected def getFirstBlockHash(chainId: Long): Option[BlockHash] = getBlockHash(s"chain${chainId}FirstBlock") - private def getBinaryData(key: String): Option[ByteStr] = + protected def getBinaryData(key: String): Option[ByteStr] = extractBinaryValue(key, extractData(key)) - private def getStringData(key: String): Option[String] = + protected def getStringData(key: String): Option[String] = extractStringValue(key, extractData(key)) - private def getLongData(key: String): Option[Long] = + protected def getLongData(key: String): Option[Long] = extractLongValue(key, extractData(key)) private def extractLongValue(context: String, extractedDataEntry: Option[DataEntry[?]]): Option[Long] = @@ -263,7 +263,7 @@ trait ChainContractClient { object ChainContractClient { val MinMinerBalance: Long = 20000_00000000L - val DefaultMainChainId = 0 + val DefaultMainChainId = 0L private val AllMinersKey = "allMiners" private val MainChainIdKey = "mainChainId" diff --git a/src/main/scala/units/client/engine/EngineApiClient.scala b/src/main/scala/units/client/engine/EngineApiClient.scala index a8c0a675..b9d8e49b 100644 --- a/src/main/scala/units/client/engine/EngineApiClient.scala +++ b/src/main/scala/units/client/engine/EngineApiClient.scala @@ -1,13 +1,14 @@ package units.client.engine import play.api.libs.json.* +import units.client.JsonRpcClient.newRequestId import units.client.engine.EngineApiClient.PayloadId import units.client.engine.model.* import units.eth.EthAddress import units.{BlockHash, JobResult} trait EngineApiClient { - def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] + def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash, requestId: Int = newRequestId): JobResult[PayloadStatus] def forkChoiceUpdateWithPayloadId( lastBlockHash: BlockHash, @@ -15,26 +16,29 @@ trait EngineApiClient { unixEpochSeconds: Long, suggestedFeeRecipient: EthAddress, prevRandao: String, - withdrawals: Vector[Withdrawal] = Vector.empty + withdrawals: Vector[Withdrawal] = Vector.empty, + requestId: Int = newRequestId ): JobResult[PayloadId] - def getPayload(payloadId: PayloadId): JobResult[JsObject] + def getPayload(payloadId: PayloadId, requestId: Int = newRequestId): JobResult[JsObject] - def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]] + def applyNewPayload(payload: JsObject, requestId: Int = newRequestId): JobResult[Option[BlockHash]] - def getPayloadBodyByHash(hash: BlockHash): JobResult[Option[JsObject]] + def getPayloadBodyByHash(hash: BlockHash, requestId: Int = newRequestId): JobResult[Option[JsObject]] - def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]] + def getBlockByNumber(number: BlockNumber, requestId: Int = newRequestId): JobResult[Option[EcBlock]] - def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]] + def getBlockByHash(hash: BlockHash, requestId: Int = newRequestId): JobResult[Option[EcBlock]] - def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]] + def getBlockByHashJson(hash: BlockHash, requestId: Int = newRequestId): JobResult[Option[JsObject]] - def getLastExecutionBlock: JobResult[EcBlock] + def getLastExecutionBlock(requestId: Int = newRequestId): JobResult[EcBlock] - def blockExists(hash: BlockHash): JobResult[Boolean] + def blockExists(hash: BlockHash, requestId: Int = newRequestId): JobResult[Boolean] - def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] + def getLogs(hash: BlockHash, address: EthAddress, topic: String, requestId: Int = newRequestId): JobResult[List[GetLogsResponseEntry]] + + def onRetry(requestId: Int): Unit = {} } object EngineApiClient { diff --git a/src/main/scala/units/client/engine/HttpEngineApiClient.scala b/src/main/scala/units/client/engine/HttpEngineApiClient.scala index 41deab4e..8f6a1671 100644 --- a/src/main/scala/units/client/engine/HttpEngineApiClient.scala +++ b/src/main/scala/units/client/engine/HttpEngineApiClient.scala @@ -4,7 +4,6 @@ import cats.syntax.either.* import cats.syntax.traverse.* import play.api.libs.json.* import sttp.client3.* -import sttp.model.Uri import units.client.JsonRpcClient import units.client.engine.EngineApiClient.PayloadId import units.client.engine.HttpEngineApiClient.* @@ -12,18 +11,16 @@ import units.client.engine.model.* import units.client.engine.model.ForkChoiceUpdatedRequest.ForkChoiceAttributes import units.client.engine.model.PayloadStatus.{Syncing, Valid} import units.eth.EthAddress -import units.{BlockHash, ClientConfig, ClientError, JobResult} +import units.{BlockHash, ClientError, JobResult} import scala.concurrent.duration.{DurationInt, FiniteDuration} -class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Identity, ?]) extends EngineApiClient with JsonRpcClient { - - val apiUrl: Uri = uri"${config.executionClientAddress}" - - def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = { +class HttpEngineApiClient(val config: JsonRpcClient.Config, val backend: SttpBackend[Identity, ?]) extends EngineApiClient with JsonRpcClient { + def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash, requestId: Int): JobResult[PayloadStatus] = { sendEngineRequest[ForkChoiceUpdatedRequest, ForkChoiceUpdatedResponse]( - ForkChoiceUpdatedRequest(blockHash, finalizedBlockHash, None), - BlockExecutionTimeout + ForkChoiceUpdatedRequest(blockHash, finalizedBlockHash, None, requestId), + BlockExecutionTimeout, + requestId ) .flatMap { case ForkChoiceUpdatedResponse(ps @ PayloadState(Valid | Syncing, _, _), None) => Right(ps.status) @@ -39,15 +36,18 @@ class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Ide unixEpochSeconds: Long, suggestedFeeRecipient: EthAddress, prevRandao: String, - withdrawals: Vector[Withdrawal] + withdrawals: Vector[Withdrawal], + requestId: Int ): JobResult[PayloadId] = { sendEngineRequest[ForkChoiceUpdatedRequest, ForkChoiceUpdatedResponse]( ForkChoiceUpdatedRequest( lastBlockHash, finalizedBlockHash, - Some(ForkChoiceAttributes(unixEpochSeconds, suggestedFeeRecipient, prevRandao, withdrawals)) + Some(ForkChoiceAttributes(unixEpochSeconds, suggestedFeeRecipient, prevRandao, withdrawals)), + requestId ), - BlockExecutionTimeout + BlockExecutionTimeout, + requestId ).flatMap { case ForkChoiceUpdatedResponse(PayloadState(Valid, _, _), Some(payloadId)) => Right(payloadId) @@ -60,12 +60,14 @@ class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Ide } } - def getPayload(payloadId: PayloadId): JobResult[JsObject] = { - sendEngineRequest[GetPayloadRequest, GetPayloadResponse](GetPayloadRequest(payloadId), NonBlockExecutionTimeout).map(_.executionPayload) + def getPayload(payloadId: PayloadId, requestId: Int): JobResult[JsObject] = { + sendEngineRequest[GetPayloadRequest, GetPayloadResponse](GetPayloadRequest(payloadId, requestId), NonBlockExecutionTimeout, requestId).map( + _.executionPayload + ) } - def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]] = { - sendEngineRequest[NewPayloadRequest, PayloadState](NewPayloadRequest(payload), BlockExecutionTimeout).flatMap { + def applyNewPayload(payload: JsObject, requestId: Int): JobResult[Option[BlockHash]] = { + sendEngineRequest[NewPayloadRequest, PayloadState](NewPayloadRequest(payload, requestId), BlockExecutionTimeout, requestId).flatMap { case PayloadState(_, _, Some(validationError)) => Left(ClientError(s"Payload validation error: $validationError")) case PayloadState(Valid, Some(latestValidHash), _) => Right(Some(latestValidHash)) case PayloadState(Syncing, latestValidHash, _) => Right(latestValidHash) @@ -74,48 +76,52 @@ class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Ide } } - def getPayloadBodyByHash(hash: BlockHash): JobResult[Option[JsObject]] = { - sendEngineRequest[GetPayloadBodyByHash, JsArray](GetPayloadBodyByHash(hash), NonBlockExecutionTimeout) + def getPayloadBodyByHash(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] = { + sendEngineRequest[GetPayloadBodyByHash, JsArray](GetPayloadBodyByHash(hash, requestId), NonBlockExecutionTimeout, requestId) .map(_.value.headOption.flatMap(_.asOpt[JsObject])) } - def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]] = { + def getBlockByNumber(number: BlockNumber, requestId: Int): JobResult[Option[EcBlock]] = { for { - json <- getBlockByNumberJson(number.str) + json <- getBlockByNumberJson(number.str, requestId) blockMeta <- json.traverse(parseJson[EcBlock](_)) } yield blockMeta } - def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]] = { - sendRequest[GetBlockByHashRequest, EcBlock](GetBlockByHashRequest(hash)) + def getBlockByHash(hash: BlockHash, requestId: Int): JobResult[Option[EcBlock]] = { + sendRequest[GetBlockByHashRequest, EcBlock](GetBlockByHashRequest(hash, requestId), NonBlockExecutionTimeout, requestId) .leftMap(err => ClientError(s"Error getting block by hash $hash: $err")) } - def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]] = { - sendRequest[GetBlockByHashRequest, JsObject](GetBlockByHashRequest(hash)) + def getBlockByHashJson(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] = { + sendRequest[GetBlockByHashRequest, JsObject](GetBlockByHashRequest(hash, requestId), NonBlockExecutionTimeout, requestId) .leftMap(err => ClientError(s"Error getting block json by hash $hash: $err")) } - def getLastExecutionBlock: JobResult[EcBlock] = for { - lastEcBlockOpt <- getBlockByNumber(BlockNumber.Latest) + def getLastExecutionBlock(requestId: Int): JobResult[EcBlock] = for { + lastEcBlockOpt <- getBlockByNumber(BlockNumber.Latest, requestId) lastEcBlock <- Either.fromOption(lastEcBlockOpt, ClientError("Impossible: EC doesn't have blocks")) } yield lastEcBlock - def blockExists(hash: BlockHash): JobResult[Boolean] = - getBlockByHash(hash).map(_.isDefined) + def blockExists(hash: BlockHash, requestId: Int): JobResult[Boolean] = + getBlockByHash(hash, requestId).map(_.isDefined) - private def getBlockByNumberJson(number: String): JobResult[Option[JsObject]] = { - sendRequest[GetBlockByNumberRequest, JsObject](GetBlockByNumberRequest(number)) + private def getBlockByNumberJson(number: String, requestId: Int): JobResult[Option[JsObject]] = { + sendRequest[GetBlockByNumberRequest, JsObject](GetBlockByNumberRequest(number, requestId), NonBlockExecutionTimeout, requestId) .leftMap(err => ClientError(s"Error getting block by number $number: $err")) } - override def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] = - sendRequest[GetLogsRequest, List[GetLogsResponseEntry]](GetLogsRequest(hash, address, List(topic))) + override def getLogs(hash: BlockHash, address: EthAddress, topic: String, requestId: Int): JobResult[List[GetLogsResponseEntry]] = + sendRequest[GetLogsRequest, List[GetLogsResponseEntry]]( + GetLogsRequest(hash, address, List(topic), requestId), + NonBlockExecutionTimeout, + requestId + ) .leftMap(err => ClientError(s"Error getting block logs by hash $hash: $err")) .map(_.getOrElse(List.empty)) - private def sendEngineRequest[A: Writes, B: Reads](request: A, timeout: FiniteDuration): JobResult[B] = { - sendRequest(request, timeout) match { + private def sendEngineRequest[A: Writes, B: Reads](request: A, timeout: FiniteDuration, requestId: Int): JobResult[B] = { + sendRequest(request, timeout, requestId) match { case Right(response) => response.toRight(ClientError(s"Unexpected engine API empty response")) case Left(err) => Left(ClientError(s"Engine API request error: $err")) } diff --git a/src/main/scala/units/client/engine/LoggedEngineApiClient.scala b/src/main/scala/units/client/engine/LoggedEngineApiClient.scala index 54a10f64..0cfde809 100644 --- a/src/main/scala/units/client/engine/LoggedEngineApiClient.scala +++ b/src/main/scala/units/client/engine/LoggedEngineApiClient.scala @@ -9,14 +9,13 @@ import units.client.engine.model.* import units.eth.EthAddress import units.{BlockHash, JobResult} -import java.util.concurrent.ThreadLocalRandom import scala.util.chaining.scalaUtilChainingOps class LoggedEngineApiClient(underlying: EngineApiClient) extends EngineApiClient { protected val log: LoggerFacade = LoggerFacade(LoggerFactory.getLogger(underlying.getClass)) - override def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = - wrap(s"forkChoiceUpdate($blockHash, f=$finalizedBlockHash)", underlying.forkChoiceUpdate(blockHash, finalizedBlockHash)) + override def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash, requestId: Int): JobResult[PayloadStatus] = + wrap(requestId, s"forkChoiceUpdate($blockHash, f=$finalizedBlockHash)", underlying.forkChoiceUpdate(blockHash, finalizedBlockHash, _)) override def forkChoiceUpdateWithPayloadId( lastBlockHash: BlockHash, @@ -24,50 +23,58 @@ class LoggedEngineApiClient(underlying: EngineApiClient) extends EngineApiClient unixEpochSeconds: Long, suggestedFeeRecipient: EthAddress, prevRandao: String, - withdrawals: Vector[Withdrawal] + withdrawals: Vector[Withdrawal], + requestId: Int ): JobResult[PayloadId] = wrap( + requestId, s"forkChoiceUpdateWithPayloadId(l=$lastBlockHash, f=$finalizedBlockHash, ts=$unixEpochSeconds, m=$suggestedFeeRecipient, " + s"r=$prevRandao, w={${withdrawals.mkString(", ")}}", - underlying.forkChoiceUpdateWithPayloadId(lastBlockHash, finalizedBlockHash, unixEpochSeconds, suggestedFeeRecipient, prevRandao, withdrawals) + underlying.forkChoiceUpdateWithPayloadId(lastBlockHash, finalizedBlockHash, unixEpochSeconds, suggestedFeeRecipient, prevRandao, withdrawals, _) ) - override def getPayload(payloadId: PayloadId): JobResult[JsObject] = - wrap(s"getPayload($payloadId)", underlying.getPayload(payloadId), filteredJson) + override def getPayload(payloadId: PayloadId, requestId: Int): JobResult[JsObject] = + wrap(requestId, s"getPayload($payloadId)", underlying.getPayload(payloadId, _), filteredJson) - override def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]] = - wrap(s"applyNewPayload(${filteredJson(payload)})", underlying.applyNewPayload(payload), _.fold("None")(_.toString)) + override def applyNewPayload(payload: JsObject, requestId: Int): JobResult[Option[BlockHash]] = + wrap(requestId, s"applyNewPayload(${filteredJson(payload)})", underlying.applyNewPayload(payload, _), _.fold("None")(_.toString)) - override def getPayloadBodyByHash(hash: BlockHash): JobResult[Option[JsObject]] = - wrap(s"getPayloadBodyByHash($hash)", underlying.getPayloadBodyByHash(hash), _.fold("None")(filteredJson)) + override def getPayloadBodyByHash(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] = + wrap(requestId, s"getPayloadBodyByHash($hash)", underlying.getPayloadBodyByHash(hash, _), _.fold("None")(filteredJson)) - override def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]] = - wrap(s"getBlockByNumber($number)", underlying.getBlockByNumber(number), _.fold("None")(_.toString)) + override def getBlockByNumber(number: BlockNumber, requestId: Int): JobResult[Option[EcBlock]] = + wrap(requestId, s"getBlockByNumber($number)", underlying.getBlockByNumber(number, _), _.fold("None")(_.toString)) - override def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]] = - wrap(s"getBlockByHash($hash)", underlying.getBlockByHash(hash), _.fold("None")(_.toString)) + override def getBlockByHash(hash: BlockHash, requestId: Int): JobResult[Option[EcBlock]] = + wrap(requestId, s"getBlockByHash($hash)", underlying.getBlockByHash(hash, _), _.fold("None")(_.toString)) - override def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]] = - wrap(s"getBlockByHashJson($hash)", underlying.getBlockByHashJson(hash), _.fold("None")(filteredJson)) + override def getBlockByHashJson(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] = + wrap(requestId, s"getBlockByHashJson($hash)", underlying.getBlockByHashJson(hash, _), _.fold("None")(filteredJson)) - override def getLastExecutionBlock: JobResult[EcBlock] = - wrap("getLastExecutionBlock", underlying.getLastExecutionBlock) + override def getLastExecutionBlock(requestId: Int): JobResult[EcBlock] = + wrap(requestId, "getLastExecutionBlock", underlying.getLastExecutionBlock) - override def blockExists(hash: BlockHash): JobResult[Boolean] = - wrap(s"blockExists($hash)", underlying.blockExists(hash)) + override def blockExists(hash: BlockHash, requestId: Int): JobResult[Boolean] = + wrap(requestId, s"blockExists($hash)", underlying.blockExists(hash, _)) - override def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] = - wrap(s"getLogs($hash, a=$address, t=$topic)", underlying.getLogs(hash, address, topic), _.view.map(_.data).mkString("{", ", ", "}")) + override def getLogs(hash: BlockHash, address: EthAddress, topic: String, requestId: Int): JobResult[List[GetLogsResponseEntry]] = + wrap(requestId, s"getLogs($hash, a=$address, t=$topic)", underlying.getLogs(hash, address, topic, _), _.view.map(_.data).mkString("{", ", ", "}")) - protected def wrap[R](method: String, f: => JobResult[R], toMsg: R => String = (_: R).toString): JobResult[R] = { - val currRequestId = ThreadLocalRandom.current().nextInt(10000, 100000).toString - log.debug(s"[$currRequestId] $method") + override def onRetry(requestId: Int): Unit = { + underlying.onRetry(requestId) + logDebug(requestId, "Retry") + } + + protected def wrap[R](requestId: Int, method: String, f: Int => JobResult[R], toMsg: R => String = (_: R).toString): JobResult[R] = { + logDebug(requestId, method) - f.tap { - case Left(e) => log.debug(s"[$currRequestId] Error: ${e.message}") - case Right(r) => log.debug(s"[$currRequestId] Success: ${toMsg(r)}") + f(requestId).tap { + case Left(e) => logDebug(requestId, s"Error: ${e.message}") + case Right(r) => logDebug(requestId, s"Success: ${toMsg(r)}") } } + protected def logDebug(requestId: Int, message: String): Unit = log.debug(s"[$requestId] $message") + private def filteredJson(jsObject: JsObject): String = JsObject( jsObject.fields.filterNot { case (k, _) => excludedJsonFields.contains(k) } ).toString() diff --git a/src/main/scala/units/client/engine/model/ForkChoiceUpdatedRequest.scala b/src/main/scala/units/client/engine/model/ForkChoiceUpdatedRequest.scala index 742257fe..06f39751 100644 --- a/src/main/scala/units/client/engine/model/ForkChoiceUpdatedRequest.scala +++ b/src/main/scala/units/client/engine/model/ForkChoiceUpdatedRequest.scala @@ -1,13 +1,13 @@ package units.client.engine.model +import play.api.libs.json.* import units.BlockHash import units.client.engine.model.ForkChoiceUpdatedRequest.ForkChoiceAttributes import units.eth.{EthAddress, EthereumConstants} import units.util.HexBytesConverter.* -import play.api.libs.json.* // https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#engine_forkchoiceupdatedv3 -case class ForkChoiceUpdatedRequest(lastBlockHash: BlockHash, finalizedBlockHash: BlockHash, attrs: Option[ForkChoiceAttributes]) +case class ForkChoiceUpdatedRequest(lastBlockHash: BlockHash, finalizedBlockHash: BlockHash, attrs: Option[ForkChoiceAttributes], id: Int) object ForkChoiceUpdatedRequest { case class ForkChoiceAttributes(unixEpochSeconds: Long, suggestedFeeRecipient: EthAddress, prevRandao: String, withdrawals: Vector[Withdrawal]) @@ -30,7 +30,7 @@ object ForkChoiceUpdatedRequest { ) .getOrElse[JsValue](JsNull) ), - "id" -> 1 + "id" -> o.id ) } } diff --git a/src/main/scala/units/client/engine/model/GetBlockByHashRequest.scala b/src/main/scala/units/client/engine/model/GetBlockByHashRequest.scala index c341e4ba..1a65b553 100644 --- a/src/main/scala/units/client/engine/model/GetBlockByHashRequest.scala +++ b/src/main/scala/units/client/engine/model/GetBlockByHashRequest.scala @@ -3,14 +3,14 @@ package units.client.engine.model import play.api.libs.json.{Json, Writes} import units.BlockHash -case class GetBlockByHashRequest(hash: BlockHash) +case class GetBlockByHashRequest(hash: BlockHash, id: Int) object GetBlockByHashRequest { implicit val writes: Writes[GetBlockByHashRequest] = (o: GetBlockByHashRequest) => { Json.obj( "jsonrpc" -> "2.0", "method" -> "eth_getBlockByHash", "params" -> Json.arr(o.hash, false), - "id" -> 1 + "id" -> o.id ) } } diff --git a/src/main/scala/units/client/engine/model/GetBlockByNumberRequest.scala b/src/main/scala/units/client/engine/model/GetBlockByNumberRequest.scala index f3fedd61..c4bbd4e4 100644 --- a/src/main/scala/units/client/engine/model/GetBlockByNumberRequest.scala +++ b/src/main/scala/units/client/engine/model/GetBlockByNumberRequest.scala @@ -2,14 +2,14 @@ package units.client.engine.model import play.api.libs.json.{Json, Writes} -case class GetBlockByNumberRequest(number: String) +case class GetBlockByNumberRequest(number: String, id: Int) object GetBlockByNumberRequest { implicit val writes: Writes[GetBlockByNumberRequest] = (o: GetBlockByNumberRequest) => { Json.obj( "jsonrpc" -> "2.0", "method" -> "eth_getBlockByNumber", "params" -> Json.arr(o.number, false), - "id" -> 1 + "id" -> o.id ) } } diff --git a/src/main/scala/units/client/engine/model/GetLogsRequest.scala b/src/main/scala/units/client/engine/model/GetLogsRequest.scala index 1d7fc759..8b37f668 100644 --- a/src/main/scala/units/client/engine/model/GetLogsRequest.scala +++ b/src/main/scala/units/client/engine/model/GetLogsRequest.scala @@ -9,7 +9,7 @@ import units.eth.EthAddress * @see * https://besu.hyperledger.org/stable/public-networks/reference/api#eth_getlogs */ -case class GetLogsRequest(hash: BlockHash, address: EthAddress, topics: List[String]) +case class GetLogsRequest(hash: BlockHash, address: EthAddress, topics: List[String], id: Int) object GetLogsRequest { implicit val writes: Writes[GetLogsRequest] = (o: GetLogsRequest) => { Json.obj( @@ -22,7 +22,7 @@ object GetLogsRequest { "topics" -> o.topics ) ), - "id" -> 1 + "id" -> o.id ) } } diff --git a/src/main/scala/units/client/engine/model/GetLogsResponseEntry.scala b/src/main/scala/units/client/engine/model/GetLogsResponseEntry.scala index 0cc0290d..edd4d198 100644 --- a/src/main/scala/units/client/engine/model/GetLogsResponseEntry.scala +++ b/src/main/scala/units/client/engine/model/GetLogsResponseEntry.scala @@ -8,8 +8,9 @@ import units.eth.EthAddress */ case class GetLogsResponseEntry( address: EthAddress, - data: String, // Bytes - topics: List[String] // TODO type + data: String, // Bytes + topics: List[String], // TODO type + transactionHash: String ) object GetLogsResponseEntry { diff --git a/src/main/scala/units/client/engine/model/GetPayloadBodyByHash.scala b/src/main/scala/units/client/engine/model/GetPayloadBodyByHash.scala index 04b456b1..81cc3607 100644 --- a/src/main/scala/units/client/engine/model/GetPayloadBodyByHash.scala +++ b/src/main/scala/units/client/engine/model/GetPayloadBodyByHash.scala @@ -1,9 +1,9 @@ package units.client.engine.model -import units.BlockHash import play.api.libs.json.{Json, Writes} +import units.BlockHash -case class GetPayloadBodyByHash(hash: BlockHash) +case class GetPayloadBodyByHash(hash: BlockHash, id: Int) object GetPayloadBodyByHash { implicit val writes: Writes[GetPayloadBodyByHash] = (o: GetPayloadBodyByHash) => { @@ -11,7 +11,7 @@ object GetPayloadBodyByHash { "jsonrpc" -> "2.0", "method" -> "engine_getPayloadBodiesByHashV1", "params" -> Json.arr(Json.arr(o.hash)), - "id" -> 1 + "id" -> o.id ) } } diff --git a/src/main/scala/units/client/engine/model/GetPayloadRequest.scala b/src/main/scala/units/client/engine/model/GetPayloadRequest.scala index 5b7973ca..c6e75e28 100644 --- a/src/main/scala/units/client/engine/model/GetPayloadRequest.scala +++ b/src/main/scala/units/client/engine/model/GetPayloadRequest.scala @@ -1,9 +1,9 @@ package units.client.engine.model -import units.client.engine.EngineApiClient.PayloadId import play.api.libs.json.{Json, Writes} +import units.client.engine.EngineApiClient.PayloadId -case class GetPayloadRequest(payloadId: PayloadId) +case class GetPayloadRequest(payloadId: PayloadId, id: Int) object GetPayloadRequest { implicit val writes: Writes[GetPayloadRequest] = (o: GetPayloadRequest) => { @@ -11,7 +11,7 @@ object GetPayloadRequest { "jsonrpc" -> "2.0", "method" -> "engine_getPayloadV3", "params" -> Json.arr(o.payloadId), - "id" -> 1 + "id" -> o.id ) } } diff --git a/src/main/scala/units/client/engine/model/NewPayloadRequest.scala b/src/main/scala/units/client/engine/model/NewPayloadRequest.scala index 6b4a0594..d9b25171 100644 --- a/src/main/scala/units/client/engine/model/NewPayloadRequest.scala +++ b/src/main/scala/units/client/engine/model/NewPayloadRequest.scala @@ -3,7 +3,7 @@ package units.client.engine.model import units.eth.EthereumConstants import play.api.libs.json.{JsObject, Json, Writes} -case class NewPayloadRequest(payload: JsObject) +case class NewPayloadRequest(payload: JsObject, id: Int) object NewPayloadRequest { implicit val writes: Writes[NewPayloadRequest] = (o: NewPayloadRequest) => { @@ -15,7 +15,7 @@ object NewPayloadRequest { Json.arr(), EthereumConstants.EmptyRootHashHex ), - "id" -> 1 + "id" -> o.id ) } } diff --git a/src/main/scala/units/client/package.scala b/src/main/scala/units/client/package.scala deleted file mode 100644 index c9994e08..00000000 --- a/src/main/scala/units/client/package.scala +++ /dev/null @@ -1,6 +0,0 @@ -package units - -package object client { - val RequestIdTag = "requestId" - val RetriesLeftTag = "retriesLeft" -} diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf index dc1e254c..7bb1c414 100644 --- a/src/test/resources/application.conf +++ b/src/test/resources/application.conf @@ -1,13 +1,20 @@ waves { wallet.seed = "2j8DJP266rEEfdDSfPNbSAqmxMfiyPySHWxCaqP2SHcen8hFbhF1KkqqFH" - l2 { +} + +units { + defaults { + chain-contract = "3MsD16zWCbJ4G7QnJizA9x7tM6yDHShoSYB" # Seed: chain-contract execution-client-address = "http://127.0.0.1:8551" + network = ${waves.network} + api-request-retries = 2 api-request-retry-wait-time = 2s block-delay = 6s block-sync-request-timeout = 500ms - chain-contract = "3MsD16zWCbJ4G7QnJizA9x7tM6yDHShoSYB" # Seed: chain-contract + mining-enable = false - network = ${waves.network} } + + chains = [ {} ] } diff --git a/src/test/scala/units/BaseIntegrationTestSuite.scala b/src/test/scala/units/BaseIntegrationTestSuite.scala index e6f07542..4acfd31a 100644 --- a/src/test/scala/units/BaseIntegrationTestSuite.scala +++ b/src/test/scala/units/BaseIntegrationTestSuite.scala @@ -36,15 +36,15 @@ trait BaseIntegrationTestSuite log.debug("EL init") val txs = List( - d.chainContract.setScript(), - d.chainContract.setup( + 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) } + settings.initialMiners.map { x => d.ChainContract.join(x.account, x.elRewardAddress) } d.appendBlock(txs*) d.advanceConsensusLayerChanged() @@ -108,5 +108,5 @@ trait BaseIntegrationTestSuite protected def step(name: String): Unit = log.info(s"========= $name =========") protected def getLogsResponseEntry(event: ElSentNativeEvent): GetLogsResponseEntry = - GetLogsResponseEntry(elBridgeAddress, Bridge.ElSentNativeEvent.encodeArgs(event), List(Bridge.ElSentNativeEventTopic)) + GetLogsResponseEntry(elBridgeAddress, Bridge.ElSentNativeEvent.encodeArgs(event), List(Bridge.ElSentNativeEventTopic), "") } diff --git a/src/test/scala/units/BlockFullValidationTestSuite.scala b/src/test/scala/units/BlockFullValidationTestSuite.scala index 643f029c..99c9a941 100644 --- a/src/test/scala/units/BlockFullValidationTestSuite.scala +++ b/src/test/scala/units/BlockFullValidationTestSuite.scala @@ -32,7 +32,7 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite { d.triggerScheduledTasks() step(s"Append a CL micro block with ecBlock ${ecBlock.hash} confirmation") - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock)) d.advanceConsensusLayerChanged() withClue("Validation doesn't happen:") { @@ -60,7 +60,7 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite { d.triggerScheduledTasks() step(s"Append a CL micro block with ecBlock ${ecBlock.hash} confirmation") - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() d.waitForCS[FollowingChain]("Following chain") { _ => } @@ -88,7 +88,7 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite { val ecBlock1 = d.createEcBlockBuilder("0", malfunction).buildAndSetLogs() d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBlock1)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(malfunction.account, ecBlock1)) d.advanceConsensusLayerChanged() step("Start new epoch for ecBlock2") @@ -98,7 +98,7 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite { val ecBlock2 = badBlockPostProcessing(d.createEcBlockBuilder("0-0", malfunction, ecBlock1).rewardPrevMiner().buildAndSetLogs(blockLogs)) step(s"Append a CL micro block with ecBlock2 ${ecBlock2.hash} confirmation") - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBlock2, e2CTransfersRootHashHex)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(malfunction.account, ecBlock2, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() step(s"Receive ecBlock2 ${ecBlock2.hash} from a peer") diff --git a/src/test/scala/units/BlockIssuesForgingTestSuite.scala b/src/test/scala/units/BlockIssuesForgingTestSuite.scala index 7cea6538..d4322398 100644 --- a/src/test/scala/units/BlockIssuesForgingTestSuite.scala +++ b/src/test/scala/units/BlockIssuesForgingTestSuite.scala @@ -5,7 +5,7 @@ import com.wavesplatform.transaction.TxHelpers import com.wavesplatform.wallet.Wallet import units.ELUpdater.State.ChainStatus.{FollowingChain, Mining, WaitForNewChain} import units.ELUpdater.WaitRequestedBlockTimeout -import units.client.contract.HasConsensusLayerDappTxHelpers.defaultFees +import units.client.contract.HasConsensusLayerDappTxHelpers.DefaultFees import units.client.engine.model.EcBlock import scala.concurrent.duration.DurationInt @@ -20,7 +20,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { override protected val defaultSettings: TestSettings = TestSettings.Default .copy( initialMiners = List(thisMiner, otherMiner1, otherMiner2), - additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, defaultFees.chainContract.withdrawFee)) + additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, DefaultFees.ChainContract.withdrawFee)) ) .withEnabledElMining @@ -30,7 +30,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { d.advanceNewBlocks(otherMiner1.address) val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs() d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBlock1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.lastBlock.hash shouldBe ecBlock1.hash } @@ -39,7 +39,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { d.advanceNewBlocks(otherMiner1.address) val ecBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs() val ecBlock2Epoch = d.blockchain.height - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock2)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBlock2)) d.waitForCS[FollowingChain](s"Waiting ecBlock2 ${ecBlock2.hash}") { s => s.nodeChainInfo.lastBlock.hash shouldBe ecBlock2.hash @@ -80,7 +80,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { d.advanceNewBlocks(otherMiner1.address) val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs() d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBlock1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true @@ -90,7 +90,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBadBlock2") d.advanceNewBlocks(otherMiner1.address) val ecBadBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs() - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBadBlock2)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBadBlock2)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true @@ -108,7 +108,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { s.chainSwitchInfo.referenceBlock.hash shouldBe ecBlock1.hash } - d.appendMicroBlockAndVerify(d.chainContract.startAltChain(otherMiner2.account, ecBlock2)) + d.appendMicroBlockAndVerify(d.ChainContract.startAltChain(otherMiner2.account, ecBlock2)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false s.nodeChainInfo.lastBlock.hash shouldBe ecBlock2.hash @@ -126,7 +126,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { d.advanceNewBlocks(otherMiner2.address) val ecBlock3 = d.createEcBlockBuilder("0-1-1", otherMiner2, parent = ecBlock2).rewardPrevMiner(1).buildAndSetLogs() val ecBlock3Epoch = d.blockchain.height - d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, ecBlock3, chainId = 1)) + d.appendMicroBlockAndVerify(d.ChainContract.extendAltChain(otherMiner2.account, ecBlock3, chainId = 1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false @@ -159,7 +159,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { d.advanceNewBlocks(otherMiner1.address) val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs() d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBlock1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true @@ -169,7 +169,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBadBlock2") d.advanceNewBlocks(otherMiner1.address) val ecBadBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs() - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBadBlock2)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBadBlock2)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true @@ -198,7 +198,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { step(s"Continue an alternative chain by otherMiner2 ${otherMiner2.address} with ecBadBlock3") d.advanceNewBlocks(otherMiner2.address) val ecBadBlock3 = d.createEcBlockBuilder("0-1-1", otherMiner2, ecBlock2).rewardMiner(otherMiner2.elRewardAddress, 1).buildAndSetLogs() - d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, ecBadBlock3, chainId = 1)) + d.appendMicroBlockAndVerify(d.ChainContract.extendAltChain(otherMiner2.account, ecBadBlock3, chainId = 1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false @@ -225,7 +225,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { d.advanceNewBlocks(otherMiner1.address) val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs() d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBlock1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true @@ -235,7 +235,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBadBlock2") d.advanceNewBlocks(otherMiner1.address) val ecBadBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs() - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBadBlock2)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBadBlock2)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe true @@ -250,7 +250,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { s.chainSwitchInfo.referenceBlock.hash shouldBe ecBlock1.hash } - d.appendMicroBlockAndVerify(d.chainContract.startAltChain(otherMiner2.account, ecBlock2)) + d.appendMicroBlockAndVerify(d.ChainContract.startAltChain(otherMiner2.account, ecBlock2)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false @@ -268,7 +268,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite { step(s"Continue an alternative chain by otherMiner2 ${otherMiner2.address} with ecBlock3") d.advanceNewBlocks(otherMiner2.address) val ecBlock3 = d.createEcBlockBuilder("0-1-1", otherMiner2, ecBlock2).rewardPrevMiner(1).buildAndSetLogs() - d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, ecBlock3, chainId = 1)) + d.appendMicroBlockAndVerify(d.ChainContract.extendAltChain(otherMiner2.account, ecBlock3, chainId = 1)) d.waitForCS[FollowingChain]() { s => s.nodeChainInfo.isMain shouldBe false diff --git a/src/test/scala/units/C2ETransfersTestSuite.scala b/src/test/scala/units/C2ETransfersTestSuite.scala index cc9b68dd..5840f43e 100644 --- a/src/test/scala/units/C2ETransfersTestSuite.scala +++ b/src/test/scala/units/C2ETransfersTestSuite.scala @@ -68,6 +68,6 @@ class C2ETransfersTestSuite extends BaseIntegrationTestSuite { ) if (queueSize > 0) d.appendMicroBlock(TxHelpers.data(d.chainContractAccount, Seq(IntegerDataEntry("nativeTransfersCount", queueSize)))) - d.appendMicroBlockE(d.chainContract.transferUnsafe(transferSenderAccount, destElAddressHex, tokenId.getOrElse(d.token), transferAmount)) + d.appendMicroBlockE(d.ChainContract.transferUnsafe(transferSenderAccount, destElAddressHex, tokenId.getOrElse(d.token), transferAmount)) } } diff --git a/src/test/scala/units/E2CTransfersTestSuite.scala b/src/test/scala/units/E2CTransfersTestSuite.scala index 6881ccf5..cefac22f 100644 --- a/src/test/scala/units/E2CTransfersTestSuite.scala +++ b/src/test/scala/units/E2CTransfersTestSuite.scala @@ -11,7 +11,7 @@ import org.web3j.abi.TypeReference import org.web3j.abi.datatypes.Event import org.web3j.abi.datatypes.generated.Bytes20 import units.ELUpdater.State.ChainStatus.{Mining, WaitForNewChain} -import units.client.contract.HasConsensusLayerDappTxHelpers.defaultFees +import units.client.contract.HasConsensusLayerDappTxHelpers.DefaultFees import units.eth.EthAddress import units.util.HexBytesConverter @@ -30,7 +30,7 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { override protected val defaultSettings: TestSettings = TestSettings.Default.copy( initialMiners = List(reliable), - additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, defaultFees.chainContract.withdrawFee)) + additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, DefaultFees.ChainContract.withdrawFee)) ) "Multiple withdrawals" in { @@ -46,8 +46,8 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { val settings = defaultSettings.copy( additionalBalances = List( - AddrWithBalance(transferReceiver1.toAddress, defaultFees.chainContract.withdrawFee), - AddrWithBalance(transferReceiver2.toAddress, defaultFees.chainContract.withdrawFee) + AddrWithBalance(transferReceiver1.toAddress, DefaultFees.ChainContract.withdrawFee), + AddrWithBalance(transferReceiver2.toAddress, DefaultFees.ChainContract.withdrawFee) ) ) @@ -57,15 +57,15 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) def tryWithdraw(): Either[Throwable, BlockId] = d.appendMicroBlockE( - d.chainContract.withdraw(transferReceiver1, ecBlock, transfer1Proofs, 0, transfer1.amount), - d.chainContract.withdraw(transferReceiver2, ecBlock, transfer2Proofs, 1, transfer2.amount) + d.ChainContract.withdraw(transferReceiver1, ecBlock, transfer1Proofs, 0, transfer1.amount), + d.ChainContract.withdraw(transferReceiver2, ecBlock, transfer2Proofs, 1, transfer2.amount) ) tryWithdraw() should produce("not found for the contract address") step("Append a CL micro block with ecBlock confirmation") d.ecClients.addKnown(ecBlock) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() tryWithdraw() should beRight @@ -92,48 +92,44 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { d.advanceNewBlocks(reliable.address) val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) d.ecClients.addKnown(ecBlock) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, index, transfer.amount)) + d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock, transferProofs, index, transfer.amount)) tryWithdraw() should produce(errorMessage) } } - "Deny withdrawals with a non-positive amount" in forAll( - Table( - "index", - 0L, - Long.MinValue - ) - ) { amount => - withExtensionDomain() { d => - step(s"Start new epoch with ecBlock") - d.advanceNewBlocks(reliable.address) - val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) - d.ecClients.addKnown(ecBlock) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) - d.advanceConsensusLayerChanged() + private def wrongAmountTest(amount: Long): Unit = withExtensionDomain() { d => + step("Start new epoch with ecBlock") + d.advanceNewBlocks(reliable.address) + val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) + d.ecClients.addKnown(ecBlock) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + d.advanceConsensusLayerChanged() - def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, amount)) + def tryWithdraw(): Either[Throwable, BlockId] = + d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, amount)) - tryWithdraw() should produce("Amount should be positive") - } + tryWithdraw() should produce("Amount should be positive") } + "L2-360 Deny negative amount" in wrongAmountTest(Long.MinValue) + + "Deny withdrawals with invalid amount" in forAll(Table("index", 0L, transfer.amount - 1))(wrongAmountTest) + "Can't get transferred tokens if the data is incorrect and able if it is correct" in withExtensionDomain() { d => step(s"Start new epoch with ecBlock") d.advanceNewBlocks(reliable.address) val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, transfer.amount)) + d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, transfer.amount)) tryWithdraw() should produce("not found for the contract address") d.ecClients.addKnown(ecBlock) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() tryWithdraw() should beRight @@ -143,9 +139,9 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { } } - "Can't get transferred tokens twice" in { + "L2-273 Can't get transferred tokens twice" in { val settings = defaultSettings.copy( - additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, defaultFees.chainContract.withdrawFee * 2)) + additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, DefaultFees.ChainContract.withdrawFee * 2)) ) withExtensionDomain(settings) { d => @@ -153,11 +149,11 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { d.advanceNewBlocks(reliable.address) val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs) def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, transfer.amount)) + d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, transfer.amount)) tryWithdraw() should produce("not found for the contract address") d.ecClients.addKnown(ecBlock) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex)) d.advanceConsensusLayerChanged() tryWithdraw() should beRight @@ -184,9 +180,9 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { withExtensionDomain(settings) { d => step(s"Start new epoch with ecBlock1") d.advanceNewBlocks(reliable.address) - val ecBlock1 = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(List(transferEvent)) + val ecBlock1 = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(List(transferEvent)) def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock1, transferProofs, 0, transfer.amount)) + d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock1, transferProofs, 0, transfer.amount)) d.ecClients.willForge(ecBlock1) d.advanceConsensusLayerChanged() @@ -209,7 +205,7 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { withExtensionDomain(settings) { d => step(s"Start new epoch with ecBlock1") d.advanceNewBlocks(reliable.address) - val ecBlock1 = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(List(transferEvent.copy(data = "d3ad884fa04292"))) + val ecBlock1 = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(List(transferEvent.copy(data = "d3ad884fa04292"))) d.ecClients.willForge(ecBlock1) d.ecClients.willForge(d.createEcBlockBuilder("0-0", reliable).build()) @@ -230,7 +226,7 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { d.advanceConsensusLayerChanged() d.ecClients.addKnown(ecBlock1) - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBlock1)) + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(malfunction.account, ecBlock1)) d.advanceConsensusLayerChanged() step(s"Try to append a block with a wrong transfers root hash") @@ -239,7 +235,7 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { d.advanceConsensusLayerChanged() // No root hash in extendMainChain tx - d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBadBlock2)) // No root hash + d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(malfunction.account, ecBadBlock2)) // No root hash d.receiveNetworkBlock(ecBadBlock2, malfunction.account) d.advanceConsensusLayerChanged() @@ -266,15 +262,15 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { step(s"Confirm startAltChain and append with new blocks and remove a malfunction miner") d.appendMicroBlockAndVerify( - d.chainContract.startAltChain(reliable.account, ecBlock2, e2CTransfersRootHashHex), - d.chainContract.leave(malfunction.account) + d.ChainContract.startAltChain(reliable.account, ecBlock2, e2CTransfersRootHashHex), + d.ChainContract.leave(malfunction.account) ) d.advanceConsensusLayerChanged() d.waitForCS[Mining]("State is expected") { _ => } def tryWithdraw(): Either[Throwable, BlockId] = - d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock2, transferProofs, 0, transfer.amount)) + d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock2, transferProofs, 0, transfer.amount)) withClue("Can't withdraw from a fork:") { tryWithdraw() should produce("is not finalized") } @@ -288,7 +284,7 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite { step("Confirm extendAltChain to make this chain main") d.advanceMining() - d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(reliable.account, ecBlock3, chainId = 1)) + d.appendMicroBlockAndVerify(d.ChainContract.extendAltChain(reliable.account, ecBlock3, chainId = 1)) d.advanceConsensusLayerChanged() d.waitForCS[Mining]("State is expected") { _ => } diff --git a/src/test/scala/units/ExtensionDomain.scala b/src/test/scala/units/ExtensionDomain.scala index a9174658..3c9eeeed 100644 --- a/src/test/scala/units/ExtensionDomain.scala +++ b/src/test/scala/units/ExtensionDomain.scala @@ -67,7 +67,7 @@ class ExtensionDomain( with ScorexLogging { self => override val chainContractAccount: KeyPair = KeyPair("chain-contract".getBytes(StandardCharsets.UTF_8)) - val l2Config = settings.config.as[ClientConfig]("waves.l2") + val l2Config = settings.config.as[ClientConfig]("units.defaults") require(l2Config.chainContractAddress == chainContractAddress, "Check settings") val ecGenesisBlock = EcBlock( diff --git a/src/test/scala/units/TestSettings.scala b/src/test/scala/units/TestSettings.scala index 80e3ef52..6aa28955 100644 --- a/src/test/scala/units/TestSettings.scala +++ b/src/test/scala/units/TestSettings.scala @@ -29,7 +29,7 @@ object TestSettings { private object Waves { val Default = DomainPresets.TransactionStateSnapshot - val WithMining = Default.copy(config = ConfigFactory.parseString("waves.l2.mining-enable = true").withFallback(Default.config)) + val WithMining = Default.copy(config = ConfigFactory.parseString("units.defaults.mining-enable = true").withFallback(Default.config)) } } diff --git a/src/test/scala/units/client/TestEcClients.scala b/src/test/scala/units/client/TestEcClients.scala index 53ad627b..0225f1b6 100644 --- a/src/test/scala/units/client/TestEcClients.scala +++ b/src/test/scala/units/client/TestEcClients.scala @@ -52,7 +52,7 @@ class TestEcClients private ( private val logs = Atomic(Map.empty[GetLogsRequest, List[GetLogsResponseEntry]]) def setBlockLogs(hash: BlockHash, address: EthAddress, topic: String, blockLogs: List[GetLogsResponseEntry]): Unit = { - val request = GetLogsRequest(hash, address, List(topic)) + val request = GetLogsRequest(hash, address, List(topic), 0) logs.transform(_.updated(request, blockLogs)) } @@ -64,7 +64,7 @@ class TestEcClients private ( val engineApi = new LoggedEngineApiClient( new EngineApiClient { - override def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = { + override def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash, requestId: Int): JobResult[PayloadStatus] = { knownBlocks.get().get(blockHash) match { case Some(cid) => currChainIdValue.set(cid) @@ -85,7 +85,8 @@ class TestEcClients private ( unixEpochSeconds: Long, suggestedFeeRecipient: EthAddress, prevRandao: String, - withdrawals: Vector[Withdrawal] + withdrawals: Vector[Withdrawal], + requestId: Int ): JobResult[PayloadId] = forgingBlocks .get() @@ -98,7 +99,7 @@ class TestEcClients private ( fb.payloadId.asRight } - override def getPayload(payloadId: PayloadId): JobResult[JsObject] = + override def getPayload(payloadId: PayloadId, requestId: Int): JobResult[JsObject] = forgingBlocks.transformAndExtract(_.withoutFirst { fb => fb.payloadId == payloadId }) match { case Some(fb) => TestEcBlocks.toPayload(fb.testBlock, fb.testBlock.prevRandao).asRight case None => @@ -107,7 +108,7 @@ class TestEcClients private ( ) } - override def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]] = { + override def applyNewPayload(payload: JsObject, requestId: Int): JobResult[Option[BlockHash]] = { val newBlock = NetworkL2Block(payload).explicitGet().toEcBlock knownBlocks.get().get(newBlock.parentHash) match { case Some(cid) => @@ -123,21 +124,21 @@ class TestEcClients private ( knownBlocks.transform(_.updated(newBlock.hash, newCid)) } - case None => throw notImplementedCase(s"Can't find a parent block ${newBlock.parentHash} for ${newBlock.hash}") + case None => notImplementedCase(s"Can't find a parent block ${newBlock.parentHash} for ${newBlock.hash}") } Some(newBlock.hash) }.asRight - override def getPayloadBodyByHash(hash: BlockHash): JobResult[Option[JsObject]] = + override def getPayloadBodyByHash(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] = getBlockByHashJson(hash) - override def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]] = + override def getBlockByNumber(number: BlockNumber, requestId: Int): JobResult[Option[EcBlock]] = number match { case BlockNumber.Latest => currChain.headOption.asRight case BlockNumber.Number(n) => currChain.find(_.height == n).asRight } - override def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]] = { + override def getBlockByHash(hash: BlockHash, requestId: Int): JobResult[Option[EcBlock]] = { for { cid <- knownBlocks.get().get(hash) c <- chains.get().get(cid) @@ -145,23 +146,30 @@ class TestEcClients private ( } yield b }.asRight - override def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]] = + override def getBlockByHashJson(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] = notImplementedMethodJob("getBlockByHashJson") - override def getLastExecutionBlock: JobResult[EcBlock] = currChain.head.asRight + override def getLastExecutionBlock(requestId: Int): JobResult[EcBlock] = currChain.head.asRight - override def blockExists(hash: BlockHash): JobResult[Boolean] = notImplementedMethodJob("blockExists") + override def blockExists(hash: BlockHash, requestId: Int): JobResult[Boolean] = notImplementedMethodJob("blockExists") - override def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] = { - val request = GetLogsRequest(hash, address, List(topic)) + override def getLogs(hash: BlockHash, address: EthAddress, topic: String, requestId: Int): JobResult[List[GetLogsResponseEntry]] = { + val request = GetLogsRequest(hash, address, List(topic), 0) // requestId is ignored, see setBlockLogs getLogsCalls.transform(_ + hash) - logs.get().getOrElse(request, throw notImplementedCase("call setBlockLogs")) + logs.get().getOrElse(request, notImplementedCase("call setBlockLogs")) }.asRight } ) - protected def notImplementedMethodJob[A](text: String): JobResult[A] = throw new NotImplementedMethod(text) - protected def notImplementedCase(text: String): Throwable = new NotImplementedCase(text) + protected def notImplementedMethodJob[A](text: String): JobResult[A] = { + log.warn(s"notImplementedMethodJob($text)") + throw new NotImplementedMethod(text) + } + + protected def notImplementedCase(text: String): Nothing = { + log.warn(s"notImplementedCase($text)") + throw new NotImplementedCase(text) + } } object TestEcClients { diff --git a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala index df79ac99..c0c67fc7 100644 --- a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala +++ b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala @@ -7,11 +7,13 @@ import com.wavesplatform.common.state.ByteStr import com.wavesplatform.common.utils.EitherExt2 import com.wavesplatform.lang.v1.compiler.Terms 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 units.client.L2BlockLike import units.client.contract.HasConsensusLayerDappTxHelpers.* -import units.client.contract.HasConsensusLayerDappTxHelpers.defaultFees.chainContract.* +import units.client.contract.HasConsensusLayerDappTxHelpers.DefaultFees.ChainContract.* import units.eth.{EthAddress, EthereumConstants} trait HasConsensusLayerDappTxHelpers { @@ -20,10 +22,16 @@ trait HasConsensusLayerDappTxHelpers { def chainContractAccount: KeyPair lazy val chainContractAddress: Address = chainContractAccount.toAddress - object chainContract { + object ChainContract { def setScript(): SetScriptTransaction = TxHelpers.setScript(chainContractAccount, CompiledChainContract.script, fee = setScriptFee) - def setup(genesisBlock: L2BlockLike, elMinerReward: Long, daoAddress: Option[Address], daoReward: Long): InvokeScriptTransaction = TxHelpers.invoke( + def setup( + genesisBlock: L2BlockLike, + elMinerReward: Long, + daoAddress: Option[Address], + daoReward: Long, + invoker: KeyPair = defaultSigner + ): InvokeScriptTransaction = TxHelpers.invoke( dApp = chainContractAddress, func = "setup".some, args = List( @@ -32,7 +40,8 @@ trait HasConsensusLayerDappTxHelpers { Terms.CONST_STRING(daoAddress.fold("")(_.toString)).explicitGet(), Terms.CONST_LONG(daoReward) ), - fee = setupFee + fee = setupFee, + invoker = invoker ) def join(minerAccount: KeyPair, elRewardAddress: EthAddress): InvokeScriptTransaction = TxHelpers.invoke( @@ -71,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, @@ -171,13 +202,21 @@ trait HasConsensusLayerDappTxHelpers { merkleProof: Seq[Digest], transferIndexInBlock: Int, amount: Long + ): InvokeScriptTransaction = withdraw(sender, block.hash, merkleProof, transferIndexInBlock, amount) + + def withdraw( + sender: KeyPair, + blockHash: BlockHash, + merkleProof: Seq[Digest], + transferIndexInBlock: Int, + amount: Long ): InvokeScriptTransaction = TxHelpers.invoke( invoker = sender, dApp = chainContractAddress, func = "withdraw".some, args = List( - Terms.CONST_STRING(block.hash.drop(2)).explicitGet(), + Terms.CONST_STRING(blockHash.drop(2)).explicitGet(), Terms.ARR(merkleProof.map[Terms.EVALUATED](x => Terms.CONST_BYTESTR(ByteStr(x)).explicitGet()).toVector, limited = false).explicitGet(), Terms.CONST_LONG(transferIndexInBlock), Terms.CONST_LONG(amount) @@ -190,8 +229,8 @@ trait HasConsensusLayerDappTxHelpers { object HasConsensusLayerDappTxHelpers { val EmptyE2CTransfersRootHashHex = EthereumConstants.NullHex - object defaultFees { - object chainContract { + object DefaultFees { + object ChainContract { val setScriptFee = 0.05.waves val setupFee = 2.waves val joinFee = 0.1.waves