Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6.0.0] Autolykos 2 validation for custom message #965

Merged
merged 1 commit into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading