diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bc1f388a0..6c1c2fffa8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,7 @@ jobs: fail_ci_if_error: true - name: Publish a snapshot ${{ github.ref }} + if: env.HAS_SECRETS == 'true' run: sbt ++${{ matrix.scala }} publish env: SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} diff --git a/README.md b/README.md index 8b95695a6d..afda393b92 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![CI](https://github.com/ScorexFoundation/sigmastate-interpreter/actions/workflows/ci.yml/badge.svg)](https://github.com/ScorexFoundation/sigmastate-interpreter/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/ScorexFoundation/sigmastate-interpreter/branch/develop/graph/badge.svg?token=HNu2ZEOoV6)](https://codecov.io/gh/ScorexFoundation/sigmastate-interpreter) + # ErgoScript compiler and ErgoTree interpreter This repository contains implementations of ErgoScript compiler and ErgoTree diff --git a/common/src/main/scala/scalan/util/CollectionUtil.scala b/common/src/main/scala/scalan/util/CollectionUtil.scala index e5aff804c7..c0f5b6e423 100644 --- a/common/src/main/scala/scalan/util/CollectionUtil.scala +++ b/common/src/main/scala/scalan/util/CollectionUtil.scala @@ -12,10 +12,11 @@ import scala.reflect.ClassTag object CollectionUtil { + /** @deprecated shouldn't be used other than for backwards compatibility with v3.x, v4.x. */ def concatArrays[T](xs: Array[T], ys: Array[T]): Array[T] = { val len = xs.length + ys.length val result = (xs match { - case arr: Array[AnyRef] => new Array[AnyRef](len) + case arr: Array[AnyRef] => new Array[AnyRef](len) // creates an array with invalid type descriptor (i.e. when T == Tuple2) case arr: Array[Byte] => new Array[Byte](len) case arr: Array[Short] => new Array[Short](len) case arr: Array[Int] => new Array[Int](len) @@ -30,6 +31,21 @@ object CollectionUtil { result } + /** Concatenates two arrays into a new resulting array. + * All items of both arrays are copied to the result using System.arraycopy. + * This method takes ClassTag to create proper resulting array. + * Can be used in v5.0 and above. + */ + def concatArrays_v5[T:ClassTag](arr1: Array[T], arr2: Array[T]): Array[T] = { + val l1 = arr1.length + val l2 = arr2.length + val length: Int = l1 + l2 + val result: Array[T] = new Array[T](length) + System.arraycopy(arr1, 0, result, 0, l1) + System.arraycopy(arr2, 0, result, l1, l2) + result + } + def deepHashCode[T](arr: Array[T]): Int = arr match { case arr: Array[AnyRef] => util.Arrays.deepHashCode(arr) case arr: Array[Byte] => util.Arrays.hashCode(arr) @@ -50,6 +66,10 @@ object CollectionUtil { } } + /** Group the given sequence of pairs by first values as keys. + * @param kvs sequence of values which is traversed once + * @return a multimap with ArrayBuffer of values for each key. + */ def createMultiMap[K,V](kvs: GenIterable[(K,V)]): Map[K, ArrayBuffer[V]] = { val res = HashMap.empty[K, ArrayBuffer[V]] kvs.foreach { case (k, v) => @@ -62,12 +82,17 @@ object CollectionUtil { res.toMap } - // TODO optimize: using cfor and avoiding allocations + /** Perform relational inner join of two sequences using the given key projections. */ def joinSeqs[O, I, K](outer: GenIterable[O], inner: GenIterable[I])(outKey: O=>K, inKey: I=>K): GenIterable[(O,I)] = { val kvs = createMultiMap(inner.map(i => (inKey(i), i))) val res = outer.flatMap(o => { val ko = outKey(o) - kvs(ko).map(i => (o,i)) + kvs.get(ko) match { + case Some(inners) => + inners.map(i => (o,i)) + case None => + Nil + } }) res } @@ -182,6 +207,21 @@ object CollectionUtil { implicit class TraversableOps[A, Source[X] <: GenIterable[X]](val xs: Source[A]) extends AnyVal { + /** Returns a copy of this collection where elements at `items(i)._1` are replaced + * with `items(i)._2` for each i. + */ + def updateMany(items: Seq[(Int, A)])(implicit tA: ClassTag[A]): Seq[A] = { + val res = xs.toArray + val nItems = items.length + var i = 0 + while (i < nItems) { + val item = items(i) + res(item._1) = item._2 + i += 1 + } + res + } + def filterCast[B:ClassTag](implicit cbf: CanBuildFrom[Source[A], B, Source[B]]): Source[B] = { val b = cbf() for (x <- xs) { diff --git a/common/src/main/scala/sigmastate/VersionContext.scala b/common/src/main/scala/sigmastate/VersionContext.scala new file mode 100644 index 0000000000..04ac2da524 --- /dev/null +++ b/common/src/main/scala/sigmastate/VersionContext.scala @@ -0,0 +1,101 @@ +package sigmastate + +import sigmastate.VersionContext.JitActivationVersion + +import scala.util.DynamicVariable + +/** Represent currently activated protocol version and currently executed ErgoTree version. + * + * This parameters, once set in DynamicVariable can be accessed everywhere on the current + * thread. + * + * @param activatedVersion Currently activated script version == Block.headerVersion - 1 + * @param ergoTreeVersion version of the currently executed ErgoTree + * + * @see + */ +case class VersionContext(activatedVersion: Byte, ergoTreeVersion: Byte) { + require(ergoTreeVersion <= activatedVersion, + s"In a valid VersionContext ergoTreeVersion must never exceed activatedVersion: $this") + + /** @return true, if the activated script version of Ergo protocol on the network is + * greater than v1. */ + def isJitActivated: Boolean = activatedVersion >= JitActivationVersion + + /** @return true, if the version of ErgoTree being executed greater than v1. */ + def isErgoTreeVersionGreaterV1: Boolean = + ergoTreeVersion >= JitActivationVersion +} + +object VersionContext { + /** Maximum version of ErgoTree supported by this interpreter release. + * See version bits in `ErgoTree.header` for more details. + * This value should be increased with each new protocol update via soft-fork. + * The following values are used for current and upcoming forks: + * - version 3.x this value must be 0 + * - in v4.0 must be 1 + * - in v5.x must be 2 + * etc. + */ + val MaxSupportedScriptVersion: Byte = 2 // supported versions 0, 1, 2 + + /** The first version of ErgoTree starting from which the JIT costing interpreter must be used. + * It must also be used for all subsequent versions (3, 4, etc). + */ + val JitActivationVersion: Byte = 2 + + private val _defaultContext = VersionContext( + activatedVersion = 1/* v4.x */, + ergoTreeVersion = 1 + ) + + /** Universally accessible version context which is used to version the code + * across the whole repository. + * + * The default value represent activated Ergo protocol and highest ErgoTree version. + */ + private val _versionContext: DynamicVariable[VersionContext] = + new DynamicVariable[VersionContext](_defaultContext) + + /** Returns the current VersionContext attached to the current thread. + * Each thread can have only one current version context at any time, which can be + * changed using `withVersions` method. + * + * @see withVersions() + */ + def current: VersionContext = { + val ctx = _versionContext.value + if (ctx == null) + throw new IllegalStateException( + s"VersionContext is not specified on thread ${Thread.currentThread().getId}") + ctx + } + + /** Executes the given block under the given version context attached to the current thread. + * + * The typical usage is to use `VersionContext.withVersions(activatedVersion, + * treeVersion) {...}` when the block of code needs to be executed with the given + * versions. + * + * For example, sigmastate.Interpreter uses it to execute operations according to the + * necessary versions of Ergo protocol and ErgoTree. + * + * @param activatedVersion Currently activated script version == Block.headerVersion - 1 + * @param ergoTreeVersion ErgoTree version to be set on the current thread + * @param block block of code to execute + * @return result of block execution + */ + def withVersions[T](activatedVersion: Byte, ergoTreeVersion: Byte)(block: => T): T = + _versionContext.withValue(VersionContext(activatedVersion, ergoTreeVersion))(block) + + /** Checks the version context has the given versions*/ + def checkVersions(activatedVersion: Byte, ergoTreeVersion: Byte) = { + val ctx = VersionContext.current + if (ctx.activatedVersion != activatedVersion || ctx.ergoTreeVersion != ergoTreeVersion) { + val expected = VersionContext(activatedVersion, ergoTreeVersion) + throw new IllegalStateException( + s"Global VersionContext.current = ${ctx} while expected $expected.") + } + } + +} diff --git a/common/src/main/scala/sigmastate/util.scala b/common/src/main/scala/sigmastate/util.scala new file mode 100644 index 0000000000..e499ab9825 --- /dev/null +++ b/common/src/main/scala/sigmastate/util.scala @@ -0,0 +1,31 @@ +package sigmastate + +import scalan.util.CollectionUtil + +import scala.reflect.ClassTag + +object util { + /** Maximum length of an allocatable array. */ + val MaxArrayLength: Int = 100000 + + private def checkLength[A](len: Int)(implicit tA: ClassTag[A]) = { + if (len > MaxArrayLength) + throw new RuntimeException( + s"Cannot allocate array of $tA with $len items: max limit is $MaxArrayLength") + } + + /** Allocates a new array with `len` items of type `A`. + * Should be used instead of `new Array[A](n)` or `Array.ofDim[A](n)` + */ + final def safeNewArray[A](len: Int)(implicit tA: ClassTag[A]): Array[A] = { + checkLength[A](len) + new Array[A](len) + } + + /** Concatenate two arrays checking length limit of the resulting array. + * Should be used in implementation of Coll operations of v5.0 and above. */ + final def safeConcatArrays_v5[A](arr1: Array[A], arr2: Array[A])(implicit tA: ClassTag[A]): Array[A] = { + checkLength[A](arr1.length + arr2.length) + CollectionUtil.concatArrays_v5(arr1, arr2) + } +} diff --git a/common/src/test/scala/scalan/util/CollectionUtilTests.scala b/common/src/test/scala/scalan/util/CollectionUtilTests.scala index 7f9b667456..9471093cad 100644 --- a/common/src/test/scala/scalan/util/CollectionUtilTests.scala +++ b/common/src/test/scala/scalan/util/CollectionUtilTests.scala @@ -9,16 +9,31 @@ class CollectionUtilTests extends BaseTests { import scalan.util.CollectionUtil._ import java.lang.{Byte => JByte, Integer} + test("updateMany") { + val xs: Seq[Byte] = Array[Byte](1,2,3) + xs.updateMany(Seq.empty) shouldBe xs + xs.updateMany(Seq(0 -> 2)) shouldBe Seq(2, 2, 3) + xs.updateMany(Seq(0 -> 2, 2 -> 2)) shouldBe Seq(2, 2, 2) + an[IndexOutOfBoundsException] should be thrownBy { + xs.updateMany(Seq(3 -> 2)) + } + } + test("concatArrays") { val xs = Array[Byte](1,2,3) val ys = Array[Byte](4,5,6) val zs = concatArrays(xs, ys) assertResult(Array[Byte](1, 2, 3, 4, 5, 6))(zs) -// val jxs = Array[JByte](new JByte(1), new JByte(2), new JByte(3)) -// val jys = Array[JByte](new JByte(4), new JByte(5), new JByte(6)) -// val jzs = concatArrays(jxs, jys) -// assertResult(Array[Byte](1, 2, 3, 4, 5, 6))(jzs) + val pairs = xs.zip(ys) + // this reproduces the problem which takes place in v3.x, v4.x (ErgoTree v0, v1) + an[ClassCastException] should be thrownBy(concatArrays(pairs, pairs)) + + // and this is the fix in v5.0 + concatArrays_v5(pairs, pairs) shouldBe Array((1, 4), (2, 5), (3, 6), (1, 4), (2, 5), (3, 6)) + + val xOpts = xs.map(Option(_)) + concatArrays_v5(xOpts, xOpts) shouldBe Array(Some(1), Some(2), Some(3), Some(1), Some(2), Some(3)) } def join(l: Map[Int,Int], r: Map[Int,Int]) = @@ -28,6 +43,30 @@ class CollectionUtilTests extends BaseTests { def joinPairs(l: Seq[(String,Int)], r: Seq[(String,Int)]) = outerJoinSeqs(l, r)(l => l._1, r => r._1)((_,l) => l._2, (_,r) => r._2, (k,l,r) => l._2 + r._2) + test("joinSeqs") { + def key(p : (Int, String)): Int = p._1 + + { + val res = CollectionUtil.joinSeqs( + outer = Seq(1 -> "o1", 1 -> "o1"), + inner = Seq(1 -> "i1", 2 -> "i2"))(key, key) + res shouldBe Seq( + (1 -> "o1") -> (1 -> "i1"), + (1 -> "o1") -> (1 -> "i1") + ) + } + + { // same as above, but swapping inner and outer + val res = CollectionUtil.joinSeqs( + outer = Seq(1 -> "o1", 2 -> "o2"), + inner = Seq(1 -> "i1", 1 -> "i1"))(key, key) + res shouldBe Seq( + (1 -> "o1") -> (1 -> "i1"), + (1 -> "o1") -> (1 -> "i1") + ) + } + } + test("outerJoin maps") { val left = Map(1 -> 1, 2 -> 2, 3 -> 3) val right = Map(2 -> 2, 3 -> 3, 4 -> 4) diff --git a/common/src/test/scala/sigmastate/VersionTesting.scala b/common/src/test/scala/sigmastate/VersionTesting.scala new file mode 100644 index 0000000000..05b3a2a6e5 --- /dev/null +++ b/common/src/test/scala/sigmastate/VersionTesting.scala @@ -0,0 +1,87 @@ +package sigmastate + +import spire.syntax.all.cfor + +import scala.util.DynamicVariable + +trait VersionTesting { + + protected val activatedVersions: Seq[Byte] = + (0 to VersionContext.MaxSupportedScriptVersion).map(_.toByte).toArray[Byte] + + private[sigmastate] val _currActivatedVersion = new DynamicVariable[Byte](0) + def activatedVersionInTests: Byte = _currActivatedVersion.value + + /** Checks if the current activated script version used in tests corresponds to v4.x. */ + def isActivatedVersion4: Boolean = activatedVersionInTests < VersionContext.JitActivationVersion + + val ergoTreeVersions: Seq[Byte] = + (0 to VersionContext.MaxSupportedScriptVersion).map(_.toByte).toArray[Byte] + + private[sigmastate] val _currErgoTreeVersion = new DynamicVariable[Byte](0) + + /** Current ErgoTree version assigned dynamically. */ + def ergoTreeVersionInTests: Byte = _currErgoTreeVersion.value + + /** Executes the given block for each combination of _currActivatedVersion and + * _currErgoTreeVersion assigned to dynamic variables. + */ + def forEachScriptAndErgoTreeVersion + (activatedVers: Seq[Byte], ergoTreeVers: Seq[Byte]) + (block: => Unit): Unit = { + cfor(0)(_ < activatedVers.length, _ + 1) { i => + val activatedVersion = activatedVers(i) + // setup each activated version + _currActivatedVersion.withValue(activatedVersion) { + + cfor(0)( + i => i < ergoTreeVers.length && ergoTreeVers(i) <= activatedVersion, + _ + 1) { j => + val treeVersion = ergoTreeVers(j) + // for each tree version up to currently activated, set it up and execute block + _currErgoTreeVersion.withValue(treeVersion)(block) + } + + } + } + } + + /** Helper method which executes the given `block` once for each `activatedVers`. + * The method sets the dynamic variable activatedVersionInTests with is then available + * in the block. + */ + def forEachActivatedScriptVersion(activatedVers: Seq[Byte])(block: => Unit): Unit = { + cfor(0)(_ < activatedVers.length, _ + 1) { i => + val activatedVersion = activatedVers(i) + _currActivatedVersion.withValue(activatedVersion)(block) + } + } + + /** Helper method which executes the given `block` once for each `ergoTreeVers`. + * The method sets the dynamic variable ergoTreeVersionInTests with is then available + * in the block. + */ + def forEachErgoTreeVersion(ergoTreeVers: Seq[Byte])(block: => Unit): Unit = { + cfor(0)(_ < ergoTreeVers.length, _ + 1) { i => + val version = ergoTreeVers(i) + _currErgoTreeVersion.withValue(version)(block) + } + } + + val printVersions: Boolean = false + + protected def testFun_Run(testName: String, testFun: => Any): Unit = { + def msg = s"""property("$testName")(ActivatedVersion = $activatedVersionInTests; ErgoTree version = $ergoTreeVersionInTests)""" + if (printVersions) println(msg) + try testFun + catch { + case t: Throwable => + if (!printVersions) { + // wasn't printed, print it now + println(msg) + } + throw t + } + } + +} diff --git a/common/src/test/scala/sigmastate/VersionTestingProperty.scala b/common/src/test/scala/sigmastate/VersionTestingProperty.scala new file mode 100644 index 0000000000..f6d8b6c400 --- /dev/null +++ b/common/src/test/scala/sigmastate/VersionTestingProperty.scala @@ -0,0 +1,26 @@ +package sigmastate + +import org.scalactic.source.Position +import org.scalatest.{PropSpec, Tag} + +/** Decorator trait which allows to redefine `property` so that it is executed repeatedly for each valid + * [[VersionContext]], which is properly initialized. + * Thus, the properties can be versioned using `VersionContext.current`. + */ +trait VersionTestingProperty extends PropSpec with VersionTesting { + + /** Redefine `property` so that testFun is executed repeatedly for each valid + * [[VersionContext]] */ + override protected def property(testName: String, testTags: Tag*) + (testFun: => Any) + (implicit pos: Position): Unit = { + super.property(testName, testTags:_*) { + forEachScriptAndErgoTreeVersion(activatedVersions, ergoTreeVersions) { + VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + testFun_Run(testName, testFun) + } + } + } + } + +} diff --git a/core/src/main/scala/scalan/TypeDescs.scala b/core/src/main/scala/scalan/TypeDescs.scala index 5f3a4536bb..6ade17a110 100644 --- a/core/src/main/scala/scalan/TypeDescs.scala +++ b/core/src/main/scala/scalan/TypeDescs.scala @@ -55,7 +55,7 @@ abstract class TypeDescs extends Base { self: Scalan => case class RMethodDesc(method: Method) extends MethodDesc case class WMethodDesc(wrapSpec: WrapSpec, method: Method) extends MethodDesc -// TODO benchmark this version agains the version below +// TODO optimize: benchmark this version agains the version below // def getSourceValues(dataEnv: DataEnv, forWrapper: Boolean, stagedValue: AnyRef, out: DBuffer[AnyRef]): Unit = { // import OverloadHack._ // stagedValue match { @@ -154,7 +154,7 @@ abstract class TypeDescs extends Base { self: Scalan => protected def collectMethods: Map[Method, MethodDesc] = Map() // TODO optimize: all implementations protected lazy val methods: Map[Method, MethodDesc] = collectMethods - // TODO benchamrk against the version below it + // TODO optimize: benchamrk against the version below it // def invokeUnlifted(mc: MethodCall, dataEnv: DataEnv): AnyRef = { // val srcArgs = DBuffer.ofSize[AnyRef](mc.args.length + 10) // with some spare space to have only single allocation // val res = methods.get(mc.method) match { @@ -244,7 +244,6 @@ abstract class TypeDescs extends Base { self: Scalan => m.getName } - // TODO optimize /** Build a mapping between methods of staged class and the corresponding methods of source class. * The methods are related using names. * The computed mapping can be used to project MethodCalls IR nodes back to the corresponding diff --git a/docs/LangSpec.md b/docs/LangSpec.md index 30eb450afc..15ff4ea31c 100644 --- a/docs/LangSpec.md +++ b/docs/LangSpec.md @@ -426,7 +426,6 @@ The following syntax is supported to access registers on box objects: ``` box.R3[Int].get // access R3 register, check that its value of type Int and return it box.R3[Int].isDefined // check that value of R3 is defined and has type Int -box.R3[Int].isEmpty // check that value of R3 is undefined box.R3[Int].getOrElse(d) // access R3 value if defined, otherwise return `d` ``` @@ -933,13 +932,19 @@ def proveDHTuple(g: GroupElement, h: GroupElement, */ def proveDlog(value: GroupElement): SigmaProp -/** Transforms Base58 encoded string litereal into constant of type Coll[Byte]. +/** Transforms Base16 encoded string literal into constant of type Coll[Byte]. + * It is a compile-time operation and only string literal (constant) can be its + * argument. + */ +def fromBase16(input: String): Coll[Byte] + +/** Transforms Base58 encoded string literal into constant of type Coll[Byte]. * It is a compile-time operation and only string literal (constant) can be its * argument. */ def fromBase58(input: String): Coll[Byte] -/** Transforms Base64 encoded string litereal into constant of type Coll[Byte]. +/** Transforms Base64 encoded string literal into constant of type Coll[Byte]. * It is a compile-time operation and only string literal (constant) can be its * argument. */ diff --git a/docs/aot-jit-switch.md b/docs/aot-jit-switch.md index 7bed588428..6a08e0248c 100644 --- a/docs/aot-jit-switch.md +++ b/docs/aot-jit-switch.md @@ -1,7 +1,7 @@ ## The Protocol and Requirements to replace AOT with JIT via soft-fork The goal of this document is to specify requirements for v4.0, v5.0 and upcoming releases. -It also specifies rules of transaction validation in the Ergo network with respect of +It also specifies rules of ErgoTree validation in the Ergo network with respect of soft-fork activation which should be followed by different versions of nodes. The v4.x -> v5.x soft-fork is motivated by the goal of switching from AOT to JIT-based costing algorithm and the simplified ErgoTree interpreter. @@ -12,79 +12,83 @@ necessary. Term | Description -----------------|------------ - _ScriptV1_ | The current version of ErgoTree (3.x releases) used in ErgoBlock v1. Bits 0-2 == 0 in the ErgoTree header byte. (see ErgoTree class). - _ScriptV2_ | The next version of ErgoTree (5.x releases) used after SF is activated. Bits 0-2 == 1 in the ErgoTree header byte. (see ErgoTree class). + _Script v0/v1_ | The current version of ErgoTree (3.x/4.x releases) used in ErgoBlock v1/v2. Bits `0-2 == 0 or 1` in the ErgoTree header byte. (see ErgoTree class). + _Script v2_ | The next version of ErgoTree (5.x releases) used after Ergo protocol v3 is activated. Bits 0-2 == 2 in the ErgoTree header byte. (see ErgoTree class). + _Script v3_ | Version of ErgoTree which corresponds to Ergo protocol v4 (i.e. v6.x releases) R4.0-AOT-cost | cost estimation using v4.0 Ahead-Of-Time costing implementation R4.0-AOT-verify | spending condition verification using v4.0 Ahead-Of-Time interpreter implementation R5.0-JIT-verify | spending condition verification using v5.0 simplified interpreter with Just-In-Time costing of fullReduction and AOT sigma protocol costing. skip-pool-tx | skip pool transaction when building a new block candidate skip-accept | skip script evaluation (both costing and verification) and treat it as True proposition (accept spending) skip-reject | skip script evaluation (both costing and verification) and treat it as False proposition (reject spending) - Validation Context | a tuple of (`Block Type`, `SF Status`, `Script Version`) + Validation Context | a tuple of (`BlockVer`, `Block Type`, `Script Version`) Validation Action | an action taken by a node in the given validation context SF Status | soft-fork status of the block. The status is `active` when enough votes have been collected. ### Script Validation Rules Summary Validation of scripts in blocks is defined for each release and depend on _validation -context_ which includes type of block, soft-fork status and script version. We denote -blocks being created by miners as `candidate` and those distributed across network as -`mined`. +context_ which includes BlockVersion, type of block and script version in ErgoTree header. +We denote blocks being created by miners as `candidate` and those distributed across +network as `mined`. -Thus, we have 8 different validation contexts multiplied by 2 node versions -having in total 16 validation rules as summarized in the following table, which +Thus, we have 12 different validation contexts multiplied by 2 node versions (v4.x, v5.x) +having in total 24 validation rules as summarized in the following table, which specifies the _validation action_ a node have to take in the given contexts. -Rule#| SF Status| Block Type| Script Version | Release | Validation Action +Rule#| BlockVer | Block Type| Script Version | Release | Validation Action -----|----------|-----------|----------------|---------|-------- -1 | inactive | candidate | Script v1 | v4.0 | R4.0-AOT-cost, R4.0-AOT-verify -2 | inactive | candidate | Script v1 | v5.0 | R4.0-AOT-cost, R4.0-AOT-verify -3 | inactive | candidate | Script v2 | v4.0 | skip-pool-tx (cannot handle) -4 | inactive | candidate | Script v2 | v5.0 | skip-pool-tx (wait activation) +1 | 1,2 | candidate | Script v0/v1 | v4.0 | R4.0-AOT-cost, R4.0-AOT-verify +2 | 1,2 | candidate | Script v0/v1 | v5.0 | R4.0-AOT-cost, R4.0-AOT-verify +3 | 1,2 | candidate | Script v2 | v4.0 | skip-pool-tx (cannot handle) +4 | 1,2 | candidate | Script v2 | v5.0 | skip-pool-tx (wait activation) |||| -5 | inactive | mined | Script v1 | v4.0 | R4.0-AOT-cost, R4.0-AOT-verify -6 | inactive | mined | Script v1 | v5.0 | R4.0-AOT-cost, R4.0-AOT-verify -7 | inactive | mined | Script v2 | v4.0 | skip-reject (cannot handle) -8 | inactive | mined | Script v2 | v5.0 | skip-reject (wait activation) +5 | 1,2 | mined | Script v0/v1 | v4.0 | R4.0-AOT-cost, R4.0-AOT-verify +6 | 1,2 | mined | Script v0/v1 | v5.0 | R4.0-AOT-cost, R4.0-AOT-verify +7 | 1,2 | mined | Script v2 | v4.0 | skip-reject (cannot handle) +8 | 1,2 | mined | Script v2 | v5.0 | skip-reject (wait activation) |||| -9 | active | candidate | Script v1 | v4.0 | R4.0-AOT-cost, R4.0-AOT-verify -10 | active | candidate | Script v1 | v5.0 | R5.0-JIT-verify -11 | active | candidate | Script v2 | v4.0 | skip-pool-tx (cannot handle) -12 | active | candidate | Script v2 | v5.0 | R5.0-JIT-verify +9 | 3 | candidate | Script v0/v1 | v4.0 | skip-pool-tx (cannot handle) +10 | 3 | candidate | Script v0/v1 | v5.0 | R5.0-JIT-verify +11 | 3 | candidate | Script v2 | v4.0 | skip-pool-tx (cannot handle) +12 | 3 | candidate | Script v2 | v5.0 | R5.0-JIT-verify |||| -13 | active | mined | Script v1 | v4.0 | skip-accept (rely on majority) -14 | active | mined | Script v1 | v5.0 | R5.0-JIT-verify -15 | active | mined | Script v2 | v4.0 | skip-accept (rely on majority) -16 | active | mined | Script v2 | v5.0 | R5.0-JIT-verify +13 | 3 | mined | Script v0/v1 | v4.0 | skip-accept (rely on majority) +14 | 3 | mined | Script v0/v1 | v5.0 | R5.0-JIT-verify +15 | 3 | mined | Script v2 | v4.0 | skip-accept (rely on majority) +16 | 3 | mined | Script v2 | v5.0 | R5.0-JIT-verify +|||| +17 | 3 | candidate | Script v3 | v5.0 | skip-reject (cannot handle) +18 | 3 | mined | Script v3 | v5.0 | skip-reject (cannot handle) +|||| +19 | 4 | candidate | Script v3 | v5.0 | skip-accept (rely on majority) +20 | 4 | mined | Script v3 | v5.0 | skip-accept (rely on majority) +|||| +21 | 4 | candidate | Script v0/v1 | v5.0 | R5.0-JIT-verify +22 | 4 | candidate | Script v2 | v5.0 | R5.0-JIT-verify +23 | 4 | mined | Script v0/v1 | v5.0 | R5.0-JIT-verify +24 | 4 | mined | Script v2 | v5.0 | R5.0-JIT-verify Note the following properties of the validation rules. 0. Please note that block creation is not a part of the Ergo consensus protocol, and -miners can do whatever they want, in particulare completely custom block assembly. For +miners can do whatever they want i.e. a completely custom block assembly. For this reason, the rules for `candidate` blocks are _reference implementation_ only, and nodes are not required to follow them exactly. 1. Rules 1-4 specify creation of new candidate blocks _before_ soft-fork is activated. They require that the behaviour of v4.0 and v5.0 nodes should be identical. -2. Rules 9-10 specify creation of new candidate blocks _after_ soft-fork is activated. +2. Rules 9-10, 13-14 specify validation of blocks _after_ soft-fork is activated. They are different for v4.0 and v5.0 nodes, but [equivalent](#equivalence-properties-of-validation-actions) with respect to preserving consensus (see also [Rule Descriptions](#rule-descriptions) for details). -3. For any given tuple (`SF Status`, `Script Version`, `Release`) the _equivalent_ `ValidationAction` is +3. For any given tuple (`BlockVer`, `Script Version`, `Release`) the _equivalent_ `ValidationAction` is applied for both `candidate` and `mined` blocks. This proves the consistency of the rules -with respect to the change of the block status from `candidate` to `mined`, both before -and after soft-fork activation. - -4. Each rule `i`, where `i` is an odd number, defines a `Validation Action` performed by -a v4.0 node. Each such rule is paired with the `i+1` rule which defines `Validation Action` -performed by a v5.0 node. Any such a pair `(i, i+1)` of rules have `Validation Actions` -which are either the same or equivalent with respect to the [Equivalence Properties of -Validation Actions](#equivalence-properties-of-validation-actions). This proves -consistency of validation actions across v4.0 and v5.0 nodes. +with respect to the change of the block status from `candidate` to `mined` for any Block Version. -5. After SF is activated (`SF Status == active`), both AOT-cost and AOT-verify +4. After Block Version v3 is activated (`BlockVer == 3`), both AOT-cost and AOT-verify implementations are no longer used in `Validation Action` of v5.0 nodes. The only context where v5.0 node needs to use AOT based verification is given by Rule 6, which is to verify a v1 script in a historical mined block before SF is activated. @@ -93,7 +97,7 @@ Rule 6 in a new v5.0.1 release with the following _equivalent_ rule Rule#| SF Status | Block Type| Script Version | Release | Validation Action -----|-----------|-----------|----------------|---------|-------- -17 | inactive | mined | Script v1 | v5.0.1 | R5.0-JIT-verify +6 | inactive | mined | Script v0/v1 | v5.0.1 | R5.0-JIT-verify This will allow to remove AOT implementation in v5.0.1 and simplify reference implementation significantly. @@ -127,7 +131,7 @@ Similar to rules 3 and 4. These rules allow v1 scripts to enter blockchain even after SF is activated (for backward compatibility with applications). Now, after SF is activated, the majority consist of v5.0 nodes and they will do -`R5.0-JIT-verify(Script v1)` which is equivalent to `R4.0-AOT-verify(Script v1)` due to +`R5.0-JIT-verify(Script v0/v1)` which is equivalent to `R4.0-AOT-verify(Script v0/v1)` due to _Prop3_. To understand this pair of rules it is important to remember that a specific version of @@ -138,7 +142,7 @@ property [Prop 3](#equivalence-properties-of-validation-actions). However, for backward compatibility with applications we DON'T NEED equivalence of costing, hence exact cost estimation is not necessary. For this reason we have the relaxed condition in -_Prop4_, which means that any ScriptV1 admitted by `R4.0-AOT-cost` will also be admitted by +_Prop4_, which means that any ScriptV0/V1 admitted by `R4.0-AOT-cost` will also be admitted by `R5.0-JIT-cost`. For this reason, the v4.0 based application interacting with v5.0 node will not notice the difference. @@ -169,13 +173,13 @@ In order to guarantee network consensus in the presence of both v4.0 and v5.0 no the implementation of `R4.0-AOT-verify`, `R5.0-AOT-verify`, `R4.0-AOT-cost`, `R5.0-AOT-cost`, `R5.0-JIT-verify` should satisfy the following properties. -_Prop 1._ AOT-verify is preserved: `forall s:ScriptV1, R4.0-AOT-verify(s) == R5.0-AOT-verify(s)` +_Prop 1._ AOT-verify is preserved: `forall s:ScriptV0/V1, R4.0-AOT-verify(s) == R5.0-AOT-verify(s)` -_Prop 2._ AOT-cost is preserved: `forall s:ScriptV1, R4.0-AOT-cost(s) == R5.0-AOT-cost(s)` +_Prop 2._ AOT-cost is preserved: `forall s:ScriptV0/V1, R4.0-AOT-cost(s) == R5.0-AOT-cost(s)` -_Prop 3._ JIT-verify can replace AOT-verify: `forall s:ScriptV1, R5.0-JIT-verify(s) == R4.0-AOT-verify(s)` +_Prop 3._ JIT-verify can replace AOT-verify: `forall s:ScriptV0/V1, R5.0-JIT-verify(s) == R4.0-AOT-verify(s)` -_Prop 4._ JIT-cost is bound by AOT-cost: `forall s:ScriptV1, R5.0-JIT-cost(s) <= R4.0-AOT-cost(s)` +_Prop 4._ JIT-cost is bound by AOT-cost: `forall s:ScriptV0/V1, R5.0-JIT-cost(s) <= R4.0-AOT-cost(s)` _Prop 5._ ScriptV2 is rejected before SF is active: `forall s:ScriptV2, if not SF is active => R4.0-verify(s) == R5.0-verify(s) == Reject` diff --git a/library-impl/src/main/scala/special/collection/CollsOverArrays.scala b/library-impl/src/main/scala/special/collection/CollsOverArrays.scala index ad78776d4f..e9c7e12912 100644 --- a/library-impl/src/main/scala/special/collection/CollsOverArrays.scala +++ b/library-impl/src/main/scala/special/collection/CollsOverArrays.scala @@ -2,21 +2,25 @@ package special.collection import java.util import java.util.Objects - import special.SpecialPredef import scala.reflect.ClassTag import scalan._ import scalan.util.CollectionUtil -import scalan.{Internal, NeverInline, Reified, RType} +import scalan.{Internal, NeverInline, RType, Reified} import Helpers._ import debox.Buffer import scalan.RType._ +import sigmastate.VersionContext +import sigmastate.util.{MaxArrayLength, safeConcatArrays_v5, safeNewArray} import spire.syntax.all._ import scala.runtime.RichInt class CollOverArray[@specialized A](val toArray: Array[A])(implicit tA: RType[A]) extends Coll[A] { + require(toArray.length <= MaxArrayLength, + s"Cannot create collection with size ${toArray.length} greater than $MaxArrayLength") + @Internal override def tItem: RType[A] = tA def builder: CollBuilder = new CollOverArrayBuilder @@ -56,7 +60,12 @@ class CollOverArray[@specialized A](val toArray: Array[A])(implicit tA: RType[A] @NeverInline def append(other: Coll[A]): Coll[A] = { if (toArray.length <= 0) return other - val result = CollectionUtil.concatArrays(toArray, other.toArray) + val result = if (VersionContext.current.isJitActivated) { + // in v5.0 and above this fixes the ClassCastException problem + safeConcatArrays_v5(toArray, other.toArray)(tA.classTag) + } else { + CollectionUtil.concatArrays(toArray, other.toArray) + } builder.fromArray(result) } @@ -93,7 +102,7 @@ class CollOverArray[@specialized A](val toArray: Array[A])(implicit tA: RType[A] if (n <= 0) builder.emptyColl else if (n >= length) this else { - val res = Array.ofDim[A](n) + val res = new Array[A](n) Array.copy(toArray, 0, res, 0, n) builder.fromArray(res) } @@ -211,12 +220,25 @@ class CollOverArrayBuilder extends CollBuilder { override def Monoids: MonoidBuilder = new MonoidBuilderInst @inline override def pairColl[@specialized A, @specialized B](as: Coll[A], bs: Coll[B]): PairColl[A, B] = { - // TODO HF (2h): use minimal length and slice longer collection - // The current implementation doesn't check the case when `as` and `bs` have different lengths. - // in which case the implementation of `PairOfCols` has inconsistent semantics of `map`, `exists` etc methods. - // To fix the problem, the longer collection have to be truncated (which is consistent - // with how zip is implemented for Arrays) - new PairOfCols(as, bs) + if (VersionContext.current.isJitActivated) { + // v5.0 and above + val asLen = as.length + val bsLen = bs.length + // if necessary, use minimal length and slice longer collection + if (asLen == bsLen) + new PairOfCols(as, bs) + else if (asLen < bsLen) { + new PairOfCols(as, bs.slice(0, asLen)) + } else { + new PairOfCols(as.slice(0, bsLen), bs) + } + } else { + // The v4.x implementation doesn't check the case when `as` and `bs` have different lengths. + // In which case appending two `PairOfCols` leads to invalid pairing of elements. + // To fix the problem, the longer collection have to be truncated (which is consistent + // with how zip is implemented for Arrays) + new PairOfCols(as, bs) + } } @Internal @@ -271,7 +293,9 @@ class CollOverArrayBuilder extends CollBuilder { } @NeverInline - override def makeView[@specialized A, @specialized B: RType](source: Coll[A], f: A => B): Coll[B] = new CViewColl(source, f) + override def makeView[@specialized A, @specialized B: RType](source: Coll[A], f: A => B): Coll[B] = { + new CViewColl(source, f) + } @NeverInline override def makePartialView[@specialized A, @specialized B: RType](source: Coll[A], f: A => B, calculated: Array[Boolean], calculatedItems: Array[B]): Coll[B] = { @@ -285,8 +309,8 @@ class CollOverArrayBuilder extends CollBuilder { val limit = xs.length implicit val tA = xs.tItem.tFst implicit val tB = xs.tItem.tSnd - val ls = Array.ofDim[A](limit) - val rs = Array.ofDim[B](limit) + val ls = Array.ofDim[A](limit)(tA.classTag) + val rs = Array.ofDim[B](limit)(tB.classTag) cfor(0)(_ < limit, _ + 1) { i => val p = xs(i) ls(i) = p._1 @@ -296,7 +320,9 @@ class CollOverArrayBuilder extends CollBuilder { } @NeverInline - override def xor(left: Coll[Byte], right: Coll[Byte]): Coll[Byte] = left.zip(right).map { case (l, r) => (l ^ r).toByte } + override def xor(left: Coll[Byte], right: Coll[Byte]): Coll[Byte] = { + left.zip(right).map { case (l, r) => (l ^ r).toByte } + } @NeverInline override def emptyColl[T](implicit cT: RType[T]): Coll[T] = cT match { @@ -578,6 +604,9 @@ class PairOfCols[@specialized L, @specialized R](val ls: Coll[L], val rs: Coll[R } class CReplColl[@specialized A](val value: A, val length: Int)(implicit tA: RType[A]) extends ReplColl[A] { + require(length <= MaxArrayLength, + s"Cannot create CReplColl with size ${length} greater than $MaxArrayLength") + @Internal override def tItem: RType[A] = tA @@ -586,7 +615,7 @@ class CReplColl[@specialized A](val value: A, val length: Int)(implicit tA: RTyp @Internal lazy val _toArray: Array[A] = { implicit val cT: ClassTag[A] = tA.classTag - val res = new Array[A](length) + val res = safeNewArray[A](length) val v = value cfor(0)(_ < length, _ + 1) { i => res(i) = v } res diff --git a/library-impl/src/main/scala/special/collection/Helpers.scala b/library-impl/src/main/scala/special/collection/Helpers.scala index e4a4d93ea5..2d65189de9 100644 --- a/library-impl/src/main/scala/special/collection/Helpers.scala +++ b/library-impl/src/main/scala/special/collection/Helpers.scala @@ -22,7 +22,7 @@ object Helpers { def mapReduce[A, K: ClassTag, V: ClassTag](arr: Array[A], m: A => (K, V), r: ((V, V)) => V): (Array[K], Array[V]) = { val keyPositions = new java.util.HashMap[K, Int](32) val keys = mutable.ArrayBuilder.make[K] - val values = Array.ofDim[V](arr.length) + val values = new Array[V](arr.length) var i = 0 var nValues = 0 while (i < arr.length) { @@ -42,7 +42,7 @@ object Helpers { } i += 1 } - val resValues = Array.ofDim[V](nValues) + val resValues = new Array[V](nValues) Array.copy(values, 0, resValues, 0, nValues) (keys.result(), resValues) } diff --git a/library/src/test/scala/special/collections/CollsTests.scala b/library/src/test/scala/special/collections/CollsTests.scala index ba3ddd1210..88220692d3 100644 --- a/library/src/test/scala/special/collections/CollsTests.scala +++ b/library/src/test/scala/special/collections/CollsTests.scala @@ -1,15 +1,21 @@ package special.collections -import scala.language.{existentials,implicitConversions} -import special.collection.{Coll, PairOfCols, CollOverArray, CReplColl} import org.scalacheck.Gen -import org.scalatest.{PropSpec, Matchers} +import org.scalatest.exceptions.TestFailedException import org.scalatest.prop.PropertyChecks +import org.scalatest.{Matchers, PropSpec} +import scalan.RType +import sigmastate.{VersionContext, VersionTestingProperty} +import special.collection.{CReplColl, Coll, CollOverArray, PairOfCols} -class CollsTests extends PropSpec with PropertyChecks with Matchers with CollGens { testSuite => +import scala.language.{existentials, implicitConversions} + +class CollsTests extends PropSpec with PropertyChecks with Matchers with CollGens with VersionTestingProperty { testSuite => import Gen._ import special.collection.ExtensionMethods._ + def squared[A](f: A => A): ((A, A)) => (A, A) = (p: (A, A)) => (f(p._1), f(p._2)) + property("Coll.indices") { val minSuccess = MinSuccessful(30) forAll(collGen, collGen, minSuccess) { (col1: Coll[Int], col2: Coll[Int]) => @@ -20,8 +26,62 @@ class CollsTests extends PropSpec with PropertyChecks with Matchers with CollGen } } - // TODO col1.zip(col2).length shouldBe col1.arr.zip(col2.arr).length - property("Coll.zip") { + property("Coll.length") { + def equalLength[A: RType](xs: Coll[A]) = { + val arr = xs.toArray + xs.length shouldBe arr.length + xs.zip(xs).length shouldBe arr.zip(arr).length + xs.zip(xs.append(xs)).length shouldBe arr.zip(arr ++ arr).length + xs.append(xs).zip(xs).length shouldBe (arr ++ arr).zip(arr).length + } + + def equalLengthMapped[A: RType](xs: Coll[A], f: A => A) = { + val arr = xs.toArray + val ys = xs.map(f) + ys.length shouldBe xs.length + ys.length shouldBe arr.map(f).length + + equalLength(ys) + equalLength(xs.append(ys)) + equalLength(ys.append(xs)) + } + + // make sure forall: T, col: Coll[T] => col.length shouldBe col.toArray.length + // The above equality should hold for all possible collection instances + + forAll(MinSuccessful(100)) { xs: Coll[Int] => + val arr = xs.toArray + equalLength(xs) + equalLengthMapped(xs, inc) + + equalLength(xs.append(xs)) + equalLengthMapped(xs.append(xs), inc) + + val pairs = xs.zip(xs) + equalLength(pairs) + + if (!xs.isInstanceOf[CReplColl[_]] && !VersionContext.current.isJitActivated) { + an[ClassCastException] should be thrownBy { + equalLengthMapped(pairs, squared(inc)) // due to problem with append + } + } + VersionContext.withVersions(VersionContext.JitActivationVersion, VersionContext.JitActivationVersion) { +// TODO v5.0: make it work +// equalLengthMapped(pairs, squared(inc)) // problem fixed in v5.0 + } + + equalLength(pairs.append(pairs)) + + if (!xs.isInstanceOf[CReplColl[_]] && !VersionContext.current.isJitActivated) { + an[ClassCastException] should be thrownBy { + equalLengthMapped(pairs.append(pairs), squared(inc)) // due to problem with append + } + } + VersionContext.withVersions(VersionContext.JitActivationVersion, VersionContext.JitActivationVersion) { +// TODO v5.0: make it work +// equalLengthMapped(pairs.append(pairs), squared(inc)) // problem fixed in v5.0 + } + } } property("Coll.flatMap") { @@ -198,6 +258,61 @@ class CollsTests extends PropSpec with PropertyChecks with Matchers with CollGen } property("Coll.append") { + + { // this shows how the problem with CollectionUtil.concatArrays manifests itself in Coll.append (v4.x) + val xs = builder.fromItems(1, 2) + val pairs = xs.zip(xs) + val ys = pairs.map(squared(inc)) // this map transforms PairOfCols to CollOverArray + + if (VersionContext.current.isJitActivated) { + // problem fixed in v5.0 + ys.append(ys).toArray shouldBe ys.toArray ++ ys.toArray + } else { + // due to the problem with CollectionUtil.concatArrays + an[ClassCastException] should be thrownBy (ys.append(ys)) + } + } + + { + val col1 = builder.fromItems(1, 2, 3) + val col2 = builder.fromItems(10, 20, 30, 40) + val pairs = col1.zip(col2) + val pairsSwap = col2.zip(col1) + assert(pairs.isInstanceOf[PairOfCols[_,_]]) + + val pairsArr = pairs.toArray + pairsArr shouldBe Array((1, 10), (2, 20), (3, 30)) + + val pairsArrSwap = pairsSwap.toArray + pairsArr shouldBe Array((1, 10), (2, 20), (3, 30)) + pairsArrSwap shouldBe Array((10, 1), (20, 2), (30, 3)) + + val appended = pairs.append(pairs) + val appendedSwap = pairsSwap.append(pairsSwap) + + // here is the problem with append which is fixed in v5.0 + if (VersionContext.current.isJitActivated) { + // the issue is fixed starting from v5.0 + appended.toArray shouldBe (pairsArr ++ pairsArr) + appended.toArray shouldBe Array((1, 10), (2, 20), (3, 30), (1, 10), (2, 20), (3, 30)) + appendedSwap.toArray shouldBe (pairsArrSwap ++ pairsArrSwap) + appendedSwap.toArray shouldBe Array((10, 1), (20, 2), (30, 3), (10, 1), (20, 2), (30, 3)) + } else { + // Append should fail for ErgoTree v0, v1 regardless of the activated version + an[TestFailedException] should be thrownBy( + appended.toArray shouldBe (pairsArr ++ pairsArr) + ) + an[TestFailedException] should be thrownBy( + appendedSwap.toArray shouldBe (pairsArrSwap ++ pairsArrSwap) + ) + // Note, the last element of col2 (40) is zipped with 1 + // this is because PairOfCols keeps ls and rs separated and zip doesn't do truncation + // the they are of different length + appended.toArray shouldBe Array((1, 10), (2, 20), (3, 30), (1, 40), (2, 10), (3, 20)) + appendedSwap.toArray shouldBe Array((10, 1), (20, 2), (30, 3), (40, 1), (10, 2), (20, 3)) + } + } + forAll(collGen, collGen, valGen, MinSuccessful(50)) { (col1, col2, v) => { @@ -212,13 +327,18 @@ class CollsTests extends PropSpec with PropertyChecks with Matchers with CollGen { val repl1 = builder.replicate(col1.length, v) val repl2 = builder.replicate(col2.length, v) + assert(repl1.isInstanceOf[CReplColl[Int]]) + val arepl = repl1.append(repl2) assert(arepl.isInstanceOf[CReplColl[Int]]) arepl.toArray shouldBe (repl1.toArray ++ repl2.toArray) val pairs1 = repl1.zip(repl1) + assert(pairs1.isInstanceOf[PairOfCols[Int, Int]]) + val pairs2 = repl2.zip(repl2) val apairs = pairs1.append(pairs2) + assert(apairs.isInstanceOf[PairOfCols[Int, Int]]) apairs.toArray shouldBe (pairs1.toArray ++ pairs2.toArray) apairs match { @@ -232,6 +352,26 @@ class CollsTests extends PropSpec with PropertyChecks with Matchers with CollGen } } + property("Coll.zip") { + val col1 = builder.fromItems(1, 2, 3) + val col2 = builder.fromItems(10, 20, 30, 40) + val pairs = col1.zip(col2) + pairs.length shouldBe col1.length + pairs.length shouldNot be(col2.length) + pairs.length shouldBe col2.zip(col1).length + + val pairOfColls = pairs.asInstanceOf[PairOfCols[Int, Int]] + + // here is problem with zip + if (VersionContext.current.isJitActivated) { + // which is fixed in v5.0 + pairOfColls.ls.length shouldBe pairOfColls.rs.length + } else { + // not fixed in v4.x + pairOfColls.ls.length should not be(pairOfColls.rs.length) + } + } + property("Coll.mapReduce") { import scalan.util.CollectionUtil.TraversableOps def m(x: Int) = (math.abs(x) % 10, x) @@ -266,10 +406,6 @@ class CollsTests extends PropSpec with PropertyChecks with Matchers with CollGen res.toArray shouldBe col.toArray.reverse val pairs = col.zip(col) pairs.reverse.toArray shouldBe pairs.toArray.reverse -// TODO should work -// val c1 = col.asInstanceOf[Coll[Any]] -// val appended = c1.append(c1) -// appended.toArray shouldBe (c1.toArray ++ c1.toArray) } } @@ -585,13 +721,6 @@ class CollsTests extends PropSpec with PropertyChecks with Matchers with CollGen assert(c2.x == c1.x) // println(s"c1=$c1; c2=$c2") } - -// TODO the following test fails because equality of Seq is not deep, and nested arrays are shallow compared -// forAll(tokensGen, minSuccess) { c1 => -// println(s"c1=${c1.x.toArray.toSeq.map { case (id, v) => (id.toSeq, v) }}") -// val tokens = c1.x -// assert(tokens.toArray.toSeq == tokensArr.toSeq) -// } } } diff --git a/sigma-api/src/main/scala/special/sigma/SigmaDsl.scala b/sigma-api/src/main/scala/special/sigma/SigmaDsl.scala index 7a5776cc22..37c86d9049 100644 --- a/sigma-api/src/main/scala/special/sigma/SigmaDsl.scala +++ b/sigma-api/src/main/scala/special/sigma/SigmaDsl.scala @@ -776,6 +776,10 @@ trait Context { /** Maximum version of ErgoTree currently activated on the network. * See [[ErgoLikeContext]] class for details. */ def activatedScriptVersion: Byte + + /** The version of ErgoTree currently executed by interpreter. + * See [[ErgoLikeContext]] class for details. */ + def currentErgoTreeVersion: Byte } @scalan.Liftable diff --git a/sigma-impl/src/main/scala/special/sigma/SigmaDslOverArrays.scala b/sigma-impl/src/main/scala/special/sigma/SigmaDslOverArrays.scala index d1180960a7..26252bae03 100644 --- a/sigma-impl/src/main/scala/special/sigma/SigmaDslOverArrays.scala +++ b/sigma-impl/src/main/scala/special/sigma/SigmaDslOverArrays.scala @@ -1,17 +1,18 @@ package special.sigma import java.math.BigInteger - import com.google.common.primitives.Longs import org.bouncycastle.crypto.ec.CustomNamedCurves import org.bouncycastle.math.ec.ECPoint import org.bouncycastle.math.ec.custom.sec.SecP256K1Point import scalan.RType._ -import scalan.{RType, Internal, NeverInline, Reified} -import scorex.crypto.hash.{Sha256, Blake2b256} +import scalan.{NeverInline, Internal, Reified} +import scorex.crypto.hash.{Blake2b256, Sha256} import special.SpecialPredef import special.collection._ import scalan.util.Extensions.BigIntegerOps +import sigmastate.VersionContext +import spire.syntax.all.cfor // TODO refactor: this class is not necessary and can be removed class TestSigmaDslBuilder extends SigmaDslBuilder { @@ -40,8 +41,21 @@ class TestSigmaDslBuilder extends SigmaDslBuilder { @NeverInline override def xorOf(conditions: Coll[Boolean]): Boolean = { - // TODO HF (2h): see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/640 - conditions.toArray.distinct.length == 2 + if (VersionContext.current.isJitActivated) { + val len = conditions.length + if (len == 0) false + else if (len == 1) conditions(0) + else { + var res = conditions(0) + cfor(1)(_ < len, _ + 1) { i => + res ^= conditions(i) + } + res + } + } else { + // This is buggy version used in v4.x interpreter (for ErgoTrees v0, v1) + conditions.toArray.distinct.length == 2 + } } @NeverInline diff --git a/sigmastate/src/main/scala/org/ergoplatform/ErgoBox.scala b/sigmastate/src/main/scala/org/ergoplatform/ErgoBox.scala index 72c2736841..e097a6302f 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/ErgoBox.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/ErgoBox.scala @@ -1,10 +1,10 @@ package org.ergoplatform -import com.google.common.primitives.Shorts -import org.ergoplatform.ErgoBox.{TokenId, NonMandatoryRegisterId} +import com.google.common.primitives.{Ints, Shorts} +import org.ergoplatform.ErgoBox.{NonMandatoryRegisterId, TokenId} import org.ergoplatform.settings.ErgoAlgos import scorex.crypto.authds.ADKey -import scorex.crypto.hash.{Digest32, Blake2b256} +import scorex.crypto.hash.{Blake2b256, Digest32} import scorex.util._ import sigmastate.SCollection.SByteArray import sigmastate.SType.AnyOps @@ -72,6 +72,7 @@ class ErgoBox( } } + // TODO optimize: avoid serialization by implementing lazy box deserialization /** Serialized content of this box. * @see [[ErgoBox.sigmaSerializer]] */ @@ -82,9 +83,7 @@ class ErgoBox( case _ => false } - // TODO refactor: fix hashCode, it should be consistent with [[equals]] and should be based on [[id]] - override def hashCode(): Int = - ScalaRunTime._hashCode((value, ergoTree, additionalTokens, additionalRegisters, index, creationHeight)) + override def hashCode(): Int = Ints.fromByteArray(id) /** Convert this box to [[ErgoBoxCandidate]] by forgetting transaction reference data * (transactionId, index). diff --git a/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala b/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala index 894dcd3bbf..1b7638f233 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala @@ -1,7 +1,6 @@ package org.ergoplatform import java.util - import org.ergoplatform.ErgoBox._ import org.ergoplatform.settings.ErgoAlgos import scorex.util.{bytesToId, ModifierId} @@ -14,6 +13,7 @@ import special.collection.Coll import sigmastate.eval._ import sigmastate.eval.Extensions._ import sigmastate.serialization.ErgoTreeSerializer.DefaultSerializer +import sigmastate.util.safeNewArray import spire.syntax.all.cfor import scala.collection.immutable @@ -50,7 +50,7 @@ class ErgoBoxCandidate(val value: Long, lazy val propositionBytes: Array[Byte] = ergoTree.bytes /** Serialized bytes of this Box without transaction reference data (transactionId and boxIndex). */ - // TODO v5.0: re-implement extracting directly from `ErgoBox.bytes` array + // TODO optimize: re-implement extracting directly from `ErgoBox.bytes` array lazy val bytesWithNoRef: Array[Byte] = ErgoBoxCandidate.serializer.toBytes(this) /** Creates a new [[ErgoBox]] based on this candidate using the given transaction reference data. @@ -71,7 +71,9 @@ class ErgoBoxCandidate(val value: Long, case ValueRegId => Some(LongConstant(value)) case ScriptRegId => Some(ByteArrayConstant(propositionBytes)) case TokensRegId => - Some(Constant(additionalTokens.map { case (id, v) => (id.toColl, v) }.asWrappedType, STokensRegType)) // TODO optimize using mapFirst + // TODO optimize using mapFirst + // However this requires fixing Coll equality (see property("ErgoBox test vectors")) + Some(Constant(additionalTokens.map { case (id, v) => (id.toColl, v) }.asWrappedType, STokensRegType)) case ReferenceRegId => val tupleVal = (creationHeight, ErgoBoxCandidate.UndefinedBoxRef) Some(Constant(tupleVal.asWrappedType, SReferenceRegType)) @@ -185,14 +187,14 @@ object ErgoBoxCandidate { r.positionLimit = r.position + ErgoBox.MaxBoxSize val value = r.getULong() // READ val tree = DefaultSerializer.deserializeErgoTree(r, SigmaSerializer.MaxPropositionSize) // READ - val creationHeight = r.getUInt().toInt // READ + val creationHeight = r.getUIntExact // READ val nTokens = r.getUByte() // READ - val tokenIds = ValueSerializer.newArray[Array[Byte]](nTokens) - val tokenAmounts = ValueSerializer.newArray[Long](nTokens) + val tokenIds = safeNewArray[Array[Byte]](nTokens) + val tokenAmounts = safeNewArray[Long](nTokens) if (digestsInTx != null) { val nDigests = digestsInTx.length cfor(0)(_ < nTokens, _ + 1) { i => - val digestIndex = r.getUInt().toInt // READ + val digestIndex = r.getUIntExact // READ if (digestIndex < 0 || digestIndex >= nDigests) sys.error(s"failed to find token id with index $digestIndex") val amount = r.getULong() // READ diff --git a/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeContext.scala b/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeContext.scala index 33c1ea0a4a..593dcb987c 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeContext.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeContext.scala @@ -1,7 +1,5 @@ package org.ergoplatform -import java.util - import org.ergoplatform.validation.SigmaValidationSettings import sigmastate.SType._ import sigmastate.Values._ @@ -9,12 +7,13 @@ import sigmastate._ import sigmastate.eval.Extensions._ import sigmastate.eval._ import sigmastate.interpreter.ErgoTreeEvaluator.DataEnv -import sigmastate.interpreter.{ContextExtension, InterpreterContext, ErgoTreeEvaluator} +import sigmastate.interpreter.{Interpreter, InterpreterContext, ErgoTreeEvaluator, ContextExtension} +import sigmastate.lang.exceptions.InterpreterException import sigmastate.serialization.OpCodes import sigmastate.serialization.OpCodes.OpCode import special.collection.Coll import special.sigma -import special.sigma.{AnyValue, PreHeader, Header} +import special.sigma.{Header, PreHeader, AnyValue} import spire.syntax.all.cfor /** Represents a script evaluation context to be passed to a prover and a verifier to execute and @@ -71,34 +70,34 @@ class ErgoLikeContext(val lastBlockUtxoRoot: AvlTreeData, Examined ergo code: all that leads to ErgoLikeContext creation. Fixed some cases in ergo where PreHeader might be null. */ - assert(preHeader != null, "preHeader cannot be null") + require(preHeader != null, "preHeader cannot be null") /* NOHF PROOF: Added: assert(spendingTransaction != null) Motivation: to fail early Safety: According to ergo design spendingTransaction should always exist. Examined ergo code: all that leads to ErgoLikeContext creation. */ - assert(spendingTransaction != null, "spendingTransaction cannot be null") + require(spendingTransaction != null, "spendingTransaction cannot be null") /* NOHF PROOF: Added: assert that box with `selfIndex` exist in boxesToSpend Motivation: to fail early, rather than when going into evaluation Safety: ergo itself uses index to identify the box Examined ergo code: all that leads to ErgoLikeContext creation. */ - assert(boxesToSpend.isDefinedAt(selfIndex), s"Self box if defined should be among boxesToSpend") - assert(headers.toArray.headOption.forall(h => java.util.Arrays.equals(h.stateRoot.digest.toArray, lastBlockUtxoRoot.digest)), "Incorrect lastBlockUtxoRoot") + require(boxesToSpend.isDefinedAt(selfIndex), s"Self box if defined should be among boxesToSpend") + require(headers.toArray.headOption.forall(h => java.util.Arrays.equals(h.stateRoot.digest.toArray, lastBlockUtxoRoot.digest)), "Incorrect lastBlockUtxoRoot") cfor(0)(_ < headers.length, _ + 1) { i => - if (i > 0) assert(headers(i - 1).parentId == headers(i).id, s"Incorrect chain: ${headers(i - 1).parentId},${headers(i).id}") + if (i > 0) require(headers(i - 1).parentId == headers(i).id, s"Incorrect chain: ${headers(i - 1).parentId},${headers(i).id}") } - assert(headers.toArray.headOption.forall(_.id == preHeader.parentId), s"preHeader.parentId should be id of the best header") + require(headers.toArray.headOption.forall(_.id == preHeader.parentId), s"preHeader.parentId should be id of the best header") /* NOHF PROOF: Added: assert that dataBoxes corresponds to spendingTransaction.dataInputs Motivation: to fail early, rather than when going into evaluation Safety: dataBoxes and spendingTransaction are supplied separately in ergo. No checks in ergo. Examined ergo code: all that leads to ErgoLikeContext creation. */ - assert(spendingTransaction.dataInputs.length == dataBoxes.length && - spendingTransaction.dataInputs.forall(dataInput => dataBoxes.exists(b => util.Arrays.equals(b.id, dataInput.boxId))), + require(spendingTransaction.dataInputs.length == dataBoxes.length && + spendingTransaction.dataInputs.forall(dataInput => dataBoxes.exists(b => java.util.Arrays.equals(b.id, dataInput.boxId))), "dataBoxes do not correspond to spendingTransaction.dataInputs") // TODO assert boxesToSpend correspond to spendingTransaction.inputs @@ -111,6 +110,18 @@ class ErgoLikeContext(val lastBlockUtxoRoot: AvlTreeData, */ val self: ErgoBox = boxesToSpend(selfIndex) + /** Current version of the ErgoTree executed by the interpreter. + * This property is used to implement version dependent operations and passed to + * interpreter via [[special.sigma.Context]]. + * The value cannot be assigned on [[ErgoLikeContext]] construction and must be + * attached using [[withErgoTreeVersion()]] method. + * When the value is None, the [[InterpreterException]] is thrown by the interpreter. + */ + val currentErgoTreeVersion: Option[Byte] = None + + override def withErgoTreeVersion(newVersion: Byte): ErgoLikeContext = + ErgoLikeContext.copy(this)(currErgoTreeVersion = Some(newVersion)) + override def withCostLimit(newCostLimit: Long): ErgoLikeContext = ErgoLikeContext.copy(this)(costLimit = newCostLimit) @@ -153,10 +164,14 @@ class ErgoLikeContext(val lastBlockUtxoRoot: AvlTreeData, } val vars = contextVars(varMap ++ extensions) val avlTree = CAvlTree(lastBlockUtxoRoot) + // so selfBox is never one of the `inputs` instances + // as result selfBoxIndex is always (erroneously) returns -1 in ErgoTree v0, v1 val selfBox = boxesToSpend(selfIndex).toTestBox(isCost) + val ergoTreeVersion = currentErgoTreeVersion.getOrElse( + Interpreter.error(s"Undefined context property: currentErgoTreeVersion")) CostingDataContext( - dataInputs, headers, preHeader, inputs, outputs, preHeader.height, selfBox, avlTree, - preHeader.minerPk.getEncoded, vars, activatedScriptVersion, isCost) + dataInputs, headers, preHeader, inputs, outputs, preHeader.height, selfBox, selfIndex, avlTree, + preHeader.minerPk.getEncoded, vars, activatedScriptVersion, ergoTreeVersion, isCost) } @@ -218,11 +233,14 @@ object ErgoLikeContext { validationSettings: SigmaValidationSettings = ctx.validationSettings, costLimit: Long = ctx.costLimit, initCost: Long = ctx.initCost, - activatedScriptVersion: Byte = ctx.activatedScriptVersion): ErgoLikeContext = { + activatedScriptVersion: Byte = ctx.activatedScriptVersion, + currErgoTreeVersion: Option[Byte] = ctx.currentErgoTreeVersion): ErgoLikeContext = { new ErgoLikeContext( lastBlockUtxoRoot, headers, preHeader, dataBoxes, boxesToSpend, spendingTransaction, selfIndex, extension, validationSettings, costLimit, initCost, - activatedScriptVersion) + activatedScriptVersion) { + override val currentErgoTreeVersion: Option[TypeCode] = currErgoTreeVersion + } } } @@ -230,7 +248,7 @@ object ErgoLikeContext { case object MinerPubkey extends NotReadyValueByteArray with ValueCompanion { override def opCode: OpCode = OpCodes.MinerPubkeyCode /** Cost of calling Context.minerPubkey Scala method. */ - override val costKind = FixedCost(20) + override val costKind = FixedCost(JitCost(20)) override val opType = SFunc(SContext, SCollection.SByteArray) override def companion = this protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { @@ -240,11 +258,11 @@ case object MinerPubkey extends NotReadyValueByteArray with ValueCompanion { } /** When interpreted evaluates to a IntConstant built from Context.currentHeight */ -case object Height extends NotReadyValueInt with ValueCompanion { +case object Height extends NotReadyValueInt with FixedCostValueCompanion { override def companion = this override def opCode: OpCode = OpCodes.HeightCode /** Cost of: 1) Calling Context.HEIGHT Scala method. */ - override val costKind = FixedCost(26) + override val costKind = FixedCost(JitCost(26)) override val opType = SFunc(SContext, SInt) protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { addCost(this.costKind) @@ -253,11 +271,11 @@ case object Height extends NotReadyValueInt with ValueCompanion { } /** When interpreted evaluates to a collection of BoxConstant built from Context.boxesToSpend */ -case object Inputs extends LazyCollection[SBox.type] with ValueCompanion { +case object Inputs extends LazyCollection[SBox.type] with FixedCostValueCompanion { override def companion = this override def opCode: OpCode = OpCodes.InputsCode /** Cost of: 1) Calling Context.INPUTS Scala method. */ - override val costKind = FixedCost(10) + override val costKind = FixedCost(JitCost(10)) override def tpe = SCollection.SBoxArray override val opType = SFunc(SContext, tpe) protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { @@ -267,11 +285,11 @@ case object Inputs extends LazyCollection[SBox.type] with ValueCompanion { } /** When interpreted evaluates to a collection of BoxConstant built from Context.spendingTransaction.outputs */ -case object Outputs extends LazyCollection[SBox.type] with ValueCompanion { +case object Outputs extends LazyCollection[SBox.type] with FixedCostValueCompanion { override def companion = this override def opCode: OpCode = OpCodes.OutputsCode /** Cost of: 1) Calling Context.OUTPUTS Scala method. */ - override val costKind = FixedCost(10) + override val costKind = FixedCost(JitCost(10)) override def tpe = SCollection.SBoxArray override val opType = SFunc(SContext, tpe) protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { @@ -286,7 +304,7 @@ case object LastBlockUtxoRootHash extends NotReadyValueAvlTree with ValueCompani override def opCode: OpCode = OpCodes.LastBlockUtxoRootHashCode /** Cost of: 1) Calling Context.LastBlockUtxoRootHash Scala method. */ - override val costKind = FixedCost(15) + override val costKind = FixedCost(JitCost(15)) override val opType = SFunc(SContext, tpe) protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { @@ -297,11 +315,11 @@ case object LastBlockUtxoRootHash extends NotReadyValueAvlTree with ValueCompani /** When interpreted evaluates to a BoxConstant built from context.boxesToSpend(context.selfIndex) */ -case object Self extends NotReadyValueBox with ValueCompanion { +case object Self extends NotReadyValueBox with FixedCostValueCompanion { override def companion = this override def opCode: OpCode = OpCodes.SelfCode /** Cost of: 1) Calling Context.SELF Scala method. */ - override val costKind = FixedCost(10) + override val costKind = FixedCost(JitCost(10)) override val opType = SFunc(SContext, SBox) protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { addCost(this.costKind) @@ -317,7 +335,7 @@ case object Context extends NotReadyValue[SContext.type] with ValueCompanion { override def opCode: OpCode = OpCodes.ContextCode /** Cost of: 1) accessing global Context instance. */ - override val costKind = FixedCost(1) + override val costKind = FixedCost(JitCost(1)) override def tpe: SContext.type = SContext override val opType: SFunc = SFunc(SUnit, SContext) @@ -330,11 +348,11 @@ case object Context extends NotReadyValue[SContext.type] with ValueCompanion { /** When interpreted evaluates to the singleton instance of [[special.sigma.SigmaDslBuilder]]. * Corresponds to `Global` variable in ErgoScript which can be used like `Global.groupGenerator`. */ -case object Global extends NotReadyValue[SGlobal.type] with ValueCompanion { +case object Global extends NotReadyValue[SGlobal.type] with FixedCostValueCompanion { override def companion = this override def opCode: OpCode = OpCodes.GlobalCode /** Cost of: 1) accessing Global instance. */ - override val costKind = FixedCost(5) + override val costKind = FixedCost(JitCost(5)) override def tpe: SGlobal.type = SGlobal override val opType: SFunc = SFunc(SUnit, SGlobal) protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { diff --git a/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeInterpreter.scala b/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeInterpreter.scala index 7e54647a06..a4bda1a54d 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeInterpreter.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeInterpreter.scala @@ -30,11 +30,11 @@ class ErgoLikeInterpreter(implicit val IR: IRContext) extends Interpreter { else Some(outVal) case _ => - // TODO HF (1h): this case is not possible because `ErgoBox.get` + // this case is not possible because `ErgoBox.get` // returns lookups values from `additionalRegisters` Map with values // of type EvaluatedValue, which are always Constant nodes in practice. - // Also, this branch is never executed so can be safely removed - // (better as part of the HF) + // Also, this branch is never executed so can be safely removed, but + // we keep it here explicitly for clarity None } }.orElse(d.default) diff --git a/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala b/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala index b673a3bb86..683602dcc8 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala @@ -9,6 +9,7 @@ import sigmastate.eval.Extensions._ import sigmastate.eval._ import sigmastate.interpreter.ProverResult import sigmastate.serialization.{SigmaSerializer, ValueSerializer} +import sigmastate.util.safeNewArray import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import special.collection.ExtensionMethods._ import spire.syntax.all.cfor @@ -147,28 +148,28 @@ object ErgoLikeTransactionSerializer extends SigmaSerializer[ErgoLikeTransaction override def parse(r: SigmaByteReader): ErgoLikeTransaction = { // parse transaction inputs val inputsCount = r.getUShort() - val inputs = ValueSerializer.newArray[Input](inputsCount) + val inputs = safeNewArray[Input](inputsCount) cfor(0)(_ < inputsCount, _ + 1) { i => inputs(i) = Input.serializer.parse(r) } // parse transaction data inputs val dataInputsCount = r.getUShort() - val dataInputs = ValueSerializer.newArray[DataInput](dataInputsCount) + val dataInputs = safeNewArray[DataInput](dataInputsCount) cfor(0)(_ < dataInputsCount, _ + 1) { i => dataInputs(i) = DataInput(ADKey @@ r.getBytes(ErgoBox.BoxId.size)) } // parse distinct ids of tokens in transaction outputs - val tokensCount = r.getUInt().toInt - val tokens = ValueSerializer.newArray[Array[Byte]](tokensCount) + val tokensCount = r.getUIntExact + val tokens = safeNewArray[Array[Byte]](tokensCount) cfor(0)(_ < tokensCount, _ + 1) { i => tokens(i) = r.getBytes(TokenId.size) } // parse outputs val outsCount = r.getUShort() - val outputCandidates = ValueSerializer.newArray[ErgoBoxCandidate](outsCount) + val outputCandidates = safeNewArray[ErgoBoxCandidate](outsCount) cfor(0)(_ < outsCount, _ + 1) { i => outputCandidates(i) = ErgoBoxCandidate.serializer.parseBodyWithIndexedDigests(tokens, r) } diff --git a/sigmastate/src/main/scala/org/ergoplatform/ErgoScriptPredef.scala b/sigmastate/src/main/scala/org/ergoplatform/ErgoScriptPredef.scala index 049ceb9029..d1f17b9809 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/ErgoScriptPredef.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/ErgoScriptPredef.scala @@ -8,8 +8,8 @@ import sigmastate.basics.DLogProtocol.ProveDlog import sigmastate.eval.IRContext import sigmastate.interpreter.CryptoConstants import sigmastate.lang.Terms.ValueOps -import sigmastate.{SLong, _} -import sigmastate.lang.{TransformingSigmaBuilder, SigmaCompiler, CompilerSettings} +import sigmastate._ +import sigmastate.lang.SigmaCompiler import sigmastate.serialization.ErgoTreeSerializer.DefaultSerializer import sigmastate.utxo._ @@ -18,8 +18,7 @@ object ErgoScriptPredef { import sigmastate.interpreter.Interpreter._ def compileWithCosting(env: ScriptEnv, code: String, networkPrefix: NetworkPrefix)(implicit IR: IRContext): Value[SType] = { - val compiler = new SigmaCompiler(CompilerSettings( - networkPrefix, TransformingSigmaBuilder, lowerMethodCalls = true)) + val compiler = new SigmaCompiler(networkPrefix) val interProp = compiler.typecheck(env, code) val IR.Pair(calcF, _) = IR.doCosting(env, interProp) IR.buildTree(calcF) diff --git a/sigmastate/src/main/scala/org/ergoplatform/validation/SigmaValidationSettingsSerializer.scala b/sigmastate/src/main/scala/org/ergoplatform/validation/SigmaValidationSettingsSerializer.scala index 4f4a05280b..2c66b05e71 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/validation/SigmaValidationSettingsSerializer.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/validation/SigmaValidationSettingsSerializer.scala @@ -19,7 +19,7 @@ object SigmaValidationSettingsSerializer extends SigmaSerializer[SigmaValidation } override def parse(r: SigmaByteReader): SigmaValidationSettings = { - val nRules = r.getUInt().toIntExact + val nRules = r.getUIntExact val parsed = (0 until nRules).map { _ => val ruleId = r.getUShort().toShortExact val status = RuleStatusSerializer.parse(r) diff --git a/sigmastate/src/main/scala/org/ergoplatform/validation/ValidationRules.scala b/sigmastate/src/main/scala/org/ergoplatform/validation/ValidationRules.scala index 86e2e38675..b3cb6cfe3b 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/validation/ValidationRules.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/validation/ValidationRules.scala @@ -49,7 +49,7 @@ case class ValidationRule( * Should be used in all validation rules to unify ValidationException instances * which can be thrown (to simplify handling). */ - def throwValidationException(cause: Throwable, args: Seq[Any]) = { + def throwValidationException(cause: Throwable, args: Seq[Any]): Nothing = { if (cause.isInstanceOf[ValidationException]) { throw cause } @@ -204,13 +204,27 @@ object ValidationRules { object CheckSerializableTypeCode extends ValidationRule(1009, "Check the data values of the type (given by type code) can be serialized") with SoftForkWhenReplaced { + + /** Creates an exception which is used as a cause when throwing a ValidationException. */ + def throwValidationException(typeCode: Byte): Nothing = { + val ex = new SerializerException( + s"Data value of the type with the code ${toUByte(typeCode)} cannot be deserialized.") + throwValidationException(ex, Array(typeCode)) + } + final def apply[T](typeCode: Byte): Unit = { checkRule() val ucode = toUByte(typeCode) - if (ucode > toUByte(OpCodes.LastDataType)) { - throwValidationException( - new SerializerException(s"Data value of the type with the code $ucode cannot be deserialized."), - Array(typeCode)) + if (typeCode == SOption.OptionTypeCode || ucode > toUByte(OpCodes.LastDataType)) { + // the `typeCode == SOption.OptionTypeCode` condition is added in v5.0 and we + // throw ValidationException for Option type as well in order to be able to + // interpret it as soft-fork condition. + // This will allow to add Option serialization in DataSerializer via v6.0 soft-fork. + // This is in contrast to v4.x of this rule where Option type is not checked and + // ordinary SigmaSerializer exception is thrown by the fallback case of DataSerializer. + // This change is consensus-safe as v4.x and v5.x nodes will both throw exceptions + // (albeit different ones) while attempting deserialization of an Option value. + throwValidationException(typeCode) } } } @@ -309,10 +323,32 @@ object ValidationRules { } } - /** This rule doesn't have it's own validation logic, however it is used in creation of - * ValidationExceptions, which in turn can be checked for soft-fork condition using `this.isSoftFork`. */ + /** Introduced in v5.0, this rule it is used in creation of ValidationExceptions, which + * in turn can be checked for soft-fork condition using `this.isSoftFork`. Thus, this + * rule can be replaced with a new rule and the limit can be increased. + */ object CheckPositionLimit extends ValidationRule(1014, "Check that the Reader has not exceeded the position limit.") with SoftForkWhenReplaced { + + /** Wraps the given cause into [[ValidationException]] and throws it. */ + def throwValidationException(cause: ReaderPositionLimitExceeded): Nothing = { + throwValidationException(cause, args = Nil) + } + + /** Throws a [[ValidationException]] with the given parameters. */ + def throwValidationException(position: Int, positionLimit: Int): Nothing = { + throwValidationException( + new ReaderPositionLimitExceeded( + s"SigmaByteReader position limit $positionLimit is reached at position $position", + position, positionLimit)) + } + + final def apply(position: Int, positionLimit: Int): Unit = { + checkRule() + if (position > positionLimit) { + throwValidationException(position, positionLimit) + } + } } object CheckLoopLevelInCostFunction extends ValidationRule(1015, diff --git a/sigmastate/src/main/scala/sigmastate/AvlTreeData.scala b/sigmastate/src/main/scala/sigmastate/AvlTreeData.scala index 87adf2fb2a..ae94121920 100644 --- a/sigmastate/src/main/scala/sigmastate/AvlTreeData.scala +++ b/sigmastate/src/main/scala/sigmastate/AvlTreeData.scala @@ -1,6 +1,5 @@ package sigmastate -import java.util import java.util.{Arrays, Objects} import scorex.crypto.authds.ADDigest @@ -67,7 +66,7 @@ case class AvlTreeData(digest: ADDigest, } override def hashCode(): Int = - (util.Arrays.hashCode(digest) * 31 + + (Arrays.hashCode(digest) * 31 + keyLength.hashCode()) * 31 + Objects.hash(valueLengthOpt, treeFlags) } @@ -93,8 +92,8 @@ object AvlTreeData { override def parse(r: SigmaByteReader): AvlTreeData = { val digest = r.getBytes(DigestSize) val tf = AvlTreeFlags(r.getByte()) - val keyLength = r.getUInt().toInt - val valueLengthOpt = r.getOption(r.getUInt().toInt) + val keyLength = r.getUIntExact + val valueLengthOpt = r.getOption(r.getUIntExact) AvlTreeData(ADDigest @@ digest, tf, keyLength, valueLengthOpt) } } diff --git a/sigmastate/src/main/scala/sigmastate/CostKind.scala b/sigmastate/src/main/scala/sigmastate/CostKind.scala index 85d20c9799..0730bb4ec0 100644 --- a/sigmastate/src/main/scala/sigmastate/CostKind.scala +++ b/sigmastate/src/main/scala/sigmastate/CostKind.scala @@ -1,5 +1,7 @@ package sigmastate +import scala.runtime.Statics + /** Cost descriptor of a single operation, usually associated with * [[sigmastate.interpreter.OperationDesc]]. */ @@ -7,7 +9,9 @@ sealed abstract class CostKind /** Descriptor of the simple fixed cost. * @param cost given cost of the operation */ -case class FixedCost(cost: Int) extends CostKind +case class FixedCost(cost: JitCost) extends CostKind { + override def hashCode(): Int = cost.value +} /** Cost of operation over collection of the known length. * See for example [[Exists]], [[MapCollection]]. @@ -15,21 +19,32 @@ case class FixedCost(cost: Int) extends CostKind * @param perChunkCost cost associated with each chunk of items * @param chunkSize number of items in a chunk */ -case class PerItemCost(baseCost: Int, perChunkCost: Int, chunkSize: Int) extends CostKind { +case class PerItemCost(baseCost: JitCost, perChunkCost: JitCost, chunkSize: Int) extends CostKind { /** Compute number of chunks necessary to cover the given number of items. */ def chunks(nItems: Int) = (nItems - 1) / chunkSize + 1 /** Computes the cost for the given number of items. */ - def cost (nItems: Int): Int = { + def cost (nItems: Int): JitCost = { val nChunks = chunks(nItems) - Math.addExact(baseCost, Math.multiplyExact(perChunkCost, nChunks)) + baseCost + (perChunkCost * nChunks) + } + + /** This override is necessary to avoid JitCost instances allocation in the default + * generated code for case class. + */ + override def hashCode(): Int = { + var var1 = -889275714 + var1 = Statics.mix(var1, this.baseCost.value) + var1 = Statics.mix(var1, this.perChunkCost.value) + var1 = Statics.mix(var1, this.chunkSize) + Statics.finalizeHash(var1, 3) } } /** Descriptor of the cost which depends on type. */ abstract class TypeBasedCost extends CostKind { /** Returns cost value depending on the given type. */ - def costFunc(tpe: SType): Int + def costFunc(tpe: SType): JitCost } /** Cost of operation cannot be described using fixed set of parameters. diff --git a/sigmastate/src/main/scala/sigmastate/DataValueComparer.scala b/sigmastate/src/main/scala/sigmastate/DataValueComparer.scala index 959cbd2dcb..bc142366a5 100644 --- a/sigmastate/src/main/scala/sigmastate/DataValueComparer.scala +++ b/sigmastate/src/main/scala/sigmastate/DataValueComparer.scala @@ -27,109 +27,119 @@ object DataValueComparer { * The constants are part of the consensus protocol and cannot be changed without forking. */ final val CostOf_MatchType = 1 - final val CostKind_MatchType = FixedCost(CostOf_MatchType) + final val CostKind_MatchType = FixedCost(JitCost(CostOf_MatchType)) final val OpDesc_MatchType = NamedDesc("MatchType") final val MatchType = OperationCostInfo(CostKind_MatchType, OpDesc_MatchType) - final val CostKind_EQ_Prim = FixedCost(3) // case 1 + final val CostKind_EQ_Prim = FixedCost(JitCost(3)) // case 1 final val OpDesc_EQ_Prim = NamedDesc("EQ_Prim") final val EQ_Prim = OperationCostInfo(CostKind_EQ_Prim, OpDesc_EQ_Prim) /** Equals two Colls of non-primitive (boxed) types. */ - final val CostKind_EQ_Coll = PerItemCost(10, 2, 1) // case 2 + final val CostKind_EQ_Coll = PerItemCost( + baseCost = JitCost(10), perChunkCost = JitCost(2), chunkSize = 1) // case 2 final val OpDesc_EQ_Coll = NamedDesc("EQ_Coll") final val EQ_Coll = OperationCostInfo(CostKind_EQ_Coll, OpDesc_EQ_Coll) - final val CostKind_EQ_Tuple = FixedCost(4) // case 3 + final val CostKind_EQ_Tuple = FixedCost(JitCost(4)) // case 3 final val OpDesc_EQ_Tuple = NamedDesc("EQ_Tuple") final val EQ_Tuple = OperationCostInfo(CostKind_EQ_Tuple, OpDesc_EQ_Tuple) /** NOTE: the value is set based on benchmarking of SigmaDslSpecification. */ - final val CostKind_EQ_GroupElement = FixedCost(172) // case 4 + final val CostKind_EQ_GroupElement = FixedCost(JitCost(172)) // case 4 final val OpDesc_EQ_GroupElement = NamedDesc("EQ_GroupElement") final val EQ_GroupElement = OperationCostInfo(CostKind_EQ_GroupElement, OpDesc_EQ_GroupElement) - final val CostKind_EQ_BigInt = FixedCost(5) // case 5 + final val CostKind_EQ_BigInt = FixedCost(JitCost(5)) // case 5 final val OpDesc_EQ_BigInt = NamedDesc("EQ_BigInt") final val EQ_BigInt = OperationCostInfo(CostKind_EQ_BigInt, OpDesc_EQ_BigInt) - final val CostKind_EQ_AvlTree = FixedCost(3 + (6 * CostOf_MatchType) / 2) // case 6 + final val CostKind_EQ_AvlTree = FixedCost(JitCost(3 + (6 * CostOf_MatchType) / 2)) // case 6 final val OpDesc_EQ_AvlTree = NamedDesc("EQ_AvlTree") final val EQ_AvlTree = OperationCostInfo(CostKind_EQ_AvlTree, OpDesc_EQ_AvlTree) - // TODO v5.0: update value after serialization is avoided to compute ErgoBox.id - final val CostKind_EQ_Box = FixedCost(6) // case 7 + final val CostKind_EQ_Box = FixedCost(JitCost(6)) // case 7 final val OpDesc_EQ_Box = NamedDesc("EQ_Box") final val EQ_Box = OperationCostInfo(CostKind_EQ_Box, OpDesc_EQ_Box) /** NOTE: In the formula `(7 + 1)` the 1 corresponds to the second type match. */ - final val CostKind_EQ_Option = FixedCost(1 + (7 + 1) * CostOf_MatchType / 2 - 1) // case 8 + final val CostKind_EQ_Option = FixedCost(JitCost(1 + (7 + 1) * CostOf_MatchType / 2 - 1)) // case 8 final val OpDesc_EQ_Option = NamedDesc("EQ_Option") final val EQ_Option = OperationCostInfo(CostKind_EQ_Option, OpDesc_EQ_Option) - final val CostKind_EQ_PreHeader = FixedCost(4) // case 9 + final val CostKind_EQ_PreHeader = FixedCost(JitCost(4)) // case 9 final val OpDesc_EQ_PreHeader = NamedDesc("EQ_PreHeader") final val EQ_PreHeader = OperationCostInfo(CostKind_EQ_PreHeader, OpDesc_EQ_PreHeader) - final val CostKind_EQ_Header = FixedCost(6) // case 10 + final val CostKind_EQ_Header = FixedCost(JitCost(6)) // case 10 final val OpDesc_EQ_Header = NamedDesc("EQ_Header") final val EQ_Header = OperationCostInfo(CostKind_EQ_Header, OpDesc_EQ_Header) /** Equals two CollOverArray of Boolean type. */ - final val CostKind_EQ_COA_Boolean = PerItemCost(15, 2, 128) + final val CostKind_EQ_COA_Boolean = PerItemCost( + baseCost = JitCost(15), perChunkCost = JitCost(2), chunkSize = 128) final val OpDesc_EQ_COA_Boolean = NamedDesc("EQ_COA_Boolean") final val EQ_COA_Boolean = OperationCostInfo(CostKind_EQ_COA_Boolean, OpDesc_EQ_COA_Boolean) /** Equals two CollOverArray of Byte type. */ - final val CostKind_EQ_COA_Byte = PerItemCost(15, 2, 128) + final val CostKind_EQ_COA_Byte = PerItemCost( + baseCost = JitCost(15), perChunkCost = JitCost(2), chunkSize = 128) final val OpDesc_EQ_COA_Byte = NamedDesc("EQ_COA_Byte") final val EQ_COA_Byte = OperationCostInfo(CostKind_EQ_COA_Byte, OpDesc_EQ_COA_Byte) /** Equals two CollOverArray of Short type. */ - final val CostKind_EQ_COA_Short = PerItemCost(15, 2, 96) + final val CostKind_EQ_COA_Short = PerItemCost( + baseCost = JitCost(15), perChunkCost = JitCost(2), chunkSize = 96) final val OpDesc_EQ_COA_Short = NamedDesc("EQ_COA_Short") final val EQ_COA_Short = OperationCostInfo(CostKind_EQ_COA_Short, OpDesc_EQ_COA_Short) /** Equals two CollOverArray of Int type. */ - final val CostKind_EQ_COA_Int = PerItemCost(15, 2, 64) + final val CostKind_EQ_COA_Int = PerItemCost( + baseCost = JitCost(15), perChunkCost = JitCost(2), chunkSize = 64) final val OpDesc_EQ_COA_Int = NamedDesc("EQ_COA_Int") final val EQ_COA_Int = OperationCostInfo(CostKind_EQ_COA_Int, OpDesc_EQ_COA_Int) /** Equals two CollOverArray of Long type. */ - final val CostKind_EQ_COA_Long = PerItemCost(15, 2, 48) + final val CostKind_EQ_COA_Long = PerItemCost( + baseCost = JitCost(15), perChunkCost = JitCost(2), chunkSize = 48) final val OpDesc_EQ_COA_Long = NamedDesc("EQ_COA_Long") final val EQ_COA_Long = OperationCostInfo(CostKind_EQ_COA_Long, OpDesc_EQ_COA_Long) /** Equals two CollOverArray of GroupElement type. */ - final val CostKind_EQ_COA_GroupElement = PerItemCost(15, 5, 1) + final val CostKind_EQ_COA_GroupElement = PerItemCost( + baseCost = JitCost(15), perChunkCost = JitCost(5), chunkSize = 1) final val OpDesc_EQ_COA_GroupElement = NamedDesc("EQ_COA_GroupElement") final val EQ_COA_GroupElement = OperationCostInfo(CostKind_EQ_COA_GroupElement, OpDesc_EQ_COA_GroupElement) /** Equals two CollOverArray of BigInt type. */ - final val CostKind_EQ_COA_BigInt = PerItemCost(15, 7, 5) + final val CostKind_EQ_COA_BigInt = PerItemCost( + baseCost = JitCost(15), perChunkCost = JitCost(7), chunkSize = 5) final val OpDesc_EQ_COA_BigInt = NamedDesc("EQ_COA_BigInt") final val EQ_COA_BigInt = OperationCostInfo(CostKind_EQ_COA_BigInt, OpDesc_EQ_COA_BigInt) /** Equals two CollOverArray of AvlTree type. */ - final val CostKind_EQ_COA_AvlTree = PerItemCost(15, 5, 2) + final val CostKind_EQ_COA_AvlTree = PerItemCost( + baseCost = JitCost(15), perChunkCost = JitCost(5), chunkSize = 2) final val OpDesc_EQ_COA_AvlTree = NamedDesc("EQ_COA_AvlTree") final val EQ_COA_AvlTree = OperationCostInfo(CostKind_EQ_COA_AvlTree, OpDesc_EQ_COA_AvlTree) - // TODO v5.0: update value after serialization is avoided to compute ErgoBox.id /** Equals two CollOverArray of Box type. */ - final val CostKind_EQ_COA_Box = PerItemCost(15, 5, 1) + final val CostKind_EQ_COA_Box = PerItemCost( + baseCost = JitCost(15), perChunkCost = JitCost(5), chunkSize = 1) final val OpDesc_EQ_COA_Box = NamedDesc("EQ_COA_Box") final val EQ_COA_Box = OperationCostInfo(CostKind_EQ_COA_Box, OpDesc_EQ_COA_Box) /** Equals two CollOverArray of PreHeader type. */ - final val CostKind_EQ_COA_PreHeader = PerItemCost(15, 3, 1) + final val CostKind_EQ_COA_PreHeader = PerItemCost( + baseCost = JitCost(15), perChunkCost = JitCost(3), chunkSize = 1) final val OpDesc_EQ_COA_PreHeader = NamedDesc("EQ_COA_PreHeader") final val EQ_COA_PreHeader = OperationCostInfo(CostKind_EQ_COA_PreHeader, OpDesc_EQ_COA_PreHeader) /** Equals two CollOverArray of Header type. */ - final val CostKind_EQ_COA_Header = PerItemCost(15, 5, 1) + final val CostKind_EQ_COA_Header = PerItemCost( + baseCost = JitCost(15), perChunkCost = JitCost(5), chunkSize = 1) final val OpDesc_EQ_COA_Header = NamedDesc("EQ_COA_Header") final val EQ_COA_Header = OperationCostInfo(CostKind_EQ_COA_Header, OpDesc_EQ_COA_Header) @@ -157,7 +167,7 @@ object DataValueComparer { (implicit E: ErgoTreeEvaluator): Boolean = { var okEqual = true E.addSeqCost(costInfo.costKind, costInfo.opDesc) { () => - // TODO v5.0: this loop is bounded when MaxCollSize limit is enforced + // this loop is bounded because MaxArrayLength limit is enforced val len = c1.length var i = 0 val a1 = c1.toArray @@ -179,7 +189,7 @@ object DataValueComparer { def equalColls[A](c1: Coll[A], c2: Coll[A])(implicit E: ErgoTreeEvaluator): Boolean = { var okEqual = true E.addSeqCost(CostKind_EQ_Coll, OpDesc_EQ_Coll) { () => - // TODO v5.0: this loop is bounded when MaxCollSize limit is enforced + // this loop is bounded because MaxArrayLength limit is enforced val len = c1.length var i = 0 while(i < len && okEqual) { @@ -295,9 +305,13 @@ object DataValueComparer { okEqual } - // TODO v5.0: introduce a new limit on structural depth of data values /** Generic comparison of any two data values. The method dispatches on a type of the * left value and then performs the specific comparison. + * The comparison recursively descends on the value structure regardless of the depth. + * However, every step costs are accrued to `E.coster` and the defined limit + * `E.coster.costLimit` is checked. Thus, the execution of this method is limited and + * always finishes at least as fast as the costLimit prescribes. + * No limit is structural depth is necessary. */ def equalDataValues(l: Any, r: Any)(implicit E: ErgoTreeEvaluator): Boolean = { var okEqual: Boolean = false diff --git a/sigmastate/src/main/scala/sigmastate/SigSerializer.scala b/sigmastate/src/main/scala/sigmastate/SigSerializer.scala index a1133bc8eb..08cec54825 100644 --- a/sigmastate/src/main/scala/sigmastate/SigSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/SigSerializer.scala @@ -8,17 +8,25 @@ import sigmastate.Values.SigmaBoolean import sigmastate.basics.DLogProtocol.{ProveDlog, SecondDLogProverMessage} import sigmastate.basics.VerifierMessage.Challenge import sigmastate.basics.{ProveDHTuple, SecondDiffieHellmanTupleProverMessage} -import sigmastate.interpreter.CryptoConstants +import sigmastate.interpreter.ErgoTreeEvaluator.{fixedCostOp, perItemCostOp} +import sigmastate.interpreter.{CryptoConstants, ErgoTreeEvaluator, NamedDesc, OperationCostInfo} import sigmastate.lang.exceptions.SerializerException -import sigmastate.serialization.{SigmaSerializer, ValueSerializer} -import sigmastate.utils.{SigmaByteReader, SigmaByteWriter, Helpers} +import sigmastate.serialization.{SigmaSerializer} +import sigmastate.util.safeNewArray +import sigmastate.utils.{Helpers, SigmaByteReader, SigmaByteWriter} import spire.syntax.all.cfor +/** Contains implementation of signature (aka proof) serialization. + * @see toProofBytes, parseAndComputeChallenges + */ object SigSerializer extends LazyLogging { /** Log warning message using this class's logger. */ def warn(msg: String) = logger.warn(msg) + /** A size of challenge in Sigma protocols, in bits. */ val hashSize = CryptoConstants.soundnessBits / 8 + + /** Number of bytes to represent any group element as byte array */ val order = CryptoConstants.groupSize /** Recursively traverses the given node and serializes challenges and prover messages @@ -106,9 +114,12 @@ object SigSerializer extends LazyLogging { * * @param exp sigma proposition which defines the structure of bytes from the reader * @param proof proof to extract challenges from + * @param E optional evaluator (can be null) which is used for profiling of operations. + * When `E` is `null`, then profiling is turned-off and has no effect on + * the execution. * @return An instance of [[UncheckedTree]] i.e. either [[NoProof]] or [[UncheckedSigmaTree]] */ - def parseAndComputeChallenges(exp: SigmaBoolean, proof: Array[Byte]): UncheckedTree = { + def parseAndComputeChallenges(exp: SigmaBoolean, proof: Array[Byte])(implicit E: ErgoTreeEvaluator): UncheckedTree = { if (proof.isEmpty) NoProof else { @@ -119,6 +130,28 @@ object SigSerializer extends LazyLogging { } } + /** Represents cost of parsing UncheckedSchnorr node from proof bytes. */ + final val ParseChallenge_ProveDlog = OperationCostInfo( + FixedCost(JitCost(10)), NamedDesc("ParseChallenge_ProveDlog")) + + /** Represents cost of parsing UncheckedDiffieHellmanTuple node from proof bytes. */ + final val ParseChallenge_ProveDHT = OperationCostInfo( + FixedCost(JitCost(10)), NamedDesc("ParseChallenge_ProveDHT")) + + /** Represents cost of parsing GF2_192_Poly from proof bytes. */ + final val ParsePolynomial = OperationCostInfo( + PerItemCost(baseCost = JitCost(10), perChunkCost = JitCost(10), chunkSize = 1), + NamedDesc("ParsePolynomial")) + + /** Represents cost of: + * 1) evaluating a polynomial + * 2) obtaining GF2_192 instance + * 3) converting it to array of bytes + */ + final val EvaluatePolynomial = OperationCostInfo( + PerItemCost(baseCost = JitCost(3), perChunkCost = JitCost(3), chunkSize = 1), + NamedDesc("EvaluatePolynomial")) + /** Helper method to read requested or remaining bytes from the reader. */ def readBytesChecked(r: SigmaByteReader, numRequestedBytes: Int, onError: String => Unit): Array[Byte] = { val bytes = r.getBytesUnsafe(numRequestedBytes) @@ -137,15 +170,18 @@ object SigSerializer extends LazyLogging { * @param r reader to extract challenges from * @param challengeOpt if non-empty, then the challenge has been computed for this node * by its parent; else it needs to be read from the proof (via reader) + * @param E optional evaluator (can be null) which is used for profiling of operations. + * When `E` is `null`, then profiling is turned-off and has no effect on + * the execution. * @return An instance of [[UncheckedSigmaTree]] * * HOTSPOT: don't beautify the code - * Note, `null` is used instead of Option to avoid allocations. + * Note, null` is used instead of Option to avoid allocations. */ def parseAndComputeChallenges( exp: SigmaBoolean, r: SigmaByteReader, - challengeOpt: Challenge = null): UncheckedSigmaTree = { + challengeOpt: Challenge = null)(implicit E: ErgoTreeEvaluator): UncheckedSigmaTree = { // Verifier Step 2: Let e_0 be the challenge in the node here (e_0 is called "challenge" in the code) val challenge = if (challengeOpt == null) { Challenge @@ readBytesChecked(r, hashSize, @@ -157,20 +193,24 @@ object SigSerializer extends LazyLogging { exp match { case dl: ProveDlog => // Verifier Step 3: For every leaf node, read the response z provided in the proof. - val z_bytes = readBytesChecked(r, order, hex => warn(s"Invalid z bytes for $dl: $hex")) - val z = BigIntegers.fromUnsignedByteArray(z_bytes) - UncheckedSchnorr(dl, None, challenge, SecondDLogProverMessage(z)) + fixedCostOp(ParseChallenge_ProveDlog) { + val z_bytes = readBytesChecked(r, order, hex => warn(s"Invalid z bytes for $dl: $hex")) + val z = BigIntegers.fromUnsignedByteArray(z_bytes) + UncheckedSchnorr(dl, None, challenge, SecondDLogProverMessage(z)) + } case dh: ProveDHTuple => // Verifier Step 3: For every leaf node, read the response z provided in the proof. - val z_bytes = readBytesChecked(r, order, hex => warn(s"Invalid z bytes for $dh: $hex")) - val z = BigIntegers.fromUnsignedByteArray(z_bytes) - UncheckedDiffieHellmanTuple(dh, None, challenge, SecondDiffieHellmanTupleProverMessage(z)) + fixedCostOp(ParseChallenge_ProveDHT) { + val z_bytes = readBytesChecked(r, order, hex => warn(s"Invalid z bytes for $dh: $hex")) + val z = BigIntegers.fromUnsignedByteArray(z_bytes) + UncheckedDiffieHellmanTuple(dh, None, challenge, SecondDiffieHellmanTupleProverMessage(z)) + } case and: CAND => // Verifier Step 2: If the node is AND, then all of its children get e_0 as the challenge val nChildren = and.children.length - val children = ValueSerializer.newArray[UncheckedSigmaTree](nChildren) + val children = safeNewArray[UncheckedSigmaTree](nChildren) cfor(0)(_ < nChildren, _ + 1) { i => children(i) = parseAndComputeChallenges(and.children(i), r, challenge) } @@ -183,7 +223,7 @@ object SigSerializer extends LazyLogging { // Read all the children but the last and compute the XOR of all the challenges including e_0 val nChildren = or.children.length - val children = ValueSerializer.newArray[UncheckedSigmaTree](nChildren) + val children = safeNewArray[UncheckedSigmaTree](nChildren) val xorBuf = challenge.clone() val iLastChild = nChildren - 1 cfor(0)(_ < iLastChild, _ + 1) { i => @@ -206,13 +246,17 @@ object SigSerializer extends LazyLogging { // Read the polynomial -- it has n-k coefficients val nChildren = th.children.length val nCoefs = nChildren - th.k - val coeffBytes = readBytesChecked(r, hashSize * nCoefs, - hex => warn(s"Invalid coeffBytes for $th: $hex")) - val polynomial = GF2_192_Poly.fromByteArray(challenge, coeffBytes) + val polynomial = perItemCostOp(ParsePolynomial, nCoefs) { () => + val coeffBytes = readBytesChecked(r, hashSize * nCoefs, + hex => warn(s"Invalid coeffBytes for $th: $hex")) + GF2_192_Poly.fromByteArray(challenge, coeffBytes) + } - val children = ValueSerializer.newArray[UncheckedSigmaTree](nChildren) + val children = safeNewArray[UncheckedSigmaTree](nChildren) cfor(0)(_ < nChildren, _ + 1) { i => - val c = Challenge @@ polynomial.evaluate((i + 1).toByte).toByteArray + val c = perItemCostOp(EvaluatePolynomial, nCoefs) { () => + Challenge @@ polynomial.evaluate((i + 1).toByte).toByteArray + } children(i) = parseAndComputeChallenges(th.children(i), r, c) } diff --git a/sigmastate/src/main/scala/sigmastate/UncheckedTree.scala b/sigmastate/src/main/scala/sigmastate/UncheckedTree.scala index b8bd59dc66..f932f7d77f 100644 --- a/sigmastate/src/main/scala/sigmastate/UncheckedTree.scala +++ b/sigmastate/src/main/scala/sigmastate/UncheckedTree.scala @@ -1,12 +1,11 @@ package sigmastate -import java.util - -import sigmastate.basics.DLogProtocol.{FirstDLogProverMessage, SecondDLogProverMessage, ProveDlog} +import java.util.Arrays +import sigmastate.basics.DLogProtocol.{FirstDLogProverMessage, ProveDlog, SecondDLogProverMessage} import sigmastate.basics.VerifierMessage.Challenge import sigmastate.Values.SigmaBoolean import gf2t.GF2_192_Poly -import sigmastate.basics.{SecondDiffieHellmanTupleProverMessage, ProveDHTuple, FirstDiffieHellmanTupleProverMessage} +import sigmastate.basics.{FirstDiffieHellmanTupleProverMessage, ProveDHTuple, SecondDiffieHellmanTupleProverMessage} sealed trait UncheckedTree extends ProofTree @@ -20,12 +19,12 @@ trait UncheckedConjecture extends UncheckedSigmaTree with ProofTreeConjecture { override def equals(obj: Any): Boolean = (this eq obj.asInstanceOf[AnyRef]) || (obj match { case x: UncheckedConjecture => - util.Arrays.equals(challenge, x.challenge) && children == x.children + Arrays.equals(challenge, x.challenge) && children == x.children case _ => false }) override def hashCode(): Int = - 31 * util.Arrays.hashCode(challenge) + children.hashCode() + 31 * Arrays.hashCode(challenge) + children.hashCode() } trait UncheckedLeaf[SP <: SigmaBoolean] extends UncheckedSigmaTree with ProofTreeLeaf { @@ -43,14 +42,14 @@ case class UncheckedSchnorr(override val proposition: ProveDlog, // NOTE, proposition is not compared because it is included into challenge // like `challenge = hash(prop ++ msg)` commitmentOpt == x.commitmentOpt && - util.Arrays.equals(challenge, x.challenge) && + Arrays.equals(challenge, x.challenge) && secondMessage == x.secondMessage case _ => false }) override def hashCode(): Int = { var h = commitmentOpt.hashCode() - h = 31 * h + util.Arrays.hashCode(challenge) + h = 31 * h + Arrays.hashCode(challenge) h = 31 * h + secondMessage.hashCode() h } @@ -68,14 +67,14 @@ case class UncheckedDiffieHellmanTuple(override val proposition: ProveDHTuple, // NOTE, proposition is not compared because it is included into challenge // like `challenge = hash(prop ++ msg)` commitmentOpt == x.commitmentOpt && - util.Arrays.equals(challenge, x.challenge) && + Arrays.equals(challenge, x.challenge) && secondMessage == x.secondMessage case _ => false }) override def hashCode(): Int = { var h = commitmentOpt.hashCode() - h = 31 * h + util.Arrays.hashCode(challenge) + h = 31 * h + Arrays.hashCode(challenge) h = 31 * h + secondMessage.hashCode() h } @@ -109,7 +108,7 @@ case class CThresholdUncheckedNode(override val challenge: Challenge, override def equals(other: Any) = (this eq other.asInstanceOf[AnyRef]) || (other match { case other: CThresholdUncheckedNode => - util.Arrays.equals(challenge, other.challenge) && + Arrays.equals(challenge, other.challenge) && children == other.children && k == other.k && polynomialOpt == other.polynomialOpt @@ -117,7 +116,7 @@ case class CThresholdUncheckedNode(override val challenge: Challenge, }) override def hashCode(): Int = { - var h = util.Arrays.hashCode(challenge) + var h = Arrays.hashCode(challenge) h = 31 * h + children.hashCode h = 31 * h + k.hashCode() h = 31 * h + polynomialOpt.hashCode() diff --git a/sigmastate/src/main/scala/sigmastate/UnprovenTree.scala b/sigmastate/src/main/scala/sigmastate/UnprovenTree.scala index 7cbe19f3d6..5e291685ad 100644 --- a/sigmastate/src/main/scala/sigmastate/UnprovenTree.scala +++ b/sigmastate/src/main/scala/sigmastate/UnprovenTree.scala @@ -7,6 +7,8 @@ import sigmastate.Values.{ErgoTree, SigmaBoolean, SigmaPropConstant} import sigmastate.basics.DLogProtocol.{FirstDLogProverMessage, ProveDlog} import sigmastate.basics.VerifierMessage.Challenge import sigmastate.basics.{FirstDiffieHellmanTupleProverMessage, FirstProverMessage, ProveDHTuple} +import sigmastate.interpreter.{ErgoTreeEvaluator, NamedDesc, OperationCostInfo} +import sigmastate.interpreter.ErgoTreeEvaluator.fixedCostOp import sigmastate.serialization.ErgoTreeSerializer.DefaultSerializer import sigmastate.serialization.SigmaSerializer import sigmastate.utils.SigmaByteWriter @@ -199,16 +201,31 @@ case class UnprovenDiffieHellmanTuple(override val proposition: ProveDHTuple, override def withPosition(updatedPosition: NodePosition) = this.copy(position = updatedPosition) } -// TODO coverage (8h): write a test that restores the tree from this string and check that the result is equal, -// in order to make sure this conversion is unambiguous object FiatShamirTree { + /** Prefix byte which is put before the other ProofTreeConjecture serialized bytes. */ val internalNodePrefix: Byte = 0 + + /** Prefix byte which is put before the other ProofTreeLeaf serialized bytes. */ val leafPrefix: Byte = 1 + /** Represents cost of serializing UncheckedSchnorr or UnprovenSchnorr node of ProofTree. */ + final val ToBytes_Schnorr = OperationCostInfo( + FixedCost(JitCost(570)), NamedDesc("ToBytes_Schnorr")) + + /** Represents cost of serializing UncheckedDiffieHellmanTuple or + * UnprovenDiffieHellmanTuple node of ProofTree. + */ + final val ToBytes_DHT = OperationCostInfo( + FixedCost(JitCost(680)), NamedDesc("ToBytes_DHT")) + + /** Represents cost of serializing ProofTreeConjecture node of ProofTree. */ + final val ToBytes_ProofTreeConjecture = OperationCostInfo( + FixedCost(JitCost(15)), NamedDesc("ToBytes_ProofTreeConjecture")) + /** Prover Step 7: Convert the tree to a byte array `s` for input to the Fiat-Shamir hash * function. - * See the other overload for details. */ - def toBytes(tree: ProofTree): Array[Byte] = { + * See the other overload for detailed docs. */ + def toBytes(tree: ProofTree)(implicit E: ErgoTreeEvaluator): Array[Byte] = { val w = SigmaSerializer.startWriter() toBytes(tree, w) w.toBytes @@ -226,32 +243,44 @@ object FiatShamirTree { * * @param tree the tree to take commitments from * @param w writer which is used for serialization + * @param E optional evaluator (can be null) which is used for profiling of operations. + * When `E` is `null`, then profiling is turned-off and has no effect on + * the execution. * * HOTSPOT: don't beautify the code */ - def toBytes(tree: ProofTree, w: SigmaByteWriter): Unit = tree match { + def toBytes(tree: ProofTree, w: SigmaByteWriter) + (implicit E: ErgoTreeEvaluator): Unit = tree match { case l: ProofTreeLeaf => - val propTree = ErgoTree.withSegregation(SigmaPropConstant(l.proposition)) - val propBytes = DefaultSerializer.serializeErgoTree(propTree) - val commitmentBytes = l.commitmentOpt.get.bytes - w.put(leafPrefix) - w.putShortBytes(propBytes.length.toShort) - w.putBytes(propBytes) - w.putShortBytes(commitmentBytes.length.toShort) - w.putBytes(commitmentBytes) + val costInfo = l match { + case _: UncheckedSchnorr | _: UnprovenSchnorr => ToBytes_Schnorr + case _: UncheckedDiffieHellmanTuple | _: UnprovenDiffieHellmanTuple => ToBytes_DHT + } + fixedCostOp(costInfo) { + val propTree = ErgoTree.withSegregation(SigmaPropConstant(l.proposition)) + val propBytes = DefaultSerializer.serializeErgoTree(propTree) + val commitmentBytes = l.commitmentOpt.get.bytes + w.put(leafPrefix) + w.putShortBytes(propBytes.length.toShort) + w.putBytes(propBytes) + w.putShortBytes(commitmentBytes.length.toShort) + w.putBytes(commitmentBytes) + } case c: ProofTreeConjecture => - w.put(internalNodePrefix) - w.put(c.conjectureType.id.toByte) - c match { - case unproven: CThresholdUnproven => - w.put(unproven.k.toByte) - case unchecked: CThresholdUncheckedNode => - w.put(unchecked.k.toByte) - case _ => + fixedCostOp(ToBytes_ProofTreeConjecture) { + w.put(internalNodePrefix) + w.put(c.conjectureType.id.toByte) + c match { + case unproven: CThresholdUnproven => + w.put(unproven.k.toByte) + case unchecked: CThresholdUncheckedNode => + w.put(unchecked.k.toByte) + case _ => + } + val childrenCount = c.children.length.toShort + w.putShortBytes(childrenCount) } - val childrenCount = c.children.length.toShort - w.putShortBytes(childrenCount) val cs = c.children.toArray cfor(0)(_ < cs.length, _ + 1) { i => diff --git a/sigmastate/src/main/scala/sigmastate/Values.scala b/sigmastate/src/main/scala/sigmastate/Values.scala index ed198420d7..4667097dbe 100644 --- a/sigmastate/src/main/scala/sigmastate/Values.scala +++ b/sigmastate/src/main/scala/sigmastate/Values.scala @@ -1,9 +1,7 @@ package sigmastate import java.math.BigInteger -import java.util -import java.util.Objects - +import java.util.{Arrays, Objects} import org.bitbucket.inkytonik.kiama.rewriting.Rewriter.{count, everywherebu, strategy} import org.ergoplatform.settings.ErgoAlgos import org.ergoplatform.validation.ValidationException @@ -11,7 +9,7 @@ import scalan.{Nullable, RType} import scalan.util.CollectionUtil._ import sigmastate.SCollection.{SByteArray, SIntArray} import sigmastate.interpreter.CryptoConstants.EcPointType -import sigmastate.interpreter.{CompanionDesc, CryptoConstants, ErgoTreeEvaluator, NamedDesc} +import sigmastate.interpreter.{CompanionDesc, CryptoConstants, ErgoTreeEvaluator, Interpreter, NamedDesc} import sigmastate.serialization.{ConstantStore, OpCodes, _} import sigmastate.serialization.OpCodes._ import sigmastate.TrivialProp.{FalseProp, TrueProp} @@ -36,6 +34,7 @@ import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import special.sigma.{AvlTree, Header, PreHeader, _} import sigmastate.lang.SourceContext import sigmastate.lang.exceptions.InterpreterException +import sigmastate.util.safeNewArray import special.collection.Coll import scala.collection.mutable @@ -60,18 +59,26 @@ object Values { /** Every value represents an operation and that operation can be associated with a function type, * describing functional meaning of the operation, kind of operation signature. - * Thus we can obtain global operation identifiers by combining Value.opName with Value.opType, + * Thus, we can obtain global operation identifiers by combining Value.opName with Value.opType, * so that if (v1.opName == v2.opName) && (v1.opType == v2.opType) then v1 and v2 are functionally * point-wise equivalent. * This in particular means that if two _different_ ops have the same opType they _should_ have * different opNames. - * Thus defined op ids are used in a Cost Model - a table of all existing primitives coupled with + * Thus defined op ids are used in a v4.x Cost Model - a table of all existing primitives coupled with * performance parameters. * */ def opType: SFunc + /** Name of the operation. */ def opName: String = this.getClass.getSimpleName + /** Transforms this expression to SigmaProp expression or throws an exception. */ + def toSigmaProp: SigmaPropValue = this match { + case b if b.tpe == SBoolean => BoolToSigmaProp(this.asBoolValue) + case p if p.tpe == SSigmaProp => p.asSigmaProp + case _ => sys.error(s"Expected SBoolean or SSigmaProp typed value, but was: $this") + } + /** Parser has some source information like line,column in the text. We need to keep it up until RuntimeCosting. * The way to do this is to add Nullable property to every Value. Since Parser is always using SigmaBuilder * to create nodes, @@ -279,9 +286,16 @@ object Values { override def costKind: PerItemCost } + /** Base class for ErgoTree nodes which represents a data value which has already been + * evaluated and no further evaluation (aka reduction) is necessary by the interpreter. + * + * @see Constant, ConcreteCollection, Tuple + */ abstract class EvaluatedValue[+S <: SType] extends Value[S] { + /** The evaluated data value of the corresponding underlying data type. */ val value: S#WrappedType - def opType: SFunc = { + + override def opType: SFunc = { val resType = tpe match { case ct : SCollection[_] => SCollection(ct.typeParams.head.ident) @@ -293,8 +307,20 @@ object Values { } } - abstract class Constant[+S <: SType] extends EvaluatedValue[S] {} + /** Base class for all constant literals whose data value is already known and never + * changes. + * @see ConstantNode + */ + abstract class Constant[+S <: SType] extends EvaluatedValue[S] + /** ErgoTree node which represents data literals, i.e. data values embedded in an + * expression. + * + * @param value data value of the underlying Scala type + * @param tpe type descriptor of the data value and also the type of the value + * represented by this node. + * @see Constant + */ case class ConstantNode[S <: SType](value: S#WrappedType, tpe: S) extends Constant[S] { require(Constant.isCorrectType(value, tpe), s"Invalid type of constant value $value, expected type $tpe") override def companion: ValueCompanion = Constant @@ -311,7 +337,7 @@ object Values { case _ => false })) - override def hashCode(): Int = util.Arrays.deepHashCode(Array(value.asInstanceOf[AnyRef], tpe)) + override def hashCode(): Int = Arrays.deepHashCode(Array(value.asInstanceOf[AnyRef], tpe)) override def toString: String = tpe.asInstanceOf[SType] match { case SGroupElement if value.isInstanceOf[GroupElement] => @@ -329,7 +355,7 @@ object Values { object Constant extends FixedCostValueCompanion { override def opCode: OpCode = ConstantCode /** Cost of: returning value from Constant node. */ - override val costKind = FixedCost(5) + override val costKind = FixedCost(JitCost(5)) /** Immutable empty array, can be used to save allocations in many places. */ val EmptyArray = Array.empty[Constant[SType]] @@ -394,7 +420,7 @@ object Values { object ConstantPlaceholder extends ValueCompanion { override def opCode: OpCode = ConstantPlaceholderCode /** Cost of: accessing Constant in array by index. */ - override val costKind = FixedCost(1) + override val costKind = FixedCost(JitCost(1)) } trait NotReadyValue[S <: SType] extends Value[S] { @@ -417,19 +443,26 @@ object Values { object TaggedVariable extends ValueCompanion { override def opCode: OpCode = TaggedVariableCode - override def costKind: CostKind = FixedCost(1) + override def costKind: CostKind = FixedCost(JitCost(1)) def apply[T <: SType](varId: Byte, tpe: T): TaggedVariable[T] = TaggedVariableNode(varId, tpe) } - case class UnitConstant() extends EvaluatedValue[SUnit.type] { - override def tpe = SUnit - val value = () - override def companion: ValueCompanion = UnitConstant - } - object UnitConstant extends ValueCompanion { - override def opCode = UnitConstantCode - override def costKind = Constant.costKind + /** High-level interface to internal representation of Unit constants in ErgoTree. */ + object UnitConstant { + /** ErgoTree node that represent a literal of Unit type. It is global immutable value + * which should be reused wherever necessary to avoid allocations. + */ + val instance = apply() + + /** Constucts a fresh new instance of Unit literal node. */ + def apply() = Constant[SUnit.type]((), SUnit) + + /** Recognizer to pattern match on Unit constant literal nodes (aka Unit constants). */ + def unapply(node: SValue): Boolean = node match { + case ConstantNode(_, SUnit) => true + case _ => false + } } type BoolValue = Value[SBoolean.type] @@ -565,7 +598,9 @@ object Values { def TaggedBox(id: Byte): Value[SBox.type] = mkTaggedVariable(id, SBox) def TaggedAvlTree(id: Byte): Value[SAvlTree.type] = mkTaggedVariable(id, SAvlTree) + /** Base type for evaluated tree nodes of Coll type. */ trait EvaluatedCollection[T <: SType, C <: SCollection[T]] extends EvaluatedValue[C] { + /** Type descriptor of the collection elements. */ def elementType: T } @@ -667,9 +702,13 @@ object Values { override def tpe = SAvlTree } + /** ErgoTree node that represents the operation of obtaining the generator of elliptic curve group. + * The generator g of the group is an element of the group such that, when written + * multiplicative form, every element of the group is a power of g. + */ case object GroupGenerator extends EvaluatedValue[SGroupElement.type] with ValueCompanion { override def opCode: OpCode = OpCodes.GroupGeneratorCode - override val costKind = FixedCost(10) + override val costKind = FixedCost(JitCost(10)) override def tpe = SGroupElement override val value = SigmaDsl.GroupElement(CryptoConstants.dlogGroup.generator) override def companion = this @@ -695,12 +734,14 @@ object Values { } } + /** ErgoTree node which represents `true` literal. */ object TrueLeaf extends ConstantNode[SBoolean.type](true, SBoolean) with ValueCompanion { override def companion = this override def opCode: OpCode = TrueCode override def costKind: FixedCost = Constant.costKind } + /** ErgoTree node which represents `false` literal. */ object FalseLeaf extends ConstantNode[SBoolean.type](false, SBoolean) with ValueCompanion { override def companion = this override def opCode: OpCode = FalseCode @@ -773,6 +814,7 @@ object Values { val dhtSerializer = ProveDHTupleSerializer(ProveDHTuple.apply) val dlogSerializer = ProveDlogSerializer(ProveDlog.apply) + // TODO v5.x: control maxTreeDepth same as in deserialize override def serialize(data: SigmaBoolean, w: SigmaByteWriter): Unit = { w.put(data.opCode) data match { @@ -817,14 +859,14 @@ object Values { case ProveDiffieHellmanTupleCode => dhtSerializer.parse(r) case AndCode => val n = r.getUShort() - val children = ValueSerializer.newArray[SigmaBoolean](n) + val children = safeNewArray[SigmaBoolean](n) cfor(0)(_ < n, _ + 1) { i => children(i) = serializer.parse(r) } CAND(children) case OrCode => val n = r.getUShort() - val children = ValueSerializer.newArray[SigmaBoolean](n) + val children = safeNewArray[SigmaBoolean](n) cfor(0)(_ < n, _ + 1) { i => children(i) = serializer.parse(r) } @@ -832,7 +874,7 @@ object Values { case AtLeastCode => val k = r.getUShort() val n = r.getUShort() - val children = ValueSerializer.newArray[SigmaBoolean](n) + val children = safeNewArray[SigmaBoolean](n) cfor(0)(_ < n, _ + 1) { i => children(i) = serializer.parse(r) } @@ -849,18 +891,22 @@ object Values { } // TODO refactor: only Constant make sense to inherit from EvaluatedValue - case class Tuple(items: IndexedSeq[Value[SType]]) extends EvaluatedValue[STuple] with EvaluatedCollection[SAny.type, STuple] { + + /** ErgoTree node which converts a collection of expressions into a tuple of data values + * of different types. Each data value of the resulting collection is obtained by + * evaluating the corresponding expression in `items`. All items may have different + * types. + * + * @param items source collection of expressions + */ + case class Tuple(items: IndexedSeq[Value[SType]]) extends Value[STuple] { override def companion = Tuple - override def elementType = SAny - lazy val tpe = STuple(items.map(_.tpe)) - lazy val value = { // TODO coverage - val xs = items.cast[EvaluatedValue[SAny.type]].map(_.value) - Colls.fromArray(xs.toArray(SAny.classTag.asInstanceOf[ClassTag[SAny.WrappedType]]))(RType.AnyType) - } + override lazy val tpe = STuple(items.map(_.tpe)) + override def opType: SFunc = ??? protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { // in v5.0 version we support only tuples of 2 elements to be equivalent with v4.x if (items.length != 2) - error(s"Invalid tuple $this") + Interpreter.error(s"Invalid tuple $this") val item0 = items(0) val x = item0.evalTo[Any](env) @@ -880,15 +926,15 @@ object Values { object Tuple extends FixedCostValueCompanion { override def opCode: OpCode = TupleCode /** Cost of: 1) allocating a new tuple (of limited max size)*/ - override val costKind = FixedCost(15) + override val costKind = FixedCost(JitCost(15)) def apply(items: Value[SType]*): Tuple = Tuple(items.toIndexedSeq) } trait OptionValue[T <: SType] extends Value[SOption[T]] { } - // TODO HF (4h): SomeValue and NoneValue are not used in ErgoTree and can be - // either removed or implemented in v4.x + // TODO v6.0 (4h): SomeValue and NoneValue are not used in ErgoTree and can be + // either removed or implemented in v6.0 case class SomeValue[T <: SType](x: Value[T]) extends OptionValue[T] { override def companion = SomeValue val tpe = SOption(x.tpe) @@ -909,6 +955,13 @@ object Values { override def costKind: CostKind = Constant.costKind } + /** ErgoTree node which converts a collection of expressions into a collection of data + * values. Each data value of the resulting collection is obtained by evaluating the + * corresponding expression in `items`. All items must have the same type. + * + * @param items source collection of expressions + * @param elementType type descriptor of elements in the resulting collection + */ case class ConcreteCollection[V <: SType](items: Seq[Value[V]], elementType: V) extends EvaluatedCollection[V, SCollection[V]] { // TODO uncomment and make sure Ergo works with it, i.e. complex data types are never used for `items`. @@ -951,11 +1004,11 @@ object Values { Colls.fromArray(is) } } - object ConcreteCollection extends ValueCompanion { + object ConcreteCollection extends FixedCostValueCompanion { override def opCode: OpCode = ConcreteCollectionCode /** Cost of: allocating new collection * @see ConcreteCollection_PerItem */ - override val costKind = FixedCost(20) + override val costKind = FixedCost(JitCost(20)) def fromSeq[V <: SType](items: Seq[Value[V]])(implicit tV: V): ConcreteCollection[V] = ConcreteCollection(items, tV) @@ -1007,14 +1060,6 @@ object Values { } } - implicit class BoolValueOps(val b: BoolValue) extends AnyVal { - def toSigmaProp: SigmaPropValue = b match { - case b if b.tpe == SBoolean => BoolToSigmaProp(b) - case p if p.tpe == SSigmaProp => p.asSigmaProp - case _ => sys.error(s"Expected SBoolean or SSigmaProp typed value, but was: $b") - } - } - sealed trait BlockItem extends NotReadyValue[SType] { def id: Int def rhs: SValue @@ -1073,7 +1118,7 @@ object Values { object ValUse extends FixedCostValueCompanion { override def opCode: OpCode = ValUseCode /** Cost of: 1) Lookup in immutable HashMap by valId: Int 2) alloc of Some(v) */ - override val costKind = FixedCost(5) + override val costKind = FixedCost(JitCost(5)) } /** The order of ValDefs in the block is used to assign ids to ValUse(id) nodes @@ -1110,13 +1155,15 @@ object Values { } object BlockValue extends ValueCompanion { override def opCode: OpCode = BlockValueCode - override val costKind = PerItemCost(1, 1, 10) + override val costKind = PerItemCost( + baseCost = JitCost(1), perChunkCost = JitCost(1), chunkSize = 10) } /** * @param args parameters list, where each parameter has an id and a type. * @param body expression, which refers function parameters with ValUse. */ case class FuncValue(args: IndexedSeq[(Int,SType)], body: Value[SType]) extends NotReadyValue[SFunc] { + import FuncValue._ override def companion = FuncValue lazy val tpe: SFunc = { val nArgs = args.length @@ -1131,52 +1178,31 @@ object Values { protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { addCost(FuncValue.costKind) - if (args.length == 0) { - // TODO coverage - () => { - body.evalTo[Any](env) - } - } - else if (args.length == 1) { + if (args.length == 1) { val arg0 = args(0) (vArg: Any) => { Value.checkType(arg0._2, vArg) var env1: DataEnv = null - E.addFixedCost(FuncValue.AddToEnvironmentDesc_CostKind, - FuncValue.AddToEnvironmentDesc) { + E.addFixedCost(AddToEnvironmentDesc_CostKind, AddToEnvironmentDesc) { env1 = env + (arg0._1 -> vArg) } val res = body.evalTo[Any](env1) Value.checkType(body, res) res } - } - else { - // TODO coverage - (vArgs: Seq[Any]) => { - var env1 = env - val len = args.length - cfor(0)(_ < len, _ + 1) { i => - val id = args(i)._1 - val v = vArgs(i) - E.addFixedCost(FuncValue.AddToEnvironmentDesc_CostKind, - FuncValue.AddToEnvironmentDesc) { - env1 = env1 + (id -> v) - } - } - body.evalTo[Any](env1) - } + } else { + Interpreter.error(s"Function must have 1 argument, but was: $this") } } } object FuncValue extends FixedCostValueCompanion { val AddToEnvironmentDesc = NamedDesc("AddToEnvironment") /** Cost of: adding value to evaluator environment */ - val AddToEnvironmentDesc_CostKind = FixedCost(5) + val AddToEnvironmentDesc_CostKind = FixedCost(JitCost(5)) override def opCode: OpCode = FuncValueCode /** Cost of: 1) switch on the number of args 2) allocating a new Scala closure * Old cost: ("Lambda", "() => (D1) => R", lambdaCost),*/ - override val costKind = FixedCost(5) + override val costKind = FixedCost(JitCost(5)) def apply(argId: Int, tArg: SType, body: SValue): FuncValue = FuncValue(IndexedSeq((argId,tArg)), body) } diff --git a/sigmastate/src/main/scala/sigmastate/eval/BigIntegerOps.scala b/sigmastate/src/main/scala/sigmastate/eval/BigIntegerOps.scala index 9ff7c033dc..477c80fdc5 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/BigIntegerOps.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/BigIntegerOps.scala @@ -79,11 +79,24 @@ object NumericOps { override def minus(x: BigInt, y: BigInt): BigInt = n.minus(x, y) override def times(x: BigInt, y: BigInt): BigInt = n.times(x, y) - override def quot(x: BigInt, y: BigInt): BigInt = - ??? // this method should not be used in v4.x + override def quot(x: BigInt, y: BigInt): BigInt = x.divide(y) - override def divisionRemainder(x: BigInt, y: BigInt): BigInt = - ??? // this method should not be used in v4.x + /** This method is used in ErgoTreeEvaluator based interpreter, to implement + * '%' operation of ErgoTree (i.e. `%: (T, T) => T` operation) for all + * numeric types T including BigInt. + * + * In the v4.x interpreter, however, the `%` operation is implemented using + * [[CBigInt]].mod method (see implementation in [[TestBigInt]], which + * delegates to [[java.math.BigInteger]].mod method. + * + * Even though this method is called `divisionRemainder`, the semantics of ErgoTree + * language requires it to correspond to [[java.math.BigInteger]].mod method. + * + * For this reason we define implementation of this method using [[BigInt]].mod. + * + * NOTE: This method should not be used in v4.x + */ + override def divisionRemainder(x: BigInt, y: BigInt): BigInt = x.mod(y) } /** The instance of [[scalan.ExactOrdering]] typeclass for [[BigInt]]. */ diff --git a/sigmastate/src/main/scala/sigmastate/eval/CompiletimeCosting.scala b/sigmastate/src/main/scala/sigmastate/eval/CompiletimeCosting.scala index d3cfcb962c..17e1103a0c 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/CompiletimeCosting.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/CompiletimeCosting.scala @@ -75,18 +75,6 @@ trait CompiletimeCosting extends RuntimeCosting { IR: IRContext => error(s"Invalid register name $regName in expression $sel", sel.sourceContext.toOption)) eval(mkExtractRegisterAs(box.asBox, reg, SOption(valType)).asValue[SOption[valType.type]]) - // opt.get => - case Select(nrv: Value[SOption[SType]]@unchecked, SOption.Get, _) => - eval(mkOptionGet(nrv)) - - // opt.getOrElse => - case Terms.Apply(Select(nrv: Value[SOption[SType]]@unchecked, SOption.GetOrElse, _), Seq(arg)) => - eval(mkOptionGetOrElse(nrv, arg)) - - // opt.isDefined => - case Select(nrv: Value[SOption[SType]]@unchecked, SOption.IsDefined, _) => - eval(mkOptionIsDefined(nrv)) - case sel @ Select(obj, field, _) if obj.tpe == SBox => (obj.asValue[SBox.type], field) match { case (box, SBox.Value) => eval(mkExtractAmount(box)) @@ -112,32 +100,9 @@ trait CompiletimeCosting extends RuntimeCosting { IR: IRContext => else eval(mkUpcast(numValue, tRes)) - case Terms.Apply(Select(col, SliceMethod.name, _), Seq(from, until)) => - eval(mkSlice(col.asValue[SCollection[SType]], from.asIntValue, until.asIntValue)) - - case Terms.Apply(Select(col, ExistsMethod.name, _), Seq(l)) if l.tpe.isFunc => - eval(mkExists(col.asValue[SCollection[SType]], l.asFunc)) - - case Terms.Apply(Select(col, ForallMethod.name, _), Seq(l)) if l.tpe.isFunc => - eval(mkForAll(col.asValue[SCollection[SType]], l.asFunc)) - - case Terms.Apply(Select(col, MapMethod.name, _), Seq(l)) if l.tpe.isFunc => - eval(mkMapCollection(col.asValue[SCollection[SType]], l.asFunc)) - - case Terms.Apply(Select(col, FoldMethod.name, _), Seq(zero, l @ Terms.Lambda(_, _, _, _))) => - eval(mkFold(col.asValue[SCollection[SType]], zero, l)) - case Terms.Apply(col, Seq(index)) if col.tpe.isCollection => eval(mkByIndex(col.asCollection[SType], index.asValue[SInt.type], None)) - case Select(input, ModQMethod.name, _) => - eval(mkModQ(input.asBigInt)) - - case Terms.Apply(Select(l, PlusModQMethod.name, _), Seq(r)) => - eval(mkPlusModQ(l.asBigInt, r.asBigInt)) - case Terms.Apply(Select(l, MinusModQMethod.name, _), Seq(r)) => - eval(mkMinusModQ(l.asBigInt, r.asBigInt)) - case _ => super.evalNode(ctx, env, node) } diff --git a/sigmastate/src/main/scala/sigmastate/eval/CostingDataContext.scala b/sigmastate/src/main/scala/sigmastate/eval/CostingDataContext.scala index 07d3705711..c91cfc2a32 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/CostingDataContext.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/CostingDataContext.scala @@ -1,8 +1,9 @@ package sigmastate.eval -import java.math.BigInteger -import java.util +import com.google.common.primitives.Ints +import java.math.BigInteger +import java.util.Arrays import org.bouncycastle.math.ec.ECPoint import org.ergoplatform.{ErgoBox, SigmaConstants} import org.ergoplatform.validation.ValidationRules @@ -346,12 +347,17 @@ case class CostingBox(isCost: Boolean, val ebox: ErgoBox) extends Box with Wrapp override def executeFromRegister[T](regId: Byte)(implicit cT: RType[T]): T = ??? // TODO implement - override def hashCode(): Int = id.toArray.hashCode() // TODO optimize using just 4 bytes of id (since it is already hash) + override def hashCode(): Int = Ints.fromByteArray(id.toArray) - override def equals(obj: Any): Boolean = (this eq obj.asInstanceOf[AnyRef]) || (obj != null && ( obj match { - case obj: Box => util.Arrays.equals(id.toArray, obj.id.toArray) - // TODO v5.0 HF: return false when obj in not Box instead of throwing an exception - })) + override def equals(obj: Any): Boolean = (this eq obj.asInstanceOf[AnyRef]) || (obj != null && { + obj match { + case obj: Box => Arrays.equals(id.toArray, obj.id.toArray) + case _ => + // this case was missing in v4.x, however has never been a problem + // Thus, v5.0 interpreter will not fail (while v4.x would fail here) + false + } + }) } object CostingBox { @@ -520,10 +526,10 @@ object CCostModel { private val AccessAvlTreeOpType: SFunc = SFunc(SContext, SAvlTree) private lazy val AccessAvlTreeCost: Int = costOf("AccessAvlTree", AccessAvlTreeOpType) - private val GetVarOpType: SFunc = SFunc(Array(SContext, SByte), SOption.ThisType) + private val GetVarOpType: SFunc = SFunc(SContext.ContextFuncDom, SOption.ThisType) private lazy val GetVarCost: Int = costOf("GetVar", GetVarOpType) - private val DeserializeVarOpType: SFunc = SFunc(Array(SContext, SByte), SOption.ThisType) + private val DeserializeVarOpType: SFunc = SFunc(SContext.ContextFuncDom, SOption.ThisType) private lazy val DeserializeVarCost: Int = costOf("DeserializeVar", DeserializeVarOpType) private val GetRegisterOpType: SFunc = SFunc(Array(SBox, SByte), SOption.ThisType) @@ -686,7 +692,7 @@ class CostingSigmaDslBuilder extends TestSigmaDslBuilder { dsl => override def substConstants[T](scriptBytes: Coll[Byte], positions: Coll[Int], newValues: Coll[T]): Coll[Byte] = { - val typedNewVals = newValues.toArray.map(v => TransformingSigmaBuilder.liftAny(v) match { + val typedNewVals = newValues.toArray.map(v => TransformingSigmaBuilder.liftToConstant(v) match { case Nullable(v) => v case _ => sys.error(s"Cannot evaluate substConstants($scriptBytes, $positions, $newValues): cannot lift value $v") }) @@ -717,10 +723,12 @@ case class CostingDataContext( outputs: Coll[Box], height: Int, selfBox: Box, + private val selfIndex: Int, lastBlockUtxoRootHash: AvlTree, _minerPubKey: Coll[Byte], vars: Coll[AnyValue], override val activatedScriptVersion: Byte, + override val currentErgoTreeVersion: Byte, var isCost: Boolean) extends Context { @inline override def builder: SigmaDslBuilder = CostingSigmaDslBuilder @@ -739,18 +747,16 @@ case class CostingDataContext( @inline override def minerPubKey = _minerPubKey - - private def findSelfBoxIndex: Int = { - var i = 0 - while (i < inputs.length) { - if (inputs(i) eq selfBox) return i - i += 1 + override def selfBoxIndex: Int = { + if (VersionContext.current.isJitActivated) { + // starting from v5.0 this is fixed + selfIndex + } else { + // this used to be a bug in v4.x (https://github.com/ScorexFoundation/sigmastate-interpreter/issues/603) + -1 } - -1 } - override val selfBoxIndex: Int = findSelfBoxIndex - override def getVar[T](id: Byte)(implicit tT: RType[T]): Option[T] = { if (isCost) { val optV = diff --git a/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala b/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala index b36f5998e2..44fc14d8bc 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala @@ -6,12 +6,12 @@ import org.bouncycastle.math.ec.ECPoint import org.ergoplatform._ import org.ergoplatform.validation.ValidationRules.{CheckLoopLevelInCostFunction, CheckCostFuncOperation} import sigmastate._ -import sigmastate.Values.{Value, GroupElementConstant, SigmaBoolean, Constant} +import sigmastate.Values.{Value, SigmaBoolean, Constant} import scala.reflect.ClassTag import scala.util.Try import sigmastate.SType._ -import scalan.{Nullable, RType} +import scalan.RType import scalan.RType._ import sigma.types.PrimViewType import sigmastate.basics.DLogProtocol.ProveDlog diff --git a/sigmastate/src/main/scala/sigmastate/eval/Profiler.scala b/sigmastate/src/main/scala/sigmastate/eval/Profiler.scala index cfc3cba11c..04c89693bb 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/Profiler.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/Profiler.scala @@ -1,6 +1,6 @@ package sigmastate.eval -import sigmastate.{FixedCost, SMethod} +import sigmastate.{FixedCost, JitCost, SMethod} import sigmastate.Values.SValue import sigmastate.serialization.OpCodes import sigmastate.serialization.OpCodes.OpCode @@ -228,6 +228,15 @@ class Profiler { measuredTimeStat.addPoint(script, actualTimeNano) } + /** Adds estimated cost and actual measured time data point to the StatCollection for + * the given script. + */ + def addJitEstimation(script: String, cost: JitCost, actualTimeNano: Long) = { + addEstimation(script, cost.value, actualTimeNano) + } + + /** Suggests a cost value for a given operation time. + * @return suggested cost in JIT scale. */ def suggestCost(time: Long): Int = { ((time - 1) / 100 + 1).toInt } @@ -242,7 +251,7 @@ class Profiler { val opDesc = ser.opDesc val (opName, cost) = opDesc.costKind match { case FixedCost(c) if opDesc != MethodCall && opDesc != PropertyCall => - (opDesc.typeName, c) + (opDesc.typeName, c.value) case _ => ("", 0) } val suggestedCost = suggestCost(time) @@ -264,7 +273,7 @@ class Profiler { val (name, timePerItem, time, comment) = { val (time, count) = stat.mean val suggestedCost = suggestCost(time) - val warn = if (suggestedCost > ciKey.costItem.cost) "!!!" else "" + val warn = if (suggestedCost > ciKey.costItem.cost.value) "!!!" else "" ciKey.costItem match { case ci: FixedCostItem => val comment = s"count: $count, suggested: $suggestedCost, actCost: ${ci.cost}$warn" diff --git a/sigmastate/src/main/scala/sigmastate/eval/RuntimeCosting.scala b/sigmastate/src/main/scala/sigmastate/eval/RuntimeCosting.scala index f6ae303cf3..67bfa87b8f 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/RuntimeCosting.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/RuntimeCosting.scala @@ -977,11 +977,11 @@ trait RuntimeCosting extends CostingRules { IR: IRContext => implicit val tA = ct.tItem implicit val sizedA = Sized.typeToSized(tA) liftConst(Sized.sizeOf(x.asInstanceOf[special.collection.Coll[a]])) - case ct: OptionType[a] => // TODO cover with tests (1h) + case ct: OptionType[a] => // TODO cover with tests or remove in v5.x (1h) implicit val tA = ct.tA implicit val sizedA = Sized.typeToSized(tA) liftConst(Sized.sizeOf(x.asInstanceOf[Option[a]])) - case ct: PairType[a, b] => // TODO cover with tests (1h) + case ct: PairType[a, b] => // TODO cover with tests or remove in v5.x (1h) implicit val tA = ct.tFst implicit val tB = ct.tSnd implicit val sizedA = Sized.typeToSized(tA) diff --git a/sigmastate/src/main/scala/sigmastate/eval/Sized.scala b/sigmastate/src/main/scala/sigmastate/eval/Sized.scala index 47aefed55b..8d8690f258 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/Sized.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/Sized.scala @@ -46,7 +46,7 @@ object Sized extends SizedLowPriority { def sizeOf[T: Sized](x: T): Size[T] = Sized[T].size(x) /** Helper constructor to support Scala 2.11. */ - def instance[T](f: T => Size[T]) = new Sized[T] { + def instance[T](f: T => Size[T]): Sized[T] = new Sized[T] { override def size(x: T): Size[T] = f(x) } diff --git a/sigmastate/src/main/scala/sigmastate/eval/Zero.scala b/sigmastate/src/main/scala/sigmastate/eval/Zero.scala index a29ef6f769..15ecdd1cc5 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/Zero.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/Zero.scala @@ -51,8 +51,8 @@ object Zero extends ZeroLowPriority { case AvlTreeRType => Zero[AvlTree] case SigmaPropRType => sigmaPropIsZero case ct: CollType[a] => collIsZero(typeToZero(ct.tItem), ct.tItem) - case ct: OptionType[a] => optionIsZero(typeToZero(ct.tA)) // TODO cover with tests (2h) - case ct: PairType[a, b] => pairIsZero(typeToZero(ct.tFst), typeToZero(ct.tSnd)) // TODO cover with tests (1h) + case ct: OptionType[a] => optionIsZero(typeToZero(ct.tA)) // TODO cover with tests or remove in v5.x (2h) + case ct: PairType[a, b] => pairIsZero(typeToZero(ct.tFst), typeToZero(ct.tSnd)) // TODO cover with tests or remove in v5.x (1h) case _ => sys.error(s"Don't know how to compute Zero for type $t") }).asInstanceOf[Zero[T]] diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/CostAccumulator.scala b/sigmastate/src/main/scala/sigmastate/interpreter/CostAccumulator.scala index 62f42bca4f..b40aec83ca 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/CostAccumulator.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/CostAccumulator.scala @@ -1,15 +1,16 @@ package sigmastate.interpreter +import sigmastate.JitCost import sigmastate.lang.exceptions.CostLimitException /** Encapsulate simple monotonic (add only) counter with reset. */ -class CostCounter(val initialCost: Int) { - private var _currentCost: Int = initialCost +class CostCounter(val initialCost: JitCost) { + private var _currentCost: JitCost = initialCost - @inline def += (n: Int) = { - this._currentCost = java.lang.Math.addExact(this._currentCost, n) + @inline def += (n: JitCost) = { + this._currentCost = this._currentCost + n } - @inline def currentCost: Int = _currentCost + @inline def currentCost: JitCost = _currentCost @inline def resetCost() = { _currentCost = initialCost } } @@ -17,7 +18,7 @@ class CostCounter(val initialCost: Int) { * which correspond to lambdas and thunks. * It accepts messages: startScope(), endScope(), add(), reset() * At any time `totalCost` is the currently accumulated cost. */ -class CostAccumulator(initialCost: Int, costLimit: Option[Long]) { +class CostAccumulator(initialCost: JitCost, costLimit: Option[JitCost]) { @inline private def initialStack() = List(new Scope(initialCost)) private var _scopeStack: List[Scope] = initialStack @@ -29,10 +30,10 @@ class CostAccumulator(initialCost: Int, costLimit: Option[Long]) { * When the evaluation enters a new scope (e.g. calling a lambda) a new Scope instance is created and pushed * to _scopeStack, then is starts receiving `add` method calls. * When the evaluation leaves the scope, the top is popped off the stack. */ - class Scope(initialCost: Int) extends CostCounter(initialCost) { + class Scope(initialCost: JitCost) extends CostCounter(initialCost) { - @inline def add(opCost: Int): Unit = { + @inline def add(opCost: JitCost): Unit = { this += opCost } @@ -51,7 +52,7 @@ class CostAccumulator(initialCost: Int, costLimit: Option[Long]) { /** Called once for each operation of a scope (lambda or thunk). * @throws CostLimitException when current accumulated cost exceeds `costLimit` */ - def add(opCost: Int): Unit = { + def add(opCost: JitCost): Unit = { currentScope.add(opCost) // check that we are still withing the limit @@ -61,7 +62,7 @@ class CostAccumulator(initialCost: Int, costLimit: Option[Long]) { val accumulatedCost = currentScope.currentCost if (accumulatedCost > limit) { throw new CostLimitException( - accumulatedCost, CostLimitException.msgCostLimitError(accumulatedCost, limit), None) + accumulatedCost.value, CostLimitException.msgCostLimitError(accumulatedCost, limit), None) } } } @@ -72,5 +73,5 @@ class CostAccumulator(initialCost: Int, costLimit: Option[Long]) { } /** Returns total accumulated cost */ - @inline def totalCost: Int = currentScope.currentCost + @inline def totalCost: JitCost = currentScope.currentCost } diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/CostDetails.scala b/sigmastate/src/main/scala/sigmastate/interpreter/CostDetails.scala index cb3ae724e4..e7585a7bb3 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/CostDetails.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/CostDetails.scala @@ -1,11 +1,13 @@ package sigmastate.interpreter +import sigmastate.JitCost +import spire.syntax.all.cfor import scala.collection.mutable /** Abstract representation of cost results obtained during evaluation. */ abstract class CostDetails { /** The total cost of evaluation. */ - def cost: Int + def cost: JitCost /** The trace of costed operations performed during evaluation. */ def trace: Seq[CostItem] /** Actual execution time (in nanoseconds) if defined. */ @@ -20,7 +22,14 @@ abstract class CostDetails { case class TracedCost(trace: Seq[CostItem], actualTimeNano: Option[Long] = None) extends CostDetails { /** Total cost of all cost items. */ - def cost: Int = trace.foldLeft(0)(_ + _.cost) + def cost: JitCost = { + val n = trace.length + var res = JitCost(0) + cfor(0)(_ < n, _ + 1) { i => + res += trace(i).cost + } + res + } } /** Result of cost evaluation represented using simple given value. @@ -28,9 +37,9 @@ case class TracedCost(trace: Seq[CostItem], * @param cost the given value of the total cost * @param actualTimeNano measured time of execution (if some) */ -case class GivenCost(cost: Int, +case class GivenCost(cost: JitCost, actualTimeNano: Option[Long] = None) extends CostDetails { - /** The trait is empty for this representation of CostDetails. + /** The trace is empty for this representation of CostDetails. */ override def trace: Seq[CostItem] = mutable.WrappedArray.empty } @@ -48,7 +57,7 @@ object CostDetails { /** Helper recognizer to work with different representations of costs in patterns * uniformly. */ - def unapply(d: CostDetails): Option[(Int, Seq[CostItem])] = d match { + def unapply(d: CostDetails): Option[(JitCost, Seq[CostItem])] = d match { case TracedCost(t, _) => Some((d.cost, t)) case GivenCost(c, _) => Some((c, EmptyTrace)) case _ => None diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/CostItem.scala b/sigmastate/src/main/scala/sigmastate/interpreter/CostItem.scala index f6479fc315..27b50779cf 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/CostItem.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/CostItem.scala @@ -1,13 +1,13 @@ package sigmastate.interpreter -import sigmastate.{FixedCost, PerItemCost, SMethod, SType, TypeBasedCost} +import sigmastate.{FixedCost, JitCost, PerItemCost, SMethod, SType, TypeBasedCost} import sigmastate.Values.{FixedCostValueCompanion, PerItemCostValueCompanion, ValueCompanion} import sigmastate.lang.Terms.MethodCall /** An item in the cost accumulation trace of a [[sigmastate.Values.ErgoTree]] evaluation. */ abstract class CostItem { def opName: String - def cost: Int + def cost: JitCost } /** An item in the cost accumulation trace of a [[sigmastate.Values.ErgoTree]] evaluation. @@ -18,7 +18,7 @@ abstract class CostItem { */ case class FixedCostItem(opDesc: OperationDesc, costKind: FixedCost) extends CostItem { override def opName: String = opDesc.operationName - override def cost: Int = costKind.cost + override def cost: JitCost = costKind.cost } object FixedCostItem { def apply(companion: FixedCostValueCompanion): FixedCostItem = { @@ -45,7 +45,7 @@ case class TypeBasedCostItem( val name = opDesc.operationName s"$name[$tpe]" } - override def cost: Int = costKind.costFunc(tpe) + override def cost: JitCost = costKind.costFunc(tpe) override def equals(obj: Any): Boolean = (this eq obj.asInstanceOf[AnyRef]) || (obj != null && (obj match { case that: TypeBasedCostItem => @@ -71,7 +71,7 @@ object TypeBasedCostItem { case class SeqCostItem(opDesc: OperationDesc, costKind: PerItemCost, nItems: Int) extends CostItem { override def opName: String = opDesc.operationName - override def cost: Int = costKind.cost(nItems) + override def cost: JitCost = costKind.cost(nItems) /** How many data chunks in this cost item. */ def chunks: Int = costKind.chunks(nItems) } @@ -88,7 +88,7 @@ object SeqCostItem { */ case class MethodCallCostItem(items: CostDetails) extends CostItem { override def opName: String = MethodCall.typeName - override def cost: Int = items.cost + override def cost: JitCost = items.cost } diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/CostingUtils.scala b/sigmastate/src/main/scala/sigmastate/interpreter/CostingUtils.scala new file mode 100644 index 0000000000..2d6481c053 --- /dev/null +++ b/sigmastate/src/main/scala/sigmastate/interpreter/CostingUtils.scala @@ -0,0 +1,65 @@ +package sigmastate.interpreter + +import sigmastate.interpreter.Interpreter.{JitReductionResult, AotReductionResult} + +object CostingUtils { + /** Checks that a jitRes is equal to res and also checks costs. + * Reports either error or warning to the console. + * + * @param ergoTreeHex used to compose the error message + * @param aotRes reduction result produced by AOT interpreter + * @param jitRes reduction result produced by JIT interpreter + * @param logger logger function + * @param es evaluation settings currently used by the interpreter + */ + def checkResults( + ergoTreeHex: String, + aotRes: AotReductionResult, jitRes: JitReductionResult, + logger: String => Unit)(implicit es: EvalSettings): Unit = { + val aotValue = aotRes.value + val jitValue = jitRes.value + if (aotValue != jitValue) { + val msg = + s"""Wrong JIT result: ----------------------------------------- + |ErgoTree: $ergoTreeHex + |AOT result: $aotValue + |JIT result: $jitValue + |------------------------------------------------------------""".stripMargin + if (es.isTestRun) { + Interpreter.error(msg) + } + else if (es.isLogEnabled) { + logger(msg) + } + } + checkCosts(ergoTreeHex, aotRes.cost, jitRes.cost, logger) + } + + /** Checks that newCost doesn't exceed oldCost. + * If it exceeds, then: + * - in test context - throws an error + * - in non-test context - sends a warning to the logger. + * + * @param ergoTreeHex used to compose the error message + * @param es evaluation settings currently used by the interpreter + */ + def checkCosts( + ergoTreeHex: String, + aotCost: Long, jitCost: Long, + logger: String => Unit)(implicit es: EvalSettings): Unit = { + if (aotCost < jitCost) { + val msg = + s"""Wrong JIT cost: ----------------------------------------- + |ErgoTree: $ergoTreeHex + |AOT cost: $aotCost + |JIT cost: $jitCost + |------------------------------------------------------------""".stripMargin + if (es.isTestRun) { + Interpreter.error(msg) + } + else if (es.isLogEnabled) { + logger(msg) + } + } + } +} diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/ErgoTreeEvaluator.scala b/sigmastate/src/main/scala/sigmastate/interpreter/ErgoTreeEvaluator.scala index a531263d3f..c553def256 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/ErgoTreeEvaluator.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/ErgoTreeEvaluator.scala @@ -2,15 +2,16 @@ package sigmastate.interpreter import org.ergoplatform.ErgoLikeContext import org.ergoplatform.SigmaConstants.ScriptCostLimit -import sigmastate.{FixedCost, PerItemCost, SType, TypeBasedCost} +import sigmastate.{FixedCost, JitCost, PerItemCost, SType, TypeBasedCost, VersionContext} import sigmastate.Values._ import sigmastate.eval.Profiler import sigmastate.interpreter.ErgoTreeEvaluator.DataEnv -import sigmastate.interpreter.Interpreter.ReductionResult +import sigmastate.interpreter.Interpreter.JitReductionResult import special.sigma.{Context, SigmaProp} import scalan.util.Extensions._ -import sigmastate.lang.Terms.MethodCall -import spire.syntax.all.cfor +import sigmastate.interpreter.EvalSettings._ +import supertagged.TaggedType +import debox.{Buffer => DBuffer} import scala.collection.mutable import scala.util.DynamicVariable @@ -35,7 +36,50 @@ case class EvalSettings( * In such a case, additional operations may be performed (such as sanity checks). */ isTestRun: Boolean = false, /** If true, then expected test vectors are pretty-printed. */ - printTestVectors: Boolean = false) + printTestVectors: Boolean = false, + /** When Some(mode) is specified then it defines which version of the Interpreter.verify + * and Interpreter.prove methods should use. + * The default value is None, which means the version is defined by ErgoTree.version + * and Context.activatedScriptVersion. + */ + evaluationMode: Option[EvaluationMode] = None) + +object EvalSettings { + /** Enumeration type of evaluation modes of [[Interpreter]]. + * This type can be removed in v5.x releases together with AOT implementation once v5.0 + * protocol is activated. + */ + object EvaluationMode extends TaggedType[Int] { + implicit class EvaluationModeOps(val x: EvaluationMode) extends AnyVal { + def name: String = x match { + case AotEvaluationMode => "AotEvaluationMode" + case JitEvaluationMode => "JitEvaluationMode" + } + + /** Returns true if AOT interpreter should be evaluated. */ + def okEvaluateAot: Boolean = { + x == AotEvaluationMode + } + + /** Returns true if JIT interpreter should be evaluated. */ + def okEvaluateJit: Boolean = { + x == JitEvaluationMode + } + } + } + type EvaluationMode = EvaluationMode.Type + + /** Evaluation mode when the interpreter is executing using AOT costing implementation + * of v4.x protocol. */ + val AotEvaluationMode: EvaluationMode = EvaluationMode @@ 1 // first bit + + /** Evaluation mode when the interpreter is executing using JIT costing implementation + * of v5.x protocol. */ + val JitEvaluationMode: EvaluationMode = EvaluationMode @@ 2 // second bit +} + +/** Result of JITC evaluation with costing. */ +case class JitEvalResult[A](value: A, cost: JitCost) /** Implements a simple and fast direct-style interpreter of ErgoTrees. * @@ -82,6 +126,7 @@ class ErgoTreeEvaluator( /** Evaluates the given expression in the given data environment. */ def eval(env: DataEnv, exp: SValue): Any = { + VersionContext.checkVersions(context.activatedScriptVersion, context.currentErgoTreeVersion) ErgoTreeEvaluator.currentEvaluator.withValue(this) { exp.evalTo[Any](env)(this) } @@ -92,18 +137,31 @@ class ErgoTreeEvaluator( * @return the value of the expression and the total accumulated cost in the coster. * The returned cost includes the initial cost accumulated in the `coster` * prior to calling this method. */ - def evalWithCost(env: DataEnv, exp: SValue): (Any, Int) = { + def evalWithCost[A](env: DataEnv, exp: SValue): JitEvalResult[A] = { val res = eval(env, exp) val cost = coster.totalCost - (res, cost) + JitEvalResult(res.asInstanceOf[A], cost) } /** Trace of cost items accumulated during execution of `eval` method. Call - * [[scala.collection.mutable.ArrayBuffer.clear()]] before each `eval` invocation. */ - private lazy val costTrace = { - val b = mutable.ArrayBuilder.make[CostItem] - b.sizeHint(1000) - b + * `clearTrace` method before each `eval` invocation. */ + private lazy val costTrace: DBuffer[CostItem] = { + DBuffer.ofSize[CostItem](1000) + } + + /** Returns currently accumulated JIT cost in this evaluator. */ + def getAccumulatedCost: JitCost = coster.totalCost + + /** Returns the currently accumulated trace of cost items in this evaluator. + * A new array is allocated and returned, the evaluator state is unaffected. + */ + def getCostTrace(): Seq[CostItem] = { + costTrace.toArray() + } + + /** Clears the accumulated trace of this evaluator. */ + def clearTrace() = { + costTrace.clear() } /** Adds the given cost to the `coster`. If tracing is enabled, associates the cost with @@ -314,17 +372,6 @@ object ErgoTreeEvaluator { isMeasureOperationTime = false, isMeasureScriptTime = false) - /** Helper method to compute cost details for the given method call. */ - def calcCost(mc: MethodCall, obj: Any, args: Array[Any]) - (implicit E: ErgoTreeEvaluator): CostDetails = { - // add approximated cost of invoked method (if specified) - val cost = mc.method.costFunc match { - case Some(costFunc) => costFunc(E, mc, obj, args) - case _ => CostDetails.ZeroCost // TODO v5.0: throw exception if not defined - } - cost - } - /** Evaluator currently is being executed on the current thread. * This variable is set in a single place, specifically in the `eval` method of * [[ErgoTreeEvaluator]]. @@ -343,13 +390,68 @@ object ErgoTreeEvaluator { * [[sigmastate.FiatShamirTree]] where cost-aware code blocks are used. */ def forProfiling(profiler: Profiler, evalSettings: EvalSettings): ErgoTreeEvaluator = { - val acc = new CostAccumulator(0, Some(ScriptCostLimit.value)) + val acc = new CostAccumulator( + initialCost = JitCost(0), + costLimit = Some(JitCost.fromBlockCost(ScriptCostLimit.value))) new ErgoTreeEvaluator( context = null, constants = mutable.WrappedArray.empty, acc, profiler, evalSettings.copy(profilerOpt = Some(profiler))) } + /** Executes [[FixedCost]] code `block` and use the given evaluator `E` to perform + * profiling and cost tracing. + * This helper method allows implementation of cost-aware code blocks by using + * thread-local instance of [[ErgoTreeEvaluator]]. + * If the `currentEvaluator` [[DynamicVariable]] is not initialized (equals to null), + * then the block is executed with minimal overhead. + * + * @param costInfo operation descriptor + * @param block block of code to be executed (given as lazy by-name argument) + * @param E evaluator to be used (or null if it is not available on the + * current thread), in which case the method is equal to the + * `block` execution. + * @return result of code block execution + * HOTSPOT: don't beautify the code + * Note, `null` is used instead of Option to avoid allocations. + */ + def fixedCostOp[R <: AnyRef](costInfo: OperationCostInfo[FixedCost]) + (block: => R)(implicit E: ErgoTreeEvaluator): R = { + if (E != null) { + var res: R = null.asInstanceOf[R] + E.addFixedCost(costInfo) { + res = block + } + res + } else + block + } + + /** Executes [[PerItemCost]] code `block` and use the given evaluator `E` to perform + * profiling and cost tracing. + * This helper method allows implementation of cost-aware code blocks by using + * thread-local instance of [[ErgoTreeEvaluator]]. + * If the `currentEvaluator` [[DynamicVariable]] is not initialized (equals to null), + * then the block is executed with minimal overhead. + * + * @param costInfo operation descriptor + * @param nItems number of data items in the operation + * @param block block of code to be executed (given as lazy by-name argument) + * @param E evaluator to be used (or null if it is not available on the + * current thread), in which case the method is equal to the + * `block` execution. + * @return result of code block execution + * HOTSPOT: don't beautify the code + * Note, `null` is used instead of Option to avoid allocations. + */ + def perItemCostOp[R](costInfo: OperationCostInfo[PerItemCost], nItems: Int) + (block: () => R)(implicit E: ErgoTreeEvaluator): R = { + if (E != null) { + E.addSeqCost(costInfo, nItems)(block) + } else + block() + } + /** Evaluate the given [[ErgoTree]] in the given Ergo context using the given settings. * The given ErgoTree is evaluated as-is and is not changed during evaluation. * @@ -358,7 +460,7 @@ object ErgoTreeEvaluator { * @param evalSettings evaluation settings * @return a sigma protocol proposition (as [[SigmaBoolean]]) and accumulated JIT cost estimation. */ - def evalToCrypto(context: ErgoLikeContext, ergoTree: ErgoTree, evalSettings: EvalSettings): ReductionResult = { + def evalToCrypto(context: ErgoLikeContext, ergoTree: ErgoTree, evalSettings: EvalSettings): JitReductionResult = { val (res, cost) = eval(context, ergoTree.constants, ergoTree.toProposition(replaceConstants = false), evalSettings) val sb = res match { case sp: SigmaProp => @@ -366,7 +468,7 @@ object ErgoTreeEvaluator { case sb: SigmaBoolean => sb case _ => error(s"Expected SigmaBoolean but was: $res") } - ReductionResult(sb, cost) + JitReductionResult(sb, cost) } /** Evaluate the given expression in the given Ergo context using the given settings. @@ -384,7 +486,9 @@ object ErgoTreeEvaluator { constants: Seq[Constant[SType]], exp: SValue, evalSettings: EvalSettings): (Any, Int) = { - val costAccumulator = new CostAccumulator(context.initCost.toIntExact, Some(context.costLimit)) + val costAccumulator = new CostAccumulator( + initialCost = JitCost.fromBlockCost(context.initCost.toIntExact), + costLimit = Some(JitCost.fromBlockCost(context.costLimit.toIntExact))) val sigmaContext = context.toSigmaContext(isCost = false) eval(sigmaContext, costAccumulator, constants, exp, evalSettings) } @@ -409,7 +513,7 @@ object ErgoTreeEvaluator { val evaluator = new ErgoTreeEvaluator( sigmaContext, constants, costAccumulator, DefaultProfiler, evalSettings) val res = evaluator.eval(Map(), exp) - val cost = costAccumulator.totalCost + val cost = costAccumulator.totalCost.toBlockCost // scale to block cost (res, cost) } diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala b/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala index 4fafa5ef0d..b702f64c5a 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala @@ -1,46 +1,92 @@ package sigmastate.interpreter -import java.lang.{Math => JMath} import java.util import org.bitbucket.inkytonik.kiama.rewriting.Rewriter.{everywherebu, rule, strategy} import org.bitbucket.inkytonik.kiama.rewriting.Strategy +import org.ergoplatform.ErgoLikeContext import org.ergoplatform.validation.SigmaValidationSettings import org.ergoplatform.validation.ValidationRules._ -import scalan.util.BenchmarkUtil import scorex.util.ScorexLogging import sigmastate.SCollection.SByteArray import sigmastate.Values._ -import sigmastate.basics.DLogProtocol.{DLogInteractiveProver, FirstDLogProverMessage} +import sigmastate.basics.DLogProtocol.{ProveDlog, DLogInteractiveProver, FirstDLogProverMessage} import sigmastate.basics._ -import sigmastate.eval.{Evaluation, IRContext} -import sigmastate.interpreter.Interpreter.{ScriptEnv, VerificationResult, WhenSoftForkReductionResult} -import sigmastate.lang.Terms.ValueOps -import sigmastate.lang.exceptions.{CostLimitException, InterpreterException} +import sigmastate.interpreter.Interpreter._ +import sigmastate.lang.exceptions.InterpreterException import sigmastate.serialization.{SigmaSerializer, ValueSerializer} -import sigmastate.utils.Helpers -import sigmastate.utils.Helpers._ import sigmastate.utxo.DeserializeContext import sigmastate.{SType, _} - -import scala.util.{Success, Try} - +import sigmastate.eval.{Evaluation, Profiler, IRContext} +import sigmastate.FiatShamirTree._ +import sigmastate.SigSerializer._ +import sigmastate.eval.Evaluation.addCostChecked +import sigmastate.interpreter.ErgoTreeEvaluator.fixedCostOp +import sigmastate.interpreter.EvalSettings._ +import sigmastate.utils.Helpers._ +import sigmastate.lang.Terms.ValueOps +import spire.syntax.all.cfor + +import scala.util.{Try, Success} + +/** Base (verifying) interpreter of ErgoTrees. + * Can perform: + * - ErgoTree evaluation (aka reduction) to sigma proposition (aka + * SigmaBoolean) in the given context. + * - verification of ErgoTree in the given context. + * + * NOTE: In version v5.0 this interpreter contains two alternative implementations. + * 1) Old implementation from v4.x which is based on AOT costing + * 2) New implementation added in v5.0 which is based on JIT costing (see methods + * with JITC suffix). + * + * Both implementations are equivalent in v5.0, but have different performance + * as result they produce different cost estimations. + * + * The interpreter has evaluationMode which defines how it should execute scripts. + * @see verify, fullReduction + */ trait Interpreter extends ScorexLogging { - import Interpreter.ReductionResult + import Interpreter.AotReductionResult type CTX <: InterpreterContext type ProofT = UncheckedTree - val IR: IRContext + protected val IR: IRContext import IR._ /** Processor instance which is used by this interpreter to execute ErgoTrees that * contain neither [[DeserializeContext]] nor [[sigmastate.utxo.DeserializeRegister]] * operations. */ - def precompiledScriptProcessor: PrecompiledScriptProcessor + protected def precompiledScriptProcessor: PrecompiledScriptProcessor + + /** Evaluation settings used by [[ErgoTreeEvaluator]] which is used by this + * interpreter to perform fullReduction. + */ + protected def evalSettings: EvalSettings = ErgoTreeEvaluator.DefaultEvalSettings + + /** Logs the given message string. Can be overridden in the derived interpreter classes + * to redefine the default behavior. */ + protected def logMessage(msg: String) = { + println(msg) + } + + /** The cost of Value[T] deserialization is O(n), where n is the length of its bytes + * array. To evaluate [[DeserializeContext]] and + * [[sigmastate.utxo.DeserializeRegister]] we add the following cost of deserialization + * for each byte. + */ + val CostPerByteDeserialized = 2 + + /** The cost of substituting [[DeserializeContext]] and + * [[sigmastate.utxo.DeserializeRegister]] nodes with the deserialized expression is + * O(n), where n is the number of bytes in ErgoTree. + * The following is the cost added for each ErgoTree.bytes. + */ + val CostPerTreeByte = 2 /** Deserializes given script bytes using ValueSerializer (i.e. assuming expression tree format). * It also measures tree complexity adding to the total estimated cost of script execution. @@ -51,11 +97,17 @@ trait Interpreter extends ScorexLogging { * NOTE: While ErgoTree is always of type SigmaProp, ValueSerializer can serialize expression of any type. * So it cannot be replaced with ErgoTreeSerializer here. */ - def deserializeMeasured(context: CTX, scriptBytes: Array[Byte]): (CTX, Value[SType]) = { + protected def deserializeMeasured(context: CTX, scriptBytes: Array[Byte]): (CTX, Value[SType]) = { val r = SigmaSerializer.startReader(scriptBytes) - r.complexity = 0 - val script = ValueSerializer.deserialize(r) // Why ValueSerializer? read NOTE above - val scriptComplexity = r.complexity + val (script, scriptComplexity) = if (VersionContext.current.isJitActivated) { + val script = ValueSerializer.deserialize(r) // Why ValueSerializer? read NOTE above + val cost = java7.compat.Math.multiplyExact(scriptBytes.length, CostPerByteDeserialized) + (script, cost) + } else { + r.complexity = 0 + val script = ValueSerializer.deserialize(r) // Why ValueSerializer? read NOTE above + (script, r.complexity) + } val currCost = Evaluation.addCostChecked(context.initCost, scriptComplexity, context.costLimit) val ctx1 = context.withInitCost(currCost).asInstanceOf[CTX] @@ -63,7 +115,7 @@ trait Interpreter extends ScorexLogging { } /** @param updateContext call back to setup new context (with updated cost limit) to be passed next time */ - def substDeserialize(context: CTX, updateContext: CTX => Unit, node: SValue): Option[SValue] = node match { + protected def substDeserialize(context: CTX, updateContext: CTX => Unit, node: SValue): Option[SValue] = node match { case d: DeserializeContext[_] => if (context.extension.values.contains(d.id)) context.extension.values(d.id) match { @@ -82,11 +134,9 @@ trait Interpreter extends ScorexLogging { case _ => None } - class MutableCell[T](var value: T) - /** Extracts proposition for ErgoTree handing soft-fork condition. * @note soft-fork handler */ - def propositionFromErgoTree(ergoTree: ErgoTree, context: CTX): SigmaPropValue = { + protected def propositionFromErgoTree(ergoTree: ErgoTree, context: CTX): SigmaPropValue = { val validationSettings = context.validationSettings val prop = ergoTree.root match { case Right(_) => @@ -102,7 +152,7 @@ trait Interpreter extends ScorexLogging { /** Substitute Deserialize* nodes with deserialized subtrees * We can estimate cost of the tree evaluation only after this step.*/ - def applyDeserializeContext(context: CTX, exp: Value[SType]): (BoolValue, CTX) = { + private def applyDeserializeContext(context: CTX, exp: Value[SType]): (BoolValue, CTX) = { val currContext = new MutableCell(context) val substRule = strategy[Any] { case x: SValue => substDeserialize(currContext.value, { ctx: CTX => currContext.value = ctx }, x) @@ -112,6 +162,20 @@ trait Interpreter extends ScorexLogging { (res, currContext.value) } + /** Same as applyDeserializeContext, but returns SigmaPropValue instead of BoolValue. + * This is necessary because new interpreter, while ultimately produces the same + * results as the old interpreter, it is implemented differently internally. + */ + private def applyDeserializeContextJITC(context: CTX, exp: Value[SType]): (SigmaPropValue, CTX) = { + val currContext = new MutableCell(context) + val substRule = strategy[Any] { case x: SValue => + substDeserialize(currContext.value, { ctx: CTX => currContext.value = ctx }, x) + } + val Some(substTree: SValue) = everywherebu(substRule)(exp) + val res = toValidScriptTypeJITC(substTree) + (res, currContext.value) + } + /** This method is used in both prover and verifier to compute SigmaBoolean value. * As the first step the cost of computing the `exp` expression in the given context is estimated. * If cost is above limit then exception is returned and `exp` is not executed @@ -124,12 +188,12 @@ trait Interpreter extends ScorexLogging { * @return result of script reduction * @see `ReductionResult` */ - def reduceToCrypto(context: CTX, env: ScriptEnv, exp: Value[SType]): Try[ReductionResult] = Try { + protected def reduceToCrypto(context: CTX, env: ScriptEnv, exp: Value[SType]): Try[AotReductionResult] = Try { import IR._ implicit val vs = context.validationSettings val maxCost = context.costLimit val initCost = context.initCost - trySoftForkable[ReductionResult](whenSoftFork = WhenSoftForkReductionResult(initCost)) { + trySoftForkable[AotReductionResult](whenSoftFork = WhenSoftForkReductionResult(initCost)) { val costingRes = doCostingEx(env, exp, true) val costF = costingRes.costF IR.onCostingResult(env, exp, costingRes) @@ -146,80 +210,250 @@ trait Interpreter extends ScorexLogging { CheckCalcFunc(IR)(calcF) val calcCtx = context.toSigmaContext(isCost = false) val res = Interpreter.calcResult(IR)(calcCtx, calcF) - ReductionResult(SigmaDsl.toSigmaBoolean(res), estimatedCost) + AotReductionResult(SigmaDsl.toSigmaBoolean(res), estimatedCost) } } - /** Helper convenience overload which uses empty environment. - * @see other overloads for details. + /** This method uses the new JIT costing with direct ErgoTree execution. It is used in + * both prover and verifier to compute SigmaProp value. + * As the first step the cost of computing the `exp` expression in the given context is + * estimated. + * If cost is above limit then exception is returned and `exp` is not executed + * else `exp` is computed in the given context and the resulting SigmaBoolean returned. + * + * @param context the context in which `exp` should be executed + * @param env environment of system variables used by the interpreter internally + * @param exp expression to be executed in the given `context` + * @return result of script reduction + * @see `ReductionResult` */ - def reduceToCrypto(context: CTX, exp: Value[SType]): Try[ReductionResult] = - reduceToCrypto(context, Interpreter.emptyEnv, exp) + protected def reduceToCryptoJITC(context: CTX, env: ScriptEnv, exp: SigmaPropValue): Try[JitReductionResult] = Try { + implicit val vs = context.validationSettings + trySoftForkable[JitReductionResult](whenSoftFork = WhenSoftForkJitReductionResult(context.initCost)) { + + val (resProp, cost) = { + val ctx = context.asInstanceOf[ErgoLikeContext] + ErgoTreeEvaluator.eval(ctx, ErgoTree.EmptyConstants, exp, evalSettings) match { + case (p: special.sigma.SigmaProp, c) => (p, c) + case (res, _) => + sys.error(s"Invalid result type of $res: expected SigmaProp when evaluating $exp") + } + } - /** - * Full reduction of initial expression given in the ErgoTree form to a SigmaBoolean value - * (which encodes whether a sigma-protocol proposition or a boolean value, so true or false). + JitReductionResult(SigmaDsl.toSigmaBoolean(resProp), cost.toLong) + } + } + + /** Full reduction of contract proposition given in the ErgoTree form to a SigmaBoolean value + * which encodes either a sigma-protocol proposition or a boolean (true or false) value. * * Works as follows: * 1) parse ErgoTree instance into a typed AST * 2) go bottom-up the tree to replace DeserializeContext nodes only - * 3) estimate cost and reduce the AST to a SigmaBoolean instance (so sigma-tree or trivial boolean value) + * 3) estimate cost and reduce the AST to a SigmaBoolean instance (either sigma-tree or + * trivial boolean value) * - * - * @param ergoTree - input ErgoTree expression to reduce - * @param context - context used in reduction - * @param env - script environment - * @return sigma boolean and the updated cost counter after reduction + * @param ergoTree input ErgoTree expression to reduce + * @param ctx context used in reduction + * @param env script environment + * @return reduction result as a pair of sigma boolean and the accumulated cost counter + * after reduction */ def fullReduction(ergoTree: ErgoTree, - context: CTX, - env: ScriptEnv): ReductionResult = { - val prop = propositionFromErgoTree(ergoTree, context) - prop match { - case SigmaPropConstant(p) => - val sb = SigmaDsl.toSigmaBoolean(p) - val cost = SigmaBoolean.estimateCost(sb) - val resCost = Evaluation.addCostChecked(context.initCost, cost, context.costLimit) - ReductionResult(sb, resCost) - case _ if !ergoTree.hasDeserialize => - val r = precompiledScriptProcessor.getReducer(ergoTree, context.validationSettings) - r.reduce(context) - case _ => - reductionWithDeserialize(prop, context, env) + ctx: CTX, + env: ScriptEnv): FullReductionResult = { + implicit val vs: SigmaValidationSettings = ctx.validationSettings + val context = ctx.withErgoTreeVersion(ergoTree.version).asInstanceOf[CTX] + VersionContext.withVersions(context.activatedScriptVersion, ergoTree.version) { + val prop = propositionFromErgoTree(ergoTree, context) + val evalMode = getEvaluationMode(context) + + val res = prop match { + case SigmaPropConstant(p) => + val sb = SigmaDsl.toSigmaBoolean(p) + + var aotRes: AotReductionResult = null + if (evalMode.okEvaluateAot) { + val aotCost = SigmaBoolean.estimateCost(sb) + val resAotCost = Evaluation.addCostChecked(context.initCost, aotCost, context.costLimit) + aotRes = AotReductionResult(sb, resAotCost) + } + + var jitRes: JitReductionResult = null + if (evalMode.okEvaluateJit) { + // NOTE, evaluator cost unit needs to be scaled to the cost unit of context + val jitCost = Eval_SigmaPropConstant.costKind.cost.toBlockCost + val resJitCost = Evaluation.addCostChecked(context.initCost, jitCost, context.costLimit) + jitRes = JitReductionResult(sb, resJitCost) + } + FullReductionResult(aotRes, jitRes) + case _ if !ergoTree.hasDeserialize => + var aotRes: AotReductionResult = null + if (evalMode.okEvaluateAot) { + val r = precompiledScriptProcessor.getReducer(ergoTree, context.validationSettings) + aotRes = r.reduce(context) + } + + var jitRes: JitReductionResult = null + if (evalMode.okEvaluateJit) { + val ctx = context.asInstanceOf[ErgoLikeContext] + jitRes = VersionContext.withVersions(ctx.activatedScriptVersion, ergoTree.version) { + ErgoTreeEvaluator.evalToCrypto(ctx, ergoTree, evalSettings) + } + } + FullReductionResult(aotRes, jitRes) + case _ => + reductionWithDeserialize(ergoTree, prop, context, env, evalMode) + } + res } } + /** Full reduction of contract proposition given in the ErgoTree form to a SigmaBoolean value + * which encodes either a sigma-protocol proposition or a boolean (true or false) value. + * See other overload for details. + */ + def fullReduction(ergoTree: ErgoTree, ctx: CTX): FullReductionResult = { + fullReduction(ergoTree, ctx, Interpreter.emptyEnv) + } + /** Performs reduction of proposition which contains deserialization operations. */ - private def reductionWithDeserialize(prop: SigmaPropValue, + private def reductionWithDeserialize(ergoTree: ErgoTree, + prop: SigmaPropValue, context: CTX, - env: ScriptEnv) = { + env: ScriptEnv, + evalMode: EvaluationMode): FullReductionResult = { implicit val vs: SigmaValidationSettings = context.validationSettings - val (propTree, context2) = trySoftForkable[(BoolValue, CTX)](whenSoftFork = (TrueLeaf, context)) { - applyDeserializeContext(context, prop) + var aotRes: AotReductionResult = null + if (evalMode.okEvaluateAot) { + val (propTree, context2) = trySoftForkable[(BoolValue, CTX)](whenSoftFork = (TrueLeaf, context)) { + applyDeserializeContext(context, prop) + } + + // here we assume that when `propTree` is TrueProp then `reduceToCrypto` always succeeds + // and the rest of the verification is also trivial + aotRes = reduceToCrypto(context2, env, propTree).getOrThrow } - // here we assume that when `propTree` is TrueProp then `reduceToCrypto` always succeeds - // and the rest of the verification is also trivial - reduceToCrypto(context2, env, propTree).getOrThrow + var jitRes: JitReductionResult = null + if (evalMode.okEvaluateJit) { + jitRes = VersionContext.withVersions(context.activatedScriptVersion, ergoTree.version) { + val deserializeSubstitutionCost = java7.compat.Math.multiplyExact(ergoTree.bytes.length, CostPerTreeByte) + val currCost = Evaluation.addCostChecked(context.initCost, deserializeSubstitutionCost, context.costLimit) + val context1 = context.withInitCost(currCost).asInstanceOf[CTX] + + val (propTree, context2) = trySoftForkable[(SigmaPropValue, CTX)](whenSoftFork = (TrueSigmaProp, context1)) { + applyDeserializeContextJITC(context, prop) + } + + // here we assume that when `propTree` is TrueProp then `reduceToCrypto` always succeeds + // and the rest of the verification is also trivial + reduceToCryptoJITC(context2, env, propTree).getOrThrow + } + } + + FullReductionResult(aotRes, jitRes) + } + + /** Adds the cost to verify sigma protocol proposition. + * This is AOT part of JITC-based interpreter, it predicts the cost of crypto + * verification, which is asymptotically much faster and protects from spam scripts. + * + * @param jitRes result of JIT-based reduction + * @param costLimit total cost limit to check and raise exception if exceeded + * @return computed jitRes.cost + crypto verification cost + */ + protected def addCryptoCost(jitRes: JitReductionResult, costLimit: Long) = { + val cryptoCost = estimateCryptoVerifyCost(jitRes.value).toBlockCost // scale JitCost to tx cost + + // Note, jitRes.cost is already scaled in fullReduction + val fullJitCost = addCostChecked(jitRes.cost, cryptoCost, costLimit) + fullJitCost + } + + /** Returns evaluation mode used by this interpreter in the given context. + * By default evaluation mode is determined based on `context.activatedScriptVersion` + * so that the interpreter works either as v4.x of v5.0. + * + * Alternatively, the required evaluation mode can be specified by giving Some(mode) + * value in `evalSettings.evaluationMode`, in which case the interpreter works as + * specified. + */ + protected def getEvaluationMode(context: CTX): EvaluationMode = { + evalSettings.evaluationMode.getOrElse { + if (context.activatedScriptVersion < VersionContext.JitActivationVersion) + AotEvaluationMode + else + JitEvaluationMode + } + } + + /** Checks the possible soft-fork condition. + * + * @param ergoTree contract which needs to be executed + * @param context evaluation context to use for detecting soft-fork condition + * @return `None`, if no soft-fork has been detected and ErgoTree execution can proceed normally + * `Some(true -> context.initCost)`, if soft-fork has been detected, but we + * cannot proceed with ErgoTree, however can accept relying on 90% of upgraded + * nodes (due to activation has already been done). + * @throws InterpreterException when cannot proceed and no activation yet. + */ + protected def checkSoftForkCondition(ergoTree: ErgoTree, context: CTX): Option[VerificationResult] = { + // TODO v6.0: the condition below should be revised if necessary + // The following conditions define behavior which depend on the version of ergoTree + // This works in addition to more fine-grained soft-forkability mechanism implemented + // using ValidationRules (see trySoftForkable method call here and in reduceToCrypto). + + if (context.activatedScriptVersion > VersionContext.MaxSupportedScriptVersion) { + // The activated protocol exceeds capabilities of this interpreter. + // NOTE: this path should never be taken for validation of candidate blocks + // in which case Ergo node should always pass Interpreter.MaxSupportedScriptVersion + // as the value of ErgoLikeContext.activatedScriptVersion. + // see also ErgoLikeContext ScalaDoc. + + // Currently more than 90% of nodes has already switched to a higher version, + // thus we can accept without verification, but only if we cannot verify + // the given ergoTree + if (ergoTree.version > VersionContext.MaxSupportedScriptVersion) { + // We accept the box spending and rely on 90% of all the other nodes. + // Thus, the old node will stay in sync with the network. + return Some(true -> context.initCost) + } + // otherwise, we can verify the box spending and thus, proceed normally + + } else { + // activated version is within the supported range [0..MaxSupportedScriptVersion] + // in addition, ErgoTree version should never exceed the currently activated protocol + + if (ergoTree.version > context.activatedScriptVersion) { + throw new InterpreterException( + s"ErgoTree version ${ergoTree.version} is higher than activated ${context.activatedScriptVersion}") + } + } + None // proceed normally } /** Executes the script in a given context. * Step 1: Deserialize context variables - * Step 2: Evaluate expression and produce SigmaProp value, which is zero-knowledge statement (see also `SigmaBoolean`). + * Step 2: Evaluate expression and produce SigmaProp value, which is zero-knowledge + * statement (see also `SigmaBoolean`). * Step 3: Verify that the proof is presented to satisfy SigmaProp conditions. * - * @param env environment of system variables used by the interpreter internally - * @param ergoTree ErgoTree expression to execute in the given context and verify its result - * @param context the context in which `exp` should be executed - * @param proof The proof of knowledge of the secrets which is expected by the resulting SigmaProp - * @param message message bytes, which are used in verification of the proof + * NOTE, ergoTree.complexity is not added to the cost when v5.0 is activated * - * @return verification result or Exception. - * If if the estimated cost of execution of the `exp` exceeds the limit (given in `context`), - * then exception if thrown and packed in Try. - * If left component is false, then: - * 1) script executed to false or - * 2) the given proof failed to validate resulting SigmaProp conditions. + * @param env environment of system variables used by the interpreter internally + * @param ergoTree ErgoTree expression to execute in the given context and verify its + * result + * @param context the context in which `exp` should be executed + * @param proof The proof of knowledge of the secrets which is expected by the + * resulting SigmaProp + * @param message message bytes, which are used in verification of the proof + * @return verification result or Exception. + * If if the estimated cost of execution of the `exp` exceeds the limit (given + * in `context`), then exception if thrown and packed in Try. + * If the first component is false, then: + * 1) script executed to false or + * 2) the given proof failed to validate resulting SigmaProp conditions. * @see `reduceToCrypto` */ def verify(env: ScriptEnv, @@ -227,64 +461,61 @@ trait Interpreter extends ScorexLogging { context: CTX, proof: Array[Byte], message: Array[Byte]): Try[VerificationResult] = { - val (res, t) = BenchmarkUtil.measureTime(Try { - // TODO v5.0: the condition below should be revised if necessary - // The following conditions define behavior which depend on the version of ergoTree - // This works in addition to more fine-grained soft-forkability mechanism implemented - // using ValidationRules (see trySoftForkable method call here and in reduceToCrypto). - if (context.activatedScriptVersion > Interpreter.MaxSupportedScriptVersion) { - // > 90% has already switched to higher version, accept without verification - // NOTE: this path should never be taken for validation of candidate blocks - // in which case Ergo node should always pass Interpreter.MaxSupportedScriptVersion - // as the value of ErgoLikeContext.activatedScriptVersion. - // see also ErgoLikeContext ScalaDoc. - return Success(true -> context.initCost) - } else { - // activated version is within the supported range [0..MaxSupportedScriptVersion] - // however - if (ergoTree.version > context.activatedScriptVersion) { - throw new InterpreterException( - s"ErgoTree version ${ergoTree.version} is higher than activated ${context.activatedScriptVersion}") - } - // else proceed normally + val res = Try { + checkSoftForkCondition(ergoTree, context) match { + case Some(resWhenSoftFork) => return Success(resWhenSoftFork) + case None => // proceed normally } - - val complexityCost = ergoTree.complexity.toLong - val initCost = Evaluation.addCostChecked(context.initCost, complexityCost, context.costLimit) - val contextWithCost = context.withInitCost(initCost).asInstanceOf[CTX] - - val res = fullReduction(ergoTree, contextWithCost, env) - - val checkingResult = res.value match { - case TrivialProp.TrueProp => true - case TrivialProp.FalseProp => false - case cProp => verifySignature(cProp, message, proof) - } - checkingResult -> res.cost - }) - if (outputComputedResults) { - res.foreach { case (_, cost) => - val scaledCost = cost * 1 // this is the scale factor of CostModel with respect to the concrete hardware - val timeMicro = t * 1000 // time in microseconds - val error = if (scaledCost > timeMicro) { - val error = ((scaledCost / timeMicro.toDouble - 1) * 100d).formatted(s"%10.3f") - error - } else { - val error = (-(timeMicro.toDouble / scaledCost.toDouble - 1) * 100d).formatted(s"%10.3f") - error + VersionContext.withVersions(context.activatedScriptVersion, ergoTree.version) { + val evalMode = getEvaluationMode(context) + evalMode match { + case AotEvaluationMode => + val complexityCost = ergoTree.complexity.toLong + val initCost = Evaluation.addCostChecked(context.initCost, complexityCost, context.costLimit) + val contextWithCost = context.withInitCost(initCost).asInstanceOf[CTX] + + val reduced = fullReduction(ergoTree, contextWithCost, env) + reduced.value match { + case TrivialProp.TrueProp => (true, reduced.cost) + case TrivialProp.FalseProp => (false, reduced.cost) + case _ => + val ok = if (evalSettings.isMeasureOperationTime) { + val E = ErgoTreeEvaluator.forProfiling(verifySignatureProfiler, evalSettings) + verifySignature(reduced.value, message, proof)(E) + } else { + verifySignature(reduced.value, message, proof)(null) + } + (ok, reduced.cost) + } + + case JitEvaluationMode => + // NOTE, ergoTree.complexity is not acrued to the cost in v5.0 + val reduced = fullReduction(ergoTree, context, env) + reduced.value match { + case TrivialProp.TrueProp => (true, reduced.cost) + case TrivialProp.FalseProp => (false, reduced.cost) + case _ => + val fullJitCost = addCryptoCost(reduced.jitRes, context.costLimit) + + val ok = if (evalSettings.isMeasureOperationTime) { + val E = ErgoTreeEvaluator.forProfiling(verifySignatureProfiler, evalSettings) + verifySignature(reduced.value, message, proof)(E) + } else { + verifySignature(reduced.value, message, proof)(null) + } + (ok, fullJitCost) + } } - val name = "\"" + env.getOrElse(Interpreter.ScriptNameProp, "") + "\"" - println(s"Name-Time-Cost-Error\t$name\t$timeMicro\t$scaledCost\t$error") } } res } // Perform Verifier Steps 4-6 - private def checkCommitments(sp: UncheckedSigmaTree, message: Array[Byte]): Boolean = { + private def checkCommitments(sp: UncheckedSigmaTree, message: Array[Byte])(implicit E: ErgoTreeEvaluator): Boolean = { // Perform Verifier Step 4 val newRoot = computeCommitments(sp).get.asInstanceOf[UncheckedSigmaTree] - val bytes = Helpers.concatArrays(FiatShamirTree.toBytes(newRoot), message) + val bytes = concatArrays(FiatShamirTree.toBytes(newRoot), message) /** * Verifier Steps 5-6: Convert the tree to a string `s` for input to the Fiat-Shamir hash function, * using the same conversion as the prover in 7 @@ -300,16 +531,22 @@ trait Interpreter extends ScorexLogging { * per the verifier algorithm of the leaf's Sigma-protocol. * If the verifier algorithm of the Sigma-protocol for any of the leaves rejects, then reject the entire proof. */ - val computeCommitments: Strategy = everywherebu(rule[Any] { + private[sigmastate] val computeCommitments: Strategy = everywherebu(rule[Any] { case c: UncheckedConjecture => c // Do nothing for internal nodes case sn: UncheckedSchnorr => - val a = DLogInteractiveProver.computeCommitment(sn.proposition, sn.challenge, sn.secondMessage) - sn.copy(commitmentOpt = Some(FirstDLogProverMessage(a))) + implicit val E = ErgoTreeEvaluator.getCurrentEvaluator + fixedCostOp(ComputeCommitments_Schnorr) { + val a = DLogInteractiveProver.computeCommitment(sn.proposition, sn.challenge, sn.secondMessage) + sn.copy(commitmentOpt = Some(FirstDLogProverMessage(a))) + } case dh: UncheckedDiffieHellmanTuple => - val (a, b) = DiffieHellmanTupleInteractiveProver.computeCommitment(dh.proposition, dh.challenge, dh.secondMessage) - dh.copy(commitmentOpt = Some(FirstDiffieHellmanTupleProverMessage(a, b))) + implicit val E = ErgoTreeEvaluator.getCurrentEvaluator + fixedCostOp(ComputeCommitments_DHT) { + val (a, b) = DiffieHellmanTupleInteractiveProver.computeCommitment(dh.proposition, dh.challenge, dh.secondMessage) + dh.copy(commitmentOpt = Some(FirstDiffieHellmanTupleProverMessage(a, b))) + } case _: UncheckedSigmaTree => ??? }) @@ -331,7 +568,6 @@ trait Interpreter extends ScorexLogging { verify(env, ergoTree, ctxv, proverResult.proof, message) } - def verify(ergoTree: ErgoTree, context: CTX, proof: ProofT, @@ -342,14 +578,17 @@ trait Interpreter extends ScorexLogging { /** * Verify a signature on given (arbitrary) message for a given public key. * - * @param sigmaTree - public key (represented as a tree) - * @param message - message - * @param signature - signature for the message - * @return - whether signature is valid or not + * @param sigmaTree public key (represented as a tree) + * @param message message + * @param signature signature for the message + * @param E optional evaluator (can be null) which is used for profiling of operations. + * When `E` is `null`, then profiling is turned-off and has no effect on + * the execution. + * @return whether signature is valid or not */ def verifySignature(sigmaTree: SigmaBoolean, message: Array[Byte], - signature: Array[Byte]): Boolean = { + signature: Array[Byte])(implicit E: ErgoTreeEvaluator): Boolean = { // Perform Verifier Steps 1-3 try { SigSerializer.parseAndComputeChallenges(sigmaTree, signature) match { @@ -360,9 +599,10 @@ trait Interpreter extends ScorexLogging { } } catch { case t: Throwable => - // TODO coverage: property("handle improper signature") doesn't lead to exception - // because the current implementation of parseAndComputeChallenges doesn't check - // signature format + // TODO cover with tests + // NOTE, property("handle improper signature") doesn't lead to exception + // because the current implementation of parseAndComputeChallenges doesn't throw + // an exception log.warn("Improper signature: ", t); false } @@ -377,12 +617,43 @@ object Interpreter { * The second component is the estimated cost of contract execution. */ type VerificationResult = (Boolean, Long) - /** Result of ErgoTree reduction procedure (see `reduceToCrypto` and friends). + /** Result of ErgoTree reduction procedure (see `fullReduction`) */ + abstract class ReductionResult { + /** The value of SigmaProp type which represents a logical statement verifiable via + * sigma protocol. + */ + def value: SigmaBoolean + + /** Estimated cost of the contract execution.*/ + def cost: Long + } + + /** Result of ErgoTree reduction procedure (see `fullReduction`). + * + * @param value the value of SigmaProp type which represents a logical statement + * verifiable via sigma protocol. + * @param cost the estimated cost of the contract execution. + */ + case class AotReductionResult(value: SigmaBoolean, cost: Long) extends ReductionResult + + /** Result of ErgoTree reduction procedure by JIT-based interpreter (see + * `reduceToCrypto` and friends). * * @param value the value of SigmaProp type which represents a logical statement * verifiable via sigma protocol. - * @param cost the estimated cost of the contract execution. */ - case class ReductionResult(value: SigmaBoolean, cost: Long) + * @param cost the estimated cost of the contract execution (in block's scale). + */ + case class JitReductionResult(value: SigmaBoolean, cost: Long) extends ReductionResult + + /** Result of fullReduction to sigma tree with costing. */ + case class FullReductionResult( + private[sigmastate] val aotRes: AotReductionResult, + private[sigmastate] val jitRes: JitReductionResult + ) extends ReductionResult { + require(aotRes != null ^ jitRes != null, s"Either AOT or JIT result must be defined, but not both: $this") + override def value: SigmaBoolean = if (aotRes != null) aotRes.value else jitRes.value + override def cost: Long = if (aotRes != null) aotRes.cost else jitRes.cost + } /** Represents properties of interpreter invocation. */ type ScriptEnv = Map[String, Any] @@ -393,21 +664,92 @@ object Interpreter { /** Property name used to store script name. */ val ScriptNameProp = "ScriptName" - /** Maximum version of ErgoTree supported by this interpreter release. - * See version bits in `ErgoTree.header` for more details. - * This value should be increased with each new protocol update via soft-fork. - * The following values are used for current and upcoming forks: - * - version 3.x this value must be 0 - * - in v4.0 must be 1 - * - in v5.x must be 2 - * etc. + /** The result of script reduction when soft-fork condition is detected by the old node, + * in which case the script is reduced to the trivial true proposition and takes up 0 cost. */ - val MaxSupportedScriptVersion: Byte = 1 // supported versions 0 and 1 + def WhenSoftForkReductionResult(cost: Long): AotReductionResult = AotReductionResult(TrivialProp.TrueProp, cost) /** The result of script reduction when soft-fork condition is detected by the old node, * in which case the script is reduced to the trivial true proposition and takes up 0 cost. */ - def WhenSoftForkReductionResult(cost: Long): ReductionResult = ReductionResult(TrivialProp.TrueProp, cost) + def WhenSoftForkJitReductionResult(cost: Long): JitReductionResult = JitReductionResult(TrivialProp.TrueProp, cost) + + /** Represents the cost of computing DLogInteractiveProver.computeCommitment. */ + final val ComputeCommitments_Schnorr = OperationCostInfo( + FixedCost(JitCost(3400)), NamedDesc("ComputeCommitments_Schnorr")) + + /** Represents the cost of computing DiffieHellmanTupleInteractiveProver.computeCommitment. */ + final val ComputeCommitments_DHT = OperationCostInfo( + FixedCost(JitCost(6450)), NamedDesc("ComputeCommitments_DHT")) + + /** Represents the cost spent by JIT evaluator on a simple ErgoTree containing + * SigmaPropConstant. + * It doesn't include cost of crypto verification. + */ + final val Eval_SigmaPropConstant = OperationCostInfo( + FixedCost(JitCost(50)), NamedDesc("Eval_SigmaPropConstant")) + + /** Verification cost of each ProveDlog node of SigmaBoolean proposition tree. */ + final val ProveDlogVerificationCost = + ParseChallenge_ProveDlog.costKind.cost + + ComputeCommitments_Schnorr.costKind.cost + + ToBytes_Schnorr.costKind.cost + + /** Verification cost of each ProveDHTuple node of SigmaBoolean proposition tree. */ + final val ProveDHTupleVerificationCost = + ParseChallenge_ProveDHT.costKind.cost + + ComputeCommitments_DHT.costKind.cost + + ToBytes_DHT.costKind.cost + + /** Computes the estimated cost of verification of sigma proposition. + * The cost is estimated ahead of time, without actually performing expencive crypto + * operations. + * @param sb sigma proposition + * @return estimated cost of verification of the given proposition in JIT scale + */ + def estimateCryptoVerifyCost(sb: SigmaBoolean): JitCost = { + /** Recursively compute the total cost of the given children. */ + def childrenCost(children: Seq[SigmaBoolean]): JitCost = { + val childrenArr = children.toArray + val nChildren = childrenArr.length + var sum = JitCost(0) + cfor(0)(_ < nChildren, _ + 1) { i => + val c = estimateCryptoVerifyCost(childrenArr(i)) + sum = sum + c + } + sum + } + sb match { + case _: ProveDlog => ProveDlogVerificationCost + case _: ProveDHTuple => ProveDHTupleVerificationCost + + case and: CAND => + val nodeC = ToBytes_ProofTreeConjecture.costKind.cost + val childrenC = childrenCost(and.children) + nodeC + childrenC + + case or: COR => + val nodeC = ToBytes_ProofTreeConjecture.costKind.cost + val childrenC = childrenCost(or.children) + nodeC + childrenC + + case th: CTHRESHOLD => + val nChildren = th.children.length + val nCoefs = nChildren - th.k + val parseC = ParsePolynomial.costKind.cost(nCoefs) + val evalC = EvaluatePolynomial.costKind.cost(nCoefs) * nChildren + val nodeC = ToBytes_ProofTreeConjecture.costKind.cost + val childernC = childrenCost(th.children) + parseC + evalC + nodeC + childernC + case _ => + JitCost(0) // the cost of trivial proposition + } + } + + /** An instance of profiler used to measure cost parameters of verifySignature + * operations. + */ + val verifySignatureProfiler = new Profiler /** Executes the given `calcF` graph in the given context. * @param IR container of the graph (see [[IRContext]]) @@ -450,6 +792,13 @@ object Interpreter { throw new Error(s"Context-dependent pre-processing should produce tree of type Boolean or SigmaProp but was $x") } + // TODO after HF: merge with old version (`toValidScriptType`) + private def toValidScriptTypeJITC(exp: SValue): SigmaPropValue = exp match { + case v: Value[SBoolean.type]@unchecked if v.tpe == SBoolean => v.toSigmaProp + case p: SValue if p.tpe == SSigmaProp => p.asSigmaProp + case x => throw new Error(s"Context-dependent pre-processing should produce tree of type Boolean or SigmaProp but was $x") + } + /** Helper method to throw errors from Interpreter. */ def error(msg: String) = throw new InterpreterException(msg) diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/InterpreterContext.scala b/sigmastate/src/main/scala/sigmastate/interpreter/InterpreterContext.scala index 84f9569c6f..6ff9a95d7d 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/InterpreterContext.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/InterpreterContext.scala @@ -87,6 +87,9 @@ trait InterpreterContext { */ def activatedScriptVersion: Byte + /** Creates a new instance with currErgoTreeVersion updated with the given value. */ + def withErgoTreeVersion(newVersion: Byte): InterpreterContext + /** Creates a new instance with costLimit updated with given value. */ def withCostLimit(newCostLimit: Long): InterpreterContext diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/PrecompiledScriptProcessor.scala b/sigmastate/src/main/scala/sigmastate/interpreter/PrecompiledScriptProcessor.scala index 586aa2433f..f1d3e13973 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/PrecompiledScriptProcessor.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/PrecompiledScriptProcessor.scala @@ -7,10 +7,9 @@ import org.ergoplatform.settings.ErgoAlgos import org.ergoplatform.validation.SigmaValidationSettings import org.ergoplatform.validation.ValidationRules.{CheckCalcFunc, CheckCostFunc, trySoftForkable} import scalan.{AVHashMap, Nullable} -import sigmastate.Values import sigmastate.Values.ErgoTree import sigmastate.eval.{IRContext, RuntimeIRContext} -import sigmastate.interpreter.Interpreter.{ReductionResult, WhenSoftForkReductionResult} +import sigmastate.interpreter.Interpreter.{AotReductionResult, WhenSoftForkReductionResult} import sigmastate.serialization.ErgoTreeSerializer import sigmastate.utils.Helpers import sigmastate.utils.Helpers._ @@ -19,7 +18,7 @@ import spire.syntax.all.cfor import scala.collection.mutable /** A reducer which represents precompiled script reduction function. - * The function takes script execution context and produces the [[ReductionResult]], + * The function takes script execution context and produces the [[AotReductionResult]], * which contains both sigma proposition and the approximation of the cost taken by the * reduction. */ @@ -28,12 +27,12 @@ trait ScriptReducer { * This is equivalent to reduceToCrypto, except that graph construction is * completely avoided. */ - def reduce(context: InterpreterContext): ReductionResult + def reduce(context: InterpreterContext): AotReductionResult } /** Used as a fallback reducer when precompilation failed due to soft-fork condition. */ case object WhenSoftForkReducer extends ScriptReducer { - override def reduce(context: InterpreterContext): ReductionResult = { + override def reduce(context: InterpreterContext): AotReductionResult = { WhenSoftForkReductionResult(context.initCost) } } @@ -73,12 +72,12 @@ case class PrecompiledScriptReducer(scriptBytes: Seq[Byte])(implicit val IR: IRC * This is equivalent to reduceToCrypto, except that graph construction is * completely avoided. */ - def reduce(context: InterpreterContext): ReductionResult = { + def reduce(context: InterpreterContext): AotReductionResult = { import IR._ implicit val vs = context.validationSettings val maxCost = context.costLimit val initCost = context.initCost - trySoftForkable[ReductionResult](whenSoftFork = WhenSoftForkReductionResult(initCost)) { + trySoftForkable[AotReductionResult](whenSoftFork = WhenSoftForkReductionResult(initCost)) { val costF = costingRes.costF val costingCtx = context.toSigmaContext(isCost = true) val estimatedCost = IR.checkCostWithContext(costingCtx, costF, maxCost, initCost).getOrThrow @@ -89,7 +88,7 @@ case class PrecompiledScriptReducer(scriptBytes: Seq[Byte])(implicit val IR: IRC val calcF = costingRes.calcF Interpreter.calcResult(IR)(calcCtx, calcF) } - ReductionResult(SigmaDsl.toSigmaBoolean(res), estimatedCost) + AotReductionResult(SigmaDsl.toSigmaBoolean(res), estimatedCost) } } } diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala b/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala index 9f3f70d62e..60e24dd70c 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala @@ -1,7 +1,5 @@ package sigmastate.interpreter -import java.math.BigInteger - import gf2t.{GF2_192, GF2_192_Poly} import org.bitbucket.inkytonik.kiama.attribution.AttributionCore import org.bitbucket.inkytonik.kiama.rewriting.Rewriter.{everywherebu, everywheretd, rule} @@ -9,16 +7,20 @@ import org.bitbucket.inkytonik.kiama.rewriting.Strategy import scalan.util.CollectionUtil._ import sigmastate.TrivialProp.{FalseProp, TrueProp} import sigmastate.Values._ +import sigmastate.VersionContext.MaxSupportedScriptVersion import sigmastate._ import sigmastate.basics.DLogProtocol._ import sigmastate.basics.VerifierMessage.Challenge import sigmastate.basics._ -import sigmastate.lang.exceptions.CostLimitException +import sigmastate.eval.Evaluation.addCostChecked +import sigmastate.interpreter.EvalSettings._ +import sigmastate.lang.exceptions.InterpreterException import sigmastate.utils.Helpers +import java.math.BigInteger import scala.util.Try - +// TODO ProverResult was moved from here, compare with new-eval after merge /** * Interpreter with enhanced functionality to prove statements. */ @@ -87,7 +89,7 @@ trait ProverInterpreter extends Interpreter with ProverUtils with AttributionCor // This bitstring corresponding to a proposition to prove is needed for Strong Fiat-Shamir transformation. // See [BPW12] paper on Strong vs Weak Fiat-Shamir, // (https://link.springer.com/content/pdf/10.1007/978-3-642-34961-4_38.pdf) - val propBytes = FiatShamirTree.toBytes(step6) + val propBytes = FiatShamirTree.toBytes(step6)(null/* prove is not profiled */) // Prover Step 8: compute the challenge for the root of the tree as the Fiat-Shamir hash of propBytes // and the message being signed. @@ -117,19 +119,34 @@ trait ProverInterpreter extends Interpreter with ProverUtils with AttributionCor context: CTX, message: Array[Byte], hintsBag: HintsBag = HintsBag.empty): Try[CostedProverResult] = Try { + checkSoftForkCondition(ergoTree, context) match { + case Some(_) => + throw new InterpreterException( + s"Both ErgoTree version ${ergoTree.version} and activated version " + + s"${context.activatedScriptVersion} is greater than MaxSupportedScriptVersion $MaxSupportedScriptVersion") - val initCost = ergoTree.complexity + context.initCost - val remainingLimit = context.costLimit - initCost - if (remainingLimit <= 0) - throw new CostLimitException(initCost, - s"Estimated execution cost $initCost exceeds the limit ${context.costLimit}", None) - - val ctxUpdInitCost = context.withInitCost(initCost).asInstanceOf[CTX] + case None => // proceed normally + } - val res = fullReduction(ergoTree, ctxUpdInitCost, env) - val proof = generateProof(res.value, message, hintsBag) + VersionContext.withVersions(context.activatedScriptVersion, ergoTree.version) { + val evalMode = getEvaluationMode(context) + val (resValue, resCost) = evalMode match { + case AotEvaluationMode => + val complexityCost = ergoTree.complexity.toLong + val initCost = addCostChecked(context.initCost, complexityCost, context.costLimit) + val contextWithCost = context.withInitCost(initCost).asInstanceOf[CTX] + val reduced = fullReduction(ergoTree, contextWithCost, env) + (reduced.value, reduced.cost) + + case JitEvaluationMode => + val reduced = fullReduction(ergoTree, context, env) + val fullCost = addCryptoCost(reduced.jitRes, context.costLimit) + (reduced.value, fullCost) + } - CostedProverResult(proof, ctxUpdInitCost.extension, res.cost) + val proof = generateProof(resValue, message, hintsBag) + CostedProverResult(proof, context.extension, resCost) + } } def generateProof(sb: SigmaBoolean, diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/ProverResult.scala b/sigmastate/src/main/scala/sigmastate/interpreter/ProverResult.scala index 8e8c979ebd..a6824a1570 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/ProverResult.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/ProverResult.scala @@ -15,11 +15,12 @@ import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} class ProverResult(val proof: Array[Byte], val extension: ContextExtension) { override def hashCode(): Int = util.Arrays.hashCode(proof) * 31 + extension.hashCode() - override def equals(obj: scala.Any): Boolean = obj match { + override def equals(obj: scala.Any): Boolean = + (this eq obj.asInstanceOf[AnyRef]) || (obj match { case obj: ProverResult => util.Arrays.equals(proof, obj.proof) && extension == obj.extension case _ => false - } + }) override def toString = s"ProverResult(${Base16.encode(proof)},$extension)" } diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala b/sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala index 6c9c0cf279..4842325892 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala @@ -17,8 +17,8 @@ trait ProverUtils extends Interpreter { def generateCommitmentsFor(ergoTree: ErgoTree, context: CTX, generateFor: Seq[SigmaBoolean]): HintsBag = { - val reducedTree = fullReduction(ergoTree, context, Interpreter.emptyEnv).value - generateCommitmentsFor(reducedTree, generateFor) + val reduced = fullReduction(ergoTree, context, Interpreter.emptyEnv) + generateCommitmentsFor(reduced.value, generateFor) } /** @@ -79,8 +79,8 @@ trait ProverUtils extends Interpreter { proof: Array[Byte], realSecretsToExtract: Seq[SigmaBoolean], simulatedSecretsToExtract: Seq[SigmaBoolean] = Seq.empty): HintsBag = { - val reducedTree = fullReduction(ergoTree, context, Interpreter.emptyEnv).value - bagForMultisig(context, reducedTree, proof, realSecretsToExtract, simulatedSecretsToExtract) + val reduced = fullReduction(ergoTree, context, Interpreter.emptyEnv) + bagForMultisig(context, reduced.value, proof, realSecretsToExtract, simulatedSecretsToExtract) } /** @@ -102,7 +102,7 @@ trait ProverUtils extends Interpreter { realSecretsToExtract: Seq[SigmaBoolean], simulatedSecretsToExtract: Seq[SigmaBoolean]): HintsBag = { - val ut = SigSerializer.parseAndComputeChallenges(sigmaTree, proof) + val ut = SigSerializer.parseAndComputeChallenges(sigmaTree, proof)(null) val proofTree = computeCommitments(ut).get.asInstanceOf[UncheckedSigmaTree] def traverseNode(tree: ProofTree, diff --git a/sigmastate/src/main/scala/sigmastate/lang/SigmaBuilder.scala b/sigmastate/src/main/scala/sigmastate/lang/SigmaBuilder.scala index 5b220109f8..cb0d72b98f 100644 --- a/sigmastate/src/main/scala/sigmastate/lang/SigmaBuilder.scala +++ b/sigmastate/src/main/scala/sigmastate/lang/SigmaBuilder.scala @@ -231,6 +231,14 @@ abstract class SigmaBuilder { */ def liftAny(obj: Any): Nullable[SValue] = obj match { case v: SValue => Nullable(v) + case _ => + liftToConstant(obj) + } + + /** Created a new Constant instance with an appropriate type derived from the given data `obj`. + * Uses scalan.Nullable instead of scala.Option to avoid allocation on consensus hot path. + */ + def liftToConstant(obj: Any): Nullable[Constant[SType]] = obj match { case arr: Array[Boolean] => Nullable(mkCollectionConstant[SBoolean.type](arr, SBoolean)) case arr: Array[Byte] => Nullable(mkCollectionConstant[SByte.type](arr, SByte)) case arr: Array[Short] => Nullable(mkCollectionConstant[SShort.type](arr, SShort)) @@ -251,7 +259,25 @@ abstract class SigmaBuilder { case b: Boolean => Nullable(if(b) TrueLeaf else FalseLeaf) case v: String => Nullable(mkConstant[SString.type](v, SString)) - case b: ErgoBox => Nullable(mkConstant[SBox.type](b, SBox)) + + // The Box lifting was broken in v4.x. `SigmaDsl.Box(b)` was missing which means the + // isCorrectType requirement would fail in ConstantNode constructor. + // This method is used as part of consensus in SubstConstants operation, however + // ErgoBox cannot be passed as argument as it is never valid value during evaluation. + // Thus we can use activation-based versioning and fix this code when v5.0 is activated. + case b: ErgoBox => + if (VersionContext.current.isJitActivated) + Nullable(mkConstant[SBox.type](SigmaDsl.Box(b), SBox)) // fixed in v5.0 + else + Nullable(mkConstant[SBox.type](b, SBox)) // same as in v4.x, i.e. broken + + // this case is added in v5.0 and it can be useful when the box value comes from a + // register or a context variable is passed to SubstConstants. + case b: special.sigma.Box => + if (VersionContext.current.isJitActivated) + Nullable(mkConstant[SBox.type](b, SBox)) + else + Nullable.None // return the same result as in v4.x when there was no this case case avl: AvlTreeData => Nullable(mkConstant[SAvlTree.type](SigmaDsl.avlTree(avl), SAvlTree)) case avl: AvlTree => Nullable(mkConstant[SAvlTree.type](avl, SAvlTree)) diff --git a/sigmastate/src/main/scala/sigmastate/lang/SigmaCompiler.scala b/sigmastate/src/main/scala/sigmastate/lang/SigmaCompiler.scala index d5fe6d93c5..cebfc366dc 100644 --- a/sigmastate/src/main/scala/sigmastate/lang/SigmaCompiler.scala +++ b/sigmastate/src/main/scala/sigmastate/lang/SigmaCompiler.scala @@ -4,7 +4,7 @@ import fastparse.core.Parsed import fastparse.core.Parsed.Success import org.ergoplatform.ErgoAddressEncoder.NetworkPrefix import sigmastate.SType -import sigmastate.Values.{Value, SValue} +import sigmastate.Values.{SValue, Value} import sigmastate.eval.IRContext import sigmastate.interpreter.Interpreter.ScriptEnv import sigmastate.lang.SigmaPredef.PredefinedFuncRegistry @@ -26,7 +26,15 @@ case class CompilerSettings( lowerMethodCalls: Boolean ) +/** Compiler which compiles ErgoScript source code into ErgoTree. + * @param settings compilation parameters \ + */ class SigmaCompiler(settings: CompilerSettings) { + /** Constructs an instance for the given network type and with default settings. */ + def this(networkPrefix: Byte) = this( + CompilerSettings(networkPrefix, TransformingSigmaBuilder, lowerMethodCalls = true) + ) + @inline final def builder = settings.builder @inline final def networkPrefix = settings.networkPrefix diff --git a/sigmastate/src/main/scala/sigmastate/lang/SigmaPredef.scala b/sigmastate/src/main/scala/sigmastate/lang/SigmaPredef.scala index 942a110d52..ed15cd783c 100644 --- a/sigmastate/src/main/scala/sigmastate/lang/SigmaPredef.scala +++ b/sigmastate/src/main/scala/sigmastate/lang/SigmaPredef.scala @@ -3,15 +3,15 @@ package sigmastate.lang import org.ergoplatform.ErgoAddressEncoder.NetworkPrefix import org.ergoplatform.{ErgoAddressEncoder, P2PKAddress} import scalan.Nullable -import scorex.util.encode.{Base64, Base58} +import scorex.util.encode.{Base64, Base58, Base16} import sigmastate.SCollection.{SIntArray, SByteArray} import sigmastate.SOption._ -import sigmastate.Values.{StringConstant, Constant, EvaluatedValue, SValue, IntValue, SigmaPropConstant, ConstantPlaceholder, BoolValue, Value, ByteArrayConstant, SigmaPropValue, ValueCompanion} +import sigmastate.Values.{StringConstant, SValue, IntValue, SigmaPropConstant, Value, ByteArrayConstant, SigmaPropValue, ValueCompanion, Constant, EvaluatedValue, ConstantPlaceholder, BoolValue} import sigmastate._ import sigmastate.lang.Terms._ import sigmastate.lang.exceptions.InvalidArguments import sigmastate.serialization.ValueSerializer -import sigmastate.utxo.{GetVar, DeserializeContext, DeserializeRegister, SelectField} +import sigmastate.utxo.{DeserializeRegister, SelectField, DeserializeContext, GetVar} object SigmaPredef { @@ -44,7 +44,7 @@ object SigmaPredef { import builder._ /** Type variable used in the signatures of global functions below. */ - import SType.{tT, tR, tK, tL, tO, paramT, paramR} + import SType.{tT, tO, tR, tL, paramR, paramT, tK} private val undefined: IrBuilderFunc = PartialFunction.empty[(SValue, Seq[SValue]), SValue] @@ -176,18 +176,36 @@ object SigmaPredef { throw new InvalidArguments(s"Wrong type after deserialization, expected $tpe, got ${res.tpe}") res }), - OperationInfo(Constant, "", + OperationInfo(Constant, "Deserializes values from Base58 encoded binary data at compile time into a value of type T.", Seq(ArgInfo("", ""))) ) + val FromBase16Func = PredefinedFunc("fromBase16", + Lambda(Array("input" -> SString), SByteArray, None), + PredefFuncInfo( + { case (_, Seq(arg: EvaluatedValue[SString.type]@unchecked)) => + ByteArrayConstant(Base16.decode(arg.value).get) + }), + OperationInfo(Constant, + """Transforms Base16 encoded string literal into constant of type Coll[Byte]. + |It is a compile-time operation and only string literal (constant) can be its + |argument. + """.stripMargin, + Seq(ArgInfo("", ""))) + ) + val FromBase58Func = PredefinedFunc("fromBase58", Lambda(Array("input" -> SString), SByteArray, None), PredefFuncInfo( { case (_, Seq(arg: EvaluatedValue[SString.type]@unchecked)) => ByteArrayConstant(Base58.decode(arg.value).get) }), - OperationInfo(Constant, "", - Seq(ArgInfo("", ""))) + OperationInfo(Constant, + """Transforms Base58 encoded string literal into constant of type Coll[Byte]. + |It is a compile-time operation and only string literal (constant) can be its + |argument. + """.stripMargin, + Seq(ArgInfo("", ""))) ) val FromBase64Func = PredefinedFunc("fromBase64", @@ -196,7 +214,11 @@ object SigmaPredef { { case (_, Seq(arg: EvaluatedValue[SString.type]@unchecked)) => ByteArrayConstant(Base64.decode(arg.value).get) }), - OperationInfo(Constant, "", + OperationInfo(Constant, + """Transforms Base64 encoded string literal into constant of type Coll[Byte]. + |It is a compile-time operation and only string literal (constant) can be its + |argument. + """.stripMargin, Seq(ArgInfo("", ""))) ) @@ -381,6 +403,7 @@ object SigmaPredef { SigmaPropFunc, GetVarFunc, DeserializeFunc, + FromBase16Func, FromBase64Func, FromBase58Func, Blake2b256Func, diff --git a/sigmastate/src/main/scala/sigmastate/lang/Terms.scala b/sigmastate/src/main/scala/sigmastate/lang/Terms.scala index 5ba0449c6f..571d5dba6c 100644 --- a/sigmastate/src/main/scala/sigmastate/lang/Terms.scala +++ b/sigmastate/src/main/scala/sigmastate/lang/Terms.scala @@ -6,13 +6,12 @@ import sigmastate.SCollection.{SIntArray, SByteArray} import sigmastate.Values._ import sigmastate.utils.Overloading.Overload1 import sigmastate._ -import sigmastate.interpreter.ErgoTreeEvaluator +import sigmastate.interpreter.{Interpreter, ErgoTreeEvaluator} import sigmastate.interpreter.ErgoTreeEvaluator.DataEnv import sigmastate.serialization.OpCodes import sigmastate.serialization.OpCodes.OpCode import sigmastate.lang.TransformingSigmaBuilder._ -import scala.collection.mutable import scala.language.implicitConversions import scala.collection.mutable.WrappedArray import spire.syntax.all.cfor @@ -140,21 +139,14 @@ object Terms { protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { addCost(Apply.costKind) - if (args.isEmpty) { - // TODO coverage - val fV = func.evalTo[() => Any](env) - fV() - } - else if (args.length == 1) { + if (args.length == 1) { val fV = func.evalTo[Any => Any](env) val argV = args(0).evalTo[Any](env) fV(argV) - } - else { - // TODO coverage - val f = func.evalTo[Seq[Any] => Any](env) - val argsV = args.map(a => a.evalTo[Any](env)) - f(argsV) + } else { + // zero or more than 1 argument functions are not supported in v4.x, v5.0 + // see `case Terms.Apply(f, Seq(x))` in RuntimeCosting which means other cases are not supported. + Interpreter.error(s"Function application must have 1 argument, but was: $this") } } } @@ -162,7 +154,7 @@ object Terms { override def opCode: OpCode = OpCodes.FuncApplyCode /** Cost of: 1) switch on the number of args 2) Scala method call 3) add args to env * Old cost: lambdaInvoke == 30 */ - override val costKind = FixedCost(30) + override val costKind = FixedCost(JitCost(30)) } /** Apply types for type parameters of input value. */ @@ -257,10 +249,10 @@ object Terms { } } - object MethodCall extends ValueCompanion { + object MethodCall extends FixedCostValueCompanion { override def opCode: OpCode = OpCodes.MethodCallCode /** Cost of: 1) packing args into Array 2) java.lang.reflect.Method.invoke */ - override val costKind = FixedCost(4) + override val costKind = FixedCost(JitCost(4)) /** Helper constructor which allows to cast the resulting node to the specified * [[sigmastate.Values.Value]] type `T`. @@ -276,7 +268,7 @@ object Terms { object PropertyCall extends FixedCostValueCompanion { override def opCode: OpCode = OpCodes.PropertyCallCode /** Cost of: 1) packing args into Array 2) java.lang.reflect.Method.invoke */ - override val costKind = FixedCost(4) + override val costKind = FixedCost(JitCost(4)) } case class STypeParam(ident: STypeVar, upperBound: Option[SType] = None, lowerBound: Option[SType] = None) { diff --git a/sigmastate/src/main/scala/sigmastate/lang/exceptions/Exceptions.scala b/sigmastate/src/main/scala/sigmastate/lang/exceptions/Exceptions.scala index af3aafd1e2..285baf6c26 100644 --- a/sigmastate/src/main/scala/sigmastate/lang/exceptions/Exceptions.scala +++ b/sigmastate/src/main/scala/sigmastate/lang/exceptions/Exceptions.scala @@ -1,7 +1,9 @@ package sigmastate.lang.exceptions +import sigmastate.JitCost import sigmastate.lang.SourceContext +/** Base class of all exceptions thrown from various Sigma components. */ class SigmaException(val message: String, val source: Option[SourceContext] = None, val cause: Option[Throwable] = None) extends Exception(message, cause.orNull) { @@ -12,30 +14,42 @@ class SigmaException(val message: String, val source: Option[SourceContext] = No }.getOrElse(message) } +/** Exception thrown by the [[sigmastate.lang.SigmaBinder]]. */ class BinderException(message: String, source: Option[SourceContext] = None) extends SigmaException(message, source) +/** Exception thrown by the [[sigmastate.lang.SigmaTyper]]. */ class TyperException(message: String, source: Option[SourceContext] = None) extends SigmaException(message, source) +/** Exception thrown by the [[sigmastate.lang.SigmaSpecializer]]. */ class SpecializerException(message: String, source: Option[SourceContext] = None) extends SigmaException(message, source) -class SerializerException(message: String, source: Option[SourceContext] = None, cause: Option[Throwable] = None) +/** Exception thrown by the [[sigmastate.serialization.SigmaSerializer]]. */ +case class SerializerException( + override val message: String, + override val source: Option[SourceContext] = None, + override val cause: Option[Throwable] = None) extends SigmaException(message, source, cause) +/** Exception thrown by the [[sigmastate.lang.SigmaBuilder]]. */ class BuilderException(message: String, source: Option[SourceContext] = None) extends SigmaException(message, source) +/** Exception thrown by interpreter during cost estimation. */ class CosterException(message: String, source: Option[SourceContext], cause: Option[Throwable] = None) extends SigmaException(message, source, cause) +/** Exception thrown by [[sigmastate.interpreter.Interpreter]]. */ class InterpreterException(message: String, source: Option[SourceContext] = None, cause: Option[Throwable] = None) extends SigmaException(message, source, cause) +/** Exception thrown by [[sigmastate.interpreter.Interpreter]] when cost limit is exceeded. */ class CostLimitException(val estimatedCost: Long, message: String, cause: Option[Throwable] = None) extends SigmaException(message, None, cause) object CostLimitException { - def msgCostLimitError(cost: Long, limit: Long) = s"Estimated execution cost $cost exceeds the limit $limit" + /** Formats the error message with the given parameters. */ + def msgCostLimitError(cost: JitCost, limit: JitCost) = s"Estimated execution cost $cost exceeds the limit $limit" } diff --git a/sigmastate/src/main/scala/sigmastate/lang/exceptions/SigmaSerializerExceptions.scala b/sigmastate/src/main/scala/sigmastate/lang/exceptions/SigmaSerializerExceptions.scala index 9d7678fa3b..69daa59a10 100644 --- a/sigmastate/src/main/scala/sigmastate/lang/exceptions/SigmaSerializerExceptions.scala +++ b/sigmastate/src/main/scala/sigmastate/lang/exceptions/SigmaSerializerExceptions.scala @@ -2,14 +2,25 @@ package sigmastate.lang.exceptions import sigmastate.lang.SourceContext +/** Thrown by TypeSerializer when type prefix <= 0. */ final class InvalidTypePrefix(message: String, source: Option[SourceContext] = None, cause: Option[Throwable] = None) extends SerializerException(message, source, cause) -final class InputSizeLimitExceeded(message: String, source: Option[SourceContext] = None, cause: Option[Throwable] = None) +/** Thrown when the current reader position > positionLimit which is set in the Reader. + * @see [[org.ergoplatform.validation.ValidationRules.CheckPositionLimit]] + */ +final class ReaderPositionLimitExceeded( + message: String, + val position: Int, + val positionLimit: Int, + source: Option[SourceContext] = None, + cause: Option[Throwable] = None) extends SerializerException(message, source, cause) +/** Thrown when the current depth level > maxDepthLevel which is set in the Reader. */ final class DeserializeCallDepthExceeded(message: String, source: Option[SourceContext] = None, cause: Option[Throwable] = None) extends SerializerException(message, source, cause) +/** Thrown by [[org.ergoplatform.validation.ValidationRules.CheckValidOpCode]] validation rule. */ final class InvalidOpCode(message: String, source: Option[SourceContext] = None, cause: Option[Throwable] = None) extends SerializerException(message, source, cause) diff --git a/sigmastate/src/main/scala/sigmastate/serialization/BlockValueSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/BlockValueSerializer.scala index 6bd7cd91ec..7348156f96 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/BlockValueSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/BlockValueSerializer.scala @@ -5,7 +5,8 @@ import sigmastate._ import scorex.util.Extensions._ import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import ValueSerializer._ -import sigmastate.utils.SigmaByteWriter.{Vlq, U, DataInfo} +import sigmastate.util.safeNewArray +import sigmastate.utils.SigmaByteWriter.{DataInfo, U, Vlq} import spire.syntax.all.cfor case class BlockValueSerializer(cons: (IndexedSeq[BlockItem], Value[SType]) => Value[SType]) @@ -24,12 +25,12 @@ case class BlockValueSerializer(cons: (IndexedSeq[BlockItem], Value[SType]) => V } override def parse(r: SigmaByteReader): Value[SType] = { - val itemsSize = r.getUInt().toIntExact + val itemsSize = r.getUIntExact val values: IndexedSeq[BlockItem] = if (itemsSize == 0) BlockItem.EmptySeq else { // HOTSPOT:: allocate new array only if it is not empty - val buf = ValueSerializer.newArray[BlockItem](itemsSize) + val buf = safeNewArray[BlockItem](itemsSize) cfor(0)(_ < itemsSize, _ + 1) { i => buf(i) = r.getValue().asInstanceOf[BlockItem] } diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionBooleanConstantSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionBooleanConstantSerializer.scala index 0eb17c0aa6..6e99f52a55 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionBooleanConstantSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionBooleanConstantSerializer.scala @@ -4,6 +4,7 @@ import sigmastate.{SCollection, SBoolean, ArgInfo} import sigmastate.Values._ import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import SigmaByteWriter._ +import sigmastate.util.safeNewArray import spire.syntax.all.cfor case class ConcreteCollectionBooleanConstantSerializer(cons: (IndexedSeq[Value[SBoolean.type]], SBoolean.type) => Value[SCollection[SBoolean.type]]) @@ -36,7 +37,7 @@ case class ConcreteCollectionBooleanConstantSerializer(cons: (IndexedSeq[Value[S // reusing pre-allocated immutable instances Value.EmptySeq.asInstanceOf[IndexedSeq[Value[SBoolean.type]]] } else { - val items = ValueSerializer.newArray[BoolValue](size) + val items = safeNewArray[BoolValue](size) cfor(0)(_ < size, _ + 1) { i => items(i) = BooleanConstant.fromBoolean(bits(i)) } diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionSerializer.scala index 31ada23423..4d4936c80f 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionSerializer.scala @@ -4,7 +4,8 @@ import sigmastate.{SCollection, ArgInfo, SType} import sigmastate.Values._ import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import ValueSerializer._ -import sigmastate.utils.SigmaByteWriter.{U, Vlq, DataInfo} +import sigmastate.util.safeNewArray +import sigmastate.utils.SigmaByteWriter.{DataInfo, U, Vlq} import spire.syntax.all.cfor case class ConcreteCollectionSerializer(cons: (IndexedSeq[Value[SType]], SType) => Value[SCollection[SType]]) @@ -29,7 +30,7 @@ case class ConcreteCollectionSerializer(cons: (IndexedSeq[Value[SType]], SType) // reusing pre-allocated immutable instances Value.EmptySeq } else { - val values = ValueSerializer.newArray[SValue](size) + val values = safeNewArray[SValue](size) cfor(0)(_ < size, _ + 1) { i => val v = r.getValue() // READ values(i) = v diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ConstantPlaceholderSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ConstantPlaceholderSerializer.scala index e9c19ca9e5..c3b834d00e 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ConstantPlaceholderSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ConstantPlaceholderSerializer.scala @@ -6,14 +6,15 @@ import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} case class ConstantPlaceholderSerializer(cons: (Int, SType) => Value[SType]) extends ValueSerializer[ConstantPlaceholder[SType]] { + import sigmastate.Operations.ConstantPlaceholderInfo._ override def opDesc = ConstantPlaceholder override def serialize(obj: ConstantPlaceholder[SType], w: SigmaByteWriter): Unit = { - w.putUInt(obj.id) + w.putUInt(obj.id, indexArg) } override def parse(r: SigmaByteReader): Value[SType] = { - val id = r.getUInt().toInt + val id = r.getUIntExact val constant = r.constantStore.get(id) if (r.resolvePlaceholdersToConstants) constant diff --git a/sigmastate/src/main/scala/sigmastate/serialization/DataSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/DataSerializer.scala index 00629cef4c..3071334c8d 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/DataSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/DataSerializer.scala @@ -19,6 +19,7 @@ import scala.collection.mutable /** This works in tandem with ConstantSerializer, if you change one make sure to check the other.*/ object DataSerializer { + // TODO v5.x: control maxTreeDepth same as in deserialize /** Use type descriptor `tpe` to deconstruct type structure and recursively serialize subcomponents. * Primitive types are leaves of the type tree, and they are served as basis of recursion. * The data value `v` is expected to conform to the type described by `tpe`. @@ -76,12 +77,17 @@ object DataSerializer { i += 1 } - // TODO HF (3h): support Option[T] (see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/659) - case _ => sys.error(s"Don't know how to serialize ($v, $tpe)") + // TODO v6.0 (3h): support Option[T] (see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/659) + case _ => + CheckSerializableTypeCode(tpe.typeCode) + throw new SerializerException(s"Don't know how to serialize ($v, $tpe)") } /** Reads a data value from Reader. The data value bytes is expected to confirm - * to the type descriptor `tpe`. */ + * to the type descriptor `tpe`. + * The data structure depth is limited by r.maxTreeDepth which is + * SigmaSerializer.MaxTreeDepth by default. + */ def deserialize[T <: SType](tpe: T, r: SigmaByteReader): T#WrappedType = { val depth = r.level r.level = depth + 1 @@ -93,12 +99,11 @@ object DataSerializer { case SInt => r.getInt() case SLong => r.getLong() case SString => - val size = r.getUInt().toInt + val size = r.getUIntExact val bytes = r.getBytes(size) new String(bytes, StandardCharsets.UTF_8) case SBigInt => val size: Short = r.getUShort().toShort - // TODO HF (2h): replace with validation rule to enable soft-forkability if (size > SBigInt.MaxSizeInBytes) { throw new SerializerException(s"BigInt value doesn't not fit into ${SBigInt.MaxSizeInBytes} bytes: $size") } diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ErgoTreeSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ErgoTreeSerializer.scala index 6dee796e68..06b39bc4eb 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ErgoTreeSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ErgoTreeSerializer.scala @@ -1,18 +1,21 @@ package sigmastate.serialization import org.ergoplatform.validation.ValidationRules.{CheckDeserializedScriptIsSigmaProp, CheckHeaderSizeBit, CheckPositionLimit} -import org.ergoplatform.validation.{ValidationException, SigmaValidationSettings} -import sigmastate.SType -import sigmastate.Values.{Value, ErgoTree, Constant, UnparsedErgoTree} +import org.ergoplatform.validation.{SigmaValidationSettings, ValidationException} +import sigmastate.{SType, VersionContext} +import sigmastate.Values.{Constant, ErgoTree, UnparsedErgoTree} import sigmastate.lang.DeserializationSigmaBuilder import sigmastate.lang.Terms.ValueOps -import sigmastate.lang.exceptions.{SerializerException, InputSizeLimitExceeded} +import sigmastate.lang.exceptions.{SerializerException, ReaderPositionLimitExceeded} import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import scalan.util.Extensions._ import sigmastate.Values.ErgoTree.EmptyConstants +import sigmastate.util.safeNewArray import sigmastate.utxo.ComplexityTable import spire.syntax.all.cfor +import java.util + /** * Rationale for soft-forkable ErgoTree serialization. * There are 2 points: @@ -114,11 +117,9 @@ class ErgoTreeSerializer { val w = SigmaSerializer.startWriter() val header = bytes(0) val contentLength = bytes.length - 1 - val contentBytes = new Array[Byte](contentLength) - Array.copy(bytes, 1, contentBytes, 0, contentLength) // TODO optimize: avoid new array by implementing putSlice(arr, from, len) w.put(header) w.putUInt(contentLength) - w.putBytes(contentBytes) + w.putBytes(bytes, 1, contentLength) w.toBytes } else bytes @@ -176,8 +177,8 @@ class ErgoTreeSerializer { propositionBytes, Some(hasDeserialize)) } catch { - case e: InputSizeLimitExceeded => - throw ValidationException(s"Data size check failed", CheckPositionLimit, Nil, Some(e)) + case e: ReaderPositionLimitExceeded => + CheckPositionLimit.throwValidationException(e) } } catch { @@ -206,8 +207,7 @@ class ErgoTreeSerializer { val header = r.getByte() CheckHeaderSizeBit(header) val sizeOpt = if (ErgoTree.hasSize(header)) { - val size = r.getUInt().toIntExact - Some(size) + Some(r.getUIntExact) } else None (header, sizeOpt) @@ -221,10 +221,10 @@ class ErgoTreeSerializer { private def deserializeConstants(header: Byte, r: SigmaByteReader): IndexedSeq[Constant[SType]] = { val constants: IndexedSeq[Constant[SType]] = if (ErgoTree.isConstantSegregation(header)) { - val nConsts = r.getUInt().toInt + val nConsts = r.getUIntExact if (nConsts > 0) { // HOTSPOT:: allocate new array only if it is not empty - val res = ValueSerializer.newArray[Constant[SType]](nConsts) + val res = safeNewArray[Constant[SType]](nConsts) cfor(0)(_ < nConsts, _ + 1) { i => res(i) = constantSerializer.deserialize(r) } @@ -246,6 +246,32 @@ class ErgoTreeSerializer { (header, sizeOpt, constants, treeBytes) } + /** Computes back references from constants to positions. + * This method helps to implement substituteConstants efficiently + * (i.e. O(n + m) time, where n - number of positions and m - number of constants) + * + * @param positions indexes in the range [0..positionsRange) + * @param positionsRange upper bound on values in `positions` + * @return array `r` of back references, i.e. indices in `positions` such that + * positions(r(i)) == i whenever r(i) != -1. When r(i) == -1 then backreference + * is not defined (which means the constant with the index `i` is not substituted. + */ + private[sigmastate] def getPositionsBackref(positions: Array[Int], positionsRange: Int): Array[Int] = { + // allocate array of back references: forall i: positionsBackref(i) is index in `positions` + val positionsBackref = safeNewArray[Int](positionsRange) + // mark all positions are not assigned + util.Arrays.fill(positionsBackref, -1) + + cfor(0)(_ < positions.length, _ + 1) { iPos => + val pos = positions(iPos) + if (0 <= pos && pos < positionsBackref.length && positionsBackref(pos) == -1) { + // back reference is not yet assigned, assign in now + positionsBackref(pos) = iPos + } + } + positionsBackref + } + /** Transforms serialized bytes of ErgoTree with segregated constants by * replacing constants at given positions with new values. This operation * allow to use serialized scripts as pre-defined templates. @@ -263,31 +289,64 @@ class ErgoTreeSerializer { */ def substituteConstants(scriptBytes: Array[Byte], positions: Array[Int], - newVals: Array[Value[SType]])(implicit vs: SigmaValidationSettings): (Array[Byte], Int) = { + newVals: Array[Constant[SType]])(implicit vs: SigmaValidationSettings): (Array[Byte], Int) = { require(positions.length == newVals.length, s"expected positions and newVals to have the same length, got: positions: ${positions.toSeq},\n newVals: ${newVals.toSeq}") val r = SigmaSerializer.startReader(scriptBytes) val (header, _, constants, treeBytes) = deserializeHeaderWithTreeBytes(r) val w = SigmaSerializer.startWriter() w.put(header) - w.putUInt(constants.length) // TODO HF (3h): this should not be serialized when segregation is off - val constantSerializer = ConstantSerializer(DeserializationSigmaBuilder) - constants.zipWithIndex.foreach { - case (c, i) if positions.contains(i) => - val newVal = newVals(positions.indexOf(i)) - // we need to get newVal's serialized constant value (see ProveDlogSerializer for example) - val constantStore = new ConstantStore() - val valW = SigmaSerializer.startWriter(constantStore) - valW.putValue(newVal) - val newConsts = constantStore.getAll - assert(newConsts.length == 1) - val newConst = newConsts.head - // TODO HF (1h): replace assert with require - assert(c.tpe == newConst.tpe, s"expected new constant to have the same ${c.tpe} tpe, got ${newConst.tpe}") - constantSerializer.serialize(newConst, w) - case (c, _) => - constantSerializer.serialize(c, w) + if (VersionContext.current.isJitActivated) { + // The following `constants.length` should not be serialized when segregation is off + // in the `header`, because in this case there is no `constants` section in the + // ErgoTree serialization format. Thus, applying this `substituteConstants` for + // non-segregated trees will return non-parsable ErgoTree bytes (when + // `constants.length` is put in `w`). + if (ErgoTree.isConstantSegregation(header)) { + w.putUInt(constants.length) + } + + // The following is optimized O(nConstants + position.length) implementation + val nConstants = constants.length + if (nConstants > 0) { + val backrefs = getPositionsBackref(positions, nConstants) + cfor(0)(_ < nConstants, _ + 1) { i => + val c = constants(i) + val iPos = backrefs(i) // index to `positions` + if (iPos == -1) { + // no position => no substitution, serialize original constant + constantSerializer.serialize(c, w) + } else { + assert(positions(iPos) == i) // INV: backrefs and positions are mutually inverse + val newConst = newVals(iPos) + require(c.tpe == newConst.tpe, + s"expected new constant to have the same ${c.tpe} tpe, got ${newConst.tpe}") + constantSerializer.serialize(newConst, w) + } + } + } + } else { + // for v4.x compatibility we save constants.length here (see the above comment to + // understand the consequences) + w.putUInt(constants.length) + + // the following is v4.x O(nConstants * positions.length) inefficient implementation + constants.zipWithIndex.foreach { + case (c, i) if positions.contains(i) => + val newVal = newVals(positions.indexOf(i)) + // we need to get newVal's serialized constant value (see ProveDlogSerializer for example) + val constantStore = new ConstantStore() + val valW = SigmaSerializer.startWriter(constantStore) + valW.putValue(newVal) + val newConsts = constantStore.getAll + require(newConsts.length == 1) + val newConst = newConsts.head + require(c.tpe == newConst.tpe, s"expected new constant to have the same ${c.tpe} tpe, got ${newConst.tpe}") + constantSerializer.serialize(newConst, w) + case (c, _) => + constantSerializer.serialize(c, w) + } } w.putBytes(treeBytes) diff --git a/sigmastate/src/main/scala/sigmastate/serialization/FuncValueSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/FuncValueSerializer.scala index 2bb35be485..9e84acddf5 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/FuncValueSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/FuncValueSerializer.scala @@ -5,6 +5,7 @@ import sigmastate._ import scorex.util.Extensions._ import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import ValueSerializer._ +import sigmastate.util.safeNewArray import sigmastate.utils.SigmaByteWriter.{DataInfo, U, Vlq} import spire.syntax.all.cfor @@ -26,10 +27,10 @@ case class FuncValueSerializer(cons: (IndexedSeq[(Int, SType)], Value[SType]) => } override def parse(r: SigmaByteReader): Value[SType] = { - val argsSize = r.getUInt().toIntExact - val args = ValueSerializer.newArray[(Int, SType)](argsSize) + val argsSize = r.getUIntExact + val args = safeNewArray[(Int, SType)](argsSize) cfor(0)(_ < argsSize, _ + 1) { i => - val id = r.getUInt().toInt + val id = r.getUIntExact val tpe = r.getType() r.valDefTypeStore(id) = tpe args(i) = (id, tpe) diff --git a/sigmastate/src/main/scala/sigmastate/serialization/GroupElementSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/GroupElementSerializer.scala index d5aa3b8427..d004d42566 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/GroupElementSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/GroupElementSerializer.scala @@ -2,6 +2,7 @@ package sigmastate.serialization import sigmastate.interpreter.CryptoConstants import sigmastate.interpreter.CryptoConstants.EcPointType +import sigmastate.util.safeNewArray import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} /** @@ -26,7 +27,7 @@ object GroupElementSerializer extends SigmaSerializer[EcPointType, EcPointType] val normed = point.normalize() val ySign = normed.getAffineYCoord.testBitZero() val X = normed.getXCoord.getEncoded - val PO = ValueSerializer.newArray[Byte](X.length + 1) + val PO = safeNewArray[Byte](X.length + 1) PO(0) = (if (ySign) 0x03 else 0x02).toByte System.arraycopy(X, 0, PO, 1, X.length) PO diff --git a/sigmastate/src/main/scala/sigmastate/serialization/MethodCallSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/MethodCallSerializer.scala index 3b86a30bf6..24b753846a 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/MethodCallSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/MethodCallSerializer.scala @@ -4,6 +4,7 @@ import sigmastate.Values._ import sigmastate._ import sigmastate.lang.SigmaTyper.STypeSubst import sigmastate.lang.Terms.MethodCall +import sigmastate.util.safeNewArray import sigmastate.utils.SigmaByteWriter.{DataInfo, valuesItemInfo} import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import sigmastate.utxo.ComplexityTable @@ -53,7 +54,7 @@ case class MethodCallSerializer(cons: (Value[SType], SMethod, IndexedSeq[Value[S val types: Seq[SType] = if (nArgs == 0) SType.EmptySeq else { - val types = ValueSerializer.newArray[SType](nArgs) + val types = safeNewArray[SType](nArgs) cfor(0)(_ < nArgs, _ + 1) { i => types(i) = args(i).tpe } diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ModQArithOpSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ModQArithOpSerializer.scala index 26d70281f3..b8d8d07093 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ModQArithOpSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ModQArithOpSerializer.scala @@ -6,7 +6,7 @@ import sigmastate.utils.SigmaByteWriter.DataInfo import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import sigmastate.{ModQArithOpCompanion, SType, ModQArithOp} -// TODO HF (2h): make sure it is covered with tests +// TODO v6.0 (2h): make sure it is covered with tests case class ModQArithOpSerializer(override val opDesc: ModQArithOpCompanion, cons: (BigIntValue, BigIntValue) => BigIntValue) extends ValueSerializer[ModQArithOp] { val leftInfo: DataInfo[SValue] = opDesc.argInfos(0) diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ModQSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ModQSerializer.scala index dee613490c..f48a6e6388 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ModQSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ModQSerializer.scala @@ -5,7 +5,7 @@ import sigmastate.lang.Terms._ import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import sigmastate.{ModQ, SType} -// TODO HF (2h): make sure it is covered with tests +// TODO v6.0 (2h): make sure it is covered with tests object ModQSerializer extends ValueSerializer[ModQ] { override def opDesc = ModQ diff --git a/sigmastate/src/main/scala/sigmastate/serialization/SigmaSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/SigmaSerializer.scala index bd00670129..5af0082333 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/SigmaSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/SigmaSerializer.scala @@ -105,18 +105,6 @@ abstract class SigmaSerializer[TFamily, T <: TFamily] extends Serializer[TFamily } trait SigmaSerializerCompanion[TFamily] { - - /** Maximum length of a serializable collection. */ - val MaxArrayLength: Int = 100000 - - /** Allocates a new array with `len` items of type `A`. */ - final def newArray[A](len: Int)(implicit tA: ClassTag[A]): Array[A] = { - if (len > MaxArrayLength) - throw new SerializerException( - s"Cannot allocate array of $tA with $len items: max limit is $MaxArrayLength") - new Array[A](len) - } - def getSerializer(opCode: OpCode): SigmaSerializer[TFamily, _ <: TFamily] def deserialize(r: SigmaByteReader): TFamily def serialize(v: TFamily, w: SigmaByteWriter): Unit diff --git a/sigmastate/src/main/scala/sigmastate/serialization/TupleSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/TupleSerializer.scala index ee84f994ef..a7373b0d53 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/TupleSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/TupleSerializer.scala @@ -1,9 +1,10 @@ package sigmastate.serialization -import sigmastate.{ArgInfo, SType} +import sigmastate.{SType, ArgInfo} import sigmastate.Values._ -import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} -import ValueSerializer._ +import sigmastate.utils.{SigmaByteWriter, SigmaByteReader} +import sigmastate.serialization.ValueSerializer._ +import sigmastate.util.safeNewArray import sigmastate.utils.SigmaByteWriter.{DataInfo, U} import spire.syntax.all.cfor @@ -24,7 +25,9 @@ case class TupleSerializer(cons: Seq[Value[SType]] => Value[SType]) override def parse(r: SigmaByteReader): Value[SType] = { val size = r.getByte() - val values = ValueSerializer.newArray[SValue](size) // assume size > 0 so always create a new array + // note, in v4.x, v5.x tuples always has 2 elements, this may change in v6.0 + // in which case allocation can be avoided for empty tuples + val values = safeNewArray[SValue](size) cfor(0)(_ < size, _ + 1) { i => values(i) = r.getValue() } diff --git a/sigmastate/src/main/scala/sigmastate/serialization/TypeSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/TypeSerializer.scala index 4ac603714e..9f3eb77fb4 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/TypeSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/TypeSerializer.scala @@ -1,10 +1,10 @@ package sigmastate.serialization import java.nio.charset.StandardCharsets - import org.ergoplatform.validation.ValidationRules.{CheckPrimitiveTypeCode, CheckTypeCode} import sigmastate._ import sigmastate.lang.exceptions.InvalidTypePrefix +import sigmastate.util.safeNewArray import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import spire.syntax.all.cfor @@ -174,7 +174,7 @@ object TypeSerializer { c match { case STuple.TupleTypeCode => { val len = r.getUByte() - val items = ValueSerializer.newArray[SType](len) + val items = safeNewArray[SType](len) cfor(0)(_ < len, _ + 1) { i => items(i) = deserialize(r, depth + 1) } diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ValDefSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ValDefSerializer.scala index ff093d061f..8a3394ee89 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ValDefSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ValDefSerializer.scala @@ -6,6 +6,7 @@ import sigmastate.serialization.OpCodes._ import scorex.util.Extensions._ import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import ValueSerializer._ +import sigmastate.util.safeNewArray import spire.syntax.all.cfor case class ValDefSerializer(override val opDesc: ValueCompanion) extends ValueSerializer[ValDef] { @@ -29,11 +30,11 @@ case class ValDefSerializer(override val opDesc: ValueCompanion) extends ValueSe } override def parse(r: SigmaByteReader): Value[SType] = { - val id = r.getUInt().toInt + val id = r.getUIntExact val tpeArgs: Seq[STypeVar] = opCode match { case FunDefCode => val nTpeArgs = r.getByte() - val inputs = ValueSerializer.newArray[STypeVar](nTpeArgs) + val inputs = safeNewArray[STypeVar](nTpeArgs) cfor(0)(_ < nTpeArgs, _ + 1) { i => inputs(i) = r.getType().asInstanceOf[STypeVar] } diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ValUseSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ValUseSerializer.scala index ff54a82a6d..3daaad877f 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ValUseSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ValUseSerializer.scala @@ -12,7 +12,7 @@ case class ValUseSerializer(cons: (Int, SType) => Value[SType]) extends ValueSer } override def parse(r: SigmaByteReader): Value[SType] = { - val id = r.getUInt().toInt + val id = r.getUIntExact val tpe = r.valDefTypeStore(id) cons(id, tpe) } diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ValueSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ValueSerializer.scala index af7b9a4a5e..40b8d64786 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ValueSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ValueSerializer.scala @@ -8,7 +8,7 @@ import sigmastate._ import sigmastate.lang.DeserializationSigmaBuilder import sigmastate.serialization.OpCodes._ import sigmastate.serialization.transformers._ -import sigmastate.serialization.trees.{QuadrupleSerializer, Relation2Serializer} +import sigmastate.serialization.trees.{Relation2Serializer, QuadrupleSerializer} import scalan.util.Extensions._ import sigmastate.utils.SigmaByteWriter.DataInfo import sigmastate.utils._ @@ -30,6 +30,14 @@ abstract class ValueSerializer[V <: Value[SType]] extends SigmaSerializer[Value[ @inline final def opCode: OpCode = opDesc.opCode } +/** Implements serialization of ErgoTree expressions. Contains global collection of + * serializers for each ErgoTree operation (see `serializers` field). + * + * It also implements optional (see collectSerInfo flag) metadata collection during serialization + * to generate serializer specification tables in LaTeX. + * + * @see GenSerializers + */ object ValueSerializer extends SigmaSerializerCompanion[Value[SType]] { type Tag = OpCode @@ -347,6 +355,7 @@ object ValueSerializer extends SigmaSerializerCompanion[Value[SType]] { } } + // TODO v5.x: control maxTreeDepth same as in deserialize (see Reader.level property and SigmaSerializer.MaxTreeDepth) override def serialize(v: Value[SType], w: SigmaByteWriter): Unit = serializable(v) match { case c: Constant[SType] => w.constantExtractionStore match { diff --git a/sigmastate/src/main/scala/sigmastate/serialization/transformers/SigmaTransformerSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/transformers/SigmaTransformerSerializer.scala index 7e30a5786a..c8fe96e1bb 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/transformers/SigmaTransformerSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/transformers/SigmaTransformerSerializer.scala @@ -3,6 +3,7 @@ package sigmastate.serialization.transformers import sigmastate.{SigmaTransformerCompanion, SigmaTransformer} import sigmastate.Values.{SigmaPropValue, SValue} import sigmastate.serialization.ValueSerializer +import sigmastate.util.safeNewArray import sigmastate.utils.SigmaByteWriter.{DataInfo, valuesItemInfo} import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import spire.syntax.all.cfor @@ -17,8 +18,8 @@ case class SigmaTransformerSerializer[I <: SigmaPropValue, O <: SigmaPropValue] w.putValues(obj.items, itemsInfo, itemsItemInfo) override def parse(r: SigmaByteReader): SigmaPropValue = { - val itemsSize = r.getUInt().toInt - val res = ValueSerializer.newArray[SigmaPropValue](itemsSize) + val itemsSize = r.getUIntExact + val res = safeNewArray[SigmaPropValue](itemsSize) cfor(0)(_ < itemsSize, _ + 1) { i => res(i) = r.getValue().asInstanceOf[SigmaPropValue] } diff --git a/sigmastate/src/main/scala/sigmastate/sigmastate.scala b/sigmastate/src/main/scala/sigmastate/sigmastate.scala index 28b817a651..32d3600cd4 100644 --- a/sigmastate/src/main/scala/sigmastate/sigmastate.scala +++ b/sigmastate/src/main/scala/sigmastate/sigmastate.scala @@ -4,6 +4,38 @@ import sigmastate.lang.CheckingSigmaBuilder package object sigmastate { import CheckingSigmaBuilder._ + /** Represents cost estimation computed by JITC interpreter. + * The JITC costs use 10x more accurate scale comparing to block cost values. + * @see toBlockCost + */ + case class JitCost private[sigmastate] (private[sigmastate] val value: Int) extends AnyVal { + /** Adds two cost values. */ + def + (y: JitCost): JitCost = + new JitCost(java7.compat.Math.addExact(value, y.value)) + + /** Multiplies this cost to the given integer. */ + def * (n: Int): JitCost = + new JitCost(java7.compat.Math.multiplyExact(value, n)) + + /** Divides this cost by the given integer. */ + def / (n: Int): JitCost = + new JitCost(value / n) + + /** Return true if this value > y.value in the normal Int ordering. */ + def > (y: JitCost): Boolean = value > y.value + + /** Return true if this value >= y.value in the normal Int ordering. */ + def >= (y: JitCost): Boolean = value >= y.value + + /** Scales JitCost back to block cost value. This is inverse to JitCost.fromBlockCost. */ + def toBlockCost: Int = value / 10 + } + object JitCost { + /** Scales the given block cost to the JitCost scale. This is inverse to toBlockCost*/ + def fromBlockCost(blockCost: Int): JitCost = + new JitCost(java7.compat.Math.multiplyExact(blockCost, 10)) + } + /** * SInt addition */ diff --git a/sigmastate/src/main/scala/sigmastate/trees.scala b/sigmastate/src/main/scala/sigmastate/trees.scala index d30592c1b3..35f88bd956 100644 --- a/sigmastate/src/main/scala/sigmastate/trees.scala +++ b/sigmastate/src/main/scala/sigmastate/trees.scala @@ -168,9 +168,9 @@ case class BoolToSigmaProp(value: BoolValue) extends SigmaPropValue { SigmaDsl.sigmaProp(v) } } -object BoolToSigmaProp extends ValueCompanion { +object BoolToSigmaProp extends FixedCostValueCompanion { override def opCode: OpCode = OpCodes.BoolToSigmaPropCode - override val costKind = FixedCost(15) + override val costKind = FixedCost(JitCost(15)) val OpType = SFunc(SBoolean, SSigmaProp) } @@ -186,9 +186,9 @@ case class CreateProveDlog(value: Value[SGroupElement.type]) extends SigmaPropVa SigmaDsl.proveDlog(v) } } -object CreateProveDlog extends ValueCompanion { +object CreateProveDlog extends FixedCostValueCompanion { override def opCode: OpCode = OpCodes.ProveDlogCode - override val costKind = FixedCost(10) + override val costKind = FixedCost(JitCost(10)) val OpType = SFunc(SGroupElement, SSigmaProp) } @@ -227,9 +227,9 @@ case class CreateProveDHTuple(gv: Value[SGroupElement.type], SigmaDsl.proveDHTuple(g, h, u, v) } } -object CreateProveDHTuple extends ValueCompanion { +object CreateProveDHTuple extends FixedCostValueCompanion { override def opCode: OpCode = OpCodes.ProveDiffieHellmanTupleCode - override val costKind = FixedCost(20) + override val costKind = FixedCost(JitCost(20)) } trait SigmaTransformer[IV <: SigmaPropValue, OV <: SigmaPropValue] extends SigmaPropValue { @@ -264,7 +264,8 @@ object SigmaAnd extends SigmaTransformerCompanion { * - constructing new CSigmaProp and allocation collection * - one iteration over collection of items */ - override val costKind = PerItemCost(baseCost = 10, perChunkCost = 2, chunkSize = 1) + override val costKind = PerItemCost( + baseCost = JitCost(10), perChunkCost = JitCost(2), chunkSize = 1) override def argInfos: Seq[ArgInfo] = SigmaAndInfo.argInfos def apply(first: SigmaPropValue, second: SigmaPropValue, tail: SigmaPropValue*): SigmaAnd = SigmaAnd(Array(first, second) ++ tail) } @@ -294,7 +295,8 @@ object SigmaOr extends SigmaTransformerCompanion { /** BaseCost: * - constructing new CSigmaProp and allocation collection * - one iteration over collection of items */ - override val costKind = PerItemCost(baseCost = 10, perChunkCost = 2, chunkSize = 1) + override val costKind = PerItemCost( + baseCost = JitCost(10), perChunkCost = JitCost(2), chunkSize = 1) override def argInfos: Seq[ArgInfo] = SigmaOrInfo.argInfos def apply(head: SigmaPropValue, tail: SigmaPropValue*): SigmaOr = SigmaOr(head +: tail) } @@ -335,7 +337,8 @@ object OR extends LogicalTransformerCompanion { * Per-chunk cost: cost of scala `||` operations amortized over a chunk of boolean values. * @see BinOr * @see AND */ - override val costKind = PerItemCost(5, 5, 64/*size of cache line in bytes*/) + override val costKind = PerItemCost( + baseCost = JitCost(5), perChunkCost = JitCost(5), chunkSize = 64/*size of cache line in bytes*/) override def argInfos: Seq[ArgInfo] = Operations.ORInfo.argInfos def apply(children: Seq[Value[SBoolean.type]]): OR = @@ -354,20 +357,7 @@ case class XorOf(input: Value[SCollection[SBoolean.type]]) val inputV = input.evalTo[Coll[Boolean]](env) val len = inputV.length addSeqCost(XorOf.costKind, len) { () => - val res = if (E.context.activatedScriptVersion >= 2) { - if (len == 0) false - else if (len == 1) inputV(0) - else { - var res = inputV(0) - cfor(1)(_ < len, _ + 1) { i => - res ^= inputV(i) - } - res - } - } else { - SigmaDsl.xorOf(inputV) - } - res + SigmaDsl.xorOf(inputV) } } } @@ -378,7 +368,8 @@ object XorOf extends LogicalTransformerCompanion { * Per-chunk cost: cost of scala `||` operations amortized over a chunk of boolean values. * @see BinOr * @see AND */ - override val costKind = PerItemCost(20, 5, 32) + override val costKind = PerItemCost( + baseCost = JitCost(20), perChunkCost = JitCost(5), chunkSize = 32) override def argInfos: Seq[ArgInfo] = Operations.XorOfInfo.argInfos def apply(children: Seq[Value[SBoolean.type]]): XorOf = @@ -416,7 +407,8 @@ object AND extends LogicalTransformerCompanion { * Per-chunk cost: cost of scala `&&` operations amortized over a chunk of boolean values. * @see BinAnd * @see OR */ - override val costKind = PerItemCost(10, 5, 32/* half size of cache line in bytes */) + override val costKind = PerItemCost( + baseCost = JitCost(10), perChunkCost = JitCost(5), chunkSize = 32/* half size of cache line in bytes */) override def argInfos: Seq[ArgInfo] = Operations.ANDInfo.argInfos def apply(children: Seq[Value[SBoolean.type]]): AND = @@ -452,7 +444,8 @@ object AtLeast extends ValueCompanion { /** Base cost: constructing new CSigmaProp value * Per chunk cost: obtaining SigmaBooleans for each chunk in AtLeast */ - override val costKind = PerItemCost(20, 3, 5) + override val costKind = PerItemCost( + baseCost = JitCost(20), perChunkCost = JitCost(3), chunkSize = 5) val OpType: SFunc = SFunc(Array(SInt, SCollection.SBooleanArray), SBoolean) val MaxChildrenCount: Int = SigmaConstants.MaxChildrenCountForAtLeastOp.value @@ -547,9 +540,9 @@ trait NumericCastCompanion extends ValueCompanion { * implementation. */ object NumericCastCostKind extends TypeBasedCost { - override def costFunc(targetTpe: SType): Int = targetTpe match { - case SBigInt => 30 - case _ => 10 + override def costFunc(targetTpe: SType): JitCost = targetTpe match { + case SBigInt => JitCost(30) + case _ => JitCost(10) } } @@ -601,7 +594,7 @@ case class LongToByteArray(input: Value[SLong.type]) object LongToByteArray extends SimpleTransformerCompanion { val OpType = SFunc(SLong, SByteArray) override def opCode: OpCode = OpCodes.LongToByteArrayCode - override val costKind = FixedCost(17) + override val costKind = FixedCost(JitCost(17)) override def argInfos: Seq[ArgInfo] = LongToByteArrayInfo.argInfos } @@ -621,7 +614,7 @@ case class ByteArrayToLong(input: Value[SByteArray]) object ByteArrayToLong extends SimpleTransformerCompanion { val OpType = SFunc(SByteArray, SLong) override def opCode: OpCode = OpCodes.ByteArrayToLongCode - override val costKind = FixedCost(16) + override val costKind = FixedCost(JitCost(16)) override def argInfos: Seq[ArgInfo] = ByteArrayToLongInfo.argInfos } @@ -641,7 +634,7 @@ case class ByteArrayToBigInt(input: Value[SByteArray]) object ByteArrayToBigInt extends SimpleTransformerCompanion { val OpType = SFunc(SByteArray, SBigInt) override def opCode: OpCode = OpCodes.ByteArrayToBigIntCode - override val costKind = FixedCost(30) + override val costKind = FixedCost(JitCost(30)) override def argInfos: Seq[ArgInfo] = ByteArrayToBigIntInfo.argInfos } @@ -665,7 +658,7 @@ object DecodePoint extends SimpleTransformerCompanion with FixedCostValueCompani * 1) create reader and read bytes in a new array * 2) calling curve.decodePoint and obtain EcPoint * 3) wrap EcPoint in GroupElement*/ - override val costKind = FixedCost(300) + override val costKind = FixedCost(JitCost(300)) override def argInfos: Seq[ArgInfo] = DecodePointInfo.argInfos } @@ -717,7 +710,8 @@ object CalcBlake2b256 extends SimpleTransformerCompanion { * * @see [[sigmastate.interpreter.ErgoTreeEvaluator.DataBlockSize]] */ - override val costKind = PerItemCost(baseCost = 20, perChunkCost = 7, chunkSize = 128) + override val costKind = PerItemCost( + baseCost = JitCost(20), perChunkCost = JitCost(7), chunkSize = 128) override def argInfos: Seq[ArgInfo] = CalcBlake2b256Info.argInfos } @@ -738,7 +732,8 @@ case class CalcSha256(override val input: Value[SByteArray]) extends CalcHash { object CalcSha256 extends SimpleTransformerCompanion { override def opCode: OpCode = OpCodes.CalcSha256Code /** perChunkCost - cost of hashing 64 bytes of data (see also CalcBlake2b256). */ - override val costKind = PerItemCost(baseCost = 80, perChunkCost = 8, chunkSize = 64) + override val costKind = PerItemCost( + baseCost = JitCost(80), perChunkCost = JitCost(8), chunkSize = 64) override def argInfos: Seq[ArgInfo] = CalcSha256Info.argInfos } @@ -767,8 +762,8 @@ case class SubstConstants[T <: SType](scriptBytes: Value[SByteArray], positions: val newValuesV = newValues.evalTo[Coll[T#WrappedType]](env) var res: Coll[Byte] = null E.addSeqCost(SubstConstants.costKind, SubstConstants.opDesc) { () => - val typedNewVals: Array[SValue] = newValuesV.toArray.map { v => - TransformingSigmaBuilder.liftAny(v) match { + val typedNewVals: Array[Constant[SType]] = newValuesV.toArray.map { v => + TransformingSigmaBuilder.liftToConstant(v) match { case Nullable(v) => v case _ => sys.error(s"Cannot evaluate substConstants($scriptBytesV, $positionsV, $newValuesV): cannot lift value $v") } @@ -788,7 +783,8 @@ case class SubstConstants[T <: SType](scriptBytes: Value[SByteArray], positions: object SubstConstants extends ValueCompanion { override def opCode: OpCode = OpCodes.SubstConstantsCode - override val costKind = PerItemCost(100, 100, 1) + override val costKind = PerItemCost( + baseCost = JitCost(100), perChunkCost = JitCost(100), chunkSize = 1) val OpType = SFunc(Array(SByteArray, SIntArray, SCollection(SType.tT)), SByteArray) @@ -807,7 +803,7 @@ object SubstConstants extends ValueCompanion { */ def eval(scriptBytes: Array[Byte], positions: Array[Int], - newVals: Array[Value[SType]])(implicit vs: SigmaValidationSettings): (Array[Byte], Int) = + newVals: Array[Constant[SType]])(implicit vs: SigmaValidationSettings): (Array[Byte], Int) = ErgoTreeSerializer.DefaultSerializer.substituteConstants(scriptBytes, positions, newVals) } @@ -885,9 +881,9 @@ object ArithOp { * 2) calling method of Numeric */ override val costKind = new TypeBasedCost { - override def costFunc(tpe: SType): Int = tpe match { - case SBigInt => 20 - case _ => 15 + override def costFunc(tpe: SType): JitCost = tpe match { + case SBigInt => JitCost(20) + case _ => JitCost(15) } } } @@ -900,9 +896,9 @@ object ArithOp { * 2) calling method of Numeric */ override val costKind = new TypeBasedCost { - override def costFunc(tpe: SType): Int = tpe match { - case SBigInt => 20 - case _ => 15 + override def costFunc(tpe: SType): JitCost = tpe match { + case SBigInt => JitCost(20) + case _ => JitCost(15) } } } @@ -915,9 +911,9 @@ object ArithOp { * 2) calling method of Numeric */ override val costKind = new TypeBasedCost { - override def costFunc(tpe: SType): Int = tpe match { - case SBigInt => 25 - case _ => 15 + override def costFunc(tpe: SType): JitCost = tpe match { + case SBigInt => JitCost(25) + case _ => JitCost(15) } } } @@ -930,9 +926,9 @@ object ArithOp { * 2) calling method of Integral */ override val costKind = new TypeBasedCost { - override def costFunc(tpe: SType): Int = tpe match { - case SBigInt => 25 - case _ => 15 + override def costFunc(tpe: SType): JitCost = tpe match { + case SBigInt => JitCost(25) + case _ => JitCost(15) } } } @@ -947,9 +943,9 @@ object ArithOp { * 2) calling method of Integral */ override val costKind = new TypeBasedCost { - override def costFunc(tpe: SType): Int = tpe match { - case SBigInt => 25 - case _ => 15 + override def costFunc(tpe: SType): JitCost = tpe match { + case SBigInt => JitCost(25) + case _ => JitCost(15) } } } @@ -962,9 +958,9 @@ object ArithOp { * 2) calling method of ExactOrdering */ override val costKind = new TypeBasedCost { - override def costFunc(tpe: SType): Int = tpe match { - case SBigInt => 10 - case _ => 5 + override def costFunc(tpe: SType): JitCost = tpe match { + case SBigInt => JitCost(10) + case _ => JitCost(5) } } } @@ -977,9 +973,9 @@ object ArithOp { * 2) calling method of ExactOrdering */ override val costKind = new TypeBasedCost { - override def costFunc(tpe: SType): Int = tpe match { - case SBigInt => 10 - case _ => 5 + override def costFunc(tpe: SType): JitCost = tpe match { + case SBigInt => JitCost(10) + case _ => JitCost(5) } } } @@ -1023,9 +1019,9 @@ case class Negation[T <: SType](input: Value[T]) extends OneArgumentOperation[T, i.negate(inputV) } } -object Negation extends OneArgumentOperationCompanion { +object Negation extends OneArgumentOperationCompanion with FixedCostValueCompanion { override def opCode: OpCode = OpCodes.NegationCode - override val costKind = FixedCost(30) + override val costKind = FixedCost(JitCost(30)) override def argInfos: Seq[ArgInfo] = NegationInfo.argInfos } @@ -1057,22 +1053,22 @@ abstract class BitOpCompanion(val opCode: OpCode, val name: String, _argInfos: = object BitOp { import OpCodes._ object BitOr extends BitOpCompanion(BitOrCode, "|", BitOrInfo.argInfos) { - override val costKind = FixedCost(1) + override val costKind = FixedCost(JitCost(1)) } object BitAnd extends BitOpCompanion(BitAndCode, "&", BitAndInfo.argInfos) { - override val costKind = FixedCost(1) + override val costKind = FixedCost(JitCost(1)) } object BitXor extends BitOpCompanion(BitXorCode, "^", BitXorInfo.argInfos) { - override val costKind = FixedCost(1) + override val costKind = FixedCost(JitCost(1)) } object BitShiftRight extends BitOpCompanion(BitShiftRightCode, ">>", BitShiftRightInfo.argInfos) { - override val costKind = FixedCost(1) + override val costKind = FixedCost(JitCost(1)) } object BitShiftLeft extends BitOpCompanion(BitShiftLeftCode, "<<", BitShiftLeftInfo.argInfos) { - override val costKind = FixedCost(1) + override val costKind = FixedCost(JitCost(1)) } object BitShiftRightZeroed extends BitOpCompanion(BitShiftRightZeroedCode, ">>>", BitShiftRightZeroedInfo.argInfos) { - override val costKind = FixedCost(1) + override val costKind = FixedCost(JitCost(1)) } val operations: Map[Byte, BitOpCompanion] = @@ -1084,7 +1080,7 @@ object BitOp { } } -// TODO HF (24h): implement modular operations +// TODO v6.0 (24h): implement modular operations case class ModQ(input: Value[SBigInt.type]) extends NotReadyValue[SBigInt.type] { override def companion = ModQ @@ -1093,7 +1089,7 @@ case class ModQ(input: Value[SBigInt.type]) } object ModQ extends ValueCompanion { override def opCode: OpCode = OpCodes.ModQCode - override val costKind: CostKind = FixedCost(1) + override val costKind: CostKind = FixedCost(JitCost(1)) } case class ModQArithOp(left: Value[SBigInt.type], right: Value[SBigInt.type], override val opCode: OpCode) @@ -1104,7 +1100,7 @@ case class ModQArithOp(left: Value[SBigInt.type], right: Value[SBigInt.type], ov } abstract class ModQArithOpCompanion(val opCode: OpCode, val name: String) extends ValueCompanion { def argInfos: Seq[ArgInfo] - override val costKind: CostKind = FixedCost(1) + override val costKind: CostKind = FixedCost(JitCost(1)) } trait OpGroup[C <: ValueCompanion] { @@ -1143,16 +1139,22 @@ case class Xor(override val left: Value[SByteArray], protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { val lV = left.evalTo[Coll[Byte]](env) val rV = right.evalTo[Coll[Byte]](env) - addSeqCost(Xor.costKind, lV.length) { () => - Colls.xor(lV, rV) - } + Xor.xorWithCosting(lV, rV) } } object Xor extends TwoArgumentOperationCompanion { val OpType = SFunc(Array(SByteArray, SByteArray), SByteArray) override def opCode: OpCode = XorCode - override val costKind = PerItemCost(10, 2, 128) + override val costKind = PerItemCost( + baseCost = JitCost(10), perChunkCost = JitCost(2), chunkSize = 128) override def argInfos: Seq[ArgInfo] = XorInfo.argInfos + + /** Helper method which compute xor with correct costing accumulation */ + def xorWithCosting(ls: Coll[Byte], rs: Coll[Byte])(implicit E: ErgoTreeEvaluator): Coll[Byte] = { + E.addSeqCost(Xor.costKind, math.min(ls.length, rs.length), Xor.opDesc) { () => + Colls.xor(ls, rs) + } + } } case class Exponentiate(override val left: Value[SGroupElement.type], @@ -1173,7 +1175,7 @@ object Exponentiate extends TwoArgumentOperationCompanion with FixedCostValueCom val OpType = SFunc(Array(SGroupElement, SBigInt), SGroupElement) override def opCode: OpCode = ExponentiateCode /** Cost of: 1) calling EcPoint.multiply 2) wrapping in GroupElement */ - override val costKind = FixedCost(900) + override val costKind = FixedCost(JitCost(900)) override def argInfos: Seq[ArgInfo] = ExponentiateInfo.argInfos } @@ -1194,7 +1196,7 @@ object MultiplyGroup extends TwoArgumentOperationCompanion with FixedCostValueCo val OpType = SFunc(Array(SGroupElement, SGroupElement), SGroupElement) override def opCode: OpCode = MultiplyGroupCode /** Cost of: 1) calling EcPoint.add 2) wrapping in GroupElement */ - override val costKind = FixedCost(40) + override val costKind = FixedCost(JitCost(40)) override def argInfos: Seq[ArgInfo] = MultiplyGroupInfo.argInfos } // Relation @@ -1234,9 +1236,9 @@ object LT extends RelationCompanion { * 2) calling method of Numeric */ override val costKind = new TypeBasedCost { - override def costFunc(tpe: SType): Int = tpe match { - case SBigInt => 20 - case _ => 20 + override def costFunc(tpe: SType): JitCost = tpe match { + case SBigInt => JitCost(20) + case _ => JitCost(20) } } override def argInfos: Seq[ArgInfo] = LTInfo.argInfos @@ -1261,9 +1263,9 @@ object LE extends RelationCompanion { * 2) calling method of Numeric */ override val costKind = new TypeBasedCost { - override def costFunc(tpe: SType): Int = tpe match { - case SBigInt => 20 // cf. comparisonBigInt - case _ => 20 // cf. comparisonCost + override def costFunc(tpe: SType): JitCost = tpe match { + case SBigInt => JitCost(20) // cf. comparisonBigInt + case _ => JitCost(20) // cf. comparisonCost } } override def argInfos: Seq[ArgInfo] = LEInfo.argInfos @@ -1288,9 +1290,9 @@ object GT extends RelationCompanion { * 2) calling method of Numeric */ override val costKind = new TypeBasedCost { - override def costFunc(tpe: SType): Int = tpe match { - case SBigInt => 20 // cf. comparisonBigInt - case _ => 20 // cf. comparisonCost + override def costFunc(tpe: SType): JitCost = tpe match { + case SBigInt => JitCost(20) // cf. comparisonBigInt + case _ => JitCost(20) // cf. comparisonCost } } override def argInfos: Seq[ArgInfo] = GTInfo.argInfos @@ -1315,9 +1317,9 @@ object GE extends RelationCompanion { * 2) calling method of Numeric */ override val costKind = new TypeBasedCost { - override def costFunc(tpe: SType): Int = tpe match { - case SBigInt => 20 - case _ => 20 + override def costFunc(tpe: SType): JitCost = tpe match { + case SBigInt => JitCost(20) + case _ => JitCost(20) } } override def argInfos: Seq[ArgInfo] = GEInfo.argInfos @@ -1384,7 +1386,7 @@ object BinOr extends RelationCompanion with FixedCostValueCompanion { override def opCode: OpCode = BinOrCode /** Cost of: scala `||` operation * Old cost: ("BinOr", "(Boolean, Boolean) => Boolean", logicCost) */ - override val costKind = FixedCost(20) + override val costKind = FixedCost(JitCost(20)) override def argInfos: Seq[ArgInfo] = BinOrInfo.argInfos } @@ -1407,7 +1409,7 @@ object BinAnd extends RelationCompanion with FixedCostValueCompanion { override def opCode: OpCode = BinAndCode /** Cost of: scala `&&` operation * Old cost: ("BinAnd", "(Boolean, Boolean) => Boolean", logicCost) */ - override val costKind = FixedCost(20) + override val costKind = FixedCost(JitCost(20)) override def argInfos: Seq[ArgInfo] = BinAndInfo.argInfos } @@ -1427,7 +1429,7 @@ object BinXor extends RelationCompanion with FixedCostValueCompanion { override def opCode: OpCode = BinXorCode /** Cost of: scala `^` operation * Old cost: ("BinXor", "(Boolean, Boolean) => Boolean", logicCost) */ - override val costKind = FixedCost(20) + override val costKind = FixedCost(JitCost(20)) override def argInfos: Seq[ArgInfo] = BinXorInfo.argInfos } @@ -1496,11 +1498,11 @@ case class If[T <: SType](condition: Value[SBoolean.type], trueBranch: Value[T], } } } -object If extends QuadrupleCompanion { +object If extends QuadrupleCompanion with FixedCostValueCompanion { override def opCode: OpCode = OpCodes.IfCode /** Cost of: conditional switching to the right branch (excluding the cost both * condition itself and the branches) */ - override val costKind = FixedCost(10) + override val costKind = FixedCost(JitCost(10)) override def argInfos: Seq[ArgInfo] = IfInfo.argInfos val GenericOpType = SFunc(Array(SBoolean, SType.tT, SType.tT), SType.tT) } @@ -1518,7 +1520,7 @@ object LogicalNot extends FixedCostValueCompanion { val OpType = SFunc(Array(SBoolean), SBoolean) override def opCode: OpCode = OpCodes.LogicalNotCode /** Cost of: scala `!` operation */ - override val costKind = FixedCost(15) + override val costKind = FixedCost(JitCost(15)) } diff --git a/sigmastate/src/main/scala/sigmastate/types.scala b/sigmastate/src/main/scala/sigmastate/types.scala index 77c3fdce1a..c5ca17644d 100644 --- a/sigmastate/src/main/scala/sigmastate/types.scala +++ b/sigmastate/src/main/scala/sigmastate/types.scala @@ -62,7 +62,11 @@ trait SigmaNodeCompanion * Collection of non-primitive type is serialized as (CollectionTypeCode, serialize(elementType)) * */ sealed trait SType extends SigmaNode { + /** The underlying Scala type of data values described by this type descriptor. + * E.g. scala.Int for SInt descriptor. + */ type WrappedType + /** Type code used in serialization of SType values. * @see TypeSerializer */ @@ -818,7 +822,7 @@ object SNumericType extends STypeCompanion { /** Array of all numeric types ordered by number of bytes in the representation. */ final val allNumericTypes = Array(SByte, SShort, SInt, SLong, SBigInt) - // TODO HF (4h): this typeId is now shadowed by SGlobal.typeId + // TODO v6.0 (4h): this typeId is now shadowed by SGlobal.typeId // see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/667 override def typeId: TypeCode = 106: Byte @@ -863,7 +867,7 @@ object SNumericType extends STypeCompanion { .withInfo(PropertyCall, "Converts this numeric value to \\lst{BigInt}") /** Cost of: 1) creating Byte collection from a numeric value */ - val ToBytes_CostKind = FixedCost(5) + val ToBytes_CostKind = FixedCost(JitCost(5)) val ToBytesMethod: SMethod = SMethod( this, "toBytes", SFunc(tNum, SByteArray), 6, ToBytes_CostKind) @@ -876,7 +880,7 @@ object SNumericType extends STypeCompanion { /** Cost of: 1) creating Boolean collection (one bool for each bit) from a numeric * value. */ - val ToBits_CostKind = FixedCost(5) + val ToBits_CostKind = FixedCost(JitCost(5)) val ToBitsMethod: SMethod = SMethod( this, "toBits", SFunc(tNum, SBooleanArray), 7, ToBits_CostKind) @@ -1095,13 +1099,13 @@ case object SBigInt extends SPrimType with SEmbeddable with SNumericType with SM * is upcoming soft-forks at which point the cost parameters should be calculated and * changed. */ - val ModQMethod = SMethod(this, "modQ", SFunc(this, SBigInt), 1, FixedCost(1)) + val ModQMethod = SMethod(this, "modQ", SFunc(this, SBigInt), 1, FixedCost(JitCost(1))) .withInfo(ModQ, "Returns this \\lst{mod} Q, i.e. remainder of division by Q, where Q is an order of the cryprographic group.") - val PlusModQMethod = SMethod(this, "plusModQ", SFunc(IndexedSeq(this, SBigInt), SBigInt), 2, FixedCost(1)) + val PlusModQMethod = SMethod(this, "plusModQ", SFunc(IndexedSeq(this, SBigInt), SBigInt), 2, FixedCost(JitCost(1))) .withInfo(ModQArithOp.PlusModQ, "Adds this number with \\lst{other} by module Q.", ArgInfo("other", "Number to add to this.")) - val MinusModQMethod = SMethod(this, "minusModQ", SFunc(IndexedSeq(this, SBigInt), SBigInt), 3, FixedCost(1)) + val MinusModQMethod = SMethod(this, "minusModQ", SFunc(IndexedSeq(this, SBigInt), SBigInt), 3, FixedCost(JitCost(1))) .withInfo(ModQArithOp.MinusModQ, "Subtracts \\lst{other} number from this by module Q.", ArgInfo("other", "Number to subtract from this.")) - val MultModQMethod = SMethod(this, "multModQ", SFunc(IndexedSeq(this, SBigInt), SBigInt), 4, FixedCost(1)) + val MultModQMethod = SMethod(this, "multModQ", SFunc(IndexedSeq(this, SBigInt), SBigInt), 4, FixedCost(JitCost(1))) .withIRInfo(MethodCallIrBuilder) .withInfo(MethodCall, "Multiply this number with \\lst{other} by module Q.", ArgInfo("other", "Number to multiply with this.")) @@ -1136,7 +1140,7 @@ case object SGroupElement extends SProduct with SPrimType with SEmbeddable with override def coster: Option[CosterFactory] = Some(Coster(_.GroupElementCoster)) /** Cost of: 1) serializing EcPointType to bytes 2) packing them in Coll. */ - val GetEncodedCostKind = FixedCost(250) + val GetEncodedCostKind = FixedCost(JitCost(250)) /** The following SMethod instances are descriptors of methods defined in `GroupElement` type. */ lazy val GetEncodedMethod: SMethod = SMethod( @@ -1161,7 +1165,7 @@ case object SGroupElement extends SProduct with SPrimType with SEmbeddable with .withInfo(MultiplyGroup, "Group operation.", ArgInfo("other", "other element of the group")) /** Cost of: 1) calling EcPoint.negate 2) wrapping in GroupElement. */ - val Negate_CostKind = FixedCost(45) + val Negate_CostKind = FixedCost(JitCost(45)) lazy val NegateMethod: SMethod = SMethod( this, "negate", SFunc(this, this), 5, Negate_CostKind) @@ -1297,15 +1301,24 @@ object SOption extends STypeCompanion { /** The following SMethod instances are descriptors of methods defined in `Option` type. */ val IsDefinedMethod = SMethod( this, IsDefined, SFunc(ThisType, SBoolean), 2, OptionIsDefined.costKind) + .withIRInfo({ + case (builder, obj, _, args, _) if args.isEmpty => builder.mkOptionIsDefined(obj.asValue[SOption[SType]]) + }) .withInfo(OptionIsDefined, "Returns \\lst{true} if the option is an instance of \\lst{Some}, \\lst{false} otherwise.") val GetMethod = SMethod(this, Get, SFunc(ThisType, tT), 3, OptionGet.costKind) + .withIRInfo({ + case (builder, obj, _, args, _) if args.isEmpty => builder.mkOptionGet(obj.asValue[SOption[SType]]) + }) .withInfo(OptionGet, """Returns the option's value. The option must be nonempty. Throws exception if the option is empty.""") lazy val GetOrElseMethod = SMethod( this, GetOrElse, SFunc(Array(ThisType, tT), tT, Array[STypeParam](tT)), 4, OptionGetOrElse.costKind) + .withIRInfo(irBuilder = { + case (builder, obj, _, Seq(d), _) => builder.mkOptionGetOrElse(obj.asValue[SOption[SType]], d) + }) .withInfo(OptionGetOrElse, """Returns the option's value if the option is nonempty, otherwise |return the result of evaluating \lst{default}. @@ -1313,7 +1326,7 @@ object SOption extends STypeCompanion { // TODO soft-fork: https://github.com/ScorexFoundation/sigmastate-interpreter/issues/479 // val FoldMethod = SMethod( -// this, Fold, SFunc(Array(ThisType, tR, SFunc(tT, tR)), tR, Array[STypeParam](tT, tR)), 5, FixedCost(1)) +// this, Fold, SFunc(Array(ThisType, tR, SFunc(tT, tR)), tR, Array[STypeParam](tT, tR)), 5, FixedCost(JitCost(1))) // .withInfo(MethodCall, // """Returns the result of applying \lst{f} to this option's // | value if the option is nonempty. Otherwise, evaluates @@ -1324,7 +1337,7 @@ object SOption extends STypeCompanion { // ArgInfo("f", "the function to apply if nonempty")) val MapMethod = SMethod(this, "map", - SFunc(Array(ThisType, SFunc(tT, tR)), SOption(tR), Array(paramT, paramR)), 7, FixedCost(20)) + SFunc(Array(ThisType, SFunc(tT, tR)), SOption(tR), Array(paramT, paramR)), 7, FixedCost(JitCost(20))) .withIRInfo(MethodCallIrBuilder) .withInfo(MethodCall, """Returns a \lst{Some} containing the result of applying \lst{f} to this option's @@ -1333,7 +1346,7 @@ object SOption extends STypeCompanion { """.stripMargin, ArgInfo("f", "the function to apply")) val FilterMethod = SMethod(this, "filter", - SFunc(Array(ThisType, SFunc(tT, SBoolean)), ThisType, Array(paramT)), 8, FixedCost(20)) + SFunc(Array(ThisType, SFunc(tT, SBoolean)), ThisType, Array(paramT)), 8, FixedCost(JitCost(20))) .withIRInfo(MethodCallIrBuilder) .withInfo(MethodCall, """Returns this option if it is nonempty and applying the predicate \lst{p} to @@ -1413,7 +1426,7 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { .withInfo(SizeOf, "The size of the collection in elements.") val GetOrElseMethod = SMethod( - this, "getOrElse", SFunc(Array(ThisType, SInt, tIV), tIV, paramIVSeq), 2, ByIndex.costKind) + this, "getOrElse", SFunc(Array(ThisType, SInt, tIV), tIV, paramIVSeq), 2, DynamicCost) .withIRInfo({ case (builder, obj, _, Seq(index, defaultValue), _) => val index1 = index.asValue[SInt.type] val defaultValue1 = defaultValue.asValue[SType] @@ -1423,8 +1436,22 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { ArgInfo("index", "index of the element of this collection"), ArgInfo("default", "value to return when \\lst{index} is out of range")) + /** Implements evaluation of Coll.getOrElse method call ErgoTree node. + * Called via reflection based on naming convention. + * @see SMethod.evalMethod + */ + def getOrElse_eval[A](mc: MethodCall, xs: Coll[A], i: Int, default: A)(implicit E: ErgoTreeEvaluator): A = { + E.addCost(ByIndex.costKind, mc.method.opDesc) + // the following lines should be semantically the same as in ByIndex.eval + Value.checkType(mc.args.last.tpe, default) + xs.getOrElse(i, default) + } + val MapMethod = SMethod(this, "map", SFunc(Array(ThisType, SFunc(tIV, tOV)), tOVColl, Array(paramIV, paramOV)), 3, MapCollection.costKind) + .withIRInfo({ + case (builder, obj, _, Seq(mapper), _) => builder.mkMapCollection(obj.asValue[SCollection[SType]], mapper.asFunc) + }) .withInfo(MapCollection, """ Builds a new collection by applying a function to all elements of this collection. | Returns a new collection of type \lst{Coll[B]} resulting from applying the given function @@ -1445,6 +1472,9 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { val ExistsMethod = SMethod(this, "exists", SFunc(Array(ThisType, tPredicate), SBoolean, paramIVSeq), 4, Exists.costKind) + .withIRInfo({ + case (builder, obj, _, Seq(c), _) => builder.mkExists(obj.asValue[SCollection[SType]], c.asFunc) + }) .withInfo(Exists, """Tests whether a predicate holds for at least one element of this collection. |Returns \lst{true} if the given predicate \lst{p} is satisfied by at least one element of this collection, otherwise \lst{false} @@ -1455,12 +1485,18 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { this, "fold", SFunc(Array(ThisType, tOV, SFunc(Array(tOV, tIV), tOV)), tOV, Array(paramIV, paramOV)), 5, Fold.costKind) + .withIRInfo({ + case (builder, obj, _, Seq(z, op), _) => builder.mkFold(obj.asValue[SCollection[SType]], z, op.asFunc) + }) .withInfo(Fold, "Applies a binary operator to a start value and all elements of this collection, going left to right.", ArgInfo("zero", "a starting value"), ArgInfo("op", "the binary operator")) val ForallMethod = SMethod(this, "forall", SFunc(Array(ThisType, tPredicate), SBoolean, paramIVSeq), 6, ForAll.costKind) + .withIRInfo({ + case (builder, obj, _, Seq(c), _) => builder.mkForAll(obj.asValue[SCollection[SType]], c.asFunc) + }) .withInfo(ForAll, """Tests whether a predicate holds for all elements of this collection. |Returns \lst{true} if this collection is empty or the given predicate \lst{p} @@ -1470,6 +1506,10 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { val SliceMethod = SMethod(this, "slice", SFunc(Array(ThisType, SInt, SInt), ThisType, paramIVSeq), 7, Slice.costKind) + .withIRInfo({ + case (builder, obj, _, Seq(from, until), _) => + builder.mkSlice(obj.asCollection[SType], from.asIntValue, until.asIntValue) + }) .withInfo(Slice, """Selects an interval of elements. The returned collection is made up | of all elements \lst{x} which satisfy the invariant: @@ -1512,7 +1552,8 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { """.stripMargin, ArgInfo("i", "the index")) /** Cost of creating a collection of indices */ - val IndicesMethod_CostKind = PerItemCost(baseCost = 20, perChunkCost = 2, chunkSize = 16) + val IndicesMethod_CostKind = PerItemCost( + baseCost = JitCost(20), perChunkCost = JitCost(2), chunkSize = 16) val IndicesMethod = SMethod( this, "indices", SFunc(ThisType, SCollection(SInt)), 14, IndicesMethod_CostKind) @@ -1539,7 +1580,8 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { * 1) cost of Coll.flatMap (per item) * 2) new collection is allocated for each item * 3) each collection is then appended to the resulting collection */ - val FlatMapMethod_CostKind = PerItemCost(baseCost = 30, perChunkCost = 5, chunkSize = 8) + val FlatMapMethod_CostKind = PerItemCost( + baseCost = JitCost(60), perChunkCost = JitCost(10), chunkSize = 8) val FlatMapMethod = SMethod(this, "flatMap", SFunc(Array(ThisType, SFunc(tIV, tOVColl)), tOVColl, Array(paramIV, paramOV)), @@ -1559,7 +1601,8 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { /** We assume all flatMap body patterns have similar executon cost. */ final val CheckFlatmapBody_Info = OperationCostInfo( - PerItemCost(20, 20, 1), NamedDesc("CheckFlatmapBody")) + PerItemCost(baseCost = JitCost(20), perChunkCost = JitCost(20), chunkSize = 1), + NamedDesc("CheckFlatmapBody")) /** This patterns recognize all expressions, which are allowed as lambda body @@ -1603,7 +1646,7 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { /** Operation descriptor for matching `flatMap` method calls with valid lambdas. */ final val MatchSingleArgMethodCall_Info = OperationCostInfo( - FixedCost(30), NamedDesc("MatchSingleArgMethodCall")) + FixedCost(JitCost(30)), NamedDesc("MatchSingleArgMethodCall")) /** Recognizer of `flatMap` method calls with valid lambdas. */ object IsSingleArgMethodCall { @@ -1630,18 +1673,21 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { if isValidPropertyAccess(varId, lambdaBody) => // ok, do nothing case _ => - ErgoTreeEvaluator.error( - s"Unsupported lambda in flatMap: allowed usage `xs.flatMap(x => x.property)`: $mc") + throwInvalidFlatmap(mc) } } + def throwInvalidFlatmap(mc: MethodCall) = { + ErgoTreeEvaluator.error( + s"Unsupported lambda in flatMap: allowed usage `xs.flatMap(x => x.property)`: $mc") + } + /** Implements evaluation of Coll.flatMap method call ErgoTree node. * Called via reflection based on naming convention. * @see SMethod.evalMethod */ def flatMap_eval[A, B](mc: MethodCall, xs: Coll[A], f: A => Coll[B]) (implicit E: ErgoTreeEvaluator): Coll[B] = { - checkValidFlatmap(mc) val m = mc.method var res: Coll[B] = null E.addSeqCost(m.costKind.asInstanceOf[PerItemCost], m.opDesc) { () => @@ -1655,7 +1701,7 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { val PatchMethod = SMethod(this, "patch", SFunc(Array(ThisType, SInt, ThisType, SInt), ThisType, paramIVSeq), - 19, PerItemCost(30, 2, 10)) + 19, PerItemCost(baseCost = JitCost(30), perChunkCost = JitCost(2), chunkSize = 10)) .withIRInfo(MethodCallIrBuilder) .withInfo(MethodCall, "Produces a new Coll where a slice of elements in this Coll is replaced by another Coll.") @@ -1675,7 +1721,7 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { val UpdatedMethod = SMethod(this, "updated", SFunc(Array(ThisType, SInt, tIV), ThisType, paramIVSeq), - 20, PerItemCost(20, 1, 10)) + 20, PerItemCost(baseCost = JitCost(20), perChunkCost = JitCost(1), chunkSize = 10)) .withIRInfo(MethodCallIrBuilder, javaMethodOf[Coll[_], Int, Any]("updated")) .withInfo(MethodCall, "A copy of this Coll with one single replaced element.") @@ -1695,7 +1741,7 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { val UpdateManyMethod = SMethod(this, "updateMany", SFunc(Array(ThisType, SCollection(SInt), ThisType), ThisType, paramIVSeq), - 21, PerItemCost(20, 2, 10)) + 21, PerItemCost(baseCost = JitCost(20), perChunkCost = JitCost(2), chunkSize = 10)) .withIRInfo(MethodCallIrBuilder).withInfo(MethodCall, "") /** Implements evaluation of Coll.updateMany method call ErgoTree node. @@ -1711,11 +1757,11 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { } val IndexOfMethod = SMethod(this, "indexOf", - SFunc(Array(ThisType, tIV, SInt), SInt, paramIVSeq), 26, PerItemCost(20, 10, 2)) + SFunc(Array(ThisType, tIV, SInt), SInt, paramIVSeq), + 26, PerItemCost(baseCost = JitCost(20), perChunkCost = JitCost(10), chunkSize = 2)) .withIRInfo(MethodCallIrBuilder, javaMethodOf[Coll[_], Any, Int]("indexOf")) .withInfo(MethodCall, "") - // TODO v5.0: optimize using specialization for numeric and predefined types /** Implements evaluation of Coll.indexOf method call ErgoTree node. * Called via reflection based on naming convention. * @see SMethod.evalMethod @@ -1725,7 +1771,7 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { val costKind = mc.method.costKind.asInstanceOf[PerItemCost] var res: Int = -1 E.addSeqCost(costKind, mc.method.opDesc) { () => - // TODO v5.0: this loop is bounded when MaxCollSize limit is enforced + // this loop is bounded because MaxArrayLength limit is enforced val len = xs.length val start = math.max(from, 0) var i = start @@ -1742,7 +1788,8 @@ object SCollection extends STypeCompanion with MethodByNameUnapply { } /** Cost descriptor of Coll.zip operation. */ - val Zip_CostKind = PerItemCost(baseCost = 10, perChunkCost = 1, chunkSize = 10) + val Zip_CostKind = PerItemCost( + baseCost = JitCost(10), perChunkCost = JitCost(1), chunkSize = 10) val ZipMethod = SMethod(this, "zip", SFunc(Array(ThisType, tOVColl), SCollection(STuple(tIV, tOV)), Array[STypeParam](tIV, tOV)), @@ -2109,7 +2156,7 @@ case object SBox extends SProduct with SPredefType with SMonoType { ArgInfo("regId", "zero-based identifier of the register.")) lazy val tokensMethod = SMethod( - this, "tokens", SFunc(SBox, ErgoBox.STokensRegType), 8, FixedCost(15)) + this, "tokens", SFunc(SBox, ErgoBox.STokensRegType), 8, FixedCost(JitCost(15))) .withIRInfo(MethodCallIrBuilder) .withInfo(PropertyCall, "Secondary tokens") @@ -2142,7 +2189,7 @@ case object SAvlTree extends SProduct with SPredefType with SMonoType { lazy val TCollOptionCollByte = SCollection(SByteArrayOption) lazy val CollKeyValue = SCollection(STuple(SByteArray, SByteArray)) - lazy val digestMethod = SMethod(this, "digest", SFunc(this, SByteArray), 1, FixedCost(15)) + lazy val digestMethod = SMethod(this, "digest", SFunc(this, SByteArray), 1, FixedCost(JitCost(15))) .withIRInfo(MethodCallIrBuilder) .withInfo(PropertyCall, """Returns digest of the state represented by this tree. @@ -2156,7 +2203,7 @@ case object SAvlTree extends SProduct with SPredefType with SMonoType { } lazy val enabledOperationsMethod = SMethod( - this, "enabledOperations", SFunc(this, SByte), 2, FixedCost(15)) + this, "enabledOperations", SFunc(this, SByte), 2, FixedCost(JitCost(15))) .withIRInfo(MethodCallIrBuilder) .withInfo(PropertyCall, """ Flags of enabled operations packed in single byte. @@ -2166,7 +2213,7 @@ case object SAvlTree extends SProduct with SPredefType with SMonoType { """.stripMargin) lazy val keyLengthMethod = SMethod( - this, "keyLength", SFunc(this, SInt), 3, FixedCost(15)) + this, "keyLength", SFunc(this, SInt), 3, FixedCost(JitCost(15))) .withIRInfo(MethodCallIrBuilder) .withInfo(PropertyCall, """ @@ -2174,7 +2221,7 @@ case object SAvlTree extends SProduct with SPredefType with SMonoType { """.stripMargin) lazy val valueLengthOptMethod = SMethod( - this, "valueLengthOpt", SFunc(this, SIntOption), 4, FixedCost(15)) + this, "valueLengthOpt", SFunc(this, SIntOption), 4, FixedCost(JitCost(15))) .withIRInfo(MethodCallIrBuilder) .withInfo(PropertyCall, """ @@ -2182,7 +2229,7 @@ case object SAvlTree extends SProduct with SPredefType with SMonoType { """.stripMargin) lazy val isInsertAllowedMethod = SMethod( - this, "isInsertAllowed", SFunc(this, SBoolean), 5, FixedCost(15)) + this, "isInsertAllowed", SFunc(this, SBoolean), 5, FixedCost(JitCost(15))) .withIRInfo(MethodCallIrBuilder) .withInfo(PropertyCall, """ @@ -2195,7 +2242,7 @@ case object SAvlTree extends SProduct with SPredefType with SMonoType { } lazy val isUpdateAllowedMethod = SMethod( - this, "isUpdateAllowed", SFunc(this, SBoolean), 6, FixedCost(15)) + this, "isUpdateAllowed", SFunc(this, SBoolean), 6, FixedCost(JitCost(15))) .withIRInfo(MethodCallIrBuilder) .withInfo(PropertyCall, """ @@ -2208,7 +2255,7 @@ case object SAvlTree extends SProduct with SPredefType with SMonoType { } lazy val isRemoveAllowedMethod = SMethod( - this, "isRemoveAllowed", SFunc(this, SBoolean), 7, FixedCost(15)) + this, "isRemoveAllowed", SFunc(this, SBoolean), 7, FixedCost(JitCost(15))) .withIRInfo(MethodCallIrBuilder) .withInfo(PropertyCall, """ @@ -2221,7 +2268,7 @@ case object SAvlTree extends SProduct with SPredefType with SMonoType { } lazy val updateOperationsMethod = SMethod(this, "updateOperations", - SFunc(Array(SAvlTree, SByte), SAvlTree), 8, FixedCost(45)) + SFunc(Array(SAvlTree, SByte), SAvlTree), 8, FixedCost(JitCost(45))) .withIRInfo(MethodCallIrBuilder) .withInfo(MethodCall, """ @@ -2250,19 +2297,24 @@ case object SAvlTree extends SProduct with SPredefType with SMonoType { * So the following is an approximation of the proof parsing cost. */ final val CreateAvlVerifier_Info = OperationCostInfo( - PerItemCost(110, 20, 64), NamedDesc("CreateAvlVerifier")) + PerItemCost(baseCost = JitCost(110), perChunkCost = JitCost(20), chunkSize = 64), + NamedDesc("CreateAvlVerifier")) final val LookupAvlTree_Info = OperationCostInfo( - PerItemCost(40, 10, 1), NamedDesc("LookupAvlTree")) + PerItemCost(baseCost = JitCost(40), perChunkCost = JitCost(10), chunkSize = 1), + NamedDesc("LookupAvlTree")) final val InsertIntoAvlTree_Info = OperationCostInfo( - PerItemCost(40, 10, 1), NamedDesc("InsertIntoAvlTree")) + PerItemCost(baseCost = JitCost(40), perChunkCost = JitCost(10), chunkSize = 1), + NamedDesc("InsertIntoAvlTree")) final val UpdateAvlTree_Info = OperationCostInfo( - PerItemCost(120, 20, 1), NamedDesc("UpdateAvlTree")) + PerItemCost(baseCost = JitCost(120), perChunkCost = JitCost(20), chunkSize = 1), + NamedDesc("UpdateAvlTree")) final val RemoveAvlTree_Info = OperationCostInfo( - PerItemCost(100, 15, 1), NamedDesc("RemoveAvlTree")) + PerItemCost(baseCost = JitCost(100), perChunkCost = JitCost(15), chunkSize = 1), + NamedDesc("RemoveAvlTree")) /** Creates [[AvlTreeVerifier]] for the given tree and proof. */ def createVerifier(tree: AvlTree, proof: Coll[Byte])(implicit E: ErgoTreeEvaluator) = { @@ -2407,7 +2459,7 @@ case object SAvlTree extends SProduct with SPredefType with SMonoType { E.addSeqCost(InsertIntoAvlTree_Info, nItems) { () => val insert = Insert(ADKey @@ key.toArray, ADValue @@ value.toArray) val insertRes = bv.performOneOperation(insert) - // TODO v5.0: throwing exception is not consistent with update semantics + // TODO v6.0: throwing exception is not consistent with update semantics // however it preserves v4.0 semantics if (insertRes.isFailure) { Interpreter.error(s"Incorrect insert for $tree (key: $key, value: $value, digest: ${tree.digest}): ${insertRes.failed.get}}") @@ -2527,7 +2579,7 @@ case object SAvlTree extends SProduct with SPredefType with SMonoType { } lazy val updateDigestMethod = SMethod(this, "updateDigest", - SFunc(Array(SAvlTree, SByteArray), SAvlTree), 15, FixedCost(40)) + SFunc(Array(SAvlTree, SByteArray), SAvlTree), 15, FixedCost(JitCost(40))) .withIRInfo(MethodCallIrBuilder) .withInfo(MethodCall, """ @@ -2574,20 +2626,24 @@ case object SContext extends SProduct with SPredefType with SMonoType { } override def isConstantSize = false + /** Arguments on context operation such as getVar, DeserializeContext etc. + * This value can be reused where necessary to avoid allocations. */ + val ContextFuncDom: IndexedSeq[SType] = Array(SContext, SByte) + import SType.{tT, paramT} - lazy val dataInputsMethod = propertyCall("dataInputs", SBoxArray, 1, FixedCost(15)) - lazy val headersMethod = propertyCall("headers", SHeaderArray, 2, FixedCost(15)) - lazy val preHeaderMethod = propertyCall("preHeader", SPreHeader, 3, FixedCost(15)) + lazy val dataInputsMethod = propertyCall("dataInputs", SBoxArray, 1, FixedCost(JitCost(15))) + lazy val headersMethod = propertyCall("headers", SHeaderArray, 2, FixedCost(JitCost(15))) + lazy val preHeaderMethod = propertyCall("preHeader", SPreHeader, 3, FixedCost(JitCost(15))) lazy val inputsMethod = property("INPUTS", SBoxArray, 4, Inputs) lazy val outputsMethod = property("OUTPUTS", SBoxArray, 5, Outputs) lazy val heightMethod = property("HEIGHT", SInt, 6, Height) lazy val selfMethod = property("SELF", SBox, 7, Self) - lazy val selfBoxIndexMethod = propertyCall("selfBoxIndex", SInt, 8, FixedCost(20)) + lazy val selfBoxIndexMethod = propertyCall("selfBoxIndex", SInt, 8, FixedCost(JitCost(20))) lazy val lastBlockUtxoRootHashMethod = property("LastBlockUtxoRootHash", SAvlTree, 9, LastBlockUtxoRootHash) lazy val minerPubKeyMethod = property("minerPubKey", SByteArray, 10, MinerPubkey) lazy val getVarMethod = SMethod( - this, "getVar", SFunc(Array(SContext, SByte), SOption(tT), Array(paramT)), 11, GetVar.costKind) + this, "getVar", SFunc(ContextFuncDom, SOption(tT), Array(paramT)), 11, GetVar.costKind) .withInfo(GetVar, "Get context variable with given \\lst{varId} and type.", ArgInfo("varId", "\\lst{Byte} identifier of context variable")) @@ -2625,21 +2681,21 @@ case object SHeader extends SProduct with SPredefType with SMonoType { } override def isConstantSize = true - lazy val idMethod = propertyCall("id", SByteArray, 1, FixedCost(10)) - lazy val versionMethod = propertyCall("version", SByte, 2, FixedCost(10)) - lazy val parentIdMethod = propertyCall("parentId", SByteArray, 3, FixedCost(10)) - lazy val ADProofsRootMethod = propertyCall("ADProofsRoot", SByteArray, 4, FixedCost(10)) - lazy val stateRootMethod = propertyCall("stateRoot", SAvlTree, 5, FixedCost(10)) - lazy val transactionsRootMethod = propertyCall("transactionsRoot", SByteArray, 6, FixedCost(10)) - lazy val timestampMethod = propertyCall("timestamp", SLong, 7, FixedCost(10)) - lazy val nBitsMethod = propertyCall("nBits", SLong, 8, FixedCost(10)) - lazy val heightMethod = propertyCall("height", SInt, 9, FixedCost(10)) - lazy val extensionRootMethod = propertyCall("extensionRoot", SByteArray, 10, FixedCost(10)) - lazy val minerPkMethod = propertyCall("minerPk", SGroupElement, 11, FixedCost(10)) - lazy val powOnetimePkMethod = propertyCall("powOnetimePk", SGroupElement, 12, FixedCost(10)) - lazy val powNonceMethod = propertyCall("powNonce", SByteArray, 13, FixedCost(10)) - lazy val powDistanceMethod = propertyCall("powDistance", SBigInt, 14, FixedCost(10)) - lazy val votesMethod = propertyCall("votes", SByteArray, 15, FixedCost(10)) + lazy val idMethod = propertyCall("id", SByteArray, 1, FixedCost(JitCost(10))) + lazy val versionMethod = propertyCall("version", SByte, 2, FixedCost(JitCost(10))) + lazy val parentIdMethod = propertyCall("parentId", SByteArray, 3, FixedCost(JitCost(10))) + lazy val ADProofsRootMethod = propertyCall("ADProofsRoot", SByteArray, 4, FixedCost(JitCost(10))) + lazy val stateRootMethod = propertyCall("stateRoot", SAvlTree, 5, FixedCost(JitCost(10))) + lazy val transactionsRootMethod = propertyCall("transactionsRoot", SByteArray, 6, FixedCost(JitCost(10))) + lazy val timestampMethod = propertyCall("timestamp", SLong, 7, FixedCost(JitCost(10))) + lazy val nBitsMethod = propertyCall("nBits", SLong, 8, FixedCost(JitCost(10))) + lazy val heightMethod = propertyCall("height", SInt, 9, FixedCost(JitCost(10))) + lazy val extensionRootMethod = propertyCall("extensionRoot", SByteArray, 10, FixedCost(JitCost(10))) + lazy val minerPkMethod = propertyCall("minerPk", SGroupElement, 11, FixedCost(JitCost(10))) + lazy val powOnetimePkMethod = propertyCall("powOnetimePk", SGroupElement, 12, FixedCost(JitCost(10))) + lazy val powNonceMethod = propertyCall("powNonce", SByteArray, 13, FixedCost(JitCost(10))) + lazy val powDistanceMethod = propertyCall("powDistance", SBigInt, 14, FixedCost(JitCost(10))) + lazy val votesMethod = propertyCall("votes", SByteArray, 15, FixedCost(JitCost(10))) protected override def getMethods() = super.getMethods() ++ Seq( idMethod, versionMethod, parentIdMethod, ADProofsRootMethod, stateRootMethod, transactionsRootMethod, @@ -2669,13 +2725,13 @@ case object SPreHeader extends SProduct with SPredefType with SMonoType { } override def isConstantSize = true - lazy val versionMethod = propertyCall("version", SByte, 1, FixedCost(10)) - lazy val parentIdMethod = propertyCall("parentId", SByteArray, 2, FixedCost(10)) - lazy val timestampMethod = propertyCall("timestamp", SLong, 3, FixedCost(10)) - lazy val nBitsMethod = propertyCall("nBits", SLong, 4, FixedCost(10)) - lazy val heightMethod = propertyCall("height", SInt, 5, FixedCost(10)) - lazy val minerPkMethod = propertyCall("minerPk", SGroupElement, 6, FixedCost(10)) - lazy val votesMethod = propertyCall("votes", SByteArray, 7, FixedCost(10)) + lazy val versionMethod = propertyCall("version", SByte, 1, FixedCost(JitCost(10))) + lazy val parentIdMethod = propertyCall("parentId", SByteArray, 2, FixedCost(JitCost(10))) + lazy val timestampMethod = propertyCall("timestamp", SLong, 3, FixedCost(JitCost(10))) + lazy val nBitsMethod = propertyCall("nBits", SLong, 4, FixedCost(JitCost(10))) + lazy val heightMethod = propertyCall("height", SInt, 5, FixedCost(JitCost(10))) + lazy val minerPkMethod = propertyCall("minerPk", SGroupElement, 6, FixedCost(JitCost(10))) + lazy val votesMethod = propertyCall("votes", SByteArray, 7, FixedCost(JitCost(10))) protected override def getMethods() = super.getMethods() ++ Seq( versionMethod, parentIdMethod, timestampMethod, nBitsMethod, heightMethod, minerPkMethod, votesMethod @@ -2723,6 +2779,15 @@ case object SGlobal extends SProduct with SPredefType with SMonoType { .withInfo(Xor, "Byte-wise XOR of two collections of bytes", ArgInfo("left", "left operand"), ArgInfo("right", "right operand")) + /** Implements evaluation of Global.xor method call ErgoTree node. + * Called via reflection based on naming convention. + * @see SMethod.evalMethod, Xor.eval, Xor.xorWithCosting + */ + def xor_eval(mc: MethodCall, G: SigmaDslBuilder, ls: Coll[Byte], rs: Coll[Byte]) + (implicit E: ErgoTreeEvaluator): Coll[Byte] = { + Xor.xorWithCosting(ls, rs) + } + protected override def getMethods() = super.getMethods() ++ Seq( groupGeneratorMethod, xorMethod diff --git a/sigmastate/src/main/scala/sigmastate/utils/Helpers.scala b/sigmastate/src/main/scala/sigmastate/utils/Helpers.scala index c6d1a363f8..087d7ba33a 100644 --- a/sigmastate/src/main/scala/sigmastate/utils/Helpers.scala +++ b/sigmastate/src/main/scala/sigmastate/utils/Helpers.scala @@ -13,6 +13,10 @@ import scala.reflect.ClassTag import scala.util.{Either, Failure, Right, Success, Try} object Helpers { + + /** Helper class which encapsulates a mutable value. */ + class MutableCell[T](var value: T) + def xor(ba1: Array[Byte], ba2: Array[Byte]): Array[Byte] = ba1.zip(ba2).map(t => (t._1 ^ t._2).toByte) /** Same as `xor` but makes in-place update of the first argument (hence suffix `U`) diff --git a/sigmastate/src/main/scala/sigmastate/utils/SigmaByteReader.scala b/sigmastate/src/main/scala/sigmastate/utils/SigmaByteReader.scala index 550743cb69..235557656e 100644 --- a/sigmastate/src/main/scala/sigmastate/utils/SigmaByteReader.scala +++ b/sigmastate/src/main/scala/sigmastate/utils/SigmaByteReader.scala @@ -1,11 +1,13 @@ package sigmastate.utils +import org.ergoplatform.validation.ValidationRules.CheckPositionLimit +import scorex.util.Extensions._ import scorex.util.serialization.Reader import sigmastate.SType import sigmastate.Values.{SValue, Value} -import sigmastate.lang.exceptions.{InputSizeLimitExceeded, DeserializeCallDepthExceeded} +import sigmastate.lang.exceptions.DeserializeCallDepthExceeded import sigmastate.serialization._ -import scorex.util.Extensions._ +import sigmastate.util.safeNewArray import spire.syntax.all.cfor /** Reader used in the concrete implementations of [[SigmaSerializer]]. @@ -26,9 +28,15 @@ class SigmaByteReader(val r: Reader, val maxTreeDepth: Int = SigmaSerializer.MaxTreeDepth) extends Reader { - @inline private def checkPositionLimit(): Unit = - if (position > positionLimit) - throw new InputSizeLimitExceeded(s"SigmaByteReader position limit $positionLimit is reached at position $position") + /** Checks that the current reader position is <= positionLimit. + * NOTE, since v5.0 the same check is done via validation rule, which wraps the + * original exception into ValidationException. This makes this condition soft-forkable, + * other than that both v4.x and v5.x will work and fail at the same time, so the + * change is consensus-safe. + */ + @inline private def checkPositionLimit(): Unit = { + CheckPositionLimit(position, positionLimit) + } /** The reader should be lightweight to create. In most cases ErgoTrees don't have * ValDef nodes hence the store is not necessary and it's initialization dominates the @@ -81,6 +89,8 @@ class SigmaByteReader(val r: Reader, r.getUInt() } + @inline def getUIntExact: Int = getUInt().toIntExact + @inline override def getLong(): Long = { checkPositionLimit() r.getLong() @@ -155,10 +165,10 @@ class SigmaByteReader(val r: Reader, * @return a sequence of zero of more values read */ @inline def getValues(): IndexedSeq[SValue] = { - val size = getUInt().toIntExact + val size = getUIntExact if (size == 0) Value.EmptySeq // quick short-cut when there is nothing to read else { - val xs = ValueSerializer.newArray[SValue](size) + val xs = safeNewArray[SValue](size) cfor(0)(_ < size, _ + 1) { i => xs(i) = getValue() } diff --git a/sigmastate/src/main/scala/sigmastate/utxo/CostTable.scala b/sigmastate/src/main/scala/sigmastate/utxo/CostTable.scala index 700ede9c30..4a55887f8d 100644 --- a/sigmastate/src/main/scala/sigmastate/utxo/CostTable.scala +++ b/sigmastate/src/main/scala/sigmastate/utxo/CostTable.scala @@ -1,6 +1,8 @@ package sigmastate.utxo -import sigmastate.{Downcast, Upcast} +import org.ergoplatform.SigmaConstants +import sigmastate._ +import sigmastate.interpreter.Interpreter import sigmastate.lang.SigmaParser import sigmastate.lang.Terms.OperationId diff --git a/sigmastate/src/main/scala/sigmastate/utxo/transformers.scala b/sigmastate/src/main/scala/sigmastate/utxo/transformers.scala index d7c2086de6..525f65ba25 100644 --- a/sigmastate/src/main/scala/sigmastate/utxo/transformers.scala +++ b/sigmastate/src/main/scala/sigmastate/utxo/transformers.scala @@ -53,7 +53,8 @@ object MapCollection extends ValueCompanion { override def opCode: OpCode = OpCodes.MapCollectionCode /** Cost of: 1) obtain result RType 2) invoke map method 3) allocation of resulting * collection */ - override val costKind = PerItemCost(20, 1, 10) + override val costKind = PerItemCost( + baseCost = JitCost(20), perChunkCost = JitCost(1), chunkSize = 10) } /** Puts the elements of other collection `col2` after the elements of `input` collection @@ -74,7 +75,8 @@ case class Append[IV <: SType](input: Value[SCollection[IV]], col2: Value[SColle } object Append extends ValueCompanion { override def opCode: OpCode = OpCodes.AppendCode - override val costKind = PerItemCost(10, 2, 100) + override val costKind = PerItemCost( + baseCost = JitCost(20), perChunkCost = JitCost(2), chunkSize = 100) } /** Selects an interval of elements. The returned collection is made up @@ -105,7 +107,8 @@ case class Slice[IV <: SType](input: Value[SCollection[IV]], from: Value[SInt.ty } object Slice extends ValueCompanion { override def opCode: OpCode = OpCodes.SliceCode - override val costKind = PerItemCost(10, 2, 100) + override val costKind = PerItemCost( + baseCost = JitCost(10), perChunkCost = JitCost(2), chunkSize = 100) } /** Selects all elements of `input` collection which satisfy the condition. @@ -132,7 +135,8 @@ object Filter extends ValueCompanion { override def opCode: OpCode = OpCodes.FilterCode /** Cost of: 1) invoke Coll.filter method 2) allocation of resulting * collection */ - override val costKind = PerItemCost(20, 1, 10) + override val costKind = PerItemCost( + baseCost = JitCost(20), perChunkCost = JitCost(1), chunkSize = 10) } /** Transforms a collection of values to a boolean (see [[Exists]], [[ForAll]]). */ @@ -167,7 +171,8 @@ case class Exists[IV <: SType](override val input: Value[SCollection[IV]], object Exists extends BooleanTransformerCompanion { override def opCode: OpCode = OpCodes.ExistsCode /** Cost of: invoke exists method */ - override val costKind = PerItemCost(3, 1, 10) + override val costKind = PerItemCost( + baseCost = JitCost(3), perChunkCost = JitCost(1), chunkSize = 10) override def argInfos: Seq[ArgInfo] = ExistsInfo.argInfos } @@ -193,7 +198,8 @@ case class ForAll[IV <: SType](override val input: Value[SCollection[IV]], object ForAll extends BooleanTransformerCompanion { override def opCode: OpCode = OpCodes.ForAllCode /** Cost of: invoke forall method */ - override val costKind = PerItemCost(3, 1, 10) + override val costKind = PerItemCost( + baseCost = JitCost(3), perChunkCost = JitCost(1), chunkSize = 10) override def argInfos: Seq[ArgInfo] = ForAllInfo.argInfos } @@ -231,7 +237,8 @@ case class Fold[IV <: SType, OV <: SType](input: Value[SCollection[IV]], object Fold extends ValueCompanion { override def opCode: OpCode = OpCodes.FoldCode - override val costKind = PerItemCost(3, 1, 10) + override val costKind = PerItemCost( + baseCost = JitCost(3), perChunkCost = JitCost(1), chunkSize = 10) def sum[T <: SNumericType](input: Value[SCollection[T]], varId: Int)(implicit tT: T) = Fold(input, Constant(tT.upcast(0.toByte), tT), @@ -273,9 +280,9 @@ case class ByIndex[V <: SType](input: Value[SCollection[V]], } } } -object ByIndex extends ValueCompanion { +object ByIndex extends FixedCostValueCompanion { override def opCode: OpCode = OpCodes.ByIndexCode - override val costKind = FixedCost(30) + override val costKind = FixedCost(JitCost(30)) } /** Select tuple field by its 1-based index. E.g. input._1 is transformed to @@ -304,7 +311,7 @@ object SelectField extends FixedCostValueCompanion { override def opCode: OpCode = OpCodes.SelectFieldCode /** Cost of: 1) Calling Tuple2.{_1, _2} Scala methods. * Old cost: ("SelectField", "() => Unit", selectField) */ - override val costKind = FixedCost(10) + override val costKind = FixedCost(JitCost(10)) def typed[T <: SValue](input: Value[STuple], fieldIndex: Byte): T = { SelectField(input, fieldIndex).asInstanceOf[T] } @@ -339,7 +346,8 @@ object SigmaPropBytes extends PerItemCostValueCompanion { override def opCode: OpCode = OpCodes.SigmaPropBytesCode /** BaseCost: serializing one node of SigmaBoolean proposition * PerChunkCost: serializing one node of SigmaBoolean proposition */ - override val costKind = PerItemCost(baseCost = 35, perChunkCost = 6, chunkSize = 1) + override val costKind = PerItemCost( + baseCost = JitCost(35), perChunkCost = JitCost(6), chunkSize = 1) } trait SimpleTransformerCompanion extends ValueCompanion { def argInfos: Seq[ArgInfo] @@ -356,13 +364,13 @@ case class SizeOf[V <: SType](input: Value[SCollection[V]]) inputV.length } } -object SizeOf extends SimpleTransformerCompanion { +object SizeOf extends SimpleTransformerCompanion with FixedCostValueCompanion { val OpType = SFunc(SCollection(SType.tIV), SInt) override def opCode: OpCode = OpCodes.SizeOfCode /** Cost of: 1) calling Coll.length method (guaranteed to be O(1)) * Twice the cost of SelectField. * Old cost: ("SizeOf", "(Coll[IV]) => Int", collLength) */ - override val costKind = FixedCost(14) + override val costKind = FixedCost(JitCost(14)) override def argInfos: Seq[ArgInfo] = SizeOfInfo.argInfos } @@ -379,11 +387,11 @@ case class ExtractAmount(input: Value[SBox.type]) extends Extract[SLong.type] wi inputV.value } } -object ExtractAmount extends SimpleTransformerCompanion { +object ExtractAmount extends SimpleTransformerCompanion with FixedCostValueCompanion { val OpType = SFunc(SBox, SLong) override def opCode: OpCode = OpCodes.ExtractAmountCode /** Cost of: 1) access `value` property of a [[special.sigma.Box]] */ - override val costKind = FixedCost(8) + override val costKind = FixedCost(JitCost(8)) override def argInfos: Seq[ArgInfo] = ExtractAmountInfo.argInfos } @@ -400,17 +408,17 @@ case class ExtractScriptBytes(input: Value[SBox.type]) extends Extract[SByteArra inputV.propositionBytes } } -object ExtractScriptBytes extends SimpleTransformerCompanion { +object ExtractScriptBytes extends SimpleTransformerCompanion with FixedCostValueCompanion { val OpType = SFunc(SBox, SByteArray) override def opCode: OpCode = OpCodes.ExtractScriptBytesCode - // TODO v5.0: ensure the following is true + // TODO v5.x: ensure the following is true /** The cost is fixed and doesn't include serialization of ErgoTree because * the ErgoTree is expected to be constructed with non-null propositionBytes. * This is (and must be) guaranteed by ErgoTree deserializer. * CostOf: accessing ErgoBox.propositionBytes */ - override val costKind = FixedCost(10) + override val costKind = FixedCost(JitCost(10)) override def argInfos: Seq[ArgInfo] = ExtractScriptBytesInfo.argInfos } @@ -429,8 +437,8 @@ object ExtractBytes extends SimpleTransformerCompanion { override def opCode: OpCode = OpCodes.ExtractBytesCode /** The cost is fixed and doesn't include serialization of ErgoBox because * the ErgoBox is expected to be constructed with non-null `bytes`. - * TODO v5.0: This is not, but must be guaranteed by ErgoBox deserializer. */ - override val costKind = FixedCost(12) + * TODO v5.x: This is not currently, but must be guaranteed by lazy ErgoBox deserializer. */ + override val costKind = FixedCost(JitCost(12)) override def argInfos: Seq[ArgInfo] = ExtractBytesInfo.argInfos } @@ -450,7 +458,7 @@ object ExtractBytesWithNoRef extends SimpleTransformerCompanion { /** The cost if fixed and doesn't include serialization of ErgoBox because * the ErgoBox is expected to be constructed with non-null `bytes`. */ - override val costKind = FixedCost(12) + override val costKind = FixedCost(JitCost(12)) override def argInfos: Seq[ArgInfo] = ExtractBytesWithNoRefInfo.argInfos } @@ -469,7 +477,7 @@ object ExtractId extends SimpleTransformerCompanion { val OpType = SFunc(SBox, SByteArray) override def opCode: OpCode = OpCodes.ExtractIdCode /** CostOf: cost of computing hash from `ErgoBox.bytes` */ - override val costKind = FixedCost(12) + override val costKind = FixedCost(JitCost(12)) override def argInfos: Seq[ArgInfo] = ExtractIdInfo.argInfos } @@ -487,10 +495,10 @@ case class ExtractRegisterAs[V <: SType]( input: Value[SBox.type], inputV.getReg(registerId.number)(tV) } } -object ExtractRegisterAs extends ValueCompanion { +object ExtractRegisterAs extends FixedCostValueCompanion { override def opCode: OpCode = OpCodes.ExtractRegisterAs /** CostOf: 1) accessing `registers` collection 2) comparing types 3) allocating Some()*/ - override val costKind = FixedCost(50) + override val costKind = FixedCost(JitCost(50)) //HOTSPOT:: avoids thousands of allocations per second private val BoxAndByte: IndexedSeq[SType] = Array(SBox, SByte) @@ -517,7 +525,7 @@ case class ExtractCreationInfo(input: Value[SBox.type]) extends Extract[STuple] } object ExtractCreationInfo extends SimpleTransformerCompanion { override def opCode: OpCode = OpCodes.ExtractCreationInfoCode - override val costKind = FixedCost(16) + override val costKind = FixedCost(JitCost(16)) override def argInfos: Seq[ArgInfo] = ExtractCreationInfoInfo.argInfos val ResultType = STuple(SInt, SByteArray) val OpType = SFunc(SBox, ResultType) @@ -535,11 +543,12 @@ trait Deserialize[V <: SType] extends NotReadyValue[V] */ case class DeserializeContext[V <: SType](id: Byte, tpe: V) extends Deserialize[V] { override def companion = DeserializeContext - override val opType = SFunc(Array(SContext, SByte), tpe) + override val opType = SFunc(SContext.ContextFuncDom, tpe) } object DeserializeContext extends ValueCompanion { override def opCode: OpCode = OpCodes.DeserializeContextCode - override val costKind = PerItemCost(1, 10, 128) + override val costKind = PerItemCost( + baseCost = JitCost(1), perChunkCost = JitCost(10), chunkSize = 128) } /** Extract register of SELF box as Coll[Byte], deserialize it into Value and inline into executing script. @@ -551,13 +560,14 @@ case class DeserializeRegister[V <: SType](reg: RegisterId, tpe: V, default: Opt } object DeserializeRegister extends ValueCompanion { override def opCode: OpCode = OpCodes.DeserializeRegisterCode - override val costKind = PerItemCost(1, 10, 128) + override val costKind = PerItemCost( + baseCost = JitCost(1), perChunkCost = JitCost(10), chunkSize = 128) } /** See [[special.sigma.Context.getVar()]] for detailed description. */ case class GetVar[V <: SType](varId: Byte, override val tpe: SOption[V]) extends NotReadyValue[SOption[V]] { override def companion = GetVar - override val opType = SFunc(Array(SContext, SByte), tpe) // TODO optimize: avoid Array allocation + override val opType = SFunc(SContext.ContextFuncDom, tpe) protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = { val t = Evaluation.stypeToRType(tpe.elemType) addCost(GetVar.costKind) @@ -568,7 +578,7 @@ object GetVar extends FixedCostValueCompanion { override def opCode: OpCode = OpCodes.GetVarCode /** Cost of: 1) accessing to array of context vars by index * Old cost: ("GetVar", "(Context, Byte) => Option[T]", getVarCost) */ - override val costKind = FixedCost(10) + override val costKind = FixedCost(JitCost(10)) def apply[V <: SType](varId: Byte, innerTpe: V): GetVar[V] = GetVar[V](varId, SOption(innerTpe)) } @@ -591,7 +601,7 @@ case class OptionGet[V <: SType](input: Value[SOption[V]]) extends Transformer[S object OptionGet extends SimpleTransformerCompanion with FixedCostValueCompanion { override def opCode: OpCode = OpCodes.OptionGetCode /** Cost of: 1) Calling Option.get Scala method. */ - override val costKind = FixedCost(15) + override val costKind = FixedCost(JitCost(15)) override def argInfos: Seq[ArgInfo] = OptionGetInfo.argInfos } @@ -615,10 +625,10 @@ case class OptionGetOrElse[V <: SType](input: Value[SOption[V]], default: Value[ inputV.getOrElse(dV) } } -object OptionGetOrElse extends ValueCompanion { +object OptionGetOrElse extends ValueCompanion with FixedCostValueCompanion { override def opCode: OpCode = OpCodes.OptionGetOrElseCode /** Cost of: 1) Calling Option.getOrElse Scala method. */ - override val costKind = FixedCost(20) + override val costKind = FixedCost(JitCost(20)) } /** Returns false if the option is None, true otherwise. */ @@ -633,9 +643,9 @@ case class OptionIsDefined[V <: SType](input: Value[SOption[V]]) inputV.isDefined } } -object OptionIsDefined extends SimpleTransformerCompanion { +object OptionIsDefined extends SimpleTransformerCompanion with FixedCostValueCompanion { override def opCode: OpCode = OpCodes.OptionIsDefinedCode /** Cost of: 1) Calling Option.isDefined Scala method. */ - override val costKind = FixedCost(10) + override val costKind = FixedCost(JitCost(10)) override def argInfos: Seq[ArgInfo] = OptionIsDefinedInfo.argInfos } diff --git a/sigmastate/src/test/scala/org/ergoplatform/ErgoAddressSpecification.scala b/sigmastate/src/test/scala/org/ergoplatform/ErgoAddressSpecification.scala index f8dca4c300..c73a092670 100644 --- a/sigmastate/src/test/scala/org/ergoplatform/ErgoAddressSpecification.scala +++ b/sigmastate/src/test/scala/org/ergoplatform/ErgoAddressSpecification.scala @@ -24,7 +24,7 @@ import sigmastate.interpreter.Interpreter.{ScriptNameProp, ScriptEnv} import sigmastate.lang.Terms.ValueOps import sigmastate.lang.exceptions.{CosterException, CostLimitException} import sigmastate.utils.Helpers._ -import special.sigma.SigmaDslTesting +import special.sigma.{InvalidType, SigmaDslTesting} class ErgoAddressSpecification extends SigmaDslTesting with TryValues with CrossVersionProps { @@ -282,27 +282,46 @@ class ErgoAddressSpecification extends SigmaDslTesting // when limit is low { - // choose limit less than total cost: - // totalCost(2671) = addr.script.complexity(2277) + prop complexity(164) + scaledCost(230) - val deliberatelySmallLimit = 2600 - - assertExceptionThrown( - { - testPay2SHAddress(addr, - script = scriptVarId -> ByteArrayConstant(scriptBytes), - costLimit = deliberatelySmallLimit) - }, - { t => - t.isInstanceOf[CostLimitException] && - t.getMessage.contains( - s"Estimated execution cost 2671 exceeds the limit $deliberatelySmallLimit")} - ) + if (isActivatedVersion4) { + // choose limit less than total cost: + // totalCost(2671) = addr.script.complexity(2277) + prop complexity(164) + scaledCost(230) + val deliberatelySmallLimit = 2600 + assertExceptionThrown( + testPay2SHAddress(addr, + script = scriptVarId -> ByteArrayConstant(scriptBytes), + costLimit = deliberatelySmallLimit), + rootCauseLike[CostLimitException]( + s"Estimated execution cost 2671 exceeds the limit $deliberatelySmallLimit") + ) + } else { + val deliberatelySmallLimit = 24 + assertExceptionThrown( + testPay2SHAddress(addr, + script = scriptVarId -> ByteArrayConstant(scriptBytes), + costLimit = deliberatelySmallLimit), + rootCauseLike[CostLimitException]( + s"Estimated execution cost 88 exceeds the limit $deliberatelySmallLimit") + ) + } } // when limit is low - { + if (isActivatedVersion4) { // choose limit less than addr.script.complexity == 2277 + script complexity == 164 - val deliberatelySmallLimit = 2300 + val deliberatelySmallLimit = 1000 + + assertExceptionThrown( + { + testPay2SHAddress(addr, + script = scriptVarId -> ByteArrayConstant(scriptBytes), + costLimit = deliberatelySmallLimit) + }, + rootCauseLike[CostLimitException]( + s"Estimated execution cost 2277 exceeds the limit $deliberatelySmallLimit") + ) + } else { + // v5.0 + val deliberatelySmallLimit = 10 assertExceptionThrown( { @@ -310,17 +329,29 @@ class ErgoAddressSpecification extends SigmaDslTesting script = scriptVarId -> ByteArrayConstant(scriptBytes), costLimit = deliberatelySmallLimit) }, - { t => - t.isInstanceOf[CostLimitException] && - t.getMessage.contains( - s"Estimated execution cost 2441 exceeds the limit $deliberatelySmallLimit")} + rootCauseLike[CostLimitException]( + s"Estimated execution cost 88 exceeds the limit $deliberatelySmallLimit") ) + } // when limit is even lower than tree complexity - { + if (isActivatedVersion4) { // choose limit less than addr.script.complexity == 2277 - val deliberatelySmallLimit = 2000 + val deliberatelySmallLimit = 1000 + + assertExceptionThrown( + { + testPay2SHAddress(addr, + script = scriptVarId -> ByteArrayConstant(scriptBytes), + costLimit = deliberatelySmallLimit) + }, + rootCauseLike[CostLimitException]( + s"Estimated execution cost 2277 exceeds the limit $deliberatelySmallLimit") + ) + } else { + // v5.0 + val deliberatelySmallLimit = 10 assertExceptionThrown( { @@ -328,29 +359,38 @@ class ErgoAddressSpecification extends SigmaDslTesting script = scriptVarId -> ByteArrayConstant(scriptBytes), costLimit = deliberatelySmallLimit) }, - { t => - t.isInstanceOf[CostLimitException] && - t.getMessage.contains( - s"Estimated execution cost 2277 exceeds the limit $deliberatelySmallLimit")} + rootCauseLike[CostLimitException]( + s"Estimated execution cost 88 exceeds the limit $deliberatelySmallLimit") ) + } // when script var have invalid type - assertExceptionThrown( - testPay2SHAddress(addr, script = scriptVarId -> IntConstant(10)), - { t => - t.isInstanceOf[CosterException] && - t.getMessage.contains(s"Don't know how to evalNode(DeserializeContext(")} - ) + if (isActivatedVersion4) { + assertExceptionThrown( + testPay2SHAddress(addr, script = scriptVarId -> IntConstant(10)), + rootCauseLike[CosterException](s"Don't know how to evalNode(DeserializeContext(") + ) + } else { + assertExceptionThrown( + testPay2SHAddress(addr, script = scriptVarId -> IntConstant(10)), + rootCauseLike[InvalidType](s"invalid type of value") + ) + } // when script var have invalid id val invalidId: Byte = 2 - assertExceptionThrown( - testPay2SHAddress(addr, script = invalidId -> IntConstant(10)), - { t => - t.isInstanceOf[CosterException] && - t.getMessage.contains(s"Don't know how to evalNode(DeserializeContext(")} - ) + if (isActivatedVersion4) { + assertExceptionThrown( + testPay2SHAddress(addr, script = invalidId -> IntConstant(10)), + rootCauseLike[CosterException](s"Don't know how to evalNode(DeserializeContext(") + ) + } else { + assertExceptionThrown( + testPay2SHAddress(addr, script = invalidId -> IntConstant(10)), + rootCauseLike[NoSuchElementException]("None.get") + ) + } } } \ No newline at end of file diff --git a/sigmastate/src/test/scala/org/ergoplatform/ErgoLikeTransactionSpec.scala b/sigmastate/src/test/scala/org/ergoplatform/ErgoLikeTransactionSpec.scala index 9c92c7b74f..894010b454 100644 --- a/sigmastate/src/test/scala/org/ergoplatform/ErgoLikeTransactionSpec.scala +++ b/sigmastate/src/test/scala/org/ergoplatform/ErgoLikeTransactionSpec.scala @@ -95,7 +95,7 @@ class ErgoLikeTransactionSpec extends SigmaDslTesting { ErgoAlgos.decodeUnsafe(token1).toColl -> 10000000L, ErgoAlgos.decodeUnsafe(token2).toColl -> 500L ).map(identity).toConstant - // TODO HF (16h): fix collections equality and remove map(identity) + // TODO v6.0 (16h): fix collections equality and remove map(identity) // (PairOfColl should be equal CollOverArray but now it is not) res shouldBe exp } diff --git a/sigmastate/src/test/scala/org/ergoplatform/dsl/TestContractSpec.scala b/sigmastate/src/test/scala/org/ergoplatform/dsl/TestContractSpec.scala index 155ea7b447..08883d65a7 100644 --- a/sigmastate/src/test/scala/org/ergoplatform/dsl/TestContractSpec.scala +++ b/sigmastate/src/test/scala/org/ergoplatform/dsl/TestContractSpec.scala @@ -90,6 +90,7 @@ case class TestContractSpec(testSuite: SigmaTestingCommons)(implicit val IR: IRC spendingTransaction = createTransaction(dataBoxes, tx.outputs.map(_.ergoBox).toIndexedSeq), selfIndex = boxesToSpend.indexOf(utxoBox.ergoBox), activatedVersion = testSuite.activatedVersionInTests) + .withErgoTreeVersion(testSuite.ergoTreeVersionInTests) ctx } def runDsl(extensions: Map[Byte, AnyValue] = Map()): SigmaProp = { diff --git a/sigmastate/src/test/scala/sigmastate/CalcSha256Specification.scala b/sigmastate/src/test/scala/sigmastate/CalcSha256Specification.scala index db3922960e..44d3ac803d 100644 --- a/sigmastate/src/test/scala/sigmastate/CalcSha256Specification.scala +++ b/sigmastate/src/test/scala/sigmastate/CalcSha256Specification.scala @@ -30,10 +30,12 @@ class CalcSha256Specification extends SigmaTestingCommons property("CalcSha256: Should pass standard tests.") { val int = new ContextEnrichingTestProvingInterpreter() val ctx = ErgoLikeContextTesting.dummy(fakeSelf, activatedVersionInTests) + .withErgoTreeVersion(ergoTreeVersionInTests) forAll(objects) { (in, result) => val expectedResult = decodeString(result) val calcSha256 = EQ(CalcSha256(stringToByteConstant(in)), expectedResult) - val res = int.reduceToCrypto(ctx, calcSha256.toSigmaProp).get.value + val ergoTree = mkTestErgoTree(calcSha256.toSigmaProp) + val res = int.fullReduction(ergoTree, ctx).value res shouldBe TrivialProp.TrueProp } } diff --git a/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala b/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala index 562b5bec71..78d931ec22 100644 --- a/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala @@ -21,6 +21,12 @@ import sigmastate.lang.Terms.ValueOps import sigmastate.lang.exceptions.CostLimitException class CostingSpecification extends SigmaTestingData with CrossVersionProps { + + /** This specification only make sense for ErgoTree v0, v1 and hence v4.x interpreter. + * So we redefine the range of applicable activated versions. + */ + override val activatedVersions: Seq[Byte] = (0 until VersionContext.JitActivationVersion).map(_.toByte).toArray[Byte] + implicit lazy val IR = new TestingIRContext { // override val okPrintEvaluatedEntries = true substFromCostTable = false @@ -31,8 +37,9 @@ class CostingSpecification extends SigmaTestingData with CrossVersionProps { val printCosts = true - val (key1, _, _, avlProver) = sampleAvlProver - val keys = Colls.fromItems(key1) + val (keysArr, _, _, avlProver) = sampleAvlProver + val keys = Colls.fromItems(keysArr(0)) + val key1 = keys(0) val key2 = keyCollGen.sample.get avlProver.performOneOperation(Lookup(ADKey @@ key1.toArray)) val digest = avlProver.digest.toColl @@ -80,14 +87,21 @@ class CostingSpecification extends SigmaTestingData with CrossVersionProps { spendingTransaction = tx, selfIndex = 0, extension, ValidationRules.currentSettings, ScriptCostLimit.value, CostTable.interpreterInitCost, activatedVersionInTests - ) + ).withErgoTreeVersion(ergoTreeVersionInTests) def cost(script: String)(expCost: Int): Unit = { - val ergoTree = compiler.compile(env, script) - val res = interpreter.reduceToCrypto(context, env, ergoTree).get.cost + val exp = compiler.compile(env, script) + val ergoTree = mkTestErgoTree(exp.toSigmaProp) + val res = interpreter.fullReduction(ergoTree, context).cost + if (printCosts) println(script + s" --> cost $res") - res shouldBe ((expCost * CostTable.costFactorIncrease / CostTable.costFactorDecrease) + CostTable.interpreterInitCost).toLong + + // compute expected cost based on how the exp is compiled + val costOfBoolToSigmaProp = if (exp.tpe == SBoolean) CostTable.logicCost else 0 + val expectedCost = (expCost + CostTable.logicCost + costOfBoolToSigmaProp + CostTable.interpreterInitCost).toLong + + res shouldBe expectedCost } val ContextVarAccess = getVarCost + selectField // `getVar(id)` + `.get` @@ -398,24 +412,47 @@ class CostingSpecification extends SigmaTestingData with CrossVersionProps { val expectedCost = (expressionCost * CostTable.costFactorIncrease / CostTable.costFactorDecrease) + CostTable.interpreterInitCost + tree.complexity - pr.cost shouldBe expectedCost + if (isActivatedVersion4) { + pr.cost shouldBe expectedCost + } else { + pr.cost shouldBe 10185 + } + val verifier = new ErgoLikeTestInterpreter val cost = verifier.verify(emptyEnv, tree, context, pr, fakeMessage).get._2 - cost shouldBe expectedCost + if (isActivatedVersion4) { + cost shouldBe expectedCost + } else { + cost shouldBe 10185 + } } property("ErgoTree with SigmaPropConstant costs") { val d = new TestData; import d._ - def proveAndVerify(ctx: ErgoLikeContext, tree: ErgoTree, expectedCost: Long) = { + /** Helper method used in tests. + * @param expectedCostV5 expected value of v5.0 cost if defined, otherwise should be + * equal to `expectedCost` + */ + def proveAndVerify(ctx: ErgoLikeContext, tree: ErgoTree, expectedCost: Long, expectedCostV5: Option[Long] = None) = { val pr = interpreter.prove(tree, ctx, fakeMessage).get - pr.cost shouldBe expectedCost + + if (isActivatedVersion4) { + pr.cost shouldBe expectedCost + } else { + pr.cost shouldBe expectedCostV5.getOrElse(expectedCost) + } val verifier = new ErgoLikeTestInterpreter val cost = verifier.verify(emptyEnv, tree, ctx, pr, fakeMessage).get._2 - cost shouldBe expectedCost + + if (isActivatedVersion4) { + cost shouldBe expectedCost + } else { + cost shouldBe expectedCostV5.getOrElse(expectedCost) + } } // simple trees containing SigmaPropConstant @@ -424,23 +461,32 @@ class CostingSpecification extends SigmaTestingData with CrossVersionProps { { val ctx = context.withInitCost(0) - proveAndVerify(ctx, tree1, expectedCost = 10141) - proveAndVerify(ctx, tree2, expectedCost = 10161) + proveAndVerify(ctx, tree1, expectedCost = 10141, expectedCostV5 = Some(483)) + proveAndVerify(ctx, tree2, expectedCost = 10161, expectedCostV5 = Some(503)) } { val ctx = context.withInitCost(10000) - proveAndVerify(ctx, tree1, expectedCost = 20141) - proveAndVerify(ctx, tree2, expectedCost = 20161) + proveAndVerify(ctx, tree1, expectedCost = 20141, expectedCostV5 = Some(10483)) + proveAndVerify(ctx, tree2, expectedCost = 20161, expectedCostV5 = Some(10503)) } { - val ctx = context.withInitCost(10000).withCostLimit(20000) - assertExceptionThrown( - proveAndVerify(ctx, tree1, expectedCost = 20141), - exceptionLike[CostLimitException]( - "Estimated execution cost", "exceeds the limit") - ) + if (isActivatedVersion4) { + val ctx = context.withInitCost(10000).withCostLimit(20000) + assertExceptionThrown( + proveAndVerify(ctx, tree1, expectedCost = 20141), + exceptionLike[CostLimitException]( + "Estimated execution cost", "exceeds the limit") + ) + } else { + val ctx = context.withInitCost(10000).withCostLimit(10400) + assertExceptionThrown( + proveAndVerify(ctx, tree1, expectedCost = 20141, expectedCostV5 = Some(10483)), + exceptionLike[CostLimitException]( + "Estimated execution cost", "exceeds the limit") + ) + } } // more complex tree without Deserialize @@ -448,8 +494,8 @@ class CostingSpecification extends SigmaTestingData with CrossVersionProps { .compile(env, "{ sigmaProp(HEIGHT == 2) }") .asSigmaProp) - proveAndVerify(context.withInitCost(0), tree3, expectedCost = 541) - proveAndVerify(context.withInitCost(10000), tree3, expectedCost = 10541) + proveAndVerify(context.withInitCost(0), tree3, expectedCost = 541, expectedCostV5 = Some(495)) + proveAndVerify(context.withInitCost(10000), tree3, expectedCost = 10541, expectedCostV5 = Some(10495)) } property("laziness of AND, OR costs") { diff --git a/sigmastate/src/test/scala/sigmastate/CrossVersionProps.scala b/sigmastate/src/test/scala/sigmastate/CrossVersionProps.scala index 11ae6342b5..5065ae6d4a 100644 --- a/sigmastate/src/test/scala/sigmastate/CrossVersionProps.scala +++ b/sigmastate/src/test/scala/sigmastate/CrossVersionProps.scala @@ -9,32 +9,15 @@ import scala.util.DynamicVariable trait CrossVersionProps extends PropSpecLike with TestsBase { - val printVersions: Boolean = false - /** Number of times each test property is warmed up (i.e. executed before final execution). */ def perTestWarmUpIters: Int = 0 private[sigmastate] val _warmupProfiler = new DynamicVariable[Option[Profiler]](None) def warmupProfiler: Option[Profiler] = _warmupProfiler.value - protected def testFun_Run(testName: String, testFun: => Any): Unit = { - def msg = s"""property("$testName")(ActivatedVersion = $activatedVersionInTests; ErgoTree version = $ergoTreeVersionInTests)""" - if (printVersions) println(msg) - try testFun - catch { - case t: Throwable => - if (!printVersions) { - // wasn't printed, print it now - println(msg) - } - throw t - } - } - override protected def property(testName: String, testTags: Tag*) (testFun: => Any) (implicit pos: Position): Unit = { - super.property(testName, testTags:_*) { // do warmup if necessary if (perTestWarmUpIters > 0) { @@ -47,20 +30,8 @@ trait CrossVersionProps extends PropSpecLike with TestsBase { Thread.sleep(100) // give it some time to finish warm-up } - cfor(0)(_ < activatedVersions.length, _ + 1) { i => - val activatedVersion = activatedVersions(i) - _currActivatedVersion.withValue(activatedVersion) { - - cfor(0)( - i => i < ergoTreeVersions.length && ergoTreeVersions(i) <= activatedVersion, - _ + 1) { j => - val treeVersion = ergoTreeVersions(j) - _currErgoTreeVersion.withValue(treeVersion) { - testFun_Run(testName, testFun) - } - } - - } + forEachScriptAndErgoTreeVersion(activatedVersions, ergoTreeVersions) { + testFun_Run(testName, testFun) } if (okRunTestsWithoutMCLowering) { diff --git a/sigmastate/src/test/scala/sigmastate/ErgoTreeSpecification.scala b/sigmastate/src/test/scala/sigmastate/ErgoTreeSpecification.scala index 49460488ed..c399259f3d 100644 --- a/sigmastate/src/test/scala/sigmastate/ErgoTreeSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/ErgoTreeSpecification.scala @@ -3,18 +3,26 @@ package sigmastate import org.ergoplatform.ErgoBox import org.ergoplatform.settings.ErgoAlgos import org.ergoplatform.validation.{ValidationException, ValidationRules} -import scalan.Nullable +import scalan.RType.asType +import scalan.{Nullable, RType} +import sigmastate.SCollection.{SByteArray, SByteArray2, checkValidFlatmap} import sigmastate.Values._ +import sigmastate.VersionContext._ +import sigmastate.eval.{Evaluation, Profiler} +import sigmastate.interpreter.ErgoTreeEvaluator import sigmastate.lang.SourceContext -import special.sigma.SigmaTestingData import sigmastate.lang.Terms._ +import sigmastate.lang.exceptions.{CostLimitException, CosterException, InterpreterException} import sigmastate.serialization.ErgoTreeSerializer.DefaultSerializer -import sigmastate.utxo.{DeserializeContext, DeserializeRegister} +import sigmastate.utxo._ +import special.collection._ +import special.sigma.SigmaDslTesting + /** Regression tests with ErgoTree related test vectors. * This test vectors verify various constants which are consensus critical and should not change. */ -class ErgoTreeSpecification extends SigmaTestingData { +class ErgoTreeSpecification extends SigmaDslTesting { property("Value.sourceContext") { val srcCtx = SourceContext.fromParserIndex(0, "") @@ -270,7 +278,7 @@ class ErgoTreeSpecification extends SigmaTestingData { { import SSigmaProp._ (SSigmaProp.typeId, Seq( MInfo(1, PropBytesMethod), - MInfo(2, IsProvenMethod) // TODO HF (3h): this method must be removed + MInfo(2, IsProvenMethod) // TODO v5.x (3h): this method must be removed ), true) }, { import SBox._ @@ -439,4 +447,278 @@ class ErgoTreeSpecification extends SigmaTestingData { } } } + + implicit def IR = new TestingIRContext + implicit def cs = compilerSettingsInTests + implicit def es = evalSettings + + property("Apply with 0 arguments") { + val expr = Apply(FuncValue(Vector(), IntConstant(1)), IndexedSeq()) + + forEachScriptAndErgoTreeVersion(activatedVersions, ergoTreeVersions) { + VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + // old v4.x interpreter + assertExceptionThrown( + { + val oldF = funcFromExpr[Int, Int]("({ (x: Int) => 1 })()", expr) + }, + exceptionLike[CosterException]("Don't know how to evalNode") + ) + // new v5.0 interpreter + val newF = funcJitFromExpr[Int, Int]("({ (x: Int) => 1 })()", expr) + assertExceptionThrown( + { + val x = 100 // any value which is not used anyway + val (y, _) = VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + newF.apply(x) + } + }, + exceptionLike[InterpreterException]("Function application must have 1 argument, but was:") + ) + } + } + } + + + property("Apply with one argument") { + val expr = Apply( + FuncValue(Vector((1, SInt)), Negation(ValUse(1, SInt))), + IndexedSeq(IntConstant(1))) + val script = "({ (x: Int) => -x })(1)" + + val x = 1 + + forEachScriptAndErgoTreeVersion(activatedVersions, ergoTreeVersions) { + VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + { // old v4.x interpreter + val oldF = funcFromExpr[Int, Int](script, expr) + val (y, _) = oldF.apply(x) + y shouldBe -1 + } + + { // new v5.0 interpreter + val newF = funcJitFromExpr[Int, Int](script, expr) + val (y, _) = VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + newF.apply(x) + } + y shouldBe -1 + } + } + } + } + + property("Apply with 2 and more arguments") { + val expr = Apply( + FuncValue(Vector((1, SInt), (2, SInt)), Plus(ValUse(1, SInt), ValUse(2, SInt))), + IndexedSeq(IntConstant(1), IntConstant(1)) + ) + val script = "{ (x: Int, y: Int) => x + y }" + + forEachScriptAndErgoTreeVersion(activatedVersions, ergoTreeVersions) { + VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + + // old v4.x interpreter + assertExceptionThrown( + { + val oldF = funcFromExpr[(Int, Int), Int](script, expr) + }, + exceptionLike[CosterException]("Don't know how to evalNode") + ) + // ndw v5.0 interpreter + val newF = funcJitFromExpr[(Int, Int), Int](script, expr) + assertExceptionThrown( + { + val (y, _) = VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + newF.apply((1, 1)) + } + }, + exceptionLike[InterpreterException]("Function application must have 1 argument, but was:") + ) + } + } + } + + /** Deeply nested maps which creates deeply nested collections. + * @return lambda like `(xs: Coll[Byte]) => xs.map(_ => xs.map(... xs.map(_ => xs)...))` + */ + def mkFuncValue(nDepth: Int, level: Int): SValue = { + def mkCollection(nDepth: Int, level: Int): SValue = { + if (level < nDepth) + MapCollection( + ValUse(1, SByteArray), + FuncValue( + Array((level + 1, SByte)), + mkCollection(nDepth, level + 1) + ) + ) + else + ValUse(1, SByteArray) + } + + FuncValue( + Array((1, SByteArray)), + mkCollection(nDepth, level)) + } + + property("Building deeply nested expression") { + + mkFuncValue(1, 1) shouldBe + FuncValue( + Array((1, SByteArray)), + ValUse(1, SByteArray)) + + mkFuncValue(2, 1) shouldBe + FuncValue( + Array((1, SByteArray)), + MapCollection( + ValUse(1, SByteArray), + FuncValue( + Array((2, SByte)), + ValUse(1, SByteArray) + ) + )) + + mkFuncValue(3, 1) shouldBe + FuncValue( + Array((1, SByteArray)), + MapCollection( + ValUse(1, SByteArray), + FuncValue( + Array((2, SByte)), + MapCollection( + ValUse(1, SByteArray), + FuncValue( + Array((3, SByte)), + ValUse(1, SByteArray) + ) + ) + ) + )) + } + + def exprCostForSize(size: Int, oldF: CompiledFunc[Coll[Byte], AnyRef], expected: Option[Coll[Byte] => AnyRef]) = { + val xs = Coll(Array.tabulate[Byte](size)(i => i.toByte):_*) + + val (y, details) = oldF(xs) + assert(details.actualTimeNano.get < 1000000000 /* 1 sec */) + + if (expected.isDefined) { + val e = expected.get(xs) + y shouldBe e + } + details.cost + } + + def mkCompiledFunc(depth: Int): CompiledFunc[Coll[Byte], AnyRef] = { + val expr = Apply( + mkFuncValue(depth, 1), + Array(OptionGet(GetVar(1.toByte, SOption(SByteArray)))) + ) + val script = "{ just any string }" + implicit val tRes: RType[AnyRef] = asType[AnyRef](Evaluation.stypeToRType(expr.tpe)) + val oldF = funcJitFromExpr[Coll[Byte], AnyRef](script, expr) + oldF + } + + property("Spam: Building large nested collection") { + + forEachScriptAndErgoTreeVersion( + activatedVers = Array(JitActivationVersion), + ergoTreeVers = ergoTreeVersions) { + VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + + { // depth 3 + val cf = mkCompiledFunc(3) + val expected = (xs: Coll[Byte]) => xs.map(_ => xs.map(_ => xs)) + exprCostForSize(10, cf, Some(expected)) shouldBe JitCost(1456) + exprCostForSize(100, cf, Some(expected)) shouldBe JitCost(104605) + + assertExceptionThrown( + exprCostForSize(1000, cf, Some(expected)), + exceptionLike[CostLimitException]("Estimated execution cost JitCost(10000005) exceeds the limit JitCost(10000000)") + ) + } + + { // depth 60 + val cf = mkCompiledFunc(60) + val expected = (xs: Coll[Byte]) => xs.map(_ => xs.map(_ => xs)) + exprCostForSize(1, cf, None) shouldBe JitCost(2194) + + assertExceptionThrown( + exprCostForSize(2, cf, None), + exceptionLike[CostLimitException]("Estimated execution cost JitCost(10000001) exceeds the limit JitCost(10000000)") + ) + } + + } + } + } + + property("checkValidFlatmap") { + implicit val E = ErgoTreeEvaluator.forProfiling(new Profiler, evalSettings) + def mkLambda(t: SType, mkBody: SValue => SValue) = { + MethodCall( + ValUse(1, SCollectionType(t)), + SCollection.getMethodByName("flatMap").withConcreteTypes( + Map(STypeVar("IV") -> t, STypeVar("OV") -> SByte) + ), + Vector(FuncValue(Vector((3, t)), mkBody(ValUse(3, t)))), + Map() + ) + } + val validLambdas = Seq[(SType, SValue => SValue)]( + (SBox, x => ExtractScriptBytes(x.asBox)), + (SBox, x => ExtractId(x.asBox)), + (SBox, x => ExtractBytes(x.asBox)), + (SBox, x => ExtractBytesWithNoRef(x.asBox)), + (SSigmaProp, x => SigmaPropBytes(x.asSigmaProp)), + (SBox, x => MethodCall(x, SBox.getMethodByName("id"), Vector(), Map())) + ).map { case (t, f) => mkLambda(t, f) } + + validLambdas.foreach { l => + checkValidFlatmap(l) + } + + val invalidLambdas = Seq[(SType, SValue => SValue)]( + // identity lambda `xss.flatMap(xs => xs)` + (SByteArray, x => x), + + // identity lambda `xss.flatMap(xs => xs ++ xs)` + (SByteArray, x => Append(x.asCollection[SByte.type], x.asCollection[SByte.type])) + ).map { case (t, f) => mkLambda(t, f) } ++ + Seq( + // invalid MC like `boxes.flatMap(b => b.id, 10)` + MethodCall( + ValUse(1, SBox), + SCollection.getMethodByName("flatMap").withConcreteTypes( + Map(STypeVar("IV") -> SBox, STypeVar("OV") -> SByte) + ), + Vector( + FuncValue(Vector((3, SBox)), ExtractId(ValUse(3, SBox))), + IntConstant(10) // too much arguments + ), + Map() + ), + // invalid MC like `boxes.flatMap((b,_) => b.id)` + MethodCall( + ValUse(1, SBox), + SCollection.getMethodByName("flatMap").withConcreteTypes( + Map(STypeVar("IV") -> SBox, STypeVar("OV") -> SByte) + ), + Vector( + FuncValue(Vector((3, SBox), (4, SInt)/*too much arguments*/), ExtractId(ValUse(3, SBox))) + ), + Map() + ) + ) + + invalidLambdas.foreach { l => + assertExceptionThrown( + checkValidFlatmap(l), + exceptionLike[RuntimeException]( + s"Unsupported lambda in flatMap: allowed usage `xs.flatMap(x => x.property)`") + ) + } + + } } diff --git a/sigmastate/src/test/scala/sigmastate/JitCostSpecification.scala b/sigmastate/src/test/scala/sigmastate/JitCostSpecification.scala new file mode 100644 index 0000000000..3cf46f1075 --- /dev/null +++ b/sigmastate/src/test/scala/sigmastate/JitCostSpecification.scala @@ -0,0 +1,64 @@ +package sigmastate + +import special.sigma.SigmaTestingData + +import scala.util.{Try, Failure} + +class JitCostSpecification extends SigmaTestingData { + + type BinOp = (Int, Int) => Int + type JitBinOp = (JitCost, JitCost) => JitCost + + def testBinary[A](r1: Try[A], r2: Try[A]) = { + (r1, r2) match { + case (Failure(exception), Failure(expectedException)) => + rootCause(exception).getClass shouldBe expectedException.getClass + case _ => + r1 shouldBe r2 + } + } + + property("JitCost.$plus") { + forAll(MinSuccessful(1000)) { (x: Int, y: Int) => + { + val r1 = Try(java7.compat.Math.addExact(x, y)) + val r2 = Try((JitCost(x) + JitCost(y)).value) + testBinary(r1, r2) + } + + { + val r1 = Try(java7.compat.Math.multiplyExact(x, y)) + val r2 = Try((JitCost(x) * y).value) + testBinary(r1, r2) + } + + { + val r1 = Try(x / y) + val r2 = Try((JitCost(x) / y).value) + testBinary(r1, r2) + } + + { + val r1 = Try(x > y) + val r2 = Try(JitCost(x) > JitCost(y)) + testBinary(r1, r2) + } + + { + val r1 = Try(x >= y) + val r2 = Try(JitCost(x) >= JitCost(y)) + testBinary(r1, r2) + } + + } + } + + property("JitCost scaling") { + forAll(MinSuccessful(1000)) { (x: Int) => + whenever(0 <= x && x <= 100000000) { + JitCost.fromBlockCost(x).toBlockCost shouldBe x + } + } + } + +} diff --git a/sigmastate/src/test/scala/sigmastate/ScriptVersionSwitchSpecification.scala b/sigmastate/src/test/scala/sigmastate/ScriptVersionSwitchSpecification.scala index ebe56e2fb4..aefa5c3d1e 100644 --- a/sigmastate/src/test/scala/sigmastate/ScriptVersionSwitchSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/ScriptVersionSwitchSpecification.scala @@ -5,25 +5,32 @@ import org.ergoplatform._ import scorex.util.ModifierId import sigmastate.Values.ErgoTree.{DefaultHeader, updateVersionBits} import sigmastate.Values._ +import sigmastate.VersionContext.MaxSupportedScriptVersion import sigmastate.eval._ -import sigmastate.helpers.ErgoLikeContextTesting +import sigmastate.helpers.{ErgoLikeContextTesting, ErgoLikeTestInterpreter} import sigmastate.helpers.TestingHelpers.createBox -import sigmastate.interpreter.{Interpreter, ProverResult} +import sigmastate.interpreter.ErgoTreeEvaluator.DefaultEvalSettings +import sigmastate.interpreter.EvalSettings.EvaluationMode +import sigmastate.interpreter.{CostedProverResult, ErgoTreeEvaluator, EvalSettings, Interpreter, ProverResult} import sigmastate.lang.exceptions.InterpreterException -import sigmastate.utxo._ import sigmastate.utils.Helpers._ -import special.collection._ import special.sigma.{SigmaDslTesting, Box} -import scala.util.Success - -/** Specification to verify that the interpreter behaves according to docs/aot-jit-switch.md. */ -class ScriptVersionSwitchSpecification extends SigmaDslTesting - with CrossVersionProps { +/** Specification to verify that the interpreter behaves according to docs/aot-jit-switch.md. + * + * NOTE, this suite doesn't inherit CrossVersionProps because each test case require explicit + * setup of activatedScriptVersion and ErgoTree.version. + */ +class ScriptVersionSwitchSpecification extends SigmaDslTesting { override implicit val generatorDrivenConfig = PropertyCheckConfiguration(minSuccessful = 30) + override implicit val evalSettings: EvalSettings = + ErgoTreeEvaluator.DefaultEvalSettings.copy( + costTracingEnabled = true // should always be enabled in tests (and false by default) + ) + implicit def IR = createIR() - val b1 = CostingBox( + lazy val b1 = CostingBox( false, new ErgoBox( 1L, @@ -40,6 +47,7 @@ class ScriptVersionSwitchSpecification extends SigmaDslTesting ) ) + /** Creates ErgoTree with segregated constants and also the given header flags. */ def createErgoTree(headerFlags: Byte)(implicit IR: IRContext): ErgoTree = { val code = s"""{ @@ -62,7 +70,15 @@ class ScriptVersionSwitchSpecification extends SigmaDslTesting ErgoTree.withSegregation(headerFlags, compiledTree) } - def testProve(ergoTree: ErgoTree, activatedScriptVersion: Byte) = { + /** Proves the given ergoTree in a dummy context with the given activatedScriptVersion. + * @param activatedScriptVersion used to create the context + * @param evalMode can be specified to force specific interpreter regardless of + * activatedScriptVersion + */ + def testProve( + ergoTree: ErgoTree, + activatedScriptVersion: Byte, + evalMode: Option[EvaluationMode] = None): CostedProverResult = { val tpeA = SCollection(SBox) val input = Coll[Box](b1) val newRegisters: AdditionalRegisters = Map( @@ -75,12 +91,24 @@ class ScriptVersionSwitchSpecification extends SigmaDslTesting ).withBindings( 1.toByte -> Constant[SType](input.asInstanceOf[SType#WrappedType], tpeA) ).asInstanceOf[ErgoLikeContext] - val prover = new FeatureProvingInterpreter() + val prover = new FeatureProvingInterpreter() { + override val evalSettings: EvalSettings = DefaultEvalSettings.copy( + isMeasureOperationTime = true, + isDebug = true, + isTestRun = true, + evaluationMode = evalMode) + } val pr = prover.prove(ergoTree, ctx, fakeMessage).getOrThrow pr } - def testVerify(ergoTree: ErgoTree, activatedScriptVersion: Byte) = { + /** Proves the given ergoTree in a dummy context with the given activatedScriptVersion + * and using empty proof. + * @param activatedScriptVersion used to create the context + * @param evalMode can be specified to force specific interpreter regardless of + * activatedScriptVersion + */ + def testVerify(ergoTree: ErgoTree, activatedScriptVersion: Byte, evalMode: Option[EvaluationMode] = None) = { val tpeA = SCollection(SBox) val input = Coll[Box](b1) val newRegisters: AdditionalRegisters = Map( @@ -94,7 +122,13 @@ class ScriptVersionSwitchSpecification extends SigmaDslTesting 1.toByte -> Constant[SType](input.asInstanceOf[SType#WrappedType], tpeA) ).asInstanceOf[ErgoLikeContext] - val verifier = new ErgoLikeInterpreter() { type CTX = ErgoLikeContext } + val verifier = new ErgoLikeTestInterpreter() { + override val evalSettings: EvalSettings = DefaultEvalSettings.copy( + isMeasureOperationTime = true, + isDebug = true, + isTestRun = true, + evaluationMode = evalMode) + } val pr = ProverResult(ProverResult.empty.proof, ctx.extension) // NOTE: exactly this overload should also be called in Ergo @@ -105,134 +139,187 @@ class ScriptVersionSwitchSpecification extends SigmaDslTesting (1 to 7).foreach { version => assertExceptionThrown( createErgoTree(headerFlags = updateVersionBits(DefaultHeader, version.toByte)), - { t => - t.isInstanceOf[IllegalArgumentException] && - t.getMessage.contains("For newer version the size bit is required") - }) + exceptionLike[IllegalArgumentException]("For newer version the size bit is required") + ) } } - /** Rule#| SF Status| Block Type| Script Version | Release | Validation Action + /** Rule#| BlockVer | Block Type| Script Version | Release | Validation Action * -----|----------|-----------|----------------|---------|-------- - * 1 | inactive | candidate | Script v1 | v4.0 | R4.0-AOT-cost, R4.0-AOT-verify - * 5 | inactive | mined | Script v1 | v4.0 | R4.0-AOT-cost, R4.0-AOT-verify + * 1 | 1,2 | candidate | Script v0/v1 | v4.0 | R4.0-AOT-cost, R4.0-AOT-verify + * 2 | 1,2 | candidate | Script v0/v1 | v5.0 | R4.0-AOT-cost, R4.0-AOT-verify + * 5 | 1,2 | mined | Script v0/v1 | v4.0 | R4.0-AOT-cost, R4.0-AOT-verify + * 6 | 1,2 | mined | Script v0/v1 | v5.0 | R4.0-AOT-cost, R4.0-AOT-verify */ - property("Rules 1,5 | inactive SF | candidate or mined block | Script v1") { - val samples = genSamples[Coll[Box]](collOfN[Box](5), MinSuccessful(20)) - - // this execution corresponds to the normal validation action (R4.0-AOT-cost, R4.0-AOT-verify) - verifyCases( - { - def success[T](v: T, c: Int) = Expected(Success(v), c) - Seq( - (Coll[Box](), success(Coll[Box](), 37297)), - (Coll[Box](b1), success(Coll[Box](), 37397)) + property("Rules 1,2,5,6 | BlockVer 1,2 | candidate or mined block | Script v0/v1") { + // this test verifies the normal validation action (R4.0-AOT-cost, R4.0-AOT-verify) + // See SigmaDslSpecification for a full suite of v4.x vs. v5.0 equivalence tests + + forEachActivatedScriptVersion(activatedVers = Array[Byte](0, 1)) // for BlockVersions 1,2 + { + // tree versions that doesn't exceed activated + val treeVers = (0 to activatedVersionInTests).map(_.toByte).toArray[Byte] + + forEachErgoTreeVersion(treeVers) { + // SF inactive: check cost vectors of v4.x interpreter + val headerFlags = ErgoTree.headerWithVersion(ergoTreeVersionInTests) + val ergoTree = createErgoTree(headerFlags) + + // both prove and verify are accepting with full evaluation + val expectedCost = 5464L + val pr = testProve(ergoTree, activatedScriptVersion = activatedVersionInTests) + pr.proof shouldBe Array.emptyByteArray + pr.cost shouldBe expectedCost + + val (ok, cost) = testVerify(ergoTree, activatedScriptVersion = activatedVersionInTests) + ok shouldBe true + cost shouldBe expectedCost + } + } + } + + /** Rule#| BlockVer | Block Type| Script Version | Release | Validation Action + * -----|----------|-----------|----------------|---------|-------- + * 3 | 1,2 | candidate | Script v2 | v4.0 | skip-pool-tx (cannot handle) + * 4 | 1,2 | candidate | Script v2 | v5.0 | skip-pool-tx (wait activation) + * 7 | 1,2 | mined | Script v2 | v4.0 | skip-reject (cannot handle) + * 8 | 1,2 | mined | Script v2 | v5.0 | skip-reject (wait activation) + */ + property("Rules 3,4,7,8 | Block Version 1,2 | candidate or mined block | Script v2") { + forEachActivatedScriptVersion(Array[Byte](0, 1)) { // Block Versions 1, 2 + + forEachErgoTreeVersion(ergoTreeVers = Array[Byte](2)) { // only Script v2 + val headerFlags = ErgoTree.headerWithVersion(ergoTreeVersionInTests /* Script v2 */) + val ergoTree = createErgoTree(headerFlags = headerFlags) + + // prover is rejecting ErgoTree versions higher than activated + assertExceptionThrown( + testProve(ergoTree, activatedScriptVersion = activatedVersionInTests), + exceptionLike[InterpreterException](s"ErgoTree version ${ergoTree.version} is higher than activated $activatedVersionInTests") ) - }, - existingFeature({ (x: Coll[Box]) => x.filter({ (b: Box) => b.value > 1 }) }, - "{ (x: Coll[Box]) => x.filter({(b: Box) => b.value > 1 }) }", - FuncValue( - Vector((1, SCollectionType(SBox))), - Filter( - ValUse(1, SCollectionType(SBox)), - FuncValue(Vector((3, SBox)), GT(ExtractAmount(ValUse(3, SBox)), LongConstant(1L))) + + // verifier is rejecting ErgoTree versions higher than activated + assertExceptionThrown( + testVerify(ergoTree, activatedScriptVersion = activatedVersionInTests), + exceptionLike[InterpreterException](s"ErgoTree version ${ergoTree.version} is higher than activated $activatedVersionInTests") ) - )), - preGeneratedSamples = Some(samples)) + } + } } - /** Rule#| SF Status| Block Type| Script Version | Release | Validation Action + /** Rule#| BlockVer | Block Type| Script Version | Release | Validation Action * -----|----------|-----------|----------------|---------|-------- - * 3 | inactive | candidate | Script v2 | v4.0 | skip-pool-tx (cannot handle) - * 7 | inactive | mined | Script v2 | v4.0 | skip-reject (cannot handle) + * 10 | 3 | candidate | Script v0/v1 | v5.0 | R5.0-JIT-verify + * 12 | 3 | candidate | Script v2 | v5.0 | R5.0-JIT-verify + * 14 | 3 | mined | Script v0/v1 | v5.0 | R5.0-JIT-verify + * 16 | 3 | mined | Script v2 | v5.0 | R5.0-JIT-verify */ - property("Rules 3, 7 | inactive SF | candidate or mined block | Script v2") { - val headerFlags = ErgoTree.headerWithVersion( 2 /* Script v2 */) - val ergoTree = createErgoTree(headerFlags = headerFlags) - - // the prove is successful (since it is not part of consensus) - testProve(ergoTree, activatedScriptVersion = 1 /* SF Status: inactive */) - - // and verify is rejecting - assertExceptionThrown( - testVerify(ergoTree, activatedScriptVersion = 1 /* SF Status: inactive */), - { t => - t.isInstanceOf[InterpreterException] && - t.getMessage.contains(s"ErgoTree version ${ergoTree.version} is higher than activated 1") - }) + property("Rules 10,12,14,16 | BlockVer 3 | candidate or mined block | Script v0/v1/v2") { + // this test verifies the normal validation action (R5.0-JIT-verify) + // See SigmaDslSpecification for a full suite of v4.x vs. v5.0 equivalence tests + + forEachActivatedScriptVersion(activatedVers = Array[Byte](2)) // Block version 3 + { + // tree versions that doesn't exceed activated + val treeVers = (0 to activatedVersionInTests).map(_.toByte).toArray[Byte] + + forEachErgoTreeVersion(treeVers) { + // SF inactive: check cost vectors of v4.x interpreter + val ergoTree = createErgoTree(ergoTreeHeaderInTests) + + // both prove and verify are accepting with full evaluation + val pr = testProve(ergoTree, activatedScriptVersion = activatedVersionInTests) + pr.proof shouldBe Array.emptyByteArray + pr.cost shouldBe 24L + + val (ok, cost) = testVerify(ergoTree, activatedScriptVersion = activatedVersionInTests) + ok shouldBe true + cost shouldBe 24L + } + } } - /** Rule#| SF Status| Block Type| Script Version | Release | Validation Action + /** Rule#| BlockVer | Block Type| Script Version | Release | Validation Action * -----|----------|-----------|----------------|---------|-------- - * 9 | active | candidate | Script v1 | v4.0 | R4.0-AOT-cost, R4.0-AOT-verify + * 17 | 3 | candidate | Script v3 | v5.0 | skip-reject (cannot handle) + * 18 | 3 | mined | Script v3 | v5.0 | skip-reject (cannot handle) */ - property("Rule 9 | active SF | candidate block | Script v1") { - val headerFlags = ErgoTree.headerWithVersion( 1 /* Script v1 */) - val ergoTree = createErgoTree(headerFlags = headerFlags) - - // Even though the SF is active and the v2 scripts can be accepted in `mined` blocks - // this test case is for `candidate` block, so we MUST use - // Interpreter.MaxSupportedScriptVersion - - // both prove and verify are accepting with full evaluation - val expectedCost = 5464L - val pr = testProve( - ergoTree, - activatedScriptVersion = Interpreter.MaxSupportedScriptVersion // special case for *candidate* block - ) - pr.proof shouldBe Array.emptyByteArray - pr.cost shouldBe expectedCost - - val (ok, cost) = testVerify( - ergoTree, - activatedScriptVersion = Interpreter.MaxSupportedScriptVersion // special case for *candidate* block - ) - ok shouldBe true - cost shouldBe expectedCost + property("Rules 17,18 | Block v3 | candidate or mined block | Script v3") { + forEachActivatedScriptVersion(activatedVers = Array[Byte](2)) // version for Block v3 + { + forEachErgoTreeVersion(ergoTreeVers = Array[Byte](3, 4)) { // scripts >= v3 + val headerFlags = ErgoTree.headerWithVersion(ergoTreeVersionInTests) + val ergoTree = createErgoTree(headerFlags) + + // prover is rejecting ErgoTree versions higher than activated + assertExceptionThrown( + testProve(ergoTree, activatedScriptVersion = activatedVersionInTests), + exceptionLike[InterpreterException](s"ErgoTree version ${ergoTree.version} is higher than activated $activatedVersionInTests") + ) + + // verifier is rejecting ErgoTree versions higher than activated + assertExceptionThrown( + testVerify(ergoTree, activatedScriptVersion = activatedVersionInTests), + exceptionLike[InterpreterException](s"ErgoTree version ${ergoTree.version} is higher than activated $activatedVersionInTests") + ) + } + } } - /** Rule#| SF Status| Block Type| Script Version | Release | Validation Action + /** Rule#| BlockVer | Block Type| Script Version | Release | Validation Action * -----|----------|-----------|----------------|---------|-------- - * 11 | active | candidate | Script v2 | v4.0 | skip-pool-tx (cannot handle) - * 15 | active | mined | Script v2 | v4.0 | skip-accept (rely on majority) + * 19 | 4 | candidate | Script v3 | v5.0 | skip-accept (rely on majority) + * 20 | 4 | mined | Script v3 | v5.0 | skip-accept (rely on majority) */ - property("Rule 11, 15 | active SF | candidate or mined block | Script v2") { - val headerFlags = ErgoTree.headerWithVersion( 2 /* Script v2 */) - val ergoTree = createErgoTree(headerFlags = headerFlags) - - val activatedVersion = 2.toByte // SF Status: active - - // the prove is doing normal reduction and proof generation - val pr = testProve(ergoTree, activatedScriptVersion = activatedVersion) - pr.proof shouldBe Array.emptyByteArray - pr.cost shouldBe 5464 - - // and verify is accepting without evaluation - val expectedCost = 0L - val (ok, cost) = testVerify(ergoTree, activatedScriptVersion = activatedVersion) - ok shouldBe true - cost shouldBe expectedCost + property("Rules 19,20 | Block v4 | candidate or mined block | Script v3") { + forEachActivatedScriptVersion(activatedVers = Array[Byte](3)) // version for Block v4 + { + forEachErgoTreeVersion(ergoTreeVers = Array[Byte](3, 4)) { // scripts >= v3 + val headerFlags = ErgoTree.headerWithVersion(ergoTreeVersionInTests) + val ergoTree = createErgoTree(headerFlags) + + // prover is rejecting, because such context parameters doesn't make sense + assertExceptionThrown( + testProve(ergoTree, activatedScriptVersion = activatedVersionInTests), + exceptionLike[InterpreterException](s"Both ErgoTree version ${ergoTree.version} and activated version $activatedVersionInTests is greater than MaxSupportedScriptVersion $MaxSupportedScriptVersion") + ) + + // and verify is accepting without evaluation + val (ok, cost) = testVerify(ergoTree, activatedScriptVersion = activatedVersionInTests) + ok shouldBe true + cost shouldBe 0L + } + } } - /** Rule#| SF Status| Block Type| Script Version | Release | Validation Action + /** Rule#| BlockVer | Block Type| Script Version | Release | Validation Action * -----|----------|-----------|----------------|---------|-------- - * 13 | active | mined | Script v1 | v4.0 | skip-accept (rely on majority) + * 21 | 4 | candidate | Script v0/v1 | v5.0 | R5.0-JIT-verify + * 22 | 4 | candidate | Script v2 | v5.0 | R5.0-JIT-verify + * 23 | 4 | mined | Script v0/v1 | v5.0 | R5.0-JIT-verify + * 24 | 4 | mined | Script v2 | v5.0 | R5.0-JIT-verify */ - property("Rule 13 | active SF | mined block | Script v1") { - val headerFlags = ErgoTree.headerWithVersion( 1 /* Script v1 */) - val ergoTree = createErgoTree(headerFlags = headerFlags) + property("Rules 21,22,23,24 | Block v4 | candidate or mined block | Script v0/v1/v2") { + // this test verifies the normal validation action R5.0-JIT-verify of v5.x releases + // when Block v4 already activated, but the script is v0, v1 or v2. - val activatedVersion = 2.toByte // SF Status: active + forEachActivatedScriptVersion(Array[Byte](3)) // version for Block v4 + { + forEachErgoTreeVersion(Array[Byte](0, 1, 2)) { // tree versions supported by v5.x + // SF inactive: check cost vectors of v4.x interpreter + val headerFlags = ErgoTree.headerWithVersion(ergoTreeVersionInTests) + val ergoTree = createErgoTree(headerFlags) - // the prove is doing normal reduction and proof generation - val pr = testProve(ergoTree, activatedScriptVersion = activatedVersion) - pr.proof shouldBe Array.emptyByteArray - pr.cost shouldBe 5464 + // both prove and verify are accepting with full evaluation + val pr = testProve(ergoTree, activatedScriptVersion = activatedVersionInTests) + pr.proof shouldBe Array.emptyByteArray + pr.cost shouldBe 24L - // and verify is accepting without evaluation - val (ok, cost) = testVerify(ergoTree, activatedScriptVersion = activatedVersion) - ok shouldBe true - cost shouldBe 0L + val (ok, cost) = testVerify(ergoTree, activatedScriptVersion = activatedVersionInTests) + ok shouldBe true + cost shouldBe 24L + } + } } } diff --git a/sigmastate/src/test/scala/sigmastate/SoftForkabilitySpecification.scala b/sigmastate/src/test/scala/sigmastate/SoftForkabilitySpecification.scala index 341c29428a..6a9b09dbc9 100644 --- a/sigmastate/src/test/scala/sigmastate/SoftForkabilitySpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/SoftForkabilitySpecification.scala @@ -11,7 +11,7 @@ import sigmastate.eval.Colls import sigmastate.helpers.{ErgoLikeContextTesting, ErgoLikeTestProvingInterpreter, ErgoLikeTestInterpreter} import sigmastate.helpers.TestingHelpers._ import sigmastate.interpreter.Interpreter.{ScriptNameProp, emptyEnv, WhenSoftForkReductionResult} -import sigmastate.interpreter.{ProverResult, Interpreter, WhenSoftForkReducer, ContextExtension, CacheKey} +import sigmastate.interpreter.{ProverResult, WhenSoftForkReducer, ContextExtension, CacheKey} import sigmastate.lang.Terms._ import sigmastate.lang.exceptions.{SerializerException, SigmaException, InterpreterException, CosterException} import sigmastate.serialization.OpCodes.{OpCodeExtra, LastConstantCode, OpCode} diff --git a/sigmastate/src/test/scala/sigmastate/TestingInterpreterSpecification.scala b/sigmastate/src/test/scala/sigmastate/TestingInterpreterSpecification.scala index 8f475aa3e7..81f05a0c04 100644 --- a/sigmastate/src/test/scala/sigmastate/TestingInterpreterSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/TestingInterpreterSpecification.scala @@ -43,6 +43,7 @@ class TestingInterpreterSpecification extends SigmaTestingCommons AvlTreeData.dummy, ErgoLikeContextTesting.dummyPubkey, IndexedSeq(fakeSelf), ErgoLikeTransaction(IndexedSeq.empty, IndexedSeq.empty), fakeSelf, activatedVersionInTests) + .withErgoTreeVersion(ergoTreeVersionInTests) property("Reduction to crypto #1") { forAll() { i: Int => @@ -51,27 +52,27 @@ class TestingInterpreterSpecification extends SigmaTestingCommons val dk1 = SigmaPropConstant(DLogProverInput.random().publicImage) val ctx = testingContext(h) - prover.reduceToCrypto(ctx, SigmaAnd(GE(Height, IntConstant(h - 1)), dk1)).get.value should( + testReduce(prover)(ctx, SigmaAnd(GE(Height, IntConstant(h - 1)), dk1)) should( matchPattern { case _: SigmaBoolean => }) - prover.reduceToCrypto(ctx, SigmaAnd(GE(Height, IntConstant(h)), dk1)).get.value should ( + testReduce(prover)(ctx, SigmaAnd(GE(Height, IntConstant(h)), dk1)) should ( matchPattern { case _: SigmaBoolean => }) { - val res = prover.reduceToCrypto(ctx, SigmaAnd(GE(Height, IntConstant(h + 1)), dk1)).get.value + val res = testReduce(prover)(ctx, SigmaAnd(GE(Height, IntConstant(h + 1)), dk1)) res should matchPattern { case TrivialProp.FalseProp => } } { - val res = prover.reduceToCrypto(ctx, SigmaOr(GE(Height, IntConstant(h - 1)), dk1)).get.value + val res = testReduce(prover)(ctx, SigmaOr(GE(Height, IntConstant(h - 1)), dk1)) res should matchPattern { case TrivialProp.TrueProp => } } { - val res = prover.reduceToCrypto(ctx, SigmaOr(GE(Height, IntConstant(h)), dk1)).get.value + val res = testReduce(prover)(ctx, SigmaOr(GE(Height, IntConstant(h)), dk1)) res should matchPattern { case TrivialProp.TrueProp => } } { - val res = prover.reduceToCrypto(ctx, SigmaOr(GE(Height, IntConstant(h + 1)), dk1)).get.value + val res = testReduce(prover)(ctx, SigmaOr(GE(Height, IntConstant(h + 1)), dk1)) res should matchPattern { case _: SigmaBoolean => } } } @@ -88,23 +89,23 @@ class TestingInterpreterSpecification extends SigmaTestingCommons val ctx = testingContext(h) - assert(prover.reduceToCrypto(ctx, SigmaOr( + assert(testReduce(prover)(ctx, SigmaOr( SigmaAnd(LE(Height, IntConstant(h + 1)), SigmaAnd(dk1, dk2)), SigmaAnd(GT(Height, IntConstant(h + 1)), dk1) - )).get.value.isInstanceOf[CAND]) + )).isInstanceOf[CAND]) - assert(prover.reduceToCrypto(ctx, SigmaOr( + assert(testReduce(prover)(ctx, SigmaOr( SigmaAnd(LE(Height, IntConstant(h - 1)), SigmaAnd(dk1, dk2)), SigmaAnd(GT(Height, IntConstant(h - 1)), dk1) - )).get.value.isInstanceOf[ProveDlog]) + )).isInstanceOf[ProveDlog]) - prover.reduceToCrypto(ctx, SigmaOr( + testReduce(prover)(ctx, SigmaOr( SigmaAnd(LE(Height, IntConstant(h - 1)), SigmaAnd(dk1, dk2)), SigmaAnd(GT(Height, IntConstant(h + 1)), dk1) - )).get.value shouldBe TrivialProp.FalseProp + )) shouldBe TrivialProp.FalseProp - prover.reduceToCrypto(ctx, + testReduce(prover)(ctx, SigmaOr( SigmaOr( SigmaAnd(LE(Height, IntConstant(h - 1)), SigmaAnd(dk1, dk2)), @@ -112,7 +113,7 @@ class TestingInterpreterSpecification extends SigmaTestingCommons ), AND(GT(Height, IntConstant(h - 1)), LE(Height, IntConstant(h + 1))) ) - ).get.value shouldBe TrivialProp.TrueProp + ) shouldBe TrivialProp.TrueProp } } @@ -134,11 +135,11 @@ class TestingInterpreterSpecification extends SigmaTestingCommons reg1 -> IntArrayConstant(Array[Int](1, 2, 3)), reg2 -> BoolArrayConstant(Array[Boolean](true, false, true))))) val prop = mkTestErgoTree(compile(env, code).asBoolValue.toSigmaProp) - println(code) - println(prop) val challenge = Array.fill(32)(Random.nextInt(100).toByte) val proof1 = prover.prove(prop, ctx, challenge).get.proof - verifier.verify(Interpreter.emptyEnv, prop, ctx, proof1, challenge).map(_._1).getOrElse(false) shouldBe true + verifier.verify(Interpreter.emptyEnv, prop, ctx, proof1, challenge) + .map(_._1) + .getOrElse(false) shouldBe true } property("Evaluate array ops") { @@ -242,13 +243,13 @@ class TestingInterpreterSpecification extends SigmaTestingCommons property("failed numeric downcast (overflow)") { assertExceptionThrown(testEval("Coll(999)(0).toByte > 0"), - _.getCause.isInstanceOf[ArithmeticException]) + rootCause(_).isInstanceOf[ArithmeticException]) assertExceptionThrown(testEval("Coll(999)(0).toShort.toByte > 0"), - _.getCause.isInstanceOf[ArithmeticException]) + rootCause(_).isInstanceOf[ArithmeticException]) assertExceptionThrown(testEval(s"Coll(${Int.MaxValue})(0).toShort > 0"), - _.getCause.isInstanceOf[ArithmeticException]) + rootCause(_).isInstanceOf[ArithmeticException]) assertExceptionThrown(testEval(s"Coll(${Long.MaxValue}L)(0).toInt > 0"), - _.getCause.isInstanceOf[ArithmeticException]) + rootCause(_).isInstanceOf[ArithmeticException]) } property("Coll indexing (out of bounds with const default value)") { diff --git a/sigmastate/src/test/scala/sigmastate/TestsBase.scala b/sigmastate/src/test/scala/sigmastate/TestsBase.scala index 48ba284840..25390c1a08 100644 --- a/sigmastate/src/test/scala/sigmastate/TestsBase.scala +++ b/sigmastate/src/test/scala/sigmastate/TestsBase.scala @@ -10,29 +10,16 @@ import sigmastate.interpreter.Interpreter.ScriptEnv import sigmastate.lang.{TransformingSigmaBuilder, SigmaCompiler, CompilerSettings} import sigmastate.lang.Terms.ValueOps import sigmastate.serialization.ValueSerializer +import spire.syntax.all.cfor import scala.util.DynamicVariable -trait TestsBase extends Matchers { - - val activatedVersions: Seq[Byte] = Array[Byte](0, 1) - - private[sigmastate] val _currActivatedVersion = new DynamicVariable[Byte](0) - def activatedVersionInTests: Byte = _currActivatedVersion.value - - val ergoTreeVersions: Seq[Byte] = - (0 to Interpreter.MaxSupportedScriptVersion).map(_.toByte).toArray[Byte] - - private[sigmastate] val _currErgoTreeVersion = new DynamicVariable[Byte](0) - - /** Current ErgoTree version assigned dynamically using [[CrossVersionProps]]. */ - def ergoTreeVersionInTests: Byte = _currErgoTreeVersion.value +trait TestsBase extends Matchers with VersionTesting { /** Current ErgoTree header flags assigned dynamically using [[CrossVersionProps]] and * ergoTreeVersionInTests. */ def ergoTreeHeaderInTests: Byte = ErgoTree.headerWithVersion(ergoTreeVersionInTests) - /** Obtains [[ErgoTree]] which corresponds to True proposition using current * ergoTreeHeaderInTests. */ def TrueTree: ErgoTree = ErgoScriptPredef.TrueProp(ergoTreeHeaderInTests) diff --git a/sigmastate/src/test/scala/sigmastate/crypto/SigningSpecification.scala b/sigmastate/src/test/scala/sigmastate/crypto/SigningSpecification.scala index 5d880dc4c5..8550d64cb3 100644 --- a/sigmastate/src/test/scala/sigmastate/crypto/SigningSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/crypto/SigningSpecification.scala @@ -50,7 +50,7 @@ class SigningSpecification extends SigmaTestingCommons { val verifier = new ErgoLikeTestInterpreter val msg = "any message".getBytes("UTF-8") val sig = "invalid signature".getBytes("UTF-8") - verifier.verifySignature(sigmaTree, msg, sig) shouldBe false + verifier.verifySignature(sigmaTree, msg, sig)(null) shouldBe false } property("AND signature test vector") { @@ -140,11 +140,11 @@ class SigningSpecification extends SigmaTestingCommons { val pi = new ErgoLikeTestProvingInterpreter() val sigmaTree: SigmaBoolean = pi.publicKeys.head val sig = pi.signMessage(sigmaTree, msg, HintsBag.empty).get - pi.verifySignature(sigmaTree, msg, sig) shouldBe true - pi.verifySignature(sigmaTree, (str + "1").getBytes("UTF-8"), sig) shouldBe false - pi.verifySignature(sigmaTree, msg, sig :+ (1: Byte)) shouldBe true //possible to append bytes + pi.verifySignature(sigmaTree, msg, sig)(null) shouldBe true + pi.verifySignature(sigmaTree, (str + "1").getBytes("UTF-8"), sig)(null) shouldBe false + pi.verifySignature(sigmaTree, msg, sig :+ (1: Byte))(null) shouldBe true //possible to append bytes val wrongTree = pi.publicKeys(1) - pi.verifySignature(wrongTree, msg, sig) shouldBe false + pi.verifySignature(wrongTree, msg, sig)(null) shouldBe false } } @@ -154,11 +154,11 @@ class SigningSpecification extends SigmaTestingCommons { val pi = new ErgoLikeTestProvingInterpreter() val sigmaTree: SigmaBoolean = CAND(Seq(pi.dlogSecrets.head.publicImage, pi.dhSecrets.head.publicImage)) val sig = pi.signMessage(sigmaTree, msg, HintsBag.empty).get - pi.verifySignature(sigmaTree, msg, sig) shouldBe true - pi.verifySignature(sigmaTree, (str + "1").getBytes("UTF-8"), sig) shouldBe false - pi.verifySignature(sigmaTree, msg, sig :+ (1: Byte)) shouldBe true //possible to append bytes + pi.verifySignature(sigmaTree, msg, sig)(null) shouldBe true + pi.verifySignature(sigmaTree, (str + "1").getBytes("UTF-8"), sig)(null) shouldBe false + pi.verifySignature(sigmaTree, msg, sig :+ (1: Byte))(null) shouldBe true //possible to append bytes val wrongTree = CAND(Seq(pi.dlogSecrets.head.publicImage, pi.dhSecrets(1).publicImage)) - pi.verifySignature(wrongTree, msg, sig) shouldBe false + pi.verifySignature(wrongTree, msg, sig)(null) shouldBe false } } @@ -170,7 +170,7 @@ class SigningSpecification extends SigmaTestingCommons { // check that signature is correct val verifier = new ErgoLikeTestInterpreter - verifier.verifySignature(sk.publicImage, msg, signature) shouldBe true + verifier.verifySignature(sk.publicImage, msg, signature)(null) shouldBe true // print one more random vector for debug purposes printSimpleSignature(msg: Array[Byte]) @@ -181,7 +181,7 @@ class SigningSpecification extends SigmaTestingCommons { val sk = proverA.dlogSecrets.head val prop = sk.publicImage - val tree = prop.toSigmaProp.treeWithSegregation + val tree = mkTestErgoTree(prop) val prove = proverA.prove(tree, fakeContext, msg).get println(s"Message: ${Base16.encode(msg)}") @@ -192,7 +192,6 @@ class SigningSpecification extends SigmaTestingCommons { println(s"Signature: ${Base16.encode(prove.proof)}") } - private def printThresholdSignature(msg: Array[Byte]) { val proverA = new ErgoLikeTestProvingInterpreter diff --git a/sigmastate/src/test/scala/sigmastate/eval/ErgoScriptTestkit.scala b/sigmastate/src/test/scala/sigmastate/eval/ErgoScriptTestkit.scala index 83798dfabc..ef47d80a50 100644 --- a/sigmastate/src/test/scala/sigmastate/eval/ErgoScriptTestkit.scala +++ b/sigmastate/src/test/scala/sigmastate/eval/ErgoScriptTestkit.scala @@ -47,6 +47,7 @@ trait ErgoScriptTestkit extends ContractsTestkit with LangTests spendingTransaction = tx1, self = boxToSpend, activatedVersionInTests, extension = ContextExtension(extension)) + .withErgoTreeVersion(ergoTreeVersionInTests) ergoCtx } @@ -68,7 +69,7 @@ trait ErgoScriptTestkit extends ContractsTestkit with LangTests lazy val backerPubKey = backerProver.dlogSecrets.head.publicImage lazy val projectPubKey = projectProver.dlogSecrets.head.publicImage - val boxToSpend = testBox(10, TrueTree, 0, + lazy val boxToSpend = testBox(10, TrueTree, 0, additionalRegisters = Map(ErgoBox.R4 -> BigIntArrayConstant(bigIntegerArr1))) lazy val tx1Output1 = testBox(minToRaise, projectPubKey, 0) lazy val tx1Output2 = testBox(1, projectPubKey, 0) @@ -171,7 +172,7 @@ trait ErgoScriptTestkit extends ContractsTestkit with LangTests val compiledProp = IR.buildTree(asRep[Context => SType#WrappedType](calcF)) checkExpected(compiledProp, expectedTree, "Compiled Tree actual: %s, expected: %s") - val ergoTree = compiledProp.treeWithSegregation + val ergoTree = mkTestErgoTree(compiledProp) val compiledTreeBytes = DefaultSerializer.serializeErgoTree(ergoTree) checkExpected(DefaultSerializer.deserializeErgoTree(compiledTreeBytes), Some(ergoTree), "(de)serialization round trip actual: %s, expected: %s") diff --git a/sigmastate/src/test/scala/sigmastate/eval/EvaluationTest.scala b/sigmastate/src/test/scala/sigmastate/eval/EvaluationTest.scala index 6e794bb7cc..9edc4a33d6 100644 --- a/sigmastate/src/test/scala/sigmastate/eval/EvaluationTest.scala +++ b/sigmastate/src/test/scala/sigmastate/eval/EvaluationTest.scala @@ -104,12 +104,12 @@ class EvaluationTest extends BaseCtxTests val pk2 = DLogProverInput.random().publicImage val script1 = script(pk1) val script2 = script(pk2) - val inputBytes = DefaultSerializer.serializeErgoTree(script1.treeWithSegregation) + val inputBytes = DefaultSerializer.serializeErgoTree(mkTestErgoTree(script1)) val positions = IntArrayConstant(Array[Int](2)) // in ergo we have only byte array of a serialized group element val newVals = ConcreteCollection(Array[SigmaPropValue](CreateProveDlog(DecodePoint(pk2.pkBytes))), SSigmaProp) - val expectedBytes = DefaultSerializer.serializeErgoTree(script2.treeWithSegregation) + val expectedBytes = DefaultSerializer.serializeErgoTree(mkTestErgoTree(script2)) val ctx = newErgoContext(height = 1, boxToSpend) reduce(emptyEnv, "SubstConst", EQ(SubstConstants(inputBytes, positions, newVals), expectedBytes), diff --git a/sigmastate/src/test/scala/sigmastate/helpers/ErgoTransactionValidator.scala b/sigmastate/src/test/scala/sigmastate/helpers/ErgoTransactionValidator.scala index 74cdbcf440..f7ccaad7f6 100644 --- a/sigmastate/src/test/scala/sigmastate/helpers/ErgoTransactionValidator.scala +++ b/sigmastate/src/test/scala/sigmastate/helpers/ErgoTransactionValidator.scala @@ -1,6 +1,9 @@ package sigmastate.helpers import org.ergoplatform._ +import sigmastate.eval.IRContext +import sigmastate.interpreter.ErgoTreeEvaluator.DefaultEvalSettings +import sigmastate.interpreter.EvalSettings import org.ergoplatform.settings.ErgoAlgos import org.ergoplatform.validation.{ValidationRules, SigmaValidationSettings} import sigmastate.eval.{IRContext, CompiletimeIRContext} @@ -13,6 +16,10 @@ import scala.util.{Success, Failure} * @see derived classes */ class ErgoLikeTestInterpreter(implicit override val IR: IRContext) extends ErgoLikeInterpreter { override type CTX = ErgoLikeContext + override val evalSettings: EvalSettings = DefaultEvalSettings.copy( + isMeasureOperationTime = true, + isDebug = true, + isTestRun = true) override val precompiledScriptProcessor = ErgoLikeTestInterpreter.DefaultProcessorInTests } object ErgoLikeTestInterpreter { diff --git a/sigmastate/src/test/scala/sigmastate/helpers/NegativeTesting.scala b/sigmastate/src/test/scala/sigmastate/helpers/NegativeTesting.scala index 6748826b35..60668c8c81 100644 --- a/sigmastate/src/test/scala/sigmastate/helpers/NegativeTesting.scala +++ b/sigmastate/src/test/scala/sigmastate/helpers/NegativeTesting.scala @@ -44,7 +44,7 @@ trait NegativeTesting extends Matchers { /** Creates an assertion which checks the given type and message contents. * - * @tparam E expected type of exception + * @tparam E expected type of an exception * @param msgParts expected parts of the exception message * @return the assertion which can be used in assertExceptionThrown method */ @@ -54,6 +54,21 @@ trait NegativeTesting extends Matchers { case _ => false } + /** Creates an assertion which checks the root cause exception and message contents. + * + * @tparam E expected type of a root cause exception + * @param msgParts expected parts of the root cause exception message + * @return the assertion which can be used in assertExceptionThrown method + */ + def rootCauseLike[E <: Throwable : ClassTag] + (msgParts: String*): Throwable => Boolean = { t => + val root = rootCause(t) + root match { + case r: E => msgParts.forall(r.getMessage.contains(_)) + case _ => false + } + } + /** Checks that both computations either succeed with the same value or fail with the same * error. If this is not true, exception is thrown. * diff --git a/sigmastate/src/test/scala/sigmastate/helpers/SigmaPPrint.scala b/sigmastate/src/test/scala/sigmastate/helpers/SigmaPPrint.scala index 5e6df2a349..347bc46a0a 100644 --- a/sigmastate/src/test/scala/sigmastate/helpers/SigmaPPrint.scala +++ b/sigmastate/src/test/scala/sigmastate/helpers/SigmaPPrint.scala @@ -10,13 +10,14 @@ import pprint.{PPrinter, Tree} import scalan.RType import scalan.RType.PrimitiveType import sigmastate.SCollection._ -import sigmastate.Values.{ConstantNode, ErgoTree, ValueCompanion} +import sigmastate.Values.{ConstantNode, ErgoTree, FuncValue, ValueCompanion} import sigmastate._ import sigmastate.interpreter.CryptoConstants.EcPointType import sigmastate.lang.SigmaTyper import sigmastate.lang.Terms.MethodCall import sigmastate.serialization.GroupElementSerializer import sigmastate.utxo.SelectField +import sigmastate.interpreter.{CompanionDesc, ErgoTreeEvaluator, FixedCostItem, MethodDesc} import special.collection.Coll import special.sigma.GroupElement @@ -176,11 +177,24 @@ object SigmaPPrint extends PPrinter { )) } + def methodLiteral(m: SMethod) = { + val objType = apply(m.objType).plainText + Tree.Literal(s"$objType.${m.name}") + } + override val additionalHandlers: PartialFunction[Any, Tree] = typeHandlers .orElse(exceptionHandlers) .orElse(dataHandlers) .orElse { + case FixedCostItem(CompanionDesc(c), _) => + Tree.Apply("FixedCostItem", treeifySeq(Seq(c))) + case FixedCostItem(MethodDesc(m), cost) => + Tree.Apply("FixedCostItem", Seq(methodLiteral(m)).iterator ++ treeifySeq(Seq(cost))) + case FuncValue.AddToEnvironmentDesc => + Tree.Literal(s"FuncValue.AddToEnvironmentDesc") + case MethodDesc(method) => + Tree.Apply("MethodDesc", Seq(methodLiteral(method)).iterator) case sigmastate.SGlobal => Tree.Literal(s"SGlobal") case sigmastate.SCollection => diff --git a/sigmastate/src/test/scala/sigmastate/helpers/SigmaTestingCommons.scala b/sigmastate/src/test/scala/sigmastate/helpers/SigmaTestingCommons.scala index 8dd612e540..a0a6f207f0 100644 --- a/sigmastate/src/test/scala/sigmastate/helpers/SigmaTestingCommons.scala +++ b/sigmastate/src/test/scala/sigmastate/helpers/SigmaTestingCommons.scala @@ -2,28 +2,32 @@ package sigmastate.helpers import org.ergoplatform.SigmaConstants.ScriptCostLimit import org.ergoplatform._ -import org.ergoplatform.validation.ValidationRules.{CheckCalcFunc, CheckCostFunc} -import org.ergoplatform.validation.ValidationSpecification +import org.ergoplatform.validation.ValidationRules.{CheckCalcFunc, CheckCostFunc, CheckSerializableTypeCode} +import org.ergoplatform.validation.{ValidationException, ValidationSpecification} import org.scalacheck.Arbitrary.arbByte import org.scalacheck.Gen import org.scalatest.prop.{GeneratorDrivenPropertyChecks, PropertyChecks} import org.scalatest.{Assertion, Matchers, PropSpec} +import scalan.util.BenchmarkUtil import scalan.{RType, TestContexts, TestUtils} import scorex.crypto.hash.Blake2b256 import sigma.types.IsPrimView -import sigmastate.Values.{Constant, EvaluatedValue, GroupElementConstant, SValue, Value} -import sigmastate.interpreter.Interpreter.{ScriptEnv, ScriptNameProp} -import sigmastate.interpreter.{CryptoConstants, Interpreter} -import sigmastate.lang.Terms -import sigmastate.serialization.SigmaSerializer -import sigmastate.{SGroupElement, SType, TestsBase} +import sigmastate.Values.{Constant, ErgoTree, GroupElementConstant, SValue, SigmaBoolean, SigmaPropValue, Value} import sigmastate.eval.{CompiletimeCosting, Evaluation, IRContext, _} +import sigmastate.helpers.TestingHelpers._ +import sigmastate.interpreter.ContextExtension.VarBinding import sigmastate.interpreter.CryptoConstants.EcPointType +import sigmastate.interpreter.ErgoTreeEvaluator.DefaultProfiler +import sigmastate.interpreter.Interpreter.{ScriptEnv, ScriptNameProp} +import sigmastate.interpreter.{CryptoConstants, Interpreter, _} +import sigmastate.lang.{CompilerSettings, SigmaCompiler, Terms} +import sigmastate.serialization.SigmaSerializer import sigmastate.utils.Helpers._ -import sigmastate.helpers.TestingHelpers._ +import sigmastate.{JitCost, SGroupElement, SOption, SType, TestsBase} import special.sigma import scala.language.implicitConversions +import scala.reflect.ClassTag import scala.util.DynamicVariable trait SigmaTestingCommons extends PropSpec @@ -37,6 +41,7 @@ trait SigmaTestingCommons extends PropSpec def fakeContext: ErgoLikeContext = ErgoLikeContextTesting.dummy(fakeSelf, activatedVersionInTests) + .withErgoTreeVersion(ergoTreeVersionInTests) //fake message, in a real-life a message is to be derived from a spending transaction val fakeMessage = Blake2b256("Hello World") @@ -92,9 +97,9 @@ trait SigmaTestingCommons extends PropSpec } case class CompiledFunc[A,B] - (script: String, bindings: Seq[(Byte, EvaluatedValue[_ <: SType])], expr: SValue, func: A => (B, Int)) - (implicit val tA: RType[A], val tB: RType[B]) extends Function1[A, (B, Int)] { - override def apply(x: A): (B, Int) = func(x) + (script: String, bindings: Seq[VarBinding], expr: SValue, compiledTree: SValue, func: A => (B, CostDetails)) + (implicit val tA: RType[A], val tB: RType[B]) extends Function1[A, (B, CostDetails)] { + override def apply(x: A): (B, CostDetails) = func(x) } /** The same operations are executed as part of Interpreter.verify() */ @@ -115,34 +120,70 @@ trait SigmaTestingCommons extends PropSpec */ protected val initialCostInTests = new DynamicVariable[Long](0) - /** Returns a Scala function which is equivalent to the given function script. - * The script is embedded into valid ErgoScript which is then compiled to - * [[sigmastate.Values.Value]] tree. - * Limitations: - * 1) DeserializeContext, ConstantPlaceholder is not supported - * @param funcScript source code of the function - * @param bindings additional context variables - */ - def func[A: RType, B: RType] - (funcScript: String, bindings: (Byte, EvaluatedValue[_ <: SType])*) - (implicit IR: IRContext): CompiledFunc[A, B] = { - import IR._ - import IR.Context._; - val tA = RType[A] + def createContexts[A](in: A, bindings: Seq[VarBinding])(implicit tA: RType[A]) = { + val x = fromPrimView(in) val tpeA = Evaluation.rtypeToSType(tA) + in match { + case ctx: CostingDataContext => + // the context is passed as function argument (this is for testing only) + // This is to overcome non-functional semantics of context operations + // (such as Inputs, Height, etc which don't have arguments and refer to the + // context implicitly). + // These context operations are introduced by buildTree frontend function + // (ctx.HEIGHT method call compiled to Height IR node) + // ------- + // We add ctx as it's own variable with id = 1 + val ctxVar = Extensions.toAnyValue[special.sigma.Context](ctx)(special.sigma.ContextRType) + val newVars = if (ctx.vars.length < 2) { + val vars = ctx.vars.toArray + val buf = new Array[special.sigma.AnyValue](2) + Array.copy(vars, 0, buf, 0, vars.length) + buf(1) = ctxVar + CostingSigmaDslBuilder.Colls.fromArray(buf) + } else { + ctx.vars.updated(1, ctxVar) + } + val calcCtx = ctx.copy(vars = newVars) + val costCtx = calcCtx.copy(isCost = true) + (costCtx, calcCtx) + case _ => + val box = createBox(0, TrueTree) + + // make sure we are doing tests with the box with is actually serializable + try roundTripTest(box)(ErgoBox.sigmaSerializer) + catch { + case ValidationException(_, r: CheckSerializableTypeCode.type, Seq(SOption.OptionTypeCode), _) => + // ignore the problem with Option serialization, but test all the other cases + } + + val ergoCtx = ErgoLikeContextTesting.dummy(box, activatedVersionInTests) + .withErgoTreeVersion(ergoTreeVersionInTests) + .withBindings(1.toByte -> Constant[SType](x.asInstanceOf[SType#WrappedType], tpeA)) + .withBindings(bindings: _*) + val calcCtx = ergoCtx.toSigmaContext(isCost = false).asInstanceOf[CostingDataContext] + val costCtx = calcCtx.copy(isCost = true) + (costCtx, calcCtx) + } + } + + def compileTestScript[A] + (env: ScriptEnv, funcScript: String) + (implicit tA: RType[A], + IR: IRContext, + compilerSettings: CompilerSettings): SValue = { val code = s"""{ - | val func = $funcScript - | val res = func(getVar[${tA.name}](1).get) - | res - |} + | val func = $funcScript + | val res = func(getVar[${tA.name}](1).get) + | res + |} """.stripMargin - val env = Interpreter.emptyEnv // The following ops are performed by frontend // typecheck, create graphs, compile to Tree // The resulting tree should be serializable val compiledTree = { + val compiler = SigmaCompiler(compilerSettings) val internalProp = compiler.typecheck(env, code) val costingRes = getCostingResult(env, internalProp) val calcF = costingRes.calcF @@ -150,22 +191,41 @@ trait SigmaTestingCommons extends PropSpec checkSerializationRoundTrip(tree) tree } + compiledTree + } + + /** Returns a Scala function which is equivalent to the given function script. + * The script is embedded into valid ErgoScript which is then compiled to + * [[sigmastate.Values.Value]] tree. + * Limitations: + * 1) DeserializeContext, ConstantPlaceholder is not supported + * @param funcScript source code of the function + * @param bindings additional context variables + */ + def funcFromExpr[A: RType, B: RType] + (funcScript: String, expr: SValue, bindings: VarBinding*) + (implicit IR: IRContext, + compilerSettings: CompilerSettings): CompiledFunc[A, B] = { + import IR._ + import IR.Context._ + val tA = RType[A] + val env = Interpreter.emptyEnv // The following is done as part of Interpreter.verify() val (costF, valueFun) = { - val costingRes = getCostingResult(env, compiledTree) + val costingRes = getCostingResult(env, expr) val calcF = costingRes.calcF val tree = IR.buildTree(calcF) // sanity check that buildTree is reverse to buildGraph (see doCostingEx) if (tA != special.sigma.ContextRType) { - if (tree != compiledTree) { + if (tree != expr) { println(s"Result of buildTree:") val prettyTree = SigmaPPrint(tree, height = 150) println(prettyTree) println(s"compiledTree:") - val prettyCompiledTree = SigmaPPrint(compiledTree, height = 150) + val prettyCompiledTree = SigmaPPrint(expr, height = 150) println(prettyCompiledTree) assert(prettyTree.plainText == prettyCompiledTree.plainText, "Failed sanity check that buildTree is reverse to buildGraph") @@ -179,49 +239,131 @@ trait SigmaTestingCommons extends PropSpec } val f = (in: A) => { - implicit val cA = tA.classTag - val x = fromPrimView(in) - val (costingCtx, sigmaCtx) = in match { - case ctx: CostingDataContext => - // the context is passed as function argument (this is for testing only) - // This is to overcome non-functional semantics of context operations - // (such as Inputs, Height, etc which don't have arguments and refer to the - // context implicitly). - // These context operations are introduced by buildTree frontend function - // (ctx.HEIGHT method call compiled to Height IR node) - // ------- - // We add ctx as it's own variable with id = 1 - val ctxVar = Extensions.toAnyValue[special.sigma.Context](ctx)(special.sigma.ContextRType) - val newVars = if (ctx.vars.length < 2) { - val vars = ctx.vars.toArray - val buf = new Array[special.sigma.AnyValue](2) - Array.copy(vars, 0, buf, 0, vars.length) - buf(1) = ctxVar - CostingSigmaDslBuilder.Colls.fromArray(buf) - } else { - ctx.vars.updated(1, ctxVar) - } - val calcCtx = ctx.copy(vars = newVars) - val costCtx = calcCtx.copy(isCost = true) - (costCtx, calcCtx) - case _ => - val ergoCtx = ErgoLikeContextTesting.dummy( - createBox(0, TrueTree), activatedVersionInTests - ).withBindings(1.toByte -> Constant[SType](x.asInstanceOf[SType#WrappedType], tpeA)) - .withBindings(bindings: _*) - - val calcCtx = ergoCtx.toSigmaContext(isCost = false).asInstanceOf[CostingDataContext] - val costCtx = calcCtx.copy(isCost = true) - (costCtx, calcCtx) - } + implicit val cA: ClassTag[A] = tA.classTag + val (costingCtx, sigmaCtx) = createContexts(in, bindings) val estimatedCost = IR.checkCostWithContext(costingCtx, costF, ScriptCostLimit.value, initialCostInTests.value).getOrThrow val (res, _) = valueFun(sigmaCtx) - (res.asInstanceOf[B], estimatedCost) + (res.asInstanceOf[B], GivenCost(JitCost(estimatedCost))) + } + val Terms.Apply(funcVal, _) = expr.asInstanceOf[SValue] + CompiledFunc(funcScript, bindings, funcVal, expr, f) + } + + /** Returns a Scala function which is equivalent to the given function script. + * The script is embedded into valid ErgoScript which is then compiled to + * [[sigmastate.Values.Value]] tree. + * Limitations: + * 1) DeserializeContext, ConstantPlaceholder is not supported + * @param funcScript source code of the function + * @param bindings additional context variables + */ + def func[A: RType, B: RType] + (funcScript: String, bindings: VarBinding*) + (implicit IR: IRContext, + compilerSettings: CompilerSettings): CompiledFunc[A, B] = { + val env = Interpreter.emptyEnv + val compiledTree = compileTestScript[A](env, funcScript) + funcFromExpr[A, B](funcScript, compiledTree, bindings:_*) + } + + def evalSettings = ErgoTreeEvaluator.DefaultEvalSettings + + def printCostDetails(script: String, details: CostDetails) = { + val traceLines = SigmaPPrint(details, height = 550, width = 150) + println( + s"""------------------------ + |Script: $script + |$traceLines + |""".stripMargin) + } + + def funcJitFromExpr[A: RType, B: RType] + (funcScript: String, expr: SValue, bindings: VarBinding*) + (implicit IR: IRContext, + evalSettings: EvalSettings, + compilerSettings: CompilerSettings): CompiledFunc[A, B] = { + val tA = RType[A] + val f = (in: A) => { + implicit val cA: ClassTag[A] = tA.classTag + val (_, sigmaCtx) = createContexts(in, bindings) + val accumulator = new CostAccumulator( + initialCost = JitCost(0), + costLimit = Some(JitCost.fromBlockCost(ScriptCostLimit.value))) + val evaluator = new ErgoTreeEvaluator( + context = sigmaCtx, + constants = ErgoTree.EmptyConstants, + coster = accumulator, evalSettings.profilerOpt.getOrElse(DefaultProfiler), evalSettings) + + val (res, actualTime) = BenchmarkUtil.measureTimeNano( + evaluator.evalWithCost[B](ErgoTreeEvaluator.EmptyDataEnv, expr)) + val costDetails = if (evalSettings.costTracingEnabled) { + val trace: Seq[CostItem] = evaluator.getCostTrace() + val costDetails = TracedCost(trace, Some(actualTime)) + assert(res.cost == costDetails.cost) + costDetails + } else + GivenCost(res.cost, Some(actualTime)) + + if (evalSettings.isMeasureScriptTime) { + evaluator.profiler.addJitEstimation(funcScript, res.cost, actualTime) + } + + if (evalSettings.isLogEnabled) { + printCostDetails(funcScript, costDetails) + } + (res.value, costDetails) + } + val Terms.Apply(funcVal, _) = expr.asInstanceOf[SValue] + CompiledFunc(funcScript, bindings, funcVal, expr, f) + } + + def funcJit[A: RType, B: RType] + (funcScript: String, bindings: VarBinding*) + (implicit IR: IRContext, + evalSettings: EvalSettings, + compilerSettings: CompilerSettings): CompiledFunc[A, B] = { + val compiledTree = compileTestScript[A](Interpreter.emptyEnv, funcScript) + funcJitFromExpr(funcScript, compiledTree, bindings:_*) + } + + /** Creates a specialized (faster) version which can be used to benchmark performance of + * various scripts. */ + def funcJitFast[A: RType, B: RType] + (funcScript: String, bindings: VarBinding*) + (implicit IR: IRContext, + evalSettings: EvalSettings, + compilerSettings: CompilerSettings): CompiledFunc[A, B] = { + val tA = RType[A] + val compiledTree = compileTestScript[A](Interpreter.emptyEnv, funcScript) + implicit val cA: ClassTag[A] = tA.classTag + val tpeA = Evaluation.rtypeToSType(tA) + val ergoCtxTemp = ErgoLikeContextTesting.dummy( + createBox(0, TrueTree), activatedVersionInTests) + .withErgoTreeVersion(ergoTreeVersionInTests) + .withBindings(bindings: _*) + + val f = (in: A) => { + val x = fromPrimView(in) + val ergoCtx = ergoCtxTemp + .withBindings(1.toByte -> Constant[SType](x.asInstanceOf[SType#WrappedType], tpeA)) + val sigmaCtx = ergoCtx.toSigmaContext(isCost = false).asInstanceOf[CostingDataContext] + + val accumulator = new CostAccumulator( + initialCost = JitCost(0), + costLimit = Some(JitCost.fromBlockCost(ScriptCostLimit.value))) + val evaluator = new ErgoTreeEvaluator( + context = sigmaCtx, + constants = ErgoTree.EmptyConstants, + coster = accumulator, DefaultProfiler, evalSettings) + + val (res, actualTime) = BenchmarkUtil.measureTimeNano( + evaluator.evalWithCost[B](ErgoTreeEvaluator.EmptyDataEnv, compiledTree)) + (res.value, GivenCost(res.cost, Some(actualTime))) } val Terms.Apply(funcVal, _) = compiledTree.asInstanceOf[SValue] - CompiledFunc(funcScript, bindings.toSeq, funcVal, f) + CompiledFunc(funcScript, bindings, funcVal, compiledTree, f) } protected def roundTripTest[T](v: T)(implicit serializer: SigmaSerializer[T, T]): Assertion = { @@ -242,4 +384,9 @@ trait SigmaTestingCommons extends PropSpec serializer.parse(SigmaSerializer.startReader(randomBytes ++ bytes, randomBytesCount)) shouldBe v } + def testReduce(I: Interpreter)(ctx: I.CTX, prop: SigmaPropValue): SigmaBoolean = { + val ergoTree = ErgoTree.fromProposition(ergoTreeHeaderInTests, prop) + I.fullReduction(ergoTree, ctx).value + } + } diff --git a/sigmastate/src/test/scala/sigmastate/lang/SigmaBinderTest.scala b/sigmastate/src/test/scala/sigmastate/lang/SigmaBinderTest.scala index a283cbb07d..ad8db1ef17 100644 --- a/sigmastate/src/test/scala/sigmastate/lang/SigmaBinderTest.scala +++ b/sigmastate/src/test/scala/sigmastate/lang/SigmaBinderTest.scala @@ -142,7 +142,7 @@ class SigmaBinderTest extends PropSpec with PropertyChecks with Matchers with La If(EQ(IntConstant(10), IntConstant(11)), IntConstant(2), IntConstant(3))) } - // TODO HF (4h): SomeValue and NoneValue are not used in ErgoTree and can be + // TODO v6.0 (4h): SomeValue and NoneValue are not used in ErgoTree and can be // either removed or implemented in v4.x property("Option constructors") { bind(env, "None") shouldBe NoneValue(NoType) diff --git a/sigmastate/src/test/scala/sigmastate/lang/SigmaCompilerTest.scala b/sigmastate/src/test/scala/sigmastate/lang/SigmaCompilerTest.scala index e9f1557ab9..2d16830bb0 100644 --- a/sigmastate/src/test/scala/sigmastate/lang/SigmaCompilerTest.scala +++ b/sigmastate/src/test/scala/sigmastate/lang/SigmaCompilerTest.scala @@ -135,13 +135,14 @@ class SigmaCompilerTest extends SigmaTestingCommons with LangTests with ObjectGe } property("fromBaseX") { + comp(""" fromBase16("31") """) shouldBe ByteArrayConstant(Array[Byte](49)) comp(""" fromBase58("r") """) shouldBe ByteArrayConstant(Array[Byte](49)) comp(""" fromBase64("MQ") """) shouldBe ByteArrayConstant(Array[Byte](49)) comp(""" fromBase64("M" + "Q") """) shouldBe ByteArrayConstant(Array[Byte](49)) } property("deserialize") { - def roundtrip[T <: SType](c: EvaluatedValue[T], typeSig: String) = { + def roundtrip[T <: SType](c: Value[T], typeSig: String) = { val bytes = ValueSerializer.serialize(c) val str = Base58.encode(bytes) comp(env, s"deserialize[$typeSig](" + "\"" + str + "\")") shouldBe c diff --git a/sigmastate/src/test/scala/sigmastate/lang/SigmaParserTest.scala b/sigmastate/src/test/scala/sigmastate/lang/SigmaParserTest.scala index 45e87b3cb8..c6af3d839e 100644 --- a/sigmastate/src/test/scala/sigmastate/lang/SigmaParserTest.scala +++ b/sigmastate/src/test/scala/sigmastate/lang/SigmaParserTest.scala @@ -36,10 +36,7 @@ class SigmaParserTest extends PropSpec with PropertyChecks with Matchers with La } def fail(x: String, expectedLine: Int, expectedCol: Int): Unit = { - val compiler = SigmaCompiler(CompilerSettings( - ErgoAddressEncoder.TestnetNetworkPrefix, - TransformingSigmaBuilder, - lowerMethodCalls = true)) + val compiler = new SigmaCompiler(ErgoAddressEncoder.TestnetNetworkPrefix) val exception = the[ParserException] thrownBy compiler.parse(x) withClue(s"Exception: $exception, is missing source context:") { exception.source shouldBe defined } val sourceContext = exception.source.get @@ -603,6 +600,7 @@ class SigmaParserTest extends PropSpec with PropertyChecks with Matchers with La } property("fromBaseX string decoding") { + parse("""fromBase16("1111")""") shouldBe Apply(FromBase16Func.symNoType, IndexedSeq(StringConstant("1111"))) parse("""fromBase58("111")""") shouldBe Apply(FromBase58Func.symNoType, IndexedSeq(StringConstant("111"))) parse("""fromBase64("111")""") shouldBe Apply(FromBase64Func.symNoType, IndexedSeq(StringConstant("111"))) } diff --git a/sigmastate/src/test/scala/sigmastate/lang/SigmaTyperTest.scala b/sigmastate/src/test/scala/sigmastate/lang/SigmaTyperTest.scala index e29977cd09..fc680bfcf9 100644 --- a/sigmastate/src/test/scala/sigmastate/lang/SigmaTyperTest.scala +++ b/sigmastate/src/test/scala/sigmastate/lang/SigmaTyperTest.scala @@ -110,6 +110,7 @@ class SigmaTyperTest extends PropSpec with PropertyChecks with Matchers with Lan typecheck(env, "min(HEIGHT, INPUTS.size)") shouldBe SInt typecheck(env, "max(1, 2)") shouldBe SInt typecheck(env, "max(1L, 2)") shouldBe SLong + typecheck(env, """fromBase16("1111")""") shouldBe SByteArray typecheck(env, """fromBase58("111")""") shouldBe SByteArray typecheck(env, """fromBase64("111")""") shouldBe SByteArray @@ -652,11 +653,11 @@ class SigmaTyperTest extends PropSpec with PropertyChecks with Matchers with Lan val pk2 = DLogProverInput.random().publicImage val script1 = script(pk1) val script2 = script(pk2) - val inputBytes = ErgoTreeSerializer.DefaultSerializer.serializeErgoTree(script1.treeWithSegregation) + val inputBytes = ErgoTreeSerializer.DefaultSerializer.serializeErgoTree(mkTestErgoTree(script1)) val positions = IntArrayConstant(Array[Int](2)) val newVals = ConcreteCollection(Array[SigmaPropValue](SigmaPropConstant(pk2)), SSigmaProp) - val expectedBytes = ErgoTreeSerializer.DefaultSerializer.serializeErgoTree(script2.treeWithSegregation) + val expectedBytes = ErgoTreeSerializer.DefaultSerializer.serializeErgoTree(mkTestErgoTree(script2)) val customEnv: ScriptEnv = Map( "scriptBytes" -> Colls.fromArray(inputBytes), diff --git a/sigmastate/src/test/scala/sigmastate/serialization/ConstantSerializerSpecification.scala b/sigmastate/src/test/scala/sigmastate/serialization/ConstantSerializerSpecification.scala index db7a78324b..5156028b81 100644 --- a/sigmastate/src/test/scala/sigmastate/serialization/ConstantSerializerSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/serialization/ConstantSerializerSpecification.scala @@ -61,6 +61,7 @@ class ConstantSerializerSpecification extends TableSerializationSpecification { } property("Constant serialization round trip") { + forAll { x: Unit => roundTripTest(UnitConstant()) } forAll { x: Byte => roundTripTest(Constant[SByte.type](x, SByte)) } forAll { x: Boolean => roundTripTest(BooleanConstant.fromBoolean(x)) } forAll { x: Long => roundTripTest(Constant[SLong.type](x, SLong)) } diff --git a/sigmastate/src/test/scala/sigmastate/serialization/DataSerializerSpecification.scala b/sigmastate/src/test/scala/sigmastate/serialization/DataSerializerSpecification.scala index 68a4ebef38..311e957dfd 100644 --- a/sigmastate/src/test/scala/sigmastate/serialization/DataSerializerSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/serialization/DataSerializerSpecification.scala @@ -6,7 +6,7 @@ import org.ergoplatform.ErgoBox import org.scalacheck.Arbitrary._ import scalan.RType import sigmastate.SCollection.SByteArray -import sigmastate.Values.SigmaBoolean +import sigmastate.Values.{SigmaBoolean, ErgoTree} import sigmastate._ import sigmastate.eval.Evaluation import sigmastate.eval._ @@ -14,6 +14,9 @@ import sigmastate.eval.Extensions._ import sigmastate.interpreter.CryptoConstants.EcPointType import special.sigma.AvlTree import SType.AnyOps +import org.ergoplatform.SigmaConstants.ScriptCostLimit +import sigmastate.interpreter.{CostAccumulator, ErgoTreeEvaluator} +import sigmastate.interpreter.ErgoTreeEvaluator.DefaultProfiler import sigmastate.lang.exceptions.SerializerException import sigmastate.utils.Helpers @@ -23,9 +26,20 @@ class DataSerializerSpecification extends SerializationSpecification { val w = SigmaSerializer.startWriter() DataSerializer.serialize(obj, tpe, w) val bytes = w.toBytes - val r = SigmaSerializer.startReader(bytes, 0) + val r = SigmaSerializer.startReader(bytes) val res = DataSerializer.deserialize(tpe, r) res shouldBe obj + + val accumulator = new CostAccumulator( + initialCost = JitCost(0), + costLimit = Some(JitCost.fromBlockCost(ScriptCostLimit.value))) + val evaluator = new ErgoTreeEvaluator( + context = null, + constants = ErgoTree.EmptyConstants, + coster = accumulator, DefaultProfiler, ErgoTreeEvaluator.DefaultEvalSettings) + val ok = DataValueComparer.equalDataValues(res, obj)(evaluator) + ok shouldBe true + val randomPrefix = arrayGen[Byte].sample.get val r2 = SigmaSerializer.startReader(randomPrefix ++ bytes, randomPrefix.length) val res2 = DataSerializer.deserialize(tpe, r2) diff --git a/sigmastate/src/test/scala/sigmastate/serialization/DeserializationResilience.scala b/sigmastate/src/test/scala/sigmastate/serialization/DeserializationResilience.scala index a368f579b0..9ae7d01ab8 100644 --- a/sigmastate/src/test/scala/sigmastate/serialization/DeserializationResilience.scala +++ b/sigmastate/src/test/scala/sigmastate/serialization/DeserializationResilience.scala @@ -1,21 +1,19 @@ package sigmastate.serialization import java.nio.ByteBuffer - import org.ergoplatform.validation.ValidationException +import org.ergoplatform.validation.ValidationRules.CheckPositionLimit import org.ergoplatform.{ErgoBoxCandidate, Outputs} import org.scalacheck.Gen import scalan.util.BenchmarkUtil import scorex.util.serialization.{Reader, VLQByteBufferReader} -import sigmastate.Values.{BlockValue, ErgoTree, GetVarInt, IntConstant, SValue, SigmaBoolean, SigmaPropValue, Tuple, ValDef, ValUse} +import sigmastate.Values.{BlockValue, GetVarInt, IntConstant, SValue, SigmaBoolean, SigmaPropValue, Tuple, ValDef, ValUse} import sigmastate._ import sigmastate.eval.Extensions._ import sigmastate.eval._ import sigmastate.helpers.{ErgoLikeContextTesting, ErgoLikeTestInterpreter, SigmaTestingCommons} -import sigmastate.interpreter.Interpreter.{ScriptNameProp, emptyEnv} -import sigmastate.interpreter.{ContextExtension, CryptoConstants, CostedProverResult} -import sigmastate.lang.Terms._ -import sigmastate.lang.exceptions.{DeserializeCallDepthExceeded, InputSizeLimitExceeded, InvalidTypePrefix, SerializerException} +import sigmastate.interpreter.{ContextExtension, CostedProverResult, CryptoConstants} +import sigmastate.lang.exceptions.{DeserializeCallDepthExceeded, InvalidTypePrefix, ReaderPositionLimitExceeded, SerializerException} import sigmastate.serialization.OpCodes._ import sigmastate.utils.SigmaByteReader import sigmastate.utxo.SizeOf @@ -32,6 +30,7 @@ class DeserializationResilience extends SerializationSpecification // override val okPrintEvaluatedEntries = true } + /** Helper method which passes test-specific maxTreeDepth. */ private def reader(bytes: Array[Byte], maxTreeDepth: Int): SigmaByteReader = { val buf = ByteBuffer.wrap(bytes) val r = new SigmaByteReader( @@ -47,24 +46,48 @@ class DeserializationResilience extends SerializationSpecification } property("exceeding ergo box propositionBytes max size check") { - val oversizedTree = new SigmaAnd( + val oversizedTree = mkTestErgoTree(SigmaAnd( Gen.listOfN(SigmaSerializer.MaxPropositionSize / CryptoConstants.groupSize, - proveDlogGen.map(_.toSigmaProp)).sample.get).treeWithSegregation + proveDlogGen.map(_.toSigmaProp)).sample.get)) val b = new ErgoBoxCandidate(1L, oversizedTree, 1) val w = SigmaSerializer.startWriter() ErgoBoxCandidate.serializer.serialize(b, w) - assertExceptionThrown({ - ErgoBoxCandidate.serializer.parse(SigmaSerializer.startReader(w.toBytes)) - }, { - case e: SerializerException => rootCause(e).isInstanceOf[InputSizeLimitExceeded] - case _ => false - }) + oversizedTree.version match { + case 0 => + // for ErgoTree v0 there is no sizeBit in the header, the + // ErgoTreeSerializer.deserializeErgoTree cannot handle ValidationException and + // create ErgoTree with UnparsedErgoTree data. + // A new SerializerException is thus created and the original exception attached + // as the cause. + assertExceptionThrown( + ErgoBoxCandidate.serializer.parse(SigmaSerializer.startReader(w.toBytes)), + { + case SerializerException(_, _, + Some(ValidationException(_,CheckPositionLimit,_, + Some(_: ReaderPositionLimitExceeded)))) => true + case _ => false + }) + case _ => + // for ErgoTree v1 and above, the sizeBit is required in the header, so + // any ValidationException can be caught and wrapped in an ErgoTree with + // UnparsedErgoTree data. + + // This is what happens here, but, since the box exceeds the limit, the next + // ValidationException is thrown on the next read operation in + // ErgoBoxCandidate.serializer + assertExceptionThrown( + ErgoBoxCandidate.serializer.parse(SigmaSerializer.startReader(w.toBytes)), + { + case ValidationException(_,CheckPositionLimit,_,Some(_: ReaderPositionLimitExceeded)) => true + case _ => false + }) + } } property("ergo box propositionBytes max size check") { - val bigTree = new SigmaAnd( + val bigTree = mkTestErgoTree(SigmaAnd( Gen.listOfN((SigmaSerializer.MaxPropositionSize / 2) / CryptoConstants.groupSize, - proveDlogGen.map(_.toSigmaProp)).sample.get).treeWithSegregation + proveDlogGen.map(_.toSigmaProp)).sample.get)) val b = new ErgoBoxCandidate(1L, bigTree, 1) val w = SigmaSerializer.startWriter() ErgoBoxCandidate.serializer.serialize(b, w) @@ -233,23 +256,28 @@ class DeserializationResilience extends SerializationSpecification } property("exceed ergo box max size check") { - val bigTree = new SigmaAnd( + val bigTree = mkTestErgoTree(SigmaAnd( Gen.listOfN((SigmaSerializer.MaxPropositionSize / 2) / CryptoConstants.groupSize, - proveDlogGen.map(_.toSigmaProp)).sample.get).treeWithSegregation + proveDlogGen.map(_.toSigmaProp)).sample.get)) val tokens = additionalTokensGen(127).sample.get.map(_.sample.get).toColl val b = new ErgoBoxCandidate(1L, bigTree, 1, tokens) val w = SigmaSerializer.startWriter() ErgoBoxCandidate.serializer.serialize(b, w) val bytes = w.toBytes - an[InputSizeLimitExceeded] should be thrownBy - ErgoBoxCandidate.serializer.parse(SigmaSerializer.startReader(bytes)) + assertExceptionThrown( + ErgoBoxCandidate.serializer.parse(SigmaSerializer.startReader(bytes)), + { + case ValidationException(_, CheckPositionLimit, _, Some(_: ReaderPositionLimitExceeded)) => true + case _ => false + } + ) } private val recursiveScript: SigmaPropValue = BlockValue( Vector( ValDef(1, Plus(GetVarInt(4).get, ValUse(2, SInt))), ValDef(2, Plus(GetVarInt(5).get, ValUse(1, SInt)))), - GE(Minus(ValUse(1, SInt), ValUse(2, SInt)), 0)).asBoolValue.toSigmaProp + GE(Minus(ValUse(1, SInt), ValUse(2, SInt)), 0)).toSigmaProp property("recursion caught during deserialization") { assertExceptionThrown({ @@ -264,16 +292,17 @@ class DeserializationResilience extends SerializationSpecification property("recursion caught during verify") { assertExceptionThrown({ val verifier = new ErgoLikeTestInterpreter - val pr = CostedProverResult(Array[Byte](), ContextExtension(Map()), 0L) + val pr = CostedProverResult(Array[Byte](), + ContextExtension(Map(4.toByte -> IntConstant(1), 5.toByte -> IntConstant(2))), 0L) val ctx = ErgoLikeContextTesting.dummy(fakeSelf, activatedVersionInTests) - val (res, calcTime) = BenchmarkUtil.measureTime { - verifier.verify(emptyEnv + (ScriptNameProp -> "verify"), - ErgoTree(ErgoTree.DefaultHeader, IndexedSeq(), recursiveScript), ctx, pr, fakeMessage) + val (res, _) = BenchmarkUtil.measureTime { + verifier.verify(mkTestErgoTree(recursiveScript), ctx, pr, fakeMessage) } res.getOrThrow }, { case e: NoSuchElementException => - // this is expected because of deserialization is forced when ErgoTree.complexity is accessed in verify + // in v4.x this is expected because of deserialization is forced when ErgoTree.complexity is accessed in verify + // in v5.0 this is expected because ValUse(2, SInt) will not be resolved in env: DataEnv e.getMessage.contains("key not found: 2") case _ => false }) diff --git a/sigmastate/src/test/scala/sigmastate/serialization/ErgoTreeSerializerSpecification.scala b/sigmastate/src/test/scala/sigmastate/serialization/ErgoTreeSerializerSpecification.scala index 2d51e541c7..5815975b3d 100644 --- a/sigmastate/src/test/scala/sigmastate/serialization/ErgoTreeSerializerSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/serialization/ErgoTreeSerializerSpecification.scala @@ -1,75 +1,101 @@ package sigmastate.serialization import java.math.BigInteger - import org.ergoplatform.ErgoBox -import sigmastate.Values.{ShortConstant, BigIntConstant, ConstantPlaceholder, Value, SigmaPropValue, IntConstant, ErgoTree, ByteConstant} +import org.ergoplatform.validation.ValidationException +import org.ergoplatform.validation.ValidationRules.CheckDeserializedScriptIsSigmaProp +import sigmastate.Values.{BigIntConstant, ByteConstant, ConstantPlaceholder, ErgoTree, IntConstant, ShortConstant, SigmaPropValue, UnparsedErgoTree} import sigmastate._ -import sigmastate.eval.{IRContext, CBigInt} +import sigmastate.eval.{CBigInt, IRContext} import sigmastate.helpers.SigmaTestingCommons -import sigmastate.lang.exceptions.{SerializerException, InputSizeLimitExceeded} +import sigmastate.lang.exceptions.{ReaderPositionLimitExceeded, SerializerException} import sigmastate.serialization.ErgoTreeSerializer.DefaultSerializer import sigmastate.utxo.{DeserializeContext, DeserializeRegister} class ErgoTreeSerializerSpecification extends SerializationSpecification - with SigmaTestingCommons { + with SigmaTestingCommons with CrossVersionProps { implicit lazy val IR: TestingIRContext = new TestingIRContext { beginPass(noConstPropagationPass) } - private def extractConstants(tree: SigmaPropValue)(implicit IR: IRContext): ErgoTree = { + private def extractConstants(prop: SigmaPropValue)(implicit IR: IRContext): Seq[ErgoTree] = { import ErgoTree._ val env = Map[String, Any]() - val IR.Pair(calcF, _) = IR.doCosting(env, tree) - val extractConstants = new ConstantStore() - val outTree = IR.buildTree(calcF, Some(extractConstants)) - val constants = extractConstants.getAll - val header = if (constants.isEmpty) DefaultHeader else ConstantSegregationHeader - val ergoTree = ErgoTree(header, constants, outTree) - ergoTree + val IR.Pair(calcF, _) = IR.doCosting(env, prop) + val constantsStore = new ConstantStore() + val outExpr = IR.buildTree(calcF, Some(constantsStore)) + val constants = constantsStore.getAll + val trees = if (constants.isEmpty) { + Seq(ErgoTree(ergoTreeHeaderInTests, constants, outExpr)) + } else { + Seq( + ErgoTree((ConstantSegregationHeader | ergoTreeHeaderInTests).toByte, constants, outExpr), + ErgoTree(ergoTreeHeaderInTests, EmptyConstants, prop) + ) + } + trees } property("(de)serialization round trip using treeBytes()") { - val trees = Seq( + val exprs = Seq( EQ(Plus(10.toByte, 20.toByte), ByteConstant(30)).toSigmaProp, EQ(Plus(10.toShort, 20.toShort), ShortConstant(30)).toSigmaProp, EQ(Plus(10, 20), IntConstant(30)).toSigmaProp, EQ(Plus(CBigInt(BigInteger.valueOf(10L)), BigIntConstant(20L)), BigIntConstant(30L)).toSigmaProp ) - trees.foreach { tree => - val ergoTree = extractConstants(tree) - val bytes = DefaultSerializer.serializeErgoTree(ergoTree) - val (_, _, deserializedConstants, treeBytes) = DefaultSerializer - .deserializeHeaderWithTreeBytes(SigmaSerializer.startReader(bytes)) - deserializedConstants shouldEqual ergoTree.constants - val r = SigmaSerializer.startReader( - treeBytes, - new ConstantStore(deserializedConstants), - resolvePlaceholdersToConstants = true) - val deserializedTree = ValueSerializer.deserialize(r) - deserializedTree shouldEqual tree + exprs.foreach { expr => + extractConstants(expr).foreach { ergoTree => + val bytes = DefaultSerializer.serializeErgoTree(ergoTree) + val (_, _, deserializedConstants, treeBytes) = DefaultSerializer + .deserializeHeaderWithTreeBytes(SigmaSerializer.startReader(bytes)) + deserializedConstants shouldEqual ergoTree.constants + val r = SigmaSerializer.startReader( + treeBytes, + new ConstantStore(deserializedConstants), + resolvePlaceholdersToConstants = true) + val deserializedTree = ValueSerializer.deserialize(r) + deserializedTree shouldEqual expr + } } } property("Constant extraction via compiler pass: (de)serialization round trip") { val prop = EQ(Plus(10, 20), IntConstant(30)).toSigmaProp - val ergoTree = extractConstants(prop) - val bytes = DefaultSerializer.serializeErgoTree(ergoTree) - val deserializedTree = DefaultSerializer.deserializeErgoTree(bytes) - deserializedTree shouldEqual ergoTree + extractConstants(prop).foreach { ergoTree => + val bytes = DefaultSerializer.serializeErgoTree(ergoTree) + val deserializedTree = DefaultSerializer.deserializeErgoTree(bytes) + deserializedTree shouldEqual ergoTree + } } property("failed type check on tree deserialization") { forAll(numExprTreeNodeGen) { numProp => - val bytes = DefaultSerializer.serializeErgoTree(extractConstants(numProp.asInstanceOf[SigmaPropValue])) - an[SerializerException] should be thrownBy DefaultSerializer.deserializeErgoTree(bytes) - an[SerializerException] should be thrownBy DefaultSerializer.deserializeErgoTree(bytes) + val prop = numProp.asInstanceOf[SigmaPropValue] // this typecast doesn't check the actual type + extractConstants(prop).foreach { ergoTree => + val bytes = DefaultSerializer.serializeErgoTree(ergoTree) + + if (ergoTreeVersionInTests == 0) { + assertExceptionThrown( + DefaultSerializer.deserializeErgoTree(bytes), + rootCauseLike[SerializerException]("Failed deserialization, expected deserialized script to have type SigmaProp;")) + } else { + val tree = DefaultSerializer.deserializeErgoTree(bytes) + tree.root match { + case Left(UnparsedErgoTree(unparsedBytes, + ValidationException(_, CheckDeserializedScriptIsSigmaProp, _, Some(cause)))) => + unparsedBytes shouldBe bytes + rootCauseLike[SerializerException]( + "Failed deserialization, expected deserialized script to have type SigmaProp;") + .apply(cause) shouldBe true + } + } + } } } property("Constant extraction during serialization: (de)serialization round trip") { - val tree = EQ(Plus(10, 20), IntConstant(30)).toSigmaProp.treeWithSegregation + val tree = mkTestErgoTree(EQ(Plus(10, 20), IntConstant(30)).toSigmaProp) val bytes = DefaultSerializer.serializeErgoTree(tree) val (_, _, deserializedConstants, _) = DefaultSerializer. deserializeHeaderWithTreeBytes(SigmaSerializer.startReader(bytes)) @@ -79,8 +105,8 @@ class ErgoTreeSerializerSpecification extends SerializationSpecification } property("tree with placeholders bytes should be equal if only constants are different") { - val tree1 = EQ(Plus(10, 20), IntConstant(30)).toSigmaProp.treeWithSegregation - val tree2 = EQ(Plus(30, 40), IntConstant(70)).toSigmaProp.treeWithSegregation + val tree1 = mkTestErgoTree(EQ(Plus(10, 20), IntConstant(30)).toSigmaProp) + val tree2 = mkTestErgoTree(EQ(Plus(30, 40), IntConstant(70)).toSigmaProp) val bytes1 = DefaultSerializer.serializeErgoTree(tree1) val bytes2 = DefaultSerializer.serializeErgoTree(tree2) val (_, _, _, treeBytes1) = DefaultSerializer @@ -101,17 +127,25 @@ class ErgoTreeSerializerSpecification extends SerializationSpecification } property("max ergo tree byte size check") { - val tree = EQ(Plus(10, 20), IntConstant(30)).toSigmaProp.treeWithSegregation + val tree = mkTestErgoTree(EQ(Plus(10, 20), IntConstant(30)).toSigmaProp) val r = SigmaSerializer.startReader(DefaultSerializer.serializeErgoTree(tree)) - assertExceptionThrown({ - DefaultSerializer.deserializeErgoTree(r, 1) - }, { - case e: SerializerException => rootCause(e).isInstanceOf[InputSizeLimitExceeded] - }) + if (ergoTreeVersionInTests == 0) { + assertExceptionThrown({ + DefaultSerializer.deserializeErgoTree(r, 1) + }, { + case e: SerializerException => rootCause(e).isInstanceOf[ReaderPositionLimitExceeded] + }) + } else { + val tree = DefaultSerializer.deserializeErgoTree(r, 1) + tree.root match { + case Left(UnparsedErgoTree(_, ve: ValidationException)) => + rootCauseLike[ReaderPositionLimitExceeded]().apply(ve.cause.get) shouldBe true + } + } } property("restore reader's positionLimit") { - val tree = EQ(Plus(10, 20), IntConstant(30)).toSigmaProp.treeWithSegregation + val tree = mkTestErgoTree(EQ(Plus(10, 20), IntConstant(30)).toSigmaProp) val r = SigmaSerializer.startReader(DefaultSerializer.serializeErgoTree(tree)) r.positionLimit = 1 DefaultSerializer.deserializeErgoTree(r, SigmaSerializer.MaxPropositionSize) shouldEqual tree @@ -145,4 +179,28 @@ class ErgoTreeSerializerSpecification extends SerializationSpecification parsedTree.hasDeserialize shouldBe hasDeserialize } } + + property("getPositionsBackref") { + def test(positions: Array[Int], expected: Array[Int]) = { + val backrefs = ErgoTreeSerializer.DefaultSerializer.getPositionsBackref(positions, expected.length) + backrefs shouldBe expected + } + + test(positions = Array(), expected = Array()) // no positions, no constants + test(positions = Array(), expected = Array(-1)) // no positions, 1 constant + test(positions = Array(0), expected = Array()) // 1 position, no constants + test(positions = Array(1), expected = Array(-1)) // 1 position, but out of range + test(positions = Array(0), expected = Array(0)) // 1 position, 1 constant + test(positions = Array(-1), expected = Array()) // 1 invalid (out of range) position, no constants + test(positions = Array(-2), expected = Array(-1)) // 1 invalid position, 1 constants + + test(positions = Array(0, 0), expected = Array(0)) // duplicate positions, 1 constant + test(positions = Array(-1, 0), expected = Array(1)) // invalid positions ignored + test(positions = Array(-1, 0, 0), expected = Array(1)) // only first of the duplicates used + + test(positions = Array(), expected = Array(-1, -1, -1, -1, -1)) // no positions => no backrefs + + test(positions = Array(1, 2), expected = Array(-1, 0, 1, -1, -1)) + test(positions = Array(1, 2, 4), expected = Array(-1, 0, 1, -1, 2)) + } } diff --git a/sigmastate/src/test/scala/sigmastate/serialization/SigSerializerSpecification.scala b/sigmastate/src/test/scala/sigmastate/serialization/SigSerializerSpecification.scala index 97c94c73a3..0c02f795c4 100644 --- a/sigmastate/src/test/scala/sigmastate/serialization/SigSerializerSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/serialization/SigSerializerSpecification.scala @@ -64,7 +64,7 @@ class SigSerializerSpecification extends SigmaTestingCommons private def roundTrip(uncheckedTree: UncheckedTree, exp: SigmaBoolean): Assertion = { val proof = SigSerializer.toProofBytes(uncheckedTree) - val parsedUncheckedTree = SigSerializer.parseAndComputeChallenges(exp, proof) + val parsedUncheckedTree = SigSerializer.parseAndComputeChallenges(exp, proof)(null) isEquivalent(uncheckedTree, parsedUncheckedTree) shouldBe true } @@ -89,10 +89,10 @@ class SigSerializerSpecification extends SigmaTestingCommons try { // get sigma conjectures out of transformers val tree = mkTestErgoTree(expr) - val prop = prover.fullReduction(tree, ctx, Interpreter.emptyEnv).value + val prop = prover.fullReduction(tree, ctx).value val proof = prover.prove(tree, ctx, challenge).get.proof - val uncheckedTree = SigSerializer.parseAndComputeChallenges(prop, proof) + val uncheckedTree = SigSerializer.parseAndComputeChallenges(prop, proof)(null) roundTrip(uncheckedTree, prop) // uncomment the following lines to print test cases with test vectors @@ -128,7 +128,7 @@ class SigSerializerSpecification extends SigmaTestingCommons * that code. */ def getFiatShamirHex(uncheckedTree: UncheckedTree): String = { val newRoot = prover.computeCommitments(uncheckedTree).get.asInstanceOf[UncheckedSigmaTree] - val fiatShamirBytes = FiatShamirTree.toBytes(newRoot) + val fiatShamirBytes = FiatShamirTree.toBytes(newRoot)(null/* no profiling */) val hex = ErgoAlgos.encode(fiatShamirBytes) hex } @@ -496,7 +496,7 @@ class SigSerializerSpecification extends SigmaTestingCommons cases.zipWithIndex.foreach { case (c, iCase) => val sigBytes = SigSerializer.toProofBytes(c.uncheckedTree) sigBytes shouldBe c.proof - val uncheckedTree = SigSerializer.parseAndComputeChallenges(c.prop, c.proof) + val uncheckedTree = SigSerializer.parseAndComputeChallenges(c.prop, c.proof)(null) uncheckedTree shouldBe c.uncheckedTree val hex = getFiatShamirHex(c.uncheckedTree) diff --git a/sigmastate/src/test/scala/sigmastate/serialization/SubstConstantsSerializerSpecification.scala b/sigmastate/src/test/scala/sigmastate/serialization/SubstConstantsSerializerSpecification.scala index 43d90f55ee..da33a5f85c 100644 --- a/sigmastate/src/test/scala/sigmastate/serialization/SubstConstantsSerializerSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/serialization/SubstConstantsSerializerSpecification.scala @@ -2,13 +2,14 @@ package sigmastate.serialization import sigmastate.Values.{ConcreteCollection, IntArrayConstant, IntConstant, IntValue} import sigmastate.serialization.ErgoTreeSerializer.DefaultSerializer -import sigmastate.{EQ, SInt, SubstConstants} +import sigmastate.{CrossVersionProps, EQ, SInt, SubstConstants} -class SubstConstantsSerializerSpecification extends SerializationSpecification { +class SubstConstantsSerializerSpecification extends SerializationSpecification + with CrossVersionProps { property("SubstConstant deserialization round trip") { forAll(numExprTreeNodeGen) { prop => - val tree = EQ(prop, IntConstant(1)).toSigmaProp.treeWithSegregation + val tree = mkTestErgoTree(EQ(prop, IntConstant(1)).toSigmaProp) val bytes = DefaultSerializer.serializeErgoTree(tree) val newVals = ConcreteCollection(Array[IntValue](1), SInt) val expr = SubstConstants(bytes, IntArrayConstant(Array(0)), newVals) diff --git a/sigmastate/src/test/scala/sigmastate/serialization/TypeSerializerSpecification.scala b/sigmastate/src/test/scala/sigmastate/serialization/TypeSerializerSpecification.scala index ed4cda54ce..6521ec55f7 100644 --- a/sigmastate/src/test/scala/sigmastate/serialization/TypeSerializerSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/serialization/TypeSerializerSpecification.scala @@ -70,6 +70,8 @@ class TypeSerializerSpecification extends SerializationSpecification { } property("Specific types serialization roundtrip") { + roundtrip(SAny, Array[Byte](SAny.typeCode)) + roundtrip(SGlobal, Array[Byte](SGlobal.typeCode)) roundtrip(STuple(SCollection(SLong), SCollection(SLong)), Array[Byte](Pair1TypeCode, SLong.embedIn(CollectionTypeCode), SLong.embedIn(CollectionTypeCode))) roundtrip(STuple(SCollection(SLong), SOption(SLong)), diff --git a/sigmastate/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala b/sigmastate/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala index 673a7500dd..e716627aca 100644 --- a/sigmastate/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala +++ b/sigmastate/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala @@ -11,15 +11,15 @@ import org.scalacheck.{Arbitrary, Gen} import scalan.RType import scorex.crypto.authds.{ADDigest, ADKey} import scorex.crypto.hash.Digest32 -import scorex.util.encode.{Base64, Base58} -import scorex.util.{bytesToId, ModifierId} +import scorex.util.encode.{Base58, Base64} +import scorex.util.{ModifierId, Random, bytesToId} import sigmastate.Values._ import sigmastate.basics.DLogProtocol.ProveDlog import sigmastate.basics.ProveDHTuple import sigmastate.eval.Extensions._ import sigmastate.eval.{CostingBox, SigmaDsl, _} import sigmastate.interpreter.CryptoConstants.EcPointType -import sigmastate.interpreter.{ProverResult, ContextExtension, CryptoConstants, Interpreter} +import sigmastate.interpreter.{ContextExtension, CryptoConstants, Interpreter, ProverResult} import sigmastate.lang.TransformingSigmaBuilder._ import sigmastate._ import sigmastate.utxo._ @@ -175,7 +175,7 @@ trait ObjectGenerators extends TypeGenerators Gen.oneOf(booleanConstGen.asInstanceOf[Gen[EvaluatedValue[SType]]], byteArrayConstGen, longConstGen) def additionalRegistersGen(cnt: Byte): Seq[Gen[(NonMandatoryRegisterId, EvaluatedValue[SType])]] = { - util.Random.shuffle((0 until cnt).toList) + scala.util.Random.shuffle((0 until cnt).toList) .map(_ + ErgoBox.startingNonMandatoryIndex) .map(rI => ErgoBox.registerByIndex(rI).asInstanceOf[NonMandatoryRegisterId]) .map { r => @@ -617,7 +617,7 @@ trait ObjectGenerators extends TypeGenerators } yield ValUse(id, tpe) val blockValueGen: Gen[BlockValue] = for { - items <- Gen.nonEmptyListOf(valDefGen) + items <- Gen.listOf(valDefGen) } yield BlockValue(items.toIndexedSeq, EQ( SizeOf(Tuple(items.toIndexedSeq.map(valDef => ValUse(valDef.id, valDef.tpe)))), @@ -791,6 +791,6 @@ trait ObjectGenerators extends TypeGenerators costLimit = costLimit, initCost = initCost, activatedScriptVersion = activatedVersionInTests - ) + ).withErgoTreeVersion(ergoTreeVersionInTests) } diff --git a/sigmastate/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala b/sigmastate/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala index 9baade2639..b0709ba06c 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala @@ -1,8 +1,7 @@ package sigmastate.utxo import java.math.BigInteger - -import org.ergoplatform.ErgoBox.{R6, R8} +import org.ergoplatform.ErgoBox.{AdditionalRegisters, R6, R8} import org.ergoplatform._ import scalan.RType import sigmastate.SCollection.SByteArray @@ -16,8 +15,12 @@ import sigmastate.lang.Terms._ import special.sigma.InvalidType import SType.AnyOps import sigmastate.interpreter.ContextExtension.VarBinding -import sigmastate.interpreter.CryptoConstants +import sigmastate.interpreter.ErgoTreeEvaluator.DefaultEvalSettings +import sigmastate.interpreter.{CryptoConstants, EvalSettings} import sigmastate.utils.Helpers._ +import scalan.util.StringUtil._ +import sigmastate.basics.DLogProtocol.DLogProverInput +import sigmastate.lang.exceptions.CosterException class BasicOpsSpecification extends SigmaTestingCommons with CrossVersionProps { @@ -57,25 +60,41 @@ class BasicOpsSpecification extends SigmaTestingCommons def test(name: String, env: ScriptEnv, ext: Seq[VarBinding], - script: String, propExp: SValue, - onlyPositive: Boolean = true) = { + script: String, + propExp: SValue, + onlyPositive: Boolean = true, + testExceededCost: Boolean = true, + additionalRegistersOpt: Option[AdditionalRegisters] = None) = { val prover = new ContextEnrichingTestProvingInterpreter() { override lazy val contextExtenders: Map[Byte, EvaluatedValue[_ <: SType]] = { val p1 = dlogSecrets(0).publicImage val p2 = dlogSecrets(1).publicImage (ext ++ Seq(propVar1 -> SigmaPropConstant(p1), propVar2 -> SigmaPropConstant(p2))).toMap } + override val evalSettings: EvalSettings = DefaultEvalSettings.copy( + isMeasureOperationTime = true, + isDebug = true, + isTestRun = testExceededCost) } - val prop = compile(env, script).asBoolValue.toSigmaProp + val prop = if (script.isNullOrEmpty) { + // for some testcases the script cannot be compiled (i.e. the corresponding syntax + // is not supported by ErgoScript Compiler) + // In such cases we use expected property as the property to test + propExp.asSigmaProp + } else + compile(env, script).asBoolValue.toSigmaProp + if (propExp != null) prop shouldBe propExp val tree = ErgoTree.fromProposition(ergoTreeHeaderInTests, prop) val p3 = prover.dlogSecrets(2).publicImage - val boxToSpend = testBox(10, tree, additionalRegisters = Map( - reg1 -> SigmaPropConstant(p3), - reg2 -> IntConstant(1)), + val boxToSpend = testBox(10, tree, + additionalRegisters = additionalRegistersOpt.getOrElse(Map( + reg1 -> SigmaPropConstant(p3), + reg2 -> IntConstant(1)) + ), creationHeight = 5) val newBox1 = testBox(10, tree, creationHeight = 0, boxIndex = 0, additionalRegisters = Map( @@ -91,10 +110,79 @@ class BasicOpsSpecification extends SigmaTestingCommons val ctxExt = ctx.withExtension(pr.extension) - val verifier = new ErgoLikeTestInterpreter - if (!onlyPositive) - verifier.verify(env + (ScriptNameProp -> s"${name}_verify"), tree, ctx, pr.proof, fakeMessage).map(_._1).getOrElse(false) shouldBe false //context w/out extensions - verifier.verify(env + (ScriptNameProp -> s"${name}_verify_ext"), tree, ctxExt, pr.proof, fakeMessage).get._1 shouldBe true + val testVerifier = new ErgoLikeTestInterpreter + + if (!onlyPositive) { + // test negative case + testVerifier.verify( + env + (ScriptNameProp -> s"${name}_verify"), + tree, ctx, pr.proof, fakeMessage) + .map(_._1) + .getOrElse(false) shouldBe false //context w/out extensions + } + + // this is helper verifier which respects the requested parameter testExceededCost for + // some test cases (when testExceededCost == false) it emit message in the console + // instead of failing the test and the failing case is tested separately in that case + val flexVerifier = new ErgoLikeTestInterpreter { + override val evalSettings: EvalSettings = DefaultEvalSettings.copy( + isMeasureOperationTime = true, + isDebug = true, + isTestRun = testExceededCost) + } + val verifyEnv = env + (ScriptNameProp -> s"${name}_verify_ext") + flexVerifier.verify(verifyEnv, tree, ctxExt, pr.proof, fakeMessage).get._1 shouldBe true + } + + property("Unit register") { + VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + if (VersionContext.current.isJitActivated) { + + // TODO frontend: implement missing Unit support in compiler + // https://github.com/ScorexFoundation/sigmastate-interpreter/issues/820 + test("R1", env, ext, + script = "", /* means cannot be compiled + the corresponding script is { SELF.R4[Unit].isDefined } */ + ExtractRegisterAs[SUnit.type](Self, reg1)(SUnit).isDefined.toSigmaProp, + additionalRegistersOpt = Some(Map( + reg1 -> UnitConstant.instance + )) + ) + + test("R2", env, ext, + script = "", /* means cannot be compiled + the corresponding script is "{ SELF.R4[Unit].get == () }" */ + EQ(ExtractRegisterAs[SUnit.type](Self, reg1)(SUnit).get, UnitConstant.instance).toSigmaProp, + additionalRegistersOpt = Some(Map( + reg1 -> UnitConstant.instance + )) + ) + } else { + assertExceptionThrown( + test("R1", env, ext, + "{ SELF.R4[SigmaProp].get }", + ExtractRegisterAs[SSigmaProp.type](Self, reg1).get, + additionalRegistersOpt = Some(Map( + reg1 -> SigmaPropConstant(DLogProverInput.random().publicImage), + reg2 -> UnitConstant.instance + )) + ), + rootCauseLike[RuntimeException]("Don't know how to compute Sized for type PrimitiveType(Unit,") + ) + assertExceptionThrown( + test("R2", env, ext, + "", /* the test script "{ SELF.R4[Unit].isDefined }" cannot be compiled with SigmaCompiler, + but we nevertheless want to test how interpreter process the tree, + so we use the explicitly given tree below */ + ExtractRegisterAs[SUnit.type](Self, reg1)(SUnit).isDefined.toSigmaProp, + additionalRegistersOpt = Some(Map( + reg1 -> UnitConstant.instance + )) + ), + rootCauseLike[CosterException]("Don't know how to convert SType SUnit to Elem") + ) + } + } } property("Relation operations") { @@ -147,23 +235,28 @@ class BasicOpsSpecification extends SigmaTestingCommons property("SigmaProp operations") { test("Prop1", env, ext, "{ getVar[SigmaProp](proofVar1).get.isProven }", - GetVarSigmaProp(propVar1).get + GetVarSigmaProp(propVar1).get, + testExceededCost = false ) test("Prop2", env, ext, "{ getVar[SigmaProp](proofVar1).get || getVar[SigmaProp](proofVar2).get }", - SigmaOr(Seq(GetVarSigmaProp(propVar1).get, GetVarSigmaProp(propVar2).get)) + SigmaOr(Seq(GetVarSigmaProp(propVar1).get, GetVarSigmaProp(propVar2).get)), + testExceededCost = false ) test("Prop3", env, ext, "{ getVar[SigmaProp](proofVar1).get && getVar[SigmaProp](proofVar2).get }", - SigmaAnd(Seq(GetVarSigmaProp(propVar1).get, GetVarSigmaProp(propVar2).get)) + SigmaAnd(Seq(GetVarSigmaProp(propVar1).get, GetVarSigmaProp(propVar2).get)), + testExceededCost = false ) test("Prop4", env, ext, "{ getVar[SigmaProp](proofVar1).get.isProven && getVar[SigmaProp](proofVar2).get }", - SigmaAnd(Seq(GetVarSigmaProp(propVar1).get, GetVarSigmaProp(propVar2).get)) + SigmaAnd(Seq(GetVarSigmaProp(propVar1).get, GetVarSigmaProp(propVar2).get)), + testExceededCost = false ) test("Prop5", env, ext, "{ getVar[SigmaProp](proofVar1).get && getVar[Int](intVar1).get == 1 }", - SigmaAnd(Seq(GetVarSigmaProp(propVar1).get, BoolToSigmaProp(EQ(GetVarInt(intVar1).get, 1)))) + SigmaAnd(Seq(GetVarSigmaProp(propVar1).get, BoolToSigmaProp(EQ(GetVarInt(intVar1).get, 1)))), + testExceededCost = false ) test("Prop6", env, ext, "{ getVar[Int](intVar1).get == 1 || getVar[SigmaProp](proofVar1).get }", @@ -172,32 +265,38 @@ class BasicOpsSpecification extends SigmaTestingCommons test("Prop7", env, ext, "{ SELF.R4[SigmaProp].get.isProven }", ExtractRegisterAs[SSigmaProp.type](Self, reg1).get, - true + onlyPositive = true, + testExceededCost = false ) test("Prop8", env, ext, "{ SELF.R4[SigmaProp].get && getVar[SigmaProp](proofVar1).get}", SigmaAnd(Seq(ExtractRegisterAs[SSigmaProp.type](Self, reg1).get, GetVarSigmaProp(propVar1).get)), - true + onlyPositive = true, + testExceededCost = false ) test("Prop9", env, ext, "{ allOf(Coll(SELF.R4[SigmaProp].get, getVar[SigmaProp](proofVar1).get))}", SigmaAnd(Seq(ExtractRegisterAs[SSigmaProp.type](Self, reg1).get, GetVarSigmaProp(propVar1).get)), - true + onlyPositive = true, + testExceededCost = false ) test("Prop10", env, ext, "{ anyOf(Coll(SELF.R4[SigmaProp].get, getVar[SigmaProp](proofVar1).get))}", SigmaOr(Seq(ExtractRegisterAs[SSigmaProp.type](Self, reg1).get, GetVarSigmaProp(propVar1).get)), - true + onlyPositive = true, + testExceededCost = false ) test("Prop11", env, ext, - "{ Coll(SELF.R4[SigmaProp].get, getVar[SigmaProp](proofVar1).get).forall({ (p: SigmaProp) => p.isProven }) }", + "{ Coll(SELF.R4[SigmaProp].get, getVar[SigmaProp](proofVar1).get).forall({ (p: SigmaProp) => p.isProven }) }", SigmaAnd(Seq(ExtractRegisterAs[SSigmaProp.type](Self, reg1).get , GetVarSigmaProp(propVar1).get)), - true - ) + onlyPositive = true, + testExceededCost = false + ) test("Prop12", env, ext, "{ Coll(SELF.R4[SigmaProp].get, getVar[SigmaProp](proofVar1).get).exists({ (p: SigmaProp) => p.isProven }) }", SigmaOr(Seq(ExtractRegisterAs[SSigmaProp.type](Self, reg1).get, GetVarSigmaProp(propVar1).get)), - true + onlyPositive = true, + testExceededCost = false ) test("Prop13", env, ext, "{ SELF.R4[SigmaProp].get.propBytes != getVar[SigmaProp](proofVar1).get.propBytes }", @@ -343,7 +442,8 @@ class BasicOpsSpecification extends SigmaTestingCommons test("Extract1", env, ext, "{ SELF.R4[SigmaProp].get.isProven }", ExtractRegisterAs[SSigmaProp.type](Self, reg1).get, - true + onlyPositive = true, + testExceededCost = false ) // wrong type assertExceptionThrown( @@ -505,8 +605,10 @@ class BasicOpsSpecification extends SigmaTestingCommons test("prop1", env, ext, "sigmaProp(HEIGHT >= 0)", BoolToSigmaProp(GE(Height, IntConstant(0))), true) test("prop2", env, ext, "sigmaProp(HEIGHT >= 0) && getVar[SigmaProp](proofVar1).get", - SigmaAnd(Vector(BoolToSigmaProp(GE(Height, IntConstant(0))), GetVarSigmaProp(propVar1).get)), true) -// println(CostTableStat.costTableString) + SigmaAnd(Vector(BoolToSigmaProp(GE(Height, IntConstant(0))), GetVarSigmaProp(propVar1).get)), + onlyPositive = true, + testExceededCost = false + ) } property("numeric cast") { diff --git a/sigmastate/src/test/scala/sigmastate/utxo/ContextEnrichingSpecification.scala b/sigmastate/src/test/scala/sigmastate/utxo/ContextEnrichingSpecification.scala index e490c71d3f..fd932fc835 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/ContextEnrichingSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/ContextEnrichingSpecification.scala @@ -103,12 +103,20 @@ class ContextEnrichingSpecification extends SigmaTestingCommons val ctxv = ctx.withExtension(pr.extension) val verifier = new ErgoLikeTestInterpreter - //context w/out extensions - assertExceptionThrown( - verifier.verify(env, propTree, ctx, pr.proof, fakeMessage).get, - rootCause(_).isInstanceOf[ArrayIndexOutOfBoundsException] - ) verifier.verify(env, propTree, ctxv, pr.proof, fakeMessage).get._1 shouldBe true + + // negative tests: context w/out extensions + if (isActivatedVersion4) { + assertExceptionThrown( + verifier.verify(env, propTree, ctx, pr.proof, fakeMessage).get, + rootCause(_).isInstanceOf[ArrayIndexOutOfBoundsException] + ) + } else { + assertExceptionThrown( + verifier.verify(env, propTree, ctx, pr.proof, fakeMessage).get, + rootCause(_).isInstanceOf[NoSuchElementException] + ) + } } /** @@ -135,10 +143,18 @@ class ContextEnrichingSpecification extends SigmaTestingCommons val ctxv = ctx.withExtension(pr.extension) val verifier = new ErgoLikeTestInterpreter - //context w/out extensions - assertExceptionThrown(verifier.verify(env, propTree, ctx, pr.proof, fakeMessage).get, - rootCause(_).isInstanceOf[ArrayIndexOutOfBoundsException]) verifier.verify(env, propTree, ctxv, pr.proof, fakeMessage).get._1 shouldBe true + + // negative tests: context w/out extensions + if (isActivatedVersion4) { + assertExceptionThrown( + verifier.verify(env, propTree, ctx, pr.proof, fakeMessage).get, + rootCause(_).isInstanceOf[ArrayIndexOutOfBoundsException]) + } else { + assertExceptionThrown( + verifier.verify(env, propTree, ctx, pr.proof, fakeMessage).get, + rootCause(_).isInstanceOf[NoSuchElementException]) + } } property("prover enriching context 2") { @@ -164,11 +180,19 @@ class ContextEnrichingSpecification extends SigmaTestingCommons val ctxv = ctx.withExtension(pr.extension) val verifier = new ErgoLikeTestInterpreter - //context w/out extensions - assertExceptionThrown( - verifier.verify(env, propTree, ctx, pr.proof, fakeMessage).get, - rootCause(_).isInstanceOf[ArrayIndexOutOfBoundsException] - ) verifier.verify(env, propTree, ctxv, pr.proof, fakeMessage).get._1 shouldBe true + + // negative tests: context w/out extensions + if (isActivatedVersion4) { + assertExceptionThrown( + verifier.verify(env, propTree, ctx, pr.proof, fakeMessage).get, + rootCause(_).isInstanceOf[ArrayIndexOutOfBoundsException] + ) + } else { + assertExceptionThrown( + verifier.verify(env, propTree, ctx, pr.proof, fakeMessage).get, + rootCause(_).isInstanceOf[NoSuchElementException] + ) + } } } diff --git a/sigmastate/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala b/sigmastate/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala index 6b9d79e4d6..0972a3c5f0 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala @@ -524,9 +524,9 @@ class DistributedSigSpecification extends SigmaTestingCommons val sigBob = proverB.signMessage(sigmaTree, msg, bagB).get // Proof generated by Alice without getting Bob's part is not correct - verifier.verifySignature(sigmaTree, msg, sigAlice) shouldBe false + verifier.verifySignature(sigmaTree, msg, sigAlice)(null) shouldBe false // Compound proof from Bob is correct - verifier.verifySignature(sigmaTree, msg, sigBob) shouldBe true + verifier.verifySignature(sigmaTree, msg, sigBob)(null) shouldBe true } } diff --git a/sigmastate/src/test/scala/sigmastate/utxo/ErgoLikeInterpreterSpecification.scala b/sigmastate/src/test/scala/sigmastate/utxo/ErgoLikeInterpreterSpecification.scala index 5c304f6411..11ba35d60c 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/ErgoLikeInterpreterSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/ErgoLikeInterpreterSpecification.scala @@ -38,6 +38,7 @@ class ErgoLikeInterpreterSpecification extends SigmaTestingCommons val h2 = SigmaPropConstant(prover2.dlogSecrets.head.publicImage) val ctx = ErgoLikeContextTesting.dummy(fakeSelf, activatedVersionInTests) + .withErgoTreeVersion(ergoTreeVersionInTests) val e = compile(Map( "h1" -> h1.treeWithSegregation(ergoTreeHeaderInTests).bytes, @@ -46,12 +47,15 @@ class ErgoLikeInterpreterSpecification extends SigmaTestingCommons val exp = TrueLeaf e shouldBe exp - val res = verifier.reduceToCrypto(ctx, exp).get.value + val res = verifier.fullReduction(mkTestErgoTree(exp), ctx).value res shouldBe TrivialProp.TrueProp - - val res2 = verifier.reduceToCrypto(ctx, - EQ(ByteArrayConstant(h1.treeWithSegregation(ergoTreeHeaderInTests).bytes), - ByteArrayConstant(h2.treeWithSegregation(ergoTreeHeaderInTests).bytes))).get.value + val ergoTree = mkTestErgoTree( + EQ( + ByteArrayConstant(h1.treeWithSegregation(ergoTreeHeaderInTests).bytes), + ByteArrayConstant(h2.treeWithSegregation(ergoTreeHeaderInTests).bytes) + ).toSigmaProp + ) + val res2 = verifier.fullReduction(ergoTree, ctx).value res2 shouldBe TrivialProp.FalseProp } diff --git a/sigmastate/src/test/scala/sigmastate/utxo/ProverSpecification.scala b/sigmastate/src/test/scala/sigmastate/utxo/ProverSpecification.scala index 3439a7df4c..d5f76503b4 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/ProverSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/ProverSpecification.scala @@ -113,7 +113,7 @@ class ProverSpecification extends SigmaTestingCommons { def checkProof(sb: SigmaBoolean)(implicit prover: ProverInterpreter) = { val verifier = new ErgoLikeInterpreter() val proof = prover.generateProof(sb, message, HintsBag.empty) - val ok = verifier.verifySignature(sb, message, proof) + val ok = verifier.verifySignature(sb, message, proof)(null) ok shouldBe true } diff --git a/sigmastate/src/test/scala/sigmastate/utxo/SerializationRoundTripSpec.scala b/sigmastate/src/test/scala/sigmastate/utxo/SerializationRoundTripSpec.scala index e1d61f3591..41273131df 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/SerializationRoundTripSpec.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/SerializationRoundTripSpec.scala @@ -7,9 +7,9 @@ import scalan.util.BenchmarkUtil import sigmastate.helpers.SigmaTestingCommons import sigmastate.interpreter.{ProverResult, ContextExtension} import sigmastate.serialization.generators.ObjectGenerators -import sigmastate.serialization.ValueSerializer._ import debox.{Buffer => DBuffer} import sigmastate.lang.exceptions.SerializerException +import sigmastate.util.{MaxArrayLength, safeNewArray} import spire.algebra._ import spire.std.int._ @@ -24,15 +24,15 @@ class SerializationRoundTripSpec extends PropSpec implicit val orderRun = Order.by((r: Run) => r.size) property("ValueSerializer.newArray") { - newArray[Int](0).length shouldBe 0 - newArray[Int](MaxArrayLength).length shouldBe MaxArrayLength + safeNewArray[Int](0).length shouldBe 0 + safeNewArray[Int](MaxArrayLength).length shouldBe MaxArrayLength // test vector to catch constant changes MaxArrayLength shouldBe 100000 assertExceptionThrown( - newArray[Int](MaxArrayLength + 1), - exceptionLike[SerializerException]("Cannot allocate array of Int")) + safeNewArray[Int](MaxArrayLength + 1), + exceptionLike[RuntimeException]("Cannot allocate array of Int")) } property("ErgoBoxCandidate: Serializer round trip benchmark") { diff --git a/sigmastate/src/test/scala/sigmastate/utxo/ThresholdSpecification.scala b/sigmastate/src/test/scala/sigmastate/utxo/ThresholdSpecification.scala index 8fba6f7f47..041a6381a2 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/ThresholdSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/ThresholdSpecification.scala @@ -4,8 +4,8 @@ import sigmastate.basics.DLogProtocol.{DLogProverInput, ProveDlog} import sigmastate.Values.{ConcreteCollection, FalseLeaf, IntConstant, SigmaPropConstant, SigmaPropValue, TrueLeaf} import sigmastate._ import sigmastate.helpers.{ContextEnrichingTestProvingInterpreter, ErgoLikeContextTesting, ErgoLikeTestInterpreter, ErgoLikeTransactionTesting, SigmaTestingCommons} +import sigmastate.interpreter.Interpreter import sigmastate.lang.exceptions.CosterException -import sigmastate.utils.Helpers._ class ThresholdSpecification extends SigmaTestingCommons with CrossVersionProps { @@ -40,6 +40,7 @@ class ThresholdSpecification extends SigmaTestingCommons boxesToSpend = IndexedSeq(fakeSelf), spendingTransaction = ErgoLikeTransactionTesting.dummy, self = fakeSelf, activatedVersionInTests) + .withErgoTreeVersion(ergoTreeVersionInTests) val env = Map("pubkeyA" -> pubkeyA, "pubkeyB" -> pubkeyB, "pubkeyC" -> pubkeyC) @@ -66,8 +67,12 @@ class ThresholdSpecification extends SigmaTestingCommons prover.prove(compiledTree2, ctx, fakeMessage).isFailure shouldBe true } - val prop2And = CAND(Seq(pubkeyA, pubkeyB, pubkeyC)).toSigmaProp - proverA.reduceToCrypto(ctx, compiledProp2).get.value shouldBe proverA.reduceToCrypto(ctx, prop2And).get.value + { + val prop2And = mkTestErgoTree(CAND(Seq(pubkeyA, pubkeyB, pubkeyC))) + val res1 = proverA.fullReduction(compiledTree2, ctx).value + val res2 = proverA.fullReduction(prop2And, ctx).value + res1 shouldBe res2 + } // this example is from the white paper val (compiledTree3, compiledProp3) = compileAndCheck(env, @@ -84,8 +89,12 @@ class ThresholdSpecification extends SigmaTestingCommons } proverD.prove(compiledTree3, ctx, fakeMessage).isFailure shouldBe true - val prop3Or = COR(Seq(pubkeyA, pubkeyB, pubkeyC)).toSigmaProp - proverA.reduceToCrypto(ctx, compiledProp3).get.value shouldBe proverA.reduceToCrypto(ctx, prop3Or).get.value + { + val prop3Or = COR(Seq(pubkeyA, pubkeyB, pubkeyC)).toSigmaProp + val res1 = testReduce(proverA)(ctx, compiledProp3) + val res2 = testReduce(proverA)(ctx, prop3Or) + res1 shouldBe res2 + } val (compiledTree4, _) = compileAndCheck(env, """{ @@ -115,11 +124,12 @@ class ThresholdSpecification extends SigmaTestingCommons boxesToSpend = IndexedSeq(fakeSelf), spendingTransaction = ErgoLikeTransactionTesting.dummy, self = fakeSelf, activatedVersionInTests) + .withErgoTreeVersion(ergoTreeVersionInTests) case class TestCase(numTrue: Int, vector: Seq[SigmaPropValue], dlogOnlyVector: DlogOnlyVector) case class DlogOnlyVector(v: Seq[SigmaPropValue]) { - lazy val orVersion = prover.reduceToCrypto(ctx, SigmaOr(v)).get.value - lazy val andVersion = prover.reduceToCrypto(ctx, SigmaAnd(v)).get.value + lazy val orVersion = testReduce(prover)(ctx, SigmaOr(v)) + lazy val andVersion = testReduce(prover)(ctx, SigmaAnd(v)) } // Sequence of three secrets, in order to build test cases with 0, 1, 2, or 3 ProveDlogs inputs @@ -163,61 +173,60 @@ class ThresholdSpecification extends SigmaTestingCommons // for each test case, make into atleast and reduce it to crypto with different thresholds for (t <- testCaseSeq) { for (bound <- 0 to testCaseSeq.length + 1) { - val pReduced = prover.reduceToCrypto(ctx, AtLeast(bound, t.vector.toArray)) - pReduced.mapOrThrow(_ => true) shouldBe true + val res = testReduce(prover)(ctx, AtLeast(bound, t.vector.toArray)) if (t.dlogOnlyVector.v.isEmpty) { // Case 0: no ProveDlogs in the test vector -- just booleans if (t.numTrue >= bound) { - pReduced.get.value shouldBe TrivialProp.TrueProp + res shouldBe TrivialProp.TrueProp case0TrueHit = true } else { - pReduced.get.value shouldBe TrivialProp.FalseProp + res shouldBe TrivialProp.FalseProp case0FalseHit = true } } else if (t.dlogOnlyVector.v.length == 1) { // Case 1: 1 ProveDlog in the test vector // Should be just true if numTrue>=bound if (t.numTrue >= bound) { - pReduced.get.value shouldBe TrivialProp.TrueProp + res shouldBe TrivialProp.TrueProp case1TrueHit = true } // Should be false if bound>numTrue + 1 else if (bound > t.numTrue + 1) { - pReduced.get.value shouldBe TrivialProp.FalseProp + res shouldBe TrivialProp.FalseProp case1FalseHit = true } // if bound is exactly numTrue+1, should be just dlog else if (bound == t.numTrue + 1) { - SigmaPropConstant(pReduced.get.value.asInstanceOf[ProveDlog]) shouldBe t.dlogOnlyVector.v.head + SigmaPropConstant(res.asInstanceOf[ProveDlog]) shouldBe t.dlogOnlyVector.v.head case1DLogHit = true } } else { // Case 2: more than 1 ProveDlogs in the test vector // Should be just true if numTrue>=bound if (t.numTrue >= bound) { - pReduced.get.value shouldBe TrivialProp.TrueProp + res shouldBe TrivialProp.TrueProp case2TrueHit = true } // Should be false if bound>numTrue + dlogOnlyVector.length else if (bound > t.numTrue + t.dlogOnlyVector.v.length) { - pReduced.get.value shouldBe TrivialProp.FalseProp + res shouldBe TrivialProp.FalseProp case2FalseHit = true } // if bound is exactly numTrue+dlogOnlyVector, should be just AND of all dlogs else if (bound == t.numTrue + t.dlogOnlyVector.v.length) { - pReduced.get.value shouldBe t.dlogOnlyVector.andVersion + res shouldBe t.dlogOnlyVector.andVersion case2AndHit = true } // if bound is exactly numTrue+1, should be just OR of all dlogs else if (bound == t.numTrue + 1) { - pReduced.get.value shouldBe t.dlogOnlyVector.orVersion + res shouldBe t.dlogOnlyVector.orVersion case2OrHit = true } // else should be AtLeast else { - val atLeastReduced = prover.reduceToCrypto(ctx, AtLeast(bound - t.numTrue, t.dlogOnlyVector.v)) - pReduced.get.value shouldBe atLeastReduced.get.value + val atLeastReduced = testReduce(prover)(ctx, AtLeast(bound - t.numTrue, t.dlogOnlyVector.v)) + res shouldBe atLeastReduced case2AtLeastHit = true } } diff --git a/sigmastate/src/test/scala/sigmastate/utxo/examples/IcoExample.scala b/sigmastate/src/test/scala/sigmastate/utxo/examples/IcoExample.scala index 039ec873af..734ef352c6 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/examples/IcoExample.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/examples/IcoExample.scala @@ -9,7 +9,7 @@ import scorex.crypto.authds.avltree.batch._ import scorex.crypto.hash.{Digest32, Blake2b256} import sigmastate.Values._ import sigmastate._ -import sigmastate.helpers.{ErgoLikeContextTesting, ErgoLikeTestInterpreter, ErgoLikeTestProvingInterpreter, SigmaTestingCommons, ContextEnrichingTestProvingInterpreter} +import sigmastate.helpers.{ContextEnrichingTestProvingInterpreter, ErgoLikeContextTesting, ErgoLikeTestInterpreter, ErgoLikeTestProvingInterpreter, SigmaTestingCommons} import sigmastate.helpers.TestingHelpers._ import sigmastate.interpreter.Interpreter.ScriptNameProp import sigmastate.lang.Terms._ @@ -17,7 +17,7 @@ import sigmastate.serialization.ErgoTreeSerializer import ErgoTreeSerializer.DefaultSerializer import org.scalatest.BeforeAndAfterAll import sigmastate.eval.{CompiletimeCosting, IRContext} -import sigmastate.interpreter.{CryptoConstants, PrecompiledScriptProcessor} +import sigmastate.interpreter.{CryptoConstants, Interpreter} import scala.util.Random import sigmastate.eval._ @@ -557,6 +557,8 @@ class IcoExample extends SigmaTestingCommons * We output statistics of how PrecompiledScriptProcessor cache was used. */ override protected def afterAll(): Unit = { println(ErgoLikeTestInterpreter.DefaultProcessorInTests.getStats()) + println("verifySignatureProfiler ==========================================") + println(Interpreter.verifySignatureProfiler.generateReport) } } diff --git a/sigmastate/src/test/scala/special/sigma/ContractsTestkit.scala b/sigmastate/src/test/scala/special/sigma/ContractsTestkit.scala index ad260af0c7..291f1488b1 100644 --- a/sigmastate/src/test/scala/special/sigma/ContractsTestkit.scala +++ b/sigmastate/src/test/scala/special/sigma/ContractsTestkit.scala @@ -66,16 +66,17 @@ trait ContractsTestkit { def testContext(inputs: Array[Box], outputs: Array[Box], height: Int, self: Box, - tree: AvlTree, minerPk: Array[Byte], activatedScriptVersion: Byte, vars: Array[AnyValue]) = + tree: AvlTree, minerPk: Array[Byte], activatedScriptVersion: Byte, + currErgoTreeVersion: Byte, vars: Array[AnyValue]) = new CostingDataContext( noInputs.toColl, noHeaders, dummyPreHeader, - inputs.toColl, outputs.toColl, height, self, tree, - minerPk.toColl, vars.toColl, activatedScriptVersion, false) + inputs.toColl, outputs.toColl, height, self, inputs.indexOf(self), tree, + minerPk.toColl, vars.toColl, activatedScriptVersion, currErgoTreeVersion, false) - def newContext(height: Int, self: Box, activatedScriptVersion: Byte, vars: AnyValue*): CostingDataContext = { + def newContext(height: Int, self: Box, activatedScriptVersion: Byte, currErgoTreeVersion: Byte, vars: AnyValue*): CostingDataContext = { testContext( noInputs, noOutputs, height, self, emptyAvlTree, dummyPubkey, - activatedScriptVersion, vars.toArray) + activatedScriptVersion, currErgoTreeVersion, vars.toArray) } implicit class TestContextOps(ctx: CostingDataContext) { diff --git a/sigmastate/src/test/scala/special/sigma/DataValueComparerSpecification.scala b/sigmastate/src/test/scala/special/sigma/DataValueComparerSpecification.scala new file mode 100644 index 0000000000..c9e16b68f4 --- /dev/null +++ b/sigmastate/src/test/scala/special/sigma/DataValueComparerSpecification.scala @@ -0,0 +1,234 @@ +package special.sigma + +import org.ergoplatform.SigmaConstants.ScriptCostLimit +import org.scalatest.BeforeAndAfterAll +import scalan.RType +import scalan.util.BenchmarkUtil +import sigmastate.{DataValueComparer, JitCost, TrivialProp} +import sigmastate.Values.ErgoTree +import sigmastate.eval.{CSigmaProp, Profiler, SigmaDsl} +import sigmastate.helpers.SigmaPPrint +import sigmastate.interpreter.{CostAccumulator, ErgoTreeEvaluator, EvalSettings, TracedCost} +import special.collection.Coll + +import scala.util.{Success, Try} + +class DataValueComparerSpecification extends SigmaDslTesting + with BeforeAndAfterAll { suite => + + implicit override val generatorDrivenConfig = PropertyCheckConfiguration(minSuccessful = 30) + + implicit override val evalSettings: EvalSettings = + ErgoTreeEvaluator.DefaultEvalSettings.copy( + isMeasureOperationTime = true, + isMeasureScriptTime = true, + isLogEnabled = false, // don't commit the `true` value (CI log is too high) + costTracingEnabled = true // should always be enabled in tests (and false by default) + ) + override val nBenchmarkIters = 10 + + val nWarmUpIterations = 100 + + implicit val suiteProfiler = new Profiler + + import TestData._ + + def createEvaluator(settings: EvalSettings, profiler: Profiler): ErgoTreeEvaluator = { + val accumulator = new CostAccumulator( + initialCost = JitCost(0), + costLimit = Some(JitCost.fromBlockCost(ScriptCostLimit.value))) + val evaluator = new ErgoTreeEvaluator( + context = null, + constants = ErgoTree.EmptyConstants, + coster = accumulator, profiler, settings) + evaluator + } + + /** Checks (on positive cases) that EQ.equalDataValues used in v5.0 is equivalent to + * `==` used in v4.0 + * NOTE: the computations `x` and `y` are expected to be stable (i.e. always producing + * equal values) + * @param x computation which produced first argument + * @param y computation which produced second argument + */ + def check(x: => Any, y: => Any, expected: Boolean)(implicit settings: EvalSettings, profiler: Profiler) = { + val _x = x // force computation and obtain value + val _y = y + withClue(s"EQ.equalDataValues(${_x}, ${_y})") { + val res = sameResultOrError( + repeatAndReturnLast(nBenchmarkIters + 1) { + val evaluator = createEvaluator(settings, profiler) + // it's important to use fresh values to neutralize memory cache to some extent + val fresh_x = x + val fresh_y = y + DataValueComparer.equalDataValues(fresh_x, fresh_y)(evaluator) + }, + _x == _y) + res match { + case Success(res) => res shouldBe expected + case _ => + } + } + if (settings.isMeasureScriptTime) { + val evaluator = createEvaluator(settings, profiler) + val fresh_x = x + val fresh_y = y + val (res, actualTime) = BenchmarkUtil.measureTimeNano { + Try(DataValueComparer.equalDataValues(fresh_x, fresh_y)(evaluator)) + } + if (res.isSuccess) { + val costDetails = TracedCost(evaluator.getCostTrace(), Some(actualTime)) + val xStr = SigmaPPrint(fresh_x).plainText + val yStr = SigmaPPrint(fresh_y).plainText + val script = s"$xStr == $yStr" + evaluator.profiler.addJitEstimation(script, costDetails.cost, actualTime) + } + } + + } + + /** It is important for profiling to return a new array on every method call. + * This is to avoid reusing the same memory location during numerous iterations + * which doesn't reflect the real world scenario. Thus, creating a new array on every + * request neutralizes the effects of cache and makes profiling more accurate. */ + def zeros = Array[Any](0.toByte, 0.toShort, 0, 0.toLong) + def ones = Array[Any](1.toByte, 1.toShort, 1, 1.toLong) + + override protected def beforeAll(): Unit = { + // this method warms up the code in DataValueComparer + val warmUpProfiler = new Profiler + warmUpBeforeAllTest(nTotalIters = nWarmUpIterations) { + runBaseCases(warmUpProfiler)(evalSettings = evalSettings.copy(isLogEnabled = false)) + } + } + + /** Runs a number of equality checks for a value produced by the given computation. + * @param x computation which produces value to be exercised. */ + def checkIsEqual(x: => Any) = { + check(x, x, true) + check(Some(x), Some(x), true) + check((x, x), (x, x), true) + } + + /** This is NOT comprehensive list of possible checks. + * See also DataSerializerSpecification.roundtrip where comprehensive + * checking of positive cases is done. + * This method is used: + * 1) to warm up DataValueComparer in the beforeAll method + * 2) to profile DataValueComparer operations */ + def runBaseCases(profiler: Profiler)(implicit evalSettings: EvalSettings) = { + implicit val suiteProfiler = profiler // hide suite's profiler and use explicitly passed + ones.foreach { x => + ones.foreach { y => + check(x, y, true) // numeric values are equal regardless of their type + check(Some(x), Some(y), true) // numeric values in Option + check(Some(x), y, false) + check(x, Some(y), false) + check(Some(x), None, false) + check(None, Some(x), false) + check((x, 1), (y, 1), true) // and in Tuple + check((1, x), (1, y), true) + check((1, x), y, false) + check(x, (1, y), false) + } + } + val sizes = Array(0, 1, 4, 8, 16, 32, 64, 128, 256, 512) + def coll[T: RType](s: Int, v: => T): Coll[T] = { + val arr = Array.fill(s)(v)(RType[T].classTag) + builder.Colls.fromArray(arr) + } + sizes.foreach { s => + checkIsEqual(coll(s, 1.toByte)) + checkIsEqual(coll(s, 1.toShort)) + checkIsEqual(coll(s, 1)) + checkIsEqual(coll(s, 1L)) + checkIsEqual(coll(s, createBigIntMaxValue())) + checkIsEqual(coll(s, create_ge1())) + checkIsEqual(coll(s, create_t1())) + checkIsEqual(coll(s, create_b1())) + checkIsEqual(coll(s, create_preH1())) + checkIsEqual(coll(s, create_h1())) + // collections of complex types + checkIsEqual(coll(s, (1.toByte, 1))) + checkIsEqual(coll(s, Option((1.toByte, 1)))) + checkIsEqual(coll(s, (create_ge1(), Option((1.toByte, 1))))) + checkIsEqual(coll(s, (create_ge1(), (Option((1.toByte, 1)), coll(32, 7.toByte))))) + checkIsEqual(coll(s, SigmaDsl.SigmaProp(create_dlog()))) + checkIsEqual(coll(s, SigmaDsl.SigmaProp(create_dht()))) + checkIsEqual(coll(s, SigmaDsl.SigmaProp(create_and()))) + checkIsEqual(coll(s, SigmaDsl.SigmaProp(create_or()))) + checkIsEqual(coll(s, SigmaDsl.SigmaProp(TrivialProp.TrueProp))) + checkIsEqual(coll(s, SigmaDsl.SigmaProp(TrivialProp.FalseProp))) + } + + checkIsEqual(createBigIntMaxValue()) + checkIsEqual(create_ge1()) + checkIsEqual(create_t1) + checkIsEqual(create_b1()) + checkIsEqual(create_preH1()) + checkIsEqual(create_h1()) + checkIsEqual(CSigmaProp(create_dlog())) + checkIsEqual(CSigmaProp(create_dht())) + checkIsEqual(CSigmaProp(create_and())) + checkIsEqual(CSigmaProp(create_or())) + checkIsEqual(CSigmaProp(TrivialProp.TrueProp)) + checkIsEqual(CSigmaProp(TrivialProp.FalseProp)) + } + + /** Run this property alone for profiling and see the report generated in afterAll. */ + property("equalDataValues base cases (use for profiling)") { + runBaseCases(suiteProfiler)(evalSettings) + } + + property("equalDataValues positive cases (Coll)") { + checkIsEqual(Coll[Int]()) + checkIsEqual(Coll[Int](1)) + } + + property("equalDataValues negative cases") { + check(BigIntZero, BigIntOne, false) + check(ge1, ge2, false) + check(t1, t2, false) + check(b1, b2, false) + check(preH1, preH2, false) + check(h1, h2, false) + + ones.foreach { x => + zeros.foreach { y => + check(x, y, false) + } + } + ones.foreach { x => + check(BigIntOne, x, false) + } + val values = Array[AnyRef]( + 1.asInstanceOf[AnyRef], BigIntOne, ge1, t1, b1, preH1, h1, + Coll[Int](1), None, Option(1), (1, 1)) + values.foreach { x => + values.foreach { y => + if (!(x eq y)) { + check(x, y, false) + check(y, x, false) + } + } + } + } + + property("equalDataValues negative cases (in Coll)") { + val onesInColl = Array[AnyRef](Coll(1.toByte), Coll(1.toShort), Coll(1), Coll(1.toLong)) + onesInColl.foreach { x => + onesInColl.foreach { y => + if (!(x eq y)) { + check(x, y, false) + check(y, x, false) + } + } + } + check(Coll(1), Coll(1, 2), false) + } + + override protected def afterAll(): Unit = { + println(suiteProfiler.generateReport) + } + +} diff --git a/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala b/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala index 400818ed8e..53720fd52d 100644 --- a/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala +++ b/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala @@ -2,14 +2,13 @@ package special.sigma import java.lang.reflect.InvocationTargetException import java.math.BigInteger - import org.ergoplatform._ import org.ergoplatform.settings.ErgoAlgos import org.scalacheck.{Arbitrary, Gen} -import scalan.{ExactOrdering, ExactNumeric, RType, ExactIntegral} +import scalan.{ExactIntegral, ExactNumeric, ExactOrdering, RType} import scorex.crypto.authds.avltree.batch._ import scorex.crypto.authds.{ADDigest, ADKey, ADValue} -import scorex.crypto.hash.{Digest32, Blake2b256} +import scorex.crypto.hash.{Blake2b256, Digest32} import scalan.util.Extensions._ import sigma.util.Extensions._ import sigmastate.SCollection._ @@ -20,7 +19,7 @@ import sigmastate.Values._ import sigmastate.lang.Terms.Apply import sigmastate.eval.Extensions._ import sigmastate.eval._ -import sigmastate.lang.Terms.MethodCall +import sigmastate.lang.Terms.{MethodCall, PropertyCall} import sigmastate.utxo._ import special.collection._ import sigmastate.serialization.OpCodes.OpCode @@ -28,40 +27,200 @@ import sigmastate.utils.Helpers import sigmastate.utils.Helpers._ import sigmastate.helpers.TestingHelpers._ -import scala.util.{Success, Failure, Try} +import scala.util.{Failure, Success, Try} import OrderingOps._ import org.ergoplatform.ErgoBox.AdditionalRegisters import org.scalacheck.Arbitrary._ import org.scalacheck.Gen.frequency +import org.scalatest.{BeforeAndAfterAll, Tag} import scalan.RType._ import scorex.util.ModifierId import sigmastate.basics.ProveDHTuple +import sigmastate.interpreter._ +import org.scalactic.source.Position +import sigmastate.helpers.SigmaPPrint +import sigmastate.lang.exceptions.CosterException import scala.collection.mutable - /** This suite tests every method of every SigmaDsl type to be equivalent to - * the evaluation of the corresponding ErgoScript operation */ -class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { suite => + * the evaluation of the corresponding ErgoScript operation + * + * This suite can be used for Cost profiling, i.e. measurements of operations times and + * comparing them with cost parameteres of the operations. + * + * The following settings should be specified for profiling: + * isMeasureOperationTime = true + * isMeasureScriptTime = true + * isLogEnabled = false + * printTestVectors = false + * costTracingEnabled = false + * isTestRun = true + * perTestWarmUpIters = 1 + * nBenchmarkIters = 1 + */ +class SigmaDslSpecification extends SigmaDslTesting + with CrossVersionProps + with BeforeAndAfterAll { suite => + + /** Use VersionContext so that each property in this suite runs under correct + * parameters. + */ + protected override def testFun_Run(testName: String, testFun: => Any): Unit = { + VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + super.testFun_Run(testName, testFun) + } + } + + implicit override val generatorDrivenConfig = PropertyCheckConfiguration(minSuccessful = 30) + + val evalSettingsInTests = ErgoTreeEvaluator.DefaultEvalSettings.copy( + isMeasureOperationTime = true, + isMeasureScriptTime = true, + isLogEnabled = false, // don't commit the `true` value (travis log is too high) + printTestVectors = false, // don't commit the `true` value (travis log is too high) + + /** Should always be enabled in tests (and false by default) + * Should be disabled for cost profiling, which case the new costs are not checked. + */ + costTracingEnabled = true, + + profilerOpt = Some(ErgoTreeEvaluator.DefaultProfiler), + isTestRun = true + ) + + def warmupSettings(p: Profiler) = evalSettingsInTests.copy( + isLogEnabled = false, + printTestVectors = false, + profilerOpt = Some(p) + ) + + implicit override def evalSettings: EvalSettings = { + warmupProfiler match { + case Some(p) => warmupSettings(p) + case _ => evalSettingsInTests + } + } - override implicit val generatorDrivenConfig = PropertyCheckConfiguration(minSuccessful = 30) + override val perTestWarmUpIters = 0 + + override val nBenchmarkIters = 0 + + override val okRunTestsWithoutMCLowering: Boolean = true implicit def IR = createIR() + def testCases[A, B](cases: Seq[(A, Expected[B])], f: Feature[A, B]) = { + val table = Table(("x", "y"), cases:_*) + forAll(table) { (x, expectedRes) => + val res = f.checkEquality(x) + val resValue = res.map(_._1) + val (expected, expDetailsOpt) = expectedRes.newResults(ergoTreeVersionInTests) + checkResult(resValue, expected.value, failOnTestVectors = true, + "SigmaDslSpecifiction#testCases: compare expected new result with res = f.checkEquality(x)") + res match { + case Success((value, details)) => + details.cost shouldBe JitCost(expected.verificationCost.get) + expDetailsOpt.foreach(expDetails => + if (details.trace != expDetails.trace) { + printCostDetails(f.script, details) + details.trace shouldBe expDetails.trace + } + ) + } + } + } + import TestData._ - prepareSamples[BigInt] - prepareSamples[GroupElement] - prepareSamples[AvlTree] - prepareSamples[Box] - prepareSamples[PreHeader] - prepareSamples[Header] - prepareSamples[(BigInt, BigInt)] - prepareSamples[(GroupElement, GroupElement)] - prepareSamples[(AvlTree, AvlTree)] - prepareSamples[(Box, Box)] - prepareSamples[(PreHeader, PreHeader)] - prepareSamples[(Header, Header)] + override protected def beforeAll(): Unit = { + prepareSamples[BigInt] + prepareSamples[GroupElement] + prepareSamples[AvlTree] + prepareSamples[Box] + prepareSamples[PreHeader] + prepareSamples[Header] + prepareSamples[(BigInt, BigInt)] + prepareSamples[(GroupElement, GroupElement)] + prepareSamples[(AvlTree, AvlTree)] + prepareSamples[(Box, Box)] + prepareSamples[(PreHeader, PreHeader)] + prepareSamples[(Header, Header)] + } + + ///===================================================== + /// CostDetails shared among test cases + ///----------------------------------------------------- + val traceBase = Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FuncValue.AddToEnvironmentDesc_CostKind), + FixedCostItem(ValUse) + ) + def upcastCostDetails(tpe: SType) = TracedCost(traceBase :+ TypeBasedCostItem(Upcast, tpe)) + def downcastCostDetails(tpe: SType) = TracedCost(traceBase :+ TypeBasedCostItem(Downcast, tpe)) + def arithOpsCostDetails(tpe: SType) = CostDetails( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FuncValue.AddToEnvironmentDesc_CostKind), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FuncValue.AddToEnvironmentDesc_CostKind), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FuncValue.AddToEnvironmentDesc_CostKind), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Plus, tpe), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Minus, tpe), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Multiply, tpe), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Division, tpe), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Modulo, tpe), + FixedCostItem(Tuple), + FixedCostItem(Tuple), + FixedCostItem(Tuple), + FixedCostItem(Tuple) + ) + ) + + def binaryRelationCostDetails(rel: RelationCompanion, tpe: SType) = CostDetails( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(rel, tpe) + ) + ) + def costNEQ(neqCost: Seq[CostItem]) = CostDetails( + traceBase ++ + Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField) + ) ++ + neqCost + ) + + def methodCostDetails(sMethod: SMethod, methodCost: Int) = TracedCost( + traceBase ++ Array( + FixedCostItem(PropertyCall), + FixedCostItem(sMethod, FixedCost(JitCost(methodCost))) + ) + ) ///===================================================== /// Boolean type operations @@ -88,11 +247,21 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui SelectField.typed[BoolValue](ValUse(1, STuple(Vector(SBoolean, SBoolean))), 2.toByte) ) )) + val newDetails = CostDetails( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(BinXor) + ) + ) + val newCost = 1768 + def success(b: Boolean) = Expected(Success(b), 36518, newDetails, newCost) val cases = Seq( - (true, true) -> Expected(Success(false), 36518), - (true, false) -> Expected(Success(true), 36518), - (false, false) -> Expected(Success(false), 36518), - (false, true) -> Expected(Success(true), 36518) + (true, true) -> success(false), + (true, false) -> success(true), + (false, false) -> success(false), + (false, true) -> success(true) ) verifyCases(cases, binXor) } @@ -104,19 +273,26 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui Vector((1, STuple(Vector(SBoolean, SBoolean)))), BinXor( SelectField.typed[BoolValue](ValUse(1, STuple(Vector(SBoolean, SBoolean))), 1.toByte), - SelectField.typed[BoolValue](ValUse(1, STuple(Vector(SBoolean, SBoolean))), 2.toByte) - ) - )) + SelectField.typed[BoolValue](ValUse(1, STuple(Vector(SBoolean, SBoolean))), 2.toByte)))) + val newDetails = CostDetails( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(BinXor) + ) + ) val expectedCost = 36518 + val newCost = 1768 val cases = Seq( - (true, true) -> Expected(Success(false), expectedCost) + (true, true) -> Expected(Success(false), expectedCost, newDetails, newCost) ) verifyCases(cases, feature) val initCost = 100 initialCostInTests.withValue(initCost) { val cases = Seq( - (true, true) -> Expected(Success(false), expectedCost + initCost) + (true, true) -> Expected(Success(false), expectedCost + initCost, newDetails, newCost + initCost) ) verifyCases(cases, feature) } @@ -135,13 +311,24 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui SelectField.typed[BoolValue](ValUse(1, STuple(Vector(SInt, SBoolean))), 2.toByte) ) )) + val newDetails = CostDetails( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(BinXor) + ) + ) + def success(b: Boolean) = Expected(Success(b), 36865, newDetails, 1769) val cases = Seq( - (1095564593, true) -> Expected(Success(true), 36865), - (-901834021, true) -> Expected(Success(true), 36865), - (595045530, false) -> Expected(Success(false), 36865), - (-1157998227, false) -> Expected(Success(false), 36865), - (0, true) -> Expected(Success(false), 36865), - (0, false) -> Expected(Success(true), 36865) + (1095564593, true) -> success(true), + (-901834021, true) -> success(true), + (595045530, false) -> success(false), + (-1157998227, false) -> success(false), + (0, true) -> success(false), + (0, false) -> success(true) ) verifyCases(cases, xor) } @@ -156,11 +343,25 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui SelectField.typed[BoolValue](ValUse(1, STuple(Vector(SBoolean, SBoolean))), 2.toByte) ) )) + val newDetails1 = CostDetails( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(BinAnd) + ) + ) + val newDetails2 = CostDetails( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(BinAnd), + FixedCostItem(ValUse), + FixedCostItem(SelectField) + ) + ) val cases = Seq( - (false, true) -> Expected(Success(false), 38241), - (false, false) -> Expected(Success(false), 38241), - (true, true) -> Expected(Success(true), 38241), - (true, false) -> Expected(Success(false), 38241) + (false, true) -> Expected(Success(false), 38241, newDetails1, 1766), + (false, false) -> Expected(Success(false), 38241, newDetails1, 1766), + (true, true) -> Expected(Success(true), 38241, newDetails2, 1768), + (true, false) -> Expected(Success(false), 38241, newDetails2, 1768) ) verifyCases(cases, eq) } @@ -175,19 +376,71 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui SelectField.typed[BoolValue](ValUse(1, STuple(Vector(SBoolean, SBoolean))), 2.toByte) ) )) + val newDetails1 = CostDetails( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(BinOr) + ) + ) + val newDetails2 = CostDetails( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(BinOr), + FixedCostItem(ValUse), + FixedCostItem(SelectField) + ) + ) + val cost = 38241 val cases = Seq( - (true, false) -> Expected(Success(true), 38241), - (true, true) -> Expected(Success(true), 38241), - (false, false) -> Expected(Success(false), 38241), - (false, true) -> Expected(Success(true), 38241) + (true, false) -> Expected(Success(true), cost, newDetails1, 1766), + (true, true) -> Expected(Success(true), cost, newDetails1, 1766), + (false, false) -> Expected(Success(false), cost, newDetails2, 1768), + (false, true) -> Expected(Success(true), cost, newDetails2, 1768) ) verifyCases(cases, eq) } - property("lazy || and && boolean equivalence") { + def runLazy_And_Or_BooleanEquivalence(implicit evalSettings: EvalSettings) = { + val newDetails1 = CostDetails( + traceBase ++ Array( + FixedCostItem(BinOr) + ) + ) + val newDetails2 = CostDetails( + traceBase ++ Array( + FixedCostItem(BinAnd) + ) + ) + val newDetails3 = CostDetails( + traceBase ++ Array( + FixedCostItem(BinAnd), + FixedCostItem(ValUse), + FixedCostItem(BinOr) + ) + ) + val newDetails4 = CostDetails( + traceBase ++ Array( + FixedCostItem(BinAnd), + FixedCostItem(ValUse), + FixedCostItem(BinAnd), + FixedCostItem(ValUse), + FixedCostItem(BinOr) + ) + ) + val newDetails5 = CostDetails( + traceBase ++ Array( + FixedCostItem(BinAnd), + FixedCostItem(ValUse), + FixedCostItem(BinAnd), + FixedCostItem(ValUse), + FixedCostItem(BinAnd), + FixedCostItem(ValUse), + FixedCostItem(BinOr) + ) + ) verifyCases( Seq( - (true, Expected(Success(true), 38467)), + (true, Expected(Success(true), 38467, newDetails1, 1765)), (false, Expected(new ArithmeticException("/ by zero"))) ), existingFeature((x: Boolean) => x || (1 / 0 == 1), @@ -203,7 +456,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( Seq( (true, Expected(new ArithmeticException("/ by zero"))), - (false, Expected(Success(false), 38467)) + (false, Expected(Success(false), 38467, newDetails2, 1765)) ), existingFeature((x: Boolean) => x && (1 / 0 == 1), "{ (x: Boolean) => x && (1 / 0 == 1) }", @@ -217,8 +470,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( Seq( - (false, Expected(Success(false), 40480)), - (true, Expected(Success(true), 40480)) + (false, Expected(Success(false), 40480, newDetails2, 1765)), + (true, Expected(Success(true), 40480, newDetails3, 1768)) ), existingFeature((x: Boolean) => x && (x || (1 / 0 == 1)), "{ (x: Boolean) => x && (x || (1 / 0 == 1)) }", @@ -235,8 +488,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( Seq( - (false, Expected(Success(false), 42493)), - (true, Expected(Success(true), 42493)) + (false, Expected(Success(false), 42493, newDetails2, 1765)), + (true, Expected(Success(true), 42493, newDetails4, 1770)) ), existingFeature((x: Boolean) => x && (x && (x || (1 / 0 == 1))), "{ (x: Boolean) => x && (x && (x || (1 / 0 == 1))) }", @@ -256,8 +509,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( Seq( - (false, Expected(Success(false), 44506)), - (true, Expected(Success(true), 44506)) + (false, Expected(Success(false), 44506, newDetails2, 1765)), + (true, Expected(Success(true), 44506, newDetails5, 1773)) ), existingFeature((x: Boolean) => x && (x && (x && (x || (1 / 0 == 1)))), "{ (x: Boolean) => x && (x && (x && (x || (1 / 0 == 1)))) }", @@ -278,10 +531,20 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ))) + val newDetails6 = CostDetails( + traceBase ++ Array( + FixedCostItem(LogicalNot), + FixedCostItem(BinAnd), + FixedCostItem(LogicalNot), + FixedCostItem(BinAnd), + FixedCostItem(ValUse), + FixedCostItem(BinOr) + ) + ) verifyCases( Seq( (false, Expected(new ArithmeticException("/ by zero"))), - (true, Expected(Success(true), 43281)) + (true, Expected(Success(true), 43281, newDetails6, 1773)) ), existingFeature((x: Boolean) => !(!x && (1 / 0 == 1)) && (x || (1 / 0 == 1)), "{ (x: Boolean) => !(!x && (1 / 0 == 1)) && (x || (1 / 0 == 1)) }", @@ -301,9 +564,16 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ))) + val newDetails7 = CostDetails( + traceBase ++ Array( + FixedCostItem(BinOr), + FixedCostItem(BinAnd), + FixedCostItem(ValUse) + ) + ) verifyCases( Seq( - (true, Expected(Success(true), 40480)), + (true, Expected(Success(true), 40480, newDetails7, 1768)), (false, Expected(new ArithmeticException("/ by zero"))) ), existingFeature((x: Boolean) => (x || (1 / 0 == 1)) && x, @@ -319,9 +589,17 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ))) + val newDetails8 = CostDetails( + traceBase ++ Array( + FixedCostItem(BinOr), + FixedCostItem(BinAnd), + FixedCostItem(ValUse), + FixedCostItem(BinOr) + ) + ) verifyCases( Seq( - (true, Expected(Success(true), 43149)), + (true, Expected(Success(true), 43149, newDetails8, 1770)), (false, Expected(new ArithmeticException("/ by zero"))) ), existingFeature((x: Boolean) => (x || (1 / 0 == 1)) && (x || (1 / 0 == 1)), @@ -340,9 +618,20 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ))) + val newDetails9 = CostDetails( + traceBase ++ Array( + FixedCostItem(LogicalNot), + FixedCostItem(BinAnd), + FixedCostItem(LogicalNot), + FixedCostItem(BinOr), + FixedCostItem(BinAnd), + FixedCostItem(ValUse), + FixedCostItem(BinOr) + ) + ) verifyCases( Seq( - (true, Expected(Success(true), 45950)), + (true, Expected(Success(true), 45950, newDetails9, 1775)), (false, Expected(new ArithmeticException("/ by zero"))) ), existingFeature( @@ -367,10 +656,32 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ))) + val newDetails10 = CostDetails( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(LogicalNot), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(BinAnd), + FixedCostItem(LogicalNot), + FixedCostItem(BinOr), + FixedCostItem(BinAnd), + FixedCostItem(ValUse), + FixedCostItem(BinAnd), + FixedCostItem(LogicalNot), + FixedCostItem(BinOr) + ) + ) verifyCases( Seq( (false, Expected(new ArithmeticException("/ by zero"))), - (true, Expected(Success(true), 48862)) + (true, Expected(Success(true), 48862, newDetails10, 1780)) ), existingFeature( (x: Boolean) => (!(!x && (1 / 0 == 1)) || (1 / 0 == 0)) && (!(!x && (1 / 0 == 1)) || (1 / 0 == 1)), @@ -403,13 +714,17 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ))) } + property("lazy || and && boolean equivalence") { + runLazy_And_Or_BooleanEquivalence(evalSettings) + } + property("Byte methods equivalence") { SByte.upcast(0.toByte) shouldBe 0.toByte // boundary test case SByte.downcast(0.toByte) shouldBe 0.toByte // boundary test case verifyCases( { - def expect(v: Byte) = Expected(Success(v), 35798) + def expect(v: Byte) = Expected(Success(v), 35798, TracedCost(traceBase), 1763) Seq( (0.toByte, expect(0.toByte)), (1.toByte, expect(1.toByte)), @@ -426,7 +741,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def expected(v: Short) = Expected(Success(v), 35902) + def expected(v: Short) = Expected(Success(v), 35902, upcastCostDetails(SShort), 1764) Seq( (0.toByte, expected(0.toShort)), (1.toByte, expected(1.toShort)), @@ -443,7 +758,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def expected(v: Int) = Expected(Success(v), 35902) + def expected(v: Int) = Expected(Success(v), 35902, upcastCostDetails(SInt), 1764) Seq( (0.toByte, expected(0)), (1.toByte, expected(1)), @@ -460,7 +775,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def expected(v: Long) = Expected(Success(v), 35902) + def expected(v: Long) = Expected(Success(v), 35902, upcastCostDetails(SLong), 1764) Seq( (0.toByte, expected(0L)), (1.toByte, expected(1L)), @@ -477,7 +792,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def expected(v: BigInt) = Expected(Success(v), 35932) + def expected(v: BigInt) = Expected(Success(v), 35932, upcastCostDetails(SBigInt), 1767) Seq( (0.toByte, expected(CBigInt(new BigInteger("0", 16)))), (1.toByte, expected(CBigInt(new BigInteger("1", 16)))), @@ -495,7 +810,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val n = ExactIntegral.ByteIsExactIntegral verifyCases( { - def success[T](v: (T, (T, (T, (T, T))))) = Expected(Success(v), 39654) + def success[T](v: (T, (T, (T, (T, T))))) = Expected(Success(v), 39654, arithOpsCostDetails(SByte), 1788) Seq( ((-128.toByte, -128.toByte), Expected(new ArithmeticException("Byte overflow"))), ((-128.toByte, 0.toByte), Expected(new ArithmeticException("/ by zero"))), @@ -589,20 +904,35 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui )) } - def swapArgs[A](cases: Seq[((A, A), Expected[Boolean])], cost: Int) = - cases.map { case ((x, y), res) => ((y, x), res.copy(cost = cost)) } + def swapArgs[A](cases: Seq[((A, A), Expected[Boolean])], cost: Int, newCost: Int, newCostDetails: CostDetails) = + cases.map { case ((x, y), res) => + ((y, x), Expected(res.value, cost, newCostDetails, newCost)) + } - def newCasesFrom[A, R](cases: Seq[(A, A)])(getExpectedRes: (A, A) => R, cost: Int) = + def newCasesFrom[A, R]( + cases: Seq[(A, A)] + )( + getExpectedRes: (A, A) => R, + cost: Int, + newDetails: CostDetails, + newCost: Int + ) = cases.map { case (x, y) => - ((x, y), Expected(Success(getExpectedRes(x, y)), cost = cost)) + ((x, y), Expected(Success(getExpectedRes(x, y)), cost = cost, newDetails, newCost)) } + def newCasesFrom2[A, R](cases: Seq[(A, A)]) + (getExpectedRes: (A, A) => R, cost: Int, newCost: Int, newCostDetails: CostDetails) = + cases.map { case (x, y) => + ((x, y), Expected(Success(getExpectedRes(x, y)), cost = cost, expectedDetails = newCostDetails, expectedNewCost = newCost)) + } + def verifyOp[A: Ordering: Arbitrary] (cases: Seq[((A, A), Expected[Boolean])], opName: String, op: (SValue, SValue) => SValue) (expectedFunc: (A, A) => Boolean, generateCases: Boolean = true) - (implicit tA: RType[A], sampled: Sampled[(A, A)]) = { + (implicit tA: RType[A], sampled: Sampled[(A, A)], evalSettings: EvalSettings) = { val nameA = RType[A].name val tpeA = Evaluation.rtypeToSType(tA) verifyCases(cases, @@ -623,9 +953,11 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui preGeneratedSamples = Some(sampled.samples)) } + val constNeqCost: Seq[CostItem] = Array[CostItem](FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3)))) + property("Byte LT, GT, NEQ") { val o = ExactOrdering.ByteIsExactOrdering - def expect(v: Boolean) = Expected(Success(v), 36328) + def expect(v: Boolean) = Expected(Success(v), 36328, binaryRelationCostDetails(LT, SByte), 1768) val LT_cases: Seq[((Byte, Byte), Expected[Boolean])] = Seq( (-128.toByte, -128.toByte) -> expect(false), (-128.toByte, -127.toByte) -> expect(true), @@ -665,15 +997,17 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyOp(LT_cases, "<", LT.apply)(_ < _) - verifyOp(swapArgs(LT_cases, cost = 36342), ">", GT.apply)(_ > _) + verifyOp( + swapArgs(LT_cases, cost = 36342, newCost = 1768, newCostDetails = binaryRelationCostDetails(GT, SByte)), + ">", GT.apply)(_ > _) - val neqCases = newCasesFrom(LT_cases.map(_._1))(_ != _, cost = 36337) + val neqCases = newCasesFrom2(LT_cases.map(_._1))(_ != _, cost = 36337, newCost = 1766, newCostDetails = costNEQ(constNeqCost)) verifyOp(neqCases, "!=", NEQ.apply)(_ != _) } property("Byte LE, GE") { val o = ExactOrdering.ByteIsExactOrdering - def expect(v: Boolean) = Expected(Success(v), 36337) + def expect(v: Boolean) = Expected(Success(v), 36337, binaryRelationCostDetails(LE, SByte), 1768) val LE_cases: Seq[((Byte, Byte), Expected[Boolean])] = Seq( (-128.toByte, -128.toByte) -> expect(true), (-128.toByte, -127.toByte) -> expect(true), @@ -714,7 +1048,9 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyOp(LE_cases, "<=", LE.apply)(_ <= _) - verifyOp(swapArgs(LE_cases, cost = 36336), ">=", GE.apply)(_ >= _) + verifyOp( + swapArgs(LE_cases, cost = 36336, newCost = 1768, newCostDetails = binaryRelationCostDetails(GE, SByte)), + ">=", GE.apply)(_ >= _) } property("Byte methods equivalence (new features)") { @@ -748,7 +1084,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 35976) + def success[T](v: T) = Expected(Success(v), 35976, downcastCostDetails(SByte), 1764) Seq( (Short.MinValue, Expected(new ArithmeticException("Byte overflow"))), (-21626.toShort, Expected(new ArithmeticException("Byte overflow"))), @@ -767,7 +1103,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 35798) + def success[T](v: T) = Expected(Success(v), 35798, TracedCost(traceBase), 1763) Seq( (-32768.toShort, success(-32768.toShort)), (-27798.toShort, success(-27798.toShort)), @@ -784,7 +1120,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 35902) + def success[T](v: T) = Expected(Success(v), 35902, upcastCostDetails(SInt), 1764) Seq( (-32768.toShort, success(-32768)), (-21064.toShort, success(-21064)), @@ -801,7 +1137,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 35902) + def success[T](v: T) = Expected(Success(v), 35902, upcastCostDetails(SLong), 1764) Seq( (-32768.toShort, success(-32768L)), (-23408.toShort, success(-23408L)), @@ -818,7 +1154,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success(v: BigInt) = Expected(Success(v), 35932) + def success(v: BigInt) = Expected(Success(v), 35932, upcastCostDetails(SBigInt), 1767) Seq( (-32768.toShort, success(CBigInt(new BigInteger("-8000", 16)))), (-26248.toShort, success(CBigInt(new BigInteger("-6688", 16)))), @@ -836,7 +1172,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val n = ExactIntegral.ShortIsExactIntegral verifyCases( { - def success[T](v: T) = Expected(Success(v), 39654) + def success[T](v: T) = Expected(Success(v), 39654, arithOpsCostDetails(SShort), 1788) Seq( ((-32768.toShort, 1.toShort), Expected(new ArithmeticException("Short overflow"))), ((-32768.toShort, 4006.toShort), Expected(new ArithmeticException("Short overflow"))), @@ -928,7 +1264,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Short LT, GT, NEQ") { val o = ExactOrdering.ShortIsExactOrdering - def expect(v: Boolean) = Expected(Success(v), 36328) + def expect(v: Boolean) = Expected(Success(v), 36328, binaryRelationCostDetails(LT, SShort), 1768) val LT_cases: Seq[((Short, Short), Expected[Boolean])] = Seq( (Short.MinValue, Short.MinValue) -> expect(false), (Short.MinValue, (Short.MinValue + 1).toShort) -> expect(true), @@ -968,15 +1304,15 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyOp(LT_cases, "<", LT.apply)(_ < _) - verifyOp(swapArgs(LT_cases, cost = 36342), ">", GT.apply)(_ > _) + verifyOp(swapArgs(LT_cases, cost = 36342, newCost = 1768, newCostDetails = binaryRelationCostDetails(GT, SShort)), ">", GT.apply)(_ > _) - val neqCases = newCasesFrom(LT_cases.map(_._1))(_ != _, cost = 36337) + val neqCases = newCasesFrom2(LT_cases.map(_._1))(_ != _, cost = 36337, newCost = 1766, newCostDetails = costNEQ(constNeqCost)) verifyOp(neqCases, "!=", NEQ.apply)(_ != _) } property("Short LE, GE") { val o = ExactOrdering.ShortIsExactOrdering - def expect(v: Boolean) = Expected(Success(v), 36337) + def expect(v: Boolean) = Expected(Success(v), 36337, binaryRelationCostDetails(LE, SShort), 1768) val LE_cases: Seq[((Short, Short), Expected[Boolean])] = Seq( (Short.MinValue, Short.MinValue) -> expect(true), (Short.MinValue, (Short.MinValue + 1).toShort) -> expect(true), @@ -1017,7 +1353,9 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyOp(LE_cases, "<=", LE.apply)(_ <= _) - verifyOp(swapArgs(LE_cases, cost = 36336), ">=", GE.apply)(_ >= _) + verifyOp( + swapArgs(LE_cases, cost = 36336, newCost = 1768, newCostDetails = binaryRelationCostDetails(GE, SShort)), + ">=", GE.apply)(_ >= _) } property("Short methods equivalence (new features)") { @@ -1050,7 +1388,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 35976) + def success[T](v: T) = Expected(Success(v), 35976, downcastCostDetails(SByte), 1764) Seq( (Int.MinValue, Expected(new ArithmeticException("Byte overflow"))), (-2014394379, Expected(new ArithmeticException("Byte overflow"))), @@ -1069,7 +1407,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 35976) + def success[T](v: T) = Expected(Success(v), 35976, downcastCostDetails(SShort), 1764) Seq( (Int.MinValue, Expected(new ArithmeticException("Short overflow"))), (Short.MinValue - 1, Expected(new ArithmeticException("Short overflow"))), @@ -1088,7 +1426,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 35798) + def success[T](v: T) = Expected(Success(v), 35798, TracedCost(traceBase), 1763) Seq( (Int.MinValue, success(Int.MinValue)), (-1, success(-1)), @@ -1103,7 +1441,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 35902) + def success[T](v: T) = Expected(Success(v), 35902, upcastCostDetails(SLong), 1764) Seq( (Int.MinValue, success(Int.MinValue.toLong)), (-1, success(-1L)), @@ -1118,7 +1456,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success(v: BigInt) = Expected(Success(v), 35932) + def success(v: BigInt) = Expected(Success(v), 35932, upcastCostDetails(SBigInt), 1767) Seq( (Int.MinValue, success(CBigInt(new BigInteger("-80000000", 16)))), (-1937187314, success(CBigInt(new BigInteger("-737721f2", 16)))), @@ -1136,7 +1474,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val n = ExactNumeric.IntIsExactNumeric verifyCases( { - def success[T](v: T) = Expected(Success(v), 39654) + def success[T](v: T) = Expected(Success(v), 39654, arithOpsCostDetails(SInt), 1788) Seq( ((Int.MinValue, 449583993), Expected(new ArithmeticException("integer overflow"))), ((-1589633733, 2147483647), Expected(new ArithmeticException("integer overflow"))), @@ -1228,7 +1566,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Int LT, GT, NEQ") { val o = ExactOrdering.IntIsExactOrdering - def expect(v: Boolean) = Expected(Success(v), 36328) + def expect(v: Boolean) = Expected(Success(v), 36328, binaryRelationCostDetails(LT, SInt), 1768) val LT_cases: Seq[((Int, Int), Expected[Boolean])] = Seq( (Int.MinValue, Int.MinValue) -> expect(false), (Int.MinValue, (Int.MinValue + 1).toInt) -> expect(true), @@ -1268,15 +1606,17 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyOp(LT_cases, "<", LT.apply)(_ < _) - verifyOp(swapArgs(LT_cases, cost = 36342), ">", GT.apply)(_ > _) + verifyOp( + swapArgs(LT_cases, cost = 36342, newCost = 1768, newCostDetails = binaryRelationCostDetails(GT, SInt)), + ">", GT.apply)(_ > _) - val neqCases = newCasesFrom(LT_cases.map(_._1))(_ != _, cost = 36337) + val neqCases = newCasesFrom2(LT_cases.map(_._1))(_ != _, cost = 36337, newCost = 1766, newCostDetails = costNEQ(constNeqCost)) verifyOp(neqCases, "!=", NEQ.apply)(_ != _) } property("Int LE, GE") { val o = ExactOrdering.IntIsExactOrdering - def expect(v: Boolean) = Expected(Success(v), 36337) + def expect(v: Boolean) = Expected(Success(v), 36337, binaryRelationCostDetails(LE, SInt), 1768) val LE_cases: Seq[((Int, Int), Expected[Boolean])] = Seq( (Int.MinValue, Int.MinValue) -> expect(true), (Int.MinValue, (Int.MinValue + 1).toInt) -> expect(true), @@ -1317,7 +1657,9 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyOp(LE_cases, "<=", LE.apply)(_ <= _) - verifyOp(swapArgs(LE_cases, cost = 36336), ">=", GE.apply)(_ >= _) + verifyOp( + swapArgs(LE_cases, cost = 36336, newCost = 1768, newCostDetails = binaryRelationCostDetails(GE, SInt)), + ">=", GE.apply)(_ >= _) } property("Int methods equivalence (new features)") { @@ -1343,13 +1685,17 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } } - property("Long methods equivalence") { - SLong.upcast(0L) shouldBe 0L // boundary test case - SLong.downcast(0L) shouldBe 0L // boundary test case + property("Long downcast and upcast identity") { + forAll { x: Long => + SLong.upcast(x) shouldBe x // boundary test case + SLong.downcast(x) shouldBe x // boundary test case + } + } + property("Long.toByte method") { verifyCases( { - def success[T](v: T) = Expected(Success(v), 35976) + def success[T](v: T) = Expected(Success(v), 35976, downcastCostDetails(SByte), 1764) Seq( (Long.MinValue, Expected(new ArithmeticException("Byte overflow"))), (Byte.MinValue.toLong - 1, Expected(new ArithmeticException("Byte overflow"))), @@ -1365,10 +1711,12 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui existingFeature((x: Long) => x.toByteExact, "{ (x: Long) => x.toByte }", FuncValue(Vector((1, SLong)), Downcast(ValUse(1, SLong), SByte)))) + } + property("Long.toShort method") { verifyCases( { - def success[T](v: T) = Expected(Success(v), 35976) + def success[T](v: T) = Expected(Success(v), 35976, downcastCostDetails(SShort), 1764) Seq( (Long.MinValue, Expected(new ArithmeticException("Short overflow"))), (Short.MinValue.toLong - 1, Expected(new ArithmeticException("Short overflow"))), @@ -1384,10 +1732,12 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui existingFeature((x: Long) => x.toShortExact, "{ (x: Long) => x.toShort }", FuncValue(Vector((1, SLong)), Downcast(ValUse(1, SLong), SShort)))) + } + property("Long.toInt method") { verifyCases( { - def success[T](v: T) = Expected(Success(v), 35976) + def success[T](v: T) = Expected(Success(v), 35976, downcastCostDetails(SInt), 1764) Seq( (Long.MinValue, Expected(new ArithmeticException("Int overflow"))), (Int.MinValue.toLong - 1, Expected(new ArithmeticException("Int overflow"))), @@ -1403,10 +1753,12 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui existingFeature((x: Long) => x.toIntExact, "{ (x: Long) => x.toInt }", FuncValue(Vector((1, SLong)), Downcast(ValUse(1, SLong), SInt)))) + } + property("Long.toLong method") { verifyCases( { - def success[T](v: T) = Expected(Success(v), 35798) + def success[T](v: T) = Expected(Success(v), 35798, TracedCost(traceBase), 1763) Seq( (Long.MinValue, success(Long.MinValue)), (-1L, success(-1L)), @@ -1418,10 +1770,12 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui existingFeature((x: Long) => x.toLong, "{ (x: Long) => x.toLong }", FuncValue(Vector((1, SLong)), ValUse(1, SLong)))) + } + property("Long.toBigInt method") { verifyCases( { - def success(v: BigInt) = Expected(Success(v), 35932) + def success(v: BigInt) = Expected(Success(v), 35932, upcastCostDetails(SBigInt), 1767) Seq( (Long.MinValue, success(CBigInt(new BigInteger("-8000000000000000", 16)))), (-1074651039980347209L, success(CBigInt(new BigInteger("-ee9ed6d57885f49", 16)))), @@ -1435,11 +1789,14 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui existingFeature((x: Long) => x.toBigInt, "{ (x: Long) => x.toBigInt }", FuncValue(Vector((1, SLong)), Upcast(ValUse(1, SLong), SBigInt)))) + } + + property("Long methods equivalence") { val n = ExactNumeric.LongIsExactNumeric verifyCases( { - def success[T](v: T) = Expected(Success(v), 39654) + def success[T](v: T) = Expected(Success(v), 39654, arithOpsCostDetails(SLong), 1788) Seq( ((Long.MinValue, -4677100190307931395L), Expected(new ArithmeticException("long overflow"))), ((Long.MinValue, -1L), Expected(new ArithmeticException("long overflow"))), @@ -1529,7 +1886,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Long LT, GT, NEQ") { val o = ExactOrdering.LongIsExactOrdering - def expect(v: Boolean) = Expected(Success(v), 36328) + def expect(v: Boolean) = Expected(Success(v), 36328, binaryRelationCostDetails(LT, SLong), 1768) val LT_cases: Seq[((Long, Long), Expected[Boolean])] = Seq( (Long.MinValue, Long.MinValue) -> expect(false), (Long.MinValue, (Long.MinValue + 1).toLong) -> expect(true), @@ -1569,15 +1926,17 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyOp(LT_cases, "<", LT.apply)(_ < _) - verifyOp(swapArgs(LT_cases, cost = 36342), ">", GT.apply)(_ > _) + verifyOp( + swapArgs(LT_cases, cost = 36342, newCost = 1768, newCostDetails = binaryRelationCostDetails(GT, SLong)), + ">", GT.apply)(_ > _) - val neqCases = newCasesFrom(LT_cases.map(_._1))(_ != _, cost = 36337) + val neqCases = newCasesFrom2(LT_cases.map(_._1))(_ != _, cost = 36337, newCost = 1766, newCostDetails = costNEQ(constNeqCost)) verifyOp(neqCases, "!=", NEQ.apply)(_ != _) } property("Long LE, GE") { val o = ExactOrdering.LongIsExactOrdering - def expect(v: Boolean) = Expected(Success(v), 36337) + def expect(v: Boolean) = Expected(Success(v), 36337, binaryRelationCostDetails(LE, SLong), 1768) val LE_cases: Seq[((Long, Long), Expected[Boolean])] = Seq( (Long.MinValue, Long.MinValue) -> expect(true), (Long.MinValue, (Long.MinValue + 1).toLong) -> expect(true), @@ -1618,7 +1977,9 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyOp(LE_cases, "<=", LE.apply)(_ <= _) - verifyOp(swapArgs(LE_cases, cost = 36336), ">=", GE.apply)(_ >= _) + verifyOp( + swapArgs(LE_cases, cost = 36336, newCost = 1768, newCostDetails = binaryRelationCostDetails(GE, SLong)), + ">=", GE.apply)(_ >= _) } property("Long methods equivalence (new features)") { @@ -1648,7 +2009,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("BigInt methods equivalence") { verifyCases( { - def success(v: BigInt) = Expected(Success(v), 35798) + def success(v: BigInt) = Expected(Success(v), 35798, TracedCost(traceBase), 1764) Seq( (CBigInt(new BigInteger("-85102d7f884ca0e8f56193b46133acaf7e4681e1757d03f191ae4f445c8e0", 16)), success( CBigInt(new BigInteger("-85102d7f884ca0e8f56193b46133acaf7e4681e1757d03f191ae4f445c8e0", 16)) @@ -1670,7 +2031,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val n = NumericOps.BigIntIsExactIntegral verifyCases( { - def success(v: (BigInt, (BigInt, (BigInt, (BigInt, BigInt))))) = Expected(Success(v), 39774) + def success(v: (BigInt, (BigInt, (BigInt, (BigInt, BigInt))))) = + Expected(Success(v), 39774, arithOpsCostDetails(SBigInt), 1793) Seq( ((CBigInt(new BigInteger("-8683d1cd99d5fcf0e6eff6295c285c36526190e13dbde008c49e5ae6fddc1c", 16)), CBigInt(new BigInteger("-2ef55db3f245feddacf0182e299dd", 16))), @@ -1793,12 +2155,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("BigInt LT, GT, NEQ") { val o = NumericOps.BigIntIsExactOrdering - // TODO HF: this values have bitCount == 255 (see to256BitValueExact) - val BigIntMinValue = CBigInt(new BigInteger("-7F" + "ff" * 31, 16)) - val BigIntMaxValue = CBigInt(new BigInteger("7F" + "ff" * 31, 16)) - val BigIntOverlimit = CBigInt(new BigInteger("7F" + "ff" * 33, 16)) - def expect(v: Boolean) = Expected(Success(v), 36328) + def expect(v: Boolean) = Expected(Success(v), 36328, binaryRelationCostDetails(LT, SBigInt), 1768) val LT_cases: Seq[((BigInt, BigInt), Expected[Boolean])] = Seq( (BigIntMinValue, BigIntMinValue) -> expect(false), @@ -1835,26 +2193,29 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui (BigIntMaxValue, BigIntMinValue) -> expect(false), (BigIntMaxValue, -47.toBigInt) -> expect(false), (BigIntMaxValue, BigIntMaxValue) -> expect(false), - (BigIntMaxValue, BigIntOverlimit) -> expect(true), // TODO v5.0: reject this overlimit cases + (BigIntMaxValue, BigIntOverlimit) -> expect(true), // TODO v6.0: reject this overlimit cases (BigIntOverlimit, BigIntOverlimit) -> expect(false) ) verifyOp(LT_cases, "<", LT.apply)(o.lt(_, _)) - verifyOp(swapArgs(LT_cases, cost = 36342), ">", GT.apply)(o.gt(_, _)) + verifyOp( + swapArgs(LT_cases, cost = 36342, newCost = 1768, newCostDetails = binaryRelationCostDetails(GT, SBigInt)), + ">", GT.apply)(o.gt(_, _)) - val neqCases = newCasesFrom(LT_cases.map(_._1))(_ != _, cost = 36337) + val constBigIntCost = Array[CostItem](FixedCostItem(NamedDesc("EQ_BigInt"), FixedCost(JitCost(5)))) + val neqCases = newCasesFrom2(LT_cases.map(_._1))(_ != _, cost = 36337, newCost = 1766, newCostDetails = costNEQ(constBigIntCost)) verifyOp(neqCases, "!=", NEQ.apply)(_ != _) } property("BigInt LE, GE") { val o = NumericOps.BigIntIsExactOrdering - // TODO HF: this values have bitCount == 255 (see to256BitValueExact) + // TODO v6.0: this values have bitCount == 255 (see to256BitValueExact) val BigIntMinValue = CBigInt(new BigInteger("-7F" + "ff" * 31, 16)) val BigIntMaxValue = CBigInt(new BigInteger("7F" + "ff" * 31, 16)) val BigIntOverlimit = CBigInt(new BigInteger("7F" + "ff" * 33, 16)) - def expect(v: Boolean) = Expected(Success(v), 36337) + def expect(v: Boolean) = Expected(Success(v), 36337, binaryRelationCostDetails(LE, SBigInt), 1768) val LE_cases: Seq[((BigInt, BigInt), Expected[Boolean])] = Seq( (BigIntMinValue, BigIntMinValue) -> expect(true), @@ -1892,25 +2253,27 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui (BigIntMaxValue, BigIntMinValue) -> expect(false), (BigIntMaxValue, -47.toBigInt) -> expect(false), (BigIntMaxValue, BigIntMaxValue) -> expect(true), - (BigIntMaxValue, BigIntOverlimit) -> expect(true), // TODO v5.0: reject this overlimit cases + (BigIntMaxValue, BigIntOverlimit) -> expect(true), // TODO v6.0: reject this overlimit cases (BigIntOverlimit, BigIntOverlimit) -> expect(true) ) verifyOp(LE_cases, "<=", LE.apply)(o.lteq(_, _)) - verifyOp(swapArgs(LE_cases, cost = 36336), ">=", GE.apply)(o.gteq(_, _)) + verifyOp( + swapArgs(LE_cases, cost = 36336, newCost = 1768, newCostDetails = binaryRelationCostDetails(GE, SBigInt)), + ">=", GE.apply)(o.gteq(_, _)) } property("BigInt methods equivalence (new features)") { - // TODO HF (2h): the behavior of `upcast` for BigInt is different from all other Numeric types + // TODO v6.0 (2h): the behavior of `upcast` for BigInt is different from all other Numeric types // The `Upcast(bigInt, SBigInt)` node is never produced by ErgoScript compiler, but is still valid ErgoTree. - // It makes sense to fix this inconsistency as part of HF + // It makes sense to fix this inconsistency as part of upcoming forks assertExceptionThrown( SBigInt.upcast(CBigInt(new BigInteger("0", 16)).asInstanceOf[AnyVal]), _.getMessage.contains("Cannot upcast value") ) - // TODO HF (2h): the behavior of `downcast` for BigInt is different from all other Numeric types + // TODO v6.0 (2h): the behavior of `downcast` for BigInt is different from all other Numeric types // The `Downcast(bigInt, SBigInt)` node is never produced by ErgoScript compiler, but is still valid ErgoTree. // It makes sense to fix this inconsistency as part of HF assertExceptionThrown( @@ -1960,80 +2323,237 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui * @param cost the expected cost of `verify` (the same for all cases) */ def verifyNeq[A: Ordering: Arbitrary: RType] - (x: A, y: A, cost: Int) + (x: A, y: A, cost: Int, neqCost: Seq[CostItem] = mutable.WrappedArray.empty, newCost: Int) (copy: A => A, generateCases: Boolean = true) - (implicit sampled: Sampled[(A, A)]) = { + (implicit sampled: Sampled[(A, A)], evalSettings: EvalSettings) = { val copied_x = copy(x) + val newCostDetails = if (neqCost.isEmpty) CostDetails.ZeroCost else costNEQ(neqCost) + def expected(v: Boolean) = Expected(Success(v), cost, newCostDetails, newCost) + def expectedNoCost(v: Boolean) = Expected(Success(v), cost, CostDetails.ZeroCost) verifyOp(Seq( - (x, x) -> Expected(Success(false), cost), - (x, copied_x) -> Expected(Success(false), cost), - (copied_x, x) -> Expected(Success(false), cost), - (x, y) -> Expected(Success(true), cost), - (y, x) -> Expected(Success(true), cost) + (x, y) -> expected(true), // check cost only for this test case, because the trace depends in x and y + (x, x) -> expectedNoCost(false), // and don't check for others + (x, copied_x) -> expectedNoCost(false), + (copied_x, x) -> expectedNoCost(false), + (y, x) -> expectedNoCost(true) ), "!=", NEQ.apply)(_ != _, generateCases) } property("NEQ of pre-defined types") { - verifyNeq(ge1, ge2, 36337)(_.asInstanceOf[CGroupElement].copy()) - verifyNeq(t1, t2, 36337)(_.asInstanceOf[CAvlTree].copy()) - verifyNeq(b1, b2, 36417)(_.asInstanceOf[CostingBox].copy()) - verifyNeq(preH1, preH2, 36337)(_.asInstanceOf[CPreHeader].copy()) - verifyNeq(h1, h2, 36337)(_.asInstanceOf[CHeader].copy()) + verifyNeq(ge1, ge2, 36337, Array[CostItem](FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172)))), 1783)(_.asInstanceOf[CGroupElement].copy()) + verifyNeq(t1, t2, 36337, Array[CostItem](FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6)))), 1767)(_.asInstanceOf[CAvlTree].copy()) + verifyNeq(b1, b2, 36417, Array[CostItem](), 1767)(_.asInstanceOf[CostingBox].copy()) + verifyNeq(preH1, preH2, 36337, Array[CostItem](FixedCostItem(NamedDesc("EQ_PreHeader"), FixedCost(JitCost(4)))), 1766)(_.asInstanceOf[CPreHeader].copy()) + verifyNeq(h1, h2, 36337, Array[CostItem](FixedCostItem(NamedDesc("EQ_Header"), FixedCost(JitCost(6)))), 1767)(_.asInstanceOf[CHeader].copy()) } property("NEQ of tuples of numerics") { - verifyNeq((0.toByte, 1.toByte), (1.toByte, 1.toByte), 36337)(_.copy()) - verifyNeq((0.toShort, 1.toByte), (1.toShort, 1.toByte), 36337)(_.copy()) - verifyNeq((0, 1.toByte), (1, 1.toByte), 36337)(_.copy()) - verifyNeq((0.toLong, 1.toByte), (1.toLong, 1.toByte), 36337)(_.copy()) - verifyNeq((0.toBigInt, 1.toByte), (1.toBigInt, 1.toByte), 36337)(_.copy()) + val tuplesNeqCost = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))) + ) + verifyNeq((0.toByte, 1.toByte), (1.toByte, 1.toByte), 36337, tuplesNeqCost, 1767)(_.copy()) + verifyNeq((0.toShort, 1.toByte), (1.toShort, 1.toByte), 36337, tuplesNeqCost, 1767)(_.copy()) + verifyNeq((0, 1.toByte), (1, 1.toByte), 36337, tuplesNeqCost, 1767)(_.copy()) + verifyNeq((0.toLong, 1.toByte), (1.toLong, 1.toByte), 36337, tuplesNeqCost, 1767)(_.copy()) + verifyNeq((0.toBigInt, 1.toByte), (1.toBigInt, 1.toByte), 36337, Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_BigInt"), FixedCost(JitCost(5))) + ), 1767)(_.copy()) } property("NEQ of tuples of pre-defined types") { - verifyNeq((ge1, ge1), (ge1, ge2), 36337)(_.copy()) - verifyNeq((t1, t1), (t1, t2), 36337)(_.copy()) - verifyNeq((b1, b1), (b1, b2), 36497)(_.copy()) - verifyNeq((preH1, preH1), (preH1, preH2), 36337)(_.copy()) - verifyNeq((h1, h1), (h1, h2), 36337)(_.copy()) - } + val groupNeqCost = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172))), + FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172))) + ) + verifyNeq((ge1, ge1), (ge1, ge2), 36337, groupNeqCost, 1801)(_.copy()) - property("NEQ of nested tuples") { - verifyNeq((ge1, (t1, t1)), (ge1, (t1, t2)), 36337)(_.copy()) - verifyNeq((ge1, (t1, (b1, b1))), (ge1, (t1, (b1, b2))), 36497)(_.copy()) - verifyNeq((ge1, (t1, (b1, (preH1, preH1)))), (ge1, (t1, (b1, (preH1, preH2)))), 36417)(_.copy()) - verifyNeq((ge1, (t1, (b1, (preH1, (h1, h1))))), (ge1, (t1, (b1, (preH1, (h1, h2))))), 36427)(_.copy()) + val treeNeqCost = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))) + ) + verifyNeq((t1, t1), (t1, t2), 36337, treeNeqCost, 1768)(_.copy()) - verifyNeq(((ge1, t1), t1), ((ge1, t1), t2), 36337)(_.copy()) - verifyNeq((((ge1, t1), b1), b1), (((ge1, t1), b1), b2), 36497)(_.copy()) - verifyNeq((((ge1, t1), b1), (preH1, preH1)), (((ge1, t1), b1), (preH1, preH2)), 36417)(_.copy()) - verifyNeq((((ge1, t1), b1), (preH1, (h1, h1))), (((ge1, t1), b1), (preH1, (h1, h2))), 36427)(_.copy()) + verifyNeq((b1, b1), (b1, b2), 36497, Array[CostItem](), 1768)(_.copy()) + + val preHeaderNeqCost = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_PreHeader"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_PreHeader"), FixedCost(JitCost(4))) + ) + verifyNeq((preH1, preH1), (preH1, preH2), 36337, preHeaderNeqCost, 1767)(_.copy()) + + val headerNeqCost = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Header"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Header"), FixedCost(JitCost(6))) + ) + verifyNeq((h1, h1), (h1, h2), 36337, headerNeqCost, 1768)(_.copy()) + } + + property("NEQ of nested tuples") { + val nestedTuplesNeqCost1 = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))) + ) + val nestedTuplesNeqCost2 = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Box"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Box"), FixedCost(JitCost(6))) + ) + val nestedTuplesNeqCost3 = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Box"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_PreHeader"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_PreHeader"), FixedCost(JitCost(4))) + ) + val nestedTuplesNeqCost4 = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Box"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_PreHeader"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Header"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Header"), FixedCost(JitCost(6))) + ) + val nestedTuplesNeqCost5 = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))) + ) + val nestedTuplesNeqCost6 = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Box"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Box"), FixedCost(JitCost(6))) + ) + val nestedTuplesNeqCost7 = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Box"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_PreHeader"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_PreHeader"), FixedCost(JitCost(4))) + ) + val nestedTuplesNeqCost8 = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Box"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_PreHeader"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Header"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("EQ_Header"), FixedCost(JitCost(6))) + ) + verifyNeq((ge1, (t1, t1)), (ge1, (t1, t2)), 36337, nestedTuplesNeqCost1, 1785)(_.copy()) + verifyNeq((ge1, (t1, (b1, b1))), (ge1, (t1, (b1, b2))), 36497, nestedTuplesNeqCost2, 1786)(_.copy()) + verifyNeq((ge1, (t1, (b1, (preH1, preH1)))), (ge1, (t1, (b1, (preH1, preH2)))), 36417, nestedTuplesNeqCost3, 1787)(_.copy()) + verifyNeq((ge1, (t1, (b1, (preH1, (h1, h1))))), (ge1, (t1, (b1, (preH1, (h1, h2))))), 36427, nestedTuplesNeqCost4, 1788)(_.copy()) + + verifyNeq(((ge1, t1), t1), ((ge1, t1), t2), 36337, nestedTuplesNeqCost5, 1785)(_.copy()) + verifyNeq((((ge1, t1), b1), b1), (((ge1, t1), b1), b2), 36497, nestedTuplesNeqCost6, 1786)(_.copy()) + verifyNeq((((ge1, t1), b1), (preH1, preH1)), (((ge1, t1), b1), (preH1, preH2)), 36417, nestedTuplesNeqCost7, 1787)(_.copy()) + verifyNeq((((ge1, t1), b1), (preH1, (h1, h1))), (((ge1, t1), b1), (preH1, (h1, h2))), 36427, nestedTuplesNeqCost8, 1788)(_.copy()) } property("NEQ of collections of pre-defined types") { - verifyNeq(Coll[Byte](), Coll(1.toByte), 36337)(cloneColl(_)) - verifyNeq(Coll[Byte](0, 1), Coll(1.toByte, 1.toByte), 36337)(cloneColl(_)) - - verifyNeq(Coll[Short](), Coll(1.toShort), 36337)(cloneColl(_)) - verifyNeq(Coll[Short](0), Coll(1.toShort), 36337)(cloneColl(_)) - - verifyNeq(Coll[Int](), Coll(1), 36337)(cloneColl(_)) - verifyNeq(Coll[Int](0), Coll(1), 36337)(cloneColl(_)) - - verifyNeq(Coll[Long](), Coll(1.toLong), 36337)(cloneColl(_)) - verifyNeq(Coll[Long](0), Coll(1.toLong), 36337)(cloneColl(_)) + val collNeqCost1 = Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))) + ) + val collNeqCost2 = Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_Box"), PerItemCost(JitCost(15), JitCost(5), 1), 0) + ) + implicit val evalSettings = suite.evalSettings.copy(isMeasureOperationTime = false) + verifyNeq(Coll[Byte](), Coll(1.toByte), 36337, collNeqCost1, 1766)(cloneColl(_)) + verifyNeq(Coll[Byte](0, 1), Coll(1.toByte, 1.toByte), 36337, + Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_Byte"), PerItemCost(JitCost(15), JitCost(2), 128), 1)), + 1768 + )(cloneColl(_)) + + verifyNeq(Coll[Short](), Coll(1.toShort), 36337, collNeqCost1, 1766)(cloneColl(_)) + verifyNeq(Coll[Short](0), Coll(1.toShort), 36337, + Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_Short"), PerItemCost(JitCost(15), JitCost(2), 96), 1)), + 1768 + )(cloneColl(_)) + + verifyNeq(Coll[Int](), Coll(1), 36337, collNeqCost1, 1766)(cloneColl(_)) + verifyNeq(Coll[Int](0), Coll(1), 36337, + Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_Int"), PerItemCost(JitCost(15), JitCost(2), 64), 1)), + 1768 + )(cloneColl(_)) + + verifyNeq(Coll[Long](), Coll(1.toLong), 36337, collNeqCost1, 1766)(cloneColl(_)) + verifyNeq(Coll[Long](0), Coll(1.toLong), 36337, + Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_Long"), PerItemCost(JitCost(15), JitCost(2), 48), 1)), + 1768 + )(cloneColl(_)) prepareSamples[Coll[BigInt]] - verifyNeq(Coll[BigInt](), Coll(1.toBigInt), 36337)(cloneColl(_)) - verifyNeq(Coll[BigInt](0.toBigInt), Coll(1.toBigInt), 36337)(cloneColl(_)) + verifyNeq(Coll[BigInt](), Coll(1.toBigInt), 36337, collNeqCost1, 1766)(cloneColl(_)) + verifyNeq(Coll[BigInt](0.toBigInt), Coll(1.toBigInt), 36337, + Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_BigInt"), PerItemCost(JitCost(15), JitCost(7), 5), 1)), + 1768 + )(cloneColl(_)) prepareSamples[Coll[GroupElement]] - verifyNeq(Coll[GroupElement](), Coll(ge1), 36337)(cloneColl(_)) - verifyNeq(Coll[GroupElement](ge1), Coll(ge2), 36337)(cloneColl(_)) + verifyNeq(Coll[GroupElement](), Coll(ge1), 36337, collNeqCost1, 1766)(cloneColl(_)) + verifyNeq(Coll[GroupElement](ge1), Coll(ge2), 36337, + Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_GroupElement"), PerItemCost(JitCost(15), JitCost(5), 1), 1)), + 1768 + )(cloneColl(_)) prepareSamples[Coll[AvlTree]] - verifyNeq(Coll[AvlTree](), Coll(t1), 36337)(cloneColl(_)) - verifyNeq(Coll[AvlTree](t1), Coll(t2), 36337)(cloneColl(_)) + verifyNeq(Coll[AvlTree](), Coll(t1), 36337, collNeqCost1, 1766)(cloneColl(_)) + verifyNeq(Coll[AvlTree](t1), Coll(t2), 36337, + Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_AvlTree"), PerItemCost(JitCost(15), JitCost(5), 2), 1)), + 1768 + )(cloneColl(_)) { // since SBox.isConstantSize = false, the cost is different among cases prepareSamples[Coll[AvlTree]] @@ -2041,41 +2561,109 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val y = Coll(b1) val copied_x = cloneColl(x) verifyOp(Seq( - (x, x) -> Expected(Success(false), 36337), - (x, copied_x) -> Expected(Success(false), 36337), - (copied_x, x) -> Expected(Success(false), 36337), - (x, y) -> Expected(Success(true), 36377), - (y, x) -> Expected(Success(true), 36377) + (x, x) -> Expected(Success(false), 36337, costNEQ(collNeqCost2), 1768), + (x, copied_x) -> Expected(Success(false), 36337, costNEQ(collNeqCost2), 1768), + (copied_x, x) -> Expected(Success(false), 36337, costNEQ(collNeqCost2), 1768), + (x, y) -> Expected(Success(true), 36377, costNEQ(collNeqCost1), 1766), + (y, x) -> Expected(Success(true), 36377, costNEQ(collNeqCost1), 1766) ), "!=", NEQ.apply)(_ != _, generateCases = false) - verifyNeq(Coll[Box](b1), Coll(b2), 36417)(cloneColl(_), generateCases = false) + verifyNeq(Coll[Box](b1), Coll(b2), 36417, + Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_Box"), PerItemCost(JitCost(15), JitCost(5), 1), 1)), + 1768 + )(cloneColl(_), generateCases = false) } prepareSamples[Coll[PreHeader]] - verifyNeq(Coll[PreHeader](), Coll(preH1), 36337)(cloneColl(_)) - verifyNeq(Coll[PreHeader](preH1), Coll(preH2), 36337)(cloneColl(_)) + verifyNeq(Coll[PreHeader](), Coll(preH1), 36337, collNeqCost1, 1766)(cloneColl(_)) + verifyNeq(Coll[PreHeader](preH1), Coll(preH2), 36337, + Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_PreHeader"), PerItemCost(JitCost(15), JitCost(3), 1), 1)), + 1768 + )(cloneColl(_)) prepareSamples[Coll[Header]] - verifyNeq(Coll[Header](), Coll(h1), 36337)(cloneColl(_)) - verifyNeq(Coll[Header](h1), Coll(h2), 36337)(cloneColl(_)) + verifyNeq(Coll[Header](), Coll(h1), 36337, collNeqCost1, 1766)(cloneColl(_)) + verifyNeq(Coll[Header](h1), Coll(h2), 36337, + Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_Header"), PerItemCost(JitCost(15), JitCost(5), 1), 1)), + 1768 + )(cloneColl(_)) } property("NEQ of nested collections and tuples") { + implicit val evalSettings = suite.evalSettings.copy(isMeasureOperationTime = false) prepareSamples[Coll[Int]] prepareSamples[Coll[Coll[Int]]] prepareSamples[Coll[Coll[Int]]] - verifyNeq(Coll[Coll[Int]](), Coll(Coll[Int]()), 36337)(cloneColl(_)) - verifyNeq(Coll(Coll[Int]()), Coll(Coll[Int](1)), 36337)(cloneColl(_)) - verifyNeq(Coll(Coll[Int](1)), Coll(Coll[Int](2)), 36337)(cloneColl(_)) - verifyNeq(Coll(Coll[Int](1)), Coll(Coll[Int](1, 2)), 36337)(cloneColl(_)) + val nestedNeq1 = Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))) + ) + val nestedNeq2 = Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_Coll"), PerItemCost(JitCost(10), JitCost(2), 1), 1) + ) + val nestedNeq3 = Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + SeqCostItem(NamedDesc("EQ_COA_Int"), PerItemCost(JitCost(15), JitCost(2), 64), 1), + SeqCostItem(NamedDesc("EQ_Coll"), PerItemCost(JitCost(10), JitCost(2), 1), 1) + ) + verifyNeq(Coll[Coll[Int]](), Coll(Coll[Int]()), 36337, nestedNeq1, 1766)(cloneColl(_)) + verifyNeq(Coll(Coll[Int]()), Coll(Coll[Int](1)), 36337, nestedNeq2, 1767)(cloneColl(_)) + verifyNeq(Coll(Coll[Int](1)), Coll(Coll[Int](2)), 36337, nestedNeq3, 1769)(cloneColl(_)) + verifyNeq(Coll(Coll[Int](1)), Coll(Coll[Int](1, 2)), 36337, nestedNeq2, 1767)(cloneColl(_)) prepareSamples[Coll[(Int, BigInt)]] prepareSamples[Coll[Coll[(Int, BigInt)]]] - verifyNeq(Coll(Coll((1, 10.toBigInt))), Coll(Coll((1, 11.toBigInt))), 36337)(cloneColl(_)) - verifyNeq(Coll(Coll(Coll((1, 10.toBigInt)))), Coll(Coll(Coll((1, 11.toBigInt)))), 36337)(cloneColl(_)) + val nestedNeq4 = Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(NamedDesc("EQ_BigInt"), FixedCost(JitCost(5))), + SeqCostItem(NamedDesc("EQ_Coll"), PerItemCost(JitCost(10), JitCost(2), 1), 1), + SeqCostItem(NamedDesc("EQ_Coll"), PerItemCost(JitCost(10), JitCost(2), 1), 1) + ) + val nestedNeq5 = Array( + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(NamedDesc("EQ_BigInt"), FixedCost(JitCost(5))), + SeqCostItem(NamedDesc("EQ_Coll"), PerItemCost(JitCost(10), JitCost(2), 1), 1), + SeqCostItem(NamedDesc("EQ_Coll"), PerItemCost(JitCost(10), JitCost(2), 1), 1), + SeqCostItem(NamedDesc("EQ_Coll"), PerItemCost(JitCost(10), JitCost(2), 1), 1) + ) + val nestedNeq6 = Array( + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_AvlTree"), FixedCost(JitCost(6))), + FixedCostItem(NamedDesc("MatchType"), FixedCost(JitCost(1))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(NamedDesc("EQ_BigInt"), FixedCost(JitCost(5))), + FixedCostItem(NamedDesc("EQ_Tuple"), FixedCost(JitCost(4))), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(NamedDesc("EQ_BigInt"), FixedCost(JitCost(5))), + SeqCostItem(NamedDesc("EQ_Coll"), PerItemCost(JitCost(10), JitCost(2), 1), 2), + SeqCostItem(NamedDesc("EQ_Coll"), PerItemCost(JitCost(10), JitCost(2), 1), 1), + SeqCostItem(NamedDesc("EQ_Coll"), PerItemCost(JitCost(10), JitCost(2), 1), 1) + ) + verifyNeq(Coll(Coll((1, 10.toBigInt))), Coll(Coll((1, 11.toBigInt))), 36337, nestedNeq4, 1770)(cloneColl(_)) + verifyNeq(Coll(Coll(Coll((1, 10.toBigInt)))), Coll(Coll(Coll((1, 11.toBigInt)))), 36337, nestedNeq5, 1771)(cloneColl(_)) verifyNeq( (Coll( (Coll( @@ -2087,114 +2675,212 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui (t1, Coll((1, 10.toBigInt), (1, 11.toBigInt))) ), ge1) ), preH1), - 36337)(x => (cloneColl(x._1), x._2)) + 36337, + nestedNeq6, + 1774 + )(x => (cloneColl(x._1), x._2)) } - property("GroupElement methods equivalence") { - + property("GroupElement.getEncoded equivalence") { verifyCases( - { - def success[T](v: T) = Expected(Success(v), 37905) - Seq( - (ge1, success(Helpers.decodeBytes(ge1str))), - (ge2, success(Helpers.decodeBytes(ge2str))), - (ge3, success(Helpers.decodeBytes(ge3str))), - (SigmaDsl.groupGenerator, - success(Helpers.decodeBytes("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"))), - (SigmaDsl.groupIdentity, - success(Helpers.decodeBytes("000000000000000000000000000000000000000000000000000000000000000000"))) - ) - }, - existingFeature((x: GroupElement) => x.getEncoded, - "{ (x: GroupElement) => x.getEncoded }", - FuncValue( - Vector((1, SGroupElement)), - MethodCall(ValUse(1, SGroupElement), SGroupElement.getMethodByName("getEncoded"), Vector(), Map()) - ))) + { + def success[T](v: T) = Expected(Success(v), 37905, methodCostDetails(SGroupElement.GetEncodedMethod, 250), 1790) + Seq( + (ge1, success(Helpers.decodeBytes(ge1str))), + (ge2, success(Helpers.decodeBytes(ge2str))), + (ge3, success(Helpers.decodeBytes(ge3str))), + (SigmaDsl.groupGenerator, + success(Helpers.decodeBytes("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"))), + (SigmaDsl.groupIdentity, + success(Helpers.decodeBytes("000000000000000000000000000000000000000000000000000000000000000000"))) + ) + }, + existingFeature((x: GroupElement) => x.getEncoded, + "{ (x: GroupElement) => x.getEncoded }", + FuncValue( + Vector((1, SGroupElement)), + MethodCall(ValUse(1, SGroupElement), SGroupElement.getMethodByName("getEncoded"), Vector(), Map()) + ))) + } + property("decodePoint(GroupElement.getEncoded) equivalence") { verifyCases( - { - def success[T](v: T) = Expected(Success(v), 38340) - Seq( - (ge1, success(true)), - (ge2, success(true)), - (ge3, success(true)), - (SigmaDsl.groupGenerator, success(true)), - (SigmaDsl.groupIdentity, success(true)) + { + val costDetails = TracedCost( + traceBase ++ Array( + FixedCostItem(PropertyCall), + FixedCostItem(MethodDesc(SGroupElement.GetEncodedMethod), FixedCost(JitCost(250))), + FixedCostItem(DecodePoint), + FixedCostItem(ValUse), + FixedCostItem(NamedDesc("EQ_GroupElement"), FixedCost(JitCost(172))) ) - }, - existingFeature({ (x: GroupElement) => decodePoint(x.getEncoded) == x }, - "{ (x: GroupElement) => decodePoint(x.getEncoded) == x }", - FuncValue( - Vector((1, SGroupElement)), - EQ( - DecodePoint( - MethodCall.typed[Value[SCollection[SByte.type]]]( - ValUse(1, SGroupElement), - SGroupElement.getMethodByName("getEncoded"), - Vector(), - Map() - ) - ), - ValUse(1, SGroupElement) + ) + def success[T](v: T) = Expected(Success(v), 38340, costDetails, 1837) + Seq( + (ge1, success(true)), + (ge2, success(true)), + (ge3, success(true)), + (SigmaDsl.groupGenerator, success(true)), + (SigmaDsl.groupIdentity, success(true)) + ) + }, + existingFeature({ (x: GroupElement) => decodePoint(x.getEncoded) == x }, + "{ (x: GroupElement) => decodePoint(x.getEncoded) == x }", + FuncValue( + Vector((1, SGroupElement)), + EQ( + DecodePoint( + MethodCall.typed[Value[SCollection[SByte.type]]]( + ValUse(1, SGroupElement), + SGroupElement.getMethodByName("getEncoded"), + Vector(), + Map() ) - ))) + ), + ValUse(1, SGroupElement) + ) + ))) + } + property("GroupElement.negate equivalence") { verifyCases( - { - def success[T](v: T) = Expected(Success(v), 36292) - Seq( - (ge1, success(Helpers.decodeGroupElement("02358d53f01276211f92d0aefbd278805121d4ff6eb534b777af1ee8abae5b2056"))), - (ge2, success(Helpers.decodeGroupElement("03dba7b94b111f3894e2f9120b577da595ec7d58d488485adf73bf4e153af63575"))), - (ge3, success(Helpers.decodeGroupElement("0390449814f5671172dd696a61b8aa49aaa4c87013f56165e27d49944e98bc414d"))), - (SigmaDsl.groupGenerator, success(Helpers.decodeGroupElement("0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"))), - (SigmaDsl.groupIdentity, success(Helpers.decodeGroupElement("000000000000000000000000000000000000000000000000000000000000000000"))) - ) - }, - existingFeature({ (x: GroupElement) => x.negate }, - "{ (x: GroupElement) => x.negate }", - FuncValue( - Vector((1, SGroupElement)), - MethodCall(ValUse(1, SGroupElement), SGroupElement.getMethodByName("negate"), Vector(), Map()) - ))) - - // TODO HF (3h): related to https://github.com/ScorexFoundation/sigmastate-interpreter/issues/479 - // val isIdentity = existingFeature({ (x: GroupElement) => x.isIdentity }, - // "{ (x: GroupElement) => x.isIdentity }") + { + def success[T](v: T) = Expected(Success(v), 36292, methodCostDetails(SGroupElement.NegateMethod, 45), 1785) + Seq( + (ge1, success(Helpers.decodeGroupElement("02358d53f01276211f92d0aefbd278805121d4ff6eb534b777af1ee8abae5b2056"))), + (ge2, success(Helpers.decodeGroupElement("03dba7b94b111f3894e2f9120b577da595ec7d58d488485adf73bf4e153af63575"))), + (ge3, success(Helpers.decodeGroupElement("0390449814f5671172dd696a61b8aa49aaa4c87013f56165e27d49944e98bc414d"))), + (SigmaDsl.groupGenerator, success(Helpers.decodeGroupElement("0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"))), + (SigmaDsl.groupIdentity, success(Helpers.decodeGroupElement("000000000000000000000000000000000000000000000000000000000000000000"))) + ) + }, + existingFeature({ (x: GroupElement) => x.negate }, + "{ (x: GroupElement) => x.negate }", + FuncValue( + Vector((1, SGroupElement)), + MethodCall(ValUse(1, SGroupElement), SGroupElement.getMethodByName("negate"), Vector(), Map()) + ))) + } - verifyCases( - { - def success[T](v: T) = Expected(Success(v), 41484) - Seq( - ((ge1, CBigInt(new BigInteger("-25c80b560dd7844e2efd10f80f7ee57d", 16))), - success(Helpers.decodeGroupElement("023a850181b7b73f92a5bbfa0bfc78f5bbb6ff00645ddde501037017e1a2251e2e"))), - ((ge2, CBigInt(new BigInteger("2488741265082fb02b09f992be3dd8d60d2bbe80d9e2630", 16))), - success(Helpers.decodeGroupElement("032045b928fb7774a4cd9ef5fa8209f4e493cd4cc5bd536b52746a53871bf73431"))), - ((ge3, CBigInt(new BigInteger("-33e8fbdb13d2982e92583445e1fdcb5901a178a7aa1e100", 16))), - success(Helpers.decodeGroupElement("036128efaf14d8ac2812a662f6494dc617b87986a3dc6b4a59440048a7ac7d2729"))), - ((ge3, CBigInt(new BigInteger("1", 16))), - success(ge3)) - ) - }, - existingFeature({ (x: (GroupElement, BigInt)) => x._1.exp(x._2) }, - "{ (x: (GroupElement, BigInt)) => x._1.exp(x._2) }", - FuncValue( - Vector((1, STuple(Vector(SGroupElement, SBigInt)))), - Exponentiate( - SelectField.typed[Value[SGroupElement.type]]( - ValUse(1, STuple(Vector(SGroupElement, SBigInt))), - 1.toByte - ), - SelectField.typed[Value[SBigInt.type]]( - ValUse(1, STuple(Vector(SGroupElement, SBigInt))), - 2.toByte + property("GroupElement.exp equivalence") { + val costDetails = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(Exponentiate) + ) + ) + val cases = { + def success[T](v: T) = Expected(Success(v), 41484, costDetails, 1873) + Seq( + ((ge1, CBigInt(new BigInteger("-25c80b560dd7844e2efd10f80f7ee57d", 16))), + success(Helpers.decodeGroupElement("023a850181b7b73f92a5bbfa0bfc78f5bbb6ff00645ddde501037017e1a2251e2e"))), + ((ge2, CBigInt(new BigInteger("2488741265082fb02b09f992be3dd8d60d2bbe80d9e2630", 16))), + success(Helpers.decodeGroupElement("032045b928fb7774a4cd9ef5fa8209f4e493cd4cc5bd536b52746a53871bf73431"))), + ((ge3, CBigInt(new BigInteger("-33e8fbdb13d2982e92583445e1fdcb5901a178a7aa1e100", 16))), + success(Helpers.decodeGroupElement("036128efaf14d8ac2812a662f6494dc617b87986a3dc6b4a59440048a7ac7d2729"))), + ((ge3, CBigInt(new BigInteger("1", 16))), + success(ge3)) + ) + } + val scalaFunc = { (x: (GroupElement, BigInt)) => x._1.exp(x._2) } + val script = "{ (x: (GroupElement, BigInt)) => x._1.exp(x._2) }" + if (lowerMethodCallsInTests) { + verifyCases(cases, + existingFeature( + scalaFunc, + script, + FuncValue( + Vector((1, STuple(Vector(SGroupElement, SBigInt)))), + Exponentiate( + SelectField.typed[Value[SGroupElement.type]]( + ValUse(1, STuple(Vector(SGroupElement, SBigInt))), + 1.toByte + ), + SelectField.typed[Value[SBigInt.type]]( + ValUse(1, STuple(Vector(SGroupElement, SBigInt))), + 2.toByte + ) ) - ) - ))) + ))) + } else { + forEachScriptAndErgoTreeVersion(activatedVersions, ergoTreeVersions) { + testCases( + Seq( + ((ge1, CBigInt(new BigInteger("-25c80b560dd7844e2efd10f80f7ee57d", 16))), + Expected( + // in v4.x exp method cannot be called via MethodCall ErgoTree node + Failure(new NoSuchMethodException("sigmastate.eval.CostingRules$GroupElementCoster.exp(scalan.Base$Ref)")), + cost = 41484, + CostDetails.ZeroCost, + newCost = 0, + newVersionedResults = { + // in v5.0 MethodCall ErgoTree node is allowed + val res = ExpectedResult( + Success(Helpers.decodeGroupElement("023a850181b7b73f92a5bbfa0bfc78f5bbb6ff00645ddde501037017e1a2251e2e")), + Some(999) + ) + val details = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(SGroupElement.ExponentiateMethod, FixedCost(JitCost(900))) + ) + ) + Seq( // expected result for each version + 0 -> ( res -> Some(details) ), + 1 -> ( res -> Some(details) ), + 2 -> ( res -> Some(details) ) + ) + } + ) + ) + ), + changedFeature( + (x: (GroupElement, BigInt)) => + throw new NoSuchMethodException("sigmastate.eval.CostingRules$GroupElementCoster.exp(scalan.Base$Ref)"), + scalaFunc, ""/*can't be compiled in v4.x*/, + FuncValue( + Vector((1, STuple(Vector(SGroupElement, SBigInt)))), + MethodCall( + SelectField.typed[Value[SGroupElement.type]]( + ValUse(1, STuple(Vector(SGroupElement, SBigInt))), + 1.toByte + ), + SGroupElement.getMethodByName("exp"), + Vector( + SelectField.typed[Value[SBigInt.type]]( + ValUse(1, STuple(Vector(SGroupElement, SBigInt))), + 2.toByte + ) + ), + Map() + ) + ), + allowNewToSucceed = true, + allowDifferentErrors = true + )) + } + } + } - verifyCases( + property("GroupElement.multiply equivalence") { + val scalaFunc = { (x: (GroupElement, GroupElement)) => x._1.multiply(x._2) } + if (lowerMethodCallsInTests) { + verifyCases( { - def success[T](v: T) = Expected(Success(v), 36457) + val costDetails = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MultiplyGroup) + ) + ) + def success[T](v: T) = Expected(Success(v), 36457, costDetails, 1787) Seq( ((ge1, Helpers.decodeGroupElement("03e132ca090614bd6c9f811e91f6daae61f16968a1e6c694ed65aacd1b1092320e")), success(Helpers.decodeGroupElement("02bc48937b4a66f249a32dfb4d2efd0743dc88d46d770b8c5d39fd03325ba211df"))), @@ -2206,7 +2892,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui success(ge3)) ) }, - existingFeature({ (x: (GroupElement, GroupElement)) => x._1.multiply(x._2) }, + existingFeature(scalaFunc, "{ (x: (GroupElement, GroupElement)) => x._1.multiply(x._2) }", FuncValue( Vector((1, STuple(Vector(SGroupElement, SGroupElement)))), @@ -2221,8 +2907,70 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ) ))) + } else { + val cases = Seq( + ((ge1, Helpers.decodeGroupElement("03e132ca090614bd6c9f811e91f6daae61f16968a1e6c694ed65aacd1b1092320e")), + Expected( + // in v4.x exp method cannot be called via MethodCall ErgoTree node + Failure(new NoSuchMethodException("sigmastate.eval.CostingRules$GroupElementCoster.multiply(scalan.Base$Ref)")), + cost = 41484, + CostDetails.ZeroCost, + newCost = 0, + newVersionedResults = { + // in v5.0 MethodCall ErgoTree node is allowed + val res = ExpectedResult( + Success(Helpers.decodeGroupElement("02bc48937b4a66f249a32dfb4d2efd0743dc88d46d770b8c5d39fd03325ba211df")), + Some(139) + ) + val details = TracedCost(traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(SGroupElement.MultiplyMethod, FixedCost(JitCost(40))) + )) + Seq.tabulate(3)(_ -> ( res -> Some(details) )) // expected result for each version + } + ) + ) + ) + forEachScriptAndErgoTreeVersion(activatedVersions, ergoTreeVersions) { + testCases( + cases, + changedFeature( + (_: (GroupElement, GroupElement)) => + throw new NoSuchMethodException("java.lang.NoSuchMethodException: sigmastate.eval.CostingRules$GroupElementCoster.multiply(scalan.Base$Ref)"), + scalaFunc, ""/*can't be compiled in v4.x*/, + FuncValue( + Vector((1, STuple(Vector(SGroupElement, SGroupElement)))), + MethodCall( + SelectField.typed[Value[SGroupElement.type]]( + ValUse(1, STuple(Vector(SGroupElement, SGroupElement))), + 1.toByte + ), + SGroupElement.getMethodByName("multiply"), + Vector( + SelectField.typed[Value[SGroupElement.type]]( + ValUse(1, STuple(Vector(SGroupElement, SGroupElement))), + 2.toByte + ) + ), + Map() + ) + ), + allowNewToSucceed = true, + allowDifferentErrors = true + )) + } + } } + // TODO v6.0 (3h): related to https://github.com/ScorexFoundation/sigmastate-interpreter/issues/479 + // property("GroupElement.isIdentity equivalence") { + // // val isIdentity = existingFeature({ (x: GroupElement) => x.isIdentity }, + // // "{ (x: GroupElement) => x.isIdentity }") + // } + property("AvlTree properties equivalence") { def expectedExprFor(propName: String) = { FuncValue( @@ -2237,7 +2985,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } verifyCases( { - def success[T](v: T) = Expected(Success(v), 36182) + def success[T](v: T) = Expected(Success(v), 36182, methodCostDetails(SAvlTree.digestMethod, 15), 1767) Seq( (t1, success(Helpers.decodeBytes("000183807f66b301530120ff7fc6bd6601ff01ff7f7d2bedbbffff00187fe89094"))), (t2, success(Helpers.decodeBytes("ff000d937f80ffd731ed802d24358001ff8080ff71007f00ad37e0a7ae43fff95b"))), @@ -2250,7 +2998,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36260) + def success[T](v: T) = Expected(Success(v), 36260, methodCostDetails(SAvlTree.enabledOperationsMethod, 15), 1765) Seq( (t1, success(6.toByte)), (t2, success(0.toByte)), @@ -2263,7 +3011,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36136) + def success[T](v: T) = Expected(Success(v), 36136, methodCostDetails(SAvlTree.keyLengthMethod, 15), 1765) Seq( (t1, success(1)), (t2, success(32)), @@ -2276,11 +3024,11 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 37151) + def success[T](v: T, newCost: Int) = Expected(Success(v), 37151, methodCostDetails(SAvlTree.valueLengthOptMethod, 15), newCost) Seq( - (t1, success(Some(1))), - (t2, success(Some(64))), - (t3, success(None)) + (t1, success(Some(1), 1766)), + (t2, success(Some(64), 1766)), + (t3, success(None, 1765)) ) }, existingFeature((t: AvlTree) => t.valueLengthOpt, @@ -2289,7 +3037,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36479) + def success[T](v: T) = Expected(Success(v), 36479, methodCostDetails(SAvlTree.isInsertAllowedMethod, 15), 1765) Seq( (t1, success(false)), (t2, success(false)), @@ -2302,7 +3050,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36096) + def success[T](v: T) = Expected(Success(v), 36096, methodCostDetails(SAvlTree.isUpdateAllowedMethod, 15), 1765) Seq( (t1, success(true)), (t2, success(false)), @@ -2315,7 +3063,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36502) + def success[T](v: T) = Expected(Success(v), 36502, methodCostDetails(SAvlTree.isRemoveAllowedMethod, 15), 1765) Seq( (t1, success(true)), (t2, success(false)), @@ -2515,46 +3263,156 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) )) - val (key, value, _, avlProver) = sampleAvlProver + { + val keysArr = + Array( + Colls.fromArray(Array[Byte](0,14,54,-1,-128,-61,-89,72,27,34,1,-1,62,-48,0,8,74,1,82,-48,-41,100,-66,64,127,45,1,-1,0,-128,-16,-57)), + Colls.fromArray(Array[Byte](127,-128,-1,127,-96,-1,-63,1,25,43,127,-122,127,-43,0,-1,-57,113,-6,1,1,0,15,1,-104,-1,-55,101,115,-94,-128,94)), + Colls.fromArray(Array[Byte](-60,-26,117,124,-92,104,106,-1,-1,-1,-29,121,-98,54,10,-105,127,13,-1,0,-90,0,-125,-13,-1,-79,-87,127,0,127,0,0)), + Colls.fromArray(Array[Byte](84,127,-44,-65,1,-77,39,-128,113,14,1,-128,127,48,-53,60,-5,-19,123,0,-128,127,-28,0,104,-1,0,1,-51,124,-78,67)), + Colls.fromArray(Array[Byte](12,56,-25,-128,1,-128,-128,70,19,37,0,-1,1,87,-20,-128,80,127,-66,-1,83,-1,111,0,-34,0,49,-77,1,0,61,-1)) + ) + val valuesArr = + Array( + Colls.fromArray(Array[Byte](-87,0,-102,88,-128,-54,66,1,-128,-16,0,1,-44,0,35,-32,-23,40,127,-97,49,-1,127,1,-84,115,127,61,-84,-63,-104,-9,-116,-26,9,-93,-128,0,11,127,0,-128,-1,-128,-1,127,-110,-128,0,0,90,-126,28,0,42,-71,-1,37,-26,0,124,-72,68,26,14)), + Colls.fromArray(Array[Byte](84,106,-48,-17,-1,44,127,-128,0,86,-1)), + Colls.fromArray(Array[Byte](127,-128,-60,1,118,-32,-72,-9,101,0,0,-68,-51,8,95,127)), + Colls.fromArray(Array[Byte](127,-88,127,-101,-128,77,-25,-72,-86,127,127,127,-88,0,-128)), + Colls.fromArray(Array[Byte](2,5,-128,127,46,0,1,127,-9,64,-13,0,19,-112,124,1,20,52,65,-31,-112,114,0,18,-1,-88,-128,118,-126,-1,0,112,119,-1,20,84,11,-23,113,-1,71,-77,127,11,1,-128,63,-23,0,127,-55,42,8,127,126,115,59,70,127,102,-109,-128,127,41,-128,127,127,15,1,127,80,-29,0,8,-127,-96,-1,37,106,76,0,-128,-128,-128,-102,52,0,-11,1,-1,71)) + ) + val (_, avlProver) = createAvlTreeAndProver(keysArr.zip(valuesArr):_*) + val keys = Colls.fromArray(keysArr) + val expRes = Colls.fromArray(valuesArr).map(Option(_)) + keys.foreach { key => + avlProver.performOneOperation(Lookup(ADKey @@ key.toArray)) + } + val proof = avlProver.generateProof().toColl + val digest = avlProver.digest.toColl + val tree = SigmaDsl.avlTree(AvlTreeFlags.ReadOnly.serializeToByte, digest, 32, None) + + val input = (tree, (keys, proof)) + val costDetails = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + SeqCostItem(NamedDesc("CreateAvlVerifier"), PerItemCost(JitCost(110), JitCost(20), 64), 456), + SeqCostItem(NamedDesc("LookupAvlTree"), PerItemCost(JitCost(40), JitCost(10), 1), 3), + SeqCostItem(NamedDesc("LookupAvlTree"), PerItemCost(JitCost(40), JitCost(10), 1), 3), + SeqCostItem(NamedDesc("LookupAvlTree"), PerItemCost(JitCost(40), JitCost(10), 1), 3), + SeqCostItem(NamedDesc("LookupAvlTree"), PerItemCost(JitCost(40), JitCost(10), 1), 3), + SeqCostItem(NamedDesc("LookupAvlTree"), PerItemCost(JitCost(40), JitCost(10), 1), 3) + ) + ) + + getMany.checkExpected(input, Expected(Success(expRes), 38991, costDetails, 1845)) + } + + val key = Colls.fromArray(Array[Byte](-16,-128,99,86,1,-128,-36,-83,109,72,-124,-114,1,-32,15,127,-30,125,127,1,-102,-53,-53,-128,-107,0,64,8,1,127,22,1)) + val value = Colls.fromArray(Array[Byte](0,41,-78,1,113,-128,0,-128,1,-92,0,1,1,34,127,-1,56,-1,-60,89,1,-20,-92)) + val (tree, avlProver) = createAvlTreeAndProver(key -> value) val otherKey = key.map(x => (-x).toByte) // any other different from key - val table = Table(("key", "contains", "valueOpt"), - (key, true, Some(value)), - (otherKey, false, None) + // Final cost is baseCost + additionalCost, baseCost is specified in test scenario below + val table = Table(("key", "contains", "valueOpt", "additionalCost", "additionalCostDetails"), + (key, true, Some(value), 2, 23), + (otherKey, false, None, 0, 0) ) - def success[T](v: T) = Expected(Success(v), 0) - - forAll(table) { (key, okContains, valueOpt) => + forAll(table) { (key, okContains, valueOpt, additionalCost, additionalDetails) => avlProver.performOneOperation(Lookup(ADKey @@ key.toArray)) val proof = avlProver.generateProof().toColl val digest = avlProver.digest.toColl val tree = SigmaDsl.avlTree(AvlTreeFlags.ReadOnly.serializeToByte, digest, 32, None) - // positive test - { - val input = (tree, (key, proof)) - contains.checkExpected(input, success(okContains)) - get.checkExpected(input, success(valueOpt)) + def costDetails(i: Int) = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + SeqCostItem(NamedDesc("CreateAvlVerifier"), PerItemCost(JitCost(110), JitCost(20), 64), i), + SeqCostItem(NamedDesc("LookupAvlTree"), PerItemCost(JitCost(40), JitCost(10), 1), 1) + ) + ) - contains.checkVerify(input, Expected(value = Success(okContains), cost = 37850)) - get.checkVerify(input, Expected(value = Success(valueOpt), cost = 38372)) - } + val updateDigestCostDetails = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(SAvlTree.updateDigestMethod, FixedCost(JitCost(40))) + ) + ) + + val updateOperationsCostDetails = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(SAvlTree.updateOperationsMethod, FixedCost(JitCost(45))) + ) + ) + + // positive test + { + val input = (tree, (key, proof)) + contains.checkExpected(input, Expected(Success(okContains), 37850, costDetails(105 + additionalDetails), 1790)) + get.checkExpected(input, Expected(Success(valueOpt), 38372, costDetails(105 + additionalDetails), 1790 + additionalCost)) + } val keys = Colls.fromItems(key) val expRes = Colls.fromItems(valueOpt) { val input = (tree, (keys, proof)) - getMany.checkExpected(input, success(expRes)) - getMany.checkVerify(input, Expected(value = Success(expRes), cost = 38991)) + getMany.checkExpected(input, Expected(Success(expRes), 38991, costDetails(105 + additionalDetails), 1791 + additionalCost)) } { val input = (tree, digest) val (res, _) = updateDigest.checkEquality(input).getOrThrow res.digest shouldBe digest - updateDigest.checkVerify(input, Expected(value = Success(res), cost = 36341)) + updateDigest.checkExpected(input, Expected(Success(res), 36341, updateDigestCostDetails, 1771)) } val newOps = 1.toByte @@ -2563,7 +3421,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val input = (tree, newOps) val (res,_) = updateOperations.checkEquality(input).getOrThrow res.enabledOperations shouldBe newOps - updateOperations.checkVerify(input, Expected(value = Success(res), cost = 36341)) + updateOperations.checkExpected(input, Expected(Success(res), 36341, updateOperationsCostDetails, 1771)) } // negative tests: invalid proof @@ -2573,7 +3431,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val input = (tree, (key, invalidProof)) val (res, _) = contains.checkEquality(input).getOrThrow res shouldBe false - contains.checkVerify(input, Expected(value = Success(res), cost = 37850)) + contains.checkExpected(input, Expected(Success(res), 37850, costDetails(105 + additionalDetails), 1790)) } { @@ -2602,8 +3460,10 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui proof } - def performRemove(avlProver: BatchProver, key: Coll[Byte]) = { - avlProver.performOneOperation(Remove(ADKey @@ key.toArray)) + def performRemove(avlProver: BatchProver, keys: Seq[Coll[Byte]]) = { + keys.foreach { key => + avlProver.performOneOperation(Remove(ADKey @@ key.toArray)) + } val proof = avlProver.generateProof().toColl proof } @@ -2685,6 +3545,34 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) )) + val testTraceBase = Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(SAvlTree.isInsertAllowedMethod, FixedCost(JitCost(15))) + ) + val costDetails1 = TracedCost(testTraceBase) + val costDetails2 = TracedCost( + testTraceBase ++ Array( + SeqCostItem(NamedDesc("CreateAvlVerifier"), PerItemCost(JitCost(110), JitCost(20), 64), 70), + SeqCostItem(NamedDesc("InsertIntoAvlTree"), PerItemCost(JitCost(40), JitCost(10), 1), 1), + FixedCostItem(SAvlTree.updateDigestMethod, FixedCost(JitCost(40))) + ) + ) + forAll(keyCollGen, bytesCollGen) { (key, value) => val (tree, avlProver) = createAvlTreeAndProver() val preInsertDigest = avlProver.digest.toColl @@ -2696,7 +3584,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val input = (preInsertTree, (kvs, insertProof)) val (res, _) = insert.checkEquality(input).getOrThrow res.isDefined shouldBe true - insert.checkVerify(input, Expected(value = Success(res), cost = 38501)) + insert.checkExpected(input, Expected(Success(res), 38501, costDetails2, 1796)) } { // negative: readonly tree @@ -2704,7 +3592,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val input = (readonlyTree, (kvs, insertProof)) val (res, _) = insert.checkEquality(input).getOrThrow res.isDefined shouldBe false - insert.checkVerify(input, Expected(value = Success(res), cost = 38501)) + insert.checkExpected(input, Expected(Success(res), 38501, costDetails1, 1772)) } { // negative: invalid key @@ -2713,8 +3601,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val invalidKvs = Colls.fromItems((invalidKey -> value)) // NOTE, insertProof is based on `key` val input = (tree, (invalidKvs, insertProof)) val (res, _) = insert.checkEquality(input).getOrThrow - res.isDefined shouldBe true // TODO HF: should it really be true? (looks like a bug) - insert.checkVerify(input, Expected(value = Success(res), cost = 38501)) + res.isDefined shouldBe true // TODO v6.0: should it really be true? (looks like a bug) + insert.checkExpected(input, Expected(Success(res), 38501, costDetails2, 1796)) } { // negative: invalid proof @@ -2796,67 +3684,95 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui )) val cost = 40952 - def success[T](v: T) = Expected(Success(v), 0) - - forAll(keyCollGen, bytesCollGen) { (key, value) => - val (_, avlProver) = createAvlTreeAndProver(key -> value) - val preUpdateDigest = avlProver.digest.toColl - val newValue = bytesCollGen.sample.get - val updateProof = performUpdate(avlProver, key, newValue) - val kvs = Colls.fromItems((key -> newValue)) - val endDigest = avlProver.digest.toColl + val testTraceBase = Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(SAvlTree.isUpdateAllowedMethod, FixedCost(JitCost(15))) + ) + val costDetails1 = TracedCost(testTraceBase) + val costDetails2 = TracedCost( + testTraceBase ++ Array( + SeqCostItem(NamedDesc("CreateAvlVerifier"), PerItemCost(JitCost(110), JitCost(20), 64), 111), + SeqCostItem(NamedDesc("UpdateAvlTree"), PerItemCost(JitCost(120), JitCost(20), 1), 1), + FixedCostItem(SAvlTree.updateDigestMethod, FixedCost(JitCost(40))) + ) + ) + val costDetails3 = TracedCost( + testTraceBase ++ Array( + SeqCostItem(NamedDesc("CreateAvlVerifier"), PerItemCost(JitCost(110), JitCost(20), 64), 111), + SeqCostItem(NamedDesc("UpdateAvlTree"), PerItemCost(JitCost(120), JitCost(20), 1), 1) + ) + ) - { // positive: update to newValue - val preUpdateTree = createTree(preUpdateDigest, updateAllowed = true) - val endTree = preUpdateTree.updateDigest(endDigest) - val input = (preUpdateTree, (kvs, updateProof)) - val res = Some(endTree) - update.checkExpected(input, success(res)) - update.checkVerify(input, Expected(value = Success(res), cost = cost)) - } + val key = Colls.fromArray(Array[Byte](1,0,1,1,73,-67,-128,1,1,0,93,0,127,87,95,51,1,127,1,-3,74,-66,-128,1,89,-18,1,-1,-62,0,-33,51)) + val value = Colls.fromArray(Array[Byte](1,-50,1,-128,120,1)) + val (_, avlProver) = createAvlTreeAndProver(key -> value) + val preUpdateDigest = avlProver.digest.toColl + // val newValue = bytesCollGen.sample.get + val newValue = Colls.fromArray(Array[Byte](2,-1,127,91,0,-1,-128,-1,38,-128,-105,-68,-128,-128,127,127,127,-74,88,127,127,127,-81,-30,-89,121,127,-1,-1,-34,127,1,-12,-128,108,75,127,-14,-63,-128,-103,127,1,-57,0,1,-128,127,-85,23,0,-128,70,-110,127,-85,-30,15,-1,-71,0,127,1,42,127,-118,-1,0,-53,126,42,0,127,127,0,-10,-1,127,19,-4,-1,-88,-128,-96,61,-116,127,-111,6,-128,-1,-86,-39,114,0,127,-92,40)) + val updateProof = performUpdate(avlProver, key, newValue) + val kvs = Colls.fromItems((key -> newValue)) + val endDigest = avlProver.digest.toColl + + { // positive: update to newValue + val preUpdateTree = createTree(preUpdateDigest, updateAllowed = true) + val endTree = preUpdateTree.updateDigest(endDigest) + val input = (preUpdateTree, (kvs, updateProof)) + val res = Some(endTree) + update.checkExpected(input, Expected(Success(res), cost, costDetails2, 1805)) + } - { // positive: update to the same value (identity operation) - val tree = createTree(preUpdateDigest, updateAllowed = true) - val keys = Colls.fromItems((key -> value)) - val input = (tree, (keys, updateProof)) - val res = Some(tree) - update.checkExpected(input, success(res)) - update.checkVerify(input, Expected(value = Success(res), cost = cost)) - } + { // positive: update to the same value (identity operation) + val tree = createTree(preUpdateDigest, updateAllowed = true) + val keys = Colls.fromItems((key -> value)) + val input = (tree, (keys, updateProof)) + val res = Some(tree) + update.checkExpected(input, Expected(Success(res), cost, costDetails2, 1805)) + } - { // negative: readonly tree - val readonlyTree = createTree(preUpdateDigest) - val input = (readonlyTree, (kvs, updateProof)) - update.checkExpected(input, success(None)) - update.checkVerify(input, Expected(value = Success(None), cost = cost)) - } + { // negative: readonly tree + val readonlyTree = createTree(preUpdateDigest) + val input = (readonlyTree, (kvs, updateProof)) + update.checkExpected(input, Expected(Success(None), cost, costDetails1, 1772)) + } - { // negative: invalid key - val tree = createTree(preUpdateDigest, updateAllowed = true) - val invalidKey = key.map(x => (-x).toByte) // any other different from key - val invalidKvs = Colls.fromItems((invalidKey -> newValue)) - val input = (tree, (invalidKvs, updateProof)) - update.checkExpected(input, success(None)) - update.checkVerify(input, Expected(value = Success(None), cost = cost)) - } + { // negative: invalid key + val tree = createTree(preUpdateDigest, updateAllowed = true) + val invalidKey = key.map(x => (-x).toByte) // any other different from key + val invalidKvs = Colls.fromItems((invalidKey -> newValue)) + val input = (tree, (invalidKvs, updateProof)) + update.checkExpected(input, Expected(Success(None), cost, costDetails3, 1801)) + } - { // negative: invalid value (different from the value in the proof) - val tree = createTree(preUpdateDigest, updateAllowed = true) - val invalidValue = newValue.map(x => (-x).toByte) - val invalidKvs = Colls.fromItems((key -> invalidValue)) - val input = (tree, (invalidKvs, updateProof)) - val (res, _) = update.checkEquality(input).getOrThrow - res.isDefined shouldBe true // TODO HF: should it really be true? (looks like a bug) - update.checkVerify(input, Expected(value = Success(res), cost = cost)) - } + { // negative: invalid value (different from the value in the proof) + val tree = createTree(preUpdateDigest, updateAllowed = true) + val invalidValue = newValue.map(x => (-x).toByte) + val invalidKvs = Colls.fromItems((key -> invalidValue)) + val input = (tree, (invalidKvs, updateProof)) + val (res, _) = update.checkEquality(input).getOrThrow + res.isDefined shouldBe true // TODO v6.0: should it really be true? (looks like a bug) + update.checkExpected(input, Expected(Success(res), cost, costDetails2, 1805)) + } - { // negative: invalid proof - val tree = createTree(preUpdateDigest, updateAllowed = true) - val invalidProof = updateProof.map(x => (-x).toByte) // any other different from proof - val input = (tree, (kvs, invalidProof)) - update.checkExpected(input, success(None)) - update.checkVerify(input, Expected(value = Success(None), cost = cost)) - } + { // negative: invalid proof + val tree = createTree(preUpdateDigest, updateAllowed = true) + val invalidProof = updateProof.map(x => (-x).toByte) // any other different from proof + val input = (tree, (kvs, invalidProof)) + update.checkExpected(input, Expected(Success(None), cost, costDetails3, 1801)) } } @@ -2897,12 +3813,85 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) )) - def success[T](v: T) = Expected(Success(v), 0) + val testTraceBase = Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(SAvlTree.isRemoveAllowedMethod, FixedCost(JitCost(15))) + ) + val costDetails1 = TracedCost( + testTraceBase ++ Array( + SeqCostItem(NamedDesc("CreateAvlVerifier"), PerItemCost(JitCost(110), JitCost(20), 64), 436), + SeqCostItem(NamedDesc("RemoveAvlTree"), PerItemCost(JitCost(100), JitCost(15), 1), 3), + SeqCostItem(NamedDesc("RemoveAvlTree"), PerItemCost(JitCost(100), JitCost(15), 1), 3), + FixedCostItem(SAvlTree.digestMethod, FixedCost(JitCost(15))), + FixedCostItem(SAvlTree.updateDigestMethod, FixedCost(JitCost(40))) + ) + ) + val costDetails2 = TracedCost( + testTraceBase ++ Array( + SeqCostItem(NamedDesc("CreateAvlVerifier"), PerItemCost(JitCost(110), JitCost(20), 64), 140), + SeqCostItem(NamedDesc("RemoveAvlTree"), PerItemCost(JitCost(100), JitCost(15), 1), 1), + FixedCostItem(SAvlTree.digestMethod, FixedCost(JitCost(15))), + FixedCostItem(SAvlTree.updateDigestMethod, FixedCost(JitCost(40))) + ) + ) + val costDetails3 = TracedCost(testTraceBase) + val costDetails4 = TracedCost( + testTraceBase ++ Array( + SeqCostItem(NamedDesc("CreateAvlVerifier"), PerItemCost(JitCost(110), JitCost(20), 64), 140), + SeqCostItem(NamedDesc("RemoveAvlTree"), PerItemCost(JitCost(100), JitCost(15), 1), 1), + FixedCostItem(SAvlTree.digestMethod, FixedCost(JitCost(15))) + ) + ) - forAll(keyCollGen, bytesCollGen) { (key, value) => + { + val keys = Array( + Colls.fromArray(Array[Byte](6,71,1,-123,-1,17,-1,-1,-128,-1,-99,127,2,-37,-1,-17,-1,-90,-33,-50,-122,127,1,127,-81,1,-57,118,-38,-36,-2,1)), + Colls.fromArray(Array[Byte](-84,-128,1,0,30,0,73,127,71,1,0,39,127,-1,-109,66,0,1,-128,-43,-1,-95,-55,-97,12,1,-1,0,-128,-1,5,0)), + Colls.fromArray(Array[Byte](0,-84,1,-1,-128,93,-21,0,-128,0,-128,-128,85,-128,-123,-1,-70,0,38,-1,123,-1,1,21,0,-11,-59,122,-108,-64,1,124)), + Colls.fromArray(Array[Byte](1,-29,-1,0,-1,0,0,0,1,44,0,2,0,17,1,-36,50,58,118,-125,108,-93,3,65,-128,77,127,109,-121,-61,-128,-128)), + Colls.fromArray(Array[Byte](127,0,0,70,127,1,-109,60,-128,-106,-77,-1,-1,0,127,-108,-128,0,0,-27,-9,-128,89,127,107,68,-76,3,-102,127,4,0)) + ) + val values = Array( + Colls.fromArray(Array[Byte](-1,91,0,31,-86,-1,39,127,76,78,-1,0,0,-112,36,-16,55,-9,-21,45,0,-128,23,-1,1,-128,63,-33,-60,117,116,-53,-19,-1,0,1,-128,0,127,127,16,127,-84,-128,0,1,-5,1,-128,-103,114,-128,-105,-128,79,62,-1,46,0,-128,-40,-1,89,40,103,1,44,-128,97,-107,111,-1,0,-8,1,42,-38,88,127,127,118,127,127,127,-6,-1,20,32,-128,-1,69,1,127,1,127,22,-128,127)), + Colls.fromArray(Array[Byte](-75,-38,-1,0,-127,-104,-128,-128,-47,113,98,-128,-120,101,1,-128,1,-128,19,1,0,10,88,90,-1,-49,-13,127,26,82,-1,-1,-1,1,-1,-62,-128,-128)), + Colls.fromArray(Array[Byte](-95,-76,-128,-128,127,16,0,-1,-18,1,-93,1,127,-128,1,-92,111,-128,59,-1,-128,-1,96,-87,127,101,14,73,-9,-128,-1,1,-128,-1,127,-72,6,127,1,67,-1,-128,3,111,1,1,127,-118,127,43,-99,1,-128,0,127,-128,1,-128,-128,100,1,73,0,127,1,-121,1,104,-50,0,0,-1,119,127,80,-69,-128,23,-128,1,-1,127,18,-128,124,-128)), + Colls.fromArray(Array[Byte](-65,-128,127,0,-7,35,-128,-127,-120,-128,-8,64,-128,16)), + Colls.fromArray(Array[Byte](127,100,8,-128,1,56,113,-35,-50,53,-128,-61,-1)) + ) + val (_, avlProver) = createAvlTreeAndProver(keys.zip(values):_*) + val preRemoveDigest = avlProver.digest.toColl + val keysToRemove = keys.zipWithIndex.collect { case (k, i) if i % 2 != 0 => k } + val removeProof = performRemove(avlProver, keysToRemove) + val endDigest = avlProver.digest.toColl + + val preRemoveTree = createTree(preRemoveDigest, removeAllowed = true) + val endTree = preRemoveTree.updateDigest(endDigest) + val input = (preRemoveTree, (Colls.fromArray(keysToRemove), removeProof)) + val res = Some(endTree) + remove.checkExpected(input, Expected(Success(res), 38270, costDetails1, 1832)) + } + + { + val key = Colls.fromArray(Array[Byte](-60,42,60,-1,-128,-122,107,-1,-1,-128,47,24,-1,-13,-40,-58,-1,127,-41,-12,100,0,15,-108,-41,127,-7,-1,126,-1,-1,115)) + val value = Colls.fromArray(Array[Byte](0,-40,1,1,-60,-119,-68,0,-128,-128,127,-3,5,54,-1,49,47,33,126,-82,-115,1,0,-123,1,15,-1,-49,-107,73,-1)) val (_, avlProver) = createAvlTreeAndProver(key -> value) val preRemoveDigest = avlProver.digest.toColl - val removeProof = performRemove(avlProver, key) + val removeProof = performRemove(avlProver, Array(key)) val endDigest = avlProver.digest.toColl val keys = Colls.fromItems(key) val cost = 38270 @@ -2912,15 +3901,13 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val endTree = preRemoveTree.updateDigest(endDigest) val input = (preRemoveTree, (keys, removeProof)) val res = Some(endTree) - remove.checkExpected(input, success(res)) - remove.checkVerify(input, Expected(value = Success(res), cost = cost)) + remove.checkExpected(input, Expected(Success(res), cost, costDetails2, 1806)) } { // negative: readonly tree val readonlyTree = createTree(preRemoveDigest) val input = (readonlyTree, (keys, removeProof)) - remove.checkExpected(input, success(None)) - remove.checkVerify(input, Expected(value = Success(None), cost = cost)) + remove.checkExpected(input, Expected(Success(None), cost, costDetails3, 1772)) } { // negative: invalid key @@ -2928,24 +3915,23 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val invalidKey = key.map(x => (-x).toByte) // any other different from `key` val invalidKeys = Colls.fromItems(invalidKey) val input = (tree, (invalidKeys, removeProof)) - remove.checkExpected(input, success(None)) - remove.checkVerify(input, Expected(value = Success(None), cost = cost)) + remove.checkExpected(input, Expected(Success(None), cost, costDetails4, 1802)) } { // negative: invalid proof val tree = createTree(preRemoveDigest, removeAllowed = true) val invalidProof = removeProof.map(x => (-x).toByte) // any other different from `removeProof` val input = (tree, (keys, invalidProof)) - remove.checkExpected(input, success(None)) - remove.checkVerify(input, Expected(value = Success(None), cost = cost)) + remove.checkExpected(input, Expected(Success(None), cost, costDetails4, 1802)) } } } property("longToByteArray equivalence") { + val costDetails = CostDetails(traceBase :+ FixedCostItem(CompanionDesc(LongToByteArray), FixedCost(JitCost(17)))) verifyCases( { - def success[T](v: T) = Expected(Success(v), 36007) + def success[T](v: T) = Expected(Success(v), 36007, costDetails, 1767) Seq( (-9223372036854775808L, success(Helpers.decodeBytes("8000000000000000"))), (-1148502660425090565L, success(Helpers.decodeBytes("f00fb2ea55c579fb"))), @@ -2962,9 +3948,10 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("byteArrayToBigInt equivalence") { + val costDetails = CostDetails(traceBase :+ FixedCostItem(CompanionDesc(ByteArrayToBigInt), FixedCost(JitCost(30)))) verifyCases( { - def success[T](v: T) = Expected(Success(v), 36536) + def success[T](v: T) = Expected(Success(v), 36536, costDetails, 1767) Seq( (Helpers.decodeBytes(""), Expected(new NumberFormatException("Zero length BigInteger"))), @@ -2975,9 +3962,9 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui (Helpers.decodeBytes("ff"), success(CBigInt(new BigInteger("-1", 16)))), (Helpers.decodeBytes("80d6c201"), - Expected(Success(CBigInt(new BigInteger("-7f293dff", 16))), 36539)), + Expected(Success(CBigInt(new BigInteger("-7f293dff", 16))), 36539, costDetails, 1767)), (Helpers.decodeBytes("70d6c20170d6c201"), - Expected(Success(CBigInt(new BigInteger("70d6c20170d6c201", 16))), 36543)), + Expected(Success(CBigInt(new BigInteger("70d6c20170d6c201", 16))), 36543, costDetails, 1767)), (Helpers.decodeBytes( "80e0ff7f02807fff72807f0a00ff7fb7c57f75c11ba2802970fd250052807fc37f6480ffff007fff18eeba44" ), Expected(new ArithmeticException("BigInteger out of 256 bit range"))) @@ -2989,9 +3976,10 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("byteArrayToLong equivalence") { + val costDetails = CostDetails(traceBase :+ FixedCostItem(CompanionDesc(ByteArrayToLong), FixedCost(JitCost(16)))) verifyCases( { - def success[T](v: T) = Expected(Success(v), 36092) + def success[T](v: T) = Expected(Success(v), 36092, costDetails, 1765) Seq( (Helpers.decodeBytes(""), Expected(new IllegalArgumentException("array too small: 0 < 8"))), (Helpers.decodeBytes("81"), Expected(new IllegalArgumentException("array too small: 1 < 8"))), @@ -3021,8 +4009,9 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Box properties equivalence") { verifyCases( { - def success[T](v: T) = Expected(Success(v), 35984) - Seq( + val costDetails = CostDetails(traceBase :+ FixedCostItem(CompanionDesc(ExtractId), FixedCost(JitCost(12)))) + def success[T](v: T) = Expected(Success(v), 35984, costDetails, 1766) + Seq( (b1, success(Helpers.decodeBytes("5ee78f30ae4e770e44900a46854e9fecb6b12e8112556ef1cd19aef633b4421e"))), (b2, success(Helpers.decodeBytes("3a0089be265460e29ca47d26e5b55a6f3e3ffaf5b4aed941410a2437913848ad"))) ) @@ -3033,7 +4022,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 35882) + val costDetails = CostDetails(traceBase :+ FixedCostItem(CompanionDesc(ExtractAmount), FixedCost(JitCost(8)))) + def success[T](v: T) = Expected(Success(v), 35882, costDetails, 1764) Seq( (b1, success(9223372036854775807L)), (b2, success(12345L)) @@ -3045,7 +4035,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 35938) + val costDetails = CostDetails(traceBase :+ FixedCostItem(CompanionDesc(ExtractScriptBytes), FixedCost(JitCost(10)))) + def success[T](v: T) = Expected(Success(v), 35938, costDetails, 1766) Seq( (b1, success(Helpers.decodeBytes( "100108cd0297c44a12f4eb99a85d298fa3ba829b5b42b9f63798c980ece801cc663cc5fc9e7300" @@ -3059,7 +4050,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36012) + val costDetails = CostDetails(traceBase :+ FixedCostItem(CompanionDesc(ExtractBytes), FixedCost(JitCost(12)))) + def success[T](v: T) = Expected(Success(v), 36012, costDetails, 1766) Seq( (b1, success(Helpers.decodeBytes( "ffffffffffffffff7f100108cd0297c44a12f4eb99a85d298fa3ba829b5b42b9f63798c980ece801cc663cc5fc9e73009fac29026e789ab7b2fffff12280a6cd01557f6fb22b7f80ff7aff8e1f7f15973d7f000180ade204a3ff007f00057600808001ff8f8000019000ffdb806fff7cc0b6015eb37fa600f4030201000e067fc87f7f01ff218301ae8000018008637f0021fb9e00018055486f0b514121016a00ff718080bcb001" @@ -3075,7 +4067,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 35954) + val costDetails = CostDetails(traceBase :+ FixedCostItem(CompanionDesc(ExtractBytesWithNoRef), FixedCost(JitCost(12)))) + def success[T](v: T) = Expected(Success(v), 35954, costDetails, 1766) Seq( (b1, success(Helpers.decodeBytes( "ffffffffffffffff7f100108cd0297c44a12f4eb99a85d298fa3ba829b5b42b9f63798c980ece801cc663cc5fc9e73009fac29026e789ab7b2fffff12280a6cd01557f6fb22b7f80ff7aff8e1f7f15973d7f000180ade204a3ff007f00057600808001ff8f8000019000ffdb806fff7cc0b6015eb37fa600f4030201000e067fc87f7f01ff" @@ -3091,7 +4084,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36074) + val costDetails = CostDetails(traceBase :+ FixedCostItem(CompanionDesc(ExtractCreationInfo), FixedCost(JitCost(16)))) + def success[T](v: T) = Expected(Success(v), 36074, costDetails, 1767) Seq( (b1, success(( 677407, @@ -3107,15 +4101,15 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui "{ (x: Box) => x.creationInfo }", FuncValue(Vector((1, SBox)), ExtractCreationInfo(ValUse(1, SBox))))) - // TODO HF (2h): fix collections equality and remove map(identity) + // TODO v6.0 (2h): fix collections equality and remove map(identity) // (PairOfColl should be equal CollOverArray) verifyCases( Seq( b1 -> Expected(Success(Coll[(Coll[Byte], Long)]( (Helpers.decodeBytes("6e789ab7b2fffff12280a6cd01557f6fb22b7f80ff7aff8e1f7f15973d7f0001"), 10000000L), (Helpers.decodeBytes("a3ff007f00057600808001ff8f8000019000ffdb806fff7cc0b6015eb37fa600"), 500L) - ).map(identity)), 36167), - b2 -> Expected(Success(Coll[(Coll[Byte], Long)]().map(identity)), 36157) + ).map(identity)), 36167, methodCostDetails(SBox.tokensMethod, 15), 1772), + b2 -> Expected(Success(Coll[(Coll[Byte], Long)]().map(identity)), 36157, methodCostDetails(SBox.tokensMethod, 15), 1766) ), existingFeature({ (x: Box) => x.tokens }, "{ (x: Box) => x.tokens }", @@ -3131,7 +4125,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("Box properties equivalence (new features)") { - // TODO HF (4h): related to https://github.com/ScorexFoundation/sigmastate-interpreter/issues/416 + // TODO v6.0 (4h): related to https://github.com/ScorexFoundation/sigmastate-interpreter/issues/416 val getReg = newFeature((x: Box) => x.getReg[Int](1).get, "{ (x: Box) => x.getReg[Int](1).get }") @@ -3156,14 +4150,49 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ErgoBox.R4 -> ByteConstant(2.toByte) )) val box4 = boxWithRegisters(Map.empty) + val expCostDetails1 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(ExtractRegisterAs), FixedCost(JitCost(50))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(OptionIsDefined), FixedCost(JitCost(10))), + FixedCostItem(CompanionDesc(If), FixedCost(JitCost(10))), + FixedCostItem(ValUse), + FixedCostItem(OptionGet) + ) + ) + val expCostDetails2 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(ExtractRegisterAs), FixedCost(JitCost(50))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(OptionIsDefined), FixedCost(JitCost(10))), + FixedCostItem(CompanionDesc(If), FixedCost(JitCost(10))), + FixedCostItem(Constant) + ) + ) verifyCases( Seq( - (box1, Expected(Success(1024.toShort), cost = 37190)), + (box1, Expected(Success(1024.toShort), 37190, expCostDetails1, 1774)), (box2, Expected( new InvalidType("Cannot getReg[Short](5): invalid type of value TestValue(1048576) at id=5") )), - (box3, Expected(Success(0.toShort), cost = 37190)) + (box3, Expected(Success(0.toShort), 37190, expCostDetails2, 1772)) ), existingFeature( { (x: Box) => @@ -3194,12 +4223,104 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ))) + val expCostDetails3 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(ExtractRegisterAs), FixedCost(JitCost(50))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(OptionIsDefined), FixedCost(JitCost(10))), + FixedCostItem(CompanionDesc(If), FixedCost(JitCost(10))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(CompanionDesc(If), FixedCost(JitCost(10))), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(ExtractRegisterAs), FixedCost(JitCost(50))), + FixedCostItem(OptionGet), + TypeBasedCostItem(Upcast, SInt) + ) + ) + + val expCostDetails4 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(ExtractRegisterAs), FixedCost(JitCost(50))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(OptionIsDefined), FixedCost(JitCost(10))), + FixedCostItem(CompanionDesc(If), FixedCost(JitCost(10))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(CompanionDesc(If), FixedCost(JitCost(10))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(CompanionDesc(If), FixedCost(JitCost(10))), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(ExtractRegisterAs), FixedCost(JitCost(50))), + FixedCostItem(OptionGet) + ) + ) + + val expCostDetails5 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(ExtractRegisterAs), FixedCost(JitCost(50))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(CompanionDesc(OptionIsDefined), FixedCost(JitCost(10))), + FixedCostItem(CompanionDesc(If), FixedCost(JitCost(10))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(CompanionDesc(If), FixedCost(JitCost(10))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(CompanionDesc(If), FixedCost(JitCost(10))), + FixedCostItem(Constant) + ) + ) + + verifyCases( Seq( - (box1, Expected(Success(1024), cost = 39782)), - (box2, Expected(Success(1024 * 1024), cost = 39782)), - (box3, Expected(Success(0), cost = 39782)), - (box4, Expected(Success(-1), cost = 39782)) + (box1, Expected(Success(1024), cost = 39782, expCostDetails3, 1785)), + (box2, Expected(Success(1024 * 1024), cost = 39782, expCostDetails4, 1786)), + (box3, Expected(Success(0), cost = 39782, expCostDetails5, 1779)), + (box4, Expected(Success(-1), cost = 39782, expCostDetails2, 1772)) ), existingFeature( { (x: Box) => @@ -3278,9 +4399,22 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ErgoBox.R4 -> ByteArrayConstant(Coll(1.toByte)) ))) + val box3 = SigmaDsl.Box(testBox(20, TrueTree, 0, Seq(), Map( + ErgoBox.R4 -> Constant((10, 20L).asInstanceOf[SType#WrappedType], STuple(SInt, SLong)), + ErgoBox.R5 -> Constant((10, Some(20L)).asInstanceOf[SType#WrappedType], STuple(SInt, SOption(SLong))) + // TODO v6.0 (1h): uncomment after DataSerializer support of Option type + // ErgoBox.R6 -> Constant[SOption[SInt.type]](Option(10), SOption(SInt)), + ))) + + val expCostDetails = TracedCost( + traceBase ++ Array( + FixedCostItem(CompanionDesc(ExtractRegisterAs), FixedCost(JitCost(50))), + FixedCostItem(OptionGet) + ) + ) verifyCases( Seq( - (box1, Expected(Success(1.toByte), cost = 36253)), + (box1, Expected(Success(1.toByte), cost = 36253, expCostDetails, 1770)), (box2, Expected(new InvalidType("Cannot getReg[Byte](4): invalid type of value Value(Coll(1)) at id=4"))) ), existingFeature((x: Box) => x.R4[Byte].get, @@ -3292,7 +4426,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( Seq( - (box1, Expected(Success(1024.toShort), cost = 36253)), + (box1, Expected(Success(1024.toShort), cost = 36253, expCostDetails, 1770)), (box2, Expected(new NoSuchElementException("None.get"))) ), existingFeature((x: Box) => x.R5[Short].get, @@ -3304,7 +4438,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( Seq( - (box1, Expected(Success(1024 * 1024), cost = 36253)) + (box1, Expected(Success(1024 * 1024), cost = 36253, expCostDetails, 1770)) ), existingFeature((x: Box) => x.R6[Int].get, "{ (x: Box) => x.R6[Int].get }", @@ -3315,7 +4449,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( Seq( - (box1, Expected(Success(1024.toLong), cost = 36253)) + (box1, Expected(Success(1024.toLong), cost = 36253, expCostDetails, 1770)) ), existingFeature((x: Box) => x.R7[Long].get, "{ (x: Box) => x.R7[Long].get }", @@ -3326,7 +4460,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( Seq( - (box1, Expected(Success(CBigInt(BigInteger.valueOf(222L))), cost = 36253)) + (box1, Expected(Success(CBigInt(BigInteger.valueOf(222L))), cost = 36253, expCostDetails, 1770)) ), existingFeature((x: Box) => x.R8[BigInt].get, "{ (x: Box) => x.R8[BigInt].get }", @@ -3346,7 +4480,10 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui 32, None ) - )), cost = 36253) + )), + cost = 36253, + expCostDetails, + 1770) )), existingFeature((x: Box) => x.R9[AvlTree].get, "{ (x: Box) => x.R9[AvlTree].get }", @@ -3354,6 +4491,52 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui Vector((1, SBox)), OptionGet(ExtractRegisterAs(ValUse(1, SBox), ErgoBox.R9, SOption(SAvlTree))) ))) + + verifyCases( + Seq( + (box3, Expected(Success(10), cost = 36468))//, expCostDetails, 1790)) + ), + existingFeature((x: Box) => x.R4[(Int, Long)].get._1, + "{ (x: Box) => x.R4[(Int, Long)].get._1 }", + FuncValue( + Array((1, SBox)), + SelectField.typed[Value[SInt.type]]( + OptionGet(ExtractRegisterAs(ValUse(1, SBox), ErgoBox.R4, SOption(SPair(SInt, SLong)))), + 1.toByte + ) + ))) + + verifyCases( + Seq( + (box3, Expected(Success(10), cost = 36468))//, expCostDetails, 1790)) + ), + existingFeature((x: Box) => x.R5[(Int, Option[Long])].get._1, + "{ (x: Box) => x.R5[(Int, Option[Long])].get._1 }", + FuncValue( + Array((1, SBox)), + SelectField.typed[Value[SInt.type]]( + OptionGet(ExtractRegisterAs(ValUse(1, SBox), ErgoBox.R5, SOption(SPair(SInt, SOption(SLong))))), + 1.toByte + ) + ))) + + // TODO v6.0 (1h): uncomment after DataSerializer support of Option type + // verifyCases( + // Seq( + // (box3, Expected(Success(20L), cost = 36468)) + // ), + // existingFeature((x: Box) => x.R5[(Int, Option[Long])].get._2.get, + // "{ (x: Box) => x.R5[(Int, Option[Long])].get._2.get }", + // FuncValue( + // Array((1, SBox)), + // OptionGet( + // SelectField.typed[Value[SOption[SLong.type]]]( + // OptionGet( + // ExtractRegisterAs(ValUse(1, SBox), ErgoBox.R5, SOption(SPair(SInt, SOption(SLong)))) + // ), + // 2.toByte + // )) + // ))) } def existingPropTest[A: RType, B: RType](propName: String, scalaFunc: A => B) = { @@ -3369,36 +4552,38 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("PreHeader properties equivalence") { + + verifyCases( - Seq((preH1, Expected(Success(0.toByte), cost = 37022))), + Seq((preH1, Expected(Success(0.toByte), cost = 37022, methodCostDetails(SPreHeader.versionMethod, 10), 1765))), existingPropTest("version", { (x: PreHeader) => x.version })) verifyCases( Seq((preH1, Expected(Success( Helpers.decodeBytes("7fff7fdd6f62018bae0001006d9ca888ff7f56ff8006573700a167f17f2c9f40")), - cost = 36121))), + cost = 36121, methodCostDetails(SPreHeader.parentIdMethod, 10), 1766))), existingPropTest("parentId", { (x: PreHeader) => x.parentId })) verifyCases( - Seq((preH1, Expected(Success(6306290372572472443L), cost = 36147))), + Seq((preH1, Expected(Success(6306290372572472443L), cost = 36147, methodCostDetails(SPreHeader.timestampMethod, 10), 1765))), existingPropTest("timestamp", { (x: PreHeader) => x.timestamp })) verifyCases( - Seq((preH1, Expected(Success(-3683306095029417063L), cost = 36127))), + Seq((preH1, Expected(Success(-3683306095029417063L), cost = 36127, methodCostDetails(SPreHeader.nBitsMethod, 10), 1765))), existingPropTest("nBits", { (x: PreHeader) => x.nBits })) verifyCases( - Seq((preH1, Expected(Success(1), cost = 36136))), + Seq((preH1, Expected(Success(1), cost = 36136, methodCostDetails(SPreHeader.heightMethod, 10), 1765))), existingPropTest("height", { (x: PreHeader) => x.height })) verifyCases( Seq((preH1, Expected(Success( Helpers.decodeGroupElement("026930cb9972e01534918a6f6d6b8e35bc398f57140d13eb3623ea31fbd069939b")), - cost = 36255))), + cost = 36255, methodCostDetails(SPreHeader.minerPkMethod, 10), 1782))), existingPropTest("minerPk", { (x: PreHeader) => x.minerPk })) verifyCases( - Seq((preH1, Expected(Success(Helpers.decodeBytes("ff8087")), cost = 36249))), + Seq((preH1, Expected(Success(Helpers.decodeBytes("ff8087")), cost = 36249, methodCostDetails(SPreHeader.votesMethod,10), 1766))), existingPropTest("votes", { (x: PreHeader) => x.votes })) } @@ -3406,81 +4591,81 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( Seq((h1, Expected(Success( Helpers.decodeBytes("957f008001808080ffe4ffffc8f3802401df40006aa05e017fa8d3f6004c804a")), - cost = 36955))), + cost = 36955, methodCostDetails(SHeader.idMethod, 10), 1766))), existingPropTest("id", { (x: Header) => x.id })) verifyCases( - Seq((h1, Expected(Success(0.toByte), cost = 36124))), + Seq((h1, Expected(Success(0.toByte), cost = 36124, methodCostDetails(SHeader.versionMethod, 10), 1765))), existingPropTest("version", { (x: Header) => x.version })) verifyCases( Seq((h1, Expected(Success( Helpers.decodeBytes("0180dd805b0000ff5400b997fd7f0b9b00de00fb03c47e37806a8186b94f07ff")), - cost = 36097))), + cost = 36097, methodCostDetails(SHeader.parentIdMethod, 10), 1766))), existingPropTest("parentId", { (x: Header) => x.parentId })) verifyCases( Seq((h1, Expected(Success( Helpers.decodeBytes("01f07f60d100ffb970c3007f60ff7f24d4070bb8fffa7fca7f34c10001ffe39d")), - cost = 36111))), + cost = 36111, methodCostDetails(SHeader.ADProofsRootMethod, 10), 1766))), existingPropTest("ADProofsRoot", { (x: Header) => x.ADProofsRoot})) verifyCases( - Seq((h1, Expected(Success(CAvlTree(createAvlTreeData())), cost = 36092))), + Seq((h1, Expected(Success(CAvlTree(createAvlTreeData())), cost = 36092, methodCostDetails(SHeader.stateRootMethod, 10), 1765))), existingPropTest("stateRoot", { (x: Header) => x.stateRoot })) verifyCases( Seq((h1, Expected(Success( Helpers.decodeBytes("804101ff01000080a3ffbd006ac080098df132a7017f00649311ec0e00000100")), - cost = 36094))), + cost = 36094, methodCostDetails(SHeader.transactionsRootMethod, 10), 1766))), existingPropTest("transactionsRoot", { (x: Header) => x.transactionsRoot })) verifyCases( - Seq((h1, Expected(Success(1L), cost = 36151))), + Seq((h1, Expected(Success(1L), cost = 36151, methodCostDetails(SHeader.timestampMethod, 10), 1765))), existingPropTest("timestamp", { (x: Header) => x.timestamp })) verifyCases( - Seq((h1, Expected(Success(-1L), cost = 36125))), + Seq((h1, Expected(Success(-1L), cost = 36125, methodCostDetails(SHeader.nBitsMethod, 10), 1765))), existingPropTest("nBits", { (x: Header) => x.nBits })) verifyCases( - Seq((h1, Expected(Success(1), cost = 36134))), + Seq((h1, Expected(Success(1), cost = 36134, methodCostDetails(SHeader.heightMethod, 10), 1765))), existingPropTest("height", { (x: Header) => x.height })) verifyCases( Seq((h1, Expected(Success( Helpers.decodeBytes("e57f80885601b8ff348e01808000bcfc767f2dd37f0d01015030ec018080bc62")), - cost = 36133))), + cost = 36133, methodCostDetails(SHeader.extensionRootMethod, 10), 1766))), existingPropTest("extensionRoot", { (x: Header) => x.extensionRoot })) verifyCases( Seq((h1, Expected(Success( Helpers.decodeGroupElement("039bdbfa0b49cc6bef58297a85feff45f7bbeb500a9d2283004c74fcedd4bd2904")), - cost = 36111))), + cost = 36111, methodCostDetails(SHeader.minerPkMethod, 10), 1782))), existingPropTest("minerPk", { (x: Header) => x.minerPk })) verifyCases( Seq((h1, Expected(Success( Helpers.decodeGroupElement("0361299207fa392231e23666f6945ae3e867b978e021d8d702872bde454e9abe9c")), - cost = 36111))), + cost = 36111, methodCostDetails(SHeader.powOnetimePkMethod, 10), 1782))), existingPropTest("powOnetimePk", { (x: Header) => x.powOnetimePk })) verifyCases( Seq((h1, Expected(Success( Helpers.decodeBytes("7f4f09012a807f01")), - cost = 36176))), + cost = 36176, methodCostDetails(SHeader.powNonceMethod, 10), 1766))), existingPropTest("powNonce", { (x: Header) => x.powNonce })) verifyCases( Seq((h1, Expected(Success( CBigInt(new BigInteger("-e24990c47e15ed4d0178c44f1790cc72155d516c43c3e8684e75db3800a288", 16))), - cost = 36220))), + cost = 36220, methodCostDetails(SHeader.powDistanceMethod, 10), 1765))), existingPropTest("powDistance", { (x: Header) => x.powDistance })) verifyCases( Seq((h1, Expected(Success( Helpers.decodeBytes("7f0180")), - cost = 36100))), + cost = 36100, methodCostDetails(SHeader.votesMethod, 10), 1766))), existingPropTest("votes", { (x: Header) => x.votes })) } @@ -3639,7 +4824,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ), height = 11, - selfBox = input.copy(), // TODO HF (2h): in 3.x implementation selfBox is never the same instance as input (see toSigmaContext) + selfBox = input.copy(), // in 3.x, 4.x implementation selfBox is never the same instance as input (see toSigmaContext) + selfIndex = 0, lastBlockUtxoRootHash = CAvlTree( AvlTreeData( ADDigest @@ (ErgoAlgos.decodeUnsafe("54d23dd080006bdb56800100356080935a80ffb77e90b800057f00661601807f17")), @@ -3655,6 +4841,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui TestValue(Helpers.decodeBytes("00"), CollType(RType.ByteType)), TestValue(true, RType.BooleanType))), activatedScriptVersion = activatedVersionInTests, + currentErgoTreeVersion = ergoTreeVersionInTests, false ) val ctx2 = ctx.copy(vars = Coll[AnyValue](null, null, null)) @@ -3687,9 +4874,17 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui test(samples, existingPropTest("dataInputs", { (x: Context) => x.dataInputs })) + val testTraceBase = traceBase ++ Array( + FixedCostItem(PropertyCall), + FixedCostItem(SContext.dataInputsMethod, FixedCost(JitCost(15))), + FixedCostItem(Constant), + FixedCostItem(ByIndex) + ) + + val costDetails = TracedCost(testTraceBase) verifyCases( Seq( - (ctx, Expected(Success(dataBox), cost = 37087)), + (ctx, Expected(Success(dataBox), cost = 37087, costDetails, 1769)), (ctx.copy(_dataInputs = Coll()), Expected(new ArrayIndexOutOfBoundsException("0"))) ), existingFeature({ (x: Context) => x.dataInputs(0) }, @@ -3709,11 +4904,12 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui )), preGeneratedSamples = Some(samples)) + val idCostDetails = TracedCost(testTraceBase :+ FixedCostItem(CompanionDesc(ExtractId), FixedCost(JitCost(12)))) verifyCases( Seq( (ctx, Expected(Success( Helpers.decodeBytes("7da4b55971f19a78d007638464580f91a020ab468c0dbe608deb1f619e245bc3")), - cost = 37193)) + cost = 37193, idCostDetails, 1772)) ), existingFeature({ (x: Context) => x.dataInputs(0).id }, "{ (x: Context) => x.dataInputs(0).id }", @@ -3746,7 +4942,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui existingPropTest("headers", { (x: Context) => x.headers }), preGeneratedSamples = Some(samples)) - // TODO: testCases2 doesn't work because of equality (check the reason) + // TODO: verifyCases doesn't work because of equality (check the reason) + testCases( Seq(ctx -> Success(ctx.OUTPUTS)), existingFeature( @@ -3755,23 +4952,46 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui FuncValue(Vector((1, SContext)), Outputs)), preGeneratedSamples = Some(samples)) - // NOTE: testCases2 is not supported because SELF modified to pass input + // NOTE: verifyCases is not supported because SELF modified to pass input test(samples, existingFeature({ (x: Context) => x.INPUTS }, "{ (x: Context) => x.INPUTS }", FuncValue(Vector((1, SContext)), Inputs))) test(samples, existingFeature({ (x: Context) => x.SELF }, "{ (x: Context) => x.SELF }", FuncValue(Vector((1, SContext)), Self))) + val heightCostDetails = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(Height) + ) + ) verifyCases( - Seq(ctx -> Expected(Success(ctx.HEIGHT), cost = 35885)), + Seq(ctx -> Expected(Success(ctx.HEIGHT), cost = 35885, heightCostDetails, 1766)), existingFeature( { (x: Context) => x.HEIGHT }, "{ (x: Context) => x.HEIGHT }", FuncValue(Vector((1, SContext)), Height)), preGeneratedSamples = Some(samples)) - verifyCases( - Seq((ctx, Expected(Success(Coll[Long](80946L)), cost = 39152))), + val inputsCostDetails1 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(Inputs), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(MapCollection), PerItemCost(JitCost(20), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractAmount))) + verifyCases( + Seq((ctx, Expected(Success(Coll[Long](80946L)), cost = 39152, inputsCostDetails1, 1770))), existingFeature( { (x: Context) => x.INPUTS.map { (b: Box) => b.value } }, "{ (x: Context) => x.INPUTS.map { (b: Box) => b.value } }", @@ -3781,8 +5001,26 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui )), preGeneratedSamples = Some(samples)) - verifyCases( - Seq((ctx, Expected(Success(Coll((80946L, 80946L))), cost = 39959))), + val inputsCostDetails2 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(Inputs), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(MapCollection), PerItemCost(JitCost(20), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(ExtractAmount), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + FixedCostItem(Tuple))) + verifyCases( + Seq((ctx, Expected(Success(Coll((80946L, 80946L))), cost = 39959, inputsCostDetails2, 1774))), existingFeature( { (x: Context) => x.INPUTS.map { (b: Box) => (b.value, b.value) } }, """{ (x: Context) => @@ -3837,9 +5075,25 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui )), preGeneratedSamples = Some(samples)) + val selfCostDetails = TracedCost( + traceBase ++ Array( + FixedCostItem(PropertyCall), + FixedCostItem(SContext.selfBoxIndexMethod, FixedCost(JitCost(20))) + ) + ) verifyCases( - Seq((ctx, Expected(Success(-1), cost = 36318))), - existingFeature({ (x: Context) => x.selfBoxIndex }, + Seq( + (ctx, Expected( + Success(-1), cost = 36318, + expectedDetails = CostDetails.ZeroCost, + newCost = 1766, + newVersionedResults = { + val res = (ExpectedResult(Success(0), Some(1766)) -> Some(selfCostDetails)) + Seq(0, 1, 2).map(version => version -> res) + })) + ), + changedFeature({ (x: Context) => x.selfBoxIndex }, + { (x: Context) => x.selfBoxIndex }, // see versioning in selfBoxIndex implementation "{ (x: Context) => x.selfBoxIndex }", FuncValue( Vector((1, SContext)), @@ -3852,18 +5106,32 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui )), preGeneratedSamples = Some(samples)) - // TODO HF (2h): see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/603 + // test vectors to reproduce v4.x bug (see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/603) samples.foreach { c => - ctx.selfBoxIndex shouldBe -1 + if (VersionContext.current.isJitActivated) { + // fixed in v5.0 + c.selfBoxIndex should not be(-1) + } else { + // should be reproduced in v4.x + c.selfBoxIndex shouldBe -1 + } } verifyCases( - Seq(ctx -> Expected(Success(ctx.LastBlockUtxoRootHash), cost = 35990)), + Seq(ctx -> Expected(Success(ctx.LastBlockUtxoRootHash), cost = 35990, methodCostDetails(SContext.lastBlockUtxoRootHashMethod, 15), 1766)), existingPropTest("LastBlockUtxoRootHash", { (x: Context) => x.LastBlockUtxoRootHash }), preGeneratedSamples = Some(samples)) + val isUpdateAllowedCostDetails = TracedCost( + traceBase ++ Array( + FixedCostItem(PropertyCall), + FixedCostItem(SContext.lastBlockUtxoRootHashMethod, FixedCost(JitCost(15))), + FixedCostItem(PropertyCall), + FixedCostItem(SAvlTree.isUpdateAllowedMethod, FixedCost(JitCost(15))) + ) + ) verifyCases( - Seq(ctx -> Expected(Success(ctx.LastBlockUtxoRootHash.isUpdateAllowed), cost = 36288)), + Seq(ctx -> Expected(Success(ctx.LastBlockUtxoRootHash.isUpdateAllowed), cost = 36288, isUpdateAllowedCostDetails, 1767)), existingFeature( { (x: Context) => x.LastBlockUtxoRootHash.isUpdateAllowed }, "{ (x: Context) => x.LastBlockUtxoRootHash.isUpdateAllowed }", @@ -3884,11 +5152,11 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui preGeneratedSamples = Some(samples)) verifyCases( - Seq(ctx -> Expected(Success(ctx.minerPubKey), cost = 36047)), + Seq(ctx -> Expected(Success(ctx.minerPubKey), cost = 36047, methodCostDetails(SContext.minerPubKeyMethod, 20), 1767)), existingPropTest("minerPubKey", { (x: Context) => x.minerPubKey }), preGeneratedSamples = Some(samples)) -// TODO HF (2h): implement support of Option[T] in DataSerializer +// TODO v6.0 (2h): implement support of Option[T] in DataSerializer // this will allow passing optional values in registers and also in constants // testCases2( // Seq( @@ -3906,7 +5174,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui "{ (x: Context) => getVar[Boolean](11) }", FuncValue(Vector((1, SContext)), GetVar(11.toByte, SOption(SBoolean)))), preGeneratedSamples = Some(samples)) - + verifyCases( Seq((ctx, Expected(new InvalidType("Cannot getVar[Int](11): invalid type of value Value(true) at id=2")))), existingFeature((x: Context) => x.getVar[Int](11).get, @@ -3914,8 +5182,17 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui FuncValue(Vector((1, SContext)), OptionGet(GetVar(11.toByte, SOption(SInt))))), preGeneratedSamples = Some(samples)) - verifyCases( - Seq((ctx, Expected(Success(true), cost = 36750))), + val getVarCostDetails = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(GetVar), + FixedCostItem(OptionGet))) + verifyCases( + Seq((ctx, Expected(Success(true), cost = 36750, getVarCostDetails, 1765))), existingFeature((x: Context) => x.getVar[Boolean](11).get, "{ (x: Context) => getVar[Boolean](11).get }", FuncValue(Vector((1, SContext)), OptionGet(GetVar(11.toByte, SOption(SBoolean))))), @@ -3925,9 +5202,31 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Conditional access to data box register using isDefined") { val (_, _, _, ctx, _, _) = contextData() + val registerIsDefinedCostDetails = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SContext.dataInputsMethod, FixedCost(JitCost(15))), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(OptionGet) + ) + ) verifyCases( Seq( - ctx -> Expected(Success(-135729055492651903L), 38399) + ctx -> Expected(Success(-135729055492651903L), 38399, registerIsDefinedCostDetails, 1779) ), existingFeature( { (x: Context) => @@ -3987,15 +5286,20 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( Seq( - ctx -> Expected(expectedError) + ctx -> Expected(Failure(expectedError), 0, CostDetails.ZeroCost, 1793, + newVersionedResults = { + Seq.tabulate(3)(v => v -> (ExpectedResult(Success(true), Some(1793)) -> None)) + } + ) ), changedFeature( scalaFunc = { (x: Context) => - // this error is expected in v3.x + // this error is expected in v3.x, v4.x throw expectedError }, scalaFuncNew = { (x: Context) => - // TODO HF: this is expected in v5.0 + // this is expected in v5.0, so the semantics of the script should correspond + // to this Scala code val dataBox = x.dataInputs(0) val ok = if (x.OUTPUTS(0).R5[Long].get == 1L) { dataBox.R4[Long].get <= x.SELF.value @@ -4004,53 +5308,54 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } ok }, - s"""{ (x: Context) => - | val dataBox = x.dataInputs(0) - | val ok = if (x.OUTPUTS(0).R5[Long].get == 1L) { - | dataBox.R4[Long].get <= x.SELF.value - | } else { - | dataBox.R4[Coll[Byte]].get != x.SELF.propositionBytes - | } - | ok - |} - |""".stripMargin, - FuncValue( - Array((1, SContext)), - BlockValue( - Array( - ValDef( - 3, - List(), - ByIndex( - MethodCall.typed[Value[SCollection[SBox.type]]]( - ValUse(1, SContext), - SContext.getMethodByName("dataInputs"), - Vector(), - Map() - ), - IntConstant(0), - None + s"""{ (x: Context) => + | val dataBox = x.dataInputs(0) + | val ok = if (x.OUTPUTS(0).R5[Long].get == 1L) { + | dataBox.R4[Long].get <= x.SELF.value + | } else { + | dataBox.R4[Coll[Byte]].get != x.SELF.propositionBytes + | } + | ok + |} + |""".stripMargin, + FuncValue( + Array((1, SContext)), + BlockValue( + Array( + ValDef( + 3, + List(), + ByIndex( + MethodCall.typed[Value[SCollection[SBox.type]]]( + ValUse(1, SContext), + SContext.getMethodByName("dataInputs"), + Vector(), + Map() + ), + IntConstant(0), + None + ) ) - ) - ), - If( - EQ( - OptionGet( - ExtractRegisterAs(ByIndex(Outputs, IntConstant(0), None), ErgoBox.R5, SOption(SLong)) - ), - LongConstant(1L) - ), - LE( - OptionGet(ExtractRegisterAs(ValUse(3, SBox), ErgoBox.R4, SOption(SLong))), - ExtractAmount(Self) ), - NEQ( - OptionGet(ExtractRegisterAs(ValUse(3, SBox), ErgoBox.R4, SOption(SByteArray))), - ExtractScriptBytes(Self) + If( + EQ( + OptionGet( + ExtractRegisterAs(ByIndex(Outputs, IntConstant(0), None), ErgoBox.R5, SOption(SLong)) + ), + LongConstant(1L) + ), + LE( + OptionGet(ExtractRegisterAs(ValUse(3, SBox), ErgoBox.R4, SOption(SLong))), + ExtractAmount(Self) + ), + NEQ( + OptionGet(ExtractRegisterAs(ValUse(3, SBox), ErgoBox.R4, SOption(SByteArray))), + ExtractScriptBytes(Self) + ) ) ) - ) - ) + ), + allowNewToSucceed = true ), preGeneratedSamples = Some(mutable.WrappedArray.empty)) } @@ -4058,14 +5363,103 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Conditional access OUTPUTS(0).R4 using tag in R5") { val (_, _, _, ctx, _, _) = contextData() + val registerTagCostDetails1 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(Outputs), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(OptionGet) + ) + ) + val registerTagCostDetails2 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(Outputs), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(OptionGet), + TypeBasedCostItem(Upcast, SLong) + ) + ) + + val registerTagCostDetails3 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(Outputs), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + FixedCostItem(Constant) + ) + ) verifyCases( Seq( - ctx -> Expected(Success(5008366408131208436L), 40406), + ctx -> Expected(Success(5008366408131208436L), 40406, registerTagCostDetails1, 1791), ctxWithRegsInOutput(ctx, Map( ErgoBox.R5 -> LongConstant(0L), - ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(10L), 40396), + ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(10L), 40396, registerTagCostDetails2, 1790), ctxWithRegsInOutput(ctx, Map( - ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(-1L), 40396) + ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(-1L), 40396, registerTagCostDetails3, 1777) ), existingFeature( { (x: Context) => @@ -4138,25 +5532,152 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Conditional access OUTPUTS(0).R4 using tag in R5 (plus action)") { val (_, _, _, ctx, _, _) = contextData() + val tagRegisterCostDetails1 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(Outputs), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(OptionGet), + FixedCostItem(Self), + FixedCostItem(ExtractAmount), + TypeBasedCostItem(ArithOp.Plus, SLong) + ) + ) + val tagRegisterCostDetails2 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(Outputs), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(OptionGet), + TypeBasedCostItem(Upcast, SLong), + FixedCostItem(Self), + FixedCostItem(ExtractAmount), + TypeBasedCostItem(ArithOp.Plus, SLong) + ) + ) + val tagRegisterCostDetails3 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(Outputs), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(Constant) + ) + ) + val tagRegisterCostDetails4 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(Outputs), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + FixedCostItem(Constant) + ) + ) verifyCases( Seq( // case 1L - ctx -> Expected(Success(5008366408131289382L), 41016), + ctx -> Expected(Success(5008366408131289382L), 41016, tagRegisterCostDetails1, 1794), // case 0L ctxWithRegsInOutput(ctx, Map( ErgoBox.R5 -> LongConstant(0L), - ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(80956L), 41006), + ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(80956L), 41006, tagRegisterCostDetails2, 1793), // case returning 0L ctxWithRegsInOutput(ctx, Map( ErgoBox.R5 -> LongConstant(2L), // note R4 is required to avoid // "RuntimeException: Set of non-mandatory indexes is not densely packed" - ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(0L), 41006), + ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(0L), 41006, tagRegisterCostDetails3, 1784), // case returning -1L ctxWithRegsInOutput(ctx, Map( - ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(-1L), 41006) + ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(-1L), 41006, tagRegisterCostDetails4, 1777) ), existingFeature( { (x: Context) => @@ -4240,19 +5761,149 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Conditional access dataInputs(0).R4 using tag in R5") { val (_, _, _, ctx, _, _) = contextData() + val tagRegisterCostDetails1 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SContext.dataInputsMethod, FixedCost(JitCost(15))), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(OptionGet) + ) + ) + val tagRegisterCostDetails2 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SContext.dataInputsMethod, FixedCost(JitCost(15))), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(OptionGet), + TypeBasedCostItem(Upcast, SLong) + ) + ) + val tagRegisterCostDetails3 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SContext.dataInputsMethod, FixedCost(JitCost(15))), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(Constant) + ) + ) + val tagRegisterCostDetails4 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SContext.dataInputsMethod, FixedCost(JitCost(15))), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + FixedCostItem(Constant) + ) + ) + verifyCases( Seq( ctxWithRegsInDataInput(ctx, Map( ErgoBox.R5 -> LongConstant(1L), - ErgoBox.R4 -> LongConstant(10))) -> Expected(Success(10L), 41084), + ErgoBox.R4 -> LongConstant(10))) -> Expected(Success(10L), 41084, tagRegisterCostDetails1, 1792), ctxWithRegsInDataInput(ctx, Map( ErgoBox.R5 -> LongConstant(0L), - ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(10L), 41084), + ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(10L), 41084, tagRegisterCostDetails2, 1791), ctxWithRegsInDataInput(ctx, Map( ErgoBox.R5 -> LongConstant(2L), - ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(0L), 41084), + ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(0L), 41084, tagRegisterCostDetails3, 1786), ctxWithRegsInDataInput(ctx, Map( - ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(-1L), 41084) + ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(-1L), 41084, tagRegisterCostDetails4, 1779) ), existingFeature( { (x: Context) => @@ -4338,19 +5989,154 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Conditional access dataInputs(0).R4 using tag in R5 (plus action)") { val (_, _, _, ctx, _, _) = contextData() + val costDetails1 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SContext.dataInputsMethod, FixedCost(JitCost(15))), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(OptionGet), + FixedCostItem(Self), + FixedCostItem(ExtractAmount), + TypeBasedCostItem(ArithOp.Plus, SLong) + ) + ) + val costDetails2 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SContext.dataInputsMethod, FixedCost(JitCost(15))), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(OptionGet), + TypeBasedCostItem(Upcast, SLong), + FixedCostItem(Self), + FixedCostItem(ExtractAmount), + TypeBasedCostItem(ArithOp.Plus, SLong) + ) + ) + val costDetails3 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SContext.dataInputsMethod, FixedCost(JitCost(15))), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))), + FixedCostItem(If), + FixedCostItem(Constant) + ) + ) + val costDetails4 = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SContext.dataInputsMethod, FixedCost(JitCost(15))), + FixedCostItem(Constant), + FixedCostItem(ByIndex), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractRegisterAs), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + FixedCostItem(Constant) + ) + ) verifyCases( Seq( ctxWithRegsInDataInput(ctx, Map( ErgoBox.R5 -> LongConstant(1L), - ErgoBox.R4 -> LongConstant(10))) -> Expected(Success(80956L), 41694), + ErgoBox.R4 -> LongConstant(10))) -> Expected(Success(80956L), 41694, costDetails1, 1796), ctxWithRegsInDataInput(ctx, Map( ErgoBox.R5 -> LongConstant(0L), - ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(80956L), 41694), + ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(80956L), 41694, costDetails2, 1794), ctxWithRegsInDataInput(ctx, Map( ErgoBox.R5 -> LongConstant(2L), - ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(0L), 41694), + ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(0L), 41694, costDetails3, 1786), ctxWithRegsInDataInput(ctx, Map( - ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(-1L), 41694) + ErgoBox.R4 -> ShortConstant(10))) -> Expected(Success(-1L), 41694, costDetails4, 1779) ), existingFeature( { (x: Context) => @@ -4447,58 +6233,72 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("xorOf equivalence") { - // TODO HF (3h): see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/640 + def costDetails(i: Int) = TracedCost(traceBase :+ SeqCostItem(CompanionDesc(XorOf), PerItemCost(JitCost(20), JitCost(5), 32), i)) verifyCases( { - def success[T](v: T, c: Int) = Expected(Success(v), c) + def successNew[T](v: T, c: Int, newV: T, cd: CostDetails) = Expected( + value = Success(v), + cost = c, + expectedDetails = CostDetails.ZeroCost, + newCost = 1766, + newVersionedResults = Seq(0, 1, 2).map(i => i -> (ExpectedResult(Success(newV), Some(1766)) -> Some(cd))) + ) Seq( - (Coll[Boolean](false), success(false, 37071)), - (Coll[Boolean](true), success(false, 37071)), - (Coll[Boolean](false, false), success(false, 37081)), - (Coll[Boolean](false, true), success(true, 37081)), - (Coll[Boolean](true, false), success(true, 37081)), - (Coll[Boolean](true, true), success(false, 37081)), - (Coll[Boolean](false, false, false), success(false, 37091)), - (Coll[Boolean](false, false, true), success(true, 37091)), - (Coll[Boolean](false, true, false), success(true, 37091)), - (Coll[Boolean](false, true, true), success(true, 37091)), - (Coll[Boolean](true, false, false), success(true, 37091)), - (Coll[Boolean](true, false, true), success(true, 37091)), - (Coll[Boolean](true, true, false), success(true, 37091)), - (Coll[Boolean](true, true, true), success(false, 37091)), - (Coll[Boolean](false, false, false, false), success(false, 37101)), - (Coll[Boolean](false, false, false, true), success(true, 37101)), - (Coll[Boolean](false, false, true, false), success(true, 37101)), - (Coll[Boolean](false, false, true, true), success(true, 37101)) + (Coll[Boolean](), successNew(false, 37061, newV = false, costDetails(0))), + (Coll[Boolean](false), successNew(false, 37071, newV = false, costDetails(1))), + (Coll[Boolean](true), successNew(false, 37071, newV = true, costDetails(1))), + (Coll[Boolean](false, false), successNew(false, 37081, newV = false, costDetails(2))), + (Coll[Boolean](false, true), successNew(true, 37081, newV = true, costDetails(2))), + (Coll[Boolean](true, false), successNew(true, 37081, newV = true, costDetails(2))), + (Coll[Boolean](true, true), successNew(false, 37081, newV = false, costDetails(2))), + (Coll[Boolean](false, false, false), successNew(false, 37091, newV = false, costDetails(3))), + (Coll[Boolean](false, false, true), successNew(true, 37091, newV = true, costDetails(3))), + (Coll[Boolean](false, true, false), successNew(true, 37091, newV = true, costDetails(3))), + (Coll[Boolean](false, true, true), successNew(true, 37091, newV = false, costDetails(3))), + (Coll[Boolean](true, false, false), successNew(true, 37091, newV = true, costDetails(3))), + (Coll[Boolean](true, false, true), successNew(true, 37091, newV = false, costDetails(3))), + (Coll[Boolean](true, true, false), successNew(true, 37091, newV = false, costDetails(3))), + (Coll[Boolean](true, true, true), successNew(false, 37091, newV = true, costDetails(3))), + (Coll[Boolean](false, false, false, false), successNew(false, 37101, newV = false, costDetails(4))), + (Coll[Boolean](false, false, false, true), successNew(true, 37101, newV = true, costDetails(4))), + (Coll[Boolean](false, false, true, false), successNew(true, 37101, newV = true, costDetails(4))), + (Coll[Boolean](false, false, true, true), successNew(true, 37101, newV = false, costDetails(4))) ) }, - existingFeature((x: Coll[Boolean]) => SigmaDsl.xorOf(x), + changedFeature( + (x: Coll[Boolean]) => SigmaDsl.xorOf(x), + (x: Coll[Boolean]) => SigmaDsl.xorOf(x), "{ (x: Coll[Boolean]) => xorOf(x) }", FuncValue(Vector((1, SBooleanArray)), XorOf(ValUse(1, SBooleanArray))))) } property("LogicalNot equivalence") { + val costDetails = TracedCost(traceBase :+ FixedCostItem(LogicalNot)) verifyCases( Seq( - (true, Expected(Success(false), 35864)), - (false, Expected(Success(true), 35864))), + (true, Expected(Success(false), 35864, costDetails, 1765)), + (false, Expected(Success(true), 35864, costDetails, 1765))), existingFeature((x: Boolean) => !x, "{ (x: Boolean) => !x }", FuncValue(Vector((1, SBoolean)), LogicalNot(ValUse(1, SBoolean))))) } property("Numeric Negation equivalence") { + val costDetails = TracedCost(traceBase :+ FixedCostItem(Negation)) verifyCases( { - def success[T](v: T) = Expected(Success(v), 36136) + def success[T](v: T) = Expected(Success(v), 36136, costDetails, 1766) Seq( (Byte.MinValue, success(Byte.MinValue)), // !!! + ((Byte.MinValue + 1).toByte, success(Byte.MaxValue)), (-40.toByte, success(40.toByte)), (-1.toByte, success(1.toByte)), (0.toByte, success(0.toByte)), (1.toByte, success(-1.toByte)), (45.toByte, success(-45.toByte)), - (127.toByte, success(-127.toByte))) + (127.toByte, success(-127.toByte)), + (Byte.MaxValue, success(-127.toByte)) + ) }, existingFeature((x: Byte) => (-x).toByte, "{ (x: Byte) => -x }", @@ -4506,15 +6306,16 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36136) + def success[T](v: T) = Expected(Success(v), 36136, costDetails, 1766) Seq( (Short.MinValue, success(Short.MinValue)), // special case! - ((Short.MinValue + 1).toShort, success(32767.toShort)), + ((Short.MinValue + 1).toShort, success(Short.MaxValue)), (-1528.toShort, success(1528.toShort)), (-1.toShort, success(1.toShort)), (0.toShort, success(0.toShort)), (1.toShort, success(-1.toShort)), (7586.toShort, success(-7586.toShort)), + (32767.toShort, success(-32767.toShort)), (Short.MaxValue, success(-32767.toShort))) }, existingFeature((x: Short) => (-x).toShort, @@ -4523,10 +6324,10 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36136) + def success[T](v: T) = Expected(Success(v), 36136, costDetails, 1766) Seq( (Int.MinValue, success(Int.MinValue)), // special case! - (Int.MinValue + 1, success(2147483647)), + (Int.MinValue + 1, success(Int.MaxValue)), (-63509744, success(63509744)), (-1, success(1)), (0, success(0)), @@ -4540,16 +6341,16 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36136) + def success[T](v: T) = Expected(Success(v), 36136, costDetails, 1766) Seq( (Long.MinValue, success(Long.MinValue)), // special case! - (Long.MinValue + 1, success(9223372036854775807L)), + (Long.MinValue + 1, success(Long.MaxValue)), (-957264171003115006L, success(957264171003115006L)), (-1L, success(1L)), (0L, success(0L)), (1L, success(-1L)), (340835904095777627L, success(-340835904095777627L)), - (9223372036854775807L, success(-9223372036854775807L))) + (Long.MaxValue, success(-9223372036854775807L))) }, existingFeature((x: Long) => -x, "{ (x: Long) => -x }", @@ -4557,7 +6358,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36136) + def success[T](v: T) = Expected(Success(v), 36136, costDetails, 1767) Seq( (CBigInt(new BigInteger("-1655a05845a6ad363ac88ea21e88b97e436a1f02c548537e12e2d9667bf0680", 16)), success(CBigInt(new BigInteger("1655a05845a6ad363ac88ea21e88b97e436a1f02c548537e12e2d9667bf0680", 16)))), (CBigInt(new BigInteger("-1b24ba8badba8abf347cce054d9b9f14f229321507245b8", 16)), success(CBigInt(new BigInteger("1b24ba8badba8abf347cce054d9b9f14f229321507245b8", 16)))), @@ -4580,30 +6381,41 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui FuncValue(Vector((1, SBigInt)), Negation(ValUse(1, SBigInt))))) } - property("global functions equivalence") { - + property("groupGenerator equivalence") { + val costDetails = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(Global), + FixedCostItem(PropertyCall), + FixedCostItem(SGlobal.groupGeneratorMethod, FixedCost(JitCost(10))) + ) + ) verifyCases( { - def success[T](v: T) = Expected(Success(v), 35981) + def success[T](v: T) = Expected(Success(v), 35981, costDetails, 1782) Seq( (-1, success(Helpers.decodeGroupElement("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"))), (1, success(Helpers.decodeGroupElement("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")))) }, existingFeature({ (x: Int) => SigmaDsl.groupGenerator }, - "{ (x: Int) => groupGenerator }", - FuncValue( - Vector((1, SInt)), - MethodCall.typed[Value[SGroupElement.type]]( - Global, - SGlobal.getMethodByName("groupGenerator"), - Vector(), - Map() - ) - ))) + "{ (x: Int) => groupGenerator }", + FuncValue( + Vector((1, SInt)), + MethodCall.typed[Value[SGroupElement.type]]( + Global, + SGlobal.getMethodByName("groupGenerator"), + Vector(), + Map() + ) + ))) verifyCases( { - def success[T](v: T) = Expected(Success(v), 35981) + def success[T](v: T) = Expected(Success(v), 35981, costDetails, 1782) Seq( (-1, success(Helpers.decodeGroupElement("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"))), (1, success(Helpers.decodeGroupElement("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")))) @@ -4620,9 +6432,16 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ))) - verifyCases( + if (lowerMethodCallsInTests) { + val expCostDetails = TracedCost( + costDetails.trace ++ Array( + FixedCostItem(ValUse), + FixedCostItem(Exponentiate) + ) + ) + verifyCases( { - def success[T](v: T) = Expected(Success(v), 41237) + def success[T](v: T) = Expected(Success(v), 41237, expCostDetails, 1872) Seq( (CBigInt(new BigInteger("-e5c1a54694c85d644fa30a6fc5f3aa209ed304d57f72683a0ebf21038b6a9d", 16)), success(Helpers.decodeGroupElement("023395bcba3d7cf21d73c50f8af79d09a8c404c15ce9d04f067d672823bae91a54"))), (CBigInt(new BigInteger("-bc2d08f935259e0eebf272c66c6e1dbd484c6706390215", 16)), success(Helpers.decodeGroupElement("02ddcf4c48105faf3c16f7399b5dbedd82ab0bb50ae292d8f88f49a3f86e78974e"))), @@ -4639,36 +6458,58 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui (CBigInt(new BigInteger("102bb404f5e36bdba004fdefa34df8cfa02e7912f3caf79", 16)), success(Helpers.decodeGroupElement("03ce82f431d115d45ad555084f8b2861ce5c4561d154e931e9f778594896e46a25")))) }, existingFeature({ (n: BigInt) => SigmaDsl.groupGenerator.exp(n) }, - "{ (n: BigInt) => groupGenerator.exp(n) }", - FuncValue( - Vector((1, SBigInt)), - Exponentiate( - MethodCall.typed[Value[SGroupElement.type]]( - Global, - SGlobal.getMethodByName("groupGenerator"), - Vector(), - Map() - ), - ValUse(1, SBigInt) - ) - ))) + "{ (n: BigInt) => groupGenerator.exp(n) }", + FuncValue( + Vector((1, SBigInt)), + Exponentiate( + MethodCall.typed[Value[SGroupElement.type]]( + Global, + SGlobal.getMethodByName("groupGenerator"), + Vector(), + Map() + ), + ValUse(1, SBigInt) + ) + ))) + } + } - // TODO HF (2h): fix semantics when the left collection is longer - verifyCases( + property("Global.xor equivalence") { + if (lowerMethodCallsInTests) { + def costDetails(i: Int) = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + SeqCostItem(CompanionDesc(Xor), PerItemCost(JitCost(10), JitCost(2), 128), i) + ) + ) + verifyCases( { - def success[T](v: T) = Expected(Success(v), 36903) + def success[T](v: T, cd: CostDetails) = Expected(Success(v), 36903, cd, 1769) Seq( - ((Helpers.decodeBytes(""), Helpers.decodeBytes("")), success(Helpers.decodeBytes(""))), - ((Helpers.decodeBytes("01"), Helpers.decodeBytes("01")), success(Helpers.decodeBytes("00"))), - ((Helpers.decodeBytes("0100"), Helpers.decodeBytes("0101")), success(Helpers.decodeBytes("0001"))), - ((Helpers.decodeBytes("01"), Helpers.decodeBytes("0101")), success(Helpers.decodeBytes("00"))), - ((Helpers.decodeBytes("0100"), Helpers.decodeBytes("01")), Expected(new ArrayIndexOutOfBoundsException("1"))), + ((Helpers.decodeBytes(""), Helpers.decodeBytes("")), success(Helpers.decodeBytes(""), costDetails(0))), + ((Helpers.decodeBytes("01"), Helpers.decodeBytes("01")), success(Helpers.decodeBytes("00"), costDetails(1))), + ((Helpers.decodeBytes("0100"), Helpers.decodeBytes("0101")), success(Helpers.decodeBytes("0001"), costDetails(2))), + ((Helpers.decodeBytes("01"), Helpers.decodeBytes("0101")), success(Helpers.decodeBytes("00"), costDetails(1))), + ((Helpers.decodeBytes("0100"), Helpers.decodeBytes("01")) -> + Expected(Failure(new ArrayIndexOutOfBoundsException("1")), + cost = 0, + expectedDetails = CostDetails.ZeroCost, + newCost = 1769, + newVersionedResults = { + val res = (ExpectedResult(Success(Helpers.decodeBytes("00")), Some(1769)), Some(costDetails(1))) + Seq(0, 1, 2).map(version => version -> res) + } + )), ((Helpers.decodeBytes("800136fe89afff802acea67128a0ff007fffe3498c8001806080012b"), Helpers.decodeBytes("648018010a5d5800f5b400a580e7b4809b0cd273ff1230bfa800017f7fdb002749b3ac2b86ff")), - success(Helpers.decodeBytes("e4812eff83f2a780df7aa6d4a8474b80e4f3313a7392313fc8800054"))) + success(Helpers.decodeBytes("e4812eff83f2a780df7aa6d4a8474b80e4f3313a7392313fc8800054"), costDetails(28))) ) }, - existingFeature((x: (Coll[Byte], Coll[Byte])) => SigmaDsl.xor(x._1, x._2), + changedFeature( + (x: (Coll[Byte], Coll[Byte])) => SigmaDsl.xor(x._1, x._2), + (x: (Coll[Byte], Coll[Byte])) => SigmaDsl.xor(x._1, x._2), "{ (x: (Coll[Byte], Coll[Byte])) => xor(x._1, x._2) }", FuncValue( Vector((1, STuple(Vector(SByteArray, SByteArray)))), @@ -4683,138 +6524,297 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ) ))) - } - - property("Coll[Box] methods equivalence") { - val samples = genSamples[Coll[Box]](collOfN[Box](5), MinSuccessful(20)) - val b1 = CostingBox( - false, - new ErgoBox( - 1L, - new ErgoTree( - 0.toByte, - Vector(), - Right( - SigmaPropConstant( - CSigmaProp( - ProveDHTuple( - Helpers.decodeECPoint("02c1a9311ecf1e76c787ba4b1c0e10157b4f6d1e4db3ef0d84f411c99f2d4d2c5b"), - Helpers.decodeECPoint("027d1bd9a437e73726ceddecc162e5c85f79aee4798505bc826b8ad1813148e419"), - Helpers.decodeECPoint("0257cff6d06fe15d1004596eeb97a7f67755188501e36adc49bd807fe65e9d8281"), - Helpers.decodeECPoint("033c6021cff6ba5fdfc4f1742486030d2ebbffd9c9c09e488792f3102b2dcdabd5") + } else { + val cases = Seq( + ((Helpers.decodeBytes("01"), Helpers.decodeBytes("01")) -> + Expected( + // in v4.x exp method cannot be called via MethodCall ErgoTree node +// Failure(new NoSuchMethodException("")), + Success(Helpers.decodeBytes("00")), + cost = 41484, + CostDetails.ZeroCost, + newCost = 0, + newVersionedResults = { + // in v5.0 MethodCall ErgoTree node is allowed + val res = ExpectedResult( + Success(Helpers.decodeBytes("00")), + Some(116) + ) + val details = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(Global), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + SeqCostItem(CompanionDesc(Xor), PerItemCost(JitCost(10), JitCost(2), 128), 1) ) ) - ) + Seq.tabulate(3)(v => v -> ( res -> Some(details) )) // expected result for each version + } + ) + ) + ) + forEachScriptAndErgoTreeVersion(activatedVersions, ergoTreeVersions) { + testCases( + cases, + changedFeature( + (x: (Coll[Byte], Coll[Byte])) => SigmaDsl.xor(x._1, x._2), + (x: (Coll[Byte], Coll[Byte])) => SigmaDsl.xor(x._1, x._2), + "", // NOTE, the script for this test case cannot be compiled, thus expectedExpr is used + expectedExpr = FuncValue( + Vector((1, STuple(Vector(SByteArray, SByteArray)))), + MethodCall( + Global, + SGlobal.getMethodByName("xor"), + Vector( + SelectField.typed[Value[SCollection[SByte.type]]]( + ValUse(1, STuple(Vector(SByteArray, SByteArray))), + 1.toByte + ), + SelectField.typed[Value[SCollection[SByte.type]]]( + ValUse(1, STuple(Vector(SByteArray, SByteArray))), + 2.toByte + )), + Map() + ) + ), + allowNewToSucceed = true, + allowDifferentErrors = false + )) + } + } + } + + def sampleCollBoxes = genSamples[Coll[Box]](collOfN[Box](5), MinSuccessful(20)) + + def create_b1 = CostingBox( + false, + new ErgoBox( + 1L, + new ErgoTree( + 0.toByte, + Vector(), + Right( + SigmaPropConstant( + CSigmaProp( + ProveDHTuple( + Helpers.decodeECPoint("02c1a9311ecf1e76c787ba4b1c0e10157b4f6d1e4db3ef0d84f411c99f2d4d2c5b"), + Helpers.decodeECPoint("027d1bd9a437e73726ceddecc162e5c85f79aee4798505bc826b8ad1813148e419"), + Helpers.decodeECPoint("0257cff6d06fe15d1004596eeb97a7f67755188501e36adc49bd807fe65e9d8281"), + Helpers.decodeECPoint("033c6021cff6ba5fdfc4f1742486030d2ebbffd9c9c09e488792f3102b2dcdabd5") + ) + ) + ) + ) + ), + Coll(), + Map( + ErgoBox.R4 -> ByteArrayConstant( + Helpers.decodeBytes( + "7200004cccdac3008001bc80ffc7ff9633bca3e501801380ff007900019d7f0001a8c9dfff5600d964011617ca00583f989c7f80007fee7f99b07f7f870067dc315180828080307fbdf400" ) ), - Coll(), - Map( - ErgoBox.R4 -> ByteArrayConstant( - Helpers.decodeBytes( - "7200004cccdac3008001bc80ffc7ff9633bca3e501801380ff007900019d7f0001a8c9dfff5600d964011617ca00583f989c7f80007fee7f99b07f7f870067dc315180828080307fbdf400" - ) - ), - ErgoBox.R7 -> LongConstant(0L), - ErgoBox.R6 -> FalseLeaf, - ErgoBox.R5 -> ByteArrayConstant(Helpers.decodeBytes("7f")) - ), - ModifierId @@ ("7dffff48ab0000c101a2eac9ff17017f6180aa7fc6f2178000800179499380a5"), - 21591.toShort, - 638768 + ErgoBox.R7 -> LongConstant(0L), + ErgoBox.R6 -> FalseLeaf, + ErgoBox.R5 -> ByteArrayConstant(Helpers.decodeBytes("7f")) + ), + ModifierId @@ ("7dffff48ab0000c101a2eac9ff17017f6180aa7fc6f2178000800179499380a5"), + 21591.toShort, + 638768 + ) + ) + + def create_b2 = CostingBox( + false, + new ErgoBox( + 1000000000L, + new ErgoTree( + 0.toByte, + Vector(), + Right(BoolToSigmaProp(OR(ConcreteCollection(Array(FalseLeaf, AND(ConcreteCollection(Array(FalseLeaf, FalseLeaf), SBoolean))), SBoolean)))) + ), + Coll(), + Map(), + ModifierId @@ ("008677ffff7ff36dff00f68031140400007689ff014c9201ce8000a9ffe6ceff"), + 32767.toShort, + 32827 + ) + ) + + property("Coll.filter equivalence") { + val samples = sampleCollBoxes + val b1 = create_b1 + val b2 = create_b2 + + val costDetails = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Filter), PerItemCost(JitCost(20), JitCost(1), 10), 0) ) ) - val b2 = CostingBox( - false, - new ErgoBox( - 1000000000L, - new ErgoTree( - 0.toByte, - Vector(), - Right(BoolToSigmaProp(OR(ConcreteCollection(Array(FalseLeaf, AND(ConcreteCollection(Array(FalseLeaf, FalseLeaf), SBoolean))), SBoolean)))) - ), - Coll(), - Map(), - ModifierId @@ ("008677ffff7ff36dff00f68031140400007689ff014c9201ce8000a9ffe6ceff"), - 32767.toShort, - 32827 + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Filter), PerItemCost(JitCost(20), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractAmount), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SLong) + ) + ) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Filter), PerItemCost(JitCost(20), JitCost(1), 10), 2), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractAmount), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SLong), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractAmount), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SLong) ) ) - verifyCases( { - def success[T](v: T, c: Int) = Expected(Success(v), c) + def success[T](v: T, c: Int, costDetails: CostDetails, newCost: Int) = Expected(Success(v), c, costDetails, newCost) Seq( - (Coll[Box](), success(Coll[Box](), 37297)), - (Coll[Box](b1), success(Coll[Box](), 37397)), - (Coll[Box](b1, b2), success(Coll[Box](b2), 37537)) + (Coll[Box](), success(Coll[Box](), 37297, costDetails, 1767)), + (Coll[Box](b1), success(Coll[Box](), 37397, costDetails2, 1772)), + (Coll[Box](b1, b2), success(Coll[Box](b2), 37537, costDetails3, 1776)) ) }, existingFeature({ (x: Coll[Box]) => x.filter({ (b: Box) => b.value > 1 }) }, - "{ (x: Coll[Box]) => x.filter({(b: Box) => b.value > 1 }) }", - FuncValue( - Vector((1, SCollectionType(SBox))), - Filter( - ValUse(1, SCollectionType(SBox)), - FuncValue(Vector((3, SBox)), GT(ExtractAmount(ValUse(3, SBox)), LongConstant(1L))) - ) - )), + "{ (x: Coll[Box]) => x.filter({(b: Box) => b.value > 1 }) }", + FuncValue( + Vector((1, SCollectionType(SBox))), + Filter( + ValUse(1, SCollectionType(SBox)), + FuncValue(Vector((3, SBox)), GT(ExtractAmount(ValUse(3, SBox)), LongConstant(1L))) + ) + )), preGeneratedSamples = Some(samples)) + } + property("Coll.flatMap equivalence") { + val samples = sampleCollBoxes + val b1 = create_b1 + val b2 = create_b2 + + val costDetails1 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + SeqCostItem(MethodDesc(SCollection.FlatMapMethod), PerItemCost(JitCost(60), JitCost(10), 8), 0) + ) + ) + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractScriptBytes), + SeqCostItem(MethodDesc(SCollection.FlatMapMethod), PerItemCost(JitCost(60), JitCost(10), 8), 135) + ) + ) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractScriptBytes), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractScriptBytes), + SeqCostItem(MethodDesc(SCollection.FlatMapMethod), PerItemCost(JitCost(60), JitCost(10), 8), 147) + ) + ) verifyCases( { - def success[T](v: T, c: Int) = Expected(Success(v), c) Seq( - (Coll[Box](), success(Coll[Byte](), 38126)), - (Coll[Box](b1), success(Helpers.decodeBytes( + (Coll[Box](), Expected(Success(Coll[Byte]()), 38126, costDetails1, 1773)), + (Coll[Box](b1), Expected(Success(Helpers.decodeBytes( "0008ce02c1a9311ecf1e76c787ba4b1c0e10157b4f6d1e4db3ef0d84f411c99f2d4d2c5b027d1bd9a437e73726ceddecc162e5c85f79aee4798505bc826b8ad1813148e4190257cff6d06fe15d1004596eeb97a7f67755188501e36adc49bd807fe65e9d8281033c6021cff6ba5fdfc4f1742486030d2ebbffd9c9c09e488792f3102b2dcdabd5" - ), 38206)), - (Coll[Box](b1, b2), success(Helpers.decodeBytes( + )), 38206, costDetails2, 1791)), + (Coll[Box](b1, b2), Expected(Success(Helpers.decodeBytes( "0008ce02c1a9311ecf1e76c787ba4b1c0e10157b4f6d1e4db3ef0d84f411c99f2d4d2c5b027d1bd9a437e73726ceddecc162e5c85f79aee4798505bc826b8ad1813148e4190257cff6d06fe15d1004596eeb97a7f67755188501e36adc49bd807fe65e9d8281033c6021cff6ba5fdfc4f1742486030d2ebbffd9c9c09e488792f3102b2dcdabd500d197830201010096850200" - ), 38286)) + )), 38286, costDetails3, 1795)) ) }, existingFeature({ (x: Coll[Box]) => x.flatMap({ (b: Box) => b.propositionBytes }) }, - "{ (x: Coll[Box]) => x.flatMap({(b: Box) => b.propositionBytes }) }", - FuncValue( - Vector((1, SCollectionType(SBox))), - MethodCall.typed[Value[SCollection[SByte.type]]]( - ValUse(1, SCollectionType(SBox)), - SCollection.getMethodByName("flatMap").withConcreteTypes( - Map(STypeVar("IV") -> SBox, STypeVar("OV") -> SByte) - ), - Vector(FuncValue(Vector((3, SBox)), ExtractScriptBytes(ValUse(3, SBox)))), - Map() - ) - )), + "{ (x: Coll[Box]) => x.flatMap({(b: Box) => b.propositionBytes }) }", + FuncValue( + Vector((1, SCollectionType(SBox))), + MethodCall.typed[Value[SCollection[SByte.type]]]( + ValUse(1, SCollectionType(SBox)), + SCollection.getMethodByName("flatMap").withConcreteTypes( + Map(STypeVar("IV") -> SBox, STypeVar("OV") -> SByte) + ), + Vector(FuncValue(Vector((3, SBox)), ExtractScriptBytes(ValUse(3, SBox)))), + Map() + ) + )), preGeneratedSamples = Some(samples)) + } + + property("Coll.zip equivalence") { + val samples = sampleCollBoxes + val b1 = create_b1 + val b2 = create_b2 + def costDetails(zipElements: Int) = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + SeqCostItem(MethodDesc(SCollection.ZipMethod), PerItemCost(JitCost(10), JitCost(1), 10), zipElements) + ) + ) verifyCases( { - def success[T](v: T, c: Int) = Expected(Success(v), c) + def success[T](v: T, c: Int, cd: CostDetails, nc: Int) = Expected(Success(v), c, cd, nc) Seq( - (Coll[Box](), success(Coll[(Box, Box)](), 37399)), - (Coll[Box](b1), success(Coll[(Box, Box)]((b1, b1)), 37559)), - (Coll[Box](b1, b2), success(Coll[(Box, Box)]((b1, b1), (b2, b2)), 37719)) + (Coll[Box](), success(Coll[(Box, Box)](), 37399, costDetails(0), 1766)), + (Coll[Box](b1), success(Coll[(Box, Box)]((b1, b1)), 37559, costDetails(1), 1768)), + (Coll[Box](b1, b2), success(Coll[(Box, Box)]((b1, b1), (b2, b2)), 37719, costDetails(2), 1770)) ) }, existingFeature({ (x: Coll[Box]) => x.zip(x) }, - "{ (x: Coll[Box]) => x.zip(x) }", - FuncValue( - Vector((1, SCollectionType(SBox))), - MethodCall.typed[Value[SCollection[STuple]]]( - ValUse(1, SCollectionType(SBox)), - SCollection.getMethodByName("zip").withConcreteTypes( - Map(STypeVar("IV") -> SBox, STypeVar("OV") -> SBox) - ), - Vector(ValUse(1, SCollectionType(SBox))), - Map() - ) - )), + "{ (x: Coll[Box]) => x.zip(x) }", + FuncValue( + Vector((1, SCollectionType(SBox))), + MethodCall.typed[Value[SCollection[STuple]]]( + ValUse(1, SCollectionType(SBox)), + SCollection.getMethodByName("zip").withConcreteTypes( + Map(STypeVar("IV") -> SBox, STypeVar("OV") -> SBox) + ), + Vector(ValUse(1, SCollectionType(SBox))), + Map() + ) + )), preGeneratedSamples = Some(samples)) + } + property("Coll.size equivalence") { + val samples = sampleCollBoxes + val b1 = create_b1 + val b2 = create_b2 + val costDetails = TracedCost(traceBase :+ FixedCostItem(SizeOf)) verifyCases( { - def success[T](v: T) = Expected(Success(v), 35954) + def success[T](v: T) = Expected(Success(v), 35954, costDetails, 1765) Seq( (Coll[Box](), success(0)), (Coll[Box](b1), success(1)), @@ -4822,42 +6822,88 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) }, existingFeature({ (x: Coll[Box]) => x.size }, - "{ (x: Coll[Box]) => x.size }", - FuncValue(Vector((1, SCollectionType(SBox))), SizeOf(ValUse(1, SCollectionType(SBox))))), + "{ (x: Coll[Box]) => x.size }", + FuncValue(Vector((1, SCollectionType(SBox))), SizeOf(ValUse(1, SCollectionType(SBox))))), preGeneratedSamples = Some(samples)) + } + property("Coll.indices equivalence") { + val samples = sampleCollBoxes + val b1 = create_b1 + val b2 = create_b2 + def costDetails(indicesCount: Int) = TracedCost( + traceBase ++ Array( + FixedCostItem(PropertyCall), + SeqCostItem(MethodDesc(SCollection.IndicesMethod), PerItemCost(JitCost(20), JitCost(2), 16), indicesCount) + ) + ) verifyCases( { - def success[T](v: T) = Expected(Success(v), 36036) + def success[T](v: T, i: Int) = Expected(Success(v), 36036, costDetails(i), 1768) Seq( - (Coll[Box](), success(Coll[Int]())), - (Coll[Box](b1), success(Coll[Int](0))), - (Coll[Box](b1, b2), success(Coll[Int](0, 1))) + (Coll[Box](), success(Coll[Int](), 0)), + (Coll[Box](b1), success(Coll[Int](0), 1)), + (Coll[Box](b1, b2), success(Coll[Int](0, 1), 2)) ) }, existingFeature({ (x: Coll[Box]) => x.indices }, - "{ (x: Coll[Box]) => x.indices }", - FuncValue( - Vector((1, SCollectionType(SBox))), - MethodCall.typed[Value[SCollection[SInt.type]]]( - ValUse(1, SCollectionType(SBox)), - SCollection.getMethodByName("indices").withConcreteTypes(Map(STypeVar("IV") -> SBox)), - Vector(), - Map() - ) - )), + "{ (x: Coll[Box]) => x.indices }", + FuncValue( + Vector((1, SCollectionType(SBox))), + MethodCall.typed[Value[SCollection[SInt.type]]]( + ValUse(1, SCollectionType(SBox)), + SCollection.getMethodByName("indices").withConcreteTypes(Map(STypeVar("IV") -> SBox)), + Vector(), + Map() + ) + )), preGeneratedSamples = Some(samples)) - verifyCases( - { - def success[T](v: T, c: Int) = Expected(Success(v), c) - Seq( - (Coll[Box](), success(true, 37909)), - (Coll[Box](b1), success(false, 37969)), - (Coll[Box](b1, b2), success(false, 38029)) - ) - }, - existingFeature({ (x: Coll[Box]) => x.forall({ (b: Box) => b.value > 1 }) }, + } + + property("Coll.forall equivalence") { + val samples = sampleCollBoxes + val b1 = create_b1 + val b2 = create_b2 + val costDetails1 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(ForAll), PerItemCost(JitCost(3), JitCost(1), 10), 0) + ) + ) + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(ForAll), PerItemCost(JitCost(3), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractAmount), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SLong) + ) + ) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(ForAll), PerItemCost(JitCost(3), JitCost(1), 10), 2), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractAmount), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SLong) + ) + ) + + if (lowerMethodCallsInTests) { + verifyCases( + { + Seq( + (Coll[Box](), Expected(Success(true), 37909, costDetails1, 1764)), + (Coll[Box](b1), Expected(Success(false), 37969, costDetails2, 1769)), + (Coll[Box](b1, b2), Expected(Success(false), 38029, costDetails3, 1769)) + ) + }, + existingFeature({ (x: Coll[Box]) => x.forall({ (b: Box) => b.value > 1 }) }, "{ (x: Coll[Box]) => x.forall({(b: Box) => b.value > 1 }) }", FuncValue( Vector((1, SCollectionType(SBox))), @@ -4866,18 +6912,68 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui FuncValue(Vector((3, SBox)), GT(ExtractAmount(ValUse(3, SBox)), LongConstant(1L))) ) )), - preGeneratedSamples = Some(samples)) + preGeneratedSamples = Some(samples)) + } else { + assertExceptionThrown( + verifyCases( + Seq( (Coll[Box](), Expected(Success(true), 37909)) ), + existingFeature( + { (x: Coll[Box]) => x.forall({ (b: Box) => b.value > 1 }) }, + "{ (x: Coll[Box]) => x.forall({(b: Box) => b.value > 1 }) }" + )), + rootCauseLike[NoSuchMethodException]("sigmastate.eval.CostingRules$CollCoster.forall(scalan.Base$Ref)") + ) + } + } - verifyCases( - { - def success[T](v: T, c: Int) = Expected(Success(v), c) - Seq( - (Coll[Box](), success(false, 38455)), - (Coll[Box](b1), success(false, 38515)), - (Coll[Box](b1, b2), success(true, 38575)) - ) - }, - existingFeature({ (x: Coll[Box]) => x.exists({ (b: Box) => b.value > 1 }) }, + property("Coll.exists equivalence") { + val samples = sampleCollBoxes + val b1 = create_b1 + val b2 = create_b2 + val costDetails1 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Exists), PerItemCost(JitCost(3), JitCost(1), 10), 0) + ) + ) + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Exists), PerItemCost(JitCost(3), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractAmount), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SLong) + ) + ) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Exists), PerItemCost(JitCost(3), JitCost(1), 10), 2), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractAmount), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SLong), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(ExtractAmount), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SLong) + ) + ) + + if (lowerMethodCallsInTests) { + verifyCases( + { + Seq( + (Coll[Box](), Expected(Success(false), 38455, costDetails1, 1764)), + (Coll[Box](b1), Expected(Success(false), 38515, costDetails2, 1769)), + (Coll[Box](b1, b2), Expected(Success(true), 38575, costDetails3, 1773)) + ) + }, + existingFeature({ (x: Coll[Box]) => x.exists({ (b: Box) => b.value > 1 }) }, "{ (x: Coll[Box]) => x.exists({(b: Box) => b.value > 1 }) }", FuncValue( Vector((1, SCollectionType(SBox))), @@ -4886,77 +6982,224 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui FuncValue(Vector((3, SBox)), GT(ExtractAmount(ValUse(3, SBox)), LongConstant(1L))) ) )), - preGeneratedSamples = Some(samples)) + preGeneratedSamples = Some(samples)) + } else { + assertExceptionThrown( + verifyCases( + Seq( (Coll[Box](), Expected(Success(false), 38455)) ), + existingFeature( + { (x: Coll[Box]) => x.exists({ (b: Box) => b.value > 1 }) }, + "{ (x: Coll[Box]) => x.exists({(b: Box) => b.value > 1 }) }" + )), + rootCauseLike[NoSuchMethodException]("sigmastate.eval.CostingRules$CollCoster.exists(scalan.Base$Ref)") + ) + } } property("Coll exists with nested If") { val o = NumericOps.BigIntIsExactOrdering - verifyCases( - { - def success[T](v: T, c: Int) = Expected(Success(v), c) - Seq( - (Coll[BigInt](), success(false, 38955)), - (Coll[BigInt](BigIntZero), success(false, 39045)), - (Coll[BigInt](BigIntOne), success(true, 39045)), - (Coll[BigInt](BigIntZero, BigIntOne), success(true, 39135)), - (Coll[BigInt](BigIntZero, BigInt10), success(false, 39135)) + val costDetails1 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Exists), PerItemCost(JitCost(3), JitCost(1), 10), 0) ) - }, - existingFeature( - { (x: Coll[BigInt]) => x.exists({ (b: BigInt) => - if (o.gt(b, BigIntZero)) o.lt(b, BigInt10) else false - }) - }, - "{ (x: Coll[BigInt]) => x.exists({(b: BigInt) => if (b > 0) b < 10 else false }) }", - FuncValue( - Array((1, SCollectionType(SBigInt))), - Exists( - ValUse(1, SCollectionType(SBigInt)), + ) + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Exists), PerItemCost(JitCost(3), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SBigInt), + FixedCostItem(If), + FixedCostItem(Constant) + ) + ) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Exists), PerItemCost(JitCost(3), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SBigInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(LT, SBigInt) + ) + ) + val costDetails4 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Exists), PerItemCost(JitCost(3), JitCost(1), 10), 2), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SBigInt), + FixedCostItem(If), + FixedCostItem(Constant), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SBigInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(LT, SBigInt) + ) + ) + + if (lowerMethodCallsInTests) { + verifyCases( + { + Seq( + (Coll[BigInt](), Expected(Success(false), 38955, costDetails1, 1764)), + (Coll[BigInt](BigIntZero), Expected(Success(false), 39045, costDetails2, 1769)), + (Coll[BigInt](BigIntOne), Expected(Success(true), 39045, costDetails3, 1772)), + (Coll[BigInt](BigIntZero, BigIntOne), Expected(Success(true), 39135, costDetails4, 1777)), + (Coll[BigInt](BigIntZero, BigInt10), Expected(Success(false), 39135, costDetails4, 1777)) + ) + }, + existingFeature( + { (x: Coll[BigInt]) => x.exists({ (b: BigInt) => + if (o.gt(b, BigIntZero)) o.lt(b, BigInt10) else false + }) + }, + "{ (x: Coll[BigInt]) => x.exists({(b: BigInt) => if (b > 0) b < 10 else false }) }", FuncValue( - Array((3, SBigInt)), - If( - GT(ValUse(3, SBigInt), BigIntConstant(CBigInt(new BigInteger("0", 16)))), - LT(ValUse(3, SBigInt), BigIntConstant(CBigInt(new BigInteger("a", 16)))), - FalseLeaf + Array((1, SCollectionType(SBigInt))), + Exists( + ValUse(1, SCollectionType(SBigInt)), + FuncValue( + Array((3, SBigInt)), + If( + GT(ValUse(3, SBigInt), BigIntConstant(CBigInt(new BigInteger("0", 16)))), + LT(ValUse(3, SBigInt), BigIntConstant(CBigInt(new BigInteger("a", 16)))), + FalseLeaf + ) + ) ) - ) - ) - ))) + ))) + } else { + assertExceptionThrown( + verifyCases( + Seq( (Coll[BigInt](), Expected(Success(false), 38955)) ), + existingFeature( + { (x: Coll[BigInt]) => x.exists({ (b: BigInt) => + if (o.gt(b, BigIntZero)) o.lt(b, BigInt10) else false + }) + }, + "{ (x: Coll[BigInt]) => x.exists({(b: BigInt) => if (b > 0) b < 10 else false }) }" + )), + rootCauseLike[NoSuchMethodException]("sigmastate.eval.CostingRules$CollCoster.exists(scalan.Base$Ref)") + ) + } } property("Coll forall with nested If") { val o = NumericOps.BigIntIsExactOrdering - verifyCases( - { - def success[T](v: T, c: Int) = Expected(Success(v), c) - Seq( - (Coll[BigInt](), success(true, 38412)), - (Coll[BigInt](BigIntMinusOne), success(false, 38502)), - (Coll[BigInt](BigIntOne), success(true, 38502)), - (Coll[BigInt](BigIntZero, BigIntOne), success(true, 38592)), - (Coll[BigInt](BigIntZero, BigInt11), success(false, 38592)) + val costDetails1 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(ForAll), PerItemCost(JitCost(3), JitCost(1), 10), 0) ) - }, - existingFeature( - { (x: Coll[BigInt]) => x.forall({ (b: BigInt) => - if (o.gteq(b, BigIntZero)) o.lteq(b, BigInt10) else false - }) - }, - "{ (x: Coll[BigInt]) => x.forall({(b: BigInt) => if (b >= 0) b <= 10 else false }) }", - FuncValue( - Array((1, SCollectionType(SBigInt))), - ForAll( - ValUse(1, SCollectionType(SBigInt)), - FuncValue( - Array((3, SBigInt)), - If( - GE(ValUse(3, SBigInt), BigIntConstant(CBigInt(new BigInteger("0", 16)))), - LE(ValUse(3, SBigInt), BigIntConstant(CBigInt(new BigInteger("a", 16)))), - FalseLeaf + ) + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(ForAll), PerItemCost(JitCost(3), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GE, SBigInt), + FixedCostItem(If), + FixedCostItem(Constant) + ) + ) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(ForAll), PerItemCost(JitCost(3), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GE, SBigInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(LE, SBigInt) + ) + ) + val costDetails4 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(ForAll), PerItemCost(JitCost(3), JitCost(1), 10), 2), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GE, SBigInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(LE, SBigInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GE, SBigInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(LE, SBigInt) + ) + ) + if (lowerMethodCallsInTests) { + verifyCases( + { + Seq( + (Coll[BigInt](), Expected(Success(true), 38412, costDetails1, 1764)), + (Coll[BigInt](BigIntMinusOne), Expected(Success(false), 38502, costDetails2, 1769)), + (Coll[BigInt](BigIntOne), Expected(Success(true), 38502, costDetails3, 1772)), + (Coll[BigInt](BigIntZero, BigIntOne), Expected(Success(true), 38592, costDetails4, 1779)), + (Coll[BigInt](BigIntZero, BigInt11), Expected(Success(false), 38592, costDetails4, 1779)) + ) + }, + existingFeature( + { (x: Coll[BigInt]) => x.forall({ (b: BigInt) => + if (o.gteq(b, BigIntZero)) o.lteq(b, BigInt10) else false + }) + }, + "{ (x: Coll[BigInt]) => x.forall({(b: BigInt) => if (b >= 0) b <= 10 else false }) }", + FuncValue( + Array((1, SCollectionType(SBigInt))), + ForAll( + ValUse(1, SCollectionType(SBigInt)), + FuncValue( + Array((3, SBigInt)), + If( + GE(ValUse(3, SBigInt), BigIntConstant(CBigInt(new BigInteger("0", 16)))), + LE(ValUse(3, SBigInt), BigIntConstant(CBigInt(new BigInteger("a", 16)))), + FalseLeaf + ) ) ) - ) - ))) + ))) + } else { + assertExceptionThrown( + verifyCases( + Seq( (Coll[BigInt](), Expected(Success(true), 38412)) ), + existingFeature( + { (x: Coll[BigInt]) => x.forall({ (b: BigInt) => + if (o.gteq(b, BigIntZero)) o.lteq(b, BigInt10) else false + }) + }, + "{ (x: Coll[BigInt]) => x.forall({(b: BigInt) => if (b >= 0) b <= 10 else false }) }" + )), + rootCauseLike[NoSuchMethodException]("sigmastate.eval.CostingRules$CollCoster.forall(scalan.Base$Ref)") + ) + } + } val collWithRangeGen = for { @@ -4965,24 +7208,100 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui r <- Gen.choose(l, arr.length - 1) } yield (arr, (l, r)) property("Coll flatMap method equivalence") { + val costDetails0 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + SeqCostItem(MethodDesc(SCollection.FlatMapMethod), PerItemCost(JitCost(60), JitCost(10), 8), 0) + ) + ) + val costDetails1 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(NamedDesc("MatchSingleArgMethodCall"), FixedCost(JitCost(30))), + SeqCostItem(NamedDesc("CheckFlatmapBody"), PerItemCost(JitCost(20), JitCost(20), 1), 1), + SeqCostItem(MethodDesc(SCollection.FlatMapMethod), PerItemCost(JitCost(60), JitCost(10), 8), 0) + ) + ) + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SGroupElement.GetEncodedMethod, FixedCost(JitCost(250))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SGroupElement.GetEncodedMethod, FixedCost(JitCost(250))), + SeqCostItem(MethodDesc(SCollection.FlatMapMethod), PerItemCost(JitCost(60), JitCost(10), 8), 66) + ) + ) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SGroupElement.GetEncodedMethod, FixedCost(JitCost(250))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SGroupElement.GetEncodedMethod, FixedCost(JitCost(250))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SGroupElement.GetEncodedMethod, FixedCost(JitCost(250))), + SeqCostItem(MethodDesc(SCollection.FlatMapMethod), PerItemCost(JitCost(60), JitCost(10), 8), 99) + ) + ) + val costDetails4 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SGroupElement.GetEncodedMethod, FixedCost(JitCost(250))), + FixedCostItem(PropertyCall), + SeqCostItem(MethodDesc(SCollection.IndicesMethod), PerItemCost(JitCost(20), JitCost(2), 16), 33), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SGroupElement.GetEncodedMethod, FixedCost(JitCost(250))), + FixedCostItem(PropertyCall), + SeqCostItem(MethodDesc(SCollection.IndicesMethod), PerItemCost(JitCost(20), JitCost(2), 16), 33), + SeqCostItem(MethodDesc(SCollection.FlatMapMethod), PerItemCost(JitCost(60), JitCost(10), 8), 66) + ) + ) + verifyCases( { - def success[T](v: T, c: Int) = Expected(Success(v), c) + def success[T](v: T, c: Int, cd: CostDetails, newCost: Int) = Expected(Success(v), c, cd, newCost) Seq( - Coll[GroupElement]() -> success(Coll[Byte](), 40133), + Coll[GroupElement]() -> Expected(Success(Coll[Byte]()), 40133, CostDetails.ZeroCost, 1774, + newVersionedResults = { + val res = ExpectedResult(Success(Coll[Byte]()), Some(1773)) + Seq.tabulate(3)(v => + v -> (res -> Some(costDetails0)) + ) + }), Coll[GroupElement]( Helpers.decodeGroupElement("02d65904820f8330218cf7318b3810d0c9ab9df86f1ee6100882683f23c0aee587"), Helpers.decodeGroupElement("0390e9daa9916f30d0bc61a8e381c6005edfb7938aee5bb4fc9e8a759c7748ffaa")) -> success(Helpers.decodeBytes( "02d65904820f8330218cf7318b3810d0c9ab9df86f1ee6100882683f23c0aee5870390e9daa9916f30d0bc61a8e381c6005edfb7938aee5bb4fc9e8a759c7748ffaa" - ), 40213), + ), 40213, costDetails2, 1834), Coll[GroupElement]( Helpers.decodeGroupElement("02d65904820f8330218cf7318b3810d0c9ab9df86f1ee6100882683f23c0aee587"), Helpers.decodeGroupElement("0390e9daa9916f30d0bc61a8e381c6005edfb7938aee5bb4fc9e8a759c7748ffaa"), Helpers.decodeGroupElement("03bd839b969b02d218fd1192f2c80cbda9c6ce9c7ddb765f31b748f4666203df85")) -> success(Helpers.decodeBytes( "02d65904820f8330218cf7318b3810d0c9ab9df86f1ee6100882683f23c0aee5870390e9daa9916f30d0bc61a8e381c6005edfb7938aee5bb4fc9e8a759c7748ffaa03bd839b969b02d218fd1192f2c80cbda9c6ce9c7ddb765f31b748f4666203df85" - ), 40253) + ), 40253, costDetails3, 1864) ) }, existingFeature( @@ -5010,9 +7329,68 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ))) + { // changed behavior of flatMap in v5.0 + val cases = Seq( + Coll[GroupElement]( + Helpers.decodeGroupElement("02d65904820f8330218cf7318b3810d0c9ab9df86f1ee6100882683f23c0aee587"), + Helpers.decodeGroupElement("0390e9daa9916f30d0bc61a8e381c6005edfb7938aee5bb4fc9e8a759c7748ffaa") + ) -> Expected(Try(SCollection.throwInvalidFlatmap(null)), 0, + expectedDetails = CostDetails.ZeroCost, + newCost = 0, + newVersionedResults = (0 to 2).map(version => + // successful result for each version + version -> (ExpectedResult(Success({ + val is = Coll((0 to 32):_*) + is.append(is) + }), + verificationCost = Some(817)) -> Some(costDetails4)) + )) + ) + val f = changedFeature( + scalaFunc = { (x: Coll[GroupElement]) => SCollection.throwInvalidFlatmap(null) }, + scalaFuncNew = { (x: Coll[GroupElement]) => + // NOTE, v5.0 interpreter accepts any lambda in flatMap + x.flatMap({ (b: GroupElement) => b.getEncoded.indices }) + }, + "", // NOTE, the script for this test case cannot be compiled + FuncValue( + Vector((1, SCollectionType(SGroupElement))), + MethodCall.typed[Value[SCollection[SInt.type]]]( + ValUse(1, SCollectionType(SGroupElement)), + SCollection.getMethodByName("flatMap").withConcreteTypes( + Map(STypeVar("IV") -> SGroupElement, STypeVar("OV") -> SInt) + ), + Vector( + FuncValue( + Vector((3, SGroupElement)), + MethodCall.typed[Value[SCollection[SInt.type]]]( + MethodCall.typed[Value[SCollection[SByte.type]]]( + ValUse(3, SGroupElement), + SGroupElement.getMethodByName("getEncoded"), + Vector(), + Map() + ), + SCollection.getMethodByName("indices").withConcreteTypes( + Map(STypeVar("IV") -> SByte) + ), + Vector(), + Map() + ) + ) + ), + Map() + ) + ), + allowNewToSucceed = true, // the new 5.0 interpreter can succeed (after activation) when v4.0 fails + allowDifferentErrors = true + ) + + testCases(cases, f) + } + val f = existingFeature( - { (x: Coll[GroupElement]) => x.flatMap({ (b: GroupElement) => b.getEncoded.append(b.getEncoded) }) }, - "{ (x: Coll[GroupElement]) => x.flatMap({ (b: GroupElement) => b.getEncoded.append(b.getEncoded) }) }" ) + { (x: Coll[GroupElement]) => x.flatMap({ (b: GroupElement) => b.getEncoded.indices }) }, + "{ (x: Coll[GroupElement]) => x.flatMap({ (b: GroupElement) => b.getEncoded.indices }) }" ) assertExceptionThrown( f.oldF, t => t match { @@ -5024,22 +7402,47 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Coll patch method equivalence") { val samples = genSamples(collWithRangeGen, MinSuccessful(50)) + def costDetails(i: Int) = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + SeqCostItem(MethodDesc(SCollection.PatchMethod), PerItemCost(JitCost(30), JitCost(2), 10), i) + ) + ) + verifyCases( { - def success[T](v: T) = Expected(Success(v), 37514) + def success[T](v: T, cd: CostDetails) = Expected(Success(v), 37514, cd, 1776) Seq( - ((Coll[Int](), (0, 0)), success(Coll[Int]())), - ((Coll[Int](1), (0, 0)), success(Coll[Int](1, 1))), - ((Coll[Int](1), (0, 1)), success(Coll[Int](1))), - ((Coll[Int](1, 2), (0, 0)), success(Coll[Int](1, 2, 1, 2))), - ((Coll[Int](1, 2), (1, 0)), success(Coll[Int](1, 1, 2, 2))), - ((Coll[Int](1, 2), (0, 2)), success(Coll[Int](1, 2))), - ((Coll[Int](1, 2), (0, 3)), success(Coll[Int](1, 2))), - ((Coll[Int](1, 2), (1, 2)), success(Coll[Int](1, 1, 2))), - ((Coll[Int](1, 2), (2, 0)), success(Coll[Int](1, 2, 1, 2))), - ((Coll[Int](1, 2), (3, 0)), success(Coll[Int](1, 2, 1, 2))), - ((Coll[Int](1, 2), (3, 1)), success(Coll[Int](1, 2, 1, 2))), - ((Coll[Int](1, 2), (-1, 1)), success(Coll[Int](1, 2, 2))) + ((Coll[Int](), (0, 0)), success(Coll[Int](), costDetails(0))), + ((Coll[Int](1), (0, 0)), success(Coll[Int](1, 1), costDetails(2))), + ((Coll[Int](1), (0, 1)), success(Coll[Int](1), costDetails(2))), + ((Coll[Int](1, 2), (0, 0)), success(Coll[Int](1, 2, 1, 2), costDetails(4))), + ((Coll[Int](1, 2), (1, 0)), success(Coll[Int](1, 1, 2, 2), costDetails(4))), + ((Coll[Int](1, 2), (0, 2)), success(Coll[Int](1, 2), costDetails(4))), + ((Coll[Int](1, 2), (0, 3)), success(Coll[Int](1, 2), costDetails(4))), + ((Coll[Int](1, 2), (1, 2)), success(Coll[Int](1, 1, 2), costDetails(4))), + ((Coll[Int](1, 2), (2, 0)), success(Coll[Int](1, 2, 1, 2), costDetails(4))), + ((Coll[Int](1, 2), (3, 0)), success(Coll[Int](1, 2, 1, 2), costDetails(4))), + ((Coll[Int](1, 2), (3, 1)), success(Coll[Int](1, 2, 1, 2), costDetails(4))), + ((Coll[Int](1, 2), (-1, 1)), success(Coll[Int](1, 2, 2), costDetails(4))) ) }, existingFeature( @@ -5091,16 +7494,37 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("Coll updated method equivalence") { + def costDetails(i: Int) = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + SeqCostItem(MethodDesc(SCollection.UpdatedMethod), PerItemCost(JitCost(20), JitCost(1), 10), i) + ) + ) verifyCases( // (coll, (index, elem)) { - def success[T](v: T) = Expected(Success(v), 37180) + def success[T](v: T, cd: CostDetails) = Expected(Success(v), 37180, cd, 1774) Seq( ((Coll[Int](), (0, 0)), Expected(new IndexOutOfBoundsException("0"))), - ((Coll[Int](1), (0, 0)), success(Coll[Int](0))), - ((Coll[Int](1, 2), (0, 0)), success(Coll[Int](0, 2))), - ((Coll[Int](1, 2), (1, 0)), success(Coll[Int](1, 0))), - ((Coll[Int](1, 2, 3), (2, 0)), success(Coll[Int](1, 2, 0))), + ((Coll[Int](1), (0, 0)), success(Coll[Int](0), costDetails(1))), + ((Coll[Int](1, 2), (0, 0)), success(Coll[Int](0, 2), costDetails(2))), + ((Coll[Int](1, 2), (1, 0)), success(Coll[Int](1, 0), costDetails(2))), + ((Coll[Int](1, 2, 3), (2, 0)), success(Coll[Int](1, 2, 0), costDetails(3))), ((Coll[Int](1, 2), (2, 0)), Expected(new IndexOutOfBoundsException("2"))), ((Coll[Int](1, 2), (3, 0)), Expected(new IndexOutOfBoundsException("3"))), ((Coll[Int](1, 2), (-1, 0)), Expected(new IndexOutOfBoundsException("-1"))) @@ -5147,27 +7571,48 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } yield (coll, (is.toColl, vs)), MinSuccessful(20)) + def costDetails(i: Int) = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + SeqCostItem(MethodDesc(SCollection.UpdateManyMethod), PerItemCost(JitCost(20), JitCost(2), 10), i) + ) + ) verifyCases( // (coll, (indexes, values)) { - def success[T](v: T) = Expected(Success(v), 37817) + def success[T](v: T, i: Int) = Expected(Success(v), 37817, costDetails(i), 1774) Seq( ((Coll[Int](), (Coll(0), Coll(0))), Expected(new IndexOutOfBoundsException("0"))), ((Coll[Int](), (Coll(0, 1), Coll(0, 0))), Expected(new IndexOutOfBoundsException("0"))), ((Coll[Int](), (Coll(0, 1), Coll(0))), Expected(new IllegalArgumentException("requirement failed: Collections should have same length but was 2 and 1:\n xs=Coll(0,1);\n ys=Coll(0)"))), - ((Coll[Int](1), (Coll(0), Coll(0))), success(Coll[Int](0))), + ((Coll[Int](1), (Coll(0), Coll(0))), success(Coll[Int](0), 1)), ((Coll[Int](1), (Coll(0, 1), Coll(0, 0))), Expected(new IndexOutOfBoundsException("1"))), - ((Coll[Int](1, 2), (Coll(0), Coll(0))), success(Coll[Int](0, 2))), - ((Coll[Int](1, 2), (Coll(0, 1), Coll(0, 0))), success(Coll[Int](0, 0))), + ((Coll[Int](1, 2), (Coll(0), Coll(0))), success(Coll[Int](0, 2), 2)), + ((Coll[Int](1, 2), (Coll(0, 1), Coll(0, 0))), success(Coll[Int](0, 0), 2)), ((Coll[Int](1, 2), (Coll(0, 1, 2), Coll(0, 0, 0))), Expected(new IndexOutOfBoundsException("2"))), - ((Coll[Int](1, 2), (Coll(1), Coll(0))), success(Coll[Int](1, 0))), - ((Coll[Int](1, 2, 3), (Coll(2), Coll(0))), success(Coll[Int](1, 2, 0))), + ((Coll[Int](1, 2), (Coll(1), Coll(0))), success(Coll[Int](1, 0), 2)), + ((Coll[Int](1, 2, 3), (Coll(2), Coll(0))), success(Coll[Int](1, 2, 0), 3)), ((Coll[Int](1, 2), (Coll(2), Coll(0))), Expected(new IndexOutOfBoundsException("2"))), ((Coll[Int](1, 2), (Coll(3), Coll(0))), Expected(new IndexOutOfBoundsException("3"))), ((Coll[Int](1, 2), (Coll(-1), Coll(0))), Expected(new IndexOutOfBoundsException("-1"))), ((Coll[Int](10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140), (Coll[Int](12, 12, 4, 11, 1, 8, 0, 1), Coll[Int](-10, -20, -30, -40, -50, -60, -70, -80))), - success(Coll[Int](-70, -80, 30, 40, -30, 60, 70, 80, -60, 100, 110, -40, -20, 140))) + success(Coll[Int](-70, -80, 30, 40, -30, 60, 70, 80, -60, 100, 110, -40, -20, 140), 14)) ) }, existingFeature( @@ -5212,7 +7657,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui preGeneratedSamples = Some(samples)) } - // TODO HF (3h): https://github.com/ScorexFoundation/sigmastate-interpreter/issues/479 + // TODO v6.0 (3h): https://github.com/ScorexFoundation/sigmastate-interpreter/issues/479 property("Coll find method equivalence") { val find = newFeature((x: Coll[Int]) => x.find({ (v: Int) => v > 0 }), "{ (x: Coll[Int]) => x.find({ (v: Int) => v > 0} ) }") @@ -5221,7 +7666,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } } - // TODO HF (3h): https://github.com/ScorexFoundation/sigmastate-interpreter/issues/418 + // TODO v6.0 (3h): https://github.com/ScorexFoundation/sigmastate-interpreter/issues/418 property("Coll bitwise methods equivalence") { val shiftRight = newFeature( { (x: Coll[Boolean]) => @@ -5233,7 +7678,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } } - // TODO HF (3h): https://github.com/ScorexFoundation/sigmastate-interpreter/issues/479 + // TODO v6.0 (3h): https://github.com/ScorexFoundation/sigmastate-interpreter/issues/479 property("Coll diff methods equivalence") { val diff = newFeature((x: (Coll[Int], Coll[Int])) => x._1.diff(x._2), "{ (x: (Coll[Int], Coll[Int])) => x._1.diff(x._2) }") @@ -5244,121 +7689,440 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Coll fold method equivalence") { val n = ExactNumeric.IntIsExactNumeric - verifyCases( - // (coll, initState) - { - def success[T](v: T, c: Int) = Expected(Success(v), c) - Seq( - ((Coll[Byte](), 0), success(0, 41266)), - ((Coll[Byte](), Int.MaxValue), success(Int.MaxValue, 41266)), - ((Coll[Byte](1), Int.MaxValue - 1), success(Int.MaxValue, 41396)), - ((Coll[Byte](1), Int.MaxValue), Expected(new ArithmeticException("integer overflow"))), - ((Coll[Byte](-1), Int.MinValue + 1), success(Int.MinValue, 41396)), - ((Coll[Byte](-1), Int.MinValue), Expected(new ArithmeticException("integer overflow"))), - ((Coll[Byte](1, 2), 0), success(3, 41526)), - ((Coll[Byte](1, -1), 0), success(0, 41526)), - ((Coll[Byte](1, -1, 1), 0), success(1, 41656)) - ) - }, - existingFeature( - { (x: (Coll[Byte], Int)) => x._1.foldLeft(x._2, { i: (Int, Byte) => n.plus(i._1, i._2) }) }, - "{ (x: (Coll[Byte], Int)) => x._1.fold(x._2, { (i1: Int, i2: Byte) => i1 + i2 }) }", - FuncValue( - Vector((1, SPair(SByteArray, SInt))), - Fold( - SelectField.typed[Value[SCollection[SByte.type]]](ValUse(1, SPair(SByteArray, SInt)), 1.toByte), - SelectField.typed[Value[SInt.type]](ValUse(1, SPair(SByteArray, SInt)), 2.toByte), - FuncValue( - Vector((3, SPair(SInt, SByte))), - ArithOp( - SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SByte)), 1.toByte), - Upcast(SelectField.typed[Value[SByte.type]](ValUse(3, SPair(SInt, SByte)), 2.toByte), SInt), - OpCode @@ (-102.toByte) + val costDetails1 = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Fold), PerItemCost(JitCost(3), JitCost(1), 10), 0) + ) + ) + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Fold), PerItemCost(JitCost(3), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + TypeBasedCostItem(ArithOp.Plus, SInt) + ) + ) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Fold), PerItemCost(JitCost(3), JitCost(1), 10), 2), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + TypeBasedCostItem(ArithOp.Plus, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + TypeBasedCostItem(ArithOp.Plus, SInt) + ) + ) + val costDetails4 = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Fold), PerItemCost(JitCost(3), JitCost(1), 10), 3), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + TypeBasedCostItem(ArithOp.Plus, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + TypeBasedCostItem(ArithOp.Plus, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + TypeBasedCostItem(ArithOp.Plus, SInt) + ) + ) + if (lowerMethodCallsInTests) { + verifyCases( + // (coll, initState) + { + Seq( + ((Coll[Byte](), 0), Expected(Success(0), 41266, costDetails1, 1767)), + ((Coll[Byte](), Int.MaxValue), Expected(Success(Int.MaxValue), 41266, costDetails1, 1767)), + ((Coll[Byte](1), Int.MaxValue - 1), Expected(Success(Int.MaxValue), 41396, costDetails2, 1773)), + ((Coll[Byte](1), Int.MaxValue), Expected(new ArithmeticException("integer overflow"))), + ((Coll[Byte](-1), Int.MinValue + 1), Expected(Success(Int.MinValue), 41396, costDetails2, 1773)), + ((Coll[Byte](-1), Int.MinValue), Expected(new ArithmeticException("integer overflow"))), + ((Coll[Byte](1, 2), 0), Expected(Success(3), 41526, costDetails3, 1779)), + ((Coll[Byte](1, -1), 0), Expected(Success(0), 41526, costDetails3, 1779)), + ((Coll[Byte](1, -1, 1), 0), Expected(Success(1), 41656, costDetails4, 1785)) + ) + }, + existingFeature( + { (x: (Coll[Byte], Int)) => x._1.foldLeft(x._2, { i: (Int, Byte) => n.plus(i._1, i._2) }) }, + "{ (x: (Coll[Byte], Int)) => x._1.fold(x._2, { (i1: Int, i2: Byte) => i1 + i2 }) }", + FuncValue( + Vector((1, SPair(SByteArray, SInt))), + Fold( + SelectField.typed[Value[SCollection[SByte.type]]](ValUse(1, SPair(SByteArray, SInt)), 1.toByte), + SelectField.typed[Value[SInt.type]](ValUse(1, SPair(SByteArray, SInt)), 2.toByte), + FuncValue( + Vector((3, SPair(SInt, SByte))), + ArithOp( + SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SByte)), 1.toByte), + Upcast(SelectField.typed[Value[SByte.type]](ValUse(3, SPair(SInt, SByte)), 2.toByte), SInt), + OpCode @@ (-102.toByte) + ) ) ) - ) - ))) + ))) + } else { + assertExceptionThrown( + verifyCases( + Seq( ((Coll[Byte](), 0), Expected(Success(0), 41266)) ), + existingFeature( + { (x: (Coll[Byte], Int)) => x._1.foldLeft(x._2, { i: (Int, Byte) => n.plus(i._1, i._2) }) }, + "{ (x: (Coll[Byte], Int)) => x._1.fold(x._2, { (i1: Int, i2: Byte) => i1 + i2 }) }" + )), + rootCauseLike[CosterException]("Don't know how to evalNode(Lambda(List()") + ) + } } property("Coll fold with nested If") { val n = ExactNumeric.IntIsExactNumeric - verifyCases( - // (coll, initState) - { - def success[T](v: T, c: Int) = Expected(Success(v), c) - Seq( - ((Coll[Byte](), 0), success(0, 42037)), - ((Coll[Byte](), Int.MaxValue), success(Int.MaxValue, 42037)), - ((Coll[Byte](1), Int.MaxValue - 1), success(Int.MaxValue, 42197)), - ((Coll[Byte](1), Int.MaxValue), Expected(new ArithmeticException("integer overflow"))), - ((Coll[Byte](-1), Int.MinValue + 1), success(Int.MinValue + 1, 42197)), - ((Coll[Byte](-1), Int.MinValue), success(Int.MinValue, 42197)), - ((Coll[Byte](1, 2), 0), success(3, 42357)), - ((Coll[Byte](1, -1), 0), success(1, 42357)), - ((Coll[Byte](1, -1, 1), 0), success(2, 42517)) - ) - }, - existingFeature( - { (x: (Coll[Byte], Int)) => x._1.foldLeft(x._2, { i: (Int, Byte) => if (i._2 > 0) n.plus(i._1, i._2) else i._1 }) }, - "{ (x: (Coll[Byte], Int)) => x._1.fold(x._2, { (i1: Int, i2: Byte) => if (i2 > 0) i1 + i2 else i1 }) }", - FuncValue( - Array((1, SPair(SByteArray, SInt))), - Fold( - SelectField.typed[Value[SCollection[SByte.type]]](ValUse(1, SPair(SByteArray, SInt)), 1.toByte), - SelectField.typed[Value[SInt.type]](ValUse(1, SPair(SByteArray, SInt)), 2.toByte), - FuncValue( - Array((3, SPair(SInt, SByte))), - BlockValue( - Array( - ValDef( - 5, - List(), - Upcast( - SelectField.typed[Value[SByte.type]](ValUse(3, SPair(SInt, SByte)), 2.toByte), - SInt + val costDetails1 = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Fold), PerItemCost(JitCost(3), JitCost(1), 10), 0) + ) + ) + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Fold), PerItemCost(JitCost(3), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Plus, SInt) + ) + ) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Fold), PerItemCost(JitCost(3), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse) + ) + ) + val costDetails4 = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Fold), PerItemCost(JitCost(3), JitCost(1), 10), 2), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Plus, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Plus, SInt) + ), + Some(232417L) + ) + val costDetails5 = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Fold), PerItemCost(JitCost(3), JitCost(1), 10), 2), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Plus, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse) + ) + ) + val costDetails6 = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Fold), PerItemCost(JitCost(3), JitCost(1), 10), 3), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Plus, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 2), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + TypeBasedCostItem(Upcast, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Plus, SInt) + ) + ) + if (lowerMethodCallsInTests) { + verifyCases( + // (coll, initState) + { + Seq( + ((Coll[Byte](), 0), Expected(Success(0), 42037, costDetails1, 1767)), + ((Coll[Byte](), Int.MaxValue), Expected(Success(Int.MaxValue), 42037, costDetails1, 1767)), + ((Coll[Byte](1), Int.MaxValue - 1), Expected(Success(Int.MaxValue), 42197, costDetails2, 1779)), + ((Coll[Byte](1), Int.MaxValue), Expected(new ArithmeticException("integer overflow"))), + ((Coll[Byte](-1), Int.MinValue + 1), Expected(Success(Int.MinValue + 1), 42197, costDetails3, 1777)), + ((Coll[Byte](-1), Int.MinValue), Expected(Success(Int.MinValue), 42197, costDetails3, 1777)), + ((Coll[Byte](1, 2), 0), Expected(Success(3), 42357, costDetails4, 1791)), + ((Coll[Byte](1, -1), 0), Expected(Success(1), 42357, costDetails5, 1789)), + ((Coll[Byte](1, -1, 1), 0), Expected(Success(2), 42517, costDetails6, 1801)) + ) + }, + existingFeature( + { (x: (Coll[Byte], Int)) => x._1.foldLeft(x._2, { i: (Int, Byte) => if (i._2 > 0) n.plus(i._1, i._2) else i._1 }) }, + "{ (x: (Coll[Byte], Int)) => x._1.fold(x._2, { (i1: Int, i2: Byte) => if (i2 > 0) i1 + i2 else i1 }) }", + FuncValue( + Array((1, SPair(SByteArray, SInt))), + Fold( + SelectField.typed[Value[SCollection[SByte.type]]](ValUse(1, SPair(SByteArray, SInt)), 1.toByte), + SelectField.typed[Value[SInt.type]](ValUse(1, SPair(SByteArray, SInt)), 2.toByte), + FuncValue( + Array((3, SPair(SInt, SByte))), + BlockValue( + Array( + ValDef( + 5, + List(), + Upcast( + SelectField.typed[Value[SByte.type]](ValUse(3, SPair(SInt, SByte)), 2.toByte), + SInt + ) + ), + ValDef( + 6, + List(), + SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SByte)), 1.toByte) ) ), - ValDef( - 6, - List(), - SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SByte)), 1.toByte) + If( + GT(ValUse(5, SInt), IntConstant(0)), + ArithOp(ValUse(6, SInt), ValUse(5, SInt), OpCode @@ (-102.toByte)), + ValUse(6, SInt) ) - ), - If( - GT(ValUse(5, SInt), IntConstant(0)), - ArithOp(ValUse(6, SInt), ValUse(5, SInt), OpCode @@ (-102.toByte)), - ValUse(6, SInt) ) ) ) - ) - ) )) + ) )) + } else { + assertExceptionThrown( + verifyCases( + Seq( ((Coll[Byte](), 0), Expected(Success(0), 42037)) ), + existingFeature( + { (x: (Coll[Byte], Int)) => x._1.foldLeft(x._2, { i: (Int, Byte) => if (i._2 > 0) n.plus(i._1, i._2) else i._1 }) }, + "{ (x: (Coll[Byte], Int)) => x._1.fold(x._2, { (i1: Int, i2: Byte) => if (i2 > 0) i1 + i2 else i1 }) }" + )), + rootCauseLike[CosterException]("Don't know how to evalNode(Lambda(List()") + ) + } } property("Coll indexOf method equivalence") { + def costDetails(i: Int) = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField) + ) + ++ Array.fill(i)(FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3)))) + :+ SeqCostItem(MethodDesc(SCollection.IndexOfMethod), PerItemCost(JitCost(20), JitCost(10), 2), i) + ) verifyCases( // (coll, (elem: Byte, from: Int)) { - def success[T](v: T) = Expected(Success(v), 37649) + def success0[T](v: T) = Expected(Success(v), 37649, costDetails(0), 1773) + def success1[T](v: T) = Expected(Success(v), 37649, costDetails(1), 1773) + def success2[T](v: T) = Expected(Success(v), 37649, costDetails(2), 1774) + def success3[T](v: T) = Expected(Success(v), 37649, costDetails(3), 1775) + def success12[T](v: T) = Expected(Success(v), 37649, costDetails(12), 1782) Seq( - ((Coll[Byte](), (0.toByte, 0)), success(-1)), - ((Coll[Byte](), (0.toByte, -1)), success(-1)), - ((Coll[Byte](), (0.toByte, 1)), success(-1)), - ((Coll[Byte](1), (0.toByte, 0)), success(-1)), - ((Coll[Byte](1), (1.toByte, 0)), success(0)), - ((Coll[Byte](1), (1.toByte, 1)), success(-1)), - ((Coll[Byte](1, 1), (0.toByte, -1)), success(-1)), - ((Coll[Byte](1, 1), (0.toByte, 0)), success(-1)), - ((Coll[Byte](1, 1), (1.toByte, -1)), success(0)), - ((Coll[Byte](1, 1), (1.toByte, 0)), success(0)), - ((Coll[Byte](1, 1), (1.toByte, 1)), success(1)), - ((Coll[Byte](1, 1), (1.toByte, 2)), success(-1)), - ((Coll[Byte](1, 1), (1.toByte, 3)), success(-1)), - ((Coll[Byte](1, 2, 3), (3.toByte, 0)), success(2)), - ((Coll[Byte](1, 2, 3), (3.toByte, 1)), success(2)), - ((Coll[Byte](1, 2, 3), (3.toByte, 2)), success(2)), - ((Coll[Byte](1, 2, 3), (3.toByte, 3)), success(-1)), - ((Helpers.decodeBytes("8085623fb7cd6b7f01801f00800100"), (0.toByte, -1)), success(11)) + ((Coll[Byte](), (0.toByte, 0)), success0(-1)), + ((Coll[Byte](), (0.toByte, -1)), success0(-1)), + ((Coll[Byte](), (0.toByte, 1)), success0(-1)), + ((Coll[Byte](1), (0.toByte, 0)), success1(-1)), + ((Coll[Byte](1), (1.toByte, 0)), success1(0)), + ((Coll[Byte](1), (1.toByte, 1)), success0(-1)), + ((Coll[Byte](1, 1), (0.toByte, -1)), success2(-1)), + ((Coll[Byte](1, 1), (0.toByte, 0)), success2(-1)), + ((Coll[Byte](1, 1), (1.toByte, -1)), success1(0)), + ((Coll[Byte](1, 1), (1.toByte, 0)), success1(0)), + ((Coll[Byte](1, 1), (1.toByte, 1)), success1(1)), + ((Coll[Byte](1, 1), (1.toByte, 2)), success0(-1)), + ((Coll[Byte](1, 1), (1.toByte, 3)), success0(-1)), + ((Coll[Byte](1, 2, 3), (3.toByte, 0)), success3(2)), + ((Coll[Byte](1, 2, 3), (3.toByte, 1)), success2(2)), + ((Coll[Byte](1, 2, 3), (3.toByte, 2)), success1(2)), + ((Coll[Byte](1, 2, 3), (3.toByte, 3)), success0(-1)), + ((Helpers.decodeBytes("8085623fb7cd6b7f01801f00800100"), (0.toByte, -1)), success12(11)) ) }, existingFeature( @@ -5387,13 +8151,21 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui Map() ) ) - ))) + )), preGeneratedSamples = Some(Seq())) } property("Coll apply method equivalence") { + val costDetails = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ByIndex) + ) + ) verifyCases( { - def success[T](v: T) = Expected(Success(v), 36410) + def success[T](v: T) = Expected(Success(v), 36410, costDetails, 1769) Seq( ((Coll[Int](), 0), Expected(new ArrayIndexOutOfBoundsException("0"))), ((Coll[Int](), -1), Expected(new ArrayIndexOutOfBoundsException("-1"))), @@ -5422,10 +8194,31 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Coll getOrElse method equivalence") { val default = 10 - verifyCases( + val costDetails = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ByIndex) + ) + ) + if (lowerMethodCallsInTests) { + verifyCases( // (coll, (index, default)) { - def success[T](v: T) = Expected(Success(v), 37020) + def success[T](v: T) = Expected(Success(v), 37020, costDetails, 1773) Seq( ((Coll[Int](), (0, default)), success(default)), ((Coll[Int](), (-1, default)), success(default)), @@ -5463,12 +8256,101 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ) ))) + } else { + forEachScriptAndErgoTreeVersion(activatedVersions, ergoTreeVersions) { + testCases( + Seq( + ((Coll[Int](1, 2), (0, default)), + Expected( + // in v4.x exp method cannot be called via MethodCall ErgoTree node + Failure(new NoSuchMethodException("")), + cost = 41484, + CostDetails.ZeroCost, + newCost = 0, + newVersionedResults = { + // in v5.0 MethodCall ErgoTree node is allowed + val res = ExpectedResult( Success(1), Some(166) ) + val details = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(SCollection.GetOrElseMethod, FixedCost(JitCost(30))) + ) + ) + Seq.tabulate(3)(v => v -> ( res -> Some(details) )) // expected result for each version + } + ) + ) + ), + changedFeature( + (x: (Coll[Int], (Int, Int))) => + throw new NoSuchMethodException(""), + (x: (Coll[Int], (Int, Int))) => x._1.getOrElse(x._2._1, x._2._2), + ""/*can't be compiled in v4.x*/, + FuncValue( + Vector((1, SPair(SCollectionType(SInt), SPair(SInt, SInt)))), + BlockValue( + Vector( + ValDef( + 3, + List(), + SelectField.typed[Value[STuple]]( + ValUse(1, SPair(SCollectionType(SInt), SPair(SInt, SInt))), + 2.toByte + ) + ) + ), + MethodCall( + SelectField.typed[Value[SCollection[SInt.type]]]( + ValUse(1, SPair(SCollectionType(SInt), SPair(SInt, SInt))), + 1.toByte + ), + SCollection.getMethodByName("getOrElse").withConcreteTypes( + Map(STypeVar("IV") -> SInt) + ), + Vector( + SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SInt)), 1.toByte), + SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SInt)), 2.toByte) + ), + Map() + ) + ) + ), + allowNewToSucceed = true, + allowDifferentErrors = false + )) + } + } } property("Tuple size method equivalence") { + val costDetails = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(Constant) + ) + ) verifyCases( { - def success[T](v: T) = Expected(Success(v), 35905) + def success[T](v: T) = Expected(Success(v), 35905, costDetails, 1763) Seq( ((0, 0), success(2)), ((1, 2), success(2)) @@ -5481,8 +8363,9 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("Tuple apply method equivalence") { val samples = genSamples[(Int, Int)](DefaultMinSuccessful) + val costDetails = TracedCost(traceBase :+ FixedCostItem(SelectField)) verifyCases( - Seq(((1, 2), Expected(Success(1), cost = 36013))), + Seq(((1, 2), Expected(Success(1), cost = 36013, costDetails, 1764))), existingFeature((x: (Int, Int)) => x._1, "{ (x: (Int, Int)) => x(0) }", FuncValue( @@ -5491,7 +8374,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui )), preGeneratedSamples = Some(samples)) verifyCases( - Seq(((1, 2), Expected(Success(2), cost = 36013))), + Seq(((1, 2), Expected(Success(2), cost = 36013, costDetails, 1764))), existingFeature((x: (Int, Int)) => x._2, "{ (x: (Int, Int)) => x(1) }", FuncValue( @@ -5502,14 +8385,31 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("Coll map method equivalence") { + def repeatPlusChunk(i: Int): Array[CostItem] = Array.fill(i){ + Array( + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(ArithOp.Plus, SInt) + ) + }.flatten + def costDetails(i: Int) = TracedCost( + traceBase + ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(MapCollection), PerItemCost(JitCost(20), JitCost(1), 10), i) + ) + ++ repeatPlusChunk(i) + ) + val n = ExactNumeric.IntIsExactNumeric verifyCases( { def success[T](v: T, c: Int) = Expected(Success(v), c) Seq( - (Coll[Int](), success(Coll[Int](), 38886)), - (Coll[Int](1), success(Coll[Int](2), 38936)), - (Coll[Int](1, 2), success(Coll[Int](2, 3), 38986)), + (Coll[Int](), Expected(Success(Coll[Int]()), 38886, costDetails(0), 1768)), + (Coll[Int](1), Expected(Success(Coll[Int](2)), 38936, costDetails(1), 1771)), + (Coll[Int](1, 2), Expected(Success(Coll[Int](2, 3)), 38986, costDetails(2), 1774)), (Coll[Int](1, 2, Int.MaxValue), Expected(new ArithmeticException("integer overflow"))) ) }, @@ -5525,15 +8425,71 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("Coll map with nested if") { + val costDetails1 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(MapCollection), PerItemCost(JitCost(20), JitCost(1), 10), 0) + ) + ) + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(MapCollection), PerItemCost(JitCost(20), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(ArithOp.Plus, SInt) + ) + ) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(MapCollection), PerItemCost(JitCost(20), JitCost(1), 10), 1), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(Constant), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Multiply, SInt) + ) + ) + val costDetails4 = TracedCost( + traceBase ++ Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(MapCollection), PerItemCost(JitCost(20), JitCost(1), 10), 2), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(ArithOp.Plus, SInt), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(Constant), + FixedCostItem(ValUse), + TypeBasedCostItem(ArithOp.Multiply, SInt) + ) + ) val n = ExactNumeric.IntIsExactNumeric verifyCases( { def success[T](v: T, c: Int) = Expected(Success(v), c) Seq( - (Coll[Int](), success(Coll[Int](), 39571)), - (Coll[Int](1), success(Coll[Int](2), 39671)), - (Coll[Int](-1), success(Coll[Int](1), 39671)), - (Coll[Int](1, -2), success(Coll[Int](2, 2), 39771)), + (Coll[Int](), Expected(Success(Coll[Int]()), 39571, costDetails1, 1768)), + (Coll[Int](1), Expected(Success(Coll[Int](2)), 39671, costDetails2, 1775)), + (Coll[Int](-1), Expected(Success(Coll[Int](1)), 39671, costDetails3, 1775)), + (Coll[Int](1, -2), Expected(Success(Coll[Int](2, 2)), 39771, costDetails4, 1782)), (Coll[Int](1, 2, Int.MaxValue), Expected(new ArithmeticException("integer overflow"))), (Coll[Int](1, 2, Int.MinValue), Expected(new ArithmeticException("integer overflow"))) ) @@ -5558,16 +8514,35 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("Coll filter") { + def costDetails(i: Int) = { + val gtChunk = Array.fill(i)( + Array( + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt) + ) + ).flatten + + TracedCost( + traceBase ++ + Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Filter), PerItemCost(JitCost(20), JitCost(1), 10), i) + ) ++ + gtChunk + ) + } + val o = ExactOrdering.IntIsExactOrdering verifyCases( { - def success[T](v: T, c: Int) = Expected(Success(v), c) Seq( - (Coll[Int](), success(Coll[Int](), 37223)), - (Coll[Int](1), success(Coll[Int](1), 37273)), - (Coll[Int](1, 2), success(Coll[Int](1, 2), 37323)), - (Coll[Int](1, 2, -1), success(Coll[Int](1, 2), 37373)), - (Coll[Int](1, -1, 2, -2), success(Coll[Int](1, 2), 37423)) + (Coll[Int](), Expected(Success(Coll[Int]()), 37223, costDetails(0), 1768)), + (Coll[Int](1), Expected(Success(Coll[Int](1)), 37273, costDetails(1), 1771)), + (Coll[Int](1, 2), Expected(Success(Coll[Int](1, 2)), 37323, costDetails(2), 1775)), + (Coll[Int](1, 2, -1), Expected(Success(Coll[Int](1, 2)), 37373, costDetails(3), 1778)), + (Coll[Int](1, -1, 2, -2), Expected(Success(Coll[Int](1, 2)), 37423, costDetails(4), 1782)) ) }, existingFeature((x: Coll[Int]) => x.filter({ (v: Int) => o.gt(v, 0) }), @@ -5582,17 +8557,69 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("Coll filter with nested If") { + val leftBranch = Array( + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(LT, SInt) + ) + val rightBranch = Array( + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SInt), + FixedCostItem(If), + FixedCostItem(Constant) + ) + def repeatLeftBranch(i: Int) = Array.fill(i)(leftBranch).flatten + + def costDetails(i: Int) = { + TracedCost( + traceBase ++ + Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Filter), PerItemCost(JitCost(20), JitCost(1), 10), i) + ) ++ + repeatLeftBranch(i) + ) + } + val costDetails3 = TracedCost( + traceBase ++ + Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Filter), PerItemCost(JitCost(20), JitCost(1), 10), 3) + ) ++ + repeatLeftBranch(2) ++ + rightBranch + ) + val costDetails5 = TracedCost( + traceBase ++ + Array( + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(Filter), PerItemCost(JitCost(20), JitCost(1), 10), 5) + ) ++ + leftBranch ++ + rightBranch ++ + leftBranch ++ + rightBranch ++ + leftBranch + ) + val o = ExactOrdering.IntIsExactOrdering verifyCases( { def success[T](v: T, c: Int) = Expected(Success(v), c) Seq( - (Coll[Int](), success(Coll[Int](), 37797)), - (Coll[Int](1), success(Coll[Int](1), 37887)), - (Coll[Int](10), success(Coll[Int](), 37887)), - (Coll[Int](1, 2), success(Coll[Int](1, 2), 37977)), - (Coll[Int](1, 2, 0), success(Coll[Int](1, 2), 38067)), - (Coll[Int](1, -1, 2, -2, 11), success(Coll[Int](1, 2), 38247)) + (Coll[Int](), Expected(Success(Coll[Int]()), 37797, costDetails(0), 1768)), + (Coll[Int](1), Expected(Success(Coll[Int](1)), 37887, costDetails(1), 1775)), + (Coll[Int](10), Expected(Success(Coll[Int]()), 37887, costDetails(1), 1775)), + (Coll[Int](1, 2), Expected(Success(Coll[Int](1, 2)), 37977, costDetails(2), 1783)), + (Coll[Int](1, 2, 0), Expected(Success(Coll[Int](1, 2)), 38067, costDetails3, 1788)), + (Coll[Int](1, -1, 2, -2, 11), Expected(Success(Coll[Int](1, 2)), 38247, costDetails5, 1800)) ) }, existingFeature((x: Coll[Int]) => x.filter({ (v: Int) => if (o.gt(v, 0)) v < 10 else false }), @@ -5610,22 +8637,44 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("Coll slice method equivalence") { + def costDetails(i: Int) = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + SeqCostItem(CompanionDesc(Slice), PerItemCost(JitCost(10), JitCost(2), 100), i) + ) + ) val samples = genSamples(collWithRangeGen, DefaultMinSuccessful) - verifyCases( - // (coll, (from, until)) + if (lowerMethodCallsInTests) { + verifyCases( { - def success[T](v: T) = Expected(Success(v), 36964) + val cost = 36964 + val newCost = 1772 Seq( - ((Coll[Int](), (-1, 0)), success(Coll[Int]())), - ((Coll[Int](), (0, 0)), success(Coll[Int]())), - ((Coll[Int](1), (0, 0)), success(Coll[Int]())), - ((Coll[Int](1), (0, -1)), success(Coll[Int]())), - ((Coll[Int](1), (1, 1)), success(Coll[Int]())), - ((Coll[Int](1), (-1, 1)), success(Coll[Int](1))), - ((Coll[Int](1, 2), (1, 1)), success(Coll[Int]())), - ((Coll[Int](1, 2), (1, 0)), success(Coll[Int]())), - ((Coll[Int](1, 2), (1, 2)), success(Coll[Int](2))), - ((Coll[Int](1, 2, 3, 4), (1, 3)), success(Coll[Int](2, 3))) + // (coll, (from, until)) + ((Coll[Int](), (-1, 0)), Expected(Success(Coll[Int]()), cost, costDetails(1), newCost)), + ((Coll[Int](), (0, 0)), Expected(Success(Coll[Int]()), cost, costDetails(0), newCost)), + ((Coll[Int](1), (0, 0)), Expected(Success(Coll[Int]()), cost, costDetails(0), newCost)), + ((Coll[Int](1), (0, -1)), Expected(Success(Coll[Int]()), cost, costDetails(0), newCost)), + ((Coll[Int](1), (1, 1)), Expected(Success(Coll[Int]()), cost, costDetails(0), newCost)), + ((Coll[Int](1), (-1, 1)), Expected(Success(Coll[Int](1)), cost, costDetails(2), newCost)), + ((Coll[Int](1, 2), (1, 1)), Expected(Success(Coll[Int]()), cost, costDetails(0), newCost)), + ((Coll[Int](1, 2), (1, 0)), Expected(Success(Coll[Int]()), cost, costDetails(0), newCost)), + ((Coll[Int](1, 2), (1, 2)), Expected(Success(Coll[Int](2)), cost, costDetails(1), newCost)), + ((Coll[Int](1, 2, 3, 4), (1, 3)), Expected(Success(Coll[Int](2, 3)), cost, costDetails(2), newCost)) ) }, existingFeature((x: (Coll[Int], (Int, Int))) => x._1.slice(x._2._1, x._2._2), @@ -5653,21 +8702,43 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ) )), - preGeneratedSamples = Some(samples)) + preGeneratedSamples = Some(samples)) + } else { + assertExceptionThrown( + verifyCases( + Seq( (Coll[Int](), (-1, 0)) -> Expected(Success(Coll[Int]()), 0, CostDetails.ZeroCost, 0) ), + existingFeature((x: (Coll[Int], (Int, Int))) => x._1.slice(x._2._1, x._2._2), + "{ (x: (Coll[Int], (Int, Int))) => x._1.slice(x._2._1, x._2._2) }" + )), + rootCauseLike[NoSuchMethodException]("sigmastate.eval.CostingRules$CollCoster.slice") + ) + } } - property("Coll append method equivalence") { - verifyCases( + property("Coll.append equivalence") { + def costDetails(i: Int) = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + SeqCostItem(CompanionDesc(Append), PerItemCost(JitCost(20), JitCost(2), 100), i) + ) + ) + if (lowerMethodCallsInTests) { + verifyCases( { - def success[T](v: T) = Expected(Success(v), 37765) + def success[T](v: T, size: Int) = Expected(Success(v), 37765, costDetails(size), 1770) + val arr1 = Gen.listOfN(100, arbitrary[Int]).map(_.toArray).sample.get + val arr2 = Gen.listOfN(200, arbitrary[Int]).map(_.toArray).sample.get Seq( - (Coll[Int](), Coll[Int]()) -> success(Coll[Int]()), - (Coll[Int](), Coll[Int](1)) -> success(Coll[Int](1)), - (Coll[Int](1), Coll[Int]()) -> success(Coll[Int](1)), - (Coll[Int](1), Coll[Int](2)) -> success(Coll[Int](1, 2)), - (Coll[Int](1), Coll[Int](2, 3)) -> success(Coll[Int](1, 2, 3)), - (Coll[Int](1, 2), Coll[Int](3)) -> success(Coll[Int](1, 2, 3)), - (Coll[Int](1, 2), Coll[Int](3, 4)) -> success(Coll[Int](1, 2, 3, 4)) + (Coll[Int](), Coll[Int]()) -> success(Coll[Int](), 0), + (Coll[Int](), Coll[Int](1)) -> success(Coll[Int](1), 1), + (Coll[Int](1), Coll[Int]()) -> success(Coll[Int](1), 1), + (Coll[Int](1), Coll[Int](2)) -> success(Coll[Int](1, 2), 2), + (Coll[Int](1), Coll[Int](2, 3)) -> success(Coll[Int](1, 2, 3), 3), + (Coll[Int](1, 2), Coll[Int](3)) -> success(Coll[Int](1, 2, 3), 3), + (Coll[Int](1, 2), Coll[Int](3, 4)) -> success(Coll[Int](1, 2, 3, 4), 4), + (Coll[Int](arr1:_*), Coll[Int](arr2:_*)) -> Expected(Success(Coll[Int](arr1 ++ arr2:_*)), 37785, costDetails(300), 1771) ) }, existingFeature( @@ -5686,40 +8757,94 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ) ))) + } else { + assertExceptionThrown( + verifyCases( + Seq( (Coll[Int](), Coll[Int]()) -> Expected(Success(Coll[Int]()), 37765) ), + existingFeature( + { (x: (Coll[Int], Coll[Int])) => x._1.append(x._2) }, + "{ (x: (Coll[Int], Coll[Int])) => x._1.append(x._2) }" + )), + rootCauseLike[NoSuchMethodException]("sigmastate.eval.CostingRules$CollCoster.append(scalan.Base$Ref)") + ) + } } property("Option methods equivalence") { - def success[T](v: T, c: Int) = Expected(Success(v), c) + val costDetails1 = TracedCost(traceBase :+ FixedCostItem(OptionGet)) + val costDetails2 = TracedCost(traceBase :+ FixedCostItem(OptionIsDefined)) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(Constant), + FixedCostItem(OptionGetOrElse) + ) + ) + val costDetails4 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(SOption.FilterMethod, FixedCost(JitCost(20))) + ) + ) + val costDetails5 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(SOption.FilterMethod, FixedCost(JitCost(20))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + FixedCostItem(NamedDesc("EQ_Prim"), FixedCost(JitCost(3))) + ) + ) + val costDetails6 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(SOption.MapMethod, FixedCost(JitCost(20))) + ) + ) + val costDetails7 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(SOption.MapMethod, FixedCost(JitCost(20))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(ArithOp.Plus, SLong) + ) + ) verifyCases( Seq( (None -> Expected(new NoSuchElementException("None.get"))), - (Some(10L) -> success(10L, 36046))), + (Some(10L) -> Expected(Success(10L), 36046, costDetails1, 1765))), existingFeature({ (x: Option[Long]) => x.get }, "{ (x: Option[Long]) => x.get }", FuncValue(Vector((1, SOption(SLong))), OptionGet(ValUse(1, SOption(SLong)))))) verifyCases( Seq( - (None -> success(false, 36151)), - (Some(10L) -> success(true, 36151))), + (None -> Expected(Success(false), 36151, costDetails2, 1764)), + (Some(10L) -> Expected(Success(true), 36151, costDetails2, 1764))), existingFeature({ (x: Option[Long]) => x.isDefined }, "{ (x: Option[Long]) => x.isDefined }", FuncValue(Vector((1, SOption(SLong))), OptionIsDefined(ValUse(1, SOption(SLong)))))) verifyCases( Seq( - (None -> success(1L, 36367)), - (Some(10L) -> success(10L, 36367))), + (None -> Expected(Success(1L), 36367, costDetails3, 1766)), + (Some(10L) -> Expected(Success(10L), 36367, costDetails3, 1766))), existingFeature({ (x: Option[Long]) => x.getOrElse(1L) }, "{ (x: Option[Long]) => x.getOrElse(1L) }", FuncValue(Vector((1, SOption(SLong))), OptionGetOrElse(ValUse(1, SOption(SLong)), LongConstant(1L))))) verifyCases( Seq( - (None -> success(None, 38239)), - (Some(10L) -> success(None, 38239)), - (Some(1L) -> success(Some(1L), 38239))), + (None -> Expected(Success(None), 38239, costDetails4, 1766)), + (Some(10L) -> Expected(Success(None), 38239, costDetails5, 1768)), + (Some(1L) -> Expected(Success(Some(1L)), 38239, costDetails5, 1769))), existingFeature({ (x: Option[Long]) => x.filter({ (v: Long) => v == 1} ) }, "{ (x: Option[Long]) => x.filter({ (v: Long) => v == 1 }) }", FuncValue( @@ -5735,8 +8860,8 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui val n = ExactNumeric.LongIsExactNumeric verifyCases( Seq( - (None -> success(None, 38575)), - (Some(10L) -> success(Some(11L), 38575)), + (None -> Expected(Success(None), 38575, costDetails6, 1766)), + (Some(10L) -> Expected(Success(Some(11L)), 38575, costDetails7, 1770)), (Some(Long.MaxValue) -> Expected(new ArithmeticException("long overflow")))), existingFeature({ (x: Option[Long]) => x.map( (v: Long) => n.plus(v, 1) ) }, "{ (x: Option[Long]) => x.map({ (v: Long) => v + 1 }) }", @@ -5759,15 +8884,49 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("Option filter,map with nested If") { - def success[T](v: T, c: Int) = Expected(Success(v), c) + val costDetails1 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(SOption.FilterMethod, FixedCost(JitCost(20))) + ) + ) + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(SOption.FilterMethod, FixedCost(JitCost(20))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SLong), + FixedCostItem(If), + FixedCostItem(Constant) + ) + ) + val costDetails3 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(SOption.FilterMethod, FixedCost(JitCost(20))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(GT, SLong), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(LE, SLong) + ) + ) val o = ExactOrdering.LongIsExactOrdering verifyCases( Seq( - (None -> success(None, 38736)), - (Some(0L) -> success(None, 38736)), - (Some(10L) -> success(Some(10L), 38736)), - (Some(11L) -> success(None, 38736))), + (None -> Expected(Success(None), 38736, costDetails1, 1766)), + (Some(0L) -> Expected(Success(None), 38736, costDetails2, 1771)), + (Some(10L) -> Expected(Success(Some(10L)), 38736, costDetails3, 1774)), + (Some(11L) -> Expected(Success(None), 38736, costDetails3, 1774))), existingFeature( { (x: Option[Long]) => x.filter({ (v: Long) => if (o.gt(v, 0L)) v <= 10 else false } ) }, "{ (x: Option[Long]) => x.filter({ (v: Long) => if (v > 0) v <= 10 else false }) }", @@ -5790,13 +8949,48 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ))) + val costDetails4 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(SOption.MapMethod, FixedCost(JitCost(20))) + ) + ) + val costDetails5 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(SOption.MapMethod, FixedCost(JitCost(20))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(LT, SLong), + FixedCostItem(If), + FixedCostItem(ValUse) + ) + ) + val costDetails6 = TracedCost( + traceBase ++ Array( + FixedCostItem(MethodCall), + FixedCostItem(FuncValue), + FixedCostItem(SOption.MapMethod, FixedCost(JitCost(20))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(LT, SLong), + FixedCostItem(If), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(ArithOp.Minus, SLong) + ) + ) val n = ExactNumeric.LongIsExactNumeric verifyCases( Seq( - (None -> success(None, 39077)), - (Some(0L) -> success(Some(0L), 39077)), - (Some(10L) -> success(Some(10L), 39077)), - (Some(-1L) -> success(Some(-2L), 39077)), + (None -> Expected(Success(None), 39077, costDetails4, 1766)), + (Some(0L) -> Expected(Success(Some(0L)), 39077, costDetails5, 1772)), + (Some(10L) -> Expected(Success(Some(10L)), 39077, costDetails5, 1772)), + (Some(-1L) -> Expected(Success(Some(-2L)), 39077, costDetails6, 1774)), (Some(Long.MinValue) -> Expected(new ArithmeticException("long overflow")))), existingFeature( { (x: Option[Long]) => x.map( (v: Long) => if (o.lt(v, 0)) n.minus(v, 1) else v ) }, @@ -5823,7 +9017,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) )) } - // TODO HF (3h): implement Option.fold + // TODO v6.0 (3h): implement Option.fold property("Option new methods") { val isEmpty = newFeature({ (x: Option[Long]) => x.isEmpty }, "{ (x: Option[Long]) => x.isEmpty }") @@ -5838,13 +9032,42 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("Option fold workaround method") { + val costDetails1 = TracedCost( + traceBase ++ Array( + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + FixedCostItem(Constant) + ) + ) + val costDetails2 = TracedCost( + traceBase ++ Array( + FixedCostItem(OptionIsDefined), + FixedCostItem(If), + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(ValUse), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(Constant), + TypeBasedCostItem(ArithOp.Plus, SLong) + ) + ) val n = ExactNumeric.LongIsExactNumeric verifyCases( Seq( (None -> Expected( - Failure(new NoSuchElementException("None.get")), 0, - expectedNewValue = Success(5L), expectedNewCost = 39012)), - (Some(0L) -> Expected(Success(1L), 39012)), + value = Failure(new NoSuchElementException("None.get")), + cost = 0, + expectedDetails = CostDetails.ZeroCost, + newCost = 1766, + newVersionedResults = Seq.tabulate(3)(v => v -> (ExpectedResult(Success(5L), Some(1766)) -> Some(costDetails1))) + )), + (Some(0L) -> Expected( + Success(1L), + cost = 39012, + expectedDetails = costDetails2, + expectedNewCost = 1774)), (Some(Long.MaxValue) -> Expected(new ArithmeticException("long overflow"))) ), changedFeature( @@ -5877,22 +9100,54 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ), LongConstant(5L) ) - ))) + ), + allowNewToSucceed = true)) + } + + def formatter(costKind: PerItemCost) = (info: MeasureInfo[Coll[Byte]]) => { + val numChunks = costKind.chunks(info.input.length) + val timeUs = info.measuredTime / 1000 + val timePerBlock = info.measuredTime / info.nIters / numChunks + s"case ${info.iteration}: ${timeUs} usec; numChunks: $numChunks; timePerBlock: $timePerBlock" + } + + property("blake2b256 benchmark: to estimate timeout") { + val cases = (1 to 10).map { i => + val block = Colls.fromArray(Array.fill(ErgoTreeEvaluator.DataBlockSize * i)(0.toByte)) + block + } + benchmarkCases(cases, + existingFeature((x: Coll[Byte]) => SigmaDsl.blake2b256(x), + "{ (x: Coll[Byte]) => blake2b256(x) }", + FuncValue(Vector((1, SByteArray)), CalcBlake2b256(ValUse(1, SByteArray)))), + nIters = 1000, + formatter(CalcBlake2b256.costKind)) } property("blake2b256, sha256 equivalence") { - def success[T](v: T, c: Int) = Expected(Success(v), c) + def costDetailsBlake(i: Int) = TracedCost(traceBase :+ SeqCostItem(CompanionDesc(CalcBlake2b256), PerItemCost(JitCost(20), JitCost(7), 128), i)) + def costDetailsSha(i: Int) = TracedCost(traceBase :+ SeqCostItem(CompanionDesc(CalcSha256), PerItemCost(JitCost(80), JitCost(8), 64), i)) verifyCases( Seq( - Coll[Byte]() -> - success( - Helpers.decodeBytes("0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8"), - 36269), - Helpers.decodeBytes("e0ff0105ffffac31010017ff33") -> - success( - Helpers.decodeBytes("33707eed9aab64874ff2daa6d6a378f61e7da36398fb36c194c7562c9ff846b5"), - 36269) + Coll[Byte]() -> Expected( + Success(Helpers.decodeBytes("0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8")), + 36269, + costDetailsBlake(0), + 1768 + ), + Helpers.decodeBytes("e0ff0105ffffac31010017ff33") -> Expected( + Success(Helpers.decodeBytes("33707eed9aab64874ff2daa6d6a378f61e7da36398fb36c194c7562c9ff846b5")), + 36269, + costDetailsBlake(13), + 1768 + ), + Colls.replicate(1024, 1.toByte) -> Expected( + Success(Helpers.decodeBytes("45d8456fc5d41d1ec1124cb92e41192c1c3ec88f0bf7ae2dc6e9cf75bec22045")), + 36369, + costDetailsBlake(1024), + 1773 + ) ), existingFeature((x: Coll[Byte]) => SigmaDsl.blake2b256(x), "{ (x: Coll[Byte]) => blake2b256(x) }", @@ -5900,36 +9155,51 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( Seq( - Coll[Byte]() -> - success( - Helpers.decodeBytes("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), - 36393), - Helpers.decodeBytes("e0ff0105ffffac31010017ff33") -> - success( - Helpers.decodeBytes("367d0ec2cdc14aac29d5beb60c2bfc86d5a44a246308659af61c1b85fa2ca2cc"), - 36393) + Coll[Byte]() -> Expected( + Success(Helpers.decodeBytes("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")), + 36393, + costDetailsSha(0), + 1774 + ), + Helpers.decodeBytes("e0ff0105ffffac31010017ff33") -> Expected( + Success(Helpers.decodeBytes("367d0ec2cdc14aac29d5beb60c2bfc86d5a44a246308659af61c1b85fa2ca2cc")), + 36393, + costDetailsSha(13), + 1774 + ), + Colls.replicate(1024, 1.toByte) -> Expected( + Success(Helpers.decodeBytes("5a648d8015900d89664e00e125df179636301a2d8fa191c1aa2bd9358ea53a69")), + 36493, + costDetailsSha(1024), + 1786 + ) ), existingFeature((x: Coll[Byte]) => SigmaDsl.sha256(x), "{ (x: Coll[Byte]) => sha256(x) }", FuncValue(Vector((1, SByteArray)), CalcSha256(ValUse(1, SByteArray))))) } - property("print") { - println(ComplexityTableStat.complexityTableString) - } - property("sigmaProp equivalence") { + val costDetails = TracedCost(traceBase :+ FixedCostItem(BoolToSigmaProp)) verifyCases( Seq( - (false, Expected(Success(CSigmaProp(TrivialProp.FalseProp)), 35892)), - (true, Expected(Success(CSigmaProp(TrivialProp.TrueProp)), 35892))), + (false, Expected(Success(CSigmaProp(TrivialProp.FalseProp)), 35892, costDetails, 1765)), + (true, Expected(Success(CSigmaProp(TrivialProp.TrueProp)), 35892, costDetails, 1765))), existingFeature((x: Boolean) => sigmaProp(x), "{ (x: Boolean) => sigmaProp(x) }", FuncValue(Vector((1, SBoolean)), BoolToSigmaProp(ValUse(1, SBoolean))))) } property("atLeast equivalence") { - def success[T](v: T) = Expected(Success(v), 36462) + def costDetails(i: Int) = TracedCost( + traceBase ++ Array( + FixedCostItem(SizeOf), + FixedCostItem(Constant), + TypeBasedCostItem(ArithOp.Minus, SInt), + FixedCostItem(ValUse), + SeqCostItem(CompanionDesc(AtLeast), PerItemCost(JitCost(20), JitCost(3), 5), i) + ) + ) verifyCases( Seq( @@ -5941,7 +9211,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui Helpers.decodeECPoint("02614b14a8c6c6b4b7ce017d72fbca7f9218b72c16bdd88f170ffb300b106b9014"), Helpers.decodeECPoint("034cc5572276adfa3e283a3f1b0f0028afaadeaa362618c5ec43262d8cefe7f004") ) - )) -> success(CSigmaProp(TrivialProp.TrueProp)), + )) -> Expected(Success(CSigmaProp(TrivialProp.TrueProp)), 36462, costDetails(1), 1770), Coll[SigmaProp]( CSigmaProp( ProveDHTuple( @@ -5953,7 +9223,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ), CSigmaProp(ProveDlog(Helpers.decodeECPoint("03f7eacae7476a9ef082513a6a70ed6b208aafad0ade5f614ac6cfa2176edd0d69"))), CSigmaProp(ProveDlog(Helpers.decodeECPoint("023bddd50b917388cd2c4f478f3ea9281bf03a252ee1fefe9c79f800afaa8d86ad"))) - ) -> success( + ) -> Expected(Success( CSigmaProp( CTHRESHOLD( 2, @@ -5969,7 +9239,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ) ) - ), + ), 36462, costDetails(3), 1873), Colls.replicate[SigmaProp](AtLeast.MaxChildrenCount + 1, CSigmaProp(TrivialProp.TrueProp)) -> Expected(new IllegalArgumentException("Expected input elements count should not exceed 255, actual: 256")) ), @@ -5985,10 +9255,23 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("&& sigma equivalence") { + val testTraceBase = traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField) + ) + + val costDetails1 = TracedCost(testTraceBase :+ SeqCostItem(CompanionDesc(SigmaAnd), PerItemCost(JitCost(10), JitCost(2), 1), 2)) + val costDetails2 = TracedCost( + testTraceBase ++ Array( + FixedCostItem(BoolToSigmaProp), + SeqCostItem(CompanionDesc(SigmaAnd), PerItemCost(JitCost(10), JitCost(2), 1), 2) + ) + ) verifyCases( { - def success[T](v: T) = Expected(Success(v), 36428) + def success[T](v: T, newCost: Int) = Expected(Success(v), 36428, costDetails1, newCost) Seq( (CSigmaProp(ProveDlog(Helpers.decodeECPoint("02ea9bf6da7f512386c6ca509d40f8c5e7e0ffb3eea5dc3c398443ea17f4510798"))), CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))) -> @@ -6000,20 +9283,19 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")) ) ) - ) - ), + ), 1802), (CSigmaProp(TrivialProp.TrueProp), CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))) -> - success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))), + success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), 1784), (CSigmaProp(TrivialProp.FalseProp), CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))) -> - success(CSigmaProp(TrivialProp.FalseProp)), + success(CSigmaProp(TrivialProp.FalseProp), 1767), (CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), CSigmaProp(TrivialProp.TrueProp)) -> - success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))), + success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), 1784), (CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), CSigmaProp(TrivialProp.FalseProp)) -> - success(CSigmaProp(TrivialProp.FalseProp)) + success(CSigmaProp(TrivialProp.FalseProp), 1767) ) }, existingFeature( @@ -6031,12 +9313,11 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui verifyCases( { - def success[T](v: T) = Expected(Success(v), 36522) Seq( (CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), true) -> - success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))), + Expected(Success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))), 36522, costDetails2, 1786), (CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), false) -> - success(CSigmaProp(TrivialProp.FalseProp)) + Expected(Success(CSigmaProp(TrivialProp.FalseProp)), 36522, costDetails2, 1769) ) }, existingFeature( @@ -6056,9 +9337,16 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("|| sigma equivalence") { + val testTraceBase = traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField) + ) + + val costDetails1 = TracedCost(testTraceBase :+ SeqCostItem(CompanionDesc(SigmaOr), PerItemCost(JitCost(10), JitCost(2), 1), 2)) verifyCases( { - def success[T](v: T) = Expected(Success(v), 36494) + def success[T](v: T, newCost: Int) = Expected(Success(v), 36494, costDetails1, newCost) Seq( (CSigmaProp(ProveDlog(Helpers.decodeECPoint("02ea9bf6da7f512386c6ca509d40f8c5e7e0ffb3eea5dc3c398443ea17f4510798"))), CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))) -> @@ -6070,20 +9358,20 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")) ) ) - ) - ), + ), + 1802), (CSigmaProp(TrivialProp.FalseProp), CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))) -> - success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))), + success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), 1784), (CSigmaProp(TrivialProp.TrueProp), CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))) -> - success(CSigmaProp(TrivialProp.TrueProp)), + success(CSigmaProp(TrivialProp.TrueProp), 1767), (CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), CSigmaProp(TrivialProp.FalseProp)) -> - success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))), + success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), 1784), (CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), CSigmaProp(TrivialProp.TrueProp)) -> - success(CSigmaProp(TrivialProp.TrueProp)) + success(CSigmaProp(TrivialProp.TrueProp), 1767) ) }, existingFeature( @@ -6099,14 +9387,20 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ) ))) + val costDetails2 = TracedCost( + testTraceBase ++ Array( + FixedCostItem(BoolToSigmaProp), + SeqCostItem(CompanionDesc(SigmaOr), PerItemCost(JitCost(10), JitCost(2), 1), 2) + ) + ) verifyCases( { - def success[T](v: T) = Expected(Success(v), 36588) + def success[T](v: T, newCost: Int) = Expected(Success(v), 36588, costDetails2, newCost) Seq( (CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), false) -> - success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606")))), + success(CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), 1786), (CSigmaProp(ProveDlog(Helpers.decodeECPoint("03a426a66fc1af2792b35d9583904c3fb877b49ae5cea45b7a2aa105ffa4c68606"))), true) -> - success(CSigmaProp(TrivialProp.TrueProp)) + success(CSigmaProp(TrivialProp.TrueProp), 1769) ) }, existingFeature( @@ -6127,18 +9421,51 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui property("SigmaProp.propBytes equivalence") { verifyCases( - Seq( - CSigmaProp(ProveDlog(Helpers.decodeECPoint("039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6f"))) - -> Expected(Success( + { + def newDetails(nItems: Int) = TracedCost(traceBase :+ SeqCostItem(SigmaPropBytes, nItems)) + def pk = ProveDlog(Helpers.decodeECPoint("039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6f")) + def dht = ProveDHTuple( + Helpers.decodeECPoint("03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb"), + Helpers.decodeECPoint("023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d03"), + Helpers.decodeECPoint("03d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72"), + Helpers.decodeECPoint("037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b0441") + ) + def and = CAND(Array(pk, dht)) + def or = COR(Array(pk, dht)) + def threshold = CTHRESHOLD(2, Array(pk, dht, or, and)) + Seq( + CSigmaProp(dht) -> Expected(Success( + Helpers.decodeBytes( + "0008ce03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d0303d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b0441" + ) + ), cost = 35902, newDetails(4), expectedNewCost = 1771), + CSigmaProp(pk) -> Expected(Success( Helpers.decodeBytes("0008cd039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6f")), - cost = 35902) - ), + cost = 35902, newDetails(1), expectedNewCost = 1769), + CSigmaProp(and) -> Expected(Success( + Helpers.decodeBytes( + "00089602cd039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6fce03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d0303d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b0441" + ) + ), cost = 35902, newDetails(6), expectedNewCost = 1772), + CSigmaProp(threshold) -> Expected(Success( + Helpers.decodeBytes( + "0008980204cd039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6fce03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d0303d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b04419702cd039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6fce03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d0303d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b04419602cd039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6fce03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d0303d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b0441" + ) + ), cost = 35902, newDetails(18), expectedNewCost = 1780), + CSigmaProp(COR(Array(pk, dht, and, or, threshold))) -> Expected(Success( + Helpers.decodeBytes( + "00089705cd039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6fce03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d0303d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b04419602cd039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6fce03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d0303d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b04419702cd039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6fce03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d0303d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b0441980204cd039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6fce03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d0303d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b04419702cd039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6fce03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d0303d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b04419602cd039d0b1e46c21540d033143440d2fb7dd5d650cf89981c99ee53c6e0374d2b1b6fce03c046fccb95549910767d0543f5e8ce41d66ae6a8720a46f4049cac3b3d26dafb023479c9c3b86a0d3c8be3db0a2d186788e9af1db76d55f3dad127d15185d83d0303d7898641cb6653585a8e1dabfa7f665e61e0498963e329e6e3744bd764db2d72037ae057d89ec0b46ff8e9ff4c37e85c12acddb611c3f636421bef1542c11b0441" + ) + ), cost = 35902, newDetails(36), expectedNewCost = 1791) + ) + }, existingFeature((x: SigmaProp) => x.propBytes, "{ (x: SigmaProp) => x.propBytes }", - FuncValue(Vector((1, SSigmaProp)), SigmaPropBytes(ValUse(1, SSigmaProp))))) + FuncValue(Vector((1, SSigmaProp)), SigmaPropBytes(ValUse(1, SSigmaProp)))), + preGeneratedSamples = Some(Seq())) } - // TODO HF (3h): implement allZK func https://github.com/ScorexFoundation/sigmastate-interpreter/issues/543 + // TODO v6.0 (3h): implement allZK func https://github.com/ScorexFoundation/sigmastate-interpreter/issues/543 property("allZK equivalence") { lazy val allZK = newFeature((x: Coll[SigmaProp]) => SigmaDsl.allZK(x), "{ (x: Coll[SigmaProp]) => allZK(x) }") @@ -6147,7 +9474,7 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } } - // TODO HF (3h): implement anyZK func https://github.com/ScorexFoundation/sigmastate-interpreter/issues/543 + // TODO v6.0 (3h): implement anyZK func https://github.com/ScorexFoundation/sigmastate-interpreter/issues/543 property("anyZK equivalence") { lazy val anyZK = newFeature((x: Coll[SigmaProp]) => SigmaDsl.anyZK(x), "{ (x: Coll[SigmaProp]) => anyZK(x) }") @@ -6157,21 +9484,20 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("allOf equivalence") { - def success[T](v: T, c: Int) = Expected(Success(v), c) - + def costDetails(i: Int) = TracedCost(traceBase :+ SeqCostItem(CompanionDesc(AND), PerItemCost(JitCost(10), JitCost(5), 32), i)) verifyCases( Seq( - (Coll[Boolean]() -> success(true, 36018)), - (Coll[Boolean](true) -> success(true, 36028)), - (Coll[Boolean](false) -> success(false, 36028)), - (Coll[Boolean](false, false) -> success(false, 36038)), - (Coll[Boolean](false, true) -> success(false, 36038)), - (Coll[Boolean](true, false) -> success(false, 36038)), - (Coll[Boolean](true, true) -> success(true, 36038)), - (Coll[Boolean](true, false, false) -> success(false, 36048)), - (Coll[Boolean](true, false, true) -> success(false, 36048)), - (Coll[Boolean](true, true, false) -> success(false, 36048)), - (Coll[Boolean](true, true, true) -> success(true, 36048)) + (Coll[Boolean]() -> Expected(Success(true), 36018, costDetails(0), 1765)), + (Coll[Boolean](true) -> Expected(Success(true), 36028, costDetails(1), 1765)), + (Coll[Boolean](false) -> Expected(Success(false), 36028, costDetails(1), 1765)), + (Coll[Boolean](false, false) -> Expected(Success(false), 36038, costDetails(1), 1765)), + (Coll[Boolean](false, true) -> Expected(Success(false), 36038, costDetails(1), 1765)), + (Coll[Boolean](true, false) -> Expected(Success(false), 36038, costDetails(2), 1765)), + (Coll[Boolean](true, true) -> Expected(Success(true), 36038, costDetails(2), 1765)), + (Coll[Boolean](true, false, false) -> Expected(Success(false), 36048, costDetails(2), 1765)), + (Coll[Boolean](true, false, true) -> Expected(Success(false), 36048, costDetails(2), 1765)), + (Coll[Boolean](true, true, false) -> Expected(Success(false), 36048, costDetails(3), 1765)), + (Coll[Boolean](true, true, true) -> Expected(Success(true), 36048, costDetails(3), 1765)) ), existingFeature((x: Coll[Boolean]) => SigmaDsl.allOf(x), "{ (x: Coll[Boolean]) => allOf(x) }", @@ -6179,21 +9505,20 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("anyOf equivalence") { - def success[T](v: T, c: Int) = Expected(Success(v), c) - + def costDetails(i: Int) = TracedCost(traceBase :+ SeqCostItem(CompanionDesc(OR), PerItemCost(JitCost(5), JitCost(5), 64), i)) verifyCases( Seq( - (Coll[Boolean]() -> success(false, 36062)), - (Coll[Boolean](true) -> success(true, 36072)), - (Coll[Boolean](false) -> success(false, 36072)), - (Coll[Boolean](false, false) -> success(false, 36082)), - (Coll[Boolean](false, true) -> success(true, 36082)), - (Coll[Boolean](true, false) -> success(true, 36082)), - (Coll[Boolean](true, true) -> success(true, 36082)), - (Coll[Boolean](true, false, false) -> success(true, 36092)), - (Coll[Boolean](true, false, true) -> success(true, 36092)), - (Coll[Boolean](true, true, false) -> success(true, 36092)), - (Coll[Boolean](true, true, true) -> success(true, 36092)) + (Coll[Boolean]() -> Expected(Success(false), 36062, costDetails(0), 1764)), + (Coll[Boolean](true) -> Expected(Success(true), 36072, costDetails(1), 1764)), + (Coll[Boolean](false) -> Expected(Success(false), 36072, costDetails(1), 1764)), + (Coll[Boolean](false, false) -> Expected(Success(false), 36082, costDetails(2), 1764)), + (Coll[Boolean](false, true) -> Expected(Success(true), 36082, costDetails(2), 1764)), + (Coll[Boolean](true, false) -> Expected(Success(true), 36082, costDetails(1), 1764)), + (Coll[Boolean](true, true) -> Expected(Success(true), 36082, costDetails(1), 1764)), + (Coll[Boolean](true, false, false) -> Expected(Success(true), 36092, costDetails(1), 1764)), + (Coll[Boolean](true, false, true) -> Expected(Success(true), 36092, costDetails(1), 1764)), + (Coll[Boolean](true, true, false) -> Expected(Success(true), 36092, costDetails(1), 1764)), + (Coll[Boolean](true, true, true) -> Expected(Success(true), 36092, costDetails(1), 1764)) ), existingFeature((x: Coll[Boolean]) => SigmaDsl.anyOf(x), "{ (x: Coll[Boolean]) => anyOf(x) }", @@ -6201,12 +9526,15 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("proveDlog equivalence") { + val costDetails = TracedCost(traceBase :+ FixedCostItem(CreateProveDlog)) verifyCases( Seq( (Helpers.decodeGroupElement("02288f0e55610c3355c89ed6c5de43cf20da145b8c54f03a29f481e540d94e9a69") -> Expected(Success( CSigmaProp(ProveDlog(Helpers.decodeECPoint("02288f0e55610c3355c89ed6c5de43cf20da145b8c54f03a29f481e540d94e9a69")))), - cost = 45935)) + cost = 45935, + costDetails, + 1782)) ), existingFeature({ (x: GroupElement) => SigmaDsl.proveDlog(x) }, "{ (x: GroupElement) => proveDlog(x) }", @@ -6214,6 +9542,14 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui } property("proveDHTuple equivalence") { + val costDetails = TracedCost( + traceBase ++ Array( + FixedCostItem(ValUse), + FixedCostItem(ValUse), + FixedCostItem(ValUse), + FixedCostItem(CreateProveDHTuple) + ) + ) verifyCases( Seq( (Helpers.decodeGroupElement("039c15221a318d27c186eba84fa8d986c1f63bbd9f8060380c9bfc2ef455d8346a") @@ -6226,7 +9562,9 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui Helpers.decodeECPoint("039c15221a318d27c186eba84fa8d986c1f63bbd9f8060380c9bfc2ef455d8346a") ) )), - cost = 76215 + cost = 76215, + costDetails, + 1836 )) ), existingFeature({ (x: GroupElement) => SigmaDsl.proveDHTuple(x, x, x, x) }, @@ -6254,28 +9592,60 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ErgoTree.ConstantSegregationHeader, Vector(IntConstant(10)), BoolToSigmaProp(EQ(ConstantPlaceholder(0, SInt), IntConstant(20)))) - + def costDetails(i: Int) = TracedCost( + traceBase ++ Array( + FixedCostItem(SelectField), + FixedCostItem(ConcreteCollection), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ConcreteCollection), + FixedCostItem(Constant), + FixedCostItem(BoolToSigmaProp), + SeqCostItem(CompanionDesc(SubstConstants), PerItemCost(JitCost(100), JitCost(100), 1), i) + ) + ) verifyCases( { - def success[T](v: T) = Expected(Success(v), 37694) + def success[T](v: T, cd: CostDetails, cost: Int) = Expected(Success(v), 37694, cd, cost) Seq( (Helpers.decodeBytes(""), 0) -> Expected(new java.nio.BufferUnderflowException()), - // TODO HF (2h): fix for trees without segregation flag - // NOTE: constants count is serialized erroneously in the following 2 cases - (Coll(t1.bytes:_*), 0) -> success(Helpers.decodeBytes("000008d3")), - (Helpers.decodeBytes("000008d3"), 0) -> success(Helpers.decodeBytes("00000008d3")), + // NOTE: in v4.x the constants count is serialized erroneously in the following 2 cases + // in v5.0 (i.e. ET v2) the bug is fixed https://github.com/ScorexFoundation/sigmastate-interpreter/issues/769 + (Coll(t1.bytes:_*), 0) -> Expected( + Success(Helpers.decodeBytes("000008d3")), + cost = 37694, + expectedDetails = CostDetails.ZeroCost, + newCost = 1783, + newVersionedResults = { + val res = (ExpectedResult(Success(Helpers.decodeBytes("0008d3")), Some(1783)) -> Some(costDetails(0))) + Seq(0, 1, 2).map(version => version -> res) + }), + + (Helpers.decodeBytes("000008d3"), 0) -> Expected( + Success(Helpers.decodeBytes("00000008d3")), + cost = 37694, + expectedDetails = CostDetails.ZeroCost, + newCost = 1783, + newVersionedResults = { + // since the tree without constant segregation, substitution has no effect + val res = (ExpectedResult(Success(Helpers.decodeBytes("000008d3")), Some(1783)) -> Some(costDetails(0))) + Seq(0, 1, 2).map(version => version -> res) + }), // tree with segregation flag, empty constants array - (Coll(t2.bytes:_*), 0) -> success(Helpers.decodeBytes("100008d3")), - (Helpers.decodeBytes("100008d3"), 0) -> success(Helpers.decodeBytes("100008d3")), + (Coll(t2.bytes:_*), 0) -> success(Helpers.decodeBytes("100008d3"), costDetails(0), 1783), + (Helpers.decodeBytes("100008d3"), 0) -> success(Helpers.decodeBytes("100008d3"), costDetails(0), 1783), // tree with one segregated constant - (Coll(t3.bytes:_*), 0) -> success(Helpers.decodeBytes("100108d27300")), - (Helpers.decodeBytes("100108d37300"), 0) -> success(Helpers.decodeBytes("100108d27300")), - (Coll(t3.bytes:_*), 1) -> success(Helpers.decodeBytes("100108d37300")), - (Coll(t4.bytes:_*), 0) -> Expected(new AssertionError("assertion failed: expected new constant to have the same SInt$ tpe, got SSigmaProp")) + (Coll(t3.bytes:_*), 0) -> success(Helpers.decodeBytes("100108d27300"), costDetails(1), 1793), + (Helpers.decodeBytes("100108d37300"), 0) -> success(Helpers.decodeBytes("100108d27300"), costDetails(1), 1793), + (Coll(t3.bytes:_*), 1) -> success(Helpers.decodeBytes("100108d37300"), costDetails(1), 1793), + (Coll(t4.bytes:_*), 0) -> Expected(new IllegalArgumentException("requirement failed: expected new constant to have the same SInt$ tpe, got SSigmaProp")) ) }, - existingFeature( + changedFeature( + { (x: (Coll[Byte], Int)) => + SigmaDsl.substConstants(x._1, Coll[Int](x._2), Coll[Any](SigmaDsl.sigmaProp(false))(RType.AnyType)) + }, { (x: (Coll[Byte], Int)) => SigmaDsl.substConstants(x._1, Coll[Int](x._2), Coll[Any](SigmaDsl.sigmaProp(false))(RType.AnyType)) }, @@ -6293,4 +9663,232 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui ))) } + // Original issue: https://github.com/ScorexFoundation/sigmastate-interpreter/issues/604 + property("Random headers access and comparison (originaly from spam tests)") { + val (_, _, _, ctx, _, _) = contextData() + val costDetails = TracedCost( + Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + FixedCostItem(SContext.headersMethod, FixedCost(JitCost(15))), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(PropertyCall), + SeqCostItem(MethodDesc(SCollection.IndicesMethod), PerItemCost(JitCost(20), JitCost(2), 16), 1), + FixedCostItem(Constant), + FixedCostItem(ValUse), + FixedCostItem(SizeOf), + FixedCostItem(Constant), + TypeBasedCostItem(ArithOp.Minus, SInt), + SeqCostItem(CompanionDesc(Slice), PerItemCost(JitCost(10), JitCost(2), 100), 0), + FixedCostItem(FuncValue), + SeqCostItem(CompanionDesc(ForAll), PerItemCost(JitCost(3), JitCost(1), 10), 0) + ) + ) + + if (lowerMethodCallsInTests) { + val ir = IR + val error = new ir.StagingException("Don't know how to evaluate(s2034 -> Placeholder(BaseElemLiftable))", Nil) + verifyCases( + Seq( + ctx -> Expected( + Failure(error), + cost = 37694, + expectedDetails = CostDetails.ZeroCost, + newCost = 1776, + newVersionedResults = (0 to 2).map(i => i -> (ExpectedResult(Success(true), Some(1776)) -> Some(costDetails))) + ) + ), + changedFeature( + { (x: Context) => + throw error + true + }, + { (x: Context) => + val headers = x.headers + val ids = headers.map({ (h: Header) => h.id }) + val parentIds = headers.map({ (h: Header) => h.parentId }) + headers.indices.slice(0, headers.size - 1).forall({ (i: Int) => + val parentId = parentIds(i) + val id = ids(i + 1) + parentId == id + }) + }, + """{ + |(x: Context) => + | val headers = x.headers + | val ids = headers.map({(h: Header) => h.id }) + | val parentIds = headers.map({(h: Header) => h.parentId }) + | headers.indices.slice(0, headers.size - 1).forall({ (i: Int) => + | val parentId = parentIds(i) + | val id = ids(i + 1) + | parentId == id + | }) + |}""".stripMargin, + FuncValue( + Array((1, SContext)), + BlockValue( + Array( + ValDef( + 3, + List(), + MethodCall.typed[Value[SCollection[SHeader.type]]]( + ValUse(1, SContext), + SContext.getMethodByName("headers"), + Vector(), + Map() + ) + ) + ), + ForAll( + Slice( + MethodCall.typed[Value[SCollection[SInt.type]]]( + ValUse(3, SCollectionType(SHeader)), + SCollection.getMethodByName("indices").withConcreteTypes(Map(STypeVar("IV") -> SHeader)), + Vector(), + Map() + ), + IntConstant(0), + ArithOp( + SizeOf(ValUse(3, SCollectionType(SHeader))), + IntConstant(1), + OpCode @@ (-103.toByte) + ) + ), + FuncValue( + Array((4, SInt)), + EQ( + ByIndex( + MapCollection( + ValUse(3, SCollectionType(SHeader)), + FuncValue( + Array((6, SHeader)), + MethodCall.typed[Value[SCollection[SByte.type]]]( + ValUse(6, SHeader), + SHeader.getMethodByName("parentId"), + Vector(), + Map() + ) + ) + ), + ValUse(4, SInt), + None + ), + ByIndex( + MapCollection( + ValUse(3, SCollectionType(SHeader)), + FuncValue( + Array((6, SHeader)), + MethodCall.typed[Value[SCollection[SByte.type]]]( + ValUse(6, SHeader), + SHeader.getMethodByName("id"), + Vector(), + Map() + ) + ) + ), + ArithOp(ValUse(4, SInt), IntConstant(1), OpCode @@ (-102.toByte)), + None + ) + ) + ) + ) + ) + ), + allowDifferentErrors = true, + allowNewToSucceed = true + ), + preGeneratedSamples = Some(mutable.WrappedArray.empty) + ) + } + } + + // related issue https://github.com/ScorexFoundation/sigmastate-interpreter/issues/464 + property("nested loops: map inside fold") { + val keys = Colls.fromArray(Array(Coll[Byte](1, 2, 3, 4, 5))) + val initial = Coll[Byte](0, 0, 0, 0, 0) + val cases = Seq( + (keys, initial) -> Expected(Success(Coll[Byte](1, 2, 3, 4, 5)), cost = 46522, expectedDetails = CostDetails.ZeroCost, 1801) + ) + val scalaFunc = { (x: (Coll[Coll[Byte]], Coll[Byte])) => + x._1.foldLeft(x._2, { (a: (Coll[Byte], Coll[Byte])) => + a._1.zip(a._2).map({ (c: (Byte, Byte)) => (c._1 + c._2).toByte }) + }) + } + val script = + """{ + | (x: (Coll[Coll[Byte]], Coll[Byte])) => + | x._1.fold(x._2, { (a: Coll[Byte], b: Coll[Byte]) => + | a.zip(b).map({ (c: (Byte, Byte)) => (c._1 + c._2).toByte }) + | }) + |}""".stripMargin + if (lowerMethodCallsInTests) { + verifyCases(cases, + existingFeature(scalaFunc, script, + FuncValue( + Array((1, SPair(SByteArray2, SByteArray))), + Fold( + SelectField.typed[Value[SCollection[SCollection[SByte.type]]]]( + ValUse(1, SPair(SByteArray2, SByteArray)), + 1.toByte + ), + SelectField.typed[Value[SCollection[SByte.type]]]( + ValUse(1, SPair(SByteArray2, SByteArray)), + 2.toByte + ), + FuncValue( + Array((3, SPair(SByteArray, SByteArray))), + MapCollection( + MethodCall.typed[Value[SCollection[STuple]]]( + SelectField.typed[Value[SCollection[SByte.type]]]( + ValUse(3, SPair(SByteArray, SByteArray)), + 1.toByte + ), + SCollection.getMethodByName("zip").withConcreteTypes( + Map(STypeVar("IV") -> SByte, STypeVar("OV") -> SByte) + ), + Vector( + SelectField.typed[Value[SCollection[SByte.type]]]( + ValUse(3, SPair(SByteArray, SByteArray)), + 2.toByte + ) + ), + Map() + ), + FuncValue( + Array((5, SPair(SByte, SByte))), + ArithOp( + SelectField.typed[Value[SByte.type]](ValUse(5, SPair(SByte, SByte)), 1.toByte), + SelectField.typed[Value[SByte.type]](ValUse(5, SPair(SByte, SByte)), 2.toByte), + OpCode @@ (-102.toByte) + ) + ) + ) + ) + ) + ) + ), + preGeneratedSamples = Some(Seq.empty) + ) + } else { + assertExceptionThrown( + verifyCases(cases, + existingFeature(scalaFunc, script) + ), + rootCauseLike[CosterException]("Don't know how to evalNode(Lambda(List(),Vector((a,Coll[SByte$]), ") + ) + } + } + + override protected def afterAll(): Unit = { + println(ErgoTreeEvaluator.DefaultProfiler.generateReport) + println("==========================================================") + println(Interpreter.verifySignatureProfiler.generateReport) + } } diff --git a/sigmastate/src/test/scala/special/sigma/SigmaDslStaginTests.scala b/sigmastate/src/test/scala/special/sigma/SigmaDslStaginTests.scala index a996617525..2703930f32 100644 --- a/sigmastate/src/test/scala/special/sigma/SigmaDslStaginTests.scala +++ b/sigmastate/src/test/scala/special/sigma/SigmaDslStaginTests.scala @@ -3,10 +3,10 @@ package special.sigma import special.collection._ import scala.language.reflectiveCalls -import scalan.{BaseLiftableTests, BaseCtxTests} +import scalan.{BaseCtxTests, BaseLiftableTests} +import sigmastate.VersionContext import sigmastate.eval.Extensions._ import sigmastate.eval.{IRContext, ErgoScriptTestkit} -import sigmastate.interpreter.Interpreter class SigmaDslStaginTests extends BaseCtxTests with ErgoScriptTestkit with BaseLiftableTests { class Ctx extends TestContext with IRContext with LiftableTestKit { @@ -30,7 +30,7 @@ class SigmaDslStaginTests extends BaseCtxTests with ErgoScriptTestkit with BaseL type RSigmaProp = cake.SigmaProp val boxA1 = newAliceBox(1, 100) val boxA2 = newAliceBox(2, 200) - val ctx: SContext = newContext(10, boxA1, Interpreter.MaxSupportedScriptVersion) + val ctx: SContext = newContext(10, boxA1, VersionContext.MaxSupportedScriptVersion, VersionContext.MaxSupportedScriptVersion) .withInputs(boxA2) .withVariables(Map(1 -> toAnyValue(30), 2 -> toAnyValue(40))) val p1: SSigmaProp = new special.sigma.MockSigma(true) diff --git a/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala b/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala index 89ec0ab7e0..8c7cd58dfb 100644 --- a/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala +++ b/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala @@ -1,42 +1,47 @@ package special.sigma import java.util - -import org.ergoplatform.{ErgoAddressEncoder, ErgoLikeTransaction, ErgoLikeContext, ErgoLikeInterpreter, Input, ErgoBox, DataInput, ErgoScriptPredef} -import org.scalatest.prop.PropertyChecks -import sigmastate.interpreter.Interpreter.ScriptEnv -import org.scalacheck.{Arbitrary, Gen} -import org.scalatest.{PropSpec, Matchers} - -import scala.util.{Success, Failure, Try} -import sigmastate.Values.{Constant, SValue, ConstantNode, ByteArrayConstant, IntConstant, ErgoTree} -import scalan.RType -import scalan.util.Extensions._ -import org.ergoplatform.dsl.{SigmaContractSyntax, ContractSpec, TestContractSpec} -import org.ergoplatform.validation.{ValidationRules, SigmaValidationSettings} -import sigmastate.{eval, SSigmaProp, SType} -import SType.AnyOps import org.ergoplatform.SigmaConstants.ScriptCostLimit +import org.ergoplatform.dsl.{ContractSpec, SigmaContractSyntax, TestContractSpec} +import org.ergoplatform.validation.{SigmaValidationSettings, ValidationException, ValidationRules} +import org.ergoplatform._ +import org.ergoplatform.validation.ValidationRules.CheckSerializableTypeCode import org.scalacheck.Arbitrary._ import org.scalacheck.Gen.frequency +import org.scalacheck.{Arbitrary, Gen} +import org.scalatest.exceptions.TestFailedException +import org.scalatest.prop.PropertyChecks +import org.scalatest.{Matchers, PropSpec} +import scalan.RType import scalan.RType._ -import sigmastate.basics.DLogProtocol.{ProveDlog, DLogProverInput} -import sigmastate.basics.{SigmaProtocol, SigmaProtocolPrivateInput, SigmaProtocolCommonInput} -import sigmastate.eval.{CompiletimeIRContext, Evaluation, CostingBox, SigmaDsl, IRContext, CostingDataContext} +import scalan.util.BenchmarkUtil +import scalan.util.Extensions._ +import scalan.util.CollectionUtil._ +import scalan.util.StringUtil.StringUtilExtensions +import sigmastate.SType.AnyOps +import sigmastate.Values.{ByteArrayConstant, Constant, ConstantNode, ErgoTree, IntConstant, SValue} +import sigmastate.basics.DLogProtocol.{DLogProverInput, ProveDlog} +import sigmastate.basics.{SigmaProtocol, SigmaProtocolCommonInput, SigmaProtocolPrivateInput} import sigmastate.eval.Extensions._ -import sigmastate.utils.Helpers._ -import sigmastate.lang.Terms.ValueOps -import sigmastate.helpers.{ErgoLikeContextTesting, SigmaPPrint} +import sigmastate.eval.{CompiletimeIRContext, CostingBox, CostingDataContext, Evaluation, IRContext, SigmaDsl} import sigmastate.helpers.TestingHelpers._ -import sigmastate.interpreter.{ProverResult, PrecompiledScriptProcessor, ContextExtension, ProverInterpreter, CacheKey} +import sigmastate.helpers.{ErgoLikeContextTesting, ErgoLikeTestInterpreter, SigmaPPrint} +import sigmastate.interpreter.EvalSettings.{AotEvaluationMode, EvaluationMode, JitEvaluationMode} +import sigmastate.interpreter.Interpreter.{ScriptEnv, VerificationResult} +import sigmastate.interpreter._ +import sigmastate.lang.Terms.{Apply, ValueOps} import sigmastate.serialization.ValueSerializer import sigmastate.serialization.generators.ObjectGenerators -import sigmastate.utxo.{DeserializeContext, DeserializeRegister} +import sigmastate.utils.Helpers._ +import sigmastate.utxo.{DeserializeContext, DeserializeRegister, GetVar, OptionGet} +import sigmastate.{SOption, SSigmaProp, SType, VersionContext, eval} import special.collection.{Coll, CollType} +import spire.syntax.all.cfor import scala.collection.mutable import scala.math.Ordering import scala.reflect.ClassTag +import scala.util.{Failure, Success, Try} class SigmaDslTesting extends PropSpec with PropertyChecks @@ -54,33 +59,6 @@ class SigmaDslTesting extends PropSpec override val okMeasureOperationTime: Boolean = true } - def checkEq[A,B](scalaFunc: A => B)(g: A => (B, Int)): A => Try[(B, Int)] = { x: A => - val b1 = Try(scalaFunc(x)); val b2 = Try(g(x)) - (b1, b2) match { - case (Success(b1), res @ Success((b2, _))) => - assert(b1 == b2) - res - case (Failure(t1), res @ Failure(t2)) => - val c1 = rootCause(t1).getClass - val c2 = rootCause(t2).getClass - c1 shouldBe c2 - res - case _ => - val cause = if (b1.isFailure) - rootCause(b1.asInstanceOf[Failure[_]].exception) - else - rootCause(b2.asInstanceOf[Failure[_]].exception) - - sys.error( - s"""Should succeed with the same value or fail with the same exception, but was: - |First result: $b1 - |Second result: $b2 - |Root cause: $cause - |""".stripMargin) - } - - } - def checkEq2[A,B,R](f: (A, B) => R)(g: (A, B) => R): (A,B) => Unit = { (x: A, y: B) => val r1 = f(x, y); val r2 = g(x, y) assert(r1.getClass == r2.getClass) @@ -100,8 +78,7 @@ class SigmaDslTesting extends PropSpec indices <- Gen.containerOfN[Array, Int](nIndexes, Gen.choose(0, arrLength - 1)) } yield indices - class FeatureProvingInterpreter extends ErgoLikeInterpreter()(new CompiletimeIRContext) with ProverInterpreter { - override type CTX = ErgoLikeContext + class FeatureProvingInterpreter extends ErgoLikeTestInterpreter()(new TestingIRContext) with ProverInterpreter { def decodeSecretInput(decimalStr: String): DLogProverInput = DLogProverInput(BigInt(decimalStr).bigInteger) @@ -123,10 +100,28 @@ class SigmaDslTesting extends PropSpec val LogScriptDefault: Boolean = false + val isNewVersion = new scala.util.DynamicVariable(false) + val predefScripts = Seq[String]() - /** Descriptor of the language feature. */ - trait Feature[A, B] { + /** Descriptor of the language feature. + * Each language feature is described by so called feature-function which exercises + * some specific operation under test. + * + * For example, the following feature-function exercises binary `+` operation for `Int` type. + * `{ (in: (Int, Int)) => in._1 + in._2 }` + * + * Feature function script is compiled into expression and then executed on a series of + * test cases. The outputs if each test case is compared with expected values. + * + * Test cases are specified for all supported versions. + * @see ExistingFeature, ChangedFeature + */ + trait Feature[A, B] { feature => + /** Type descriptor for type A. */ + def tA: RType[A] + /** Type descriptor for type B. */ + def tB: RType[B] /** Script containing this feature. */ def script: String @@ -140,12 +135,16 @@ class SigmaDslTesting extends PropSpec /** Expression which represents the test case code. */ def expectedExpr: Option[SValue] - /** Function that executes the feature using v3 interpreter implementation. */ + /** Function that executes the feature using v4.x interpreter implementation. */ def oldImpl: () => CompiledFunc[A, B] - /** Function that executes the feature using v4 interpreter implementation. */ + /** Function that executes the feature using v5.x interpreter implementation. */ def newImpl: () => CompiledFunc[A, B] + def evalSettings: EvalSettings + + def allowDifferentErrors: Boolean = false + def printExpectedExpr: Boolean def logScript: Boolean @@ -165,7 +164,7 @@ class SigmaDslTesting extends PropSpec def checkExpectedExprIn(cf: CompiledFunc[_,_]): Boolean = { expectedExpr match { case Some(e) => - if (cf.expr != e) { + if (cf.expr != null && cf.expr != e) { printSuggestion(cf) cf.expr shouldBe e } @@ -201,7 +200,7 @@ class SigmaDslTesting extends PropSpec * @param input data which is used to execute feature * @param logInputOutput if true, then pretty-print input and output values * @return result of feature execution */ - def checkEquality(input: A, logInputOutput: Boolean = false): Try[(B, Int)] + def checkEquality(input: A, logInputOutput: Boolean = false): Try[(B, CostDetails)] /** Depending on the featureType compares the old and new implementations against * semantic function (scalaFunc) on the given input, also checking the given expected result. @@ -225,6 +224,39 @@ class SigmaDslTesting extends PropSpec printTestCases: Boolean = PrintTestCasesDefault, failOnTestVectors: Boolean = FailOnTestVectorsDefault): Unit + def checkEq[A,B](scalaFunc: A => B)(g: A => (B, CostDetails)): A => Try[(B, CostDetails)] = { x: A => + val b1 = Try(scalaFunc(x)) + val b2 = Try(g(x)) + (b1, b2) match { + case (Success(b1), res @ Success((b2, _))) => + assert(b1 == b2) + res + case (Failure(t1), res @ Failure(t2)) => + val c1 = rootCause(t1).getClass + val c2 = rootCause(t2).getClass + if (c1 != c2 && !allowDifferentErrors) { + assert(c1 == c2, + s"""Different errors: + |First result: $t1 + |Second result: $t2 + |""".stripMargin) + } + res + case _ => + val cause = if (b1.isFailure) + rootCause(b1.asInstanceOf[Failure[_]].exception) + else + rootCause(b2.asInstanceOf[Failure[_]].exception) + + fail( + s"""Should succeed with the same value or fail with the same exception, but was: + |First result: $b1 + |Second result: $b2 + |Root cause: $cause + |""".stripMargin) + } + } + /** Creates a new ErgoLikeContext using given [[CostingDataContext]] as template. * Copies most of the data from ctx and the missing data is taken from the args. * This is a helper method to be used in tests only. @@ -259,18 +291,13 @@ class SigmaDslTesting extends PropSpec } /** Executes the default feature verification wrapper script for the specific ErgoTree - * version. - * @param input the given input + * version using both v4.x and v5.x interpreters. + * @param input the given test case input data * @param expected the given expected results (values and costs) */ def checkVerify(input: A, expected: Expected[B]): Unit = { - val tpeA = Evaluation.rtypeToSType(oldF.tA) - val tpeB = Evaluation.rtypeToSType(oldF.tB) - - val prover = new FeatureProvingInterpreter() - val verifier = new ErgoLikeInterpreter()(createIR()) { - type CTX = ErgoLikeContext - } + val tpeA = Evaluation.rtypeToSType(tA) + val tpeB = Evaluation.rtypeToSType(tB) // Create synthetic ErgoTree which uses all main capabilities of evaluation machinery. // 1) first-class functions (lambdas); 2) Context variables; 3) Registers; 4) Equality @@ -278,7 +305,7 @@ class SigmaDslTesting extends PropSpec // 7) Deserialization from SELF and Context // Every language Feature is tested as part of this wrapper script. // Inclusion of all the features influences the expected cost estimation values - val compiledTree = { + def compiledTree(prover: FeatureProvingInterpreter) = { val code = s"""{ | val func = ${oldF.script} @@ -307,99 +334,228 @@ class SigmaDslTesting extends PropSpec ErgoTree.withSegregation(header, sigmastate.SigmaOr(prop, multisig)) } - val pkBobBytes = ValueSerializer.serialize(prover.pubKeys(1).toSigmaProp) - val pkCarolBytes = ValueSerializer.serialize(prover.pubKeys(2).toSigmaProp) - val newRegisters = Map( - ErgoBox.R4 -> Constant[SType](expected.value.get.asInstanceOf[SType#WrappedType], tpeB), - ErgoBox.R5 -> ByteArrayConstant(pkBobBytes) - ) + def ergoCtx(prover: FeatureProvingInterpreter, compiledTree: ErgoTree, expectedValue: B) = { + val pkBobBytes = ValueSerializer.serialize(prover.pubKeys(1).toSigmaProp) + val pkCarolBytes = ValueSerializer.serialize(prover.pubKeys(2).toSigmaProp) + val newRegisters = Map( + ErgoBox.R4 -> Constant[SType](expectedValue.asInstanceOf[SType#WrappedType], tpeB), + ErgoBox.R5 -> ByteArrayConstant(pkBobBytes) + ) + + val ctx = input match { + case ctx: CostingDataContext => + // the context is passed as function argument (see func in the script) + // Since Context is singleton, we should use this instance as the basis + // for execution of verify instead of a new dummy context. + val self = ctx.selfBox.asInstanceOf[CostingBox] + val newSelf = self.copy( + ebox = updatedRegisters(self.ebox, newRegisters) + ) + + // We add ctx as it's own variable with id = 1 + val ctxVar = eval.Extensions.toAnyValue[special.sigma.Context](ctx)(special.sigma.ContextRType) + val carolVar = eval.Extensions.toAnyValue[Coll[Byte]](pkCarolBytes.toColl)(RType[Coll[Byte]]) + val newCtx = ctx + .withUpdatedVars(1 -> ctxVar, 2 -> carolVar) + .copy( + selfBox = newSelf, + inputs = { + val selfIndex = ctx.inputs.indexWhere(b => b.id == ctx.selfBox.id, 0) + ctx.inputs.updated(selfIndex, newSelf) + }) + + createErgoLikeContext( + newCtx, + ValidationRules.currentSettings, + ScriptCostLimit.value, + initCost = initialCostInTests.value + ) + + case _ => + val box = createBox(0, compiledTree, additionalRegisters = newRegisters) + + // make sure we are doing tests with the box with is actually serializable + try roundTripTest(box)(ErgoBox.sigmaSerializer) + catch { + case ValidationException(_, r: CheckSerializableTypeCode.type, Seq(SOption.OptionTypeCode), _) => + // ignore the problem with Option serialization, but test all the other cases + } + + ErgoLikeContextTesting.dummy(box, activatedVersionInTests) + .withBindings( + 1.toByte -> Constant[SType](input.asInstanceOf[SType#WrappedType], tpeA), + 2.toByte -> ByteArrayConstant(pkCarolBytes)) + .withInitCost(initialCostInTests.value) + .asInstanceOf[ErgoLikeContext] + } + ctx + } - val ergoCtx = input match { - case ctx: CostingDataContext => - // the context is passed as function argument (see func in the script) - // Since Context is singleton, we should use this instance as the basis - // for execution of verify instead of a new dummy context. - val self = ctx.selfBox.asInstanceOf[CostingBox] - val newSelf = self.copy( - ebox = updatedRegisters(self.ebox, newRegisters) - ) - - // We add ctx as it's own variable with id = 1 - val ctxVar = eval.Extensions.toAnyValue[special.sigma.Context](ctx)(special.sigma.ContextRType) - val carolVar = eval.Extensions.toAnyValue[Coll[Byte]](pkCarolBytes.toColl)(RType[Coll[Byte]]) - val newCtx = ctx - .withUpdatedVars(1 -> ctxVar, 2 -> carolVar) - .copy( - selfBox = newSelf, - inputs = { - val selfIndex = ctx.inputs.indexWhere(b => b.id == ctx.selfBox.id, 0) - ctx.inputs.updated(selfIndex, newSelf) - }) - - createErgoLikeContext( - newCtx, - ValidationRules.currentSettings, - ScriptCostLimit.value, - initCost = initialCostInTests.value - ) + val (evalMode, expectedResult, expectedCost) = if (activatedVersionInTests < VersionContext.JitActivationVersion) + (AotEvaluationMode, expected.oldResult, expected.verificationCostOpt) + else { + val res = expected.newResults(ergoTreeVersionInTests) + (JitEvaluationMode, res._1, res._1.verificationCost) + } - case _ => - ErgoLikeContextTesting.dummy( - createBox(0, compiledTree, additionalRegisters = newRegisters), - activatedVersionInTests) - .withBindings( - 1.toByte -> Constant[SType](input.asInstanceOf[SType#WrappedType], tpeA), - 2.toByte -> ByteArrayConstant(pkCarolBytes)) - .withInitCost(initialCostInTests.value) - .asInstanceOf[ErgoLikeContext] + if (expectedResult.value.isSuccess) { + // we can run both proving and verification + val prover = new FeatureProvingInterpreter() + val tree = compiledTree(prover) + val ctx = ergoCtx(prover, tree, expectedResult.value.get) + val pr = prover.prove(tree, ctx, fakeMessage).getOrThrow + val verificationCtx = ctx.withExtension(pr.extension) + val verifier = new ErgoLikeTestInterpreter()(createIR()) + val res = verifier.verify(tree, verificationCtx, pr, fakeMessage) + checkExpectedResult(evalMode, res, expectedCost) + + if (expectedCost.isEmpty) { + val (_, cost) = res.getOrThrow + // new verification cost expectation is missing, print out actual cost results + if (feature.evalSettings.printTestVectors) { + printCostTestVector("Missing New Cost", input, cost.toInt) + } + } } + } - val pr = prover.prove(compiledTree, ergoCtx, fakeMessage).getOrThrow + /** Prints the actual cost test vector (when it is not defined). */ + private def printCostTestVector(title: String, input: Any, actualCost: Int) = { + println( + s"""-- $title ---------------------- + |ActivatedVersion: $activatedVersionInTests + |ErgoTreeVersion: $ergoTreeVersionInTests + |Input: $input + |Script: $script + |Actual New Verification Cost: $actualCost + |""".stripMargin) + } - val verificationCtx = ergoCtx.withExtension(pr.extension) + private def checkEqualResults(res1: Try[VerificationResult], res2: Try[VerificationResult]): Unit = { + (res1, res2) match { + case (Success((v1, c1)), Success((v2, c2))) => + v1 shouldBe v2 + case (Failure(t1), Failure(t2)) => + rootCause(t1) shouldBe rootCause(t2) + case _ => + res1 shouldBe res2 + } + } - val vres = verifier.verify(compiledTree, verificationCtx, pr, fakeMessage) - vres match { + private def checkExpectedResult( + evalMode: EvaluationMode, + res: Try[VerificationResult], expectedCost: Option[Int]): Unit = { + res match { case Success((ok, cost)) => ok shouldBe true val verificationCost = cost.toIntExact - // NOTE: you can uncomment this line and comment the assertion in order to - // simplify adding new test vectors for cost estimation - // if (expectedCost != verificationCost) { - // println(s"Script: $script") - // println(s"Cost: $verificationCost\n") - // } - assertResult(expected.cost, - s"Actual verify() cost $cost != expected ${expected.cost}")(verificationCost) + if (expectedCost.isDefined) { + assertResult(expectedCost.get, + s"Evaluation Mode: ${evalMode.name}; Actual verify() cost $cost != expected ${expectedCost.get}")(verificationCost) + } case Failure(t) => throw t } } + + } + + /** A number of times the newF function in each test feature is repeated. + * In combination with [[sigmastate.eval.Profiler]] it allows to collect more accurate + * timings for all operations. + * @see SigmaDslSpecification */ + def nBenchmarkIters: Int = 0 + + def warmUpBeforeAllTest(nTotalIters: Int)(block: => Unit) = { + // each test case is executed nBenchmarkIters times in `check` method + // so we account for that here + val nIters = nTotalIters / (nBenchmarkIters + 1) + repeatAndReturnLast(nIters)(block) + System.gc() + Thread.sleep(1000) // let GC to its job before running the tests } - case class ExistingFeature[A: RType, B: RType]( + /** Derived class ExistingFeature is used to describe features which don't change from + * v4.x to v5.0. + * + * @tparam A type of an input test data + * @tparam B type of an output of the feature function + * @param script script of the feature function (see Feature trait) + * @param scalaFunc feature function written in Scala and used to simulate the behavior + * of the script + * @param expectedExpr expected ErgoTree expression to be produced by ErgoScript + * compiler for the given feature-function + * @param printExpectedExpr if true, print test vectors for expectedExpr + * @param logScript if true, log scripts to console + */ + case class ExistingFeature[A, B]( script: String, scalaFunc: A => B, expectedExpr: Option[SValue], printExpectedExpr: Boolean = true, logScript: Boolean = LogScriptDefault - )(implicit IR: IRContext) extends Feature[A, B] { + )(implicit IR: IRContext, val tA: RType[A], val tB: RType[B], + override val evalSettings: EvalSettings) extends Feature[A, B] { + + implicit val cs = compilerSettingsInTests val oldImpl = () => func[A, B](script) - val newImpl = oldImpl // TODO HF (16h): use actual new implementation here + val newImpl = () => funcJit[A, B](script) - def checkEquality(input: A, logInputOutput: Boolean = false): Try[(B, Int)] = { + def checkEquality(input: A, logInputOutput: Boolean = false): Try[(B, CostDetails)] = { // check the old implementation against Scala semantic function val oldRes = checkEq(scalaFunc)(oldF)(input) - if (!(newImpl eq oldImpl)) { - // check the new implementation against Scala semantic function - val newRes = checkEq(scalaFunc)(newF)(input) - newRes shouldBe oldRes + val newRes = VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + checkEq(scalaFunc)({ x => + var y: (B, CostDetails) = null + val N = nBenchmarkIters + 1 + cfor(0)(_ < N, _ + 1) { _ => + y = newF(x) + } + y + })(input) } - if (logInputOutput) - println(s"(${SigmaPPrint(input, height = 550, width = 150)}, ${SigmaPPrint(oldRes, height = 550, width = 150)}),${if (logScript) " // " + script else ""}") - oldRes + + (oldRes, newRes) match { + case (Success((oldRes, oldDetails)), + Success((newRes, newDetails))) => + newRes shouldBe oldRes + val oldCost = oldDetails.cost + val newCost = newDetails.cost + if (newDetails.cost != oldDetails.cost) { + assertResult(true, + s""" + |New cost should not exceed old cost: (new: $newCost, old:$oldCost) + |ExistingFeature.checkEquality( + | script = "$script", + | compiledTree = "${SigmaPPrint(newF.compiledTree, height = 550, width = 150)}" + |) + |""".stripMargin + )(oldCost >= newCost / 20) + + if (evalSettings.isLogEnabled) { + println( + s"""Different Costs (new: $newCost, old:$oldCost) + | input = ${SigmaPPrint(input, height = 550, width = 150)} + | script = "$script" + | + |""".stripMargin) + } + } + case _ => + checkResult(rootCause(newRes), rootCause(oldRes), failOnTestVectors = true, + "ExistingFeature.checkEquality: Comparing newRes with oldRes when failure") + } + + if (logInputOutput) { + val scriptComment = if (logScript) " // " + script else "" + val inputStr = SigmaPPrint(input, height = 550, width = 150) + val oldResStr = SigmaPPrint(oldRes, height = 550, width = 150) + println(s"($inputStr, $oldResStr),$scriptComment") + } + + newRes } /** Depending on the featureType compares the old and new implementations against @@ -410,11 +566,19 @@ class SigmaDslTesting extends PropSpec val (oldRes, _) = checkEq(scalaFunc)(oldF)(input).get oldRes shouldBe expected.value.get - if (!(newImpl eq oldImpl)) { - // check the new implementation with Scala semantic - val (newRes, _) = checkEq(scalaFunc)(newF)(input).get - newRes shouldBe expected.value.get + // check the new implementation with Scala semantic + val (newRes, newDetails) = VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + checkEq(scalaFunc)(newF)(input).get + } + newRes shouldBe expected.value.get + expected.newResults(ergoTreeVersionInTests)._2.foreach { expDetails => + if (newDetails.trace != expDetails.trace) { + printCostDetails(script, newDetails) + newDetails.trace shouldBe expDetails.trace + } } + + checkVerify(input, expected) } override def testCase(input: A, @@ -422,67 +586,200 @@ class SigmaDslTesting extends PropSpec printTestCases: Boolean, failOnTestVectors: Boolean): Unit = { val res = checkEquality(input, printTestCases).map(_._1) - checkResult(res, expectedResult, failOnTestVectors) + checkResult(res, expectedResult, failOnTestVectors, "ExistingFeature#testCase: ") } override def verifyCase(input: A, expected: Expected[B], printTestCases: Boolean, failOnTestVectors: Boolean): Unit = { - val funcRes = checkEquality(input, printTestCases) - - checkResult(funcRes.map(_._1), expected.value, failOnTestVectors) - - expected.value match { - case Success(y) => - checkVerify(input, expected) - case _ => + val funcRes = checkEquality(input, printTestCases) // NOTE: funcRes comes from newImpl + + checkResult(funcRes.map(_._1), expected.value, failOnTestVectors, + "ExistingFeature#verifyCase: ") + + val newRes = expected.newResults(ergoTreeVersionInTests) + val expectedTrace = newRes._2.fold(Seq.empty[CostItem])(_.trace) + if (expectedTrace.isEmpty) { + // new cost expectation is missing, print out actual cost results + if (evalSettings.printTestVectors) { + funcRes.foreach { case (_, newDetails) => + printCostDetails(script, newDetails) + } + } + } + else { + // new cost expectation is specified, compare it with the actual result + funcRes.foreach { case (_, newDetails) => + if (newDetails.trace != expectedTrace) { + printCostDetails(script, newDetails) + newDetails.trace shouldBe expectedTrace + } + } } + + checkVerify(input, expected) } } - case class ChangedFeature[A: RType, B: RType]( + /** Descriptor of a language feature which is changed in v5.0. + * + * @tparam A type of an input test data + * @tparam B type of an output of the feature function + * @param script script of the feature function (see Feature trait) + * @param scalaFunc feature function written in Scala and used to simulate the behavior + * of the script + * @param scalaFuncNew function in Scala to simulate the behavior of the script in v5.0 + * @param expectedExpr expected ErgoTree expression to be produced by ErgoScript + * compiler for the given feature-function + * @param printExpectedExpr if true, print test vectors for expectedExpr + * @param logScript if true, log scripts to console + * @param allowNewToSucceed if true, we allow some scripts which fail in v4.x to pass in v5.0. + * Before activation, all ErgoTrees with version < JitActivationVersion are + * processed by v4.x interpreter, so this difference is not a problem. + * After activation, all ErgoTrees with version < JitActivationVersion are + * processed by v5.0 interpreter (which is 90% of nodes) and the rest are + * accepting without check. + * This approach allows to fix bugs in the implementation of + * some of v4.x operations. + * @param allowDifferentErrors if true, allow v4.x and v5.0 to fail with different error + */ + case class ChangedFeature[A, B]( script: String, scalaFunc: A => B, override val scalaFuncNew: A => B, expectedExpr: Option[SValue], printExpectedExpr: Boolean = true, - logScript: Boolean = LogScriptDefault - )(implicit IR: IRContext) extends Feature[A, B] { + logScript: Boolean = LogScriptDefault, + allowNewToSucceed: Boolean = false, + override val allowDifferentErrors: Boolean = false + )(implicit IR: IRContext, override val evalSettings: EvalSettings, val tA: RType[A], val tB: RType[B]) + extends Feature[A, B] { + + implicit val cs = compilerSettingsInTests + + /** Apply given function to the context variable 1 */ + private def getApplyExpr(funcValue: SValue) = { + val sType = Evaluation.rtypeToSType(RType[A]) + Apply(funcValue, IndexedSeq(OptionGet(GetVar[SType](1.toByte, sType)))) + } - val oldImpl = () => func[A, B](script) - val newImpl = oldImpl // TODO HF (16h): use actual new implementation here + val oldImpl = () => { + if (script.isNullOrEmpty) { + val funcValue = expectedExpr.getOrElse( + sys.error("When `script` is not defined, the expectedExpr is used for CompiledFunc")) + val expr = getApplyExpr(funcValue) + funcFromExpr[A, B]("not defined", expr) + } else { + func[A, B](script) + } + } + + val newImpl = () => { + if (script.isNullOrEmpty) { + val funcValue = expectedExpr.getOrElse( + sys.error("When `script` is not defined, the expectedExpr is used for CompiledFunc")) + val expr = getApplyExpr(funcValue) + funcJitFromExpr[A, B]("not defined", expr) + } else { + funcJit[A, B](script) + } + } - def checkEquality(input: A, logInputOutput: Boolean = false): Try[(B, Int)] = { + override def checkEquality(input: A, logInputOutput: Boolean = false): Try[(B, CostDetails)] = { // check the old implementation against Scala semantic function - val oldRes = checkEq(scalaFunc)(oldF)(input) + var oldRes: Try[(B, CostDetails)] = null + if (ergoTreeVersionInTests < VersionContext.JitActivationVersion) + oldRes = VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + try checkEq(scalaFunc)(oldF)(input) + catch { + case e: TestFailedException => throw e + case t: Throwable => + Failure(t) + } + } - if (!(newImpl eq oldImpl)) { + val newRes = { // check the new implementation against Scala semantic function - val newRes = checkEq(scalaFuncNew)(newF)(input) + val newRes = VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + checkEq(scalaFuncNew)(newF)(input) + } + if (ergoTreeVersionInTests < VersionContext.JitActivationVersion) { + (oldRes, newRes) match { + case (_: Failure[_], _: Success[_]) if allowNewToSucceed => + // NOTE, we are in ChangedFeature (compare with ExistingFeature) + // this is the case when old v4.x version fails with exception (e.g. due to AOT costing), + // but the new v5.0 produces result. (See property("Option fold workaround method")) + // Thus, we allow some scripts which fail in v4.x to pass in v5.0. + // see ScalaDoc for allowNewToSucceed + case (_: Failure[_], _: Failure[_]) if allowDifferentErrors => + // this is the case when old v4.x version fails with one exception (e.g. due to AOT costing), + // but the new v5.0 fails with another exception. + case _ => + val inputStr = SigmaPPrint(input, height = 550, width = 150) + checkResult(oldRes.map(_._1), newRes.map(_._1), true, + s"ChangedFeature.checkEquality($inputStr): use allowNewToSucceed=true to allow v5.0 to succeed when v4.x fails") + } + } + newRes } - if (logInputOutput) { + if (logInputOutput && oldRes != null) { val inputStr = SigmaPPrint(input, height = 550, width = 150) val oldResStr = SigmaPPrint(oldRes, height = 550, width = 150) val scriptComment = if (logScript) " // " + script else "" println(s"($inputStr, $oldResStr),$scriptComment") } - oldRes + newRes } - /** Depending on the featureType compares the old and new implementations against - * semantic function (scalaFunc) on the given input, also checking the given expected result. + /** compares the old and new implementations against semantic functions (scalaFunc and + * scalaFuncNew) on the given input, also checking the given expected result. */ override def checkExpected(input: A, expected: Expected[B]): Unit = { - // check the old implementation with Scala semantic - val (oldRes, _) = checkEq(scalaFunc)(oldF)(input).get - oldRes shouldBe expected.value.get + // check the new implementation with Scala semantic function + val newRes = VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + checkEq(scalaFuncNew)(newF)(input) + } - if (!(newImpl eq oldImpl)) { - // check the new implementation with Scala semantic - val (newRes, _) = checkEq(scalaFuncNew)(newF)(input).get - newRes shouldBe expected.newValue.get + if (!VersionContext.current.isJitActivated) { + // check the old implementation with Scala semantic + val expectedOldRes = expected.value + val oldRes = checkEq(scalaFunc)(oldF)(input) + checkResult(oldRes.map(_._1), expectedOldRes, failOnTestVectors = true, + "Comparing oldRes with expected: !isJitActivated: ") + + val newValue = newRes.map(_._1) + (expectedOldRes, newValue) match { + case (_: Failure[_], _: Success[_]) if allowNewToSucceed => + // NOTE, we are in ChangedFeature (compare with ExistingFeature) + // this is the case when old v4.x version fails with exception (e.g. due to AOT costing), + // but the new v5.0 produces result. (See property("Option fold workaround method")) + // Thus, we allow some scripts which fail in v4.x to pass in v5.0. + // see ScalaDoc for allowNewToSucceed + case (_: Failure[_], _: Failure[_]) if allowDifferentErrors => + // this is the case when old v4.x version fails with one exception (e.g. due to AOT costing), + // but the new v5.0 fails with another exception. + case _ => + checkResult(newValue, expectedOldRes, failOnTestVectors = true, + "Comparing newRes with expected old result: !isJitActivated: ") + } + } else { + val (newExpectedRes, newExpectedDetailsOpt) = expected.newResults(ergoTreeVersionInTests) + newRes match { + case Success((newValue, newDetails)) => + newValue shouldBe newExpectedRes.value.get + newExpectedDetailsOpt.foreach { expDetails => + if (newDetails.trace != expDetails.trace) { + printCostDetails(script, newDetails) + newDetails.trace shouldBe expDetails.trace + } + } + case _ => + checkResult(newRes.map(_._1), newExpectedRes.value, failOnTestVectors = true, + "Comparing newRes with new expected: isJitActivated: ") + } } + checkVerify(input, expected) } override def testCase(input: A, @@ -490,44 +787,49 @@ class SigmaDslTesting extends PropSpec printTestCases: Boolean, failOnTestVectors: Boolean): Unit = { val res = checkEquality(input, printTestCases).map(_._1) - checkResult(res, expectedResult, failOnTestVectors) + checkResult(res, expectedResult, failOnTestVectors, + "ChangedFeature#testCase: ") } override def verifyCase(input: A, expected: Expected[B], printTestCases: Boolean, failOnTestVectors: Boolean): Unit = { - val funcRes = checkEquality(input, printTestCases) - - checkResult(funcRes.map(_._1), expected.value, failOnTestVectors) - - expected.value match { - case Success(y) => - checkVerify(input, expected) - case _ => - } + checkExpected(input, expected) + checkVerify(input, expected) } } - case class NewFeature[A: RType, B: RType]( + /** Derived class NewFeature is used to describe features which should be introduced + * from specific version. + * This in not yet implemented and will be finished in v6.0. + * In v5.0 is only checks that some features are NOT implemented, i.e. work for + * negative tests. + */ + case class NewFeature[A, B]( script: String, override val scalaFuncNew: A => B, expectedExpr: Option[SValue], printExpectedExpr: Boolean = true, logScript: Boolean = LogScriptDefault - )(implicit IR: IRContext) extends Feature[A, B] { + )(implicit IR: IRContext, override val evalSettings: EvalSettings, val tA: RType[A], val tB: RType[B]) + extends Feature[A, B] { override def scalaFunc: A => B = { x => sys.error(s"Semantic Scala function is not defined for old implementation: $this") } + implicit val cs = compilerSettingsInTests val oldImpl = () => func[A, B](script) - val newImpl = oldImpl // TODO HF (16h): use actual new implementation here + val newImpl = oldImpl // funcJit[A, B](script) // TODO v6.0 (16h): use actual new implementation here - override def checkEquality(input: A, logInputOutput: Boolean = false): Try[(B, Int)] = { + /** In v5.x this method just checks the old implementations fails on the new feature. */ + override def checkEquality(input: A, logInputOutput: Boolean = false): Try[(B, CostDetails)] = { val oldRes = Try(oldF(input)) oldRes.isFailure shouldBe true if (!(newImpl eq oldImpl)) { - val newRes = checkEq(scalaFuncNew)(newF)(input) + val newRes = VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + checkEq(scalaFuncNew)(newF)(input) + } } oldRes } @@ -535,9 +837,13 @@ class SigmaDslTesting extends PropSpec override def checkExpected(input: A, expected: Expected[B]): Unit = { Try(oldF(input)).isFailure shouldBe true if (!(newImpl eq oldImpl)) { - val (newRes, _) = checkEq(scalaFuncNew)(newF)(input).get - newRes shouldBe expected.newValue.get + val (newRes, _) = VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + checkEq(scalaFuncNew)(newF)(input).get + } + val newExpectedRes = expected.newResults(ergoTreeVersionInTests) + newRes shouldBe newExpectedRes._1.value.get } + checkVerify(input, expected) } override def testCase(input: A, @@ -559,25 +865,110 @@ class SigmaDslTesting extends PropSpec } } - /** Represents expected result of successful feature test exectuion. - * @param value value returned by feature function (and the corresponding Scala function) - * @param cost expected cost value of the verification execution + /** Represents expected result, verification cost and costing trace for a single + * interpreter run. + * @param value expected results returned by feature function (and the corresponding Scala function) + * @param verificationCost expected cost value of the verification execution + */ + case class ExpectedResult[+A](value: Try[A], verificationCost: Option[Int]) + + /** Represents expected results (aka test vectors) for a single feature test case. + * @param oldResult expected results returned by v4.x interpreter * @see [[testCases]] */ - case class Expected[+A](value: Try[A], cost: Int) { - def newCost: Int = cost - def newValue: Try[A] = value + case class Expected[+A](oldResult: ExpectedResult[A]) { + /** Expected results returned by v4.x interpreter on the feature function. */ + def value: Try[A] = oldResult.value + + /** Expected verification cost returned by v4.x interpreter on the feature function. */ + def verificationCostOpt: Option[Int] = oldResult.verificationCost + + /** One expected result for each supported ErgoTree version. + * By default (and for most operations) the new values are equal to the old value for + * all versions, which means there are no changes in operation semantics. + * However, new verification costs are different for the old ones. To simplify + * augmentation of test cases with new test vectors, the default value of None + * signals that the test vectors should be defined. The test harness code can print + * suggestions for new test vectors. + */ + final def defaultNewResults: Seq[(ExpectedResult[A], Option[CostDetails])] = { + val n = VersionContext.MaxSupportedScriptVersion + 1 + // NOTE: by default, tests vectors for both verification cost and cost details are not defined + val res = ExpectedResult(oldResult.value, None) + Array.fill(n)((res, None)) + } + + /** One expected result for each supported ErgoTree version. + * This expectations are applied to v5.+ interpreter (i.e. new JITC based implementation). + */ + val newResults: Seq[(ExpectedResult[A], Option[CostDetails])] = defaultNewResults } object Expected { - def apply[A](error: Throwable) = new Expected[A](Failure(error), 0) - def apply[A](value: Try[A], cost: Int, expectedNewCost: Int) = new Expected(value, cost) { - override val newCost = expectedNewCost - } - def apply[A](value: Try[A], cost: Int, expectedNewValue: Try[A], expectedNewCost: Int) = new Expected(value, cost) { - override val newCost = expectedNewCost - override val newValue = expectedNewValue - } + /** Used when exception is expected. + * @param error expected during execution + */ + def apply[A](error: Throwable) = new Expected[A](ExpectedResult(Failure(error), None)) + + /** Used when the old and new value and costs are the same for all versions. + * @param value expected result of tested function + * @param cost expected verification cost + */ + def apply[A](value: Try[A], cost: Int): Expected[A] = + new Expected(ExpectedResult(value, Some(cost))) + + /** Used when the old and new value and costs are the same for all versions. + * @param value expected result of tested function + * @param cost expected verification cost + * @param expectedDetails expected cost details for all versions + */ + def apply[A](value: Try[A], cost: Int, expectedDetails: CostDetails): Expected[A] = + new Expected(ExpectedResult(value, Some(cost))) { + override val newResults = defaultNewResults.map { case (r, _) => + (r, Some(expectedDetails)) + } + } + + /** Used when the old and new value and costs are the same for all versions. + * + * @param value expected result of tested function + * @param cost expected verification cost + * @param expectedDetails expected cost details for all versions + * @param expectedNewCost expected new verification cost for all versions + */ + def apply[A](value: Try[A], + cost: Int, + expectedDetails: CostDetails, + expectedNewCost: Int): Expected[A] = + new Expected(ExpectedResult(value, Some(cost))) { + override val newResults = defaultNewResults.map { + case (ExpectedResult(v, _), _) => + (ExpectedResult(v, Some(expectedNewCost)), Some(expectedDetails)) + } + } + + /** Used when operation semantics changes in new versions. For those versions expected + * test vectors can be specified. + * + * @param value value returned by feature function v4.x + * @param cost expected cost value of the verification execution (v4.x) + * @param expectedDetails expected cost details for all versions + * @param newCost expected new verification cost for all versions + * @param newVersionedResults new results returned by each changed feature function in + * v5.+ for each ErgoTree version. + */ + def apply[A](value: Try[A], cost: Int, + expectedDetails: CostDetails, newCost: Int, + newVersionedResults: Seq[(Int, (ExpectedResult[A], Option[CostDetails]))]): Expected[A] = + new Expected[A](ExpectedResult(value, Some(cost))) { + override val newResults = { + val commonNewResults = defaultNewResults.map { + case (res, _) => + (ExpectedResult(res.value, Some(newCost)), Option(expectedDetails)) + } + commonNewResults.updateMany(newVersionedResults) + } + } } /** Describes existing language feature which should be equally supported in both @@ -591,9 +982,11 @@ class SigmaDslTesting extends PropSpec * various ways */ def existingFeature[A: RType, B: RType] - (scalaFunc: A => B, script: String, expectedExpr: SValue = null) - (implicit IR: IRContext): Feature[A, B] = { - ExistingFeature(script, scalaFunc, Option(expectedExpr)) + (scalaFunc: A => B, script: String, + expectedExpr: SValue = null) + (implicit IR: IRContext, evalSettings: EvalSettings): Feature[A, B] = { + ExistingFeature( + script, scalaFunc, Option(expectedExpr)) } /** Describes existing language feature which should be differently supported in both @@ -608,9 +1001,16 @@ class SigmaDslTesting extends PropSpec * various ways */ def changedFeature[A: RType, B: RType] - (scalaFunc: A => B, scalaFuncNew: A => B, script: String, expectedExpr: SValue = null) - (implicit IR: IRContext): Feature[A, B] = { - ChangedFeature(script, scalaFunc, scalaFuncNew, Option(expectedExpr)) + (scalaFunc: A => B, + scalaFuncNew: A => B, + script: String, + expectedExpr: SValue = null, + allowNewToSucceed: Boolean = false, + allowDifferentErrors: Boolean = false) + (implicit IR: IRContext, evalSettings: EvalSettings): Feature[A, B] = { + ChangedFeature(script, scalaFunc, scalaFuncNew, Option(expectedExpr), + allowNewToSucceed = allowNewToSucceed, + allowDifferentErrors = allowDifferentErrors) } /** Describes a NEW language feature which must NOT be supported in v4 and @@ -624,7 +1024,7 @@ class SigmaDslTesting extends PropSpec */ def newFeature[A: RType, B: RType] (scalaFunc: A => B, script: String, expectedExpr: SValue = null) - (implicit IR: IRContext): Feature[A, B] = { + (implicit IR: IRContext, es: EvalSettings): Feature[A, B] = { NewFeature(script, scalaFunc, Option(expectedExpr)) } @@ -634,17 +1034,24 @@ class SigmaDslTesting extends PropSpec /** NOTE, this should be `def` to allow overriding of generatorDrivenConfig in derived Spec classes. */ def DefaultMinSuccessful: MinSuccessful = MinSuccessful(generatorDrivenConfig.minSuccessful) - val PrintTestCasesDefault: Boolean = false // true + val PrintTestCasesDefault: Boolean = false val FailOnTestVectorsDefault: Boolean = true - private def checkResult[B](res: Try[B], expectedRes: Try[B], failOnTestVectors: Boolean): Unit = { + /** Because this method is called from many places it should always be called with `hint`. */ + protected def checkResult[B](res: Try[B], expectedRes: Try[B], failOnTestVectors: Boolean, hint: String): Unit = { (res, expectedRes) match { case (Failure(exception), Failure(expectedException)) => - rootCause(exception).getClass shouldBe expectedException.getClass + withClue(hint) { + rootCause(exception).getClass shouldBe rootCause(expectedException).getClass + } case _ => if (failOnTestVectors) { - val actual = res.fold(t => Failure(rootCause(t)), Success(_)) - assertResult(expectedRes, s"Actual: ${SigmaPPrint(actual, height = 150).plainText}")(actual) + val actual = rootCause(res) + if (expectedRes != actual) { + val actualPrinted = SigmaPPrint(actual, height = 150).plainText + val expectedPrinted = SigmaPPrint(expectedRes, height = 150).plainText + assert(false, s"$hint\nActual: $actualPrinted;\nExpected: $expectedPrinted\n") + } } else { if (expectedRes != res) { @@ -698,6 +1105,61 @@ class SigmaDslTesting extends PropSpec test(preGeneratedSamples, f, printTestCases) } + def verifyCasesMany[A: Ordering : Arbitrary : ClassTag, B] + (cases: Seq[(A, Expected[B])], + features: Seq[Feature[A, B]], + printTestCases: Boolean = PrintTestCasesDefault, + failOnTestVectors: Boolean = FailOnTestVectorsDefault, + preGeneratedSamples: Option[Seq[A]] = None): Unit = { + features.foreach { f => + verifyCases(cases, f, printTestCases, failOnTestVectors, preGeneratedSamples) + } + } + + case class MeasureInfo[A](input: A, iteration: Int, nIters: Int, measuredTime: Long) + + type MeasureFormatter[A] = MeasureInfo[A] => String + + def benchmarkCases[A: Ordering : Arbitrary : ClassTag, B] + (cases: Seq[A], f: Feature[A, B], nIters: Int, formatter: MeasureFormatter[A]) + (implicit IR: IRContext, evalSettings: EvalSettings): Seq[Long] = { + val fNew = f.newF + implicit val tA = fNew.tA + implicit val tB = fNew.tB + implicit val cs = defaultCompilerSettings + val func = funcJitFast[A, B](f.script) + val noTraceSettings = evalSettings.copy( + isMeasureOperationTime = false, + costTracingEnabled = false) + val funcNoTrace = funcJitFast[A, B](f.script)(tA, tB, IR, noTraceSettings, cs) + var iCase = 0 + val (res, total) = BenchmarkUtil.measureTimeNano { + VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) { + cases.map { x => + assert(func(x)._1 == f.newF(x)._1) + iCase += 1 + def benchmarkCase(func: CompiledFunc[A,B], printOut: Boolean) = { + val (_, t) = BenchmarkUtil.measureTimeNano { + cfor(0)(_ < nIters, _ + 1) { i => + val res = func(x) + } + } + if (printOut) { + val info = MeasureInfo(x, iCase, nIters, t) + val out = formatter(info) + println(out) + } + t + } + benchmarkCase(func, printOut = false) + benchmarkCase(funcNoTrace, printOut = true) + } + } + } + println(s"Total time: ${total / 1000000} msec") + res + } + /** Generate samples in sorted order. * @param gen generator to be used for sample generation * @param config generation configuration diff --git a/sigmastate/src/test/scala/special/sigma/SigmaTestingData.scala b/sigmastate/src/test/scala/special/sigma/SigmaTestingData.scala index 30751521ba..7e73a0ae09 100644 --- a/sigmastate/src/test/scala/special/sigma/SigmaTestingData.scala +++ b/sigmastate/src/test/scala/special/sigma/SigmaTestingData.scala @@ -8,7 +8,7 @@ import org.scalacheck.Gen.containerOfN import sigmastate._ import org.scalacheck.{Arbitrary, Gen} import sigmastate.helpers.SigmaTestingCommons -import sigmastate.eval._ +import sigmastate.eval.{Colls, _} import sigmastate.eval.Extensions._ import org.scalacheck.util.Buildable import scalan.RType @@ -19,6 +19,7 @@ import sigmastate.Values._ import sigmastate.basics.DLogProtocol.ProveDlog import sigmastate.basics.ProveDHTuple import sigmastate.interpreter.CryptoConstants.EcPointType +import sigmastate.Values.ErgoTree import sigmastate.serialization.ErgoTreeSerializer import sigmastate.serialization.generators.ObjectGenerators import sigmastate.utils.Helpers @@ -56,10 +57,10 @@ trait SigmaTestingData extends SigmaTestingCommons with ObjectGenerators { } protected def sampleAvlProver = { - val key = keyCollGen.sample.get - val value = bytesCollGen.sample.get - val (tree, prover) = createAvlTreeAndProver(key -> value) - (key, value, tree, prover) + val keys = arrayOfN(100, keyCollGen).sample.get + val values = arrayOfN(100, bytesCollGen).sample.get + val (tree, prover) = createAvlTreeAndProver(keys.zip(values):_*) + (keys, values, tree, prover) } protected def sampleAvlTree: AvlTree = { @@ -127,7 +128,7 @@ trait SigmaTestingData extends SigmaTestingCommons with ObjectGenerators { def createBigIntMaxValue(): BigInt = BigIntMaxValue_instances.getNext - // TODO HF: this values have bitCount == 255 (see to256BitValueExact) + // TODO v6.0: this values have bitCount == 255 (see to256BitValueExact) val BigIntMinValue = CBigInt(new BigInteger("-7F" + "ff" * 31, 16)) val BigIntMaxValue = createBigIntMaxValue() val BigIntOverlimit = CBigInt(new BigInteger("7F" + "ff" * 33, 16))