Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6.0] Global.serialize method #989

Merged
merged 19 commits into from
Sep 20, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
merging w. 6.0.0
kushti committed Aug 13, 2024
commit 65144a9b9e3745d68effbc494171b47c4b472e04
2 changes: 1 addition & 1 deletion core/shared/src/main/scala/sigma/VersionContext.scala
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ case class VersionContext(activatedVersion: Byte, ergoTreeVersion: Byte) {
def isJitActivated: Boolean = activatedVersion >= JitActivationVersion

/** @return true, if the activated script version of Ergo protocol on the network is
* including Evolution update. */
* including v6.0 update. */
def isV6SoftForkActivated: Boolean = activatedVersion >= V6SoftForkVersion
}

30 changes: 17 additions & 13 deletions core/shared/src/main/scala/sigma/ast/SType.scala
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import sigma.data.OverloadHack.Overloaded1
import sigma.data.{CBigInt, Nullable, SigmaConstants}
import sigma.reflection.{RClass, RMethod, ReflectionData}
import sigma.util.Extensions.{IntOps, LongOps, ShortOps}
import sigma.{AvlTree, BigInt, Box, Coll, Context, Evaluation, GroupElement, Header, PreHeader, SigmaDslBuilder, SigmaProp}
import sigma.{AvlTree, BigInt, Box, Coll, Context, Evaluation, GroupElement, Header, PreHeader, SigmaDslBuilder, SigmaProp, VersionContext}

import java.math.BigInteger

@@ -375,6 +375,7 @@ case object SByte extends SPrimType with SEmbeddable with SNumericType with SMon
case s: Short => s.toByteExact
case i: Int => i.toByteExact
case l: Long => l.toByteExact
case bi: BigInt if VersionContext.current.isV6SoftForkActivated => bi.toByte // toByteExact from int is called under the hood
case _ => sys.error(s"Cannot downcast value $v to the type $this")
}
}
@@ -396,6 +397,7 @@ case object SShort extends SPrimType with SEmbeddable with SNumericType with SMo
case s: Short => s
case i: Int => i.toShortExact
case l: Long => l.toShortExact
case bi: BigInt if VersionContext.current.isV6SoftForkActivated => bi.toShort // toShortExact from int is called under the hood
case _ => sys.error(s"Cannot downcast value $v to the type $this")
}
}
@@ -419,6 +421,7 @@ case object SInt extends SPrimType with SEmbeddable with SNumericType with SMono
case s: Short => s.toInt
case i: Int => i
case l: Long => l.toIntExact
case bi: BigInt if VersionContext.current.isV6SoftForkActivated => bi.toInt
case _ => sys.error(s"Cannot downcast value $v to the type $this")
}
}
@@ -444,6 +447,7 @@ case object SLong extends SPrimType with SEmbeddable with SNumericType with SMon
case s: Short => s.toLong
case i: Int => i.toLong
case l: Long => l
case bi: BigInt if VersionContext.current.isV6SoftForkActivated => bi.toLong
case _ => sys.error(s"Cannot downcast value $v to the type $this")
}
}
@@ -465,24 +469,24 @@ case object SBigInt extends SPrimType with SEmbeddable with SNumericType with SM
override def numericTypeIndex: Int = 4

