Skip to content

Commit

Permalink
Merge pull request #965 from ergoplatform/i958
Browse files Browse the repository at this point in the history
[6.0.0] Autolykos 2 validation for custom message
  • Loading branch information
kushti authored Nov 22, 2024
2 parents 47b5558 + c0ea4b0 commit 6d5a18b
Show file tree
Hide file tree
Showing 18 changed files with 295 additions and 22 deletions.
3 changes: 3 additions & 0 deletions core/shared/src/main/scala/sigma/SigmaDsl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,9 @@ trait SigmaDslBuilder {
/** Returns a byte-wise XOR of the two collections of bytes. */
def xor(l: Coll[Byte], r: Coll[Byte]): Coll[Byte]

/** Calculates value of a custom Autolykos 2 hash function */
def powHit(k: Int, msg: Coll[Byte], nonce: Coll[Byte], h: Coll[Byte], N: Int): BigInt

/** Deserializes provided `bytes` into a value of type `T`. **/
def deserializeTo[T](bytes: Coll[Byte])(implicit cT: RType[T]): T

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,10 @@ object ReflectionData {
mkMethod(clazz, "fromBigEndianBytes", Array[Class[_]](cColl, classOf[RType[_]])) { (obj, args) =>
obj.asInstanceOf[SigmaDslBuilder].fromBigEndianBytes(args(0).asInstanceOf[Coll[Byte]])(args(1).asInstanceOf[RType[_]])
},
mkMethod(clazz, "powHit", Array[Class[_]](classOf[Int], cColl, cColl, cColl, classOf[Int])) { (obj, args) =>
obj.asInstanceOf[SigmaDslBuilder].powHit(args(0).asInstanceOf[Int], args(1).asInstanceOf[Coll[Byte]],
args(2).asInstanceOf[Coll[Byte]], args(3).asInstanceOf[Coll[Byte]], args(4).asInstanceOf[Int])
},
mkMethod(clazz, "encodeNbits", Array[Class[_]](classOf[BigInt])) { (obj, args) =>
obj.asInstanceOf[SigmaDslBuilder].encodeNbits(args(0).asInstanceOf[BigInt])
},
Expand Down
10 changes: 10 additions & 0 deletions data/shared/src/main/scala/sigma/SigmaDataReflection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,16 @@ object SigmaDataReflection {
obj.asInstanceOf[SGlobalMethods.type].deserializeTo_eval(args(0).asInstanceOf[MethodCall],
args(1).asInstanceOf[SigmaDslBuilder],
args(2).asInstanceOf[Coll[Byte]])(args(3).asInstanceOf[ErgoTreeEvaluator])
},
mkMethod(clazz, "powHit_eval", Array[Class[_]](classOf[MethodCall], classOf[SigmaDslBuilder], classOf[Int], classOf[Coll[_]], classOf[Coll[_]], classOf[Coll[_]], classOf[Int], classOf[ErgoTreeEvaluator])) { (obj, args) =>
obj.asInstanceOf[SGlobalMethods.type].powHit_eval(args(0).asInstanceOf[MethodCall],
args(1).asInstanceOf[SigmaDslBuilder],
args(2).asInstanceOf[Int],
args(3).asInstanceOf[Coll[Byte]],
args(4).asInstanceOf[Coll[Byte]],
args(5).asInstanceOf[Coll[Byte]],
args(6).asInstanceOf[Int]
)(args(7).asInstanceOf[ErgoTreeEvaluator])
}
)
)
Expand Down
34 changes: 34 additions & 0 deletions data/shared/src/main/scala/sigma/ast/CostKind.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package sigma.ast

import sigma.Coll

import scala.runtime.Statics

/** Cost descriptor of a single operation, usually associated with
Expand Down Expand Up @@ -52,5 +54,37 @@ abstract class TypeBasedCost extends CostKind {
* See [[EQ]], [[NEQ]]. */
case object DynamicCost extends CostKind

/**
* Cost of converting numeric value to the numeric value of the given type, i.e. Byte -> Int
*/
object NumericCastCostKind extends TypeBasedCost {
override def costFunc(targetTpe: SType): JitCost = targetTpe match {
case SBigInt => JitCost(30)
case _ => JitCost(10)
}
}

