From bb322af6a79a91b73e4c84963e4cdfabe5ab069c Mon Sep 17 00:00:00 2001 From: Artyom Sayadyan Date: Tue, 28 Nov 2023 11:30:07 +0300 Subject: [PATCH] NODE-2528 Typed errors replacing exceptions in RIDE sources (#3896) --- .../lang/v1/EvaluatorV2Benchmark.scala | 11 +- .../lang/v1/FractionIntBenchmark.scala | 12 +- .../com/wavesplatform/lang/v1/package.scala | 11 +- .../common/utils/FastBase58.scala | 1 + .../wavesplatform/lang/ExecutionError.scala | 1 + .../wavesplatform/lang/v1/BaseGlobal.scala | 1 + .../lang/v1/evaluator/ContractEvaluator.scala | 21 +- .../lang/v1/evaluator/EvaluatorV2.scala | 55 ++-- .../v1/evaluator/ctx/impl/CryptoContext.scala | 23 +- .../v1/evaluator/ctx/impl/PureContext.scala | 2 +- .../evaluator/ctx/impl/waves/Functions.scala | 244 +++++++++++------- .../lang/ContractIntegrationTest.scala | 14 +- .../wavesplatform/lang/IntegrationTest.scala | 11 +- .../lang/estimator/package.scala | 3 +- .../lang/evaluator/EvaluatorSpec.scala | 11 +- .../lang/evaluator/EvaluatorV1V2Test.scala | 6 +- .../lang/evaluator/EvaluatorV2Test.scala | 3 +- .../com/wavesplatform/utils/MerkleTest.scala | 2 +- .../InvokeMultiplePaymentsSuite.scala | 2 +- .../api/http/utils/UtilsEvaluator.scala | 3 +- .../state/diffs/invoke/InvokeScriptDiff.scala | 4 +- .../invoke/InvokeScriptTransactionDiff.scala | 5 +- .../transaction/TxValidationError.scala | 4 +- .../smart/script/ScriptRunner.scala | 9 +- .../state/diffs/ci/LeaseActionDiffTest.scala | 2 +- .../diffs/ci/ScriptTransferByAliasTest.scala | 2 +- .../diffs/smart/RideExceptionsTest.scala | 172 ++++++++++++ .../smart/predef/MatcherBlockchainTest.scala | 3 +- .../AddressFromRecipientScenarioTest.scala | 2 +- 29 files changed, 475 insertions(+), 165 deletions(-) create mode 100644 node/src/test/scala/com/wavesplatform/state/diffs/smart/RideExceptionsTest.scala diff --git a/benchmark/src/test/scala/com/wavesplatform/lang/v1/EvaluatorV2Benchmark.scala b/benchmark/src/test/scala/com/wavesplatform/lang/v1/EvaluatorV2Benchmark.scala index 85b60437dc2..f9770b1630c 100644 --- a/benchmark/src/test/scala/com/wavesplatform/lang/v1/EvaluatorV2Benchmark.scala +++ b/benchmark/src/test/scala/com/wavesplatform/lang/v1/EvaluatorV2Benchmark.scala @@ -1,25 +1,24 @@ package com.wavesplatform.lang.v1 -import java.util.concurrent.TimeUnit -import cats.Id import com.wavesplatform.lang.Common import com.wavesplatform.lang.directives.values.{V1, V3} import com.wavesplatform.lang.v1.EvaluatorV2Benchmark.* import com.wavesplatform.lang.v1.compiler.Terms.{EXPR, IF, TRUE} import com.wavesplatform.lang.v1.compiler.TestCompiler import com.wavesplatform.lang.v1.evaluator.EvaluatorV2 -import com.wavesplatform.lang.v1.evaluator.ctx.{DisabledLogEvaluationContext, EvaluationContext} +import com.wavesplatform.lang.v1.evaluator.ctx.DisabledLogEvaluationContext import com.wavesplatform.lang.v1.evaluator.ctx.impl.PureContext import com.wavesplatform.lang.v1.traits.Environment import org.openjdk.jmh.annotations.* import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit import scala.annotation.tailrec object EvaluatorV2Benchmark { - val pureContext: CTX[Environment] = PureContext.build(V1, useNewPowPrecision = true).withEnvironment[Environment] - val pureEvalContext: EvaluationContext[Environment, Id] = pureContext.evaluationContext(Common.emptyBlockchainEnvironment()) - val evaluatorV2: EvaluatorV2 = new EvaluatorV2(DisabledLogEvaluationContext(pureEvalContext), V1, true, true, false) + val pureContext = PureContext.build(V1, useNewPowPrecision = true).withEnvironment[Environment] + val pureEvalContext = pureContext.evaluationContext(Common.emptyBlockchainEnvironment()) + val evaluatorV2 = new EvaluatorV2(DisabledLogEvaluationContext(pureEvalContext), V1, Int.MaxValue, true, false, true, true, true) } @OutputTimeUnit(TimeUnit.MILLISECONDS) diff --git a/benchmark/src/test/scala/com/wavesplatform/lang/v1/FractionIntBenchmark.scala b/benchmark/src/test/scala/com/wavesplatform/lang/v1/FractionIntBenchmark.scala index 05457b8df30..8c23d783ab1 100644 --- a/benchmark/src/test/scala/com/wavesplatform/lang/v1/FractionIntBenchmark.scala +++ b/benchmark/src/test/scala/com/wavesplatform/lang/v1/FractionIntBenchmark.scala @@ -20,22 +20,22 @@ import org.openjdk.jmh.infra.Blackhole @Measurement(iterations = 10, time = 1) class FractionIntBenchmark { @Benchmark - def fraction1(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr1, LogExtraInfo(), V5, true, true, false)) + def fraction1(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr1, LogExtraInfo(), V5, true, true, false, true)) @Benchmark - def fraction2(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr2, LogExtraInfo(), V5, true, true, false)) + def fraction2(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr2, LogExtraInfo(), V5, true, true, false, true)) @Benchmark - def fraction3(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr3, LogExtraInfo(), V5, true, true, false)) + def fraction3(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr3, LogExtraInfo(), V5, true, true, false, true)) @Benchmark - def fraction1Round(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr1Round, LogExtraInfo(), V5, true, true, false)) + def fraction1Round(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr1Round, LogExtraInfo(), V5, true, true, false, true)) @Benchmark - def fraction2Round(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr2Round, LogExtraInfo(), V5, true, true, false)) + def fraction2Round(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr2Round, LogExtraInfo(), V5, true, true, false, true)) @Benchmark - def fraction3Round(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr3Round, LogExtraInfo(), V5, true, true, false)) + def fraction3Round(bh: Blackhole, s: St): Unit = bh.consume(EvaluatorV2.applyCompleted(s.ctx, s.expr3Round, LogExtraInfo(), V5, true, true, false, true)) } @State(Scope.Benchmark) diff --git a/benchmark/src/test/scala/com/wavesplatform/lang/v1/package.scala b/benchmark/src/test/scala/com/wavesplatform/lang/v1/package.scala index 95367b95d8d..eedd9024c08 100644 --- a/benchmark/src/test/scala/com/wavesplatform/lang/v1/package.scala +++ b/benchmark/src/test/scala/com/wavesplatform/lang/v1/package.scala @@ -31,5 +31,14 @@ package object v1 { expr: EXPR, stdLibVersion: StdLibVersion = StdLibVersion.VersionDic.all.max ): (Log[Id], Int, Either[ExecutionError, Terms.EVALUATED]) = - EvaluatorV2.applyCompleted(ctx, expr, LogExtraInfo(), stdLibVersion, newMode = true, correctFunctionCallScope = true, enableExecutionLog = false) + EvaluatorV2.applyCompleted( + ctx, + expr, + LogExtraInfo(), + stdLibVersion, + newMode = true, + correctFunctionCallScope = true, + enableExecutionLog = false, + fixedThrownError = true + ) } diff --git a/lang/shared/src/main/scala/com/wavesplatform/common/utils/FastBase58.scala b/lang/shared/src/main/scala/com/wavesplatform/common/utils/FastBase58.scala index 05024ebd276..1590f02191f 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/common/utils/FastBase58.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/common/utils/FastBase58.scala @@ -73,6 +73,7 @@ object FastBase58 extends BaseXXEncDec { outArray(outIndex) = longValue & 0xffffffffL } + // called only from Try structure, see BaseXXEncDec.tryDecode(str: String) if (base58EncMask > 0) throw new IllegalArgumentException("Output number too big (carry to the next int32)") if ((outArray(0) & zeroMask) != 0) throw new IllegalArgumentException("Output number too big (last int32 filled too far)") } diff --git a/lang/shared/src/main/scala/com/wavesplatform/lang/ExecutionError.scala b/lang/shared/src/main/scala/com/wavesplatform/lang/ExecutionError.scala index d02bdd7a911..63dfdfea2ec 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/lang/ExecutionError.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/lang/ExecutionError.scala @@ -7,4 +7,5 @@ case class CommonError(details: String, cause: Option[ValidationError] = None) e override def toString: String = s"CommonError($message)" override def message: String = cause.map(_.toString).getOrElse(details) } +case class ThrownError(message: String) extends ExecutionError case class FailOrRejectError(message: String, skipInvokeComplexity: Boolean = true) extends ExecutionError with ValidationError diff --git a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/BaseGlobal.scala b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/BaseGlobal.scala index 08b562a68f0..9e5002fdba5 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/BaseGlobal.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/BaseGlobal.scala @@ -393,6 +393,7 @@ object BaseGlobal { def apply(n: Int): T = if (from + n < until) arr(from + n) else throw new ArrayIndexOutOfBoundsException(n) + // should be never thrown due to passing Random.nextInt(arr.size) at the single point of call def partitionInPlace(p: T => Boolean): (ArrayView[T], ArrayView[T]) = { var upper = until - 1 diff --git a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ContractEvaluator.scala b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ContractEvaluator.scala index 531b5e6576c..f66b0202f1f 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ContractEvaluator.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ContractEvaluator.scala @@ -119,7 +119,8 @@ object ContractEvaluator { limit: Int, correctFunctionCallScope: Boolean, newMode: Boolean, - enableExecutionLog: Boolean + enableExecutionLog: Boolean, + fixedThrownError: Boolean ): Coeval[Either[(ExecutionError, Int, Log[Id]), (ScriptResult, Log[Id])]] = Coeval .now(buildExprFromInvocation(dApp, i, version).leftMap((_, limit, Nil))) @@ -134,7 +135,8 @@ object ContractEvaluator { limit, correctFunctionCallScope, newMode, - enableExecutionLog + enableExecutionLog, + fixedThrownError ) case Left(error) => Coeval.now(Left(error)) } @@ -148,10 +150,21 @@ object ContractEvaluator { limit: Int, correctFunctionCallScope: Boolean, newMode: Boolean, - enableExecutionLog: Boolean + enableExecutionLog: Boolean, + fixedThrownError: Boolean ): Coeval[Either[(ExecutionError, Int, Log[Id]), (ScriptResult, Log[Id])]] = EvaluatorV2 - .applyLimitedCoeval(expr, logExtraInfo, limit, ctx, version, correctFunctionCallScope, newMode, enableExecutionLog = enableExecutionLog) + .applyLimitedCoeval( + expr, + logExtraInfo, + limit, + ctx, + version, + correctFunctionCallScope, + newMode, + enableExecutionLog = enableExecutionLog, + fixedThrownError = fixedThrownError + ) .map(_.flatMap { case (expr, unusedComplexity, log) => val result = expr match { diff --git a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/EvaluatorV2.scala b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/EvaluatorV2.scala index aed5720e7c4..77c16ee5f5b 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/EvaluatorV2.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/EvaluatorV2.scala @@ -9,20 +9,13 @@ import com.wavesplatform.lang.v1.FunctionHeader import com.wavesplatform.lang.v1.compiler.Terms.* import com.wavesplatform.lang.v1.compiler.Types.CASETYPEREF import com.wavesplatform.lang.v1.evaluator.ContextfulNativeFunction.{Extended, Simple} -import com.wavesplatform.lang.v1.evaluator.ctx.{ - DisabledLogEvaluationContext, - EnabledLogEvaluationContext, - EvaluationContext, - LoggedEvaluationContext, - NativeFunction, - UserFunction -} import com.wavesplatform.lang.v1.evaluator.ContractEvaluator.LogExtraInfo -import com.wavesplatform.lang.v1.evaluator.EvaluatorV2.logFunc import com.wavesplatform.lang.v1.evaluator.EvaluatorV2.LogKeys.* +import com.wavesplatform.lang.v1.evaluator.EvaluatorV2.logFunc import com.wavesplatform.lang.v1.evaluator.ctx.impl.waves.Bindings +import com.wavesplatform.lang.v1.evaluator.ctx.* import com.wavesplatform.lang.v1.traits.Environment -import com.wavesplatform.lang.{CommonError, ExecutionError} +import com.wavesplatform.lang.{CommonError, ExecutionError, ThrownError} import monix.eval.Coeval import shapeless.syntax.std.tuple.* @@ -32,10 +25,12 @@ import scala.collection.mutable.ListBuffer class EvaluatorV2( val ctx: LoggedEvaluationContext[Environment, Id], val stdLibVersion: StdLibVersion, + val limit: Int, val correctFunctionCallScope: Boolean, val newMode: Boolean, val enableExecutionLog: Boolean, - val checkConstructorArgsTypes: Boolean = false + val checkConstructorArgsTypes: Boolean, + val fixedThrownError: Boolean ) { private val overheadCost: Int = if (newMode) 0 else 1 @@ -110,13 +105,21 @@ class EvaluatorV2( evaluated } -> unusedComplexity } - case f: Simple[Environment] => Coeval((f.evaluate(ctx.ec.environment, args), limit - cost)) + case f: Simple[Environment] => + Coeval((f.evaluate(ctx.ec.environment, args), limit - cost)) } for { (result, unusedComplexity) <- EvaluationResult( evaluation .map { case (result, evaluatedComplexity) => - result.bimap((_, evaluatedComplexity), (_, evaluatedComplexity)) + result.bimap( + { + case e: ThrownError if !fixedThrownError && function.ev.isInstanceOf[Extended[Environment]] => (e, this.limit) + case e: ThrownError if !fixedThrownError => (e, 0) + case e => (e, evaluatedComplexity) + }, + (_, evaluatedComplexity) + ) } .onErrorHandleWith { case _: SecurityException => @@ -359,7 +362,8 @@ object EvaluatorV2 { correctFunctionCallScope: Boolean, newMode: Boolean, checkConstructorArgsTypes: Boolean = false, - enableExecutionLog: Boolean = false + enableExecutionLog: Boolean = false, + fixedThrownError: Boolean ): Coeval[Either[(ExecutionError, Int, Log[Id]), (EXPR, Int, Log[Id])]] = { val log = ListBuffer[LogItem[Id]]() @@ -370,7 +374,16 @@ object EvaluatorV2 { } var ref = expr.deepCopy.value logCall(loggedCtx, logExtraInfo, ref, enableExecutionLog) - new EvaluatorV2(loggedCtx, stdLibVersion, correctFunctionCallScope, newMode, enableExecutionLog, checkConstructorArgsTypes) + new EvaluatorV2( + loggedCtx, + stdLibVersion, + limit, + correctFunctionCallScope, + newMode, + enableExecutionLog, + checkConstructorArgsTypes, + fixedThrownError + ) .root(ref, v => EvaluationResult { ref = v }, limit, Nil) .map((ref, _)) .value @@ -389,7 +402,8 @@ object EvaluatorV2 { correctFunctionCallScope: Boolean, newMode: Boolean, handleExpr: EXPR => Either[ExecutionError, EVALUATED], - enableExecutionLog: Boolean + enableExecutionLog: Boolean, + fixedThrownError: Boolean ): (Log[Id], Int, Either[ExecutionError, EVALUATED]) = EvaluatorV2 .applyLimitedCoeval( @@ -400,7 +414,8 @@ object EvaluatorV2 { stdLibVersion, correctFunctionCallScope, newMode, - enableExecutionLog = enableExecutionLog + enableExecutionLog = enableExecutionLog, + fixedThrownError = fixedThrownError ) .value() .fold( @@ -420,7 +435,8 @@ object EvaluatorV2 { stdLibVersion: StdLibVersion, correctFunctionCallScope: Boolean, newMode: Boolean, - enableExecutionLog: Boolean + enableExecutionLog: Boolean, + fixedThrownError: Boolean ): (Log[Id], Int, Either[ExecutionError, EVALUATED]) = applyOrDefault( ctx, @@ -431,7 +447,8 @@ object EvaluatorV2 { correctFunctionCallScope, newMode, expr => Left(s"Unexpected incomplete evaluation result $expr"), - enableExecutionLog + enableExecutionLog, + fixedThrownError ) private def logCall( diff --git a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/CryptoContext.scala b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/CryptoContext.scala index 4c5968fca64..52b0a4a6a9f 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/CryptoContext.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/CryptoContext.scala @@ -4,8 +4,7 @@ import cats.implicits.* import cats.{Id, Monad} import com.wavesplatform.common.merkle.Merkle.createRoot import com.wavesplatform.common.state.ByteStr -import com.wavesplatform.lang.{ExecutionError, CommonError} -import com.wavesplatform.lang.directives.values.{StdLibVersion, V3, *} +import com.wavesplatform.lang.directives.values.* import com.wavesplatform.lang.v1.compiler.Terms.* import com.wavesplatform.lang.v1.compiler.Types.* import com.wavesplatform.lang.v1.compiler.{CompilerContext, Terms} @@ -15,8 +14,10 @@ import com.wavesplatform.lang.v1.evaluator.FunctionIds.* import com.wavesplatform.lang.v1.evaluator.ctx.impl.crypto.RSA.DigestAlgorithm import com.wavesplatform.lang.v1.evaluator.ctx.{BaseFunction, EvaluationContext, NativeFunction} import com.wavesplatform.lang.v1.{BaseGlobal, CTX} +import com.wavesplatform.lang.{CommonError, ExecutionError, ThrownError} import scala.collection.mutable +import scala.util.Try object CryptoContext { @@ -30,7 +31,7 @@ object CryptoContext { UNION.create(rsaHashAlgs(v), if (v > V3 && v < V6) Some("RsaDigestAlgs") else None) private val rsaHashLib = { - import com.wavesplatform.lang.v1.evaluator.ctx.impl.crypto.RSA._ + import com.wavesplatform.lang.v1.evaluator.ctx.impl.crypto.RSA.* rsaTypeNames.zip(List(NONE, MD5, SHA1, SHA224, SHA256, SHA384, SHA512, SHA3224, SHA3256, SHA3384, SHA3512)).toMap } @@ -370,16 +371,16 @@ object CryptoContext { ("index", LONG) ) { case xs @ ARR(proof) :: CONST_BYTESTR(value) :: CONST_LONG(index) :: Nil => - val filteredProofs = proof.collect { - case bs @ CONST_BYTESTR(v) if v.size == 32 => bs - } - - if (value.size == 32 && proof.length <= 16 && filteredProofs.size == proof.size) { - CONST_BYTESTR(ByteStr(createRoot(value.arr, Math.toIntExact(index), filteredProofs.reverse.map(_.bs.arr)))) + val sizeCheckedProofs = proof.collect { case bs @ CONST_BYTESTR(v) if v.size == 32 => bs } + if (value.size == 32 && proof.length <= 16 && sizeCheckedProofs.size == proof.size) { + Try(createRoot(value.arr, Math.toIntExact(index), sizeCheckedProofs.reverse.map(_.bs.arr))) + .toEither + .leftMap(e => ThrownError(if (e.getMessage != null) e.getMessage else "error")) + .flatMap(r => CONST_BYTESTR(ByteStr(r))) } else { - notImplemented[Id, EVALUATED](s"createMerkleRoot(merkleProof: ByteVector, valueBytes: ByteVector)", xs) + notImplemented[Id, EVALUATED](s"createMerkleRoot(merkleProof: ByteVector, valueBytes: ByteVector, index: Int)", xs) } - case xs => notImplemented[Id, EVALUATED](s"createMerkleRoot(merkleProof: ByteVector, valueBytes: ByteVector)", xs) + case xs => notImplemented[Id, EVALUATED](s"createMerkleRoot(merkleProof: ByteVector, valueBytes: ByteVector, index: Int)", xs) } def toBase16StringF(checkLength: Boolean): BaseFunction[NoContext] = NativeFunction("toBase16String", 10, TOBASE16, STRING, ("bytes", BYTESTR)) { diff --git a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/PureContext.scala b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/PureContext.scala index 0098d6b7413..2fbb5dc0d91 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/PureContext.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/PureContext.scala @@ -42,7 +42,7 @@ object PureContext { val MaxListLengthV4 = 1000 // As an optimization, JVM might throw an ArithmeticException with empty stack trace and null message. - // The workaround below retrows an exception with the message explicitly set. + // The workaround below rethrows an exception with the message explicitly set. lazy val divLong: BaseFunction[NoContext] = createTryOp(DIV_OP, LONG, LONG, DIV_LONG) { (a, b) => try Math.floorDiv(a, b) diff --git a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/waves/Functions.scala b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/waves/Functions.scala index 0e955ab4980..c12a726ef33 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/waves/Functions.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/waves/Functions.scala @@ -1,5 +1,7 @@ package com.wavesplatform.lang.v1.evaluator.ctx.impl.waves +import cats.data.EitherT +import cats.implicits.toTraverseOps import cats.syntax.applicative.* import cats.syntax.either.* import cats.syntax.functor.* @@ -11,17 +13,17 @@ import com.wavesplatform.lang.v1.FunctionHeader.{Native, User} import com.wavesplatform.lang.v1.compiler.Terms.* import com.wavesplatform.lang.v1.compiler.Types.* import com.wavesplatform.lang.v1.evaluator.FunctionIds.* +import com.wavesplatform.lang.v1.evaluator.ctx.impl.* import com.wavesplatform.lang.v1.evaluator.ctx.impl.EnvironmentFunctions.AddressLength import com.wavesplatform.lang.v1.evaluator.ctx.impl.converters.* import com.wavesplatform.lang.v1.evaluator.ctx.impl.waves.Bindings.{scriptTransfer as _, *} import com.wavesplatform.lang.v1.evaluator.ctx.impl.waves.Types.* -import com.wavesplatform.lang.v1.evaluator.ctx.impl.* import com.wavesplatform.lang.v1.evaluator.ctx.{BaseFunction, NativeFunction, UserFunction} import com.wavesplatform.lang.v1.evaluator.{ContextfulNativeFunction, ContextfulUserFunction, FunctionIds, Log} import com.wavesplatform.lang.v1.traits.domain.{Issue, Lease, Recipient} import com.wavesplatform.lang.v1.traits.{DataType, Environment} import com.wavesplatform.lang.v1.{BaseGlobal, FunctionHeader} -import com.wavesplatform.lang.{CoevalF, CommonError, ExecutionError, FailOrRejectError} +import com.wavesplatform.lang.{CoevalF, CommonError, ExecutionError, FailOrRejectError, ThrownError} import monix.eval.Coeval import shapeless.Coproduct.unsafeGet @@ -411,11 +413,12 @@ object Functions { case xs => notImplemented[Id, EVALUATED](s"toString(a: Address)", xs) } - private def caseObjToRecipient(c: CaseObj): Recipient = c.caseType.name match { - case addressType.name => Recipient.Address(c.fields("bytes").asInstanceOf[CONST_BYTESTR].bs) - case aliasType.name => Recipient.Alias(c.fields("alias").asInstanceOf[CONST_STRING].s) - case t => throw new IllegalArgumentException(s"Unexpected recipient type $t") - } + private def caseObjToRecipient(c: CaseObj): Either[ExecutionError, Recipient] = + c.caseType.name match { + case addressType.name => Right(Recipient.Address(c.fields("bytes").asInstanceOf[CONST_BYTESTR].bs)) + case aliasType.name => Right(Recipient.Alias(c.fields("alias").asInstanceOf[CONST_STRING].s)) + case t => Left(ThrownError(s"Unexpected recipient type $t")) + } val assetBalanceF: BaseFunction[Environment] = NativeFunction.withEnvironment[Environment]( @@ -434,9 +437,17 @@ object Functions { override def evaluate[F[_]: Monad](env: Environment[F], args: List[EVALUATED]): F[Either[ExecutionError, EVALUATED]] = args match { case (c: CaseObj) :: u :: Nil if u == unit => - env.accountBalanceOf(caseObjToRecipient(c), None).map(_.map(CONST_LONG).leftMap(CommonError(_))) + caseObjToRecipient(c) + .fold( + _.asLeft[EVALUATED].pure[F], + r => env.accountBalanceOf(r, None).map(_.map(CONST_LONG).leftMap(CommonError(_))) + ) case (c: CaseObj) :: CONST_BYTESTR(assetId: ByteStr) :: Nil => - env.accountBalanceOf(caseObjToRecipient(c), Some(assetId.arr)).map(_.map(CONST_LONG).leftMap(CommonError(_))) + caseObjToRecipient(c) + .fold( + _.asLeft[EVALUATED].pure[F], + r => env.accountBalanceOf(r, Some(assetId.arr)).map(_.map(CONST_LONG).leftMap(CommonError(_))) + ) case xs => notImplemented[F, EVALUATED](s"assetBalance(a: Address|Alias, u: ByteVector|Unit)", xs) } @@ -456,7 +467,11 @@ object Functions { override def evaluate[F[_]: Monad](env: Environment[F], args: List[EVALUATED]): F[Either[ExecutionError, EVALUATED]] = args match { case (c: CaseObj) :: CONST_BYTESTR(assetId: ByteStr) :: Nil => - env.accountBalanceOf(caseObjToRecipient(c), Some(assetId.arr)).map(_.map(CONST_LONG).leftMap(CommonError(_))) + caseObjToRecipient(c) + .fold( + _.asLeft[EVALUATED].pure[F], + r => env.accountBalanceOf(r, Some(assetId.arr)).map(_.map(CONST_LONG).leftMap(CommonError(_))) + ) case xs => notImplemented[F, EVALUATED](s"assetBalance(a: Address|Alias, u: ByteVector)", xs) } @@ -475,20 +490,25 @@ object Functions { override def evaluate[F[_]: Monad](env: Environment[F], args: List[EVALUATED]): F[Either[ExecutionError, EVALUATED]] = args match { case (c: CaseObj) :: Nil => - env - .accountWavesBalanceOf(caseObjToRecipient(c)) - .map( - _.map(b => - CaseObj( - balanceDetailsType, - Map( - "available" -> CONST_LONG(b.available), - "regular" -> CONST_LONG(b.regular), - "generating" -> CONST_LONG(b.generating), - "effective" -> CONST_LONG(b.effective) + caseObjToRecipient(c) + .fold( + _.asLeft[EVALUATED].pure[F], + r => + env + .accountWavesBalanceOf(r) + .map( + _.map(b => + CaseObj( + balanceDetailsType, + Map( + "available" -> CONST_LONG(b.available), + "regular" -> CONST_LONG(b.regular), + "generating" -> CONST_LONG(b.generating), + "effective" -> CONST_LONG(b.effective) + ) + ) + ).leftMap(CommonError(_)) ) - ) - ).leftMap(CommonError(_)) ) case xs => notImplemented[F, EVALUATED](s"wavesBalance(a: Address|Alias)", xs) @@ -580,7 +600,7 @@ object Functions { Map[StdLibVersion, Long](V5 -> 75L), id, ANY, - ("dapp", addressOrAliasType), + ("dApp", addressOrAliasType), ("name", optionString), ("args", LIST(ANY)), ("payments", listPayment) @@ -588,59 +608,90 @@ object Functions { new ContextfulNativeFunction.Extended[Environment]( name, ANY, - Seq(("dapp", BYTESTR), ("name", STRING), ("args", LIST(ANY)), ("payments", listPayment)) + Seq(("dApp", BYTESTR), ("name", STRING), ("args", LIST(ANY)), ("payments", listPayment)) ) { override def evaluate[F[_]: Monad]( env: Environment[F], args: List[EVALUATED], availableComplexity: Int - )(implicit m: Monad[CoevalF[F, *]]): Coeval[F[(Either[ExecutionError, (EVALUATED, Log[F])], Int)]] = { - val dAppBytes = args match { - case (dApp: CaseObj) :: _ if dApp.caseType == addressType => - dApp.fields("bytes") match { - case CONST_BYTESTR(d) => d.pure[F] - case a => throw new IllegalArgumentException(s"Unexpected address bytes $a") + )(implicit monad: Monad[CoevalF[F, *]]): Coeval[F[(Either[ExecutionError, (EVALUATED, Log[F])], Int)]] = { + def thrown[R](message: String): F[Either[ExecutionError, R]] = + (ThrownError(message): ExecutionError).asLeft[R].pure[F] + + val processedArgs = for { + dAppBytes <- EitherT[F, ExecutionError, ByteStr]( + args match { + case (dApp: CaseObj) :: _ if dApp.caseType == addressType => + dApp.fields.get("bytes") match { + case Some(CONST_BYTESTR(d)) => d.asRight[ExecutionError].pure[F] + case a => thrown(s"Unexpected address bytes $a") + } + case (dApp: CaseObj) :: _ if dApp.caseType == aliasType => + dApp.fields.get("alias") match { + case Some(CONST_STRING(a)) => env.resolveAlias(a).map(_.bimap(ThrownError, _.bytes)) + case arg => thrown(s"Unexpected alias arg $arg") + } + case arg :: _ => + thrown(s"Unexpected recipient arg $arg") + case args => + thrown(s"Unexpected args $args") } - case (dApp: CaseObj) :: _ if dApp.caseType == aliasType => - (dApp.fields("alias"): @unchecked) match { - case CONST_STRING(a) => env.resolveAlias(a).map(_.explicitGet().bytes) + ) + name <- EitherT[F, ExecutionError, String]( + args match { + case _ :: CONST_STRING(name) :: _ => name.asRight[ExecutionError].pure[F] + case _ :: CaseObj(UNIT, _) :: _ => "default".asRight[ExecutionError].pure[F] + case _ :: arg :: _ => thrown(s"Unexpected name arg $arg") + case args => thrown(s"Unexpected args $args") } - case args => throw new IllegalArgumentException(s"Unexpected recipient args $args") - } - val name = args match { - case _ :: CONST_STRING(name) :: _ => name - case _ :: CaseObj(UNIT, _) :: _ => "default" - case args => throw new IllegalArgumentException(s"Unexpected input args $args") - } - args match { - case _ :: _ :: ARR(args) :: ARR(payments) :: Nil => - env - .callScript( - Recipient.Address(dAppBytes.asInstanceOf[ByteStr]), - name, - args.toList, - payments.map { - case p: CaseObj if p.caseType == paymentType => - (List("assetId", "amount").map(p.fields): @unchecked) match { - case CONST_BYTESTR(a) :: CONST_LONG(v) :: Nil => (Some(a.arr), v) - case CaseObj(UNIT, _) :: CONST_LONG(v) :: Nil => (None, v) + ) + payments <- EitherT[F, ExecutionError, Seq[(Option[Array[Byte]], Long)]]( + args match { + case _ :: _ :: _ :: ARR(payments) :: Nil => + (payments: Seq[EVALUATED]) + .traverse { + case p: CaseObj if p.caseType == paymentType => + List("assetId", "amount").flatMap(p.fields.get) match { + case CONST_BYTESTR(a) :: CONST_LONG(v) :: Nil => Right((Some(a.arr), v)) + case CaseObj(UNIT, _) :: CONST_LONG(v) :: Nil => Right((None, v)) + case args => Left(ThrownError(s"Unexpected payment args $args"): ExecutionError) + } + case arg => + Left(ThrownError(s"Unexpected payment arg $arg"): ExecutionError) + } + .pure[F] + case args => + (s"Unexpected args $args": ExecutionError).asLeft[Seq[(Option[Array[Byte]], Long)]].pure[F] + } + ) + } yield (dAppBytes, name, payments) + monad.flatMap(Coeval(processedArgs.value)) { + case Right((dAppBytes, name, payments)) => + args match { + case _ :: _ :: ARR(passedArgs) :: _ :: Nil => + env + .callScript( + Recipient.Address(dAppBytes), + name, + passedArgs.toList, + payments, + availableComplexity, + reentrant + ) + .map(_.map { case (result, spentComplexity) => + val mappedError = result.leftMap { + case reject: FailOrRejectError => reject + case other => CommonError("Nested invoke error", Some(other)): ExecutionError } - case arg => throw new IllegalArgumentException(s"Unexpected payment arg $arg") - }, - availableComplexity, - reentrant - ) - .map(_.map { case (result, spentComplexity) => - val mappedError = result.leftMap { - case reject: FailOrRejectError => reject - case other => CommonError("Nested invoke error", Some(other)) - } - (mappedError, availableComplexity - spentComplexity) - }) - case xs => - val err = - notImplemented[F, (EVALUATED, Log[F])](s"invoke(dApp: Address, function: String, args: List[Any], payments: List[Payment])", xs) - Coeval.now(err.map((_, 0))) + (mappedError, availableComplexity - spentComplexity) + }) + case xs => + val signature = "invoke(dApp: Address, func: String, args: List[Any], payments: List[Payment])" + val err = notImplemented[F, (EVALUATED, Log[F])](signature, xs) + Coeval.now(err.map((_, 0))) + } + case Left(error) => + (error.asLeft[(EVALUATED, Log[F])], availableComplexity).pure[F].pure[Coeval] } } } @@ -886,26 +937,27 @@ object Functions { override def evaluate[F[_]: Monad](env: Environment[F], args: List[EVALUATED]): F[Either[ExecutionError, EVALUATED]] = args match { case CaseObj(`leaseActionType`, fields) :: Nil => - val recipient = caseObjToRecipient(fields(FieldNames.LeaseRecipient).asInstanceOf[CaseObj]) - val r = recipient match { - case Recipient.Address(bytes) if bytes.arr.length > AddressLength => - Left(CommonError(s"Address bytes length=${bytes.arr.length} exceeds limit=$AddressLength"): ExecutionError) - case Recipient.Alias(name) if name.length > MaxAliasLength => - Left(CommonError(s"Alias name length=${name.length} exceeds limit=$MaxAliasLength"): ExecutionError) - case _ => - CONST_BYTESTR( - Lease.calculateId( - Lease( - recipient, - fields(FieldNames.LeaseAmount).asInstanceOf[CONST_LONG].t, - fields(FieldNames.LeaseNonce).asInstanceOf[CONST_LONG].t - ), - env.txId + caseObjToRecipient(fields(FieldNames.LeaseRecipient).asInstanceOf[CaseObj]) + .flatMap { + case Recipient.Address(bytes) if bytes.arr.length > AddressLength => + Left(CommonError(s"Address bytes length=${bytes.arr.length} exceeds limit=$AddressLength"): ExecutionError) + case Recipient.Alias(name) if name.length > MaxAliasLength => + Left(CommonError(s"Alias name length=${name.length} exceeds limit=$MaxAliasLength"): ExecutionError) + case recipient => + CONST_BYTESTR( + Lease.calculateId( + Lease( + recipient, + fields(FieldNames.LeaseAmount).asInstanceOf[CONST_LONG].t, + fields(FieldNames.LeaseNonce).asInstanceOf[CONST_LONG].t + ), + env.txId + ) ) - ) - } - r.pure[F] - case xs => notImplemented[F, EVALUATED](s"calculateLeaseId(l: Lease)", xs) + } + .pure[F] + case xs => + notImplemented[F, EVALUATED](s"calculateLeaseId(l: Lease)", xs) } } } @@ -929,13 +981,17 @@ object Functions { override def evaluate[F[_]: Monad](env: Environment[F], args: List[EVALUATED]): F[Either[ExecutionError, EVALUATED]] = args match { case List(addr: CaseObj) => - env - .accountScript(caseObjToRecipient(addr)) - .map( - _.map(si => CONST_BYTESTR(ByteStr(global.blake2b256(si.bytes().arr)))) - .getOrElse(Right(unit)) + caseObjToRecipient(addr) + .fold( + _.asLeft[EVALUATED].pure[F], + recipient => + env + .accountScript(recipient) + .map( + _.map(si => CONST_BYTESTR(ByteStr(global.blake2b256(si.bytes().arr)))) + .getOrElse(Right(unit)) + ) ) - case xs => notImplemented[F, EVALUATED](s"scriptHash(account: AddressOrAlias))", xs) } diff --git a/lang/tests/src/test/scala/com/wavesplatform/lang/ContractIntegrationTest.scala b/lang/tests/src/test/scala/com/wavesplatform/lang/ContractIntegrationTest.scala index f4d504ccf74..f53431ea3e2 100644 --- a/lang/tests/src/test/scala/com/wavesplatform/lang/ContractIntegrationTest.scala +++ b/lang/tests/src/test/scala/com/wavesplatform/lang/ContractIntegrationTest.scala @@ -175,7 +175,8 @@ class ContractIntegrationTest extends PropSpec with Inside { Int.MaxValue, correctFunctionCallScope = true, newMode = false, - enableExecutionLog = true + enableExecutionLog = true, + fixedThrownError = true ) .value() .leftMap { case (e, _, log) => (e, log) } @@ -190,7 +191,16 @@ class ContractIntegrationTest extends PropSpec with Inside { compiled.decs, compiled.verifierFuncOpt.get, EvaluatorV2 - .applyCompleted(ctx.evaluationContext(environment), _, _, V3, correctFunctionCallScope = true, newMode = false, enableExecutionLog = false), + .applyCompleted( + ctx.evaluationContext(environment), + _, + _, + V3, + correctFunctionCallScope = true, + newMode = false, + enableExecutionLog = false, + fixedThrownError = true + ), txObject ) ._3 diff --git a/lang/tests/src/test/scala/com/wavesplatform/lang/IntegrationTest.scala b/lang/tests/src/test/scala/com/wavesplatform/lang/IntegrationTest.scala index 4985b44bba9..1905a9fa6a6 100755 --- a/lang/tests/src/test/scala/com/wavesplatform/lang/IntegrationTest.scala +++ b/lang/tests/src/test/scala/com/wavesplatform/lang/IntegrationTest.scala @@ -93,7 +93,16 @@ class IntegrationTest extends PropSpec with Inside { val evalCtx = ctx.evaluationContext(env).asInstanceOf[EvaluationContext[Environment, Id]] compiled.flatMap(v => EvaluatorV2 - .applyCompleted(evalCtx, v._1, LogExtraInfo(), version, correctFunctionCallScope = true, newMode = true, enableExecutionLog = false) + .applyCompleted( + evalCtx, + v._1, + LogExtraInfo(), + version, + correctFunctionCallScope = true, + newMode = true, + enableExecutionLog = false, + fixedThrownError = true + ) ._3 .bimap(_.message, _.asInstanceOf[T]) ) diff --git a/lang/tests/src/test/scala/com/wavesplatform/lang/estimator/package.scala b/lang/tests/src/test/scala/com/wavesplatform/lang/estimator/package.scala index 505857d05da..5f7bf558678 100644 --- a/lang/tests/src/test/scala/com/wavesplatform/lang/estimator/package.scala +++ b/lang/tests/src/test/scala/com/wavesplatform/lang/estimator/package.scala @@ -28,7 +28,8 @@ package object estimator { V3, correctFunctionCallScope = true, overhead, - enableExecutionLog = false + enableExecutionLog = false, + fixedThrownError = true ) def evaluatorV2AsEstimator(overhead: Boolean): ScriptEstimator = new ScriptEstimator { diff --git a/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorSpec.scala b/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorSpec.scala index 059eb1f2b52..80042803f63 100644 --- a/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorSpec.scala +++ b/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorSpec.scala @@ -75,7 +75,16 @@ abstract class EvaluatorSpec extends PropSpec with ScriptGen with Inside { private def evalExpr(expr: EXPR, version: StdLibVersion, useNewPowPrecision: Boolean): (Log[Id], Int, Either[ExecutionError, EVALUATED]) = { val ctx = lazyContexts((DirectiveSet(version, Account, Expression).explicitGet(), useNewPowPrecision, true)).value() val evalCtx = ctx.evaluationContext(Common.emptyBlockchainEnvironment()) - EvaluatorV2.applyCompleted(evalCtx, expr, LogExtraInfo(), version, correctFunctionCallScope = true, newMode = true, enableExecutionLog = false) + EvaluatorV2.applyCompleted( + evalCtx, + expr, + LogExtraInfo(), + version, + correctFunctionCallScope = true, + newMode = true, + enableExecutionLog = false, + fixedThrownError = true + ) } private def compile(code: String, version: StdLibVersion): Either[String, EXPR] = { diff --git a/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorV1V2Test.scala b/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorV1V2Test.scala index f040ca89b32..93359c2b6e6 100644 --- a/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorV1V2Test.scala +++ b/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorV1V2Test.scala @@ -75,7 +75,8 @@ class EvaluatorV1V2Test extends PropSpec with EitherValues { implicitly[StdLibVersion], correctFunctionCallScope = true, newMode = true, - enableExecutionLog = false + enableExecutionLog = false, + fixedThrownError = true ) ._3 .asInstanceOf[Either[ExecutionError, T]] @@ -101,7 +102,8 @@ class EvaluatorV1V2Test extends PropSpec with EitherValues { implicitly[StdLibVersion], correctFunctionCallScope = true, newMode = true, - enableExecutionLog = true + enableExecutionLog = true, + fixedThrownError = true ) evaluatorV2Result shouldBe evaluatorV1Result.bimap(_._1, _._1) diff --git a/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorV2Test.scala b/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorV2Test.scala index bc20e70fafc..c62dddc1cb4 100644 --- a/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorV2Test.scala +++ b/lang/tests/src/test/scala/com/wavesplatform/lang/evaluator/EvaluatorV2Test.scala @@ -32,7 +32,8 @@ class EvaluatorV2Test extends PropSpec with Inside { ctx.evaluationContext(environment), version, correctFunctionCallScope = true, - newMode + newMode, + fixedThrownError = true ) .value() .bimap(_._1.message, { case (result, complexity, _) => (result, complexity) }) diff --git a/lang/tests/src/test/scala/com/wavesplatform/utils/MerkleTest.scala b/lang/tests/src/test/scala/com/wavesplatform/utils/MerkleTest.scala index 95039717194..102d83ba4d6 100644 --- a/lang/tests/src/test/scala/com/wavesplatform/utils/MerkleTest.scala +++ b/lang/tests/src/test/scala/com/wavesplatform/utils/MerkleTest.scala @@ -110,7 +110,7 @@ class MerkleTest extends PropSpec { val ctx = lazyContexts((DirectiveSet(version, Account, Expression).explicitGet(), true, true))() val evalCtx = ctx.evaluationContext[Id](Common.emptyBlockchainEnvironment()) val typed = ExpressionCompiler(ctx.compilerContext, V3, untyped) - typed.flatMap(v => EvaluatorV2.applyCompleted(evalCtx, v._1, LogExtraInfo(), version, true, true, false)._3.leftMap(_.toString)) + typed.flatMap(v => EvaluatorV2.applyCompleted(evalCtx, v._1, LogExtraInfo(), version, true, true, false, true)._3.leftMap(_.toString)) } private def scriptSrc(root: Array[Byte], proof: Array[Byte], value: Array[Byte]): String = { diff --git a/node-it/src/test/scala/com/wavesplatform/it/sync/smartcontract/InvokeMultiplePaymentsSuite.scala b/node-it/src/test/scala/com/wavesplatform/it/sync/smartcontract/InvokeMultiplePaymentsSuite.scala index 98360c47a89..c79de4138c5 100644 --- a/node-it/src/test/scala/com/wavesplatform/it/sync/smartcontract/InvokeMultiplePaymentsSuite.scala +++ b/node-it/src/test/scala/com/wavesplatform/it/sync/smartcontract/InvokeMultiplePaymentsSuite.scala @@ -44,7 +44,7 @@ class InvokeMultiplePaymentsSuite extends BaseTransactionSuite with CancelAfterF sender.balance(callerAddress).balance shouldBe callerBalance - smartMinFee } - test("script should sheck if alias not exists") { + test("script should sheck if alias not exist") { val alias = "unknown" assertBadRequestAndMessage( diff --git a/node/src/main/scala/com/wavesplatform/api/http/utils/UtilsEvaluator.scala b/node/src/main/scala/com/wavesplatform/api/http/utils/UtilsEvaluator.scala index 9731bdd1740..9126bad6c10 100644 --- a/node/src/main/scala/com/wavesplatform/api/http/utils/UtilsEvaluator.scala +++ b/node/src/main/scala/com/wavesplatform/api/http/utils/UtilsEvaluator.scala @@ -155,7 +155,8 @@ object UtilsEvaluator { correctFunctionCallScope = blockchain.checkEstimatorSumOverflow, newMode = blockchain.newEvaluatorMode, checkConstructorArgsTypes = true, - enableExecutionLog = true + enableExecutionLog = true, + fixedThrownError = true ) .value() .leftMap { case (err, _, log) => InvokeRejectError(err.message, log) } 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 54cf3fe6800..f1f02c6d1c4 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 @@ -8,6 +8,7 @@ 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.* @@ -391,7 +392,8 @@ object InvokeScriptDiff { limit, blockchain.correctFunctionCallScope, blockchain.newEvaluatorMode, - enableExecutionLog + enableExecutionLog, + blockchain.isFeatureActivated(LightNode) ) .map( _.leftMap[ValidationError] { diff --git a/node/src/main/scala/com/wavesplatform/state/diffs/invoke/InvokeScriptTransactionDiff.scala b/node/src/main/scala/com/wavesplatform/state/diffs/invoke/InvokeScriptTransactionDiff.scala index 5673c35cdb9..78c08900af9 100644 --- a/node/src/main/scala/com/wavesplatform/state/diffs/invoke/InvokeScriptTransactionDiff.scala +++ b/node/src/main/scala/com/wavesplatform/state/diffs/invoke/InvokeScriptTransactionDiff.scala @@ -8,7 +8,7 @@ import com.wavesplatform.account.* import com.wavesplatform.common.state.ByteStr import com.wavesplatform.common.utils.EitherExt2 import com.wavesplatform.features.BlockchainFeatures -import com.wavesplatform.features.BlockchainFeatures.RideV6 +import com.wavesplatform.features.BlockchainFeatures.{LightNode, RideV6} import com.wavesplatform.features.EstimatorProvider.* import com.wavesplatform.features.EvaluatorFixProvider.* import com.wavesplatform.features.FunctionCallPolicyProvider.* @@ -334,7 +334,8 @@ object InvokeScriptTransactionDiff { startLimit, blockchain.correctFunctionCallScope, blockchain.newEvaluatorMode, - enableExecutionLog + enableExecutionLog, + blockchain.isFeatureActivated(LightNode) ) .runAttempt() .leftMap(error => (error.getMessage: ExecutionError, 0, Nil: Log[Id])) diff --git a/node/src/main/scala/com/wavesplatform/transaction/TxValidationError.scala b/node/src/main/scala/com/wavesplatform/transaction/TxValidationError.scala index 5d4f1cabc58..eb8d61aa53b 100644 --- a/node/src/main/scala/com/wavesplatform/transaction/TxValidationError.scala +++ b/node/src/main/scala/com/wavesplatform/transaction/TxValidationError.scala @@ -32,8 +32,8 @@ object TxValidationError { case class BlockFromFuture(ts: Long) extends ValidationError case class AlreadyInTheState(txId: ByteStr, txHeight: Int) extends ValidationError case class AccountBalanceError(errs: Map[Address, String]) extends ValidationError - case class AliasDoesNotExist(a: Alias) extends ValidationError { override def toString: String = s"Alias '$a' does not exists." } - case class AliasIsDisabled(a: Alias) extends ValidationError + case class AliasDoesNotExist(a: Alias) extends ValidationError { override def toString: String = s"Alias '$a' does not exist." } + case class AliasIsDisabled(a: Alias) extends ValidationError case class OrderValidationError(order: Order, err: String) extends ValidationError case class SenderIsBlacklisted(addr: String) extends ValidationError case class Mistiming(err: String) extends ValidationError diff --git a/node/src/main/scala/com/wavesplatform/transaction/smart/script/ScriptRunner.scala b/node/src/main/scala/com/wavesplatform/transaction/smart/script/ScriptRunner.scala index 1b6802082ca..3d19d95f3d6 100644 --- a/node/src/main/scala/com/wavesplatform/transaction/smart/script/ScriptRunner.scala +++ b/node/src/main/scala/com/wavesplatform/transaction/smart/script/ScriptRunner.scala @@ -54,7 +54,8 @@ object ScriptRunner { blockchain.newEvaluatorMode, blockchain.isFeatureActivated(RideV6), enableExecutionLog, - blockchain.isFeatureActivated(ConsensusImprovements) + blockchain.isFeatureActivated(ConsensusImprovements), + blockchain.isFeatureActivated(LightNode) ) def applyGeneric( @@ -72,7 +73,8 @@ object ScriptRunner { newEvaluatorMode: Boolean, checkWeakPk: Boolean, enableExecutionLog: Boolean, - fixBigScriptField: Boolean + fixBigScriptField: Boolean, + fixedThrownError: Boolean ): (Log[Id], Int, Either[ExecutionError, EVALUATED]) = { def evalVerifier( @@ -135,7 +137,8 @@ object ScriptRunner { correctFunctionCallScope = checkEstimatorSumOverflow, newMode = newEvaluatorMode, onExceed, - enableExecutionLog + enableExecutionLog, + fixedThrownError ) (log, limit - unusedComplexity, result) diff --git a/node/src/test/scala/com/wavesplatform/state/diffs/ci/LeaseActionDiffTest.scala b/node/src/test/scala/com/wavesplatform/state/diffs/ci/LeaseActionDiffTest.scala index 3ee74a82db6..9d5819074af 100644 --- a/node/src/test/scala/com/wavesplatform/state/diffs/ci/LeaseActionDiffTest.scala +++ b/node/src/test/scala/com/wavesplatform/state/diffs/ci/LeaseActionDiffTest.scala @@ -543,7 +543,7 @@ class LeaseActionDiffTest extends PropSpec with WithDomain { TestBlock.create(Seq(invoke)), v5Features ) { case (snapshot, _) => - snapshot.errorMessage(invoke.id()).get.text shouldBe "Alias 'alias:T:alias2' does not exists." + snapshot.errorMessage(invoke.id()).get.text shouldBe "Alias 'alias:T:alias2' does not exist." } } diff --git a/node/src/test/scala/com/wavesplatform/state/diffs/ci/ScriptTransferByAliasTest.scala b/node/src/test/scala/com/wavesplatform/state/diffs/ci/ScriptTransferByAliasTest.scala index 04db51ad1e7..f1ba5fb0ea5 100644 --- a/node/src/test/scala/com/wavesplatform/state/diffs/ci/ScriptTransferByAliasTest.scala +++ b/node/src/test/scala/com/wavesplatform/state/diffs/ci/ScriptTransferByAliasTest.scala @@ -103,7 +103,7 @@ class ScriptTransferByAliasTest extends PropSpec with WithDomain { """.stripMargin ) d.appendBlock(setScript(secondSigner, dApp)) - d.appendBlockE(invoke()) should produce("Alias 'alias:T:alias' does not exists") + d.appendBlockE(invoke()) should produce("Alias 'alias:T:alias' does not exist") } } } diff --git a/node/src/test/scala/com/wavesplatform/state/diffs/smart/RideExceptionsTest.scala b/node/src/test/scala/com/wavesplatform/state/diffs/smart/RideExceptionsTest.scala new file mode 100644 index 00000000000..18c9497a196 --- /dev/null +++ b/node/src/test/scala/com/wavesplatform/state/diffs/smart/RideExceptionsTest.scala @@ -0,0 +1,172 @@ +package com.wavesplatform.state.diffs.smart + +import com.wavesplatform.common.state.ByteStr +import com.wavesplatform.common.utils.EitherExt2 +import com.wavesplatform.db.WithDomain +import com.wavesplatform.db.WithState.AddrWithBalance +import com.wavesplatform.features.BlockchainFeatures.LightNode +import com.wavesplatform.lang.contract.DApp +import com.wavesplatform.lang.contract.DApp.{CallableAnnotation, CallableFunction} +import com.wavesplatform.lang.directives.values.V7 +import com.wavesplatform.lang.script.ContractScript.ContractScriptImpl +import com.wavesplatform.lang.script.v1.ExprScript.ExprScriptImpl +import com.wavesplatform.lang.v1.FunctionHeader.{Native, User} +import com.wavesplatform.lang.v1.compiler.Terms.* +import com.wavesplatform.lang.v1.compiler.TestCompiler +import com.wavesplatform.lang.v1.evaluator.FunctionIds.* +import com.wavesplatform.protobuf.dapp.DAppMeta +import com.wavesplatform.test.DomainPresets.{TransactionStateSnapshot, WavesSettingsOps} +import com.wavesplatform.test.{PropSpec, produce} +import com.wavesplatform.transaction.TxHelpers.* + +class RideExceptionsTest extends PropSpec with WithDomain { + property("throwing java exception from ride functions should correctly fail or reject invoke after light node activation") { + assert( + FUNCTION_CALL(Native(ACCOUNTWAVESBALANCE), List(REF("unit"))), + "Unexpected recipient type Unit", + rejectBefore = false + ) + assert( + FUNCTION_CALL(Native(ACCOUNTASSETONLYBALANCE), List(REF("unit"), CONST_BYTESTR(ByteStr.empty).explicitGet())), + "Unexpected recipient type Unit", + rejectBefore = false + ) + assert( + FUNCTION_CALL(Native(ACCOUNTSCRIPTHASH), List(REF("unit"))), + "Unexpected recipient type Unit", + rejectBefore = false + ) + assert( + FUNCTION_CALL(Native(CALCULATE_LEASE_ID), List(FUNCTION_CALL(User("Lease"), List(REF("unit"))))), + "Unexpected recipient type Unit", + rejectBefore = false + ) + assert( + FUNCTION_CALL(Native(CALLDAPP), List(CONST_LONG(1), CONST_LONG(1), CONST_LONG(1), CONST_LONG(1))), + "Unexpected recipient arg", + rejectBefore = true, + checkVerifier = false + ) + assert( + FUNCTION_CALL(Native(CALLDAPP), List(FUNCTION_CALL(User("Address"), List(CONST_LONG(1))), CONST_LONG(1), CONST_LONG(1), CONST_LONG(1))), + "Unexpected address bytes", + rejectBefore = true, + checkVerifier = false + ) + assert( + FUNCTION_CALL( + Native(CALLDAPP), + List(FUNCTION_CALL(User("Alias"), List(CONST_STRING("alias").explicitGet())), CONST_LONG(1), CONST_LONG(1), CONST_LONG(1)) + ), + "Alias 'alias:T:alias' does not exist", + rejectBefore = true, + checkVerifier = false + ) + assert( + FUNCTION_CALL( + Native(CALLDAPP), + List(FUNCTION_CALL(User("Alias"), List(CONST_LONG(1))), CONST_LONG(1), CONST_LONG(1), CONST_LONG(1)) + ), + "Unexpected alias arg", + rejectBefore = true, + checkVerifier = false + ) + assert( + FUNCTION_CALL( + Native(CALLDAPP), + List(FUNCTION_CALL(User("Address"), List(CONST_BYTESTR(ByteStr.empty).explicitGet())), CONST_LONG(1), CONST_LONG(1), CONST_LONG(1)) + ), + "Unexpected name arg", + rejectBefore = true, + checkVerifier = false + ) + assert( + FUNCTION_CALL( + Native(CALLDAPP), + List( + FUNCTION_CALL(User("Address"), List(CONST_BYTESTR(ByteStr.empty).explicitGet())), + REF("unit"), + CONST_LONG(1), + ARR(Vector(CONST_LONG(1)), limited = false).explicitGet() + ) + ), + "Unexpected payment arg", + rejectBefore = true, + checkVerifier = false + ) + assert( + FUNCTION_CALL( + Native(CREATE_MERKLE_ROOT), + List( + ARR(Vector.fill(16)(CONST_BYTESTR(ByteStr.fill(32)(1)).explicitGet()), true).explicitGet(), + CONST_BYTESTR(ByteStr.fill(32)(1)).explicitGet(), + CONST_LONG(Long.MaxValue) + ) + ), + "integer overflow", + rejectBefore = false + ) + assert( + FUNCTION_CALL( + Native(CREATE_MERKLE_ROOT), + List( + ARR(Vector.fill(1)(CONST_BYTESTR(ByteStr.fill(32)(1)).explicitGet()), true).explicitGet(), + CONST_BYTESTR(ByteStr.fill(32)(1)).explicitGet(), + CONST_LONG(100) + ) + ), + "Index 100 out of range allowed by proof list length 1", + rejectBefore = false + ) + } + + private def assert(expr: EXPR, error: String, rejectBefore: Boolean, checkVerifier: Boolean = true) = + withDomain( + TransactionStateSnapshot.setFeaturesHeight(LightNode -> 7), + AddrWithBalance.enoughBalances(defaultSigner, secondSigner, signer(2)) + ) { d => + val func = FUNC("default", Nil, expr) + val dApp = DApp(DAppMeta(), Nil, List(CallableFunction(CallableAnnotation("i"), func)), None) + d.appendBlock(setScript(secondSigner, ContractScriptImpl(V7, dApp))) + + // dApp before activation + if (rejectBefore) { + d.appendBlockE(invoke()) should produce(error) + d.appendBlock() + } else + d.appendAndAssertFailed(invoke(), error) + + // dApp before activation with enough complexity to fail + val complexCond = TestCompiler(V7).compileExpression(s"${(1 to 6).map(_ => "sigVerify(base58'', base58'', base58'')").mkString(" || ")}") + val complexExpr = IF(complexCond.expr, TRUE, expr) + val complexFunc = FUNC("default", Nil, complexExpr) + val complexDApp = DApp(DAppMeta(), Nil, List(CallableFunction(CallableAnnotation("i"), complexFunc)), None) + d.appendBlock(setScript(secondSigner, ContractScriptImpl(V7, complexDApp))) + if (rejectBefore) { + d.appendBlockE(invoke()) should produce(error) + d.appendBlock() + } else + d.appendAndAssertFailed(invoke(), error) + + // verifier before activation + if (checkVerifier) { + d.appendBlock(setScript(signer(2), ExprScriptImpl(V7, false, complexExpr))) + d.appendBlockE(transfer(signer(2), defaultAddress)) should produce(error) + } else + d.appendBlock() + + // dApp after activation + d.blockchain.isFeatureActivated(LightNode) shouldBe false + d.appendBlock(setScript(secondSigner, ContractScriptImpl(V7, dApp))) + d.blockchain.isFeatureActivated(LightNode) shouldBe true + d.appendBlockE(invoke()) should produce(error) + + // dApp after activation with enough complexity to fail + d.appendBlock(setScript(secondSigner, ContractScriptImpl(V7, complexDApp))) + d.appendAndAssertFailed(invoke(), error) + + // verifier after activation + if (checkVerifier) + d.appendBlockE(transfer(signer(2), defaultAddress)) should produce(error) + } +} diff --git a/node/src/test/scala/com/wavesplatform/state/diffs/smart/predef/MatcherBlockchainTest.scala b/node/src/test/scala/com/wavesplatform/state/diffs/smart/predef/MatcherBlockchainTest.scala index 96a8a45c238..77e52a6ebea 100644 --- a/node/src/test/scala/com/wavesplatform/state/diffs/smart/predef/MatcherBlockchainTest.scala +++ b/node/src/test/scala/com/wavesplatform/state/diffs/smart/predef/MatcherBlockchainTest.scala @@ -102,7 +102,8 @@ class MatcherBlockchainTest extends PropSpec with MockFactory with WithDomain { newEvaluatorMode = true, checkWeakPk = true, enableExecutionLog = false, - fixBigScriptField = true + fixBigScriptField = true, + fixedThrownError = true ) ._3 shouldBe Right(CONST_BOOLEAN(true)) } diff --git a/node/src/test/scala/com/wavesplatform/state/diffs/smart/scenarios/AddressFromRecipientScenarioTest.scala b/node/src/test/scala/com/wavesplatform/state/diffs/smart/scenarios/AddressFromRecipientScenarioTest.scala index 93e5646af9f..8e7ea048ec2 100644 --- a/node/src/test/scala/com/wavesplatform/state/diffs/smart/scenarios/AddressFromRecipientScenarioTest.scala +++ b/node/src/test/scala/com/wavesplatform/state/diffs/smart/scenarios/AddressFromRecipientScenarioTest.scala @@ -49,7 +49,7 @@ class AddressFromRecipientScenarioTest extends PropSpec with WithState { val (gen, _, _, transferViaAlias) = preconditionsAndAliasCreations assertDiffAndState(Seq(TestBlock.create(gen)), TestBlock.create(Seq())) { case (_, state) => - runScript(script, transferViaAlias, state) should produce(" does not exists") + runScript(script, transferViaAlias, state) should produce(" does not exist") } } }