override def upcast(v: AnyVal): BigInt = {
val bi = v match {
case x: Byte => BigInteger.valueOf(x.toLong)
case x: Short => BigInteger.valueOf(x.toLong)
case x: Int => BigInteger.valueOf(x.toLong)
case x: Long => BigInteger.valueOf(x)
v match {
case x: Byte => CBigInt(BigInteger.valueOf(x.toLong))
case x: Short => CBigInt(BigInteger.valueOf(x.toLong))
case x: Int => CBigInt(BigInteger.valueOf(x.toLong))
case x: Long => CBigInt(BigInteger.valueOf(x))
case x: BigInt if VersionContext.current.isV6SoftForkActivated => x
case _ => sys.error(s"Cannot upcast value $v to the type $this")
}
CBigInt(bi)
}
override def downcast(v: AnyVal): BigInt = {
val bi = v match {
case x: Byte => BigInteger.valueOf(x.toLong)
case x: Short => BigInteger.valueOf(x.toLong)
case x: Int => BigInteger.valueOf(x.toLong)
case x: Long => BigInteger.valueOf(x)
v match {
case x: Byte => CBigInt(BigInteger.valueOf(x.toLong))
case x: Short => CBigInt(BigInteger.valueOf(x.toLong))
case x: Int => CBigInt(BigInteger.valueOf(x.toLong))
case x: Long => CBigInt(BigInteger.valueOf(x))
case x: BigInt if VersionContext.current.isV6SoftForkActivated => x
case _ => sys.error(s"Cannot downcast value $v to the type $this")
}
CBigInt(bi)
}
}

Original file line number Diff line number Diff line change
@@ -155,20 +155,6 @@ object ValidationRules {
override protected lazy val settings: SigmaValidationSettings = currentSettings
}

object CheckMinimalErgoTreeVersion extends ValidationRule(1016,
"ErgoTree should have at least required version") with SoftForkWhenReplaced {
override protected lazy val settings: SigmaValidationSettings = currentSettings

final def apply(currentVersion: Byte, minVersion: Byte): Unit = {
checkRule()
if (currentVersion < minVersion) {
throwValidationException(
new SigmaException(s"ErgoTree should have at least $minVersion version, but was $currentVersion"),
Array(currentVersion, minVersion))
}
}
}

val ruleSpecs: Seq[ValidationRule] = Seq(
CheckDeserializedScriptType,
CheckDeserializedScriptIsSigmaProp,
@@ -185,8 +171,7 @@ object ValidationRules {
CheckHeaderSizeBit,
CheckCostFuncOperation,
CheckPositionLimit,
CheckLoopLevelInCostFunction,
CheckMinimalErgoTreeVersion
CheckLoopLevelInCostFunction
)

/** Validation settings that correspond to the current version of the ErgoScript implementation.
25 changes: 22 additions & 3 deletions data/shared/src/main/scala/sigma/ast/SMethod.scala
Original file line number Diff line number Diff line change
@@ -63,6 +63,7 @@ case class MethodIRInfo(
* @param docInfo optional human readable method description data
* @param costFunc optional specification of how the cost should be computed for the
* given method call (See ErgoTreeEvaluator.calcCost method).
* @param userDefinedInvoke optional custom method evaluation function
*/
case class SMethod(
objType: MethodsContainer,
@@ -73,7 +74,9 @@ case class SMethod(
explicitTypeArgs: Seq[STypeVar],
irInfo: MethodIRInfo,
docInfo: Option[OperationInfo],
costFunc: Option[MethodCostFunc]) {
costFunc: Option[MethodCostFunc],
userDefinedInvoke: Option[SMethod.InvokeHandler]
) {

/** Operation descriptor of this method. */
lazy val opDesc = MethodDesc(this)
@@ -114,7 +117,12 @@ case class SMethod(
/** Invoke this method on the given object with the arguments.
* This is used for methods with FixedCost costKind. */
def invokeFixed(obj: Any, args: Array[Any]): Any = {
javaMethod.invoke(obj, args.asInstanceOf[Array[AnyRef]]:_*)
userDefinedInvoke match {
case Some(h) =>
h(this, obj, args)
case None =>
javaMethod.invoke(obj, args.asInstanceOf[Array[AnyRef]]:_*)
}
}

// TODO optimize: avoid lookup when this SMethod is created via `specializeFor`
@@ -154,6 +162,11 @@ case class SMethod(
m
}

/** Create a new instance with the given user-defined invoke handler. */
def withUserDefinedInvoke(handler: SMethod.InvokeHandler): SMethod = {
copy(userDefinedInvoke = Some(handler))
}

/** Create a new instance with the given stype. */
def withSType(newSType: SFunc): SMethod = copy(stype = newSType)

@@ -263,6 +276,12 @@ object SMethod {
*/
type InvokeDescBuilder = SFunc => Seq[SType]

/** Type of user-defined function which is called to handle method invocation.
* Instances of this type can be attached to [[SMethod]] instances.
* @see SNumericTypeMethods.ToBytesMethod
*/
type InvokeHandler = (SMethod, Any, Array[Any]) => Any

/** Return [[Method]] descriptor for the given `methodName` on the given `cT` type.
* @param methodName the name of the method to lookup
* @param cT the class where to search the methodName
@@ -297,7 +316,7 @@ object SMethod {
): SMethod = {
SMethod(
objType, name, stype, methodId, costKind, explicitTypeArgs,
MethodIRInfo(None, None, None), None, None)
MethodIRInfo(None, None, None), None, None, None)
}


1 change: 0 additions & 1 deletion data/shared/src/main/scala/sigma/ast/SigmaPredef.scala
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@ import org.ergoplatform.{ErgoAddressEncoder, P2PKAddress}
import scorex.util.encode.{Base16, Base58, Base64}
import sigma.ast.SCollection.{SByteArray, SIntArray}
import sigma.ast.SOption.SIntOption
import sigma.ast.SigmaPropConstant
import sigma.ast.syntax._
import sigma.data.Nullable
import sigma.exceptions.InvalidArguments
2 changes: 0 additions & 2 deletions data/shared/src/main/scala/sigma/ast/methods.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package sigma.ast

import org.ergoplatform._
import org.ergoplatform.validation.ValidationRules.CheckMinimalErgoTreeVersion
import org.ergoplatform.validation._
import sigma._
import sigma.ast.SCollection.{SBooleanArray, SBoxArray, SByteArray, SByteArray2, SHeaderArray}
@@ -1534,7 +1533,6 @@ case object SGlobalMethods extends MonoTypeMethods {
*/
def serialize_eval(mc: MethodCall, G: SigmaDslBuilder, value: SType#WrappedType)
(implicit E: ErgoTreeEvaluator): Coll[Byte] = {
CheckMinimalErgoTreeVersion(E.context.currentErgoTreeVersion, VersionContext.V6SoftForkVersion)

E.addCost(SigmaByteWriter.StartWriterCost)

2 changes: 2 additions & 0 deletions data/shared/src/main/scala/sigma/ast/values.scala
Original file line number Diff line number Diff line change
@@ -1298,8 +1298,10 @@ case class MethodCall(
method: SMethod,
args: IndexedSeq[Value[SType]],
typeSubst: Map[STypeVar, SType]) extends Value[SType] {

require(method.explicitTypeArgs.forall(tyArg => typeSubst.contains(tyArg)),
s"Generic method call should have concrete type for each explicit type parameter, but was: $this")

override def companion = if (args.isEmpty) PropertyCall else MethodCall

override def opType: SFunc = SFunc(obj.tpe +: args.map(_.tpe), tpe)
Original file line number Diff line number Diff line change
@@ -287,6 +287,9 @@ class ErgoTreeSerializer {
* allow to use serialized scripts as pre-defined templates.
* See [[SubstConstants]] for details.
*
* Note, this operation doesn't require (de)serialization of ErgoTree expression,
* thus it is more efficient than serialization roundtrip.
*
* @param scriptBytes serialized ErgoTree with ConstantSegregationFlag set to 1.
* @param positions zero based indexes in ErgoTree.constants array which
* should be replaced with new values
@@ -304,39 +307,62 @@ class ErgoTreeSerializer {
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)
val nConstants = constants.length

val resBytes = if (VersionContext.current.isJitActivated) {
// need to measure the serialized size of the new constants
// by serializing them into a separate writer
val constW = SigmaSerializer.startWriter()

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)
constW.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)
constantSerializer.serialize(c, constW)
} else {
assert(positions(iPos) == i) // INV: backrefs and positions are mutually inverse
require(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)
constantSerializer.serialize(newConst, constW)
}
}
}

val constBytes = constW.toBytes // nConstants + serialized new constants

// start composing the resulting tree bytes
val w = SigmaSerializer.startWriter()
w.put(header) // header byte

if (VersionContext.current.isV6SoftForkActivated) {
// fix in v6.0 to save tree size to respect size bit of the original tree
if (ErgoTree.hasSize(header)) {
val size = constBytes.length + treeBytes.length
w.putUInt(size) // tree size
}
}

w.putBytes(constBytes) // constants section
w.putBytes(treeBytes) // tree section
w.toBytes
} else {
val w = SigmaSerializer.startWriter()
w.put(header)

// for v4.x compatibility we save constants.length here (see the above comment to
// understand the consequences)
w.putUInt(constants.length)
@@ -357,10 +383,12 @@ class ErgoTreeSerializer {
case (c, _) =>
constantSerializer.serialize(c, w)
}

w.putBytes(treeBytes)
w.toBytes
}

w.putBytes(treeBytes)
(w.toBytes, constants.length)
(resBytes, nConstants)
}

}
Original file line number Diff line number Diff line change
@@ -247,7 +247,12 @@ trait Interpreter {
val currCost = 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)
// Before ErgoTree V3 the deserialization cost was not added to the total cost
applyDeserializeContextJITC(if (VersionContext.current.activatedVersion >= VersionContext.V6SoftForkVersion) {
context1
} else {
context
}, prop)
}

// here we assume that when `propTree` is TrueProp then `reduceToCrypto` always succeeds
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ package sigma.serialization

import sigma.VersionContext
import sigma.ast.SCollection.SByteArray
import sigma.ast.SType.tT
import sigma.ast._
import sigma.validation.ValidationException

Original file line number Diff line number Diff line change
@@ -139,7 +139,8 @@ class SigmaTyper(val builder: SigmaBuilder,
obj.tpe match {
case p: SProduct =>
MethodsContainer.getMethod(p, n) match {
case Some(method @ SMethod(_, _, genFunTpe @ SFunc(_, _, _), _, _, _, _, _, _)) =>
case Some(method: SMethod) =>
val genFunTpe = method.stype
val subst = Map(genFunTpe.tpeParams.head.ident -> rangeTpe)
val concrFunTpe = applySubst(genFunTpe, subst)
val expectedArgs = concrFunTpe.asFunc.tDom.tail
24 changes: 22 additions & 2 deletions sc/shared/src/test/scala/sigma/LanguageSpecificationBase.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package sigma

import org.scalatest.BeforeAndAfterAll
import sigma.ast.JitCost
import sigma.eval.{EvalSettings, Profiler}
import sigma.ast.{Apply, FixedCostItem, FuncValue, GetVar, JitCost, OptionGet, ValUse}
import sigma.eval.{CostDetails, EvalSettings, Profiler}
import sigmastate.CompilerCrossVersionProps
import sigmastate.interpreter.CErgoTreeEvaluator

import scala.util.Success

/** Base class for language test suites (one suite for each language version: 5.0, 6.0, etc.)
@@ -123,4 +124,23 @@ abstract class LanguageSpecificationBase extends SigmaDslTesting
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)
)

/** Helper method to create the given expected results for all tree versions. */
def expectedSuccessForAllTreeVersions[A](value: A, cost: Int, costDetails: CostDetails) = {
val res = ExpectedResult(Success(value), Some(cost)) -> Some(costDetails)
Seq(0, 1, 2, 3).map(version => version -> res)
}

}
1,890 changes: 1,025 additions & 865 deletions sc/shared/src/test/scala/sigma/LanguageSpecificationV5.scala

Large diffs are not rendered by default.

161 changes: 145 additions & 16 deletions sc/shared/src/test/scala/sigma/LanguageSpecificationV6.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package sigma

import sigma.ast.{Apply, Downcast, FixedCost, FixedCostItem, FuncValue, GetVar, Global, JitCost, MethodCall, NamedDesc, OptionGet, SBigInt, SByte, SGlobalMethods, SInt, SLong, SShort, STypeVar, ValUse}
import sigma.ast.ErgoTree.ZeroHeader
import sigma.ast.SCollection.SByteArray
import sigma.ast.syntax.TrueSigmaProp
import sigma.ast._
import sigma.data.{CBigInt, ExactNumeric, RType}
import sigma.eval.{SigmaDsl, TracedCost}
import sigma.eval.{CostDetails, SigmaDsl, TracedCost}
import sigma.util.Extensions.{BooleanOps, ByteOps, IntOps, LongOps}
import sigmastate.exceptions.MethodNotFound
import sigmastate.utils.Helpers

import java.math.BigInteger
import scala.util.Success
@@ -234,21 +238,26 @@ class LanguageSpecificationV6 extends LanguageSpecificationBase { suite =>
}

property("BigInt methods equivalence (new features)") {
// TODO v6.0: the behavior of `upcast` for BigInt is different from all other Numeric types (see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/877)
// 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 upcoming forks
assertExceptionThrown(
SBigInt.upcast(CBigInt(new BigInteger("0", 16)).asInstanceOf[AnyVal]),
_.getMessage.contains("Cannot upcast value")
)
if (activatedVersionInTests < VersionContext.V6SoftForkVersion) {
// The `Upcast(bigInt, SBigInt)` node is never produced by ErgoScript compiler, but is still valid ErgoTree.
// Fixed in 6.0
assertExceptionThrown(
SBigInt.upcast(CBigInt(new BigInteger("0", 16)).asInstanceOf[AnyVal]),
_.getMessage.contains("Cannot upcast value")
)

// TODO v6.0: the behavior of `downcast` for BigInt is different from all other Numeric types (see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/877)
// 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(
SBigInt.downcast(CBigInt(new BigInteger("0", 16)).asInstanceOf[AnyVal]),
_.getMessage.contains("Cannot downcast value")
)
// The `Downcast(bigInt, SBigInt)` node is never produced by ErgoScript compiler, but is still valid ErgoTree.
// Fixed in 6.0
assertExceptionThrown(
SBigInt.downcast(CBigInt(new BigInteger("0", 16)).asInstanceOf[AnyVal]),
_.getMessage.contains("Cannot downcast value")
)
} else {
forAll { x: BigInteger =>
SBigInt.upcast(CBigInt(x).asInstanceOf[AnyVal]) shouldBe CBigInt(x)
SBigInt.downcast(CBigInt(x).asInstanceOf[AnyVal]) shouldBe CBigInt(x)
}
}

if (activatedVersionInTests < VersionContext.V6SoftForkVersion) {
// NOTE, for such versions the new features are not supported
@@ -287,6 +296,44 @@ class LanguageSpecificationV6 extends LanguageSpecificationBase { suite =>
forAll { x: (BigInt, BigInt) =>
Seq(compareTo, bitOr, bitAnd).foreach(_.checkEquality(x))
}

forAll { x: Long =>
assertExceptionThrown(
SLong.downcast(CBigInt(new BigInteger(x.toString)).asInstanceOf[AnyVal]),
_.getMessage.contains("Cannot downcast value")
)
}
forAll { x: Int =>
assertExceptionThrown(
SInt.downcast(CBigInt(new BigInteger(x.toString)).asInstanceOf[AnyVal]),
_.getMessage.contains("Cannot downcast value")
)
}
forAll { x: Byte =>
assertExceptionThrown(
SByte.downcast(CBigInt(new BigInteger(x.toString)).asInstanceOf[AnyVal]),
_.getMessage.contains("Cannot downcast value")
)
}
forAll { x: Short =>
assertExceptionThrown(
SShort.downcast(CBigInt(new BigInteger(x.toString)).asInstanceOf[AnyVal]),
_.getMessage.contains("Cannot downcast value")
)
}
} else {
forAll { x: Long =>
SLong.downcast(CBigInt(new BigInteger(x.toString)).asInstanceOf[AnyVal]) shouldBe x
}
forAll { x: Int =>
SInt.downcast(CBigInt(new BigInteger(x.toString)).asInstanceOf[AnyVal]) shouldBe x
}
forAll { x: Byte =>
SByte.downcast(CBigInt(new BigInteger(x.toString)).asInstanceOf[AnyVal]) shouldBe x
}
forAll { x: Short =>
SShort.downcast(CBigInt(new BigInteger(x.toString)).asInstanceOf[AnyVal]) shouldBe x
}
}
}

@@ -406,5 +453,87 @@ class LanguageSpecificationV6 extends LanguageSpecificationBase { suite =>
}
}

property("Fix substConstants in v6.0 for ErgoTree version > 0") {
// tree with one segregated constant and v0
val t1 = ErgoTree(
header = ErgoTree.setConstantSegregation(ZeroHeader),
constants = Vector(TrueSigmaProp),
ConstantPlaceholder(0, SSigmaProp))

// tree with one segregated constant and max supported version
val t2 = ErgoTree(
header = ErgoTree.setConstantSegregation(
ErgoTree.headerWithVersion(ZeroHeader, VersionContext.MaxSupportedScriptVersion)
),
Vector(TrueSigmaProp),
ConstantPlaceholder(0, SSigmaProp))

def costDetails(nItems: Int) = TracedCost(
traceBase ++ Array(
FixedCostItem(SelectField),
FixedCostItem(ConcreteCollection),
FixedCostItem(ValUse),
FixedCostItem(SelectField),
FixedCostItem(ConcreteCollection),
FixedCostItem(Constant),
FixedCostItem(BoolToSigmaProp),
ast.SeqCostItem(CompanionDesc(SubstConstants), PerItemCost(JitCost(100), JitCost(100), 1), nItems)
)
)

val expectedTreeBytes_beforeV6 = Helpers.decodeBytes("1b0108d27300")
val expectedTreeBytes_V6 = Helpers.decodeBytes("1b050108d27300")

verifyCases(
Seq(
// for tree v0, the result is the same for all versions
(Coll(t1.bytes: _*), 0) -> Expected(
Success(Helpers.decodeBytes("100108d27300")),
cost = 1793,
expectedDetails = CostDetails.ZeroCost,
newCost = 2065,
newVersionedResults = expectedSuccessForAllTreeVersions(Helpers.decodeBytes("100108d27300"), 2065, costDetails(1))
),
// for tree version > 0, the result depend on activated version
(Coll(t2.bytes: _*), 0) -> Expected(
Success(expectedTreeBytes_beforeV6),
cost = 1793,
expectedDetails = CostDetails.ZeroCost,
newCost = 2065,
newVersionedResults = expectedSuccessForAllTreeVersions(expectedTreeBytes_V6, 2065, costDetails(1)))
),
changedFeature(
changedInVersion = VersionContext.V6SoftForkVersion,
{ (x: (Coll[Byte], Int)) =>
SigmaDsl.substConstants(x._1, Coll[Int](x._2), Coll[Any](SigmaDsl.sigmaProp(false))(sigma.AnyType))
},
{ (x: (Coll[Byte], Int)) =>
SigmaDsl.substConstants(x._1, Coll[Int](x._2), Coll[Any](SigmaDsl.sigmaProp(false))(sigma.AnyType))
},
"{ (x: (Coll[Byte], Int)) => substConstants[Any](x._1, Coll[Int](x._2), Coll[Any](sigmaProp(false))) }",
FuncValue(
Vector((1, SPair(SByteArray, SInt))),
SubstConstants(
SelectField.typed[Value[SCollection[SByte.type]]](ValUse(1, SPair(SByteArray, SInt)), 1.toByte),
ConcreteCollection(
Array(SelectField.typed[Value[SInt.type]](ValUse(1, SPair(SByteArray, SInt)), 2.toByte)),
SInt
),
ConcreteCollection(Array(BoolToSigmaProp(FalseLeaf)), SSigmaProp)
)
)
)
)

// before v6.0 the expected tree is not parsable
ErgoTree.fromBytes(expectedTreeBytes_beforeV6.toArray).isRightParsed shouldBe false

// in v6.0 the expected tree should be parsable and similar to the original tree
val tree = ErgoTree.fromBytes(expectedTreeBytes_V6.toArray)
tree.isRightParsed shouldBe true
tree.header shouldBe t2.header
tree.constants.length shouldBe t2.constants.length
tree.root shouldBe t2.root
}

}
84 changes: 71 additions & 13 deletions sc/shared/src/test/scala/sigma/SigmaDslTesting.scala
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import scalan.Platform.threadSleepOrNoOp
import sigma.Extensions.ArrayOps
import sigma.data.{CBox, CollType, OptionType, PairType, ProveDlog, RType, SigmaLeaf}
import sigma.VersionContext.V6SoftForkVersion
import sigma.util.BenchmarkUtil
import sigma.util.CollectionUtil._
import sigma.util.Extensions._
@@ -126,6 +127,9 @@ class SigmaDslTesting extends AnyPropSpec
/** Checks if this feature is supported in the given version context. */
def isSupportedIn(vc: VersionContext): Boolean

/** Version in which the feature is first implemented of changed. */
def sinceVersion: Byte

/** Script containing this feature. */
def script: String

@@ -362,14 +366,16 @@ class SigmaDslTesting extends AnyPropSpec
// Compile script the same way it is performed by applications (i.e. via Ergo Appkit)
val prop = compile(env, code)(IR).asSigmaProp

// Add additional oparations which are not yet implemented in ErgoScript compiler
// Add additional operations which are not yet implemented in ErgoScript compiler
val multisig = AtLeast(
IntConstant(2),
Array(
pkAlice,
DeserializeRegister(ErgoBox.R5, SSigmaProp), // deserialize pkBob
DeserializeContext(2, SSigmaProp))) // deserialize pkCarol
val header = ErgoTree.headerWithVersion(ZeroHeader, ergoTreeVersionInTests)
// We set size for trees v0 as well, to have the same size and so the same cost in V6 interpreter
// (where tree size is accounted in cost)
val header = ErgoTree.setSizeBit(ErgoTree.headerWithVersion(ZeroHeader, ergoTreeVersionInTests))
ErgoTree.withSegregation(header, SigmaOr(prop, multisig))
}

@@ -433,7 +439,7 @@ class SigmaDslTesting extends AnyPropSpec
ctx
}

