diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index 2f780ff3..4471d07c 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -13,7 +13,7 @@ jobs: check-pr: name: Check PR runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 45 env: JAVA_OPTS: -Dfile.encoding=UTF-8 BRANCH_NAME: ${{ github.head_ref || github.ref_name }} diff --git a/consensus-client-it/build.sbt b/consensus-client-it/build.sbt index 62b28ac9..edeecbcb 100644 --- a/consensus-client-it/build.sbt +++ b/consensus-client-it/build.sbt @@ -20,15 +20,11 @@ libraryDependencies ++= Seq( ).map(_ % Test) Test / sourceGenerators += Def.task { - val generateSourcesFromContracts = Seq("Bridge", "StandardBridge", "ERC20") + val generateSourcesFromContracts = Seq("Bridge", "StandardBridge", "ERC20", "TERC20") val contractSources = baseDirectory.value / ".." / "contracts" / "eth" val compiledDir = contractSources / "target" // --silent to bypass garbage "Counting objects" git logs - s"forge build --silent --config-path ${contractSources / "foundry.toml"} --contracts " + - s"${contractSources / "src" / "utils" / "TERC20.sol"} " + - s"${contractSources / "src" / "StandardBridge.sol"} " + - s"${contractSources / "src" / "Bridge.sol"} " + - s"${contractSources / "src" / "UnitsMintableERC20.sol"}" ! + s"forge build --silent --config-path ${contractSources / "foundry.toml"} --contracts ${contractSources / "src"}" ! generateSourcesFromContracts.foreach { contract => val json = Json.parse(new FileInputStream(compiledDir / s"$contract.sol" / s"$contract.json")) diff --git a/consensus-client-it/src/test/scala/units/MultipleFailedAssetTransfersTestSuite.scala b/consensus-client-it/src/test/scala/units/MultipleFailedAssetTransfersTestSuite.scala new file mode 100644 index 00000000..fe07489f --- /dev/null +++ b/consensus-client-it/src/test/scala/units/MultipleFailedAssetTransfersTestSuite.scala @@ -0,0 +1,316 @@ +package units + +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.state.IntegerDataEntry +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.{EthSendTransaction, TransactionReceipt} +import org.web3j.tx.RawTransactionManager +import org.web3j.tx.gas.DefaultGasProvider +import units.client.contract.{ChainContractClient, ContractBlock} +import units.docker.EcContainer +import units.el.{BridgeMerkleTree, E2CTopics, Erc20Client, TERC20Client} +import units.eth.EthAddress + +import scala.annotation.tailrec +import scala.jdk.OptionConverters.RichOptional + +class MultipleFailedAssetTransfersTestSuite 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) + private lazy val terc20client = new TERC20Client(ec1.web3j, TErc20Address, txnManager, gasProvider) + + private val issuedE2CAmount = UnitsConvert.toAtomic(userAmount * 3, TErc20Decimals) + private val nativeE2CAmount = UnitsConvert.toAtomic(userAmount, NativeTokenElDecimals) + private val burnE2CAmount = UnitsConvert.toAtomic(userAmount * 3, TErc20Decimals) + private val leftoverE2CAmount = UnitsConvert.toAtomic(0, TErc20Decimals) + private val wavesE2CAmount = UnitsConvert.toAtomic(userAmount, WwavesDecimals) + + private lazy val currNonce = + AtomicInt(ec1.web3j.ethGetTransactionCount(elSenderAddress.hex, DefaultBlockParameterName.PENDING).send().getTransactionCount.intValueExact()) + def nextNonce: Int = currNonce.getAndIncrement() + + "Mining continues after 3 failed C2E transfers, 2 consecutive non-last and 1 last" in { + withClue("Reduce Standard Bridge balance to make C2E transfer fail") { + waitForTxn(terc20client.sendBurn(StandardBridgeAddress, burnE2CAmount.bigInteger, nextNonce)) + terc20.getBalance(StandardBridgeAddress) shouldBe leftoverE2CAmount + } + + step("Initiate C2E transfers") + val c2eRecipientAddress = EthAddress.unsafeFrom("0xAAAA00000000000000000000000000000000AAAA") + + val recipientAssetBalanceBeforeC2ETransfer = clRecipientAssetBalance + def mkC2ETransferTxn(asset: Asset, decimals: Byte): InvokeScriptTransaction = + ChainContract.transfer( + clRecipient, + c2eRecipientAddress, + asset, + UnitsConvert.toWavesAtomic(userAmount, decimals) + ) + + val c2eTransferTxns = List( + mkC2ETransferTxn(Asset.Waves, WavesDecimals), + mkC2ETransferTxn(issueAsset, issueAssetDecimals), + mkC2ETransferTxn(issueAsset, issueAssetDecimals), + mkC2ETransferTxn(chainContract.nativeTokenId, NativeTokenClDecimals), + mkC2ETransferTxn(issueAsset, issueAssetDecimals) + ) + c2eTransferTxns.foreach(waves1.api.broadcast) + c2eTransferTxns.map(txn => waves1.api.waitForSucceeded(txn.id())) + + eventually { + withClue("Issued asset: the sender balance has been reduced even though the transfer has failed") { + val balanceAfter = clRecipientAssetBalance + balanceAfter shouldBe (recipientAssetBalanceBeforeC2ETransfer - UnitsConvert.toWavesAtomic(userAmount * 3, issueAssetDecimals)) + } + + withClue("Issued asset: the transfer has failed") { + terc20.getBalance(c2eRecipientAddress) shouldBe leftoverE2CAmount + } + + withClue("Native token: the other transfers have succeeded") { + val balanceAfter = ec1.web3j.ethGetBalance(c2eRecipientAddress.hex, DefaultBlockParameterName.PENDING).send().getBalance + BigInt(balanceAfter) shouldBe nativeE2CAmount + } + + withClue("WAVES: the other transfers have succeeded") { + wwaves.getBalance(c2eRecipientAddress) shouldBe wavesE2CAmount + } + } + + step("Mining continues") + val clHeightAfterTransfers = waves1.api.height() + val elHeightAfterTransfers = ec1.web3j.ethBlockNumber().send().getBlockNumber.longValueExact() + + withClue("CL height grows") { + waves1.api.waitForHeight(clHeightAfterTransfers + 2) + } + withClue("EL height grows") { + chainContract.waitForHeight(elHeightAfterTransfers + 2) + } + + step("Sender can get their funds back from a failed transfer using a chain contract method") + val failedTransferIndexes = List(1L, 2L, 4L) + val expectedFailedTransfersRoot = BridgeMerkleTree.getFailedTransfersRootHash(failedTransferIndexes) + + @tailrec + def loop(cb: ContractBlock): ContractBlock = + if (java.util.Arrays.equals(cb.failedC2ETransfersRootHash, expectedFailedTransfersRoot)) cb + else + chainContract.getBlock(cb.parentHash) match { + case Some(parent) => loop(parent) + case None => fail("Failed to locate block with failed transfer data") + } + + val blockWithFailedTransfers = loop(chainContract.getLastBlockMeta(ChainContractClient.DefaultMainChainId).value) + + withClue("Refund for transfer 1") { + val failedTransferIndex = 1L + val failedTransferIndexInBlock = 0 + val balanceBeforeRefund = clRecipientAssetBalance + val refundAmount = UnitsConvert.toWavesAtomic(userAmount, issueAssetDecimals) + val failedTransferProof = BridgeMerkleTree + .mkFailedTransferProofs(failedTransferIndexes, transferIndex = failedTransferIndexInBlock) + .reverse + + val refundInvoke = ChainContract.refundFailedC2ETransfer( + sender = clRecipient, + blockHash = blockWithFailedTransfers.hash, + merkleProof = failedTransferProof, + failedTransferIndex = failedTransferIndex, + transferIndexInBlock = failedTransferIndexInBlock + ) + waves1.api.broadcastAndWait(refundInvoke) + + withClue("Issued asset: balance after refund increased by the returned funds") { + eventually { + clRecipientAssetBalance shouldBe (balanceBeforeRefund + refundAmount) + } + } + } + + withClue("Refund for transfer 2") { + val failedTransferIndex = 2L + val failedTransferIndexInBlock = 1 + val balanceBeforeRefund = clRecipientAssetBalance + val refundAmount = UnitsConvert.toWavesAtomic(userAmount, issueAssetDecimals) + val failedTransferProof = BridgeMerkleTree + .mkFailedTransferProofs(failedTransferIndexes, transferIndex = failedTransferIndexInBlock) + .reverse + + val refundInvoke = ChainContract.refundFailedC2ETransfer( + sender = clRecipient, + blockHash = blockWithFailedTransfers.hash, + merkleProof = failedTransferProof, + failedTransferIndex = failedTransferIndex, + transferIndexInBlock = failedTransferIndexInBlock + ) + waves1.api.broadcastAndWait(refundInvoke) + + withClue("Issued asset: balance after refund increased by the returned funds") { + eventually { + clRecipientAssetBalance shouldBe (balanceBeforeRefund + refundAmount) + } + } + } + + withClue("Refund for transfer 4") { + val failedTransferIndex = 4L + val failedTransferIndexInBlock = 2 + val balanceBeforeRefund = clRecipientAssetBalance + val refundAmount = UnitsConvert.toWavesAtomic(userAmount, issueAssetDecimals) + val failedTransferProof = BridgeMerkleTree + .mkFailedTransferProofs(failedTransferIndexes, transferIndex = failedTransferIndexInBlock) + .reverse + + val refundInvoke = ChainContract.refundFailedC2ETransfer( + sender = clRecipient, + blockHash = blockWithFailedTransfers.hash, + merkleProof = failedTransferProof, + failedTransferIndex = failedTransferIndex, + transferIndexInBlock = failedTransferIndexInBlock + ) + waves1.api.broadcastAndWait(refundInvoke) + + withClue("Issued asset: balance after refund increased by the returned funds") { + eventually { + clRecipientAssetBalance shouldBe (balanceBeforeRefund + refundAmount) + } + } + } + + } + + private def clRecipientAssetBalance: Long = waves1.api.balance(clRecipient.toAddress, issueAsset) + + private def waitForTxn(txnResult: EthSendTransaction): TransactionReceipt = eventually { + ec1.web3j.ethGetTransactionReceipt(txnResult.getTransactionHash).send().getTransactionReceipt.toScala.value + } + + 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.toInt) + ) + ) + + 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 + } + + 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)) + .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 + + 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, NativeTokenClDecimals), + mkE2CWithdrawTxn(1, issueAsset, userAmount * 3, issueAssetDecimals), + mkE2CWithdrawTxn(2, Asset.Waves, userAmount, WavesDecimals) + ) + + e2cWithdrawTxns.foreach(waves1.api.broadcast) + e2cWithdrawTxns.foreach(txn => waves1.api.waitForSucceeded(txn.id())) + + withClue("Assets received after E2C: ") { + withClue("Issued asset: the balance was initially sufficient on CL") { + val balanceAfter = clRecipientAssetBalance + balanceAfter shouldBe (recipientAssetBalanceBefore + UnitsConvert.toWavesAtomic(userAmount * 3, issueAssetDecimals)) + } + withClue("Issued asset: the StandardBridge balance was initially sufficient on EL") { + terc20.getBalance(StandardBridgeAddress) shouldBe issuedE2CAmount + } + } + } +} diff --git a/consensus-client-it/src/test/scala/units/SingleFailedAssetTransferTestSuite.scala b/consensus-client-it/src/test/scala/units/SingleFailedAssetTransferTestSuite.scala new file mode 100644 index 00000000..9632a110 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/SingleFailedAssetTransferTestSuite.scala @@ -0,0 +1,276 @@ +package units + +import com.wavesplatform.common.utils.EitherExt2.explicitGet +import com.wavesplatform.state.IntegerDataEntry +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.{EthSendTransaction, TransactionReceipt} +import org.web3j.tx.RawTransactionManager +import org.web3j.tx.gas.DefaultGasProvider +import com.wavesplatform.api.NodeHttpApi.ErrorResponse +import units.client.contract.{ChainContractClient, ContractBlock} +import units.docker.EcContainer +import units.el.{BridgeMerkleTree, E2CTopics, Erc20Client, TERC20Client} +import units.eth.EthAddress + +import scala.annotation.tailrec +import scala.jdk.OptionConverters.RichOptional + +class SingleFailedAssetTransferTestSuite 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) + private lazy val terc20client = new TERC20Client(ec1.web3j, TErc20Address, txnManager, gasProvider) + + private val issuedE2CAmount = UnitsConvert.toAtomic(userAmount * 2, TErc20Decimals) + private val nativeE2CAmount = UnitsConvert.toAtomic(userAmount, NativeTokenElDecimals) + private val burnE2CAmount = UnitsConvert.toAtomic(userAmount, TErc20Decimals) + private val leftoverE2CAmount = UnitsConvert.toAtomic(userAmount, TErc20Decimals) + private val wavesE2CAmount = UnitsConvert.toAtomic(userAmount, WwavesDecimals) + + private lazy val currNonce = + AtomicInt(ec1.web3j.ethGetTransactionCount(elSenderAddress.hex, DefaultBlockParameterName.PENDING).send().getTransactionCount.intValueExact()) + def nextNonce: Int = currNonce.getAndIncrement() + + "Mining continues after 2 equivalent transfers: 1 successful and 1 failed" in { + withClue("Reduce Standard Bridge balance to make C2E transfer fail") { + waitForTxn(terc20client.sendBurn(StandardBridgeAddress, burnE2CAmount.bigInteger, nextNonce)) + terc20.getBalance(StandardBridgeAddress) shouldBe leftoverE2CAmount + } + + step("Initiate C2E transfers") + val c2eRecipientAddress = EthAddress.unsafeFrom("0xAAAA00000000000000000000000000000000AAAA") + + val recipientAssetBalanceBeforeC2ETransfer = clRecipientAssetBalance + def mkC2ETransferTxn(asset: Asset, decimals: Byte): InvokeScriptTransaction = + ChainContract.transfer( + clRecipient, + c2eRecipientAddress, + asset, + UnitsConvert.toWavesAtomic(userAmount, decimals) + ) + + val c2eTransferTxns = List( + mkC2ETransferTxn(Asset.Waves, WavesDecimals), + mkC2ETransferTxn(issueAsset, issueAssetDecimals), + mkC2ETransferTxn(issueAsset, issueAssetDecimals), + mkC2ETransferTxn(chainContract.nativeTokenId, NativeTokenClDecimals) + ) + c2eTransferTxns.foreach(waves1.api.broadcast) + c2eTransferTxns.map(txn => waves1.api.waitForSucceeded(txn.id())) + + eventually { + withClue("Issued asset: the sender balance has been reduced even though the transfer has failed") { + val balanceAfter = clRecipientAssetBalance + // Note: 1 for successful, 1 for failed + balanceAfter shouldBe (recipientAssetBalanceBeforeC2ETransfer - UnitsConvert.toWavesAtomic(userAmount * 2, issueAssetDecimals)) + } + + withClue("Issued asset: the transfer has failed") { + terc20.getBalance(c2eRecipientAddress) shouldBe leftoverE2CAmount + } + + withClue("Native token: the other transfers have succeeded") { + val balanceAfter = ec1.web3j.ethGetBalance(c2eRecipientAddress.hex, DefaultBlockParameterName.PENDING).send().getBalance + BigInt(balanceAfter) shouldBe nativeE2CAmount + } + + withClue("WAVES: the other transfers have succeeded") { + wwaves.getBalance(c2eRecipientAddress) shouldBe wavesE2CAmount + } + } + + step("Mining continues") + val clHeightAfterTransfers = waves1.api.height() + val elHeightAfterTransfers = ec1.web3j.ethBlockNumber().send().getBlockNumber.longValueExact() + + withClue("CL height grows") { + waves1.api.waitForHeight(clHeightAfterTransfers + 2) + } + withClue("EL height grows") { + chainContract.waitForHeight(elHeightAfterTransfers + 2) + } + + step("Sender can get their funds back from a failed transfer using a chain contract method") + val refundAmount = UnitsConvert.toWavesAtomic(userAmount, issueAssetDecimals) + val balanceBeforeRefund = clRecipientAssetBalance + + val failedTransferIndex = 2 + val expectedFailedTransfersRoot = BridgeMerkleTree.getFailedTransfersRootHash(Seq(failedTransferIndex)) + + @tailrec + def loop(cb: ContractBlock): ContractBlock = + if (java.util.Arrays.equals(cb.failedC2ETransfersRootHash, expectedFailedTransfersRoot)) cb + else + chainContract.getBlock(cb.parentHash) match { + case Some(parent) => loop(parent) + case None => fail("Failed to locate block with failed transfer data") + } + + val blockWithFailedTransfer = loop(chainContract.getLastBlockMeta(ChainContractClient.DefaultMainChainId).value) + + val failedTransferProof = BridgeMerkleTree + .mkFailedTransferProofs(List(failedTransferIndex), transferIndex = 0) + .reverse + + val refundInvoke = ChainContract.refundFailedC2ETransfer( + sender = clRecipient, + blockHash = blockWithFailedTransfer.hash, + merkleProof = failedTransferProof, + failedTransferIndex = failedTransferIndex, + transferIndexInBlock = 0 + ) + waves1.api.broadcastAndWait(refundInvoke) + + withClue("Issued asset: balance after refund increased by the returned funds") { + eventually { + clRecipientAssetBalance shouldBe (balanceBeforeRefund + refundAmount) + } + } + + step("Attempting to refund the same failed transfer again") + val refundInvoke2 = ChainContract.refundFailedC2ETransfer( + sender = clRecipient, + blockHash = blockWithFailedTransfer.hash, + merkleProof = failedTransferProof, + failedTransferIndex = failedTransferIndex, + transferIndexInBlock = 0 + ) + val res2 = waves1.api.broadcast(refundInvoke2) + + val expectedError = ErrorResponse(306, "Error while executing dApp: The funds for this transfer have already been refunded") + res2 shouldBe Left(expectedError) + } + + private def clRecipientAssetBalance: Long = waves1.api.balance(clRecipient.toAddress, issueAsset) + + private def waitForTxn(txnResult: EthSendTransaction): TransactionReceipt = eventually { + ec1.web3j.ethGetTransactionReceipt(txnResult.getTransactionHash).send().getTransactionReceipt.toScala.value + } + + 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.toInt) + ) + ) + + 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 + } + + 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)) + .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 + + 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, NativeTokenClDecimals), + mkE2CWithdrawTxn(1, issueAsset, userAmount * 2, issueAssetDecimals), + mkE2CWithdrawTxn(2, Asset.Waves, userAmount, WavesDecimals) + ) + + e2cWithdrawTxns.foreach(waves1.api.broadcast) + e2cWithdrawTxns.foreach(txn => waves1.api.waitForSucceeded(txn.id())) + + withClue("Assets received after E2C: ") { + withClue("Issued asset: the balance was initially sufficient on CL") { + val balanceAfter = clRecipientAssetBalance + balanceAfter shouldBe (recipientAssetBalanceBefore + UnitsConvert.toWavesAtomic(userAmount * 2, issueAssetDecimals)) + } + withClue("Issued asset: the StandardBridge balance was initially sufficient on EL") { + terc20.getBalance(StandardBridgeAddress) shouldBe issuedE2CAmount + } + } + } +} diff --git a/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidAmountTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidAmountTestSuite.scala index b1d688b9..f2a1da3d 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidAmountTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidAmountTestSuite.scala @@ -6,7 +6,7 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.eth.EthAddress @@ -50,14 +50,15 @@ class AssetInvalidAmountTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidBridgeTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidBridgeTestSuite.scala index 837bd8cc..8937acdb 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidBridgeTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidBridgeTestSuite.scala @@ -6,7 +6,7 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.eth.EthAddress @@ -50,14 +50,15 @@ class AssetInvalidBridgeTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidRecipientTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidRecipientTestSuite.scala index 4590b6e6..a16a4502 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidRecipientTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidRecipientTestSuite.scala @@ -6,7 +6,7 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.eth.EthAddress @@ -50,14 +50,15 @@ class AssetInvalidRecipientTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidSenderTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidSenderTestSuite.scala index 7e49e814..ee51c69e 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidSenderTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidSenderTestSuite.scala @@ -5,7 +5,7 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.* @@ -48,14 +48,15 @@ class AssetInvalidSenderTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidTokenTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidTokenTestSuite.scala index 946eefd0..54f666b6 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidTokenTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/AssetInvalidTokenTestSuite.scala @@ -6,7 +6,7 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.eth.EthAddress @@ -50,14 +50,15 @@ class AssetInvalidTokenTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/AssetValidTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/AssetValidTestSuite.scala index e707ad3f..45abb737 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/AssetValidTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/AssetValidTestSuite.scala @@ -8,7 +8,7 @@ import com.wavesplatform.transaction.TxHelpers import com.wavesplatform.transaction.smart.InvokeScriptTransaction import org.scalatest.concurrent.PatienceConfiguration.{Interval, Timeout} import units.* -import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.eth.EthAddress @@ -51,14 +51,15 @@ class AssetValidTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidAmountTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidAmountTestSuite.scala index 384ac578..cd5fedc7 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidAmountTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidAmountTestSuite.scala @@ -7,7 +7,7 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.eth.EthAddress @@ -50,14 +50,15 @@ class NativeInvalidAmountTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidBridgeTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidBridgeTestSuite.scala index c0715f98..8f6592bf 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidBridgeTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidBridgeTestSuite.scala @@ -7,7 +7,7 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.eth.EthAddress @@ -50,14 +50,15 @@ class NativeInvalidBridgeTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidRecipientTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidRecipientTestSuite.scala index 126dd648..159dc9ee 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidRecipientTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidRecipientTestSuite.scala @@ -7,7 +7,7 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.eth.EthAddress @@ -50,14 +50,15 @@ class NativeInvalidRecipientTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidSenderTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidSenderTestSuite.scala index 5f206187..da406602 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidSenderTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/NativeInvalidSenderTestSuite.scala @@ -6,7 +6,7 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.{BlockHash, NetworkL2Block, TestNetworkClient} @@ -48,14 +48,15 @@ class NativeInvalidSenderTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/NativeMissingDepositTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/NativeMissingDepositTestSuite.scala index 1e4c39f1..510c219a 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/NativeMissingDepositTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/NativeMissingDepositTestSuite.scala @@ -6,11 +6,13 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.{BlockHash, NetworkL2Block, TestNetworkClient} class NativeMissingDepositTestSuite extends BaseBlockValidationSuite { + // Note: This test doesn't assert behavior in case of a failed transfer (which also means that the deposited tx is missing). For that case we have FailedTransfersTestSuite. + // This test only asserts behavior in case of a missing deposited tx while there were no unsuccessful transfers (meaning that the block is considered invalid). "Invalid block: native token, missing deposited transaction" in { val ethBalanceBefore = ec1.web3j.ethGetBalance(elRecipient.toString, DefaultBlockParameterName.LATEST).send().getBalance val elParentBlock: EcBlock = getMainChainLastBlock @@ -37,14 +39,15 @@ class NativeMissingDepositTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/NativeUnexpectedDepositTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/NativeUnexpectedDepositTestSuite.scala index 269ad381..48ad89c5 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/NativeUnexpectedDepositTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/NativeUnexpectedDepositTestSuite.scala @@ -7,7 +7,7 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.eth.EthAddress @@ -40,14 +40,15 @@ class NativeUnexpectedDepositTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/NativeUnexpectedWithdrawalTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/NativeUnexpectedWithdrawalTestSuite.scala index 5c74b1df..dd64995e 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/NativeUnexpectedWithdrawalTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/NativeUnexpectedWithdrawalTestSuite.scala @@ -7,7 +7,7 @@ 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.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.{EcBlock, Withdrawal} import units.el.* import units.eth.{EthAddress, Gwei} @@ -50,14 +50,15 @@ class NativeUnexpectedWithdrawalTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/NativeValidTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/NativeValidTestSuite.scala index 8da260ba..59717762 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/NativeValidTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/NativeValidTestSuite.scala @@ -8,7 +8,7 @@ import com.wavesplatform.transaction.TxHelpers import com.wavesplatform.transaction.smart.InvokeScriptTransaction import org.scalatest.concurrent.PatienceConfiguration.{Interval, Timeout} import org.web3j.protocol.core.DefaultBlockParameterName -import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.eth.EthAddress @@ -51,14 +51,15 @@ class NativeValidTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/block/validation/NoTransfersTestSuite.scala b/consensus-client-it/src/test/scala/units/block/validation/NoTransfersTestSuite.scala index 656d8d7b..6817966f 100644 --- a/consensus-client-it/src/test/scala/units/block/validation/NoTransfersTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/block/validation/NoTransfersTestSuite.scala @@ -6,7 +6,7 @@ import com.wavesplatform.lang.v1.compiler.Terms import com.wavesplatform.transaction.TxHelpers import com.wavesplatform.transaction.smart.InvokeScriptTransaction import org.scalatest.concurrent.PatienceConfiguration.{Interval, Timeout} -import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.{BlockHash, NetworkL2Block, TestNetworkClient} @@ -29,14 +29,15 @@ class NoTransfersTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.hexNoPrefix).explicitGet(), Terms.CONST_STRING(elParentBlock.hash.hexNoPrefix).explicitGet(), Terms.CONST_BYTESTR(hitSource).explicitGet(), Terms.CONST_STRING(EmptyE2CTransfersRootHashHex.drop(2)).explicitGet(), Terms.CONST_LONG(0), - Terms.CONST_LONG(-1) + Terms.CONST_LONG(-1), + Terms.CONST_STRING(EmptyFailedC2ETransfersRootHashHex.drop(2)).explicitGet() ) ) ) diff --git a/consensus-client-it/src/test/scala/units/el/TERC20Client.scala b/consensus-client-it/src/test/scala/units/el/TERC20Client.scala new file mode 100644 index 00000000..be660558 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/el/TERC20Client.scala @@ -0,0 +1,43 @@ +package units.el + +import com.typesafe.scalalogging.StrictLogging +import org.web3j.crypto.RawTransaction +import org.web3j.protocol.Web3j +import org.web3j.protocol.core.methods.response.EthSendTransaction +import org.web3j.protocol.exceptions.TransactionException +import org.web3j.tx.RawTransactionManager +import org.web3j.tx.gas.DefaultGasProvider +import units.bridge.TERC20 +import units.docker.EcContainer +import units.eth.EthAddress + +import java.math.BigInteger + +class TERC20Client( + web3j: Web3j, + erc20Address: EthAddress, + txnManager: RawTransactionManager, + gasProvider: DefaultGasProvider = new DefaultGasProvider +) extends StrictLogging { + private lazy val contract = TERC20.load(erc20Address.hex, web3j, txnManager, gasProvider) + + def sendBurn(spender: EthAddress, amount: BigInt, nonce: Int): EthSendTransaction = { + val to = contract.getContractAddress + val funcCall = contract.send_burn(spender.hex, amount.bigInteger).encodeFunctionCall() + val rawTxn = RawTransaction.createTransaction( + EcContainer.ChainId, + BigInteger.valueOf(nonce), + gasProvider.getGasLimit, + to, + BigInteger.ZERO, + funcCall, + BigInteger.ONE, + gasProvider.getGasPrice + ) + + logger.debug(s"Send ${txnManager.getFromAddress} burn for ${spender.hex}, nonce: $nonce") + val r = txnManager.signAndSend(rawTxn) + if (r.hasError) throw new TransactionException(s"Can't send burn: ${r.getError}, ${r.getError.getMessage}") + r + } +} diff --git a/contracts/eth/src/utils/TERC20.sol b/contracts/eth/src/utils/TERC20.sol index 139d9e19..d034638e 100644 --- a/contracts/eth/src/utils/TERC20.sol +++ b/contracts/eth/src/utils/TERC20.sol @@ -9,4 +9,8 @@ contract TERC20 is ERC20 { _mint(0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73, 10 ** 21); // rich account 1 _mint(0xf17f52151EbEF6C7334FAD080c5704D77216b732, 10 ** 21); // rich account 2 } + + function burn(address from, uint256 amount) external { + _burn(from, amount); + } } diff --git a/contracts/waves/src/main.ride b/contracts/waves/src/main.ride index f55aa103..c73d263b 100644 --- a/contracts/waves/src/main.ride +++ b/contracts/waves/src/main.ride @@ -36,6 +36,7 @@ let transfersCountKey = "nativeTransfersCount" let daoAddressKey = "daoAddress" let daoRewardKey = "daoReward" let maxSkippedEpochCountKey = "maxSkippedEpochCount" +let failedC2ETransfersKey = "failedC2ETransfersForBlock_0x" let emergencyStopKey = "stopped" func requireNotStopped() = getBoolean(emergencyStopKey).valueOrElse(false) && throw("The network has been stopped. Please contact the administrator.") @@ -74,19 +75,21 @@ func minerPkKey(rewardAddress: String) = "miner_0x" + rewardAddress + func minerChainIdKey(miner: Address) = "miner_" + miner.toString() + "_ChainId" func minerSkippedEpochCountKey(minerAddr: String) = "miner_" + minerAddr + "_SkippedEpochCount" +func refundedFailedC2ETransfersKey(blockHashHex: String, transferIndex: Int) = "refundedFailedC2ETransfers_0x" + blockHashHex + "_" + transferIndex.toString() # any boolean value (true) + let transfersCount = this.getInteger(transfersCountKey).valueOrElse(0) let maxSkippedEpochCount = getInteger(maxSkippedEpochCountKey).valueOrElse(200) # transferKey value variants: # - Native transfer, before strict transfers activation # {destElAddressHex with 0x}_{amount} -# +# # - Native transfer, after strict transfers activation # {epoch}_{destElAddressHex with 0x}_{fromClAddressHex with 0x}_{amount} -# +# # - Asset transfer, before strict transfers activation # {destElAddressHex with 0x}_{fromClAddressHex with 0x}_{amount}_{assetRegistryIndex} -# +# # - Asset transfer, after strict transfers activation # {epoch}_{destElAddressHex with 0x}_{fromClAddressHex with 0x}_{amount}_{assetRegistryIndex} func transferKey(index: Int) = "nativeTransfer_" + index.toString() @@ -94,6 +97,8 @@ func transferKey(index: Int) = "nativeTransfer_" + index.toString() let strictC2ETransfersActivationEpochKey = "strictC2ETransfersActivationEpoch" let strictC2ETransfersActivationEpoch = getInteger(strictC2ETransfersActivationEpochKey).valueOrElse(INT_MAX) let strictC2ETransfersActivated = height >= strictC2ETransfersActivationEpoch +func requireStrictC2ETransfersActivated() = strictC2ETransfersActivated || throw("Strict C2E transfers must be activated") +func requireStrictC2ETransfersNotActivated() = strictC2ETransfersActivated && throw("Strict C2E transfers must not be activated") # destElAddressHex - without 0x func mkNativeTransferViaWithdrawalEntries(destElAddressHex: String, amount: Int) = { @@ -263,13 +268,26 @@ func mkBlockMetaEntry( let blockMetaBytes = blockHeight.toBytes() + epoch.toBytes() + blockParentHex.fromBase16String() + chainId.toBytes() + e2cTransfersRootHashBytes + lastC2ETransferIndex.toBytes() - let updatedBlockMetaBytes = if (assetTransfersActivated) + let blockMetaWithAssets = if (assetTransfersActivated) then blockMetaBytes + lastAssetRegistryIndex.toBytes() else blockMetaBytes - BinaryEntry(blockMetaK + blockHashHex, updatedBlockMetaBytes) + BinaryEntry(blockMetaK + blockHashHex, blockMetaWithAssets) } +func mkFailedC2ETransfersEntry(blockHashHex: String, failedC2ETransfersRootHashHex: String) = + if (strictC2ETransfersActivated) + then { + let bytes = failedC2ETransfersRootHashHex.fromBase16String() + let bytesSize = bytes.size() + if (bytesSize == 0) then [] + else { + strict failedRootHashIsValid = bytesSize == ROOT_HASH_SIZE || + throw("Failed transfers root hash should have " + ROOT_HASH_SIZE.toString() + " bytes, got " + bytesSize.toString()) + [BinaryEntry(failedC2ETransfersKey + blockHashHex, bytes)] + } + } else [] + func lastHeightBy(miner: Address, chainId: Int) = match getInteger(chainLastHeightKey(chainId, miner)) { case h: Int => h case _ => @@ -512,7 +530,8 @@ func extendMainChain_base( vrf: ByteVector, e2cTransfersRootHashHex: String, lastC2ETransferIndex: Int, - lastAssetRegistryIndex: Int + lastAssetRegistryIndex: Int, + failedC2ETransfersRootHashHex: String ) = { strict notStopped = requireNotStopped() strict checkBlockHash = validateBlockHash(blockHashHex) @@ -529,6 +548,7 @@ func extendMainChain_base( strict checkGenerator = ensureMiningEpoch(originCaller) let updateFinalizedBlock = getUpdateFinalizedBlockAction(originCaller, blockHashHex, mainChainEpoch) + let failedC2ETransfersEntry = mkFailedC2ETransfersEntry(blockHashHex, failedC2ETransfersRootHashHex) let newChainHeight = mainChainHeight + 1 [ @@ -537,7 +557,7 @@ func extendMainChain_base( IntegerEntry(minerChainIdKey(originCaller), mainChainId), IntegerEntry(chainLastHeightKey(mainChainId, originCaller), newChainHeight), thisEpochMeta - ] ++ updateFinalizedBlock ++ markEpochNonEmpty + ] ++ failedC2ETransfersEntry ++ updateFinalizedBlock ++ markEpochNonEmpty } func startAltChain_base( @@ -547,7 +567,8 @@ func startAltChain_base( vrf: ByteVector, e2cTransfersRootHashHex: String, lastC2ETransferIndex: Int, - lastAssetRegistryIndex: Int + lastAssetRegistryIndex: Int, + failedC2ETransfersRootHashHex: String ) = { strict notStopped = requireNotStopped() strict checkBlockHash = validateBlockHash(blockHashHex) @@ -570,6 +591,8 @@ func startAltChain_base( strict checkGenerator = ensureMiningEpoch(originCaller) + let failedC2ETransfersEntry = mkFailedC2ETransfersEntry(blockHashHex, failedC2ETransfersRootHashHex) + [ thisEpochMeta, mkBlockMetaEntry(blockHashHex, newChainHeight, referenceHex, newChainId, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex), @@ -580,7 +603,7 @@ func startAltChain_base( IntegerEntry(chainLastHeightKey(mainChainId, originCaller), newChainHeight), StringEntry(supportersKey(newChainId), originCaller.toString()), IntegerEntry(lastChainIdKey, newChainId) - ] ++ markEpochNonEmpty + ] ++ failedC2ETransfersEntry ++ markEpochNonEmpty } func extendAltChain_base( @@ -591,7 +614,8 @@ func extendAltChain_base( chainId: Int, e2cTransfersRootHashHex: String, lastC2ETransferIndex: Int, - lastAssetRegistryIndex: Int + lastAssetRegistryIndex: Int, + failedC2ETransfersRootHashHex: String ) = { # 1. if this is the first block of a new epoch in a fork, store total supporting balance of a fork. # 2. if this fork's total supporting balance is >= 1/2 of the total balance, make this the new main chain. @@ -632,13 +656,16 @@ func extendAltChain_base( let updateMainChainLastMinedBlock = if (updateMainChainData == [] && minerChainId(originCaller).valueOrElse(0) != chainId) then { [IntegerEntry(chainLastHeightKey(mainChainId, originCaller), chainFirstBlockMeta._1)] } else [] + + let failedC2ETransfersEntry = mkFailedC2ETransfersEntry(blockHashHex, failedC2ETransfersRootHashHex) + [ mkBlockMetaEntry(blockHashHex, newChainHeight, referenceHex, chainId, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex), mkChainMetaEntry(chainId, newChainHeight, blockHashHex), thisEpochMeta, IntegerEntry(minerChainIdKey(originCaller), chainId), IntegerEntry(chainLastHeightKey(chainId, originCaller), newChainHeight) - ] ++ updateMainChainData ++ addSupporter(chainId, originCaller) ++ updateMainChainLastMinedBlock ++ markEpochNonEmpty + ] ++ failedC2ETransfersEntry ++ updateMainChainData ++ addSupporter(chainId, originCaller) ++ updateMainChainLastMinedBlock ++ markEpochNonEmpty } func appendBlock_base( @@ -647,7 +674,8 @@ func appendBlock_base( referenceHex: String, e2cTransfersRootHashHex: String, lastC2ETransferIndex: Int, - lastAssetRegistryIndex: Int + lastAssetRegistryIndex: Int, + failedC2ETransfersRootHashHex: String ) = { strict notStopped = requireNotStopped() strict checkCaller = if (thisEpochMiner == originCaller) @@ -665,12 +693,15 @@ func appendBlock_base( let newChainHeight = chainHeight + 1 strict checkBlockHash = validateBlockHash(blockHashHex) + + let failedC2ETransfersEntry = mkFailedC2ETransfersEntry(blockHashHex, failedC2ETransfersRootHashHex) + [ mkBlockMetaEntry(blockHashHex, newChainHeight, lastBlockId, chainId, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex), IntegerEntry(chainLastHeightKey(chainId, originCaller), newChainHeight), mkChainMetaEntry(chainId, newChainHeight, blockHashHex), StringEntry(epochMetaKey(height), thisEpochMiner.value().toString() + SEP + thisEpochRef.toString() + SEP + blockHashHex) - ] + ] ++ failedC2ETransfersEntry } func isFinalized(blockHashHex: String) = { @@ -724,7 +755,8 @@ func extendMainChain( lastC2ETransferIndex: Int ) = { strict check = requireAssetTransfersNotActivated() - extendMainChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, -1) + strict checkStrictTransfers = requireStrictC2ETransfersNotActivated() + extendMainChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, -1, base16''.toBase16String()) } @Callable(i) @@ -737,7 +769,22 @@ func extendMainChain_v2( lastAssetRegistryIndex: Int ) = { strict check = requireAssetTransfersActivated() - extendMainChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex) + strict checkStrictTransfers = requireStrictC2ETransfersNotActivated() + extendMainChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, base16''.toBase16String()) +} + +@Callable(i) +func extendMainChain_v3( + blockHashHex: String, + referenceHex: String, + vrf: ByteVector, + e2cTransfersRootHashHex: String, + lastC2ETransferIndex: Int, + lastAssetRegistryIndex: Int, + failedC2ETransfersRootHashHex: String +) = { + strict checkStrictTransfers = requireStrictC2ETransfersActivated() + extendMainChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } @Callable(i) @@ -749,7 +796,8 @@ func startAltChain( lastC2ETransferIndex: Int ) = { strict check = requireAssetTransfersNotActivated() - startAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, -1) + strict checkStrictTransfers = requireStrictC2ETransfersNotActivated() + startAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, -1, base16''.toBase16String()) } @Callable(i) @@ -762,7 +810,22 @@ func startAltChain_v2( lastAssetRegistryIndex: Int ) = { strict check = requireAssetTransfersActivated() - startAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex) + strict checkStrictTransfers = requireStrictC2ETransfersNotActivated() + startAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, base16''.toBase16String()) +} + +@Callable(i) +func startAltChain_v3( + blockHashHex: String, + referenceHex: String, + vrf: ByteVector, + e2cTransfersRootHashHex: String, + lastC2ETransferIndex: Int, + lastAssetRegistryIndex: Int, + failedC2ETransfersRootHashHex: String +) = { + strict checkStrictTransfers = requireStrictC2ETransfersActivated() + startAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } @Callable(i) @@ -775,7 +838,8 @@ func extendAltChain( lastC2ETransferIndex: Int ) = { strict check = requireAssetTransfersNotActivated() - extendAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, chainId, e2cTransfersRootHashHex, lastC2ETransferIndex, -1) + strict checkStrictTransfers = requireStrictC2ETransfersNotActivated() + extendAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, chainId, e2cTransfersRootHashHex, lastC2ETransferIndex, -1, base16''.toBase16String()) } @Callable(i) @@ -789,7 +853,23 @@ func extendAltChain_v2( lastAssetRegistryIndex: Int ) = { strict check = requireAssetTransfersActivated() - extendAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, chainId, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex) + strict checkStrictTransfers = requireStrictC2ETransfersNotActivated() + extendAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, chainId, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, base16''.toBase16String()) +} + +@Callable(i) +func extendAltChain_v3( + blockHashHex: String, + referenceHex: String, + vrf: ByteVector, + chainId: Int, + e2cTransfersRootHashHex: String, + lastC2ETransferIndex: Int, + lastAssetRegistryIndex: Int, + failedC2ETransfersRootHashHex: String +) = { + strict checkStrictTransfers = requireStrictC2ETransfersActivated() + extendAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, chainId, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } @Callable(i) @@ -800,7 +880,8 @@ func appendBlock( lastC2ETransferIndex: Int ) = { strict check = requireAssetTransfersNotActivated() - appendBlock_base(i.originCaller, blockHashHex, referenceHex, e2cTransfersRootHashHex, lastC2ETransferIndex, -1) + strict checkStrictTransfers = requireStrictC2ETransfersNotActivated() + appendBlock_base(i.originCaller, blockHashHex, referenceHex, e2cTransfersRootHashHex, lastC2ETransferIndex, -1, base16''.toBase16String()) } @Callable(i) @@ -812,7 +893,21 @@ func appendBlock_v2( lastAssetRegistryIndex: Int ) = { strict check = requireAssetTransfersActivated() - appendBlock_base(i.originCaller, blockHashHex, referenceHex, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex) + strict checkStrictTransfers = requireStrictC2ETransfersNotActivated() + appendBlock_base(i.originCaller, blockHashHex, referenceHex, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, base16''.toBase16String()) +} + +@Callable(i) +func appendBlock_v3( + blockHashHex: String, + referenceHex: String, + e2cTransfersRootHashHex: String, + lastC2ETransferIndex: Int, + lastAssetRegistryIndex: Int, + failedC2ETransfersRootHashHex: String +) = { + strict checkStrictTransfers = requireStrictC2ETransfersActivated() + appendBlock_base(i.originCaller, blockHashHex, referenceHex, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } @Callable(i) @@ -911,7 +1006,7 @@ func transfer(destElAddressHex: String) = { # else if (queueSize < 3200) then amountGtEq(t, 100_000_000, queueSize) # else if (queueSize < 6400) then amountGtEq(t, 1_000_000_000, queueSize) # else throw("Transfers denied for queue size of " + queueSize.toString() + ". Wait until current transfers processed") - let nativeTransferEntries = + let nativeTransferEntries = if (strictC2ETransfersActivated) then mkNativeTransferViaDepositEntries(normalizedDestElAddressHex, fromAddressHex, t.amount) else mkNativeTransferViaWithdrawalEntries(normalizedDestElAddressHex, t.amount) @@ -966,6 +1061,86 @@ func withdraw(blockHashHex: String, merkleProof: List[ByteVector], transferIndex ] } +# blockHashHex without 0x +@Callable(i) +func refundFailedC2ETransfer(blockHashHex: String, merkleProof: List[ByteVector], failedC2ETransferIndex: Int, transferIndexInBlock: Int) = { + strict check = requireStrictC2ETransfersActivated() + strict normalizedBlockHashHex = validateBlockHash(cut0x(blockHashHex)) + let expectedFailedC2ETransfersRootHash = getBinaryValue(failedC2ETransfersKey + normalizedBlockHashHex) + strict rootExists = expectedFailedC2ETransfersRootHash.size() > 0 || throw("No failed transfers recorded for block " + normalizedBlockHashHex) + let valueBytes = blake2b256(failedC2ETransferIndex.toBytes()) + let actualRootHash = createMerkleRoot(merkleProof, valueBytes, transferIndexInBlock) + strict hashesMatch = actualRootHash == expectedFailedC2ETransfersRootHash || throw( + "Expected failed transfers root hash: " + expectedFailedC2ETransfersRootHash.toBase16String() + + ", got: " + actualRootHash.toBase16String() + ) + + let transferOpt = getString(transferKey(failedC2ETransferIndex)) + let transferStr = transferOpt.valueOrErrorMessage("Transfer #" + failedC2ETransferIndex.toString() + " not found") + let parts = transferStr.split(SEP) + + strict nativeTransferBeforeStrictGuard = + parts.size() == 2 && { + # - Native transfer, before strict transfers activation + # {destElAddressHex with 0x}_{amount} + throw("Refund is supported only for strict C2E transfers") + } + + strict assetTransferBeforeStrictGuard = + (parts.size() == 4 && parts[0].take(2) == "0x" && parts[1].take(2) == "0x") && { + # - Asset transfer, before strict transfers activation + # {destElAddressHex with 0x}_{fromClAddressHex with 0x}_{amount}_{assetRegistryIndex} + throw("Refund is supported only for strict C2E transfers") + } + + # - Native transfer, after strict transfers activation + # {epoch}_{destElAddressHex with 0x}_{fromClAddressHex with 0x}_{amount} + let isNativeStrict = parts.size() == 4 && parts[0].take(2) != "0x" && parts[1].take(2) == "0x" && parts[2].take(2) == "0x" + + # - Asset transfer, after strict transfers activation + # {epoch}_{destElAddressHex with 0x}_{fromClAddressHex with 0x}_{amount}_{assetRegistryIndex} + let isAssetStrict = parts.size() == 5 && parts[0].take(2) != "0x" && parts[1].take(2) == "0x" && parts[2].take(2) == "0x" + + strict supportedTransfer = (isNativeStrict || isAssetStrict) || throw("Refund is supported only for strict C2E transfers") + + let storedFrom = cut0x(parts[2]).fromBase16String() + let callerPkHash = i.caller.bytes.drop(2).take(PUBLIC_KEY_HASH_SIZE) + strict senderMatches = storedFrom == callerPkHash || throw("Only original sender can refund this transfer") + + let refundsKey = refundedFailedC2ETransfersKey(normalizedBlockHashHex, transferIndexInBlock) + strict noPreviousRefunds = match getBoolean(refundsKey) { + case _: Boolean => throw("The funds for this transfer have already been refunded") + case _ => true + } + + if (isNativeStrict) then { + let amount = parts[3].parseIntValue() + let tokenId = getStringValue(tokenIdKey).fromBase58String() + [ + Reissue(tokenId, amount, true), + ScriptTransfer(i.caller, amount, tokenId), + BooleanEntry(refundsKey, true) + ] + } else { + let amount = parts[3].parseIntValue() + let assetIndexStr = parts[4] + let assetRegistryKey = assetRegistryIndexK + assetIndexStr + let assetIdBase58OrWaves = getString(assetRegistryKey) + .valueOrErrorMessage("Can't find asset index " + assetIndexStr) + let assetId = parseAssetId(assetIdBase58OrWaves) + let reissueActions = match assetId { + case assetId: ByteVector => + let asset = assetInfo(assetId).valueOrErrorMessage("Unknown asset " + assetIdBase58OrWaves) + if (asset.issuer == this) then [Reissue(assetId, amount, true)] else [] + case _ => [] + } + reissueActions ++ [ + ScriptTransfer(i.caller, amount, assetId), + BooleanEntry(refundsKey, true) + ] + } +} + # assetIdBase58OrWaves - "Asset ID in Base58" | WAVES_ASSET_NAME @Callable(i) func withdrawAsset(blockHashHex: String, merkleProof: List[ByteVector], transferIndexInBlock: Int, amount: Int, assetIdBase58OrWaves: String) = { diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala index c958808c..e71638db 100644 --- a/src/main/scala/units/ELUpdater.scala +++ b/src/main/scala/units/ELUpdater.scala @@ -4,6 +4,7 @@ import cats.syntax.either.* import cats.syntax.traverse.* import com.typesafe.scalalogging.StrictLogging import com.wavesplatform.account.{Address, AddressScheme, KeyPair, PKKeyPair} +import com.wavesplatform.common.merkle.Digest import com.wavesplatform.common.state.ByteStr import com.wavesplatform.crypto import com.wavesplatform.lang.ValidationError @@ -212,7 +213,7 @@ class ELUpdater( val withdrawals = rewardWithdrawal ++ nativeTransferWithdrawals - val (depositedTransactions, addedAssets, updateAssetRegistryTransaction, nativeTransfersViaDeposits, assetTransfers) = + val (depositedTransactions, addedAssets, updateAssetRegistryTransaction, transferTransactions, nativeTransfersViaDeposits, assetTransfers) = prepareTransactions(epochInfo.number, chainContractOptions, lastAssetRegistryIndex + 1, chainContractClient.getAssetRegistrySize, transfers) val prevRandao = calculateRandao(epochInfo.hitSource, parentBlock.hash) @@ -237,7 +238,9 @@ class ELUpdater( nextBlockUnixTs = nextBlockUnixTs, lastC2ETransferIndex = transfers.lastOption.fold(lastC2ETransferIndex)(_.index), lastElWithdrawalIndex = lastElWithdrawalIndex + withdrawals.size, - lastAssetRegistryIndex = addedAssets.lastOption.fold(lastAssetRegistryIndex)(_.index) + lastAssetRegistryIndex = addedAssets.lastOption.fold(lastAssetRegistryIndex)(_.index), + transfers = transfers, + transferTransactions = transferTransactions ) } } else { @@ -268,7 +271,9 @@ class ELUpdater( nextBlockUnixTs = nextBlockUnixTs, lastC2ETransferIndex = transfers.lastOption.fold(lastC2ETransferIndex)(_.index), lastElWithdrawalIndex = lastElWithdrawalIndex + withdrawals.size, - lastAssetRegistryIndex = addedAssets.lastOption.fold(lastAssetRegistryIndex)(_.index) + lastAssetRegistryIndex = addedAssets.lastOption.fold(lastAssetRegistryIndex)(_.index), + transfers = transfers, + transferTransactions = transferTransactions ) } } @@ -336,6 +341,8 @@ class ELUpdater( scheduler.scheduleOnceLabeled("tryToForgeNextBlock", (miningData.nextBlockUnixTs - currentUnixTs).seconds)( tryToForgeNextBlock( miningData.payload, + miningData.transfers, + miningData.transferTransactions, parentBlock.hash, miningData.nextBlockUnixTs, newState.options.startEpochChainFunction(epochInfo.number, parentBlock.hash, epochInfo.hitSource, nodeChainInfo.toOption), @@ -353,6 +360,8 @@ class ELUpdater( private def tryToForgeNextBlock( payloadOrId: PayloadId | JsObject, + transfers: Seq[ContractTransfer], + transferTransactions: Seq[DepositedTransaction], referenceHash: BlockHash, timestamp: Long, contractFunction: ContractFunction, @@ -373,7 +382,15 @@ class ELUpdater( waitForRefApprovalOnCl match { case Some(waitingTime) => scheduler.scheduleOnceLabeled("waitForApproval", waitingTime) { - tryToForgeNextBlock(payloadOrId, referenceHash, timestamp, contractFunction, chainContractOptions) + tryToForgeNextBlock( + payloadOrId, + transfers, + transferTransactions, + referenceHash, + timestamp, + contractFunction, + chainContractOptions + ) } case _ => val getAndApplyPayloadResult = for { @@ -415,6 +432,10 @@ class ELUpdater( hash = ecBlock.hash, addresses = chainContractOptions.bridgeAddresses(epochInfo.number) ) + + failedTransfers = getFailedTransfers(ecBlockLogs, transfers.zip(transferTransactions)) + failedTransfersRootHash = BridgeMerkleTree.getFailedTransfersRootHash(failedTransfers.map(_.index)) + transfersRootHash <- BridgeMerkleTree.getE2CTransfersRootHash(ecBlockLogs) // A forged block can be invalid for some reason. In this case we won't send it and its confirmation transaction to the network. expectedContractBlock = ContractBlock( @@ -426,19 +447,21 @@ class ELUpdater( chainId = m.nodeChainInfo.fold(_.prevChainId + 1, _.id), e2cTransfersRootHash = transfersRootHash, lastC2ETransferIndex = m.lastC2ETransferIndex, - lastAssetRegistryIndex = m.lastAssetRegistryIndex + lastAssetRegistryIndex = m.lastAssetRegistryIndex, + failedC2ETransfersRootHash = failedTransfersRootHash ) _ = logger.debug(s"Trying to do a full validation of a forged block ${ecBlock.hash}") _ <- validateAppliedBlock(expectedContractBlock, ecBlock, origState, Some(ecBlockLogs), updateState = false) - } yield (transfersRootHash, expectedContractBlock) + } yield (transfersRootHash, expectedContractBlock, failedTransfersRootHash) validateResult match { case Left(err) => logger.error(s"Forged an invalid block ${ecBlock.hash}: $err") - case Right((transfersRootHash, expectedContractBlock)) => + case Right((transfersRootHash, expectedContractBlock, failedTransfersRootHash)) => val confirmElBlockOnCl = for { funcCall <- contractFunction.toFunctionCall( ecBlock.hash, transfersRootHash, + failedTransfersRootHash, m.lastC2ETransferIndex, m.lastAssetRegistryIndex ) @@ -479,6 +502,8 @@ class ELUpdater( scheduler.scheduleOnceLabeled("forgeSecond", (nextBlockUnixTs * 1000 - time.correctedTime()).min(200).millis)( tryToForgeNextBlock( payloadOrId = nextMiningData.payload, + transfers, + transferTransactions, referenceHash = ecBlock.hash, timestamp = nextBlockUnixTs, contractFunction = chainContractOptions.appendFunction(epochInfo.number, ecBlock.hash), @@ -1320,16 +1345,18 @@ class ELUpdater( .traverse { txJson => DepositedTransaction.parseValidDepositedTransaction(txJson) } .map(_.flatten.toVector) + (depositedTransactions = expectedDepositedTransactions, transferTransactions = transferTransactions) = prepareTransactions( + contractBlock.epoch, + options, + parentContractBlock.lastAssetRegistryIndex + 1, + contractBlock.lastAssetRegistryIndex + 1, + expectedTransfers + ) + _ <- if strictC2ETransfersActivated then { - val (expectedDepositedTransactions, _, _, _, _) = prepareTransactions( - contractBlock.epoch, - options, - parentContractBlock.lastAssetRegistryIndex + 1, - contractBlock.lastAssetRegistryIndex + 1, - expectedTransfers - ) + Either.raiseUnless(expectedDepositedTransactions == actualDepositedTransactions)( s"Block is not valid, expected and actual deposited transactions don't match." ) @@ -1390,7 +1417,16 @@ class ELUpdater( lastElWithdrawalIndex <- { val c2eLogs = ecBlockLogs.filter(_.topics.intersect(C2ETopics).nonEmpty) - validateC2ETransfers(actualTransferWithdrawals, c2eLogs, expectedTransfers, prevWithdrawalIndex, strictC2ETransfersActivated) + validateC2ETransfers( + actualTransferWithdrawals, + c2eLogs, + expectedTransfers, + transferTransactions, + prevWithdrawalIndex, + strictC2ETransfersActivated, + contractBlock.failedC2ETransfersRootHash, + contractBlock.hash + ) } } yield Some(lastElWithdrawalIndex) @@ -1401,11 +1437,12 @@ class ELUpdater( endAssetRegistryIndexExcl: Int, transfers: Vector[ContractTransfer] ): ( - Vector[DepositedTransaction], - List[ChainContractClient.Registry.RegisteredAsset], - Option[DepositedTransaction], - Vector[ContractTransfer.NativeViaDeposit], - Vector[ContractTransfer.Asset] + depositedTransactions: Vector[DepositedTransaction], + addedAssets: List[ChainContractClient.Registry.RegisteredAsset], + updateAssetRegistryTransaction: Option[DepositedTransaction], + transferTransactions: Vector[DepositedTransaction], + nativeTransfersViaDeposits: Vector[ContractTransfer.NativeViaDeposit], + assetTransfers: Vector[ContractTransfer.Asset] ) = { val (addedAssets, updateAssetRegistryTransaction) = if (epochNumber < chainContractOptions.assetTransfersActivationEpoch) (Nil, None) @@ -1438,7 +1475,7 @@ class ELUpdater( case x: ContractTransfer.Asset => Right(x) } - val depositedTransactions = updateAssetRegistryTransaction.toVector ++ + val transferTransactions = (for { sba <- chainContractOptions.elStandardBridgeAddress.toVector transfer <- nativeAndAssetTransfersViaDeposits @@ -1464,17 +1501,43 @@ class ELUpdater( } }) - (depositedTransactions, addedAssets, updateAssetRegistryTransaction, nativeTransfersViaDeposits, assetTransfers) + val depositedTransactions = updateAssetRegistryTransaction.toVector ++ transferTransactions + + (depositedTransactions, addedAssets, updateAssetRegistryTransaction, transferTransactions, nativeTransfersViaDeposits, assetTransfers) + } + + private def getFailedTransfers( + c2eTransferLogs: List[GetLogsResponseEntry], + transfersWithTransactions: Seq[(ContractTransfer, DepositedTransaction)] + ): Seq[ContractTransfer.Asset | ContractTransfer.NativeViaDeposit] = { + val successfulTransferHashes = c2eTransferLogs.map(_.transactionHash).toSet + transfersWithTransactions.collect { + case (transfer: (ContractTransfer.Asset | ContractTransfer.NativeViaDeposit), dt) if !successfulTransferHashes.contains(dt.hash) => + transfer + } } private def validateC2ETransfers( actualWithdrawals: Seq[Withdrawal], actualTransferLogs: List[GetLogsResponseEntry], expectedTransfers: Seq[ContractTransfer], + transferTransactions: Seq[DepositedTransaction], prevWithdrawalIndex: Long, - strictC2ETransfersActivated: Boolean + strictC2ETransfersActivated: Boolean, + expectedFailedC2ETransfersRootHash: Digest, + blockHash: BlockHash ): Either[String, Long] = { - val totalTransfers = expectedTransfers.size + val totalTransfers = expectedTransfers.size + val failedTransfers = getFailedTransfers(actualTransferLogs, expectedTransfers.zip(transferTransactions)) + val actualFailedTransfersRootHash = BridgeMerkleTree.getFailedTransfersRootHash(failedTransfers.map(_.index)) + + val failedC2ETransfersRootHashCheck = + Either.raiseUnless(java.util.Arrays.equals(actualFailedTransfersRootHash, expectedFailedC2ETransfersRootHash)) { + s"Failed CL to EL transfers root hash mismatch in block $blockHash: " + + s"EL=${toHexNoPrefix(actualFailedTransfersRootHash)}, " + + s"CL=${toHexNoPrefix(expectedFailedC2ETransfersRootHash)}" + } + if (failedTransfers.nonEmpty) logger.debug(s"Failed C2E transfers: ${failedTransfers.mkString(", ")}") @tailrec def loop( @@ -1499,31 +1562,17 @@ class ELUpdater( case expectedTransfer +: restExpectedTransfers => expectedTransfer match { 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) - } - } + 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") + 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 @@ -1539,7 +1588,9 @@ class ELUpdater( } } - loop(actualWithdrawals, actualTransferLogs, expectedTransfers, prevWithdrawalIndex, currTransferNumber = 1) + if strictC2ETransfersActivated + then failedC2ETransfersRootHashCheck.map(_ => prevWithdrawalIndex) + else loop(actualWithdrawals, actualTransferLogs, expectedTransfers, prevWithdrawalIndex, currTransferNumber = 1) } private def validateAndApplyBlockFull( @@ -1796,7 +1847,9 @@ object ELUpdater { nextBlockUnixTs: Long, lastC2ETransferIndex: WithdrawalIndex, lastElWithdrawalIndex: WithdrawalIndex, - lastAssetRegistryIndex: Int + lastAssetRegistryIndex: Int, + transfers: Seq[ContractTransfer], + transferTransactions: Seq[DepositedTransaction] ) private case class BlockForValidation(contractBlock: ContractBlock, ecBlock: EcBlock) { @@ -1842,26 +1895,6 @@ 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, diff --git a/src/main/scala/units/client/contract/ChainContractClient.scala b/src/main/scala/units/client/contract/ChainContractClient.scala index a960d0a6..20f01a4a 100644 --- a/src/main/scala/units/client/contract/ChainContractClient.scala +++ b/src/main/scala/units/client/contract/ChainContractClient.scala @@ -105,7 +105,8 @@ trait ChainContractClient { e2cTransfersRootHash, lastC2ETransferIndex, if (lastAssetRegistryIndex.isValidInt) lastAssetRegistryIndex.toInt - else fail(s"$lastAssetRegistryIndex is not a valid int") + else fail(s"$lastAssetRegistryIndex is not a valid int"), + getBinaryData(s"failedC2ETransfersForBlock_${hash}").map(_.arr).getOrElse(Array.empty) ) } catch { case e: Throwable => fail(s"Can't read a block $hash meta, bytes: ${blockMeta.base64}, remaining: ${bb.remaining()}", e) @@ -238,6 +239,7 @@ trait ChainContractClient { getStringData("elStandardBridgeAddress") .map(EthAddress.unsafeFrom), getAssetTransfersActivationEpoch, + getStrictC2ETransfersActivationEpoch, blockDelay ) } diff --git a/src/main/scala/units/client/contract/ChainContractOptions.scala b/src/main/scala/units/client/contract/ChainContractOptions.scala index 3cac8e01..972e9006 100644 --- a/src/main/scala/units/client/contract/ChainContractOptions.scala +++ b/src/main/scala/units/client/contract/ChainContractOptions.scala @@ -11,6 +11,7 @@ case class ChainContractOptions( elNativeBridgeAddress: EthAddress, elStandardBridgeAddress: Option[EthAddress], assetTransfersActivationEpoch: Height, + strictC2ETransfersActivationEpoch: Height, blockDelayInSeconds: ValueAtEpoch[Int] ) { def bridgeAddresses(epoch: Height): List[EthAddress] = { @@ -32,7 +33,10 @@ case class ChainContractOptions( def appendFunction(epoch: Height, reference: BlockHash): AppendBlock = AppendBlock(reference, versionOf(epoch)) - private def versionOf(epoch: Height): Int = if (epoch < assetTransfersActivationEpoch) 1 else 2 + private def versionOf(epoch: Height): Int = + if (epoch < assetTransfersActivationEpoch) 1 + else if (epoch < strictC2ETransfersActivationEpoch) 2 + else 3 } case class ValueAtEpoch[A](oldValue: A, newValue: A, changeAtEpoch: Height) { diff --git a/src/main/scala/units/client/contract/ContractBlock.scala b/src/main/scala/units/client/contract/ContractBlock.scala index 21688107..7f67e2bd 100644 --- a/src/main/scala/units/client/contract/ContractBlock.scala +++ b/src/main/scala/units/client/contract/ContractBlock.scala @@ -16,12 +16,13 @@ case class ContractBlock( chainId: Long, e2cTransfersRootHash: Digest, lastC2ETransferIndex: Long, - lastAssetRegistryIndex: Int + lastAssetRegistryIndex: Int, + failedC2ETransfersRootHash: Digest ) extends L2BlockLike { override def toString: String = s"ContractBlock($hash, p=$parentHash, e=$epoch, h=$height, m=$minerRewardL2Address, c=$chainId, " + s"e2c=${if (e2cTransfersRootHash.isEmpty) "" else toHex(e2cTransfersRootHash)}, c2e=$lastC2ETransferIndex, " + - s"lari=$lastAssetRegistryIndex)" + s"lari=$lastAssetRegistryIndex)" + s"fc2e=${toHex(failedC2ETransfersRootHash)}" } object ContractBlock { diff --git a/src/main/scala/units/client/contract/ContractFunction.scala b/src/main/scala/units/client/contract/ContractFunction.scala index 133f79ea..f448d164 100644 --- a/src/main/scala/units/client/contract/ContractFunction.scala +++ b/src/main/scala/units/client/contract/ContractFunction.scala @@ -18,17 +18,20 @@ abstract class ContractFunction(baseName: String, extraArgs: Either[CommonError, def toFunctionCall( blockHash: BlockHash, transfersRootHash: Digest, + failedTransfersRootHash: Digest, lastC2ETransferIndex: Long, lastAssetRegistrySyncedIndex: Long ): Result[FUNCTION_CALL] = (for { hash <- CONST_STRING(cleanHexPrefix(blockHash.str)) ref <- CONST_STRING(cleanHexPrefix(reference.str)) ntrh <- CONST_STRING(toHexNoPrefix(transfersRootHash)) + ftrh <- CONST_STRING(toHexNoPrefix(failedTransfersRootHash)) xtra <- extraArgs } yield FUNCTION_CALL( FunctionHeader.User(name), List(hash, ref) ++ xtra ++ List(ntrh, CONST_LONG(lastC2ETransferIndex)) ++ - (if (version >= 2) List(CONST_LONG(lastAssetRegistrySyncedIndex)) else Nil) + (if (version >= 2) List(CONST_LONG(lastAssetRegistrySyncedIndex)) else Nil) ++ + (if (version >= 3) List(ftrh) else Nil) )).leftMap(e => s"Error building function call for $name: $e") } diff --git a/src/main/scala/units/el/BridgeMerkleTree.scala b/src/main/scala/units/el/BridgeMerkleTree.scala index 1fe79d2c..a3a37bf0 100644 --- a/src/main/scala/units/el/BridgeMerkleTree.scala +++ b/src/main/scala/units/el/BridgeMerkleTree.scala @@ -2,6 +2,7 @@ package units.el import cats.syntax.either.* import cats.syntax.traverse.* +import com.google.common.primitives.Longs import com.wavesplatform.common.merkle.{Digest, Merkle} import units.client.engine.model.GetLogsResponseEntry import units.util.HexBytesConverter @@ -47,4 +48,25 @@ object BridgeMerkleTree { case _ => List.empty[Array[Byte]].asRight } + + def getFailedTransfersRootHash(indexes: Seq[Long]): Digest = + mkFailedTransfersHash(getFailedTransferData(indexes)) + + private def mkFailedTransfersHash(data: Seq[Array[Byte]]): Digest = + if (data.isEmpty) Array.emptyByteArray + else { + val levels = Merkle.mkLevels(data) + levels.head.head + } + + def mkFailedTransferProofs(indexes: List[Long], transferIndex: Int): Seq[Digest] = + if (indexes.isEmpty) Nil + else { + val data = getFailedTransferData(indexes) + val levels = Merkle.mkLevels(data) + Merkle.mkProofs(transferIndex, levels) + } + + private def getFailedTransferData(indexes: Seq[Long]): Seq[Array[Byte]] = + indexes.map(Longs.toByteArray) } diff --git a/src/main/scala/units/el/DepositedTransaction.scala b/src/main/scala/units/el/DepositedTransaction.scala index 297f7538..98a2add9 100644 --- a/src/main/scala/units/el/DepositedTransaction.scala +++ b/src/main/scala/units/el/DepositedTransaction.scala @@ -8,7 +8,8 @@ import org.web3j.utils.Numeric import play.api.libs.functional.syntax.* import play.api.libs.json.* import units.eth.EthAddress -import units.util.HexBytesConverter.* +import units.util.HexBytesConverter +import units.util.HexBytesConverter.{toHex, toInt, toUint256} import java.math.BigInteger import java.util @@ -46,8 +47,7 @@ case class DepositedTransaction( isSystemTx: Boolean, data: String ) { - def toHex: String = { - + private def toBytes: Array[Byte] = { val rlpList = new RlpList( RlpString.create(sourceHash), RlpString.create(Numeric.hexStringToByteArray(from.hex)), @@ -56,14 +56,15 @@ case class DepositedTransaction( RlpString.create(value), RlpString.create(gas), RlpString.create(if (isSystemTx) 1 else 0), - RlpString.create(toBytes(data)) + RlpString.create(HexBytesConverter.toBytes(data)) ) - val rlpEncoded = RlpEncoder.encode(rlpList) - val transactionBytes = Array(DepositedTransaction.Type) ++ rlpEncoded - Numeric.toHexString(transactionBytes) + val rlpEncoded = RlpEncoder.encode(rlpList) + Array(DepositedTransaction.Type) ++ rlpEncoded } + def toHex: String = Numeric.toHexString(this.toBytes) + override def equals(obj: Any): Boolean = obj match { case that: DepositedTransaction => util.Arrays.equals(sourceHash, that.sourceHash) && @@ -76,6 +77,8 @@ case class DepositedTransaction( this.data == that.data case _ => false } + + def hash: String = HexBytesConverter.toHex(Keccak256.hash(this.toBytes)) } object DepositedTransaction { @@ -100,7 +103,7 @@ object DepositedTransaction { def parseValidDepositedTransaction(json: JsValue): Either[String, Option[DepositedTransaction]] = if (toInt((json \ "type").as[String]) == Type) - ((JsPath \ "sourceHash").read[String].map(toBytes) and + ((JsPath \ "sourceHash").read[String].map(HexBytesConverter.toBytes) and (JsPath \ "from").read[EthAddress] and (JsPath \ "to").readNullable[EthAddress] and (JsPath \ "mint").read[String].map(toUint256).map(_.getValue) and diff --git a/src/test/scala/units/BridgeMerkleTreeTestSuite.scala b/src/test/scala/units/BridgeMerkleTreeTestSuite.scala index 25746620..e051a8bd 100644 --- a/src/test/scala/units/BridgeMerkleTreeTestSuite.scala +++ b/src/test/scala/units/BridgeMerkleTreeTestSuite.scala @@ -79,4 +79,29 @@ class BridgeMerkleTreeTestSuite extends BaseTestSuite { val actualRootHash = Base64.encode(BridgeMerkleTree.getE2CTransfersRootHash(logs).value) actualRootHash shouldBe "TE571PexW4ErsrcEKhn1wRaVjGW/RkvaaOEFN/AMXuQ=" } + + "getFailedTransfersRootHash, 0 indexes" in { + val failedTransfersHash = Base64.encode(BridgeMerkleTree.getFailedTransfersRootHash(List.empty)) + failedTransfersHash shouldBe "" + } + + "getFailedTransfersRootHash, 1 index" in { + val failedTransfersHash = Base64.encode(BridgeMerkleTree.getFailedTransfersRootHash(List(1))) + failedTransfersHash shouldBe "6wti5laigNSEWvM8kfJjSdt90kLQMMCwb59lJTJnFXU=" + } + + "getFailedTransfersRootHash, 2 indexes" in { + val failedTransfersHash = Base64.encode(BridgeMerkleTree.getFailedTransfersRootHash(List(1, 2))) + failedTransfersHash shouldBe "O4J9Z0+sQmyTvf2teltyZzTOkT9NkKlJWbRWs6AT1Bs=" + } + + "getFailedTransfersRootHash, 3 indexes" in { + val failedTransfersHash = Base64.encode(BridgeMerkleTree.getFailedTransfersRootHash(List(1, 2, 3))) + failedTransfersHash shouldBe "HfIfxbQd7rf0YiFX99DQz8K6Y0Hnd5DvWFFawYhvXXc=" + } + + "getFailedTransfersRootHash, 4 indexes" in { + val failedTransfersHash = Base64.encode(BridgeMerkleTree.getFailedTransfersRootHash(List(1, 2, 3, 4))) + failedTransfersHash shouldBe "bKsOAvL+iFcLAHTgq2HHjXqeBtxz3WUn2QnHkulYXf8=" + } } diff --git a/src/test/scala/units/RideCreateMerkleRootTest.scala b/src/test/scala/units/RideCreateMerkleRootTest.scala new file mode 100644 index 00000000..d333fe59 --- /dev/null +++ b/src/test/scala/units/RideCreateMerkleRootTest.scala @@ -0,0 +1,45 @@ +package units + +import com.wavesplatform.block.Block.* +import com.wavesplatform.common.state.ByteStr +import com.wavesplatform.common.utils.EitherExt2.* +import com.wavesplatform.db.WithState.AddrWithBalance +import com.wavesplatform.lang.directives.values.V8 +import com.wavesplatform.lang.v1.compiler.Terms.* +import com.wavesplatform.lang.v1.compiler.TestCompiler +import com.wavesplatform.test.DomainPresets.* +import com.wavesplatform.transaction.TxHelpers.* +import units.el.BridgeMerkleTree + +class RideCreateMerkleRootTest extends BaseTestSuite { + + withDomain(TransactionStateSnapshot, AddrWithBalance.enoughBalances(secondSigner)) { d => + val dApp = TestCompiler(V8).compileContract( + """ + | @Callable(i) + | func merkle(proof: List[ByteVector], failedTransferIndex: Int, transferIndexInBlock: Int) = { + | let valueBytes = blake2b256(failedTransferIndex.toBytes()) + | [ BinaryEntry("root", createMerkleRoot(proof, valueBytes, transferIndexInBlock)) ] + | } + """.stripMargin + ) + d.appendBlock(d.createBlock(PlainBlockVersion, Seq(setScript(secondSigner, dApp)))) + + val failedC2ETransferIndexes = List(11L, 12L, 103L, 104L) + val transferIndex = 103L + val transferIndexInBlock = failedC2ETransferIndexes.indexOf(transferIndex) + + val root: Array[Byte] = BridgeMerkleTree.getFailedTransfersRootHash(failedC2ETransferIndexes) + val proofs: Seq[Array[Byte]] = BridgeMerkleTree + .mkFailedTransferProofs(failedC2ETransferIndexes, transferIndexInBlock) + .reverse + + val digests = ARR(proofs.map(b => CONST_BYTESTR(ByteStr(b)).explicitGet()).toVector, limited = false).explicitGet() + val invokeTx = invoke(func = Some("merkle"), args = Seq(digests, CONST_LONG(transferIndex), CONST_LONG(transferIndexInBlock))) + d.appendBlock(d.createBlock(PlainBlockVersion, Seq(invokeTx))) + + val actual = d.blockchain.accountData(secondAddress, "root").get.value + actual shouldBe ByteStr(root) + } + +} diff --git a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala index 30bde978..463bc145 100644 --- a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala +++ b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala @@ -376,6 +376,31 @@ trait HasConsensusLayerDappTxHelpers { fee = withdrawFee ) + def refundFailedC2ETransfer( + sender: KeyPair, + blockHash: BlockHash, + merkleProof: Seq[Digest], + failedTransferIndex: Long, + transferIndexInBlock: Int + ): InvokeScriptTransaction = + TxHelpers.invoke( + invoker = sender, + dApp = chainContractAddress, + func = "refundFailedC2ETransfer".some, + args = List( + Terms.CONST_STRING(blockHash.hexNoPrefix).explicitGet(), + Terms + .ARR( + merkleProof.map[Terms.EVALUATED](digest => Terms.CONST_BYTESTR(ByteStr(digest)).explicitGet()).toVector, + limited = false + ) + .explicitGet(), + Terms.CONST_LONG(failedTransferIndex), + Terms.CONST_LONG(transferIndexInBlock.toLong) + ), + fee = refundFailedC2ETransferFee + ) + def reportEmptyEpoch(minerAccount: KeyPair, vrf: ByteStr = currentHitSource): InvokeScriptTransaction = TxHelpers.invoke( invoker = minerAccount, dApp = chainContractAddress, @@ -395,7 +420,8 @@ trait HasConsensusLayerDappTxHelpers { } object HasConsensusLayerDappTxHelpers { - val EmptyE2CTransfersRootHashHex = EthereumConstants.NullHex + val EmptyE2CTransfersRootHashHex = EthereumConstants.NullHex + val EmptyFailedC2ETransfersRootHashHex = EthereumConstants.NullHex object DefaultFees { object ChainContract { @@ -413,6 +439,7 @@ object HasConsensusLayerDappTxHelpers { val reportEmptyEpochFee = 0.1.waves val claimEmptyEpochReportRewardsFee = 0.1.waves val stopFee = 0.1.waves + val refundFailedC2ETransferFee = 0.1.waves } } }