All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.wavesplatform.state.diffs.invoke.InvokeScriptDiff.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform.state.diffs.invoke

import cats.Id
import cats.implicits.{catsSyntaxSemigroup, toFlatMapOps}
import cats.instances.list.*
import cats.syntax.either.*
import cats.syntax.traverseFilter.*
import com.wavesplatform.account.*
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.features.BlockchainFeatures
import com.wavesplatform.features.BlockchainFeatures.LightNode
import com.wavesplatform.features.EstimatorProvider.EstimatorBlockchainExt
import com.wavesplatform.features.EvaluatorFixProvider.*
import com.wavesplatform.features.FunctionCallPolicyProvider.*
import com.wavesplatform.lang.*
import com.wavesplatform.lang.contract.DApp
import com.wavesplatform.lang.directives.DirectiveSet
import com.wavesplatform.lang.directives.values.{DApp as DAppType, *}
import com.wavesplatform.lang.script.ContractScript.ContractScriptImpl
import com.wavesplatform.lang.v1.ContractLimits
import com.wavesplatform.lang.v1.compiler.Terms.*
import com.wavesplatform.lang.v1.evaluator.ctx.impl.unit
import com.wavesplatform.lang.v1.evaluator.{ContractEvaluator, IncompleteResult, Log, ScriptResult, ScriptResultV3, ScriptResultV4}
import com.wavesplatform.lang.v1.traits.Environment
import com.wavesplatform.lang.v1.traits.domain.Tx.{InvokePseudoTx, ScriptTransfer}
import com.wavesplatform.lang.v1.traits.domain.{Recipient as RideRecipient, *}
import com.wavesplatform.metrics.*
import com.wavesplatform.state.*
import com.wavesplatform.state.diffs.BalanceDiffValidation
import com.wavesplatform.state.diffs.invoke.CallArgumentPolicy.*
import com.wavesplatform.transaction.Asset.{IssuedAsset, Waves}
import com.wavesplatform.transaction.TxValidationError.*
import com.wavesplatform.transaction.smart.DAppEnvironment.ActionLimits
import com.wavesplatform.transaction.smart.script.ScriptRunner
import com.wavesplatform.transaction.smart.script.ScriptRunner.TxOrd
import com.wavesplatform.transaction.smart.script.trace.CoevalR.traced
import com.wavesplatform.transaction.smart.script.trace.{AssetVerifierTrace, CoevalR, TracedResult}
import com.wavesplatform.transaction.smart.{DApp as DAppTarget, *}
import com.wavesplatform.transaction.validation.impl.DataTxValidator
import com.wavesplatform.transaction.{TransactionType, TxValidationError}
import monix.eval.Coeval
import shapeless.Coproduct

object InvokeScriptDiff {
  private val stats = TxProcessingStats
  import stats.TxTimerExt

