sigma.ast.ErgoTree.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sigma-state_2.12 Show documentation
Show all versions of sigma-state_2.12 Show documentation
Interpreter of a Sigma-State language
The newest version!
package sigma.ast
import scorex.util.encode.Base16
import sigma.VersionContext
import sigma.ast.ErgoTree.{HeaderType, substConstants}
import sigma.ast.syntax._
import sigma.data.{CSigmaProp, SigmaBoolean}
import sigma.kiama.rewriting.Rewriter.{everywherebu, strategy}
import sigma.validation.ValidationException
import sigma.ast.syntax.ValueOps
import sigma.eval.Extensions.SigmaBooleanOps
import sigma.serialization.ErgoTreeSerializer.DefaultSerializer
import sigma.serialization.{ConstantStore, ErgoTreeSerializer, SigmaSerializer, ValueSerializer}
import supertagged.TaggedType
import java.util.Objects
import scala.collection.mutable
/** This is alternative representation of ErgoTree expression when it cannot be parsed
* due to `error`. This is used by the nodes running old versions of code to recognize
* soft-fork conditions and skip validation of box propositions which are unparsable. */
case class UnparsedErgoTree(bytes: mutable.WrappedArray[Byte], error: ValidationException)
/** The root of ErgoScript IR. Serialized instances of this class are self sufficient and can be passed around.
* ErgoTreeSerializer defines top-level serialization format of the scripts.
* The interpretation of the byte array depend on the first `header` byte, which uses VLQ encoding up to 30 bits.
* Currently we define meaning for only first byte, which may be extended in future versions.
* 7 6 5 4 3 2 1 0
* -------------------------
* | | | | | | | | |
* -------------------------
* Bit 7 == 1 if the header contains more than 1 byte (default == 0)
* Bit 6 - reserved for GZIP compression (should be 0)
* Bit 5 == 1 - reserved for context dependent costing (should be = 0)
* Bit 4 == 1 if constant segregation is used for this ErgoTree (default = 0)
* (see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/264)
* Bit 3 == 1 if size of the whole tree is serialized after the header byte (default = 0)
* Bits 2-0 - language version (current version == 0)
*
* Currently we don't specify interpretation for the second and other bytes of the header.
* We reserve the possibility to extend header by using Bit 7 == 1 and chain additional bytes as in VLQ.
* Once the new bytes are required, a new version of the language should be created and implemented.
* That new language will give an interpretation for the new bytes.
*
* Consistency between fields is ensured by private constructor and factory methods in `ErgoTree` object.
* For performance reasons, ErgoTreeSerializer can be configured to perform additional constant segregation.
* In such a case after deserialization there may be more constants segregated. This is done for example to
* support caching optimization described in #264 mentioned above.
*
* The default behavior of ErgoTreeSerializer is to preserve original structure of ErgoTree and check
* consistency. In case of any inconsistency the serializer throws exception.
*
* @param header the first byte of serialized byte array which determines interpretation of the rest of the array
* @param constants If isConstantSegregation == true contains the constants for which there may be
* ConstantPlaceholders in the tree.
* If isConstantSegregation == false this array should be empty and any placeholder in
* the tree will lead to exception.
* @param root On the right side it has valid expression of `SigmaProp` type. Or alternatively,
* on the left side, it has unparsed bytes along with the ValidationException,
* which caused the deserializer to fail.
* `Right(tree)` if isConstantSegregation == true contains ConstantPlaceholder
* instead of some Constant nodes. Otherwise, it may not contain placeholders.
* It is possible to have both constants and placeholders in the tree, but for every placeholder
* there should be a constant in `constants` array.
* @param propositionBytes original bytes of this tree from which it has been deserialized.
* If null then the bytes are not provided, and will be lazily generated when `bytes`
* method is called.
* 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[sigma](
header: HeaderType,
constants: IndexedSeq[Constant[SType]],
root: Either[UnparsedErgoTree, SigmaPropValue],
private val propositionBytes: Array[Byte],
private val givenDeserialize: Option[Boolean],
private val givenIsUsingBlockchainContext: Option[Boolean]
) {
def this(
header: HeaderType,
constants: IndexedSeq[Constant[SType]],
root: Either[UnparsedErgoTree, SigmaPropValue]) =
this(
header, constants, root,
propositionBytes = DefaultSerializer.serializeErgoTree(
ErgoTree(header, constants, root, null, None, None)
),
givenDeserialize = None,
givenIsUsingBlockchainContext = None
)
require(isConstantSegregation || constants.isEmpty)
require(version == 0 || hasSize, s"For newer version the size bit is required: $this")
/** Then it throws the error from UnparsedErgoTree.
* It does so on every usage of `proposition` because the lazy value remains uninitialized.
*/
@deprecated("Use toProposition instead", "v2.1")
lazy val proposition: SigmaPropValue = toProposition(isConstantSegregation)
/** Version of this tree (== BlockVersion - 1). */
@inline final def version: Byte = ErgoTree.getVersion(header)
@inline final def isRightParsed: Boolean = root.isRight
/** @return true, if constant segregation bit is set in the header. */
@inline final def isConstantSegregation: Boolean = ErgoTree.isConstantSegregation(header)
/** @return true, if size bit is set in the header. */
@inline final def hasSize: Boolean = ErgoTree.hasSize(header)
private[sigma] var _bytes: Array[Byte] = propositionBytes
/** Serialized bytes of this tree. */
final def bytes: Array[Byte] = {
if (_bytes == null) {
_bytes = DefaultSerializer.serializeErgoTree(this)
}
_bytes
}
/** Hexadecimal encoded string of ErgoTree.bytes. */
final def bytesHex: String = Base16.encode(bytes)
private[sigma] 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
}
private[sigma] var _isUsingBlockchainContext: Option[Boolean] = givenIsUsingBlockchainContext
/** Returns true if the tree depends on the blockchain context.
*/
lazy val isUsingBlockchainContext: Boolean = {
if (_isUsingBlockchainContext.isEmpty) {
_isUsingBlockchainContext = Some(root match {
case Right(p) => Value.isUsingBlockchainContext(p)
case _ => false
})
}
_isUsingBlockchainContext.get
}
/** Serialized proposition expression of SigmaProp type with
* ConstantPlaceholder nodes not replaced by Constant nodes.
*/
lazy val template: Array[Byte] = {
val r = SigmaSerializer.startReader(bytes)
DefaultSerializer.deserializeHeaderWithTreeBytes(r)._4
}
/** Base16 encoding of `template` bytes. */
def templateHex: String = Base16.encode(template)
/** Get proposition expression from this contract.
* When root.isRight then
* if replaceConstants == false this is the same as `root.right.get`.
* Otherwise, it is equivalent to `root.right.get` where all placeholders are replaced by Constants.
* When root.isLeft then
* throws the error from UnparsedErgoTree.
* It does so on every usage of `proposition` because the lazy value remains uninitialized.
*/
def toProposition(replaceConstants: Boolean): SigmaPropValue = root match {
case Right(tree) =>
val prop = if (replaceConstants)
substConstants(tree, constants).asSigmaProp
else
tree
prop
case Left(UnparsedErgoTree(_, error)) =>
throw error
}
/** This method attempts to convert the current instance into a `SigmaBoolean`.
* It does so by first converting the instance to a `SigmaPropValue` using the
* `toProposition` method, and then checks if the resulting expression is a
* `SigmaPropConstant`. If it is, the method extracts the `SigmaBoolean` from the
* `SigmaPropConstant`. Otherwise, the method returns `None`.
*
* @note This method relies on constant segregation flag in the header to determine the
* behavior of `toProposition` method.
*
* @return `Some(SigmaBoolean)` if conversion is successful, `None` otherwise.
*/
def toSigmaBooleanOpt: Option[SigmaBoolean] = {
val prop = this.toProposition(this.isConstantSegregation)
prop match {
case SigmaPropConstant(p) => Some(p.asInstanceOf[CSigmaProp].wrappedValue)
case _ => None
}
}
/** The default equality of case class is overridden to exclude `complexity`. */
override def canEqual(that: Any): Boolean = that.isInstanceOf[ErgoTree]
override def hashCode(): Int = header * 31 + Objects.hash(constants, root)
override def equals(obj: Any): Boolean = (this eq obj.asInstanceOf[AnyRef]) ||
((obj.asInstanceOf[AnyRef] != null) && (obj match {
case other: ErgoTree =>
other.header == header && other.constants == constants && other.root == root
case _ => false
}))
}
object ErgoTree {
/** Represents information in ErgoTree header. */
object HeaderType extends TaggedType[Byte]
type HeaderType = HeaderType.Type
/** Current version of ErgoTree serialization format (aka bite-code language version) */
val VersionFlag: Byte = VersionContext.MaxSupportedScriptVersion
/** Header flag to indicate that constant segregation should be applied. */
val ConstantSegregationFlag: Byte = 0x10
/** Header flag to indicate that whole size of ErgoTree should be saved before tree content. */
val SizeFlag: Byte = 0x08
/** Header mask to extract version bits. */
val VersionMask: Byte = 0x07
/** Header with all the flags set to 0 and version 0. */
val ZeroHeader: HeaderType = HeaderType @@ 0.toByte
/** Default value of ErgoTree.header byte */
val DefaultHeader: HeaderType = ZeroHeader //HeaderType @@ (VersionFlag | SizeFlag).toByte
/** Default header with constant segregation enabled. */
val ConstantSegregationHeader: HeaderType = HeaderType @@ (DefaultHeader | ConstantSegregationFlag).toByte
/** @return true if the constant segregation flag is set to 1 in the given header byte. */
@inline final def isConstantSegregation(header: HeaderType): Boolean = (header & ConstantSegregationFlag) != 0
/** @return true if the size flag is set to 1 in the given header byte. */
@inline final def hasSize(header: HeaderType): Boolean = (header & SizeFlag) != 0
/** @return a value of the version bits from the given header byte. */
@inline final def getVersion(header: HeaderType): Byte = (header & VersionMask).toByte
/** Update the version bits of the given header byte with the given version value,
* leaving all other bits unchanged.
*/
@inline final def setVersionBits(header: HeaderType, version: Byte): HeaderType = {
require(version < 8, s"ErgoTree.version should be < 8: $version")
val h = header & (~VersionMask) // clear version bits
HeaderType @@ (h | version).toByte
}
/** Sets the required bit in the given header:
* - The SizeFlag is set if version > 0
*/
@inline final def setRequiredBits(header: HeaderType): HeaderType = {
if (getVersion(header) > 0) {
// set SizeFlag if version is greater then 0 (see require() in ErgoTree constructor)
HeaderType @@ (header | ErgoTree.SizeFlag).toByte
} else
header
}
/** Sets the ConstantSegregationFlag in the given header */
@inline final def setConstantSegregation(header: HeaderType): HeaderType = {
HeaderType @@ (header | ConstantSegregationFlag).toByte
}
/** Sets the ConstantSegregationFlag in the given header */
@inline final def setSizeBit(header: HeaderType): HeaderType = {
HeaderType @@ (header | SizeFlag).toByte
}
/** Creates valid header byte with the given version.
* The SizeFlag is set if version > 0 */
@inline final def defaultHeaderWithVersion(version: Byte): HeaderType = {
headerWithVersion(DefaultHeader, version)
}
/** Creates valid header byte with the given version.
* The SizeFlag is set if version > 0 */
@inline final def headerWithVersion(header: HeaderType, version: Byte): HeaderType = {
// take the header and embedd the given version in it
val h = setVersionBits(header, version)
setRequiredBits(h)
}
/** Substitute [[ConstantPlaceholder]] nodes in the given expression with the constants
* taken from the given collection.
*
* @param root expression to transform
* @param constants collection of constants to replace placeholders
* @return new expression without placeholders
*/
def substConstants(root: SValue, constants: IndexedSeq[Constant[SType]]): SValue = {
val store = new ConstantStore(constants)
val substRule = strategy[Any] {
case ph: ConstantPlaceholder[_] =>
Some(store.get(ph.id))
case _ => None
}
everywherebu(substRule)(root).fold(root)(_.asInstanceOf[SValue])
}
/** Create an ErgoTree with the given parameters. */
def apply(
header: HeaderType,
constants: IndexedSeq[Constant[SType]],
root: SigmaPropValue): ErgoTree = {
new ErgoTree(setRequiredBits(header), constants, Right(root))
}
val EmptyConstants: IndexedSeq[Constant[SType]] = Array[Constant[SType]]()
/** Create new ErgoTree for the given proposition using default header.
* If the property is not a simple constant, then constant segregation is performed.
*/
def fromProposition(prop: SigmaPropValue): ErgoTree = {
fromProposition(ErgoTree.ZeroHeader, prop)
}
/** Create new ErgoTree for the given proposition using the given header flags.
* If the property is not a simple constant, then constant segregation is performed.
*/
def fromProposition(header: HeaderType, prop: SigmaPropValue): ErgoTree = {
prop match {
case SigmaPropConstant(_) => withoutSegregation(header, prop)
case _ => withSegregation(header, prop)
}
}
/** Create new ErgoTree for the given sigma proposition using default header and
* without performing constant segregation.
*/
def fromSigmaBoolean(pk: SigmaBoolean): ErgoTree = {
withoutSegregation(ZeroHeader, pk.toSigmaPropValue)
}
/** Create new ErgoTree for the given sigma proposition using the given header flags
* and without performing constant segregation.
*/
def fromSigmaBoolean(header: HeaderType, pk: SigmaBoolean): ErgoTree = {
withoutSegregation(header, pk.toSigmaPropValue)
}
/** Create new ErgoTree for the given proposition using the given header flags and
* without performing constant segregation.
*/
def withoutSegregation(header: HeaderType, root: SigmaPropValue): ErgoTree =
ErgoTree(setRequiredBits(header), EmptyConstants, root)
/** Build ErgoTree via serialization of the value with ConstantSegregationHeader, constants segregated
* from the tree and ConstantPlaceholders referring to the segregated constants.
*
* This method uses single traverse of the tree to:
* 1) find and segregate all constants;
* 2) replace constants with ConstantPlaceholders in the `tree`;
* 3) write the `tree` to the Writer's buffer obtaining `treeBytes`;
* 4) deserialize `tree` with ConstantPlaceholders.
*
* @param headerFlags additional header flags to combine with
* ConstantSegregationHeader flag.
* @param prop expression to be transformed into ErgoTree
* */
def withSegregation(header: HeaderType, prop: SigmaPropValue): ErgoTree = {
val constantStore = new ConstantStore()
val w = SigmaSerializer.startWriter(constantStore)
// serialize value and segregate constants into constantStore
ValueSerializer.serialize(prop, w)
val extractedConstants = constantStore.getAll
val r = SigmaSerializer.startReader(w.toBytes)
r.constantStore = new ConstantStore(extractedConstants)
// deserialize value with placeholders
val valueWithPlaceholders = ValueSerializer.deserialize(r).asSigmaProp
new ErgoTree(
header = setRequiredBits(setConstantSegregation(header)),
constants = extractedConstants,
root = Right(valueWithPlaceholders))
}
/** Deserializes an ErgoTree instance from a hexadecimal string.
*
* @param hex a hexadecimal string representing the serialized ErgoTree
*/
def fromHex(hex: String): ErgoTree = {
val bytes = Base16.decode(hex).get
fromBytes(bytes)
}
/** Deserializes an ErgoTree instance from an array of bytes.
*
* @param bytes an array of bytes representing the serialized ErgoTree
*/
def fromBytes(bytes: Array[Byte]): ErgoTree = {
ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(bytes)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy