Skip to content

Commit

Permalink
todo-v5.x: fix undeterministic behaviour in JsonCodecs
Browse files Browse the repository at this point in the history
  • Loading branch information
aslesarenko committed Aug 20, 2023
1 parent 748962d commit 1bf6205
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 29 deletions.
14 changes: 6 additions & 8 deletions interpreter/shared/src/main/scala/org/ergoplatform/ErgoBox.scala
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
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._
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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 <-
Expand Down
13 changes: 9 additions & 4 deletions sdk/shared/src/main/scala/org/ergoplatform/sdk/JsonCodecs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
})


Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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") {
Expand All @@ -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
}
}
Expand All @@ -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),
Expand Down

0 comments on commit 1bf6205

Please sign in to comment.