val (expectedResult, expectedCost) = if (activatedVersionInTests < VersionContext.JitActivationVersion)
val (expectedResult, expectedCost) = if (activatedVersionInTests < sinceVersion)
(expected.oldResult, expected.verificationCostOpt)
else {
val res = expected.newResults(ergoTreeVersionInTests)
@@ -492,7 +498,7 @@ class SigmaDslTesting extends AnyPropSpec
val verificationCost = cost.toIntExact
if (expectedCost.isDefined) {
assertResult(expectedCost.get,
s"Actual verify() cost $cost != expected ${expectedCost.get}")(verificationCost)
s"Actual verify() cost $cost != expected ${expectedCost.get} (version: ${VersionContext.current.activatedVersion})")(verificationCost)
}

case Failure(t) => throw t
@@ -540,6 +546,8 @@ class SigmaDslTesting extends AnyPropSpec

implicit val cs = compilerSettingsInTests

override def sinceVersion: Byte = 0

override def isSupportedIn(vc: VersionContext): Boolean = true

/** in v5.x the old and the new interpreters are the same */
@@ -652,16 +660,35 @@ class SigmaDslTesting extends AnyPropSpec
checkResult(funcRes.map(_._1), expected.value, failOnTestVectors,
"ExistingFeature#verifyCase: ")

checkResultAgainstExpected(funcRes, expected)
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)
}

}

/** Descriptor of a language feature which is changed in v5.0.
/** Descriptor of a language feature which is changed in the specified version.
*
* @tparam A type of an input test data
* @tparam B type of an output of the feature function
* @param changedInVersion version in which the feature behaviour is changed
* @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
@@ -681,6 +708,7 @@ class SigmaDslTesting extends AnyPropSpec
* @param allowDifferentErrors if true, allow v4.x and v5.0 to fail with different error
*/
case class ChangedFeature[A, B](
changedInVersion: Byte,
script: String,
scalaFunc: A => B,
override val scalaFuncNew: A => B,
@@ -694,6 +722,8 @@ class SigmaDslTesting extends AnyPropSpec

implicit val cs = compilerSettingsInTests

override def sinceVersion: Byte = changedInVersion

override def isSupportedIn(vc: VersionContext): Boolean = true

/** Apply given function to the context variable 1 */
@@ -773,7 +803,7 @@ class SigmaDslTesting extends AnyPropSpec
checkEq(scalaFuncNew)(newF)(input)
}

if (!VersionContext.current.isJitActivated) {
if (VersionContext.current.activatedVersion < changedInVersion) {
// check the old implementation with Scala semantic
val expectedOldRes = expected.value

@@ -1035,6 +1065,30 @@ class SigmaDslTesting extends AnyPropSpec
}
}

