diff --git a/node/src/main/scala/com/wavesplatform/state/diffs/TransactionDiffer.scala b/node/src/main/scala/com/wavesplatform/state/diffs/TransactionDiffer.scala index bb3a66cb1b..25785141cb 100644 --- a/node/src/main/scala/com/wavesplatform/state/diffs/TransactionDiffer.scala +++ b/node/src/main/scala/com/wavesplatform/state/diffs/TransactionDiffer.scala @@ -108,7 +108,8 @@ object TransactionDiffer { InvokeRejectError(fte.message, fte.log) case fte: FailedTransactionError if fte.isFailFree && blockchain.isFeatureActivated(RideV6) => ScriptExecutionError(fte.message, fte.log, fte.assetId) - case err => err + case err => + err } .leftMap(TransactionValidationError(_, tx)) } diff --git a/node/src/main/scala/com/wavesplatform/state/diffs/invoke/InvokeScriptDiff.scala b/node/src/main/scala/com/wavesplatform/state/diffs/invoke/InvokeScriptDiff.scala index ecd082bffb..427ae759f8 100644 --- a/node/src/main/scala/com/wavesplatform/state/diffs/invoke/InvokeScriptDiff.scala +++ b/node/src/main/scala/com/wavesplatform/state/diffs/invoke/InvokeScriptDiff.scala @@ -242,7 +242,11 @@ object InvokeScriptDiff { _ = invocationRoot.setLog(log) spentComplexity = remainingComplexity - scriptResult.unusedComplexity.max(0) - _ <- validateIntermediateBalances(blockchain, resultSnapshot, spentComplexity, log) + _ <- + if (blockchain.isFeatureActivated(LightNode)) + traced(Right(())) + else + validateIntermediateBalances(blockchain, resultSnapshot, spentComplexity, log) doProcessActions = (actions: List[CallableAction], unusedComplexity: Int) => { val storingComplexity = complexityAfterPayments - unusedComplexity @@ -350,7 +354,11 @@ object InvokeScriptDiff { resultSnapshot <- traced( (resultSnapshot.setScriptsComplexity(0) |+| actionsSnapshot.addScriptsComplexity(paymentsComplexity)).asRight ) - _ <- validateIntermediateBalances(blockchain, resultSnapshot, resultSnapshot.scriptsComplexity, log) + _ <- + if (blockchain.isFeatureActivated(LightNode)) + traced(Right(())) + else + validateIntermediateBalances(blockchain, resultSnapshot, resultSnapshot.scriptsComplexity, log) _ = invocationRoot.setResult(scriptResult) } yield ( resultSnapshot, @@ -423,30 +431,31 @@ object InvokeScriptDiff { ) } - private def validateIntermediateBalances(blockchain: Blockchain, snapshot: StateSnapshot, spentComplexity: Long, log: Log[Id]) = traced( - if (blockchain.isFeatureActivated(BlockchainFeatures.RideV6)) { - BalanceDiffValidation(blockchain)(snapshot) - .leftMap { be => FailedTransactionError.dAppExecution(be.toString, spentComplexity, log) } - } else if (blockchain.height >= blockchain.settings.functionalitySettings.enforceTransferValidationAfter) { - // reject transaction if any balance is negative - snapshot.balances.view - .flatMap { - case ((address, asset), balance) if balance < 0 => Some(address -> asset) - case _ => None - } - .headOption - .fold[Either[ValidationError, Unit]](Right(())) { case (address, asset) => - val msg = asset match { - case Waves => - s"$address: Negative waves balance: old = ${blockchain.balance(address)}, new = ${snapshot.balances((address, Waves))}" - case ia: IssuedAsset => - s"$address: Negative asset $ia balance: old = ${blockchain.balance(address, ia)}, new = ${snapshot.balances((address, ia))}" + def validateIntermediateBalances(blockchain: Blockchain, snapshot: StateSnapshot, spentComplexity: Long, log: Log[Id]): CoevalR[Any] = + traced( + if (blockchain.isFeatureActivated(BlockchainFeatures.RideV6)) { + BalanceDiffValidation(blockchain)(snapshot) + .leftMap { be => FailedTransactionError.dAppExecution(be.toString, spentComplexity, log) } + } else if (blockchain.height >= blockchain.settings.functionalitySettings.enforceTransferValidationAfter) { + // reject transaction if any balance is negative + snapshot.balances.view + .flatMap { + case ((address, asset), balance) if balance < 0 => Some(address -> asset) + case _ => None + } + .headOption + .fold[Either[ValidationError, Unit]](Right(())) { case (address, asset) => + val msg = asset match { + case Waves => + s"$address: Negative waves balance: old = ${blockchain.balance(address)}, new = ${snapshot.balances((address, Waves))}" + case ia: IssuedAsset => + s"$address: Negative asset $ia balance: old = ${blockchain.balance(address, ia)}, new = ${snapshot.balances((address, ia))}" + } + Left(FailOrRejectError(msg)) } - Left(FailOrRejectError(msg)) - } - } else Right(()) - ) + } else Right(()) + ) private def ensurePaymentsAreNotNegative(blockchain: Blockchain, tx: InvokeScript, invoker: Address, dAppAddress: Address) = traced { tx.payments.collectFirst { diff --git a/node/src/main/scala/com/wavesplatform/transaction/smart/WavesEnvironment.scala b/node/src/main/scala/com/wavesplatform/transaction/smart/WavesEnvironment.scala index 8e451eded7..f8bbd054bf 100644 --- a/node/src/main/scala/com/wavesplatform/transaction/smart/WavesEnvironment.scala +++ b/node/src/main/scala/com/wavesplatform/transaction/smart/WavesEnvironment.scala @@ -24,6 +24,7 @@ import com.wavesplatform.lang.v1.traits.domain.Recipient.* import com.wavesplatform.lang.{Global, ValidationError} import com.wavesplatform.state.* import com.wavesplatform.state.BlockRewardCalculator.CurrentBlockRewardPart +import com.wavesplatform.state.diffs.invoke.InvokeScriptDiff.validateIntermediateBalances import com.wavesplatform.state.diffs.invoke.{InvokeScript, InvokeScriptDiff, InvokeScriptTransactionLike} import com.wavesplatform.state.SnapshotBlockchain import com.wavesplatform.transaction.Asset.* @@ -475,6 +476,11 @@ class DAppEnvironment( invocationTracker, wrapDAppEnv )(invoke) + _ <- + if (blockchain.isFeatureActivated(LightNode)) + validateIntermediateBalances(blockchain, snapshot, totalComplexityLimit - availableComplexity, Nil) + else + traced(Right(())) fixedSnapshot = snapshot .setScriptResults(Map(txId -> InvokeScriptResult(invokes = Seq(invocation.copy(stateChanges = snapshot.scriptResults(txId)))))) } yield { diff --git a/node/src/main/scala/com/wavesplatform/transaction/smart/script/trace/CoevalR.scala b/node/src/main/scala/com/wavesplatform/transaction/smart/script/trace/CoevalR.scala index 1ce52abee1..3ae8623429 100644 --- a/node/src/main/scala/com/wavesplatform/transaction/smart/script/trace/CoevalR.scala +++ b/node/src/main/scala/com/wavesplatform/transaction/smart/script/trace/CoevalR.scala @@ -1,6 +1,4 @@ package com.wavesplatform.transaction.smart.script.trace -import scala.util.Right - import com.wavesplatform.lang.ValidationError import monix.eval.Coeval diff --git a/node/src/test/scala/com/wavesplatform/state/diffs/ci/sync/SyncInvokePaymentValidationOrderTest.scala b/node/src/test/scala/com/wavesplatform/state/diffs/ci/sync/SyncInvokePaymentValidationOrderTest.scala new file mode 100644 index 0000000000..ad3f88c1ee --- /dev/null +++ b/node/src/test/scala/com/wavesplatform/state/diffs/ci/sync/SyncInvokePaymentValidationOrderTest.scala @@ -0,0 +1,71 @@ +package com.wavesplatform.state.diffs.ci.sync + +import com.wavesplatform.db.WithDomain +import com.wavesplatform.db.WithState.AddrWithBalance +import com.wavesplatform.lang.directives.values.V7 +import com.wavesplatform.lang.v1.compiler.Terms.CONST_BOOLEAN +import com.wavesplatform.lang.v1.compiler.TestCompiler +import com.wavesplatform.test.DomainPresets.{BlockRewardDistribution, TransactionStateSnapshot} +import com.wavesplatform.test.{PropSpec, produce} +import com.wavesplatform.transaction.Asset.IssuedAsset +import com.wavesplatform.transaction.TxHelpers.* + +class SyncInvokePaymentValidationOrderTest extends PropSpec with WithDomain { + private val issueTx = issue() + private val asset = IssuedAsset(issueTx.id()) + private val dApp = TestCompiler(V7).compileContract( + s""" + | @Callable(i) + | func f1(bigComplexity: Boolean, error: Boolean) = { + | strict r = Address(base58'$defaultAddress').invoke("f2", [bigComplexity, error], [AttachedPayment(base58'$asset', 123)]) + | [] + | } + | + | @Callable(i) + | func f2(bigComplexity: Boolean, error: Boolean) = { + | strict c = if (bigComplexity) then ${(1 to 6).map(_ => "sigVerify(base58'', base58'', base58'')").mkString(" || ")} else 0 + | strict e = if (error) then throw("custom error") else 0 + | [] + | } + """.stripMargin + ) + + property("sync invoke payment should be validated after calling dApp if light node isn't activated") { + withDomain(BlockRewardDistribution, AddrWithBalance.enoughBalances(defaultSigner, secondSigner)) { d => + d.appendBlock(setScript(defaultSigner, dApp), setScript(secondSigner, dApp), issueTx) + d.appendAndAssertFailed( + invoke(invoker = secondSigner, func = Some("f1"), args = Seq(CONST_BOOLEAN(true), CONST_BOOLEAN(false))), + "negative asset balance" + ) + d.appendBlockE(invoke(invoker = secondSigner, func = Some("f1"), args = Seq(CONST_BOOLEAN(false), CONST_BOOLEAN(false)))) should produce( + "negative asset balance" + ) + } + } + + property("sync invoke payment should be validated before calling dApp if light node is activated") { + withDomain(TransactionStateSnapshot, AddrWithBalance.enoughBalances(defaultSigner, secondSigner)) { d => + d.appendBlock(setScript(defaultSigner, dApp), setScript(secondSigner, dApp), issueTx) + d.appendBlockE(invoke(invoker = secondSigner, func = Some("f1"), args = Seq(CONST_BOOLEAN(true), CONST_BOOLEAN(false)))) should produce( + "negative asset balance" + ) + d.appendBlockE(invoke(invoker = secondSigner, func = Some("f1"), args = Seq(CONST_BOOLEAN(false), CONST_BOOLEAN(false)))) should produce( + "negative asset balance" + ) + } + } + + property("sync invoke should be correctly rejected and failed on enough balance and RIDE error if light node is activated") { + withDomain(TransactionStateSnapshot, AddrWithBalance.enoughBalances(defaultSigner, secondSigner)) { d => + d.appendBlock(setScript(defaultSigner, dApp), setScript(secondSigner, dApp), issueTx) + d.appendBlock(transfer(asset = asset)) + d.appendBlockE(invoke(invoker = secondSigner, func = Some("f1"), args = Seq(CONST_BOOLEAN(false), CONST_BOOLEAN(true)))) should produce( + "custom error" + ) + d.appendAndAssertFailed( + invoke(invoker = secondSigner, func = Some("f1"), args = Seq(CONST_BOOLEAN(true), CONST_BOOLEAN(true))), + "custom error" + ) + } + } +}