From 1bf620514899e6dd4db2d087487695d4a61362bf Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Sun, 20 Aug 2023 17:53:26 +0200 Subject: [PATCH] todo-v5.x: fix undeterministic behaviour in JsonCodecs --- .../main/scala/org/ergoplatform/ErgoBox.scala | 14 ++-- .../org/ergoplatform/ErgoBoxCandidate.scala | 2 +- .../generators/ObjectGenerators.scala | 6 +- .../org/ergoplatform/sdk/JsonCodecs.scala | 13 ++-- .../sdk/JsonSerializationSpec.scala | 67 +++++++++++++++---- 5 files changed, 73 insertions(+), 29 deletions(-) diff --git a/interpreter/shared/src/main/scala/org/ergoplatform/ErgoBox.scala b/interpreter/shared/src/main/scala/org/ergoplatform/ErgoBox.scala index 37a6e96193..9d373fb36a 100644 --- a/interpreter/shared/src/main/scala/org/ergoplatform/ErgoBox.scala +++ b/interpreter/shared/src/main/scala/org/ergoplatform/ErgoBox.scala @@ -1,11 +1,11 @@ package org.ergoplatform -import scorex.utils.{Ints, Shorts} -import org.ergoplatform.ErgoBox.{NonMandatoryRegisterId, Token} +import org.ergoplatform.ErgoBox.{AdditionalRegisters, Token} import org.ergoplatform.settings.ErgoAlgos import scorex.crypto.authds.ADKey -import scorex.crypto.hash.{Blake2b256, Digest32} +import scorex.crypto.hash.Blake2b256 import scorex.util._ +import scorex.utils.{Ints, Shorts} import sigmastate.SCollection.SByteArray import sigmastate.SType.AnyOps import sigmastate.Values._ @@ -13,12 +13,10 @@ import sigmastate._ import sigmastate.eval.Extensions._ import sigmastate.eval._ import sigmastate.serialization.SigmaSerializer -import sigmastate.utils.{SigmaByteReader, SigmaByteWriter, Helpers} +import sigmastate.utils.{Helpers, SigmaByteReader, SigmaByteWriter} import sigmastate.utxo.ExtractCreationInfo import special.collection._ -import scala.runtime.ScalaRunTime - /** * Box (aka coin, or an unspent output) is a basic concept of a UTXO-based cryptocurrency. In Bitcoin, such an object * is associated with some monetary value (arbitrary, but with predefined precision, so we use integer arithmetic to @@ -52,7 +50,7 @@ class ErgoBox( override val value: Long, override val ergoTree: ErgoTree, override val additionalTokens: Coll[Token] = Colls.emptyColl[Token], - override val additionalRegisters: Map[NonMandatoryRegisterId, _ <: EvaluatedValue[_ <: SType]] = Map.empty, + override val additionalRegisters: AdditionalRegisters = Map.empty, val transactionId: ModifierId, val index: Short, override val creationHeight: Int @@ -136,7 +134,7 @@ object ErgoBox { /** Represents id of optional registers of a box. */ sealed abstract class NonMandatoryRegisterId(override val number: Byte) extends RegisterId - type AdditionalRegisters = Map[NonMandatoryRegisterId, _ <: EvaluatedValue[_ <: SType]] + type AdditionalRegisters = scala.collection.Map[NonMandatoryRegisterId, EvaluatedValue[_ <: SType]] object R0 extends MandatoryRegisterId(0, "Monetary value, in Ergo tokens") object R1 extends MandatoryRegisterId(1, "Guarding script") diff --git a/interpreter/shared/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala b/interpreter/shared/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala index 8d5765caf5..da1fdc94fd 100644 --- a/interpreter/shared/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala +++ b/interpreter/shared/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala @@ -38,7 +38,7 @@ class ErgoBoxCandidate(val value: Long, val ergoTree: ErgoTree, val creationHeight: Int, val additionalTokens: Coll[Token] = Colls.emptyColl, - val additionalRegisters: Map[NonMandatoryRegisterId, _ <: EvaluatedValue[_ <: SType]] = Map()) + val additionalRegisters: AdditionalRegisters = Map()) extends ErgoBoxAssets { /** Transforms this tree to a proposition, substituting the constants if the constant diff --git a/interpreter/shared/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala b/interpreter/shared/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala index 92538aca22..2f032dface 100644 --- a/interpreter/shared/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala +++ b/interpreter/shared/src/test/scala/sigmastate/serialization/generators/ObjectGenerators.scala @@ -210,7 +210,7 @@ trait ObjectGenerators extends TypeGenerators lazy val evaluatedValueGen: Gen[EvaluatedValue[SType]] = Gen.oneOf(booleanConstGen.asInstanceOf[Gen[EvaluatedValue[SType]]], byteArrayConstGen, longConstGen) - def additionalRegistersGen(cnt: Byte): Seq[Gen[(NonMandatoryRegisterId, EvaluatedValue[SType])]] = { + def additionalRegistersGen(cnt: Byte): Seq[Gen[(NonMandatoryRegisterId, EvaluatedValue[_ <: SType])]] = { scala.util.Random.shuffle((0 until cnt).toList) .map(_ + ErgoBox.startingNonMandatoryIndex) .map(rI => ErgoBox.registerByIndex(rI).asInstanceOf[NonMandatoryRegisterId]) @@ -348,7 +348,9 @@ trait ObjectGenerators extends TypeGenerators lazy val additionalRegistersGen: Gen[AdditionalRegisters] = for { regNum <- Gen.chooseNum[Byte](0, ErgoBox.nonMandatoryRegistersCount) regs <- Gen.sequence(additionalRegistersGen(regNum))(Buildable.buildableSeq) - } yield regs.toMap + } yield + Map(regs.toIndexedSeq:_*) +// mutable.LinkedHashMap(regs.toIndexedSeq.sortBy(_._1.number):_*) def ergoBoxTokens(availableTokens: Seq[TokenId]): Gen[Coll[Token]] = for { tokens <- diff --git a/sdk/shared/src/main/scala/org/ergoplatform/sdk/JsonCodecs.scala b/sdk/shared/src/main/scala/org/ergoplatform/sdk/JsonCodecs.scala index aae3410df4..741c84ae9f 100644 --- a/sdk/shared/src/main/scala/org/ergoplatform/sdk/JsonCodecs.scala +++ b/sdk/shared/src/main/scala/org/ergoplatform/sdk/JsonCodecs.scala @@ -18,6 +18,7 @@ import sigmastate.interpreter.{ContextExtension, ProverResult} import sigmastate.{AvlTreeData, AvlTreeFlags, SType} import special.collection.Coll import special.sigma.{AnyValue, Header, PreHeader} + import scala.util.Try import sigmastate.utils.Helpers._ // required for Scala 2.11 import org.ergoplatform.ErgoBox @@ -259,8 +260,8 @@ trait JsonCodecs { implicit val proverResultDecoder: Decoder[ProverResult] = Decoder.instance({ cursor => for { proofBytes <- cursor.downField("proofBytes").as[Array[Byte]] - extMap <- cursor.downField("extension").as[Map[Byte, EvaluatedValue[SType]]] - } yield ProverResult(proofBytes, ContextExtension(extMap)) + ext <- cursor.downField("extension").as[ContextExtension] + } yield ProverResult(proofBytes, ext) }) @@ -296,13 +297,17 @@ trait JsonCodecs { decodeErgoTree(_.asInstanceOf[ErgoTree]) } - implicit def registersEncoder[T <: EvaluatedValue[_ <: SType]]: Encoder[Map[NonMandatoryRegisterId, T]] = Encoder.instance({ m => + implicit def registersEncoder[T <: EvaluatedValue[_ <: SType]]: Encoder[scala.collection.Map[NonMandatoryRegisterId, T]] = Encoder.instance({ m => Json.obj( m.toSeq .sortBy(_._1.number) .map { case (k, v) => registerIdEncoder(k) -> evaluatedValueEncoder(v) }: _*) }) + implicit def registersDecoder[T <: EvaluatedValue[_ <: SType]]: Decoder[scala.collection.Map[NonMandatoryRegisterId, T]] = Decoder.instance({ implicit m => + m.as[mutable.LinkedHashMap[NonMandatoryRegisterId, EvaluatedValue[SType]]].asInstanceOf[Decoder.Result[scala.collection.Map[NonMandatoryRegisterId, T]]] + }) + implicit val ergoBoxEncoder: Encoder[ErgoBox] = Encoder.instance({ box => Json.obj( "boxId" -> box.id.asJson, @@ -322,7 +327,7 @@ trait JsonCodecs { ergoTreeBytes <- cursor.downField("ergoTree").as[Array[Byte]] additionalTokens <- cursor.downField("assets").as(Decoder.decodeSeq(assetDecoder)) creationHeight <- cursor.downField("creationHeight").as[Int] - additionalRegisters <- cursor.downField("additionalRegisters").as[Map[NonMandatoryRegisterId, EvaluatedValue[SType]]] + additionalRegisters <- cursor.downField("additionalRegisters").as[scala.collection.Map[NonMandatoryRegisterId, EvaluatedValue[SType]]] transactionId <- cursor.downField("transactionId").as[ModifierId] index <- cursor.downField("index").as[Short] } yield new ErgoBox( diff --git a/sdk/shared/src/test/scala/org/ergoplatform/sdk/JsonSerializationSpec.scala b/sdk/shared/src/test/scala/org/ergoplatform/sdk/JsonSerializationSpec.scala index 44d7030df2..1ff7402379 100644 --- a/sdk/shared/src/test/scala/org/ergoplatform/sdk/JsonSerializationSpec.scala +++ b/sdk/shared/src/test/scala/org/ergoplatform/sdk/JsonSerializationSpec.scala @@ -19,14 +19,9 @@ import sigmastate.serialization.SerializationSpecification import sigmastate.utils.Helpers.DecoderResultOps // required for Scala 2.11 (extension method toTry) import special.collection.Coll import special.sigma.{Header, PreHeader} -import org.ergoplatform.ErgoLikeContext -import org.ergoplatform.DataInput -import org.ergoplatform.Input -import org.ergoplatform.UnsignedInput -import org.ergoplatform.ErgoBox -import org.ergoplatform.ErgoLikeTransaction -import org.ergoplatform.UnsignedErgoLikeTransaction -import org.ergoplatform.ErgoLikeTransactionTemplate +import org.ergoplatform.{DataInput, ErgoBox, ErgoBoxCandidate, ErgoLikeContext, ErgoLikeTransaction, ErgoLikeTransactionTemplate, Input, UnsignedErgoLikeTransaction, UnsignedInput} + +import scala.collection.mutable class JsonSerializationSpec extends SerializationSpecification with JsonCodecs { @@ -84,19 +79,25 @@ class JsonSerializationSpec extends SerializationSpecification with JsonCodecs { } property("Input should be encoded into JSON and decoded back correctly") { - forAll(inputGen) { v: Input => jsonRoundTrip(v) } + forAll(inputGen, MinSuccessful(50)) { v: Input => jsonRoundTrip(v) } } property("UnsignedInput should be encoded into JSON and decoded back correctly") { - forAll(unsignedInputGen) { v: UnsignedInput => jsonRoundTrip(v) } + forAll(unsignedInputGen, MinSuccessful(50)) { v: UnsignedInput => jsonRoundTrip(v) } } property("ContextExtension should be encoded into JSON and decoded back correctly") { - forAll(contextExtensionGen) { v: ContextExtension => jsonRoundTrip(v) } + forAll(contextExtensionGen, MinSuccessful(500)) { v: ContextExtension => jsonRoundTrip(v) } + } + + property("AdditionalRegisters should be encoded into JSON and decoded back correctly") { + forAll(additionalRegistersGen, MinSuccessful(500)) { regs => + jsonRoundTrip(regs)(registersEncoder, registersDecoder) + } } property("ProverResult should be encoded into JSON and decoded back correctly") { - forAll(serializedProverResultGen) { v: ProverResult => jsonRoundTrip(v) } + forAll(serializedProverResultGen, MinSuccessful(500)) { v: ProverResult => jsonRoundTrip(v) } } property("AvlTreeData should be encoded into JSON and decoded back correctly") { @@ -119,8 +120,46 @@ class JsonSerializationSpec extends SerializationSpecification with JsonCodecs { forAll(unsignedErgoLikeTransactionGen) { v: UnsignedErgoLikeTransaction => jsonRoundTrip(v) } } + private def sortRegisters(box: ErgoBoxCandidate): ErgoBoxCandidate = box match { + case box: ErgoBox => + new ErgoBox(box.value, + box.ergoTree, + box.additionalTokens, + mutable.LinkedHashMap(box.additionalRegisters.toIndexedSeq.sortBy(_._1.number): _*), + box.transactionId, + box.index, + box.creationHeight + ) + case box: ErgoBoxCandidate => + new ErgoBoxCandidate(box.value, + box.ergoTree, + box.creationHeight, + box.additionalTokens, + mutable.LinkedHashMap(box.additionalRegisters.toIndexedSeq.sortBy(_._1.number): _*) + ) + } + + private def sortRegisters(tx: ErgoLikeTransactionTemplate[_ <: UnsignedInput]): ErgoLikeTransactionTemplate[_ <: UnsignedInput] = { + tx match { + case tx: ErgoLikeTransaction => + new ErgoLikeTransaction(tx.inputs, + tx.dataInputs, + tx.outputCandidates.map { out => + sortRegisters(out) + } + ) + case tx: UnsignedErgoLikeTransaction => + new UnsignedErgoLikeTransaction(tx.inputs, + tx.dataInputs, + tx.outputCandidates.map { out => + sortRegisters(out) + } + ) + } + } + property("ErgoLikeTransactionTemplate should be encoded into JSON and decoded back correctly") { - forAll(ergoLikeTransactionTemplateGen) { v: ErgoLikeTransactionTemplate[_ <: UnsignedInput] => + forAll(ergoLikeTransactionTemplateGen, MinSuccessful(50)) { v: ErgoLikeTransactionTemplate[_ <: UnsignedInput] => v.asJson.as(ergoLikeTransactionTemplateDecoder).toTry.get shouldEqual v } } @@ -136,7 +175,7 @@ class JsonSerializationSpec extends SerializationSpecification with JsonCodecs { CryptoConstants.dlogGroup.ctx.decodePoint(point).asInstanceOf[CryptoConstants.EcPointType] ) }.get - val regs = Map( + val regs = scala.collection.Map( R7 -> LongArrayConstant(Array(1L, 2L, 1234123L)), R4 -> ByteConstant(1), R6 -> IntConstant(10),