Skip to content

Commit

Permalink
NODE-2531 Corrected validation order of sync payment balance (#3892)
Browse files Browse the repository at this point in the history
  • Loading branch information
xrtm000 authored Nov 30, 2023
1 parent c087618 commit c590007
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
package com.wavesplatform.transaction.smart.script.trace
import scala.util.Right

import com.wavesplatform.lang.ValidationError
import monix.eval.Coeval

Expand Down
Original file line number Diff line number Diff line change
@@ -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"
)
}
}
}

0 comments on commit c590007

Please sign in to comment.