diff --git a/.gitignore b/.gitignore index 4dc1640004..3570923b1d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ yarn.lock *.log +yarn.lock docs/spec/out/ test-out/ flamegraphs/ diff --git a/core/shared/src/main/scala/sigma/SigmaDsl.scala b/core/shared/src/main/scala/sigma/SigmaDsl.scala index 52c2f97bd7..ba22d911e2 100644 --- a/core/shared/src/main/scala/sigma/SigmaDsl.scala +++ b/core/shared/src/main/scala/sigma/SigmaDsl.scala @@ -471,6 +471,11 @@ trait Header { */ def serializeWithoutPoW: Coll[Byte] + /** + * @return result of header's proof-of-work validation + */ + def checkPow: Boolean + } /** Runtime representation of Context ErgoTree type. diff --git a/core/shared/src/main/scala/sigma/reflection/ReflectionData.scala b/core/shared/src/main/scala/sigma/reflection/ReflectionData.scala index 028e68bf72..2a5e74e659 100644 --- a/core/shared/src/main/scala/sigma/reflection/ReflectionData.scala +++ b/core/shared/src/main/scala/sigma/reflection/ReflectionData.scala @@ -346,6 +346,9 @@ object ReflectionData { }, mkMethod(clazz, "powDistance", Array[Class[_]]()) { (obj, _) => obj.asInstanceOf[Header].powDistance + }, + mkMethod(clazz, "checkPow", Array[Class[_]]()) { (obj, _) => + obj.asInstanceOf[Header].checkPow } ) ) diff --git a/core/shared/src/main/scala/sigma/util/NBitsUtils.scala b/core/shared/src/main/scala/sigma/util/NBitsUtils.scala new file mode 100644 index 0000000000..36d526d1d5 --- /dev/null +++ b/core/shared/src/main/scala/sigma/util/NBitsUtils.scala @@ -0,0 +1,84 @@ +package sigma.util + +import java.math.BigInteger + +object NBitsUtils { + + /** + *

The "compact" format is a representation of a whole number N using an unsigned 32 bit number similar to a + * floating point format. The most significant 8 bits are the unsigned exponent of base 256. This exponent can + * be thought of as "number of bytes of N". The lower 23 bits are the mantissa. Bit number 24 (0x800000) represents + * the sign of N. Therefore, N = (-1^sign) * mantissa * 256^(exponent-3).

+ * + *

Satoshi's original implementation used BN_bn2mpi() and BN_mpi2bn(). MPI uses the most significant bit of the + * first byte as sign. Thus 0x1234560000 is compact 0x05123456 and 0xc0de000000 is compact 0x0600c0de. Compact + * 0x05c0de00 would be -0x40de000000.

+ * + *

Bitcoin only uses this "compact" format for encoding difficulty targets, which are unsigned 256bit quantities. + * Thus, all the complexities of the sign bit and using base 256 are probably an implementation accident.

+ */ + def decodeCompactBits(compact: Long): BigInt = { + val size: Int = (compact >> 24).toInt & 0xFF + val bytes: Array[Byte] = new Array[Byte](4 + size) + bytes(3) = size.toByte + if (size >= 1) bytes(4) = ((compact >> 16) & 0xFF).toByte + if (size >= 2) bytes(5) = ((compact >> 8) & 0xFF).toByte + if (size >= 3) bytes(6) = (compact & 0xFF).toByte + decodeMPI(bytes) + } + + /** + * @see Utils#decodeCompactBits(long) + */ + def encodeCompactBits(requiredDifficulty: BigInt): Long = { + val value = requiredDifficulty.bigInteger + var result: Long = 0L + var size: Int = value.toByteArray.length + if (size <= 3) { + result = value.longValue << 8 * (3 - size) + } else { + result = value.shiftRight(8 * (size - 3)).longValue + } + // The 0x00800000 bit denotes the sign. + // Thus, if it is already set, divide the mantissa by 256 and increase the exponent. + if ((result & 0x00800000L) != 0) { + result >>= 8 + size += 1 + } + result |= size << 24 + val a: Int = if (value.signum == -1) 0x00800000 else 0 + result |= a + result + } + + + /** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in big endian format. */ + def readUint32BE(bytes: Array[Byte]): Long = ((bytes(0) & 0xffL) << 24) | ((bytes(1) & 0xffL) << 16) | ((bytes(2) & 0xffL) << 8) | (bytes(3) & 0xffL) + + /** + * MPI encoded numbers are produced by the OpenSSL BN_bn2mpi function. They consist of + * a 4 byte big endian length field, followed by the stated number of bytes representing + * the number in big endian format (with a sign bit). + * + */ + private def decodeMPI(mpi: Array[Byte]): BigInteger = { + + val length: Int = readUint32BE(mpi).toInt + val buf = new Array[Byte](length) + System.arraycopy(mpi, 4, buf, 0, length) + + if (buf.length == 0) { + BigInteger.ZERO + } else { + val isNegative: Boolean = (buf(0) & 0x80) == 0x80 + if (isNegative) buf(0) = (buf(0) & 0x7f).toByte + val result: BigInteger = new BigInteger(buf) + if (isNegative) { + result.negate + } else { + result + } + } + } + +} diff --git a/data/shared/src/main/scala/org/ergoplatform/HeaderWithoutPow.scala b/data/shared/src/main/scala/org/ergoplatform/HeaderWithoutPow.scala index 4eba9b708e..21a10a5036 100644 --- a/data/shared/src/main/scala/org/ergoplatform/HeaderWithoutPow.scala +++ b/data/shared/src/main/scala/org/ergoplatform/HeaderWithoutPow.scala @@ -23,6 +23,12 @@ class HeaderWithoutPow(val version: Byte, // 1 byte def toHeader(powSolution: AutolykosSolution, bytes: Array[Byte]): ErgoHeader = ErgoHeader(version, parentId, ADProofsRoot, stateRoot, transactionsRoot, timestamp, nBits, height, extensionRoot, powSolution, votes, unparsedBytes, bytes) + + override def toString: String = { + s"HeaderWithoutPow($version, $parentId, ${bytesToId(ADProofsRoot)}, ${bytesToId(stateRoot)}, " + + s"${bytesToId(transactionsRoot)}, $timestamp, $nBits, $height, ${bytesToId(extensionRoot)}, ${bytesToId(votes)}, " + + s"${bytesToId(unparsedBytes)} )" + } } object HeaderWithoutPow { diff --git a/data/shared/src/main/scala/sigma/ast/methods.scala b/data/shared/src/main/scala/sigma/ast/methods.scala index e4cf0007e0..969effd57d 100644 --- a/data/shared/src/main/scala/sigma/ast/methods.scala +++ b/data/shared/src/main/scala/sigma/ast/methods.scala @@ -1456,11 +1456,27 @@ case object SHeaderMethods extends MonoTypeMethods { lazy val powDistanceMethod = propertyCall("powDistance", SBigInt, 14, FixedCost(JitCost(10))) lazy val votesMethod = propertyCall("votes", SByteArray, 15, FixedCost(JitCost(10))) - protected override def getMethods() = super.getMethods() ++ Seq( + // cost of checkPoW is 700 as about 2*32 hashes required, and 1 hash (id) over short data costs 10 + lazy val checkPowMethod = SMethod( + this, "checkPow", SFunc(Array(SHeader), SBoolean), 16, FixedCost(JitCost(700))) + .withIRInfo(MethodCallIrBuilder) + .withInfo(MethodCall, "Validate header's proof-of-work") + + private lazy val v5Methods = super.getMethods() ++ Seq( idMethod, versionMethod, parentIdMethod, ADProofsRootMethod, stateRootMethod, transactionsRootMethod, timestampMethod, nBitsMethod, heightMethod, extensionRootMethod, minerPkMethod, powOnetimePkMethod, - powNonceMethod, powDistanceMethod, votesMethod - ) + powNonceMethod, powDistanceMethod, votesMethod) + + // 6.0 : checkPow method added + private lazy val v6Methods = v5Methods ++ Seq(checkPowMethod) + + protected override def getMethods() = { + if (VersionContext.current.isV6SoftForkActivated) { + v6Methods + } else { + v5Methods + } + } } /** Type descriptor of `PreHeader` type of ErgoTree. */ diff --git a/data/shared/src/main/scala/sigma/data/CHeader.scala b/data/shared/src/main/scala/sigma/data/CHeader.scala index d0b8db6173..aa5a5756d2 100644 --- a/data/shared/src/main/scala/sigma/data/CHeader.scala +++ b/data/shared/src/main/scala/sigma/data/CHeader.scala @@ -4,6 +4,7 @@ import org.ergoplatform.{AutolykosSolution, ErgoHeader, HeaderWithoutPow, Header import scorex.crypto.authds.ADDigest import scorex.crypto.hash.Digest32 import scorex.util.{bytesToId, idToBytes} +import sigma.pow.Autolykos2PowValidation import sigma.{AvlTree, BigInt, Coll, Colls, GroupElement, Header} /** A default implementation of [[Header]] interface. @@ -66,10 +67,11 @@ class CHeader(val ergoHeader: ErgoHeader) extends Header with WrapperOf[ErgoHead override def wrappedValue: ErgoHeader = ergoHeader override def serializeWithoutPoW: Coll[Byte] = { - val headerWithoutPow = HeaderWithoutPow(version, bytesToId(parentId.toArray), Digest32 @@ ADProofsRoot.toArray, - ADDigest @@ stateRoot.digest.toArray, Digest32 @@ transactionsRoot.toArray, timestamp, - nBits, height, Digest32 @@ extensionRoot.toArray, votes.toArray, unparsedBytes.toArray) - Colls.fromArray(HeaderWithoutPowSerializer.toBytes(headerWithoutPow)) + Colls.fromArray(HeaderWithoutPowSerializer.toBytes(ergoHeader)) + } + + override def checkPow: Boolean = { + Autolykos2PowValidation.checkPoWForVersion2(this) } override def toString: String = diff --git a/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala b/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala index 52f839354c..6df82125df 100644 --- a/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala +++ b/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala @@ -1,6 +1,6 @@ package sigma.eval -import sigma.{AvlTree, Coll, Context} +import sigma.{AvlTree, Coll, Context, Header} import sigma.ast.{Constant, FixedCost, MethodCall, OperationCostInfo, OperationDesc, PerItemCost, SType, TypeBasedCost} import sigma.data.KeyValueColl diff --git a/data/shared/src/main/scala/sigma/pow/Autolykos2PowValidation.scala b/data/shared/src/main/scala/sigma/pow/Autolykos2PowValidation.scala new file mode 100644 index 0000000000..23cf722194 --- /dev/null +++ b/data/shared/src/main/scala/sigma/pow/Autolykos2PowValidation.scala @@ -0,0 +1,176 @@ +package sigma.pow + + +import scorex.crypto.hash.Blake2b256 +import scorex.utils.{Bytes, Ints, Longs} +import sigma.Header +import sigma.crypto.{BcDlogGroup, BigIntegers, CryptoConstants} +import sigma.util.NBitsUtils + +/** + * Functions used to validate Autolykos2 Proof-of-Work. + */ +object Autolykos2PowValidation { + + type Height = Int + + /** + * k value for k-sum problem Autolykos is based on (find k numbers in table on N size) + */ + private val k = 32 + + /** + * Initial size of N value for k-sum problem Autolykos is based on (find k numbers in table on N size). + * It grows from it since predefined block height in Autolykos 2. + */ + private val NStart = 26 + + /** + * Group order, used in Autolykos V.1 for non-outsourceability, + * and also to obtain target in both Autolykos v1 and v2 + */ + private val q: BigInt = CryptoConstants.dlogGroup.order + + /** + * Number of elements in a table to find k-sum problem solution on top of + */ + val NBase: Int = Math.pow(2, NStart.toDouble).toInt + + /** + * Initial height since which table (`N` value) starting to increase by 5% per `IncreasePeriodForN` blocks + */ + val IncreaseStart: Height = 600 * 1024 + + /** + * Table size (`N`) increased every 50 * 1024 blocks + */ + val IncreasePeriodForN: Height = 50 * 1024 + + /** + * On this height, the table (`N` value) will stop to grow. + * Max N on and after this height would be 2,143,944,600 which is still less than 2^^31. + */ + val NIncreasementHeightMax: Height = 4198400 + + /** + * Blake2b256 hash function invocation + * @param in - input bit-string + * @return - 256 bits (32 bytes) array + */ + def hash(in: Array[Byte]): Array[Byte] = Blake2b256.hash(in) + + /** + * Convert byte array to unsigned integer + * @param in - byte array + * @return - unsigned integer + */ + def toBigInt(in: Array[Byte]): BigInt = BigInt(BigIntegers.fromUnsignedByteArray(in)) + + /** + * Constant data to be added to hash function to increase its calculation time + */ + val M: Array[Byte] = (0 until 1024).toArray.flatMap(i => Longs.toByteArray(i.toLong)) + + /** + * Calculates table size (N value) for a given height (moment of time) + * + * @see papers/yellow/pow/ErgoPow.tex for full description and test vectors + * @param headerHeight - height of a header to mine + * @return - N value + */ + def calcN(headerHeight: Height): Int = { + val height = Math.min(NIncreasementHeightMax, headerHeight) + if (height < IncreaseStart) { + NBase + } else { + val itersNumber = (height - IncreaseStart) / IncreasePeriodForN + 1 + (1 to itersNumber).foldLeft(NBase) { case (step, _) => + step / 100 * 105 + } + } + } + + def calcN(header: Header): Int = calcN(header.height) + + /** + * Hash function that takes `m` and `nonceBytes` and returns a list of size `k` with numbers in + * [0,`N`) + */ + private def genIndexes(k: Int, seed: Array[Byte], N: Int): Seq[Int] = { + val hash = Blake2b256(seed) + val extendedHash = Bytes.concat(hash, hash.take(3)) + (0 until k).map { i => + BigInt(1, extendedHash.slice(i, i + 4)).mod(N).toInt + } + }.ensuring(_.length == k) + + /** + * Generate element of Autolykos equation. + */ + private def genElementV2(indexBytes: Array[Byte], heightBytes: => Array[Byte]): BigInt = { + // Autolykos v. 2: H(j|h|M) (line 5 from the Algo 2 of the spec) + toBigInt(hash(Bytes.concat(indexBytes, heightBytes, M)).drop(1)) + } + + def hitForVersion2ForMessage(k: Int, msg: Array[Byte], nonce: Array[Byte], h: Array[Byte], N: Int): BigInt = { + + val prei8 = BigIntegers.fromUnsignedByteArray(hash(Bytes.concat(msg, nonce)).takeRight(8)) + val i = BigIntegers.asUnsignedByteArray(4, prei8.mod(BigInt(N).underlying())) + val f = Blake2b256(Bytes.concat(i, h, M)).drop(1) // .drop(1) is the same as takeRight(31) + val seed = Bytes.concat(f, msg, nonce) // Autolykos v1, Alg. 2, line4: + + val indexes = genIndexes(k, seed, N) + //pk and w not used in v2 + val elems = indexes.map(idx => genElementV2(Ints.toByteArray(idx), h)) + val f2 = elems.sum + + // sum as byte array is always about 32 bytes + val array: Array[Byte] = BigIntegers.asUnsignedByteArray(32, f2.underlying()) + val ha = hash(array) + toBigInt(ha) + } + + /** + * Header digest ("message" for default GPU miners) a miner is working on + */ + def msgByHeader(h: Header): Array[Byte] = Blake2b256(h.serializeWithoutPoW.toArray) + + /** + * Get hit for Autolykos v2 header (to test it then against PoW target) + * + * @param header - header to check PoW for + * @return PoW hit + */ + def hitForVersion2(header: Header): BigInt = { + + val msg = msgByHeader(header) + val nonce = header.powNonce + + val h = Ints.toByteArray(header.height) // used in AL v.2 only + + val N = calcN(header) + + hitForVersion2ForMessage(k, msg, nonce.toArray, h, N) + } + + /** + * Get target `b` from encoded difficulty `nBits` + */ + def getB(nBits: Long): BigInt = { + q / NBitsUtils.decodeCompactBits(nBits) + } + + /** + * Check PoW for Autolykos v2 header + * + * @param header - header to check PoW for + * @return whether PoW is valid or not + */ + def checkPoWForVersion2(header: Header): Boolean = { + val b = getB(header.nBits) + // for version 2, we're calculating hit and compare it with target + val hit = hitForVersion2(header) + hit < b + } + +} diff --git a/docs/LangSpec.md b/docs/LangSpec.md index ba66748f08..1f05a3b403 100644 --- a/docs/LangSpec.md +++ b/docs/LangSpec.md @@ -249,6 +249,10 @@ class Context { /** Represents data of the block headers available in scripts. */ class Header { + + /** Validate header's proof-of-work */ + def checkPow: Boolean + /** Bytes representation of ModifierId of this Header */ def id: Coll[Byte] diff --git a/interpreter/shared/src/main/scala/sigmastate/interpreter/CErgoTreeEvaluator.scala b/interpreter/shared/src/main/scala/sigmastate/interpreter/CErgoTreeEvaluator.scala index 2f7d527e74..a72510f641 100644 --- a/interpreter/shared/src/main/scala/sigmastate/interpreter/CErgoTreeEvaluator.scala +++ b/interpreter/shared/src/main/scala/sigmastate/interpreter/CErgoTreeEvaluator.scala @@ -10,10 +10,12 @@ import sigma.util.Extensions._ import debox.{cfor, Buffer => DBuffer} import scorex.crypto.authds.ADKey import sigma.ast.SAvlTreeMethods._ +import sigma.ast.SHeaderMethods.checkPowMethod import sigma.ast.SType import sigma.data.{CSigmaProp, KeyValueColl, SigmaBoolean} import sigma.eval.{AvlTreeVerifier, ErgoTreeEvaluator, EvalSettings, Profiler} import sigma.eval.ErgoTreeEvaluator.DataEnv +import sigmastate.interpreter.CErgoTreeEvaluator.fixedCostOp import scala.collection.compat.immutable.ArraySeq import scala.util.{DynamicVariable, Failure, Success} @@ -449,7 +451,7 @@ object CErgoTreeEvaluator { * HOTSPOT: don't beautify the code * Note, `null` is used instead of Option to avoid allocations. */ - def fixedCostOp[R <: AnyRef](costInfo: OperationCostInfo[FixedCost]) + def fixedCostOp[R](costInfo: OperationCostInfo[FixedCost]) (block: => R)(implicit E: ErgoTreeEvaluator): R = { if (E != null) { var res: R = null.asInstanceOf[R] diff --git a/interpreter/shared/src/test/scala/sigma/serialization/MethodCallSerializerSpecification.scala b/interpreter/shared/src/test/scala/sigma/serialization/MethodCallSerializerSpecification.scala index 1db166c685..f93f0ccaf7 100644 --- a/interpreter/shared/src/test/scala/sigma/serialization/MethodCallSerializerSpecification.scala +++ b/interpreter/shared/src/test/scala/sigma/serialization/MethodCallSerializerSpecification.scala @@ -45,4 +45,27 @@ class MethodCallSerializerSpecification extends SerializationSpecification { } ) } + + property("MethodCall deserialization round trip for Header.checkPow") { + def code = { + val bi = HeaderConstant(headerGen.sample.get) + val expr = MethodCall(bi, + SHeaderMethods.checkPowMethod, + Vector(), + Map() + ) + roundTripTest(expr) + } + + VersionContext.withVersions(VersionContext.V6SoftForkVersion, 1) { + code + } + + an[Exception] should be thrownBy ( + VersionContext.withVersions((VersionContext.V6SoftForkVersion - 1).toByte, 1) { + code + } + ) + } + } diff --git a/sc/shared/src/main/scala/sigma/compiler/ir/GraphBuilding.scala b/sc/shared/src/main/scala/sigma/compiler/ir/GraphBuilding.scala index 7c7b80d39a..f9fc2b2545 100644 --- a/sc/shared/src/main/scala/sigma/compiler/ir/GraphBuilding.scala +++ b/sc/shared/src/main/scala/sigma/compiler/ir/GraphBuilding.scala @@ -13,7 +13,7 @@ import sigma.data.{CSigmaDslBuilder, ExactIntegral, ExactNumeric, ExactOrdering, import sigma.exceptions.GraphBuildingException import sigma.serialization.OpCodes import sigma.util.Extensions.ByteOps -import sigma.{SigmaException, ast} +import sigma.{SigmaException, VersionContext, ast} import sigmastate.interpreter.Interpreter.ScriptEnv import scala.collection.mutable.ArrayBuffer @@ -1137,6 +1137,8 @@ trait GraphBuilding extends Base with DefRewriting { IR: IRContext => h.powDistance case SHeaderMethods.votesMethod.name => h.votes + case SHeaderMethods.checkPowMethod.name if VersionContext.current.isV6SoftForkActivated => + h.checkPow case _ => throwError } case (g: Ref[SigmaDslBuilder]@unchecked, SGlobalMethods) => method.name match { diff --git a/sc/shared/src/main/scala/sigma/compiler/ir/GraphIRReflection.scala b/sc/shared/src/main/scala/sigma/compiler/ir/GraphIRReflection.scala index 69736a0224..d4512e1297 100644 --- a/sc/shared/src/main/scala/sigma/compiler/ir/GraphIRReflection.scala +++ b/sc/shared/src/main/scala/sigma/compiler/ir/GraphIRReflection.scala @@ -407,6 +407,9 @@ object GraphIRReflection { }, mkMethod(clazz, "powDistance", Array[Class[_]]()) { (obj, args) => obj.asInstanceOf[ctx.Header].powDistance + }, + mkMethod(clazz, "checkPow", Array[Class[_]]()) { (obj, args) => + obj.asInstanceOf[ctx.Header].checkPow } ) ) diff --git a/sc/shared/src/main/scala/sigma/compiler/ir/wrappers/sigma/SigmaDslUnit.scala b/sc/shared/src/main/scala/sigma/compiler/ir/wrappers/sigma/SigmaDslUnit.scala index 2a6a341686..f38748bbe4 100644 --- a/sc/shared/src/main/scala/sigma/compiler/ir/wrappers/sigma/SigmaDslUnit.scala +++ b/sc/shared/src/main/scala/sigma/compiler/ir/wrappers/sigma/SigmaDslUnit.scala @@ -78,6 +78,7 @@ import scalan._ def powNonce: Ref[Coll[Byte]]; def powDistance: Ref[BigInt]; def votes: Ref[Coll[Byte]] + def checkPow: Ref[Boolean] }; trait Context extends Def[Context] { def OUTPUTS: Ref[Coll[Box]]; diff --git a/sc/shared/src/main/scala/sigma/compiler/ir/wrappers/sigma/impl/SigmaDslImpl.scala b/sc/shared/src/main/scala/sigma/compiler/ir/wrappers/sigma/impl/SigmaDslImpl.scala index c113cb7de3..e73d3bbeec 100644 --- a/sc/shared/src/main/scala/sigma/compiler/ir/wrappers/sigma/impl/SigmaDslImpl.scala +++ b/sc/shared/src/main/scala/sigma/compiler/ir/wrappers/sigma/impl/SigmaDslImpl.scala @@ -1368,6 +1368,13 @@ object Header extends EntityObject("Header") { ArraySeq.empty, true, false, element[Coll[Byte]])) } + + override def checkPow: Ref[Boolean] = { + asRep[Boolean](mkMethodCall(self, + HeaderClass.getMethod("checkPow"), + ArraySeq.empty, + true, false, element[Boolean])) + } } implicit object LiftableHeader @@ -1492,6 +1499,13 @@ object Header extends EntityObject("Header") { ArraySeq.empty, true, true, element[Coll[Byte]])) } + + def checkPow: Ref[Boolean] = { + asRep[Boolean](mkMethodCall(source, + HeaderClass.getMethod("checkPow"), + ArraySeq.empty, + true, true, element[Boolean])) + } } // entityUnref: single unref method for each type family @@ -1509,7 +1523,7 @@ object Header extends EntityObject("Header") { override protected def collectMethods: Map[RMethod, MethodDesc] = { super.collectMethods ++ Elem.declaredMethods(RClass(classOf[Header]), RClass(classOf[SHeader]), Set( - "id", "version", "parentId", "ADProofsRoot", "stateRoot", "transactionsRoot", "timestamp", "nBits", "height", "extensionRoot", "minerPk", "powOnetimePk", "powNonce", "powDistance", "votes" + "id", "version", "parentId", "ADProofsRoot", "stateRoot", "transactionsRoot", "timestamp", "nBits", "height", "extensionRoot", "minerPk", "powOnetimePk", "powNonce", "powDistance", "votes", "checkPow" )) } } diff --git a/sc/shared/src/test/scala/sigma/LanguageSpecificationV6.scala b/sc/shared/src/test/scala/sigma/LanguageSpecificationV6.scala index 382c47403c..61859a81cc 100644 --- a/sc/shared/src/test/scala/sigma/LanguageSpecificationV6.scala +++ b/sc/shared/src/test/scala/sigma/LanguageSpecificationV6.scala @@ -1,10 +1,12 @@ package sigma +import org.ergoplatform.ErgoHeader +import scorex.util.encode.Base16 import sigma.ast.ErgoTree.ZeroHeader import sigma.ast.SCollection.SByteArray import sigma.ast.syntax.TrueSigmaProp import sigma.ast._ -import sigma.data.{CBigInt, ExactNumeric} +import sigma.data.{CBigInt, CHeader, ExactNumeric} import sigma.eval.{CostDetails, SigmaDsl, TracedCost} import sigma.util.Extensions.{BooleanOps, ByteOps, IntOps, LongOps} import sigmastate.exceptions.MethodNotFound @@ -463,4 +465,35 @@ class LanguageSpecificationV6 extends LanguageSpecificationBase { suite => tree.root shouldBe t2.root } + property("Header new methods") { + + def checkPoW = { + newFeature( + { (x: Header) => x.checkPow}, + "{ (x: Header) => x.checkPow }", + FuncValue( + Array((1, SHeader)), + MethodCall.typed[Value[SBoolean.type]]( + ValUse(1, SHeader), + SHeaderMethods.checkPowMethod, + IndexedSeq(), + Map() + ) + ), + sinceVersion = VersionContext.V6SoftForkVersion + ) + } + + // bytes of real mainnet block header at height 614,440 + val headerBytes = "02ac2101807f0000ca01ff0119db227f202201007f62000177a080005d440896d05d3f80dcff7f5e7f59007294c180808d0158d1ff6ba10000f901c7f0ef87dcfff17fffacb6ff7f7f1180d2ff7f1e24ffffe1ff937f807f0797b9ff6ebdae007e5c8c00b8403d3701557181c8df800001b6d5009e2201c6ff807d71808c00019780f087adb3fcdbc0b3441480887f80007f4b01cf7f013ff1ffff564a0000b9a54f00770e807f41ff88c00240000080c0250000000003bedaee069ff4829500b3c07c4d5fe6b3ea3d3bf76c5c28c1d4dcdb1bed0ade0c0000000000003105" + val header1 = new CHeader(ErgoHeader.sigmaSerializer.fromBytes(Base16.decode(headerBytes).get)) + + verifyCases( + Seq( + header1 -> new Expected(ExpectedResult(Success(true), None)) + ), + checkPoW + ) + } + } diff --git a/sc/shared/src/test/scala/sigma/SigmaDslTesting.scala b/sc/shared/src/test/scala/sigma/SigmaDslTesting.scala index 58873449b4..4410786fd4 100644 --- a/sc/shared/src/test/scala/sigma/SigmaDslTesting.scala +++ b/sc/shared/src/test/scala/sigma/SigmaDslTesting.scala @@ -872,10 +872,14 @@ class SigmaDslTesting extends AnyPropSpec extends Feature[A, B] { override def isSupportedIn(vc: VersionContext): Boolean = - vc.activatedVersion >= sinceVersion && vc.ergoTreeVersion >= sinceVersion + vc.activatedVersion >= sinceVersion override def scalaFunc: A => B = { x => - sys.error(s"Semantic Scala function is not defined for old implementation: $this") + if (isSupportedIn(VersionContext.current)) { + scalaFuncNew(x) + } else { + sys.error(s"Semantic Scala function is not defined for old implementation: $this") + } } implicit val cs = compilerSettingsInTests @@ -925,8 +929,14 @@ class SigmaDslTesting extends AnyPropSpec printTestCases: Boolean, failOnTestVectors: Boolean): Unit = { val funcRes = checkEquality(input, printTestCases) - funcRes.isFailure shouldBe true - Try(scalaFunc(input)) shouldBe expected.value + if(!isSupportedIn(VersionContext.current)) { + funcRes.isFailure shouldBe true + } + if(isSupportedIn(VersionContext.current)) { + Try(scalaFunc(input)) shouldBe expected.value + } else { + Try(scalaFunc(input)).isFailure shouldBe true + } } } diff --git a/sc/shared/src/test/scala/sigmastate/TestingInterpreterSpecification.scala b/sc/shared/src/test/scala/sigmastate/TestingInterpreterSpecification.scala index 6265462c6c..4ba6b1a9f7 100644 --- a/sc/shared/src/test/scala/sigmastate/TestingInterpreterSpecification.scala +++ b/sc/shared/src/test/scala/sigmastate/TestingInterpreterSpecification.scala @@ -6,16 +6,13 @@ import sigma.ast._ import sigma.ast.syntax._ import sigmastate.interpreter._ import Interpreter._ -import io.circe.parser.parse import org.ergoplatform._ -import org.ergoplatform.sdk.JsonCodecs import org.scalatest.BeforeAndAfterAll -import scorex.util.encode.Base58 +import scorex.util.encode.{Base16, Base58} import sigma.Colls import sigma.VersionContext.V6SoftForkVersion import sigma.VersionContext -import sigma.crypto.CryptoConstants -import sigma.data.{CAND, CAvlTree, ProveDlog, SigmaBoolean, TrivialProp} +import sigma.data.{CAND, CAvlTree, CHeader, ProveDlog, SigmaBoolean, TrivialProp} import sigma.interpreter.ContextExtension import sigma.util.Extensions.IntOps import sigmastate.helpers.{CompilerTestingCommons, ErgoLikeContextTesting, ErgoLikeTestInterpreter, ErgoLikeTestProvingInterpreter} @@ -39,40 +36,9 @@ class TestingInterpreterSpecification extends CompilerTestingCommons lazy val verifier = new ErgoLikeTestInterpreter def testingContext(h: Int = 614401) = { - - // valid header from Ergo blockchain - val headerJson = - """ - |{ - | "extensionId" : "00cce45975d87414e8bdd8146bc88815be59cd9fe37a125b5021101e05675a18", - | "votes" : "000000", - | "timestamp" : 4928911477310178288, - | "size" : 223, - | "unparsedBytes" : "", - | "stateRoot" : { - | "digest" : "5c8c00b8403d3701557181c8df800001b6d5009e2201c6ff807d71808c00019780", - | "treeFlags" : "0", - | "keyLength" : "32" - | }, - | "height" : 614400, - | "nBits" : 37748736, - | "version" : 2, - | "id" : "5603a937ec1988220fc44fb5022fb82d5565b961f005ebb55d85bd5a9e6f801f", - | "adProofsRoot" : "5d3f80dcff7f5e7f59007294c180808d0158d1ff6ba10000f901c7f0ef87dcff", - | "transactionsRoot" : "f17fffacb6ff7f7f1180d2ff7f1e24ffffe1ff937f807f0797b9ff6ebdae007e", - | "extensionRoot" : "1480887f80007f4b01cf7f013ff1ffff564a0000b9a54f00770e807f41ff88c0", - | "minerPk" : "03bedaee069ff4829500b3c07c4d5fe6b3ea3d3bf76c5c28c1d4dcdb1bed0ade0c", - | "powOnetimePk" : "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - | "powNonce" : "0000000000003105", - | "powDistance" : 0, - | "adProofsId" : "dec129290a763f4de41f04e87e2b661dd59758af6bdd00dd51f5d97c3a8cb9b5", - | "transactionsId" : "eba1dd82cf51147232e09c1f72b37c554c30f63274d5093bff36849a83472a42", - | "parentId" : "ac2101807f0000ca01ff0119db227f202201007f62000177a080005d440896d0" - |} - |""".stripMargin - - object JsonCodecs extends JsonCodecs - val header1 = JsonCodecs.headerDecoder.decodeJson(parse(headerJson).toOption.get).toOption.get + // bytes of real mainnet block header at height 614,440 + val headerBytes = "02ac2101807f0000ca01ff0119db227f202201007f62000177a080005d440896d05d3f80dcff7f5e7f59007294c180808d0158d1ff6ba10000f901c7f0ef87dcfff17fffacb6ff7f7f1180d2ff7f1e24ffffe1ff937f807f0797b9ff6ebdae007e5c8c00b8403d3701557181c8df800001b6d5009e2201c6ff807d71808c00019780f087adb3fcdbc0b3441480887f80007f4b01cf7f013ff1ffff564a0000b9a54f00770e807f41ff88c00240000080c0250000000003bedaee069ff4829500b3c07c4d5fe6b3ea3d3bf76c5c28c1d4dcdb1bed0ade0c0000000000003105" + val header1 = new CHeader(ErgoHeader.sigmaSerializer.fromBytes(Base16.decode(headerBytes).get)) val boxesToSpend = IndexedSeq(fakeSelf) @@ -436,6 +402,24 @@ class TestingInterpreterSpecification extends CompilerTestingCommons verifier.verify(prop3, env, proof, challenge).map(_._1).getOrElse(false) shouldBe false } + property("blake2b - test vector") { + testEval( + """ { + | val input = fromBase16("68656c6c6f20776f726c64") + | val output = fromBase16("256c83b297114d201b30179f3f0ef0cace9783622da5974326b436178aeef610") + | blake2b256(input) == output + | }""".stripMargin) + } + + property("blake2b - test vector #2") { + testEval( + """ { + | val input = fromBase16("02ac2101807f0000ca01ff0119db227f202201007f62000177a080005d440896d05d3f80dcff7f5e7f59007294c180808d0158d1ff6ba10000f901c7f0ef87dcfff17fffacb6ff7f7f1180d2ff7f1e24ffffe1ff937f807f0797b9ff6ebdae007e5c8c00b8403d3701557181c8df800001b6d5009e2201c6ff807d71808c00019780d085adb3fcdbc0b3441480887f80007f4b01cf7f013ff1ffff564a0000b9a54f00770e807f41ff88c00240000080c02500000000") + | val output = fromBase16("bdb84cda5b105c3eb522857b50a0882f88ed5bb3cc8cf3325a1edf7eeb6a0954") + | blake2b256(input) == output + | }""".stripMargin) + } + property("passing a lambda argument") { // single expression testEval( @@ -470,6 +454,29 @@ class TestingInterpreterSpecification extends CompilerTestingCommons testEval(s"""deserialize[Coll[Byte]]("$str")(0) == 2""") } + property("header.id") { + testEval( + """ { + | val h = CONTEXT.headers(0) + | val id = h.id + | id.size == 32 + | }""".stripMargin) + } + + property("checkPow") { + val source = """ { + | val h = CONTEXT.headers(0) + | h.checkPow + | } + | """.stripMargin + + if (activatedVersionInTests < V6SoftForkVersion) { + an [Exception] should be thrownBy testEval(source) + } else { + testEval(source) + } + } + override protected def afterAll(): Unit = { } diff --git a/sdk/js/src/main/scala/org/ergoplatform/sdk/js/Header.scala b/sdk/js/src/main/scala/org/ergoplatform/sdk/js/Header.scala index ec3767c1cd..2220c9827a 100644 --- a/sdk/js/src/main/scala/org/ergoplatform/sdk/js/Header.scala +++ b/sdk/js/src/main/scala/org/ergoplatform/sdk/js/Header.scala @@ -12,7 +12,7 @@ import scala.scalajs.js.annotation.JSExportTopLevel class Header( /** Hex representation of ModifierId of this Header */ val id: String, - /** Block version, to be increased on every soft and hardfork. */ + /** Block version, to be increased on every soft- or hard-fork. */ val version: Byte, /** Hex representation of ModifierId of the parent block */ val parentId: String, diff --git a/sdk/js/src/test/scala/org/ergoplatform/sdk/js/IsosSpec.scala b/sdk/js/src/test/scala/org/ergoplatform/sdk/js/IsosSpec.scala index a040731b20..31b8a84c2b 100644 --- a/sdk/js/src/test/scala/org/ergoplatform/sdk/js/IsosSpec.scala +++ b/sdk/js/src/test/scala/org/ergoplatform/sdk/js/IsosSpec.scala @@ -1,8 +1,9 @@ package org.ergoplatform.sdk.js +import io.circe.parser.parse import org.ergoplatform.ErgoBox.{AdditionalRegisters, BoxId, TokenId} import org.ergoplatform._ -import org.ergoplatform.sdk.ExtendedInputBox +import org.ergoplatform.sdk.{ExtendedInputBox, JsonCodecs} import org.ergoplatform.sdk.wallet.protocol.context.BlockchainStateContext import org.scalacheck.Arbitrary import sigma.ast.{Constant, SType} @@ -11,6 +12,7 @@ import sigma.interpreter.{ContextExtension, ProverResult} import sigma.js.AvlTree import sigma.{Coll, GroupElement} import sigma.data.js.{Isos => DataIsos} + import scala.scalajs.js class IsosSpec extends IsosSpecBase with sdk.generators.ObjectGenerators {