/** Used when the old and new value and costs are the same for all versions, but Version 3 (Ergo 6.0) will have a different cost due to deserialization cost being added.
* Different versions of ErgoTree can have different deserialization costs as well
*
* @param value expected result of tested function
* @param cost expected verification cost
* @param expectedDetails expected cost details for all versions <= V3
* @param expectedNewCost expected new verification cost for all versions <= V3
* @param expectedV3Cost expected cost for >=V3
*/
def apply[A](value: Try[A],
cost: Int,
expectedDetails: CostDetails,
expectedNewCost: Int,
expectedV3Costs: Seq[Int]
)(implicit dummy: DummyImplicit): Expected[A] =
new Expected(ExpectedResult(value, Some(cost))) {
override val newResults = defaultNewResults.zipWithIndex.map {
case ((ExpectedResult(v, _), _), version) => {
var cost = if (activatedVersionInTests >= V6SoftForkVersion) expectedV3Costs(version) else expectedNewCost
(ExpectedResult(v, Some(cost)), Some(expectedDetails))
}
}
}

/** Used when operation semantics changes in new versions. For those versions expected
* test vectors can be specified.
*
@@ -1045,8 +1099,10 @@ class SigmaDslTesting extends AnyPropSpec
* @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,
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 = {
@@ -1089,14 +1145,16 @@ class SigmaDslTesting extends AnyPropSpec
* various ways
*/
def changedFeature[A: RType, B: RType]
(scalaFunc: A => B,
(changedInVersion: Byte,
scalaFunc: A => B,
scalaFuncNew: A => B,
script: String,
expectedExpr: SValue = null,
allowNewToSucceed: Boolean = false,
allowDifferentErrors: Boolean = false)
allowDifferentErrors: Boolean = false
)
(implicit IR: IRContext, evalSettings: EvalSettings): Feature[A, B] = {
ChangedFeature(script, scalaFunc, scalaFuncNew, Option(expectedExpr),
ChangedFeature(changedInVersion, script, scalaFunc, scalaFuncNew, Option(expectedExpr),
allowNewToSucceed = allowNewToSucceed,
allowDifferentErrors = allowDifferentErrors)
}