diff --git a/data/shared/src/main/scala/sigma/SigmaDataReflection.scala b/data/shared/src/main/scala/sigma/SigmaDataReflection.scala index c79473afe..3f10e66b2 100644 --- a/data/shared/src/main/scala/sigma/SigmaDataReflection.scala +++ b/data/shared/src/main/scala/sigma/SigmaDataReflection.scala @@ -236,12 +236,6 @@ object SigmaDataReflection { { val clazz = SAvlTreeMethods.getClass registerClassEntry(clazz, methods = Map( - mkMethod(clazz, "update_eval", Array[Class[_]](classOf[MethodCall], classOf[AvlTree], classOf[Coll[_]], classOf[Coll[_]], classOf[ErgoTreeEvaluator])) { (obj, args) => - obj.asInstanceOf[SAvlTreeMethods.type].update_eval(args(0).asInstanceOf[MethodCall], - args(1).asInstanceOf[AvlTree], - args(2).asInstanceOf[KeyValueColl], - args(3).asInstanceOf[Coll[Byte]])(args(4).asInstanceOf[ErgoTreeEvaluator]) - }, mkMethod(clazz, "contains_eval", Array[Class[_]](classOf[MethodCall], classOf[AvlTree], classOf[Coll[_]], classOf[Coll[_]], classOf[ErgoTreeEvaluator])) { (obj, args) => obj.asInstanceOf[SAvlTreeMethods.type].contains_eval(args(0).asInstanceOf[MethodCall], args(1).asInstanceOf[AvlTree], @@ -271,6 +265,18 @@ object SigmaDataReflection { args(1).asInstanceOf[AvlTree], args(2).asInstanceOf[KeyValueColl], args(3).asInstanceOf[Coll[Byte]])(args(4).asInstanceOf[ErgoTreeEvaluator]) + }, + mkMethod(clazz, "update_eval", Array[Class[_]](classOf[MethodCall], classOf[AvlTree], classOf[Coll[_]], classOf[Coll[_]], classOf[ErgoTreeEvaluator])) { (obj, args) => + obj.asInstanceOf[SAvlTreeMethods.type].update_eval(args(0).asInstanceOf[MethodCall], + args(1).asInstanceOf[AvlTree], + args(2).asInstanceOf[KeyValueColl], + args(3).asInstanceOf[Coll[Byte]])(args(4).asInstanceOf[ErgoTreeEvaluator]) + }, + mkMethod(clazz, "insertOrUpdate_eval", Array[Class[_]](classOf[MethodCall], classOf[AvlTree], classOf[Coll[_]], classOf[Coll[_]], classOf[ErgoTreeEvaluator])) { (obj, args) => + obj.asInstanceOf[SAvlTreeMethods.type].insertOrUpdate_eval(args(0).asInstanceOf[MethodCall], + args(1).asInstanceOf[AvlTree], + args(2).asInstanceOf[KeyValueColl], + args(3).asInstanceOf[Coll[Byte]])(args(4).asInstanceOf[ErgoTreeEvaluator]) } ) ) diff --git a/data/shared/src/main/scala/sigma/ast/methods.scala b/data/shared/src/main/scala/sigma/ast/methods.scala index a69654de7..25ad4f94c 100644 --- a/data/shared/src/main/scala/sigma/ast/methods.scala +++ b/data/shared/src/main/scala/sigma/ast/methods.scala @@ -1759,23 +1759,63 @@ case object SAvlTreeMethods extends MonoTypeMethods { OperationCostInfo(m.costKind.asInstanceOf[FixedCost], m.opDesc) } - protected override def getMethods(): Seq[SMethod] = super.getMethods() ++ Seq( - digestMethod, - enabledOperationsMethod, - keyLengthMethod, - valueLengthOptMethod, - isInsertAllowedMethod, - isUpdateAllowedMethod, - isRemoveAllowedMethod, - updateOperationsMethod, - containsMethod, - getMethod, - getManyMethod, - insertMethod, - updateMethod, - removeMethod, - updateDigestMethod - ) + // 6.0 methods below + lazy val insertOrUpdateMethod = SMethod(this, "insertOrUpdate", + SFunc(Array(SAvlTree, CollKeyValue, SByteArray), SAvlTreeOption), 16, DynamicCost) + .withIRInfo(MethodCallIrBuilder) + .withInfo(MethodCall, + """ + | /** Perform insertions or updates of key-value entries into this tree using proof `proof`. + | * Throws exception if proof is incorrect + | * Return Some(newTree) if successful + | * Return None if operations were not performed. + | * + | * @note CAUTION! Pairs must be ordered the same way they were in insert ops before proof was generated. + | * @param operations collection of key-value pairs to insert or update in this authenticated dictionary. + | * @param proof + | */ + | + """.stripMargin) + + /** Implements evaluation of AvlTree.insertOrUpdate method call ErgoTree node. + * Called via reflection based on naming convention. + * @see SMethod.evalMethod + */ + def insertOrUpdate_eval(mc: MethodCall, tree: AvlTree, entries: KeyValueColl, proof: Coll[Byte]) + (implicit E: ErgoTreeEvaluator): Option[AvlTree] = { + E.insertOrUpdate_eval(mc, tree, entries, proof) + } + + lazy val v5Methods = { + super.getMethods() ++ Seq( + digestMethod, + enabledOperationsMethod, + keyLengthMethod, + valueLengthOptMethod, + isInsertAllowedMethod, + isUpdateAllowedMethod, + isRemoveAllowedMethod, + updateOperationsMethod, + containsMethod, + getMethod, + getManyMethod, + insertMethod, + updateMethod, + removeMethod, + updateDigestMethod + ) + } + + lazy val v6Methods = v5Methods ++ Seq(insertOrUpdateMethod) + + protected override def getMethods(): Seq[SMethod] = { + if (VersionContext.current.isV6SoftForkActivated) { + v6Methods + } else { + v5Methods + } + } + } /** Type descriptor of `Context` type of ErgoTree. */ diff --git a/data/shared/src/main/scala/sigma/ast/values.scala b/data/shared/src/main/scala/sigma/ast/values.scala index b50bf70e1..87b8a2b21 100644 --- a/data/shared/src/main/scala/sigma/ast/values.scala +++ b/data/shared/src/main/scala/sigma/ast/values.scala @@ -9,6 +9,7 @@ import sigma.ast.syntax._ import sigma.crypto.{CryptoConstants, EcPointType} import sigma.data.OverloadHack.Overloaded1 import sigma.data.{CSigmaDslBuilder, CSigmaProp, CUnsignedBigInt, Nullable, RType, SigmaBoolean} +import sigma.data.{AvlTreeData, CAvlTree, CSigmaDslBuilder, CSigmaProp, Nullable, RType, SigmaBoolean} import sigma.eval.ErgoTreeEvaluator.DataEnv import sigma.eval.{ErgoTreeEvaluator, SigmaDsl} import sigma.exceptions.InterpreterException @@ -550,6 +551,7 @@ object SigmaPropConstant { object AvlTreeConstant { def apply(value: AvlTree): Constant[SAvlTree.type] = Constant[SAvlTree.type](value, SAvlTree) + def apply(value: AvlTreeData): Constant[SAvlTree.type] = Constant[SAvlTree.type](CAvlTree(value), SAvlTree) } object PreHeaderConstant { diff --git a/data/shared/src/main/scala/sigma/eval/AvlTreeVerifier.scala b/data/shared/src/main/scala/sigma/eval/AvlTreeVerifier.scala index bc4a62545..c8e52a6a4 100644 --- a/data/shared/src/main/scala/sigma/eval/AvlTreeVerifier.scala +++ b/data/shared/src/main/scala/sigma/eval/AvlTreeVerifier.scala @@ -50,6 +50,19 @@ trait AvlTreeVerifier { */ def performUpdate(key: Array[Byte], value: Array[Byte]): Try[Option[Array[Byte]]] + /** + * Returns Failure if the proof does not verify. + * Otherwise, successfully modifies tree and so returns Success. + * After one failure, all subsequent operations with this verifier will fail and digest + * is None. + * + * @param key key to look up + * @param value value to check it was inserted or updated + * @return Success(Some(oldValue)) if there was some oldValue associated with the key, + * Success(None) in case of insertion, or Failure if proof invalid + */ + def performInsertOrUpdate(key: Array[Byte], value: Array[Byte]): Try[Option[Array[Byte]]] + /** Check the key has been removed in the tree. * If `key` exists in the tree and the operation succeeds, * returns `Success(Some(v))`, where v is old value associated with `key`. diff --git a/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala b/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala index b98652897..ba3f6d5c9 100644 --- a/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala +++ b/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala @@ -134,6 +134,13 @@ abstract class ErgoTreeEvaluator { mc: MethodCall, tree: AvlTree, operations: KeyValueColl, proof: Coll[Byte]): Option[AvlTree] + /** Implements evaluation of AvlTree.insertOrUpdate method call ErgoTree node. */ + def insertOrUpdate_eval( + mc: MethodCall, + tree: AvlTree, + entries: KeyValueColl, + proof: Coll[Byte]): Option[AvlTree] + /** Implements evaluation of AvlTree.remove method call ErgoTree node. */ def remove_eval( mc: MethodCall, tree: AvlTree, diff --git a/docs/LangSpec.md b/docs/LangSpec.md index 3f5453072..407614679 100644 --- a/docs/LangSpec.md +++ b/docs/LangSpec.md @@ -749,10 +749,24 @@ class AvlTree { * Return None if operations were not performed. * @param operations collection of key-value pairs to update in this * authenticated dictionary. - * @param proof data to reconstruct part of the tree + * @param proof subtree which is enough to check operations */ def update(operations: Coll[(Coll[Byte], Coll[Byte])], proof: Coll[Byte]): Option[AvlTree] + + /** Perform insertions or updates of key-value entries into this tree using proof `proof`. + * Throws exception if proof is incorrect + * + * @note CAUTION! Pairs must be ordered the same way they were in ops + * before proof was generated. + * Return Some(newTree) if successful + * Return None if operations were not performed. + * @param operations collection of key-value pairs to insert or update in this + * authenticated dictionary. + * @param proof subtree which is enough to check operations + */ + def insertOrUpdate(operations: Coll[(Coll[Byte], Coll[Byte])], proof: Coll[Byte]): Option[AvlTree] + /** Perform removal of entries into this tree using proof `proof`. * Throws exception if proof is incorrect * Return Some(newTree) if successful diff --git a/interpreter/shared/src/main/scala/sigmastate/eval/CAvlTreeVerifier.scala b/interpreter/shared/src/main/scala/sigmastate/eval/CAvlTreeVerifier.scala index 5739e65ad..53736ee0b 100644 --- a/interpreter/shared/src/main/scala/sigmastate/eval/CAvlTreeVerifier.scala +++ b/interpreter/shared/src/main/scala/sigmastate/eval/CAvlTreeVerifier.scala @@ -1,6 +1,6 @@ package sigmastate.eval -import scorex.crypto.authds.avltree.batch.{BatchAVLVerifier, Insert, Lookup, Remove, Update} +import scorex.crypto.authds.avltree.batch.{BatchAVLVerifier, Insert, InsertOrUpdate, Lookup, Remove, Update} import scorex.crypto.authds.{ADDigest, ADKey, ADValue, SerializedAdProof} import scorex.crypto.hash.{Blake2b256, Digest32} import sigma.data.CAvlTree @@ -32,6 +32,9 @@ class CAvlTreeVerifier private( override def performUpdate(key: Array[Byte], value: Array[Byte]): Try[Option[Array[Byte]]] = performOneOperation(Update(ADKey @@ key, ADValue @@ value)) + override def performInsertOrUpdate(key: Array[Byte], value: Array[Byte]): Try[Option[Array[Byte]]] = + performOneOperation(InsertOrUpdate(ADKey @@ key, ADValue @@ value)) + override def performRemove(key: Array[Byte]): Try[Option[Array[Byte]]] = performOneOperation(Remove(ADKey @@ key)) diff --git a/interpreter/shared/src/main/scala/sigmastate/eval/Extensions.scala b/interpreter/shared/src/main/scala/sigmastate/eval/Extensions.scala index f9cb0e9f7..98f044af9 100644 --- a/interpreter/shared/src/main/scala/sigmastate/eval/Extensions.scala +++ b/interpreter/shared/src/main/scala/sigmastate/eval/Extensions.scala @@ -3,7 +3,7 @@ package sigmastate.eval import debox.cfor import org.ergoplatform.ErgoBox import org.ergoplatform.ErgoBox.TokenId -import scorex.crypto.authds.avltree.batch.{Insert, Lookup, Remove, Update} +import scorex.crypto.authds.avltree.batch.{Insert, InsertOrUpdate, Lookup, Remove, Update} import scorex.crypto.authds.{ADKey, ADValue} import scorex.util.encode.Base16 import sigma.ast.SType.AnyOps @@ -91,7 +91,7 @@ object Extensions { val bv = CAvlTreeVerifier(tree, proof) entries.forall { case (key, value) => val insertRes = bv.performOneOperation(Insert(ADKey @@ key.toArray, ADValue @@ value.toArray)) - if (insertRes.isFailure) { + if (insertRes.isFailure && !VersionContext.current.isV6SoftForkActivated) { syntax.error(s"Incorrect insert for $tree (key: $key, value: $value, digest: ${tree.digest}): ${insertRes.failed.get}}") } insertRes.isSuccess @@ -120,6 +120,24 @@ object Extensions { } } + def insertOrUpdate( + entries: Coll[(Coll[Byte], Coll[Byte])], + proof: Coll[Byte]): Option[AvlTree] = { + if (!tree.isInsertAllowed || !tree.isUpdateAllowed) { + None + } else { + val bv = CAvlTreeVerifier(tree, proof) + entries.forall { case (key, value) => + val insertRes = bv.performOneOperation(InsertOrUpdate(ADKey @@ key.toArray, ADValue @@ value.toArray)) + insertRes.isSuccess + } + bv.digest match { + case Some(d) => Some(tree.updateDigest(Colls.fromArray(d))) + case _ => None + } + } + } + def remove(operations: Coll[Coll[Byte]], proof: Coll[Byte]): Option[AvlTree] = { if (!tree.isRemoveAllowed) { None diff --git a/interpreter/shared/src/main/scala/sigmastate/interpreter/CErgoTreeEvaluator.scala b/interpreter/shared/src/main/scala/sigmastate/interpreter/CErgoTreeEvaluator.scala index 9916a6c56..809b16270 100644 --- a/interpreter/shared/src/main/scala/sigmastate/interpreter/CErgoTreeEvaluator.scala +++ b/interpreter/shared/src/main/scala/sigmastate/interpreter/CErgoTreeEvaluator.scala @@ -144,9 +144,8 @@ class CErgoTreeEvaluator( // the cost of tree lookup is O(bv.treeHeight) addSeqCost(InsertIntoAvlTree_Info, nItems) { () => val insertRes = bv.performInsert(key.toArray, value.toArray) - // TODO v6.0: throwing exception is not consistent with update semantics - // however it preserves v4.0 semantics (see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/908) - if (insertRes.isFailure) { + // For versioned change details, see see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/908 + if (insertRes.isFailure && !VersionContext.current.isV6SoftForkActivated) { syntax.error(s"Incorrect insert for $tree (key: $key, value: $value, digest: ${tree.digest}): ${insertRes.failed.get}}") } res = insertRes.isSuccess @@ -173,7 +172,7 @@ class CErgoTreeEvaluator( // when the tree is empty we still need to add the insert cost val nItems = Math.max(bv.treeHeight, 1) - // here we use forall as looping with fast break on first failed tree oparation + // here we use forall as looping with fast break on first failed tree operation operations.forall { case (key, value) => var res = true // the cost of tree update is O(bv.treeHeight) @@ -192,6 +191,40 @@ class CErgoTreeEvaluator( } } + override def insertOrUpdate_eval( + mc: MethodCall, tree: AvlTree, + operations: KeyValueColl, proof: Coll[Byte]): Option[AvlTree] = { + addCost(isUpdateAllowed_Info) + addCost(isInsertAllowed_Info) + if (!(tree.isUpdateAllowed && tree.isInsertAllowed)) { + None + } else { + val bv = createVerifier(tree, proof) + // when the tree is empty we still need to add the insert cost + val nItems = Math.max(bv.treeHeight, 1) + + // here we use forall as looping with fast break on first failed tree operation + operations.forall { case (key, value) => + var res = true + // the cost of tree update is O(bv.treeHeight) + // Here (and in the previous methods) the cost is not properly approximated. + // When the tree is small (or empty), but there are many `operations`, the treeHeight will grow on every iteration. + // So should the cost on every iteration. + addSeqCost(UpdateAvlTree_Info, nItems) { () => + val updateRes = bv.performInsertOrUpdate(key.toArray, value.toArray) + res = updateRes.isSuccess + } + res + } + bv.digest match { + case Some(d) => + addCost(updateDigest_Info) + Some(tree.updateDigest(Colls.fromArray(d))) + case _ => None + } + } + } + override def remove_eval( mc: MethodCall, tree: AvlTree, operations: Coll[Coll[Byte]], proof: Coll[Byte]): Option[AvlTree] = { 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 f5235aba5..7ab784569 100644 --- a/sc/shared/src/main/scala/sigma/compiler/ir/GraphBuilding.scala +++ b/sc/shared/src/main/scala/sigma/compiler/ir/GraphBuilding.scala @@ -1138,6 +1138,10 @@ trait GraphBuilding extends Base with DefRewriting { IR: IRContext => val operations = asRep[Coll[(Coll[Byte], Coll[Byte])]](argsV(0)) val proof = asRep[Coll[Byte]](argsV(1)) tree.update(operations, proof) + case SAvlTreeMethods.insertOrUpdateMethod.name => + val operations = asRep[Coll[(Coll[Byte], Coll[Byte])]](argsV(0)) + val proof = asRep[Coll[Byte]](argsV(1)) + tree.insertOrUpdate(operations, proof) case _ => throwError() } case (ph: Ref[PreHeader]@unchecked, SPreHeaderMethods) => 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 a49dbe53a..68dc1d2a3 100644 --- a/sc/shared/src/main/scala/sigma/compiler/ir/GraphIRReflection.scala +++ b/sc/shared/src/main/scala/sigma/compiler/ir/GraphIRReflection.scala @@ -290,10 +290,6 @@ object GraphIRReflection { obj.asInstanceOf[ctx.AvlTree].getMany(args(0).asInstanceOf[ctx.Ref[ctx.Coll[ctx.Coll[Byte]]]], args(1).asInstanceOf[ctx.Ref[ctx.Coll[Byte]]]) }, - mkMethod(clazz, "update", Array[Class[_]](classOf[Base#Ref[_]], classOf[Base#Ref[_]])) { (obj, args) => - obj.asInstanceOf[ctx.AvlTree].update(args(0).asInstanceOf[ctx.Ref[ctx.Coll[(ctx.Coll[Byte], ctx.Coll[Byte])]]], - args(1).asInstanceOf[ctx.Ref[ctx.Coll[Byte]]]) - }, mkMethod(clazz, "keyLength", Array[Class[_]]()) { (obj, _) => obj.asInstanceOf[ctx.AvlTree].keyLength }, @@ -310,6 +306,14 @@ object GraphIRReflection { obj.asInstanceOf[ctx.AvlTree].insert(args(0).asInstanceOf[ctx.Ref[ctx.Coll[(ctx.Coll[Byte], ctx.Coll[Byte])]]], args(1).asInstanceOf[ctx.Ref[ctx.Coll[Byte]]]) }, + mkMethod(clazz, "update", Array[Class[_]](classOf[Base#Ref[_]], classOf[Base#Ref[_]])) { (obj, args) => + obj.asInstanceOf[ctx.AvlTree].update(args(0).asInstanceOf[ctx.Ref[ctx.Coll[(ctx.Coll[Byte], ctx.Coll[Byte])]]], + args(1).asInstanceOf[ctx.Ref[ctx.Coll[Byte]]]) + }, + mkMethod(clazz, "insertOrUpdate", Array[Class[_]](classOf[Base#Ref[_]], classOf[Base#Ref[_]])) { (obj, args) => + obj.asInstanceOf[ctx.AvlTree].insertOrUpdate(args(0).asInstanceOf[ctx.Ref[ctx.Coll[(ctx.Coll[Byte], ctx.Coll[Byte])]]], + args(1).asInstanceOf[ctx.Ref[ctx.Coll[Byte]]]) + }, mkMethod(clazz, "isRemoveAllowed", Array[Class[_]]()) { (obj, _) => obj.asInstanceOf[ctx.AvlTree].isRemoveAllowed }, 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 84c43f14d..c0d29bc3b 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 @@ -68,6 +68,7 @@ import scalan._ def getMany(keys: Ref[Coll[Coll[Byte]]], proof: Ref[Coll[Byte]]): Ref[Coll[WOption[Coll[Byte]]]]; def insert(operations: Ref[Coll[scala.Tuple2[Coll[Byte], Coll[Byte]]]], proof: Ref[Coll[Byte]]): Ref[WOption[AvlTree]]; def update(operations: Ref[Coll[scala.Tuple2[Coll[Byte], Coll[Byte]]]], proof: Ref[Coll[Byte]]): Ref[WOption[AvlTree]]; + def insertOrUpdate(operations: Ref[Coll[scala.Tuple2[Coll[Byte], Coll[Byte]]]], proof: Ref[Coll[Byte]]): Ref[WOption[AvlTree]]; def remove(operations: Ref[Coll[Coll[Byte]]], proof: Ref[Coll[Byte]]): Ref[WOption[AvlTree]] }; trait PreHeader extends Def[PreHeader] { 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 695f50324..34b4270db 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 @@ -1208,6 +1208,13 @@ object AvlTree extends EntityObject("AvlTree") { true, false, element[WOption[AvlTree]])) } + override def insertOrUpdate(operations: Ref[Coll[(Coll[Byte], Coll[Byte])]], proof: Ref[Coll[Byte]]): Ref[WOption[AvlTree]] = { + asRep[WOption[AvlTree]](mkMethodCall(self, + AvlTreeClass.getMethod("insertOrUpdate", classOf[Sym], classOf[Sym]), + Array[AnyRef](operations, proof), + true, false, element[WOption[AvlTree]])) + } + override def remove(operations: Ref[Coll[Coll[Byte]]], proof: Ref[Coll[Byte]]): Ref[WOption[AvlTree]] = { asRep[WOption[AvlTree]](mkMethodCall(self, AvlTreeClass.getMethod("remove", classOf[Sym], classOf[Sym]), @@ -1332,6 +1339,13 @@ object AvlTree extends EntityObject("AvlTree") { true, true, element[WOption[AvlTree]])) } + def insertOrUpdate(operations: Ref[Coll[(Coll[Byte], Coll[Byte])]], proof: Ref[Coll[Byte]]): Ref[WOption[AvlTree]] = { + asRep[WOption[AvlTree]](mkMethodCall(source, + AvlTreeClass.getMethod("insertOrUpdate", classOf[Sym], classOf[Sym]), + Array[AnyRef](operations, proof), + true, true, element[WOption[AvlTree]])) + } + def remove(operations: Ref[Coll[Coll[Byte]]], proof: Ref[Coll[Byte]]): Ref[WOption[AvlTree]] = { asRep[WOption[AvlTree]](mkMethodCall(source, AvlTreeClass.getMethod("remove", classOf[Sym], classOf[Sym]), @@ -1355,7 +1369,7 @@ object AvlTree extends EntityObject("AvlTree") { override protected def collectMethods: Map[RMethod, MethodDesc] = { super.collectMethods ++ Elem.declaredMethods(RClass(classOf[AvlTree]), RClass(classOf[SAvlTree]), Set( - "digest", "enabledOperations", "keyLength", "valueLengthOpt", "isInsertAllowed", "isUpdateAllowed", "isRemoveAllowed", "updateDigest", "updateOperations", "contains", "get", "getMany", "insert", "update", "remove" + "digest", "enabledOperations", "keyLength", "valueLengthOpt", "isInsertAllowed", "isUpdateAllowed", "isRemoveAllowed", "updateDigest", "updateOperations", "contains", "get", "getMany", "insert", "update", "insertOrUpdate", "remove" )) } } diff --git a/sc/shared/src/test/scala/sigma/LanguageSpecificationV5.scala b/sc/shared/src/test/scala/sigma/LanguageSpecificationV5.scala index 0bb92f824..c670d60c6 100644 --- a/sc/shared/src/test/scala/sigma/LanguageSpecificationV5.scala +++ b/sc/shared/src/test/scala/sigma/LanguageSpecificationV5.scala @@ -3174,11 +3174,6 @@ class LanguageSpecificationV5 extends LanguageSpecificationBase { suite => type BatchProver = BatchAVLProver[Digest32, Blake2b256.type] - def performInsert(avlProver: BatchProver, key: Coll[Byte], value: Coll[Byte]) = { - avlProver.performOneOperation(Insert(ADKey @@ key.toArray, ADValue @@ value.toArray)) - val proof = avlProver.generateProof().toColl - proof - } def performUpdate(avlProver: BatchProver, key: Coll[Byte], value: Coll[Byte]) = { avlProver.performOneOperation(Update(ADKey @@ key.toArray, ADValue @@ value.toArray)) @@ -3202,144 +3197,6 @@ class LanguageSpecificationV5 extends LanguageSpecificationBase { suite => type KV = (Coll[Byte], Coll[Byte]) - property("AvlTree.insert equivalence") { - val insert = existingFeature((t: (AvlTree, (Coll[KV], Coll[Byte]))) => t._1.insert(t._2._1, t._2._2), - "{ (t: (AvlTree, (Coll[(Coll[Byte], Coll[Byte])], Coll[Byte]))) => t._1.insert(t._2._1, t._2._2) }", - FuncValue( - Vector( - ( - 1, - STuple( - Vector( - SAvlTree, - STuple(Vector(SCollectionType(STuple(Vector(SByteArray, SByteArray))), SByteArray)) - ) - ) - ) - ), - BlockValue( - Vector( - ValDef( - 3, - List(), - SelectField.typed[Value[STuple]]( - ValUse( - 1, - STuple( - Vector( - SAvlTree, - STuple(Vector(SCollectionType(STuple(Vector(SByteArray, SByteArray))), SByteArray)) - ) - ) - ), - 2.toByte - ) - ) - ), - MethodCall.typed[Value[SOption[SAvlTree.type]]]( - SelectField.typed[Value[SAvlTree.type]]( - ValUse( - 1, - STuple( - Vector( - SAvlTree, - STuple(Vector(SCollectionType(STuple(Vector(SByteArray, SByteArray))), SByteArray)) - ) - ) - ), - 1.toByte - ), - SAvlTreeMethods.getMethodByName("insert"), - Vector( - SelectField.typed[Value[SCollection[STuple]]]( - ValUse( - 3, - STuple(Vector(SCollectionType(STuple(Vector(SByteArray, SByteArray))), SByteArray)) - ), - 1.toByte - ), - SelectField.typed[Value[SCollection[SByte.type]]]( - ValUse( - 3, - STuple(Vector(SCollectionType(STuple(Vector(SByteArray, SByteArray))), SByteArray)) - ), - 2.toByte - ) - ), - Map() - ) - ) - )) - - val testTraceBase = Array( - FixedCostItem(Apply), - FixedCostItem(FuncValue), - FixedCostItem(GetVar), - FixedCostItem(OptionGet), - FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), - ast.SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), - FixedCostItem(ValUse), - FixedCostItem(SelectField), - FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), - FixedCostItem(ValUse), - FixedCostItem(SelectField), - FixedCostItem(MethodCall), - FixedCostItem(ValUse), - FixedCostItem(SelectField), - FixedCostItem(ValUse), - FixedCostItem(SelectField), - FixedCostItem(SAvlTreeMethods.isInsertAllowedMethod, FixedCost(JitCost(15))) - ) - val costDetails1 = TracedCost(testTraceBase) - val costDetails2 = TracedCost( - testTraceBase ++ Array( - ast.SeqCostItem(NamedDesc("CreateAvlVerifier"), PerItemCost(JitCost(110), JitCost(20), 64), 70), - ast.SeqCostItem(NamedDesc("InsertIntoAvlTree"), PerItemCost(JitCost(40), JitCost(10), 1), 1), - FixedCostItem(SAvlTreeMethods.updateDigestMethod, FixedCost(JitCost(40))) - ) - ) - - forAll(keyCollGen, bytesCollGen) { (key, value) => - val (tree, avlProver) = createAvlTreeAndProver() - val preInsertDigest = avlProver.digest.toColl - val insertProof = performInsert(avlProver, key, value) - val kvs = Colls.fromItems((key -> value)) - - { // positive - val preInsertTree = createTree(preInsertDigest, insertAllowed = true) - val input = (preInsertTree, (kvs, insertProof)) - val (res, _) = insert.checkEquality(input).getOrThrow - res.isDefined shouldBe true - insert.checkExpected(input, Expected(Success(res), 1796, costDetails2, 1796, Seq.fill(4)(2102))) - } - - { // negative: readonly tree - val readonlyTree = createTree(preInsertDigest) - val input = (readonlyTree, (kvs, insertProof)) - val (res, _) = insert.checkEquality(input).getOrThrow - res.isDefined shouldBe false - insert.checkExpected(input, Expected(Success(res), 1772, costDetails1, 1772, Seq.fill(4)(2078))) - } - - { // negative: invalid key - val tree = createTree(preInsertDigest, insertAllowed = true) - val invalidKey = key.map(x => (-x).toByte) // any other different from key - val invalidKvs = Colls.fromItems((invalidKey -> value)) // NOTE, insertProof is based on `key` - val input = (tree, (invalidKvs, insertProof)) - val (res, _) = insert.checkEquality(input).getOrThrow - res.isDefined shouldBe true // TODO v6.0: should it really be true? (looks like a bug) (see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/908) - insert.checkExpected(input, Expected(Success(res), 1796, costDetails2, 1796, Seq.fill(4)(2102))) - } - - { // negative: invalid proof - val tree = createTree(preInsertDigest, insertAllowed = true) - val invalidProof = insertProof.map(x => (-x).toByte) // any other different from proof - val res = insert.checkEquality((tree, (kvs, invalidProof))) - res.isFailure shouldBe true - } - } - } - property("AvlTree.update equivalence") { val update = existingFeature((t: (AvlTree, (Coll[KV], Coll[Byte]))) => t._1.update(t._2._1, t._2._2), "{ (t: (AvlTree, (Coll[(Coll[Byte], Coll[Byte])], Coll[Byte]))) => t._1.update(t._2._1, t._2._2) }", diff --git a/sc/shared/src/test/scala/sigma/LanguageSpecificationV6.scala b/sc/shared/src/test/scala/sigma/LanguageSpecificationV6.scala index 29b78d0e9..e6844e736 100644 --- a/sc/shared/src/test/scala/sigma/LanguageSpecificationV6.scala +++ b/sc/shared/src/test/scala/sigma/LanguageSpecificationV6.scala @@ -5,6 +5,9 @@ import scorex.util.encode.Base16 import sigma.VersionContext.V6SoftForkVersion import org.ergoplatform.ErgoBox.Token import org.ergoplatform.settings.ErgoAlgos +import scorex.crypto.authds.{ADKey, ADValue} +import scorex.crypto.authds.avltree.batch.{BatchAVLProver, Insert, InsertOrUpdate} +import scorex.crypto.hash.{Blake2b256, Digest32} import scorex.util.ModifierId import scorex.utils.{Ints, Longs, Shorts} import sigma.ast.ErgoTree.{HeaderType, ZeroHeader} @@ -36,10 +39,13 @@ import sigmastate.utils.Helpers import sigma.Extensions.ArrayOps import sigma.Extensions.{ArrayOps, CollOps} import sigma.crypto.CryptoConstants +import sigma.data.CSigmaDslBuilder.Colls +import sigma.exceptions.InterpreterException import sigma.interpreter.{ContextExtension, ProverResult} +import java.lang.reflect.InvocationTargetException import java.math.BigInteger -import scala.util.{Failure, Success} +import scala.util.{Failure, Success, Try} /** This suite tests all operations for v6.0 version of the language. * The base classes establish the infrastructure for the tests. @@ -2858,4 +2864,212 @@ class LanguageSpecificationV6 extends LanguageSpecificationBase { suite => testCases(cases, some) } + type BatchProver = BatchAVLProver[Digest32, Blake2b256.type] + + type KV = (Coll[Byte], Coll[Byte]) + + def performInsertOrUpdate(avlProver: BatchProver, keys: Seq[Coll[Byte]], values: Seq[Coll[Byte]]) = { + keys.zip(values).foreach{case (key, value) => + avlProver.performOneOperation(InsertOrUpdate(ADKey @@ key.toArray, ADValue @@ value.toArray)) + } + val proof = avlProver.generateProof().toColl + proof + } + def performInsert(avlProver: BatchProver, key: Coll[Byte], value: Coll[Byte]) = { + avlProver.performOneOperation(Insert(ADKey @@ key.toArray, ADValue @@ value.toArray)) + val proof = avlProver.generateProof().toColl + proof + } + + def createTree(digest: Coll[Byte], insertAllowed: Boolean = false, updateAllowed: Boolean = false, removeAllowed: Boolean = false) = { + val flags = AvlTreeFlags(insertAllowed, updateAllowed, removeAllowed).serializeToByte + val tree = SigmaDsl.avlTree(flags, digest, 32, None) + tree + } + + property("AvlTree.insert equivalence") { + import sigmastate.eval.Extensions.AvlTreeOps + import sigmastate.utils.Helpers._ + + val insert = existingFeature( + (t: (AvlTree, (Coll[KV], Coll[Byte]))) => t._1.insert(t._2._1, t._2._2), + "{ (t: (AvlTree, (Coll[(Coll[Byte], Coll[Byte])], Coll[Byte]))) => t._1.insert(t._2._1, t._2._2) }", + FuncValue( + Vector( + ( + 1, + STuple( + Vector( + SAvlTree, + STuple(Vector(SCollectionType(STuple(Vector(SByteArray, SByteArray))), SByteArray)) + ) + ) + ) + ), + BlockValue( + Vector( + ValDef( + 3, + List(), + SelectField.typed[Value[STuple]]( + ValUse( + 1, + STuple( + Vector( + SAvlTree, + STuple(Vector(SCollectionType(STuple(Vector(SByteArray, SByteArray))), SByteArray)) + ) + ) + ), + 2.toByte + ) + ) + ), + MethodCall.typed[Value[SOption[SAvlTree.type]]]( + SelectField.typed[Value[SAvlTree.type]]( + ValUse( + 1, + STuple( + Vector( + SAvlTree, + STuple(Vector(SCollectionType(STuple(Vector(SByteArray, SByteArray))), SByteArray)) + ) + ) + ), + 1.toByte + ), + SAvlTreeMethods.getMethodByName("insert"), + Vector( + SelectField.typed[Value[SCollection[STuple]]]( + ValUse( + 3, + STuple(Vector(SCollectionType(STuple(Vector(SByteArray, SByteArray))), SByteArray)) + ), + 1.toByte + ), + SelectField.typed[Value[SCollection[SByte.type]]]( + ValUse( + 3, + STuple(Vector(SCollectionType(STuple(Vector(SByteArray, SByteArray))), SByteArray)) + ), + 2.toByte + ) + ), + Map() + ) + ) + )) + + val testTraceBase = Array( + FixedCostItem(Apply), + FixedCostItem(FuncValue), + FixedCostItem(GetVar), + FixedCostItem(OptionGet), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + ast.SeqCostItem(CompanionDesc(BlockValue), PerItemCost(JitCost(1), JitCost(1), 10), 1), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(MethodCall), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(ValUse), + FixedCostItem(SelectField), + FixedCostItem(SAvlTreeMethods.isInsertAllowedMethod, FixedCost(JitCost(15))) + ) + val costDetails1 = TracedCost(testTraceBase) + val costDetails2 = TracedCost( + testTraceBase ++ Array( + ast.SeqCostItem(NamedDesc("CreateAvlVerifier"), PerItemCost(JitCost(110), JitCost(20), 64), 70), + ast.SeqCostItem(NamedDesc("InsertIntoAvlTree"), PerItemCost(JitCost(40), JitCost(10), 1), 1), + FixedCostItem(SAvlTreeMethods.updateDigestMethod, FixedCost(JitCost(40))) + ) + ) + + forAll(keyCollGen, bytesCollGen) { (key, value) => + val (tree, avlProver) = createAvlTreeAndProver() + val preInsertDigest = avlProver.digest.toColl + val insertProof = performInsert(avlProver, key, value) + val kvs = Colls.fromItems((key -> value)) + + { // positive + val preInsertTree = createTree(preInsertDigest, insertAllowed = true) + val input = (preInsertTree, (kvs, insertProof)) + val (res, _) = insert.checkEquality(input).getOrThrow + res.isDefined shouldBe true + insert.checkExpected(input, Expected(Success(res), 1796, costDetails2, 1796, Seq.fill(4)(2102))) + } + + { // negative: readonly tree + val readonlyTree = createTree(preInsertDigest) + val input = (readonlyTree, (kvs, insertProof)) + val (res, _) = insert.checkEquality(input).getOrThrow + res.isDefined shouldBe false + insert.checkExpected(input, Expected(Success(res), 1772, costDetails1, 1772, Seq.fill(4)(2078))) + } + + { // positive: invalid key, but proof is enough to validate insert + val tree = createTree(preInsertDigest, insertAllowed = true) + val negKey = key.map(x => (-x).toByte) + val kvs = Colls.fromItems((negKey -> value)) + val input = (tree, (kvs, insertProof)) + val (res, _) = insert.checkEquality(input).getOrThrow + res.isDefined shouldBe true + insert.checkExpected(input, Expected(Success(res), 1796, costDetails2, 1796, Seq.fill(4)(2102))) + } + + { // nagative: duplicate keys + val tree = createTree(preInsertDigest, insertAllowed = true) + val invalidKvs = Colls.fromItems((key -> value), (key -> value)) + val input = (tree, (invalidKvs, insertProof)) + if (VersionContext.current.isV6SoftForkActivated) { + insert.verifyCase(input, new Expected(ExpectedResult(Success(None), Some(2103)))) + } else { + val res = insert.checkEquality(input) + res.isFailure shouldBe true + } + } + + + { // negative: invalid proof + val tree = createTree(preInsertDigest, insertAllowed = true) + val invalidProof = insertProof.map(x => (-x).toByte) // any other different from proof + val input = (tree, (kvs, invalidProof)) + if (VersionContext.current.isV6SoftForkActivated) { + insert.verifyCase(input, new Expected(ExpectedResult(Success(None), Some(2103)))) + } else { + val res = insert.checkEquality(input) + res.isFailure shouldBe true + } + } + } + } + + property("AvlTree.insertOrUpdate") { + import sigmastate.eval.Extensions.AvlTreeOps + + lazy val iou = newFeature( + (t: (AvlTree, (Coll[KV], Coll[Byte]))) => t._1.insertOrUpdate(t._2._1, t._2._2), + "{ (t: (AvlTree, (Coll[(Coll[Byte], Coll[Byte])], Coll[Byte]))) => t._1.insertOrUpdate(t._2._1, t._2._2) }", + sinceVersion = V6SoftForkVersion) + + val key = keyCollGen.sample.get + val value = bytesCollGen.sample.get + val (_, avlProver) = createAvlTreeAndProver() + val preInsertDigest = avlProver.digest.toColl + val tree = createTree(preInsertDigest, insertAllowed = true, updateAllowed = true) + val insertProof = performInsertOrUpdate(avlProver, Seq(key, key), Seq(value, value)) + val kvs = Colls.fromItems((key -> value), (key -> value)) + val input1 = (tree, (kvs, insertProof)) + + val digest = avlProver.digest + val updTree = tree.updateDigest(Colls.fromArray(digest)) + + val cases = Seq(input1 -> Success((Some(updTree)))) + + testCases(cases, iou, preGeneratedSamples = Some(Seq.empty)) + } + } diff --git a/sc/shared/src/test/scala/sigmastate/ErgoTreeSpecification.scala b/sc/shared/src/test/scala/sigmastate/ErgoTreeSpecification.scala index 0504a79c6..0655aaf8e 100644 --- a/sc/shared/src/test/scala/sigmastate/ErgoTreeSpecification.scala +++ b/sc/shared/src/test/scala/sigmastate/ErgoTreeSpecification.scala @@ -523,7 +523,11 @@ class ErgoTreeSpecification extends SigmaDslTesting with ContractsTestkit with C MInfo(13, updateMethod), MInfo(14, removeMethod), MInfo(15, updateDigestMethod) - ), true) + ) ++ (if (isV6Activated) { + Seq(MInfo(16, insertOrUpdateMethod)) + } else { + Seq.empty + }), true) }, { import SHeaderMethods._ (SHeader.typeId, Seq( diff --git a/sc/shared/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala b/sc/shared/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala index 2194414ab..25cfbba35 100644 --- a/sc/shared/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala +++ b/sc/shared/src/test/scala/sigmastate/utxo/BasicOpsSpecification.scala @@ -4,7 +4,7 @@ import org.ergoplatform.ErgoBox.{AdditionalRegisters, R6, R8} import org.ergoplatform._ import org.scalatest.Assertion import scorex.crypto.authds.{ADKey, ADValue} -import scorex.crypto.authds.avltree.batch.{BatchAVLProver, Insert} +import scorex.crypto.authds.avltree.batch.{BatchAVLProver, Insert, InsertOrUpdate} import scorex.crypto.hash.{Blake2b256, Digest32} import scorex.util.ByteArrayBuilder import scorex.util.encode.Base16 @@ -13,15 +13,16 @@ import scorex.util.encode.Base16 import scorex.utils.Ints import scorex.util.serialization.VLQByteBufferWriter import scorex.utils.Longs -import sigma.{Colls, SigmaTestingData} +import sigma.{Coll, Colls, SigmaTestingData, VersionContext} import sigma.Extensions.ArrayOps -import sigma.{SigmaTestingData, VersionContext} import sigma.VersionContext.{V6SoftForkVersion, withVersions} import sigma.VersionContext.V6SoftForkVersion import sigma.VersionContext import sigma.GroupElement import sigma.VersionContext.V6SoftForkVersion import sigma.ast.SCollection.SByteArray +import sigma.ast.SType.{AnyOps, tD} +import sigma.data.{AvlTreeData, AvlTreeFlags, CAND, CAnyValue, CAvlTree, CHeader, CSigmaDslBuilder, CSigmaProp} import sigma.ast.SType.AnyOps import sigma.data.{AvlTreeData, CAnyValue, CBigInt, CGroupElement, CSigmaDslBuilder} import sigma.data.{AvlTreeData, CAnyValue, CHeader, CSigmaDslBuilder} @@ -40,10 +41,10 @@ import sigma.ast.Apply import sigma.eval.EvalSettings import sigma.exceptions.InvalidType import sigma.serialization.{ErgoTreeSerializer, SerializerException} +import sigma.serialization.{DataSerializer, ErgoTreeSerializer, SigmaByteWriter, SigmaSerializer, ValueSerializer} import sigma.interpreter.{ContextExtension, ProverResult} import sigma.validation.ValidationException import sigma.util.NBitsUtils -import sigma.serialization.{DataSerializer, ErgoTreeSerializer, SigmaByteWriter} import sigma.util.Extensions import sigma.validation.ValidationException import sigmastate.utils.Helpers @@ -3200,4 +3201,51 @@ class BasicOpsSpecification extends CompilerTestingCommons } } + property("avltree.insertOrUpdate") { + val avlProver = new BatchAVLProver[Digest32, Blake2b256.type](keyLength = 32, None) + + val elements = Seq(123, 22) + val treeElements = elements.map(i => Longs.toByteArray(i)).map(s => (ADKey @@@ Blake2b256(s), ADValue @@ s)) + treeElements.foreach(s => avlProver.performOneOperation(Insert(s._1, s._2))) + avlProver.generateProof() + val treeData = new AvlTreeData(avlProver.digest.toColl, AvlTreeFlags.AllOperationsAllowed, 32, None) + + val elements2 = Seq(1, 22) + val treeElements2 = elements2.map(i => Longs.toByteArray(i)).map(s => (ADKey @@@ Blake2b256(s), ADValue @@ s)) + treeElements2.foreach(s => avlProver.performOneOperation(InsertOrUpdate(s._1, s._2))) + val updateProof = avlProver.generateProof() + val treeData2 = new AvlTreeData(avlProver.digest.toColl, AvlTreeFlags.AllOperationsAllowed, 32, None) + + val v: Coll[(Coll[Byte], Coll[Byte])] = treeElements2.map(t => t._1.toColl -> t._2.toColl).toArray.toColl + val ops = IR.builder.mkConstant[SType](v.asWrappedType, SCollection(STuple(SByteArray, SByteArray))) + + val customExt = Seq( + 21.toByte -> AvlTreeConstant(treeData), + 22.toByte -> AvlTreeConstant(treeData2), + 23.toByte -> ops, + 24.toByte -> ByteArrayConstant(updateProof) + ) + + def deserTest() = test("deserializeTo", env, customExt, + s"""{ + val tree1 = getVar[AvlTree](21).get + val tree2 = getVar[AvlTree](22).get + + val toInsert = getVar[Coll[(Coll[Byte], Coll[Byte])]](23).get + val proof = getVar[Coll[Byte]](24).get + + val tree1Updated = tree1.insertOrUpdate(toInsert, proof).get + tree2.digest == tree1Updated.digest + }""", + null, + true + ) + + if (VersionContext.current.isV6SoftForkActivated) { + deserTest() + } else { + an[ValidationException] should be thrownBy deserTest() + } + } + }