  def apply(
      blockchain: Blockchain,
      blockTime: Long,
      rootVersion: StdLibVersion,
      limitedExecution: Boolean,
      enableExecutionLog: Boolean,
      totalComplexityLimit: Int,
      remainingComplexity: Int,
      remainingCalls: Int,
      remainingActions: ActionLimits,
      remainingPayments: Int,
      calledAddresses: Set[Address],
      invocationRoot: DAppEnvironment.InvocationTreeTracker,
      wrapDAppEnv: DAppEnvironment => DAppEnvironmentInterface = identity
  )(
      tx: InvokeScript
  ): CoevalR[(StateSnapshot, EVALUATED, ActionLimits, Int)] = {
    val dAppAddress = tx.dApp
    val invoker     = tx.sender.toAddress

    val result = blockchain.accountScript(dAppAddress) match {
      case Some(AccountScriptInfo(pk, ContractScriptImpl(version, contract), _, _)) =>
        for {
          _ <- traced {
            if (blockchain.checkSyncCallArgumentsTypes)
              tx.funcCall.check(CallArgumentPolicy.PrimitivesAndListsOfPrimitives).leftMap(GenericError(_))
            else
              Right(())
          }
          _ <- traced(
            Either.cond(
              version >= V5,
              (),
              GenericError(
                s"DApp $invoker invoked DApp $dAppAddress that uses RIDE $version, but dApp-to-dApp invocation requires version 5 or higher"
              )
            )
          )
          _ <- traced(
            Either.cond(
              remainingCalls > 0,
              (),
              ValidationError.ScriptRunsLimitError(s"DApp calls limit = ${ContractLimits.MaxSyncDAppCalls(version)} is exceeded")
            )
          )
          _ <- traced(
            Either.cond(
              !blockchain.isFeatureActivated(BlockchainFeatures.RideV6) || remainingPayments >= tx.payments.size,
              (),
              GenericError(s"Invoke payments limit = ${ContractLimits.MaxTotalPaymentAmountRideV6} is exceeded")
            )
          )
          _ <- ensurePaymentsAreNotNegative(blockchain, tx, invoker, dAppAddress)

          directives: DirectiveSet <- traced(DirectiveSet(version, Account, DAppType).leftMap(GenericError.apply))
          payments <- traced(AttachedPaymentExtractor.extractPayments(tx, version, blockchain, DAppTarget).leftMap(GenericError.apply))
          checkedPayments <- traced {
            payments.payments.toList
              .traverseFilter {
                case (amount, Some(assetId)) =>
                  InvokeDiffsCommon
                    .checkAsset(blockchain, assetId)
                    .map(_ => blockchain.assetScript(IssuedAsset(assetId)).flatMap(s => Some((s, amount, assetId))))
                case _ => Right(None)
              }
              .leftMap(GenericError(_))
          }
          complexityAfterPaymentsTraced = checkedPayments.foldLeft(TracedResult(Right(remainingComplexity): TxValidationError.Validation[Int])) {
            case (error @ TracedResult(Left(_), _, _), _) => error
            case (TracedResult(Right(nextRemainingComplexity), _, _), (script, amount, assetId)) =>
              val usedComplexity = totalComplexityLimit - nextRemainingComplexity
              val pseudoTx = if (blockchain.isFeatureActivated(BlockchainFeatures.RideV6)) {
                InvokePseudoTx(
                  tx.txId,
                  tx.timestamp,
                  RideRecipient.Address(ByteStr(tx.sender.toAddress.bytes)),
                  tx.sender,
                  RideRecipient.Address(ByteStr(tx.dApp.bytes)),
                  None,
                  Some(tx.funcCall.function.funcName),
                  tx.funcCall.args.collect { case ev: EVALUATED => ev },
                  payments
                )
              } else {
                ScriptTransfer(
                  Some(assetId),
                  RideRecipient.Address(ByteStr(tx.sender.toAddress.bytes)),
                  tx.sender,
                  RideRecipient.Address(ByteStr(tx.dApp.bytes)),
                  amount,
                  tx.timestamp,
                  tx.txId
                )
              }
              val (log, evaluatedComplexity, result) = ScriptRunner(
                Coproduct[TxOrd](pseudoTx: PseudoTx),
                blockchain,
                script.script,
                isAssetScript = true,
                scriptContainerAddress = Coproduct[Environment.Tthis](Environment.AssetId(assetId.arr)),
                enableExecutionLog = enableExecutionLog,
                nextRemainingComplexity
              )
              val scriptComplexity = if (blockchain.storeEvaluatedComplexity) evaluatedComplexity else script.complexity.toInt
              val totalComplexity  = usedComplexity + scriptComplexity
              result match {
                case Left(error) =>
                  val err = FailedTransactionError.assetExecutionInAction(error.message, totalComplexity, log, assetId)
                  TracedResult(Left(err), List(AssetVerifierTrace(assetId, Some(err))))
                case Right(FALSE) =>
                  val err = FailedTransactionError.notAllowedByAsset(totalComplexity, log, assetId)
                  TracedResult(Left(err), List(AssetVerifierTrace(assetId, Some(err))))
                case Right(TRUE) =>
                  TracedResult(Right(nextRemainingComplexity - scriptComplexity))
                case Right(x) =>
                  val err = FailedTransactionError.assetExecution(s"Script returned not a boolean result, but $x", totalComplexity, log, assetId)
                  TracedResult(Left(err), List(AssetVerifierTrace(assetId, Some(err))))
              }
          }
          complexityAfterPayments <- CoevalR(Coeval.now(complexityAfterPaymentsTraced))
          paymentsComplexity = checkedPayments.map(_._1.complexity).sum.toInt

          tthis = Coproduct[Environment.Tthis](RideRecipient.Address(ByteStr(dAppAddress.bytes)))
          input <- traced(buildThisValue(Coproduct[TxOrd](tx.root), blockchain, directives, tthis).leftMap(GenericError(_)))

          result <- for {
            paymentsPart <- traced(InvokeDiffsCommon.paymentsPart(blockchain, tx, tx.dApp, Map()))
            (
              resultSnapshot,
              (scriptResult, log),
              availableActions,
              availablePayments
            ) <- {
              stats.invokedScriptExecution.measureForType(TransactionType.InvokeScript)({
                val height = blockchain.height
                val invocation = ContractEvaluator.Invocation(
                  tx.funcCall,
                  RideRecipient.Address(ByteStr(invoker.bytes)),
                  ByteStr(tx.sender.arr),
                  RideRecipient.Address(ByteStr(tx.root.sender.toAddress.bytes)),
                  ByteStr(tx.root.sender.arr),
                  payments,
                  tx.txId,
                  tx.root.fee,
                  tx.root.feeAssetId.compatId
                )
                val (paymentsPartInsideDApp, paymentsPartToResolve) =
                  if (version < V5) (StateSnapshot.empty, paymentsPart) else (paymentsPart, StateSnapshot.empty)
                val environment = wrapDAppEnv(new DAppEnvironment(
                  AddressScheme.current.chainId,
                  Coeval.evalOnce(input),
                  Coeval(height),
                  blockchain,
                  tthis,
                  directives,
                  rootVersion,
                  tx.root,
                  tx.dApp,
                  pk,
                  calledAddresses,
                  limitedExecution,
                  enableExecutionLog,
                  totalComplexityLimit,
                  remainingCalls - 1,
                  remainingActions,
                  remainingPayments - tx.payments.size,
                  paymentsPartInsideDApp,
                  invocationRoot,
                  wrapDAppEnv
                ))
                for {
                  _ <-
                    if (
                      blockchain.height >= blockchain.settings.functionalitySettings.paymentsCheckHeight && blockchain
                        .isFeatureActivated(BlockchainFeatures.LightNode)
                    )
                      validateIntermediateBalances(blockchain, paymentsPartInsideDApp, 0, Nil)
                    else traced(Right(()))
                  evaluated <- CoevalR(
                    evaluateV2(
                      version,
                      blockchain,
                      contract,
                      dAppAddress,
                      invocation,
                      environment,
                      complexityAfterPayments,
                      remainingComplexity,
                      enableExecutionLog
                    ).map(TracedResult(_))
                  )
                  resultSnapshot = environment.currentSnapshot |+| paymentsPartToResolve
                } yield (
                  resultSnapshot,
                  evaluated,
                  environment.availableActions,
                  environment.availablePayments
                )
              })
            }
            _               = invocationRoot.setLog(log)
            spentComplexity = remainingComplexity - scriptResult.unusedComplexity.max(0)

            _ <-
              if (blockchain.isFeatureActivated(LightNode))
                traced(Right(()))
              else
                validateIntermediateBalances(blockchain, resultSnapshot, spentComplexity, log)

            doProcessActions = (actions: List[CallableAction], unusedComplexity: Int) => {
              val storingComplexity = complexityAfterPayments - unusedComplexity
              CoevalR(
                Coeval.now(
                  InvokeDiffsCommon.processActions(
                    StructuredCallableActions(actions, blockchain),
                    version,
                    rootVersion,
                    dAppAddress,
                    pk,
                    storingComplexity,
                    tx,
                    SnapshotBlockchain(blockchain, resultSnapshot),
                    blockTime,
                    isSyncCall = true,
                    limitedExecution,
                    totalComplexityLimit,
                    Seq(),
                    enableExecutionLog,
                    log
                  )
                )
              )
            }

            process = {
              (
                  actions: List[CallableAction],
                  unusedComplexity: Int,
                  actionsCount: Int,
                  balanceActionsCount: Int,
                  assetActionsCount: Int,
                  dataCount: Int,
                  dataSize: Int,
                  ret: EVALUATED
              ) =>
                for {
                  _ <- CoevalR(
                    Coeval(
                      InvokeDiffsCommon.checkCallResultLimits(
                        version,
                        rootVersion,
                        blockchain,
                        remainingComplexity - unusedComplexity,
                        log,
                        actionsCount,
                        balanceActionsCount,
                        assetActionsCount,
                        dataCount,
                        dataSize,
                        availableActions
                      )
                    )
                  )
                  snapshot <- doProcessActions(actions, unusedComplexity)
                } yield (
                  snapshot,
                  ret,
                  availableActions.decrease(actionsCount, balanceActionsCount, assetActionsCount, dataCount, dataSize),
                  availablePayments
                )
            }

            (
              actionsSnapshot,
              evaluated,
              remainingActions1,
              remainingPayments1
            ) <-
              scriptResult match {
                case ScriptResultV3(dataItems, transfers, unusedComplexity) =>
                  val dataEntries  = dataItems.map(InvokeDiffsCommon.dataItemToEntry)
                  val dataCount    = dataItems.length
                  val dataSize     = DataTxValidator.invokeWriteSetSize(blockchain, dataEntries)
                  val actionsCount = transfers.length
                  process(dataItems ::: transfers, unusedComplexity, actionsCount, actionsCount, 0, dataCount, dataSize, unit)
                case ScriptResultV4(actions, unusedComplexity, ret) =>
                  val dataEntries = actions.collect { case d: DataOp => InvokeDiffsCommon.dataItemToEntry(d) }
                  val dataCount   = dataEntries.length
                  val balanceActionsCount = actions.collect {
                    case tr: AssetTransfer => tr
                    case l: Lease          => l
                    case lc: LeaseCancel   => lc
                  }.length
                  val assetActionsCount = actions.length - dataCount - balanceActionsCount
                  val dataSize          = DataTxValidator.invokeWriteSetSize(blockchain, dataEntries)
                  val actionsCount      = actions.length - dataCount
                  process(actions, unusedComplexity, actionsCount, balanceActionsCount, assetActionsCount, dataCount, dataSize, ret)
                case _: IncompleteResult if limitedExecution =>
                  doProcessActions(Nil, 0).map(
                    (
                      _,
                      unit,
                      availableActions,
                      availablePayments
                    )
                  )
                case r: IncompleteResult =>
                  val usedComplexity = remainingComplexity - r.unusedComplexity
                  val error =
                    FailedTransactionError.dAppExecution(s"Invoke complexity limit = $totalComplexityLimit is exceeded", usedComplexity, log)
                  traced(error.asLeft[(StateSnapshot, EVALUATED, ActionLimits, Int)])
              }
            resultSnapshot <- traced(
              (resultSnapshot.setScriptsComplexity(0) |+| actionsSnapshot.addScriptsComplexity(paymentsComplexity)).asRight
            )
            _ <-
              if (blockchain.isFeatureActivated(LightNode))
                traced(Right(()))
              else
                validateIntermediateBalances(blockchain, resultSnapshot, resultSnapshot.scriptsComplexity, log)
            _ = invocationRoot.setResult(scriptResult)
          } yield (
            resultSnapshot,
            evaluated,
            remainingActions1,
            remainingPayments1
          )
        } yield result

      case Some(AccountScriptInfo(_, _, _, _)) => traced(InvokeDiffsCommon.callExpressionError)
      case _                                   => traced(Left(GenericError(s"No contract at address ${tx.dApp}")))
    }

    result.leftMap { err =>
      invocationRoot.setError(err)
      err
    }
  }

