From 12c3098423b3a94f6811b35746b78fc33fd75045 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Wed, 24 Mar 2021 16:30:37 +0300 Subject: [PATCH 1/6] fix-cost: introduced initialCostInTests variable --- .../helpers/SigmaTestingCommons.scala | 26 ++++++++++++------- .../scala/special/sigma/SigmaDslTesting.scala | 13 +++++----- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/sigmastate/src/test/scala/sigmastate/helpers/SigmaTestingCommons.scala b/sigmastate/src/test/scala/sigmastate/helpers/SigmaTestingCommons.scala index c30aa86916..8dd612e540 100644 --- a/sigmastate/src/test/scala/sigmastate/helpers/SigmaTestingCommons.scala +++ b/sigmastate/src/test/scala/sigmastate/helpers/SigmaTestingCommons.scala @@ -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 @@ -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. @@ -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) diff --git a/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala b/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala index 2bec5196c8..ea5eab5e14 100644 --- a/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala +++ b/sigmastate/src/test/scala/special/sigma/SigmaDslTesting.scala @@ -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 From d2d471e8a0b2b6581b6e8d082696f43d9544eaa3 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Wed, 24 Mar 2021 18:57:47 +0300 Subject: [PATCH 2/6] fix-cost: added SigmaDslSpecification property("verify should respect Context.initCost") --- .../special/sigma/SigmaDslSpecification.scala | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala b/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala index 2291d762b5..9e1793da5f 100644 --- a/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala +++ b/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala @@ -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 }", From 78156b7b19fa1cf498cfb13fc194da63df84b217 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Wed, 24 Mar 2021 23:16:26 +0300 Subject: [PATCH 3/6] fix-cost: added CostingSpecification property("ErgoTree with SigmaPropConstant costs") --- .../sigmastate/CostingSpecification.scala | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala b/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala index ce20a30ea4..78f359b5c5 100644 --- a/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala @@ -17,6 +17,7 @@ import sigmastate.interpreter.Interpreter.{ScriptNameProp, ScriptEnv, emptyEnv} import sigmastate.utxo.CostTable import sigmastate.utxo.CostTable._ import special.sigma.{SigmaTestingData, AvlTree} +import sigmastate.lang.Terms.ValueOps class CostingSpecification extends SigmaTestingData with CrossVersionProps { implicit lazy val IR = new TestingIRContext { @@ -404,6 +405,38 @@ class CostingSpecification extends SigmaTestingData with CrossVersionProps { cost shouldBe expectedCost } + property("ErgoTree with SigmaPropConstant costs") { + val d = new TestData; import d._ + + def testTree(tree: ErgoTree, initCost: Long, expectedCost: Long) = { + val ctx = context.withInitCost(initCost) + 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) + + testTree(tree1, 0, 10061) + testTree(tree2, 0, 10061) + + testTree(tree1, context.initCost, 20061) + testTree(tree2, context.initCost, 20061) + + // more complex tree without Deserialize + val tree3 = ErgoTree.fromProposition(compiler + .compile(env, "{ sigmaProp(HEIGHT == 2) }") + .asSigmaProp) + + testTree(tree3, 0, 541) + testTree(tree3, context.initCost, 10541) + } + property("laziness of AND, OR costs") { val d = new TestData; import d._ cost("{ val cond = getVar[Boolean](2).get; !(!cond && (1 / 0 == 1)) }")( From 2a6302e27e1fed586c80ca1e2120ce869e65db75 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Wed, 24 Mar 2021 23:53:57 +0300 Subject: [PATCH 4/6] fix-cost: added initCost when tree is SigmaPropConstant --- .../scala/sigmastate/eval/Evaluation.scala | 18 ++++++++++++++++++ .../sigmastate/interpreter/Interpreter.scala | 3 ++- .../sigmastate/CostingSpecification.scala | 10 +++++----- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala b/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala index e2e91693c1..0aaab8b302 100644 --- a/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala +++ b/sigmastate/src/main/scala/sigmastate/eval/Evaluation.scala @@ -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. */ diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala b/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala index 0f213abcbb..d94a1bb6e2 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala @@ -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) diff --git a/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala b/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala index 78f359b5c5..e03f20900f 100644 --- a/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala @@ -420,13 +420,13 @@ class CostingSpecification extends SigmaTestingData with CrossVersionProps { // simple trees containing SigmaPropConstant val tree1 = ErgoTree.fromSigmaBoolean(pkA) // without segregation - val tree2 = ErgoTree.withSegregation(pkA) + val tree2 = ErgoTree.withSegregation(pkA) // with segregation, have different `complexity` - testTree(tree1, 0, 10061) - testTree(tree2, 0, 10061) + testTree(tree1, 0, 10141) + testTree(tree2, 0, 10161) - testTree(tree1, context.initCost, 20061) - testTree(tree2, context.initCost, 20061) + testTree(tree1, context.initCost, 20141) + testTree(tree2, context.initCost, 20161) // more complex tree without Deserialize val tree3 = ErgoTree.fromProposition(compiler From e5397ed5c2c502b87ff5d00315d3aa6c3b575ff2 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Thu, 25 Mar 2021 00:14:04 +0300 Subject: [PATCH 5/6] fix-cost: fix Scala 2.11 compilation --- .../src/test/scala/special/sigma/SigmaDslSpecification.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala b/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala index 9e1793da5f..2ba7adb6f1 100644 --- a/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala +++ b/sigmastate/src/test/scala/special/sigma/SigmaDslSpecification.scala @@ -264,14 +264,14 @@ class SigmaDslSpecification extends SigmaDslTesting with CrossVersionProps { sui )) val expectedCost = 36518 val cases = Seq( - (true, true) -> Expected(Success(false), expectedCost), + (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), + (true, true) -> Expected(Success(false), expectedCost + initCost) ) verifyCases(cases, feature) } From e2faf2e8ccb0bd25778624beeeaa60a11d9b4944 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Thu, 25 Mar 2021 12:03:54 +0300 Subject: [PATCH 6/6] fix-cost: negative test with CostLimitException --- .../sigmastate/CostingSpecification.scala | 37 +++++++++++++------ .../sigmastate/helpers/NegativeTesting.scala | 13 +++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala b/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala index e03f20900f..8b0e4552b7 100644 --- a/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/CostingSpecification.scala @@ -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 @@ -13,11 +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 { @@ -408,8 +409,7 @@ class CostingSpecification extends SigmaTestingData with CrossVersionProps { property("ErgoTree with SigmaPropConstant costs") { val d = new TestData; import d._ - def testTree(tree: ErgoTree, initCost: Long, expectedCost: Long) = { - val ctx = context.withInitCost(initCost) + def proveAndVerify(ctx: ErgoLikeContext, tree: ErgoTree, expectedCost: Long) = { val pr = interpreter.prove(tree, ctx, fakeMessage).get pr.cost shouldBe expectedCost @@ -422,19 +422,34 @@ class CostingSpecification extends SigmaTestingData with CrossVersionProps { val tree1 = ErgoTree.fromSigmaBoolean(pkA) // without segregation val tree2 = ErgoTree.withSegregation(pkA) // with segregation, have different `complexity` - testTree(tree1, 0, 10141) - testTree(tree2, 0, 10161) + { + 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) + } - testTree(tree1, context.initCost, 20141) - testTree(tree2, context.initCost, 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) - testTree(tree3, 0, 541) - testTree(tree3, context.initCost, 10541) + proveAndVerify(context.withInitCost(0), tree3, expectedCost = 541) + proveAndVerify(context.withInitCost(10000), tree3, expectedCost = 10541) } property("laziness of AND, OR costs") { diff --git a/sigmastate/src/test/scala/sigmastate/helpers/NegativeTesting.scala b/sigmastate/src/test/scala/sigmastate/helpers/NegativeTesting.scala index 3fc245b684..a4bda66032 100644 --- a/sigmastate/src/test/scala/sigmastate/helpers/NegativeTesting.scala +++ b/sigmastate/src/test/scala/sigmastate/helpers/NegativeTesting.scala @@ -3,6 +3,7 @@ package sigmastate.helpers import org.scalatest.Matchers import scala.annotation.tailrec +import scala.reflect.ClassTag trait NegativeTesting extends Matchers { @@ -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 + } + }