diff --git a/build.sbt b/build.sbt index f5252a2f7a..7b9d1cb2c2 100644 --- a/build.sbt +++ b/build.sbt @@ -61,21 +61,15 @@ dynverSeparator in ThisBuild := "-" val bouncycastleBcprov = "org.bouncycastle" % "bcprov-jdk15on" % "1.66" -val scrypto = "org.scorexfoundation" %% "scrypto" % "2.2.1-37-59c4fbd9-SNAPSHOT" -def scryptoDependency(platform: String) = { +val scrypto = "org.scorexfoundation" %% "scrypto" % "2.3.0" +def scryptoDependency = { libraryDependencies += - "org.scorexfoundation" %%% "scrypto" % ( - if (platform == "js") "0.0.0-1-59c4fbd9-SNAPSHOT" - else "2.2.1-37-59c4fbd9-SNAPSHOT" // for JVM - ) + "org.scorexfoundation" %%% "scrypto" % "2.3.0" } -//val scorexUtil = "org.scorexfoundation" %% "scorex-util" % "0.1.8" -//val debox = "org.scorexfoundation" %% "debox" % "0.9.0" -val scorexUtil = "org.scorexfoundation" %% "scorex-util" % "0.1.8-20-565873cd-SNAPSHOT" +val scorexUtil = "org.scorexfoundation" %% "scorex-util" % "0.2.0" val scorexUtilDependency = - libraryDependencies += "org.scorexfoundation" %%% "scorex-util" % "0.1.8-20-565873cd-SNAPSHOT" - -val debox = "org.scorexfoundation" %% "debox" % "0.9.0-9-f853cdce-SNAPSHOT" + libraryDependencies += "org.scorexfoundation" %%% "scorex-util" % "0.2.0" +val debox = "org.scorexfoundation" %% "debox" % "0.10.0" val spireMacros = "org.typelevel" %% "spire-macros" % "0.17.0-M1" val fastparse = "com.lihaoyi" %% "fastparse" % "2.3.3" @@ -160,7 +154,7 @@ def libraryDefSettings = commonSettings ++ crossScalaSettings ++ testSettings lazy val commonDependenies2 = libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value, - "org.scorexfoundation" %%% "debox" % "0.9.0-9-f853cdce-SNAPSHOT", + "org.scorexfoundation" %%% "debox" % "0.10.0", "org.scala-lang.modules" %%% "scala-collection-compat" % "2.7.0" ) @@ -195,11 +189,11 @@ lazy val corelib = crossProject(JVMPlatform, JSPlatform) ) .jvmSettings( crossScalaSettings, - scryptoDependency("jvm") + scryptoDependency ) .jsSettings( crossScalaSettingsJS, - scryptoDependency("js"), + scryptoDependency, libraryDependencies ++= Seq( "org.scala-js" %%% "scala-js-macrotask-executor" % "1.0.0" ), diff --git a/common/shared/src/main/scala/scalan/util/ReflectionUtil.scala b/common/shared/src/main/scala/scalan/util/ReflectionUtil.scala new file mode 100644 index 0000000000..dd98f3069e --- /dev/null +++ b/common/shared/src/main/scala/scalan/util/ReflectionUtil.scala @@ -0,0 +1,21 @@ +package scalan.util + +import scala.language.existentials + +object ReflectionUtil { + + implicit class ClassOps(val cl: Class[_]) extends AnyVal { + private def isSpecialChar(c: Char): Boolean = { + ('0' <= c && c <= '9') || c == '$' + } + def safeSimpleName: String = { + if (cl.getEnclosingClass == null) return cl.getSimpleName + val simpleName = cl.getName.substring(cl.getEnclosingClass.getName.length) + val length = simpleName.length + var index = 0 + while (index < length && isSpecialChar(simpleName.charAt(index))) { index += 1 } + // Eventually, this is the empty string iff this is an anonymous class + simpleName.substring(index) + } + } +} diff --git a/graph-ir/src/main/scala/scalan/Entities.scala b/graph-ir/src/main/scala/scalan/Entities.scala index 6c1c062f4c..376bd5a529 100644 --- a/graph-ir/src/main/scala/scalan/Entities.scala +++ b/graph-ir/src/main/scala/scalan/Entities.scala @@ -1,6 +1,7 @@ package scalan import scala.language.higherKinds +import scalan.util.ReflectionUtil.ClassOps /** A slice in the Scalan cake with base classes for various descriptors. */ trait Entities extends TypeDescs { self: Scalan => @@ -13,7 +14,7 @@ trait Entities extends TypeDescs { self: Scalan => def parent: Option[Elem[_]] = None /** Name of the entity type without `Elem` suffix. */ def entityName: String = { - val n = this.getClass.getSimpleName.stripSuffix("Elem") + val n = this.getClass.safeSimpleName.stripSuffix("Elem") n } def convert(x: Ref[Def[_]]): Ref[A] = !!!("should not be called") diff --git a/graph-ir/src/main/scala/scalan/TypeDescs.scala b/graph-ir/src/main/scala/scalan/TypeDescs.scala index 029304bb9e..8963d4faf5 100644 --- a/graph-ir/src/main/scala/scalan/TypeDescs.scala +++ b/graph-ir/src/main/scala/scalan/TypeDescs.scala @@ -5,7 +5,7 @@ import scala.annotation.implicitNotFound import scala.collection.immutable.ListMap import scalan.util._ import scalan.RType._ - +import scalan.util.ReflectionUtil.ClassOps import scala.collection.mutable import debox.cfor import scalan.reflection.{RClass, RConstructor, RMethod} @@ -108,7 +108,7 @@ abstract class TypeDescs extends Base { self: Scalan => lazy val name: String = getName(_.name) // <> to delimit because: [] is used inside name; {} looks bad with structs. - override def toString = s"${getClass.getSimpleName}<$name>" + override def toString = s"${getClass.safeSimpleName}<$name>" } /** Type descriptor of staged types, which correspond to source (unstaged) RTypes @@ -134,7 +134,7 @@ abstract class TypeDescs extends Base { self: Scalan => be.sourceType.name case e => val cl = e.getClass - val name = cl.getSimpleName.stripSuffix("Elem") + val name = cl.safeSimpleName.stripSuffix("Elem") name } if (typeArgs.isEmpty) diff --git a/interpreter/shared/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala b/interpreter/shared/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala index a4feda1ed7..9bce0f0246 100644 --- a/interpreter/shared/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala +++ b/interpreter/shared/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala @@ -680,6 +680,12 @@ trait ObjectGenerators extends TypeGenerators ErgoTree.withoutSegregation)) } yield treeBuilder(prop) + lazy val ergoTreeWithSegregationGen: Gen[ErgoTree] = for { + sigmaBoolean <- Gen.delay(sigmaBooleanGen) + propWithConstants <- Gen.delay(logicalExprTreeNodeGen(Seq(AND.apply, OR.apply, XorOf.apply)).map(_.toSigmaProp)) + prop <- Gen.oneOf(propWithConstants, sigmaBoolean.toSigmaProp) + } yield ErgoTree.withSegregation(prop) + def headerGen(stateRoot: AvlTree, parentId: Coll[Byte]): Gen[Header] = for { id <- modifierIdBytesGen version <- arbByte.arbitrary diff --git a/sdk/shared/src/main/scala/org/ergoplatform/sdk/ContractTemplate.scala b/sdk/shared/src/main/scala/org/ergoplatform/sdk/ContractTemplate.scala new file mode 100644 index 0000000000..c24a9fb4b3 --- /dev/null +++ b/sdk/shared/src/main/scala/org/ergoplatform/sdk/ContractTemplate.scala @@ -0,0 +1,380 @@ +package org.ergoplatform.sdk + +import cats.syntax.either._ +import debox.cfor +import io.circe._ +import io.circe.syntax.{EncoderOps, _} +import org.ergoplatform.sdk.utils.SerializationUtils.{parseString, serializeString} +import org.ergoplatform.sdk.utils.Zero +import sigmastate.Values.ErgoTree.headerWithVersion +import sigmastate.Values.{ErgoTree, _} +import sigmastate._ +import sigmastate.eval.{Colls, _} +import sigmastate.exceptions.SerializerException +import sigmastate.lang.{DeserializationSigmaBuilder, StdSigmaBuilder} +import sigmastate.serialization._ +import sigmastate.util.safeNewArray +import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} + +import java.util.Objects +import scala.collection.mutable +import scala.language.implicitConversions + +/** + * Represents a ContractTemplate parameter. + * @param name user readable parameter name (string bytes in UTF-8 encoding) + * @param description user readable parameter description (string bytes in UTF-8 encoding) + * @param constantIndex index in the ErgoTree.constants array + */ +case class Parameter( + name: String, + description: String, + constantIndex: Int +) + +object Parameter { + + /** Immutable empty IndexedSeq, can be used to save allocations in many places. */ + val EmptySeq: IndexedSeq[Parameter] = Array.empty[Parameter] + + /** HOTSPOT: don't beautify this code. */ + object serializer extends SigmaSerializer[Parameter, Parameter] { + override def serialize(data: Parameter, w: SigmaByteWriter): Unit = { + serializeString(data.name, w) + serializeString(data.description, w) + w.putUInt(data.constantIndex) + } + + override def parse(r: SigmaByteReader): Parameter = { + val name = parseString(r) + val description = parseString(r) + val constantIndex = r.getUInt().toInt + Parameter(name, description, constantIndex) + } + } + + /** Json encoder for Parameter. */ + implicit val encoder: Encoder[Parameter] = Encoder.instance({ p => + Json.obj( + "name" -> Json.fromString(p.name), + "description" -> Json.fromString(p.description), + "constantIndex" -> Json.fromInt(p.constantIndex) + ) + }) + + /** Json decoder for Parameter. */ + implicit val decoder: Decoder[Parameter] = Decoder.instance({ cursor => + for { + name <- cursor.downField("name").as[String] + description <- cursor.downField("description").as[String] + constantIndex <- cursor.downField("constantIndex").as[Int] + } yield new Parameter(name, description, constantIndex) + }) +} + +/** + * Represents a reusable ContractTemplate with support to generate ErgoTree based on provided parameters. + * + * @param treeVersion the optional version of ErgoTree which should be used. If this value is not provided here then + * it must be provided while generating the `ErgoTree` by calling `applyTemplate`. + * @param name user readable name (non-empty string bytes in UTF-8 encoding) + * @param description user readable contract description (string bytes in UTF-8 encoding) + * @param constTypes list denoting the type of ConstantPlaceholders in the expressionTree + * @param constValues optional list of optional default values for the ConstantPlaceholders in the expressionTree. + * If an entry in the sequence is None, it must have a corresponding entry in parameters and its + * value must be provided while generating the `ErgoTree` by calling `applyTemplate`. If all the + * entries are None, the whole `constValues` field can be set to None. + * @param parameters typed template parameters of the contract template. It must have an entry for each + * `ConstantPlaceholder` which has a `None` in the `constValues` field. Other fields which do have + * a value defined in `constValues` can also be allowed to be optionally overridden by accepting + * it in `parameters`. + * @param expressionTree root of the contract which is a valid expression of `SigmaProp` type. Must have constants + * segregated into `constTypes` and optionally `constValues` + */ +case class ContractTemplate( + treeVersion: Option[Byte], + name: String, + description: String, + constTypes: IndexedSeq[SType], + constValues: Option[IndexedSeq[Option[SType#WrappedType]]], + parameters: IndexedSeq[Parameter], + expressionTree: SigmaPropValue +) { + + validate() + + private def validate(): Unit = { + require(constValues.isEmpty || constValues.get.size == constTypes.size, + s"constValues must be empty or of same length as constTypes. Got ${constValues.get.size}, expected ${constTypes.size}") + require(parameters.size <= constTypes.size, "number of parameters must be <= number of constants") + + // Validate that no parameter is duplicated i.e. points to the same position & also to a valid constant. + // Also validate that no two parameters exist with the same name. + val paramNames = mutable.Set[String]() + val paramIndices = this.parameters.map(p => { + require(constTypes.isDefinedAt(p.constantIndex), + s"parameter constantIndex must be in range [0, ${constTypes.size})") + require(!paramNames.contains(p.name), + s"parameter names must be unique. Found duplicate parameters with name ${p.name}") + paramNames += p.name + p.constantIndex + }).toSet + require(paramIndices.size == parameters.size, "multiple parameters point to the same constantIndex") + + // Validate that any constValues[i] = None has a parameter. + if (constValues.isEmpty) { + require(parameters.size == constTypes.size, + "all the constants must be provided via parameter since constValues == None") + } else { + cfor(0)(_ < constTypes.size, _ + 1) { i => + require(constValues.get(i).isDefined || paramIndices.contains(i), + s"constantIndex $i does not have a default value and absent from parameter as well") + } + } + } + + /** + * Generate the ErgoTree from the template by providing the values for parameters. + * + * @param version the version of the `ErgoTree` to use. Must be provided if the `treeVersion` was not provided in the + * template. + * @param paramValues the name-value map for the parameters accepted by the `ContractTemplate`. Must contain an entry + * for each parameter for which no default value was provided in the template. Optionally, can also + * provide values to override for parameters which do have a default value defined in the template. + * The type of the provided value must match with the corresponding entry in the `constTypes` + * provided in the template. + * @return `ErgoTree` generated by replacing the template parameters with the value provided in `paramValues`. + */ + def applyTemplate(version: Option[Byte], paramValues: Map[String, Values.Constant[SType]]): Values.ErgoTree = { + require(treeVersion.isDefined || version.isDefined, "ErgoTreeVersion must be provided to generate the ErgoTree.") + val nConsts = constTypes.size + val requiredParameterNames = + if (constValues.isEmpty) + this.parameters.map(p => p.name) + else this.parameters + .filter(p => constValues.get(p.constantIndex).isEmpty) + .map(p => p.name) + requiredParameterNames.foreach(name => require( + paramValues.contains(name), + s"value for parameter $name was not provided while it does not have a default value.")) + + val parameterizedConstantIndices = this.parameters.map(p => p.constantIndex).toSet + val constIndexToParamIndex = this.parameters.zipWithIndex.map(pi => pi._1.constantIndex -> pi._2).toMap + val constants = safeNewArray[Constant[SType]](nConsts) + cfor(0)(_ < nConsts, _ + 1) { i => + // constants which are parameterized have to taken from the parameters when available. + if (parameterizedConstantIndices.contains(i) && paramValues.contains(parameters(constIndexToParamIndex(i)).name)) { + val paramValue = paramValues(parameters(constIndexToParamIndex(i)).name) + require(paramValue.tpe == constTypes(i), + s"parameter type mismatch, expected ${constTypes(i)}, got ${paramValue.tpe}") + constants(i) = StdSigmaBuilder.mkConstant(paramValue.value, constTypes(i)) + } else { + constants(i) = StdSigmaBuilder.mkConstant(constValues.get(i).get, constTypes(i)) + } + } + + val usedErgoTreeVersion = headerWithVersion(if (version.isDefined) version.get else treeVersion.get) + ErgoTree( + (ErgoTree.ConstantSegregationHeader | usedErgoTreeVersion).toByte, + constants, + this.expressionTree + ) + } + + override def hashCode(): Int = Objects.hash(treeVersion, name, description, constTypes, constValues, parameters, expressionTree) + + override def equals(obj: Any): Boolean = (this eq obj.asInstanceOf[AnyRef]) || + ((obj.asInstanceOf[AnyRef] != null) && (obj match { + case other: ContractTemplate => + other.treeVersion == treeVersion && other.name == name && other.description == description && other.constTypes == constTypes && other.constValues == constValues && other.parameters == parameters && other.expressionTree == expressionTree + case _ => false + })) +} + +object ContractTemplate { + def apply(name: String, + description: String, + constTypes: IndexedSeq[SType], + constValues: Option[IndexedSeq[Option[SType#WrappedType]]], + parameters: IndexedSeq[Parameter], + expressionTree: SigmaPropValue): ContractTemplate = { + new ContractTemplate(None, name, description, constTypes, constValues, parameters, expressionTree) + } + + object serializer extends SigmaSerializer[ContractTemplate, ContractTemplate] { + + override def serialize(data: ContractTemplate, w: SigmaByteWriter): Unit = { + w.putOption(data.treeVersion)(_.putUByte(_)) + serializeString(data.name, w) + serializeString(data.description, w) + + val nConstants = data.constTypes.length + w.putUInt(nConstants) + cfor(0)(_ < nConstants, _ + 1) { i => + TypeSerializer.serialize(data.constTypes(i), w) + } + w.putOption(data.constValues)((_, values) => { + cfor(0)(_ < nConstants, _ + 1) { i => + w.putOption(values(i))((_, const) => + DataSerializer.serialize(const, data.constTypes(i), w)) + } + }) + + val nParameters = data.parameters.length + w.putUInt(nParameters) + cfor(0)(_ < nParameters, _ + 1) { i => + Parameter.serializer.serialize(data.parameters(i), w) + } + + val expressionTreeWriter = SigmaSerializer.startWriter() + ValueSerializer.serialize(data.expressionTree, expressionTreeWriter) + val expressionBytes = expressionTreeWriter.toBytes + w.putUInt(expressionBytes.length) + w.putBytes(expressionBytes) + } + + override def parse(r: SigmaByteReader): ContractTemplate = { + val treeVersion = r.getOption(r.getUByte().toByte) + val name = parseString(r) + val description = parseString(r) + + val nConstants = r.getUInt().toInt + val constTypes: IndexedSeq[SType] = { + if (nConstants > 0) { + val res = safeNewArray[SType](nConstants) + cfor(0)(_ < nConstants, _ + 1) { i => + res(i) = TypeSerializer.deserialize(r) + } + res + } else { + SType.EmptySeq + } + } + val constValues: Option[IndexedSeq[Option[SType#WrappedType]]] = r.getOption { + if (nConstants > 0) { + val res = safeNewArray[Option[SType#WrappedType]](nConstants) + cfor(0)(_ < nConstants, _ + 1) { i => + res(i) = r.getOption((() => DataSerializer.deserialize(constTypes(i), r))()) + } + res + } else { + Array.empty[Option[SType#WrappedType]] + } + } + + val nParameters = r.getUInt().toInt + val parameters: IndexedSeq[Parameter] = { + if (nParameters > 0) { + val res = safeNewArray[Parameter](nParameters) + cfor(0)(_ < nParameters, _ + 1) { i => + res(i) = Parameter.serializer.parse(r) + } + res + } else { + Parameter.EmptySeq + } + } + + // Populate constants in constantStore so that the expressionTree can be deserialized. + val constants = constTypes.indices.map(i => { + val t = constTypes(i) + DeserializationSigmaBuilder.mkConstant(Zero.zeroOf(Zero.typeToZero(Evaluation.stypeToRType(t))), t) + }) + constants.foreach(c => r.constantStore.put(c)(DeserializationSigmaBuilder)) + + val _ = r.getUInt().toInt + val expressionTree = ValueSerializer.deserialize(r) + if (!expressionTree.tpe.isSigmaProp) { + throw SerializerException( + s"Failed deserialization, expected deserialized script to have type SigmaProp; got ${expressionTree.tpe}") + } + + ContractTemplate( + treeVersion, name, description, + constTypes, constValues, parameters, + expressionTree.toSigmaProp) + } + } + + object jsonEncoder extends JsonCodecs { + + /** Json encoder for SType */ + implicit val sTypeEncoder: Encoder[SType] = Encoder.instance({ tpe => + val w = SigmaSerializer.startWriter() + TypeSerializer.serialize(tpe, w) + w.toBytes.asJson + }) + + /** Json decoder for SType */ + implicit val sTypeDecoder: Decoder[SType] = Decoder.instance({ implicit cursor => + cursor.as[Array[Byte]] flatMap { bytes => + val r = SigmaSerializer.startReader(bytes) + fromThrows(TypeSerializer.deserialize(r)) + } + }) + + /** Json encoder for ContractTemplate */ + implicit val encoder: Encoder[ContractTemplate] = Encoder.instance({ ct => + val expressionTreeWriter = SigmaSerializer.startWriter() + ValueSerializer.serialize(ct.expressionTree, expressionTreeWriter) + + Json.obj( + "treeVersion" -> ct.treeVersion.asJson, + "name" -> Json.fromString(ct.name), + "description" -> Json.fromString(ct.description), + "constTypes" -> ct.constTypes.asJson, + "constValues" -> ( + if (ct.constValues.isEmpty) Json.Null + else ct.constValues.get.indices.map(i => ct.constValues.get(i) match { + case Some(const) => DataJsonEncoder.encodeData(const, ct.constTypes(i)) + case None => Json.Null + }).asJson), + "parameters" -> ct.parameters.asJson, + "expressionTree" -> expressionTreeWriter.toBytes.asJson + ) + }) + + /** Json decoder for ContractTemplate */ + implicit val decoder: Decoder[ContractTemplate] = Decoder.instance({ implicit cursor => + val constTypesResult = cursor.downField("constTypes").as[IndexedSeq[SType]] + val expressionTreeBytesResult = cursor.downField("expressionTree").as[Array[Byte]] + (constTypesResult, expressionTreeBytesResult) match { + case (Right(constTypes), Right(expressionTreeBytes)) => + val constValuesOpt = { + val constValuesJson = cursor.downField("constValues").focus.get + if (constValuesJson != Json.Null) { + val jsonValues = constValuesJson.asArray.get + Some(jsonValues.indices.map( + i => if (jsonValues(i) == Json.Null) None + else Some(DataJsonEncoder.decodeData(jsonValues(i), constTypes(i))))) + } else { + None + } + } + + // Populate synthetic constants in the constant store for deserialization of expression tree. + val r = SigmaSerializer.startReader(expressionTreeBytes) + val constants = constTypes.indices.map(i => { + val t = constTypes(i) + DeserializationSigmaBuilder.mkConstant(Zero.zeroOf(Zero.typeToZero(Evaluation.stypeToRType(t))), t) + }) + constants.foreach(c => r.constantStore.put(c)(DeserializationSigmaBuilder)) + + for { + treeVersion <- cursor.downField("treeVersion").as[Option[Byte]] + name <- cursor.downField("name").as[String] + description <- cursor.downField("description").as[String] + parameters <- cursor.downField("parameters").as[IndexedSeq[Parameter]] + } yield new ContractTemplate( + treeVersion, + name, + description, + constTypes, + constValuesOpt, + parameters, + ValueSerializer.deserialize(r).toSigmaProp) + case _ => Left(DecodingFailure("Failed to decode contract template", cursor.history)) + } + }) + } +} diff --git a/sdk/shared/src/main/scala/org/ergoplatform/sdk/DataJsonEncoder.scala b/sdk/shared/src/main/scala/org/ergoplatform/sdk/DataJsonEncoder.scala index e81faeb889..35c80b1433 100644 --- a/sdk/shared/src/main/scala/org/ergoplatform/sdk/DataJsonEncoder.scala +++ b/sdk/shared/src/main/scala/org/ergoplatform/sdk/DataJsonEncoder.scala @@ -46,7 +46,7 @@ object DataJsonEncoder { ) } - private def encodeData[T <: SType](v: T#WrappedType, tpe: T): Json = tpe match { + def encodeData[T <: SType](v: T#WrappedType, tpe: T): Json = tpe match { case SUnit => Json.fromFields(ArraySeq.empty) case SBoolean => v.asInstanceOf[Boolean].asJson case SByte => v.asInstanceOf[Byte].asJson @@ -159,7 +159,7 @@ object DataJsonEncoder { } } - private def decodeData[T <: SType](json: Json, tpe: T): (T#WrappedType) = { + def decodeData[T <: SType](json: Json, tpe: T): (T#WrappedType) = { val res = (tpe match { case SUnit => () case SBoolean => json.asBoolean.get diff --git a/sdk/shared/src/main/scala/org/ergoplatform/sdk/utils/SerializationUtils.scala b/sdk/shared/src/main/scala/org/ergoplatform/sdk/utils/SerializationUtils.scala new file mode 100644 index 0000000000..1530e1876a --- /dev/null +++ b/sdk/shared/src/main/scala/org/ergoplatform/sdk/utils/SerializationUtils.scala @@ -0,0 +1,30 @@ +package org.ergoplatform.sdk.utils + +import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} + +import java.nio.charset.StandardCharsets + +object SerializationUtils { + + /** + * Serialize the string as UTF_8. + * + * @param s the string to serialize + * @param w the writer to which the serialized string will be appended + */ + def serializeString(s: String, w: SigmaByteWriter): Unit = { + val bytes = s.getBytes(StandardCharsets.UTF_8) + w.putUInt(bytes.length) + w.putBytes(bytes) + } + + /** + * Parse a string from the reader + * @param r the reader from which the string will be parsed + * @return the parsed string + */ + def parseString(r: SigmaByteReader): String = { + val length = r.getUInt().toInt + new String(r.getBytes(length), StandardCharsets.UTF_8) + } +} diff --git a/sdk/shared/src/main/scala/org/ergoplatform/sdk/utils/Zero.scala b/sdk/shared/src/main/scala/org/ergoplatform/sdk/utils/Zero.scala new file mode 100644 index 0000000000..e899319231 --- /dev/null +++ b/sdk/shared/src/main/scala/org/ergoplatform/sdk/utils/Zero.scala @@ -0,0 +1,108 @@ +package org.ergoplatform.sdk.utils + +import org.ergoplatform.ErgoBox +import scalan.RType +import scalan.RType.{BooleanType, ByteType, FuncType, IntType, LongType, OptionType, PairType, ShortType, UnitType} +import scorex.crypto.authds.avltree.batch.BatchAVLProver +import scorex.crypto.hash.{Blake2b256, Digest32} +import scorex.util.ModifierId +import sigmastate.{AvlTreeData, AvlTreeFlags, TrivialProp} +import sigmastate.Values.ErgoTree +import sigmastate.basics.CryptoConstants +import sigmastate.eval.{CAvlTree, CBigInt, CGroupElement, CHeader, CPreHeader, CSigmaProp, Colls, CostingBox, CostingDataContext} +import special.Types.TupleType +import special.collection.{Coll, CollType} +import special.sigma.{AvlTree, AvlTreeRType, BigInt, BigIntRType, Box, BoxRType, Context, ContextRType, GroupElement, GroupElementRType, Header, HeaderRType, PreHeader, PreHeaderRType, SigmaDslBuilder, SigmaDslBuilderRType, SigmaProp, SigmaPropRType} + +import java.math.BigInteger +import scalan.RType._ +import sigmastate.eval._ +import special.sigma._ +import scala.language.implicitConversions + +/** + * A trait representing the zero value of each type in the ErgoTree. + * @tparam T The type of the zero value. + */ +trait Zero[T] { + /** Get the underlying zero value. */ + def zero: T +} + +/** + * A wrapper over the zero value of a type. + * @param zero the zero value of the type T. + * @tparam T The type of the zero value. + */ +case class CZero[T](zero: T) extends Zero[T] + +/** A trait providing implicit conversions to create instances of Zero for various types. */ +trait ZeroLowPriority { + implicit def collIsZero[T: Zero: RType]: Zero[Coll[T]] = CZero(Colls.emptyColl[T]) + implicit def optionIsZero[T: Zero]: Zero[Option[T]] = CZero(None) + implicit def pairIsZero[A: Zero, B: Zero]: Zero[(A,B)] = CZero(Zero[A].zero, Zero[B].zero) + implicit def funcIsZero[A, B: Zero]: Zero[A =>B] = CZero((_ : A) => { Zero[B].zero }) +} + +object Zero extends ZeroLowPriority { + def apply[T](implicit z: Zero[T]): Zero[T] = z + def zeroOf[T: Zero]: T = Zero[T].zero + + implicit val BooleanIsZero: Zero[Boolean] = CZero(false) + implicit val ByteIsZero: Zero[Byte] = CZero(0.toByte) + implicit val ShortIsZero: Zero[Short] = CZero(0.toShort) + implicit val IntIsZero: Zero[Int] = CZero(0) + implicit val LongIsZero: Zero[Long] = CZero(0L) + implicit val BigIntIsZero: Zero[BigInt] = CZero(CBigInt(BigInteger.ZERO)) + implicit val GroupElementIsZero: Zero[GroupElement] = CZero(CGroupElement(CryptoConstants.dlogGroup.identity)) + implicit val AvlTreeIsZero: Zero[AvlTree] = CZero({ + val avlProver = new BatchAVLProver[Digest32, Blake2b256.type](keyLength = 32, None) + val digest = avlProver.digest + val treeData = new AvlTreeData(digest, AvlTreeFlags.AllOperationsAllowed, 32, None) + CAvlTree(treeData) + }) + implicit val sigmaPropIsZero: Zero[SigmaProp] = CZero(CSigmaProp(TrivialProp.FalseProp)) + implicit val UnitIsZero: Zero[Unit] = CZero(()) + implicit val BoxIsZero: Zero[Box] = CZero({ + new ErgoBox( + LongIsZero.zero, + new ErgoTree( + ByteIsZero.zero, + IndexedSeq.empty, + Right(sigmaPropIsZero.zero) + ), + Colls.emptyColl, + Map.empty, + ModifierId @@ ("synthetic_transaction_id"), + ShortIsZero.zero, + IntIsZero.zero + ) + }) + + /** + * Returns the zero value of the specified type `T` using the provided runtime type t. + * @param t the runtime type of the value whose zero value is to be returned. + * @tparam T the type of value whose zero value is to be returned. + * @return the zero value of type `T` + * @throws `RuntimeException` if the method is unable to compute the zero value for the specified type + */ + def typeToZero[T](t: RType[T]): Zero[T] = (t match { + case BooleanType => Zero[Boolean] + case ByteType => Zero[Byte] + case ShortType => Zero[Short] + case IntType => Zero[Int] + case LongType => Zero[Long] + case UnitType => Zero[Unit] + case BigIntRType => Zero[BigInt] + case BoxRType => Zero[Box] + case GroupElementRType => Zero[GroupElement] + 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)) + case ct: PairType[a, b] => pairIsZero(typeToZero(ct.tFst), typeToZero(ct.tSnd)) + case tt: TupleType => CZero(tt.emptyArray) + case ft: FuncType[a, b] => funcIsZero(typeToZero(ft.tRange)) + case _ => sys.error(s"Don't know how to compute Zero for type $t") + }).asInstanceOf[Zero[T]] +} diff --git a/sdk/shared/src/test/scala/org/ergoplatform/sdk/ContractTemplateSpecification.scala b/sdk/shared/src/test/scala/org/ergoplatform/sdk/ContractTemplateSpecification.scala new file mode 100644 index 0000000000..c660f44e02 --- /dev/null +++ b/sdk/shared/src/test/scala/org/ergoplatform/sdk/ContractTemplateSpecification.scala @@ -0,0 +1,286 @@ +package org.ergoplatform.sdk + +import org.ergoplatform.sdk.generators.ObjectGenerators +import org.scalatest.compatible.Assertion +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import sigmastate.Values._ +import sigmastate._ +import sigmastate.eval.CBigInt +import sigmastate.helpers.NegativeTesting +import sigmastate.serialization.{SerializationSpecification, SigmaSerializer} +import special.sigma.ContractsTestkit + +import java.math.BigInteger + +class ContractTemplateSpecification extends SerializationSpecification + with ScalaCheckPropertyChecks + with ContractsTestkit + with NegativeTesting + with CrossVersionProps + with ObjectGenerators { + object JsonCodecs extends JsonCodecs + + private def jsonRoundTrip[T <: SType](obj: ContractTemplate) = { + val json = ContractTemplate.jsonEncoder.encoder(obj) + val res = ContractTemplate.jsonEncoder.decoder(json.hcursor).right.get + res shouldBe obj + val json2 = ContractTemplate.jsonEncoder.encoder(res) + json shouldBe json2 + } + + private def serializationRoundTrip(template: ContractTemplate): Assertion = { + val w = SigmaSerializer.startWriter() + ContractTemplate.serializer.serialize(template, w) + val bytes = w.toBytes + val r = SigmaSerializer.startReader(bytes) + val res2 = ContractTemplate.serializer.parse(r) + res2 shouldEqual template + + val w2 = SigmaSerializer.startWriter() + ContractTemplate.serializer.serialize(res2, w2) + bytes shouldEqual w2.toBytes + } + + private def createParameter(name: String, constantIndex: Int): Parameter = { + Parameter( + name, + s"${name}_description", + constantIndex + ) + } + + private def createContractTemplate(constTypes: IndexedSeq[SType], + constValues: Option[IndexedSeq[Option[SType#WrappedType]]], + parameters: IndexedSeq[Parameter], + expressionTree: SigmaPropValue): ContractTemplate = { + ContractTemplate( + contractTemplateNameInTests, + contractTemplateDescriptionInTests, + constTypes, + constValues, + parameters, + expressionTree + ) + } + + property("unequal length of constTypes and constValues") { + assertExceptionThrown( + createContractTemplate( + IndexedSeq(SType.typeByte, SType.typeByte, SType.typeByte).asInstanceOf[IndexedSeq[SType]], + Some(IndexedSeq(Some(10.toByte), Some(20.toByte)).asInstanceOf[IndexedSeq[Option[SType#WrappedType]]]), + IndexedSeq( + createParameter("p1", 0), + createParameter("p2", 1), + createParameter("p3", 2)), + EQ(Plus(ConstantPlaceholder(0, SType.typeByte), + ConstantPlaceholder(1, SType.typeByte)), + ConstantPlaceholder(2, SType.typeByte)).toSigmaProp + ), + exceptionLike[IllegalArgumentException]("constValues must be empty or of same length as constTypes. Got 2, expected 3") + ) + } + + property("more parameters than constants") { + assertExceptionThrown( + createContractTemplate( + IndexedSeq(SType.typeByte, SType.typeByte, SType.typeByte).asInstanceOf[IndexedSeq[SType]], + Some(IndexedSeq(Some(10.toByte), Some(20.toByte), Some(30.toByte)).asInstanceOf[IndexedSeq[Option[SType#WrappedType]]]), + IndexedSeq( + createParameter("p1", 0), + createParameter("p2", 1), + createParameter("p3", 2), + createParameter("p4", 3)), + EQ(Plus(ConstantPlaceholder(0, SType.typeByte), + ConstantPlaceholder(1, SType.typeByte)), + ConstantPlaceholder(2, SType.typeByte)).toSigmaProp + ), + exceptionLike[IllegalArgumentException]("number of parameters must be <= number of constants") + ) + } + + property("invalid parameter constantIndex") { + assertExceptionThrown( + createContractTemplate( + IndexedSeq(SType.typeByte, SType.typeByte, SType.typeByte).asInstanceOf[IndexedSeq[SType]], + Some(IndexedSeq(Some(10.toByte), Some(20.toByte), Some(30.toByte)).asInstanceOf[IndexedSeq[Option[SType#WrappedType]]]), + IndexedSeq( + createParameter("p1", 0), + createParameter("p2", 1), + createParameter("p3", 100)), + EQ(Plus(ConstantPlaceholder(0, SType.typeByte), + ConstantPlaceholder(1, SType.typeByte)), + ConstantPlaceholder(2, SType.typeByte)).toSigmaProp + ), + exceptionLike[IllegalArgumentException]("parameter constantIndex must be in range [0, 3)") + ) + } + + property("duplicate parameter constantIndex") { + assertExceptionThrown( + createContractTemplate( + IndexedSeq(SType.typeByte, SType.typeByte, SType.typeByte).asInstanceOf[IndexedSeq[SType]], + Some(IndexedSeq(Some(10.toByte), Some(20.toByte), Some(30.toByte)).asInstanceOf[IndexedSeq[Option[SType#WrappedType]]]), + IndexedSeq( + createParameter("p1", 0), + createParameter("p2", 1), + createParameter("p3", 1)), + EQ(Plus(ConstantPlaceholder(0, SType.typeByte), + ConstantPlaceholder(1, SType.typeByte)), + ConstantPlaceholder(2, SType.typeByte)).toSigmaProp + ), + exceptionLike[IllegalArgumentException]("multiple parameters point to the same constantIndex") + ) + } + + property("duplicate parameter names") { + assertExceptionThrown( + createContractTemplate( + IndexedSeq(SType.typeByte, SType.typeByte, SType.typeByte).asInstanceOf[IndexedSeq[SType]], + Some(IndexedSeq(Some(10.toByte), Some(20.toByte), Some(30.toByte)).asInstanceOf[IndexedSeq[Option[SType#WrappedType]]]), + IndexedSeq( + createParameter("duplicate_name", 0), + createParameter("p2", 1), + createParameter("duplicate_name", 2)), + EQ(Plus(ConstantPlaceholder(0, SType.typeByte), + ConstantPlaceholder(1, SType.typeByte)), + ConstantPlaceholder(2, SType.typeByte)).toSigmaProp + ), + exceptionLike[IllegalArgumentException]("parameter names must be unique. Found duplicate parameters with name duplicate_name") + ) + } + + property("constantIndex without default value and parameter") { + assertExceptionThrown( + createContractTemplate( + IndexedSeq(SType.typeByte, SType.typeByte, SType.typeByte).asInstanceOf[IndexedSeq[SType]], + Some(IndexedSeq(None, Some(20.toByte), Some(30.toByte)).asInstanceOf[IndexedSeq[Option[SType#WrappedType]]]), + IndexedSeq( + createParameter("p2", 1), + createParameter("p3", 2)), + EQ(Plus(ConstantPlaceholder(0, SType.typeByte), + ConstantPlaceholder(1, SType.typeByte)), + ConstantPlaceholder(2, SType.typeByte)).toSigmaProp + ), + exceptionLike[IllegalArgumentException]("constantIndex 0 does not have a default value and absent from parameter as well") + ) + } + + property("applyTemplate") { + val parameters = IndexedSeq( + createParameter("p1", 0), + createParameter("p2", 1), + createParameter("p3", 2)) + val expressionTrees = IndexedSeq( + EQ(Plus(ConstantPlaceholder(0, SType.typeByte), + ConstantPlaceholder(1, SType.typeByte)), + ConstantPlaceholder(2, SType.typeByte)).toSigmaProp, + EQ(Plus(ConstantPlaceholder(0, SType.typeInt), + ConstantPlaceholder(1, SType.typeInt)), + ConstantPlaceholder(2, SType.typeInt)).toSigmaProp, + EQ(Plus(CBigInt(BigInteger.valueOf(10L)), BigIntConstant(20L)), BigIntConstant(30L)).toSigmaProp + ) + val templates = Seq( + createContractTemplate( + IndexedSeq(SType.typeByte, SType.typeByte, SType.typeByte).asInstanceOf[IndexedSeq[SType]], + Some(IndexedSeq(Some(10.toByte), Some(20.toByte), Some(30.toByte)).asInstanceOf[IndexedSeq[Option[SType#WrappedType]]]), + parameters, + expressionTrees(0) + ), + createContractTemplate( + IndexedSeq(SType.typeInt, SType.typeInt, SType.typeInt).asInstanceOf[IndexedSeq[SType]], + Some(IndexedSeq(Some(10), None, Some(30)).asInstanceOf[IndexedSeq[Option[SType#WrappedType]]]), + parameters, + expressionTrees(1) + ), + createContractTemplate( + SType.EmptySeq, + None, + Parameter.EmptySeq, + expressionTrees(2) + ) + ) + val templateValues = Seq( + Map("p1" -> ByteConstant(10.toByte), "p2" -> ByteConstant(40.toByte), "p3" -> ByteConstant(50.toByte)), + Map("p1" -> IntConstant(10), "p2" -> IntConstant(20)), + Map.empty[String, Constant[SType]] + ) + var expectedErgoTreeVersion = (ErgoTree.ConstantSegregationHeader | ergoTreeVersionInTests).toByte + if (ergoTreeVersionInTests > 0) { + expectedErgoTreeVersion = (expectedErgoTreeVersion | ErgoTree.SizeFlag).toByte + } + val expectedErgoTree = Seq( + ErgoTree( + expectedErgoTreeVersion, + IndexedSeq( + ByteConstant(10.toByte), + ByteConstant(40.toByte), + ByteConstant(50.toByte) + ), + expressionTrees(0) + ), + ErgoTree( + expectedErgoTreeVersion, + IndexedSeq( + IntConstant(10), + IntConstant(20), + IntConstant(30) + ), + expressionTrees(1) + ), + ErgoTree( + expectedErgoTreeVersion, + Constant.EmptySeq, + expressionTrees(2) + ) + ) + + templates.indices.foreach(i => + templates(i).applyTemplate(Some(ergoTreeVersionInTests), templateValues(i)) shouldEqual expectedErgoTree(i) + ) + } + + property("applyTemplate num(parameters) < num(constants)") { + val parameters = IndexedSeq( + createParameter("p1", 0), + createParameter("p2", 2)) + val expressionTree = + EQ(Plus(ConstantPlaceholder(0, SType.typeInt), + ConstantPlaceholder(1, SType.typeInt)), + ConstantPlaceholder(2, SType.typeInt)).toSigmaProp + val template = createContractTemplate( + IndexedSeq(SType.typeInt, SType.typeInt, SType.typeInt).asInstanceOf[IndexedSeq[SType]], + Some(IndexedSeq(None, Some(20), None).asInstanceOf[IndexedSeq[Option[SType#WrappedType]]]), + parameters, + expressionTree + ) + val templateValues = Map("p1" -> IntConstant(10), "p2" -> IntConstant(30)) + + var expectedErgoTreeVersion = (ErgoTree.ConstantSegregationHeader | ergoTreeVersionInTests).toByte + if (ergoTreeVersionInTests > 0) { + expectedErgoTreeVersion = (expectedErgoTreeVersion | ErgoTree.SizeFlag).toByte + } + val expectedErgoTree = ErgoTree( + expectedErgoTreeVersion, + IndexedSeq( + IntConstant(10), + IntConstant(20), + IntConstant(30) + ), + expressionTree + ) + + template.applyTemplate(Some(ergoTreeVersionInTests), templateValues) shouldEqual expectedErgoTree + } + + property("(de)serialization round trip") { + forAll(contractTemplateGen, minSuccessful(500)) { template => + serializationRoundTrip(template) + } + } + + property("Data Json serialization round trip") { + forAll(contractTemplateGen, minSuccessful(500)) { template => + jsonRoundTrip(template) + } + } +} diff --git a/sdk/shared/src/test/scala/org/ergoplatform/sdk/generators/ObjectGenerators.scala b/sdk/shared/src/test/scala/org/ergoplatform/sdk/generators/ObjectGenerators.scala new file mode 100644 index 0000000000..3908760e59 --- /dev/null +++ b/sdk/shared/src/test/scala/org/ergoplatform/sdk/generators/ObjectGenerators.scala @@ -0,0 +1,60 @@ +package org.ergoplatform.sdk.generators + +import org.ergoplatform.sdk.{ContractTemplate, Parameter} +import org.ergoplatform.validation.ValidationSpecification +import org.scalacheck.Gen +import sigmastate.Values.{ErgoTree, SigmaPropValue} +import sigmastate.serialization.generators.{ConcreteCollectionGenerators, TypeGenerators, ObjectGenerators => InterpreterObjectGenerators} +import sigmastate.{SType, TestsBase} + +import scala.util.Random + +trait ObjectGenerators extends TypeGenerators + with ValidationSpecification + with ConcreteCollectionGenerators + with TestsBase + with InterpreterObjectGenerators { + + def contractTemplateNameInTests: String = "TestContractTemplate" + + def contractTemplateDescriptionInTests: String = "TestContractTemplateDescription" + + private def getConstValues(ergoTree: ErgoTree, + noDefaultValueIndices: Set[Int]): + Option[IndexedSeq[Option[SType#WrappedType]]] = { + val values = ergoTree + .constants + .zipWithIndex + .map(c_i => if (noDefaultValueIndices.contains(c_i._2)) None else Some(c_i._1.value)) + val allNone = values.forall(v => v.isEmpty) + if (allNone) { + None + } else { + Some(values) + } + } + + private def getConstAndParams(ergoTree: ErgoTree): (IndexedSeq[SType], Option[IndexedSeq[Option[SType#WrappedType]]], IndexedSeq[Parameter]) = { + val paramIndices = ergoTree.constants.indices.filter(_ => (Random.nextDouble() < 0.5)).toSet + val noDefaultValueIndices = paramIndices.filter(_ => (Random.nextDouble() < 0.5)) + val constTypes = ergoTree.constants.map(c => c.tpe) + val constValues = getConstValues(ergoTree, noDefaultValueIndices) + val parameters = paramIndices + .map(i => Parameter(s"param_for_idx$i", s"description_for_idx$i", i)).toIndexedSeq + (constTypes, constValues, parameters) + } + + lazy val contractTemplateGen: Gen[ContractTemplate] = for { + ergoTree <- Gen.delay(ergoTreeWithSegregationGen) + } yield { + val (constTypes, constValues, parameters) = getConstAndParams(ergoTree) + ContractTemplate( + contractTemplateNameInTests, + contractTemplateDescriptionInTests, + constTypes, + constValues, + parameters, + ergoTree.toProposition(false) + ) + } +}