diff --git a/build.sbt b/build.sbt index 54d13579..4f8fc9cd 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,7 @@ git.uncommittedSignifier := Some("DIRTY") inScope(Global)( Seq( onChangedBuildSource := ReloadOnSourceChanges, - scalaVersion := "3.7.2", + scalaVersion := "3.7.3", organization := "network.units", organizationName := "Units Network", resolvers ++= Seq(Resolver.sonatypeCentralSnapshots, Resolver.mavenLocal), @@ -33,14 +33,14 @@ name := "consensus-client" maintainer := "Units Network Team" libraryDependencies ++= { - val node = "1.5.11" + val node = "1.5.12-SNAPSHOT" val sttpVersion = "3.11.0" Seq( "com.wavesplatform" % "node-testkit" % node % Test, "com.wavesplatform" % "node" % node % Provided, "com.softwaremill.sttp.client3" %% "core" % sttpVersion, "com.softwaremill.sttp.client3" %% "play-json" % sttpVersion, - "com.github.jwt-scala" %% "jwt-play-json" % "11.0.2", + "com.github.jwt-scala" %% "jwt-play-json" % "11.0.3", "org.web3j" % "core" % "4.9.8" ) } 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 f2f68c24..86d584a8 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 @@ -11,9 +11,8 @@ 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 import com.wavesplatform.transaction.Asset.IssuedAsset -import com.wavesplatform.transaction.Transaction +import com.wavesplatform.transaction.{Asset, Transaction} import com.wavesplatform.utils.ScorexLogging import org.scalatest.matchers.should.Matchers import play.api.libs.json.* @@ -159,12 +158,12 @@ class NodeHttpApi(apiUri: Uri, backend: SttpBackend[Identity, ?], apiKeyValue: S } } } - + def balance(address: Address, asset: Asset)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Long = { if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} balance($address, $asset)") basicRequest .get(asset match { - case Asset.Waves => uri"$apiUri/addresses/balance/$address" + case Asset.Waves => uri"$apiUri/addresses/balance/$address" case IssuedAsset(id) => uri"$apiUri/assets/balance/$address/$id" }) .response(asJson[BalanceResponse]) diff --git a/consensus-client-it/src/test/scala/units/Accounts.scala b/consensus-client-it/src/test/scala/units/Accounts.scala index f094c374..860c6d7a 100644 --- a/consensus-client-it/src/test/scala/units/Accounts.scala +++ b/consensus-client-it/src/test/scala/units/Accounts.scala @@ -21,6 +21,9 @@ trait Accounts { val miner21Account = mkKeyPair("devnet-2", 0) val miner21RewardAddress = EthAddress.unsafeFrom("0xcf0b9e13fdd593f4ca26d36afcaa44dd3fdccbed") + val miner31Account = mkKeyPair("devnet-3", 0) + val miner31RewardAddress = EthAddress.unsafeFrom("0xf1FE6d7bfebead68A8C06cCcee97B61d7DAA0338") + val clRichAccount1 = mkKeyPair("devnet rich", 0) val clRichAccount2 = mkKeyPair("devnet rich", 1) diff --git a/consensus-client-it/src/test/scala/units/BaseBlockValidationSuite.scala b/consensus-client-it/src/test/scala/units/BaseBlockValidationSuite.scala new file mode 100644 index 00000000..0595f122 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BaseBlockValidationSuite.scala @@ -0,0 +1,269 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.state.ByteStr +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.crypto.Keccak256 +import com.wavesplatform.state.{Height, IntegerDataEntry} +import com.wavesplatform.transaction.Asset.IssuedAsset +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import com.wavesplatform.transaction.{Asset, TxHelpers} +import monix.execution.atomic.AtomicInt +import org.web3j.protocol.core.DefaultBlockParameterName +import org.web3j.tx.RawTransactionManager +import org.web3j.tx.gas.DefaultGasProvider +import play.api.libs.json.* +import units.{BlockHash, ELUpdater} +import units.client.engine.model.{EcBlock, Withdrawal} +import units.docker.EcContainer +import units.el.* +import units.eth.{EmptyL2Block, EthAddress, EthereumConstants} +import units.util.{BlockToPayloadMapper, HexBytesConverter} + +import scala.concurrent.duration.DurationInt +import scala.jdk.OptionConverters.RichOptional + +trait BaseBlockValidationSuite extends BaseDockerTestSuite { + protected val setupMiner: SeedKeyPair = miner11Account // Leaves after setting up the contracts + protected val actingMiner: SeedKeyPair = miner12Account + protected val actingMinerRewardAddress: EthAddress = miner12RewardAddress + + // Note: additional miners are needed to avoid the actingMiner having majority of the stake + protected val additionalMiner1: SeedKeyPair = miner21Account + protected val additionalMiner1RewardAddress: EthAddress = miner21RewardAddress + protected val additionalMiner2: SeedKeyPair = miner31Account + protected val additionalMiner2RewardAddress: EthAddress = miner31RewardAddress + + // transfers + protected val clSender: SeedKeyPair = clRichAccount1 + protected val elRecipient: EthAddress = elRichAddress1 + + // native transfers + protected val userNativeTokenAmount = 1 + protected val clNativeTokenAmount: Long = UnitsConvert.toUnitsInWaves(userNativeTokenAmount) + protected val elNativeTokenAmount: BigInt = UnitsConvert.toWei(userNativeTokenAmount) + + // asset transfers + protected val gasProvider = new DefaultGasProvider + protected lazy val txnManager = new RawTransactionManager(ec1.web3j, elRichAccount1, EcContainer.ChainId, 20, 2000) + protected lazy val terc20 = new Erc20Client(ec1.web3j, TErc20Address, txnManager, gasProvider) + protected val issueAssetDecimals: Byte = 8.toByte + protected lazy val issueAsset: IssuedAsset = chainContract.getRegisteredAsset(1) match { + case ia: IssuedAsset => ia + case _ => fail("Expected issued asset") + } + protected val userAssetTokenAmount = 1 + protected val clAssetTokenAmount: Long = UnitsConvert.toWavesAtomic(userAssetTokenAmount, issueAssetDecimals) + protected val elAssetTokenAmount: BigInt = UnitsConvert.toAtomic(userAssetTokenAmount, TErc20Decimals) + + protected final def mkRewardWithdrawal(elParentBlock: EcBlock): Withdrawal = { + val chainContractOptions = chainContract.getOptions + + val elWithdrawalIndexBefore = (elParentBlock.withdrawals.lastOption.map(_.index) match { + case Some(r) => Right(r) + case None => + if (elParentBlock.height - 1 <= EthereumConstants.GenesisBlockHeight) Right(-1L) + else ec1.engineApi.getLastWithdrawalIndex(elParentBlock.parentHash) + }).explicitGet() + Withdrawal(elWithdrawalIndexBefore + 1, elParentBlock.minerRewardL2Address, chainContractOptions.miningReward) + } + + protected final def mkSimulatedBlock( + elParentBlock: EcBlock, + withdrawals: Seq[Withdrawal], + depositedTransactions: Seq[DepositedTransaction] + ): (JsObject, String, ByteStr) = { + step("Building a simulated block") + val feeRecipient = actingMinerRewardAddress + + val currentUnixTs = System.currentTimeMillis() / 1000 + val blockDelay = 6 + val nextBlockUnixTs = (elParentBlock.timestamp + blockDelay).max(currentUnixTs) + + val currentEpochHeader = waves1.api.blockHeader(waves1.api.height()).value + val hitSource = ByteStr.decodeBase58(currentEpochHeader.VRF).get + val prevRandao = ELUpdater.calculateRandao(hitSource, elParentBlock.hash) + + val txHashes = depositedTransactions.map(t => HexBytesConverter.toHex(Keccak256.hash(HexBytesConverter.toBytes(t.toHex)))).mkString(", ") + log.debug(s"Deposited transactions hashes: $txHashes") + + val simulatedBlock: JsObject = ec1.engineApi + .simulate( + EmptyL2Block.mkSimulateCall(elParentBlock, feeRecipient, nextBlockUnixTs, prevRandao, withdrawals, depositedTransactions), + elParentBlock.hash + ) + .explicitGet() + .head + + val payload = BlockToPayloadMapper.toPayloadJson( + simulatedBlock, + Json.obj( + "transactions" -> depositedTransactions.map(_.toHex), + "withdrawals" -> Json.toJson(withdrawals) + ) + ) + + val simulatedBlockHash: String = (simulatedBlock \ "hash").as[String] + + (payload, simulatedBlockHash, hitSource) + } + + protected def deployContractsAndActivateTransferFeatures(): Unit = { + deploySolidityContracts() + + step("Enable token transfers") + val activationEpoch = waves1.api.height() + 1 + waves1.api.broadcastAndWait( + ChainContract.enableTokenTransfersWithWaves( + StandardBridgeAddress, + WWavesAddress, + activationEpoch = activationEpoch + ) + ) + + step("Set strict C2E transfers feature activation epoch") + waves1.api.broadcastAndWait( + TxHelpers.dataEntry( + chainContractAccount, + IntegerDataEntry("strictC2ETransfersActivationEpoch", activationEpoch) + ) + ) + + step("Wait for features activation") + waves1.api.waitForHeight(activationEpoch) + } + + protected def transferNativeTokenToClSender(): Unit = { + step("Prepare: issue tokens on chain contract and transfer to a user") + waves1.api.broadcastAndWait( + TxHelpers.reissue( + asset = chainContract.nativeTokenId, + sender = chainContractAccount, + amount = clNativeTokenAmount + ) + ) + waves1.api.broadcastAndWait( + TxHelpers.transfer( + from = chainContractAccount, + to = clSender.toAddress, + amount = clNativeTokenAmount, + asset = chainContract.nativeTokenId + ) + ) + } + + protected def transferAssetTokenToClSender(): Unit = { + step("Register asset") + waves1.api.broadcastAndWait(ChainContract.issueAndRegister(TErc20Address, TErc20Decimals, "TERC20", "Test ERC20 token", issueAssetDecimals)) + + eventually { + standardBridge.isRegistered(TErc20Address, ignoreExceptions = true) shouldBe true + } + + step("Transfer asset from EL to CL") + + val currNonce = + AtomicInt(ec1.web3j.ethGetTransactionCount(elRichAddress1.hex, DefaultBlockParameterName.PENDING).send().getTransactionCount.intValueExact()) + def nextNonce: Int = currNonce.getAndIncrement() + + waitFor(terc20.sendApprove(StandardBridgeAddress, elAssetTokenAmount, nextNonce)) + + val e2cIssuedTxn = standardBridge.sendBridgeErc20(elRichAccount1, TErc20Address, clSender.toAddress, elAssetTokenAmount, nextNonce) + + chainContract.waitForEpoch(waves1.api.height() + 1) + val e2cReceipt = + eventually { + val hash = e2cIssuedTxn.getTransactionHash + withClue(s"$hash: ") { + ec1.web3j.ethGetTransactionReceipt(hash).send().getTransactionReceipt.toScala.value + } + } + + val e2cBlockHash = BlockHash(e2cReceipt.getBlockHash) + + val e2cLogsInBlock = ec1.engineApi + .getLogs(e2cBlockHash, List(NativeBridgeAddress, StandardBridgeAddress), Nil) + .explicitGet() + .filter(_.topics.intersect(E2CTopics).nonEmpty) + + val e2cBlockConfirmationHeight = eventually { + chainContract.getBlock(e2cBlockHash).value.height + } + + step(s"Wait for block $e2cBlockHash ($e2cBlockConfirmationHeight) finalization") + eventually { + val currFinalizedHeight = chainContract.getFinalizedBlock.height + step(s"Current finalized height: $currFinalizedHeight") + currFinalizedHeight should be >= e2cBlockConfirmationHeight + } + + step("Broadcast withdrawAsset transactions") + waves1.api.broadcastAndWait( + ChainContract.withdrawAsset( + sender = clSender, + blockHash = e2cBlockHash, + merkleProof = BridgeMerkleTree.mkTransferProofs(e2cLogsInBlock, 0).explicitGet().reverse, + transferIndexInBlock = 0, + amount = UnitsConvert.toWavesAtomic(userAssetTokenAmount, issueAssetDecimals), + asset = issueAsset + ) + ) + } + + protected def leaveSetupMinerAndJoinOthers(): Unit = { + log.debug(s"setupMiner: ${setupMiner.toAddress}") + log.debug(s"actingMiner: ${actingMiner.toAddress}") + log.debug(s"additionalMiner1: ${additionalMiner1.toAddress}") + log.debug(s"additionalMiner2: ${additionalMiner2.toAddress}") + + step(s"additionalMiner1 join") + waves1.api.broadcastAndWait( + ChainContract.join( + minerAccount = additionalMiner1, + elRewardAddress = additionalMiner1RewardAddress + ) + ) + + step(s"Wait additionalMiner1 epoch") + chainContract.waitForMinerEpoch(additionalMiner1) + + step(s"setupMiner leave") + eventually(interval(500 millis)) { + waves1.api.broadcastAndWait(ChainContract.leave(setupMiner)) + } + + step(s"additionalMiner2 join") + waves1.api.broadcastAndWait( + ChainContract.join( + minerAccount = additionalMiner2, + elRewardAddress = additionalMiner2RewardAddress + ) + ) + + step(s"actingMiner join") + waves1.api.broadcastAndWait( + ChainContract.join( + minerAccount = actingMiner, + elRewardAddress = actingMinerRewardAddress + ) + ) + + step(s"Wait actingMiner epoch") + chainContract.waitForMinerEpoch(actingMiner) + } + + protected def setupForNativeTokenTransfer(): Unit = { + super.beforeAll() + deployContractsAndActivateTransferFeatures() + transferNativeTokenToClSender() + leaveSetupMinerAndJoinOthers() + } + + protected def setupForAssetTokenTransfer(): Unit = { + super.beforeAll() + deployContractsAndActivateTransferFeatures() + transferAssetTokenToClSender() + leaveSetupMinerAndJoinOthers() + } +} diff --git a/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala index 6a9a8b9c..210afad1 100644 --- a/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala @@ -46,8 +46,13 @@ trait BaseDockerTestSuite private implicit val httpClientBackend: SttpBackend[Identity, Any] = new LoggingBackend(HttpClientSyncBackend()) - protected lazy val ec1: EcContainer = - new OpGethContainer(network, 1, Networks.ipForNode(2) /* ipForNode(1) is assigned to Ryuk */ ) + /* + * ipForNode(1) -> Ryuk + * ipForNode(2) -> ec1 + * ipForNode(3) -> waves1 + */ + + protected lazy val ec1: EcContainer = new OpGethContainer(network, 1, Networks.ipForNode(2)) protected lazy val waves1: WavesNodeContainer = new WavesNodeContainer( network = network, diff --git a/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidAmountTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidAmountTestSuite.scala new file mode 100644 index 00000000..6270a329 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidAmountTestSuite.scala @@ -0,0 +1,89 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.eth.EthAddress +import units.{BlockHash, TestNetworkClient} + +class BlockValidationAssetInvalidAmountTestSuite extends BaseBlockValidationSuite { + "Invalid block: asset token, invalid amount" in { + val balanceBefore = terc20.getBalance(elRecipient) + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val invalidAmount = EAmount((elAssetTokenAmount - 1).bigInteger) + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeErc20Transaction( + transferIndex = 0L, + standardBridgeAddress = StandardBridgeAddress, + token = TErc20Address, + from = EthAddress.unsafeFrom(clSender.toAddress.bytes.drop(2).take(20)), + to = elRecipient, + amount = invalidAmount + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + issueAsset, + clAssetTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Deposited transaction doesn't change balance") + val balanceAfter = terc20.getBalance(elRecipient) + balanceAfter.longValue shouldBe balanceBefore.longValue + + step("Assertion: EL height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForAssetTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidBridgeTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidBridgeTestSuite.scala new file mode 100644 index 00000000..8c56a1f5 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidBridgeTestSuite.scala @@ -0,0 +1,89 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.eth.EthAddress +import units.{BlockHash, TestNetworkClient} + +class BlockValidationAssetInvalidBridgeTestSuite extends BaseBlockValidationSuite { + "Invalid block: asset token, invalid standardBridgeAddress" in { + val balanceBefore = terc20.getBalance(elRecipient) + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val invalidStandardBridgeAddress = additionalMiner1RewardAddress + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeErc20Transaction( + transferIndex = 0L, + standardBridgeAddress = invalidStandardBridgeAddress, + token = TErc20Address, + from = EthAddress.unsafeFrom(clSender.toAddress.bytes.drop(2).take(20)), + to = elRecipient, + amount = EAmount(elAssetTokenAmount.bigInteger) + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + issueAsset, + clAssetTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Deposited transaction doesn't change balance") + val balanceAfter = terc20.getBalance(elRecipient) + balanceAfter.longValue shouldBe balanceBefore.longValue + + step("Assertion: EL height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForAssetTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidRecipientTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidRecipientTestSuite.scala new file mode 100644 index 00000000..db049904 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidRecipientTestSuite.scala @@ -0,0 +1,89 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import units.BlockHash +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.eth.EthAddress + +class BlockValidationAssetInvalidRecipientTestSuite extends BaseBlockValidationSuite { + "Invalid block: asset token, invalid recipient address" in { + val balanceBefore = terc20.getBalance(elRecipient) + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val invalidRecipientAddress = additionalMiner1RewardAddress + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeErc20Transaction( + transferIndex = 0L, + standardBridgeAddress = StandardBridgeAddress, + token = TErc20Address, + from = EthAddress.unsafeFrom(clSender.toAddress.bytes.drop(2).take(20)), + to = invalidRecipientAddress, + amount = EAmount(elAssetTokenAmount.bigInteger) + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + issueAsset, + clAssetTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Deposited transaction doesn't change balance") + val balanceAfter = terc20.getBalance(elRecipient) + balanceAfter.longValue shouldBe balanceBefore.longValue + + step("Assertion: EL height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForAssetTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidSenderTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidSenderTestSuite.scala new file mode 100644 index 00000000..3eb5df38 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidSenderTestSuite.scala @@ -0,0 +1,87 @@ +package units + +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.{BlockHash, TestNetworkClient} + +class BlockValidationAssetInvalidSenderTestSuite extends BaseBlockValidationSuite { + "Invalid block: asset token, invalid sender address" in { + val balanceBefore = terc20.getBalance(elRecipient) + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val invalidSenderAddress = additionalMiner1RewardAddress + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeErc20Transaction( + transferIndex = 0L, + standardBridgeAddress = StandardBridgeAddress, + token = TErc20Address, + from = invalidSenderAddress, + to = elRecipient, + amount = EAmount(elAssetTokenAmount.bigInteger) + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + issueAsset, + clAssetTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Deposited transaction doesn't change balance") + val balanceAfter = terc20.getBalance(elRecipient) + balanceAfter.longValue shouldBe balanceBefore.longValue + + step("Assertion: EL height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForAssetTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidTokenTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidTokenTestSuite.scala new file mode 100644 index 00000000..02f13c1e --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidTokenTestSuite.scala @@ -0,0 +1,89 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.eth.EthAddress +import units.{BlockHash, TestNetworkClient} + +class BlockValidationAssetInvalidTokenTestSuite extends BaseBlockValidationSuite { + "Invalid block: asset token, invalid token address" in { + val balanceBefore = terc20.getBalance(elRecipient) + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val invalidTokenAddress = WWavesAddress + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeErc20Transaction( + transferIndex = 0L, + standardBridgeAddress = StandardBridgeAddress, + token = invalidTokenAddress, + from = EthAddress.unsafeFrom(clSender.toAddress.bytes.drop(2).take(20)), + to = elRecipient, + amount = EAmount(elAssetTokenAmount.bigInteger) + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + issueAsset, + clAssetTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Deposited transaction doesn't change balance") + val balanceAfter = terc20.getBalance(elRecipient) + balanceAfter.longValue shouldBe balanceBefore.longValue + + step("Assertion: EL height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForAssetTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationAssetValidTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetValidTestSuite.scala new file mode 100644 index 00000000..d61c9884 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetValidTestSuite.scala @@ -0,0 +1,87 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.eth.EthAddress +import units.{BlockHash, TestNetworkClient} + +class BlockValidationAssetValidTestSuite extends BaseBlockValidationSuite { + "Valid block: asset token, correct transfer" in { + val balanceBefore = terc20.getBalance(elRecipient) + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeErc20Transaction( + transferIndex = 0L, + standardBridgeAddress = StandardBridgeAddress, + token = TErc20Address, + from = EthAddress.unsafeFrom(clSender.toAddress.bytes.drop(2).take(20)), + to = elRecipient, + amount = EAmount(elAssetTokenAmount.bigInteger) + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + issueAsset, + clAssetTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Deposited transaction changes balances") + val balanceAfter = terc20.getBalance(elRecipient) + balanceAfter.longValue shouldBe (balanceBefore.longValue + elAssetTokenAmount.longValue) + + step("Assertion: EL height grows") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe (elParentBlock.height.longValue + 1) + } + + override def beforeAll(): Unit = setupForAssetTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidAmountTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidAmountTestSuite.scala new file mode 100644 index 00000000..21d789b1 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidAmountTestSuite.scala @@ -0,0 +1,97 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import org.web3j.protocol.core.DefaultBlockParameterName +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.eth.EthAddress +import units.{BlockHash, TestNetworkClient} + +class BlockValidationNativeInvalidAmountTestSuite extends BaseBlockValidationSuite { + "Invalid block: native token, invalid amount" in { + val ethBalanceBefore = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val invalidAmount = clNativeTokenAmount.longValue - 1L + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeETHTransaction( + transferIndex = 0L, + standardBridgeAddress = StandardBridgeAddress, + from = EthAddress.unsafeFrom(clSender.toAddress.bytes.drop(2).take(20)), + to = elRecipient, + amount = invalidAmount + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + chainContract.nativeTokenId, + clNativeTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Unexpected deposited transaction doesn't affect balances") + val ethBalanceAfter = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + ethBalanceBefore shouldBe ethBalanceAfter + + step("Assertion: While the block exists on EC1, the height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForNativeTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidBridgeTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidBridgeTestSuite.scala new file mode 100644 index 00000000..022b85f3 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidBridgeTestSuite.scala @@ -0,0 +1,97 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import org.web3j.protocol.core.DefaultBlockParameterName +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.eth.EthAddress +import units.{BlockHash, TestNetworkClient} + +class BlockValidationNativeInvalidBridgeTestSuite extends BaseBlockValidationSuite { + "Invalid block: native token, invalid standard bridge address" in { + val ethBalanceBefore = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val invalidStandardBridgeAddress = additionalMiner1RewardAddress + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeETHTransaction( + transferIndex = 0L, + standardBridgeAddress = invalidStandardBridgeAddress, + from = EthAddress.unsafeFrom(clSender.toAddress.bytes.drop(2).take(20)), + to = elRecipient, + amount = clNativeTokenAmount.longValue + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + chainContract.nativeTokenId, + clNativeTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Unexpected deposited transaction doesn't affect balances") + val ethBalanceAfter = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + ethBalanceBefore shouldBe ethBalanceAfter + + step("Assertion: While the block exists on EC1, the height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForNativeTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidRecipientTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidRecipientTestSuite.scala new file mode 100644 index 00000000..0fa82652 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidRecipientTestSuite.scala @@ -0,0 +1,97 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import org.web3j.protocol.core.DefaultBlockParameterName +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.eth.EthAddress +import units.{BlockHash, TestNetworkClient} + +class BlockValidationNativeInvalidRecipientTestSuite extends BaseBlockValidationSuite { + "Invalid block: native token, invalid recipient" in { + val invalidRecipient = additionalMiner1RewardAddress + + val ethBalanceBefore = ec1.web3j.ethGetBalance(invalidRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeETHTransaction( + transferIndex = 0L, + standardBridgeAddress = StandardBridgeAddress, + from = EthAddress.unsafeFrom(clSender.toAddress.bytes.drop(2).take(20)), + to = invalidRecipient, + amount = clNativeTokenAmount.longValue + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, // Valid recipient, while the deposited transaction has invalid one + chainContract.nativeTokenId, + clNativeTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Unexpected deposited transaction doesn't affect balances") + val ethBalanceAfter = ec1.web3j.ethGetBalance(invalidRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + ethBalanceBefore shouldBe ethBalanceAfter + + step("Assertion: While the block exists on EC1, the height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForNativeTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidSenderTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidSenderTestSuite.scala new file mode 100644 index 00000000..7de12b0e --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidSenderTestSuite.scala @@ -0,0 +1,95 @@ +package units + +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import org.web3j.protocol.core.DefaultBlockParameterName +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.{BlockHash, TestNetworkClient} + +class BlockValidationNativeInvalidSenderTestSuite extends BaseBlockValidationSuite { + "Invalid block: native token, invalid sender" in { + val invalidSender = additionalMiner1RewardAddress + + val ethBalanceBefore = ec1.web3j.ethGetBalance(invalidSender.toString, DefaultBlockParameterName.LATEST).send().getBalance + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeETHTransaction( + transferIndex = 0L, + standardBridgeAddress = StandardBridgeAddress, + from = invalidSender, + to = elRecipient, + amount = clNativeTokenAmount.longValue + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + chainContract.nativeTokenId, + clNativeTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Unexpected deposited transaction doesn't affect balances") + val ethBalanceAfter = ec1.web3j.ethGetBalance(invalidSender.toString, DefaultBlockParameterName.LATEST).send().getBalance + ethBalanceBefore shouldBe ethBalanceAfter + + step("Assertion: While the block exists on EC1, the height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForNativeTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationNativeMissingDepositTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeMissingDepositTestSuite.scala new file mode 100644 index 00000000..9c66e9e4 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeMissingDepositTestSuite.scala @@ -0,0 +1,84 @@ +package units + +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import org.web3j.protocol.core.DefaultBlockParameterName +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.{BlockHash, TestNetworkClient} + +class BlockValidationNativeMissingDepositTestSuite extends BaseBlockValidationSuite { + "Invalid block: native token, missing deposited transaction" in { + val ethBalanceBefore = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val depositedTransactions = Vector() + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + chainContract.nativeTokenId, + clNativeTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Doesn't affect balances") + val ethBalanceAfter = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + ethBalanceBefore shouldBe ethBalanceAfter + + step("Assertion: While the block exists on EC1, the height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForNativeTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedDepositTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedDepositTestSuite.scala new file mode 100644 index 00000000..fb5b81db --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedDepositTestSuite.scala @@ -0,0 +1,79 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import org.web3j.protocol.core.DefaultBlockParameterName +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.eth.EthAddress +import units.{BlockHash, TestNetworkClient} + +class BlockValidationNativeUnexpectedDepositTestSuite extends BaseBlockValidationSuite { + "Invalid block: unexpected deposited transaction" in { + val ethBalanceBefore = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeETHTransaction( + transferIndex = 0L, + standardBridgeAddress = StandardBridgeAddress, + from = EthAddress.unsafeFrom(clSender.toAddress.bytes.drop(2).take(20)), + to = elRecipient, + amount = clNativeTokenAmount.longValue + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + // Note: No transfers on the chain contract in this test case + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Unexpected deposited transaction doesn't affect balances") + val ethBalanceAfter = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + ethBalanceBefore shouldBe ethBalanceAfter + + step("Assertion: While the block exists on EC1, the height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForNativeTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedWithdrawalTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedWithdrawalTestSuite.scala new file mode 100644 index 00000000..480f0110 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedWithdrawalTestSuite.scala @@ -0,0 +1,97 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import org.web3j.protocol.core.DefaultBlockParameterName +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.{EcBlock, Withdrawal} +import units.el.* +import units.eth.{EthAddress, Gwei} +import units.{BlockHash, TestNetworkClient} + +class BlockValidationNativeUnexpectedWithdrawalTestSuite extends BaseBlockValidationSuite { + "Invalid block: native token, unexpected extra withdrawal" in { + val ethBalanceBefore = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val rewardWithdrawal = mkRewardWithdrawal(elParentBlock) + val unexpectedWithdrawal = Withdrawal(rewardWithdrawal.index + 1, elRecipient, Gwei.ofRawGwei(3_000_000_000L)) + val withdrawals = Vector(rewardWithdrawal, unexpectedWithdrawal) + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeETHTransaction( + transferIndex = 0L, + standardBridgeAddress = StandardBridgeAddress, + from = EthAddress.unsafeFrom(clSender.toAddress.bytes.drop(2).take(20)), + to = elRecipient, + amount = clNativeTokenAmount.longValue + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + chainContract.nativeTokenId, + clNativeTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Deposited transaction doesn't affect balances") + val ethBalanceAfter = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + ethBalanceBefore shouldBe ethBalanceAfter + + step("Assertion: While the block exists on EC1, the height doesn't grow") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe elParentBlock.height.longValue + } + + override def beforeAll(): Unit = setupForNativeTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationNativeValidTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeValidTestSuite.scala new file mode 100644 index 00000000..1ab20b92 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeValidTestSuite.scala @@ -0,0 +1,87 @@ +package units + +import com.wavesplatform.* +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import org.web3j.protocol.core.DefaultBlockParameterName +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.el.* +import units.eth.EthAddress +import units.{BlockHash, TestNetworkClient} + +class BlockValidationNativeValidTestSuite extends BaseBlockValidationSuite { + "Valid block: native token, correct transfer" in { + val ethBalanceBefore = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val depositedTransactions = Vector( + StandardBridge.mkFinalizeBridgeETHTransaction( + transferIndex = 0L, + standardBridgeAddress = StandardBridgeAddress, + from = EthAddress.unsafeFrom(clSender.toAddress.bytes.drop(2).take(20)), + to = elRecipient, + amount = clNativeTokenAmount.longValue + ) + ) + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + step("Transfer on the chain contract") + waves1.api.broadcastAndWait( + ChainContract.transfer( + clSender, + elRecipient, + chainContract.nativeTokenId, + clNativeTokenAmount + ) + ) + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: Deposited transaction changes balances") + val ethBalanceAfter = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance + ethBalanceBefore.longValue shouldBe ethBalanceAfter.longValue - elNativeTokenAmount.longValue + + step("Assertion: EL height grows") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe (elParentBlock.height.longValue + 1) + } + + override def beforeAll(): Unit = setupForNativeTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/BlockValidationNoTransfersTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNoTransfersTestSuite.scala new file mode 100644 index 00000000..b9a81667 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/BlockValidationNoTransfersTestSuite.scala @@ -0,0 +1,62 @@ +package units + +import com.wavesplatform.account.* +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.lang.v1.compiler.Terms +import com.wavesplatform.transaction.TxHelpers +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.engine.model.EcBlock +import units.{BlockHash, TestNetworkClient} + +class BlockValidationNoTransfersTestSuite extends BaseBlockValidationSuite { + "Valid block: no transfers" in { + val elParentBlock: EcBlock = ec1.engineApi.getLastExecutionBlock().explicitGet() + + val withdrawals = Vector(mkRewardWithdrawal(elParentBlock)) + + val depositedTransactions = Vector.empty + + val (payload, simulatedBlockHash, hitSource) = mkSimulatedBlock(elParentBlock, withdrawals, depositedTransactions) + + // Note: No transfers on the chain contract in this test case + + step("Register the simulated block on the chain contract") + waves1.api.broadcastAndWait( + TxHelpers.invoke( + invoker = actingMiner, + dApp = chainContractAddress, + func = Some("extendMainChain_v2"), + args = List( + Terms.CONST_STRING(simulatedBlockHash.drop(2)).explicitGet(), + Terms.CONST_STRING(elParentBlock.hash.drop(2)).explicitGet(), + Terms.CONST_BYTESTR(hitSource).explicitGet(), + Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), + Terms.CONST_LONG(0), + Terms.CONST_LONG(-1) + ) + ) + ) + + step("Send the simulated block to waves1") + TestNetworkClient.send( + waves1, + chainContractAddress, + NetworkL2Block.signed(payload, actingMiner.privateKey).explicitGet() + ) + + step("Assertion: Block exists on EC1") + eventually { + ec1.engineApi + .getBlockByHash(BlockHash(simulatedBlockHash)) + .explicitGet() + .getOrElse(fail(s"Block $simulatedBlockHash was not found on EC1")) + } + + step("Assertion: EL height grows") + val elBlockAfter = ec1.engineApi.getLastExecutionBlock().explicitGet() + elBlockAfter.height.longValue shouldBe (elParentBlock.height.longValue + 1) + } + + override def beforeAll(): Unit = setupForNativeTokenTransfer() +} diff --git a/consensus-client-it/src/test/scala/units/C2ENativeTokenTransfersViaDepositsTestSuite.scala b/consensus-client-it/src/test/scala/units/C2ENativeTokenTransfersViaDepositsTestSuite.scala new file mode 100644 index 00000000..f04550ce --- /dev/null +++ b/consensus-client-it/src/test/scala/units/C2ENativeTokenTransfersViaDepositsTestSuite.scala @@ -0,0 +1,84 @@ +package units + +import com.wavesplatform.state.IntegerDataEntry +import com.wavesplatform.transaction.TxHelpers +import org.web3j.protocol.core.DefaultBlockParameterName +import units.eth.EthAddress + +class C2ENativeTokenTransfersViaDepositsTestSuite extends BaseDockerTestSuite { + private val clSender = clRichAccount1 + private val elReceiver = elRichAccount1 + private val elReceiverAddress = EthAddress.unsafeFrom(elReceiver.getAddress) + + private val userAmount = 1 + private val clAmount = UnitsConvert.toUnitsInWaves(userAmount) + private val elAmount = UnitsConvert.toWei(userAmount) + + "C2E native token transfers via deposited transactions" in { + def getElBalance: BigInt = ec1.web3j.ethGetBalance(elReceiverAddress.hex, DefaultBlockParameterName.PENDING).send().getBalance + val elBalanceBefore = getElBalance + step(s"elBalanceBefore: $elBalanceBefore") + + waves1.api.broadcastAndWait( + ChainContract.transfer( + sender = clSender, + destElAddress = elReceiverAddress, + asset = chainContract.nativeTokenId, + amount = clAmount + ) + ) + + withClue("Expected amount: ") { + eventually { + val elBalanceAfter = getElBalance + step(s"elBalanceAfter: $elBalanceAfter") + val expectedBalanceAfter = elBalanceBefore + elAmount + UnitsConvert.toUser(elBalanceAfter, NativeTokenElDecimals) shouldBe UnitsConvert.toUser(expectedBalanceAfter, NativeTokenElDecimals) + } + } + } + + override def beforeAll(): Unit = { + + super.beforeAll() + deploySolidityContracts() + + step("Enable token transfers") + val activationEpoch = waves1.api.height() + 1 + waves1.api.broadcastAndWait( + ChainContract.enableTokenTransfersWithWaves( + StandardBridgeAddress, + WWavesAddress, + activationEpoch = activationEpoch + ) + ) + + step("Set strict C2E transfers feature activation epoch") + waves1.api.broadcastAndWait( + TxHelpers.dataEntry( + chainContractAccount, + IntegerDataEntry("strictC2ETransfersActivationEpoch", activationEpoch) + ) + ) + + step("Wait for features activation") + waves1.api.waitForHeight(activationEpoch) + + step("Prepare: issue tokens on chain contract and transfer to a user") + waves1.api.broadcastAndWait( + TxHelpers.reissue( + asset = chainContract.nativeTokenId, + sender = chainContractAccount, + amount = clAmount + ) + ) + waves1.api.broadcastAndWait( + TxHelpers.transfer( + from = chainContractAccount, + to = clSender.toAddress, + amount = clAmount, + asset = chainContract.nativeTokenId + ) + ) + } +} diff --git a/consensus-client-it/src/test/scala/units/ManyTransfersTestSuite.scala b/consensus-client-it/src/test/scala/units/ManyTransfersTestSuite.scala new file mode 100644 index 00000000..b0e736ae --- /dev/null +++ b/consensus-client-it/src/test/scala/units/ManyTransfersTestSuite.scala @@ -0,0 +1,199 @@ +package units + +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.state.IntegerDataEntry +import com.wavesplatform.transaction.Asset.IssuedAsset +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import com.wavesplatform.transaction.{Asset, TxHelpers} +import monix.execution.atomic.AtomicInt +import org.web3j.protocol.core.DefaultBlockParameterName +import org.web3j.protocol.core.methods.response.TransactionReceipt +import org.web3j.tx.RawTransactionManager +import org.web3j.tx.gas.DefaultGasProvider +import units.docker.EcContainer +import units.el.{BridgeMerkleTree, E2CTopics, Erc20Client} +import units.eth.EthAddress + +import scala.jdk.OptionConverters.RichOptional + +class ManyTransfersTestSuite extends BaseDockerTestSuite { + private val clRecipient = clRichAccount1 + private val elSender = elRichAccount1 + private val elSenderAddress = elRichAddress1 + + private val issueAssetDecimals = 8.toByte + private lazy val issueAsset = chainContract.getRegisteredAsset(1) // 0 is WAVES + + private val userAmount = BigDecimal("1") + private val eachTransferTypeCount = 20 + + private val gasProvider = new DefaultGasProvider + private lazy val txnManager = new RawTransactionManager(ec1.web3j, elSender, EcContainer.ChainId, 20, 2000) + private lazy val terc20 = new Erc20Client(ec1.web3j, TErc20Address, txnManager, gasProvider) + + "Many native + many asset transfers" in { + + val currNonce = + AtomicInt(ec1.web3j.ethGetTransactionCount(elSenderAddress.hex, DefaultBlockParameterName.PENDING).send().getTransactionCount.intValueExact()) + def nextNonce: Int = currNonce.getAndIncrement() + + val nativeE2CAmount = UnitsConvert.toAtomic(userAmount * eachTransferTypeCount, NativeTokenElDecimals) + val issuedE2CAmount = UnitsConvert.toAtomic(userAmount * eachTransferTypeCount, TErc20Decimals) + + step("Send allowances") + waitFor(terc20.sendApprove(StandardBridgeAddress, issuedE2CAmount, nextNonce)) + + step("Initiate E2C transfers") + val e2cNativeTxn = nativeBridge.sendSendNative(elSender, clRecipient.toAddress, nativeE2CAmount, nextNonce) + val e2cIssuedTxn = standardBridge.sendBridgeErc20(elSender, TErc20Address, clRecipient.toAddress, issuedE2CAmount, nextNonce) + + chainContract.waitForEpoch(waves1.api.height() + 1) // Bypass rollbacks + val e2cReceipts = List(e2cNativeTxn, e2cIssuedTxn).map { txn => + eventually { + val hash = txn.getTransactionHash + withClue(s"$hash: ") { + ec1.web3j.ethGetTransactionReceipt(hash).send().getTransactionReceipt.toScala.value + } + } + } + + withClue("E2C should be on same height, can't continue the test: ") { + val e2cHeights = e2cReceipts.map(_.getBlockNumber.intValueExact()).toSet + e2cHeights.size shouldBe 1 + } + + val e2cBlockHash = BlockHash(e2cReceipts.head.getBlockHash) + log.debug(s"Block with e2c transfers: $e2cBlockHash") + + val e2cLogsInBlock = ec1.engineApi + .getLogs(e2cBlockHash, List(NativeBridgeAddress, StandardBridgeAddress), Nil) + .explicitGet() + .filter(_.topics.intersect(E2CTopics).nonEmpty) + + withClue("We have logs for all transactions: ") { + e2cLogsInBlock.size shouldBe e2cReceipts.size + } + + step(s"Wait block $e2cBlockHash with transfers on contract") + val e2cBlockConfirmationHeight = eventually { + chainContract.getBlock(e2cBlockHash).value.height + } + + step(s"Wait for block $e2cBlockHash ($e2cBlockConfirmationHeight) finalization") + eventually { + val currFinalizedHeight = chainContract.getFinalizedBlock.height + step(s"Current finalized height: $currFinalizedHeight") + currFinalizedHeight should be >= e2cBlockConfirmationHeight + } + + step("Broadcast withdrawAsset transactions") + val recipientAssetBalanceBefore = clRecipientAssetBalance + val recipientNativeTokenBalanceBefore = clRecipientNativeTokenBalance + + def mkE2CWithdrawTxn(transferIndex: Int, asset: Asset, amount: BigDecimal, decimals: Byte): InvokeScriptTransaction = ChainContract.withdrawAsset( + sender = clRecipient, + blockHash = e2cBlockHash, + merkleProof = BridgeMerkleTree.mkTransferProofs(e2cLogsInBlock, transferIndex).explicitGet().reverse, + transferIndexInBlock = transferIndex, + amount = UnitsConvert.toWavesAtomic(amount, decimals), + asset = asset + ) + + val e2cWithdrawTxns = List( + mkE2CWithdrawTxn(0, chainContract.nativeTokenId, userAmount * eachTransferTypeCount, NativeTokenClDecimals), + mkE2CWithdrawTxn(1, issueAsset, userAmount * eachTransferTypeCount, issueAssetDecimals) + ) + + e2cWithdrawTxns.foreach(waves1.api.broadcast) + e2cWithdrawTxns.foreach(txn => waves1.api.waitForSucceeded(txn.id())) + + withClue("Assets received after E2C: ") { + withClue("Native token: ") { + val balanceAfter = clRecipientNativeTokenBalance + balanceAfter shouldBe (recipientNativeTokenBalanceBefore + UnitsConvert.toWavesAtomic( + userAmount * eachTransferTypeCount, + NativeTokenClDecimals + )) + } + + withClue("Issued asset: ") { + val balanceAfter = clRecipientAssetBalance + balanceAfter shouldBe (recipientAssetBalanceBefore + UnitsConvert.toWavesAtomic(userAmount * eachTransferTypeCount, issueAssetDecimals)) + } + } + + step("Initiate C2E transfers") + val c2eRecipientAddress = EthAddress.unsafeFrom("0xAAAA00000000000000000000000000000000AAAA") + + def mkC2ETransferTxn(asset: Asset, decimals: Byte): InvokeScriptTransaction = ChainContract.transfer( + clRecipient, + c2eRecipientAddress, + asset, + UnitsConvert.toWavesAtomic(userAmount, decimals) + ) + + val c2eTransferTxns = for { + _ <- 1 to eachTransferTypeCount + tx <- Seq( + mkC2ETransferTxn(issueAsset, issueAssetDecimals), + mkC2ETransferTxn(chainContract.nativeTokenId, NativeTokenClDecimals) + ) + } yield tx + + c2eTransferTxns.foreach(waves1.api.broadcast) + val c2eTransferTxnResults = c2eTransferTxns.map(txn => waves1.api.waitForSucceeded(txn.id())) + + withClue("C2E should be on same height, can't continue the test: ") { + val c2eHeights = c2eTransferTxnResults.map(_.height).toSet + c2eHeights.size shouldBe 1 + } + + withClue("Assets received after C2E: ") { + eventually { + withClue("Native token: ") { + val balanceAfter = ec1.web3j.ethGetBalance(c2eRecipientAddress.hex, DefaultBlockParameterName.PENDING).send().getBalance + BigInt(balanceAfter) shouldBe (nativeE2CAmount) + } + + withClue("Issued asset: ") { + terc20.getBalance(c2eRecipientAddress) shouldBe issuedE2CAmount + } + } + } + } + + private def clRecipientAssetBalance: Long = waves1.api.balance(clRecipient.toAddress, issueAsset) + private def clRecipientNativeTokenBalance: Long = waves1.api.balance(clRecipient.toAddress, chainContract.nativeTokenId) + + override def beforeAll(): Unit = { + super.beforeAll() + deploySolidityContracts() + + step("Enable token transfers") + val activationEpoch = waves1.api.height() + 1 + waves1.api.broadcastAndWait( + ChainContract.enableTokenTransfersWithWaves( + StandardBridgeAddress, + WWavesAddress, + activationEpoch = activationEpoch + ) + ) + + step("Set strict C2E transfers feature activation epoch") + waves1.api.broadcastAndWait( + TxHelpers.dataEntry( + chainContractAccount, + IntegerDataEntry("strictC2ETransfersActivationEpoch", activationEpoch) + ) + ) + + step("Wait for features activation") + waves1.api.waitForHeight(activationEpoch) + + step("Register asset") + waves1.api.broadcastAndWait(ChainContract.issueAndRegister(TErc20Address, TErc20Decimals, "TERC20", "Test ERC20 token", issueAssetDecimals)) + eventually { + standardBridge.isRegistered(TErc20Address, ignoreExceptions = true) shouldBe true + } + } +} diff --git a/consensus-client-it/src/test/scala/units/MultipleTransfersViaDepositsTestSuite.scala b/consensus-client-it/src/test/scala/units/MultipleTransfersViaDepositsTestSuite.scala new file mode 100644 index 00000000..0c86c5f7 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/MultipleTransfersViaDepositsTestSuite.scala @@ -0,0 +1,212 @@ +package units + +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.state.IntegerDataEntry +import com.wavesplatform.transaction.Asset.IssuedAsset +import com.wavesplatform.transaction.{Asset, TxHelpers} +import com.wavesplatform.transaction.smart.InvokeScriptTransaction +import monix.execution.atomic.AtomicInt +import org.web3j.protocol.core.DefaultBlockParameterName +import org.web3j.protocol.core.methods.response.{EthSendTransaction, TransactionReceipt} +import org.web3j.tx.RawTransactionManager +import org.web3j.tx.gas.DefaultGasProvider +import units.client.contract.HasConsensusLayerDappTxHelpers.DefaultFees +import units.docker.EcContainer +import units.el.{BridgeMerkleTree, E2CTopics, Erc20Client} +import units.eth.EthAddress + +import scala.jdk.OptionConverters.RichOptional + +class MultipleTransfersViaDepositsTestSuite extends BaseDockerTestSuite { + private val clRecipient = clRichAccount1 + private val elSender = elRichAccount1 + private val elSenderAddress = elRichAddress1 + + private val issueAssetDecimals = 8.toByte + private lazy val issueAsset = chainContract.getRegisteredAsset(1) // 0 is WAVES + + private val userAmount = BigDecimal("1") + + private val gasProvider = new DefaultGasProvider + private lazy val txnManager = new RawTransactionManager(ec1.web3j, elSender, EcContainer.ChainId, 20, 2000) + private lazy val wwaves = new Erc20Client(ec1.web3j, WWavesAddress, txnManager, gasProvider) + private lazy val terc20 = new Erc20Client(ec1.web3j, TErc20Address, txnManager, gasProvider) + + "Checking balances in E2C2E transfers" in { + val currNonce = + AtomicInt(ec1.web3j.ethGetTransactionCount(elSenderAddress.hex, DefaultBlockParameterName.PENDING).send().getTransactionCount.intValueExact()) + def nextNonce: Int = currNonce.getAndIncrement() + + val nativeE2CAmount = UnitsConvert.toAtomic(userAmount, NativeTokenElDecimals) + val issuedE2CAmount = UnitsConvert.toAtomic(userAmount, TErc20Decimals) + val wavesE2CAmount = UnitsConvert.toAtomic(userAmount, WwavesDecimals) + + step("Send allowances") + List( + terc20.sendApprove(StandardBridgeAddress, issuedE2CAmount, nextNonce), + wwaves.sendApprove(StandardBridgeAddress, wavesE2CAmount, nextNonce) + ).foreach(waitFor) + + step("Initiate E2C transfers") + val e2cNativeTxn = nativeBridge.sendSendNative(elSender, clRecipient.toAddress, nativeE2CAmount, nextNonce) + val e2cIssuedTxn = standardBridge.sendBridgeErc20(elSender, TErc20Address, clRecipient.toAddress, issuedE2CAmount, nextNonce) + val e2cWavesTxn = standardBridge.sendBridgeErc20(elSender, WWavesAddress, clRecipient.toAddress, wavesE2CAmount, nextNonce) + + chainContract.waitForEpoch(waves1.api.height() + 1) // Bypass rollbacks + val e2cReceipts = List(e2cNativeTxn, e2cIssuedTxn, e2cWavesTxn).map { txn => + eventually { + val hash = txn.getTransactionHash + withClue(s"$hash: ") { + ec1.web3j.ethGetTransactionReceipt(hash).send().getTransactionReceipt.toScala.value + } + } + } + + withClue("E2C should be on same height, can't continue the test: ") { + val e2cHeights = e2cReceipts.map(_.getBlockNumber.intValueExact()).toSet + e2cHeights.size shouldBe 1 + } + + val e2cBlockHash = BlockHash(e2cReceipts.head.getBlockHash) + log.debug(s"Block with e2c transfers: $e2cBlockHash") + + val e2cLogsInBlock = ec1.engineApi + .getLogs(e2cBlockHash, List(NativeBridgeAddress, StandardBridgeAddress), Nil) + .explicitGet() + .filter(_.topics.intersect(E2CTopics).nonEmpty) + + withClue("We have logs for all transactions: ") { + e2cLogsInBlock.size shouldBe e2cReceipts.size + } + + step(s"Wait block $e2cBlockHash with transfers on contract") + val e2cBlockConfirmationHeight = eventually { + chainContract.getBlock(e2cBlockHash).value.height + } + + step(s"Wait for block $e2cBlockHash ($e2cBlockConfirmationHeight) finalization") + eventually { + val currFinalizedHeight = chainContract.getFinalizedBlock.height + step(s"Current finalized height: $currFinalizedHeight") + currFinalizedHeight should be >= e2cBlockConfirmationHeight + } + + step("Broadcast withdrawAsset transactions") + val recipientWavesBalanceBefore = clRecipientWavesBalance + val recipientAssetBalanceBefore = clRecipientAssetBalance + val recipientNativeTokenBalanceBefore = clRecipientNativeTokenBalance + + def mkE2CWithdrawTxn(transferIndex: Int, asset: Asset, decimals: Byte): InvokeScriptTransaction = ChainContract.withdrawAsset( + sender = clRecipient, + blockHash = e2cBlockHash, + merkleProof = BridgeMerkleTree.mkTransferProofs(e2cLogsInBlock, transferIndex).explicitGet().reverse, + transferIndexInBlock = transferIndex, + amount = UnitsConvert.toWavesAtomic(userAmount, decimals), + asset = asset + ) + + val e2cWithdrawTxns = List( + mkE2CWithdrawTxn(0, chainContract.nativeTokenId, NativeTokenClDecimals), + mkE2CWithdrawTxn(1, issueAsset, issueAssetDecimals), + mkE2CWithdrawTxn(2, Asset.Waves, WavesDecimals) + ) + + e2cWithdrawTxns.foreach(waves1.api.broadcast) + e2cWithdrawTxns.foreach(txn => waves1.api.waitForSucceeded(txn.id())) + + withClue("Assets received after E2C: ") { + withClue("Native token: ") { + val balanceAfter = clRecipientNativeTokenBalance + balanceAfter shouldBe (recipientNativeTokenBalanceBefore + UnitsConvert.toWavesAtomic(userAmount, NativeTokenClDecimals)) + } + + withClue("Issued asset: ") { + val balanceAfter = clRecipientAssetBalance + balanceAfter shouldBe (recipientAssetBalanceBefore + UnitsConvert.toWavesAtomic(userAmount, issueAssetDecimals)) + } + + withClue("WAVES: ") { + val balanceAfter = clRecipientWavesBalance + val fee = DefaultFees.ChainContract.withdrawFee * e2cWithdrawTxns.size + balanceAfter shouldBe (recipientWavesBalanceBefore + UnitsConvert.toWavesAtomic(userAmount, WavesDecimals) - fee) + } + } + + step("Initiate C2E transfers") + val c2eRecipientAddress = EthAddress.unsafeFrom("0xAAAA00000000000000000000000000000000AAAA") + + def mkC2ETransferTxn(asset: Asset, decimals: Byte): InvokeScriptTransaction = ChainContract.transfer( + clRecipient, + c2eRecipientAddress, + asset, + UnitsConvert.toWavesAtomic(userAmount, decimals) + ) + + val c2eTransferTxns = List( + mkC2ETransferTxn(chainContract.nativeTokenId, NativeTokenClDecimals), + mkC2ETransferTxn(issueAsset, issueAssetDecimals), + mkC2ETransferTxn(Asset.Waves, WavesDecimals) + ) + + c2eTransferTxns.foreach(waves1.api.broadcast) + val c2eTransferTxnResults = c2eTransferTxns.map(txn => waves1.api.waitForSucceeded(txn.id())) + + withClue("C2E should be on same height, can't continue the test: ") { + val c2eHeights = c2eTransferTxnResults.map(_.height).toSet + c2eHeights.size shouldBe 1 + } + + withClue("Assets received after C2E: ") { + eventually { + withClue("Native token: ") { + val balanceAfter = ec1.web3j.ethGetBalance(c2eRecipientAddress.hex, DefaultBlockParameterName.PENDING).send().getBalance + BigInt(balanceAfter) shouldBe nativeE2CAmount + } + + withClue("Issued asset: ") { + terc20.getBalance(c2eRecipientAddress) shouldBe issuedE2CAmount + } + + withClue("WAVES: ") { + wwaves.getBalance(c2eRecipientAddress) shouldBe wavesE2CAmount + } + } + } + } + + private def clRecipientWavesBalance: Long = waves1.api.balance(clRecipient.toAddress, Asset.Waves) + private def clRecipientAssetBalance: Long = waves1.api.balance(clRecipient.toAddress, issueAsset) + private def clRecipientNativeTokenBalance: Long = waves1.api.balance(clRecipient.toAddress, chainContract.nativeTokenId) + + override def beforeAll(): Unit = { + super.beforeAll() + deploySolidityContracts() + + step("Enable token transfers") + val activationEpoch = waves1.api.height() + 1 + waves1.api.broadcastAndWait( + ChainContract.enableTokenTransfersWithWaves( + StandardBridgeAddress, + WWavesAddress, + activationEpoch = activationEpoch + ) + ) + + step("Set strict C2E transfers feature activation epoch") + waves1.api.broadcastAndWait( + TxHelpers.dataEntry( + chainContractAccount, + IntegerDataEntry("strictC2ETransfersActivationEpoch", activationEpoch) + ) + ) + + step("Wait for features activation") + waves1.api.waitForHeight(activationEpoch) + + step("Register asset") + waves1.api.broadcastAndWait(ChainContract.issueAndRegister(TErc20Address, TErc20Decimals, "TERC20", "Test ERC20 token", issueAssetDecimals)) + eventually { + standardBridge.isRegistered(TErc20Address, ignoreExceptions = true) shouldBe true + } + } +} diff --git a/consensus-client-it/src/test/scala/units/TestNetworkClient.scala b/consensus-client-it/src/test/scala/units/TestNetworkClient.scala new file mode 100644 index 00000000..28c94de1 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/TestNetworkClient.scala @@ -0,0 +1,30 @@ +package units + +import com.wavesplatform.account.Address +import com.wavesplatform.network.PeerDatabase +import com.wavesplatform.network.client.NetworkClient +import units.docker.WavesNodeContainer +import units.network.{BlockSpec, LegacyFrameCodec, RawBytes} + +import java.net.InetSocketAddress +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.* + +object TestNetworkClient { + def send(node: WavesNodeContainer, chainContractAddress: Address, block: NetworkL2Block): Unit = { + val applicationName: String = "wavesl2-" + chainContractAddress.toString.substring(0, 8) + val client = NetworkClient(applicationName, frameCodec = new LegacyFrameCodec(PeerDatabase.NoOp)) + val blockMessage = RawBytes(BlockSpec.messageCode, BlockSpec.serializeData(block)) + + val f = client + .connect(new InetSocketAddress("localhost", node.unitsNetworkPort)) + .map { _.writeAndFlush(blockMessage) } + .andThen { case _ => + client.shutdown() + } + + Await.result(f, 20.seconds) + + } +} 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 10accc29..a45be460 100644 --- a/consensus-client-it/src/test/scala/units/docker/DockerImages.scala +++ b/consensus-client-it/src/test/scala/units/docker/DockerImages.scala @@ -5,5 +5,5 @@ import units.test.TestEnvironment.WavesDockerImage object DockerImages { val WavesNode = parse(WavesDockerImage) - val OpGethExecutionClient = parse("ghcr.io/unitsnetwork/op-geth:v1.101503.1-simulate-fixes") + val OpGethExecutionClient = parse("ghcr.io/unitsnetwork/op-geth:v1.101603.0-1") } diff --git a/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala b/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala index 61e5fc84..6c450967 100644 --- a/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala +++ b/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala @@ -35,7 +35,7 @@ class WavesNodeContainer( protected override val container = new GenericContainer(DockerImages.WavesNode) .withNetwork(network) - .withExposedPorts(ApiPort) + .withExposedPorts(ApiPort, UnitsNetworkPort) .withEnv( Map( "NODE_NUMBER" -> s"$number", @@ -67,14 +67,16 @@ class WavesNodeContainer( } lazy val apiPort = container.getMappedPort(ApiPort) + lazy val unitsNetworkPort = container.getMappedPort(UnitsNetworkPort) lazy val api = new NodeHttpApi(uri"http://${container.getHost}:$apiPort", httpClientBackend) - override def logPorts(): Unit = log.debug(s"External host: ${container.getHost}, api: $apiPort") + override def logPorts(): Unit = log.debug(s"External host: ${container.getHost}, api: $apiPort, units network: $unitsNetworkPort") } object WavesNodeContainer { val ApiPort = 6869 + val UnitsNetworkPort = 6865 val GenesisTemplateFile = new File(s"$ConfigsDir/wavesnode/genesis-template.conf") val GenesisTemplate = ConfigFactory.parseFile(GenesisTemplateFile) diff --git a/contracts/eth/src/SafeCall.sol b/contracts/eth/src/SafeCall.sol new file mode 100644 index 00000000..775f39ad --- /dev/null +++ b/contracts/eth/src/SafeCall.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +/// @title SafeCall +/// @notice Perform low level safe calls +library SafeCall { + /// @notice Perform a low level call without copying any returndata + /// @param _target Address to call + /// @param _gas Amount of gas to pass to the call + /// @param _value Amount of value to pass to the call + /// @param _calldata Calldata to pass to the call + function call( + address _target, + uint256 _gas, + uint256 _value, + bytes memory _calldata + ) + internal + returns (bool success_) + { + assembly { + success_ := + call( + _gas, // gas + _target, // recipient + _value, // ether value + add(_calldata, 32), // inloc + mload(_calldata), // inlen + 0, // outloc + 0 // outlen + ) + } + } +} diff --git a/contracts/eth/src/StandardBridge.sol b/contracts/eth/src/StandardBridge.sol index 31944c28..5ad29c39 100644 --- a/contracts/eth/src/StandardBridge.sol +++ b/contracts/eth/src/StandardBridge.sol @@ -7,6 +7,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IUnitsMintableERC20} from "@units/IUnitsMintableERC20.sol"; +import {SafeCall} from "@units/SafeCall.sol"; contract StandardBridge { using SafeERC20 for IERC20; @@ -15,6 +16,12 @@ contract StandardBridge { mapping(address => uint256) public _unusedDeposits; mapping(address => uint256) public tokenRatios; + event ETHBridgeFinalized( + address indexed from, + address indexed elTo, + uint256 amount + ); + event ERC20BridgeInitiated( address indexed localToken, address indexed from, @@ -29,6 +36,7 @@ contract StandardBridge { uint256 amount ); + event RegistryUpdated(address[] addedTokens, uint8[] addedTokenExponents, address[] removedTokens); /// @notice Ensures that the caller is an empty address. @@ -63,24 +71,57 @@ contract StandardBridge { return ERC165Checker.supportsInterface(_token, type(IUnitsMintableERC20).interfaceId); } - /// @notice Emits the ERC20BridgeInitiated event and if necessary the appropriate legacy - /// event when an ERC20 bridge is initiated to the other chain. + + /// @notice Finalizes a native token bridge on this chain. Can only be triggered by the + /// Chain contract on the remote chain. + /// @param _from Address of the sender. + /// @param _to Address of the receiver. + /// @param _amount Amount of the native token being bridged. + function finalizeBridgeETH( + address _from, + address _to, + uint256 _amount + ) public payable onlyMiner { + require( + msg.value == _amount, + "StandardBridge: amount sent does not match amount required" + ); + require(_to != address(this), "StandardBridge: cannot send to self"); + + _emitETHBridgeFinalized(_from, _to, _amount); + + bool success = SafeCall.call(_to, gasleft(), _amount, hex""); + require(success, "StandardBridge: ETH transfer failed"); + } + + /// @notice Finalizes an ERC20 bridge on this chain. Can only be triggered by the other + /// StandardBridge contract on the remote chain. /// @param _localToken Address of the ERC20 on this chain. /// @param _from Address of the sender. /// @param _to Address of the receiver. - /// @param _amount Amount of the ERC20 sent. - function _emitERC20BridgeInitiated( + /// @param _amount Amount of the ERC20 being bridged. + function finalizeBridgeERC20( address _localToken, address _from, address _to, uint256 _amount ) - internal - virtual + public + onlyMiner { - emit ERC20BridgeInitiated(_localToken, _from, _to, int64(uint64(_amount))); + if (_isUnitsMintableERC20(_localToken)) { + IUnitsMintableERC20(_localToken).mint(_to, _amount); + } else { + // deposits[_localToken] -= _amount; + IERC20(_localToken).safeTransfer(_to, _amount); + } + + // Emit the correct events. By default this will be ERC20BridgeFinalized, but child + // contracts may override this function in order to emit legacy events as well. + _emitERC20BridgeFinalized(_localToken, _from, _to, _amount); } + /// @notice Sends ERC20 tokens to a receiver's address on the other chain. /// @param _localToken Address of the ERC20 on this chain. /// @param _to Address of the receiver. @@ -118,31 +159,37 @@ contract StandardBridge { _emitERC20BridgeInitiated(_localToken, _from, _to, clAmount); } - /// @notice Finalizes an ERC20 bridge on this chain. Can only be triggered by the other - /// StandardBridge contract on the remote chain. + /// @notice Emits the ETHBridgeFinalized event. + /// @param _from Address of the sender. + /// @param _to Address of the receiver. + /// @param _amount Amount of the ETH sent. + function _emitETHBridgeFinalized( + address _from, + address _to, + uint256 _amount + ) + internal + virtual + { + emit ETHBridgeFinalized(_from, _to, _amount); + } + + /// @notice Emits the ERC20BridgeInitiated event and if necessary the appropriate legacy + /// event when an ERC20 bridge is initiated to the other chain. /// @param _localToken Address of the ERC20 on this chain. /// @param _from Address of the sender. /// @param _to Address of the receiver. - /// @param _amount Amount of the ERC20 being bridged. - function finalizeBridgeERC20( + /// @param _amount Amount of the ERC20 sent. + function _emitERC20BridgeInitiated( address _localToken, address _from, address _to, uint256 _amount ) - public - onlyMiner + internal + virtual { - if (_isUnitsMintableERC20(_localToken)) { - IUnitsMintableERC20(_localToken).mint(_to, _amount); - } else { - // deposits[_localToken] -= _amount; - IERC20(_localToken).safeTransfer(_to, _amount); - } - - // Emit the correct events. By default this will be ERC20BridgeFinalized, but child - // contracts may override this function in order to emit legacy events as well. - _emitERC20BridgeFinalized(_localToken, _from, _to, _amount); + emit ERC20BridgeInitiated(_localToken, _from, _to, int64(uint64(_amount))); } /// @notice Emits the ERC20BridgeFinalized event and if necessary the appropriate legacy diff --git a/local-network/all-images-build.sh b/local-network/all-images-build.sh index c844c335..25312c17 100755 --- a/local-network/all-images-build.sh +++ b/local-network/all-images-build.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -eu + DIR="$(cd "$(dirname "$0")" && pwd)" cd "${DIR}" || exit diff --git a/local-network/consensus_client-image-build.sh b/local-network/consensus_client-image-build.sh index ef60ac80..2d0e24ac 100755 --- a/local-network/consensus_client-image-build.sh +++ b/local-network/consensus_client-image-build.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -eu + DIR="$(cd "$(dirname "$0")" && pwd)" cd "${DIR}/.." || exit diff --git a/local-network/deploy/deploy.py b/local-network/deploy/deploy.py index e32818d7..5d25c94e 100755 --- a/local-network/deploy/deploy.py +++ b/local-network/deploy/deploy.py @@ -43,7 +43,7 @@ os.path.join(contracts_dir, "waves", "src", "main.ride"), "r", encoding="utf-8" ) as file: source = file.read() - r = network.cl_chain_contract.setScript(source, txFee=5_700_000) + r = network.cl_chain_contract.setScript(source, txFee=7_000_000) waves.force_success(log, r, "Can not set the chain contract script") if not network.cl_chain_contract.isContractSetup(): @@ -100,7 +100,7 @@ for txn_id in txn_ids: waves.wait_for_approval(log, txn_id) - log.info(f"Distrubute UNIT0 tokens transaction {txn_id} confirmed") + log.info(f"Distribute UNIT0 tokens transaction {txn_id} confirmed") r = network.cl_chain_contract.evaluate("allMiners") joined_miners = [] @@ -161,41 +161,55 @@ log.error(f"Execution failed: {e}") exit(1) -key = "assetTransfersActivationEpoch" -if len(network.cl_chain_contract.getData(regex="assetTransfersActivationEpoch")) == 0: +features_to_activate = [] + +key_asset_transfers = "assetTransfersActivationEpoch" +if len(network.cl_chain_contract.getData(regex=key_asset_transfers)) == 0: + features_to_activate.append("asset_transfers") + +key_strict_transfers = "strictC2ETransfersActivationEpoch" +if len(network.cl_chain_contract.getData(regex=key_strict_transfers)) == 0: + features_to_activate.append("strict_transfers") + +if features_to_activate: activation_height = pw.height() + 2 - enable_transfers_txn = network.cl_chain_contract.oracleAcc.invokeScript( - dappAddress=network.cl_chain_contract.oracleAddress, - functionName="enableTokenTransfers", - params=[ - {"type": "string", "value": network.el_standard_bridge_address}, - {"type": "string", "value": network.el_wwaves_address}, - {"type": "integer", "value": activation_height}, - ], - txFee=900_000, - ) - strict_transfers_txn = network.cl_chain_contract.oracleAcc.dataTransaction( - [ - { - "type": "integer", - "key": "strictC2ETransfersActivationEpoch", - "value": activation_height, - } - ] - ) - waves.force_success( - log, - enable_transfers_txn, - "Could not enable token transfers", - wait=True, - ) - waves.force_success( - log, - strict_transfers_txn, - "Could not enable strict transfers", - wait=True, - ) - log.info("Wait activation") + if "asset_transfers" in features_to_activate: + log.info("Activating asset transfers...") + enable_transfers_txn = network.cl_chain_contract.oracleAcc.invokeScript( + dappAddress=network.cl_chain_contract.oracleAddress, + functionName="enableTokenTransfers", + params=[ + {"type": "string", "value": network.el_standard_bridge_address}, + {"type": "string", "value": network.el_wwaves_address}, + {"type": "integer", "value": activation_height}, + ], + txFee=900_000, + ) + waves.force_success( + log, + enable_transfers_txn, + "Could not enable token transfers", + wait=True, + ) + + if "strict_transfers" in features_to_activate: + strict_transfers_txn = network.cl_chain_contract.oracleAcc.dataTransaction( + [ + { + "type": "integer", + "key": "strictC2ETransfersActivationEpoch", + "value": activation_height, + } + ] + ) + waves.force_success( + log, + strict_transfers_txn, + "Could not enable strict transfers", + wait=True, + ) + + log.info("Waiting for activation height...") while pw.height() < activation_height: sleep(3) diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala index 2642b0cd..c712aade 100644 --- a/src/main/scala/units/ELUpdater.scala +++ b/src/main/scala/units/ELUpdater.scala @@ -15,7 +15,7 @@ import com.wavesplatform.state.{Blockchain, BooleanDataEntry} import com.wavesplatform.transaction.TxValidationError.InvokeRejectError import com.wavesplatform.transaction.smart.InvokeScriptTransaction import com.wavesplatform.transaction.smart.script.trace.TracedResult -import com.wavesplatform.transaction.{Asset, Proofs, Transaction, TransactionSignOps, TransactionType, TxPositiveAmount, TxVersion} +import com.wavesplatform.transaction.* import com.wavesplatform.utils.{Time, UnsupportedFeature, forceStopApplication} import com.wavesplatform.wallet.Wallet import io.netty.channel.Channel @@ -191,53 +191,28 @@ class ELUpdater( .map(Withdrawal(startElWithdrawalIndex, _, chainContractOptions.miningReward)) .toVector + val strictC2ETransfersActivated = epochInfo.number >= chainContractClient.getStrictC2ETransfersActivationEpoch + val transfers = chainContractClient.getTransfersForPayload( fromIndex = startC2ETransferIndex, - maxNative = MaxC2ENativeTransfers - rewardWithdrawal.size + maxNative = if strictC2ETransfersActivated then None else Some(MaxWithdrawals - rewardWithdrawal.size) ) - val (nativeTransfers, assetTransfers) = transfers.partitionMap { - case x: ContractTransfer.Native => x.asLeft - case x: ContractTransfer.Asset => x.asRight + val nativeTransfersViaWithdrawals = transfers.flatMap { + case x: ContractTransfer.NativeViaWithdrawal => Some(x) + case _: ContractTransfer.NativeViaDeposit => None + case _: ContractTransfer.Asset => None } - val nativeTransferWithdrawals = toWithdrawals(nativeTransfers, rewardWithdrawal.lastOption.fold(startElWithdrawalIndex)(_.index + 1)) - val withdrawals = rewardWithdrawal ++ nativeTransferWithdrawals + val nativeTransferWithdrawals = + if strictC2ETransfersActivated + then Vector.empty + else toWithdrawals(nativeTransfersViaWithdrawals, rewardWithdrawal.lastOption.fold(startElWithdrawalIndex)(_.index + 1)) - val (addedAssets, updateAssetRegistryTransaction) = - if (epochInfo.number < chainContractOptions.assetTransfersActivationEpoch) (Nil, None) - else { - val startAssetRegistryIndex = lastAssetRegistryIndex + 1 - val assetRegistrySize = chainContractClient.getAssetRegistrySize - val addedAssets = - if (startAssetRegistryIndex == assetRegistrySize) Nil - else chainContractClient.getRegisteredAssets(startAssetRegistryIndex until assetRegistrySize) + val withdrawals = rewardWithdrawal ++ nativeTransferWithdrawals - val txn = - if (addedAssets.isEmpty) None - else - chainContractOptions.elStandardBridgeAddress.map { sba => - StandardBridge.mkUpdateAssetRegistryTransaction( - standardBridgeAddress = sba, - addedTokenExponents = addedAssets.map(_.exponent), - addedTokens = addedAssets.map(_.erc20Address) - ) - } - - (addedAssets, txn) - } - - val depositedTransactions = updateAssetRegistryTransaction.toVector ++ (for { - sba <- chainContractOptions.elStandardBridgeAddress.toVector - x <- assetTransfers - } yield StandardBridge.mkFinalizeBridgeErc20Transaction( - transferIndex = x.index, - standardBridgeAddress = sba, - token = x.tokenAddress, - to = x.to, - from = x.from, - amount = x.amount - )) + val (depositedTransactions, addedAssets, updateAssetRegistryTransaction, nativeTransfersViaDeposits, assetTransfers) = + prepareTransactions(epochInfo.number, chainContractOptions, lastAssetRegistryIndex + 1, chainContractClient.getAssetRegistrySize, transfers) val prevRandao = calculateRandao(epochInfo.hitSource, parentBlock.hash) @@ -250,7 +225,8 @@ class ELUpdater( s"of epoch ${epochInfo.number} (ref=${parentBlock.hash})" + (if (withdrawals.isEmpty) "" else s", ${withdrawals.size} withdrawals from EL index=$startElWithdrawalIndex") + (if (transfers.isEmpty) "" else s", total ${transfers.size} transfers from $startC2ETransferIndex") + - (if (nativeTransfers.isEmpty) "" else s", ${nativeTransfers.size} native") + + (if (nativeTransfersViaWithdrawals.isEmpty) "" else s", ${nativeTransfersViaWithdrawals.size} native via withdrawals") + + (if (nativeTransfersViaDeposits.isEmpty) "" else s", ${nativeTransfersViaDeposits.size} native via deposits") + (if (assetTransfers.isEmpty) "" else s", ${assetTransfers.size} asset transfers") + updateAssetRegistryTransaction.fold("")(_ => s", ${addedAssets.size} new assets: {${addedAssets.mkString(", ")}}") ) @@ -280,7 +256,8 @@ class ELUpdater( s"of epoch ${epochInfo.number} (ref=${parentBlock.hash})" + (if (withdrawals.isEmpty) "" else s", ${withdrawals.size} withdrawals from EL index=$startElWithdrawalIndex") + (if (transfers.isEmpty) "" else s", total ${transfers.size} transfers from $startC2ETransferIndex") + - (if (nativeTransfers.isEmpty) "" else s", ${nativeTransfers.size} native") + + (if (nativeTransfersViaWithdrawals.isEmpty) "" else s", ${nativeTransfersViaWithdrawals.size} native via withdrawals") + + (if (nativeTransfersViaDeposits.isEmpty) "" else s", ${nativeTransfersViaDeposits.size} native via deposits") + (if (assetTransfers.isEmpty) "" else s", ${assetTransfers.size} asset transfers") + updateAssetRegistryTransaction.fold("")(_ => s", ${addedAssets.size} new assets: {${addedAssets.mkString(", ")}}") ) @@ -318,7 +295,7 @@ class ELUpdater( case Some(r) => Right(r) case None => if (parentBlock.height - 1 <= EthereumConstants.GenesisBlockHeight) Right(-1L) - else getLastWithdrawalIndex(parentBlock.parentHash) + else engineApiClient.getLastWithdrawalIndex(parentBlock.parentHash) } lastEcBlock <- engineApiClient.getLastExecutionBlock() willSimulateBlock = lastEcBlock.hash != parentBlock.hash @@ -1228,29 +1205,15 @@ class ELUpdater( } } - private def toWithdrawals(transfers: Vector[ContractTransfer.Native], firstElBlockWithdrawalIndex: Long): Vector[Withdrawal] = + private def toWithdrawals(transfers: Vector[ContractTransfer.NativeViaWithdrawal], firstElBlockWithdrawalIndex: Long): Vector[Withdrawal] = transfers.zipWithIndex.map { case (x, i) => val index = firstElBlockWithdrawalIndex + i toWithdrawal(x, index) } - private def toWithdrawal(transfer: ContractTransfer.Native, ecBlockWithdrawalIndex: Long): Withdrawal = + private def toWithdrawal(transfer: ContractTransfer.NativeViaWithdrawal, ecBlockWithdrawalIndex: Long): Withdrawal = Withdrawal(ecBlockWithdrawalIndex, transfer.to, NativeBridge.clToGweiNativeTokenAmount(transfer.amount)) - @tailrec - private def getLastWithdrawalIndex(hash: BlockHash): JobResult[WithdrawalIndex] = - engineApiClient.getBlockByHash(hash) match { - case Left(e) => Left(e) - case Right(None) => Left(ClientError(s"Can't find $hash block on EC during withdrawal search")) - case Right(Some(ecBlock)) => - ecBlock.withdrawals.lastOption match { - case Some(lastWithdrawal) => Right(lastWithdrawal.index) - case None => - if (ecBlock.height == 0) Right(-1L) - else getLastWithdrawalIndex(ecBlock.parentHash) - } - } - private def validateAssetRegistryUpdate( ecBlockLogs: List[GetLogsResponseEntry], contractBlock: ContractBlock, @@ -1364,30 +1327,8 @@ class ELUpdater( ): JobResult[Option[WithdrawalIndex]] = for { blockJsonE <- engineApiClient.getBlockByHashJson(ecBlock.hash, fullTransactionObjects = true) blockJson <- blockJsonE.toRight(ClientError(s"Can't find EC block ${ecBlock.hash} transactions")) - _ <- (blockJson \ "transactions").asOpt[Seq[JsObject]].getOrElse(Seq.empty).traverse { txJson => - DepositedTransaction - .parseValidDepositedTransaction(txJson) - .leftMap(ClientError(_)) - .flatMap { - case None => Right(()) - case Some(tx) => - Either.raiseUnless( - options.elStandardBridgeAddress.isDefined && - tx.to == options.elStandardBridgeAddress && - tx.mint == BigInteger.ZERO && - tx.value == BigInteger.ZERO - ) { - ClientError(s"Transaction not allowed, to: ${tx.to}, standard bridge address: ${options.elStandardBridgeAddress}") - } - } - } - elWithdrawalIndexBefore <- fullValidationStatus.checkedLastElWithdrawalIndex(ecBlock.parentHash) match { - case Some(r) => Right(r) - case None => - if (ecBlock.height - 1 <= EthereumConstants.GenesisBlockHeight) Right(-1L) - else getLastWithdrawalIndex(ecBlock.parentHash) - } + strictC2ETransfersActivated = contractBlock.epoch >= chainContractClient.getStrictC2ETransfersActivationEpoch parentContractBlock = chainContractClient .getBlock(contractBlock.parentHash) @@ -1404,28 +1345,65 @@ class ELUpdater( else (xs.init, xs.lastOption) } - expectedNativeTransfersNumber = expectedTransfers.count { - case _: ContractTransfer.Native => true - case _ => false + actualDepositedTransactions <- (blockJson \ "transactions") + .asOpt[Seq[JsObject]] + .getOrElse(Seq.empty) + .traverse { txJson => + DepositedTransaction + .parseValidDepositedTransaction(txJson) + .leftMap(ClientError(_)) + } + .map(_.flatten.toVector) + + _ <- + if strictC2ETransfersActivated + then { + val (expectedDepositedTransactions, _, _, _, _) = prepareTransactions( + contractBlock.epoch, + options, + parentContractBlock.lastAssetRegistryIndex + 1, + contractBlock.lastAssetRegistryIndex + 1, + expectedTransfers + ) + Either.raiseUnless(expectedDepositedTransactions == actualDepositedTransactions)( + ClientError(s"Block is not valid, expected and actual deposited transactions don't match.") + ) + } else + actualDepositedTransactions.traverse { tx => + Either.raiseUnless( + options.elStandardBridgeAddress.isDefined && + tx.to == options.elStandardBridgeAddress && + tx.mint == BigInteger.ZERO && tx.value == BigInteger.ZERO + ) { + ClientError( + s"Transaction not allowed, to: ${tx.to}, mint: ${tx.mint}, value: ${tx.value}, standard bridge address: ${options.elStandardBridgeAddress}" + ) + } + } + + elWithdrawalIndexBefore <- fullValidationStatus.checkedLastElWithdrawalIndex(ecBlock.parentHash) match { + case Some(r) => Right(r) + case None => + if (ecBlock.height - 1 <= EthereumConstants.GenesisBlockHeight) Right(-1L) + else engineApiClient.getLastWithdrawalIndex(ecBlock.parentHash) } + expectedNativeTransfersNumber = + if strictC2ETransfersActivated then 0 + else + expectedTransfers.count { + case _: ContractTransfer.NativeViaWithdrawal => true + case _: ContractTransfer.NativeViaDeposit => false + case _: ContractTransfer.Asset => false + } + // Checks for maximum transfers processing _ <- nextTransfer match { case None => Either.unit case Some(nextTransfer) => - val strictC2ETransfersActivated = contractBlock.epoch >= chainContractClient.getStrictC2ETransfersActivationEpoch - if (nextTransfer.epoch >= contractBlock.epoch || !strictC2ETransfersActivated) Either.unit - else { // This transfer was on a previous epoch, miner saw it - val blockHasMaxTransfers = nextTransfer match { - case _: ContractTransfer.Asset => false // Could add an asset transfer even it took maximum native transfers - case _: ContractTransfer.Native => - // Could not take a native transfer only if there were no free slots - val maxNativeTransfersInBlock = EcBlock.MaxWithdrawals - miningReward.size - expectedNativeTransfersNumber == maxNativeTransfersInBlock - } - - Either.raiseUnless(blockHasMaxTransfers)(ClientError(s"Block should contain a next C2E transfer: $nextTransfer")) - } + Either.raiseUnless(nextTransfer.epoch >= contractBlock.epoch || !strictC2ETransfersActivated)( + ClientError(s"Block should contain a next C2E transfer: $nextTransfer") + ) } (prevWithdrawalIndex, actualTransferWithdrawals) <- { @@ -1449,15 +1427,91 @@ class ELUpdater( lastElWithdrawalIndex <- { val c2eLogs = ecBlockLogs.filter(_.topics.intersect(C2ETopics).nonEmpty) - validateC2ETransfers(actualTransferWithdrawals, c2eLogs, expectedTransfers, prevWithdrawalIndex).leftMap(ClientError.apply) + validateC2ETransfers(actualTransferWithdrawals, c2eLogs, expectedTransfers, prevWithdrawalIndex, strictC2ETransfersActivated).leftMap( + ClientError.apply + ) } } yield Some(lastElWithdrawalIndex) + private def prepareTransactions( + epochNumber: Int, + chainContractOptions: ChainContractOptions, + startAssetRegistryIndex: Int, + endAssetRegistryIndexExcl: Int, + transfers: Vector[ContractTransfer] + ): ( + Vector[DepositedTransaction], + List[ChainContractClient.Registry.RegisteredAsset], + Option[DepositedTransaction], + Vector[ContractTransfer.NativeViaDeposit], + Vector[ContractTransfer.Asset] + ) = { + val (addedAssets, updateAssetRegistryTransaction) = + if (epochNumber < chainContractOptions.assetTransfersActivationEpoch) (Nil, None) + else { + val addedAssets = + if (startAssetRegistryIndex == endAssetRegistryIndexExcl) Nil + else chainContractClient.getRegisteredAssets(startAssetRegistryIndex until endAssetRegistryIndexExcl) + + val txn = + if (addedAssets.isEmpty) None + else + chainContractOptions.elStandardBridgeAddress.map { sba => + StandardBridge.mkUpdateAssetRegistryTransaction( + standardBridgeAddress = sba, + addedTokenExponents = addedAssets.map(_.exponent), + addedTokens = addedAssets.map(_.erc20Address) + ) + } + + (addedAssets, txn) + } + + val nativeAndAssetTransfersViaDeposits = transfers.flatMap { + case _: ContractTransfer.NativeViaWithdrawal => None + case x: (ContractTransfer.NativeViaDeposit | ContractTransfer.Asset) => Some(x) + } + + val (nativeTransfersViaDeposits, assetTransfers) = nativeAndAssetTransfersViaDeposits.partitionMap { + case x: ContractTransfer.NativeViaDeposit => Left(x) + case x: ContractTransfer.Asset => Right(x) + } + + val depositedTransactions = updateAssetRegistryTransaction.toVector ++ + (for { + sba <- chainContractOptions.elStandardBridgeAddress.toVector + transfer <- nativeAndAssetTransfersViaDeposits + } yield { + transfer match { + case x: ContractTransfer.NativeViaDeposit => + StandardBridge.mkFinalizeBridgeETHTransaction( + transferIndex = x.index, + standardBridgeAddress = sba, + from = x.from, + to = x.to, + amount = x.amount + ) + case x: ContractTransfer.Asset => + StandardBridge.mkFinalizeBridgeErc20Transaction( + transferIndex = x.index, + standardBridgeAddress = sba, + token = x.tokenAddress, + to = x.to, + from = x.from, + amount = x.amount + ) + } + }) + + (depositedTransactions, addedAssets, updateAssetRegistryTransaction, nativeTransfersViaDeposits, assetTransfers) + } + private def validateC2ETransfers( actualWithdrawals: Seq[Withdrawal], actualTransferLogs: List[GetLogsResponseEntry], expectedTransfers: Seq[ContractTransfer], - prevWithdrawalIndex: Long + prevWithdrawalIndex: Long, + strictC2ETransfersActivated: Boolean ): Either[String, Long] = { val totalTransfers = expectedTransfers.size @@ -1483,24 +1537,39 @@ class ELUpdater( case expectedTransfer +: restExpectedTransfers => expectedTransfer match { - case expectedTransfer: ContractTransfer.Native => - actualWithdrawals match { - case Seq() => s"$logPrefix Not found EL block withdrawal #$prevWithdrawalIndex, expected $expectedTransfer transfer".asLeft - case actualWithdrawal +: restActualWithdrawals => - val expectedWithdrawal = toWithdrawal(expectedTransfer, prevWithdrawalIndex + 1) - validateWithdrawal(actualWithdrawal, expectedWithdrawal) match { - case Left(e) => e.asLeft - case _ => loop(restActualWithdrawals, actualTransferLogs, restExpectedTransfers, expectedWithdrawal.index, currTransferNumber + 1) - } - } - + case expectedTransfer: ContractTransfer.NativeViaWithdrawal => + if strictC2ETransfersActivated then Left("Native transfers via withdrawals are unexpected after strict C2E transfers activation") + else + actualWithdrawals match { + case Seq() => s"$logPrefix Not found EL block withdrawal #$prevWithdrawalIndex, expected $expectedTransfer transfer".asLeft + case actualWithdrawal +: restActualWithdrawals => + val expectedWithdrawal = toWithdrawal(expectedTransfer, prevWithdrawalIndex + 1) + validateWithdrawal(actualWithdrawal, expectedWithdrawal) match { + case Left(e) => e.asLeft + case _ => + loop(restActualWithdrawals, actualTransferLogs, restExpectedTransfers, expectedWithdrawal.index, currTransferNumber + 1) + } + } + case expectedTransfer: ContractTransfer.NativeViaDeposit => + if strictC2ETransfersActivated then { + actualTransferLogs match { + case Nil => s"$logPrefix Not found EL transfer log, expected $expectedTransfer transfer".asLeft + case actualTransferLog :: restActualTransferLogs => + StandardBridge.ETHBridgeFinalized + .decodeLog(actualTransferLog) + .flatMap(validateC2ENativeTransfer(actualTransferLog.logIndex, _, expectedTransfer)) match { + case Left(e) => e.asLeft + case _ => loop(actualWithdrawals, restActualTransferLogs, restExpectedTransfers, prevWithdrawalIndex, currTransferNumber + 1) + } + } + } else Left("Native transfers via deposits are unexpected before strict C2E transfers activation") case expectedTransfer: ContractTransfer.Asset => actualTransferLogs match { case Nil => s"$logPrefix Not found EL transfer log, expected $expectedTransfer transfer".asLeft case actualTransferLog :: restActualTransferLogs => StandardBridge.ERC20BridgeFinalized .decodeLog(actualTransferLog) - .flatMap(validateC2EAssetTransfer(actualTransferLog.logIndex, _, expectedTransfer)) match { + .flatMap(validateC2EAssetTransfer(actualTransferLog.logIndex, _, expectedTransfer, strictC2ETransfersActivated)) match { case Left(e) => e.asLeft case _ => loop(actualWithdrawals, restActualTransferLogs, restExpectedTransfers, prevWithdrawalIndex, currTransferNumber + 1) } @@ -1813,18 +1882,45 @@ object ELUpdater { } } yield () + private def validateC2ENativeTransfer( + logIndex: EthNumber, + elTransferEvent: StandardBridge.ETHBridgeFinalized, + expectedTransfer: ContractTransfer.NativeViaDeposit + ): Either[String, Unit] = { + def errorPrefix = s"C2E native transfer with logIndex=$logIndex, transferIndex=${expectedTransfer.index}" + for { + _ <- Either.raiseUnless(elTransferEvent.from == expectedTransfer.from) { + s"$errorPrefix: got from address: ${elTransferEvent.from}, expected: ${expectedTransfer.from}" + } + _ <- Either.raiseUnless(elTransferEvent.to == expectedTransfer.to)( + s"$errorPrefix: got to address: ${elTransferEvent.to}, expected: ${expectedTransfer.to}" + ) + expectedAmount = WAmount(expectedTransfer.amount).scale(NativeTokenElDecimals - NativeTokenClDecimals) + _ <- Either.raiseUnless(elTransferEvent.amount == expectedAmount)( + s"$errorPrefix: got amount: ${elTransferEvent.amount}, expected ${expectedAmount}" + ) + } yield () + } + private def validateC2EAssetTransfer( logIndex: EthNumber, elTransferEvent: StandardBridge.ERC20BridgeFinalized, - expectedTransfer: ContractTransfer.Asset + expectedTransfer: ContractTransfer.Asset, + strictC2ETransfersActivated: Boolean ): Either[String, Unit] = { def errorPrefix = s"C2E asset transfer with logIndex=$logIndex, transferIndex=${expectedTransfer.index}" for { _ <- Either.raiseUnless(elTransferEvent.localToken == expectedTransfer.tokenAddress) { s"$errorPrefix: got ERC20 address: ${elTransferEvent.localToken}, expected: ${expectedTransfer.tokenAddress}" } + _ <- + if strictC2ETransfersActivated then + Either.raiseUnless(elTransferEvent.from == expectedTransfer.from) { + s"$errorPrefix: got from address: ${elTransferEvent.from}, expected: ${expectedTransfer.from}" + } + else Right(()) _ <- Either.raiseUnless(elTransferEvent.elTo == expectedTransfer.to) { - s"$errorPrefix: got address: ${elTransferEvent.elTo}, expected: ${expectedTransfer.to}" + s"$errorPrefix: got to address: ${elTransferEvent.elTo}, expected: ${expectedTransfer.to}" } _ <- Either.raiseUnless(elTransferEvent.amount == expectedTransfer.amount) { s"$errorPrefix: got amount: ${elTransferEvent.amount}, expected ${expectedTransfer.amount}" diff --git a/src/main/scala/units/client/contract/ChainContractClient.scala b/src/main/scala/units/client/contract/ChainContractClient.scala index 75ec2af5..f4a5242d 100644 --- a/src/main/scala/units/client/contract/ChainContractClient.scala +++ b/src/main/scala/units/client/contract/ChainContractClient.scala @@ -245,17 +245,18 @@ trait ChainContractClient { } } - def getTransfersForPayload(fromIndex: Long, maxNative: Long): Vector[ContractTransfer] = { + def getTransfersForPayload(fromIndex: Long, maxNative: Option[Long]): Vector[ContractTransfer] = { val maxIndex = getTransfersCount - 1 @tailrec def loop(currIndex: Long, foundNative: Long, acc: Vector[ContractTransfer]): Vector[ContractTransfer] = if (currIndex > maxIndex) acc else requireTransfer(currIndex) match { - case x: ContractTransfer.Native => + case x: (ContractTransfer.NativeViaWithdrawal | ContractTransfer.NativeViaDeposit) => val updatedFoundNative = foundNative + 1 - if (updatedFoundNative > maxNative) acc - else loop(currIndex + 1, updatedFoundNative, acc :+ x) // if equals - we still can collect asset transfers + maxNative match + case Some(maxNative) if (updatedFoundNative > maxNative) => acc + case _ => loop(currIndex + 1, updatedFoundNative, acc :+ x) case x: ContractTransfer.Asset => loop(currIndex + 1, foundNative, acc :+ x) } @@ -276,36 +277,26 @@ trait ChainContractClient { xs match { // Native transfer, before strict transfers activation // {destElAddressHex with 0x}_{amount} - case Array(rawDestElAddress, rawAmount) => - ContractTransfer.Native( - index = atIndex, - epoch = 0, - to = EthAddress.unsafeFrom(rawDestElAddress), - amount = rawAmount.toLongOption.getOrElse(fail(s"Expected an integer amount of a native transfer, got: ${rawAmount}")) - ) + case Array(EthAddress(destElAddress), NativeTransferAmount(amount)) => + ContractTransfer.NativeViaWithdrawal(atIndex, 0, destElAddress, amount) // Native transfer, after strict transfers activation // {epoch}_{destElAddressHex with 0x}_{fromClAddressHex with 0x}_{amount} - case Array(rawEpoch, rawDestElAddress, _, rawAmount) if EthAddress.from(rawEpoch).isLeft => - ContractTransfer.Native( - index = atIndex, - epoch = rawEpoch.toIntOption.getOrElse(fail(s"Expected an integer epoch, got: ${rawEpoch}")), - to = EthAddress.unsafeFrom(rawDestElAddress), - amount = rawAmount.toLongOption.getOrElse(fail(s"Expected an integer amount of a native transfer, got: ${rawAmount}")) - ) + + case Array(Epoch(epoch), EthAddress(destElAddress), EthAddress(fromAddress), NativeTransferAmount(amount)) => + ContractTransfer.NativeViaDeposit(atIndex, epoch, fromAddress, destElAddress, amount) // Asset transfer, before strict transfers activation // {destElAddressHex with 0x}_{fromClAddressHex with 0x}_{amount}_{assetRegistryIndex} - case Array(rawDestElAddress, rawFromAddress, rawAmount, rawAssetIndex) if EthAddress.from(rawDestElAddress).isRight => { - val assetIndex = rawAssetIndex.toIntOption.getOrElse(fail(s"Expected an asset index in asset transfer, got: ${rawAssetIndex}")) - val asset = getRegisteredAsset(assetIndex) - val assetData = getRegisteredAssetData(asset) + case Array(EthAddress(destElAddress), EthAddress(fromAddress), rawAmount, AssetIndex(assetIndex)) => { + val asset = getRegisteredAsset(assetIndex) + val assetData = getRegisteredAssetData(asset) ContractTransfer.Asset( index = atIndex, epoch = 0, - from = EthAddress.unsafeFrom(rawFromAddress), - to = EthAddress.unsafeFrom(rawDestElAddress), + from = fromAddress, + to = destElAddress, amount = try WAmount(rawAmount).scale(assetData.exponent) catch { case e: ArithmeticException => fail(s"Expected an integer amount of a native transfer, got: ${rawAmount}", e) }, @@ -316,16 +307,15 @@ trait ChainContractClient { // Asset transfer, after strict transfers activation // {epoch}_{destElAddressHex with 0x}_{fromClAddressHex with 0x}_{amount}_{assetRegistryIndex} - case Array(rawEpoch, rawDestElAddress, rawFromAddress, rawAmount, rawAssetIndex) => { - val assetIndex = rawAssetIndex.toIntOption.getOrElse(fail(s"Expected an asset index in asset transfer, got: ${rawAssetIndex}")) - val asset = getRegisteredAsset(assetIndex) - val assetData = getRegisteredAssetData(asset) + case Array(Epoch(epoch), EthAddress(destElAddress), EthAddress(fromAddress), rawAmount, AssetIndex(assetIndex)) => { + val asset = getRegisteredAsset(assetIndex) + val assetData = getRegisteredAssetData(asset) ContractTransfer.Asset( index = atIndex, - epoch = rawEpoch.toIntOption.getOrElse(fail(s"Expected an integer epoch, got: ${rawEpoch}")), - from = EthAddress.unsafeFrom(rawFromAddress), - to = EthAddress.unsafeFrom(rawDestElAddress), + epoch = epoch, + from = fromAddress, + to = destElAddress, amount = try WAmount(rawAmount).scale(assetData.exponent) catch { case e: ArithmeticException => fail(s"Expected an integer amount of a native transfer, got: ${rawAmount}", e) }, @@ -334,7 +324,7 @@ trait ChainContractClient { ) } - case _ => fail(s"Unexpected number of elements in a transfer key '$key', got ${xs.length}: $raw") + case _ => fail(s"Expected one of ContractTransfer variants in a transfer key '$key', got: $raw") } } @@ -456,7 +446,7 @@ object ChainContractClient { val Sep = "," private class InconsistentContractData(message: String, cause: Throwable = null) - extends IllegalStateException(s"Probably, your have to upgrade your client. $message", cause) + extends IllegalStateException(s"Probably, you have to upgrade your client. $message", cause) case class EpochContractMeta(miner: Address, prevEpoch: Int, lastBlockHash: BlockHash) @@ -464,7 +454,8 @@ object ChainContractClient { val index: Long val epoch: Int - case Native(index: Long, epoch: Int, to: EthAddress, amount: Long) + case NativeViaWithdrawal(index: Long, epoch: Int, to: EthAddress, amount: Long) + case NativeViaDeposit(index: Long, epoch: Int, from: EthAddress, to: EthAddress, amount: Long) case Asset( index: Long, epoch: Int, @@ -491,4 +482,16 @@ object ChainContractClient { } private def fail(reason: String, cause: Throwable = null): Nothing = throw new InconsistentContractData(reason, cause) + + object Epoch { + def unapply(raw: String): Option[Int] = raw.toIntOption + } + + object NativeTransferAmount { + def unapply(raw: String): Option[Long] = raw.toLongOption + } + + object AssetIndex { + def unapply(raw: String): Option[Int] = raw.toIntOption + } } diff --git a/src/main/scala/units/client/engine/EngineApiClient.scala b/src/main/scala/units/client/engine/EngineApiClient.scala index 1a8ce0f5..505a032a 100644 --- a/src/main/scala/units/client/engine/EngineApiClient.scala +++ b/src/main/scala/units/client/engine/EngineApiClient.scala @@ -4,11 +4,14 @@ import play.api.libs.json.* import units.client.JsonRpcClient.newRequestId import units.client.engine.EngineApiClient.PayloadId import units.client.engine.model.* +import units.client.engine.model.Withdrawal.WithdrawalIndex import units.el.DepositedTransaction import units.eth.{EmptyL2Block, EthAddress} import units.util.BlockToPayloadMapper import units.{BlockHash, ClientError, JobResult} +import scala.annotation.tailrec + trait EngineApiClient { def forkchoiceUpdated(blockHash: BlockHash, finalizedBlockHash: BlockHash, requestId: Int = newRequestId): JobResult[PayloadStatus] @@ -70,5 +73,19 @@ object EngineApiClient { targetBlock.hash ) } yield BlockToPayloadMapper.toPayloadJson(simulatedBlockJson.head, Json.obj("transactions" -> Json.arr(), "withdrawals" -> withdrawals)) + + @tailrec + def getLastWithdrawalIndex(hash: BlockHash): JobResult[WithdrawalIndex] = + c.getBlockByHash(hash) match { + case Left(e) => Left(e) + case Right(None) => Left(ClientError(s"Can't find $hash block on EC during withdrawal search")) + case Right(Some(ecBlock)) => + ecBlock.withdrawals.lastOption match { + case Some(lastWithdrawal) => Right(lastWithdrawal.index) + case None => + if (ecBlock.height == 0) Right(-1L) + else getLastWithdrawalIndex(ecBlock.parentHash) + } + } } } diff --git a/src/main/scala/units/client/engine/HttpEngineApiClient.scala b/src/main/scala/units/client/engine/HttpEngineApiClient.scala index 57bc4561..8ca5179f 100644 --- a/src/main/scala/units/client/engine/HttpEngineApiClient.scala +++ b/src/main/scala/units/client/engine/HttpEngineApiClient.scala @@ -7,12 +7,11 @@ import sttp.client3.* import units.client.JsonRpcClient import units.client.engine.EngineApiClient.PayloadId import units.client.engine.HttpEngineApiClient.* -import units.client.engine.model.* import units.client.engine.model.ForkchoiceUpdatedRequest.ForkChoiceAttributes import units.client.engine.model.PayloadStatus.{Syncing, Valid} +import units.client.engine.model.{*, given} import units.eth.EthAddress import units.{BlockHash, ClientError, JobResult} -import units.client.engine.model.given import scala.concurrent.duration.{DurationInt, FiniteDuration} diff --git a/src/main/scala/units/client/engine/model/EcBlock.scala b/src/main/scala/units/client/engine/model/EcBlock.scala index 8bdce114..7277c071 100644 --- a/src/main/scala/units/client/engine/model/EcBlock.scala +++ b/src/main/scala/units/client/engine/model/EcBlock.scala @@ -32,8 +32,6 @@ case class EcBlock( } object EcBlock { - val MaxWithdrawals = 16 - implicit val reads: Reads[EcBlock] = ( (JsPath \ "hash").read[BlockHash] and (JsPath \ "parentHash").read[BlockHash] and diff --git a/src/main/scala/units/el/DepositedTransaction.scala b/src/main/scala/units/el/DepositedTransaction.scala index 7313d5bc..bb137a53 100644 --- a/src/main/scala/units/el/DepositedTransaction.scala +++ b/src/main/scala/units/el/DepositedTransaction.scala @@ -62,6 +62,19 @@ case class DepositedTransaction( val transactionBytes = Array(DepositedTransaction.Type) ++ rlpEncoded Numeric.toHexString(transactionBytes) } + + override def equals(obj: Any): Boolean = obj match { + case that: DepositedTransaction => + this.sourceHash.sameElements(that.sourceHash) && + this.from == that.from && + this.to == that.to && + this.mint == that.mint && + this.value == that.value && + this.gas == that.gas && + this.isSystemTx == that.isSystemTx && + this.data == that.data + case _ => false + } } object DepositedTransaction { diff --git a/src/main/scala/units/el/StandardBridge.scala b/src/main/scala/units/el/StandardBridge.scala index 7384721c..7f5f1fde 100644 --- a/src/main/scala/units/el/StandardBridge.scala +++ b/src/main/scala/units/el/StandardBridge.scala @@ -6,10 +6,10 @@ import com.wavesplatform.utils.EthEncoding import org.web3j.abi.* import org.web3j.abi.datatypes.generated.{Int64, Uint256, Uint8} import org.web3j.abi.datatypes.{Event, Function, Type, Address as Web3JAddress, DynamicArray as Web3JArray} +import units.* import units.client.engine.model.GetLogsResponseEntry import units.eth.{EthAddress, EthereumConstants} import units.util.HexBytesConverter -import units.{EAmount, raw} import java.math.BigInteger import java.util @@ -19,12 +19,68 @@ import scala.util.Try import scala.util.control.NonFatal object StandardBridge { + private val FinalizeBridgeETHFunction = "finalizeBridgeETH" + private val FinalizeBridgeETHGas = BigInteger.valueOf(1_000_000L) + private val FinalizeBridgeErc20Function = "finalizeBridgeERC20" private val FinalizeBridgeErc20Gas = BigInteger.valueOf(1_000_000L) // Should be enough to run this function private val UpdateAssetRegistryFunction = "updateAssetRegistry" private val UpdateAssetRegistryGas = BigInteger.valueOf(1_000_000L) + case class ETHBridgeFinalized(from: EthAddress, to: EthAddress, amount: EAmount) + + object ETHBridgeFinalized { + type FromType = Web3JAddress + type ElToType = Web3JAddress + type EAmountType = Uint256 + + private val FromTypeRef = new TypeReference[FromType](true) {} + private val ElToTypeRef = new TypeReference[ElToType](true) {} + private val AmountTypeRef = new TypeReference[EAmountType](false) {} + + private val EventDef = new Event( + "ETHBridgeFinalized", + List[TypeReference[?]]( + FromTypeRef, + ElToTypeRef, + AmountTypeRef + ).asJava + ) + + val Topic = EventEncoder.encode(EventDef) + + def decodeLog(log: GetLogsResponseEntry): Either[String, ETHBridgeFinalized] = + (try { + for { + (from, elTo) <- log.topics match { + case _ :: from :: elTo :: Nil => Right((from, elTo)) + case _ => Left(s"Topics should contain 3 or more elements, got ${log.topics.size}") + } + from <- Try(FunctionReturnDecoder.decodeIndexedValue(from, FromTypeRef)).toEither.bimap( + e => s"Can't decode from: ${e.getMessage}", + r => r.asInstanceOf[FromType] + ) + from <- EthAddress.from(from.getValue) + elTo <- Try(FunctionReturnDecoder.decodeIndexedValue(elTo, ElToTypeRef)).toEither.bimap( + e => s"Can't decode elTo: ${e.getMessage}", + r => r.asInstanceOf[ElToType] + ) + elTo <- EthAddress.from(elTo.getValue) + amount <- FunctionReturnDecoder.decode(log.data, EventDef.getNonIndexedParameters).asScala.toList match { + case (amount: EAmountType) :: Nil => + for { + amount <- Try(amount.getValue).toEither.left.map(e => s"Can't decode amount: ${e.getMessage}") + _ <- Either.raiseUnless(amount.compareTo(BigInteger.ZERO) > 0)(s"amount must be positive, got: $amount") + } yield amount + case xs => Left(s"Expected (amount: ${classOf[EAmountType].getSimpleName}) non-indexed fields, got: ${xs.mkString(", ")}") + } + } yield ETHBridgeFinalized(from, elTo, EAmount(amount)) + } catch { + case NonFatal(e) => Left(e.getMessage) + }).left.map(e => s"Can't decode ${EventDef.getName} event from $log. $e") + } + case class ERC20BridgeInitiated(localToken: EthAddress, clTo: Address, elFrom: EthAddress, clAmount: Long) object ERC20BridgeInitiated extends BridgeMerkleTree[ERC20BridgeInitiated] { type LocalTokenType = Web3JAddress @@ -58,7 +114,7 @@ object StandardBridge { for { (localToken, from, clTo) <- log.topics match { case _ :: localToken :: from :: clTo :: Nil => Right((localToken, from, clTo)) - case _ => Left(s"Topics should contain 3 or more elements, got ${log.topics.size}") + case _ => Left(s"Topics should contain 4 or more elements, got ${log.topics.size}") } localToken <- Try(FunctionReturnDecoder.decodeIndexedValue(localToken, LocalTokenTypeRef)).toEither.bimap( e => s"Can't decode localToken: ${e.getMessage}", @@ -118,7 +174,7 @@ object StandardBridge { for { (localToken, from, elTo) <- log.topics match { case _ :: localToken :: from :: elTo :: Nil => Right((localToken, from, elTo)) - case _ => Left(s"Topics should contain 3 or more elements, got ${log.topics.size}") + case _ => Left(s"Topics should contain 4 or more elements, got ${log.topics.size}") } localToken <- Try(FunctionReturnDecoder.decodeIndexedValue(localToken, LocalTokenTypeRef)).toEither.bimap( e => s"Can't decode localToken: ${e.getMessage}", @@ -136,13 +192,13 @@ object StandardBridge { ) elTo <- EthAddress.from(elTo.getValue) amount <- FunctionReturnDecoder.decode(log.data, EventDef.getNonIndexedParameters).asScala.toList match { - case (clAmount: EAmountType) :: Nil => + case (amount: EAmountType) :: Nil => for { - clAmount <- Try(clAmount.getValue).toEither.left.map(e => s"Can't decode clAmount: ${e.getMessage}") - _ <- Either.cond(clAmount.compareTo(BigInteger.ZERO) > 0, (), s"clAmount must be positive, got: $clAmount") - } yield clAmount + amount <- Try(amount.getValue).toEither.left.map(e => s"Can't decode amount: ${e.getMessage}") + _ <- Either.cond(amount.compareTo(BigInteger.ZERO) > 0, (), s"amount must be positive, got: $amount") + } yield amount - case xs => Left(s"Expected (clAmount: ${classOf[EAmountType].getSimpleName}) non-indexed fields, got: ${xs.mkString(", ")}") + case xs => Left(s"Expected (amount: ${classOf[EAmountType].getSimpleName}) non-indexed fields, got: ${xs.mkString(", ")}") } } yield new ERC20BridgeFinalized(localToken, from, elTo, EAmount(amount)) } catch { @@ -203,6 +259,39 @@ object StandardBridge { }).left.map(e => s"Can't decode event ${EventDef.getName} from $ethEventData. $e") } + def mkFinalizeBridgeETHTransaction( + transferIndex: Long, + standardBridgeAddress: EthAddress, + from: EthAddress, + to: EthAddress, + amount: Long + ): DepositedTransaction = { + val elAmount = WAmount(amount).scale(NativeTokenElDecimals - NativeTokenClDecimals) + DepositedTransaction( + sourceHash = DepositedTransaction.mkUserDepositedSourceHash(transferIndex), + from = EthereumConstants.ZeroAddress, + to = Some(standardBridgeAddress), + mint = elAmount.raw, + value = elAmount.raw, + gas = FinalizeBridgeETHGas, + isSystemTx = true, + data = finalizeBridgeETHCall(from, to, elAmount) + ) + } + + private def finalizeBridgeETHCall(from: EthAddress, elTo: EthAddress, amount: EAmount): String = { + val function = new Function( + FinalizeBridgeETHFunction, + util.Arrays.asList[Type[?]]( + new Web3JAddress(from.hexNoPrefix), + new Web3JAddress(elTo.hexNoPrefix), + new Uint256(amount.raw) + ), + Collections.emptyList + ) + FunctionEncoder.encode(function) + } + // See https://specs.optimism.io/protocol/deposits.html#execution def mkFinalizeBridgeErc20Transaction( transferIndex: Long, diff --git a/src/main/scala/units/el/package.scala b/src/main/scala/units/el/package.scala index 94436442..60b21c1f 100644 --- a/src/main/scala/units/el/package.scala +++ b/src/main/scala/units/el/package.scala @@ -8,9 +8,9 @@ package object el { if (data.size >= size) data else data ++ Array.fill(size - data.size)(emptyData) - val MaxC2ENativeTransfers = 16 - val MinE2CTransfers = 1024 + val MaxWithdrawals = 16 + val MinE2CTransfers = 1024 - val C2ETopics = List(StandardBridge.ERC20BridgeFinalized.Topic) - val E2CTopics = List(NativeBridge.ElSentNativeEventTopic, StandardBridge.ERC20BridgeInitiated.Topic) + val C2ETopics: Seq[String] = List(StandardBridge.ERC20BridgeFinalized.Topic, StandardBridge.ETHBridgeFinalized.Topic) + val E2CTopics: Seq[String] = List(NativeBridge.ElSentNativeEventTopic, StandardBridge.ERC20BridgeInitiated.Topic) } diff --git a/src/main/scala/units/eth/EthAddress.scala b/src/main/scala/units/eth/EthAddress.scala index fc434f43..6b7316b2 100644 --- a/src/main/scala/units/eth/EthAddress.scala +++ b/src/main/scala/units/eth/EthAddress.scala @@ -34,6 +34,8 @@ object EthAddress { } yield new EthAddress(hex.toLowerCase()) }.left.map(e => s"Can't decode address '$hex': $e") + def unapply(hex: String): Option[EthAddress] = from(hex).toOption + def unsafeFrom(hex: String): EthAddress = from(hex).left.map(new RuntimeException(_)).toTry.get def unsafeFrom(bytes: Array[Byte]): EthAddress = unsafeFrom(HexBytesConverter.toHex(bytes)) } diff --git a/src/main/scala/units/network/TrafficLogger.scala b/src/main/scala/units/network/TrafficLogger.scala index 918ddc05..20ace40f 100644 --- a/src/main/scala/units/network/TrafficLogger.scala +++ b/src/main/scala/units/network/TrafficLogger.scala @@ -1,12 +1,13 @@ package units.network +import com.typesafe.scalalogging.LazyLogging import com.wavesplatform.network.{Handshake, HandshakeSpec, TrafficLogger as TL} import io.netty.channel.ChannelHandler.Sharable import units.NetworkL2Block import units.network.BasicMessagesRepo.specsByCodes @Sharable -class TrafficLogger(settings: TL.Settings) extends TL(settings) { +class TrafficLogger(settings: TL.Settings) extends TL(settings), LazyLogging { import BasicMessagesRepo.specsByClasses diff --git a/src/main/scala/units/package.scala b/src/main/scala/units/package.scala index 0fdc344b..fc140cb4 100644 --- a/src/main/scala/units/package.scala +++ b/src/main/scala/units/package.scala @@ -4,6 +4,9 @@ import java.math.{BigDecimal, BigInteger} import scala.concurrent.duration.FiniteDuration package object units { + val NativeTokenElDecimals: Byte = 18.toByte + val NativeTokenClDecimals: Byte = 8.toByte + type BlockHash = BlockHash.Type type JobResult[A] = Either[ClientError, A] @@ -16,6 +19,7 @@ package object units { opaque type WAmount = Long object WAmount: def apply(x: String): WAmount = x.toLong + def apply(x: Long): WAmount = x extension (x: WAmount) def scale(powerOfTen: Int): EAmount = BigDecimal.valueOf(x).scaleByPowerOfTen(powerOfTen).toBigIntegerExact diff --git a/src/test/scala/units/BaseTestSuite.scala b/src/test/scala/units/BaseTestSuite.scala index fdd10e5a..136dd247 100644 --- a/src/test/scala/units/BaseTestSuite.scala +++ b/src/test/scala/units/BaseTestSuite.scala @@ -13,7 +13,7 @@ import org.scalatest.freespec.AnyFreeSpec import org.scalatest.{BeforeAndAfterAll, EitherValues, OptionValues, TryValues} import org.web3j.abi.TypeEncoder import org.web3j.abi.datatypes.Address as Web3JAddress -import org.web3j.abi.datatypes.generated.Int64 +import org.web3j.abi.datatypes.generated.{Int64, Uint256} import units.client.engine.model.GetLogsResponseEntry import units.el.NativeBridge.ElSentNativeEvent import units.el.{NativeBridge, StandardBridge} @@ -152,4 +152,17 @@ trait BaseTestSuite "" ) } + + protected def getLogsResponseEntryETH(event: StandardBridge.ETHBridgeFinalized, logIndex: Int = 0): GetLogsResponseEntry = + GetLogsResponseEntry( + EthNumber(logIndex), + StandardBridgeAddress, + TypeEncoder.encode(new Uint256(event.amount.raw)), + List( + StandardBridge.ETHBridgeFinalized.Topic, + TypeEncoder.encode(new Web3JAddress(event.from.hex)), + TypeEncoder.encode(new Web3JAddress(event.to.hex)) + ), + "" + ) } diff --git a/src/test/scala/units/C2ETransfersTestSuite.scala b/src/test/scala/units/C2ETransfersTestSuite.scala index a6630faf..aa716dfe 100644 --- a/src/test/scala/units/C2ETransfersTestSuite.scala +++ b/src/test/scala/units/C2ETransfersTestSuite.scala @@ -4,12 +4,7 @@ import com.wavesplatform.block.Block.BlockId import com.wavesplatform.db.WithState.AddrWithBalance import com.wavesplatform.state.IntegerDataEntry import com.wavesplatform.test.produce -import com.wavesplatform.transaction.utils.EthConverters.EthereumAddressExt import com.wavesplatform.transaction.{Asset, TxHelpers} -import com.wavesplatform.wallet.Wallet -import units.client.engine.model.{EcBlock, Withdrawal} -import units.el.StandardBridge -import units.eth.EthAddress class C2ETransfersTestSuite extends BaseTestSuite { private val transferSenderAccount = TxHelpers.secondSigner @@ -96,203 +91,6 @@ class C2ETransfersTestSuite extends BaseTestSuite { registeredAssets shouldBe List(Asset.Waves, issueAsset, issueAsset2Txn.asset) } - "Strict C2E transfers" - { - val userAmount = 1 - val nativeTokensClAmount = UnitsConvert.toUnitsInWaves(userAmount) - val nativeTokensElAmount = UnitsConvert.toGwei(userAmount) - val assetTokensClAmount = UnitsConvert.toWavesAtomic(userAmount, WavesDecimals) - val assetTokensElAmount = UnitsConvert.toAtomic(userAmount, WavesDecimals) - val destElAddress = EthAddress.unsafeFrom(s"0x$validTransferRecipient") - - def mkNativeTransfer(d: ExtensionDomain, ts: Long) = d.ChainContract.transfer( - sender = transferSenderAccount, - destElAddress = destElAddress, - asset = d.nativeTokenId, - amount = nativeTokensClAmount, - timestamp = ts - ) - - def mkAssetTransfer(d: ExtensionDomain, ts: Long) = d.ChainContract.transfer( - sender = transferSenderAccount, - destElAddress = destElAddress, - asset = Asset.Waves, - amount = assetTokensClAmount, - timestamp = ts - ) - - val reliable = ElMinerSettings(Wallet.generateNewAccount(super.defaultSettings.walletSeed, 0)) - val second = ElMinerSettings(TxHelpers.signer(2)) - val settings = defaultSettings.copy(initialMiners = List(reliable, second)).withEnabledElMining - "Not enough" - { - val malfunction = second - - "Native C2E transfers" in withExtensionDomain(settings) { d => - step("Activate strict C2E transfers") - val now = System.currentTimeMillis() - d.appendBlock( - d.ChainContract.enableStrictTransfers(d.blockchain.height + 1), - // Transfers of native token - TxHelpers.reissue(d.nativeTokenId, d.chainContractAccount, nativeTokensClAmount * 2), - TxHelpers.transfer(d.chainContractAccount, transferSenderAccount.toAddress, nativeTokensClAmount * 2, d.nativeTokenId), - mkNativeTransfer(d, now), - mkNativeTransfer(d, now + 1) - ) - - step(s"Start a new epoch of malfunction miner ${malfunction.address}") - d.advanceNewBlocks(malfunction) - d.advanceConsensusLayerChanged() - - val wrongBlock = d - .createEcBlockBuilder("0", malfunction) - .updateBlock { b => - b.copy(withdrawals = Vector(Withdrawal(0, destElAddress, nativeTokensElAmount))) // Only one - } - .buildAndSetLogs() - - d.appendMicroBlock(d.ChainContract.extendMainChain(malfunction.account, wrongBlock, lastC2ETransferIndex = 0)) - d.advanceConsensusLayerChanged() - - step(s"Receive wrongBlock ${wrongBlock.hash}") - d.receiveNetworkBlock(wrongBlock, malfunction.account) - withClue("Full EL block validation:") { - d.triggerScheduledTasks() - if (d.pollSentNetworkBlock().isDefined) fail(s"${wrongBlock.hash} should be ignored") - } - } - - "Asset C2E transfers" in withExtensionDomain(settings) { d => - val malfunction = second - - step("Activate strict C2E transfers") - val now = System.currentTimeMillis() - d.appendBlock( - d.ChainContract.enableStrictTransfers(d.blockchain.height + 1), - // Transfers of Waves - mkAssetTransfer(d, now), - mkAssetTransfer(d, now + 1) - ) - - val transferEvents = List( - StandardBridge.ERC20BridgeFinalized( // Only one - WWavesAddress, - EthAddress.unsafeFrom(transferSenderAccount.toAddress.toEthAddress), - destElAddress, - EAmount(assetTokensElAmount.bigInteger) - ) - ) - - step(s"Start a new epoch of malfunction miner ${malfunction.address}") - d.advanceNewBlocks(malfunction) - d.advanceConsensusLayerChanged() - - val wrongBlock = d - .createEcBlockBuilder("0", malfunction) - .buildAndSetLogs(transferEvents.map(getLogsResponseEntry(_))) - - d.appendMicroBlock(d.ChainContract.extendMainChain(malfunction.account, wrongBlock, lastC2ETransferIndex = 0)) - d.advanceConsensusLayerChanged() - - step(s"Receive wrongBlock ${wrongBlock.hash}") - d.receiveNetworkBlock(wrongBlock, malfunction.account) - withClue("Full EL block validation:") { - d.triggerScheduledTasks() - if (d.pollSentNetworkBlock().isDefined) fail(s"${wrongBlock.hash} should be ignored") - } - } - } - - "Enough C2E transfers" in withExtensionDomain(settings) { d => - step("Activate strict C2E transfers") - val now = System.currentTimeMillis() - d.appendBlock( - d.ChainContract.enableStrictTransfers(d.blockchain.height + 1), - // Transfers - TxHelpers.reissue(d.nativeTokenId, d.chainContractAccount, nativeTokensClAmount * 3), - TxHelpers.transfer(d.chainContractAccount, transferSenderAccount.toAddress, nativeTokensClAmount * 3, d.nativeTokenId), - mkNativeTransfer(d, now), - mkNativeTransfer(d, now + 1), - mkAssetTransfer(d, now + 2), - mkAssetTransfer(d, now + 3) - ) - - val transferLogEntries = (0 to 1).map { i => - val event = StandardBridge.ERC20BridgeFinalized( - WWavesAddress, - EthAddress.unsafeFrom(transferSenderAccount.toAddress.toEthAddress), - destElAddress, - EAmount(assetTokensElAmount.bigInteger) - ) - getLogsResponseEntry(event, i) - }.toList - - step(s"Start a new epoch of miner ${second.address}") - d.advanceNewBlocks(second) - d.advanceConsensusLayerChanged() - - val block = d - .createEcBlockBuilder("0", second) - .updateBlock { b => - b.copy(withdrawals = (0 to 1).map(Withdrawal(_, destElAddress, nativeTokensElAmount)).toVector) - } - .buildAndSetLogs(transferLogEntries) - - d.appendMicroBlock( - // Transfers, those miner could not see during building a payload - mkNativeTransfer(d, now + 4), - mkAssetTransfer(d, now + 5), - // EL-block confirmation - d.ChainContract.extendMainChain(second.account, block, lastC2ETransferIndex = 3) - ) - d.advanceConsensusLayerChanged() - - step(s"Receive block ${block.hash}") - d.receiveNetworkBlock(block, second.account) - withClue("Full EL block validation:") { - d.triggerScheduledTasks() - if (d.pollSentNetworkBlock().isEmpty) fail(s"${block.hash} should not be ignored") - } - } - - "More than maximum C2E native transfers" in withExtensionDomain(settings) { d => - step("Activate strict C2E transfers") - val now = System.currentTimeMillis() - val transfersNumber = EcBlock.MaxWithdrawals + 1 - val txns = Seq( - d.ChainContract.enableStrictTransfers(d.blockchain.height + 1), - // Transfers - TxHelpers.reissue(d.nativeTokenId, d.chainContractAccount, nativeTokensClAmount * transfersNumber), - TxHelpers.transfer(d.chainContractAccount, transferSenderAccount.toAddress, nativeTokensClAmount * transfersNumber, d.nativeTokenId) - ) ++ (0 until transfersNumber).map { i => - mkNativeTransfer(d, now + i) - } - - d.appendBlock(txns*) - - step(s"Start a new epoch of miner ${second.address}") - d.advanceNewBlocks(second) - d.advanceConsensusLayerChanged() - - val block = d - .createEcBlockBuilder("0", second) - .updateBlock { b => - b.copy(withdrawals = (0 until EcBlock.MaxWithdrawals).map(Withdrawal(_, destElAddress, nativeTokensElAmount)).toVector) - } - .buildAndSetLogs() - - d.appendMicroBlock( - d.ChainContract.extendMainChain(second.account, block, lastC2ETransferIndex = EcBlock.MaxWithdrawals - 1) - ) - d.advanceConsensusLayerChanged() - - step(s"Receive block ${block.hash}") - d.receiveNetworkBlock(block, second.account) - withClue("Full EL block validation:") { - d.triggerScheduledTasks() - if (d.pollSentNetworkBlock().isEmpty) fail(s"${block.hash} should not be ignored") - } - } - } - private def transferFuncTest( destElAddressHex: String, transferAmount: Int = 1_000_000, diff --git a/src/test/scala/units/TestDefaults.scala b/src/test/scala/units/TestDefaults.scala index b7cbc6a6..ebd20f4c 100644 --- a/src/test/scala/units/TestDefaults.scala +++ b/src/test/scala/units/TestDefaults.scala @@ -9,9 +9,6 @@ trait TestDefaults { val NativeBridgeAddress = EthAddress.unsafeFrom("0x0000000000000000000000000000000000006a7e") val StandardBridgeAddress = EthAddress.unsafeFrom("0xa50a51c09a5c451C52BB714527E1974b686D8e77") - val NativeTokenElDecimals = 18.toByte - val NativeTokenClDecimals = 8.toByte - val WWavesAddress = EthAddress.unsafeFrom("0x9a3DBCa554e9f6b9257aAa24010DA8377C57c17e") val WwavesDecimals = 8.toByte val WavesDecimals = 8.toByte diff --git a/src/test/scala/units/UnitsConvert.scala b/src/test/scala/units/UnitsConvert.scala index 8f680776..ce2c0113 100644 --- a/src/test/scala/units/UnitsConvert.scala +++ b/src/test/scala/units/UnitsConvert.scala @@ -4,7 +4,7 @@ import org.web3j.utils.Convert import units.eth.Gwei object UnitsConvert { - def toUnitsInWaves(userAmount: BigDecimal): Long = toWavesAtomic(userAmount, TestDefaults.NativeTokenClDecimals) + def toUnitsInWaves(userAmount: BigDecimal): Long = toWavesAtomic(userAmount, NativeTokenClDecimals) def toWavesAtomic(userAmount: BigDecimal, decimals: Int): Long = toAtomic(userAmount, decimals).bigInteger.longValueExact() def toWei(userAmount: BigDecimal): BigInt = Convert.toWei(userAmount.bigDecimal, Convert.Unit.ETHER).toBigIntegerExact diff --git a/src/test/scala/units/WAmountTestSuite.scala b/src/test/scala/units/WAmountTestSuite.scala new file mode 100644 index 00000000..1a366f5e --- /dev/null +++ b/src/test/scala/units/WAmountTestSuite.scala @@ -0,0 +1,30 @@ +package units + +import com.wavesplatform.test.FlatSpec + +import java.math.BigInteger + +class WAmountTestSuite extends FlatSpec { + val unit0decimalsDifference = NativeTokenElDecimals - NativeTokenClDecimals + + "scale" should "1 UNIT0: scale CL amount to EL amount given the decimals difference" in { + val userAmount = 1 + val nativeTokensClAmount = UnitsConvert.toUnitsInWaves(userAmount) + val nativeTokensElAmount = WAmount(nativeTokensClAmount).scale(unit0decimalsDifference) + + // 1 UNIT0 in CL is represented as 100_000_000L + nativeTokensClAmount shouldBe 100_000_000L + + // 1 UNIT0 in EL is represented as 1_000_000_000_000_000_000 + nativeTokensElAmount.raw shouldBe BigInt("1000000000000000000").bigInteger + } + + "scale" should "1.23456789 UNIT0: scale CL amount to EL amount given the decimals difference" in { + // 1.23456789 UNIT0 in CL is represented as 123456789L + val clAmount = WAmount(123456789L) + + // 1.234567890000000000 UNIT0 in EL is represented as 1234567890000000000 + val ecpecteedElAmount = EAmount(BigInteger("1234567890000000000")) + clAmount.scale(unit0decimalsDifference) shouldBe ecpecteedElAmount + } +} diff --git a/src/test/scala/units/el/DepositedTransactionTestSuite.scala b/src/test/scala/units/el/DepositedTransactionTestSuite.scala new file mode 100644 index 00000000..7550bc1c --- /dev/null +++ b/src/test/scala/units/el/DepositedTransactionTestSuite.scala @@ -0,0 +1,34 @@ +package units.el + +import com.wavesplatform.test.FlatSpec +import units.eth.EthAddress + +import java.math.BigInteger + +class DepositedTransactionTestSuite extends FlatSpec { + "equals" should "return true for transaction made of equal values" in { + val dt1 = DepositedTransaction( + sourceHash = DepositedTransaction.mkUserDepositedSourceHash(42L), + from = EthAddress.unsafeFrom("0x0000000000000000000000000000000000000000"), + to = Some(EthAddress.unsafeFrom("0xa50a51c09a5c451c52bb714527e1974b686d8e77")), + mint = BigInteger.valueOf(0), + value = BigInteger.valueOf(0), + gas = BigInteger.valueOf(1000000), + isSystemTx = true, + data = + "0xdc00abd10000000000000000000000009a3dbca554e9f6b9257aaa24010da8377c57c17e000000000000000000000000dc1c695f29d77b4ea04281c4833730f5efd0209f000000000000000000000000aaaa00000000000000000000000000000000aaaa0000000000000000000000000000000000000000000000000000000005f5e100" + ) + val dt2 = DepositedTransaction( + sourceHash = DepositedTransaction.mkUserDepositedSourceHash(42L), + from = EthAddress.unsafeFrom("0x0000000000000000000000000000000000000000"), + to = Some(EthAddress.unsafeFrom("0xa50a51c09a5c451c52bb714527e1974b686d8e77")), + mint = BigInteger.valueOf(0), + value = BigInteger.valueOf(0), + gas = BigInteger.valueOf(1000000), + isSystemTx = true, + data = + "0xdc00abd10000000000000000000000009a3dbca554e9f6b9257aaa24010da8377c57c17e000000000000000000000000dc1c695f29d77b4ea04281c4833730f5efd0209f000000000000000000000000aaaa00000000000000000000000000000000aaaa0000000000000000000000000000000000000000000000000000000005f5e100" + ) + dt1 shouldBe dt2 + } +}