From ea3152334a2209a964f5e1627d70fba02feef44d Mon Sep 17 00:00:00 2001 From: Alexander Chepurnoy Date: Mon, 16 Dec 2024 23:33:03 +0300 Subject: [PATCH] polishing, doc and comments update --- .../src/main/scala/sigma/ast/methods.scala | 10 +- .../scala/sigma/eval/AvlTreeVerifier.scala | 2 +- .../scala/sigma/eval/ErgoTreeEvaluator.scala | 2 +- docs/LangSpec.md | 16 +- .../scala/sigmastate/eval/Extensions.scala | 22 +- .../scala/sigma/LanguageSpecificationV5.scala | 143 ------------ .../scala/sigma/LanguageSpecificationV6.scala | 216 +++++++++++++++++- 7 files changed, 257 insertions(+), 154 deletions(-) diff --git a/data/shared/src/main/scala/sigma/ast/methods.scala b/data/shared/src/main/scala/sigma/ast/methods.scala index 097a20a515..25ad4f94cb 100644 --- a/data/shared/src/main/scala/sigma/ast/methods.scala +++ b/data/shared/src/main/scala/sigma/ast/methods.scala @@ -1765,19 +1765,19 @@ case object SAvlTreeMethods extends MonoTypeMethods { .withIRInfo(MethodCallIrBuilder) .withInfo(MethodCall, """ - | /** Perform insertions of key-value entries into this tree using proof `proof`. + | /** 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 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. + | * + | * @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.insert method call ErgoTree node. + /** Implements evaluation of AvlTree.insertOrUpdate method call ErgoTree node. * Called via reflection based on naming convention. * @see SMethod.evalMethod */ diff --git a/data/shared/src/main/scala/sigma/eval/AvlTreeVerifier.scala b/data/shared/src/main/scala/sigma/eval/AvlTreeVerifier.scala index 60247c00ba..757b89afb6 100644 --- a/data/shared/src/main/scala/sigma/eval/AvlTreeVerifier.scala +++ b/data/shared/src/main/scala/sigma/eval/AvlTreeVerifier.scala @@ -57,7 +57,7 @@ trait AvlTreeVerifier { * is None. * * @param key key to look up - * @param value value to check it was updated + * @param value value to check it was inserted or updated * @return Success(Some(value)), Success(None), or Failure */ def performInsertOrUpdate(key: Array[Byte], value: Array[Byte]): Try[Option[Array[Byte]]] diff --git a/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala b/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala index 4015524718..ba3f6d5c95 100644 --- a/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala +++ b/data/shared/src/main/scala/sigma/eval/ErgoTreeEvaluator.scala @@ -134,7 +134,7 @@ abstract class ErgoTreeEvaluator { mc: MethodCall, tree: AvlTree, operations: KeyValueColl, proof: Coll[Byte]): Option[AvlTree] - /** Implements evaluation of AvlTree.insert method call ErgoTree node. */ + /** Implements evaluation of AvlTree.insertOrUpdate method call ErgoTree node. */ def insertOrUpdate_eval( mc: MethodCall, tree: AvlTree, diff --git a/docs/LangSpec.md b/docs/LangSpec.md index 3f5453072f..4076146795 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/Extensions.scala b/interpreter/shared/src/main/scala/sigmastate/eval/Extensions.scala index f9cb0e9f75..98f044af9b 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/sc/shared/src/test/scala/sigma/LanguageSpecificationV5.scala b/sc/shared/src/test/scala/sigma/LanguageSpecificationV5.scala index 0bb92f8249..c670d60c66 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 29b78d0e97..e6844e7363 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)) + } + }