Skip to content

Commit

Permalink
Merge pull request #721 from ScorexFoundation/fix-cost
Browse files Browse the repository at this point in the history
Release v4.0.3
  • Loading branch information
aslesarenko authored Mar 25, 2021
2 parents fada073 + e2faf2e commit 62dab57
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 19 deletions.
18 changes: 18 additions & 0 deletions sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,24 @@ object Evaluation {

def msgCostLimitError(cost: Long, limit: Long) = s"Estimated execution cost $cost exceeds the limit $limit"

/** Helper method to accumulate cost while checking limit.
* @param current current cost value
* @param more additional cost to add to the current value
* @param limit total cost limit
* @return new increased cost when it doesn't exceed the limit
* @throws CostLimitException
*/
def addCostChecked(current: Long, more: Long, limit: Long): Long = {
val newCost = Math.addExact(current, more)
if (newCost > limit) {
throw new CostLimitException(
estimatedCost = newCost,
message = msgCostLimitError(newCost, limit), cause = None)
}
newCost
}


/** Transforms a serializable ErgoTree type descriptor to the corresponding RType descriptor of SigmaDsl,
* which is used during evaluation.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ trait Interpreter extends ScorexLogging {
case SigmaPropConstant(p) =>
val sb = SigmaDsl.toSigmaBoolean(p)
val cost = SigmaBoolean.estimateCost(sb)
(sb, cost)
val resCost = Evaluation.addCostChecked(context.initCost, cost, context.costLimit)
(sb, resCost)
case _ if !ergoTree.hasDeserialize =>
val r = precompiledScriptProcessor.getReducer(ergoTree, context.validationSettings)
r.reduce(context)
Expand Down
54 changes: 51 additions & 3 deletions sigmastate/src/test/scala/sigmastate/CostingSpecification.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package sigmastate

import org.ergoplatform.SigmaConstants.ScriptCostLimit
import org.ergoplatform.validation.ValidationRules
import org.ergoplatform.{ErgoLikeContext, ErgoBox}
import org.ergoplatform.{ErgoBox, ErgoLikeContext}
import scorex.crypto.authds.avltree.batch.Lookup
import scorex.crypto.authds.{ADDigest, ADKey}
import scorex.crypto.hash.Blake2b256
Expand All @@ -13,10 +13,12 @@ import sigmastate.eval._
import sigmastate.helpers.TestingHelpers._
import sigmastate.helpers.{ContextEnrichingTestProvingInterpreter, ErgoLikeTestInterpreter}
import sigmastate.interpreter.ContextExtension
import sigmastate.interpreter.Interpreter.{ScriptNameProp, ScriptEnv, emptyEnv}
import sigmastate.interpreter.Interpreter.{ScriptEnv, ScriptNameProp, emptyEnv}
import sigmastate.utxo.CostTable
import sigmastate.utxo.CostTable._
import special.sigma.{SigmaTestingData, AvlTree}
import special.sigma.{AvlTree, SigmaTestingData}
import sigmastate.lang.Terms.ValueOps
import sigmastate.lang.exceptions.CostLimitException

class CostingSpecification extends SigmaTestingData with CrossVersionProps {
implicit lazy val IR = new TestingIRContext {
Expand Down Expand Up @@ -404,6 +406,52 @@ class CostingSpecification extends SigmaTestingData with CrossVersionProps {
cost shouldBe expectedCost
}

property("ErgoTree with SigmaPropConstant costs") {
val d = new TestData; import d._

def proveAndVerify(ctx: ErgoLikeContext, tree: ErgoTree, expectedCost: Long) = {
val pr = interpreter.prove(tree, ctx, fakeMessage).get
pr.cost shouldBe expectedCost

val verifier = new ErgoLikeTestInterpreter
val cost = verifier.verify(emptyEnv, tree, ctx, pr, fakeMessage).get._2
cost shouldBe expectedCost
}

// simple trees containing SigmaPropConstant
val tree1 = ErgoTree.fromSigmaBoolean(pkA) // without segregation
val tree2 = ErgoTree.withSegregation(pkA) // with segregation, have different `complexity`

{
val ctx = context.withInitCost(0)
proveAndVerify(ctx, tree1, expectedCost = 10141)
proveAndVerify(ctx, tree2, expectedCost = 10161)
}

{
val ctx = context.withInitCost(10000)
proveAndVerify(ctx, tree1, expectedCost = 20141)
proveAndVerify(ctx, tree2, expectedCost = 20161)
}

{
val ctx = context.withInitCost(10000).withCostLimit(20000)
assertExceptionThrown(
proveAndVerify(ctx, tree1, expectedCost = 20141),
exceptionLike[CostLimitException](
"Estimated execution cost", "exceeds the limit")
)
}

// more complex tree without Deserialize
val tree3 = ErgoTree.fromProposition(compiler
.compile(env, "{ sigmaProp(HEIGHT == 2) }")
.asSigmaProp)

proveAndVerify(context.withInitCost(0), tree3, expectedCost = 541)
proveAndVerify(context.withInitCost(10000), tree3, expectedCost = 10541)
}

property("laziness of AND, OR costs") {
val d = new TestData; import d._
cost("{ val cond = getVar[Boolean](2).get; !(!cond && (1 / 0 == 1)) }")(
Expand Down
13 changes: 13 additions & 0 deletions sigmastate/src/test/scala/sigmastate/helpers/NegativeTesting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sigmastate.helpers
import org.scalatest.Matchers

import scala.annotation.tailrec
import scala.reflect.ClassTag

trait NegativeTesting extends Matchers {

Expand Down Expand Up @@ -33,4 +34,16 @@ trait NegativeTesting extends Matchers {
if (t.getCause == null) t
else rootCause(t.getCause)

/** Creates an assertion which checks the given type and message contents.
*
* @tparam E expected type of exception
* @param msgParts expected parts of the exception message
* @return the assertion which can be used in assertExceptionThrown method
*/
def exceptionLike[E <: Throwable : ClassTag]
(msgParts: String*): Throwable => Boolean = {
case t: E => msgParts.forall(t.getMessage.contains(_))
case _ => false
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@ package sigmastate.helpers

import org.ergoplatform.SigmaConstants.ScriptCostLimit
import org.ergoplatform._
import org.ergoplatform.validation.ValidationRules.{CheckCostFunc, CheckCalcFunc}
import org.ergoplatform.validation.ValidationRules.{CheckCalcFunc, CheckCostFunc}
import org.ergoplatform.validation.ValidationSpecification
import org.scalacheck.Arbitrary.arbByte
import org.scalacheck.Gen
import org.scalatest.prop.{PropertyChecks, GeneratorDrivenPropertyChecks}
import org.scalatest.{PropSpec, Assertion, Matchers}
import scalan.{TestUtils, TestContexts, RType}
import org.scalatest.prop.{GeneratorDrivenPropertyChecks, PropertyChecks}
import org.scalatest.{Assertion, Matchers, PropSpec}
import scalan.{RType, TestContexts, TestUtils}
import scorex.crypto.hash.Blake2b256
import sigma.types.IsPrimView
import sigmastate.Values.{Constant, EvaluatedValue, SValue, Value, GroupElementConstant}
import sigmastate.interpreter.Interpreter.{ScriptNameProp, ScriptEnv}
import sigmastate.Values.{Constant, EvaluatedValue, GroupElementConstant, SValue, Value}
import sigmastate.interpreter.Interpreter.{ScriptEnv, ScriptNameProp}
import sigmastate.interpreter.{CryptoConstants, Interpreter}
import sigmastate.lang.Terms
import sigmastate.serialization.SigmaSerializer
import sigmastate.{SGroupElement, TestsBase, SType}
import sigmastate.eval.{CompiletimeCosting, IRContext, Evaluation, _}
import sigmastate.{SGroupElement, SType, TestsBase}
import sigmastate.eval.{CompiletimeCosting, Evaluation, IRContext, _}
import sigmastate.interpreter.CryptoConstants.EcPointType
import sigmastate.utils.Helpers._
import sigmastate.helpers.TestingHelpers._
import special.sigma

import scala.language.implicitConversions
import scala.util.DynamicVariable

trait SigmaTestingCommons extends PropSpec
with PropertyChecks
Expand Down Expand Up @@ -107,6 +108,13 @@ trait SigmaTestingCommons extends PropSpec
costingRes
}

/** This value is used as Context.initCost value. The default value is used for most
* test vectors.
* Change this value using `withValue` method to test behavior with non-default
* initial cost.
*/
protected val initialCostInTests = new DynamicVariable[Long](0)

/** Returns a Scala function which is equivalent to the given function script.
* The script is embedded into valid ErgoScript which is then compiled to
* [[sigmastate.Values.Value]] tree.
Expand Down Expand Up @@ -207,7 +215,7 @@ trait SigmaTestingCommons extends PropSpec
(costCtx, calcCtx)
}

val estimatedCost = IR.checkCostWithContext(costingCtx, costF, ScriptCostLimit.value, 0L).getOrThrow
val estimatedCost = IR.checkCostWithContext(costingCtx, costF, ScriptCostLimit.value, initialCostInTests.value).getOrThrow

val (res, _) = valueFun(sigmaCtx)
(res.asInstanceOf[B], estimatedCost)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,31 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui
verifyCases(cases, binXor)
}

property("verify should respect Context.initCost") {
val feature = existingFeature((x: (Boolean, Boolean)) => x._1 ^ x._2,
"{ (x: (Boolean, Boolean)) => x._1 ^ x._2 }",
FuncValue(
Vector((1, STuple(Vector(SBoolean, SBoolean)))),
BinXor(
SelectField.typed[BoolValue](ValUse(1, STuple(Vector(SBoolean, SBoolean))), 1.toByte),
SelectField.typed[BoolValue](ValUse(1, STuple(Vector(SBoolean, SBoolean))), 2.toByte)
)
))
val expectedCost = 36518
val cases = Seq(
(true, true) -> Expected(Success(false), expectedCost)
)
verifyCases(cases, feature)

val initCost = 100
initialCostInTests.withValue(initCost) {
val cases = Seq(
(true, true) -> Expected(Success(false), expectedCost + initCost)
)
verifyCases(cases, feature)
}
}

property("BinXor(logical XOR) test") {
val xor = existingFeature((x: (Int, Boolean)) => (x._1 == 0) ^ x._2,
"{ (x: (Int, Boolean)) => (x._1 == 0) ^ x._2 }",
Expand Down
13 changes: 7 additions & 6 deletions sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -339,17 +339,18 @@ class SigmaDslTesting extends PropSpec
newCtx,
ValidationRules.currentSettings,
ScriptCostLimit.value,
initCost = 0L
initCost = initialCostInTests.value
)

case _ =>
ErgoLikeContextTesting.dummy(
createBox(0, compiledTree, additionalRegisters = newRegisters),
activatedVersionInTests
).withBindings(
createBox(0, compiledTree, additionalRegisters = newRegisters),
activatedVersionInTests)
.withBindings(
1.toByte -> Constant[SType](input.asInstanceOf[SType#WrappedType], tpeA),
2.toByte -> ByteArrayConstant(pkCarolBytes)
).asInstanceOf[ErgoLikeContext]
2.toByte -> ByteArrayConstant(pkCarolBytes))
.withInitCost(initialCostInTests.value)
.asInstanceOf[ErgoLikeContext]
}

val pr = prover.prove(compiledTree, ergoCtx, fakeMessage).getOrThrow
Expand Down

0 comments on commit 62dab57

Please sign in to comment.