/**
* Cost of Global.powHit method, which is dependent on few parameters, see cost() function description
*/
object PowHitCostKind extends CostKind {
/**
* @param k - k parameter of Autolykos 2 (number of inputs in k-sum problem)"
* @param msg - message to calculate Autolykos hash 2 for
* @param nonce - used to pad the message to get Proof-of-Work hash function output with desirable properties
* @param h - PoW protocol specific padding for table uniqueness (e.g. block height in Ergo)
* @return cost of custom Autolykos2 hash function invocation
*/
def cost(k: Int, msg: Coll[Byte], nonce: Coll[Byte], h: Coll[Byte]): JitCost = {
val chunkSize = CalcBlake2b256.costKind.chunkSize
val perChunkCost = CalcBlake2b256.costKind.perChunkCost
val baseCost = 300

// the heaviest part inside is k + 1 Blake2b256 invocations
val c = baseCost + (k + 1) * ((msg.length + nonce.length + h.length) / chunkSize + 1) * perChunkCost.value
JitCost(c)
}
}



5 changes: 2 additions & 3 deletions data/shared/src/main/scala/sigma/ast/SMethod.scala
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,7 @@ case class SMethod(
val methodName = name + "_eval"
val m = try {
objType.thisRClass.getMethod(methodName, paramTypes:_*)
}
catch { case e: NoSuchMethodException =>
} catch { case e: NoSuchMethodException =>
throw new RuntimeException(s"Cannot find eval method def $methodName(${Seq(paramTypes:_*)})", e)
}
m
Expand Down Expand Up @@ -339,7 +338,7 @@ object SMethod {
* @return an instance of [[SMethod]] which may contain generic type variables in the
* signature (see SMethod.stype). As a result `specializeFor` is called by
* deserializer to obtain monomorphic method descriptor.
* @consensus this is method is used in [[sigmastate.serialization.MethodCallSerializer]]
* @consensus this is method is used in [[sigma.serialization.MethodCallSerializer]]
* `parse` method and hence it is part of consensus protocol
*/
def fromIds(typeId: Byte, methodId: Byte): SMethod = {
Expand Down
23 changes: 23 additions & 0 deletions data/shared/src/main/scala/sigma/ast/methods.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sigma.ast

import org.ergoplatform._
import org.ergoplatform.validation._
import sigma.{Coll, VersionContext, _}
import sigma.Evaluation.stypeToRType
import sigma._
import sigma.{VersionContext, _}
Expand All @@ -15,6 +16,7 @@ import sigma.data.NumericOps.BigIntIsExactIntegral
import sigma.data.OverloadHack.Overloaded1
import sigma.data.{CBigInt, DataValueComparer, KeyValueColl, Nullable, RType, SigmaConstants}
import sigma.eval.{CostDetails, ErgoTreeEvaluator, TracedCost}
import sigma.pow.Autolykos2PowValidation
import sigma.reflection.RClass
import sigma.serialization.CoreByteWriter.ArgInfo
import sigma.serialization.{DataSerializer, SigmaByteWriter, SigmaSerializer}
Expand Down Expand Up @@ -1825,6 +1827,26 @@ case object SGlobalMethods extends MonoTypeMethods {
.withInfo(Xor, "Byte-wise XOR of two collections of bytes",
ArgInfo("left", "left operand"), ArgInfo("right", "right operand"))

lazy val powHitMethod = SMethod(
this, "powHit", SFunc(Array(SGlobal, SInt, SByteArray, SByteArray, SByteArray, SInt), SBigInt), methodId = 8,
PowHitCostKind)
.withIRInfo(MethodCallIrBuilder)
.withInfo(MethodCall,
"Calculating Proof-of-Work hit (Autolykos 2 hash value) for custom Autolykos 2 function",
ArgInfo("k", "k parameter of Autolykos 2 (number of inputs in k-sum problem)"),
ArgInfo("msg", "Message to calculate Autolykos hash 2 for"),
ArgInfo("nonce", "Nonce used to pad the message to get Proof-of-Work hash function output with desirable properties"),
ArgInfo("h", "PoW protocol specific padding for table uniqueness (e.g. block height in Ergo)"),
ArgInfo("N", "Size of table filled with pseudo-random data to find k elements in")
)

def powHit_eval(mc: MethodCall, G: SigmaDslBuilder, k: Int, msg: Coll[Byte], nonce: Coll[Byte], h: Coll[Byte], N: Int)
(implicit E: ErgoTreeEvaluator): BigInt = {
val cost = PowHitCostKind.cost(k, msg, nonce, h)
E.addCost(FixedCost(cost), powHitMethod.opDesc)
CBigInt(Autolykos2PowValidation.hitForVersion2ForMessageWithChecks(k, msg.toArray, nonce.toArray, h.toArray, N).bigInteger)
}

private val deserializeCostKind = PerItemCost(baseCost = JitCost(30), perChunkCost = JitCost(20), chunkSize = 32)

lazy val deserializeToMethod = SMethod(
Expand Down Expand Up @@ -1913,6 +1935,7 @@ case object SGlobalMethods extends MonoTypeMethods {
groupGeneratorMethod,
xorMethod,
serializeMethod,
powHitMethod,
deserializeToMethod,
encodeNBitsMethod,
decodeNBitsMethod,
Expand Down
12 changes: 0 additions & 12 deletions data/shared/src/main/scala/sigma/ast/trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -414,18 +414,6 @@ trait NumericCastCompanion extends ValueCompanion {
def costKind: TypeBasedCost = NumericCastCostKind
}

/** Cost of:
* 1) converting numeric value to the numeric value of the given type, i.e. Byte -> Int
* NOTE: the cost of BigInt casting is the same in JITC (comparing to AOTC) to simplify
* implementation.
*/
object NumericCastCostKind extends TypeBasedCost {
override def costFunc(targetTpe: SType): JitCost = targetTpe match {
case SBigInt => JitCost(30)
case _ => JitCost(10)
}
}

object Upcast extends NumericCastCompanion {
override def opCode: OpCode = OpCodes.UpcastCode
override def argInfos: Seq[ArgInfo] = UpcastInfo.argInfos
Expand Down
9 changes: 8 additions & 1 deletion data/shared/src/main/scala/sigma/data/CSigmaDslBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import sigma.crypto.{CryptoConstants, EcPointType, Ecp}
import sigma.eval.Extensions.EvalCollOps
import sigma.serialization.{ConstantStore, DataSerializer, GroupElementSerializer, SigmaByteReader, SigmaSerializer}
import sigma.serialization.{DataSerializer, GroupElementSerializer, SigmaSerializer}
import sigma.serialization.{GroupElementSerializer, SerializerException, SigmaSerializer}
import sigma.serialization.SerializerException
import sigma.pow.Autolykos2PowValidation
import sigma.util.Extensions.BigIntegerOps
import sigma.util.NBitsUtils
import sigma.validation.SigmaValidationSettings
Expand Down Expand Up @@ -259,6 +260,12 @@ class CSigmaDslBuilder extends SigmaDslBuilder { dsl =>
Colls.fromArray(w.toBytes)
}

override def powHit(k: Int, msg: Coll[Byte], nonce: Coll[Byte], h: Coll[Byte], N: Int): BigInt = {
val bi = Autolykos2PowValidation.hitForVersion2ForMessageWithChecks(k, msg.toArray, nonce.toArray, h.toArray, N)
this.BigInt(bi.bigInteger)
}


def deserializeTo[T](bytes: Coll[Byte])(implicit cT: RType[T]): T = {
val tpe = rtypeToSType(cT)
val reader = new SigmaByteReader(new VLQByteBufferReader(ByteBuffer.wrap(bytes.toArray)), new ConstantStore(), false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,14 @@ object Autolykos2PowValidation {
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 = {
def hitForVersion2ForMessageWithChecks(k: Int, msg: Array[Byte], nonce: Array[Byte], h: Array[Byte], N: Int): BigInt = {
require(k >= 2) // at least 2 elements needed for sum
require(k <= 32) // genIndexes function of Autolykos2 not supporting k > 32
require(N >= 16) // min table size
hitForVersion2ForMessage(k, msg, nonce, h, N)
}

private 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package sigma.serialization

import scorex.utils.Ints
import sigma.VersionContext
import sigma.ast.SCollection.SByteArray
import sigma.ast.SType.tT
Expand Down Expand Up @@ -41,7 +42,34 @@ class MethodCallSerializerSpecification extends SerializationSpecification {
code
}

an[SerializerException] should be thrownBy (
a[SerializerException] should be thrownBy (
VersionContext.withVersions((VersionContext.V6SoftForkVersion - 1).toByte, 1) {
code
}
)
}

property("MethodCall deserialization round trip for Global.powHit") {
val k = IntConstant(32)
val msg = ByteArrayConstant(Array.fill(5)(1.toByte))
val nonce = ByteArrayConstant(Array.fill(8)(2.toByte))
val h = ByteArrayConstant(Ints.toByteArray(5))
val N = IntConstant(1024 * 1024)

def code = {
val expr = MethodCall(Global,
SGlobalMethods.powHitMethod,
Vector(k, msg, nonce, h, N),
Map()
)
roundTripTest(expr)
}

VersionContext.withVersions(VersionContext.V6SoftForkVersion, 1) {
code
}

an[ValidationException] should be thrownBy (
VersionContext.withVersions((VersionContext.V6SoftForkVersion - 1).toByte, 1) {
code
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package sigmastate.eval

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import sigma.ast.{BigIntConstant, ErgoTree, Global, JitCost, MethodCall, SBigIntMethods, SGlobalMethods}
import sigma.ast.{BigIntConstant, ErgoTree, Global, JitCost, MethodCall, SGlobalMethods}
import scorex.util.encode.Base16
import sigma.Extensions.ArrayOps
import sigma.ast.{ByteArrayConstant, IntConstant}
import sigma.crypto.SecP256K1Group
import sigma.data.{CBigInt, TrivialProp}
import sigma.eval.SigmaDsl
Expand All @@ -13,6 +16,7 @@ import java.math.BigInteger
import sigma.{ContractsTestkit, SigmaProp}
import sigmastate.interpreter.{CErgoTreeEvaluator, CostAccumulator}
import sigmastate.interpreter.CErgoTreeEvaluator.DefaultProfiler
import sigma.{Box, VersionContext}

import scala.language.implicitConversions

Expand Down Expand Up @@ -68,6 +72,69 @@ class BasicOpsTests extends AnyFunSuite with ContractsTestkit with Matchers {
box.creationInfo._1 shouldBe a [Integer]
}

test("xor evaluation") {
val es = CErgoTreeEvaluator.DefaultEvalSettings
val accumulator = new CostAccumulator(
initialCost = JitCost(0),
costLimit = Some(JitCost.fromBlockCost(es.scriptCostLimitInEvaluator)))

val context = new CContext(
noInputs.toColl, noHeaders, dummyPreHeader,
Array[Box]().toColl, Array[Box]().toColl, 0, null, 0, null,
dummyPubkey.toColl, Colls.emptyColl, null, VersionContext.V6SoftForkVersion, VersionContext.V6SoftForkVersion)

val evaluator = new CErgoTreeEvaluator(
context = context,
constants = ErgoTree.EmptyConstants,
coster = accumulator, DefaultProfiler, es)

val msg = Colls.fromArray(Base16.decode("0a101b8c6a4f2e").get)
VersionContext.withVersions(VersionContext.V6SoftForkVersion, VersionContext.V6SoftForkVersion) {
val res = MethodCall(Global, SGlobalMethods.xorMethod,
IndexedSeq(ByteArrayConstant(msg), ByteArrayConstant(msg)), Map.empty)
.evalTo[sigma.Coll[Byte]](Map.empty)(evaluator)

res should be(Colls.fromArray(Base16.decode("00000000000000").get))
}
}

/**
* Checks BigInt.nbits evaluation for SigmaDSL as well as AST interpreter (MethodCall) layers
*/
test("powHit evaluation") {
val k = 32
val msg = Colls.fromArray(Base16.decode("0a101b8c6a4f2e").get)
val nonce = Colls.fromArray(Base16.decode("000000000000002c").get)
val hbs = Colls.fromArray(Base16.decode("00000000").get)
val N = 1024 * 1024

SigmaDsl.powHit(k, msg, nonce, hbs, N) shouldBe CBigInt(new BigInteger("326674862673836209462483453386286740270338859283019276168539876024851191344"))

val es = CErgoTreeEvaluator.DefaultEvalSettings
val accumulator = new CostAccumulator(
initialCost = JitCost(0),
costLimit = Some(JitCost.fromBlockCost(es.scriptCostLimitInEvaluator)))

val context = new CContext(
noInputs.toColl, noHeaders, dummyPreHeader,
Array[Box]().toColl, Array[Box]().toColl, 0, null, 0, null,
dummyPubkey.toColl, Colls.emptyColl, null, VersionContext.V6SoftForkVersion, VersionContext.V6SoftForkVersion)

val evaluator = new CErgoTreeEvaluator(
context = context,
constants = ErgoTree.EmptyConstants,
coster = accumulator, DefaultProfiler, es)

VersionContext.withVersions(VersionContext.V6SoftForkVersion, VersionContext.V6SoftForkVersion) {
val res = MethodCall(Global, SGlobalMethods.powHitMethod,
IndexedSeq(IntConstant(k), ByteArrayConstant(msg), ByteArrayConstant(nonce),
ByteArrayConstant(hbs), IntConstant(N)), Map.empty)
.evalTo[sigma.BigInt](Map.empty)(evaluator)

res should be(CBigInt(new BigInteger("326674862673836209462483453386286740270338859283019276168539876024851191344")))
}
}

/**
* Checks BigInt.nbits evaluation for SigmaDSL as well as AST interpreter (MethodCall) layers
*/
Expand All @@ -88,7 +155,6 @@ class BasicOpsTests extends AnyFunSuite with ContractsTestkit with Matchers {
.evalTo[Long](Map.empty)(evaluator)

res should be (NBitsUtils.encodeCompactBits(0))

}

}
14 changes: 14 additions & 0 deletions sc/shared/src/main/scala/sigma/compiler/ir/GraphBuilding.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import sigma.ast.SType.tT
import sigma.{SigmaException, VersionContext, ast}
import sigma.Evaluation.stypeToRType
import sigma.ast.SType.tT
import sigma.{SigmaException, VersionContext, ast}
import sigma.ast.TypeCodes.LastConstantCode
import sigma.ast.Value.Typed
import sigma.ast.syntax.{SValue, ValueOps}
Expand Down Expand Up @@ -1185,6 +1186,19 @@ trait GraphBuilding extends Base with DefRewriting { IR: IRContext =>
case SGlobalMethods.decodeNBitsMethod.name if VersionContext.current.isV6SoftForkActivated =>
val c1 = asRep[Long](argsV(0))
g.decodeNbits(c1)
case SGlobalMethods.powHitMethod.name if VersionContext.current.isV6SoftForkActivated =>
val k = asRep[Int](argsV(0))
val msg = asRep[Coll[Byte]](argsV(1))
val nonce = asRep[Coll[Byte]](argsV(2))
val h = asRep[Coll[Byte]](argsV(3))
val N = asRep[Int](argsV(4))
g.powHit(k, msg, nonce, h, N)
case SGlobalMethods.encodeNBitsMethod.name if VersionContext.current.isV6SoftForkActivated =>
val c1 = asRep[BigInt](argsV(0))
g.encodeNbits(c1)
case SGlobalMethods.decodeNBitsMethod.name if VersionContext.current.isV6SoftForkActivated =>
val c1 = asRep[Long](argsV(0))
g.decodeNbits(c1)
case SGlobalMethods.deserializeToMethod.name if VersionContext.current.isV6SoftForkActivated =>
val c1 = asRep[Coll[Byte]](argsV(0))
val c2 = stypeToElem(method.stype.tRange.withSubstTypes(typeSubst))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,12 @@ object GraphIRReflection {
mkMethod(clazz, "serialize", Array[Class[_]](classOf[Base#Ref[_]])) { (obj, args) =>
obj.asInstanceOf[ctx.SigmaDslBuilder].serialize(args(0).asInstanceOf[ctx.Ref[Any]])
},
mkMethod(clazz, "powHit", Array[Class[_]](classOf[Base#Ref[_]], classOf[Base#Ref[_]],
classOf[Base#Ref[_]], classOf[Base#Ref[_]], classOf[Base#Ref[_]])) { (obj, args) =>
obj.asInstanceOf[ctx.SigmaDslBuilder].powHit(args(0).asInstanceOf[ctx.Ref[Int]],
args(1).asInstanceOf[ctx.Ref[ctx.Coll[Byte]]], args(2).asInstanceOf[ctx.Ref[ctx.Coll[Byte]]],
args(3).asInstanceOf[ctx.Ref[ctx.Coll[Byte]]], args(4).asInstanceOf[ctx.Ref[Int]])
},
mkMethod(clazz, "encodeNbits", Array[Class[_]](classOf[Base#Ref[_]])) { (obj, args) =>
obj.asInstanceOf[ctx.SigmaDslBuilder].encodeNbits(args(0).asInstanceOf[ctx.Ref[ctx.BigInt]])
},
Expand Down
Loading

0 comments on commit 6d5a18b

Please sign in to comment.