diff --git a/build.sbt b/build.sbt index e6adf65cdf..29e0c208e8 100644 --- a/build.sbt +++ b/build.sbt @@ -9,11 +9,6 @@ name := "sigma-state" lazy val scala212 = "2.12.10" lazy val scala211 = "2.11.12" -javacOptions ++= - "-source" :: "1.7" :: - "-target" :: "1.7" :: - Nil - lazy val allConfigDependency = "compile->compile;test->test" lazy val commonSettings = Seq( diff --git a/common/src/main/scala/scalan/util/Extensions.scala b/common/src/main/scala/scalan/util/Extensions.scala index a1bf61f03b..7a820585fe 100644 --- a/common/src/main/scala/scalan/util/Extensions.scala +++ b/common/src/main/scala/scalan/util/Extensions.scala @@ -13,7 +13,7 @@ object Extensions { def toByte: Byte = if (b) 1 else 0 } - /** @hotspot it is used in deserialization so we avoid allocation by any means. */ + /** HOTSPOT: it is used in deserialization so we avoid allocation by any means. */ @inline final def toUByte(b: Byte) = b & 0xFF implicit class ByteOps(val b: Byte) extends AnyVal { diff --git a/core/src/main/scala/scalan/Base.scala b/core/src/main/scala/scalan/Base.scala index 47cae5da08..84c1314ac5 100644 --- a/core/src/main/scala/scalan/Base.scala +++ b/core/src/main/scala/scalan/Base.scala @@ -538,7 +538,7 @@ abstract class Base { scalan: Scalan => /** Transforms this object into new one by applying `t` to every Ref inside * its structure. The structure is build out of Seq, Array, Option and Def values. * Other structure items remain unchanged and copied to the new instance. - * @hotspot don't beautify the code */ + * HOTSPOT: don't beautify the code */ protected def transformProductParam(x: Any, t: Transformer): Any = x match { case (_: UnOp[_, _]) | (_: BinOp[_, _]) => // allows use of context bounds in classes extending UnOp/BinOp. @@ -687,7 +687,7 @@ abstract class Base { scalan: Scalan => /** Create or find symbol (node Ref) which refers to the given node in the table of all created symbols. * The d.nodeId is the index in the _symbolTable which is DBuffer (backed by Array) * @return new of existing symbol - * @hotspot the method should be allocation-free (make it sure by examining the generated Java code) + * HOTSPOT: the method should be allocation-free (make it sure by examining the generated Java code) */ final def updateSymbolTable[T](s: Ref[T], d: Def[T]): Ref[T] = { val id = d.nodeId @@ -769,7 +769,7 @@ abstract class Base { scalan: Scalan => * @param d node to be added to the head of nodes * @param newSym producer of the reference to be used as the reference to `d` node. * @return return a reference to `d` node in the heap - * @hotspot */ + * HOTSPOT: */ def findOrCreateDefinition[T](d: Def[T], newSym: => Ref[T]): Ref[T] = { val optScope = thunkStack.top var sym = optScope match { diff --git a/core/src/main/scala/scalan/staged/AstGraphs.scala b/core/src/main/scala/scalan/staged/AstGraphs.scala index dd801e25e3..8723da5633 100644 --- a/core/src/main/scala/scalan/staged/AstGraphs.scala +++ b/core/src/main/scala/scalan/staged/AstGraphs.scala @@ -49,7 +49,7 @@ trait AstGraphs extends Transforming { self: Scalan => * If the graph represents a compound definition (Lambda, Thunk etc), * then each item in `freeVars` is used in the body, but not part of it. * Intersection of free vars with bound vars is empty. - * @hotspot don't beautify the code + * HOTSPOT: don't beautify the code */ def freeVars: Seq[Sym] = { val sch = schedule.toArray @@ -89,7 +89,7 @@ trait AstGraphs extends Transforming { self: Scalan => def scheduleIds: DBuffer[Int] /** Sequence of node references forming a schedule. - * @hotspot don't beautify the code */ + * HOTSPOT: don't beautify the code */ lazy val schedule: Schedule = { val ids = scheduleIds val len = ids.length @@ -155,7 +155,7 @@ trait AstGraphs extends Transforming { self: Scalan => /** Build usage information induced by the given schedule. * For each symbol of the schedule a GraphNode is created and usages are collected. - * @hotspot don't beautify the code + * HOTSPOT: don't beautify the code */ def buildUsageMap(schedule: Schedule, usingDeps: Boolean): DMap[Int, GraphNode] = { val len = schedule.length @@ -203,7 +203,7 @@ trait AstGraphs extends Transforming { self: Scalan => def hasManyUsagesGlobal(s: Sym): Boolean = globalUsagesOf(s).length > 1 - /** @hotspot for performance we return mutable structure, but it should never be changed. */ + /** HOTSPOT: for performance we return mutable structure, but it should never be changed. */ def usagesOf(id: Int): DBuffer[Int] = { val node = usageMap.getOrElse(id, null) if (node == null) return emptyDBufferOfInt diff --git a/core/src/main/scala/scalan/staged/Transforming.scala b/core/src/main/scala/scalan/staged/Transforming.scala index 22f021b8cb..c7760f8899 100644 --- a/core/src/main/scala/scalan/staged/Transforming.scala +++ b/core/src/main/scala/scalan/staged/Transforming.scala @@ -71,7 +71,7 @@ trait Transforming { self: Scalan => } /** Concrete and default implementation of Transformer using underlying HashMap. - * @hotspot don't beatify the code */ + * HOTSPOT: don't beatify the code */ case class MapTransformer(private val subst: util.HashMap[Sym, Sym]) extends Transformer { def this(substPairs: (Sym, Sym)*) { this({ @@ -145,7 +145,7 @@ trait Transforming { self: Scalan => /** Base class for mirrors of graph nodes. Provides default implementations which can be * overriden if special logic is required. - * @hotspot don't beautify the code */ + * HOTSPOT: don't beautify the code */ abstract class Mirror { def apply[A](t: Transformer, rewriter: Rewriter, node: Ref[A], d: Def[A]): Sym = d.mirror(t) @@ -246,7 +246,7 @@ trait Transforming { self: Scalan => } } - /** @hotspot */ + /** HOTSPOT: */ def mirrorSymbols(t0: Transformer, rewriter: Rewriter, g: AstGraph, nodes: DBuffer[Int]) = { var t: Transformer = t0 cfor(0)(_ < nodes.length, _ + 1) { i => diff --git a/sigmastate/src/main/scala/org/ergoplatform/ErgoBox.scala b/sigmastate/src/main/scala/org/ergoplatform/ErgoBox.scala index 90fb04a8fd..79fbb8c436 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/ErgoBox.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/ErgoBox.scala @@ -46,7 +46,7 @@ import scala.runtime.ScalaRunTime * @param creationHeight - height when a transaction containing the box was created. * This height is declared by user and should not exceed height of the block, * containing the transaction with this box. - * @hotspot don't beautify the code of this class + * HOTSPOT: don't beautify the code of this class */ class ErgoBox( override val value: Long, @@ -149,7 +149,7 @@ object ErgoBox { val maxRegisters: Int = SigmaConstants.MaxRegisters.value - /** @hotspot don't beautify the code in this companion */ + /** HOTSPOT: don't beautify the code in this companion */ private val _mandatoryRegisters: Array[MandatoryRegisterId] = Array(R0, R1, R2, R3) val mandatoryRegisters: Seq[MandatoryRegisterId] = _mandatoryRegisters @@ -169,7 +169,7 @@ object ErgoBox { val registerByName: Map[String, RegisterId] = allRegisters.map(r => s"R${r.number}" -> r).toMap - /** @hotspot called from ErgoBox serializer */ + /** HOTSPOT: called from ErgoBox serializer */ @inline final def registerByIndex(index: Int): RegisterId = allRegisters(index) def findRegisterByIndex(i: Int): Option[RegisterId] = diff --git a/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala b/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala index 0b11792137..82f6f5f877 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala @@ -4,7 +4,6 @@ import java.util import org.ergoplatform.ErgoBox._ import org.ergoplatform.settings.ErgoAlgos -import scorex.crypto.hash.Digest32 import scorex.util.{bytesToId, ModifierId} import sigmastate.Values._ import sigmastate._ @@ -115,7 +114,7 @@ object ErgoBoxCandidate { */ val UndefinedBoxRef: Coll[Byte] = Array.fill(34)(0: Byte).toColl - /** @hotspot don't beautify the code */ + /** HOTSPOT: don't beautify the code */ object serializer extends SigmaSerializer[ErgoBoxCandidate, ErgoBoxCandidate] { /** Helper method for [[ErgoBoxCandidate]] serialization. @@ -180,28 +179,31 @@ object ErgoBoxCandidate { /** Helper method to parse [[ErgoBoxCandidate]] previously serialized by * [[serializeBodyWithIndexedDigests()]]. */ - def parseBodyWithIndexedDigests(digestsInTx: Option[Coll[TokenId]], r: SigmaByteReader): ErgoBoxCandidate = { + def parseBodyWithIndexedDigests(digestsInTx: Array[Array[Byte]], r: SigmaByteReader): ErgoBoxCandidate = { val previousPositionLimit = r.positionLimit r.positionLimit = r.position + ErgoBox.MaxBoxSize val value = r.getULong() // READ val tree = DefaultSerializer.deserializeErgoTree(r, SigmaSerializer.MaxPropositionSize) // READ val creationHeight = r.getUInt().toInt // READ val nTokens = r.getUByte() // READ - val tokenIds = new Array[Digest32](nTokens) + val tokenIds = new Array[Array[Byte]](nTokens) val tokenAmounts = new Array[Long](nTokens) - val tokenIdSize = TokenId.size - cfor(0)(_ < nTokens, _ + 1) { i => - val tokenId = if (digestsInTx.isDefined) { - val digestIndex = r.getUInt().toInt // READ - val digests = digestsInTx.get - if (!digests.isDefinedAt(digestIndex)) sys.error(s"failed to find token id with index $digestIndex") - digests(digestIndex) - } else { - r.getBytes(tokenIdSize) // READ + if (digestsInTx != null) { + val nDigests = digestsInTx.length + cfor(0)(_ < nTokens, _ + 1) { i => + val digestIndex = r.getUInt().toInt // READ + if (digestIndex < 0 || digestIndex >= nDigests) + sys.error(s"failed to find token id with index $digestIndex") + val amount = r.getULong() // READ + tokenIds(i) = digestsInTx(digestIndex) + tokenAmounts(i) = amount + } + } else { + val tokenIdSize = TokenId.size // optimization: access the value once + cfor(0)(_ < nTokens, _ + 1) { i => + tokenIds(i) = r.getBytes(tokenIdSize) // READ + tokenAmounts(i) = r.getULong() // READ } - val amount = r.getULong() // READ - tokenIds(i) = tokenId.asInstanceOf[Digest32] - tokenAmounts(i) = amount } val tokens = Colls.pairCollFromArrays(tokenIds, tokenAmounts) @@ -215,11 +217,13 @@ object ErgoBoxCandidate { b += ((reg, v)) // don't use `->` since it incur additional wrapper overhead } r.positionLimit = previousPositionLimit - new ErgoBoxCandidate(value, tree, creationHeight, tokens, b.result()) + new ErgoBoxCandidate( + value, tree, creationHeight, + tokens.asInstanceOf[Coll[(TokenId, Long)]], b.result()) } override def parse(r: SigmaByteReader): ErgoBoxCandidate = { - parseBodyWithIndexedDigests(None, r) + parseBodyWithIndexedDigests(digestsInTx = null, r) } } diff --git a/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeInterpreter.scala b/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeInterpreter.scala index 01efc82022..1ce5bede46 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeInterpreter.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeInterpreter.scala @@ -3,14 +3,20 @@ package org.ergoplatform import sigmastate.SCollection.SByteArray import sigmastate.Values._ import sigmastate.eval.IRContext -import sigmastate.interpreter.Interpreter +import sigmastate.interpreter.{Interpreter, PrecompiledScriptProcessor} import sigmastate.utxo._ +/** Base class of verifying interpreter which expects ErgoLikeContext as input of + * verify method. + * It implements deserialization of register of SELF box. + */ class ErgoLikeInterpreter(implicit val IR: IRContext) extends Interpreter { override type CTX <: ErgoLikeContext + override val precompiledScriptProcessor: PrecompiledScriptProcessor = PrecompiledScriptProcessor.Default + override def substDeserialize(context: CTX, updateContext: CTX => Unit, node: SValue): Option[SValue] = node match { case d: DeserializeRegister[_] => context.boxesToSpend(context.selfIndex).get(d.reg).flatMap { v => diff --git a/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala b/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala index c5d850d457..c747ff9684 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala @@ -2,7 +2,7 @@ package org.ergoplatform import org.ergoplatform.ErgoBox.TokenId import scorex.crypto.authds.ADKey -import scorex.crypto.hash.{Blake2b256, Digest32} +import scorex.crypto.hash.Blake2b256 import scorex.util._ import sigmastate.SType._ import sigmastate.eval.Extensions._ @@ -13,34 +13,34 @@ import sigmastate.utils.{SigmaByteReader, SigmaByteWriter} import special.collection.ExtensionMethods._ import spire.syntax.all.cfor -import scala.collection.mutable import scala.util.Try trait ErgoBoxReader { def byId(boxId: ADKey): Try[ErgoBox] } -/** - * Base trait of a real transaction to be used in Ergo network. +/** Base trait of a real transaction to be used in Ergo network. * May be in unsigned (`UnsignedErgoLikeTransaction`) or in signed (`ErgoLikeTransaction`) version. - * - * Consists of: - * - * @param inputs - inputs, that will be spent by this transaction. - * @param dataInputs - inputs, that are not going to be spent by transaction, but will be - * reachable from inputs scripts. `dataInputs` scripts will not be executed, - * thus their scripts costs are not included in transaction cost and - * they do not contain spending proofs. - * @param outputCandidates - box candidates to be created by this transaction. - * Differ from ordinary ones in that they do not include transaction id and index */ trait ErgoLikeTransactionTemplate[IT <: UnsignedInput] { + /** Inputs, that are not going to be spent by transaction, but will be + * reachable from inputs scripts. `dataInputs` scripts will not be executed, + * thus their scripts costs are not included in transaction cost and + * they do not contain spending proofs. + */ val dataInputs: IndexedSeq[DataInput] + + /** Inputs, that will be spent by this transaction. */ val inputs: IndexedSeq[IT] + + /** Box candidates to be created by this transaction. + * Differ from ordinary ones in that they do not include transaction id and index. + */ val outputCandidates: IndexedSeq[ErgoBoxCandidate] require(outputCandidates.size <= Short.MaxValue) + /** Identifier of this transaction as state Modifier. */ val id: ModifierId lazy val outputs: IndexedSeq[ErgoBox] = @@ -143,34 +143,37 @@ object ErgoLikeTransactionSerializer extends SigmaSerializer[ErgoLikeTransaction } } + /** HOTSPOT: don't beautify the code */ override def parse(r: SigmaByteReader): ErgoLikeTransaction = { // parse transaction inputs val inputsCount = r.getUShort() - val inputsBuilder = mutable.ArrayBuilder.make[Input]() - for (_ <- 0 until inputsCount) { - inputsBuilder += Input.serializer.parse(r) + val inputs = new Array[Input](inputsCount) + cfor(0)(_ < inputsCount, _ + 1) { i => + inputs(i) = Input.serializer.parse(r) } + // parse transaction data inputs val dataInputsCount = r.getUShort() - val dataInputsBuilder = mutable.ArrayBuilder.make[DataInput]() - for (_ <- 0 until dataInputsCount) { - dataInputsBuilder += DataInput(ADKey @@ r.getBytes(ErgoBox.BoxId.size)) + val dataInputs = new Array[DataInput](dataInputsCount) + cfor(0)(_ < dataInputsCount, _ + 1) { i => + dataInputs(i) = DataInput(ADKey @@ r.getBytes(ErgoBox.BoxId.size)) } + // parse distinct ids of tokens in transaction outputs val tokensCount = r.getUInt().toInt - val tokensBuilder = mutable.ArrayBuilder.make[TokenId]() - for (_ <- 0 until tokensCount) { - tokensBuilder += Digest32 @@ r.getBytes(TokenId.size) + val tokens = new Array[Array[Byte]](tokensCount) + cfor(0)(_ < tokensCount, _ + 1) { i => + tokens(i) = r.getBytes(TokenId.size) } - val tokens = tokensBuilder.result().toColl - // parse outputs + // parse outputs val outsCount = r.getUShort() - val outputCandidatesBuilder = mutable.ArrayBuilder.make[ErgoBoxCandidate]() - for (_ <- 0 until outsCount) { - outputCandidatesBuilder += ErgoBoxCandidate.serializer.parseBodyWithIndexedDigests(Some(tokens), r) + val outputCandidates = new Array[ErgoBoxCandidate](outsCount) + cfor(0)(_ < outsCount, _ + 1) { i => + outputCandidates(i) = ErgoBoxCandidate.serializer.parseBodyWithIndexedDigests(tokens, r) } - new ErgoLikeTransaction(inputsBuilder.result(), dataInputsBuilder.result(), outputCandidatesBuilder.result()) + + new ErgoLikeTransaction(inputs, dataInputs, outputCandidates) } } diff --git a/sigmastate/src/main/scala/org/ergoplatform/validation/SigmaValidationSettings.scala b/sigmastate/src/main/scala/org/ergoplatform/validation/SigmaValidationSettings.scala index e74e41c8c1..c269c0346f 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/validation/SigmaValidationSettings.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/validation/SigmaValidationSettings.scala @@ -60,7 +60,7 @@ sealed class MapSigmaValidationSettings(private val map: Map[Short, (ValidationR override def iterator: Iterator[(Short, (ValidationRule, RuleStatus))] = map.iterator override def get(id: Short): Option[(ValidationRule, RuleStatus)] = map.get(id) - /** @hotspot don't beautify this code */ + /** HOTSPOT: don't beautify this code */ override def getStatus(id: Short): Option[RuleStatus] = { val statusOpt = map.get(id) val res = if (statusOpt.isDefined) Some(statusOpt.get._2) else None diff --git a/sigmastate/src/main/scala/org/ergoplatform/validation/ValidationRules.scala b/sigmastate/src/main/scala/org/ergoplatform/validation/ValidationRules.scala index 87aa64fde1..86e2e38675 100644 --- a/sigmastate/src/main/scala/org/ergoplatform/validation/ValidationRules.scala +++ b/sigmastate/src/main/scala/org/ergoplatform/validation/ValidationRules.scala @@ -33,7 +33,7 @@ case class ValidationRule( /** Check the rule is registered and enabled. * Since it is easy to forget to register new rule, we need to do this check. * But because it is hotspot, we do this check only once for each rule. - * @hotspot executed on every typeCode and opCode during script deserialization + * HOTSPOT: executed on every typeCode and opCode during script deserialization */ @inline protected final def checkRule(): Unit = { if (!_checked) { diff --git a/sigmastate/src/main/scala/sigmastate/Values.scala b/sigmastate/src/main/scala/sigmastate/Values.scala index 07e8e3f4d0..63c6deda93 100644 --- a/sigmastate/src/main/scala/sigmastate/Values.scala +++ b/sigmastate/src/main/scala/sigmastate/Values.scala @@ -5,7 +5,7 @@ import java.util import java.util.Objects import org.bitbucket.inkytonik.kiama.relation.Tree -import org.bitbucket.inkytonik.kiama.rewriting.Rewriter.{strategy, everywherebu} +import org.bitbucket.inkytonik.kiama.rewriting.Rewriter.{strategy, everywherebu, count} import org.ergoplatform.validation.ValidationException import scalan.{Nullable, RType} import scalan.util.CollectionUtil._ @@ -120,6 +120,17 @@ object Values { /** Immutable empty Seq of values. Can be used to avoid allocation. */ val EmptySeq: IndexedSeq[SValue] = EmptyArray + + /** Traverses the given expression tree and counts the number of deserialization + * operations. If it is non-zero, returns true. */ + def hasDeserialize(exp: SValue): Boolean = { + val deserializeNode: PartialFunction[Any, Int] = { + case _: DeserializeContext[_] => 1 + case _: DeserializeRegister[_] => 1 + } + val c = count(deserializeNode)(exp) + c > 0 + } } trait ValueCompanion extends SigmaNodeCompanion { @@ -561,7 +572,50 @@ object Values { } object SigmaBoolean { - /** @hotspot don't beautify this code */ + /** Computes the estimated cost of verifying the given sigma proposition. + * This method should be O(nNodes), where nNodes is the number of the nodes in the + * tree. + * + * @param sb sigma proposition to estimate + * @return the value of estimated cost + * + * HOTSPOT: don't beautify the code + */ + def estimateCost(sb: SigmaBoolean): Int = sb match { + case _: ProveDlog => CostTable.proveDlogEvalCost + case _: ProveDHTuple => CostTable.proveDHTupleEvalCost + case and: CAND => + val children = and.children.toArray + val nChildren = children.length + var sum = 0 + cfor(0)(_ < nChildren, _ + 1) { i => + val c = estimateCost(children(i)) + sum = Math.addExact(sum, c) + } + sum + case or: COR => + val children = or.children.toArray + val nChildren = children.length + var sum = 0 + cfor(0)(_ < nChildren, _ + 1) { i => + val c = estimateCost(children(i)) + sum = Math.addExact(sum, c) + } + sum + case th: CTHRESHOLD => + val children = th.children.toArray + val nChildren = children.length + var sum = 0 + cfor(0)(_ < nChildren, _ + 1) { i => + val c = estimateCost(children(i)) + sum = Math.addExact(sum, c) + } + sum + case _ => + CostTable.MinimalCost + } + + /** HOTSPOT: don't beautify this code */ object serializer extends SigmaSerializer[SigmaBoolean, SigmaBoolean] { val dhtSerializer = ProveDHTupleSerializer(ProveDHTuple.apply) val dlogSerializer = ProveDlogSerializer(ProveDlog.apply) @@ -934,20 +988,33 @@ object Values { * These bytes are obtained in two ways: * 1) in the ErgoTreeSerializer from Reader * 2) in the alternative constructor using ErgoTreeSerializer.serializeErgoTree - * + * @param givenDeserialize optional flag, which contains information about presence of + * deserialization operations in the tree. If it is None, the information is not + * available. If Some(true) then there are deserialization operations, otherwise + * the tree doesn't contain deserialization and is eligible + * for optimized execution. + * ErgoTreeSerializer parsing method computes the value of + * this flag and provides it to the constructor. */ case class ErgoTree private[sigmastate]( header: Byte, constants: IndexedSeq[Constant[SType]], root: Either[UnparsedErgoTree, SigmaPropValue], private val givenComplexity: Int, - private val propositionBytes: Array[Byte] + private val propositionBytes: Array[Byte], + private val givenDeserialize: Option[Boolean] ) { def this(header: Byte, constants: IndexedSeq[Constant[SType]], root: Either[UnparsedErgoTree, SigmaPropValue]) = - this(header, constants, root, 0, DefaultSerializer.serializeErgoTree(ErgoTree(header, constants, root, 0, null))) + this( + header, constants, root, 0, + propositionBytes = DefaultSerializer.serializeErgoTree( + ErgoTree(header, constants, root, 0, null, None) + ), + givenDeserialize = None + ) require(isConstantSegregation || constants.isEmpty) require(version == 0 || hasSize, s"For newer version the size bit is required: $this") @@ -985,7 +1052,22 @@ object Values { _complexity } - /** Serialized proposition expression of SigmaProp type with + private[sigmastate] var _hasDeserialize: Option[Boolean] = givenDeserialize + + /** Returns true if the tree contains at least one deserialization operation, + * false otherwise. + */ + lazy val hasDeserialize: Boolean = { + if (_hasDeserialize.isEmpty) { + _hasDeserialize = Some(root match { + case Right(p) => Value.hasDeserialize(p) + case _ => false + }) + } + _hasDeserialize.get + } + + /** Serialized proposition expression of SigmaProp type with * ConstantPlaceholder nodes instead of Constant nodes */ lazy val template: Array[Byte] = { diff --git a/sigmastate/src/main/scala/sigmastate/eval/CostingDataContext.scala b/sigmastate/src/main/scala/sigmastate/eval/CostingDataContext.scala index 2ff25a714c..256290ef60 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/CostingDataContext.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/CostingDataContext.scala @@ -598,7 +598,7 @@ class CostingSigmaDslBuilder extends TestSigmaDslBuilder { dsl => /** Extracts [[ErgoBox]] from the given [[Box]] instance. This is inverse to the Box method. */ def toErgoBox(b: Box): ErgoBox = b.asInstanceOf[CostingBox].ebox - /** @hotspot don't beautify this code */ + /** HOTSPOT: don't beautify this code */ private def toSigmaTrees(props: Array[SigmaProp]): Array[SigmaBoolean] = { val len = props.length val res = new Array[SigmaBoolean](len) diff --git a/sigmastate/src/main/scala/sigmastate/eval/RuntimeCosting.scala b/sigmastate/src/main/scala/sigmastate/eval/RuntimeCosting.scala index ffbb87e8fa..d6457d0fe5 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/RuntimeCosting.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/RuntimeCosting.scala @@ -165,15 +165,6 @@ trait RuntimeCosting extends CostingRules { IR: IRContext => val _costOfProveDlogEval = CostOf("ProveDlogEval", SFunc(SUnit, SSigmaProp)) val _costOfProveDHTuple = CostOf("ProveDHTuple", SFunc(SUnit, SSigmaProp)) - def costOfSigmaTree(sigmaTree: SigmaBoolean): Int = sigmaTree match { - case _: ProveDlog => _costOfProveDlogEval.eval - case _: ProveDHTuple => _costOfProveDHTuple.eval - case CAND(children) => Colls.fromArray(children.toArray).map(costOfSigmaTree(_)).sum(intPlusMonoidValue) - case COR(children) => Colls.fromArray(children.toArray).map(costOfSigmaTree(_)).sum(intPlusMonoidValue) - case CTHRESHOLD(_, children) => Colls.fromArray(children.toArray).map(costOfSigmaTree(_)).sum(intPlusMonoidValue) - case _ => CostTable.MinimalCost - } - def perKbCostOf(opName: String, opType: SFunc, dataSize: Ref[Long]): Ref[Int] = { val opNamePerKb = s"${opName}_per_kb" PerKbCostOf(OperationId(opNamePerKb, opType), dataSize) @@ -520,7 +511,7 @@ trait RuntimeCosting extends CostingRules { IR: IRContext => * Unfortunately, this is less readable, but gives significant performance boost * Look at comments to understand the logic of the rules. * - * @hotspot executed for each node of the graph, don't beautify. + * HOTSPOT: executed for each node of the graph, don't beautify. */ override def rewriteDef[T](d: Def[T]): Ref[_] = { // First we match on node type, and then depending on it, we have further branching logic. @@ -1036,7 +1027,7 @@ trait RuntimeCosting extends CostingRules { IR: IRContext => _contextDependantNodes(sym.node.nodeId) } - /** @hotspot don't beautify the code */ + /** HOTSPOT: don't beautify the code */ @inline final def allContextDependant(syms: Array[Sym]): Boolean = { val len = syms.length cfor(0)(_ < len, _ + 1) { i => @@ -1164,7 +1155,7 @@ trait RuntimeCosting extends CostingRules { IR: IRContext => case p: SSigmaProp => assert(tpe == SSigmaProp) val resV = liftConst(p) - RCCostedPrim(resV, opCost(resV, Nil, costOfSigmaTree(p)), SizeSigmaProposition) + RCCostedPrim(resV, opCost(resV, Nil, SigmaBoolean.estimateCost(p)), SizeSigmaProposition) case bi: SBigInt => assert(tpe == SBigInt) val resV = liftConst(bi) diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala b/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala index 543dd6bbfb..0f213abcbb 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala @@ -3,26 +3,27 @@ package sigmastate.interpreter import java.util import java.lang.{Math => JMath} -import org.bitbucket.inkytonik.kiama.rewriting.Rewriter.{everywherebu, rule, strategy} +import org.bitbucket.inkytonik.kiama.rewriting.Rewriter.{strategy, rule, everywherebu} import org.bitbucket.inkytonik.kiama.rewriting.Strategy import org.ergoplatform.validation.SigmaValidationSettings -import sigmastate.basics.DLogProtocol.{DLogInteractiveProver, FirstDLogProverMessage} +import sigmastate.basics.DLogProtocol.{FirstDLogProverMessage, DLogInteractiveProver} import scorex.util.ScorexLogging import sigmastate.SCollection.SByteArray import sigmastate.Values._ import sigmastate.eval.{IRContext, Evaluation} import sigmastate.lang.Terms.ValueOps import sigmastate.basics._ -import sigmastate.interpreter.Interpreter.{ScriptEnv, VerificationResult} -import sigmastate.lang.exceptions.{CostLimitException, InterpreterException} -import sigmastate.serialization.{SigmaSerializer, ValueSerializer} +import sigmastate.interpreter.Interpreter.{VerificationResult, ScriptEnv, WhenSoftForkReductionResult} +import sigmastate.lang.exceptions.{InterpreterException, CostLimitException} +import sigmastate.serialization.{ValueSerializer, SigmaSerializer} import sigmastate.utxo.DeserializeContext import sigmastate.{SType, _} import org.ergoplatform.validation.ValidationRules._ +import scalan.{MutableLazy, Nullable} import scalan.util.BenchmarkUtil import sigmastate.utils.Helpers._ -import scala.util.{Try, Success} +import scala.util.{Success, Try} trait Interpreter extends ScorexLogging { @@ -35,6 +36,12 @@ trait Interpreter extends ScorexLogging { val IR: IRContext import IR._ + /** Processor instance which is used by this interpreter to execute ErgoTrees that + * contain neither [[DeserializeContext]] nor [[sigmastate.utxo.DeserializeRegister]] + * operations. + */ + def precompiledScriptProcessor: PrecompiledScriptProcessor + /** Deserializes given script bytes using ValueSerializer (i.e. assuming expression tree format). * It also measures tree complexity adding to the total estimated cost of script execution. * The new returned context contains increased `initCost` and should be used for further processing. @@ -79,15 +86,6 @@ trait Interpreter extends ScorexLogging { case _ => None } - def toValidScriptType(exp: SValue): BoolValue = exp match { - case v: Value[SBoolean.type]@unchecked if v.tpe == SBoolean => v - case p: SValue if p.tpe == SSigmaProp => p.asSigmaProp.isProven - case x => - // This case is not possible, due to exp is always of Boolean/SigmaProp type. - // In case it will ever change, leave it here to throw an explaining message. - throw new Error(s"Context-dependent pre-processing should produce tree of type Boolean or SigmaProp but was $x") - } - class MutableCell[T](var value: T) /** Extracts proposition for ErgoTree handing soft-fork condition. @@ -114,27 +112,10 @@ trait Interpreter extends ScorexLogging { substDeserialize(currContext.value, { ctx: CTX => currContext.value = ctx }, x) } val Some(substTree: SValue) = everywherebu(substRule)(exp) - val res = toValidScriptType(substTree) + val res = Interpreter.toValidScriptType(substTree) (res, currContext.value) } - private def calcResult(context: special.sigma.Context, calcF: Ref[IR.Context => Any]): special.sigma.SigmaProp = { - import IR._ - import Context._ - import SigmaProp._ - val res = calcF.elem.eRange.asInstanceOf[Any] match { - case _: SigmaPropElem[_] => - val valueFun = compile[SContext, SSigmaProp, Context, SigmaProp](getDataEnv, asRep[Context => SigmaProp](calcF)) - val (sp, _) = valueFun(context) - sp - case BooleanElement => - val valueFun = compile[SContext, Boolean, IR.Context, Boolean](IR.getDataEnv, asRep[Context => Boolean](calcF)) - val (b, _) = valueFun(context) - sigmaDslBuilderValue.sigmaProp(b) - } - res - } - /** This method is used in both prover and verifier to compute SigmaBoolean value. * As the first step the cost of computing the `exp` expression in the given context is estimated. * If cost is above limit @@ -152,7 +133,7 @@ trait Interpreter extends ScorexLogging { implicit val vs = context.validationSettings val maxCost = context.costLimit val initCost = context.initCost - trySoftForkable[ReductionResult](whenSoftFork = TrivialProp.TrueProp -> 0) { + trySoftForkable[ReductionResult](whenSoftFork = WhenSoftForkReductionResult) { val costingRes = doCostingEx(env, exp, true) val costF = costingRes.costF IR.onCostingResult(env, exp, costingRes) @@ -168,7 +149,7 @@ trait Interpreter extends ScorexLogging { val calcF = costingRes.calcF CheckCalcFunc(IR)(calcF) val calcCtx = context.toSigmaContext(isCost = false) - val res = calcResult(calcCtx, calcF) + val res = Interpreter.calcResult(IR)(calcCtx, calcF) SigmaDsl.toSigmaBoolean(res) -> estimatedCost } } @@ -195,8 +176,25 @@ trait Interpreter extends ScorexLogging { def fullReduction(ergoTree: ErgoTree, context: CTX, env: ScriptEnv): (SigmaBoolean, Long) = { - implicit val vs: SigmaValidationSettings = context.validationSettings val prop = propositionFromErgoTree(ergoTree, context) + prop match { + case SigmaPropConstant(p) => + val sb = SigmaDsl.toSigmaBoolean(p) + val cost = SigmaBoolean.estimateCost(sb) + (sb, cost) + case _ if !ergoTree.hasDeserialize => + val r = precompiledScriptProcessor.getReducer(ergoTree, context.validationSettings) + r.reduce(context) + case _ => + reductionWithDeserialize(prop, context, env) + } + } + + /** Performs reduction of proposition which contains deserialization operations. */ + private def reductionWithDeserialize(prop: SigmaPropValue, + context: CTX, + env: ScriptEnv) = { + implicit val vs: SigmaValidationSettings = context.validationSettings val (propTree, context2) = trySoftForkable[(BoolValue, CTX)](whenSoftFork = (TrueLeaf, context)) { applyDeserializeContext(context, prop) } @@ -379,10 +377,10 @@ object Interpreter { * The second component is the estimated cost of contract execution. */ type VerificationResult = (Boolean, Long) - /** Result of ErgoTree reduction procedure (see `reduceToCrypto`). + /** Result of ErgoTree reduction procedure (see `reduceToCrypto` and friends). * The first component is the value of SigmaProp type which represents a statement * verifiable via sigma protocol. - * The second component is the estimated cost of contract execution */ + * The second component is the estimated cost of consumed by the contract execution. */ type ReductionResult = (SigmaBoolean, Long) type ScriptEnv = Map[String, Any] @@ -400,6 +398,52 @@ object Interpreter { */ val MaxSupportedScriptVersion: Byte = 1 // supported versions 0 and 1 + /** The result of script reduction when soft-fork condition is detected by the old node, + * in which case the script is reduced to the trivial true proposition and takes up 0 cost. + */ + val WhenSoftForkReductionResult: ReductionResult = TrivialProp.TrueProp -> 0 + + /** Executes the given `calcF` graph in the given context. + * @param IR container of the graph (see [[IRContext]]) + * @param context script execution context (built from [[org.ergoplatform.ErgoLikeContext]]) + * @param calcF graph which represents a reduction function from Context to SigmaProp. + * @return a reduction result + */ + def calcResult(IR: IRContext) + (context: special.sigma.Context, + calcF: IR.Ref[IR.Context => Any]): special.sigma.SigmaProp = { + import IR._ + import IR.Context._ + import IR.SigmaProp._ + import IR.Liftables._ + val res = calcF.elem.eRange.asInstanceOf[Any] match { + case _: SigmaPropElem[_] => + val valueFun = compile[IR.Context.SContext, IR.SigmaProp.SSigmaProp, IR.Context, IR.SigmaProp]( + getDataEnv, + IR.asRep[IR.Context => IR.SigmaProp](calcF))(LiftableContext, LiftableSigmaProp) + val (sp, _) = valueFun(context) + sp + case BooleanElement => + val valueFun = compile[SContext, Boolean, IR.Context, Boolean]( + IR.getDataEnv, + asRep[IR.Context => Boolean](calcF))(LiftableContext, BooleanIsLiftable) + val (b, _) = valueFun(context) + sigmaDslBuilderValue.sigmaProp(b) + } + res + } + + /** Special helper function which converts the given expression to expression returning + * boolean or throws an exception if the conversion is not defined. */ + def toValidScriptType(exp: SValue): BoolValue = exp match { + case v: Value[SBoolean.type]@unchecked if v.tpe == SBoolean => v + case p: SValue if p.tpe == SSigmaProp => p.asSigmaProp.isProven + case x => + // This case is not possible, due to exp is always of Boolean/SigmaProp type. + // In case it will ever change, leave it here to throw an explaining message. + throw new Error(s"Context-dependent pre-processing should produce tree of type Boolean or SigmaProp but was $x") + } + def error(msg: String) = throw new InterpreterException(msg) } \ No newline at end of file diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/PrecompiledScriptProcessor.scala b/sigmastate/src/main/scala/sigmastate/interpreter/PrecompiledScriptProcessor.scala new file mode 100644 index 0000000000..d5124b5e43 --- /dev/null +++ b/sigmastate/src/main/scala/sigmastate/interpreter/PrecompiledScriptProcessor.scala @@ -0,0 +1,266 @@ +package sigmastate.interpreter + +import java.util.concurrent.ExecutionException +import java.util.concurrent.atomic.AtomicInteger + +import com.google.common.cache.{CacheBuilder, RemovalNotification, RemovalListener, LoadingCache, CacheLoader, CacheStats} +import org.ergoplatform.settings.ErgoAlgos +import org.ergoplatform.validation.SigmaValidationSettings +import org.ergoplatform.validation.ValidationRules.{CheckCostFunc, CheckCalcFunc, trySoftForkable} +import scalan.{AVHashMap, Nullable} +import sigmastate.Values +import sigmastate.Values.ErgoTree +import sigmastate.eval.{RuntimeIRContext, IRContext} +import sigmastate.interpreter.Interpreter.{ReductionResult, WhenSoftForkReductionResult} +import sigmastate.serialization.ErgoTreeSerializer +import sigmastate.utils.Helpers._ +import spire.syntax.all.cfor + +import scala.collection.mutable + +/** A reducer which represents precompiled script reduction function. + * The function takes script execution context and produces the [[ReductionResult]], + * which contains both sigma proposition and the approximation of the cost taken by the + * reduction. + */ +trait ScriptReducer { + /** Reduce this pre-compiled script in the given context. + * This is equivalent to reduceToCrypto, except that graph construction is + * completely avoided. + */ + def reduce(context: InterpreterContext): ReductionResult +} + +/** Used as a fallback reducer when precompilation failed due to soft-fork condition. */ +case object WhenSoftForkReducer extends ScriptReducer { + override def reduce(context: InterpreterContext): (Values.SigmaBoolean, Long) = { + WhenSoftForkReductionResult + } +} + +/** This class implements optimized reduction of the given pre-compiled script. + * Pre-compilation of the necessary graphs is performed as part of constructor and + * the graphs are stored in the given IR instance. + * + * The code make the following assumptions: + * 1) the given script doesn't contain both [[sigmastate.utxo.DeserializeContext]] and + * [[sigmastate.utxo.DeserializeRegister]] + * + * The code should correspond to reduceToCrypto method, but some operations may be + * optimized due to assumptions above. + */ +case class PrecompiledScriptReducer(scriptBytes: Seq[Byte])(implicit val IR: IRContext) + extends ScriptReducer { + + /** The following operations create [[IR.RCostingResultEx]] structure for the given + * `scriptBytes` and they should be the same as in `reduceToCrypto` method. + * This can be viewed as ahead of time pre-compilation of the cost and calc graphs + * which are reused over many invocations of the `reduce` method. + */ + val costingRes: IR.RCostingResultEx[Any] = { + val tree = ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(scriptBytes.toArray) + val prop = tree.toProposition(tree.isConstantSegregation) + val validProp = Interpreter.toValidScriptType(prop) + val res = IR.doCostingEx(Interpreter.emptyEnv, validProp, true) + val costF = res.costF + CheckCostFunc(IR)(IR.asRep[Any => Int](costF)) + val calcF = res.calcF + CheckCalcFunc(IR)(calcF) + res + } + + /** Reduce this pre-compiled script in the given context. + * This is equivalent to reduceToCrypto, except that graph construction is + * completely avoided. + */ + def reduce(context: InterpreterContext): ReductionResult = { + import IR._ + implicit val vs = context.validationSettings + val maxCost = context.costLimit + val initCost = context.initCost + trySoftForkable[ReductionResult](whenSoftFork = WhenSoftForkReductionResult) { + val costF = costingRes.costF + val costingCtx = context.toSigmaContext(isCost = true) + val estimatedCost = IR.checkCostWithContext(costingCtx, costF, maxCost, initCost).getOrThrow + + // check calc + val calcF = costingRes.calcF + val calcCtx = context.toSigmaContext(isCost = false) + val res = Interpreter.calcResult(IR)(calcCtx, calcF) + SigmaDsl.toSigmaBoolean(res) -> estimatedCost + } + } +} + +/** Represents keys in the cache of precompiled ErgoTrees for repeated evaluation. + * Note, [[SigmaValidationSettings]] are part of the key, which is important, because + * the output of compilation depends on validation settings. + * + * @param ergoTreeBytes serialized bytes of ErgoTree instance (returned by ErgoTreeSerializer) + * @param vs validation settings which where used for soft-forkable compilation. + */ +case class CacheKey(ergoTreeBytes: Seq[Byte], vs: SigmaValidationSettings) + +/** Settings to configure script processor. + * @param predefScripts collection of scripts to ALWAYS pre-compile (each given by ErgoTree bytes) + * @param maxCacheSize maximum number of entries in the cache + * @param recordCacheStats if true, then cache statistics is recorded + * @param reportingInterval number of cache load operations between two reporting events + */ +case class ScriptProcessorSettings( + predefScripts: Seq[CacheKey], + maxCacheSize: Int = 1000, + recordCacheStats: Boolean = false, + reportingInterval: Int = 100 +) + +/** Statistics of ScriptProcessor operations. + * @param cacheStats cache statistics such as hits and misses + * @param predefHits one hit counter for each predefined script + */ +case class ProcessorStats(cacheStats: CacheStats, predefHits: Seq[Int]) + +/** Script processor which holds pre-compiled reducers for the given scripts. + * This class is thread-safe. + */ +class PrecompiledScriptProcessor(val settings: ScriptProcessorSettings) { + + /** Creates a new instance of IRContex to be used in reducers. + * The default implementation can be overriden in derived classes. + */ + protected def createIR(): IRContext = new RuntimeIRContext + + /** Holds for each ErgoTree bytes the corresponding pre-compiled reducer. + * Since [[AVHashMap]] is not thread-safe it should be immutable + * after it is constructed. However the counters will be updated on each increase. + */ + protected val predefReducers: AVHashMap[CacheKey, (ScriptReducer, AtomicInteger)] = { + implicit val IR: IRContext = createIR() + val predefScripts = settings.predefScripts + val res = AVHashMap[CacheKey, (ScriptReducer, AtomicInteger)](predefScripts.length) + predefScripts.foreach { s => + val r = PrecompiledScriptReducer(s.ergoTreeBytes) + val old = res.put(s, (r, new AtomicInteger(0))) + require(old == null, s"duplicate predefined script: '${ErgoAlgos.encode(s.ergoTreeBytes.toArray)}'") + } + res + } + + /** Obtain hit counter for each predefined script. The order of counters corresponds to + * the order of the scripts in `settings`. + * */ + def getPredefStats(): Seq[Int] = { + val scriptKeys = settings.predefScripts.toArray + val nScripts = scriptKeys.length + val res = new Array[Int](nScripts) + cfor(0)(_ < nScripts, _ + 1) { i => + val key = scriptKeys(i) + val counter = predefReducers.get(key).get._2 + res(i) = counter.get() + } + res + } + + /** Returns accumulated statistics info. */ + def getStats(): ProcessorStats = ProcessorStats(cache.stats(), getPredefStats()) + + /** Called when the cache entry is evicted. */ + protected def onEvictedCacheEntry(key: CacheKey): Unit = { + } + + /** Called when a cache item is removed. */ + protected val cacheListener = new RemovalListener[CacheKey, ScriptReducer]() { + override def onRemoval(notification: RemovalNotification[CacheKey, ScriptReducer]): Unit = { + if (notification.wasEvicted()) { + onEvictedCacheEntry(notification.getKey) + } + } + } + + /** Called to report processor stats. */ + protected def onReportStats(stats: ProcessorStats) = { + } + + /** Loader to be used on a cache miss. The loader creates a new [[ScriptReducer]] for + * the given [[CacheKey]]. The default loader creates an instance of + * [[PrecompiledScriptReducer]] which stores its own IRContext and compiles costF, + * calcF graphs. */ + protected val cacheLoader = new CacheLoader[CacheKey, ScriptReducer]() { + /** Internal counter of all load operations happening an different threads. */ + private val loadCounter = new AtomicInteger(1) + + override def load(key: CacheKey): ScriptReducer = { + val r = trySoftForkable[ScriptReducer](whenSoftFork = WhenSoftForkReducer) { + PrecompiledScriptReducer(key.ergoTreeBytes)(createIR()) + }(key.vs) + + val c = loadCounter.incrementAndGet() + if (c > settings.reportingInterval) { + if (loadCounter.compareAndSet(c, 1)) { + // call reporting only if we was able to reset the counter + // avoid double reporting + onReportStats(ProcessorStats(cache.stats(), getPredefStats())) + } + } + + r + } + } + + /** The cache which stores MRU set of pre-compiled reducers. */ + val cache: LoadingCache[CacheKey, ScriptReducer] = { + var b = CacheBuilder.newBuilder + .maximumSize(settings.maxCacheSize) + .removalListener(cacheListener) + if (settings.recordCacheStats) { + b = b.recordStats() + } + b.build(cacheLoader) + } + + /** An overload version of the method which looks up reducer for the given ErgoTree + * using its 'bytes' property. See also `getReducer(key)`. + * + * @param ergoTree a tree to lookup pre-compiled reducer. + * @param vs validation settings which are used to detect soft-fork condition + * @return a reducer for the given tree + * May throw an exception if error happens and no soft-fork condition detected in `vs`. + */ + def getReducer(ergoTree: ErgoTree, vs: SigmaValidationSettings): ScriptReducer = { + val key = CacheKey(ergoTree.bytes, vs) + getReducer(key) + } + + /** Looks up reducer for the given key using its 'ergoTreeBytes' property. + * It first looks up for predefReducers, if not found it looks up in the cache. + * If there is no cache entry, the `cacheLoader` is used to load a new `ScriptReducer`. + * + * @param key a script key to lookup pre-compiled reducer. + * @return a reducer for the given script + * May throw an exception if error happens and no soft-fork condition detected in `key.vs`. + */ + def getReducer(key: CacheKey): ScriptReducer = { + predefReducers.get(key) match { + case Nullable(r) => + if (settings.recordCacheStats) { + r._2.incrementAndGet() // update hit counter + } + r._1 + case _ => + val r = try { + cache.get(key) + } catch { + case e: ExecutionException => + throw e.getCause + } + r + } + } +} + +object PrecompiledScriptProcessor { + /** Default script processor which uses [[RuntimeIRContext]] to process graphs. */ + val Default = new PrecompiledScriptProcessor( + ScriptProcessorSettings(mutable.WrappedArray.empty[CacheKey])) + +} diff --git a/sigmastate/src/main/scala/sigmastate/lang/SigmaBuilder.scala b/sigmastate/src/main/scala/sigmastate/lang/SigmaBuilder.scala index 5ec5145474..a322718a17 100644 --- a/sigmastate/src/main/scala/sigmastate/lang/SigmaBuilder.scala +++ b/sigmastate/src/main/scala/sigmastate/lang/SigmaBuilder.scala @@ -651,7 +651,7 @@ class StdSigmaBuilder extends SigmaBuilder { trait TypeConstraintCheck { - /** @hotspot called during script deserialization (don't beautify this code) + /** HOTSPOT: called during script deserialization (don't beautify this code) * @consensus */ def check2[T <: SType](left: Value[T], diff --git a/sigmastate/src/main/scala/sigmastate/serialization/BlockValueSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/BlockValueSerializer.scala index 04b05ea70d..c0461b8148 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/BlockValueSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/BlockValueSerializer.scala @@ -28,7 +28,7 @@ case class BlockValueSerializer(cons: (IndexedSeq[BlockItem], Value[SType]) => V val values: IndexedSeq[BlockItem] = if (itemsSize == 0) BlockItem.EmptySeq else { - // @hotspot: allocate new array only if it is not empty + // HOTSPOT:: allocate new array only if it is not empty val buf = new Array[BlockItem](itemsSize) cfor(0)(_ < itemsSize, _ + 1) { i => buf(i) = r.getValue().asInstanceOf[BlockItem] diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionBooleanConstantSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionBooleanConstantSerializer.scala index e2d5895e9b..36a0e22801 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionBooleanConstantSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionBooleanConstantSerializer.scala @@ -28,7 +28,7 @@ case class ConcreteCollectionBooleanConstantSerializer(cons: (IndexedSeq[Value[S w.putBits(bits, bitsInfo) } - /** @hotspot don't beautify this code */ + /** HOTSPOT: don't beautify this code */ override def parse(r: SigmaByteReader): Value[SCollection[SBoolean.type]] = { val size = r.getUShort() // READ val bits = r.getBits(size) // READ diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionSerializer.scala index 82c3e24f22..d44f3adf2a 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ConcreteCollectionSerializer.scala @@ -21,7 +21,7 @@ case class ConcreteCollectionSerializer(cons: (IndexedSeq[Value[SType]], SType) foreach(numItemsInfo.info.name, cc.items)(w.putValue(_, itemInfo)) } - /** @hotspot don't beautify this code */ + /** HOTSPOT: don't beautify this code */ override def parse(r: SigmaByteReader): Value[SCollection[SType]] = { val size = r.getUShort() // READ val tItem = r.getType() // READ diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ConstantStore.scala b/sigmastate/src/main/scala/sigmastate/serialization/ConstantStore.scala index 0bd4588828..dfa7dbb30a 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ConstantStore.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ConstantStore.scala @@ -5,7 +5,7 @@ import sigmastate.Values.{Constant, ConstantNode, ConstantPlaceholder} import sigmastate.lang.SigmaBuilder import debox.Buffer -/** @hotspot used in deserialization (don't beautify this code) */ +/** HOTSPOT: used in deserialization (don't beautify this code) */ class ConstantStore(private val constants: IndexedSeq[Constant[SType]] = Constant.EmptySeq) { private val store: Buffer[Constant[SType]] = Buffer.fromIterable(constants) diff --git a/sigmastate/src/main/scala/sigmastate/serialization/ErgoTreeSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/ErgoTreeSerializer.scala index ad4401dd7f..a4aa1b33b7 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/ErgoTreeSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/ErgoTreeSerializer.scala @@ -151,7 +151,13 @@ class ErgoTreeSerializer { val previousConstantStore = r.constantStore // reader with constant store attached is required (to get tpe for a constant placeholder) r.constantStore = new ConstantStore(cs) + + val wasDeserialize_saved = r.wasDeserialize + r.wasDeserialize = false + val root = ValueSerializer.deserialize(r) + val hasDeserialize = r.wasDeserialize // == true if there was deserialization node + r.wasDeserialize = wasDeserialize_saved if (checkType) { CheckDeserializedScriptIsSigmaProp(root) @@ -165,7 +171,9 @@ class ErgoTreeSerializer { r.position = startPos val propositionBytes = r.getBytes(treeSize) - new ErgoTree(h, cs, Right(root.asSigmaProp), complexity, propositionBytes) + new ErgoTree( + h, cs, Right(root.asSigmaProp), complexity, + propositionBytes, Some(hasDeserialize)) } catch { case e: InputSizeLimitExceeded => @@ -180,7 +188,7 @@ class ErgoTreeSerializer { r.position = startPos val bytes = r.getBytes(numBytes) val complexity = ComplexityTable.OpCodeComplexity(Constant.opCode) - new ErgoTree(ErgoTree.DefaultHeader, EmptyConstants, Left(UnparsedErgoTree(bytes, ve)), complexity, bytes) + new ErgoTree(ErgoTree.DefaultHeader, EmptyConstants, Left(UnparsedErgoTree(bytes, ve)), complexity, bytes, None) case None => throw new SerializerException( s"Cannot handle ValidationException, ErgoTree serialized without size bit.", None, Some(ve)) @@ -208,14 +216,14 @@ class ErgoTreeSerializer { private val constantSerializer = ConstantSerializer(DeserializationSigmaBuilder) /** Deserialize constants section only. - * @hotspot don't beautify this code + * HOTSPOT: don't beautify this code */ private def deserializeConstants(header: Byte, r: SigmaByteReader): IndexedSeq[Constant[SType]] = { val constants: IndexedSeq[Constant[SType]] = if (ErgoTree.isConstantSegregation(header)) { val nConsts = r.getUInt().toInt if (nConsts > 0) { - // @hotspot: allocate new array only if it is not empty + // HOTSPOT:: allocate new array only if it is not empty val res = new Array[Constant[SType]](nConsts) cfor(0)(_ < nConsts, _ + 1) { i => res(i) = constantSerializer.deserialize(r) diff --git a/sigmastate/src/main/scala/sigmastate/serialization/MethodCallSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/MethodCallSerializer.scala index 99f4dc29e8..9e609a8763 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/MethodCallSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/MethodCallSerializer.scala @@ -38,7 +38,7 @@ case class MethodCallSerializer(cons: (Value[SType], SMethod, IndexedSeq[Value[S * This is limitation of MethodCall, because we cannot use it to represent for example * def Box.getReg[T](id: Int): Option[T], which require serialization of expected type `T` * However it can be implemented using separate node type (new type code) and can be added via soft-fork. - * @hotspot don't beautify this code + * HOTSPOT: don't beautify this code */ override def parse(r: SigmaByteReader): Value[SType] = { val typeId = r.getByte() diff --git a/sigmastate/src/main/scala/sigmastate/serialization/transformers/DeserializeContextSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/transformers/DeserializeContextSerializer.scala index e172028221..f2fe92434d 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/transformers/DeserializeContextSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/transformers/DeserializeContextSerializer.scala @@ -21,6 +21,7 @@ case class DeserializeContextSerializer(cons: (Byte, SType) => Value[SType]) override def parse(r: SigmaByteReader): Value[SType] = { val tpe = r.getType() val id = r.getByte() + r.wasDeserialize ||= true // mark the flag cons(id, tpe) } } diff --git a/sigmastate/src/main/scala/sigmastate/serialization/transformers/DeserializeRegisterSerializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/transformers/DeserializeRegisterSerializer.scala index 9925d3eeed..47c1ead8e4 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/transformers/DeserializeRegisterSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/transformers/DeserializeRegisterSerializer.scala @@ -28,6 +28,7 @@ case class DeserializeRegisterSerializer(cons: (RegisterId, SType, Option[Value[ val registerId = ErgoBox.findRegisterByIndex(r.getByte()).get val tpe = r.getType() val dv = r.getOption(r.getValue()) + r.wasDeserialize ||= true // mark the flag cons(registerId, tpe, dv) } diff --git a/sigmastate/src/main/scala/sigmastate/serialization/trees/Relation2Serializer.scala b/sigmastate/src/main/scala/sigmastate/serialization/trees/Relation2Serializer.scala index 58345a08cd..23f6fcb6d4 100644 --- a/sigmastate/src/main/scala/sigmastate/serialization/trees/Relation2Serializer.scala +++ b/sigmastate/src/main/scala/sigmastate/serialization/trees/Relation2Serializer.scala @@ -35,7 +35,7 @@ case class Relation2Serializer[S1 <: SType, S2 <: SType, R <: Value[SBoolean.typ } } - /** @hotspot don't beautify this code */ + /** HOTSPOT: don't beautify this code */ override def parse(r: SigmaByteReader): R = { if (r.peekByte() == ConcreteCollectionBooleanConstantCode) { val _ = r.getByte() // skip collection op code diff --git a/sigmastate/src/main/scala/sigmastate/trees.scala b/sigmastate/src/main/scala/sigmastate/trees.scala index 2600ab89a1..d2c32cb158 100644 --- a/sigmastate/src/main/scala/sigmastate/trees.scala +++ b/sigmastate/src/main/scala/sigmastate/trees.scala @@ -317,7 +317,7 @@ object AtLeast extends ValueCompanion { def apply(bound: Value[SInt.type], head: SigmaPropValue, tail: SigmaPropValue*): AtLeast = apply(bound, head +: tail) - /** @hotspot don't beautify this code */ + /** HOTSPOT: don't beautify this code */ def reduce(bound: Int, children: Seq[SigmaBoolean]): SigmaBoolean = { import sigmastate.TrivialProp._ if (bound <= 0) return TrueProp diff --git a/sigmastate/src/main/scala/sigmastate/types.scala b/sigmastate/src/main/scala/sigmastate/types.scala index 0b453d2067..a65cab2988 100644 --- a/sigmastate/src/main/scala/sigmastate/types.scala +++ b/sigmastate/src/main/scala/sigmastate/types.scala @@ -1436,7 +1436,7 @@ case class SFunc(tDom: IndexedSeq[SType], tRange: SType, tpeParams: Seq[STypePa object SFunc { final val FuncTypeCode: TypeCode = OpCodes.FirstFuncType - def apply(tDom: SType, tRange: SType): SFunc = SFunc(Array(tDom), tRange) // @hotspot + def apply(tDom: SType, tRange: SType): SFunc = SFunc(Array(tDom), tRange) // HOTSPOT: val identity = { x: Any => x } } diff --git a/sigmastate/src/main/scala/sigmastate/utils/SigmaByteReader.scala b/sigmastate/src/main/scala/sigmastate/utils/SigmaByteReader.scala index c736dc1d0a..ee55690b3a 100644 --- a/sigmastate/src/main/scala/sigmastate/utils/SigmaByteReader.scala +++ b/sigmastate/src/main/scala/sigmastate/utils/SigmaByteReader.scala @@ -33,7 +33,7 @@ class SigmaByteReader(val r: Reader, /** The reader should be lightweight to create. In most cases ErgoTrees don't have * ValDef nodes hence the store is not necessary and it's initialization dominates the * reader instantiation time. Hence it's lazy. - * @hotspot + * HOTSPOT: */ lazy val valDefTypeStore: ValDefTypeStore = new ValDefTypeStore() @@ -154,6 +154,7 @@ class SigmaByteReader(val r: Reader, } private var _complexity: Int = 0 + /** Helper property which is used to accumulate complexity during parsing. */ @inline final def complexity: Int = _complexity @inline final def complexity_=(v: Int): Unit = { _complexity = v @@ -162,4 +163,11 @@ class SigmaByteReader(val r: Reader, @inline final def addComplexity(delta: Int): Unit = { _complexity += delta } + + private var _wasDeserialize: Boolean = false + /** Helper property which is used to track deserialization operations during parsing. */ + @inline final def wasDeserialize: Boolean = _wasDeserialize + @inline final def wasDeserialize_=(v: Boolean): Unit = { + _wasDeserialize = v + } } diff --git a/sigmastate/src/main/scala/sigmastate/utxo/transformers.scala b/sigmastate/src/main/scala/sigmastate/utxo/transformers.scala index 2c79e001e6..9deb31c0e1 100644 --- a/sigmastate/src/main/scala/sigmastate/utxo/transformers.scala +++ b/sigmastate/src/main/scala/sigmastate/utxo/transformers.scala @@ -236,7 +236,7 @@ case class ExtractRegisterAs[V <: SType]( input: Value[SBox.type], object ExtractRegisterAs extends ValueCompanion { override def opCode: OpCode = OpCodes.ExtractRegisterAs - //@hotspot: avoids thousands of allocations per second + //HOTSPOT:: avoids thousands of allocations per second private val BoxAndByte: IndexedSeq[SType] = Array(SBox, SByte) def apply[V <: SType](input: Value[SBox.type], diff --git a/sigmastate/src/test/scala/sigmastate/ErgoTreeSpecification.scala b/sigmastate/src/test/scala/sigmastate/ErgoTreeSpecification.scala index 0d22c7400c..49460488ed 100644 --- a/sigmastate/src/test/scala/sigmastate/ErgoTreeSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/ErgoTreeSpecification.scala @@ -1,5 +1,6 @@ package sigmastate +import org.ergoplatform.ErgoBox import org.ergoplatform.settings.ErgoAlgos import org.ergoplatform.validation.{ValidationException, ValidationRules} import scalan.Nullable @@ -8,6 +9,7 @@ import sigmastate.lang.SourceContext import special.sigma.SigmaTestingData import sigmastate.lang.Terms._ import sigmastate.serialization.ErgoTreeSerializer.DefaultSerializer +import sigmastate.utxo.{DeserializeContext, DeserializeRegister} /** Regression tests with ErgoTree related test vectors. * This test vectors verify various constants which are consensus critical and should not change. @@ -80,6 +82,44 @@ class ErgoTreeSpecification extends SigmaTestingData { t._bytes shouldBe expectedBytes } + property("Value.hasDeserialize") { + val const = IntConstant(10) + Value.hasDeserialize(const) shouldBe false + val dc = DeserializeContext(1.toByte, SInt) + Value.hasDeserialize(dc) shouldBe true + val dr = DeserializeRegister(ErgoBox.R4, SInt) + Value.hasDeserialize(dr) shouldBe true + Value.hasDeserialize(EQ(const, dc)) shouldBe true + Value.hasDeserialize(Plus(Plus(const, dc), dr)) shouldBe true + val t = new ErgoTree( + 16.toByte, + Array(IntConstant(1)), + Right(BoolToSigmaProp(EQ(ConstantPlaceholder(0, SInt), IntConstant(1)))) + ) + Value.hasDeserialize(t.toProposition(replaceConstants = true)) shouldBe false + } + + property("ErgoTree.hasDeserialize") { + { + val t = new ErgoTree( + 0.toByte, + Array[Constant[SType]](), + Right(TrueSigmaProp)) + t._hasDeserialize shouldBe None + t.hasDeserialize shouldBe false + } + + { + val t = new ErgoTree( + 16.toByte, + Array(IntConstant(1)), + Right(BoolToSigmaProp(EQ(ConstantPlaceholder(0, SInt), DeserializeContext(1.toByte, SInt)))) + ) + t._hasDeserialize shouldBe None + t.hasDeserialize shouldBe true + } + } + property("ErgoTree equality") { val t1 = new ErgoTree( 16.toByte, diff --git a/sigmastate/src/test/scala/sigmastate/SoftForkabilitySpecification.scala b/sigmastate/src/test/scala/sigmastate/SoftForkabilitySpecification.scala index f830fbb2cd..2574a009be 100644 --- a/sigmastate/src/test/scala/sigmastate/SoftForkabilitySpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/SoftForkabilitySpecification.scala @@ -1,25 +1,27 @@ package sigmastate import org.ergoplatform._ -import org.ergoplatform.validation.ValidationRules.{CheckValidOpCode, trySoftForkable, CheckCostFuncOperation, CheckTupleType, CheckCostFunc, CheckDeserializedScriptIsSigmaProp, _} +import org.ergoplatform.validation.ValidationRules._ import org.ergoplatform.validation._ +import org.scalatest.BeforeAndAfterAll import sigmastate.SPrimType.MaxPrimTypeCode import sigmastate.Values.ErgoTree.EmptyConstants import sigmastate.Values.{UnparsedErgoTree, NotReadyValueInt, ByteArrayConstant, Tuple, IntConstant, ErgoTree, ValueCompanion} import sigmastate.eval.Colls import sigmastate.helpers.{ErgoLikeContextTesting, ErgoLikeTestProvingInterpreter, ErgoLikeTestInterpreter} import sigmastate.helpers.TestingHelpers._ -import sigmastate.interpreter.Interpreter.{ScriptNameProp, emptyEnv} -import sigmastate.interpreter.{ProverResult, ContextExtension} +import sigmastate.interpreter.Interpreter.{ScriptNameProp, emptyEnv, WhenSoftForkReductionResult} +import sigmastate.interpreter.{ProverResult, Interpreter, WhenSoftForkReducer, ContextExtension, CacheKey} import sigmastate.lang.Terms._ -import sigmastate.lang.exceptions.{SerializerException, SigmaException, CosterException, InterpreterException} +import sigmastate.lang.exceptions.{SerializerException, SigmaException, InterpreterException, CosterException} import sigmastate.serialization.OpCodes.{OpCodeExtra, LastConstantCode, OpCode} +import sigmastate.serialization.SigmaSerializer.startReader import sigmastate.serialization._ import sigmastate.utxo.{DeserializeContext, SelectField} import special.sigma.SigmaTestingData import sigmastate.utils.Helpers._ -class SoftForkabilitySpecification extends SigmaTestingData { +class SoftForkabilitySpecification extends SigmaTestingData with BeforeAndAfterAll { implicit lazy val IR = new TestingIRContext lazy val prover = new ErgoLikeTestProvingInterpreter() @@ -82,7 +84,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { property("node v1, received tx with script v1, incorrect script") { assertExceptionThrown({ // CheckDeserializedScriptIsSigmaProp rule violated - ErgoLikeTransaction.serializer.parse(SigmaSerializer.startReader(invalidTxV1bytes)) + ErgoLikeTransaction.serializer.parse(startReader(invalidTxV1bytes)) }, { case se: SerializerException if se.cause.isDefined => val ve = se.cause.get.asInstanceOf[ValidationException] @@ -93,7 +95,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { property("node v1, received tx with script v1, correct script") { // able to parse - val tx = ErgoLikeTransaction.serializer.parse(SigmaSerializer.startReader(txV1bytes)) + val tx = ErgoLikeTransaction.serializer.parse(startReader(txV1bytes)) // validating script proveAndVerifyTx("propV1", tx, vs) @@ -104,7 +106,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { case object Height2 extends NotReadyValueInt with ValueCompanion { override def companion = this override val opCode: OpCode = Height2Code // use reserved code - def opType = SFunc(SContext, SInt) + override val opType = SFunc(SContext, SInt) } val Height2Ser = CaseObjectSerialization(Height2, Height2) @@ -165,7 +167,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { // v1 node should fail assertExceptionThrown( { - val r = SigmaSerializer.startReader(v2tree_withoutSize_bytes) + val r = startReader(v2tree_withoutSize_bytes) ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(r, SigmaSerializer.MaxPropositionSize) }, { @@ -178,7 +180,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { runOnV2Node { assertExceptionThrown( { - val r = SigmaSerializer.startReader(v2tree_withoutSize_bytes) + val r = startReader(v2tree_withoutSize_bytes) ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(r, SigmaSerializer.MaxPropositionSize) }, { @@ -193,7 +195,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { val txV2bytes = txV2messageToSign // parse and validate tx with v2 settings - val tx = ErgoLikeTransaction.serializer.parse(SigmaSerializer.startReader(txV2bytes)) + val tx = ErgoLikeTransaction.serializer.parse(startReader(txV2bytes)) proveAndVerifyTx("propV2", tx, v2vs) // and with v1 settings @@ -221,7 +223,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { property("node v1, no soft-fork, received script v2, raise error") { assertExceptionThrown({ val invalidTxV2bytes = runOnV2Node { invalidTxV2.messageToSign } - ErgoLikeTransaction.serializer.parse(SigmaSerializer.startReader(invalidTxV2bytes)) + ErgoLikeTransaction.serializer.parse(startReader(invalidTxV2bytes)) },{ case se: SerializerException if se.cause.isDefined => val ve = se.cause.get.asInstanceOf[ValidationException] @@ -237,7 +239,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { runOnV2Node { // parse and validate tx with v2 script (since serializers were extended to v2) - val tx = ErgoLikeTransaction.serializer.parse(SigmaSerializer.startReader(txV2bytes)) + val tx = ErgoLikeTransaction.serializer.parse(startReader(txV2bytes)) tx shouldBe txV2 // fails evaluation of v2 script (due to the rest of the implementation is still v1) @@ -256,7 +258,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { ValueSerializer.serialize(booleanPropV2) } - // v1 main script which deserializes from context v2 script + // v1 main script which deserializes v2 script from context val mainProp = BinAnd(GT(Height, IntConstant(deadline)), DeserializeContext(1, SBoolean)).toSigmaProp val mainTree = ErgoTree.fromProposition( headerFlags = ErgoTree.headerWithVersion(0), // ErgoTree v0 @@ -303,7 +305,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { val typeBytes = Array[Byte](MaxPrimTypeCode) val v2vs = vs.updated(CheckPrimitiveTypeCode.id, ChangedRule(Array[Byte](MaxPrimTypeCode))) checkRule(CheckPrimitiveTypeCode, v2vs, { - val r = SigmaSerializer.startReader(typeBytes) + val r = startReader(typeBytes) TypeSerializer.deserialize(r) }) } @@ -312,7 +314,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { val typeBytes = Array[Byte](newTypeCode) val v2vs = vs.updated(CheckTypeCode.id, ChangedRule(Array[Byte](newTypeCode))) checkRule(CheckTypeCode, v2vs, { - val r = SigmaSerializer.startReader(typeBytes) + val r = startReader(typeBytes) TypeSerializer.deserialize(r) }) } @@ -322,7 +324,7 @@ class SoftForkabilitySpecification extends SigmaTestingData { val dataBytes = Array[Byte](1, 2, 3) // any random bytes will work val v2vs = vs.updated(CheckSerializableTypeCode.id, ReplacedRule(0)) checkRule(CheckSerializableTypeCode, v2vs, { - val r = SigmaSerializer.startReader(dataBytes) + val r = startReader(dataBytes) DataSerializer.deserialize(newType, r) }) } @@ -347,11 +349,16 @@ class SoftForkabilitySpecification extends SigmaTestingData { property("CheckCostFuncOperation rule") { val exp = Height - val v2vs = vs.updated(CheckCostFuncOperation.id, + val v2vs = vs.updated( + CheckCostFuncOperation.id, ChangedRule(CheckCostFuncOperation.encodeVLQUShort(Seq(OpCodes.toExtra(Height.opCode))))) checkRule(CheckCostFuncOperation, v2vs, { val costingRes = IR.doCostingEx(emptyEnv, exp, okRemoveIsProven = false) - // use calcF as costing function to have forbidden (not allowed) op (Height) in the costing function + // We need to exercise CheckCostFunc rule. + // The calcF graph have Height operation in it, which is not allowed to be in cost graph. + // This leads to a ValidationException to be thrown with the CheckCostFunc rule in it. + // And the rule is changed in v2vs, so Height is allowed, which is interpreted as + // soft-fork condition CheckCostFunc(IR)(IR.asRep[Any => Int](costingRes.calcF)) }) } @@ -371,4 +378,24 @@ class SoftForkabilitySpecification extends SigmaTestingData { CheckCostFunc(tIR)(asRep[Any => Int](costF)) }) } + + property("PrecompiledScriptProcessor is soft-forkable") { + val p = ErgoLikeTestInterpreter.DefaultProcessorInTests + val v1key = CacheKey(propV2treeBytes, vs) + checkRule(CheckValidOpCode, v2vs, { + p.getReducer(v1key) + }) + + val v2key = CacheKey(propV2treeBytes, v2vs) + val r = p.getReducer(v2key) + r shouldBe WhenSoftForkReducer + + val ctx = createContext(blockHeight, txV2, v2vs) + r.reduce(ctx) shouldBe WhenSoftForkReductionResult + } + + override protected def afterAll(): Unit = { + println(ErgoLikeTestInterpreter.DefaultProcessorInTests.getStats()) + } + } diff --git a/sigmastate/src/test/scala/sigmastate/TestingInterpreterSpecification.scala b/sigmastate/src/test/scala/sigmastate/TestingInterpreterSpecification.scala index e29d97838e..47f9fc457d 100644 --- a/sigmastate/src/test/scala/sigmastate/TestingInterpreterSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/TestingInterpreterSpecification.scala @@ -1,6 +1,6 @@ package sigmastate -import sigmastate.basics.DLogProtocol.{DLogProverInput, ProveDlog} +import sigmastate.basics.DLogProtocol.{ProveDlog, DLogProverInput} import scorex.crypto.hash.Blake2b256 import sigmastate.Values._ import sigmastate.interpreter._ @@ -8,19 +8,33 @@ import scalan.util.Extensions._ import Interpreter._ import sigmastate.lang.Terms._ import org.ergoplatform._ +import org.scalatest.BeforeAndAfterAll import scorex.util.encode.Base58 -import sigmastate.helpers.{ErgoLikeContextTesting, ErgoLikeTestInterpreter, ErgoLikeTestProvingInterpreter, SigmaTestingCommons} +import sigmastate.eval.IRContext +import sigmastate.helpers.{ErgoLikeContextTesting, ErgoLikeTestProvingInterpreter, SigmaTestingCommons, ErgoLikeTestInterpreter} import sigmastate.helpers.TestingHelpers._ import sigmastate.serialization.ValueSerializer import sigmastate.utils.Helpers._ +import scala.collection.mutable import scala.util.Random class TestingInterpreterSpecification extends SigmaTestingCommons - with CrossVersionProps { + with CrossVersionProps with BeforeAndAfterAll { implicit lazy val IR = new TestingIRContext - lazy val prover = new ErgoLikeTestProvingInterpreter() - lazy val verifier = new ErgoLikeTestInterpreter + + lazy val processor = new PrecompiledScriptProcessor( + ScriptProcessorSettings(mutable.WrappedArray.empty)) { + override protected def createIR(): IRContext = new TestingIRContext + } + + lazy val prover = new ErgoLikeTestProvingInterpreter() { + override val precompiledScriptProcessor = processor + } + + lazy val verifier = new ErgoLikeTestInterpreter { + override val precompiledScriptProcessor = processor + } implicit val soundness = CryptoConstants.soundnessBits @@ -364,5 +378,10 @@ class TestingInterpreterSpecification extends SigmaTestingCommons val str = Base58.encode(ValueSerializer.serialize(ByteArrayConstant(Array[Byte](2)))) testEval(s"""deserialize[Coll[Byte]]("$str")(0) == 2""") } + + override protected def afterAll(): Unit = { + println(processor.getStats()) + } + } diff --git a/sigmastate/src/test/scala/sigmastate/helpers/ErgoTransactionValidator.scala b/sigmastate/src/test/scala/sigmastate/helpers/ErgoTransactionValidator.scala index 22f8bae18d..74cdbcf440 100644 --- a/sigmastate/src/test/scala/sigmastate/helpers/ErgoTransactionValidator.scala +++ b/sigmastate/src/test/scala/sigmastate/helpers/ErgoTransactionValidator.scala @@ -1,13 +1,57 @@ package sigmastate.helpers import org.ergoplatform._ -import sigmastate.eval.IRContext +import org.ergoplatform.settings.ErgoAlgos +import org.ergoplatform.validation.{ValidationRules, SigmaValidationSettings} +import sigmastate.eval.{IRContext, CompiletimeIRContext} import sigmastate.interpreter.Interpreter.{ScriptNameProp, emptyEnv} +import sigmastate.interpreter.{CacheKey, PrecompiledScriptProcessor, ProcessorStats, ScriptProcessorSettings} -import scala.util.{Failure, Success} +import scala.util.{Success, Failure} +/** Base class for interpreters used in tests. + * @see derived classes */ class ErgoLikeTestInterpreter(implicit override val IR: IRContext) extends ErgoLikeInterpreter { override type CTX = ErgoLikeContext + override val precompiledScriptProcessor = ErgoLikeTestInterpreter.DefaultProcessorInTests +} +object ErgoLikeTestInterpreter { + val predefScriptsInTests = Seq( + // for property("emission specification") + "100b0400040004e04e0580bcc1960b0580bcc1960b05808c8d9e02040204e04e04a00b040005808c8d9e02d1ec9683040193e4c6b2a57300000404a391a3e4c6a7040493c2a7c2b2a573010093958fa3730273039973049c73057e9a73069d99a3730773080599c1a7c1b2a5730900ed91a3e4c6a7040490c1a7730a" + ) + + /** A list of settings, one for each soft-fork. */ + val validationSettings: Seq[SigmaValidationSettings] = Array( + ValidationRules.currentSettings // for v4.0 + // add v5.0 settings here... + ) + + /** Script processor which uses [[CompiletimeIRContext]] to process graphs. */ + val DefaultProcessorInTests: PrecompiledScriptProcessor = { + val scriptKeys = predefScriptsInTests.flatMap { s => + val bytes = ErgoAlgos.decodeUnsafe(s) + validationSettings.map(vs => CacheKey(bytes, vs)) + } + + new PrecompiledScriptProcessor( + ScriptProcessorSettings( + predefScripts = scriptKeys, + maxCacheSize = 10, + recordCacheStats = true, + reportingInterval = 10)) { + override protected def createIR(): IRContext = new CompiletimeIRContext + + override protected def onReportStats(stats: ProcessorStats): Unit = { + println(s"Processor Stats: $stats") + } + + override protected def onEvictedCacheEntry(key: CacheKey): Unit = { + val scriptHex = ErgoAlgos.encode(key.ergoTreeBytes.toArray) + println(s"Evicted: ${scriptHex}") + } + } + } } class ErgoTransactionValidator(activatedVersion: Byte)(implicit IR: IRContext) { diff --git a/sigmastate/src/test/scala/sigmastate/interpreter/PrecompiledScriptProcessorSpecification.scala b/sigmastate/src/test/scala/sigmastate/interpreter/PrecompiledScriptProcessorSpecification.scala new file mode 100644 index 0000000000..d5ccce7931 --- /dev/null +++ b/sigmastate/src/test/scala/sigmastate/interpreter/PrecompiledScriptProcessorSpecification.scala @@ -0,0 +1,57 @@ +package sigmastate.interpreter + +import org.ergoplatform.validation.ValidationRules +import sigmastate.helpers.SigmaPPrint +import special.sigma.SigmaDslTesting + +class PrecompiledScriptProcessorSpecification extends SigmaDslTesting { + + property("deserialize from hex") { + predefScriptHexes.foreach { hex => + val tree = parseTree(hex) + println( + s"""Tree: '$hex' + |------------------------------------------ + |""".stripMargin) + SigmaPPrint.pprintln(tree, width = 100, height = 150) + println() + } + } + + property("equality") { + val predefTrees = predefScriptHexes.map { h => parseTree(h) } + val extraTrees = Seq(TrueTree, FalseTree) + val trees = extraTrees ++ predefTrees + val scripts = trees.map { t => CacheKey(t.bytes, ValidationRules.currentSettings) } + val processor = new PrecompiledScriptProcessor(ScriptProcessorSettings(scripts)) + trees.foreach { t => + processor.getReducer(t, ValidationRules.currentSettings) should not be null + } + } + + property("rejects duplicates") { + val trees = Seq(TrueTree, TrueTree) + val scripts = trees.map { t => CacheKey(t.bytes, ValidationRules.currentSettings) } + assertExceptionThrown( + new PrecompiledScriptProcessor(ScriptProcessorSettings(scripts)), + { case _: IllegalArgumentException => true + case _ => false } + ) + } + + property("collects predef hit statistics") { + val predefTrees = predefScriptHexes.map { h => parseTree(h) } + val scriptKeys = predefTrees.map { t => CacheKey(t.bytes, ValidationRules.currentSettings) } + Array(false, true).foreach { recordStats => + val processor = new PrecompiledScriptProcessor( + ScriptProcessorSettings( + predefScripts = scriptKeys, + recordCacheStats = recordStats)) + predefTrees.foreach { t => + processor.getReducer(t, ValidationRules.currentSettings) should not be null + } + val expectedHits = if (recordStats) 1 else 0 + processor.getPredefStats() shouldBe Array.fill(scriptKeys.length)(expectedHits).toSeq + } + } +} diff --git a/sigmastate/src/test/scala/sigmastate/serialization/ErgoTreeSerializerSpecification.scala b/sigmastate/src/test/scala/sigmastate/serialization/ErgoTreeSerializerSpecification.scala index 4dd1cb21d4..2d51e541c7 100644 --- a/sigmastate/src/test/scala/sigmastate/serialization/ErgoTreeSerializerSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/serialization/ErgoTreeSerializerSpecification.scala @@ -2,12 +2,14 @@ package sigmastate.serialization import java.math.BigInteger -import sigmastate.Values.{ShortConstant, BigIntConstant, SigmaPropValue, IntConstant, ErgoTree, ByteConstant} +import org.ergoplatform.ErgoBox +import sigmastate.Values.{ShortConstant, BigIntConstant, ConstantPlaceholder, Value, SigmaPropValue, IntConstant, ErgoTree, ByteConstant} import sigmastate._ import sigmastate.eval.{IRContext, CBigInt} import sigmastate.helpers.SigmaTestingCommons import sigmastate.lang.exceptions.{SerializerException, InputSizeLimitExceeded} import sigmastate.serialization.ErgoTreeSerializer.DefaultSerializer +import sigmastate.utxo.{DeserializeContext, DeserializeRegister} class ErgoTreeSerializerSpecification extends SerializationSpecification with SigmaTestingCommons { @@ -115,4 +117,32 @@ class ErgoTreeSerializerSpecification extends SerializationSpecification DefaultSerializer.deserializeErgoTree(r, SigmaSerializer.MaxPropositionSize) shouldEqual tree r.positionLimit shouldBe 1 } + + property("should compute hasDeserialize during parsing") { + val const = IntConstant(10) + val dc = DeserializeContext(1.toByte, SInt) + val dr = DeserializeRegister(ErgoBox.R4, SInt) + + val samples = Table(("exp", "hasDeserialize"), + const -> false, + dc -> true, + dr -> true, + Plus(Plus(const, dc), dr) -> true, + Plus(Plus(const, const), const) -> false + ) + + forAll(samples) { (exp, hasDeserialize) => + val t = new ErgoTree( + 16.toByte, + Array(IntConstant(1)), + Right(BoolToSigmaProp(EQ(ConstantPlaceholder(0, SInt), exp))) + ) + t._hasDeserialize shouldBe None + + val parsedTree = ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(t.bytes) + parsedTree shouldBe t + parsedTree._hasDeserialize.isDefined shouldBe true + parsedTree.hasDeserialize shouldBe hasDeserialize + } + } } diff --git a/sigmastate/src/test/scala/sigmastate/utxo/examples/CoinEmissionSpecification.scala b/sigmastate/src/test/scala/sigmastate/utxo/examples/CoinEmissionSpecification.scala index abbf7a883c..c3cff93dd8 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/examples/CoinEmissionSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/examples/CoinEmissionSpecification.scala @@ -1,8 +1,9 @@ package sigmastate.utxo.examples import org.ergoplatform._ +import org.ergoplatform.settings.ErgoAlgos import scorex.util.ScorexLogging -import sigmastate.Values.{IntConstant, ErgoTree} +import sigmastate.Values.{ErgoTree, IntConstant} import sigmastate.helpers.{ContextEnrichingTestProvingInterpreter, ErgoLikeContextTesting, SigmaTestingCommons} import sigmastate.helpers.TestingHelpers._ import sigmastate.interpreter.ContextExtension @@ -112,6 +113,7 @@ block 1600 in 1622 ms, 30000000000 coins remain, defs: 61661 AND(heightCorrect, heightIncreased, sameScriptRule, correctCoinsConsumed), BinAnd(heightIncreased, lastCoins) ).toSigmaProp + val tree = mkTestErgoTree(prop) val env = Map("fixedRatePeriod" -> s.fixedRatePeriod, "epochLength" -> s.epochLength, @@ -155,7 +157,7 @@ block 1600 in 1622 ms, 30000000000 coins remain, defs: 61661 val ut = if (emissionBox.value > s.oneEpochReduction) { val minerBox = new ErgoBoxCandidate(emissionAtHeight(height), minerProp, height, Colls.emptyColl, Map()) val newEmissionBox: ErgoBoxCandidate = - new ErgoBoxCandidate(emissionBox.value - minerBox.value, mkTestErgoTree(prop), height, Colls.emptyColl, Map(register -> IntConstant(height))) + new ErgoBoxCandidate(emissionBox.value - minerBox.value, tree, height, Colls.emptyColl, Map(register -> IntConstant(height))) UnsignedErgoLikeTransaction( IndexedSeq(new UnsignedInput(emissionBox.id)), @@ -178,7 +180,8 @@ block 1600 in 1622 ms, 30000000000 coins remain, defs: 61661 emissionBox, activatedVersionInTests, ContextExtension.empty) - val proverResult = prover.prove(emptyEnv + (ScriptNameProp -> "prove"), mkTestErgoTree(prop), context, ut.messageToSign).get + val proverResult = prover.prove( + emptyEnv + (ScriptNameProp -> "prove"), tree, context, ut.messageToSign).get ut.toSigned(IndexedSeq(proverResult)) } @@ -212,5 +215,8 @@ block 1600 in 1622 ms, 30000000000 coins remain, defs: 61661 } chainGen(genesisState, initialBox, 0, 1000000) + + println(s"Emission Tree: ${ErgoAlgos.encode(tree.bytes)}") + println(prover.precompiledScriptProcessor.getStats()) } } diff --git a/sigmastate/src/test/scala/sigmastate/utxo/examples/IcoExample.scala b/sigmastate/src/test/scala/sigmastate/utxo/examples/IcoExample.scala index 3774fce497..039ec873af 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/examples/IcoExample.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/examples/IcoExample.scala @@ -9,15 +9,15 @@ import scorex.crypto.authds.avltree.batch._ import scorex.crypto.hash.{Digest32, Blake2b256} import sigmastate.Values._ import sigmastate._ -import sigmastate.helpers.{ContextEnrichingTestProvingInterpreter, ErgoLikeTestProvingInterpreter, SigmaTestingCommons} -import sigmastate.helpers.ErgoLikeContextTesting +import sigmastate.helpers.{ErgoLikeContextTesting, ErgoLikeTestInterpreter, ErgoLikeTestProvingInterpreter, SigmaTestingCommons, ContextEnrichingTestProvingInterpreter} import sigmastate.helpers.TestingHelpers._ import sigmastate.interpreter.Interpreter.ScriptNameProp import sigmastate.lang.Terms._ import sigmastate.serialization.ErgoTreeSerializer import ErgoTreeSerializer.DefaultSerializer +import org.scalatest.BeforeAndAfterAll import sigmastate.eval.{CompiletimeCosting, IRContext} -import sigmastate.interpreter.CryptoConstants +import sigmastate.interpreter.{CryptoConstants, PrecompiledScriptProcessor} import scala.util.Random import sigmastate.eval._ @@ -233,7 +233,7 @@ reasonable to have an additional input from the project with the value equal to */ class IcoExample extends SigmaTestingCommons - with CrossVersionProps { suite => + with CrossVersionProps with BeforeAndAfterAll { suite => // Not mixed with TestContext since it is not possible to call commpiler.compile outside tests if mixed implicit lazy val IR: IRContext = new IRContext with CompiletimeCosting @@ -553,4 +553,10 @@ class IcoExample extends SigmaTestingCommons println(ComplexityTableStat.complexityTableString) } + /** This is the last executed test suite, so this method is executed after all tests. + * We output statistics of how PrecompiledScriptProcessor cache was used. */ + override protected def afterAll(): Unit = { + println(ErgoLikeTestInterpreter.DefaultProcessorInTests.getStats()) + } + } diff --git a/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala b/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala index 2917899a5a..2bec5196c8 100644 --- a/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala +++ b/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala @@ -12,7 +12,7 @@ import scala.util.{Success, Failure, Try} import sigmastate.Values.{Constant, SValue, ConstantNode, ByteArrayConstant, IntConstant, ErgoTree} import scalan.RType import scalan.util.Extensions._ -import org.ergoplatform.dsl.{SigmaContractSyntax, TestContractSpec, ContractSpec} +import org.ergoplatform.dsl.{SigmaContractSyntax, ContractSpec, TestContractSpec} import org.ergoplatform.validation.{ValidationRules, SigmaValidationSettings} import sigmastate.{eval, SSigmaProp, SType} import SType.AnyOps @@ -28,7 +28,7 @@ import sigmastate.utils.Helpers._ import sigmastate.lang.Terms.ValueOps import sigmastate.helpers.{ErgoLikeContextTesting, SigmaPPrint} import sigmastate.helpers.TestingHelpers._ -import sigmastate.interpreter.{ProverResult, ContextExtension, ProverInterpreter} +import sigmastate.interpreter.{ProverResult, PrecompiledScriptProcessor, ContextExtension, ProverInterpreter, CacheKey} import sigmastate.serialization.ValueSerializer import sigmastate.serialization.generators.ObjectGenerators import sigmastate.utxo.{DeserializeContext, DeserializeRegister} @@ -122,6 +122,8 @@ class SigmaDslTesting extends PropSpec val LogScriptDefault: Boolean = false + val predefScripts = Seq[String]() + /** Descriptor of the language feature. */ trait Feature[A, B] { @@ -265,7 +267,9 @@ class SigmaDslTesting extends PropSpec val tpeB = Evaluation.rtypeToSType(oldF.tB) val prover = new FeatureProvingInterpreter() - val verifier = new ErgoLikeInterpreter()(createIR()) { type CTX = ErgoLikeContext } + val verifier = new ErgoLikeInterpreter()(createIR()) { + type CTX = ErgoLikeContext + } // Create synthetic ErgoTree which uses all main capabilities of evaluation machinery. // 1) first-class functions (lambdas); 2) Context variables; 3) Registers; 4) Equality diff --git a/sigmastate/src/test/scala/special/sigma/SigmaTestingData.scala b/sigmastate/src/test/scala/special/sigma/SigmaTestingData.scala index d5c21c9f64..1ce73cdbe2 100644 --- a/sigmastate/src/test/scala/special/sigma/SigmaTestingData.scala +++ b/sigmastate/src/test/scala/special/sigma/SigmaTestingData.scala @@ -1,5 +1,6 @@ package special.sigma +import org.ergoplatform.settings.ErgoAlgos import org.scalacheck.Gen.containerOfN import sigmastate.AvlTreeFlags import org.scalacheck.{Arbitrary, Gen} @@ -10,6 +11,8 @@ import org.scalacheck.util.Buildable import scalan.RType import scorex.crypto.hash.{Digest32, Blake2b256} import scorex.crypto.authds.{ADKey, ADValue} +import sigmastate.Values.ErgoTree +import sigmastate.serialization.ErgoTreeSerializer import sigmastate.serialization.generators.ObjectGenerators import special.collection.Coll @@ -98,4 +101,26 @@ trait SigmaTestingData extends SigmaTestingCommons with ObjectGenerators { minerPk = SigmaDsl.groupGenerator, votes = Colls.emptyColl[Byte] ) + + /** A list of ErgoTree hexes which are the most often used in Ergo mainnet. */ + val predefScriptHexes = Seq( + "0008cd03a40b2249d6a9cc7eedf21188842acef44f3df110a58a687ba3e28cbc2e97ead2", + "1005040004000e36100204a00b08cd0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ea02d192a39a8cc7a701730073011001020402d19683030193a38cc7b2a57300000193c2b2a57301007473027303830108cdeeac93b1a57304", + "101004020e36100204a00b08cd0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ea02d192a39a8cc7a7017300730110010204020404040004c0fd4f05808c82f5f6030580b8c9e5ae040580f882ad16040204c0944004c0f407040004000580f882ad16d19683030191a38cc7a7019683020193c2b2a57300007473017302830108cdeeac93a38cc7b2a573030001978302019683040193b1a5730493c2a7c2b2a573050093958fa3730673079973089c73097e9a730a9d99a3730b730c0599c1a7c1b2a5730d00938cc7b2a5730e0001a390c1a7730f", + "10160e201a6a8c16e4b1cc9d73d03183565cfb8e79dd84198cb66beeed7d3463e0da2b9805000500040008cd03b038b0783c899be6b5b98bcf55df573c87cb2e01c16604c174e5a7e6105e848e04040406040204000400040604080402040004000402040405000500050205040100d808d601b1a4d602c2a7d603c6a70405d6047300d605860272047301d6067302d607d9010763d806d609c27207d60a9372097202d60bd805d60bc17207d60cc1a7d60de47203d60e99720c720dd60f92720b720e720fd60ced720a720bd60dd802d60dc672070405d60e93720d7203720ed60eed720c720d720ed608d9010863d806d60adb63087208d60bb2720a7303017205d60c8c720b01d60d93720c7204d60ed801d60e8c720b02720ed60f95720d720e7206720feb02730495ed937201730593b1a57306d802d6097207d60a7208d1edda720901b2a57307008fda720a01b2a5730800da720a01b2a473090095ed937201730a93b1a5730bd803d609da720801b2a4730c00d60ada720801b2a4730d00d60b999a720a72099ada720801b2a5730e00da720801b2a5730f00d1edda720701b2a5731000eded917209731191720a7312ec93720b731393720b7314d17315", + "10060e2002d1541415c323527f19ef5b103eb33c220ea8b66fcb711806b0037d115d63f204000402040004040e201a6a8c16e4b1cc9d73d03183565cfb8e79dd84198cb66beeed7d3463e0da2b98d803d601e4c6a70507d602d901026393cbc27202e4c6a7070ed6037300ea02eb02cd7201cedb6a01dde4c6a70407e4c6a706077201d1ececedda720201b2a573010093cbc2b2a47302007203edda720201b2a473030093cbc2b2a47304007203afa5d9010463afdb63087204d901064d0e948c7206017305", + "100d0e201a6a8c16e4b1cc9d73d03183565cfb8e79dd84198cb66beeed7d3463e0da2b9804000e20c9ffb7bf74cd7a0fc2b76baf54b4c6192b0a1689e6b0ea6b5d988447c353a3ee0400040004020504040205000402040204000100d806d601e4c6a70407d6027300d603b2a5730100d604c672030407d6057302d606db6a01ddeb02ea02cd7201d1afa5d9010763afdb63087207d901094d0e948c720901720295e67204d808d607db63087203d608b27207730300d609db6308a7d60ab27209730400d60bd9010b63eded93cbc2720b720593e4c6720b070ecbc2a793c1720bc1a7d60cb2a5730500d60de4c672030507d60ee47204ea02d1edededededededeced938c7208018c720a01919c8c72080273068c720a0293cbc2b2a47307007205edededda720b017203da720b01720c937207db6308720cd801d60f86027202730893b27209730901720fb27207730a01720f93e4c672030607720193e4c6720c0607720193e4c6720c0407720d93e4c6720c0507e4720493c5a7c5b2a4730b0094e47204720deb02ce72067201720e720dce72067201720d720ed1730c", + "100c040004000e201a6a8c16e4b1cc9d73d03183565cfb8e79dd84198cb66beeed7d3463e0da2b98040005020400040005020402040204000400d805d601e4c6a70507d602d9010263ed93cbc27202e4c6a7070e93c17202c1a7d603b2a5730000d604b2a4730100d6057302ea02eb02cd7201cedb6a01dde4c6a70407e4c6a706077201d1ececededda720201720393c57204c5a7eddad9010663d801d608b2db63087206730300ed938c7208017205928c7208027304017203938cb2db6308720373050002998cb2db63087204730600027307ededda720201720493c5b2a4730800c5a7938cb2db6308b2a4730900730a00017205afa5d901066393b1db63087206730b", + "100c0e201008c10aea11a275eaac3bffdd08427ec6e80b7512476a89396cd25d415a2de10e206c143ec48214ce2d204c959839c8ddfd0ca030007dba4a95894a0815fe4d41380404040604000400040604080400040205c08db70108cd03b038b0783c899be6b5b98bcf55df573c87cb2e01c16604c174e5a7e6105e848ed803d601b1a4d6027300d6037301eb02d1edecededed937201730293b1a57303dad901046393cbc27204720201b2a573040093cbc2b2a47305007203ededed937201730693b1a57307dad901046393cbc27204720201b2a473080093cbc2b2a47309007203aea5d9010463ed93c27204c2a792c1720499c1a7730a730b", + "1000d801d601e4c6a70507eb02cd7201cedb6a01dde4c6a70407e4c6a706077201", + "100204a00b08cd02b3a06d6eaa8671431ba1db4dd427a77f75a5c2acbd71bfb725d38adc2b55f669ea02d192a39a8cc7a70173007301", + "100204a00b08cd02ebaaeb381c9d855af1807781fa20ef6c0c34833275ce7913a9e4469f7bcb3becea02d192a39a8cc7a70173007301" + ) + + /** Parses ErgoTree instance from hex string. */ + def parseTree(hex: String): ErgoTree = { + val bytes = ErgoAlgos.decodeUnsafe(hex) + val tree = ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(bytes) + tree + } }