diff --git a/.gitignore b/.gitignore index a3d1c520e6..3192ecc461 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,5 @@ spam-tests/ .metals/ .vscode/ project/metals.sbt -/project/project/ \ No newline at end of file +/project/project/ +.bsp/ \ No newline at end of file diff --git a/README.md b/README.md index 6ecc4ac99b..64f08f0277 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ be verified before the spending transaction is added to the blockchain. To allow valid coin spending a [verifier](sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala) is running the ErgoTree interpreter with the following three inputs: -- a quarding proposition given by an ErgoTree +- a guarding proposition given by an ErgoTree - a blockchain _context_ of the transaction being verified - a _proof_ (aka transaction signature) generated by a _prover_ diff --git a/build.sbt b/build.sbt index 269ad14997..9d95ffa3a2 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ organization := "org.scorexfoundation" name := "sigma-state" -lazy val scala213 = "2.13.8" +lazy val scala213 = "2.13.9" lazy val scala212 = "2.12.15" lazy val scala211 = "2.11.12" @@ -257,9 +257,28 @@ lazy val interpreterJS = interpreter.js ) ) +lazy val parsers = crossProject(JVMPlatform, JSPlatform) + .in(file("parsers")) + .dependsOn(interpreter % allConfigDependency) + .settings(libraryDefSettings) + .settings(libraryDependencies ++= + Seq(scorexUtil, fastparse) ++ circeDeps(scalaVersion.value) + ) + .jvmSettings( + crossScalaSettings + ) + .jsSettings( + crossScalaSettingsJS, + libraryDependencies ++= Seq( + "org.scala-js" %%% "scala-js-macrotask-executor" % "1.0.0" + ), + useYarn := true + ) +lazy val parsersJS = parsers.js + .enablePlugins(ScalaJSBundlerPlugin) lazy val sc = (project in file("sc")) - .dependsOn(graphir % allConfigDependency, interpreter.jvm % allConfigDependency) + .dependsOn(graphir % allConfigDependency, interpreter.jvm % allConfigDependency, parsers.jvm % allConfigDependency) .settings(libraryDefSettings) .settings(libraryDependencies ++= Seq(scorexUtil, fastparse) ++ circeDeps(scalaVersion.value) @@ -268,7 +287,7 @@ lazy val sc = (project in file("sc")) lazy val sdk = crossProject(JVMPlatform, JSPlatform) .in(file("sdk")) - .dependsOn(corelib % allConfigDependency, interpreter % allConfigDependency) + .dependsOn(corelib % allConfigDependency, interpreter % allConfigDependency, parsers % allConfigDependency) .settings(commonSettings ++ testSettings2, commonDependenies2, testingDependencies2, @@ -298,13 +317,13 @@ lazy val sdkJS = sdk.js ) lazy val sigma = (project in file(".")) - .aggregate(common.jvm, corelib.jvm, graphir, interpreter.jvm, sc, sdk.jvm) + .aggregate(common.jvm, corelib.jvm, graphir, interpreter.jvm, parsers.jvm, sc, sdk.jvm) .settings(libraryDefSettings, rootSettings) .settings(publish / aggregate := false) .settings(publishLocal / aggregate := false) lazy val aggregateCompile = ScopeFilter( - inProjects(common.jvm, corelib.jvm, graphir, interpreter.jvm, sc, sdk.jvm), + inProjects(common.jvm, corelib.jvm, graphir, interpreter.jvm, parsers.jvm, sc, sdk.jvm), inConfigurations(Compile)) lazy val rootSettings = Seq( diff --git a/docs/LangSpec.md b/docs/LangSpec.md index 77f03e7023..6aae2a9880 100644 --- a/docs/LangSpec.md +++ b/docs/LangSpec.md @@ -643,6 +643,10 @@ class Option[A] { /** Returns true if the option is an instance of Some(value), false otherwise. */ def isDefined: Boolean; + + /** Returns true if the option is None, false otherwise. + */ + def isEmpty: Boolean; /** Returns the option's value if the option is nonempty, otherwise * return the result of evaluating `default`. 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/sc/src/main/scala/sigmastate/lang/SigmaParser.scala b/parsers/shared/src/main/scala/sigmastate/lang/SigmaParser.scala similarity index 100% rename from sc/src/main/scala/sigmastate/lang/SigmaParser.scala rename to parsers/shared/src/main/scala/sigmastate/lang/SigmaParser.scala diff --git a/sc/src/main/scala/sigmastate/lang/Types.scala b/parsers/shared/src/main/scala/sigmastate/lang/Types.scala similarity index 100% rename from sc/src/main/scala/sigmastate/lang/Types.scala rename to parsers/shared/src/main/scala/sigmastate/lang/Types.scala diff --git a/sc/src/main/scala/sigmastate/lang/syntax/Basic.scala b/parsers/shared/src/main/scala/sigmastate/lang/syntax/Basic.scala similarity index 100% rename from sc/src/main/scala/sigmastate/lang/syntax/Basic.scala rename to parsers/shared/src/main/scala/sigmastate/lang/syntax/Basic.scala diff --git a/sc/src/main/scala/sigmastate/lang/syntax/Core.scala b/parsers/shared/src/main/scala/sigmastate/lang/syntax/Core.scala similarity index 100% rename from sc/src/main/scala/sigmastate/lang/syntax/Core.scala rename to parsers/shared/src/main/scala/sigmastate/lang/syntax/Core.scala diff --git a/sc/src/main/scala/sigmastate/lang/syntax/Exprs.scala b/parsers/shared/src/main/scala/sigmastate/lang/syntax/Exprs.scala similarity index 100% rename from sc/src/main/scala/sigmastate/lang/syntax/Exprs.scala rename to parsers/shared/src/main/scala/sigmastate/lang/syntax/Exprs.scala diff --git a/sc/src/main/scala/sigmastate/lang/syntax/Identifiers.scala b/parsers/shared/src/main/scala/sigmastate/lang/syntax/Identifiers.scala similarity index 100% rename from sc/src/main/scala/sigmastate/lang/syntax/Identifiers.scala rename to parsers/shared/src/main/scala/sigmastate/lang/syntax/Identifiers.scala diff --git a/sc/src/main/scala/sigmastate/lang/syntax/Literals.scala b/parsers/shared/src/main/scala/sigmastate/lang/syntax/Literals.scala similarity index 100% rename from sc/src/main/scala/sigmastate/lang/syntax/Literals.scala rename to parsers/shared/src/main/scala/sigmastate/lang/syntax/Literals.scala diff --git a/sc/src/test/scala/sigmastate/lang/LangTests.scala b/parsers/shared/src/test/scala/sigmastate/lang/LangTests.scala similarity index 100% rename from sc/src/test/scala/sigmastate/lang/LangTests.scala rename to parsers/shared/src/test/scala/sigmastate/lang/LangTests.scala diff --git a/sc/src/test/scala/sigmastate/lang/SigmaParserTest.scala b/parsers/shared/src/test/scala/sigmastate/lang/SigmaParserTest.scala similarity index 98% rename from sc/src/test/scala/sigmastate/lang/SigmaParserTest.scala rename to parsers/shared/src/test/scala/sigmastate/lang/SigmaParserTest.scala index 3bc64cf0cf..e28cd71473 100644 --- a/sc/src/test/scala/sigmastate/lang/SigmaParserTest.scala +++ b/parsers/shared/src/test/scala/sigmastate/lang/SigmaParserTest.scala @@ -32,13 +32,20 @@ class SigmaParserTest extends AnyPropSpec with ScalaCheckPropertyChecks with Mat } } + def parseWithException(x: String): SValue = { + SigmaParser(x) match { + case Parsed.Success(v, _) => v + case f: Parsed.Failure => + throw new ParserException(s"Syntax error: $f", Some(SourceContext.fromParserFailure(f))) + } + } + def parseType(x: String): SType = { SigmaParser.parseType(x) } def fail(x: String, expectedLine: Int, expectedCol: Int): Unit = { - val compiler = new SigmaCompiler(ErgoAddressEncoder.TestnetNetworkPrefix) - val exception = the[ParserException] thrownBy compiler.parse(x) + val exception = the[ParserException] thrownBy parseWithException(x) withClue(s"Exception: $exception, is missing source context:") { exception.source shouldBe defined } val sourceContext = exception.source.get sourceContext.line shouldBe expectedLine @@ -539,11 +546,13 @@ class SigmaParserTest extends AnyPropSpec with ScalaCheckPropertyChecks with Mat parse("X[Byte]") shouldBe ApplyTypes(Ident("X"), Seq(SByte)) parse("X[Int]") shouldBe ApplyTypes(Ident("X"), Seq(SInt)) parse("X[Int].isDefined") shouldBe Select(ApplyTypes(Ident("X"), Seq(SInt)), "isDefined") + parse("X[Int].isEmpty") shouldBe Select(ApplyTypes(Ident("X"), Seq(SInt)), "isEmpty") parse("X[(Int, Boolean)]") shouldBe ApplyTypes(Ident("X"), Seq(STuple(SInt, SBoolean))) parse("X[Int, Boolean]") shouldBe ApplyTypes(Ident("X"), Seq(SInt, SBoolean)) parse("SELF.R1[Int]") shouldBe ApplyTypes(Select(Ident("SELF"), "R1"), Seq(SInt)) parse("SELF.getReg[Int](1)") shouldBe Apply(ApplyTypes(Select(Ident("SELF"), "getReg"), Seq(SInt)), IndexedSeq(IntConstant(1))) parse("SELF.R1[Int].isDefined") shouldBe Select(ApplyTypes(Select(Ident("SELF"), "R1"), Seq(SInt)),"isDefined") + parse("SELF.R1[Int].isEmpty") shouldBe Select(ApplyTypes(Select(Ident("SELF"), "R1"), Seq(SInt)),"isEmpty") parse("f[Int](10)") shouldBe Apply(ApplyTypes(Ident("f"), Seq(SInt)), IndexedSeq(IntConstant(10))) parse("INPUTS.map[Int]") shouldBe ApplyTypes(Select(Ident("INPUTS"), "map"), Seq(SInt)) parse("INPUTS.map[Int](10)") shouldBe Apply(ApplyTypes(Select(Ident("INPUTS"), "map"), Seq(SInt)), IndexedSeq(IntConstant(10))) diff --git a/sc/src/main/scala/sigmastate/lang/SigmaBinder.scala b/sc/src/main/scala/sigmastate/lang/SigmaBinder.scala index 7ca4f4db73..07616af0c4 100644 --- a/sc/src/main/scala/sigmastate/lang/SigmaBinder.scala +++ b/sc/src/main/scala/sigmastate/lang/SigmaBinder.scala @@ -112,6 +112,9 @@ class SigmaBinder(env: ScriptEnv, builder: SigmaBuilder, case a @ Apply(PKFunc.symNoType, args) => Some(PKFunc.irInfo.irBuilder(PKFunc.sym, args).withPropagatedSrcCtx(a.sourceContext)) + case sel @ Select(obj, "isEmpty", _) => + Some(mkLogicalNot(mkSelect(obj, "isDefined").asBoolValue).withPropagatedSrcCtx(sel.sourceContext)) + })))(e) def bind(e: SValue): SValue = diff --git a/sc/src/test/scala/sigmastate/eval/EvaluationTest.scala b/sc/src/test/scala/sigmastate/eval/EvaluationTest.scala index fa87615de7..4ef470d172 100644 --- a/sc/src/test/scala/sigmastate/eval/EvaluationTest.scala +++ b/sc/src/test/scala/sigmastate/eval/EvaluationTest.scala @@ -42,11 +42,14 @@ class EvaluationTest extends BaseCtxTests val ctx = newErgoContext(height = 1, self) // guarded register access: existing reg reduce(emptyEnv, "lazy1", "SELF.R4[Int].isDefined && SELF.R4[Int].get == 10", ctx, true) + reduce(emptyEnv, "lazy4", "SELF.R4[Int].isEmpty == false && SELF.R4[Int].get == 10", ctx, true) // guarded register access: non-existing reg reduce(emptyEnv, "lazy2", "SELF.R5[Int].isDefined && SELF.R5[Int].get == 10", ctx, false) + reduce(emptyEnv, "lazy5", "SELF.R5[Int].isEmpty == false && SELF.R5[Int].get == 10", ctx, false) // guarded register access: reading register if it is defined and another one is undefined reduce(emptyEnv, "lazy3", "SELF.R4[Int].isDefined && (SELF.R5[Int].isDefined || SELF.R4[Int].get == 10)", ctx, true) + reduce(emptyEnv, "lazy6", "SELF.R4[Int].isEmpty == false && (SELF.R5[Int].isEmpty == false || SELF.R4[Int].get == 10)", ctx, true) } test("context data") { diff --git a/sc/src/test/scala/sigmastate/lang/SigmaBinderTest.scala b/sc/src/test/scala/sigmastate/lang/SigmaBinderTest.scala index de52c93fc3..b8aefc1041 100644 --- a/sc/src/test/scala/sigmastate/lang/SigmaBinderTest.scala +++ b/sc/src/test/scala/sigmastate/lang/SigmaBinderTest.scala @@ -191,7 +191,7 @@ class SigmaBinderTest extends AnyPropSpec with ScalaCheckPropertyChecks with Mat bind(env, "X[(Int, Boolean)]") shouldBe ApplyTypes(Ident("X"), Seq(STuple(SInt, SBoolean))) bind(env, "X[Int, Boolean]") shouldBe ApplyTypes(Ident("X"), Seq(SInt, SBoolean)) bind(env, "SELF.R1[Int]") shouldBe ApplyTypes(Select(Self, "R1"), Seq(SInt)) - bind(env, "SELF.R1[Int].isEmpty") shouldBe Select(ApplyTypes(Select(Self, "R1"), Seq(SInt)), "isEmpty") + bind(env, "SELF.R1[Int].isEmpty") shouldBe LogicalNot(Select(ApplyTypes(Select(Self, "R1"), Seq(SInt)), "isDefined").asBoolValue) bind(env, "f[Int](10)") shouldBe Apply(ApplyTypes(Ident("f"), Seq(SInt)), IndexedSeq(IntConstant(10))) bind(env, "INPUTS.map[Int]") shouldBe ApplyTypes(Select(Inputs, "map"), Seq(SInt)) bind(env, "INPUTS.map[Int](10)") shouldBe Apply(ApplyTypes(Select(Inputs, "map"), Seq(SInt)), IndexedSeq(IntConstant(10))) diff --git a/sc/src/test/scala/sigmastate/lang/SigmaTyperTest.scala b/sc/src/test/scala/sigmastate/lang/SigmaTyperTest.scala index dc87d2dc2f..22c2a1fd7a 100644 --- a/sc/src/test/scala/sigmastate/lang/SigmaTyperTest.scala +++ b/sc/src/test/scala/sigmastate/lang/SigmaTyperTest.scala @@ -299,8 +299,7 @@ class SigmaTyperTest extends AnyPropSpec property("type parameters") { typecheck(env, "SELF.R1[Int]") shouldBe SOption(SInt) typecheck(env, "SELF.R1[Int].isDefined") shouldBe SBoolean - // TODO soft-fork: https://github.com/ScorexFoundation/sigmastate-interpreter/issues/479 - // typecheck(env, "SELF.R1[Int].isEmpty") shouldBe SBoolean + typecheck(env, "SELF.R1[Int].isEmpty") shouldBe SBoolean typecheck(env, "SELF.R1[Int].get") shouldBe SInt // TODO soft-fork: https://github.com/ScorexFoundation/sigmastate-interpreter/issues/416 // typecheck(env, "SELF.getReg[Int](1)") shouldBe SOption.SIntOption diff --git a/sc/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala b/sc/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala index ace7abb082..54f1358590 100644 --- a/sc/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala +++ b/sc/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala @@ -502,6 +502,32 @@ class BasicOpsSpecification extends CompilerTestingCommons ) } + property("OptionIsEmpty") { + test("Def1", env, ext, + "{ SELF.R4[SigmaProp].isEmpty == false }", + ExtractRegisterAs[SSigmaProp.type](Self, reg1).isDefined.toSigmaProp, + true + ) + // no value + test("Def2", env, ext, + "{ SELF.R8[Int].isEmpty }", + LogicalNot(ExtractRegisterAs[SInt.type](Self, R8).isDefined).toSigmaProp, + true + ) + + test("Def3", env, ext, + "{ getVar[Int](intVar2).isEmpty == false }", + GetVarInt(intVar2).isDefined.toSigmaProp, + true + ) + // there should be no variable with this id + test("Def4", env, ext, + "{ getVar[Int](99).isEmpty }", + LogicalNot(GetVarInt(99).isDefined).toSigmaProp, + true + ) + } + // TODO this is valid for BigIntModQ type (https://github.com/ScorexFoundation/sigmastate-interpreter/issues/554) ignore("ByteArrayToBigInt: big int should always be positive") { test("BATBI1", env, ext, diff --git a/sc/src/test/scala/special/sigma/SigmaDslSpecification.scala b/sc/src/test/scala/special/sigma/SigmaDslSpecification.scala index 0ab8ca97bd..11f129600e 100644 --- a/sc/src/test/scala/special/sigma/SigmaDslSpecification.scala +++ b/sc/src/test/scala/special/sigma/SigmaDslSpecification.scala @@ -9099,15 +9099,12 @@ class SigmaDslSpecification extends SigmaDslTesting // TODO v6.0 (3h): implement Option.fold property("Option new methods") { - val isEmpty = newFeature({ (x: Option[Long]) => x.isEmpty }, - "{ (x: Option[Long]) => x.isEmpty }") - val n = ExactNumeric.LongIsExactNumeric val fold = newFeature({ (x: Option[Long]) => x.fold(5.toLong)( (v: Long) => n.plus(v, 1) ) }, "{ (x: Option[Long]) => x.fold(5, { (v: Long) => v + 1 }) }") forAll { x: Option[Long] => - Seq(isEmpty, fold).map(_.checkEquality(x)) + Seq(fold).map(_.checkEquality(x)) } } 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/sc/src/main/scala/sigmastate/serialization/DataJsonEncoder.scala b/sdk/shared/src/main/scala/org/ergoplatform/sdk/DataJsonEncoder.scala similarity index 96% rename from sc/src/main/scala/sigmastate/serialization/DataJsonEncoder.scala rename to sdk/shared/src/main/scala/org/ergoplatform/sdk/DataJsonEncoder.scala index fa30825aae..d36bd947a3 100644 --- a/sc/src/main/scala/sigmastate/serialization/DataJsonEncoder.scala +++ b/sdk/shared/src/main/scala/org/ergoplatform/sdk/DataJsonEncoder.scala @@ -1,4 +1,4 @@ -package sigmastate.serialization +package org.ergoplatform.sdk import java.math.BigInteger import io.circe._ @@ -10,14 +10,18 @@ import scalan.RType import scorex.util._ import sigmastate.Values.{Constant, EvaluatedValue} import sigmastate._ -import sigmastate.eval._ import sigmastate.lang.SigmaParser +import sigmastate.eval._ import special.collection.Coll import special.sigma._ import debox.cfor import sigmastate.exceptions.SerializerException import scala.collection.compat.immutable.ArraySeq import scala.collection.mutable +import fastparse.{Parsed, parse} +import sigmastate.serialization.SigmaSerializer +import sigmastate.serialization.DataSerializer +import sigmastate.serialization.ErgoTreeSerializer object DataJsonEncoder { def encode[T <: SType](v: T#WrappedType, tpe: T): Json = { @@ -42,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 @@ -155,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/sc/src/main/scala/org/ergoplatform/JsonCodecs.scala b/sdk/shared/src/main/scala/org/ergoplatform/sdk/JsonCodecs.scala similarity index 97% rename from sc/src/main/scala/org/ergoplatform/JsonCodecs.scala rename to sdk/shared/src/main/scala/org/ergoplatform/sdk/JsonCodecs.scala index ea239af46f..2c357a558f 100644 --- a/sc/src/main/scala/org/ergoplatform/JsonCodecs.scala +++ b/sdk/shared/src/main/scala/org/ergoplatform/sdk/JsonCodecs.scala @@ -1,4 +1,4 @@ -package org.ergoplatform +package org.ergoplatform.sdk import java.math.BigInteger @@ -16,12 +16,22 @@ import sigmastate.eval.Extensions._ import sigmastate.eval.{CPreHeader, WrapperOf, _} import sigmastate.exceptions.SigmaException import sigmastate.interpreter.{ContextExtension, ProverResult} -import sigmastate.serialization.{DataJsonEncoder, ErgoTreeSerializer, ValueSerializer} import sigmastate.{AvlTreeData, AvlTreeFlags, SType} import special.collection.Coll import special.sigma.{AnyValue, Header, PreHeader} import scala.util.Try import sigmastate.utils.Helpers._ // required for Scala 2.11 +import org.ergoplatform.ErgoBox +import sigmastate.serialization.ValueSerializer +import org.ergoplatform.DataInput +import org.ergoplatform.Input +import org.ergoplatform.UnsignedInput +import sigmastate.serialization.ErgoTreeSerializer +import org.ergoplatform.ErgoLikeTransaction +import org.ergoplatform.UnsignedErgoLikeTransaction +import org.ergoplatform.ErgoLikeTransactionTemplate +import org.ergoplatform.ErgoBoxCandidate +import org.ergoplatform.ErgoLikeContext trait JsonCodecs { 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/sc/src/test/scala/sigmastate/serialization/DataJsonEncoderSpecification.scala b/sdk/shared/src/test/scala/org/ergoplatform/sdk/DataJsonEncoderSpecification.scala similarity index 99% rename from sc/src/test/scala/sigmastate/serialization/DataJsonEncoderSpecification.scala rename to sdk/shared/src/test/scala/org/ergoplatform/sdk/DataJsonEncoderSpecification.scala index a5465ad366..3627f99582 100644 --- a/sc/src/test/scala/sigmastate/serialization/DataJsonEncoderSpecification.scala +++ b/sdk/shared/src/test/scala/org/ergoplatform/sdk/DataJsonEncoderSpecification.scala @@ -1,9 +1,8 @@ -package sigmastate.serialization +package org.ergoplatform.sdk import java.math.BigInteger -import org.ergoplatform.JsonCodecs import org.scalacheck.Arbitrary._ import scalan.RType import sigmastate.SCollection.SByteArray @@ -15,6 +14,7 @@ import sigmastate.eval.{Evaluation, _} import sigmastate.basics.CryptoConstants.EcPointType import sigmastate.exceptions.SerializerException import special.sigma.{Box, AvlTree} +import sigmastate.serialization.SerializationSpecification class DataJsonEncoderSpecification extends SerializationSpecification { object JsonCodecs extends JsonCodecs diff --git a/sc/src/test/scala/org/ergoplatform/JsonSerializationSpec.scala b/sdk/shared/src/test/scala/org/ergoplatform/sdk/JsonSerializationSpec.scala similarity index 93% rename from sc/src/test/scala/org/ergoplatform/JsonSerializationSpec.scala rename to sdk/shared/src/test/scala/org/ergoplatform/sdk/JsonSerializationSpec.scala index fec85d4095..e651a542b8 100644 --- a/sc/src/test/scala/org/ergoplatform/JsonSerializationSpec.scala +++ b/sdk/shared/src/test/scala/org/ergoplatform/sdk/JsonSerializationSpec.scala @@ -1,4 +1,5 @@ -package org.ergoplatform +package org.ergoplatform.sdk + import io.circe._ import io.circe.syntax._ @@ -13,14 +14,21 @@ import sigmastate.Values.{ByteArrayConstant, ByteConstant, ErgoTree, EvaluatedVa import sigmastate.basics.CryptoConstants import sigmastate.basics.DLogProtocol.ProveDlog import sigmastate.eval.Digest32Coll -import sigmastate.helpers.CompilerTestingCommons import sigmastate.interpreter.{ContextExtension, ProverResult} import sigmastate.serialization.SerializationSpecification import sigmastate.utils.Helpers._ import special.collection.Coll import special.sigma.{Header, PreHeader} - -class JsonSerializationSpec extends CompilerTestingCommons with SerializationSpecification with JsonCodecs { +import org.ergoplatform.ErgoLikeContext +import org.ergoplatform.DataInput +import org.ergoplatform.Input +import org.ergoplatform.UnsignedInput +import org.ergoplatform.ErgoBox +import org.ergoplatform.ErgoLikeTransaction +import org.ergoplatform.UnsignedErgoLikeTransaction +import org.ergoplatform.ErgoLikeTransactionTemplate + +class JsonSerializationSpec extends SerializationSpecification with JsonCodecs { def jsonRoundTrip[T](v: T)(implicit encoder: Encoder[T], decoder: Decoder[T]): Unit = { val json = v.asJson 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) + ) + } +}