From 1e771918df0884efa384236634f84c7e5f1334d1 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Thu, 6 Nov 2025 11:49:37 +0400 Subject: [PATCH 01/16] Rebase on updated main --- .github/workflows/check-pr.yml | 2 +- consensus-client-it/build.sbt | 8 +- ...alidationAssetInvalidAmountTestSuite.scala | 7 +- ...alidationAssetInvalidBridgeTestSuite.scala | 7 +- ...dationAssetInvalidRecipientTestSuite.scala | 7 +- ...alidationAssetInvalidSenderTestSuite.scala | 7 +- ...ValidationAssetInvalidTokenTestSuite.scala | 7 +- .../BlockValidationAssetValidTestSuite.scala | 7 +- ...lidationNativeInvalidAmountTestSuite.scala | 7 +- ...lidationNativeInvalidBridgeTestSuite.scala | 7 +- ...ationNativeInvalidRecipientTestSuite.scala | 7 +- ...lidationNativeInvalidSenderTestSuite.scala | 7 +- ...idationNativeMissingDepositTestSuite.scala | 9 +- ...tionNativeUnexpectedDepositTestSuite.scala | 7 +- ...nNativeUnexpectedWithdrawalTestSuite.scala | 7 +- .../BlockValidationNativeValidTestSuite.scala | 7 +- .../BlockValidationNoTransfersTestSuite.scala | 7 +- .../units/FailedTransfersTestSuite1.scala | 276 +++++++++++++++ .../units/FailedTransfersTestSuite2.scala | 316 ++++++++++++++++++ .../test/scala/units/el/TERC20Client.scala | 43 +++ contracts/eth/src/utils/TERC20.sol | 4 + contracts/waves/src/main.ride | 226 +++++++++++-- src/main/scala/units/ELUpdater.scala | 164 +++++++-- .../client/contract/ChainContractClient.scala | 8 +- .../contract/ChainContractOptions.scala | 9 +- .../units/client/contract/ContractBlock.scala | 5 +- .../client/contract/ContractFunction.scala | 5 +- .../scala/units/el/BridgeMerkleTree.scala | 22 ++ .../scala/units/el/DepositedTransaction.scala | 3 + .../units/BridgeMerkleTreeTestSuite.scala | 25 ++ .../units/RideCreateMerkleRootTest.scala | 45 +++ .../HasConsensusLayerDappTxHelpers.scala | 29 +- 32 files changed, 1183 insertions(+), 114 deletions(-) create mode 100644 consensus-client-it/src/test/scala/units/FailedTransfersTestSuite1.scala create mode 100644 consensus-client-it/src/test/scala/units/FailedTransfersTestSuite2.scala create mode 100644 consensus-client-it/src/test/scala/units/el/TERC20Client.scala create mode 100644 src/test/scala/units/RideCreateMerkleRootTest.scala diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index 09a4fbbe..77f40a8c 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 c8bdfbd4..74e89c14 100644 --- a/consensus-client-it/build.sbt +++ b/consensus-client-it/build.sbt @@ -17,15 +17,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/BlockValidationAssetInvalidAmountTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidAmountTestSuite.scala index c083df0c..869cca16 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidAmountTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidAmountTestSuite.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 @@ -49,14 +49,15 @@ class BlockValidationAssetInvalidAmountTestSuite extends BaseBlockValidationSuit TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationAssetInvalidBridgeTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidBridgeTestSuite.scala index cb04cfbd..d2c822ab 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidBridgeTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidBridgeTestSuite.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 @@ -49,14 +49,15 @@ class BlockValidationAssetInvalidBridgeTestSuite extends BaseBlockValidationSuit TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationAssetInvalidRecipientTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidRecipientTestSuite.scala index e47525e1..a6583f32 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidRecipientTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidRecipientTestSuite.scala @@ -7,7 +7,7 @@ import com.wavesplatform.lang.v1.compiler.Terms import com.wavesplatform.transaction.TxHelpers import com.wavesplatform.transaction.smart.InvokeScriptTransaction import units.BlockHash -import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex +import units.client.contract.HasConsensusLayerDappTxHelpers.{EmptyE2CTransfersRootHashHex, EmptyFailedC2ETransfersRootHashHex} import units.client.engine.model.EcBlock import units.el.* import units.eth.EthAddress @@ -49,14 +49,15 @@ class BlockValidationAssetInvalidRecipientTestSuite extends BaseBlockValidationS TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationAssetInvalidSenderTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidSenderTestSuite.scala index be99b86e..b01dd7d0 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidSenderTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidSenderTestSuite.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.{BlockHash, TestNetworkClient} @@ -47,14 +47,15 @@ class BlockValidationAssetInvalidSenderTestSuite extends BaseBlockValidationSuit TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationAssetInvalidTokenTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidTokenTestSuite.scala index 09165d46..0942e52d 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidTokenTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetInvalidTokenTestSuite.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 @@ -49,14 +49,15 @@ class BlockValidationAssetInvalidTokenTestSuite extends BaseBlockValidationSuite TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationAssetValidTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationAssetValidTestSuite.scala index c2a0de3f..e23415e3 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationAssetValidTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationAssetValidTestSuite.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 @@ -47,14 +47,15 @@ class BlockValidationAssetValidTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationNativeInvalidAmountTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidAmountTestSuite.scala index fd36ab99..0c880ce5 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidAmountTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidAmountTestSuite.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 @@ -49,14 +49,15 @@ class BlockValidationNativeInvalidAmountTestSuite extends BaseBlockValidationSui TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationNativeInvalidBridgeTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidBridgeTestSuite.scala index b480dd28..24e2046c 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidBridgeTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidBridgeTestSuite.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 @@ -49,14 +49,15 @@ class BlockValidationNativeInvalidBridgeTestSuite extends BaseBlockValidationSui TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationNativeInvalidRecipientTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidRecipientTestSuite.scala index 2c9052fc..e0a92a53 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidRecipientTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidRecipientTestSuite.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 @@ -49,14 +49,15 @@ class BlockValidationNativeInvalidRecipientTestSuite extends BaseBlockValidation TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationNativeInvalidSenderTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidSenderTestSuite.scala index 89221ea6..3b81e62b 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidSenderTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeInvalidSenderTestSuite.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, TestNetworkClient} @@ -47,14 +47,15 @@ class BlockValidationNativeInvalidSenderTestSuite extends BaseBlockValidationSui TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationNativeMissingDepositTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeMissingDepositTestSuite.scala index c1c88d59..0fff3a3f 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationNativeMissingDepositTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeMissingDepositTestSuite.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, TestNetworkClient} class BlockValidationNativeMissingDepositTestSuite 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 = ec1.engineApi.getLastExecutionBlock().explicitGet() @@ -36,14 +38,15 @@ class BlockValidationNativeMissingDepositTestSuite extends BaseBlockValidationSu TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationNativeUnexpectedDepositTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedDepositTestSuite.scala index cdcadd04..6cdd7f63 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedDepositTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedDepositTestSuite.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 @@ -39,14 +39,15 @@ class BlockValidationNativeUnexpectedDepositTestSuite extends BaseBlockValidatio TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationNativeUnexpectedWithdrawalTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedWithdrawalTestSuite.scala index 07217b5f..b3f4b9ca 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedWithdrawalTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeUnexpectedWithdrawalTestSuite.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} @@ -49,14 +49,15 @@ class BlockValidationNativeUnexpectedWithdrawalTestSuite extends BaseBlockValida TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationNativeValidTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNativeValidTestSuite.scala index a89556ab..8b548802 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationNativeValidTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationNativeValidTestSuite.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 @@ -47,14 +47,15 @@ class BlockValidationNativeValidTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/BlockValidationNoTransfersTestSuite.scala b/consensus-client-it/src/test/scala/units/BlockValidationNoTransfersTestSuite.scala index 676fe31a..22d6f7d2 100644 --- a/consensus-client-it/src/test/scala/units/BlockValidationNoTransfersTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/BlockValidationNoTransfersTestSuite.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.{BlockHash, TestNetworkClient} @@ -26,14 +26,15 @@ class BlockValidationNoTransfersTestSuite extends BaseBlockValidationSuite { TxHelpers.invoke( invoker = actingMiner, dApp = chainContractAddress, - func = Some("extendMainChain_v2"), + func = Some("extendMainChain_v3"), args = List( Terms.CONST_STRING(simulatedBlockHash.drop(2)).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/FailedTransfersTestSuite1.scala b/consensus-client-it/src/test/scala/units/FailedTransfersTestSuite1.scala new file mode 100644 index 00000000..f96a1c02 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/FailedTransfersTestSuite1.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 FailedTransfersTestSuite1 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) + ) + ) + + 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/FailedTransfersTestSuite2.scala b/consensus-client-it/src/test/scala/units/FailedTransfersTestSuite2.scala new file mode 100644 index 00000000..7f397eb3 --- /dev/null +++ b/consensus-client-it/src/test/scala/units/FailedTransfersTestSuite2.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 FailedTransfersTestSuite2 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) + ) + ) + + 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/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 8ef88edd..f4ac5589 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,23 @@ 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 check = requireAssetTransfersActivated() + strict checkStrictTransfers = requireStrictC2ETransfersActivated() + extendMainChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } @Callable(i) @@ -749,7 +797,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 +811,23 @@ 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 check = requireAssetTransfersActivated() + strict checkStrictTransfers = requireStrictC2ETransfersActivated() + startAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } @Callable(i) @@ -775,7 +840,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 +855,24 @@ 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 check = requireAssetTransfersActivated() + strict checkStrictTransfers = requireStrictC2ETransfersActivated() + extendAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, chainId, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } @Callable(i) @@ -800,7 +883,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 +896,22 @@ 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 check = requireAssetTransfersActivated() + strict checkStrictTransfers = requireStrictC2ETransfersActivated() + appendBlock_base(i.originCaller, blockHashHex, referenceHex, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } @Callable(i) @@ -904,7 +1003,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) @@ -959,6 +1058,89 @@ 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 = match transferOpt { + case s: String => s + case _ => throw("Transfer #" + failedC2ETransferIndex.toString() + " not found") + } + let parts = transferStr.split(SEP) + + strict nativeTransferBeforeStrictGuard = + if (parts.size() == 2) then { + # - Native transfer, before strict transfers activation + # {destElAddressHex with 0x}_{amount} + throw("Refund is supported only for strict C2E transfers") + } else true + + strict assetTransferBeforeStrictGuard = + if (parts.size() == 4 && parts[0].take(2) == "0x" && parts[1].take(2) == "0x") then { + # - Asset transfer, before strict transfers activation + # {destElAddressHex with 0x}_{fromClAddressHex with 0x}_{amount}_{assetRegistryIndex} + throw("Refund is supported only for strict C2E transfers") + } else true + + # - 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 c04e0f3f..c3111132 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 @@ -12,10 +13,10 @@ import com.wavesplatform.network.ChannelGroupExt import com.wavesplatform.state.diffs.FeeValidation.{FeeConstants, FeeUnit, ScriptExtraFee} import com.wavesplatform.state.diffs.TransactionDiffer.TransactionValidationError import com.wavesplatform.state.{Blockchain, BooleanDataEntry} +import com.wavesplatform.transaction.* import com.wavesplatform.transaction.TxValidationError.InvokeRejectError import com.wavesplatform.transaction.smart.InvokeScriptTransaction import com.wavesplatform.transaction.smart.script.trace.TracedResult -import com.wavesplatform.transaction.* import com.wavesplatform.utils.{Time, UnsupportedFeature, forceStopApplication} import com.wavesplatform.wallet.Wallet import io.netty.channel.Channel @@ -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 { @@ -414,6 +431,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( @@ -425,19 +446,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 ) @@ -478,6 +501,8 @@ class ELUpdater( scheduler.scheduleOnceLabeled("forgeSecond", (nextBlockUnixTs - time.correctedTime() / 1000).min(1).seconds)( tryToForgeNextBlock( payloadOrId = nextMiningData.payload, + nextMiningData.transfers, // TODO: nextMiningData.transfers or transfers + nextMiningData.transferTransactions, // TODO: nextMiningData.transferTransactions or transferTransactions referenceHash = ecBlock.hash, timestamp = nextBlockUnixTs, contractFunction = chainContractOptions.appendFunction(epochInfo.number, ecBlock.hash), @@ -1330,16 +1355,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." ) @@ -1400,7 +1427,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) @@ -1411,11 +1447,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) @@ -1448,7 +1485,7 @@ class ELUpdater( case x: ContractTransfer.Asset => Right(x) } - val depositedTransactions = updateAssetRegistryTransaction.toVector ++ + val transferTransactions = (for { sba <- chainContractOptions.elStandardBridgeAddress.toVector transfer <- nativeAndAssetTransfersViaDeposits @@ -1474,17 +1511,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 failedTransfersSet = failedTransfers.toSet + 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)}" + } @tailrec def loop( @@ -1524,24 +1587,58 @@ class ELUpdater( case expectedTransfer: ContractTransfer.NativeViaDeposit => if strictC2ETransfersActivated then { actualTransferLogs match { - case Nil => s"$logPrefix Not found EL transfer log, expected $expectedTransfer transfer".asLeft + case Nil => + val canSkipFailedTransfer = strictC2ETransfersActivated && failedTransfersSet.contains(expectedTransfer) + if canSkipFailedTransfer then { + logger.debug(s"Transfer $expectedTransfer has failed, skipping") + prevWithdrawalIndex.asRight + } else 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 Left(e) => + val canSkipFailedTransfer = strictC2ETransfersActivated && failedTransfersSet.contains(expectedTransfer) + if canSkipFailedTransfer then { + logger.debug(s"Transfer $expectedTransfer has failed, skipping") + loop( + actualWithdrawals, + actualTransferLog :: restActualTransferLogs, + restExpectedTransfers, + prevWithdrawalIndex, + currTransferNumber + 1 + ) + } else e.asLeft case _ => loop(actualWithdrawals, restActualTransferLogs, restExpectedTransfers, prevWithdrawalIndex, currTransferNumber + 1) } } } else Left("Native transfers via deposits are unexpected before strict C2E transfers activation") case expectedTransfer: ContractTransfer.Asset => actualTransferLogs match { - case Nil => s"$logPrefix Not found EL transfer log, expected $expectedTransfer transfer".asLeft + case Nil => + val canSkipFailedTransfer = strictC2ETransfersActivated && failedTransfersSet.contains(expectedTransfer) + if canSkipFailedTransfer + then { + logger.debug(s"Transfer $expectedTransfer has failed, skipping") + prevWithdrawalIndex.asRight + } else s"$logPrefix Not found EL transfer log, expected $expectedTransfer transfer".asLeft case actualTransferLog :: restActualTransferLogs => StandardBridge.ERC20BridgeFinalized .decodeLog(actualTransferLog) .flatMap(validateC2EAssetTransfer(actualTransferLog.logIndex, _, expectedTransfer, strictC2ETransfersActivated)) match { - case Left(e) => e.asLeft + case Left(e) => + val canSkipFailedTransfer = strictC2ETransfersActivated && failedTransfersSet.contains(expectedTransfer) + if canSkipFailedTransfer + then { + logger.debug(s"Transfer $expectedTransfer has failed, skipping") + loop( + actualWithdrawals, + actualTransferLog :: restActualTransferLogs, + restExpectedTransfers, + prevWithdrawalIndex, + currTransferNumber + 1 + ) + } else e.asLeft case _ => loop(actualWithdrawals, restActualTransferLogs, restExpectedTransfers, prevWithdrawalIndex, currTransferNumber + 1) } } @@ -1549,7 +1646,10 @@ class ELUpdater( } } - loop(actualWithdrawals, actualTransferLogs, expectedTransfers, prevWithdrawalIndex, currTransferNumber = 1) + for { + _ <- failedC2ETransfersRootHashCheck + res <- loop(actualWithdrawals, actualTransferLogs, expectedTransfers, prevWithdrawalIndex, currTransferNumber = 1) + } yield res } private def validateAndApplyBlockFull( @@ -1804,7 +1904,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) { diff --git a/src/main/scala/units/client/contract/ChainContractClient.scala b/src/main/scala/units/client/contract/ChainContractClient.scala index 238b2cef..a7a3db38 100644 --- a/src/main/scala/units/client/contract/ChainContractClient.scala +++ b/src/main/scala/units/client/contract/ChainContractClient.scala @@ -6,7 +6,7 @@ import com.wavesplatform.common.state.ByteStr import com.wavesplatform.consensus.{FairPoSCalculator, PoSCalculator} import com.wavesplatform.lang.Global import com.wavesplatform.serialization.ByteBufferOps -import com.wavesplatform.state.{BinaryDataEntry, Blockchain, BooleanDataEntry, DataEntry, EmptyDataEntry, IntegerDataEntry, StringDataEntry} +import com.wavesplatform.state.* import com.wavesplatform.transaction.Asset import org.web3j.utils.Numeric.cleanHexPrefix import units.ELUpdater.EpochInfo @@ -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) @@ -222,7 +223,8 @@ trait ChainContractClient { .getOrElse(throw new IllegalStateException("elBridgeAddress is empty on contract")), elStandardBridgeAddress = getStringData("elStandardBridgeAddress") .map(EthAddress.unsafeFrom), - assetTransfersActivationEpoch = getAssetTransfersActivationEpoch + assetTransfersActivationEpoch = getAssetTransfersActivationEpoch, + strictC2ETransfersActivationEpoch = getStrictC2ETransfersActivationEpoch ) private def getAssetTransfersActivationEpoch: Long = getLongData("assetTransfersActivationEpoch").getOrElse(Long.MaxValue) diff --git a/src/main/scala/units/client/contract/ChainContractOptions.scala b/src/main/scala/units/client/contract/ChainContractOptions.scala index 4df04dbc..c88f2093 100644 --- a/src/main/scala/units/client/contract/ChainContractOptions.scala +++ b/src/main/scala/units/client/contract/ChainContractOptions.scala @@ -12,7 +12,8 @@ case class ChainContractOptions( miningReward: Gwei, elNativeBridgeAddress: EthAddress, elStandardBridgeAddress: Option[EthAddress], - assetTransfersActivationEpoch: Long + assetTransfersActivationEpoch: Long, + strictC2ETransfersActivationEpoch: Long ) { def bridgeAddresses(epoch: Int): List[EthAddress] = { val before = List(elNativeBridgeAddress) @@ -33,5 +34,9 @@ case class ChainContractOptions( def appendFunction(epoch: Int, reference: BlockHash): AppendBlock = AppendBlock(reference, versionOf(epoch)) - private def versionOf(epoch: Int): Int = if (epoch < assetTransfersActivationEpoch) 1 else 2 + private def versionOf(epoch: Int): Int = () match { + case _ if epoch < assetTransfersActivationEpoch => 1 + case _ if epoch >= assetTransfersActivationEpoch && epoch < strictC2ETransfersActivationEpoch => 2 + case _ => 3 + } } diff --git a/src/main/scala/units/client/contract/ContractBlock.scala b/src/main/scala/units/client/contract/ContractBlock.scala index 530546b0..af305fcc 100644 --- a/src/main/scala/units/client/contract/ContractBlock.scala +++ b/src/main/scala/units/client/contract/ContractBlock.scala @@ -15,12 +15,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 bb137a53..596958ff 100644 --- a/src/main/scala/units/el/DepositedTransaction.scala +++ b/src/main/scala/units/el/DepositedTransaction.scala @@ -8,6 +8,7 @@ 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 java.math.BigInteger @@ -75,6 +76,8 @@ case class DepositedTransaction( this.data == that.data case _ => false } + + def hash: String = HexBytesConverter.toHex(Keccak256.hash(HexBytesConverter.toBytes(this.toHex))) } object DepositedTransaction { 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 9da1d2d8..6fb27d04 100644 --- a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala +++ b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala @@ -370,6 +370,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, @@ -389,7 +414,8 @@ trait HasConsensusLayerDappTxHelpers { } object HasConsensusLayerDappTxHelpers { - val EmptyE2CTransfersRootHashHex = EthereumConstants.NullHex + val EmptyE2CTransfersRootHashHex = EthereumConstants.NullHex + val EmptyFailedC2ETransfersRootHashHex = EthereumConstants.NullHex object DefaultFees { object ChainContract { @@ -407,6 +433,7 @@ object HasConsensusLayerDappTxHelpers { val reportEmptyEpochFee = 0.1.waves val claimEmptyEpochReportRewardsFee = 0.1.waves val stopFee = 0.1.waves + val refundFailedC2ETransferFee = 0.1.waves } } } From 325c7a93d87d58a1ac277c8c89543a2de0086802 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Thu, 6 Nov 2025 18:25:12 +0400 Subject: [PATCH 02/16] Simplify validation after strict transfers activation --- src/main/scala/units/ELUpdater.scala | 80 ++++++---------------------- 1 file changed, 15 insertions(+), 65 deletions(-) diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala index c3111132..4e9c0ec9 100644 --- a/src/main/scala/units/ELUpdater.scala +++ b/src/main/scala/units/ELUpdater.scala @@ -1539,7 +1539,6 @@ class ELUpdater( ): Either[String, Long] = { val totalTransfers = expectedTransfers.size val failedTransfers = getFailedTransfers(actualTransferLogs, expectedTransfers.zip(transferTransactions)) - val failedTransfersSet = failedTransfers.toSet val actualFailedTransfersRootHash = BridgeMerkleTree.getFailedTransfersRootHash(failedTransfers.map(_.index)) val failedC2ETransfersRootHashCheck = @@ -1572,73 +1571,25 @@ 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 => - val canSkipFailedTransfer = strictC2ETransfersActivated && failedTransfersSet.contains(expectedTransfer) - if canSkipFailedTransfer then { - logger.debug(s"Transfer $expectedTransfer has failed, skipping") - prevWithdrawalIndex.asRight - } else 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) => - val canSkipFailedTransfer = strictC2ETransfersActivated && failedTransfersSet.contains(expectedTransfer) - if canSkipFailedTransfer then { - logger.debug(s"Transfer $expectedTransfer has failed, skipping") - loop( - actualWithdrawals, - actualTransferLog :: restActualTransferLogs, - restExpectedTransfers, - prevWithdrawalIndex, - currTransferNumber + 1 - ) - } else 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 => - val canSkipFailedTransfer = strictC2ETransfersActivated && failedTransfersSet.contains(expectedTransfer) - if canSkipFailedTransfer - then { - logger.debug(s"Transfer $expectedTransfer has failed, skipping") - prevWithdrawalIndex.asRight - } else s"$logPrefix Not found EL transfer log, expected $expectedTransfer transfer".asLeft + case Nil => s"$logPrefix Not found EL transfer log, expected $expectedTransfer transfer".asLeft case actualTransferLog :: restActualTransferLogs => StandardBridge.ERC20BridgeFinalized .decodeLog(actualTransferLog) .flatMap(validateC2EAssetTransfer(actualTransferLog.logIndex, _, expectedTransfer, strictC2ETransfersActivated)) match { - case Left(e) => - val canSkipFailedTransfer = strictC2ETransfersActivated && failedTransfersSet.contains(expectedTransfer) - if canSkipFailedTransfer - then { - logger.debug(s"Transfer $expectedTransfer has failed, skipping") - loop( - actualWithdrawals, - actualTransferLog :: restActualTransferLogs, - restExpectedTransfers, - prevWithdrawalIndex, - currTransferNumber + 1 - ) - } else e.asLeft + case Left(e) => e.asLeft case _ => loop(actualWithdrawals, restActualTransferLogs, restExpectedTransfers, prevWithdrawalIndex, currTransferNumber + 1) } } @@ -1646,10 +1597,9 @@ class ELUpdater( } } - for { - _ <- failedC2ETransfersRootHashCheck - res <- loop(actualWithdrawals, actualTransferLogs, expectedTransfers, prevWithdrawalIndex, currTransferNumber = 1) - } yield res + if strictC2ETransfersActivated + then failedC2ETransfersRootHashCheck.map(_ => prevWithdrawalIndex) + else loop(actualWithdrawals, actualTransferLogs, expectedTransfers, prevWithdrawalIndex, currTransferNumber = 1) } private def validateAndApplyBlockFull( From 141448fd213585b7147b542d09955f27fd7c92c8 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Thu, 6 Nov 2025 18:28:14 +0400 Subject: [PATCH 03/16] Remove validateC2ENativeTransfer --- src/main/scala/units/ELUpdater.scala | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala index 4e9c0ec9..e23e212d 100644 --- a/src/main/scala/units/ELUpdater.scala +++ b/src/main/scala/units/ELUpdater.scala @@ -1902,26 +1902,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, From e740a1ba1822bbedde3065acf6745adaa2da2f33 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Thu, 6 Nov 2025 18:57:07 +0400 Subject: [PATCH 04/16] Rename test suites --- ...Suite2.scala => MultipleFailedAssetTransfersTestSuite.scala} | 2 +- ...estSuite1.scala => SingleFailedAssetTransferTestSuite.scala} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename consensus-client-it/src/test/scala/units/{FailedTransfersTestSuite2.scala => MultipleFailedAssetTransfersTestSuite.scala} (99%) rename consensus-client-it/src/test/scala/units/{FailedTransfersTestSuite1.scala => SingleFailedAssetTransferTestSuite.scala} (99%) diff --git a/consensus-client-it/src/test/scala/units/FailedTransfersTestSuite2.scala b/consensus-client-it/src/test/scala/units/MultipleFailedAssetTransfersTestSuite.scala similarity index 99% rename from consensus-client-it/src/test/scala/units/FailedTransfersTestSuite2.scala rename to consensus-client-it/src/test/scala/units/MultipleFailedAssetTransfersTestSuite.scala index 7f397eb3..981881ab 100644 --- a/consensus-client-it/src/test/scala/units/FailedTransfersTestSuite2.scala +++ b/consensus-client-it/src/test/scala/units/MultipleFailedAssetTransfersTestSuite.scala @@ -17,7 +17,7 @@ import units.eth.EthAddress import scala.annotation.tailrec import scala.jdk.OptionConverters.RichOptional -class FailedTransfersTestSuite2 extends BaseDockerTestSuite { +class MultipleFailedAssetTransfersTestSuite extends BaseDockerTestSuite { private val clRecipient = clRichAccount1 private val elSender = elRichAccount1 private val elSenderAddress = elRichAddress1 diff --git a/consensus-client-it/src/test/scala/units/FailedTransfersTestSuite1.scala b/consensus-client-it/src/test/scala/units/SingleFailedAssetTransferTestSuite.scala similarity index 99% rename from consensus-client-it/src/test/scala/units/FailedTransfersTestSuite1.scala rename to consensus-client-it/src/test/scala/units/SingleFailedAssetTransferTestSuite.scala index f96a1c02..709b5f3f 100644 --- a/consensus-client-it/src/test/scala/units/FailedTransfersTestSuite1.scala +++ b/consensus-client-it/src/test/scala/units/SingleFailedAssetTransferTestSuite.scala @@ -18,7 +18,7 @@ import units.eth.EthAddress import scala.annotation.tailrec import scala.jdk.OptionConverters.RichOptional -class FailedTransfersTestSuite1 extends BaseDockerTestSuite { +class SingleFailedAssetTransferTestSuite extends BaseDockerTestSuite { private val clRecipient = clRichAccount1 private val elSender = elRichAccount1 private val elSenderAddress = elRichAddress1 From 079bf5f120eef1e533078543723e092bbdfd96e8 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Fri, 7 Nov 2025 15:29:02 +0400 Subject: [PATCH 05/16] Cleanup --- src/main/scala/units/ELUpdater.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala index e23e212d..2fd363b3 100644 --- a/src/main/scala/units/ELUpdater.scala +++ b/src/main/scala/units/ELUpdater.scala @@ -501,8 +501,8 @@ class ELUpdater( scheduler.scheduleOnceLabeled("forgeSecond", (nextBlockUnixTs - time.correctedTime() / 1000).min(1).seconds)( tryToForgeNextBlock( payloadOrId = nextMiningData.payload, - nextMiningData.transfers, // TODO: nextMiningData.transfers or transfers - nextMiningData.transferTransactions, // TODO: nextMiningData.transferTransactions or transferTransactions + transfers, + transferTransactions, referenceHash = ecBlock.hash, timestamp = nextBlockUnixTs, contractFunction = chainContractOptions.appendFunction(epochInfo.number, ecBlock.hash), From dc5c8555a781bb0ba9f1633be5fd8bd611c607b1 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Fri, 7 Nov 2025 15:42:58 +0400 Subject: [PATCH 06/16] Add log --- src/main/scala/units/ELUpdater.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala index 2fd363b3..2855b4f1 100644 --- a/src/main/scala/units/ELUpdater.scala +++ b/src/main/scala/units/ELUpdater.scala @@ -1547,7 +1547,7 @@ class ELUpdater( + s"EL=${toHexNoPrefix(actualFailedTransfersRootHash)}, " + s"CL=${toHexNoPrefix(expectedFailedC2ETransfersRootHash)}" } - + logger.debug(s"Failed C2E transfers: ${failedTransfers.mkString(", ")}") @tailrec def loop( actualWithdrawals: Seq[Withdrawal], From 26fed16f22be9edec0bfcff114cf27a632d7abd7 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Fri, 7 Nov 2025 19:28:47 +0400 Subject: [PATCH 07/16] Log failed transfers only if non-empty --- src/main/scala/units/ELUpdater.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala index 2855b4f1..cce2f58b 100644 --- a/src/main/scala/units/ELUpdater.scala +++ b/src/main/scala/units/ELUpdater.scala @@ -1547,7 +1547,8 @@ class ELUpdater( + s"EL=${toHexNoPrefix(actualFailedTransfersRootHash)}, " + s"CL=${toHexNoPrefix(expectedFailedC2ETransfersRootHash)}" } - logger.debug(s"Failed C2E transfers: ${failedTransfers.mkString(", ")}") + if (failedTransfers.nonEmpty) logger.debug(s"Failed C2E transfers: ${failedTransfers.mkString(", ")}") + @tailrec def loop( actualWithdrawals: Seq[Withdrawal], From 236fa0b22e5cc2df5984918ffd7ffa33332c6fdb Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Mon, 10 Nov 2025 18:56:31 +0400 Subject: [PATCH 08/16] Update contracts/waves/src/main.ride Co-authored-by: Vyatcheslav Suharnikov --- contracts/waves/src/main.ride | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/waves/src/main.ride b/contracts/waves/src/main.ride index f4ac5589..8b4ec5ef 100644 --- a/contracts/waves/src/main.ride +++ b/contracts/waves/src/main.ride @@ -1073,10 +1073,7 @@ func refundFailedC2ETransfer(blockHashHex: String, merkleProof: List[ByteVector] ) let transferOpt = getString(transferKey(failedC2ETransferIndex)) - let transferStr = match transferOpt { - case s: String => s - case _ => throw("Transfer #" + failedC2ETransferIndex.toString() + " not found") - } + let transferStr = transferOpt.valueOrErrorMessage("Transfer #" + failedC2ETransferIndex.toString() + " not found") let parts = transferStr.split(SEP) strict nativeTransferBeforeStrictGuard = From 5725dd9d91b79f99d25a1eb154e7c0a25c2d82fe Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Mon, 10 Nov 2025 19:00:04 +0400 Subject: [PATCH 09/16] Update contracts/waves/src/main.ride Co-authored-by: Vyatcheslav Suharnikov --- contracts/waves/src/main.ride | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/waves/src/main.ride b/contracts/waves/src/main.ride index 8b4ec5ef..55fb4a97 100644 --- a/contracts/waves/src/main.ride +++ b/contracts/waves/src/main.ride @@ -1077,11 +1077,11 @@ func refundFailedC2ETransfer(blockHashHex: String, merkleProof: List[ByteVector] let parts = transferStr.split(SEP) strict nativeTransferBeforeStrictGuard = - if (parts.size() == 2) then { + parts.size() == 2 && { # - Native transfer, before strict transfers activation # {destElAddressHex with 0x}_{amount} throw("Refund is supported only for strict C2E transfers") - } else true + } strict assetTransferBeforeStrictGuard = if (parts.size() == 4 && parts[0].take(2) == "0x" && parts[1].take(2) == "0x") then { From e9caacba806b7c8586218ebd29453f98de7e6592 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Mon, 10 Nov 2025 19:10:57 +0400 Subject: [PATCH 10/16] Refactor assetTransferBeforeStrictGuard --- contracts/waves/src/main.ride | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/waves/src/main.ride b/contracts/waves/src/main.ride index 55fb4a97..f58b1661 100644 --- a/contracts/waves/src/main.ride +++ b/contracts/waves/src/main.ride @@ -1084,11 +1084,11 @@ func refundFailedC2ETransfer(blockHashHex: String, merkleProof: List[ByteVector] } strict assetTransferBeforeStrictGuard = - if (parts.size() == 4 && parts[0].take(2) == "0x" && parts[1].take(2) == "0x") then { + (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") - } else true + } # - Native transfer, after strict transfers activation # {epoch}_{destElAddressHex with 0x}_{fromClAddressHex with 0x}_{amount} From 56b252e20e6e9949e0ac83a3e62d736eeccf5760 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Mon, 10 Nov 2025 19:20:20 +0400 Subject: [PATCH 11/16] Update src/main/scala/units/ELUpdater.scala Co-authored-by: Vyatcheslav Suharnikov --- src/main/scala/units/ELUpdater.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala index cce2f58b..2fc0157f 100644 --- a/src/main/scala/units/ELUpdater.scala +++ b/src/main/scala/units/ELUpdater.scala @@ -1543,8 +1543,8 @@ class ELUpdater( 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"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(", ")}") From 666b74c084da7be87ad1937766cc8b9978cec137 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Mon, 10 Nov 2025 19:28:16 +0400 Subject: [PATCH 12/16] Update src/main/scala/units/el/DepositedTransaction.scala Co-authored-by: Vyatcheslav Suharnikov --- src/main/scala/units/el/DepositedTransaction.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/units/el/DepositedTransaction.scala b/src/main/scala/units/el/DepositedTransaction.scala index 596958ff..6eaaf103 100644 --- a/src/main/scala/units/el/DepositedTransaction.scala +++ b/src/main/scala/units/el/DepositedTransaction.scala @@ -77,7 +77,7 @@ case class DepositedTransaction( case _ => false } - def hash: String = HexBytesConverter.toHex(Keccak256.hash(HexBytesConverter.toBytes(this.toHex))) + def hash: String = HexBytesConverter.toHex(Keccak256.hash(this.toBytes)) } object DepositedTransaction { From 8a156c79ae7fc1fb64618adc1abaf36c38dc5ee2 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Mon, 10 Nov 2025 19:50:57 +0400 Subject: [PATCH 13/16] Refactor DepositedTransaction.hash --- src/main/scala/units/el/DepositedTransaction.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/scala/units/el/DepositedTransaction.scala b/src/main/scala/units/el/DepositedTransaction.scala index 6eaaf103..53d8fb59 100644 --- a/src/main/scala/units/el/DepositedTransaction.scala +++ b/src/main/scala/units/el/DepositedTransaction.scala @@ -9,7 +9,7 @@ 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 @@ -56,7 +56,7 @@ 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) @@ -77,6 +77,8 @@ case class DepositedTransaction( case _ => false } + def toBytes: Array[Byte] = HexBytesConverter.toBytes(this.toHex) + def hash: String = HexBytesConverter.toHex(Keccak256.hash(this.toBytes)) } @@ -102,7 +104,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 From 74b8e41ee03a4ec93c9f2bd26b868aa5247ba1d5 Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Mon, 10 Nov 2025 19:54:27 +0400 Subject: [PATCH 14/16] Remove redundant checks --- contracts/waves/src/main.ride | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/waves/src/main.ride b/contracts/waves/src/main.ride index f58b1661..f1b438d8 100644 --- a/contracts/waves/src/main.ride +++ b/contracts/waves/src/main.ride @@ -783,7 +783,6 @@ func extendMainChain_v3( lastAssetRegistryIndex: Int, failedC2ETransfersRootHashHex: String ) = { - strict check = requireAssetTransfersActivated() strict checkStrictTransfers = requireStrictC2ETransfersActivated() extendMainChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } @@ -825,7 +824,6 @@ func startAltChain_v3( lastAssetRegistryIndex: Int, failedC2ETransfersRootHashHex: String ) = { - strict check = requireAssetTransfersActivated() strict checkStrictTransfers = requireStrictC2ETransfersActivated() startAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } @@ -870,7 +868,6 @@ func extendAltChain_v3( lastAssetRegistryIndex: Int, failedC2ETransfersRootHashHex: String ) = { - strict check = requireAssetTransfersActivated() strict checkStrictTransfers = requireStrictC2ETransfersActivated() extendAltChain_base(i.originCaller, blockHashHex, referenceHex, vrf, chainId, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } @@ -909,7 +906,6 @@ func appendBlock_v3( lastAssetRegistryIndex: Int, failedC2ETransfersRootHashHex: String ) = { - strict check = requireAssetTransfersActivated() strict checkStrictTransfers = requireStrictC2ETransfersActivated() appendBlock_base(i.originCaller, blockHashHex, referenceHex, e2cTransfersRootHashHex, lastC2ETransferIndex, lastAssetRegistryIndex, failedC2ETransfersRootHashHex) } From 73c5a46ea9ad51a36373f4763d22f1fdf748b33a Mon Sep 17 00:00:00 2001 From: Vladimir Logachev Date: Tue, 11 Nov 2025 11:38:39 +0400 Subject: [PATCH 15/16] Refactor DepositedTransaction --- src/main/scala/units/el/DepositedTransaction.scala | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/scala/units/el/DepositedTransaction.scala b/src/main/scala/units/el/DepositedTransaction.scala index 53d8fb59..6e5ccfda 100644 --- a/src/main/scala/units/el/DepositedTransaction.scala +++ b/src/main/scala/units/el/DepositedTransaction.scala @@ -46,8 +46,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)), @@ -59,11 +58,12 @@ case class DepositedTransaction( 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 => this.sourceHash.sameElements(that.sourceHash) && @@ -77,8 +77,6 @@ case class DepositedTransaction( case _ => false } - def toBytes: Array[Byte] = HexBytesConverter.toBytes(this.toHex) - def hash: String = HexBytesConverter.toHex(Keccak256.hash(this.toBytes)) } From c20ec76592645606d5ca0f349cbcda1024eabe01 Mon Sep 17 00:00:00 2001 From: Sergey Nazarov Date: Thu, 20 Nov 2025 11:44:52 +0300 Subject: [PATCH 16/16] fixes --- .../scala/units/MultipleFailedAssetTransfersTestSuite.scala | 2 +- .../test/scala/units/SingleFailedAssetTransferTestSuite.scala | 2 +- src/main/scala/units/client/contract/ChainContractClient.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/consensus-client-it/src/test/scala/units/MultipleFailedAssetTransfersTestSuite.scala b/consensus-client-it/src/test/scala/units/MultipleFailedAssetTransfersTestSuite.scala index 981881ab..fe07489f 100644 --- a/consensus-client-it/src/test/scala/units/MultipleFailedAssetTransfersTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/MultipleFailedAssetTransfersTestSuite.scala @@ -218,7 +218,7 @@ class MultipleFailedAssetTransfersTestSuite extends BaseDockerTestSuite { waves1.api.broadcastAndWait( TxHelpers.dataEntry( chainContractAccount, - IntegerDataEntry("strictC2ETransfersActivationEpoch", activationEpoch) + IntegerDataEntry("strictC2ETransfersActivationEpoch", activationEpoch.toInt) ) ) diff --git a/consensus-client-it/src/test/scala/units/SingleFailedAssetTransferTestSuite.scala b/consensus-client-it/src/test/scala/units/SingleFailedAssetTransferTestSuite.scala index 709b5f3f..9632a110 100644 --- a/consensus-client-it/src/test/scala/units/SingleFailedAssetTransferTestSuite.scala +++ b/consensus-client-it/src/test/scala/units/SingleFailedAssetTransferTestSuite.scala @@ -178,7 +178,7 @@ class SingleFailedAssetTransferTestSuite extends BaseDockerTestSuite { waves1.api.broadcastAndWait( TxHelpers.dataEntry( chainContractAccount, - IntegerDataEntry("strictC2ETransfersActivationEpoch", activationEpoch) + IntegerDataEntry("strictC2ETransfersActivationEpoch", activationEpoch.toInt) ) ) diff --git a/src/main/scala/units/client/contract/ChainContractClient.scala b/src/main/scala/units/client/contract/ChainContractClient.scala index 8ccfa80c..20f01a4a 100644 --- a/src/main/scala/units/client/contract/ChainContractClient.scala +++ b/src/main/scala/units/client/contract/ChainContractClient.scala @@ -239,7 +239,7 @@ trait ChainContractClient { getStringData("elStandardBridgeAddress") .map(EthAddress.unsafeFrom), getAssetTransfersActivationEpoch, - getStrictC2ETransfersActivationEpoch + getStrictC2ETransfersActivationEpoch, blockDelay ) }