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] Fix semantics of AvlTree.insert & new AvlTree.insertOrUpdate method #1038

Open
wants to merge 5 commits into
base: v6.0.0
Choose a base branch
from
Open
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
18 changes: 12 additions & 6 deletions data/shared/src/main/scala/sigma/SigmaDataReflection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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])
}
)
)
Expand Down
74 changes: 57 additions & 17 deletions data/shared/src/main/scala/sigma/ast/methods.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 insert 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 in this authenticated dictionary.
| * @param proof
| */
|
""".stripMargin)

/** Implements evaluation of AvlTree.insert 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. */
Expand Down
2 changes: 2 additions & 0 deletions data/shared/src/main/scala/sigma/ast/values.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions data/shared/src/main/scala/sigma/eval/AvlTreeVerifier.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ 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 updated
* @return Success(Some(value)), Success(None), or Failure
*/
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`.
Expand Down
7 changes: 7 additions & 0 deletions data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ abstract class ErgoTreeEvaluator {
mc: MethodCall, tree: AvlTree,
operations: KeyValueColl, proof: Coll[Byte]): Option[AvlTree]

/** Implements evaluation of AvlTree.insert 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class CErgoTreeEvaluator(
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) {
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
Expand All @@ -173,7 +173,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)
Expand All @@ -192,6 +192,37 @@ 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)
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] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand All @@ -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
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand Down Expand Up @@ -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]),
Expand All @@ -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"
))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading