com.wavesplatform.state.diffs.invoke.InvokeDiffsCommon.scala Maven / Gradle / Ivy
The newest version!
package com.wavesplatform.state.diffs.invoke
import cats.Id
import cats.implicits.*
import com.google.common.base.Throwables
import com.google.protobuf.ByteString
import com.wavesplatform.account.{Address, AddressOrAlias, PublicKey}
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.common.utils.EitherExt2
import com.wavesplatform.features.BlockchainFeatures
import com.wavesplatform.features.BlockchainFeatures.*
import com.wavesplatform.features.EstimatorProvider.*
import com.wavesplatform.features.InvokeScriptSelfPaymentPolicyProvider.*
import com.wavesplatform.features.ScriptTransferValidationProvider.*
import com.wavesplatform.lang.*
import com.wavesplatform.lang.directives.values.*
import com.wavesplatform.lang.script.Script
import com.wavesplatform.lang.v1.ContractLimits
import com.wavesplatform.lang.v1.compiler.Terms.*
import com.wavesplatform.lang.v1.evaluator.{Log, ScriptResult, ScriptResultV4}
import com.wavesplatform.lang.v1.traits.Environment
import com.wavesplatform.lang.v1.traits.domain.*
import com.wavesplatform.lang.v1.traits.domain.Tx.{BurnPseudoTx, ReissuePseudoTx, ScriptTransfer, SponsorFeePseudoTx}
import com.wavesplatform.state.*
import com.wavesplatform.state.diffs.FeeValidation.*
import com.wavesplatform.state.diffs.{BalanceDiffValidation, DiffsCommon}
import com.wavesplatform.state.SnapshotBlockchain
import com.wavesplatform.transaction.Asset.{IssuedAsset, Waves}
import com.wavesplatform.transaction.TxValidationError.*
import com.wavesplatform.transaction.assets.IssueTransaction
import com.wavesplatform.transaction.smart.*
import com.wavesplatform.transaction.smart.DAppEnvironment.ActionLimits
import com.wavesplatform.transaction.smart.InvokeScriptTransaction.Payment
import com.wavesplatform.transaction.smart.script.ScriptRunner
import com.wavesplatform.transaction.smart.script.ScriptRunner.TxOrd
import com.wavesplatform.transaction.smart.script.trace.AssetVerifierTrace.AssetContext
import com.wavesplatform.transaction.smart.script.trace.TracedResult.Attribute
import com.wavesplatform.transaction.smart.script.trace.{AssetVerifierTrace, TracedResult}
import com.wavesplatform.transaction.validation.impl.{DataTxValidator, LeaseCancelTxValidator, LeaseTxValidator, SponsorFeeTxValidator}
import com.wavesplatform.transaction.{Asset, AssetIdLength, ERC20Address, PBSince, TransactionType}
import com.wavesplatform.utils.*
import shapeless.Coproduct
import scala.collection.immutable.VectorMap
import scala.util.{Failure, Right, Success, Try}
object InvokeDiffsCommon {
val callExpressionError: Either[GenericError, Nothing] =
Left(GenericError("Trying to call dApp on the account with expression script"))
def txFeeDiff(blockchain: Blockchain, tx: InvokeScriptTransactionLike): Either[GenericError, (Long, Map[Address, Portfolio])] = {
val attachedFee = tx.fee
tx.feeAssetId match {
case Waves => Right((attachedFee, Map(tx.sender.toAddress -> Portfolio(-attachedFee))))
case asset @ IssuedAsset(_) =>
for {
assetInfo <- blockchain
.assetDescription(asset)
.toRight(GenericError(s"Asset $asset does not exist, cannot be used to pay fees"))
feeInWaves <- Either.cond(
assetInfo.sponsorship > 0,
Sponsorship.toWaves(attachedFee, assetInfo.sponsorship),
GenericError(s"Asset $asset is not sponsored, cannot be used to pay fees")
)
portfolioDiff <- Portfolio
.combine(
Map[Address, Portfolio](tx.sender.toAddress -> Portfolio.build(asset, -attachedFee)),
Map[Address, Portfolio](assetInfo.issuer.toAddress -> Portfolio.build(-feeInWaves, asset, attachedFee))
)
.leftMap(GenericError(_))
} yield (feeInWaves, portfolioDiff)
}
}
private def calculateMinFee(
tx: InvokeScriptTransactionLike,
blockchain: Blockchain,
issueList: List[Issue],
additionalScriptsInvoked: Int,
stepsNumber: Long
): Long = {
val dAppFee = FeeConstants(tx.tpe) * FeeUnit * stepsNumber
val issuesFee = issueList.count(!blockchain.isNFT(_)) * FeeConstants(TransactionType.Issue) * FeeUnit
val actionsFee = additionalScriptsInvoked * ScriptExtraFee
dAppFee + issuesFee + actionsFee
}
private[invoke] def calcAndCheckFee[E <: ValidationError](
makeError: (String, Long) => E,
tx: InvokeScriptTransactionLike,
blockchain: Blockchain,
stepLimit: Long,
invocationComplexity: Long,
issueList: List[Issue],
additionalScriptsInvoked: Int
): TracedResult[ValidationError, (Long, Map[Address, Portfolio])] = {
val stepsNumber =
if (invocationComplexity % stepLimit == 0)
invocationComplexity / stepLimit
else
invocationComplexity / stepLimit + 1
val minFee = calculateMinFee(tx, blockchain, issueList, additionalScriptsInvoked, stepsNumber)
val resultE = for {
(attachedFeeInWaves, portfolioDiff) <- txFeeDiff(blockchain, tx)
_ <- {
lazy val errorMessage = {
val stepsInfo =
if (stepsNumber > 1)
s" with $stepsNumber invocation steps"
else
""
val totalScriptsInvokedInfo =
if (additionalScriptsInvoked > 0)
s" with $additionalScriptsInvoked total scripts invoked"
else
""
val issuesInfo =
if (issueList.nonEmpty)
s" with ${issueList.length} assets issued"
else
""
val assetName = tx.assetFee._1.fold("WAVES")(_.id.toString)
val txName = tx.tpe.transactionName
s"Fee in $assetName for $txName (${tx.assetFee._2} in $assetName)" +
s"$stepsInfo$totalScriptsInvokedInfo$issuesInfo " +
s"does not exceed minimal value of $minFee WAVES."
}
Either.cond(
attachedFeeInWaves >= minFee,
(),
makeError(errorMessage, invocationComplexity)
)
}
} yield (attachedFeeInWaves, portfolioDiff)
TracedResult(resultE).withAttributes(Attribute.MinFee -> minFee)
}
def processActions(
actions: StructuredCallableActions,
version: StdLibVersion,
rootVersion: StdLibVersion,
dAppAddress: Address,
dAppPublicKey: PublicKey,
storingComplexity: Int,
tx: InvokeScriptLike,
blockchain: Blockchain,
blockTime: Long,
isSyncCall: Boolean,
limitedExecution: Boolean,
totalComplexityLimit: Int,
otherIssues: Seq[Issue],
enableExecutionLog: Boolean,
log: Log[Id]
): TracedResult[ValidationError, StateSnapshot] = {
val verifierCount = if (blockchain.hasPaidVerifier(tx.sender.toAddress)) 1 else 0
val additionalScriptsCount = actions.complexities.size + verifierCount + tx.paymentAssets.count(blockchain.hasAssetScript)
for {
_ <- checkActions(
actions,
version,
rootVersion,
dAppAddress,
storingComplexity,
tx,
limitedExecution,
totalComplexityLimit,
log,
blockchain.isFeatureActivated(BlockRewardDistribution)
)
feePortfolios <-
if (isSyncCall)
TracedResult.wrapValue(Map[Address, Portfolio]())
else {
val feeActionsCount = if (blockchain.isFeatureActivated(SynchronousCalls)) verifierCount else additionalScriptsCount
val stepLimit = ContractLimits.MaxComplexityByVersion(version)
calcAndCheckFee(
FailedTransactionError.feeForActions(_, _, log),
tx.root,
blockchain,
stepLimit,
storingComplexity.min(stepLimit), // complexity increased by sync calls should not require fee for additional steps
actions.issueList ++ otherIssues,
feeActionsCount
).map(_._2)
}
paymentsAndFeeSnapshot <-
if (isSyncCall)
TracedResult.wrapValue(StateSnapshot.empty)
else if (version < V5)
TracedResult(paymentsPart(blockchain, tx, dAppAddress, feePortfolios))
else
TracedResult(StateSnapshot.build(blockchain, txFeeDiff(blockchain, tx.root).explicitGet()._2))
complexityLimit =
if (limitedExecution) ContractLimits.FailFreeInvokeComplexity - storingComplexity
else Int.MaxValue
compositeSnapshot <- foldActions(blockchain, blockTime, tx, dAppAddress, dAppPublicKey, enableExecutionLog)(
actions.list,
paymentsAndFeeSnapshot,
complexityLimit
)
.leftMap {
case failed: FailedTransactionError => failed.addComplexity(storingComplexity).withLog(log)
case other => other
}
isr <- actionsToScriptResult(actions, storingComplexity, tx, log)
resultSnapshot = compositeSnapshot
.setScriptResults(Map(tx.txId -> isr))
.setScriptsComplexity(storingComplexity + compositeSnapshot.scriptsComplexity)
} yield resultSnapshot
}
private def checkActions(
actions: StructuredCallableActions,
version: StdLibVersion,
rootVersion: StdLibVersion,
dAppAddress: Address,
storingComplexity: Int,
tx: InvokeScriptLike,
limitedExecution: Boolean,
totalComplexityLimit: Int,
log: Log[Id],
limitsByRootVersion: Boolean
): TracedResult[ValidationError, Unit] = {
import actions.*
for {
_ <- TracedResult(checkDataEntries(blockchain, tx, dataEntries, version)).leftMap(
FailedTransactionError.dAppExecution(_, storingComplexity, log)
)
_ <- TracedResult(checkLeaseCancels(leaseCancelList)).leftMap(FailedTransactionError.dAppExecution(_, storingComplexity, log))
_ <- TracedResult(
checkScriptActionsAmount(version, rootVersion, actions.list, transferList, leaseList, leaseCancelList, dataEntries, limitsByRootVersion)
.leftMap(FailedTransactionError.dAppExecution(_, storingComplexity, log))
)
_ <- TracedResult(checkSelfPayments(dAppAddress, blockchain, tx, version, transferList))
.leftMap(FailedTransactionError.dAppExecution(_, storingComplexity, log))
_ <- TracedResult(
Either.cond(transferList.map(_.amount).forall(_ >= 0), (), FailedTransactionError.dAppExecution("Negative amount", storingComplexity, log))
)
_ <- TracedResult(checkOverflow(transferList.map(_.amount))).leftMap(FailedTransactionError.dAppExecution(_, storingComplexity, log))
_ <- TracedResult(
Either.cond(
actions.complexities.sum + storingComplexity <= totalComplexityLimit || limitedExecution, // limited execution has own restriction "complexityLimit"
(),
FailedTransactionError.feeForActions(s"Invoke complexity limit = $totalComplexityLimit is exceeded", storingComplexity, log)
)
)
} yield ()
}
private def actionsToScriptResult(
actions: StructuredCallableActions,
storingComplexity: Int,
tx: InvokeScriptLike,
log: Log[Id]
): TracedResult[ValidationError, InvokeScriptResult] = {
import actions.*
for {
resultTransfers <- transferList.traverse { transfer =>
resolveAddress(transfer.recipientAddressBytes, blockchain)
.map(InvokeScriptResult.Payment(_, Asset.fromCompatId(transfer.assetId), transfer.amount))
.leftMap {
case f: FailedTransactionError => f.addComplexity(storingComplexity).withLog(log)
case e => e
}
}
leaseListWithIds <- leaseList.traverse { case l @ Lease(recipient, amount, nonce) =>
val id = Lease.calculateId(l, tx.txId)
AddressOrAlias.fromRide(recipient).map(r => InvokeScriptResult.Lease(r, amount, nonce, id))
}
} yield InvokeScriptResult(
dataEntries,
resultTransfers,
issueList,
reissueList,
burnList,
sponsorFeeList,
leaseListWithIds,
leaseCancelList
)
}
def paymentsPart(
blockchain: Blockchain,
tx: InvokeScriptLike,
dAppAddress: Address,
feePart: Map[Address, Portfolio]
): Either[ValidationError, StateSnapshot] =
tx.payments
.traverse { case InvokeScriptTransaction.Payment(amt, assetId) =>
assetId match {
case asset @ IssuedAsset(_) =>
Portfolio.combine(
Map(tx.sender.toAddress -> Portfolio.build(asset, -amt)),
Map(dAppAddress -> Portfolio.build(asset, amt))
)
case Waves =>
Portfolio.combine(
Map(tx.sender.toAddress -> Portfolio(-amt)),
Map(dAppAddress -> Portfolio(amt))
)
}
}
.flatMap(_.foldM(Map[Address, Portfolio]())(Portfolio.combine))
.flatMap(Portfolio.combine(feePart, _))
.leftMap(GenericError(_))
.flatMap(StateSnapshot.build(blockchain, _))
def dataItemToEntry(item: DataOp): DataEntry[?] =
item match {
case DataItem.Bool(k, b) => BooleanDataEntry(k, b)
case DataItem.Str(k, b) => StringDataEntry(k, b)
case DataItem.Lng(k, b) => IntegerDataEntry(k, b)
case DataItem.Bin(k, b) => BinaryDataEntry(k, b)
case DataItem.Delete(k) => EmptyDataEntry(k)
}
private def checkSelfPayments(
dAppAddress: Address,
blockchain: Blockchain,
tx: InvokeScriptLike,
version: StdLibVersion,
transfers: List[AssetTransfer]
): Either[String, Unit] =
if (blockchain.disallowSelfPayment && version >= V4)
if (tx.payments.nonEmpty && tx.sender.toAddress == dAppAddress)
"DApp self-payment is forbidden since V4".asLeft[Unit]
else if (transfers.exists(_.recipientAddressBytes.bytes == ByteStr(dAppAddress.bytes)))
"DApp self-transfer is forbidden since V4".asLeft[Unit]
else
().asRight[String]
else
().asRight[String]
private def checkOverflow(dataList: Iterable[Long]): Either[String, Unit] = {
Try(dataList.foldLeft(0L)(Math.addExact))
.fold(
_ => "ScriptTransfer overflow".asLeft[Unit],
_ => ().asRight[String]
)
}
def checkPayments(blockchain: Blockchain, payments: Seq[Payment]): Either[GenericError, Unit] =
payments
.collectFirstSome {
case Payment(_, IssuedAsset(id)) => InvokeDiffsCommon.checkAsset(blockchain, id).swap.toOption
case Payment(_, Waves) => None
}
.map(GenericError(_))
.toLeft(())
def checkAsset(blockchain: Blockchain, assetId: ByteStr): Either[String, Unit] =
if (blockchain.isFeatureActivated(BlockchainFeatures.SynchronousCalls))
if (assetId.size != AssetIdLength)
Left(s"Transfer error: invalid asset ID '$assetId' length = ${assetId.size} bytes, must be $AssetIdLength")
else if (blockchain.assetDescription(IssuedAsset(assetId)).isEmpty)
Left(s"Transfer error: asset '$assetId' is not found on the blockchain")
else
Right(())
else
Right(())
private def checkDataEntries(blockchain: Blockchain, tx: InvokeScriptLike, dataEntries: Seq[DataEntry[?]], stdLibVersion: StdLibVersion) =
for {
_ <- Either.cond(
dataEntries.length <= ContractLimits.MaxWriteSetSize,
(),
s"WriteSet can't contain more than ${ContractLimits.MaxWriteSetSize} entries"
)
_ <- Either.cond(
tx.enableEmptyKeys || dataEntries.forall(_.key.nonEmpty),
(), {
val versionInfo = tx.root match {
case s: PBSince => s" in tx version >= ${PBSince.version(s)}"
case _ => ""
}
s"Empty keys aren't allowed$versionInfo"
}
)
maxKeySize = ContractLimits.MaxKeySizeInBytesByVersion(stdLibVersion)
_ <- dataEntries
.collectFirst {
Function.unlift { entry =>
val length = entry.key.utf8Bytes.length
if (length > maxKeySize)
Some(s"Data entry key size = $length bytes must be less than $maxKeySize")
else if (entry.key.isEmpty && stdLibVersion >= V4)
Some(s"Data entry key should not be empty")
else
None
}
}
.toLeft(())
_ <- DataTxValidator.verifyInvokeWriteSet(blockchain, dataEntries)
} yield ()
private def checkLeaseCancels(leaseCancels: Seq[LeaseCancel]): Either[String, Unit] = {
val duplicates = leaseCancels.diff(leaseCancels.distinct)
Either.cond(
duplicates.isEmpty,
(),
s"Duplicate LeaseCancel id(s): ${duplicates.distinct.map(_.id).mkString(", ")}"
)
}
private def checkScriptActionsAmount(
version: StdLibVersion,
rootVersion: StdLibVersion,
actions: List[CallableAction],
transferList: List[AssetTransfer],
leaseList: List[Lease],
leaseCancelList: List[LeaseCancel],
dataEntries: Seq[DataEntry[?]],
limitsByRootVersion: Boolean
): Either[String, Unit] = {
if (!limitsByRootVersion && version >= V6 || limitsByRootVersion && rootVersion >= V6) {
val balanceChangeActionsAmount = transferList.length + leaseList.length + leaseCancelList.length
val assetsActionsAmount = actions.length - dataEntries.length - balanceChangeActionsAmount
for {
_ <- Either.cond(
balanceChangeActionsAmount <= ContractLimits.MaxBalanceScriptActionsAmountV6,
(),
s"Too many ScriptTransfer, Lease, LeaseCancel actions: max: ${ContractLimits.MaxBalanceScriptActionsAmountV6}, actual: $balanceChangeActionsAmount"
)
_ <- Either.cond(
assetsActionsAmount <= ContractLimits.MaxAssetScriptActionsAmountV6,
(),
s"Too many Issue, Reissue, Burn, SponsorFee actions: max: ${ContractLimits.MaxAssetScriptActionsAmountV6}, actual: $assetsActionsAmount"
)
} yield ()
} else {
val actionsAmount = actions.length - dataEntries.length
Either.cond(
actionsAmount <= ContractLimits.MaxCallableActionsAmountBeforeV6(version),
(),
s"Too many script actions: max: ${ContractLimits.MaxCallableActionsAmountBeforeV6(version)}, actual: $actionsAmount"
)
}
}
private def resolveAddress(recipient: Recipient.Address, blockchain: Blockchain): TracedResult[ValidationError, Address] =
TracedResult {
val address = Address.fromBytes(recipient.bytes.arr)
if (blockchain.isFeatureActivated(BlockchainFeatures.RideV6))
address.leftMap(e => FailedTransactionError.dAppExecution(e.reason, 0))
else
address
}
private def foldActions(
sblockchain: Blockchain,
blockTime: Long,
tx: InvokeScriptLike,
dAppAddress: Address,
pk: PublicKey,
enableExecutionLog: Boolean
)(
actions: List[CallableAction],
initSnapshot: StateSnapshot,
remainingLimit: Int
): TracedResult[ValidationError, StateSnapshot] = {
actions.foldLeft(TracedResult(initSnapshot.asRight[ValidationError])) {
case (r@TracedResult(Left(_), _, _), _) => r
case (TracedResult(Right(currentSnapshot), prevTrace, prevAttrs), action) =>
val complexityLimit =
if (remainingLimit < Int.MaxValue) remainingLimit - currentSnapshot.scriptsComplexity.toInt
else remainingLimit
val blockchain = SnapshotBlockchain(sblockchain, currentSnapshot)
val actionSender = Recipient.Address(ByteStr(dAppAddress.bytes))
def applyTransfer(transfer: AssetTransfer, pk: PublicKey): TracedResult[ValidationError, StateSnapshot] = {
val AssetTransfer(addressRepr, recipient, amount, asset) = transfer
for {
address <- resolveAddress(addressRepr, blockchain)
diff <- Asset.fromCompatId(asset) match {
case Waves =>
val portfolio = Portfolio.combine(Map(address -> Portfolio(amount)), Map(dAppAddress -> Portfolio(-amount))).leftMap(GenericError(_))
TracedResult(
portfolio.flatMap(p =>
StateSnapshot
.build(blockchain, portfolios = p)
.leftMap(e =>
if (blockchain.isFeatureActivated(BlockchainFeatures.RideV6))
FailedTransactionError.asFailedScriptError(e)
else
e
)
)
)
case a @ IssuedAsset(id) =>
TracedResult(
Portfolio
.combine(
Map(address -> Portfolio(assets = VectorMap(a -> amount))),
Map(dAppAddress -> Portfolio(assets = VectorMap(a -> -amount)))
)
.leftMap(GenericError(_))
.flatMap(p =>
StateSnapshot
.build(blockchain, portfolios = p)
.leftMap(e =>
if (blockchain.isFeatureActivated(BlockchainFeatures.RideV6))
FailedTransactionError.asFailedScriptError(e)
else
e
)
)
).flatMap(portfolioSnapshot =>
blockchain
.assetScript(a)
.fold {
val r = checkAsset(blockchain, id)
.map(_ => portfolioSnapshot)
.leftMap(FailedTransactionError.dAppExecution(_, 0): ValidationError)
TracedResult(r)
} { case AssetScriptInfo(script, complexity) =>
val assetVerifierSnapshot =
if (blockchain.disallowSelfPayment) portfolioSnapshot
else
StateSnapshot
.build(
blockchain,
Map(
address -> Portfolio(assets = VectorMap(a -> amount)),
dAppAddress -> Portfolio(assets = VectorMap(a -> -amount))
)
)
.explicitGet()
val pseudoTxRecipient =
if (blockchain.isFeatureActivated(BlockchainFeatures.SynchronousCalls))
recipient
else
Recipient.Address(addressRepr.bytes)
val pseudoTx = ScriptTransfer(
asset,
actionSender,
pk,
pseudoTxRecipient,
amount,
tx.timestamp,
tx.txId
)
val assetValidationSnapshot = for {
_ <- BalanceDiffValidation
.cond(blockchain, _.isFeatureActivated(RideV6))(assetVerifierSnapshot)
.leftMap(e => if (blockchain.isFeatureActivated(LightNode)) FailedTransactionError.asFailedScriptError(e) else e)
snapshot <- validatePseudoTxWithSmartAssetScript(blockchain, tx)(
pseudoTx,
a.id,
assetVerifierSnapshot,
script,
complexity,
complexityLimit,
enableExecutionLog
)
} yield snapshot
val errorOpt = assetValidationSnapshot.fold(Some(_), _ => None)
TracedResult(
assetValidationSnapshot.map(d => portfolioSnapshot.setScriptsComplexity(d.scriptsComplexity)),
prevTrace :+ AssetVerifierTrace(id, errorOpt, AssetContext.Transfer)
)
}
)
}
} yield diff
}
def applyDataItem(item: DataOp): TracedResult[FailedTransactionError, StateSnapshot] =
TracedResult(StateSnapshot.build(blockchain, accountData = Map(dAppAddress -> Map(item.key -> dataItemToEntry(item)))))
.leftMap(FailedTransactionError.asFailedScriptError)
def applyIssue(itx: InvokeScriptLike, pk: PublicKey, issue: Issue): TracedResult[ValidationError, StateSnapshot] = {
val asset = IssuedAsset(issue.id)
if (
issue.name.getBytes("UTF-8").length < IssueTransaction.MinAssetNameLength ||
issue.name.getBytes("UTF-8").length > IssueTransaction.MaxAssetNameLength
) {
TracedResult(Left(FailedTransactionError.dAppExecution("Invalid asset name", 0L)), List())
} else if (issue.description.length > IssueTransaction.MaxAssetDescriptionLength) {
TracedResult(Left(FailedTransactionError.dAppExecution("Invalid asset description", 0L)), List())
} else if (blockchain.resolveERC20Address(ERC20Address(asset)).isDefined) {
val error = s"Asset ${issue.id} is already issued"
if (blockchain.isFeatureActivated(RideV6) || blockchain.height < blockchain.settings.functionalitySettings.enforceTransferValidationAfter) {
TracedResult(Left(FailedTransactionError.dAppExecution(error, 0L)), List())
} else {
TracedResult(Left(FailOrRejectError(error)))
}
} else {
val staticInfo = AssetStaticInfo(asset.id, TransactionId @@ itx.txId, pk, issue.decimals, blockchain.isNFT(issue))
val volumeInfo = AssetVolumeInfo(issue.isReissuable, BigInt(issue.quantity))
val info = AssetInfo(ByteString.copyFromUtf8(issue.name), ByteString.copyFromUtf8(issue.description), Height @@ blockchain.height)
StateSnapshot.build(
blockchain,
portfolios = Map(pk.toAddress -> Portfolio(assets = VectorMap(asset -> issue.quantity))),
issuedAssets = Seq(asset -> NewAssetInfo(staticInfo, info, volumeInfo))
)
}
}
def applyReissue(reissue: Reissue, pk: PublicKey): TracedResult[ValidationError, StateSnapshot] = {
val reissueDiff =
DiffsCommon.processReissue(blockchain, dAppAddress, blockTime, fee = 0, reissue).leftMap(FailedTransactionError.asFailedScriptError)
val pseudoTx = ReissuePseudoTx(reissue, actionSender, pk, tx.txId, tx.timestamp)
callAssetVerifierWithPseudoTx(reissueDiff, reissue.assetId, pseudoTx, AssetContext.Reissue)
}
def applyBurn(burn: Burn, pk: PublicKey): TracedResult[ValidationError, StateSnapshot] = {
val burnDiff = DiffsCommon.processBurn(blockchain, dAppAddress, fee = 0, burn).leftMap(FailedTransactionError.asFailedScriptError)
val pseudoTx = BurnPseudoTx(burn, actionSender, pk, tx.txId, tx.timestamp)
callAssetVerifierWithPseudoTx(burnDiff, burn.assetId, pseudoTx, AssetContext.Burn)
}
def applySponsorFee(sponsorFee: SponsorFee, pk: PublicKey): TracedResult[ValidationError, StateSnapshot] =
for {
_ <- TracedResult(
Either.cond(
blockchain.assetDescription(IssuedAsset(sponsorFee.assetId)).exists(_.issuer == pk),
(),
FailedTransactionError.dAppExecution(s"SponsorFee assetId=${sponsorFee.assetId} was not issued from address of current dApp", 0L)
)
)
_ <- TracedResult(
SponsorFeeTxValidator.checkMinSponsoredAssetFee(sponsorFee.minSponsoredAssetFee).leftMap(FailedTransactionError.asFailedScriptError)
)
sponsorDiff = DiffsCommon
.processSponsor(blockchain, dAppAddress, fee = 0, sponsorFee)
.leftMap(FailedTransactionError.asFailedScriptError)
pseudoTx = SponsorFeePseudoTx(sponsorFee, actionSender, pk, tx.txId, tx.timestamp)
r <- callAssetVerifierWithPseudoTx(sponsorDiff, sponsorFee.assetId, pseudoTx, AssetContext.Sponsor)
} yield r
def applyLease(l: Lease): TracedResult[ValidationError, StateSnapshot] =
for {
validAmount <- TracedResult(LeaseTxValidator.validateAmount(l.amount))
recipient <- TracedResult(AddressOrAlias.fromRide(l.recipient))
leaseId = Lease.calculateId(l, tx.txId)
diff <- DiffsCommon.processLease(blockchain, validAmount, pk, recipient, fee = 0, leaseId, tx.txId)
} yield diff
def applyLeaseCancel(l: LeaseCancel): TracedResult[ValidationError, StateSnapshot] =
for {
_ <- TracedResult(LeaseCancelTxValidator.checkLeaseId(l.id))
diff <- DiffsCommon.processLeaseCancel(blockchain, pk, fee = 0, blockTime, l.id, tx.txId)
} yield diff
def callAssetVerifierWithPseudoTx(
actionDiff: Either[FailedTransactionError, StateSnapshot],
assetId: ByteStr,
pseudoTx: PseudoTx,
assetType: AssetContext
): TracedResult[ValidationError, StateSnapshot] =
blockchain.assetScript(IssuedAsset(assetId)).fold(TracedResult(actionDiff)) { case AssetScriptInfo(script, complexity) =>
val assetValidationDiff =
for {
result <- actionDiff
validatedResult <- validatePseudoTxWithSmartAssetScript(blockchain, tx)(
pseudoTx,
assetId,
result,
script,
complexity,
complexityLimit,
enableExecutionLog
)
} yield validatedResult
val errorOpt = assetValidationDiff.fold(Some(_), _ => None)
TracedResult(
assetValidationDiff,
prevTrace :+ AssetVerifierTrace(assetId, errorOpt, assetType)
)
}
val nextDiff = action match {
case t: AssetTransfer =>
applyTransfer(
t,
if (blockchain.isFeatureActivated(BlockV5)) {
pk
} else {
PublicKey(new Array[Byte](32))
}
)
case d: DataOp => applyDataItem(d)
case i: Issue => applyIssue(tx, pk, i)
case r: Reissue => applyReissue(r, pk)
case b: Burn => applyBurn(b, pk)
case sf: SponsorFee => applySponsorFee(sf, pk)
case l: Lease => applyLease(l).leftMap(FailedTransactionError.asFailedScriptError)
case lc: LeaseCancel => applyLeaseCancel(lc).leftMap(FailedTransactionError.asFailedScriptError)
}
nextDiff
.flatMap(baseDiff =>
TracedResult(
BalanceDiffValidation
.cond(blockchain, _.isFeatureActivated(BlockchainFeatures.RideV6))(baseDiff)
.map(_ => baseDiff)
.leftMap(FailedTransactionError.asFailedScriptError(_).addComplexity(baseDiff.scriptsComplexity))
)
)
.leftMap {
case f: FailedTransactionError => f.addComplexity(currentSnapshot.scriptsComplexity)
case e => e
}
.map(d => currentSnapshot |+| d)
}
}
private def validatePseudoTxWithSmartAssetScript(blockchain: Blockchain, tx: InvokeScriptLike)(
pseudoTx: PseudoTx,
assetId: ByteStr,
nextSnapshot: StateSnapshot,
script: Script,
estimatedComplexity: Long,
complexityLimit: Int,
enableExecutionLog: Boolean
): Either[FailedTransactionError, StateSnapshot] =
Try {
val (log, evaluatedComplexity, result) = ScriptRunner(
Coproduct[TxOrd](pseudoTx),
blockchain,
script,
isAssetScript = true,
scriptContainerAddress =
if (blockchain.passCorrectAssetId) Coproduct[Environment.Tthis](Environment.AssetId(assetId.arr))
else Coproduct[Environment.Tthis](Environment.AssetId(tx.dApp.bytes)),
enableExecutionLog = enableExecutionLog,
complexityLimit
)
val complexity = if (blockchain.storeEvaluatedComplexity) evaluatedComplexity else estimatedComplexity
result match {
case Left(error) => Left(FailedTransactionError.assetExecutionInAction(error.message, complexity, log, assetId))
case Right(FALSE) => Left(FailedTransactionError.notAllowedByAssetInAction(complexity, log, assetId))
case Right(TRUE) => Right(nextSnapshot.setScriptsComplexity(nextSnapshot.scriptsComplexity + complexity))
case Right(x) =>
Left(FailedTransactionError.assetExecutionInAction(s"Script returned not a boolean result, but $x", complexity, log, assetId))
}
} match {
case Failure(e) =>
Left(
FailedTransactionError
.assetExecutionInAction(s"Uncaught execution error: ${Throwables.getStackTraceAsString(e)}", estimatedComplexity, List.empty, assetId)
)
case Success(s) => s
}
def checkCallResultLimits(
currentVersion: StdLibVersion,
rootVersion: StdLibVersion,
blockchain: Blockchain,
usedComplexity: Long,
log: Log[Id],
actionsCount: Int,
balanceActionsCount: Int,
assetActionsCount: Int,
dataCount: Int,
dataSize: Int,
availableActions: ActionLimits
): TracedResult[ValidationError, Unit] = {
def error(message: String) = TracedResult(Left(FailedTransactionError.dAppExecution(message, usedComplexity, log)))
def checkLimitsByVersion(version: StdLibVersion) = {
if (version >= V6 && balanceActionsCount > availableActions.balanceActions) {
error("ScriptTransfer, Lease, LeaseCancel actions count limit is exceeded")
} else if (version >= V6 && assetActionsCount > availableActions.assetActions) {
error("Issue, Reissue, Burn, SponsorFee actions count limit is exceeded")
} else if (version < V6 && actionsCount > availableActions.nonDataActions)
error("Actions count limit is exceeded")
else TracedResult(Right(()))
}
if (dataCount > availableActions.data)
error("Stored data count limit is exceeded")
else if (dataSize > availableActions.dataSize) {
val limit = ContractLimits.MaxTotalWriteSetSizeInBytes
val actual = limit + dataSize - availableActions.dataSize
val message = s"Storing data size should not exceed $limit, actual: $actual bytes"
if (blockchain.isFeatureActivated(RideV6)) {
error(message)
} else if (
blockchain.isFeatureActivated(
SynchronousCalls
) && blockchain.height >= blockchain.settings.functionalitySettings.enforceTransferValidationAfter
) {
TracedResult(Left(FailOrRejectError(message)))
} else
TracedResult(Right(()))
} else if (blockchain.isFeatureActivated(BlockchainFeatures.BlockRewardDistribution)) {
checkLimitsByVersion(rootVersion)
} else checkLimitsByVersion(currentVersion)
}
def checkScriptResultFields(blockchain: Blockchain, r: ScriptResult): Either[ValidationError, ScriptResult] =
r match {
case rv4: ScriptResultV4 =>
rv4.actions
.collectFirstSome {
case Reissue(_, _, quantity) if quantity < 0 => Some(s"Negative reissue quantity = $quantity")
case Burn(_, quantity) if quantity < 0 => Some(s"Negative burn quantity = $quantity")
case t: AssetTransfer if t.amount < 0 => Some(s"Negative transfer amount = ${t.amount}")
case l: Lease if l.amount < 0 => Some(s"Negative lease amount = ${l.amount}")
case SponsorFee(_, Some(amount)) if amount < 0 => Some(s"Negative sponsor amount = $amount")
case i: Issue =>
val length = i.name.getBytes("UTF-8").length
if (length < IssueTransaction.MinAssetNameLength || length > IssueTransaction.MaxAssetNameLength)
Some("Invalid asset name")
else if (i.description.length > IssueTransaction.MaxAssetDescriptionLength)
Some("Invalid asset description")
else
None
case _ =>
None
}
.collect {
case e if blockchain.isFeatureActivated(BlockchainFeatures.RideV6) => GenericError(e)
case e if blockchain.height >= blockchain.settings.functionalitySettings.enforceTransferValidationAfter => FailOrRejectError(e)
}
.toLeft(r)
case _ => Right(r)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy