Skip to content
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 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. */
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 inserted or updated
* @return Success(Some(value)), Success(None), or Failure
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense to describe each case of returned result.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

*/
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.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,
Expand Down
16 changes: 15 additions & 1 deletion docs/LangSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
22 changes: 20 additions & 2 deletions interpreter/shared/src/main/scala/sigmastate/eval/Extensions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -120,6 +120,24 @@ object Extensions {
}
}

def insertOrUpdate(
entries: Coll[(Coll[Byte], Coll[Byte])],
proof: Coll[Byte]): Option[AvlTree] = {
if (!tree.isInsertAllowed || !tree.isUpdateAllowed) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both flags are required, however for each given tree it will either be insert or update (but not both).
Is it possible to make it more flexible and require the flag only if the corresponding operation is happening for a given tree.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

" for each given tree it will either be insert or update (but not both)" - no, it could be insert for one key and update for another

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
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo can be removed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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) { () =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
addSeqCost(UpdateAvlTree_Info, nItems) { () =>
// 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] = {
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
Loading
Loading