From 75fc16d8fa6e319a5c10ea05717a4b17196b345a Mon Sep 17 00:00:00 2001 From: Vyatcheslav Suharnikov Date: Mon, 25 Nov 2024 16:04:18 +0400 Subject: [PATCH 1/9] op-geth support WIP --- .../com/wavesplatform/api/NodeHttpApi.scala | 2 +- .../scala/units/BaseDockerTestSuite.scala | 7 +- .../test/scala/units/BridgeE2CTestSuite.scala | 13 +++- .../scala/units/docker/DockerImages.scala | 7 +- .../scala/units/docker/OpGethContainer.scala | 69 +++++++++++++++++++ .../test/scala/units/el/ElBridgeClient.scala | 51 +++++++++----- .../scala/units/test/TestEnvironment.scala | 2 +- local-network/configs/besu/run-besu.sh | 2 +- local-network/configs/geth/run-geth.sh | 2 +- local-network/configs/op-geth/op-geth.yml | 15 ++++ local-network/configs/op-geth/run-op-geth.sh | 47 +++++++++++++ 11 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 consensus-client-it/src/test/scala/units/docker/OpGethContainer.scala create mode 100644 local-network/configs/op-geth/op-geth.yml create mode 100755 local-network/configs/op-geth/run-op-geth.sh diff --git a/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala b/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala index bbae09a6..eba66b9e 100644 --- a/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala +++ b/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala @@ -52,7 +52,7 @@ class NodeHttpApi(apiUri: Uri, backend: SttpBackend[Identity, ?], apiKeyValue: S if (currHeight >= atLeast) currHeight else { Thread.sleep(patienceConfig.interval.toMillis) - val waitBlocks = (atLeast - currHeight).min(1) + val waitBlocks = (atLeast - currHeight).max(1) eventually(timeout(MaxBlockDelay * waitBlocks)) { val h = height()(subsequentLoggingOptions) h should be >= atLeast diff --git a/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala index 2cce8fb5..13725174 100644 --- a/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala @@ -42,9 +42,10 @@ trait BaseDockerTestSuite 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") + case "besu" => new BesuContainer(_, _, _) + case "geth" => new GethContainer(_, _, _) + case "op-geth" => new OpGethContainer(_, _, _) + 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 */ ) diff --git a/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala b/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala index c0221452..32ef42ff 100644 --- a/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala @@ -8,14 +8,23 @@ import org.web3j.protocol.exceptions.TransactionException import org.web3j.utils.Convert import units.el.ElBridgeClient +import scala.jdk.OptionConverters.RichOptional + 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 def sendNative(amount: BigInt = UnitsConvert.toWei(userAmount)): TransactionReceipt = { + val txnResult = elBridge.sendNative(elSender, clRecipient.toAddress, amount) + val r = eventually { + ec1.web3j.ethGetTransactionReceipt(txnResult.getTransactionHash).send().getTransactionReceipt.toScala.value + } + + if (!r.isStatusOK) fail(s"Expected successful sendNative, got: ${ElBridgeClient.decodeRevertReason(r.getRevertReason)}") + r + } private val tenGwei = BigInt(Convert.toWei("10", Convert.Unit.GWEI).toBigIntegerExact) diff --git a/consensus-client-it/src/test/scala/units/docker/DockerImages.scala b/consensus-client-it/src/test/scala/units/docker/DockerImages.scala index 3806cfa5..8a39bcd2 100644 --- a/consensus-client-it/src/test/scala/units/docker/DockerImages.scala +++ b/consensus-client-it/src/test/scala/units/docker/DockerImages.scala @@ -4,7 +4,8 @@ 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") + val WavesNode = parse(WavesDockerImage) + val BesuExecutionClient = parse("hyperledger/besu:latest") + val GethExecutionClient = parse("ethereum/client-go:stable") + val OpGethExecutionClient = parse("us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth:v1.101411.2") } diff --git a/consensus-client-it/src/test/scala/units/docker/OpGethContainer.scala b/consensus-client-it/src/test/scala/units/docker/OpGethContainer.scala new file mode 100644 index 00000000..f245e294 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/docker/OpGethContainer.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 OpGethContainer(network: NetworkImpl, number: Int, ip: String)(implicit httpClientBackend: SttpBackend[Identity, Any]) + extends EcContainer(number) { + protected override val container = new GenericContainer(DockerImages.OpGethExecutionClient) + .withNetwork(network) + .withExposedPorts(RpcPort, EnginePort) + .withFileSystemBind(s"$ConfigsDir/ec-common/genesis.json", "/tmp/genesis.json", BindMode.READ_ONLY) + .withFileSystemBind(s"$ConfigsDir/op-geth/run-op-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/el/ElBridgeClient.scala b/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala index a75be3b3..1f2f6294 100644 --- a/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala +++ b/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala @@ -4,11 +4,14 @@ 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.crypto.{Credentials, RawTransaction} import org.web3j.protocol.Web3j -import org.web3j.protocol.core.methods.response.TransactionReceipt +import org.web3j.protocol.core.DefaultBlockParameterName +import org.web3j.protocol.core.methods.response.EthSendTransaction +import org.web3j.tx.RawTransactionManager import org.web3j.tx.gas.DefaultGasProvider import units.bridge.BridgeContract +import units.docker.EcContainer import units.eth.EthAddress import java.util.Collections @@ -18,29 +21,45 @@ class ElBridgeClient(web3j: Web3j, address: EthAddress) extends ScorexLogging { sender: Credentials, recipient: Address, amountInEther: BigInt - ): TransactionReceipt = { + ): EthSendTransaction = { 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() + val txnManager = new RawTransactionManager(web3j, sender, EcContainer.ChainId) + val gasProvider = new DefaultGasProvider + val bridgeContract = BridgeContract.load(address.hex, web3j, txnManager, gasProvider) + val funcCall = bridgeContract.send_sendNative(recipient.publicKeyHash, amountInEther.bigInteger).encodeFunctionCall() + + val nonce = web3j.ethGetTransactionCount(address.hex, DefaultBlockParameterName.LATEST).send().getTransactionCount + val rawTxn = RawTransaction.createTransaction( + nonce, + gasProvider.getGasPrice, + gasProvider.getGasLimit, + address.hex, + amountInEther.bigInteger, + funcCall + ) + + txnManager.signAndSend(rawTxn) } } 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) + def decodeRevertReason(hexRevert: String): String = + if (Option(hexRevert).isEmpty) "???" + else { + 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") + 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 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] - } + 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/test/TestEnvironment.scala b/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala index f5ef1377..e8043130 100644 --- a/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala +++ b/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala @@ -8,5 +8,5 @@ object TestEnvironment { Files.createDirectories(DefaultLogsDir) val WavesDockerImage: String = System.getProperty("cc.it.docker.image") - val ExecutionClient: String = System.getProperty("cc.it.ec", "besu") // | geth + val ExecutionClient: String = System.getProperty("cc.it.ec", "op-geth") // besu | geth | op-geth } diff --git a/local-network/configs/besu/run-besu.sh b/local-network/configs/besu/run-besu.sh index 36c69169..935ea88f 100755 --- a/local-network/configs/besu/run-besu.sh +++ b/local-network/configs/besu/run-besu.sh @@ -7,6 +7,6 @@ IP: $IP EOF # --p2p-host="ec-1" # Doesn't work: https://github.com/hyperledger/besu/issues/4380 -besu \ +exec besu \ --config-file=/config/besu.conf \ --p2p-host=${IP} diff --git a/local-network/configs/geth/run-geth.sh b/local-network/configs/geth/run-geth.sh index b608afd0..f392e03c 100755 --- a/local-network/configs/geth/run-geth.sh +++ b/local-network/configs/geth/run-geth.sh @@ -16,7 +16,7 @@ PREFIX: ${PREFIX} EOF # --syncmode full, because default "snap" mode and starting concurrently with ec-1 cause a stopped sync -geth \ +exec geth \ --http \ --http.addr=0.0.0.0 \ --http.vhosts=* \ diff --git a/local-network/configs/op-geth/op-geth.yml b/local-network/configs/op-geth/op-geth.yml new file mode 100644 index 00000000..b7e32b44 --- /dev/null +++ b/local-network/configs/op-geth/op-geth.yml @@ -0,0 +1,15 @@ +services: + op-geth: + image: ethereum/client-go:stable + stop_grace_period: 10s + entrypoint: /tmp/run.sh + volumes: + - ../ec-common/genesis.json:/tmp/genesis.json:ro + - ./run-op-geth.sh:/tmp/run.sh:ro + env_file: + - ../ec-common/peers.env + healthcheck: + test: 'wget -qO /dev/null --header "content-type: application/json" --post-data {\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"params\":[],\"id\":1} http://127.0.0.1:8545' + interval: 5s + timeout: 1s + retries: 10 diff --git a/local-network/configs/op-geth/run-op-geth.sh b/local-network/configs/op-geth/run-op-geth.sh new file mode 100755 index 00000000..8432efc8 --- /dev/null +++ b/local-network/configs/op-geth/run-op-geth.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env sh + +if [ ! -d /root/.ethereum/geth ] ; then + geth init /tmp/genesis.json 2>&1 | tee /root/logs/init.log +fi + +IP_RAW=$(ip -4 addr show dev eth0 | awk '/inet / {print $2}') +IP=$(echo "$IP_RAW" | cut -d/ -f1) +NETWORK=$(echo "$IP_RAW" | xargs ipcalc -n | awk -F= '{print $2}') +PREFIX=$(echo "$IP_RAW" | xargs ipcalc -p | awk -F= '{print $2}') + +tee /root/logs/log < Date: Tue, 26 Nov 2024 10:17:58 +0400 Subject: [PATCH 2/9] BaseE2CTestSuite: supports geth and op-geth --- .../scala/units/BaseDockerTestSuite.scala | 3 +- .../test/scala/units/BridgeE2CTestSuite.scala | 166 ++++++++---------- .../test/scala/units/el/ElBridgeClient.scala | 45 ++++- .../scala/units/test/TestEnvironment.scala | 2 +- 4 files changed, 115 insertions(+), 101 deletions(-) diff --git a/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala index 13725174..16487fa0 100644 --- a/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala @@ -41,11 +41,10 @@ trait BaseDockerTestSuite private implicit val httpClientBackend: SttpBackend[Identity, Any] = new LoggingBackend(HttpClientSyncBackend()) protected lazy val ec1: EcContainer = { - val constructor = TestEnvironment.ExecutionClient match { + val constructor = TestEnvironment.ExecutionClient.getOrElse("op-geth") match { case "besu" => new BesuContainer(_, _, _) case "geth" => new GethContainer(_, _, _) case "op-geth" => new OpGethContainer(_, _, _) - 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 */ ) diff --git a/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala b/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala index 32ef42ff..83c739dd 100644 --- a/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala @@ -4,7 +4,6 @@ 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 @@ -16,121 +15,110 @@ class BridgeE2CTestSuite extends BaseDockerTestSuite { private val userAmount = 1 private val wavesAmount = UnitsConvert.toWavesAmount(userAmount) - private def sendNative(amount: BigInt = UnitsConvert.toWei(userAmount)): TransactionReceipt = { - val txnResult = elBridge.sendNative(elSender, clRecipient.toAddress, amount) - val r = eventually { - ec1.web3j.ethGetTransactionReceipt(txnResult.getTransactionHash).send().getTransactionReceipt.toScala.value - } - - if (!r.isStatusOK) fail(s"Expected successful sendNative, got: ${ElBridgeClient.decodeRevertReason(r.getRevertReason)}") - r - } - 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" + def test(amount: BigInt, expectedError: String): Unit = { + val e = elBridge.callRevertedSendNative(elSender, clRecipient.toAddress, amount) + e should include(expectedError) } + "L2-264 Amount should % 10 Gwei" in test(tenGwei + 1, "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" + test(1, "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" + val maxAmountInWei = BigInt(Long.MaxValue) * tenGwei + val biggerAmount = (maxAmountInWei / tenGwei + 1) * tenGwei + test(biggerAmount, 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) + "Positive" - { + def sendNative(amount: BigInt = UnitsConvert.toWei(userAmount)): TransactionReceipt = { + val txnResult = elBridge.sendSendNative(elSender, clRecipient.toAddress, amount) + eventually { + val r = ec1.web3j.ethGetTransactionReceipt(txnResult.getTransactionHash).send().getTransactionReceipt.toScala.value + if (!r.isStatusOK) fail(s"Expected successful sendNative, got: ${ElBridgeClient.decodeRevertReason(r.getRevertReason)}") + r + } + } - burnedTokensAfter shouldBe (transferAmount + burnedTokensBefore) - } + "L2-325 Sent tokens burned" in { + def burnedTokens = ec1.web3j.ethGetBalance(ElBridgeClient.BurnAddress.hex, DefaultBlockParameterName.LATEST).send().getBalance + val burnedTokensBefore = BigInt(burnedTokens) - "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() + val transferAmount = tenGwei + sendNative(transferAmount) + val burnedTokensAfter = BigInt(burnedTokens) - withClue("1. The balance of Bridge contract wasn't changed: ") { - val bridgeBalanceAfter = bridgeBalance - bridgeBalanceAfter shouldBe bridgeBalanceBefore + burnedTokensAfter shouldBe (transferAmount + burnedTokensBefore) } - val blockHash = BlockHash(sendTxnReceipt.getBlockHash) - step(s"Block with transaction: $blockHash") + "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 logsInBlock = ec1.engineApi.getLogs(blockHash, elBridgeAddress, Bridge.ElSentNativeEventTopic).explicitGet() + // TODO block can be changed - val transferEvents = logsInBlock.map { x => - Bridge.ElSentNativeEvent.decodeArgs(x.data).explicitGet() - } - step(s"Transfer events: ${transferEvents.mkString(", ")}") + val blockHash = BlockHash(sendTxnReceipt.getBlockHash) + step(s"Block with transaction: $blockHash") - val sendTxnLogIndex = logsInBlock.indexWhere(_.transactionHash == sendTxnReceipt.getTransactionHash) - val transferProofs = Bridge.mkTransferProofs(transferEvents, sendTxnLogIndex).reverse + val logsInBlock = ec1.engineApi.getLogs(blockHash, elBridgeAddress, Bridge.ElSentNativeEventTopic).explicitGet() - step(s"Wait block $blockHash on contract") - val blockConfirmationHeight = eventually { - chainContract.getBlock(blockHash).value.height - } + val transferEvents = logsInBlock.map { x => + Bridge.ElSentNativeEvent.decodeArgs(x.data).explicitGet() + } + step(s"Transfer events: ${transferEvents.mkString(", ")}") - step(s"Wait block $blockHash ($blockConfirmationHeight) finalization") - eventually { - val currFinalizedHeight = chainContract.getFinalizedBlock.height - step(s"Current finalized height: $currFinalizedHeight") - currFinalizedHeight should be >= blockConfirmationHeight - } + 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 + withClue("3. Tokens received: ") { + step( + s"Broadcast withdraw transaction: transferIndexInBlock=$sendTxnLogIndex, amount=$wavesAmount, " + + s"merkleProof={${transferProofs.map(EthEncoding.toHexString).mkString(",")}}" ) - ) - val balanceAfter = receiverBalance - balanceAfter shouldBe (receiverBalanceBefore + wavesAmount) + 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/el/ElBridgeClient.scala b/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala index 1f2f6294..70bb5f6a 100644 --- a/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala +++ b/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala @@ -7,7 +7,9 @@ import org.web3j.abi.{FunctionReturnDecoder, TypeReference} import org.web3j.crypto.{Credentials, RawTransaction} import org.web3j.protocol.Web3j import org.web3j.protocol.core.DefaultBlockParameterName +import org.web3j.protocol.core.methods.request.Transaction import org.web3j.protocol.core.methods.response.EthSendTransaction +import org.web3j.protocol.exceptions.TransactionException import org.web3j.tx.RawTransactionManager import org.web3j.tx.gas.DefaultGasProvider import units.bridge.BridgeContract @@ -16,20 +18,17 @@ import units.eth.EthAddress import java.util.Collections -class ElBridgeClient(web3j: Web3j, address: EthAddress) extends ScorexLogging { - def sendNative( +class ElBridgeClient(web3j: Web3j, address: EthAddress, gasProvider: DefaultGasProvider = new DefaultGasProvider) extends ScorexLogging { + def sendSendNative( sender: Credentials, recipient: Address, amountInEther: BigInt ): EthSendTransaction = { val senderAddress = sender.getAddress - log.debug(s"sendNative($senderAddress->$recipient: $amountInEther Wei)") - val txnManager = new RawTransactionManager(web3j, sender, EcContainer.ChainId) - val gasProvider = new DefaultGasProvider - val bridgeContract = BridgeContract.load(address.hex, web3j, txnManager, gasProvider) - val funcCall = bridgeContract.send_sendNative(recipient.publicKeyHash, amountInEther.bigInteger).encodeFunctionCall() + val txnManager = new RawTransactionManager(web3j, sender, EcContainer.ChainId) + val funcCall = getSendNativeFunctionCall(sender, recipient, amountInEther) - val nonce = web3j.ethGetTransactionCount(address.hex, DefaultBlockParameterName.LATEST).send().getTransactionCount + val nonce = web3j.ethGetTransactionCount(senderAddress, DefaultBlockParameterName.PENDING).send().getTransactionCount val rawTxn = RawTransaction.createTransaction( nonce, gasProvider.getGasPrice, @@ -39,7 +38,35 @@ class ElBridgeClient(web3j: Web3j, address: EthAddress) extends ScorexLogging { funcCall ) - txnManager.signAndSend(rawTxn) + log.debug(s"Send sendNative($senderAddress->$recipient: $amountInEther Wei), nonce: $nonce") + val r = txnManager.signAndSend(rawTxn) + if (r.hasError) throw new TransactionException(s"Can't call sendNative: ${r.getError}, ${r.getError.getMessage}") + r + } + + def callRevertedSendNative( + sender: Credentials, + recipient: Address, + amountInEther: BigInt + ): String = { + val senderAddress = sender.getAddress + val funcCall = getSendNativeFunctionCall(sender, recipient, amountInEther) + val txn = Transaction.createEthCallTransaction(senderAddress, address.hex, funcCall, amountInEther.bigInteger) + + log.debug(s"Call sendNative($senderAddress->$recipient: $amountInEther Wei)") + val r = web3j.ethCall(txn, DefaultBlockParameterName.PENDING).send() + if (r.isReverted) r.getRevertReason + else throw new TransactionException(s"Expected $txn to be reverted") + } + + def getSendNativeFunctionCall( + sender: Credentials, + recipient: Address, + amountInEther: BigInt + ): String = { + val txnManager = new RawTransactionManager(web3j, sender, EcContainer.ChainId) + val bridgeContract = BridgeContract.load(address.hex, web3j, txnManager, gasProvider) + bridgeContract.send_sendNative(recipient.publicKeyHash, amountInEther.bigInteger).encodeFunctionCall() } } diff --git a/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala b/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala index e8043130..74d6aa05 100644 --- a/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala +++ b/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala @@ -8,5 +8,5 @@ object TestEnvironment { Files.createDirectories(DefaultLogsDir) val WavesDockerImage: String = System.getProperty("cc.it.docker.image") - val ExecutionClient: String = System.getProperty("cc.it.ec", "op-geth") // besu | geth | op-geth + val ExecutionClient = Option(System.getProperty("cc.it.ec")) } From e1f8a8e4e246965f770d9b775c70d8705b19dfe1 Mon Sep 17 00:00:00 2001 From: Vyatcheslav Suharnikov Date: Tue, 26 Nov 2024 13:51:31 +0400 Subject: [PATCH 3/9] BaseE2CTestSuite: fix failed confirmation issue --- .../src/test/scala/units/BridgeE2CTestSuite.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala b/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala index 83c739dd..538dd6cb 100644 --- a/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala @@ -41,6 +41,10 @@ class BridgeE2CTestSuite extends BaseDockerTestSuite { "Positive" - { def sendNative(amount: BigInt = UnitsConvert.toWei(userAmount)): TransactionReceipt = { val txnResult = elBridge.sendSendNative(elSender, clRecipient.toAddress, amount) + + // To overcome a failed block confirmation in a new epoch issue + chainContract.waitForHeight(ec1.web3j.ethBlockNumber().send().getBlockNumber.longValueExact() + 2) + eventually { val r = ec1.web3j.ethGetTransactionReceipt(txnResult.getTransactionHash).send().getTransactionReceipt.toScala.value if (!r.isStatusOK) fail(s"Expected successful sendNative, got: ${ElBridgeClient.decodeRevertReason(r.getRevertReason)}") @@ -70,8 +74,6 @@ class BridgeE2CTestSuite extends BaseDockerTestSuite { bridgeBalanceAfter shouldBe bridgeBalanceBefore } - // TODO block can be changed - val blockHash = BlockHash(sendTxnReceipt.getBlockHash) step(s"Block with transaction: $blockHash") From d746ffb0b212f0bc2ba5d10c8c47f40381f3cb18 Mon Sep 17 00:00:00 2001 From: Vyatcheslav Suharnikov Date: Tue, 26 Nov 2024 15:15:17 +0400 Subject: [PATCH 4/9] op-geth in local network --- local-network/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/local-network/docker-compose.yml b/local-network/docker-compose.yml index ca880fbe..062243db 100644 --- a/local-network/docker-compose.yml +++ b/local-network/docker-compose.yml @@ -20,8 +20,8 @@ services: container_name: ec-2 hostname: ec-2 extends: - file: ./configs/geth/geth.yml - service: geth + file: ./configs/op-geth/op-geth.yml + service: op-geth ports: - "127.0.0.1:28551:8551" # Engine port - "127.0.0.1:28545:8545" # RPC port From 00bd1c0ce57183238f740f7eae3da707b1814d79 Mon Sep 17 00:00:00 2001 From: Vyatcheslav Suharnikov Date: Tue, 26 Nov 2024 16:04:30 +0400 Subject: [PATCH 5/9] SyncingTestSuite now works? --- .../src/test/scala/units/SyncingTestSuite.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/consensus-client-it/src/test/scala/units/SyncingTestSuite.scala b/consensus-client-it/src/test/scala/units/SyncingTestSuite.scala index 311130b3..26bc8e27 100644 --- a/consensus-client-it/src/test/scala/units/SyncingTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/SyncingTestSuite.scala @@ -15,8 +15,7 @@ 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 { + "L2-381 EL transactions appear after rollback" in { step("Send transaction 1") val txn1Result = sendTxn(0) waitForTxn(txn1Result) From 9d815b73db36e1b21018f1fab56480d981ee9197 Mon Sep 17 00:00:00 2001 From: Vyatcheslav Suharnikov Date: Tue, 26 Nov 2024 18:29:53 +0400 Subject: [PATCH 6/9] Cleanup --- local-network/configs/op-geth/run-op-geth.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/local-network/configs/op-geth/run-op-geth.sh b/local-network/configs/op-geth/run-op-geth.sh index 8432efc8..eb0950b1 100755 --- a/local-network/configs/op-geth/run-op-geth.sh +++ b/local-network/configs/op-geth/run-op-geth.sh @@ -15,10 +15,6 @@ NETWORK: $NETWORK PREFIX: ${PREFIX} EOF -# TODO: -# --rollup.sequencerhttp="$BEDROCK_SEQUENCER_HTTP" \ -# --rollup.disabletxpoolgossip=true \ - # --syncmode full, because default "snap" mode and starting concurrently with ec-1 cause a stopped sync exec geth \ --http \ From 314530e9bed9fc42b700cbe7bb8535c11964196e Mon Sep 17 00:00:00 2001 From: Vyatcheslav Suharnikov Date: Mon, 2 Dec 2024 16:49:19 +0400 Subject: [PATCH 7/9] Mining stabilization, fixes --- local-network/configs/wavesnode/genesis-template.conf | 4 ++-- local-network/deploy/README.md | 2 +- local-network/deploy/pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/local-network/configs/wavesnode/genesis-template.conf b/local-network/configs/wavesnode/genesis-template.conf index 99724a5c..ca567e32 100644 --- a/local-network/configs/wavesnode/genesis-template.conf +++ b/local-network/configs/wavesnode/genesis-template.conf @@ -24,7 +24,7 @@ genesis-generator { # Private key: 5r7KpS3MNXxRcz9jMbpjZKhaZVxDxF74H4VLW6ydi4aT # Public key: 2JYMTjUK7tC8NQi6TD6oWgy41YbrnXuoLzZydrFKTKt6 # Address: 3FSgXpgbT6m1speWgVx3cVxAZKmdr4barHU - { seed-text = "devnet-1", nonce = 1, amount = 998036000000000 } + { seed-text = "devnet-1", nonce = 1, amount = 900000000000000 } # Miner on wavesnode-2 # Seed text: devnet-2 @@ -33,7 +33,7 @@ genesis-generator { # Private key: F17jrRWmrR7en6eY6ptvnUTNqc9W8RSP34hpX7bNhbht # Public key: FuibB1rJ1uHVvvY6FNmhLqnbvQBNtziWbh7zh5c1CQCG # Address: 3FSrRN8X7cDsLyYTScS8Yf8KSwZgJBwf1jU - { seed-text = "devnet-2", nonce = 0, amount = 998036000000000 } + { seed-text = "devnet-2", nonce = 0, amount = 800900102030000 } # Additional addresses diff --git a/local-network/deploy/README.md b/local-network/deploy/README.md index 36dfd701..a80abd1d 100644 --- a/local-network/deploy/README.md +++ b/local-network/deploy/README.md @@ -6,7 +6,7 @@ To re-deploy from a container run the following command from the project's root 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` +3. Run test, e.g.: `./tests/transfer-multiple-c2e.py` To generate `Bridge.java`, run: ```bash diff --git a/local-network/deploy/pyproject.toml b/local-network/deploy/pyproject.toml index 01fdf4f2..bb16f36d 100644 --- a/local-network/deploy/pyproject.toml +++ b/local-network/deploy/pyproject.toml @@ -8,7 +8,7 @@ version = "0.1" description = "Scripts and tests for a locally deployed Unit network" dependencies = [ "solc-select", - "units-network @ git+https://github.com/UnitsNetwork/examples.git@dao-rewards", + "units-network @ git+https://github.com/UnitsNetwork/examples.git", ] readme = "README.md" requires-python = ">=3.9" From 6f2cea499662009b682fb5383b41b1fc4cabb20b Mon Sep 17 00:00:00 2001 From: Vyatcheslav Suharnikov Date: Mon, 2 Dec 2024 17:07:36 +0400 Subject: [PATCH 8/9] Fixed op-geth image --- local-network/configs/op-geth/op-geth.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-network/configs/op-geth/op-geth.yml b/local-network/configs/op-geth/op-geth.yml index b7e32b44..e923c938 100644 --- a/local-network/configs/op-geth/op-geth.yml +++ b/local-network/configs/op-geth/op-geth.yml @@ -1,6 +1,6 @@ services: op-geth: - image: ethereum/client-go:stable + image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth:v1.101411.2 stop_grace_period: 10s entrypoint: /tmp/run.sh volumes: From ee2419c11f72dad59eec77ce4808a8e2a363714a Mon Sep 17 00:00:00 2001 From: Vyatcheslav Suharnikov Date: Tue, 3 Dec 2024 14:25:57 +0400 Subject: [PATCH 9/9] geth and op-geth: bootnodes and static nodes --- local-network/configs/ec-common/peers-geth.toml | 10 ++++++++++ local-network/configs/geth/geth.yml | 1 + local-network/configs/geth/run-geth.sh | 1 + local-network/configs/op-geth/op-geth.yml | 1 + local-network/configs/op-geth/run-op-geth.sh | 2 +- 5 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 local-network/configs/ec-common/peers-geth.toml diff --git a/local-network/configs/ec-common/peers-geth.toml b/local-network/configs/ec-common/peers-geth.toml new file mode 100644 index 00000000..1ccbbd59 --- /dev/null +++ b/local-network/configs/ec-common/peers-geth.toml @@ -0,0 +1,10 @@ +[Node.P2P] +BootstrapNodes = [ + "enode://b2ce9caff5e7472eafaf006904e2cb39cdd79801cda1328c510118cafdb0e9574526af6d05a89dae07a376606227c54c724cab1e88edf43190b7544976b275b8@ec-1:30303", + "enode://4e355eebfd77e5c2c0c20328c2bd5f3fde033c58e06e758c3e0a4ad88e8ced176f0d5eb32e214461b73e014591587f7c6567ee373e9c389b872a6d97d74a913c@ec-2:30303" +] +BootstrapNodesV5 = [] +StaticNodes = [ + "enode://b2ce9caff5e7472eafaf006904e2cb39cdd79801cda1328c510118cafdb0e9574526af6d05a89dae07a376606227c54c724cab1e88edf43190b7544976b275b8@ec-1:30303", + "enode://4e355eebfd77e5c2c0c20328c2bd5f3fde033c58e06e758c3e0a4ad88e8ced176f0d5eb32e214461b73e014591587f7c6567ee373e9c389b872a6d97d74a913c@ec-2:30303" +] diff --git a/local-network/configs/geth/geth.yml b/local-network/configs/geth/geth.yml index 7f4fe47c..2be9fa91 100644 --- a/local-network/configs/geth/geth.yml +++ b/local-network/configs/geth/geth.yml @@ -5,6 +5,7 @@ services: entrypoint: /tmp/run.sh volumes: - ../ec-common/genesis.json:/tmp/genesis.json:ro + - ../ec-common/peers-geth.toml:/tmp/peers.toml:ro - ./run-geth.sh:/tmp/run.sh:ro env_file: - ../ec-common/peers.env diff --git a/local-network/configs/geth/run-geth.sh b/local-network/configs/geth/run-geth.sh index f392e03c..5cefa17e 100755 --- a/local-network/configs/geth/run-geth.sh +++ b/local-network/configs/geth/run-geth.sh @@ -17,6 +17,7 @@ EOF # --syncmode full, because default "snap" mode and starting concurrently with ec-1 cause a stopped sync exec geth \ + --config=/tmp/peers.toml \ --http \ --http.addr=0.0.0.0 \ --http.vhosts=* \ diff --git a/local-network/configs/op-geth/op-geth.yml b/local-network/configs/op-geth/op-geth.yml index e923c938..00ef7724 100644 --- a/local-network/configs/op-geth/op-geth.yml +++ b/local-network/configs/op-geth/op-geth.yml @@ -5,6 +5,7 @@ services: entrypoint: /tmp/run.sh volumes: - ../ec-common/genesis.json:/tmp/genesis.json:ro + - ../ec-common/peers-geth.toml:/tmp/peers.toml:ro - ./run-op-geth.sh:/tmp/run.sh:ro env_file: - ../ec-common/peers.env diff --git a/local-network/configs/op-geth/run-op-geth.sh b/local-network/configs/op-geth/run-op-geth.sh index eb0950b1..5a0e5d5f 100755 --- a/local-network/configs/op-geth/run-op-geth.sh +++ b/local-network/configs/op-geth/run-op-geth.sh @@ -17,6 +17,7 @@ EOF # --syncmode full, because default "snap" mode and starting concurrently with ec-1 cause a stopped sync exec geth \ + --config=/tmp/peers.toml \ --http \ --http.addr=0.0.0.0 \ --http.vhosts=* \ @@ -33,7 +34,6 @@ exec geth \ --nodekey=/etc/secrets/p2p-key \ --nat="extip:${IP}" \ --netrestrict="${NETWORK}/${PREFIX}" \ - --bootnodes="${BESU_BOOTNODES}" \ --syncmode="full" \ --gcmode="full" \ --log.file="/root/logs/log" \