  private[invoke] def evaluateV2(
      version: StdLibVersion,
      blockchain: Blockchain,
      contract: DApp,
      dAppAddress: Address,
      invocation: ContractEvaluator.Invocation,
      environment: Environment[Id],
      limit: Int,
      startComplexityLimit: Int,
      enableExecutionLog: Boolean
  ): Coeval[Either[ValidationError, (ScriptResult, Log[Id])]] = {
    val evaluationCtx = CachedDAppCTX.get(version, blockchain).completeContext(environment)
    ContractEvaluator
      .applyV2Coeval(
        evaluationCtx,
        contract,
        ByteStr(dAppAddress.bytes),
        invocation,
        version,
        limit,
        blockchain.correctFunctionCallScope,
        blockchain.newEvaluatorMode,
        enableExecutionLog,
        blockchain.isFeatureActivated(LightNode)
      )
      .map(
        _.leftMap[ValidationError] {
          case (reject @ FailOrRejectError(_, true), _, _) =>
            reject.copy(skipInvokeComplexity = false)
          case (error, unusedComplexity, log) =>
            val usedComplexity = startComplexityLimit - unusedComplexity
            val msg = error match {
              case CommonError(_, Some(fte: FailedTransactionError)) => fte.error.getOrElse(error.message)
              case _                                                 => error.message
            }
            FailedTransactionError.dAppExecution(msg, usedComplexity, log)
        }.flatTap { case (r, log) =>
          InvokeDiffsCommon
            .checkScriptResultFields(blockchain, r)
            .leftMap[ValidationError]({
              case reject: FailOrRejectError => reject
              case error =>
                val usedComplexity = startComplexityLimit - r.unusedComplexity
                val msg = error match {
                  case fte: FailedTransactionError => fte.error.getOrElse(error.toString)
                  case _                           => error.toString
                }
                FailedTransactionError.dAppExecution(msg, usedComplexity, log)
            })
        }
      )
  }

  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))
          }

      } else Right(())
    )

  private def ensurePaymentsAreNotNegative(blockchain: Blockchain, tx: InvokeScript, invoker: Address, dAppAddress: Address) = traced {
    tx.payments.collectFirst {
      case p if p.amount < 0 =>
        s"DApp $invoker invoked DApp $dAppAddress with attached ${p.assetId.fold("WAVES")(a => s"token $a")} amount = ${p.amount}"
    } match {
      case Some(e) if blockchain.isFeatureActivated(BlockchainFeatures.RideV6) =>
        Left(GenericError(e))
      case Some(e)
          if blockchain.isFeatureActivated(BlockchainFeatures.SynchronousCalls) &&
            blockchain.height >= blockchain.settings.functionalitySettings.enforceTransferValidationAfter =>
        Left(FailOrRejectError(e))
      case _ => Right(())
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy