diff --git a/LICENSE b/LICENSE index 7a1866a..dc5d448 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 0bsNetwork +Copyright (c) 2019 0bsNetwork Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6a3c9ca..52e667a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -# Zbs [![Build Status](https://travis-ci.org/0bsnetwork/Zbs.svg?branch=master)](https://travis-ci.org/0bsnetwork/Zbs) - +# 0bsNetwork Full Node In the master branch there is a code with functions that is under development. The latest release for each network can be found in the [Releases section](https://github.com/0bsnetwork/Zbs/releases), you can switch to the corresponding tag and build the application. @@ -48,10 +47,13 @@ Mutiple features can be seperated by commas; features = [9,10,11] ``` -Note: Features < 9 have been pre-activated on this current node version. +Note: Features <= 11 have been pre-activated on the chain. # Tests & Coverage ``` - sbt -J-XX:MaxMetaspaceSize=512M -J-XX:MetaspaceSize=512M -J-Xms2048M -J-Xmx2048M -J-Xss6M -J-XX:MaxPermSize=512M ";coverage;checkPR;coverageReport" +unset _JAVA_OPTIONS +unset SBT_OPTS +export JAVA_TOOL_OPTIONS="-Xmx1548m" +sbt -J-Xms128m -J-Xmx1248m -J-XX:+UseConcMarkSweepGC -J-XX:+CMSClassUnloadingEnabled ";coverage;checkPR;coverageReport" ``` \ No newline at end of file diff --git a/benchmark/src/test/scala/com/zbsnetwork/serialization/protobuf/ProtoBufBenchmark.scala b/benchmark/src/test/scala/com/zbsnetwork/serialization/protobuf/ProtoBufBenchmark.scala new file mode 100644 index 0000000..a3efbdb --- /dev/null +++ b/benchmark/src/test/scala/com/zbsnetwork/serialization/protobuf/ProtoBufBenchmark.scala @@ -0,0 +1,75 @@ +package com.zbsnetwork.serialization.protobuf + +import java.util.concurrent.TimeUnit + +import com.zbsnetwork.account.PublicKeyAccount +import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.Base58 +import com.zbsnetwork.protobuf.transaction.PBTransactions +import com.zbsnetwork.transaction.Proofs +import com.zbsnetwork.transaction.transfer.MassTransferTransaction +import com.zbsnetwork.transaction.transfer.MassTransferTransaction.Transfer +import org.openjdk.jmh.annotations._ +import org.openjdk.jmh.infra.Blackhole + +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Array(Mode.AverageTime)) +@Threads(1) +@Fork(1) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +class ProtoBufBenchmark { + + @Benchmark + def serializeMassTransferPB_test(bh: Blackhole): Unit = { + val vanillaTx = { + val transfers = MassTransferTransaction + .parseTransfersList( + List(Transfer("3N5GRqzDBhjVXnCn44baHcz2GoZy5qLxtTh", 100000000L), Transfer("3N5GRqzDBhjVXnCn44baHcz2GoZy5qLxtTh", 200000000L))) + .right + .get + + MassTransferTransaction + .create( + None, + PublicKeyAccount.fromBase58String("FM5ojNqW7e9cZ9zhPYGkpSP1Pcd8Z3e3MNKYVS5pGJ8Z").right.get, + transfers, + 1518091313964L, + 200000, + Base58.decode("59QuUcqP6p").get, + Proofs(Seq(ByteStr.decodeBase58("FXMNu3ecy5zBjn9b69VtpuYRwxjCbxdkZ3xZpLzB8ZeFDvcgTkmEDrD29wtGYRPtyLS3LPYrL2d5UM6TpFBMUGQ").get)) + ) + .right + .get + } + + val tx = PBTransactions.protobuf(vanillaTx) + bh.consume(tx.toByteArray) + } + + @Benchmark + def serializeMassTransferVanilla_test(bh: Blackhole): Unit = { + val vanillaTx = { + val transfers = MassTransferTransaction + .parseTransfersList( + List(Transfer("3N5GRqzDBhjVXnCn44baHcz2GoZy5qLxtTh", 100000000L), Transfer("3N5GRqzDBhjVXnCn44baHcz2GoZy5qLxtTh", 200000000L))) + .right + .get + + MassTransferTransaction + .create( + None, + PublicKeyAccount.fromBase58String("FM5ojNqW7e9cZ9zhPYGkpSP1Pcd8Z3e3MNKYVS5pGJ8Z").right.get, + transfers, + 1518091313964L, + 200000, + Base58.decode("59QuUcqP6p").get, + Proofs(Seq(ByteStr.decodeBase58("FXMNu3ecy5zBjn9b69VtpuYRwxjCbxdkZ3xZpLzB8ZeFDvcgTkmEDrD29wtGYRPtyLS3LPYrL2d5UM6TpFBMUGQ").get)) + ) + .right + .get + } + + bh.consume(vanillaTx.bytes()) + } +} diff --git a/benchmark/src/test/scala/com/zbsnetwork/state/LevelDBWriterBenchmark.scala b/benchmark/src/test/scala/com/zbsnetwork/state/LevelDBWriterBenchmark.scala index 9e18f02..6ff03ab 100644 --- a/benchmark/src/test/scala/com/zbsnetwork/state/LevelDBWriterBenchmark.scala +++ b/benchmark/src/test/scala/com/zbsnetwork/state/LevelDBWriterBenchmark.scala @@ -11,6 +11,7 @@ import com.zbsnetwork.database.LevelDBWriter import com.zbsnetwork.db.LevelDBFactory import com.zbsnetwork.settings.{ZbsSettings, loadConfig} import com.zbsnetwork.state.LevelDBWriterBenchmark._ +import com.zbsnetwork.transaction.AssetId import com.zbsnetwork.utils.Implicits.SubjectOps import monix.reactive.subjects.Subject import org.iq80.leveldb.{DB, Options} @@ -91,8 +92,10 @@ object LevelDBWriterBenchmark { LevelDBFactory.factory.open(dir, new Options) } - private val ignorePortfolioChanged: Subject[Address, Address] = Subject.empty[Address] - val db = new LevelDBWriter(rawDB, ignorePortfolioChanged, zbsSettings.blockchainSettings.functionalitySettings, 100000, 2000, 120 * 60 * 1000) + private val ignoreSpendableBalanceChanged = Subject.empty[(Address, Option[AssetId])] + + val db = + new LevelDBWriter(rawDB, ignoreSpendableBalanceChanged, zbsSettings.blockchainSettings.functionalitySettings, 100000, 2000, 120 * 60 * 1000) @TearDown def close(): Unit = { diff --git a/build.sbt b/build.sbt index 0bfdd9f..dd0fb66 100644 --- a/build.sbt +++ b/build.sbt @@ -15,7 +15,7 @@ val versionSource = Def.task { // Please, update the fallback version every major and minor releases. // This version is used then building from sources without Git repository // In case of not updating the version nodes build from headless sources will fail to connect to newer versions - val FallbackVersion = (0, 16, 0) + val FallbackVersion = (0, 16, 2) val versionFile = (sourceManaged in Compile).value / "com" / "zbsnetwork" / "Version.scala" val versionExtractor = """(\d+)\.(\d+)\.(\d+).*""".r @@ -41,7 +41,7 @@ name := "zbs" normalizedName := s"${name.value}${network.value.packageSuffix}" git.useGitDescribe := true -git.uncommittedSignifier := Some("") +git.uncommittedSignifier := Some("MT") logBuffered := false inThisBuild( @@ -49,7 +49,13 @@ inThisBuild( scalaVersion := "2.12.8", organization := "com.zbsnetwork", crossPaths := false, - scalacOptions ++= Seq("-feature", "-deprecation", "-language:higherKinds", "-language:implicitConversions", "-Ywarn-unused:-implicits", "-Xlint") + scalacOptions ++= Seq("-feature", + "-deprecation", + "-language:higherKinds", + "-language:implicitConversions", + "-Ywarn-unused:-implicits", + "-Xlint", + "-Ywarn-unused-import") )) resolvers ++= Seq( @@ -67,7 +73,7 @@ val java9Options = Seq( fork in run := true javaOptions in run ++= java9Options -Test / fork := true +Test / fork := false Test / javaOptions ++= java9Options Jmh / javaOptions ++= java9Options @@ -110,7 +116,9 @@ inConfig(Compile)( mainClass := Some("com.zbsnetwork.Application"), publishArtifact in packageDoc := false, publishArtifact in packageSrc := false, - sourceGenerators += versionSource + sourceGenerators += versionSource, + PB.targets += scalapb.gen(flatPackage = true) -> (sourceManaged in Compile).value, + PB.deleteTargetDirectory := false )) inConfig(Test)( @@ -213,8 +221,8 @@ def allProjects: List[ProjectReference] = ReflectUtilities.allVals[Project](this addCommandAlias( "checkPR", + // set scalacOptions in ThisBuild ++= Seq("-Xfatal-warnings"); """; - |set scalacOptions in ThisBuild ++= Seq("-Xfatal-warnings"); |Global / checkPRRaw; |set scalacOptions in ThisBuild -= "-Xfatal-warnings"; """.stripMargin @@ -233,6 +241,7 @@ checkPRRaw in Global := { lazy val common = crossProject(JSPlatform, JVMPlatform) .withoutSuffixFor(JVMPlatform) + .disablePlugins(ProtocPlugin) .settings( libraryDependencies ++= Dependencies.scalatest ) @@ -243,6 +252,7 @@ lazy val commonJVM = common.jvm lazy val lang = crossProject(JSPlatform, JVMPlatform) .withoutSuffixFor(JVMPlatform) + .disablePlugins(ProtocPlugin) .settings( version := "1.0.0", coverageExcludedPackages := ".*", @@ -308,7 +318,7 @@ lazy val node = project Dependencies.http ++ Dependencies.akka ++ Dependencies.serialization ++ - Dependencies.testKit.map(_ % "test") ++ + Dependencies.testKit.map(_ % Test) ++ Dependencies.logging ++ Dependencies.matcher ++ Dependencies.metrics ++ @@ -317,11 +327,18 @@ lazy val node = project Dependencies.ficus ++ Dependencies.scorex ++ Dependencies.commons_net ++ - Dependencies.monix.value + Dependencies.monix.value ++ + Dependencies.protobuf.value ++ + Dependencies.grpc, + dependencyOverrides ++= Seq( + Dependencies.AkkaActor, + Dependencies.AkkaStream, + Dependencies.AkkaHTTP + ) ) .dependsOn(langJVM, commonJVM) -lazy val discovery = project +///lazy val discovery = project lazy val it = project .dependsOn(node) diff --git a/common/shared/src/main/scala/com/zbsnetwork/common/state/ByteStr.scala b/common/shared/src/main/scala/com/zbsnetwork/common/state/ByteStr.scala index 5f27493..2faf824 100644 --- a/common/shared/src/main/scala/com/zbsnetwork/common/state/ByteStr.scala +++ b/common/shared/src/main/scala/com/zbsnetwork/common/state/ByteStr.scala @@ -5,14 +5,6 @@ import com.zbsnetwork.common.utils.{Base58, Base64} import scala.util.Try case class ByteStr(arr: Array[Byte]) { - - override def equals(a: Any): Boolean = a match { - case other: ByteStr => arr.sameElements(other.arr) - case _ => false - } - - override def hashCode(): Int = java.util.Arrays.hashCode(arr) - lazy val base58: String = Base58.encode(arr) lazy val base64: String = "base64:" + Base64.encode(arr) @@ -52,23 +44,27 @@ case class ByteStr(arr: Array[Byte]) { def dropRight(n: Long): ByteStr = take(arr.length.toLong - n.max(0)) + override def equals(a: Any): Boolean = a match { + case other: ByteStr => arr.sameElements(other.arr) + case _ => false + } + + override def hashCode(): Int = java.util.Arrays.hashCode(arr) } object ByteStr { - val empty: ByteStr = ByteStr(Array.emptyByteArray) - def fromBytes(bytes: Byte*): ByteStr = { - - val buf = new Array[Byte](bytes.size) - var i = 0 + implicit def fromByteArray(arr: Array[Byte]): ByteStr = { + new ByteStr(arr) + } - bytes.foreach { b => - buf(i) = b - i += 1 - } + implicit def toByteArray(bs: ByteStr): Array[Byte] = { + bs.arr + } - ByteStr(buf) + def fromBytes(bytes: Byte*): ByteStr = { + ByteStr(bytes.toArray) } def fromLong(l: Long): ByteStr = { diff --git a/common/shared/src/main/scala/com/zbsnetwork/common/utils/package.scala b/common/shared/src/main/scala/com/zbsnetwork/common/utils/package.scala index 3aa7775..b62f0e8 100644 --- a/common/shared/src/main/scala/com/zbsnetwork/common/utils/package.scala +++ b/common/shared/src/main/scala/com/zbsnetwork/common/utils/package.scala @@ -1,12 +1,20 @@ package com.zbsnetwork.common +import scala.util.{Failure, Success, Try} package object utils { implicit class EitherExt2[A, B](ei: Either[A, B]) { + def explicitGet(): B = ei match { case Left(value) => throw new Exception(value.toString) case Right(value) => value } - } + def foldToTry: Try[B] = { + ei.fold( + left => Failure(new Exception(left.toString)), + right => Success(right) + ) + } + } } diff --git a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/DexGenApp.scala b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/DexGenApp.scala index abf628f..84872c3 100644 --- a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/DexGenApp.scala +++ b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/DexGenApp.scala @@ -3,11 +3,11 @@ package com.zbsnetwork.dexgen import java.util.concurrent.Executors import cats.implicits.showInterpolator -import cats.instances.byte import com.typesafe.config.ConfigFactory import com.zbsnetwork.account.{AddressOrAlias, AddressScheme, PrivateKeyAccount} import com.zbsnetwork.api.http.assets.{SignedIssueV2Request, SignedMassTransferRequest} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.dexgen.cli.ScoptImplicits import com.zbsnetwork.dexgen.config.FicusImplicits import com.zbsnetwork.dexgen.utils.{ApiRequests, GenOrderType} @@ -19,7 +19,6 @@ import com.zbsnetwork.transaction.assets.IssueTransactionV2 import com.zbsnetwork.transaction.transfer.MassTransferTransaction import com.zbsnetwork.transaction.transfer.MassTransferTransaction.ParsedTransfer import com.zbsnetwork.utils.LoggerFacade -import com.zbsnetwork.common.utils.EitherExt2 import net.ceedubs.ficus.Ficus._ import net.ceedubs.ficus.readers.ArbitraryTypeReader._ import net.ceedubs.ficus.readers.{EnumerationReader, NameMapper} @@ -79,6 +78,7 @@ object DexGenApp extends App with ScoptImplicits with FicusImplicits with Enumer def issueAssets(endpoint: String, richAddressSeed: String, n: Int)(implicit tag: String): Seq[AssetId] = { val node = api.to(endpoint) + val now = System.currentTimeMillis() val assetsTx: Seq[IssueTransactionV2] = (1 to n).map { i => IssueTransactionV2 .selfSigned( @@ -89,7 +89,7 @@ object DexGenApp extends App with ScoptImplicits with FicusImplicits with Enumer decimals = 2, reissuable = false, fee = 100000000, - timestamp = System.currentTimeMillis(), + timestamp = now + i, sender = PrivateKeyAccount.fromSeed(richAddressSeed).explicitGet(), script = None ) diff --git a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/Worker.scala b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/Worker.scala index 89a48ba..cba2f65 100644 --- a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/Worker.scala +++ b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/Worker.scala @@ -122,7 +122,7 @@ class Worker(workerSettings: Settings, implicit val signedTransferRequestWrites: Writes[SignedTransferV1Request] = Json.writes[SignedTransferV1Request].transform((jsobj: JsObject) => jsobj + ("type" -> JsNumber(TransferTransactionV1.typeId.toInt))) - def transfer(sender: PrivateKeyAccount, assetId: Option[AssetId], recipient: PrivateKeyAccount, halfBalance: Boolean)( + def transfer(i: Long, sender: PrivateKeyAccount, assetId: Option[AssetId], recipient: PrivateKeyAccount, halfBalance: Boolean)( implicit tag: String): Future[Transaction] = to(endpoint).balance(sender.address, assetId).flatMap { balance => val halfAmount = if (halfBalance) balance / 2 else balance @@ -132,7 +132,7 @@ class Worker(workerSettings: Settings, sender, AddressOrAlias.fromString(PublicKeyAccount(recipient.publicKey).address).right.get, transferAmount, - now, + now + i, None, fee, Array.emptyByteArray) match { @@ -147,7 +147,7 @@ class Worker(workerSettings: Settings, } } - def send(orderType: GenOrderType.Value): Future[Any] = { + def send(i: Long, orderType: GenOrderType.Value): Future[Any] = { implicit val tag: String = s"$orderType, ${Random.nextInt(1, 1000000)}" val tradingAssetsSize = tradingAssets.size @@ -196,9 +196,9 @@ class Worker(workerSettings: Settings, for { _ <- cancelAllOrders(fakeAccounts) _ <- sellOrder(DefaultAmount, DefaultPrice, seller, pair)._2 - _ <- transfer(seller, pair.amountAsset, buyer, halfBalance = false) + _ <- transfer(i, seller, pair.amountAsset, buyer, halfBalance = false) _ <- buyOrder(DefaultAmount, DefaultPrice, buyer, pair)._2 - _ <- transfer(buyer, pair.amountAsset, seller, halfBalance = true) + _ <- transfer(i, buyer, pair.amountAsset, seller, halfBalance = true) _ <- cancelAllOrders(fakeAccounts) } yield () @@ -209,9 +209,9 @@ class Worker(workerSettings: Settings, for { _ <- cancelAllOrders(fakeAccounts) _ <- buyOrder(DefaultAmount, DefaultPrice, buyer, pair)._2 - _ <- transfer(buyer, pair.amountAsset, seller, halfBalance = false) + _ <- transfer(i, buyer, pair.amountAsset, seller, halfBalance = false) _ <- sellOrder(DefaultAmount, DefaultPrice, seller, pair)._2 - _ <- transfer(seller, pair.amountAsset, buyer, halfBalance = true) + _ <- transfer(i, seller, pair.amountAsset, buyer, halfBalance = true) } yield () } @@ -220,22 +220,22 @@ class Worker(workerSettings: Settings, } } - private def serial(times: Int)(f: => Future[Any]): Future[Unit] = { - def loop(rest: Int, acc: Future[Unit]): Future[Unit] = { + private def serial(times: Int)(f: Int => Future[Any]): Future[Unit] = { + def loop(rest: Int, i: Int, acc: Future[Unit]): Future[Unit] = { if (rest <= 0) acc else { - val newAcc = acc.flatMap(_ => f).map(_ => ()) - loop(rest - 1, newAcc) + val newAcc = acc.flatMap(_ => f(i)).map(_ => ()) + loop(rest - 1, i + 1, newAcc) } } - loop(times, Future.successful(())) + loop(times, 0, Future.successful(())) } private def placeOrders(maxIterations: Int): Future[Unit] = { def sendAll(step: Int): Future[Unit] = { log.info(s"Step $step") - serial(ordersCount)(send(orderType)) // @TODO Should work in parallel, but now it leads to invalid transfers + serial(ordersCount)(send(_, orderType)) // @TODO Should work in parallel, but now it leads to invalid transfers } def runStepsFrom(step: Int): Future[Unit] = sendAll(step).flatMap { _ => diff --git a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/utils/Gen.scala b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/utils/Gen.scala index 6810f36..1985316 100644 --- a/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/utils/Gen.scala +++ b/dexgenerator/src/main/scala/com/zbsnetwork/dexgen/utils/Gen.scala @@ -21,12 +21,14 @@ object Gen { } def transfers(senderGen: Iterator[PrivateKeyAccount], recipientGen: Iterator[Address], feeGen: Iterator[Long]): Iterator[Transaction] = { + val now = System.currentTimeMillis() senderGen .zip(recipientGen) .zip(feeGen) + .zipWithIndex .map { - case ((src, dst), fee) => - TransferTransactionV1.selfSigned(None, src, dst, fee, System.currentTimeMillis(), None, fee, Array.emptyByteArray) + case (((src, dst), fee), i) => + TransferTransactionV1.selfSigned(None, src, dst, fee, now + i, None, fee, Array.emptyByteArray) } .collect { case Right(x) => x } } diff --git a/generator/src/main/scala/com.zbsnetwork.generator/MultisigTransactionGenerator.scala b/generator/src/main/scala/com.zbsnetwork.generator/MultisigTransactionGenerator.scala index f94d8fe..1a218a0 100644 --- a/generator/src/main/scala/com.zbsnetwork.generator/MultisigTransactionGenerator.scala +++ b/generator/src/main/scala/com.zbsnetwork.generator/MultisigTransactionGenerator.scala @@ -30,19 +30,12 @@ class MultisigTransactionGenerator(settings: MultisigTransactionGenerator.Settin val script: Script = Gen.multiSigScript(owners, 3) - val setScript = SetScriptTransaction.selfSigned(bank, Some(script), enoughFee, System.currentTimeMillis()).explicitGet() + val now = System.currentTimeMillis() + val setScript = SetScriptTransaction.selfSigned(bank, Some(script), enoughFee, now).explicitGet() val res = Range(0, settings.transactions).map { i => val tx = TransferTransactionV2 - .create(None, - bank, - owners(1), - totalAmountOnNewAccount - 2 * enoughFee - i, - System.currentTimeMillis(), - None, - enoughFee, - Array.emptyByteArray, - Proofs.empty) + .create(None, bank, owners(1), totalAmountOnNewAccount - 2 * enoughFee - i, now + i, None, enoughFee, Array.emptyByteArray, Proofs.empty) .explicitGet() val signatures = owners.map(crypto.sign(_, tx.bodyBytes())).map(ByteStr(_)) tx.copy(proofs = Proofs(signatures)) diff --git a/generator/src/main/scala/com.zbsnetwork.generator/NarrowTransactionGenerator.scala b/generator/src/main/scala/com.zbsnetwork.generator/NarrowTransactionGenerator.scala index 1e88cb8..1339fad 100644 --- a/generator/src/main/scala/com.zbsnetwork.generator/NarrowTransactionGenerator.scala +++ b/generator/src/main/scala/com.zbsnetwork.generator/NarrowTransactionGenerator.scala @@ -43,6 +43,8 @@ class NarrowTransactionGenerator(settings: Settings, val accounts: Seq[PrivateKe def generate(n: Int): Seq[Transaction] = { val issueTransactionSender = randomFrom(accounts).get + + val now = System.currentTimeMillis() val tradeAssetIssue = IssueTransactionV1 .selfSigned( issueTransactionSender, @@ -52,7 +54,7 @@ class NarrowTransactionGenerator(settings: Settings, val accounts: Seq[PrivateKe 2, reissuable = false, 100000000L + r.nextInt(100000000), - System.currentTimeMillis() + now ) .right .get @@ -81,10 +83,10 @@ class NarrowTransactionGenerator(settings: Settings, val accounts: Seq[PrivateKe Seq.empty[LeaseTransactionV1], Seq.empty[CreateAliasTransaction] )) { - case ((allTxsWithValid, validIssueTxs, reissuableIssueTxs, activeLeaseTransactions, aliases), _) => + case ((allTxsWithValid, validIssueTxs, reissuableIssueTxs, activeLeaseTransactions, aliases), i) => def moreThatStandartFee = 100000L + r.nextInt(100000) - def ts = System.currentTimeMillis() + val ts = now + i val tx = typeGen.getRandom match { case IssueTransactionV1 => diff --git a/generator/src/main/scala/com.zbsnetwork.generator/OracleTransactionGenerator.scala b/generator/src/main/scala/com.zbsnetwork.generator/OracleTransactionGenerator.scala index a851e06..7b9870b 100644 --- a/generator/src/main/scala/com.zbsnetwork.generator/OracleTransactionGenerator.scala +++ b/generator/src/main/scala/com.zbsnetwork.generator/OracleTransactionGenerator.scala @@ -32,13 +32,12 @@ class OracleTransactionGenerator(settings: Settings, val accounts: Seq[PrivateKe .selfSigned(oracle, settings.requiredData.toList, enoughFee, System.currentTimeMillis()) .explicitGet() - val transactions: List[Transaction] = - List - .fill(settings.transactions) { - TransferTransactionV2 - .selfSigned(None, scriptedAccount, oracle, 1.zbs, System.currentTimeMillis(), None, enoughFee, Array.emptyByteArray) - .explicitGet() - } + val now = System.currentTimeMillis() + val transactions: List[Transaction] = (1 to settings.transactions).map { i => + TransferTransactionV2 + .selfSigned(None, scriptedAccount, oracle, 1.zbs, now + i, None, enoughFee, Array.emptyByteArray) + .explicitGet() + }.toList setScript +: setDataTx +: transactions } diff --git a/generator/src/main/scala/com.zbsnetwork.generator/SmartGenerator.scala b/generator/src/main/scala/com.zbsnetwork.generator/SmartGenerator.scala index 24237bb..24b6273 100644 --- a/generator/src/main/scala/com.zbsnetwork.generator/SmartGenerator.scala +++ b/generator/src/main/scala/com.zbsnetwork.generator/SmartGenerator.scala @@ -20,8 +20,6 @@ class SmartGenerator(settings: SmartGenerator.Settings, val accounts: Seq[Privat private def r = ThreadLocalRandom.current private def randomFrom[T](c: Seq[T]): Option[T] = if (c.nonEmpty) Some(c(r.nextInt(c.size))) else None - def ts = System.currentTimeMillis() - override def next(): Iterator[Transaction] = { generate(settings).toIterator } @@ -38,13 +36,16 @@ class SmartGenerator(settings: SmartGenerator.Settings, val accounts: Seq[Privat SetScriptTransaction.selfSigned(i, Some(script), 1.zbs, System.currentTimeMillis()).explicitGet() }) + val now = System.currentTimeMillis() val txs = Range(0, settings.transfers).map { i => TransferTransactionV2 - .selfSigned(None, bank, bank, 1.zbs - 2 * fee, System.currentTimeMillis(), None, fee, Array.emptyByteArray) + .selfSigned(None, bank, bank, 1.zbs - 2 * fee, now + i, None, fee, Array.emptyByteArray) .explicitGet() } val extxs = Range(0, settings.exchange).map { i => + val ts = now + i + val matcher = randomFrom(accounts).get val seller = randomFrom(accounts).get val buyer = randomFrom(accounts).get diff --git a/generator/src/main/scala/com.zbsnetwork.generator/utils/Gen.scala b/generator/src/main/scala/com.zbsnetwork.generator/utils/Gen.scala index 415ff6b..917be92 100644 --- a/generator/src/main/scala/com.zbsnetwork.generator/utils/Gen.scala +++ b/generator/src/main/scala/com.zbsnetwork.generator/utils/Gen.scala @@ -112,25 +112,30 @@ object Gen { } def transfers(senderGen: Iterator[PrivateKeyAccount], recipientGen: Iterator[Address], feeGen: Iterator[Long]): Iterator[Transaction] = { + val now = System.currentTimeMillis() + senderGen .zip(recipientGen) .zip(feeGen) + .zipWithIndex .map { - case ((src, dst), fee) => - TransferTransactionV1.selfSigned(None, src, dst, fee, System.currentTimeMillis(), None, fee, Array.emptyByteArray) + case (((src, dst), fee), i) => + TransferTransactionV1.selfSigned(None, src, dst, fee, now + i, None, fee, Array.emptyByteArray) } .collect { case Right(x) => x } } def massTransfers(senderGen: Iterator[PrivateKeyAccount], recipientGen: Iterator[Address], amountGen: Iterator[Long]): Iterator[Transaction] = { + val now = System.currentTimeMillis() val transferCountGen = Iterator.continually(random.nextInt(MassTransferTransaction.MaxTransferCount + 1)) senderGen .zip(transferCountGen) + .zipWithIndex .map { - case (sender, count) => + case ((sender, count), i) => val transfers = List.tabulate(count)(_ => ParsedTransfer(recipientGen.next(), amountGen.next())) val fee = 100000 + count * 50000 - MassTransferTransaction.selfSigned(None, sender, transfers, System.currentTimeMillis, fee, Array.emptyByteArray) + MassTransferTransaction.selfSigned(None, sender, transfers, now + i, fee, Array.emptyByteArray) } .collect { case Right(tx) => tx } } diff --git a/it/build.sbt b/it/build.sbt index fb6c299..ecbb10c 100644 --- a/it/build.sbt +++ b/it/build.sbt @@ -22,7 +22,7 @@ inTask(docker)( val withAspectJ = Option(System.getenv("WITH_ASPECTJ")).fold(false)(_.toBoolean) val aspectjAgentUrl = "http://search.maven.org/remotecontent?filepath=org/aspectj/aspectjweaver/1.9.1/aspectjweaver-1.9.1.jar" - val yourKitArchive = "YourKit-JavaProfiler-2018.04-docker.zip" + val yourKitArchive = "YourKit-JavaProfiler-2019.1-docker.zip" new Dockerfile { from("anapsix/alpine-java:8_server-jre") @@ -31,9 +31,9 @@ inTask(docker)( // Install YourKit runRaw(s"""apk update && \\ |apk add --no-cache openssl ca-certificates && \\ - |wget https://www.yourkit.com/download/docker/$yourKitArchive -P /tmp/ && \\ + |wget --quiet https://www.yourkit.com/download/docker/$yourKitArchive -P /tmp/ && \\ |unzip /tmp/$yourKitArchive -d /usr/local && \\ - |rm /tmp/$yourKitArchive""".stripMargin) + |rm -f /tmp/$yourKitArchive""".stripMargin) if (withAspectJ) run("wget", "--quiet", aspectjAgentUrl, "-O", "/opt/zbs/aspectjweaver.jar") diff --git a/it/src/main/resources/template.conf b/it/src/main/resources/template.conf index 4c07f5a..cd0b18b 100644 --- a/it/src/main/resources/template.conf +++ b/it/src/main/resources/template.conf @@ -39,6 +39,7 @@ zbs { 9 = 0 10 = 0 11 = 0 + 12 = 0 } double-features-periods-after-height = 100000000 max-transaction-time-back-offset = 120m diff --git a/it/src/main/scala/com/zbsnetwork/it/Docker.scala b/it/src/main/scala/com/zbsnetwork/it/Docker.scala index 801bfeb..6ecd22a 100644 --- a/it/src/main/scala/com/zbsnetwork/it/Docker.scala +++ b/it/src/main/scala/com/zbsnetwork/it/Docker.scala @@ -236,8 +236,9 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo s"-Dlogback.stdout.level=TRACE -Dlogback.file.level=OFF -Dzbs.network.declared-address=$ip:$networkPort $ntpServer $maxCacheSize $kafkaServer" if (enableProfiling) { - config += s"-agentpath:/usr/local/YourKit-JavaProfiler-2018.04/bin/linux-x86-64/libyjpagent.so=port=$ProfilerPort,listen=all," + - s"sampling,monitors,sessionname=ZbsNode,dir=$ContainerRoot/profiler,logdir=$ContainerRoot " + // https://www.yourkit.com/docs/java/help/startup_options.jsp + config += s"-agentpath:/usr/local/YourKit-JavaProfiler-2019.1/bin/linux-x86-64/libyjpagent.so=port=$ProfilerPort,listen=all," + + s"sampling,monitors,sessionname=ZbsNode,dir=$ContainerRoot/profiler,logdir=$ContainerRoot,onexit=snapshot " } val withAspectJ = Option(System.getenv("WITH_ASPECTJ")).fold(false)(_.toBoolean) @@ -313,7 +314,6 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo def stopContainer(node: DockerNode): Unit = { val id = node.containerId log.info(s"Stopping container with id: $id") - takeProfileSnapshot(node) client.stopContainer(node.containerId, 10) saveProfile(node) saveLog(node) @@ -328,7 +328,6 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo def killAndStartContainer(node: DockerNode): DockerNode = { val id = node.containerId log.info(s"Killing container with id: $id") - takeProfileSnapshot(node) client.killContainer(id, DockerClient.Signal.SIGINT) saveProfile(node) saveLog(node) @@ -364,8 +363,8 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo log.info("Stopping containers") nodes.asScala.foreach { node => - takeProfileSnapshot(node) - client.stopContainer(node.containerId, 0) + client.stopContainer(node.containerId, if (enableProfiling) 60 else 0) + log.debug(s"Container ${node.name} stopped with exit status: ${client.waitContainer(node.containerId).statusCode()}") saveProfile(node) saveLog(node) @@ -416,28 +415,6 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo } } - private def takeProfileSnapshot(node: DockerNode): Unit = if (enableProfiling) { - val task = client.execCreate( - node.containerId, - Array( - "java", - "-jar", - ProfilerController.toString, - "127.0.0.1", - ProfilerPort.toString, - "capture-performance-snapshot" - ), - DockerClient.ExecCreateParam.attachStdout(), - DockerClient.ExecCreateParam.attachStderr() - ) - Option(task.warnings()).toSeq.flatMap(_.asScala).foreach(log.warn(_)) - client.execStart(task.id()) - while (client.execInspect(task.id()).running()) { - log.trace(s"Snapshot of ${node.name} has not been took yet, wait...") - blocking(Thread.sleep(1000)) - } - } - private def saveProfile(node: DockerNode): Unit = if (enableProfiling) { try { val profilerDirStream = client.archiveContainer(node.containerId, ContainerRoot.resolve("profiler").toString) @@ -542,7 +519,6 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo def runMigrationToolInsideContainer(node: DockerNode): DockerNode = { val id = node.containerId - takeProfileSnapshot(node) updateStartScript(node) stopContainer(node) saveProfile(node) @@ -589,11 +565,10 @@ class Docker(suiteConfig: Config = empty, tag: String = "", enableProfiling: Boo } object Docker { - private val ContainerRoot = Paths.get("/opt/zbs") - private val ProfilerController = ContainerRoot.resolve("yjp-controller-api-redist.jar") - private val ProfilerPort = 10001 - private val jsonMapper = new ObjectMapper - private val propsMapper = new JavaPropsMapper + private val ContainerRoot = Paths.get("/opt/zbs") + private val ProfilerPort = 10001 + private val jsonMapper = new ObjectMapper + private val propsMapper = new JavaPropsMapper val configTemplate = parseResources("template.conf") def genesisOverride = { diff --git a/it/src/main/scala/com/zbsnetwork/it/api/AsyncHttpApi.scala b/it/src/main/scala/com/zbsnetwork/it/api/AsyncHttpApi.scala index 87046a5..689f5f8 100644 --- a/it/src/main/scala/com/zbsnetwork/it/api/AsyncHttpApi.scala +++ b/it/src/main/scala/com/zbsnetwork/it/api/AsyncHttpApi.scala @@ -190,6 +190,10 @@ object AsyncHttpApi extends Assertions { def transactionsByAddress(address: String, limit: Int): Future[Seq[Seq[TransactionInfo]]] = get(s"/transactions/address/$address/limit/$limit").as[Seq[Seq[TransactionInfo]]] + def transactionsByAddress(address: String, limit: Int, after: String): Future[Seq[Seq[TransactionInfo]]] = { + get(s"/transactions/address/$address/limit/$limit?after=$after").as[Seq[Seq[TransactionInfo]]] + } + def assetDistributionAtHeight(asset: String, height: Int, limit: Int, maybeAfter: Option[String] = None): Future[AssetDistributionPage] = { val after = maybeAfter.fold("")(a => s"?after=$a") val url = s"/assets/$asset/distribution/$height/limit/$limit$after" diff --git a/it/src/main/scala/com/zbsnetwork/it/api/AsyncMatcherHttpApi.scala b/it/src/main/scala/com/zbsnetwork/it/api/AsyncMatcherHttpApi.scala index 2a5d57e..3a6dfec 100644 --- a/it/src/main/scala/com/zbsnetwork/it/api/AsyncMatcherHttpApi.scala +++ b/it/src/main/scala/com/zbsnetwork/it/api/AsyncMatcherHttpApi.scala @@ -274,10 +274,10 @@ object AsyncMatcherHttpApi extends Assertions { def getAllSnapshotOffsets: Future[Map[String, QueueEventWithMeta.Offset]] = matcherGetWithApiKey("/matcher/debug/allSnapshotOffsets").as[Map[String, QueueEventWithMeta.Offset]] - def waitForStableOffset(confirmations: Int, maxTries: Int, interval: FiniteDuration): Future[Either[Unit, QueueEventWithMeta.Offset]] = { - def loop(n: Int, currConfirmations: Int, currOffset: QueueEventWithMeta.Offset): Future[Either[Unit, QueueEventWithMeta.Offset]] = - if (currConfirmations >= confirmations) Future.successful(Right(currOffset)) - else if (n > maxTries) Future.successful(Left(())) + def waitForStableOffset(confirmations: Int, maxTries: Int, interval: FiniteDuration): Future[QueueEventWithMeta.Offset] = { + def loop(n: Int, currConfirmations: Int, currOffset: QueueEventWithMeta.Offset): Future[QueueEventWithMeta.Offset] = + if (currConfirmations >= confirmations) Future.successful(currOffset) + else if (n > maxTries) Future.failed(new IllegalStateException(s"Offset is not stable: $maxTries tries is out")) else GlobalTimer.instance .sleep(interval) diff --git a/it/src/main/scala/com/zbsnetwork/it/api/SyncHttpApi.scala b/it/src/main/scala/com/zbsnetwork/it/api/SyncHttpApi.scala index 0136cd3..74f8c6d 100644 --- a/it/src/main/scala/com/zbsnetwork/it/api/SyncHttpApi.scala +++ b/it/src/main/scala/com/zbsnetwork/it/api/SyncHttpApi.scala @@ -169,6 +169,9 @@ object SyncHttpApi extends Assertions { def transactionsByAddress(address: String, limit: Int): Seq[Seq[TransactionInfo]] = sync(async(n).transactionsByAddress(address, limit)) + def transactionsByAddress(address: String, limit: Int, after: String): Seq[Seq[TransactionInfo]] = + sync(async(n).transactionsByAddress(address, limit, after)) + def scriptCompile(code: String): CompiledScript = sync(async(n).scriptCompile(code)) @@ -269,6 +272,9 @@ object SyncHttpApi extends Assertions { def waitForHeight(expectedHeight: Int, requestAwaitTime: FiniteDuration = RequestAwaitTime): Int = sync(async(n).waitForHeight(expectedHeight), requestAwaitTime) + def blacklist(address: InetSocketAddress): Unit = + sync(async(n).blacklist(address)) + def debugMinerInfo(): Seq[State] = sync(async(n).debugMinerInfo()) diff --git a/it/src/main/scala/com/zbsnetwork/it/api/SyncMatcherHttpApi.scala b/it/src/main/scala/com/zbsnetwork/it/api/SyncMatcherHttpApi.scala index b2edfcc..0963d0d 100644 --- a/it/src/main/scala/com/zbsnetwork/it/api/SyncMatcherHttpApi.scala +++ b/it/src/main/scala/com/zbsnetwork/it/api/SyncMatcherHttpApi.scala @@ -106,6 +106,29 @@ object SyncMatcherHttpApi extends Assertions { waitTime: Duration = OrderRequestAwaitTime): MatcherStatusResponse = sync(async(m).waitOrderStatusAndAmount(assetPair, orderId, expectedStatus, expectedFilledAmount), waitTime) + def waitOrderProcessed(assetPair: AssetPair, orderId: String, checkTimes: Int = 5, retryInterval: FiniteDuration = 1.second): Unit = { + val fixedStatus = sync { + async(m).waitFor[MatcherStatusResponse](s"$orderId processed")( + _.orderStatus(orderId, assetPair), + _.status != "NotFound", + retryInterval + ) + } + + // Wait until something changed or not :) + def loop(n: Int): Unit = + if (n == 0) m.log.debug(s"$orderId wasn't changed (tried $checkTimes times)") + else { + val currStatus = orderStatus(orderId, assetPair) + if (currStatus == fixedStatus) { + Thread.sleep(retryInterval.toMillis) + loop(n - 1) + } else m.log.debug(s"$orderId was changed on ${checkTimes - n} step") + } + + loop(checkTimes) + } + def waitOrderInBlockchain(orderId: String, retryInterval: FiniteDuration = 1.second, waitTime: Duration = OrderRequestAwaitTime): Seq[TransactionInfo] = @@ -213,7 +236,7 @@ object SyncMatcherHttpApi extends Assertions { def waitForStableOffset(confirmations: Int, maxTries: Int, interval: FiniteDuration, - waitTime: Duration = RequestAwaitTime): Either[Unit, QueueEventWithMeta.Offset] = + waitTime: Duration = RequestAwaitTime): QueueEventWithMeta.Offset = sync(async(m).waitForStableOffset(confirmations, maxTries, interval), (maxTries + 1) * interval) def matcherState(assetPairs: Seq[AssetPair], diff --git a/it/src/main/scala/com/zbsnetwork/it/matcher/MatcherSuiteBase.scala b/it/src/main/scala/com/zbsnetwork/it/matcher/MatcherSuiteBase.scala index 76bc510..31b8fd3 100644 --- a/it/src/main/scala/com/zbsnetwork/it/matcher/MatcherSuiteBase.scala +++ b/it/src/main/scala/com/zbsnetwork/it/matcher/MatcherSuiteBase.scala @@ -21,11 +21,11 @@ abstract class MatcherSuiteBase val defaultAssetQuantity = 999999999999L val smartFee = 0.004.zbs - val minFee = 0.001.zbs + smartFee - val issueFee = 1.zbs + val minFee = 0.02.zbs + smartFee + val issueFee = 500.zbs val smartIssueFee = 1.zbs + smartFee - val leasingFee = 0.002.zbs + smartFee - val tradeFee = 0.003.zbs + val leasingFee = 5.zbs + smartFee + val tradeFee = 0.02.zbs val smartTradeFee = tradeFee + smartFee val twoSmartTradeFee = tradeFee + 2 * smartFee diff --git a/it/src/test/scala/com/wavesplatform/it/sync/transactions/TransactionAPISuite.scala b/it/src/test/scala/com/wavesplatform/it/sync/transactions/TransactionAPISuite.scala new file mode 100644 index 0000000..5a585a6 --- /dev/null +++ b/it/src/test/scala/com/wavesplatform/it/sync/transactions/TransactionAPISuite.scala @@ -0,0 +1,135 @@ +package com.zbsnetwork.it.sync.transactions + +import com.typesafe.config.Config +import com.zbsnetwork.account.Address +import com.zbsnetwork.common.utils._ +import com.zbsnetwork.it.api.SyncHttpApi._ +import com.zbsnetwork.it.api.TransactionInfo +import com.zbsnetwork.it.transactions.NodesFromDocker +import com.zbsnetwork.it.{Node, NodeConfigs, ReportingTestName} +import com.zbsnetwork.transaction.transfer.{TransferTransaction, TransferTransactionV1} +import org.scalatest.{CancelAfterFailure, FreeSpec, Matchers} +import play.api.libs.json.JsNumber +import scala.concurrent.duration._ + +class TransactionAPISuite extends FreeSpec with NodesFromDocker with Matchers with ReportingTestName with CancelAfterFailure { + + override def nodeConfigs: Seq[Config] = + NodeConfigs.newBuilder + .overrideBase(_.quorum(0)) + .overrideBase(_.raw("zbs.rest-api.transactions-by-address-limit=10")) + .withDefault(1) + .withSpecial(1, _.nonMiner) + .buildNonConflicting() + + val sender: Node = nodes.head + val recipient: Address = Address.fromString(sender.createAddress()).explicitGet() + + val Zbs: Long = 100000000L + + val AMT: Long = 1 * Zbs + val FEE: Long = (0.05 * Zbs).toLong + + val transactions: List[TransferTransaction] = + (for (i <- 0 to 30) yield { + TransferTransactionV1 + .selfSigned( + None, + sender.privateKey, + recipient, + AMT, + System.currentTimeMillis() + i, + None, + FEE + i * 100, + Array.emptyByteArray + ) + .explicitGet() + }).toList + + val transactionIds = transactions.map(_.id().base58) + + "should accept transactions" in { + transactions.foreach { tx => + sender.broadcastRequest(tx.json() + ("type" -> JsNumber(tx.builder.typeId.toInt))) + } + + val h = sender.height + + sender.waitForHeight(h + 3, 2.minutes) + } + + "should return correct N txs on request without `after`" in { + + def checkForLimit(limit: Int): Unit = { + val expected = + transactionIds + .take(limit) + + val received = + sender + .transactionsByAddress(recipient.address, limit) + .flatten + .map(_.id) + + expected shouldEqual received + } + + for (limit <- 2 to 10 by 1) { + checkForLimit(limit) + } + } + + "should return correct N txs on request with `after`" in { + + def checkForLimit(limit: Int): Unit = { + val expected = + transactionIds + .slice(limit, limit + limit) + + val afterParam = + transactions + .drop(limit - 1) + .head + .id() + .base58 + + val received = + sender + .transactionsByAddress(recipient.address, limit, afterParam) + .flatten + .map(_.id) + + expected shouldEqual received + } + + for (limit <- 2 to 10 by 1) { + checkForLimit(limit) + } + } + + "should return all transactions" in { + def checkForLimit(limit: Int): Unit = { + val received = + loadAll(sender, recipient.address, limit, None, Nil) + .map(_.id) + + received shouldEqual transactionIds + } + + for (limit <- 2 to 10 by 1) { + checkForLimit(limit) + } + } + + def loadAll(node: Node, address: String, limit: Int, maybeAfter: Option[String], acc: List[TransactionInfo]): List[TransactionInfo] = { + val txs = maybeAfter match { + case None => node.transactionsByAddress(address, limit).flatten.toList + case Some(lastId) => node.transactionsByAddress(address, limit, lastId).flatten.toList + } + + txs.lastOption match { + case None => acc ++ txs + case Some(tx) => loadAll(node, address, limit, Some(tx.id), acc ++ txs) + } + } +} diff --git a/it/src/test/scala/com/zbsnetwork/it/async/WideStateGenerationSuite.scala b/it/src/test/scala/com/zbsnetwork/it/async/WideStateGenerationSuite.scala index 0ecc11b..dd56c0e 100644 --- a/it/src/test/scala/com/zbsnetwork/it/async/WideStateGenerationSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/async/WideStateGenerationSuite.scala @@ -29,7 +29,6 @@ class WideStateGenerationSuite extends FreeSpec with WaitForHeight2 with Matcher | ignore-rx-messages = [1, 2, 25] | } | miner.minimal-block-generation-offset = 10s - | utx.cleanup-interval = 1m | synchronization.utx-synchronizer { | max-buffer-size = 500 | max-buffer-time = 100ms diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/BlacklistTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/BlacklistTestSuite.scala new file mode 100644 index 0000000..1f9301b --- /dev/null +++ b/it/src/test/scala/com/zbsnetwork/it/sync/BlacklistTestSuite.scala @@ -0,0 +1,45 @@ +package com.zbsnetwork.it.sync + +import com.typesafe.config.Config +import com.zbsnetwork.it.api.SyncHttpApi._ +import com.zbsnetwork.it.api._ +import com.zbsnetwork.it.transactions.NodesFromDocker +import com.zbsnetwork.it.{NodeConfigs, ReportingTestName} +import org.scalatest._ +import scala.concurrent.duration._ + +class BlacklistTestSuite extends FreeSpec with Matchers with CancelAfterFailure with ReportingTestName with NodesFromDocker { + + override protected def nodeConfigs: Seq[Config] = + NodeConfigs.newBuilder + .overrideBase(_.quorum(1)) + .withDefault(2) + .withSpecial(_.quorum(0)) + .buildNonConflicting() + + private def primaryNode = dockerNodes().last + + private def otherNodes = dockerNodes().init + + "primary node should blacklist other nodes" in { + otherNodes.foreach(n => primaryNode.blacklist(n.containerNetworkAddress)) + + val expectedBlacklistedPeers = nodes.size - 1 + + primaryNode.waitFor[Seq[BlacklistedPeer]](s"blacklistedPeers.size == $expectedBlacklistedPeers")( + _ => primaryNode.blacklistedPeers, + _.lengthCompare(expectedBlacklistedPeers) == 0, + 1.second + ) + } + + "sleep while nodes are blocked" in { + primaryNode.waitFor[Seq[BlacklistedPeer]](s"blacklistedPeers is empty")(_.blacklistedPeers, _.isEmpty, 5.second) + } + + "and sync again" in { + val baseHeight = nodes.map(_.height).max + nodes.waitForSameBlockHeadesAt(baseHeight + 5) + } + +} diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/activation/FeatureActivationTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/activation/FeatureActivationTestSuite.scala index 909b75a..f460ca1 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/activation/FeatureActivationTestSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/activation/FeatureActivationTestSuite.scala @@ -25,7 +25,7 @@ class FeatureActivationTestSuite NodeConfigs.newBuilder .overrideBase(_.raw(s"""zbs { | blockchain.custom.functionality { - | pre-activated-features = null + | pre-activated-features = {} | feature-check-blocks-period = $votingInterval | blocks-for-feature-activation = $blocksForActivation | } diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/activation/PreActivatedFeaturesTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/activation/PreActivatedFeaturesTestSuite.scala new file mode 100644 index 0000000..02f7ab1 --- /dev/null +++ b/it/src/test/scala/com/zbsnetwork/it/sync/activation/PreActivatedFeaturesTestSuite.scala @@ -0,0 +1,105 @@ +package com.zbsnetwork.it.sync.activation +import com.typesafe.config.{Config, ConfigFactory} +import com.zbsnetwork.features.api.NodeFeatureStatus +import com.zbsnetwork.features.{BlockchainFeatureStatus, BlockchainFeatures} +import com.zbsnetwork.it.{Docker, ReportingTestName} +import com.zbsnetwork.it.api.SyncHttpApi._ +import com.zbsnetwork.it.transactions.NodesFromDocker +import org.scalatest.{CancelAfterFailure, FreeSpec, Matchers} +class PreActivatedFeaturesTestSuite + extends FreeSpec + with Matchers + with CancelAfterFailure + with NodesFromDocker + with ActivationStatusRequest + with ReportingTestName { + override protected def nodeConfigs: Seq[Config] = PreActivatedFeaturesTestSuite.Configs + + nodes.foreach(n => n.accountBalances(n.address)) + + "before activation check" in { + nodes.waitForHeight(PreActivatedFeaturesTestSuite.votingInterval / 2) + + val mainNodeStatus = nodes.head.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum) + mainNodeStatus.description shouldBe PreActivatedFeaturesTestSuite.featureDescr + assertVotingStatus(mainNodeStatus, mainNodeStatus.supportingBlocks.get, BlockchainFeatureStatus.Undefined, NodeFeatureStatus.Voted) + + val otherNodes = nodes.tail.map(_.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum)) + otherNodes.foreach { s => + s.description shouldBe PreActivatedFeaturesTestSuite.featureDescr + assertActivatedStatus(s, 0, NodeFeatureStatus.Voted) + } + } + "on activation height check" in { + nodes.waitForHeight(PreActivatedFeaturesTestSuite.votingInterval + 3) + + val mainNodeStatus = nodes.head.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum) + mainNodeStatus.description shouldBe PreActivatedFeaturesTestSuite.featureDescr + assertApprovedStatus(mainNodeStatus, PreActivatedFeaturesTestSuite.votingInterval * 2, NodeFeatureStatus.Voted) + + val otherNodes = nodes.tail + otherNodes.foreach { node => + val feature = node.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum) + feature.description shouldBe PreActivatedFeaturesTestSuite.featureDescr + assertActivatedStatus(feature, 0, NodeFeatureStatus.Voted) + + val node1 = docker.restartNode(node.asInstanceOf[Docker.DockerNode]) + + val feature2 = node1.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum) + assertActivatedStatus(feature2, 0, NodeFeatureStatus.Voted) + } + } + "after activation height check" in { + nodes.waitForHeight(PreActivatedFeaturesTestSuite.votingInterval * 2 + 4) + + val mainNodeStatus = nodes.head.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum) + mainNodeStatus.description shouldBe PreActivatedFeaturesTestSuite.featureDescr + assertActivatedStatus(mainNodeStatus, PreActivatedFeaturesTestSuite.votingInterval * 2, NodeFeatureStatus.Voted) + + val otherNodes = nodes.tail.map(_.featureActivationStatus(PreActivatedFeaturesTestSuite.featureNum)) + otherNodes.foreach { s => + s.description shouldBe PreActivatedFeaturesTestSuite.featureDescr + assertActivatedStatus(s, 0, NodeFeatureStatus.Voted) + } + } +} +object PreActivatedFeaturesTestSuite { + import com.zbsnetwork.it.NodeConfigs._ + val votingInterval = 10 + val featureNum: Short = BlockchainFeatures.SmallerMinimalGeneratingBalance.id + val featureDescr = BlockchainFeatures.SmallerMinimalGeneratingBalance.description + private val supportedConfig = ConfigFactory.parseString(s"""zbs { + | blockchain.custom.functionality { + | pre-activated-features = {} + | feature-check-blocks-period = $votingInterval + | blocks-for-feature-activation = 1 + | } + | features.supported = [$featureNum] + | miner.quorum = 1 + |}""".stripMargin) + private val preactivatedConfig = ConfigFactory.parseString(s"""zbs { + | blockchain.custom.functionality { + | feature-check-blocks-period = $votingInterval + | pre-activated-features { + | 1: 0 + | 2: 100 + | 3: 100 + | 4: 100 + | 5: 100 + | 6: 100 + | 7: 100 + | 8: 100 + | 9: 100 + | 10: 100 + | 11: 100 + | } + | } + | features.supported = [$featureNum] + | miner.quorum = 1 + |}""".stripMargin) + val Configs: Seq[Config] = Seq( + supportedConfig.withFallback(Default.last), + preactivatedConfig.withFallback(Default.head), + preactivatedConfig.withFallback(Default(1)) + ) +} diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/debug/DebugPortfoliosSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/debug/DebugPortfoliosSuite.scala index 71a830f..6f19bbd 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/debug/DebugPortfoliosSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/debug/DebugPortfoliosSuite.scala @@ -1,10 +1,30 @@ package com.zbsnetwork.it.sync.debug +import com.typesafe.config.Config +import com.zbsnetwork.it.{Node, NodeConfigs} import com.zbsnetwork.it.api.SyncHttpApi._ -import com.zbsnetwork.it.transactions.BaseTransactionSuite +import com.zbsnetwork.it.transactions.NodesFromDocker import com.zbsnetwork.it.util._ +import com.zbsnetwork.it.sync._ +import org.scalatest.FunSuite -class DebugPortfoliosSuite extends BaseTransactionSuite { +class DebugPortfoliosSuite extends FunSuite with NodesFromDocker { + override protected def nodeConfigs: Seq[Config] = + NodeConfigs.newBuilder + .overrideBase(_.quorum(0)) + .withDefault(entitiesNumber = 1) + .buildNonConflicting() + + private def sender: Node = nodes.head + + private val firstAddress = sender.createAddress() + private val secondAddress = sender.createAddress() + + override protected def beforeAll(): Unit = { + super.beforeAll() + sender.transfer(sender.address, firstAddress, 20.zbs, minFee, waitForTx = true) + sender.transfer(sender.address, secondAddress, 20.zbs, minFee, waitForTx = true) + } test("getting a balance considering pessimistic transactions from UTX pool - changed after UTX") { val portfolioBefore = sender.debugPortfoliosFor(firstAddress, considerUnspent = true) diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRecoveryTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRecoveryTestSuite.scala index ed93243..10a3583 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRecoveryTestSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRecoveryTestSuite.scala @@ -31,12 +31,14 @@ class MatcherRecoveryTestSuite extends MatcherSuiteBase { Seq(issue1, issue2).map(matcherNode.signedIssue).map(x => nodes.waitForTransaction(x.id)) - private val orders = Gen.containerOfN[Vector, Order](placesNumber, orderGen(matcherNode.publicKey, aliceAcc, assetPairs)).sample.get + private val orders = Gen.containerOfN[Vector, Order](placesNumber, orderGen(matcherNode.publicKey, aliceAcc, assetPairs)).sample.get + private val lastOrder = orderGen(matcherNode.publicKey, aliceAcc, assetPairs).sample.get "Place, fill and cancel a lot of orders" in { val cancels = (1 to cancelsNumber).map(_ => choose(orders)) val commands = Random.shuffle(orders.map(MatcherCommand.Place(matcherNode, _))) ++ cancels.map(MatcherCommand.Cancel(matcherNode, aliceAcc, _)) executeCommands(commands) + executeCommands(List(MatcherCommand.Place(matcherNode, lastOrder))) } "Wait until all requests are processed - 1" in matcherNode.waitForStableOffset(10, 100, 200.millis) @@ -56,7 +58,10 @@ class MatcherRecoveryTestSuite extends MatcherSuiteBase { "Restart the matcher" in docker.restartContainer(matcherNode.asInstanceOf[DockerNode]) "Wait until all requests are processed - 2" in { - matcherNode.waitFor[QueueEventWithMeta.Offset]("all requests are processed")(_.getCurrentOffset, _ == stateBefore.offset, 300.millis) + matcherNode.waitFor[QueueEventWithMeta.Offset]("all events are consumed")(_.getCurrentOffset, _ == stateBefore.offset, 300.millis) + withClue("Last command processed") { + matcherNode.waitOrderProcessed(lastOrder.assetPair, lastOrder.idStr()) + } } "Verify the state" in { diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRestartTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRestartTestSuite.scala index 6580e80..d259121 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRestartTestSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MatcherRestartTestSuite.scala @@ -2,6 +2,7 @@ package com.zbsnetwork.it.sync.matcher import com.typesafe.config.Config import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.it.api.OrderBookResponse import com.zbsnetwork.it.api.SyncHttpApi._ import com.zbsnetwork.it.api.SyncMatcherHttpApi._ import com.zbsnetwork.it.matcher.MatcherSuiteBase @@ -65,8 +66,8 @@ class MatcherRestartTestSuite extends MatcherSuiteBase { matcherNode.placeOrder(aliceAcc, aliceZbsPair, OrderType.SELL, 500, 2.zbs * Order.PriceConstant, matcherFee, orderVersion, 5.minutes) aliceSecondOrder.status shouldBe "OrderAccepted" - val orders2 = matcherNode.orderBook(aliceZbsPair) - orders2.asks.head.amount shouldBe 1000 + val orders2 = + matcherNode.waitFor[OrderBookResponse]("Top ask has 1000 amount")(_.orderBook(aliceZbsPair), _.asks.head.amount == 1000, 1.second) orders2.asks.head.price shouldBe 2.zbs * Order.PriceConstant val cancel = matcherNode.cancelOrder(aliceAcc, aliceZbsPair, firstOrder) diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MultipleMatchersTestSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MultipleMatchersTestSuite.scala index 632e1a5..bb3290d 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MultipleMatchersTestSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/MultipleMatchersTestSuite.scala @@ -21,11 +21,12 @@ class MultipleMatchersTestSuite extends MatcherSuiteBase { | snapshots-interval = 51 |}""".stripMargin) + private def matcher1NodeConfig = Default.last private def matcher2NodeConfig = ConfigFactory.parseString("""zbs.network.node-name = node11 - |akka.kafka.consumer.kafka-clients.group.id = 1""".stripMargin).withFallback(Default.last) + |akka.kafka.consumer.kafka-clients.group.id = 1""".stripMargin).withFallback(matcher1NodeConfig) override protected def nodeConfigs: Seq[Config] = - List(Default.last, matcher2NodeConfig, Default(2 + Random.nextInt(Default.size - 2))) + (List(matcher1NodeConfig, matcher2NodeConfig) ++ Random.shuffle(Default.init).take(1)) .zip(Seq(matcherConfig, matcherConfig, minerEnabled)) .map { case (n, o) => o.withFallback(n) } .map(configOverrides.withFallback) @@ -59,6 +60,7 @@ class MultipleMatchersTestSuite extends MatcherSuiteBase { private val aliceOrders = mkOrders(aliceAcc) private val bobOrders = mkOrders(aliceAcc) private val orders = aliceOrders ++ bobOrders + private val lastOrder = orderGen(matcherPublicKey, aliceAcc, assetPairs).sample.get "Place, fill and cancel a lot of orders" in { val alicePlaces = aliceOrders.map(MatcherCommand.Place(matcher1Node, _)) @@ -70,11 +72,17 @@ class MultipleMatchersTestSuite extends MatcherSuiteBase { val cancels = Random.shuffle(aliceCancels ++ bobCancels) executeCommands(places ++ cancels) + executeCommands(List(MatcherCommand.Place(matcher1Node, lastOrder))) } "Wait until all requests are processed" in { - matcher1Node.waitForStableOffset(10, 100, 200.millis) - matcher2Node.waitForStableOffset(10, 100, 200.millis) + val offset1 = matcher1Node.waitForStableOffset(10, 100, 200.millis) + matcher2Node.waitFor[Long](s"Offset is $offset1")(_.getCurrentOffset, _ == offset1, 2.seconds) + + withClue("Last command processed") { + matcher1Node.waitOrderProcessed(lastOrder.assetPair, lastOrder.idStr()) + matcher2Node.waitOrderProcessed(lastOrder.assetPair, lastOrder.idStr()) + } } "States on both matcher should be equal" in { diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/config/MatcherDefaultConfig.scala b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/config/MatcherDefaultConfig.scala index 1827a6b..ac06b61 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/matcher/config/MatcherDefaultConfig.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/matcher/config/MatcherDefaultConfig.scala @@ -35,7 +35,8 @@ object MatcherDefaultConfig { | rest-order-limit=$orderLimit |}""".stripMargin) - val Configs: Seq[Config] = (Default.last +: Random.shuffle(Default.init).take(2)) + val Configs: Seq[Config] = List(9, 5, 7) + .map(Default) .zip(Seq(matcherConfig, minerDisabled, minerEnabled)) .map { case (n, o) => o.withFallback(n) } diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/network/SimpleTransactionsSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/network/SimpleTransactionsSuite.scala index a1fd209..7f160f6 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/network/SimpleTransactionsSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/network/SimpleTransactionsSuite.scala @@ -4,32 +4,31 @@ import java.nio.charset.StandardCharsets import com.typesafe.config.Config import com.zbsnetwork.account.Address -import com.zbsnetwork.it._ import com.zbsnetwork.it.api.SyncHttpApi._ import com.zbsnetwork.it.api.AsyncNetworkApi._ -import com.zbsnetwork.it.api._ import com.zbsnetwork.it.transactions.BaseTransactionSuite import com.zbsnetwork.network.{RawBytes, TransactionSpec} import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.it.NodeConfigs import com.zbsnetwork.transaction.transfer._ import org.scalatest._ -import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures} +import com.zbsnetwork.it.sync._ + import scala.concurrent.duration._ import scala.language.postfixOps -class SimpleTransactionsSuite extends BaseTransactionSuite with Matchers with ScalaFutures with IntegrationPatience with RecoverMethods { - +class SimpleTransactionsSuite extends BaseTransactionSuite with Matchers { override protected def nodeConfigs: Seq[Config] = NodeConfigs.newBuilder - .overrideBase(_.quorum(2)) - .withDefault(3) - .build() + .overrideBase(_.quorum(0)) + .withDefault(entitiesNumber = 1) + .buildNonConflicting() private def node = nodes.head test("valid tx send by network to node should be in blockchain") { val tx = TransferTransactionV1 - .selfSigned(None, node.privateKey, Address.fromString(node.address).explicitGet(), 1L, System.currentTimeMillis(), None, 100000L, Array()) + .selfSigned(None, node.privateKey, Address.fromString(node.address).explicitGet(), 1L, System.currentTimeMillis(), None, minFee, Array()) .right .get @@ -46,7 +45,7 @@ class SimpleTransactionsSuite extends BaseTransactionSuite with Matchers with Sc 1L, System.currentTimeMillis() + (1 days).toMillis, None, - 100000L, + minFee, Array()) .right .get diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/package.scala b/it/src/test/scala/com/zbsnetwork/it/sync/package.scala index 0ec8278..7cc671b 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/package.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/package.scala @@ -9,7 +9,7 @@ import com.zbsnetwork.transaction.smart.script.{Script, ScriptCompiler} package object sync { val smartFee: Long = 0.004.zbs - val minFee: Long = 0.001.zbs + val minFee: Long = 0.005.zbs val leasingFee: Long = 0.002.zbs val issueFee: Long = 1.zbs val burnFee: Long = 1.zbs diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ContractInvocationTransactionSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ContractInvocationTransactionSuite.scala index a390070..d7cd47f 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ContractInvocationTransactionSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ContractInvocationTransactionSuite.scala @@ -66,10 +66,12 @@ class ContractInvocationTransactionSuite extends BaseTransactionSuite with Cance test("set contract to contract account") { val scriptText = """ + |{-# STDLIB_VERSION 3 #-} + |{-# CONTENT_TYPE CONTRACT #-} | | @Callable(inv) - | func foo(a:ByteStr) = { - | WriteSet(List(DataEntry("a", a), DataEntry("sender", inv.caller.bytes))) + | func foo(a:ByteVector) = { + | WriteSet([DataEntry("a", a), DataEntry("sender", inv.caller.bytes)]) | } | | @Verifier(t) @@ -80,7 +82,7 @@ class ContractInvocationTransactionSuite extends BaseTransactionSuite with Cance | """.stripMargin - val script = ScriptCompiler.contract(scriptText).explicitGet() + val script = ScriptCompiler.compile(scriptText).explicitGet()._1 val setScriptTransaction = SetScriptTransaction .selfSigned(contract, Some(script), setScriptFee, System.currentTimeMillis()) .explicitGet() diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ExchangeWithContractsSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ExchangeWithContractsSuite.scala index 462831a..d89186a 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ExchangeWithContractsSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/ExchangeWithContractsSuite.scala @@ -10,7 +10,6 @@ import com.zbsnetwork.state._ import com.zbsnetwork.transaction.DataTransaction import com.zbsnetwork.transaction.assets.exchange._ import org.scalatest.CancelAfterFailure -import play.api.libs.json._ import scorex.crypto.encode.Base64 class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFailure with NTPTime { @@ -80,10 +79,15 @@ class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFa (contr2, acc1), (mcontr, acc2), ) + for ((o1ver, o2ver) <- Seq( + (2: Byte, 2: Byte), + (2: Byte, 3: Byte), + )) { - sender.signedBroadcast(exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, acc1, acc0, acc2), waitForTx = true) + sender.signedBroadcast(exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, o1ver, o2ver, acc1, acc0, acc2), waitForTx = true) - //TODO : add assert balances + //TODO : add assert balances + } } setContracts( @@ -105,10 +109,14 @@ class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFa (contr2, acc1), (mcontr, acc2), ) - - assertBadRequestAndMessage(sender.signedBroadcast(exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, acc1, acc0, acc2)), - "Transaction is not allowed by account-script") - //TODO : add assert balances + for ((o1ver, o2ver) <- Seq( + (2: Byte, 2: Byte), + (3: Byte, 3: Byte), + )) { + assertBadRequestAndMessage(sender.signedBroadcast(exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, o1ver, o2ver, acc1, acc0, acc2)), + "Transaction is not allowed by account-script") + //TODO : add assert balances + } } setContracts( (None, acc0), @@ -126,10 +134,14 @@ class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFa (contr2, acc1), (mcontr, acc2), ) - - val tx = exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, acc1, acc0, acc2) - assertBadRequestAndMessage(sender.signedBroadcast(tx), "Error while executing account-script: Some generic error") - //TODO : add assert balances + for ((o1ver, o2ver) <- Seq( + (2: Byte, 2: Byte), + (3: Byte, 3: Byte), + )) { + val tx = exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, o1ver, o2ver, acc1, acc0, acc2) + assertBadRequestAndMessage(sender.signedBroadcast(tx), "Error while executing account-script: Some generic error") + //TODO : add assert balances + } } setContracts( (None, acc0), @@ -152,8 +164,53 @@ class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFa val matcher = acc2 val sellPrice = (0.50 * Order.PriceConstant).toLong - val buy = orders(pair, 1, orderFee, ntpTime, acc1, acc0, acc2)._1 - val sell = orders(pair, 2, orderFee, ntpTime, acc1, acc0, acc2)._2 + for ((o1ver, o2ver) <- Seq( + (1: Byte, 2: Byte), + (1: Byte, 3: Byte), + )) { + + val (buy, sell) = orders(pair, o1ver, o2ver, orderFee, ntpTime, acc1, acc0, acc2) + + val amount = math.min(buy.amount, sell.amount) + val tx = ExchangeTransactionV2 + .create( + matcher = matcher, + buyOrder = buy, + sellOrder = sell, + amount = amount, + price = sellPrice, + buyMatcherFee = (BigInt(orderFee) * amount / buy.amount).toLong, + sellMatcherFee = (BigInt(orderFee) * amount / sell.amount).toLong, + fee = smartMatcherFee, + timestamp = ntpTime.correctedTime() + ) + .explicitGet() + .json() + + val txId = sender.signedBroadcast(tx).id + nodes.waitForHeightAriseAndTxPresent(txId) + + //TODO : add assert balances + } + } + setContracts( + (None, acc0), + (None, acc1), + (None, acc2), + ) + } + + test("negative - exchange tx v2 and order v1 from scripted acc") { + setContracts((sc1, acc0)) + + val matcher = acc2 + val sellPrice = (0.50 * Order.PriceConstant).toLong + + for ((o1ver, o2ver) <- Seq( + (2: Byte, 1: Byte), + (3: Byte, 1: Byte), + )) { + val (buy, sell) = orders(pair, o1ver, o2ver, orderFee, ntpTime, acc1, acc0, acc2) val amount = math.min(buy.amount, sell.amount) val tx = ExchangeTransactionV2 @@ -171,50 +228,7 @@ class ExchangeWithContractsSuite extends BaseTransactionSuite with CancelAfterFa .explicitGet() .json() - val txId = sender.signedBroadcast(tx).id - nodes.waitForHeightAriseAndTxPresent(txId) - - //TODO : add assert balances + assertBadRequestAndMessage(sender.signedBroadcast(tx), "Reason: Can't process order with signature from scripted account") } - setContracts( - (None, acc0), - (None, acc1), - (None, acc2), - ) } - - test("negative - check orders v2 with exchange tx v1") { - val tx = exchangeTx(pair, smartMatcherFee, orderFee, ntpTime, acc1, acc0, acc2) - val sig = (Json.parse(tx.toString()) \ "proofs").as[Seq[JsString]].head - val changedTx = tx + ("version" -> JsNumber(1)) + ("signature" -> sig) - assertBadRequestAndMessage(sender.signedBroadcast(changedTx), "can only contain orders of version 1", 400) - } - - test("negative - exchange tx v2 and order v1 from scripted acc") { - setContracts((sc1, acc0)) - - val matcher = acc2 - val sellPrice = (0.50 * Order.PriceConstant).toLong - val buy = orders(pair, 2, orderFee, ntpTime, acc1, acc0, acc2)._1 - val sell = orders(pair, 1, orderFee, ntpTime, acc1, acc0, acc2)._2 - - val amount = math.min(buy.amount, sell.amount) - val tx = ExchangeTransactionV2 - .create( - matcher = matcher, - buyOrder = buy, - sellOrder = sell, - amount = amount, - price = sellPrice, - buyMatcherFee = (BigInt(orderFee) * amount / buy.amount).toLong, - sellMatcherFee = (BigInt(orderFee) * amount / sell.amount).toLong, - fee = smartMatcherFee, - timestamp = ntpTime.correctedTime() - ) - .explicitGet() - .json() - - assertBadRequestAndMessage(sender.signedBroadcast(tx), "Reason: Can't process order with signature from scripted account") - } - } diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/HodlContractTransactionSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/HodlContractTransactionSuite.scala index cbb5114..e0b01a0 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/HodlContractTransactionSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/HodlContractTransactionSuite.scala @@ -64,6 +64,8 @@ class HodlContractTransactionSuite extends BaseTransactionSuite with CancelAfter test("set contract to contract account") { val scriptText = """ + |{-# STDLIB_VERSION 3 #-} + |{-# CONTENT_TYPE CONTRACT #-} | | @Callable(i) | func deposit() = { @@ -76,7 +78,7 @@ class HodlContractTransactionSuite extends BaseTransactionSuite with CancelAfter | case _ => 0 | } | let newAmount = currentAmount + pmt.amount - | WriteSet(List(DataEntry(currentKey, newAmount))) + | WriteSet([DataEntry(currentKey, newAmount)]) | | } | } @@ -94,8 +96,8 @@ class HodlContractTransactionSuite extends BaseTransactionSuite with CancelAfter | else if (newAmount < 0) | then throw("Not enough balance") | else ContractResult( - | WriteSet(List(DataEntry(currentKey, newAmount))), - | TransferSet(List(ContractTransfer(i.caller, amount, unit))) + | WriteSet([DataEntry(currentKey, newAmount)]), + | TransferSet([ContractTransfer(i.caller, amount, unit)]) | ) | } | @@ -103,7 +105,7 @@ class HodlContractTransactionSuite extends BaseTransactionSuite with CancelAfter | """.stripMargin - val script = ScriptCompiler.contract(scriptText).explicitGet() + val script = ScriptCompiler.compile(scriptText).explicitGet()._1 val setScriptTransaction = SetScriptTransaction .selfSigned(contract, Some(script), setScriptFee, System.currentTimeMillis()) .explicitGet() diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/RIDEFuncSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/RIDEFuncSuite.scala index 8b993d0..7f7e264 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/RIDEFuncSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/RIDEFuncSuite.scala @@ -1,7 +1,9 @@ package com.zbsnetwork.it.sync.smartcontract +import com.typesafe.config.Config import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.it.NodeConfigs import com.zbsnetwork.it.api.SyncHttpApi._ import com.zbsnetwork.it.sync._ import com.zbsnetwork.it.transactions.BaseTransactionSuite @@ -12,6 +14,12 @@ import com.zbsnetwork.transaction.transfer.TransferTransactionV2 import org.scalatest.CancelAfterFailure class RIDEFuncSuite extends BaseTransactionSuite with CancelAfterFailure { + override protected def nodeConfigs: Seq[Config] = + NodeConfigs.newBuilder + .overrideBase(_.quorum(0)) + .withDefault(entitiesNumber = 1) + .buildNonConflicting() + private val acc0 = pkByAddress(firstAddress) test("assetBalance() verification") { diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/package.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/package.scala index 9d29646..43075f5 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/package.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/package.scala @@ -106,12 +106,12 @@ package object smartcontract { | } """.stripMargin - def exchangeTx(pair: AssetPair, exTxFee: Long, orderFee: Long, time: Time, accounts: PrivateKeyAccount*): JsObject = { + def exchangeTx(pair: AssetPair, exTxFee: Long, orderFee: Long, time: Time, ord1Ver: Byte, ord2Ver: Byte, accounts: PrivateKeyAccount*): JsObject = { val buyer = accounts.head // first one val seller = accounts.tail.head // second one val matcher = accounts.last val sellPrice = (0.50 * Order.PriceConstant).toLong - val (buy, sell) = orders(pair, 2, orderFee, time, buyer, seller, matcher) + val (buy, sell) = orders(pair, ord1Ver, ord2Ver, orderFee, time, buyer, seller, matcher) val amount = math.min(buy.amount, sell.amount) @@ -138,7 +138,7 @@ package object smartcontract { tx } - def orders(pair: AssetPair, version: Byte, fee: Long, time: Time, accounts: PrivateKeyAccount*): (Order, Order) = { + def orders(pair: AssetPair, ord1Ver: Byte, ord2Ver: Byte, fee: Long, time: Time, accounts: PrivateKeyAccount*): (Order, Order) = { val buyer = accounts.head // first one val seller = accounts.tail.head // second one val matcher = accounts.last @@ -149,8 +149,8 @@ package object smartcontract { val buyAmount = 2 val sellAmount = 3 - val buy = Order.buy(buyer, matcher, pair, buyAmount, buyPrice, ts, expirationTimestamp, fee, version) - val sell = Order.sell(seller, matcher, pair, sellAmount, sellPrice, ts, expirationTimestamp, fee, version) + val buy = Order.buy(buyer, matcher, pair, buyAmount, buyPrice, ts, expirationTimestamp, fee, ord1Ver) + val sell = Order.sell(seller, matcher, pair, sellAmount, sellPrice, ts, expirationTimestamp, fee, ord2Ver) (buy, sell) } diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/smartasset/ExchangeSmartAssetsSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/smartasset/ExchangeSmartAssetsSuite.scala index 4a91954..a5b207e 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/smartasset/ExchangeSmartAssetsSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/smartcontract/smartasset/ExchangeSmartAssetsSuite.scala @@ -41,10 +41,10 @@ class ExchangeSmartAssetsSuite extends BaseTransactionSuite with CancelAfterFail val s = Some( ScriptCompiler( s""" - |match tx { - |case s : SetAssetScriptTransaction => true - |case e: ExchangeTransaction => e.sender == addressFromPublicKey(base58'${ByteStr(acc2.publicKey).base58}') - |case _ => false}""".stripMargin, + |match tx { + |case s : SetAssetScriptTransaction => true + |case e: ExchangeTransaction => e.sender == addressFromPublicKey(base58'${ByteStr(acc2.publicKey).base58}') + |case _ => false}""".stripMargin, isAssetScript = true ).explicitGet()._1.bytes.value.base64) @@ -61,24 +61,24 @@ class ExchangeSmartAssetsSuite extends BaseTransactionSuite with CancelAfterFail setContracts((contr1, acc0), (contr2, acc1), (mcontr, acc2)) - sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, acc1, acc0, acc2), + sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, 2, 3, acc1, acc0, acc2), waitForTx = true) } val sUpdated = Some( ScriptCompiler( s""" - |match tx { - |case s : SetAssetScriptTransaction => true - |case e: ExchangeTransaction => e.sender == addressFromPublicKey(base58'${ByteStr(acc1.publicKey).base58}') - |case _ => false}""".stripMargin, + |match tx { + |case s : SetAssetScriptTransaction => true + |case e: ExchangeTransaction => e.sender == addressFromPublicKey(base58'${ByteStr(acc1.publicKey).base58}') + |case _ => false}""".stripMargin, isAssetScript = true ).explicitGet()._1.bytes.value.base64) sender.setAssetScript(sAsset, firstAddress, setAssetScriptFee, sUpdated, waitForTx = true) assertBadRequestAndMessage( - sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, acc1, acc0, acc2)), + sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, 3, 2, acc1, acc0, acc2)), errNotAllowedByToken) setContracts((None, acc0), (None, acc1), (None, acc2)) @@ -116,19 +116,20 @@ class ExchangeSmartAssetsSuite extends BaseTransactionSuite with CancelAfterFail priceAsset = Some(ByteStr.decodeBase58(assetB).get) ) - sender.signedBroadcast(exchangeTx(smartAssetPair, matcherFee + 2 * smartFee, matcherFee + 2 * smartFee, ntpTime, acc1, acc0, acc2), + sender.signedBroadcast(exchangeTx(smartAssetPair, matcherFee + 2 * smartFee, matcherFee + 2 * smartFee, ntpTime, 3, 2, acc1, acc0, acc2), waitForTx = true) withClue("check fee for smart accounts and smart AssetPair - extx.fee == 0.015.zbs") { setContracts((sc1, acc0), (sc1, acc1), (sc1, acc2)) assertBadRequestAndMessage( - sender.signedBroadcast(exchangeTx(smartAssetPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, acc1, acc0, acc2)), + sender.signedBroadcast(exchangeTx(smartAssetPair, smartMatcherFee + smartFee, smartMatcherFee + smartFee, ntpTime, 2, 2, acc1, acc0, acc2)), "com.zbsnetwork.transaction.assets.exchange.ExchangeTransactionV2 does not exceed minimal value of 1500000" ) - sender.signedBroadcast(exchangeTx(smartAssetPair, smartMatcherFee + 2 * smartFee, smartMatcherFee + 2 * smartFee, ntpTime, acc1, acc0, acc2), - waitForTx = true) + sender.signedBroadcast( + exchangeTx(smartAssetPair, smartMatcherFee + 2 * smartFee, smartMatcherFee + 2 * smartFee, ntpTime, 2, 3, acc1, acc0, acc2), + waitForTx = true) setContracts((None, acc0), (None, acc1), (None, acc2)) } @@ -138,7 +139,7 @@ class ExchangeSmartAssetsSuite extends BaseTransactionSuite with CancelAfterFail priceAsset = None ) assertBadRequestAndMessage( - sender.signedBroadcast(exchangeTx(incorrectSmartAssetPair, smartMatcherFee, smartMatcherFee, ntpTime, acc1, acc0, acc2)), + sender.signedBroadcast(exchangeTx(incorrectSmartAssetPair, smartMatcherFee, smartMatcherFee, ntpTime, 3, 2, acc1, acc0, acc2)), errNotAllowedByToken) } @@ -157,7 +158,7 @@ class ExchangeSmartAssetsSuite extends BaseTransactionSuite with CancelAfterFail val smartPair = AssetPair(ByteStr.decodeBase58(asset).toOption, None) - sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee, smartMatcherFee, ntpTime, acc1, acc0, acc2), waitForTx = true) + sender.signedBroadcast(exchangeTx(smartPair, smartMatcherFee, smartMatcherFee, ntpTime, 2, 3, acc1, acc0, acc2), waitForTx = true) } } } diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/DataTransactionSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/DataTransactionSuite.scala index 1a53bd7..caeef3f 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/DataTransactionSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/DataTransactionSuite.scala @@ -238,15 +238,15 @@ class DataTransactionSuite extends BaseTransactionSuite { "Duplicate keys found") val extraValueData = List(BinaryDataEntry("key", ByteStr(Array.fill(MaxValueSize + 1)(1.toByte)))) - assertBadRequestAndResponse(sender.putData(firstAddress, extraValueData, calcDataFee(extraValueData)), TooBig) + assertBadRequestAndResponse(sender.putData(firstAddress, extraValueData, 1.zbs), TooBig) nodes.waitForHeightArise() val largeBinData = List.tabulate(5)(n => BinaryDataEntry(extraKey, ByteStr(Array.fill(MaxValueSize)(n.toByte)))) - assertBadRequestAndResponse(sender.putData(firstAddress, largeBinData, calcDataFee(largeBinData)), TooBig) + assertBadRequestAndResponse(sender.putData(firstAddress, largeBinData, 1.zbs), TooBig) nodes.waitForHeightArise() val largeStrData = List.tabulate(5)(n => StringDataEntry(extraKey, "A" * MaxValueSize)) - assertBadRequestAndResponse(sender.putData(firstAddress, largeStrData, calcDataFee(largeStrData)), TooBig) + assertBadRequestAndResponse(sender.putData(firstAddress, largeStrData, 1.zbs), TooBig) nodes.waitForHeightArise() val tooManyEntriesData = List.tabulate(MaxEntryCount + 1)(n => IntegerDataEntry("key", 88)) diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/ExchangeTransactionSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/ExchangeTransactionSuite.scala index 7e95254..144f507 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/ExchangeTransactionSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/ExchangeTransactionSuite.scala @@ -3,51 +3,59 @@ package com.zbsnetwork.it.sync.transactions import com.zbsnetwork.it.NTPTime import com.zbsnetwork.it.api.SyncHttpApi._ import com.zbsnetwork.it.sync._ +import com.zbsnetwork.it.sync.smartcontract.exchangeTx import com.zbsnetwork.it.transactions.BaseTransactionSuite import com.zbsnetwork.it.util._ import com.zbsnetwork.transaction.assets.IssueTransactionV1 import com.zbsnetwork.transaction.assets.exchange._ +import play.api.libs.json.{JsNumber, JsString, Json} class ExchangeTransactionSuite extends BaseTransactionSuite with NTPTime { + var exchAsset: IssueTransactionV1 = IssueTransactionV1 + .selfSigned( + sender = sender.privateKey, + name = "myasset".getBytes(), + description = "my asset description".getBytes(), + quantity = someAssetAmount, + decimals = 2, + reissuable = true, + fee = 1.zbs, + timestamp = System.currentTimeMillis() + ) + .right + .get + + var pair: AssetPair = _ + + private val acc0 = pkByAddress(firstAddress) + private val acc1 = pkByAddress(secondAddress) + private val acc2 = pkByAddress(thirdAddress) + + val transactionV1versions = (1: Byte, 1: Byte, 1: Byte) // in ExchangeTransactionV1 only orders V1 are supported + val transactionV2versions = for { + o1ver <- 1 to 3 + o2ver <- 1 to 3 + } yield (o1ver.toByte, o2ver.toByte, 2.toByte) + + val versions = transactionV1versions +: transactionV2versions + test("cannot exchange non-issued assets") { - for ((o1ver, o2ver, tver) <- Seq( - (1: Byte, 1: Byte, 1: Byte), - (1: Byte, 1: Byte, 2: Byte), - (1: Byte, 2: Byte, 2: Byte), - (2: Byte, 1: Byte, 2: Byte), - (2: Byte, 2: Byte, 2: Byte) - )) { - val assetName = "myasset" - val assetDescription = "my asset description" - - val IssueTx: IssueTransactionV1 = IssueTransactionV1 - .selfSigned( - sender = sender.privateKey, - name = assetName.getBytes(), - description = assetDescription.getBytes(), - quantity = someAssetAmount, - decimals = 2, - reissuable = true, - fee = 1.zbs, - timestamp = System.currentTimeMillis() - ) - .right - .get + for ((o1ver, o2ver, tver) <- versions) { - val assetId = IssueTx.id().base58 + val assetId = exchAsset.id().base58 - val buyer = pkByAddress(firstAddress) - val seller = pkByAddress(secondAddress) - val matcher = pkByAddress(thirdAddress) + val buyer = acc0 + val seller = acc1 + val matcher = acc2 val ts = ntpTime.correctedTime() val expirationTimestamp = ts + Order.MaxLiveTime val buyPrice = 2 * Order.PriceConstant val sellPrice = 2 * Order.PriceConstant val buyAmount = 1 val sellAmount = 1 - val assetPair = AssetPair.createAssetPair("ZBS", assetId).get - val buy = Order.buy(buyer, matcher, assetPair, buyAmount, buyPrice, ts, expirationTimestamp, matcherFee, o1ver) - val sell = Order.sell(seller, matcher, assetPair, sellAmount, sellPrice, ts, expirationTimestamp, matcherFee, o2ver) + pair = AssetPair.createAssetPair("ZBS", assetId).get + val buy = Order.buy(buyer, matcher, pair, buyAmount, buyPrice, ts, expirationTimestamp, matcherFee, o1ver) + val sell = Order.sell(seller, matcher, pair, sellAmount, sellPrice, ts, expirationTimestamp, matcherFee, o2ver) val amount = 1 if (tver != 1) { @@ -95,4 +103,98 @@ class ExchangeTransactionSuite extends BaseTransactionSuite with NTPTime { } + test("negative - check orders v2 and v3 with exchange tx v1") { + if (sender.findTransactionInfo(exchAsset.id().base58).isEmpty) sender.postJson("/transactions/broadcast", exchAsset.json()) + pair = AssetPair.createAssetPair("ZBS", exchAsset.id().base58).get + + for ((o1ver, o2ver) <- Seq( + (2: Byte, 1: Byte), + (2: Byte, 3: Byte), + )) { + val tx = exchangeTx(pair, matcherFee, orderFee, ntpTime, o1ver, o2ver, acc1, acc0, acc2) + val sig = (Json.parse(tx.toString()) \ "proofs").as[Seq[JsString]].head + val changedTx = tx + ("version" -> JsNumber(1)) + ("signature" -> sig) + assertBadRequestAndMessage(sender.signedBroadcast(changedTx), "can only contain orders of version 1", 400) + } + } + + test("exchange tx with orders v3") { + val buyer = acc0 + val seller = acc1 + + val assetDescription = "my asset description" + + val IssueTx: IssueTransactionV1 = IssueTransactionV1 + .selfSigned( + sender = buyer, + name = "myasset".getBytes(), + description = assetDescription.getBytes(), + quantity = someAssetAmount, + decimals = 8, + reissuable = true, + fee = 1.zbs, + timestamp = System.currentTimeMillis() + ) + .right + .get + + val assetId = IssueTx.id() + + sender.postJson("/transactions/broadcast", IssueTx.json()) + + nodes.waitForHeightAriseAndTxPresent(assetId.base58) + + for ((o1ver, o2ver, matcherFeeOrder1, matcherFeeOrder2) <- Seq( + (1: Byte, 3: Byte, None, Some(assetId)), + (1: Byte, 3: Byte, None, None), + (2: Byte, 3: Byte, None, Some(assetId)), + (3: Byte, 1: Byte, Some(assetId), None), + (2: Byte, 3: Byte, None, None), + (3: Byte, 2: Byte, Some(assetId), None), + )) { + + val matcher = pkByAddress(thirdAddress) + val ts = ntpTime.correctedTime() + val expirationTimestamp = ts + Order.MaxLiveTime + var assetBalanceBefore: Long = 0l + + if (matcherFeeOrder1.isEmpty && matcherFeeOrder2.isDefined) { + assetBalanceBefore = sender.assetBalance(secondAddress, assetId.base58).balance + sender.transfer(buyer.address, seller.address, 100000, minFee, Some(assetId.base58), waitForTx = true) + } + + val buyPrice = 500000 + val sellPrice = 500000 + val buyAmount = 40000000 + val sellAmount = 40000000 + val assetPair = AssetPair.createAssetPair("ZBS", assetId.base58).get + val buy = Order.buy(buyer, matcher, assetPair, buyAmount, buyPrice, ts, expirationTimestamp, matcherFee, o1ver, matcherFeeOrder1) + val sell = Order.sell(seller, matcher, assetPair, sellAmount, sellPrice, ts, expirationTimestamp, matcherFee, o2ver, matcherFeeOrder2) + val amount = 40000000 + + val tx = + ExchangeTransactionV2 + .create( + matcher = matcher, + buyOrder = buy, + sellOrder = sell, + amount = amount, + price = sellPrice, + buyMatcherFee = (BigInt(matcherFee) * amount / buy.amount).toLong, + sellMatcherFee = (BigInt(matcherFee) * amount / sell.amount).toLong, + fee = matcherFee, + timestamp = ntpTime.correctedTime() + ) + .right + .get + + sender.postJson("/transactions/broadcast", tx.json()) + + nodes.waitForHeightAriseAndTxPresent(tx.id().base58) + + if (matcherFeeOrder1.isEmpty && matcherFeeOrder2.isDefined) { + sender.assetBalance(secondAddress, assetId.base58).balance shouldBe assetBalanceBefore + } + } + } } diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/SignAndBroadcastApiSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/SignAndBroadcastApiSuite.scala index e6ec24d..5725121 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/SignAndBroadcastApiSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/SignAndBroadcastApiSuite.scala @@ -11,7 +11,8 @@ import com.zbsnetwork.it.sync.{someAssetAmount, _} import com.zbsnetwork.it.transactions.BaseTransactionSuite import com.zbsnetwork.it.util._ import com.zbsnetwork.state._ -import com.zbsnetwork.transaction.{CreateAliasTransaction, DataTransaction, GenesisTransaction, PaymentTransaction} +import com.zbsnetwork.transaction.assets.exchange.AssetPair.extractAssetId +import com.zbsnetwork.transaction._ import com.zbsnetwork.transaction.assets.{BurnTransaction, IssueTransaction, ReissueTransaction, SponsorFeeTransaction} import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order, _} import com.zbsnetwork.transaction.lease.{LeaseCancelTransaction, LeaseTransaction} @@ -285,7 +286,7 @@ class SignAndBroadcastApiSuite extends BaseTransactionSuite with NTPTime { "version" -> 1, "sender" -> firstAddress, "assetId" -> assetId, - "minSponsoredAssetFee" -> 100 + "minSponsoredAssetFee" -> 500 ), usesProofs = true, version = 1 @@ -343,13 +344,25 @@ class SignAndBroadcastApiSuite extends BaseTransactionSuite with NTPTime { version = 1 ) - for ((o1ver, o2ver, tver) <- Seq( - (1: Byte, 1: Byte, 1: Byte), - (1: Byte, 1: Byte, 2: Byte), - (1: Byte, 2: Byte, 2: Byte), - (2: Byte, 1: Byte, 2: Byte), - (2: Byte, 2: Byte, 2: Byte) - )) { + val assetId = extractAssetId(issueTx).get + + val transactionV1versions = (1: Byte, 1: Byte, 1: Byte) // in ExchangeTransactionV1 only orders V1 are supported + val transactionV2versions = for { + o1ver <- 1 to 3 + o2ver <- 1 to 3 + } yield (o1ver.toByte, o2ver.toByte, 2.toByte) + + val versionsWithZbsFee = + (transactionV1versions +: transactionV2versions) + .map { case (o1ver, o2ver, tver) => (o1ver, o2ver, tver, Option.empty[AssetId], Option.empty[AssetId]) } + + val versionsWithAssetFee = for { + o2ver <- 1 to 3 + buyMatcherFeeAssetId = assetId + sellMatcherFeeAssetId = Option.empty[AssetId] + } yield (3.toByte, o2ver.toByte, 2.toByte, buyMatcherFeeAssetId, sellMatcherFeeAssetId) + + for ((o1ver, o2ver, tver, matcherFeeOrder1, matcherFeeOrder2) <- versionsWithZbsFee ++ versionsWithAssetFee) { val buyer = pkByAddress(firstAddress) val seller = pkByAddress(secondAddress) val matcher = pkByAddress(thirdAddress) @@ -361,8 +374,8 @@ class SignAndBroadcastApiSuite extends BaseTransactionSuite with NTPTime { val buyAmount = 2 val sellAmount = 3 val assetPair = AssetPair.createAssetPair("ZBS", issueTx).get - val buy = Order.buy(buyer, matcher, assetPair, buyAmount, buyPrice, ts, expirationTimestamp, mf, o1ver) - val sell = Order.sell(seller, matcher, assetPair, sellAmount, sellPrice, ts, expirationTimestamp, mf, o2ver) + val buy = Order.buy(buyer, matcher, assetPair, buyAmount, buyPrice, ts, expirationTimestamp, mf, o1ver, matcherFeeOrder1) + val sell = Order.sell(seller, matcher, assetPair, sellAmount, sellPrice, ts, expirationTimestamp, mf, o2ver, matcherFeeOrder2) val amount = math.min(buy.amount, sell.amount) val tx = @@ -397,6 +410,9 @@ class SignAndBroadcastApiSuite extends BaseTransactionSuite with NTPTime { .explicitGet() .json() } + val s = sell.getReceiveAmount(amount, sellPrice).right.get + log.info(s"SELLER: ${s}") + log.info(s"BUYER: ${buy.getReceiveAmount(amount, sellPrice).right.get}") val txId = sender.signedBroadcast(tx).id sender.waitForTransaction(txId) diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/transactions/TransactionAPISuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/TransactionAPISuite.scala new file mode 100644 index 0000000..987ea02 --- /dev/null +++ b/it/src/test/scala/com/zbsnetwork/it/sync/transactions/TransactionAPISuite.scala @@ -0,0 +1,135 @@ +package com.zbsnetwork.it.sync.transactions + +import com.typesafe.config.Config +import com.zbsnetwork.account.Address +import com.zbsnetwork.common.utils._ +import com.zbsnetwork.it.api.SyncHttpApi._ +import com.zbsnetwork.it.api.TransactionInfo +import com.zbsnetwork.it.transactions.NodesFromDocker +import com.zbsnetwork.it.{Node, NodeConfigs, ReportingTestName} +import com.zbsnetwork.transaction.transfer.{TransferTransaction, TransferTransactionV1} +import org.scalatest.{CancelAfterFailure, FreeSpec, Matchers} +import play.api.libs.json.JsNumber +import scala.concurrent.duration._ + +class TransactionAPISuite extends FreeSpec with NodesFromDocker with Matchers with ReportingTestName with CancelAfterFailure { + + override def nodeConfigs: Seq[Config] = + NodeConfigs.newBuilder + .overrideBase(_.quorum(0)) + .overrideBase(_.raw("zbs.rest-api.transactions-by-address-limit=10")) + .withDefault(1) + .withSpecial(1, _.nonMiner) + .buildNonConflicting() + + val sender: Node = nodes.head + val recipient: Address = Address.fromString(sender.createAddress()).explicitGet() + + val Zbs: Long = 100000000L + + val AMT: Long = 1 * Zbs + val FEE: Long = (0.001 * Zbs).toLong + + val transactions: List[TransferTransaction] = + (for (i <- 0 to 30) yield { + TransferTransactionV1 + .selfSigned( + None, + sender.privateKey, + recipient, + AMT, + System.currentTimeMillis() + i, + None, + FEE + i * 100, + Array.emptyByteArray + ) + .explicitGet() + }).toList + + val transactionIds = transactions.map(_.id().base58) + + "should accept transactions" in { + transactions.foreach { tx => + sender.broadcastRequest(tx.json() + ("type" -> JsNumber(tx.builder.typeId.toInt))) + } + + val h = sender.height + + sender.waitForHeight(h + 3, 2.minutes) + } + + "should return correct N txs on request without `after`" in { + + def checkForLimit(limit: Int): Unit = { + val expected = + transactionIds + .take(limit) + + val received = + sender + .transactionsByAddress(recipient.address, limit) + .flatten + .map(_.id) + + expected shouldEqual received + } + + for (limit <- 2 to 10 by 1) { + checkForLimit(limit) + } + } + + "should return correct N txs on request with `after`" in { + + def checkForLimit(limit: Int): Unit = { + val expected = + transactionIds + .slice(limit, limit + limit) + + val afterParam = + transactions + .drop(limit - 1) + .head + .id() + .base58 + + val received = + sender + .transactionsByAddress(recipient.address, limit, afterParam) + .flatten + .map(_.id) + + expected shouldEqual received + } + + for (limit <- 2 to 10 by 1) { + checkForLimit(limit) + } + } + + "should return all transactions" in { + def checkForLimit(limit: Int): Unit = { + val received = + loadAll(sender, recipient.address, limit, None, Nil) + .map(_.id) + + received shouldEqual transactionIds + } + + for (limit <- 2 to 10 by 1) { + checkForLimit(limit) + } + } + + def loadAll(node: Node, address: String, limit: Int, maybeAfter: Option[String], acc: List[TransactionInfo]): List[TransactionInfo] = { + val txs = maybeAfter match { + case None => node.transactionsByAddress(address, limit).flatten.toList + case Some(lastId) => node.transactionsByAddress(address, limit, lastId).flatten.toList + } + + txs.lastOption match { + case None => acc ++ txs + case Some(tx) => loadAll(node, address, limit, Some(tx.id), acc ++ txs) + } + } +} diff --git a/it/src/test/scala/com/zbsnetwork/it/sync/utils/TransactionSerializeSuite.scala b/it/src/test/scala/com/zbsnetwork/it/sync/utils/TransactionSerializeSuite.scala index fa6a971..ee620a4 100644 --- a/it/src/test/scala/com/zbsnetwork/it/sync/utils/TransactionSerializeSuite.scala +++ b/it/src/test/scala/com/zbsnetwork/it/sync/utils/TransactionSerializeSuite.scala @@ -14,7 +14,11 @@ import scorex.crypto.encode.Base64 import com.zbsnetwork.common.utils.Base58 import com.zbsnetwork.it.sync._ import com.zbsnetwork.it.util._ +import com.zbsnetwork.lang.v1.FunctionHeader +import com.zbsnetwork.lang.v1.compiler.Terms +import com.zbsnetwork.lang.v1.compiler.Terms.TRUE import com.zbsnetwork.transaction.smart.SetScriptTransaction +import com.zbsnetwork.transaction.smart.ContractInvocationTransaction import com.zbsnetwork.transaction.smart.script.Script import com.zbsnetwork.transaction.transfer.{MassTransferTransaction, TransferTransactionV1, TransferTransactionV2} import com.zbsnetwork.transaction.transfer.MassTransferTransaction.Transfer @@ -349,6 +353,22 @@ class TransactionSerializeSuite extends BaseTransactionSuite with TableDrivenPro .right .get + private val contractInvocation = ContractInvocationTransaction + .create( + PublicKeyAccount.fromBase58String("BqeJY8CP3PeUDaByz57iRekVUGtLxoow4XxPvXfHynaZ").right.get, + PublicKeyAccount.fromBase58String("Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP").right.get, + Terms.FUNCTION_CALL( + function = FunctionHeader.User("testfunc"), + args = List(TRUE) + ), + Some(ContractInvocationTransaction.Payment(7, Some(ByteStr.decodeBase58("73pu8pHFNpj9tmWuYjqnZ962tXzJvLGX86dxjZxGYhoK").get))), + smartMinFee, + ts, + Proofs(Seq(ByteStr.decodeBase58("4bfDaqBcnK3hT8ywFEFndxtS1DTSYfncUqd4s5Vyaa66PZHawtC73rDswUur6QZu5RpqM7L9NFgBHT1vhCoox4vi").get)) + ) + .right + .get + forAll( Table( ("tx", "name"), @@ -373,6 +393,7 @@ class TransactionSerializeSuite extends BaseTransactionSuite with TableDrivenPro (sponsor, "sponsor"), (transferV1, "transferV1"), (transferV2, "transferV2"), + (contractInvocation, "contractInvocation") )) { (tx, name) => test(s"Serialize check of $name transaction") { val r = sender.transactionSerializer(tx.json()).bytes.map(_.toByte) diff --git a/lang/js/src/main/scala/JsAPI.scala b/lang/js/src/main/scala/JsAPI.scala index 380ba1a..8c54a86 100644 --- a/lang/js/src/main/scala/JsAPI.scala +++ b/lang/js/src/main/scala/JsAPI.scala @@ -2,8 +2,9 @@ import cats.kernel.Monoid import com.zbsnetwork.lang.StdLibVersion.{StdLibVersion, _} import com.zbsnetwork.lang.contract.Contract import com.zbsnetwork.lang.directives.DirectiveParser -import com.zbsnetwork.lang.utils.{extractScriptType, extractStdLibVersion} +import com.zbsnetwork.lang.utils.{extractContentType, extractScriptType, extractStdLibVersion} import com.zbsnetwork.lang.v1.CTX +import com.zbsnetwork.lang.v1.ContractLimits import com.zbsnetwork.lang.v1.FunctionHeader.{Native, User} import com.zbsnetwork.lang.v1.compiler.Terms._ import com.zbsnetwork.lang.v1.compiler.Types._ @@ -11,7 +12,7 @@ import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext} import com.zbsnetwork.lang.v1.traits.domain.{Recipient, Tx} import com.zbsnetwork.lang.v1.traits.{DataType, Environment} -import com.zbsnetwork.lang.{Global, ScriptType} +import com.zbsnetwork.lang.{ContentType, Global, ScriptType, StdLibVersion} import scala.scalajs.js import scala.scalajs.js.Dynamic.{literal => jObj} @@ -84,52 +85,72 @@ object JsAPI { Monoid.combineAll(Seq(PureContext.build(v), cryptoContext, zbsContext(V3))) } - @JSExportTopLevel("fullContext") - val fullContext: CTX = - buildScriptContext(V3, isTokenContext = false) - @JSExportTopLevel("getTypes") - def getTypes() = fullContext.types.map(v => js.Dynamic.literal("name" -> v.name, "type" -> typeRepr(v.typeRef))).toJSArray + def getTypes(ver: Int = 2, isTokenContext: Boolean = false): js.Array[js.Object with js.Dynamic] = + buildScriptContext(StdLibVersion.parseVersion(ver), isTokenContext).types + .map(v => js.Dynamic.literal("name" -> v.name, "type" -> typeRepr(v.typeRef))) + .toJSArray @JSExportTopLevel("getVarsDoc") - def getVarsDoc() = fullContext.vars.map(v => js.Dynamic.literal("name" -> v._1, "type" -> typeRepr(v._2._1._1), "doc" -> v._2._1._2)).toJSArray + def getVarsDoc(ver: Int = 2, isTokenContext: Boolean = false): js.Array[js.Object with js.Dynamic] = + buildScriptContext(StdLibVersion.parseVersion(ver), isTokenContext).vars + .map(v => js.Dynamic.literal("name" -> v._1, "type" -> typeRepr(v._2._1._1), "doc" -> v._2._1._2)) + .toJSArray @JSExportTopLevel("getFunctionsDoc") - def getFunctionnsDoc() = - fullContext.functions - .map( - f => - js.Dynamic.literal( - "name" -> f.name, - "doc" -> f.docString, - "resultType" -> typeRepr(f.signature.result), - "args" -> ((f.argsDoc zip f.signature.args) map { arg => - js.Dynamic.literal("name" -> arg._1._1, "type" -> typeRepr(arg._2._2), "doc" -> arg._1._2) - }).toJSArray - )) + def getFunctionsDoc(ver: Int = 2, isTokenContext: Boolean = false): js.Array[js.Object with js.Dynamic] = + buildScriptContext(StdLibVersion.parseVersion(ver), isTokenContext).functions + .map(f => + js.Dynamic.literal( + "name" -> f.name, + "doc" -> f.docString, + "resultType" -> typeRepr(f.signature.result), + "args" -> ((f.argsDoc zip f.signature.args) map { arg => + js.Dynamic.literal("name" -> arg._1._1, "type" -> typeRepr(arg._2._2), "doc" -> arg._1._2) + }).toJSArray + )) .toJSArray - @JSExportTopLevel("compilerContext") - val compilerContext = fullContext.compilerContext + @JSExportTopLevel("contractLimits") + def contractLimits(): js.Dynamic = js.Dynamic.literal( + "MaxExprComplexity" -> ContractLimits.MaxExprComplexity, + "MaxExprSizeInBytes" -> ContractLimits.MaxExprSizeInBytes, + "MaxContractComplexity" -> ContractLimits.MaxContractComplexity, + "MaxContractSizeInBytes" -> ContractLimits.MaxContractSizeInBytes, + "MaxContractInvocationArgs" -> ContractLimits.MaxContractInvocationArgs, + "MaxContractInvocationSizeInBytes" -> ContractLimits.MaxContractInvocationSizeInBytes, + "MaxWriteSetSizeInBytes" -> ContractLimits.MaxWriteSetSizeInBytes, + "MaxPaymentAmount" -> ContractLimits.MaxPaymentAmount + ) - @JSExportTopLevel("compile") - def compile(input: String, isTokenScript: Boolean): js.Dynamic = { + @JSExportTopLevel("scriptInfo") + def scriptInfo(input: String): js.Dynamic = { val directives = DirectiveParser(input) + val info = for { + ver <- extractStdLibVersion(directives) + contentType <- extractContentType(directives) + scriptType <- extractScriptType(directives) + } yield js.Dynamic.literal("stdLibVersion" -> ver, "contentType" -> contentType, "scriptType" -> scriptType) - val scriptWithoutDirectives = - input.linesIterator - .filter(str => !str.contains("{-#")) - .mkString("\n") + info.fold( + err => js.Dynamic.literal("error" -> err), + identity + ) + } + @JSExportTopLevel("compile") + def compile(input: String): js.Dynamic = { + val directives = DirectiveParser(input) val compiled = for { - ver <- extractStdLibVersion(directives) - tpe <- extractScriptType(directives) + ver <- extractStdLibVersion(directives) + contentType <- extractContentType(directives) + scriptType <- extractScriptType(directives) } yield { - tpe match { - case ScriptType.Expression => - val ctx = buildScriptContext(ver, isTokenScript) + contentType match { + case ContentType.Expression => + val ctx = buildScriptContext(ver, scriptType == ScriptType.Asset) Global - .compileScript(scriptWithoutDirectives, ctx.compilerContext) + .compileScript(input, ctx.compilerContext) .fold( err => { js.Dynamic.literal("error" -> err) @@ -138,10 +159,10 @@ object JsAPI { js.Dynamic.literal("result" -> Global.toBuffer(bytes), "ast" -> toJs(ast)) } ) - case ScriptType.Contract => + case ContentType.Contract => // Just ignore stdlib version here Global - .compileContract(scriptWithoutDirectives, fullContractContext.compilerContext) + .compileContract(input, fullContractContext.compilerContext) .fold( err => { js.Dynamic.literal("error" -> err) diff --git a/lang/jvm/src/main/scala/com/zbsnetwork/utils/DocExport.scala b/lang/jvm/src/main/scala/com/zbsnetwork/utils/DocExport.scala index e6f81c2..e696072 100644 --- a/lang/jvm/src/main/scala/com/zbsnetwork/utils/DocExport.scala +++ b/lang/jvm/src/main/scala/com/zbsnetwork/utils/DocExport.scala @@ -4,7 +4,7 @@ import com.zbsnetwork.lang.v1.CTX import com.zbsnetwork.lang.v1.compiler.Types._ import com.zbsnetwork.lang.v1.evaluator.ctx._ import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext -import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext, _} +import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext} import com.zbsnetwork.lang.v1.traits.domain.{Recipient, Tx} import com.zbsnetwork.lang.v1.traits.{DataType, Environment} import com.zbsnetwork.lang.{Global, StdLibVersion} diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractIntegrationTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractIntegrationTest.scala index e549044..5d97fda 100644 --- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractIntegrationTest.scala +++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractIntegrationTest.scala @@ -2,6 +2,7 @@ package com.zbsnetwork.lang import cats.syntax.monoid._ import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.state.diffs.ProduceError._ import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.lang.Common.{NoShrink, sampleTypes} import com.zbsnetwork.lang.v1.compiler.{ContractCompiler, Terms} @@ -18,13 +19,13 @@ import org.scalatest.{Matchers, PropSpec} class ContractIntegrationTest extends PropSpec with PropertyChecks with ScriptGen with Matchers with NoShrink { - property("Simple test") { - val ctx: CTX = - PureContext.build(StdLibVersion.V3) |+| - CTX(sampleTypes, Map.empty, Array.empty) |+| - ZbsContext.build(StdLibVersion.V3, Common.emptyBlockchainEnvironment(), false) + val ctx: CTX = + PureContext.build(StdLibVersion.V3) |+| + CTX(sampleTypes, Map.empty, Array.empty) |+| + ZbsContext.build(StdLibVersion.V3, Common.emptyBlockchainEnvironment(), false) - val src = + property("Simple call") { + parseCompileAndEvaluate( """ | |func fooHelper2() = { @@ -36,11 +37,11 @@ class ContractIntegrationTest extends PropSpec with PropertyChecks with ScriptGe |} | |@Callable(invocation) - |func foo(a:ByteStr) = { + |func foo(a:ByteVector) = { | let x = invocation.caller.bytes | if (fooHelper()) - | then WriteSet(List(DataEntry("b", 1), DataEntry("sender", x))) - | else WriteSet(List(DataEntry("a", a), DataEntry("sender", x))) + | then WriteSet([DataEntry("b", 1), DataEntry("sender", x)]) + | else WriteSet([DataEntry("a", a), DataEntry("sender", x)]) |} | |@Verifier(t) @@ -48,27 +49,55 @@ class ContractIntegrationTest extends PropSpec with PropertyChecks with ScriptGe | true |} | - """.stripMargin - - val parsed = Parser.parseContract(src).get.value - - val compiled = ContractCompiler(ctx.compilerContext, parsed).explicitGet() - - val expectedResult = ContractResult( + """.stripMargin, + "foo" + ).explicitGet() shouldBe ContractResult( List( DataItem.Bin("a", ByteStr.empty), DataItem.Bin("sender", ByteStr.empty) ), List() ) + } + + property("Callable can have 22 args") { + parseCompileAndEvaluate( + """ + |@Callable(invocation) + |func foo(a1:Int, a2:Int, a3:Int, a4:Int, a5:Int, a6:Int, a7:Int, a8:Int, a9:Int, a10:Int, + | a11:Int, a12:Int, a13:Int, a14:Int, a15:Int, a16:Int, a17:Int, a18:Int, a19:Int, a20:Int, + | a21:Int, a22:Int) = { WriteSet([DataEntry(toString(a1), a22)]) } + """.stripMargin, + "foo", + Range(1, 23).map(i => Terms.CONST_LONG(i)).toList + ).explicitGet() shouldBe ContractResult(List(DataItem.Lng("1", 22)), List()) + } - val result = ContractEvaluator( + property("Callable can't have more than 22 args") { + val src = + """ + |@Callable(invocation) + |func foo(a1:Int, a2:Int, a3:Int, a4:Int, a5:Int, a6:Int, a7:Int, a8:Int, a9:Int, a10:Int, + | a11:Int, a12:Int, a13:Int, a14:Int, a15:Int, a16:Int, a17:Int, a18:Int, a19:Int, a20:Int, + | a21:Int, a22:Int, a23:Int) = { throw() } + """.stripMargin + + val parsed = Parser.parseContract(src).get.value + + ContractCompiler(ctx.compilerContext, parsed) should produce("no more than 22 arguments") + } + + def parseCompileAndEvaluate(script: String, + func: String, + args: List[Terms.EXPR] = List(Terms.CONST_BYTESTR(ByteStr.empty))): Either[ExecutionError, ContractResult] = { + val parsed = Parser.parseContract(script).get.value + val compiled = ContractCompiler(ctx.compilerContext, parsed).explicitGet() + + ContractEvaluator( ctx.evaluationContext, compiled, - Invocation(Terms.FUNCTION_CALL(FunctionHeader.User("foo"), List(Terms.CONST_BYTESTR(ByteStr.empty))), ByteStr.empty, None, ByteStr.empty) - ).explicitGet() - - result shouldBe expectedResult + Invocation(Terms.FUNCTION_CALL(FunctionHeader.User(func), args), ByteStr.empty, None, ByteStr.empty) + ) } } diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractParserTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractParserTest.scala index f8a386f..25c3c6c 100644 --- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractParserTest.scala +++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/ContractParserTest.scala @@ -185,4 +185,37 @@ class ContractParserTest extends PropSpec with PropertyChecks with Matchers with |""".stripMargin parse(code) } + + property("parse directives as comments (ignore)") { + val code = + """ + | # comment + | {-# STDLIB_VERSION 3 #-} + | {-# TEST_TEST 123 #-} + | # comment + | + | @Ann(foo) + | func bar(arg:Baz) = { + | 3 + | } + | + | + |""".stripMargin + parse(code) shouldBe CONTRACT( + AnyPos, + List.empty, + List( + ANNOTATEDFUNC( + AnyPos, + List(Expressions.ANNOTATION(AnyPos, PART.VALID(AnyPos, "Ann"), List(PART.VALID(AnyPos, "foo")))), + Expressions.FUNC( + AnyPos, + PART.VALID(AnyPos, "bar"), + List((PART.VALID(AnyPos, "arg"), List(PART.VALID(AnyPos, "Baz")))), + CONST_LONG(AnyPos, 3) + ) + ) + ) + ) + } } diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/DirectiveParserTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/DirectiveParserTest.scala index 33213e4..3122a30 100644 --- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/DirectiveParserTest.scala +++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/DirectiveParserTest.scala @@ -9,7 +9,7 @@ class DirectiveParserTest extends PropSpec with PropertyChecks with Matchers { def parse(s: String): List[Directive] = DirectiveParser(s) - property("parse STDLIB_VERSION directive") { + property("parse directives") { parse("{-# STDLIB_VERSION 10 #-}") shouldBe List(Directive(STDLIB_VERSION, "10")) parse(""" | @@ -18,8 +18,13 @@ class DirectiveParserTest extends PropSpec with PropertyChecks with Matchers { """.stripMargin) shouldBe List(Directive(STDLIB_VERSION, "10")) parse(""" | - |{-# SCRIPT_TYPE FOO #-} + |{-# CONTENT_TYPE FOO #-} | - """.stripMargin) shouldBe List(Directive(SCRIPT_TYPE, "FOO")) + """.stripMargin) shouldBe List(Directive(CONTENT_TYPE, "FOO")) + parse(""" + | + |{-# SCRIPT_TYPE BAR #-} + | + """.stripMargin) shouldBe List(Directive(SCRIPT_TYPE, "BAR")) } } diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/EvaluatorV1Test.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/EvaluatorV1Test.scala index 34d6efb..3090b17 100644 --- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/EvaluatorV1Test.scala +++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/EvaluatorV1Test.scala @@ -7,8 +7,8 @@ import cats.kernel.Monoid import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.{Base58, Base64, EitherExt2} import com.zbsnetwork.lang.Common._ -import com.zbsnetwork.lang.Testing._ import com.zbsnetwork.lang.StdLibVersion._ +import com.zbsnetwork.lang.Testing._ import com.zbsnetwork.lang.v1.compiler.ExpressionCompiler import com.zbsnetwork.lang.v1.compiler.Terms._ import com.zbsnetwork.lang.v1.compiler.Types._ @@ -36,6 +36,8 @@ class EvaluatorV1Test extends PropSpec with PropertyChecks with Matchers with Sc private val defaultCryptoContext = CryptoContext.build(Global) + val blockBuilder: Gen[(LET, EXPR) => EXPR] = Gen.oneOf(true, false).map(if (_) (BLOCK.apply _) else (LET_BLOCK.apply _)) + private def defaultFullContext(environment: Environment): CTX = Monoid.combineAll( Seq( defaultCryptoContext, @@ -49,7 +51,7 @@ class EvaluatorV1Test extends PropSpec with PropertyChecks with Matchers with Sc private def ev[T <: EVALUATED](context: EvaluationContext = pureEvalContext, expr: EXPR): Either[ExecutionError, T] = EvaluatorV1[T](context, expr) - private def simpleDeclarationAndUsage(i: Int) = BLOCK(LET("x", CONST_LONG(i)), REF("x")) + private def simpleDeclarationAndUsage(i: Int, blockBuilder: (LET, EXPR) => EXPR) = blockBuilder(LET("x", CONST_LONG(i)), REF("x")) property("successful on very deep expressions (stack overflow check)") { val term = (1 to 100000).foldLeft[EXPR](CONST_LONG(0))((acc, _) => FUNCTION_CALL(sumLong.header, List(acc, CONST_LONG(1)))) @@ -58,80 +60,101 @@ class EvaluatorV1Test extends PropSpec with PropertyChecks with Matchers with Sc } property("return error and log of failed evaluation") { - val (log, Left(err)) = EvaluatorV1.applywithLogging[EVALUATED]( - pureEvalContext, - expr = BLOCK( - LET("x", CONST_LONG(3)), - BLOCK( - LET("x", FUNCTION_CALL(sumLong.header, List(CONST_LONG(3), CONST_LONG(0)))), - FUNCTION_CALL(PureContext.eq.header, List(REF("z"), CONST_LONG(1))) + forAll(blockBuilder) { block => + val (log, Left(err)) = EvaluatorV1.applywithLogging[EVALUATED]( + pureEvalContext, + expr = block( + LET("x", CONST_LONG(3)), + block( + LET("x", FUNCTION_CALL(sumLong.header, List(CONST_LONG(3), CONST_LONG(0)))), + FUNCTION_CALL(PureContext.eq.header, List(REF("z"), CONST_LONG(1))) + ) ) ) - ) - val expectedError = "A definition of 'z' not found" + val expectedError = "A definition of 'z' not found" + err shouldBe expectedError + log.isEmpty shouldBe true + } - err shouldBe expectedError - log.isEmpty shouldBe true } property("successful on unused let") { - ev[EVALUATED]( - expr = BLOCK( - LET("x", CONST_LONG(3)), - CONST_LONG(3) - )) shouldBe evaluated(3) + forAll(blockBuilder) { block => + ev[EVALUATED]( + expr = block( + LET("x", CONST_LONG(3)), + CONST_LONG(3) + )) shouldBe evaluated(3) + } } property("successful on x = y") { - ev[EVALUATED]( - expr = BLOCK(LET("x", CONST_LONG(3)), - BLOCK( - LET("y", REF("x")), - FUNCTION_CALL(sumLong.header, List(REF("x"), REF("y"))) - ))) shouldBe evaluated(6) + forAll(blockBuilder) { block => + ev[EVALUATED]( + expr = block(LET("x", CONST_LONG(3)), + block( + LET("y", REF("x")), + FUNCTION_CALL(sumLong.header, List(REF("x"), REF("y"))) + ))) shouldBe evaluated(6) + } } property("successful on simple get") { - ev[EVALUATED](expr = simpleDeclarationAndUsage(3)) shouldBe evaluated(3) + forAll(blockBuilder) { block => + ev[EVALUATED](expr = simpleDeclarationAndUsage(3, block)) shouldBe evaluated(3) + } } property("successful on get used further in expr") { - ev[EVALUATED]( - expr = BLOCK( - LET("x", CONST_LONG(3)), - FUNCTION_CALL(PureContext.eq.header, List(REF("x"), CONST_LONG(2))) - )) shouldBe evaluated(false) + forAll(blockBuilder) { block => + ev[EVALUATED]( + expr = block( + LET("x", CONST_LONG(3)), + FUNCTION_CALL(PureContext.eq.header, List(REF("x"), CONST_LONG(2))) + )) shouldBe evaluated(false) + } } property("successful on multiple lets") { - ev[EVALUATED]( - expr = BLOCK( - LET("x", CONST_LONG(3)), - BLOCK(LET("y", CONST_LONG(3)), FUNCTION_CALL(PureContext.eq.header, List(REF("x"), REF("y")))) - )) shouldBe evaluated(true) + forAll(blockBuilder) { block => + ev[EVALUATED]( + expr = block( + LET("x", CONST_LONG(3)), + block(LET("y", CONST_LONG(3)), FUNCTION_CALL(PureContext.eq.header, List(REF("x"), REF("y")))) + )) shouldBe evaluated(true) + } } property("successful on multiple lets with expression") { - ev[EVALUATED]( - expr = BLOCK( - LET("x", CONST_LONG(3)), - BLOCK( - LET("y", FUNCTION_CALL(sumLong.header, List(CONST_LONG(3), CONST_LONG(0)))), - FUNCTION_CALL(PureContext.eq.header, List(REF("x"), REF("y"))) - ) - )) shouldBe evaluated(true) + forAll(blockBuilder) { block => + ev[EVALUATED]( + expr = block( + LET("x", CONST_LONG(3)), + block( + LET("y", FUNCTION_CALL(sumLong.header, List(CONST_LONG(3), CONST_LONG(0)))), + FUNCTION_CALL(PureContext.eq.header, List(REF("x"), REF("y"))) + ) + )) shouldBe evaluated(true) + } } property("successful on deep type resolution") { - ev[EVALUATED](expr = IF(FUNCTION_CALL(PureContext.eq.header, List(CONST_LONG(1), CONST_LONG(2))), simpleDeclarationAndUsage(3), CONST_LONG(4))) shouldBe evaluated( - 4) + forAll(blockBuilder) { block => + ev[EVALUATED](expr = IF(FUNCTION_CALL(PureContext.eq.header, List(CONST_LONG(1), CONST_LONG(2))), + simpleDeclarationAndUsage(3, block), + CONST_LONG(4))) shouldBe evaluated(4) + } } property("successful on same value names in different branches") { - val expr = - IF(FUNCTION_CALL(PureContext.eq.header, List(CONST_LONG(1), CONST_LONG(2))), simpleDeclarationAndUsage(3), simpleDeclarationAndUsage(4)) - ev[EVALUATED](expr = expr) shouldBe evaluated(4) + forAll(blockBuilder) { block => + val expr = + IF(FUNCTION_CALL(PureContext.eq.header, List(CONST_LONG(1), CONST_LONG(2))), + simpleDeclarationAndUsage(3, block), + simpleDeclarationAndUsage(4, block)) + ev[EVALUATED](expr = expr) shouldBe evaluated(4) + } } property("fails if definition not found") { @@ -173,32 +196,37 @@ class EvaluatorV1Test extends PropSpec with PropertyChecks with Matchers with Sc functions = Map.empty ) ) - ev[EVALUATED]( - context = context, - expr = BLOCK(LET("Z", REF("badVal")), FUNCTION_CALL(sumLong.header, List(GETTER(REF("p"), "X"), CONST_LONG(2)))) - ) shouldBe evaluated(5) + forAll(blockBuilder) { block => + ev[EVALUATED]( + context = context, + expr = block(LET("Z", REF("badVal")), FUNCTION_CALL(sumLong.header, List(GETTER(REF("p"), "X"), CONST_LONG(2)))) + ) shouldBe evaluated(5) + } } property("let is evaluated maximum once") { - var functionEvaluated = 0 + forAll(blockBuilder) { block => + var functionEvaluated = 0 - val f = NativeFunction("F", 1: Long, 258: Short, LONG: TYPE, "test function", Seq(("_", LONG, "")): _*) { _ => - functionEvaluated = functionEvaluated + 1 - evaluated(1L) - } + val f = NativeFunction("F", 1: Long, 258: Short, LONG: TYPE, "test function", Seq(("_", LONG, "")): _*) { _ => + functionEvaluated = functionEvaluated + 1 + evaluated(1L) + } - val context = Monoid.combine(pureEvalContext, - EvaluationContext( - typeDefs = Map.empty, - letDefs = Map.empty, - functions = Map(f.header -> f) - )) - ev[EVALUATED]( - context = context, - expr = BLOCK(LET("X", FUNCTION_CALL(f.header, List(CONST_LONG(1000)))), FUNCTION_CALL(sumLong.header, List(REF("X"), REF("X")))) - ) shouldBe evaluated(2L) + val context = Monoid.combine(pureEvalContext, + EvaluationContext( + typeDefs = Map.empty, + letDefs = Map.empty, + functions = Map(f.header -> f) + )) - functionEvaluated shouldBe 1 + ev[EVALUATED]( + context = context, + expr = block(LET("X", FUNCTION_CALL(f.header, List(CONST_LONG(1000)))), FUNCTION_CALL(sumLong.header, List(REF("X"), REF("X")))) + ) shouldBe evaluated(2L) + + functionEvaluated shouldBe 1 + } } property("successful on ref getter evaluation") { @@ -261,15 +289,16 @@ class EvaluatorV1Test extends PropSpec with PropertyChecks with Matchers with Sc ) ) - val expr = GETTER( - BLOCK( - LET("fooInstance", FUNCTION_CALL(fooCtor.header, List.empty)), - FUNCTION_CALL(fooTransform.header, List(REF("fooInstance"))) - ), - "bar" - ) - - ev[EVALUATED](context, expr) shouldBe evaluated("TRANSFORMED_BAR") + forAll(blockBuilder) { block => + val expr = GETTER( + block( + LET("fooInstance", FUNCTION_CALL(fooCtor.header, List.empty)), + FUNCTION_CALL(fooTransform.header, List(REF("fooInstance"))) + ), + "bar" + ) + ev[EVALUATED](context, expr) shouldBe evaluated("TRANSFORMED_BAR") + } } property("successful on simple function evaluation") { diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/IntegrationTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/IntegrationTest.scala index d84aced..a7ccfb8 100755 --- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/IntegrationTest.scala +++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/IntegrationTest.scala @@ -359,12 +359,28 @@ class IntegrationTest extends PropSpec with PropertyChecks with ScriptGen with M ev[CONST_LONG](context, expr) shouldBe evaluated(8) } - property("list constructor primitive") { + property("listN constructor primitive") { val src = """ - |List(1,2) + |cons(1, cons(2, cons(3, cons(4, cons(5, nil))))) """.stripMargin - eval[EVALUATED](src) shouldBe evaluated(List(1, 2)) + eval[EVALUATED](src) shouldBe evaluated(List(1, 2, 3, 4, 5)) + } + + property("listN constructor binary op") { + val src = + """ + |1::2::3::4::5::nil + """.stripMargin + eval[EVALUATED](src) shouldBe evaluated(List(1, 2, 3, 4, 5)) + } + + property("list syntax sugar") { + val src = + """ + |[1,2,3, 4, 5] + """.stripMargin + eval[EVALUATED](src) shouldBe evaluated(List(1, 2, 3, 4, 5)) } property("list constructor for different data entries") { @@ -373,7 +389,7 @@ class IntegrationTest extends PropSpec with PropertyChecks with ScriptGen with M |let x = DataEntry("foo",1) |let y = DataEntry("bar","2") |let z = DataEntry("baz","2") - |List(x,y,z) + |[x,y,z] """.stripMargin eval[EVALUATED](src) shouldBe Right( ARR(Vector( @@ -383,4 +399,147 @@ class IntegrationTest extends PropSpec with PropertyChecks with ScriptGen with M ))) } + property("allow 'throw' in '==' arguments") { + val src = + """true == throw("test passed")""" + eval[EVALUATED](src) shouldBe Left("test passed") + } + + property("ban to compare different types") { + val src = + """true == "test passed" """ + eval[EVALUATED](src) should produce("Compilation failed: Can't match inferred types") + } + + property("ensure user function: success") { + val src = + """ + |let x = true + |ensure(x, "test fail") + """.stripMargin + eval[EVALUATED](src) shouldBe Right(TRUE) + } + + property("ensure user function: fail") { + val src = + """ + |let x = false + |ensure(x, "test fail") + """.stripMargin + eval[EVALUATED](src) shouldBe Left("test fail") + } + + property("postfix syntax (one argument)") { + val src = + """ + |let x = true + |x.ensure("test fail") + """.stripMargin + eval[EVALUATED](src) shouldBe Right(TRUE) + } + + property("postfix syntax (no arguments)") { + val src = + """unit.isDefined()""" + eval[EVALUATED](src) shouldBe Right(FALSE) + } + + property("postfix syntax (many argument)") { + val src = + """ 5.fraction(7,2) """ + eval[EVALUATED](src) shouldBe Right(CONST_LONG(17L)) + } + + property("postfix syntax (users defined function)") { + val src = + """ + |func dub(s:String) = { s+s } + |"qwe".dub() + """.stripMargin + eval[EVALUATED](src) shouldBe Right(CONST_STRING("qweqwe")) + } + + property("extract UTF8 string") { + val src = + """ base58'2EtvziXsJaBRS'.toUtf8String() """ + eval[EVALUATED](src) shouldBe Right(CONST_STRING("abcdefghi")) + } + + property("extract Long") { + val src = + """ base58'2EtvziXsJaBRS'.toInt() """ + eval[EVALUATED](src) shouldBe Right(CONST_LONG(0x6162636465666768l)) + } + + property("extract Long by offset") { + val src = + """ base58'2EtvziXsJaBRS'.toInt(1) """ + eval[EVALUATED](src) shouldBe Right(CONST_LONG(0x6263646566676869l)) + } + + property("extract Long by offset (patrial)") { + val src = + """ base58'2EtvziXsJaBRS'.toInt(2) """ + eval[EVALUATED](src) should produce("IndexOutOfBounds") + } + + property("extract Long by offset (out of bounds)") { + val src = + """ base58'2EtvziXsJaBRS'.toInt(10) """ + eval[EVALUATED](src) should produce("IndexOutOfBounds") + } + + property("extract Long by offset (negative)") { + val src = + """ base58'2EtvziXsJaBRS'.toInt(-2) """ + eval[EVALUATED](src) should produce("IndexOutOfBounds") + } + + property("indexOf") { + val src = + """ "qweqwe".indexOf("we") """ + eval[EVALUATED](src) shouldBe Right(CONST_LONG(1L)) + } + + property("indexOf with start offset") { + val src = + """ "qweqwe".indexOf("we", 2) """ + eval[EVALUATED](src) shouldBe Right(CONST_LONG(4L)) + } + + property("indexOf (not present)") { + val src = + """ "qweqwe".indexOf("ww") """ + eval[EVALUATED](src) shouldBe Right(unit) + } + + property("split") { + val src = + """ "q:we:.;q;we:x;q.we".split(":.;") """ + eval[EVALUATED](src) shouldBe Right(ARR(IndexedSeq(CONST_STRING("q:we"), CONST_STRING("q;we:x;q.we")))) + } + + property("parseInt") { + val src = + """ "42".parseInt() """ + eval[EVALUATED](src) shouldBe Right(CONST_LONG(42L)) + } + + property("parseIntValue") { + val src = + """ "42".parseInt() """ + eval[EVALUATED](src) shouldBe Right(CONST_LONG(42L)) + } + + property("parseInt fail") { + val src = + """ "x42".parseInt() """ + eval[EVALUATED](src) shouldBe Right(unit) + } + + property("parseIntValue fail") { + val src = + """ "x42".parseIntValue() """ + eval[EVALUATED](src) shouldBe 'left + } } diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/TypeInferrerTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/TypeInferrerTest.scala index 8ff3014..76e1d0d 100644 --- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/TypeInferrerTest.scala +++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/TypeInferrerTest.scala @@ -5,7 +5,6 @@ import org.scalatest.{FreeSpec, Matchers} import Common._ import com.zbsnetwork.lang.v1.compiler.TypeInferrer import com.zbsnetwork.lang.v1.evaluator.ctx.CaseType -import com.zbsnetwork.lang.v1.evaluator.ctx.impl._ class TypeInferrerTest extends FreeSpec with Matchers { @@ -125,6 +124,11 @@ class TypeInferrerTest extends FreeSpec with Matchers { TypeInferrer(Seq((LONG, PARAMETERIZEDUNION(List(typeparamT, typeparamG))))) should produce("Can't resolve correct type") } } + + "Lists" in { + TypeInferrer(Seq( /*(LONG, typeparamT),*/ (LIST(NOTHING), PARAMETERIZEDLIST(typeparamG)))) shouldBe Right( + Map( /*typeparamT -> LONG,*/ typeparamG -> NOTHING)) + } } } } diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ContractCompilerTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ContractCompilerTest.scala index f17f578..cda1aad 100644 --- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ContractCompilerTest.scala +++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ContractCompilerTest.scala @@ -27,9 +27,9 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi """ | | @Callable(invocation) - | func foo(a:ByteStr) = { + | func foo(a:ByteVector) = { | let sender0 = invocation.caller.bytes - | WriteSet(List(DataEntry("a", a), DataEntry("sender", sender0))) + | WriteSet([DataEntry("a", a), DataEntry("sender", sender0)]) | } | | @Verifier(t) @@ -49,14 +49,16 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi Terms.FUNC( "foo", List("a"), - BLOCK( + LET_BLOCK( LET("sender0", GETTER(GETTER(REF("invocation"), "caller"), "bytes")), FUNCTION_CALL( User(FieldNames.WriteSet), List(FUNCTION_CALL( - Native(1102), - List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("a"), REF("a"))), - FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("sender0")))) + Native(1100), + List( + FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("a"), REF("a"))), + FUNCTION_CALL(Native(1100), List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("sender0"))), REF("nil"))) + ) )) ) ) @@ -78,13 +80,13 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi """ | | @Callable(invocation) - | func foo(a:ByteStr) = { + | func foo(a:ByteVector) = { | let sender0 = invocation.caller.bytes - | WriteSet(List(DataEntry("a", a), DataEntry("sender", sender0))) + | WriteSet([DataEntry("a", a), DataEntry("sender", sender0)]) | } | | @Callable(invocation) - | func foo1(a:ByteStr) = { + | func foo1(a:ByteVector) = { | foo(a) | } | @@ -107,10 +109,10 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi | } | | @Callable(invocation) - | func foo(a:ByteStr) = { + | func foo(a:ByteVector) = { | let aux = bar() | let sender0 = invocation.caller.bytes - | WriteSet(List(DataEntry("a", a), DataEntry("sender", sender0))) + | WriteSet([DataEntry("a", a), DataEntry("sender", sender0)]) | } | | @@ -127,7 +129,7 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi """ | | @Callable(invocation) - | func foo(a:ByteStr) = { + | func foo(a:ByteVector) = { | a + invocation.caller.bytes | } | @@ -138,6 +140,24 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi compiler.ContractCompiler(ctx, expr) should produce(FieldNames.Error) } + property("annotation binding can have the same name as annotated function") { + val ctx = compilerContext + val expr = { + val script = + """ + | + |@Callable(sameName) + |func sameName() = { + | throw() + |} + | + | + """.stripMargin + Parser.parseContract(script).get.value + } + compiler.ContractCompiler(ctx, expr) shouldBe 'right + } + property("contract compiles fails if has more than one verifier function") { val ctx = compilerContext val expr = { @@ -215,7 +235,7 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi | case _ => 0 | } | let newAmount = currentAmount + pmt.amount - | WriteSet(List(DataEntry(currentKey, newAmount))) + | WriteSet([DataEntry(currentKey, newAmount)]) | | } | } @@ -233,8 +253,8 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi | else if (newAmount < 0) | then throw("Not enough balance") | else ContractResult( - | WriteSet(List(DataEntry(currentKey, newAmount))), - | TransferSet(List(ContractTransfer(i.caller, amount, unit))) + | WriteSet([DataEntry(currentKey, newAmount)]), + | TransferSet([ContractTransfer(i.caller, amount, unit)]) | ) | } | @@ -253,14 +273,14 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi """ | | @Callable(invocation) - | func foo(a:ByteStr) = { + | func foo(a:ByteVector) = { | throw() | } | | @Callable(i) | func bar() = { - | if (true) then WriteSet(List(DataEntry("entr1","entr2"))) - | else TransferSet(List(ContractTransfer(i.caller, zbsBalance(i.contractAddress), base58'somestr'))) + | if (true) then WriteSet([DataEntry("entr1","entr2")]) + | else TransferSet([ContractTransfer(i.caller, zbsBalance(i.contractAddress), base58'somestr')]) | } | | @Verifier(t) @@ -282,12 +302,12 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi | |@Callable(i) |func sameName() = { - | WriteSet(List(DataEntry("a", "a"))) + | WriteSet([DataEntry("a", "a")]) |} | |@Callable(i) |func sameName() = { - | WriteSet(List(DataEntry("b", "b"))) + | WriteSet([DataEntry("b", "b")]) |} | |@Verifier(i) @@ -298,7 +318,7 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi """.stripMargin Parser.parseContract(script).get.value } - compiler.ContractCompiler(ctx, expr) should produce("Contract functions must have unique names") + compiler.ContractCompiler(ctx, expr) should produce("is already defined") } property("contract compilation fails if declaration and annotation bindings has the same name") { @@ -311,7 +331,7 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi | |@Callable(x) |func some(i: Int) = { - | WriteSet(List(DataEntry("a", "a"))) + | WriteSet([DataEntry("a", "a")]) |} | """.stripMargin @@ -329,9 +349,9 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi |@Callable(i) |func some(i: Int) = { | if (i.contractAddress == "abc") then - | WriteSet(List(DataEntry("a", "a"))) + | WriteSet([DataEntry("a", "a")]) | else - | WriteSet(List(DataEntry("a", "b"))) + | WriteSet([DataEntry("a", "b")]) |} | """.stripMargin @@ -348,12 +368,12 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi | |@Callable(x) |func foo(i: Int) = { - | WriteSet(List(DataEntry("a", "a"))) + | WriteSet([DataEntry("a", "a")]) |} | |@Callable(i) |func bar(x: Int) = { - | WriteSet(List(DataEntry("a", "a"))) + | WriteSet([DataEntry("a", "a")]) |} | """.stripMargin @@ -372,7 +392,7 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi | |@Callable(i) |func some(x: Int) = { - | WriteSet(List(DataEntry("a", "a"))) + | WriteSet([DataEntry("a", "a")]) |} | """.stripMargin @@ -380,4 +400,5 @@ class ContractCompilerTest extends PropSpec with PropertyChecks with Matchers wi } compiler.ContractCompiler(ctx, expr) shouldBe 'right } + } diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/DecompilerTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/DecompilerTest.scala index ad10ecd..82ad380 100644 --- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/DecompilerTest.scala +++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/DecompilerTest.scala @@ -3,48 +3,29 @@ package com.zbsnetwork.lang.compiler import cats.kernel.Monoid import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.Base58 +import com.zbsnetwork.lang.Global import com.zbsnetwork.lang.contract.Contract import com.zbsnetwork.lang.contract.Contract._ import com.zbsnetwork.lang.v1.FunctionHeader.{Native, User} import com.zbsnetwork.lang.v1.compiler.Terms._ import com.zbsnetwork.lang.v1.compiler.{Decompiler, Terms} import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext} -import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext -import com.zbsnetwork.lang.v1.parser.BinaryOperation -import com.zbsnetwork.lang.v1.{CTX, FunctionHeader, compiler} -import com.zbsnetwork.lang.{Common, Global, StdLibVersion} +import com.zbsnetwork.lang.v1.parser.BinaryOperation.NE_OP +import com.zbsnetwork.lang.v1.{CTX, FunctionHeader} import org.scalatest.prop.PropertyChecks import org.scalatest.{Matchers, PropSpec} class DecompilerTest extends PropSpec with PropertyChecks with Matchers { + implicit class StringCmp(s1: String) { + def shouldEq(s2: String) = s1.replace("\r\n", "\n") shouldEqual s2.replace("\r\n", "\n") + } + val CTX: CTX = Monoid.combineAll(Seq(PureContext.build(com.zbsnetwork.lang.StdLibVersion.V3), CryptoContext.build(Global))) val decompilerContext = CTX.decompilerContext - property("ctx debug test") { - val ctx = Monoid.combine(compilerContext, ZbsContext.build(StdLibVersion.V3, Common.emptyBlockchainEnvironment(), false).compilerContext) - val defs = ctx.functionDefs - .filterKeys(BinaryOperation.opsByPriority.flatten.map(x => BinaryOperation.opsToFunctions(x) -> x).toMap.keys.toList.contains(_)) - .mapValues(_.map(_.header) - .filter(_.isInstanceOf[Native]) - .map(_.asInstanceOf[Native].name)) - .toList - .flatMap { case (name, codes) => codes.map((_, name)) } - defs.mkString("\n").toString shouldBe - """(104,*) - |(106,%) - |(103,>=) - |(101,-) - |(0,==) - |(100,+) - |(300,+) - |(203,+) - |(105,/) - |(102,>)""".stripMargin - } - property("successful on very deep expressions (stack overflow check)") { val expr = (1 to 10000).foldLeft[EXPR](CONST_LONG(0)) { (acc, _) => FUNCTION_CALL(function = FunctionHeader.Native(100), List(CONST_LONG(1), acc)) @@ -53,16 +34,31 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { } property("simple let") { - val expr = Terms.LET_BLOCK(LET("a", CONST_LONG(1)), TRUE) - Decompiler(expr, decompilerContext) shouldBe "{ let a = 1; true }" + val expr = Terms.LET_BLOCK(LET("a", CONST_LONG(1)), Terms.LET_BLOCK(LET("b", CONST_LONG(2)), Terms.LET_BLOCK(LET("c", CONST_LONG(3)), TRUE))) + Decompiler(expr, decompilerContext) shouldEq + """let a = 1 + |let b = 2 + |let c = 3 + |true""".stripMargin } + property("let in let") { + val expr = + Terms.LET_BLOCK(LET("a", Terms.LET_BLOCK(LET("x", CONST_LONG(0)), TRUE)), Terms.LET_BLOCK(LET("c", CONST_LONG(3)), TRUE)) + Decompiler(expr, decompilerContext) shouldEq + """let a = { + | let x = 0 + | true + | } + |let c = 3 + |true""".stripMargin + } property("native function call with one arg") { val expr = Terms.FUNCTION_CALL( function = FunctionHeader.Native(500), args = List(TRUE) ) - Decompiler(expr, decompilerContext) shouldBe "sigVerify(true)" + Decompiler(expr, decompilerContext) shouldEq "sigVerify(true)" } property("native function call with two arg (binary operations)") { @@ -70,12 +66,12 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { function = FunctionHeader.Native(100), args = List(CONST_LONG(1), CONST_LONG(2)) ) - Decompiler(expr, decompilerContext) shouldBe "(1 + 2)" + Decompiler(expr, decompilerContext) shouldEq "(1 + 2)" } property("nested binary operations") { val expr = FUNCTION_CALL(Native(105), List(FUNCTION_CALL(Native(101), List(REF("height"), REF("startHeight"))), REF("interval"))) - Decompiler(expr, decompilerContext) shouldBe "((height - startHeight) / interval)" + Decompiler(expr, decompilerContext) shouldEq "((height - startHeight) / interval)" } property("unknown native function call") { @@ -83,7 +79,7 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { function = FunctionHeader.Native(254), args = List(CONST_LONG(1), CONST_LONG(2)) ) - Decompiler(expr, decompilerContext) shouldBe "Native<254>(1, 2)" + Decompiler(expr, decompilerContext) shouldEq "Native<254>(1, 2)" } property("user function call with one args") { @@ -91,7 +87,7 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { function = FunctionHeader.User("foo"), args = List(TRUE) ) - Decompiler(expr, decompilerContext) shouldBe "foo(true)" + Decompiler(expr, decompilerContext) shouldEq "foo(true)" } property("user function call with empty args") { @@ -99,7 +95,7 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { function = FunctionHeader.User("foo"), args = List.empty ) - Decompiler(expr, decompilerContext) shouldBe "foo()" + Decompiler(expr, decompilerContext) shouldEq "foo()" } property("v2 with LET in BLOCK") { @@ -107,12 +103,10 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { LET("vari", REF("p")), TRUE ) - Decompiler(expr, decompilerContext) shouldBe - """{ - | let vari = - | p; - | true - |}""".stripMargin + val actual = Decompiler(expr, decompilerContext) + val expected = """|let vari = p + |true""".stripMargin + actual shouldEq expected } property("let and function call in block") { @@ -121,12 +115,43 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { function = FunctionHeader.Native(100), args = List(REF("v"), CONST_LONG(2)) )) - Decompiler(expr, decompilerContext) shouldBe - """{ - | let v = - | 1; - | (v + 2) - |}""".stripMargin + Decompiler(expr, decompilerContext) shouldEq + """let v = 1 + |(v + 2)""".stripMargin + } + + ignore("neq binary op") { + val expr = + Terms.FUNCTION_CALL( + function = FunctionHeader.User(NE_OP.func), + args = List(CONST_LONG(4), CONST_LONG(2)) + ) + Decompiler(expr, decompilerContext) shouldEq + """4 != 2""".stripMargin + } + + property("function with complex args") { + val expr = BLOCK( + LET( + "x", + BLOCK(LET("y", + Terms.FUNCTION_CALL( + function = FunctionHeader.User("foo"), + args = List(BLOCK(LET("a", CONST_LONG(4)), REF("a")), CONST_LONG(2)) + )), + TRUE) + ), + FALSE + ) + Decompiler(expr, decompilerContext) shouldEq + """let x = { + | let y = foo({ + | let a = 4 + | a + | }, 2) + | true + | } + |false""".stripMargin } property("complicated let in let and function call in block") { @@ -136,16 +161,12 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { Terms.BLOCK(Terms.LET("v", CONST_LONG(1)), Terms.FUNCTION_CALL(function = FunctionHeader.Native(100), args = List(REF("v"), CONST_LONG(2))))), Terms.FUNCTION_CALL(function = FunctionHeader.Native(100), args = List(REF("p"), CONST_LONG(3))) ) - Decompiler(expr, decompilerContext) shouldBe - """{ - | let p = - | { - | let v = - | 1; - | (v + 2) - | }; - | (p + 3) - |}""".stripMargin + Decompiler(expr, decompilerContext) shouldEq + """let p = { + | let v = 1 + | (v + 2) + | } + |(p + 3)""".stripMargin } property("old match") { @@ -161,101 +182,31 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { FALSE ) ) - Decompiler(expr, decompilerContext) shouldBe - """{ - | let v = - | 1; - | { - | if ( - | { - | if ( - | (v + 2) - | ) - | then - | true - | else - | (v + 3) - | } - | ) - | then - | { - | let p = - | v; - | true - | } - | else - | false - | } - |}""".stripMargin + Decompiler(expr, decompilerContext) shouldEq + """let v = 1 + |if (if ((v + 2)) + | then true + | else (v + 3)) + | then { + | let p = v + | true + | } + | else false""".stripMargin } - property("new match") { - val expr = Terms.BLOCK( - Terms.LET("v", CONST_LONG(1)), - Terms.IF( - Terms.IF( - Terms.FUNCTION_CALL(function = FunctionHeader.Native(100), args = List(REF("v"), CONST_LONG(2))), - TRUE, - Terms.FUNCTION_CALL(function = FunctionHeader.Native(100), args = List(REF("v"), CONST_LONG(3))) - ), - Terms.BLOCK(Terms.LET("z", CONST_LONG(4)), TRUE), - FALSE - ) - ) - Decompiler(expr, decompilerContext) shouldBe - """{ - | let v = - | 1; - | { - | if ( - | { - | if ( - | (v + 2) - | ) - | then - | true - | else - | (v + 3) - | } - | ) - | then - | { - | let z = - | 4; - | true - | } - | else - | false - | } - |}""".stripMargin + property("ref getter idents") { + val expr = GETTER(REF("a"), "foo") + Decompiler(expr, decompilerContext) shouldEq + """a.foo""".stripMargin } - property("Invoke contract compilation") { - val scriptText = - """ - | @Callable(i) - | func testfunc(amount: Int) = { - | let pmt = 1 - | - | if (false) - | then - | throw("impossible") - | else { - | ContractResult( - | WriteSet(List(DataEntry("1", "1"))), - | TransferSet(List(ContractTransfer(i.caller, amount, unit))) - | ) - | } - | } - """.stripMargin - val parsedScript = com.zbsnetwork.lang.v1.parser.Parser.parseContract(scriptText).get.value - - val ctx = Monoid.combine(compilerContext, ZbsContext.build(StdLibVersion.V3, Common.emptyBlockchainEnvironment(), false).compilerContext) - val compledContract = compiler.ContractCompiler(ctx, parsedScript) - - compledContract.getOrElse("error").toString shouldBe - """Contract(List(),List(CallableFunction(CallableAnnotation(i),FUNC(testfunc,List(amount),BLOCK(LET(pmt,CONST_LONG(1)),IF(FALSE,FUNCTION_CALL(Native(2),List(CONST_STRING(impossible))),FUNCTION_CALL(User(ContractResult),List(FUNCTION_CALL(User(WriteSet),List(FUNCTION_CALL(Native(1101),List(FUNCTION_CALL(User(DataEntry),List(CONST_STRING(1), CONST_STRING(1))))))), FUNCTION_CALL(User(TransferSet),List(FUNCTION_CALL(Native(1101),List(FUNCTION_CALL(User(ContractTransfer),List(GETTER(REF(i),caller), REF(amount), REF(unit)))))))))))))),None)""" - + property("block getter idents") { + val expr = GETTER(BLOCK(LET("a", FALSE), REF("a")), "foo") + Decompiler(expr, decompilerContext) shouldEq + """{ + | let a = false + | a + | }.foo""".stripMargin } property("Invoke contract with verifier decompilation") { @@ -274,17 +225,21 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { FUNCTION_CALL( User("WriteSet"), List(FUNCTION_CALL( - Native(1102), - List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("b"), CONST_LONG(1))), - FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("x")))) + Native(1100), + List( + FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("b"), CONST_LONG(1))), + FUNCTION_CALL(Native(1100), List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("x"))), REF("nil"))) + ) )) ), FUNCTION_CALL( User("WriteSet"), List(FUNCTION_CALL( - Native(1102), - List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("a"), REF("a"))), - FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("x")))) + Native(1100), + List( + FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("a"), REF("a"))), + FUNCTION_CALL(Native(1100), List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), REF("x"))), REF("nil"))) + ) )) ) ) @@ -293,46 +248,27 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { )), Some(VerifierFunction(VerifierAnnotation("t"), FUNC("verify", List(), TRUE))) ) - Decompiler(contract: Contract, decompilerContext) shouldBe - """func foo () = { - | false - |} - | - | - |func bar () = { - | { - | if ( - | foo() - | ) - | then - | true - | else - | false - | } - |} - | - |@Callable(invocation) - |func baz (a) = { - | { - | let x = - | invocation.caller.bytes; - | { - | if ( - | foo() - | ) - | then - | WriteSet(List(DataEntry("b", 1), DataEntry("sender", x))) - | else - | WriteSet(List(DataEntry("a", a), DataEntry("sender", x))) - | } - | } - |} - | - |@Verifier(t) - |func verify () = { - | true - |} - |""".stripMargin + Decompiler(contract: Contract, decompilerContext) shouldEq + """|func foo () = false + | + | + |func bar () = if (foo()) + | then true + | else false + | + | + |@Callable(invocation) + |func baz (a) = { + | let x = invocation.caller.bytes + | if (foo()) + | then WriteSet(cons(DataEntry("b", 1), cons(DataEntry("sender", x), nil))) + | else WriteSet(cons(DataEntry("a", a), cons(DataEntry("sender", x), nil))) + | } + | + | + |@Verifier(t) + |func verify () = true + |""".stripMargin } property("Invoke contract decompilation") { @@ -346,62 +282,33 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { List("amount"), BLOCK( LET("pmt", CONST_LONG(1)), - IF( - FALSE, - FUNCTION_CALL(Native(2), List(CONST_STRING("impossible"))), - FUNCTION_CALL( - User("ContractResult"), - List( - FUNCTION_CALL( - User("WriteSet"), - List(FUNCTION_CALL(Native(1101), List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("1"), CONST_STRING("1"))))))), - FUNCTION_CALL( - User("TransferSet"), - List(FUNCTION_CALL(Native(1101), - List(FUNCTION_CALL(User("ContractTransfer"), List(GETTER(REF("i"), "caller"), REF("amount"), REF("unit")))))) - ) - ) - ) - ) + TRUE ) ) )), None ) - Decompiler(contract: Contract, decompilerContext) shouldBe - """func foo (bar,buz) = { - | true - |} + Decompiler(contract, decompilerContext) shouldEq + """func foo (bar,buz) = true + | | |@Callable(i) |func testfunc (amount) = { - | { - | let pmt = - | 1; - | { - | if ( - | false - | ) - | then - | throw("impossible") - | else - | ContractResult(WriteSet(List(DataEntry("1", "1"))), TransferSet(List(ContractTransfer(i.caller, amount, unit)))) - | } + | let pmt = 1 + | true | } - |} + | |""".stripMargin + } property("bytestring") { val test = Base58.encode("abc".getBytes("UTF-8")) // ([REVIEW]: may be i`am make a mistake here) val expr = Terms.BLOCK(Terms.LET("param", CONST_BYTESTR(ByteStr(test.getBytes()))), REF("param")) - Decompiler(expr, decompilerContext) shouldBe - """{ - | let param = - | base58'3K3F4C'; - | param - |}""".stripMargin + Decompiler(expr, decompilerContext) shouldEq + """let param = base58'3K3F4C' + |param""".stripMargin } property("getter") { @@ -410,66 +317,36 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { args = List(TRUE) ), "testfield") - Decompiler(expr, decompilerContext) shouldBe + Decompiler(expr, decompilerContext) shouldEq """testfunc(true).testfield""".stripMargin } property("simple if") { val expr = IF(TRUE, CONST_LONG(1), CONST_STRING("XXX")) - Decompiler(expr, decompilerContext) shouldBe - """{ - | if ( - | true - | ) - | then - | 1 - | else - | "XXX" - |}""".stripMargin + Decompiler(expr, decompilerContext) shouldEq + """if (true) + | then 1 + | else "XXX"""".stripMargin } property("if with complicated else branch") { val expr = IF(TRUE, CONST_LONG(1), IF(TRUE, CONST_LONG(1), CONST_STRING("XXX"))) - Decompiler(expr, decompilerContext) shouldBe - """{ - | if ( - | true - | ) - | then - | 1 - | else - | { - | if ( - | true - | ) - | then - | 1 - | else - | "XXX" - | } - |}""".stripMargin + Decompiler(expr, decompilerContext) shouldEq + """if (true) + | then 1 + | else if (true) + | then 1 + | else "XXX"""".stripMargin } property("if with complicated then branch") { val expr = IF(TRUE, IF(TRUE, CONST_LONG(1), CONST_STRING("XXX")), CONST_LONG(1)) - Decompiler(expr, decompilerContext) shouldBe - """{ - | if ( - | true - | ) - | then - | { - | if ( - | true - | ) - | then - | 1 - | else - | "XXX" - | } - | else - | 1 - |}""".stripMargin + Decompiler(expr, decompilerContext) shouldEq + """if (true) + | then if (true) + | then 1 + | else "XXX" + | else 1""".stripMargin } property("Surge smart accet") { @@ -535,80 +412,30 @@ class DecompilerTest extends PropSpec with PropertyChecks with Matchers { ) ) ) - Decompiler(expr, decompilerContext) shouldBe - """{ - | let startHeight = - | 1375557; - | { - | let startPrice = - | 100000; - | { - | let interval = - | (24 * 60); - | { - | let exp = - | ((100 * 60) * 1000); - | { - | let $match0 = - | tx; - | { - | if ( - | _isInstanceOf($match0, "ExchangeTransaction") - | ) - | then - | { - | let e = - | $match0; - | { - | let days = - | ((height - startHeight) / interval); - | { - | if ( - | { - | if ( - | { - | if ( - | (e.price >= (startPrice * (1 + (days * days)))) - | ) - | then - | !(isDefined(e.sellOrder.assetPair.priceAsset)) - | else - | false - | } - | ) - | then - | (exp >= (e.sellOrder.expiration - e.sellOrder.timestamp)) - | else - | false - | } - | ) - | then - | (exp >= (e.buyOrder.expiration - e.buyOrder.timestamp)) - | else - | false - | } - | } - | } - | else - | { - | if ( - | _isInstanceOf($match0, "BurnTransaction") - | ) - | then - | { - | let tx = - | $match0; - | true - | } - | else - | false - | } - | } - | } - | } + Decompiler(expr, decompilerContext) shouldEq + """let startHeight = 1375557 + |let startPrice = 100000 + |let interval = (24 * 60) + |let exp = ((100 * 60) * 1000) + |let $match0 = tx + |if (_isInstanceOf($match0, "ExchangeTransaction")) + | then { + | let e = $match0 + | let days = ((height - startHeight) / interval) + | if (if (if ((e.price >= (startPrice * (1 + (days * days))))) + | then !(isDefined(e.sellOrder.assetPair.priceAsset)) + | else false) + | then (exp >= (e.sellOrder.expiration - e.sellOrder.timestamp)) + | else false) + | then (exp >= (e.buyOrder.expiration - e.buyOrder.timestamp)) + | else false | } - | } - |}""".stripMargin + | else if (_isInstanceOf($match0, "BurnTransaction")) + | then { + | let tx = $match0 + | true + | } + | else false""".stripMargin } } diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ErrorTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ErrorTest.scala index 49d8768..5ab4573 100644 --- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ErrorTest.scala +++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ErrorTest.scala @@ -15,11 +15,6 @@ class ErrorTest extends PropSpec with PropertyChecks with Matchers with ScriptGe import com.zbsnetwork.lang.v1.parser.Expressions._ errorTests( - "can't define LET with the same name as already defined in scope" -> "already defined in the scope" -> BLOCK( - AnyPos, - LET(AnyPos, PART.VALID(AnyPos, "X"), CONST_LONG(AnyPos, 1), Seq.empty), - BLOCK(AnyPos, LET(AnyPos, PART.VALID(AnyPos, "X"), CONST_LONG(AnyPos, 2), Seq.empty), TRUE(AnyPos)) - ), "can't define LET with the same name as predefined constant" -> "already defined in the scope" -> BLOCK( AnyPos, LET(AnyPos, PART.VALID(AnyPos, "unit"), CONST_LONG(AnyPos, 2), Seq.empty), diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ExpressionCompilerV1Test.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ExpressionCompilerV1Test.scala index 92fc87f..2adc715 100644 --- a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ExpressionCompilerV1Test.scala +++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/ExpressionCompilerV1Test.scala @@ -7,7 +7,7 @@ import com.zbsnetwork.lang.v1.compiler.Terms._ import com.zbsnetwork.lang.v1.compiler.Types._ import com.zbsnetwork.lang.v1.compiler.{CompilerContext, ExpressionCompiler} import com.zbsnetwork.lang.v1.evaluator.ctx.impl.PureContext._ -import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{PureContext, _} +import com.zbsnetwork.lang.v1.evaluator.ctx.impl.PureContext import com.zbsnetwork.lang.v1.parser.BinaryOperation.SUM_OP import com.zbsnetwork.lang.v1.parser.Expressions.Pos import com.zbsnetwork.lang.v1.parser.Expressions.Pos.AnyPos @@ -140,7 +140,7 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher ) ), expectedResult = Right( - (BLOCK(LET("a", IF(TRUE, CONST_LONG(1), CONST_STRING(""))), FUNCTION_CALL(PureContext.eq.header, List(REF("a"), CONST_LONG(3)))), BOOLEAN)) + (LET_BLOCK(LET("a", IF(TRUE, CONST_LONG(1), CONST_STRING(""))), FUNCTION_CALL(PureContext.eq.header, List(REF("a"), CONST_LONG(3)))), BOOLEAN)) ) treeTypeTest("idOptionLong(())")( @@ -174,7 +174,7 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher ) ), expectedResult = Right( - (BLOCK( + (LET_BLOCK( LET("$match0", REF("p")), IF( IF( @@ -188,7 +188,7 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher List(REF("$match0"), CONST_STRING("PointA")) ) ), - BLOCK(LET("p", REF("$match0")), TRUE), + LET_BLOCK(LET("p", REF("$match0")), TRUE), FALSE ) ), @@ -215,9 +215,10 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher }, expectedResult = { Right( - (BLOCK( - LET("a", BLOCK(LET("$match0", REF("p")), BLOCK(LET("$match1", REF("p")), CONST_LONG(1)))), - BLOCK(LET("b", BLOCK(LET("$match0", REF("p")), CONST_LONG(2))), FUNCTION_CALL(FunctionHeader.Native(100), List(REF("a"), REF("b")))) + (LET_BLOCK( + LET("a", LET_BLOCK(LET("$match0", REF("p")), LET_BLOCK(LET("$match1", REF("p")), CONST_LONG(1)))), + LET_BLOCK(LET("b", LET_BLOCK(LET("$match0", REF("p")), CONST_LONG(2))), + FUNCTION_CALL(FunctionHeader.Native(100), List(REF("a"), REF("b")))) ), LONG)) @@ -303,7 +304,7 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher ) ), expectedResult = Left( - "Compilation failed: Value 'p1' declared as non-existing type, while all possible types are List(Point, PointB, Boolean, Int, PointA, ByteStr, Unit, String) in -1--1") + "Compilation failed: Value 'p1' declared as non-existing type, while all possible types are List(Point, PointB, Boolean, Int, PointA, ByteVector, Unit, String) in -1--1") ) treeTypeTest("Invalid LET")( @@ -405,6 +406,33 @@ class ExpressionCompilerV1Test extends PropSpec with PropertyChecks with Matcher LONG)) ) + treeTypeTest("union type inferrer with list")( + ctx = compilerContext, + expr = { + val script = """[1,""]""" + Parser.parseExpr(script).get.value + }, + expectedResult = { + Right( + (FUNCTION_CALL( + FunctionHeader.Native(1100), + List( + CONST_LONG(1), + FUNCTION_CALL( + FunctionHeader.Native(1100), + List( + CONST_STRING(""), + REF("nil") + ) + ) + ) + ), + LIST(UNION(List(LONG, STRING)))) + ) + + } + ) + private def treeTypeTest(propertyName: String)(expr: Expressions.EXPR, expectedResult: Either[String, (EXPR, TYPE)], ctx: CompilerContext): Unit = property(propertyName) { compiler.ExpressionCompiler(ctx, expr) shouldBe expectedResult diff --git a/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/names/NameDuplicationTest.scala b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/names/NameDuplicationTest.scala new file mode 100644 index 0000000..14788b8 --- /dev/null +++ b/lang/jvm/src/test/scala/com/zbsnetwork/lang/compiler/names/NameDuplicationTest.scala @@ -0,0 +1,204 @@ +package com.zbsnetwork.lang.compiler.names +import cats.kernel.Monoid +import com.zbsnetwork.lang.Common.{NoShrink, produce} +import com.zbsnetwork.lang.compiler.compilerContext +import com.zbsnetwork.lang.contract.Contract +import com.zbsnetwork.lang.v1.compiler +import com.zbsnetwork.lang.v1.compiler.CompilerContext +import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext +import com.zbsnetwork.lang.v1.parser.Parser +import com.zbsnetwork.lang.v1.testing.ScriptGen +import com.zbsnetwork.lang.{Common, StdLibVersion} +import org.scalatest.prop.PropertyChecks +import org.scalatest.{FreeSpec, Matchers} + +class NameDuplicationTest extends FreeSpec with PropertyChecks with Matchers with ScriptGen with NoShrink { + + val ctx: CompilerContext = + Monoid.combine(compilerContext, ZbsContext.build(StdLibVersion.V3, Common.emptyBlockchainEnvironment(), isTokenContext = false).compilerContext) + + "Contract compilation" - { + + "should succeed when" - { + "these have the same name:" - { + + "constant and user function argument" in { + compileOf(""" + |let x = 42 + | + |func some(y: Boolean, x: Boolean) = !x + |""") shouldBe 'right + } + + "constant and callable function argument" in { + compileOf(""" + |let x = 42 + | + |@Callable(i) + |func some(a: Int, x: String) = WriteSet([DataEntry("a", x)]) + |""") shouldBe 'right + } + + "user function and its argument" in { + compileOf(""" + |func sameName(sameName: Boolean) = !sameName + |""") shouldBe 'right + } + + "user function and argument; callable annotation bindings and arguments" in { + compileOf(""" + |func i(i: Int) = i + 1 + | + |@Callable(x) + |func foo(i: Int) = WriteSet([DataEntry("a", i + 1)]) + | + |@Callable(i) + |func bar(x: Int) = WriteSet([DataEntry("a", i.contractAddress.bytes)]) + |""") shouldBe 'right + } + + } + } + + "should fail when" - { + "these have the same name:" - { + + "two constants" in { + compileOf(""" + |let x = 42 + |let x = true + |""") should produce("already defined") + } + + "constant and user function" in { + compileOf(""" + |let x = 42 + | + |func x() = true + |""") should produce("already defined") + } + + "constant and callable function" in { + compileOf(""" + |let x = 42 + | + |@Callable(i) + |func x() = WriteSet([DataEntry("a", "a")]) + |""") should produce("already defined") + } + + "constant and verifier function" in { + compileOf(""" + |let x = 42 + | + |@Verifier(i) + |func x() = WriteSet([DataEntry("a", "a")]) + |""") should produce("already defined") + } + + "constant and callable annotation binding" in { + compileOf(""" + |let x = 42 + | + |@Callable(x) + |func some(i: Int) = WriteSet([DataEntry("a", "a")]) + |""") should produce("Annotation bindings overrides already defined var") + } + + "constant and verifier annotation binding" in { + compileOf(""" + |let x = 42 + | + |@Verifier(x) + |func some() = true + |""") should produce("Annotation bindings overrides already defined var") + } + + "two user functions" in { + compileOf(""" + |func sameName() = true + | + |func sameName() = 1 + |""") should produce("already defined") + } + + "two user function arguments" in { + compileOf(""" + |func some(sameName: String, sameName: Int) = sameName + |""") should produce("declared with duplicating argument names") + } + + "user and callable functions" in { + compileOf(""" + |func sameName() = true + | + |@Callable(i) + |func sameName() = WriteSet([DataEntry("a", "a")]) + |""") should produce("already defined") + } + + "user and verifier functions" in { + compileOf(""" + |func sameName() = true + | + |@Verifier(i) + |func sameName() = true + |""") should produce("already defined") + } + + "two callable functions" in { + compileOf(""" + |@Callable(i) + |func sameName() = WriteSet([DataEntry("a", "a")]) + | + |@Callable(i) + |func sameName() = WriteSet([DataEntry("b", "b")]) + |""") should produce("already defined") + } + + "two callable function arguments" in { + compileOf(""" + |@Callable(i) + |func some(sameName: String, sameName: Int) = WriteSet([DataEntry("b", sameName)]) + |""") should produce("declared with duplicating argument names") + } + + "callable and verifier functions" in { + compileOf(""" + |@Callable(i) + |func sameName() = WriteSet([DataEntry("a", "a")]) + | + |@Verifier(i) + |func sameName() = true + |""") should produce("already defined") + } + + "callable function and its callable annotation binding" in { + compileOf(""" + |@Callable(sameName) + |func sameName() = WriteSet([DataEntry("a", sameName.contractAddress.bytes)]) + |""") shouldBe 'right + } + + "callable annotation binding and its function argument" in { + compileOf(""" + |@Callable(i) + |func some(s: String, i: Int) = + | if (i.contractAddress == "abc") then + | WriteSet([DataEntry("a", "a")]) + | else + | WriteSet([DataEntry("a", "b")]) + |""") should produce("override annotation bindings") + } + + } + } + + } + + def compileOf(script: String): Either[String, Contract] = { + val expr = Parser.parseContract(script.stripMargin).get.value + compiler.ContractCompiler(ctx, expr) + } + +} diff --git a/lang/shared/src/main/resources/fomo.ride b/lang/shared/src/main/resources/fomo.ride index e3257ab..665ffdf 100644 --- a/lang/shared/src/main/resources/fomo.ride +++ b/lang/shared/src/main/resources/fomo.ride @@ -1,9 +1,13 @@ +{-# STDLIB_VERSION 3 #-} +{-# CONTENT_TYPE CONTRACT -#} + +let lpKey = "lastPayment" +let liKey = "bestFomoer" +let lhKey = "height" +let day = 1440 + @Callable(i) func fearmissing() = { - let lpKey = "lastPayment" - let liKey = "bestFomoer" - let lhKey = "height" - let payment = match i.payment { case p:AttachedPayment => match p.asset { @@ -21,25 +25,20 @@ func fearmissing() = { if(payment <= lastPayment) then throw("min payment is " +toString(payment)) else # storing best payment, caller and height - WriteSet(List( + WriteSet([ DataEntry(lpKey, payment), DataEntry(liKey, i.caller.bytes), DataEntry(lhKey, height) - )) + ]) } @Callable(i) -func withdraw(amount: Int) = { - - let day = 1440 - let liKey = "bestFomoer" - let lhKey = "height" - +func withdraw() = { let callerCorrect = i.caller.bytes == extract(getBinary(i.contractAddress, liKey)) let heightCorrect = extract(getInteger(i.contractAddress, lhKey)) - height >= day let canWithdraw = heightCorrect && callerCorrect if (canWithdraw) - then TransferSet(List(ContractTransfer(i.caller, zbsBalance(i.contractAddress), unit))) + then TransferSet([ContractTransfer(i.caller, zbsBalance(i.contractAddress), unit)]) else throw("behold") } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/StdLibVersion.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/StdLibVersion.scala index 4e1c236..31bfc0e 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/StdLibVersion.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/StdLibVersion.scala @@ -18,15 +18,15 @@ object StdLibVersion extends TaggedType[Int] { } } -object ScriptType extends TaggedType[Int] { - type ScriptType = ScriptType.Type +object ContentType extends TaggedType[Int] { + type ContentType = ContentType.Type - val Expression: ScriptType = 1 @@ ScriptType - val Contract: ScriptType = 2 @@ ScriptType + val Expression: ContentType = 1 @@ ContentType + val Contract: ContentType = 2 @@ ContentType - val SupportedVersions: Set[ScriptType] = Set(Expression, Contract) + val SupportedTypes: Set[ContentType] = Set(Expression, Contract) - def parseVersion(i: Int) = i match { + def parseId(i: Int) = i match { case 1 => Expression case 2 => Contract } @@ -36,3 +36,23 @@ object ScriptType extends TaggedType[Int] { case "CONTRACT" => Contract } } + +object ScriptType extends TaggedType[Int] { + type ScriptType = ScriptType.Type + + val Account: ScriptType = 1 @@ ScriptType + val Asset: ScriptType = 2 @@ ScriptType + + val SupportedTypes: Set[ScriptType] = Set(Account, Asset) + + def parseId(i: Int) = i match { + case 1 => Account + case 2 => Asset + } + + def parseString(s: String) = s match { + case "ACCOUNT" => Account + case "ASSET" => Asset + } +} + diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/directives/DirectiveKey.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/directives/DirectiveKey.scala index 2259c85..c9d8298 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/directives/DirectiveKey.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/directives/DirectiveKey.scala @@ -3,11 +3,13 @@ package com.zbsnetwork.lang.directives sealed trait DirectiveKey object DirectiveKey { final case object STDLIB_VERSION extends DirectiveKey + final case object CONTENT_TYPE extends DirectiveKey final case object SCRIPT_TYPE extends DirectiveKey val dictionary = Map( "STDLIB_VERSION" -> DirectiveKey.STDLIB_VERSION, + "CONTENT_TYPE" -> DirectiveKey.CONTENT_TYPE, "SCRIPT_TYPE" -> DirectiveKey.SCRIPT_TYPE ) } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/utils/package.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/utils/package.scala index e0382ab..bf2947c 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/utils/package.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/utils/package.scala @@ -2,6 +2,7 @@ package com.zbsnetwork.lang import cats.implicits._ import com.zbsnetwork.lang.ScriptType.ScriptType +import com.zbsnetwork.lang.ContentType.ContentType import com.zbsnetwork.lang.StdLibVersion.StdLibVersion import com.zbsnetwork.lang.directives.{Directive, DirectiveKey} @@ -28,23 +29,41 @@ package object utils { .getOrElse(StdLibVersion.V2.asRight) } + def extractContentType(directives: List[Directive]): Either[String, ContentType] = { + directives + .find(_.key == DirectiveKey.CONTENT_TYPE) + .map(d => + Try(d.value) match { + case Success(v) => + val cType = ContentType.parseString(v) + Either + .cond( + ContentType.SupportedTypes(cType), + cType, + "Unsupported content type" + ) + case Failure(ex) => + Left("Can't parse content type") + }) + .getOrElse(ContentType.Expression.asRight) + } + def extractScriptType(directives: List[Directive]): Either[String, ScriptType] = { directives .find(_.key == DirectiveKey.SCRIPT_TYPE) .map(d => Try(d.value) match { case Success(v) => - val ver = ScriptType.parseString(v) + val sType = ScriptType.parseString(v) Either .cond( - ScriptType.SupportedVersions(ver), - ver, + ScriptType.SupportedTypes(sType), + sType, "Unsupported script type" ) case Failure(ex) => Left("Can't parse script type") - }) - .getOrElse(ScriptType.Expression.asRight) + }) + .getOrElse(ScriptType.Account.asRight) } - } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/CTX.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/CTX.scala index 7ff40a2..a81a550 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/CTX.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/CTX.scala @@ -29,7 +29,13 @@ case class CTX(@(JSExport @field) types: Seq[DefinedType], functionDefs = functions.groupBy(_.name).map { case (k, v) => k -> v.map(_.signature).toList } ) - val opsNames = BinaryOperation.opsByPriority.flatten.map(x => BinaryOperation.opsToFunctions(x)).toSet + val opsNames = BinaryOperation.opsByPriority + .flatMap({ + case Left(l) => l + case Right(l) => l + }) + .map(x => BinaryOperation.opsToFunctions(x)) + .toSet lazy val decompilerContext: DecompilerContext = DecompilerContext( opCodes = compilerContext.functionDefs @@ -39,12 +45,14 @@ case class CTX(@(JSExport @field) types: Seq[DefinedType], .toMap, binaryOps = compilerContext.functionDefs .filterKeys(opsNames(_)) - .mapValues(_.map(_.header) - .filter(_.isInstanceOf[Native]) - .map(_.asInstanceOf[Native].name)) + .mapValues( + _.map(_.header) + .filter(_.isInstanceOf[Native]) + .map(_.asInstanceOf[Native].name)) .toList .flatMap { case (name, codes) => codes.map((_, name)) } - .toMap + .toMap, + ident = 0 ) } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/CompilationError.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/CompilationError.scala index b77e9e0..62926f5 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/CompilationError.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/CompilationError.scala @@ -49,7 +49,7 @@ object CompilationError { } final case class BadFunctionSignatureSameArgNames(start: Int, end: Int, name: String) extends CompilationError { - val message = s"Function'$name' declared with duplicating argument names" + val message = s"Function '$name' declared with duplicating argument names" } final case class FunctionNotFound(start: Int, end: Int, name: String, argTypes: List[String]) extends CompilationError { diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractCompiler.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractCompiler.scala index d09004d..8f2f862 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractCompiler.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractCompiler.scala @@ -3,7 +3,7 @@ import cats.Show import cats.implicits._ import com.zbsnetwork.lang.contract.Contract import com.zbsnetwork.lang.contract.Contract._ -import com.zbsnetwork.lang.v1.compiler.CompilationError.Generic +import com.zbsnetwork.lang.v1.compiler.CompilationError.{AlreadyDefined, Generic} import com.zbsnetwork.lang.v1.compiler.CompilerContext.vars import com.zbsnetwork.lang.v1.compiler.ExpressionCompiler._ import com.zbsnetwork.lang.v1.compiler.Terms.DECLARATION @@ -11,10 +11,9 @@ import com.zbsnetwork.lang.v1.compiler.Types.{BOOLEAN, UNION} import com.zbsnetwork.lang.v1.evaluator.ctx.FunctionTypeSignature import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.{FieldNames, ZbsContext} import com.zbsnetwork.lang.v1.parser.Expressions.FUNC -import com.zbsnetwork.lang.v1.parser.Expressions.Pos.AnyPos import com.zbsnetwork.lang.v1.parser.{Expressions, Parser} import com.zbsnetwork.lang.v1.task.imports._ -import com.zbsnetwork.lang.v1.{FunctionHeader, compiler} +import com.zbsnetwork.lang.v1.{ContractLimits, FunctionHeader, compiler} object ContractCompiler { def compileAnnotatedFunc(af: Expressions.ANNOTATEDFUNC): CompileM[AnnotatedFunction] = { @@ -31,7 +30,7 @@ object ContractCompiler { compiledBody <- local { for { _ <- modify[CompilerContext, CompilationError](vars.modify(_)(_ ++ annotationBindings)) - r <- compiler.ExpressionCompiler.compileFunc(AnyPos, af.f) + r <- compiler.ExpressionCompiler.compileFunc(af.f.position, af.f, annotationBindings.map(_._1)) } yield r } } yield (annotations, compiledBody) @@ -89,11 +88,22 @@ object ContractCompiler { ds <- contract.decs.traverse[CompileM, DECLARATION](compileDeclaration) _ <- validateDuplicateVarsInContract(contract) l <- contract.fs.traverse[CompileM, AnnotatedFunction](af => local(compileAnnotatedFunc(af))) + duplicatedFuncNames = l.map(_.u.name).groupBy(identity).collect { case (x, List(_, _, _*)) => x }.toList _ <- Either .cond( - l.map(_.u.name).toSet.size == l.size, + duplicatedFuncNames.isEmpty, (), - Generic(contract.position.start, contract.position.start, "Contract functions must have unique names") + AlreadyDefined(contract.position.start, contract.position.start, duplicatedFuncNames.mkString(", "), isFunction = true) + ) + .toCompileM + + _ <- Either + .cond( + l.forall(_.u.args.size <= ContractLimits.MaxContractInvocationArgs), + (), + Generic(contract.position.start, + contract.position.end, + s"Contract functions can have no more than ${ContractLimits.MaxContractInvocationArgs} arguments") ) .toCompileM verifierFunctions = l.filter(_.isInstanceOf[VerifierFunction]).map(_.asInstanceOf[VerifierFunction]) diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractLimits.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractLimits.scala new file mode 100644 index 0000000..4551774 --- /dev/null +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ContractLimits.scala @@ -0,0 +1,19 @@ +package com.zbsnetwork.lang.v1 + +object ContractLimits { + val MaxExprComplexity = 20 * 100 + val MaxExprSizeInBytes = 8 * 1024 + + val MaxContractComplexity = 20 * 100 + val MaxContractSizeInBytes = 32 * 1024 + + // As in Scala + val MaxContractInvocationArgs = 22 + + // Data 0.001 per kilobyte, rounded up, fee for CI is 0.005 + val MaxContractInvocationSizeInBytes = 5 * 1024 + val MaxWriteSetSizeInBytes = 5 * 1024 + + // Mass Transfer 0.001 + 0.0005*N, rounded up to 0.001, fee for CI is 0.005 + val MaxPaymentAmount = 10 +} diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Decompiler.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Decompiler.scala index 593ef4c..9e65453 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Decompiler.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Decompiler.scala @@ -1,150 +1,134 @@ package com.zbsnetwork.lang.v1.compiler +import cats.implicits._ import com.zbsnetwork.lang.contract.Contract import com.zbsnetwork.lang.contract.Contract.{CallableFunction, VerifierFunction} import com.zbsnetwork.lang.v1.FunctionHeader import com.zbsnetwork.lang.v1.compiler.Terms._ +import monix.eval.Coeval object Decompiler { - case class Delay(seq: Seq[Either[String, Function[Unit, Delay]]]) - - private def show(d: Delay): String = { - var w = d.seq - val s = new StringBuffer() - while (w.nonEmpty) { - w.head match { - case Left(o) => - s.append(o) - w = w.tail - case Right(f) => - w = f(()).seq ++ w.tail - } - } - s.toString - } + sealed trait BlockBraces + case object NoBraces extends BlockBraces + case object BracesWhenNeccessary extends BlockBraces + + sealed trait FirstLinePolicy + case object DontIndentFirstLine extends FirstLinePolicy + case object IdentFirstLine extends FirstLinePolicy + + private[lang] def pure[A](a: A) = Coeval.evalOnce(a) private def out(in: String, ident: Int): String = Array.fill(4 * ident)(" ").mkString("") + in - private def decl(e: DECLARATION, ident: Int, ctx: DecompilerContext): String = - e match { + private def pureOut(in: String, ident: Int): Coeval[String] = pure(out(in, ident)) + + private val NEWLINE = scala.util.Properties.lineSeparator + + private def decl(e: Coeval[DECLARATION], ctx: DecompilerContext): Coeval[String] = + e flatMap { case Terms.FUNC(name, args, body) => - out("func " + name + " (" + args.map(_.toString).mkString(","), ident) + ") = {\n" + - show(expr(body, 1 + ident, ctx)) + "\n" + - out("}\n", ident) + expr(pure(body), ctx, BracesWhenNeccessary, DontIndentFirstLine).map( + fb => + out("func " + name + " (" + args.mkString(",") + ") = ", ctx.ident) + + out(fb + NEWLINE, ctx.ident)) case Terms.LET(name, value) => - out("let " + name + " =\n", 0 + ident) + - show(expr(value, 1 + ident, ctx)) - } - - private def argso(args: List[Terms.EXPR], ctx: DecompilerContext): Seq[Either[String, Function[Unit, Delay]]] = { - args.foldLeft(Seq[Either[String, Function[Unit, Delay]]]()) { (alfa, beta) => - val gamma = Right((_: Unit) => expr(beta, 0, ctx)) - if (alfa.isEmpty) Seq(gamma) - else alfa ++ Seq(Left(", "), gamma) + expr(pure(value), ctx, BracesWhenNeccessary, DontIndentFirstLine).map(e => out("let " + name + " = " + e, ctx.ident)) } - } - private def expr(e: EXPR, ident: Int, ctx: DecompilerContext): Delay = - e match { - case Terms.TRUE => Delay(Seq(Left(out("true", ident)))) - case Terms.FALSE => Delay(Seq(Left(out("false", ident)))) - case Terms.CONST_BOOLEAN(b) => Delay(Seq(Left(out(b.toString.toLowerCase(), ident)))) - case Terms.IF(cond, it, iff) => - Delay( - Seq( - Left(out("{\n", 0 + ident)), - Left(out("if (\n", 1 + ident)), - Right(_ => expr(cond, 2 + ident, ctx)), - Left("\n"), - Left(out(")\n", 1 + ident)), - Left(out("then\n", 1 + ident)), - Right(_ => expr(it, 2 + ident, ctx)), - Left("\n"), - Left(out("else\n", 1 + ident)), - Right(_ => expr(iff, 2 + ident, ctx)), - Left("\n"), - Left(out("}", 0 + ident)) - )) - case Terms.CONST_LONG(t) => Delay(Seq(Left(out(t.toLong.toString, ident)))) - case Terms.CONST_STRING(s) => Delay(Seq(Left(out('"' + s + '"', ident)))) - case Terms.LET_BLOCK(let, exprPar) => - Delay( - Seq( - Left(out("{ let " + let.name + " = ", ident)), - Right(_ => expr(let.value, 0, ctx)), - Left(out("; ", 0)), - Right(_ => expr(exprPar, 0, ctx)), - Left(out(" }", 0)) - )) + private[lang] def expr(e: Coeval[EXPR], ctx: DecompilerContext, braces: BlockBraces, firstLinePolicy: FirstLinePolicy): Coeval[String] = { + val i = if (braces == BracesWhenNeccessary) 0 else ctx.ident + e flatMap { case Terms.BLOCK(declPar, body) => - Delay( - Seq( - Left(out("{\n", ident) + decl(declPar, 1 + ident, ctx) + ";\n"), - Right(_ => expr(body, 1 + ident, ctx)), - Left(out("\n", 0)), - Left(out("}", ident)) - )) - case Terms.CONST_BYTESTR(bs) => Delay(Seq(Left(out("base58'" + bs.base58 + "'", ident)))) + val braceThis = braces match { + case NoBraces => false + case BracesWhenNeccessary => true + } + val modifiedCtx = if (braceThis) ctx.incrementIdent() else ctx + for { + d <- decl(pure(declPar), modifiedCtx) + b <- expr(pure(body), modifiedCtx, NoBraces, IdentFirstLine) + } yield { + if (braceThis) + out("{" + NEWLINE, ident = 0) + + out(d + NEWLINE, 0) + + out(b + NEWLINE, 0) + + out("}", ctx.ident + 1) + else + out(d + NEWLINE, 0) + + out(b, 0) + } + case Terms.LET_BLOCK(let, exprPar) => expr(pure(Terms.BLOCK(let, exprPar)), ctx, braces, firstLinePolicy) + case Terms.TRUE => pureOut("true", i) + case Terms.FALSE => pureOut("false", i) + case Terms.CONST_BOOLEAN(b) => pureOut(b.toString.toLowerCase(), i) + case Terms.CONST_LONG(t) => pureOut(t.toLong.toString, i) + case Terms.CONST_STRING(s) => pureOut('"' + s + '"', i) + case Terms.CONST_BYTESTR(bs) => pureOut("base58'" + bs.base58 + "'", i) + case Terms.REF(ref) => pureOut(ref, i) + case Terms.GETTER(getExpr, fld) => expr(pure(getExpr), ctx, BracesWhenNeccessary, firstLinePolicy).map(a => a + "." + fld) + case Terms.IF(cond, it, iff) => + for { + c <- expr(pure(cond), ctx, BracesWhenNeccessary, DontIndentFirstLine) + it <- expr(pure(it), ctx.incrementIdent(), BracesWhenNeccessary, DontIndentFirstLine) + iff <- expr(pure(iff), ctx.incrementIdent(), BracesWhenNeccessary, DontIndentFirstLine) + } yield + out("if (" + c + ")" + NEWLINE, i) + + out("then " + it + NEWLINE, ctx.ident + 1) + + out("else " + iff, ctx.ident + 1) case Terms.FUNCTION_CALL(func, args) => + val argsCoeval = args + .map(a => expr(pure(a), ctx, BracesWhenNeccessary, DontIndentFirstLine)) + .toVector + .sequence func match { - case FunctionHeader.User(name) => Delay(Seq(Left(out(name + "(", ident))) ++ argso(args, ctx) ++ Seq(Left(")"))) + case FunctionHeader.User(name) => argsCoeval.map(as => out(name + "(" + as.mkString(", ") + ")", i)) case FunctionHeader.Native(name) => - val binOp: Option[String] = ctx.binaryOps.get(name) - binOp match { + ctx.binaryOps.get(name) match { case Some(binOp) => - Delay(Seq( - Left(out("(", ident)), - Right(_ => expr(args.head, 0, ctx)), - Left(out(" " + binOp + " ", 0)), - Right(_ => expr(args.tail.head, 0, ctx)), - Left(out(")", 0)))) + argsCoeval.map(as => out("(" + as.head + " " + binOp + " " + as.tail.head + ")", i)) case None => - val opCode = ctx.opCodes.get(name) - opCode match { - case None => - Delay( - Seq(Left(out("Native<" + name + ">", ident))) ++ - Seq(Left(out("(", 0))) ++ argso(args, ctx) ++ Seq(Left(")"))) - case Some(opCode) => - Delay(Seq(Left(out(opCode.toString + "(", ident))) ++ argso(args, ctx) ++ Seq(Left(")"))) - } + argsCoeval.map( + as => + out(ctx.opCodes.getOrElse(name, "Native<" + name + ">") + "(" + as.mkString(", ") + + ")", + i)) } } - case Terms.REF(ref) => Delay(Seq(Left(out(ref, ident)))) - case Terms.GETTER(get_expr, fld) => - Delay( - Seq( - Left(out("", 0)), - Right(_ => expr(get_expr, ident, ctx)), - Left(out("." + fld, 0)) - )) - case Terms.ARR(_) => ??? // never happens + case _: Terms.ARR => ??? // never happens case _: Terms.CaseObj => ??? // never happens } + } def apply(e: Contract, ctx: DecompilerContext): String = { - e match { - case Contract(dec, cfs, vf) => - dec.map(expr => decl(expr, 0, ctx)).mkString("\n\n") + - cfs - .map { - case CallableFunction(annotation, u) => - out("\n@Callable(" + annotation.invocationArgName + ")\n", 0) + - Decompiler.decl(u, 0, ctx) - } - .mkString("\n") + - (vf match { - case Some(VerifierFunction(annotation, u)) => - out("\n@Verifier(" + annotation.invocationArgName + ")\n", 0) + - Decompiler.decl(u, 0, ctx) - case None => "" - }) - } + + def intersperse(s: Seq[Coeval[String]]): Coeval[String] = s.toVector.sequence.map(v => v.mkString(NEWLINE + NEWLINE)) + + import e._ + + val decls: Seq[Coeval[String]] = dec.map(expr => decl(pure(expr), ctx)) + val callables: Seq[Coeval[String]] = cfs + .map { + case CallableFunction(annotation, u) => + Decompiler.decl(pure(u), ctx).map(out(NEWLINE + "@Callable(" + annotation.invocationArgName + ")" + NEWLINE, 0) + _) + } + + val verifier: Seq[Coeval[String]] = vf.map { + case VerifierFunction(annotation, u) => + Decompiler.decl(pure(u), ctx).map(out(NEWLINE + "@Verifier(" + annotation.invocationArgName + ")" + NEWLINE, 0) + _) + }.toSeq + + val result = for { + d <- intersperse(decls) + c <- intersperse(callables) + v <- intersperse(verifier) + } yield d + NEWLINE + c + NEWLINE + v + + result() } def apply(e0: EXPR, ctx: DecompilerContext): String = - show(expr(e0, 0, ctx)) + expr(pure(e0), ctx, NoBraces, IdentFirstLine).apply() } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/DecompilerContext.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/DecompilerContext.scala index fd7bb2b..7a0076e 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/DecompilerContext.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/DecompilerContext.scala @@ -1,3 +1,5 @@ package com.zbsnetwork.lang.v1.compiler -case class DecompilerContext(opCodes: Map[Short, String], binaryOps: Map[Short, String]) +case class DecompilerContext(opCodes: Map[Short, String], binaryOps: Map[Short, String], ident: Int) { + def incrementIdent(): DecompilerContext = this.copy(ident = this.ident + 1) +} diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ExpressionCompiler.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ExpressionCompiler.scala index 38568ac..4f4bda9 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ExpressionCompiler.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/ExpressionCompiler.scala @@ -10,7 +10,6 @@ import com.zbsnetwork.lang.v1.compiler.Types.{FINAL, _} import com.zbsnetwork.lang.v1.evaluator.ctx._ import com.zbsnetwork.lang.v1.evaluator.ctx.impl.PureContext import com.zbsnetwork.lang.v1.parser.BinaryOperation._ -import com.zbsnetwork.lang.v1.parser.Expressions.Pos.AnyPos import com.zbsnetwork.lang.v1.parser.Expressions.{BINARY_OP, MATCH_CASE, PART, Pos} import com.zbsnetwork.lang.v1.parser.{BinaryOperation, Expressions, Parser} import com.zbsnetwork.lang.v1.task.imports._ @@ -91,9 +90,9 @@ object ExpressionCompiler { case _ => None } ifCases <- inspectFlat[CompilerContext, CompilationError, Expressions.EXPR](updatedCtx => { - mkIfCases(updatedCtx, cases, Expressions.REF(p, PART.VALID(AnyPos, refTmpKey)), allowShadowVarName).toCompileM + mkIfCases(updatedCtx, cases, Expressions.REF(p, PART.VALID(p, refTmpKey)), allowShadowVarName).toCompileM }) - compiledMatch <- compileLetBlock(p, Expressions.LET(AnyPos, PART.VALID(AnyPos, refTmpKey), expr, Seq.empty), ifCases) + compiledMatch <- compileLetBlock(p, Expressions.LET(p, PART.VALID(p, refTmpKey), expr, Seq.empty), ifCases) _ <- cases .flatMap(_.types) .traverse[CompileM, String](handlePart) @@ -121,11 +120,11 @@ object ExpressionCompiler { private def handleTypeUnion(types: List[String], f: FINAL, ctx: CompilerContext) = if (types.isEmpty) f else UNION.create(types.map(ctx.predefTypes).map(_.typeRef)) - private def validateShadowing(p: Pos, dec: Expressions.Declaration): CompileM[String] = + private def validateShadowing(p: Pos, dec: Expressions.Declaration, allowedExceptions: List[String] = List.empty): CompileM[String] = for { ctx <- get[CompilerContext, CompilationError] letName <- handlePart(dec.name) - .ensureOr(n => AlreadyDefined(p.start, p.end, n, isFunction = false))(n => !ctx.varDefs.contains(n) || dec.allowShadowing) + .ensureOr(n => AlreadyDefined(p.start, p.end, n, isFunction = false))(n => !ctx.varDefs.contains(n) || dec.allowShadowing || allowedExceptions.contains(n)) .ensureOr(n => AlreadyDefined(p.start, p.end, n, isFunction = true))(n => !ctx.functionDefs.contains(n)) } yield letName @@ -140,9 +139,9 @@ object ExpressionCompiler { typeUnion = handleTypeUnion(letTypes, compiledLet._2, ctx) } yield (letName, typeUnion, compiledLet._1) - def compileFunc(p: Pos, func: Expressions.FUNC): CompileM[(FUNC, FINAL, List[(String, FINAL)])] = { + def compileFunc(p: Pos, func: Expressions.FUNC, annListVars: List[String] = List.empty): CompileM[(FUNC, FINAL, List[(String, FINAL)])] = { for { - funcName <- validateShadowing(p, func) + funcName <- validateShadowing(p, func, annListVars) _ <- func.args.toList .pure[CompileM] .ensure(BadFunctionSignatureSameArgNames(p.start, p.end, funcName)) { l => @@ -185,7 +184,7 @@ object ExpressionCompiler { updateCtx(letName, letType, p) .flatMap(_ => compileExpr(body)) } - } yield (BLOCK(LET(letName, letExpr), compiledBody._1), compiledBody._2) + } yield (LET_BLOCK(LET(letName, letExpr), compiledBody._1), compiledBody._2) } private def compileFuncBlock(p: Pos, func: Expressions.FUNC, body: Expressions.EXPR): CompileM[(Terms.EXPR, FINAL)] = { @@ -305,7 +304,8 @@ object ExpressionCompiler { } } - val default: Either[CompilationError, Expressions.EXPR] = Right(Expressions.FUNCTION_CALL(AnyPos, PART.VALID(AnyPos, "throw"), List.empty)) + val default: Either[CompilationError, Expressions.EXPR] = Right(Expressions.FUNCTION_CALL(cases.head.position, PART.VALID(cases.head.position, "throw"), List.empty)) + cases.foldRight(default) { case (mc, furtherEi) => furtherEi match { @@ -313,7 +313,6 @@ object ExpressionCompiler { case Left(e) => Left(e) } } - } private def mkGetter(p: Pos, ctx: CompilerContext, types: List[FINAL], fieldName: String, expr: EXPR): Either[CompilationError, (GETTER, FINAL)] = { diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/TypeInferrer.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/TypeInferrer.scala index fc1ae5d..d3416e9 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/TypeInferrer.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/TypeInferrer.scala @@ -44,7 +44,6 @@ object TypeInferrer { lazy val err = s"Non-matching types: expected: $placeholder, actual: $argType" (placeholder, argType) match { - case (_, NOTHING) => Right(None) case (tp @ TYPEPARAM(char), _) => Right(Some(MatchResult(argType, tp))) case (tp @ PARAMETERIZEDLIST(innerTypeParam), LIST(t)) => matchTypes(t, innerTypeParam, knownTypes) diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Types.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Types.scala index 8229e12..5b76fbc 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Types.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/compiler/Types.scala @@ -51,7 +51,7 @@ object Types { case NOTHING => List.empty case UNION(inner) => inner case s: REAL => List(s) - }.toList) + }.toList.distinct) } def apply(l: REAL*): UNION = create(l.toList) @@ -79,4 +79,10 @@ object Types { def <=(l2: FINAL): Boolean = l2 >= l1 } + + val UNIT: CASETYPEREF = CASETYPEREF("Unit", List.empty) + val optionByteVector = UNION(BYTESTR, UNIT) + val optionLong = UNION(LONG, UNIT) + val listByteVector: LIST = LIST(BYTESTR) + val listString: LIST = LIST(STRING) } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ContractEvaluator.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ContractEvaluator.scala index 282aa1e..51e3adf 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ContractEvaluator.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ContractEvaluator.scala @@ -11,15 +11,19 @@ import com.zbsnetwork.lang.v1.task.imports.{raiseError, _} import com.zbsnetwork.lang.v1.traits.domain.Tx.{ContractTransfer, Pmt} import com.zbsnetwork.lang.v1.traits.domain.{Ord, Recipient, Tx} -import scala.collection.mutable.ListBuffer - object ContractEvaluator { case class Invocation(fc: FUNCTION_CALL, invoker: ByteStr, payment: Option[(Long, Option[ByteStr])], contractAddress: ByteStr) def eval(c: Contract, i: Invocation): EvalM[EVALUATED] = { val functionName = i.fc.function.asInstanceOf[FunctionHeader.User].name c.cfs.find(_.u.name == functionName) match { - case None => raiseError[LoggedEvaluationContext, ExecutionError, EVALUATED](s"Callable function '$functionName doesn't exist in the contract") + case None => + val otherFuncs = c.dec.filter(_.isInstanceOf[FUNC]).map(_.asInstanceOf[FUNC].name) + val message = + if (otherFuncs contains functionName) + s"function '$functionName exists in the contract but is not marked as @Callable, therefore cannot not be invoked" + else s"@Callable function '$functionName doesn't exist in the contract" + raiseError[LoggedEvaluationContext, ExecutionError, EVALUATED](message) case Some(f) => val zeroExpr = Right( BLOCK( @@ -60,11 +64,6 @@ object ContractEvaluator { EvaluatorV1.evalExpr(expr) } - - def apply(ctx: EvaluationContext, c: Contract, i: Invocation): Either[ExecutionError, ContractResult] = { - val log = ListBuffer[LogItem]() - val llc = (str: String) => (v: LetExecResult) => log.append((str, v)) - val lec = LoggedEvaluationContext(llc, ctx) - eval(c, i).run(lec).value._2.flatMap(ContractResult.fromObj) - } + def apply(ctx: EvaluationContext, c: Contract, i: Invocation): Either[ExecutionError, ContractResult] = + EvaluatorV1.evalWithLogging(ctx, eval(c, i))._2.flatMap(ContractResult.fromObj) } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/FunctionIds.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/FunctionIds.scala index 67bedbd..7c1cd15 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/FunctionIds.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/FunctionIds.scala @@ -33,6 +33,17 @@ object FunctionIds { val LONG_TO_STRING: Short = 420 val BOOLEAN_TO_STRING: Short = 421 + val CREATE_LIST: Short = 1100 + + val UTF8STRING: Short = 1200 + val BININT: Short = 1201 + val BININT_OFF: Short = 1202 + val INDEXOF: Short = 1203 + val INDEXOFN: Short = 1204 + val SPLIT: Short = 1205 + val PARSEINT: Short = 1206 + val PARSEINTV: Short = 1207 + // Crypto val SIGVERIFY: Short = 500 val KECCAK256: Short = 501 @@ -62,10 +73,4 @@ object FunctionIds { val ADDRESSFROMRECIPIENT: Short = 1060 - val CREATE_LIST0: Short = 1100 - val CREATE_LIST1: Short = 1101 - val CREATE_LIST2: Short = 1102 - val CREATE_LIST3: Short = 1103 - - } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/NativeFunction.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/NativeFunction.scala index 12f9f9a..0681242 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/NativeFunction.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/NativeFunction.scala @@ -1,7 +1,8 @@ package com.zbsnetwork.lang.v1.evaluator.ctx import cats.data.EitherT -import com.zbsnetwork.lang.TrampolinedExecResult +import com.zbsnetwork.lang.StdLibVersion.StdLibVersion +import com.zbsnetwork.lang.{StdLibVersion, TrampolinedExecResult} import com.zbsnetwork.lang.v1.FunctionHeader import com.zbsnetwork.lang.v1.compiler.Terms.{EVALUATED, EXPR} import com.zbsnetwork.lang.v1.compiler.Types._ @@ -17,6 +18,7 @@ sealed trait BaseFunction { @JSExport def name: String @JSExport def docString: String @JSExport def argsDoc: Array[(String, String)] + @JSExport def deprecated: Boolean = false } object BaseFunction { @@ -55,27 +57,63 @@ object NativeFunction { @JSExportTopLevel("UserFunction") case class UserFunction(@(JSExport @field) name: String, @(JSExport @field) internalName: String, - @(JSExport @field) cost: Long, + costByLibVersion: Map[StdLibVersion, Long], @(JSExport @field) signature: FunctionTypeSignature, ev: EXPR, @(JSExport @field) docString: String, - @(JSExport @field) argsDoc: Array[(String, String)]) - extends BaseFunction + @(JSExport @field) argsDoc: Array[(String, String)] + ) + extends BaseFunction { + + @(JSExport @field) + def cost: Long = costByLibVersion.get(StdLibVersion.SupportedVersions.last).get +} object UserFunction { def apply(name: String, cost: Long, resultType: TYPE, docString: String, args: (String, TYPE, String)*)(ev: EXPR): UserFunction = - UserFunction(name, name, cost, resultType, docString, args: _*)(ev) + UserFunction(name, name, StdLibVersion.SupportedVersions.map(_ -> cost).toMap, resultType, docString, args: _*)(ev) + + def deprecated(name: String, cost: Long, resultType: TYPE, docString: String, args: (String, TYPE, String)*)(ev: EXPR): UserFunction = + UserFunction.deprecated(name, name, StdLibVersion.SupportedVersions.map(_ -> cost).toMap, resultType, docString, args: _*)(ev) + + def apply(name: String, costByLibVersion: Map[StdLibVersion, Long], resultType: TYPE, docString: String, args: (String, TYPE, String)*)( + ev: EXPR): UserFunction = + UserFunction(name, name, costByLibVersion, resultType, docString, args: _*)(ev) def apply(name: String, internalName: String, cost: Long, resultType: TYPE, docString: String, args: (String, TYPE, String)*)( ev: EXPR): UserFunction = + UserFunction(name, internalName, StdLibVersion.SupportedVersions.map(_ -> cost).toMap, resultType, docString, args: _*)(ev) + + def apply(name: String, + internalName: String, + costByLibVersion: Map[StdLibVersion, Long], + resultType: TYPE, + docString: String, + args: (String, TYPE, String)*)(ev: EXPR): UserFunction = new UserFunction( name = name, internalName = internalName, - cost = cost, + costByLibVersion = costByLibVersion, signature = FunctionTypeSignature(result = resultType, args = args.map(a => (a._1, a._2)), header = FunctionHeader.User(internalName)), ev = ev, docString = docString, argsDoc = args.map(a => (a._1 -> a._3)).toArray ) + + def deprecated(name: String, + internalName: String, + costByLibVersion: Map[StdLibVersion, Long], + resultType: TYPE, + docString: String, + args: (String, TYPE, String)*)(ev: EXPR): UserFunction = + new UserFunction( + name = name, + internalName = internalName, + costByLibVersion = costByLibVersion, + signature = FunctionTypeSignature(result = resultType, args = args.map(a => (a._1, a._2)), header = FunctionHeader.User(internalName)), + ev = ev, + docString = docString, + argsDoc = args.map(a => (a._1 -> a._3)).toArray + ) { override def deprecated = true } } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/PureContext.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/PureContext.scala index 9f061c2..965ed38 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/PureContext.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/PureContext.scala @@ -5,7 +5,7 @@ import java.nio.charset.StandardCharsets import cats.data.EitherT import cats.kernel.Monoid import com.zbsnetwork.common.state.ByteStr -import com.zbsnetwork.lang.StdLibVersion._ +import com.zbsnetwork.lang.StdLibVersion import com.zbsnetwork.lang.v1.CTX import com.zbsnetwork.lang.v1.compiler.Terms._ import com.zbsnetwork.lang.v1.compiler.Types._ @@ -13,11 +13,18 @@ import com.zbsnetwork.lang.v1.evaluator.FunctionIds._ import com.zbsnetwork.lang.v1.evaluator.ctx._ import com.zbsnetwork.lang.v1.parser.BinaryOperation import com.zbsnetwork.lang.v1.parser.BinaryOperation._ +import scala.collection.mutable.ArrayBuffer +import java.nio.charset.StandardCharsets.UTF_8 +import java.nio.ByteBuffer -import scala.util.Try +import scala.util.{Try, Success} object PureContext { + import StdLibVersion._ + + implicit def intToLong(num: Int): Long = num.toLong + private lazy val defaultThrowMessage = "Explicit script termination" lazy val MaxStringResult = Short.MaxValue lazy val MaxBytesResult = 65536 @@ -59,7 +66,12 @@ object PureContext { } lazy val ne: BaseFunction = - UserFunction(NE_OP.func, 26, BOOLEAN, "Inequality", ("@a", TYPEPARAM('T'), "value"), ("@b", TYPEPARAM('T'), "value")) { + UserFunction(NE_OP.func, + Map[StdLibVersion, Long](V1 -> 26, V2 -> 26, V3 -> 1), + BOOLEAN, + "Inequality", + ("@a", TYPEPARAM('T'), "value"), + ("@b", TYPEPARAM('T'), "value")) { FUNCTION_CALL(uNot, List(FUNCTION_CALL(eq, List(REF("@a"), REF("@b"))))) } @@ -68,12 +80,12 @@ object PureContext { case _ => Left(defaultThrowMessage) } - lazy val throwNoMessage: BaseFunction = UserFunction("throw", 2, NOTHING, "Fail script") { + lazy val throwNoMessage: BaseFunction = UserFunction("throw", Map[StdLibVersion, Long](V1 -> 2, V2 -> 2, V3 -> 1), NOTHING, "Fail script") { FUNCTION_CALL(throwWithMessage, List(CONST_STRING(defaultThrowMessage))) } lazy val extract: BaseFunction = - UserFunction("extract", + UserFunction.deprecated("extract", 13, TYPEPARAM('T'), "Extract value from option or fail", @@ -86,7 +98,13 @@ object PureContext { } lazy val isDefined: BaseFunction = - UserFunction("isDefined", 35, BOOLEAN, "Check the value is defined", ("@a", PARAMETERIZEDUNION(List(TYPEPARAM('T'), UNIT)), "Option value")) { + UserFunction( + "isDefined", + Map[StdLibVersion, Long](V1 -> 35, V2 -> 35, V3 -> 1), + BOOLEAN, + "Check the value is defined", + ("@a", PARAMETERIZEDUNION(List(TYPEPARAM('T'), UNIT)), "Option value") + ) { FUNCTION_CALL(ne, List(REF("@a"), REF("unit"))) } @@ -124,7 +142,7 @@ object PureContext { case _ => Right(FALSE) } - lazy val sizeBytes: BaseFunction = NativeFunction("size", 1, SIZE_BYTES, LONG, "Size of bytes str", ("byteStr", BYTESTR, "vector")) { + lazy val sizeBytes: BaseFunction = NativeFunction("size", 1, SIZE_BYTES, LONG, "Size of bytes str", ("byteVector", BYTESTR, "vector")) { case CONST_BYTESTR(bv) :: Nil => Right(CONST_LONG(bv.arr.length)) case xs => notImplemented("size(byte[])", xs) } @@ -218,28 +236,19 @@ object PureContext { case xs => notImplemented("take(xs: String, number: Long)", xs) } - lazy val listConstructor1 = - NativeFunction("List", 1, CREATE_LIST1, PARAMETERIZEDLIST(TYPEPARAM('T')), "Construct a new List[T]", ("arg1", TYPEPARAM('T'), "arg1"))(xs => - Right(ARR(xs.toIndexedSeq))) - - lazy val listConstructor2 = NativeFunction("List", - 1, - CREATE_LIST2, - PARAMETERIZEDLIST(TYPEPARAM('T')), - "Construct a new List[T]", - ("arg1", TYPEPARAM('T'), "arg1"), - ("arg2", TYPEPARAM('T'), "arg2"))(xs => Right(ARR(xs.toIndexedSeq))) - - lazy val listConstructor3 = NativeFunction( - "List", - 1, - CREATE_LIST3, - PARAMETERIZEDLIST(TYPEPARAM('T')), - "Construct a new List[T]", - ("arg1", TYPEPARAM('T'), "arg1"), - ("arg2", TYPEPARAM('T'), "arg2"), - ("arg3", TYPEPARAM('T'), "arg3") - )(xs => Right(ARR(xs.toIndexedSeq))) + lazy val listConstructor: NativeFunction = + NativeFunction( + "cons", + 2, + CREATE_LIST, + PARAMETERIZEDLIST(PARAMETERIZEDUNION(List(TYPEPARAM('A'), TYPEPARAM('B')))), + "Construct a new List[T]", + ("head", TYPEPARAM('A'), "head"), + ("tail", PARAMETERIZEDLIST(TYPEPARAM('B')), "tail") + ) { + case h :: ARR(t) :: Nil => Right(ARR(h +: t)) + case xs => notImplemented("cons(head: T, tail: LIST[T]", xs) + } lazy val dropString: BaseFunction = NativeFunction("drop", 1, DROP_STRING, STRING, "Remmove sring prefix", ("xs", STRING, "string"), ("number", LONG, "prefix size")) { @@ -281,6 +290,87 @@ object PureContext { ) } + val UTF8Decoder = UTF_8.newDecoder + + lazy val toUtf8String: BaseFunction = + NativeFunction("toUtf8String", 20, UTF8STRING, STRING, "Convert UTF8 bytes to string", ("u", BYTESTR, "utf8")) { + case CONST_BYTESTR(u) :: Nil => Try(CONST_STRING(UTF8Decoder.decode(ByteBuffer.wrap(u.arr)).toString)).toEither.left.map(_.toString) + case xs => notImplemented("toUtf8String(u: byte[])", xs) + } + + lazy val toLong: BaseFunction = + NativeFunction("toInt", 10, BININT, LONG, "Deserialize big endian 8-bytes value", ("bin", BYTESTR, "8-bytes BE binaries")) { + case CONST_BYTESTR(u) :: Nil => Try(CONST_LONG(ByteBuffer.wrap(u.arr).getLong())).toEither.left.map(_.toString) + case xs => notImplemented("toInt(u: byte[])", xs) + } + + lazy val toLongOffset: BaseFunction = + NativeFunction("toInt", 10, BININT_OFF, LONG, "Deserialize big endian 8-bytes value", ("bin", BYTESTR, "8-bytes BE binaries"), ("offet", LONG, "bytes offset")) { + case CONST_BYTESTR(ByteStr(u)) :: CONST_LONG(o) :: Nil => if( o >= 0 && o <= u.size - 8) { + Try(CONST_LONG(ByteBuffer.wrap(u).getLong(o.toInt))).toEither.left.map(_.toString) + } else { + Left("IndexOutOfBounds") + } + case xs => notImplemented("toInt(u: byte[], off: int)", xs) + } + + lazy val indexOf: BaseFunction = + NativeFunction("indexOf", 20, INDEXOF, optionLong, "index of substring", ("str", STRING, "String for analize"), ("substr", STRING, "String for searching")) { + case CONST_STRING(m) :: CONST_STRING(sub) :: Nil => Right({ + val i = m.indexOf(sub) + if( i != -1 ) { + CONST_LONG(i.toLong) + } else { + unit + } + }) + case xs => notImplemented("indexOf(STRING, STRING)", xs) + } + + lazy val indexOfN: BaseFunction = + NativeFunction("indexOf", 20, INDEXOFN, optionLong, "index of substring after offset", ("str", STRING, "String for analize"), ("substr", STRING, "String for searching"), ("offset", LONG, "offset")) { + case CONST_STRING(m) :: CONST_STRING(sub) :: CONST_LONG(off) :: Nil => Right( if(off >= 0 && off <= m.length) { + val i = m.indexOf(sub, off.toInt) + if( i != -1 ) { + CONST_LONG(i.toLong) + } else { + unit + } + } else { + unit + } ) + case xs => notImplemented("indexOf(STRING, STRING)", xs) + } + + def split(m: String, sep: String, buffer: ArrayBuffer[CONST_STRING] = ArrayBuffer[CONST_STRING](), start: Int = 0): IndexedSeq[CONST_STRING] = { + m.indexOf(sep, start) match { + case -1 => + buffer += CONST_STRING(m.substring(start)) + buffer.result + case n => + buffer += CONST_STRING(m.substring(0, n)) + split(m, sep, buffer, n + sep.length) + } + } + + lazy val splitStr: BaseFunction = + NativeFunction("split", 100, SPLIT, listString, "split string by separator", ("str", STRING, "String for splitting"), ("separator", STRING, "separator")) { + case CONST_STRING(m) :: CONST_STRING(sep) :: Nil => Right( ARR(split(m, sep))) + case xs => notImplemented("split(STRING, STRING)", xs) + } + + lazy val parseInt: BaseFunction = + NativeFunction("parseInt", 20, PARSEINT, optionLong, "parse string to integer", ("str", STRING, "String for parsing")) { + case CONST_STRING(u) :: Nil => Try(CONST_LONG(u.toInt)).orElse(Success(unit)).toEither.left.map(_.toString) + case xs => notImplemented("parseInt(STRING)", xs) + } + + lazy val parseIntVal: BaseFunction = + NativeFunction("parseIntValue", 20, PARSEINTV, LONG, "parse string to integer with fail on errors", ("str", STRING, "String for parsing")) { + case CONST_STRING(u) :: Nil => Try(CONST_LONG(u.toInt)).toEither.left.map(_.toString) + case xs => notImplemented("parseInt(STRING)", xs) + } + def createRawOp(op: BinaryOperation, t: TYPE, r: TYPE, func: Short, docString: String, arg1Doc: String, arg2Doc: String, complicity: Int = 1)( body: (EVALUATED, EVALUATED) => Either[String, EVALUATED]): BaseFunction = NativeFunction(opsToFunctions(op), complicity, func, r, docString, ("a", t, arg1Doc), ("b", t, arg2Doc)) { @@ -327,13 +417,20 @@ object PureContext { case _ => ??? } - lazy val uMinus: BaseFunction = UserFunction("-", 9, LONG, "Change integer sign", ("@n", LONG, "value")) { - FUNCTION_CALL(subLong, List(CONST_LONG(0), REF("@n"))) - } + lazy val uMinus: BaseFunction = + UserFunction("-", Map[StdLibVersion, Long](V1 -> 9, V2 -> 9, V3 -> 1), LONG, "Change integer sign", ("@n", LONG, "value")) { + FUNCTION_CALL(subLong, List(CONST_LONG(0), REF("@n"))) + } - lazy val uNot: BaseFunction = UserFunction("!", 11, BOOLEAN, "unary negation", ("@p", BOOLEAN, "boolean")) { - IF(FUNCTION_CALL(eq, List(REF("@p"), FALSE)), TRUE, FALSE) - } + lazy val uNot: BaseFunction = + UserFunction("!", Map[StdLibVersion, Long](V1 -> 11, V2 -> 11, V3 -> 1), BOOLEAN, "unary negation", ("@p", BOOLEAN, "boolean")) { + IF(REF("@p"), FALSE, TRUE) + } + + lazy val ensure: BaseFunction = + UserFunction("ensure", 16, BOOLEAN, "Ensure parameter is true", ("@b", BOOLEAN, "condition"), ("@msg", STRING, "error message")) { + IF(REF("@b"), TRUE, FUNCTION_CALL(throwWithMessage, List(REF("@msg")))) + } private lazy val operators: Array[BaseFunction] = Array( mulLong, @@ -353,7 +450,9 @@ object PureContext { uNot ) - private lazy val vars: Map[String, ((FINAL, String), LazyVal)] = Map(("unit", ((UNIT, "Single instance value"), LazyVal(EitherT.pure(unit))))) + private lazy val vars: Map[String, ((FINAL, String), LazyVal)] = Map( + ("unit", ((UNIT, "Single instance value"), LazyVal(EitherT.pure(unit)))) + ) private lazy val functions = Array( fraction, sizeBytes, @@ -380,11 +479,11 @@ object PureContext { private lazy val ctx = CTX( Seq( - new DefinedType { lazy val name = "Unit"; lazy val typeRef = UNIT }, - new DefinedType { lazy val name = "Int"; lazy val typeRef = LONG }, - new DefinedType { lazy val name = "Boolean"; lazy val typeRef = BOOLEAN }, - new DefinedType { lazy val name = "ByteStr"; lazy val typeRef = BYTESTR }, - new DefinedType { lazy val name = "String"; lazy val typeRef = STRING } + new DefinedType { lazy val name = "Unit"; lazy val typeRef = UNIT }, + new DefinedType { lazy val name = "Int"; lazy val typeRef = LONG }, + new DefinedType { lazy val name = "Boolean"; lazy val typeRef = BOOLEAN }, + new DefinedType { lazy val name = "ByteVector"; lazy val typeRef = BYTESTR }, + new DefinedType { lazy val name = "String"; lazy val typeRef = STRING } ), vars, functions @@ -393,7 +492,15 @@ object PureContext { def build(version: StdLibVersion): CTX = version match { case V1 | V2 => ctx - case V3 => Monoid.combine(ctx, CTX(Seq.empty, Map.empty, Array(listConstructor1, listConstructor2, listConstructor3))) + case V3 => + Monoid.combine( + ctx, + CTX( + Seq.empty, + Map(("nil", ((LIST(NOTHING), "empty list of any type"), LazyVal(EitherT.pure(ARR(IndexedSeq.empty[EVALUATED])))))), + Array(listConstructor, ensure, toUtf8String, toLong, toLongOffset, indexOf, indexOfN, splitStr, parseInt, parseIntVal) + ) + ) } } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/package.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/package.scala index 591bb42..78158a1 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/package.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/package.scala @@ -1,13 +1,12 @@ package com.zbsnetwork.lang.v1.evaluator.ctx import com.zbsnetwork.lang.v1.compiler.Terms.CaseObj -import com.zbsnetwork.lang.v1.compiler.Types.CASETYPEREF +import com.zbsnetwork.lang.v1.compiler.Types.UNIT package object impl { def notImplemented(funcName: String, args: List[Any]): Nothing = throw new Exception( s"Can't apply (${args.map(_.getClass.getSimpleName).mkString(", ")}) to '$funcName'" ) - lazy val UNIT: CASETYPEREF = CASETYPEREF("Unit", List.empty) lazy val unit: CaseObj = CaseObj(UNIT, Map.empty) } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Bindings.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Bindings.scala index 9de7e8a..bfd23d7 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Bindings.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Bindings.scala @@ -76,18 +76,19 @@ object Bindings { CaseObj( buildOrderType(proofsEnabled).typeRef, Map( - "id" -> ord.id, - "sender" -> senderObject(ord.sender), - "senderPublicKey" -> ord.senderPublicKey, - "matcherPublicKey" -> ord.matcherPublicKey, - "assetPair" -> assetPair(ord.assetPair), - "orderType" -> ordType(ord.orderType), - "amount" -> ord.amount, - "price" -> ord.price, - "timestamp" -> ord.timestamp, - "expiration" -> ord.expiration, - "matcherFee" -> ord.matcherFee, - "bodyBytes" -> ord.bodyBytes, + "id" -> ord.id, + "sender" -> senderObject(ord.sender), + "senderPublicKey" -> ord.senderPublicKey, + "matcherPublicKey" -> ord.matcherPublicKey, + "assetPair" -> assetPair(ord.assetPair), + "orderType" -> ordType(ord.orderType), + "amount" -> ord.amount, + "price" -> ord.price, + "timestamp" -> ord.timestamp, + "expiration" -> ord.expiration, + "matcherFee" -> ord.matcherFee, + "bodyBytes" -> ord.bodyBytes, + "matcherFeeAssetId" -> ord.matcherFeeAssetId, proofsPart(ord.proofs) ) ) diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Types.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Types.scala index e91065f..a9da516 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Types.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/Types.scala @@ -3,7 +3,6 @@ package com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs import com.zbsnetwork.lang.StdLibVersion import com.zbsnetwork.lang.StdLibVersion.StdLibVersion import com.zbsnetwork.lang.v1.compiler.Types._ -import com.zbsnetwork.lang.v1.evaluator.ctx.impl._ import com.zbsnetwork.lang.v1.evaluator.ctx.{CaseType, DefinedType, UnionType} object Types { @@ -13,17 +12,15 @@ object Types { val addressOrAliasType = UNION(addressType.typeRef, aliasType.typeRef) val transfer = CaseType("Transfer", List("recipient" -> addressOrAliasType, "amount" -> LONG)) - val optionByteVector = UNION(BYTESTR, UNIT) val optionAddress = UNION(addressType.typeRef, UNIT) - val optionLong = UNION(LONG, UNIT) - val listByteVector: LIST = LIST(BYTESTR) val listTransfers = LIST(transfer.typeRef) val paymentType = CaseType("AttachedPayment", List("asset" -> optionByteVector, "amount" -> LONG)) val optionPayment = UNION(paymentType.typeRef, UNIT) - val invocationType = CaseType("Invocation", List("caller" -> addressType.typeRef, "contractAddress" -> addressType.typeRef, "payment" -> optionPayment)) + val invocationType = + CaseType("Invocation", List("caller" -> addressType.typeRef, "contractAddress" -> addressType.typeRef, "payment" -> optionPayment)) private val header = List( "id" -> BYTESTR, @@ -192,15 +189,16 @@ object Types { "Order", addProofsIfNeeded( List( - "id" -> BYTESTR, - "matcherPublicKey" -> BYTESTR, - "assetPair" -> assetPairType.typeRef, - "orderType" -> ordTypeType, - "price" -> LONG, - "amount" -> LONG, - "timestamp" -> LONG, - "expiration" -> LONG, - "matcherFee" -> LONG + "id" -> BYTESTR, + "matcherPublicKey" -> BYTESTR, + "assetPair" -> assetPairType.typeRef, + "orderType" -> ordTypeType, + "price" -> LONG, + "amount" -> LONG, + "timestamp" -> LONG, + "expiration" -> LONG, + "matcherFee" -> LONG, + "matcherFeeAssetId" -> optionByteVector ) ++ proven, proofsEnabled ) @@ -258,18 +256,17 @@ object Types { List(genesisTransactionType, buildPaymentTransactionType(proofsEnabled)) } - def buildAssetSupportedTransactions(proofsEnabled: Boolean) = List( + def buildAssetSupportedTransactions(proofsEnabled: Boolean, v: StdLibVersion) = List( buildReissueTransactionType(proofsEnabled), buildBurnTransactionType(proofsEnabled), buildMassTransferTransactionType(proofsEnabled), buildExchangeTransactionType(proofsEnabled), buildTransferTransactionType(proofsEnabled), - buildSetAssetScriptTransactionType(proofsEnabled), - buildContractInvokationTransactionType(proofsEnabled), - ) + buildSetAssetScriptTransactionType(proofsEnabled) + ) ++ (if (v == StdLibVersion.V3) List(buildContractInvokationTransactionType(proofsEnabled)) else List.empty) def buildActiveTransactionTypes(proofsEnabled: Boolean, v: StdLibVersion): List[CaseType] = { - buildAssetSupportedTransactions(proofsEnabled) ++ + buildAssetSupportedTransactions(proofsEnabled, v) ++ List( buildIssueTransactionType(proofsEnabled), buildLeaseTransactionType(proofsEnabled), @@ -278,7 +275,7 @@ object Types { buildSetScriptTransactionType(proofsEnabled), buildSponsorFeeTransactionType(proofsEnabled), buildDataTransactionType(proofsEnabled) - ) ++ (if (v == StdLibVersion.V3) List(buildContractInvokationTransactionType(proofsEnabled)) else List.empty) + ) } def buildZbsTypes(proofsEnabled: Boolean, v: StdLibVersion): Seq[DefinedType] = { diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/ZbsContext.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/ZbsContext.scala index 9835fd2..0716e62 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/ZbsContext.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/evaluator/ctx/impl/zbs/ZbsContext.scala @@ -107,6 +107,22 @@ object ZbsContext { val getBinaryByIndexF: BaseFunction = getDataByIndexF("getBinary", DataType.ByteArray) val getStringByIndexF: BaseFunction = getDataByIndexF("getString", DataType.String) + def withExtract(f: BaseFunction) = { + val args = f.signature.args.zip(f.argsDoc).map { + case ((name, ty), (_name, doc)) => ("@" ++ name, ty, doc) + } + UserFunction( + f.name ++ "Value", + "@extr" ++ f.header.toString, + f.cost, + f.signature.result.asInstanceOf[UNION].l.find(_ != UNIT).get, + f.docString ++ " (fail on error)", + args : _* + ) { + FUNCTION_CALL(PureContext.extract, List(FUNCTION_CALL(f.header, args.map(a => REF(a._1)).toList))) + } + } + def secureHashExpr(xs: EXPR): EXPR = FUNCTION_CALL( FunctionHeader.Native(KECCAK256), List( @@ -332,7 +348,7 @@ object ZbsContext { val scriptInputType = if (isTokenContext) - UNION(buildAssetSupportedTransactions(proofsEnabled).map(_.typeRef)) + UNION(buildAssetSupportedTransactions(proofsEnabled, version).map(_.typeRef)) else UNION((buildOrderType(proofsEnabled) :: buildActiveTransactionTypes(proofsEnabled, version)).map(_.typeRef)) @@ -348,8 +364,8 @@ object ZbsContext { ("tx", ((scriptInputType, "Processing transaction"), LazyVal(EitherT(inputEntityCoeval)))) ), 3 -> Map( - ("Sell", ((ordTypeType, "Sell OrderType"), LazyVal(EitherT(sellOrdTypeCoeval)))), - ("Buy", ((ordTypeType, "Buy OrderType"), LazyVal(EitherT(buyOrdTypeCoeval)))) + ("Sell", ((sellType.typeRef, "Sell OrderType"), LazyVal(EitherT(sellOrdTypeCoeval)))), + ("Buy", ((buyType.typeRef, "Buy OrderType"), LazyVal(EitherT(buyOrdTypeCoeval)))) ) ) @@ -378,11 +394,24 @@ object ZbsContext { val types = buildZbsTypes(proofsEnabled, version) CTX( - types ++ (if (version == V3) + types ++ (if (version == V3) { List(writeSetType, paymentType, contractTransfer, contractTransferSetType, contractResultType, invocationType) - else List.empty), + } else List.empty), commonVars ++ vars(version), - functions + functions ++ List(getIntegerFromStateF, + getBooleanFromStateF, + getBinaryFromStateF, + getStringFromStateF, + getIntegerFromArrayF, + getBooleanFromArrayF, + getBinaryFromArrayF, + getStringFromArrayF, + getIntegerByIndexF, + getBooleanByIndexF, + getBinaryByIndexF, + getStringByIndexF, + addressFromStringF + ).map(withExtract) ) } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/BinaryOperation.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/BinaryOperation.scala index ee3be86..8544188 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/BinaryOperation.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/BinaryOperation.scala @@ -13,12 +13,14 @@ sealed abstract class BinaryOperation { object BinaryOperation { - val opsByPriority: List[List[BinaryOperation]] = List( - List(OR_OP, AND_OP), - List(EQ_OP, NE_OP), - List(GT_OP, GE_OP, LT_OP, LE_OP), - List(SUM_OP, SUB_OP), - List(MUL_OP, DIV_OP, MOD_OP) + // No monadic notion here, Left and Right mean `left-assosiative and `right-assosiative` + val opsByPriority: List[Either[List[BinaryOperation], List[BinaryOperation]]] = List( + Right(List(CONS_OP)), + Left(List(OR_OP, AND_OP)), + Left(List(EQ_OP, NE_OP)), + Left(List(GT_OP, GE_OP, LT_OP, LE_OP)), + Left(List(SUM_OP, SUB_OP)), + Left(List(MUL_OP, DIV_OP, MOD_OP)) ) def opsToFunctions(op: BinaryOperation): String = op.func @@ -71,5 +73,12 @@ object BinaryOperation { BINARY_OP(Pos(start, end), op2, LT_OP, op1) } } + case object CONS_OP extends BinaryOperation { + override val func: String = "::" + override def expr(start: Int, end: Int, op1: EXPR, op2: EXPR): EXPR = { + val pos = Pos(start, end) + FUNCTION_CALL(Pos(start, end), PART.VALID(pos, "cons"), List(op1, op2)) + } + } } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/Parser.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/Parser.scala index 1da10f2..1cdd4c8 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/Parser.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/parser/Parser.scala @@ -2,7 +2,6 @@ package com.zbsnetwork.lang.v1.parser import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.lang.v1.parser.BinaryOperation._ -import com.zbsnetwork.lang.v1.parser.Expressions.Pos.AnyPos import com.zbsnetwork.lang.v1.parser.Expressions._ import com.zbsnetwork.lang.v1.parser.UnaryOperation._ import fastparse.{WhitespaceApi, core} @@ -29,6 +28,9 @@ object Parser { val notEndOfString = CharPred(_ != '\"') val specialSymbols = P("\\" ~~ AnyChar) val comment: P[Unit] = P("#" ~~ CharPred(_ != '\n').repX).rep.map(_ => ()) + val directive: P[Unit] = P("{-#" ~ CharPred(el => el != '\n' && el != '#').rep ~ "#-}").rep.map(_ => ()) + + val unusedText = comment ~ directive ~ comment val escapedUnicodeSymbolP: P[(Int, String, Int)] = P(Index ~~ (NoCut(unicodeSymbolP) | specialSymbols).! ~~ Index) val stringP: P[EXPR] = P(Index ~~ "\"" ~/ Pass ~~ (escapedUnicodeSymbolP | notEndOfString).!.repX ~~ "\"" ~~ Index) @@ -160,9 +162,17 @@ object Parser { case (_, id, None, _) => id } - val extractableAtom: P[EXPR] = P(curlyBracesP | bracesP | maybeFunctionCallP) + val list: P[EXPR] = (Index ~~ P("[") ~ functionCallArgs ~ P("]") ~~ Index).map { case (s,e,f) => + val pos = Pos(s, f) + e.foldRight(REF(pos, PART.VALID(pos,"nil")):EXPR) { (v,l) => FUNCTION_CALL(pos, PART.VALID(pos, "cons"), List(v,l)) } + } + + val extractableAtom: P[EXPR] = P(curlyBracesP | bracesP | + byteVectorP | stringP | numberP | trueP | falseP | list | + maybeFunctionCallP) abstract class Accessor + case class Method(name: PART[String], args: Seq[EXPR]) extends Accessor case class Getter(name: PART[String]) extends Accessor case class ListIndex(index: EXPR) extends Accessor val typesP: P[Seq[PART[String]]] = anyVarName.rep(min = 1, sep = comment ~ "|" ~ comment) @@ -170,18 +180,18 @@ object Parser { val funcname = anyVarName val argWithType = anyVarName ~ ":" ~ typesP val args = "(" ~ argWithType.rep(sep = ",") ~ ")" - val funcHeader = "func" ~ funcname ~ args ~ "=" ~ P(singleBaseExpr | ("{" ~ baseExpr ~ "}")) + val funcHeader = Index ~~ "func" ~ funcname ~ args ~ "=" ~ P(singleBaseExpr | ("{" ~ baseExpr ~ "}")) ~~ Index funcHeader.map { - case (name, args, expr) => FUNC(AnyPos, name, args, expr) + case (start, name, args, expr, end) => FUNC(Pos(start, end), name, args, expr) } } - val annotationP: P[ANNOTATION] = ("@" ~ anyVarName ~ "(" ~ anyVarName.rep(sep = ",") ~ ")").map { - case (name: PART[String], args: Seq[PART[String]]) => ANNOTATION(AnyPos, name, args) + val annotationP: P[ANNOTATION] = (Index ~~ "@" ~ anyVarName ~ "(" ~ anyVarName.rep(sep = ",") ~ ")" ~~ Index).map { + case (start, name: PART[String], args: Seq[PART[String]], end) => ANNOTATION(Pos(start, end), name, args) } - val annotatedFunc: P[ANNOTATEDFUNC] = (annotationP.rep ~ funcP).map { - case (as, f) => ANNOTATEDFUNC(AnyPos, as, f) + val annotatedFunc: P[ANNOTATEDFUNC] = (Index ~~ annotationP.rep ~ funcP ~~ Index).map { + case (start, as, f, end) => ANNOTATEDFUNC(Pos(start, end), as, f) } val matchCaseP: P[MATCH_CASE] = { @@ -226,8 +236,9 @@ object Parser { } val accessP: P[(Int, Accessor, Int)] = P( - ("" ~ comment ~ Index ~ "." ~/ comment ~ anyVarName.map(Getter) ~~ Index) | - (Index ~~ "[" ~/ baseExpr.map(ListIndex) ~ "]" ~~ Index) + (("" ~ comment ~ Index ~ "." ~/ comment ~ (anyVarName.map(Getter) ~/ comment ~~ ("(" ~/ comment ~ functionCallArgs ~/ comment ~ ")").?).map { + case ((g@Getter(name)), args) => args.fold(g:Accessor)(a => Method(name, a)) + }) ~~ Index) | (Index ~~ "[" ~/ baseExpr.map(ListIndex) ~ "]" ~~ Index) ) val maybeAccessP: P[EXPR] = @@ -238,6 +249,7 @@ object Parser { case (e, (accessStart, a, accessEnd)) => a match { case Getter(n) => GETTER(Pos(start, accessEnd), e, n) + case Method(n, args) => FUNCTION_CALL(Pos(start, accessEnd), n, (e :: args.toList)) case ListIndex(index) => FUNCTION_CALL(Pos(start, accessEnd), PART.VALID(Pos(accessStart, accessEnd), "getElement"), List(e, index)) } } @@ -306,30 +318,42 @@ object Parser { } val baseAtom = comment ~ - P(ifP | matchP | byteVectorP | stringP | numberP | trueP | falseP | block | maybeAccessP) ~ + P(ifP | matchP | block | maybeAccessP) ~ comment - lazy val baseExpr = P(binaryOp(baseAtom, opsByPriority) | baseAtom) + lazy val baseExpr = P(binaryOp(baseAtom, opsByPriority)) val singleBaseAtom = comment ~ - P(ifP | matchP | byteVectorP | stringP | numberP | trueP | falseP | maybeAccessP) ~ + P(ifP | matchP | maybeAccessP) ~ comment - lazy val singleBaseExpr = P(binaryOp(singleBaseAtom, opsByPriority) | singleBaseAtom) + val singleBaseExpr = P(binaryOp(singleBaseAtom, opsByPriority)) - lazy val declaration = P(letP | funcP) + val declaration = P(letP | funcP) + + def revp[A,B](l:A, s:Seq[(B,A)], o:Seq[(A,B)]=Seq.empty) : (Seq[(A,B)], A) = { + s.foldLeft((o,l)) { (acc, op) => (acc, op) match { case ((o,l),(b,a)) => ((l,b) +: o) -> a } } + } - def binaryOp(atom: P[EXPR], rest: List[List[BinaryOperation]]): P[EXPR] = rest match { + def binaryOp(atom: P[EXPR], rest: List[Either[List[BinaryOperation], List[BinaryOperation]]]): P[EXPR] = rest match { case Nil => unaryOp(atom, unaryOps) - case kinds :: restOps => + case Left(kinds) :: restOps => val operand = binaryOp(atom, restOps) val kind = kinds.map(_.parser).reduce((pl, pr) => P(pl | pr)) P(Index ~~ operand ~ P(kind ~ (NoCut(operand) | Index.map(i => INVALID(Pos(i, i), "expected a second operator")))).rep).map { case (start, left: EXPR, r: Seq[(BinaryOperation, EXPR)]) => r.foldLeft(left) { case (acc, (currKind, currOperand)) => currKind.expr(start, currOperand.position.end, acc, currOperand) } } + case Right(kinds) :: restOps => + val operand = binaryOp(atom, restOps) + val kind = kinds.map(_.parser).reduce((pl, pr) => P(pl | pr)) + P(Index ~~ operand ~ P(kind ~ (NoCut(operand) | Index.map(i => INVALID(Pos(i, i), "expected a second operator")))).rep).map { + case (start, left: EXPR, r: Seq[(BinaryOperation, EXPR)]) => + val (ops,s) = revp(left, r) + ops.foldLeft(s) { case (acc, (currOperand, currKind)) => currKind.expr(start, currOperand.position.end, currOperand, acc) } + } } def unaryOp(atom: P[EXPR], ops: List[UnaryOperation]): P[EXPR] = ops.foldRight(atom) { @@ -339,12 +363,12 @@ object Parser { } | acc } - def parseExpr(str: String): core.Parsed[EXPR, Char, String] = P(Start ~ (baseExpr | invalid) ~ End).parse(str) + def parseExpr(str: String): core.Parsed[EXPR, Char, String] = P(Start ~ unusedText ~ (baseExpr | invalid) ~ End).parse(str) def parseContract(str: String): core.Parsed[CONTRACT, Char, String] = - P(Start ~ comment.? ~ (declaration.rep) ~ comment.? ~ (annotatedFunc.rep) ~ End) + P(Start ~ unusedText ~ (declaration.rep) ~ comment ~ (annotatedFunc.rep) ~ End ~~ Index) .map { - case (ds, fs) => CONTRACT(AnyPos, ds.toList, fs.toList) + case (ds, fs, end) => CONTRACT(Pos(0, end), ds.toList, fs.toList) } .parse(str) } diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/ScriptGen.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/ScriptGen.scala index 16d3f6f..211bd05 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/ScriptGen.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/ScriptGen.scala @@ -3,6 +3,7 @@ package com.zbsnetwork.lang.v1.testing import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.lang.v1.parser.BinaryOperation import com.zbsnetwork.lang.v1.parser.BinaryOperation._ +import com.zbsnetwork.lang.v1.parser.Expressions.Pos.AnyPos import com.zbsnetwork.lang.v1.parser.Expressions._ import com.zbsnetwork.lang.v1.parser.Parser.keywords import org.scalacheck._ @@ -12,11 +13,11 @@ import scala.reflect.ClassTag trait ScriptGen { - def CONST_LONGgen: Gen[(EXPR, Long)] = Gen.choose(Long.MinValue, Long.MaxValue).map(v => (CONST_LONG(Pos(0, 0), v), v)) + def CONST_LONGgen: Gen[(EXPR, Long)] = Gen.choose(Long.MinValue, Long.MaxValue).map(v => (CONST_LONG(AnyPos, v), v)) def BOOLgen(gas: Int): Gen[(EXPR, Boolean)] = if (gas > 0) Gen.oneOf(GEgen(gas - 1), GTgen(gas - 1), EQ_INTgen(gas - 1), ANDgen(gas - 1), ORgen(gas - 1), IF_BOOLgen(gas - 1)) - else Gen.const((TRUE(Pos(0, 0)), true)) + else Gen.const((TRUE(AnyPos), true)) def SUMgen(gas: Int): Gen[(EXPR, Long)] = for { @@ -24,9 +25,9 @@ trait ScriptGen { (i2, v2) <- INTGen((gas - 2) / 2) } yield if ((BigInt(v1) + BigInt(v2)).isValidLong) { - (BINARY_OP(Pos(0, 0), i1, SUM_OP, i2), v1 + v2) + (BINARY_OP(AnyPos, i1, SUM_OP, i2), v1 + v2) } else { - (BINARY_OP(Pos(0, 0), i1, SUB_OP, i2), v1 - v2) + (BINARY_OP(AnyPos, i1, SUB_OP, i2), v1 - v2) } def SUBgen(gas: Int): Gen[(EXPR, Long)] = @@ -35,9 +36,9 @@ trait ScriptGen { (i2, v2) <- INTGen((gas - 2) / 2) } yield if ((BigInt(v1) - BigInt(v2)).isValidLong) { - (BINARY_OP(Pos(0, 0), i1, SUB_OP, i2), v1 - v2) + (BINARY_OP(AnyPos, i1, SUB_OP, i2), v1 - v2) } else { - (BINARY_OP(Pos(0, 0), i1, SUM_OP, i2), v1 + v2) + (BINARY_OP(AnyPos, i1, SUM_OP, i2), v1 + v2) } def INTGen(gas: Int): Gen[(EXPR, Long)] = @@ -47,7 +48,7 @@ trait ScriptGen { SUMgen(gas - 1), SUBgen(gas - 1), IF_INTgen(gas - 1), - INTGen(gas - 1).filter(v => (-BigInt(v._2)).isValidLong).map(e => (FUNCTION_CALL(Pos(0, 0), PART.VALID(Pos(0, 0), "-"), List(e._1)), -e._2)) + INTGen(gas - 1).filter(v => (-BigInt(v._2)).isValidLong).map(e => (FUNCTION_CALL(AnyPos, PART.VALID(AnyPos, "-"), List(e._1)), -e._2)) ) else CONST_LONGgen @@ -55,63 +56,63 @@ trait ScriptGen { for { (i1, v1) <- INTGen((gas - 2) / 2) (i2, v2) <- INTGen((gas - 2) / 2) - } yield (BINARY_OP(Pos(0, 0), i1, GE_OP, i2), v1 >= v2) + } yield (BINARY_OP(AnyPos, i1, GE_OP, i2), v1 >= v2) def GTgen(gas: Int): Gen[(EXPR, Boolean)] = for { (i1, v1) <- INTGen((gas - 2) / 2) (i2, v2) <- INTGen((gas - 2) / 2) - } yield (BINARY_OP(Pos(0, 0), i1, GT_OP, i2), v1 > v2) + } yield (BINARY_OP(AnyPos, i1, GT_OP, i2), v1 > v2) def EQ_INTgen(gas: Int): Gen[(EXPR, Boolean)] = for { (i1, v1) <- INTGen((gas - 2) / 2) (i2, v2) <- INTGen((gas - 2) / 2) - } yield (BINARY_OP(Pos(0, 0), i1, EQ_OP, i2), v1 == v2) + } yield (BINARY_OP(AnyPos, i1, EQ_OP, i2), v1 == v2) def ANDgen(gas: Int): Gen[(EXPR, Boolean)] = for { (i1, v1) <- BOOLgen((gas - 2) / 2) (i2, v2) <- BOOLgen((gas - 2) / 2) - } yield (BINARY_OP(Pos(0, 0), i1, AND_OP, i2), v1 && v2) + } yield (BINARY_OP(AnyPos, i1, AND_OP, i2), v1 && v2) def ORgen(gas: Int): Gen[(EXPR, Boolean)] = for { (i1, v1) <- BOOLgen((gas - 2) / 2) (i2, v2) <- BOOLgen((gas - 2) / 2) - } yield (BINARY_OP(Pos(0, 0), i1, OR_OP, i2), v1 || v2) + } yield (BINARY_OP(AnyPos, i1, OR_OP, i2), v1 || v2) def IF_BOOLgen(gas: Int): Gen[(EXPR, Boolean)] = for { (cnd, vcnd) <- BOOLgen((gas - 3) / 3) (t, vt) <- BOOLgen((gas - 3) / 3) (f, vf) <- BOOLgen((gas - 3) / 3) - } yield (IF(Pos(0, 0), cnd, t, f), if (vcnd) { vt } else { vf }) + } yield (IF(AnyPos, cnd, t, f), if (vcnd) { vt } else { vf }) def IF_INTgen(gas: Int): Gen[(EXPR, Long)] = for { (cnd, vcnd) <- BOOLgen((gas - 3) / 3) (t, vt) <- INTGen((gas - 3) / 3) (f, vf) <- INTGen((gas - 3) / 3) - } yield (IF(Pos(0, 0), cnd, t, f), if (vcnd) { vt } else { vf }) + } yield (IF(AnyPos, cnd, t, f), if (vcnd) { vt } else { vf }) def STRgen: Gen[EXPR] = - Gen.identifier.map(PART.VALID[String](Pos(0, 0), _)).map(CONST_STRING(Pos(0, 0), _)) + Gen.identifier.map(PART.VALID[String](AnyPos, _)).map(CONST_STRING(AnyPos, _)) def LETgen(gas: Int): Gen[LET] = for { name <- Gen.identifier (value, _) <- BOOLgen((gas - 3) / 3) - } yield LET(Pos(0, 0), PART.VALID(Pos(0, 0), name), value, Seq.empty) + } yield LET(AnyPos, PART.VALID(AnyPos, name), value, Seq.empty) def REFgen: Gen[EXPR] = - Gen.identifier.filter(!keywords(_)).map(PART.VALID[String](Pos(0, 0), _)).map(REF(Pos(0, 0), _)) + Gen.identifier.filter(!keywords(_)).map(PART.VALID[String](AnyPos, _)).map(REF(AnyPos, _)) def BLOCKgen(gas: Int): Gen[EXPR] = for { let <- LETgen((gas - 3) / 3) body <- Gen.oneOf(BOOLgen((gas - 3) / 3).map(_._1), BLOCKgen((gas - 3) / 3)) // BLOCKGen wasn't add to BOOLGen since issue: NODE-700 - } yield BLOCK(Pos(0, 0), let, body) + } yield BLOCK(AnyPos, let, body) private val spaceChars: Seq[Char] = " \t\n\r" @@ -171,7 +172,7 @@ trait ScriptGenParser extends ScriptGen { override def BOOLgen(gas: Int): Gen[(EXPR, Boolean)] = { if (gas > 0) Gen.oneOf(GEgen(gas - 1), GTgen(gas - 1), EQ_INTgen(gas - 1), ANDgen(gas - 1), ORgen(gas - 1), IF_BOOLgen(gas - 1), REFgen.map(r => (r, false))) - else Gen.const((TRUE(Pos(0, 0)), true)) + else Gen.const((TRUE(AnyPos), true)) } override def INTGen(gas: Int): Gen[(EXPR, Long)] = diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/TypedScriptGen.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/TypedScriptGen.scala index 6fad21d..7e90ee1 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/TypedScriptGen.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/testing/TypedScriptGen.scala @@ -1,7 +1,9 @@ package com.zbsnetwork.lang.v1.testing -import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.lang.contract.{Contract, ContractSerDe} +import com.zbsnetwork.lang.contract.Contract.{CallableAnnotation, CallableFunction, VerifierAnnotation, VerifierFunction} import com.zbsnetwork.lang.v1.FunctionHeader +import com.zbsnetwork.lang.v1.compiler.Terms import com.zbsnetwork.lang.v1.compiler.Terms._ import com.zbsnetwork.lang.v1.compiler.Types._ import com.zbsnetwork.lang.v1.evaluator.FunctionIds._ @@ -9,50 +11,86 @@ import org.scalacheck._ trait TypedScriptGen { + def exprGen = BOOLEANgen(100) + private def letGen = + for { + name <- Gen.alphaStr + expr <- exprGen + } yield Terms.LET(name, expr) + + private def funcGen = + for { + name <- Gen.alphaStr + arg0 <- Gen.alphaStr + args <- Gen.listOf(Gen.alphaStr) + allArgs = arg0 +: args + returned <- Gen.oneOf(allArgs) + } yield Terms.FUNC(name, allArgs, Terms.REF(returned)) + + private def callableGen = + for { + binding <- Gen.alphaStr + fnc <- funcGen + } yield CallableFunction(CallableAnnotation(binding), fnc) + + private def verifierGen = + for { + binding <- Gen.alphaStr + name <- Gen.alphaStr + expr <- exprGen + } yield VerifierFunction(VerifierAnnotation(binding), Terms.FUNC(name, List.empty, expr)) + + def contractGen = + for { + nLets <- Gen.chooseNum(0, 10) + nFuncs <- Gen.chooseNum(0, 10) + nCallables <- Gen.chooseNum(0, 10) + lets <- Gen.listOfN(nLets, letGen) + funcs <- Gen.listOfN(nFuncs, funcGen) + callables <- Gen.listOfN(nCallables, callableGen) + verifier <- Gen.option(verifierGen) + c = Contract(lets ++ funcs, callables, verifier) + if ContractSerDe.serialize(c).size < Short.MaxValue - 3 - 4 + } yield c + def BOOLEANgen(gas: Int): Gen[EXPR] = if (gas > 0) Gen.oneOf(CONST_BOOLEANgen, BLOCK_BOOLEANgen(gas - 1), IF_BOOLEANgen(gas - 1), FUNCTION_CALLgen(BOOLEAN)) else Gen.const(TRUE) - def CONST_BOOLEANgen: Gen[EXPR] = Gen.oneOf(FALSE, TRUE) + private def CONST_BOOLEANgen: Gen[EXPR] = Gen.oneOf(FALSE, TRUE) - def BLOCK_BOOLEANgen(gas: Int): Gen[EXPR] = + private def BLOCK_BOOLEANgen(gas: Int): Gen[EXPR] = for { let <- LETgen((gas - 3) / 3) body <- Gen.oneOf(BOOLEANgen((gas - 3) / 3), BLOCK_BOOLEANgen((gas - 3) / 3)) } yield BLOCK(let, body) - def IF_BOOLEANgen(gas: Int): Gen[EXPR] = + private def IF_BOOLEANgen(gas: Int): Gen[EXPR] = for { cnd <- BOOLEANgen((gas - 3) / 3) t <- BOOLEANgen((gas - 3) / 3) f <- BOOLEANgen((gas - 3) / 3) } yield IF(cnd, t, f) - def LONGgen(gas: Int): Gen[EXPR] = + private def LONGgen(gas: Int): Gen[EXPR] = if (gas > 0) Gen.oneOf(CONST_LONGgen, BLOCK_LONGgen(gas - 1), IF_LONGgen(gas - 1), FUNCTION_CALLgen(LONG)) else CONST_LONGgen - def CONST_LONGgen: Gen[EXPR] = Gen.choose(Long.MinValue, Long.MaxValue).map(CONST_LONG) + private def CONST_LONGgen: Gen[EXPR] = Gen.choose(Long.MinValue, Long.MaxValue).map(CONST_LONG) - def BLOCK_LONGgen(gas: Int): Gen[EXPR] = + private def BLOCK_LONGgen(gas: Int): Gen[EXPR] = for { let <- LETgen((gas - 3) / 3) body <- Gen.oneOf(LONGgen((gas - 3) / 3), BLOCK_LONGgen((gas - 3) / 3)) } yield LET_BLOCK(let, body) - def IF_LONGgen(gas: Int): Gen[EXPR] = + private def IF_LONGgen(gas: Int): Gen[EXPR] = for { cnd <- BOOLEANgen((gas - 3) / 3) t <- LONGgen((gas - 3) / 3) f <- LONGgen((gas - 3) / 3) } yield IF(cnd, t, f) - def STRINGgen: Gen[EXPR] = Gen.identifier.map(CONST_STRING) - - def BYTESTRgen: Gen[EXPR] = Gen.identifier.map(x => CONST_BYTESTR(ByteStr(x.getBytes))) - - def REFgen(tpe: TYPE): Gen[EXPR] = Gen.identifier.map(REF) - - def FUNCTION_CALLgen(resultType: TYPE): Gen[EXPR] = + private def FUNCTION_CALLgen(resultType: TYPE): Gen[EXPR] = Gen.const( FUNCTION_CALL( function = FunctionHeader.Native(SUM_LONG), @@ -60,7 +98,7 @@ trait TypedScriptGen { ) ) - def LETgen(gas: Int): Gen[LET] = + private def LETgen(gas: Int): Gen[LET] = for { name <- Gen.identifier value <- BOOLEANgen((gas - 3) / 3) diff --git a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/traits/domain/Ord.scala b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/traits/domain/Ord.scala index 47c3dba..099e18b 100644 --- a/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/traits/domain/Ord.scala +++ b/lang/shared/src/main/scala/com/zbsnetwork/lang/v1/traits/domain/Ord.scala @@ -14,4 +14,5 @@ case class Ord(id: ByteStr, expiration: Long, matcherFee: Long, bodyBytes: ByteStr, - proofs: IndexedSeq[ByteStr]) + proofs: IndexedSeq[ByteStr], + matcherFeeAssetId: Option[ByteStr] = None) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 39e43bd..850eb1f 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -3,15 +3,19 @@ import sbt._ object Dependencies { - def akkaModule(module: String) = "com.typesafe.akka" %% s"akka-$module" % "2.5.16" + def akkaModule(module: String) = "com.typesafe.akka" %% s"akka-$module" % "2.5.20" def swaggerModule(module: String) = ("io.swagger.core.v3" % s"swagger-$module" % "2.0.5").exclude("com.google.guava", "guava") - def akkaHttpModule(module: String) = "com.typesafe.akka" %% module % "10.1.4" + def akkaHttpModule(module: String = "") = "com.typesafe.akka" %% s"akka-http${if (module.isEmpty) "" else s"-$module"}" % "10.1.7" def nettyModule(module: String) = "io.netty" % s"netty-$module" % "4.1.24.Final" def kamonModule(module: String, v: String) = "io.kamon" %% s"kamon-$module" % v + + val AkkaActor = akkaModule("actor") + val AkkaStream = akkaModule("stream") + val AkkaHTTP = akkaHttpModule() val asyncHttpClient = "org.asynchttpclient" % "async-http-client" % "2.4.7" @@ -27,7 +31,7 @@ object Dependencies { "org.mockito" % "mockito-all" % "1.10.19", "org.scalamock" %% "scalamock-scalatest-support" % "3.6.0", ("org.iq80.leveldb" % "leveldb" % "0.9").exclude("com.google.guava", "guava"), - akkaHttpModule("akka-http-testkit") + akkaHttpModule("testkit") ) lazy val itKit = scalatest ++ Seq( @@ -43,7 +47,7 @@ object Dependencies { "com.typesafe.play" %% "play-json" % "2.6.10" ) - lazy val akka = Seq("actor", "slf4j").map(akkaModule) + lazy val akka = Seq(AkkaActor, akkaModule("slf4j")) lazy val db = Seq( "org.ethereum" % "leveldbjni-all" % "1.18.3" @@ -61,21 +65,21 @@ object Dependencies { "com.github.swagger-akka-http" %% "swagger-akka-http" % "1.0.0", "com.fasterxml.jackson.core" % "jackson-databind" % "2.9.6", "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.9.6", - akkaHttpModule("akka-http") + AkkaHTTP ) lazy val matcher = Seq( akkaModule("persistence"), akkaModule("persistence-tck") % "test", "com.github.dnvriend" %% "akka-persistence-inmemory" % "2.5.15.1" % "test", - "com.typesafe.akka" %% "akka-stream-kafka" % "1.0-RC1", + "com.typesafe.akka" %% "akka-stream-kafka" % "1.0-RC2", "org.ethereum" % "leveldbjni-all" % "1.18.3" ) lazy val metrics = Seq( kamonModule("core", "1.1.3"), kamonModule("system-metrics", "1.0.0").exclude("io.kamon", "kamon-core_2.12"), - kamonModule("akka-2.5", "1.1.1").exclude("io.kamon", "kamon-core_2.12"), + kamonModule("akka-2.5", "1.1.3").exclude("io.kamon", "kamon-core_2.12"), kamonModule("influxdb", "1.0.2"), "org.influxdb" % "influxdb-java" % "2.11" ).map(_.exclude("org.asynchttpclient", "async-http-client")) @@ -110,4 +114,19 @@ object Dependencies { ) lazy val kindProjector = "org.spire-math" %% "kind-projector" % "0.9.6" lazy val betterFor = "com.olegpy" %% "better-monadic-for" % "0.3.0-M4" + + lazy val protobuf = Def.setting { + val version = scalapb.compiler.Version.scalapbVersion + Seq( + // "com.google.protobuf" % "protobuf-java" % "3.4.0", + "com.thesamet.scalapb" %%% "scalapb-runtime" % version, + "com.thesamet.scalapb" %%% "scalapb-runtime" % version % "protobuf", + "com.thesamet.scalapb" %% "scalapb-json4s" % "0.7.0" + ) + } + + lazy val grpc = Seq( + "io.grpc" % "grpc-netty" % scalapb.compiler.Version.grpcJavaVersion, + "com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapb.compiler.Version.scalapbVersion + ) } diff --git a/project/build.properties b/project/build.properties index 7c58a83..4f2bf11 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1,2 @@ sbt.version=1.2.6 +project.version=0.16.2 diff --git a/project/plugins.sbt b/project/plugins.sbt index 9e182c4..dc2499b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,6 +5,13 @@ resolvers ++= Seq( Resolver.sbtPluginRepo("releases") ) +// Should go before Scala.js +addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.19") +addSbtPlugin("org.ensime" % "sbt-ensime" % "2.5.1") + + +libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.8.4" + Seq( "com.eed3si9n" % "sbt-assembly" % "0.14.5", "com.typesafe.sbt" % "sbt-native-packager" % "1.3.2", diff --git a/release-notes.md b/release-notes.md deleted file mode 100644 index 5fbf1d7..0000000 --- a/release-notes.md +++ /dev/null @@ -1,67 +0,0 @@ -**0.6.0** - -* The DEX's Order Match transaction has been changed. This is the main reason for restarting Testnet. Now, a second asset of transaction's pair is used to set an amount of the transaction. -* LPOS was implemented. New Leasing and Leasing Cancel transactions were added. -* New, HOCON based, configuration file. Old configuration file (JSON based) is supported in this release for backward compatibility. Automatic configuration file conversion added to DEB packages. - -**0.3.2** - -* By default walletDir and dataDir located in $HOME/zbs - -**0.3.1** - -* HTTP API /scorex removed. Use /node instead. - -**0.2.2** - -* Switch network by "testnet" in settings. Default value is true." -* /scorex/* HTTP API deprecated. Use /node/* instead. -* All logs goes to stdout and stderr. Use "loggingLevel" in config. - -**0.2.1** - -* peers.dat format changed. Delete old version. -* Different HTTP status codes in replies in HTTP API were implemented -* Zbs' Scorex v1.3.2 - -**0.2.0** - -* Peers blacklist ttl configuration via "p2p"/"blacklistResidenceTimeMilliseconds" -* Upgrade to Zbs' Scorex v1.3.1 - -**0.2.0-RC7** - -* New API /zbs/payment returns senderPublicKey -* New API /zbs/create-signed-payment -* /zbs/external-payment deprecated. - Use new /zbs/broadcast-signed-payment. -* New API /zbs/payment/signature -* minimumTxFee verification for API - -**0.2.0-RC5** - -* /zbs/external-payment returns error for incorrect recipient - -**0.2.0-RC4** - -* Fixed issue with incorrect Handshake -* Balance with confirmations is the minimum balance -* /zbs/external-payment returns error if account balance invalid -* New API method /consensus/generatingbalance/{address} - -**0.2.0-RC3** - -* Incompatible with 0.1.3 -* Upgrade to Scorex 1.2.8 -* New Address format -* New hash chain for Address - Blake2b256, Keccak32 -* New Testnet Genesis - -**0.1.3** - -* Upgrade to Scorex 1.2.6. -* New http api method /external-payment for lite client - -**0.1.2** - -* Upgrade to Scorex 1.2.4. Clean /scorex/zbs/data/ before run. diff --git a/src/main/protobuf/address.proto b/src/main/protobuf/address.proto new file mode 100644 index 0000000..98d4d5a --- /dev/null +++ b/src/main/protobuf/address.proto @@ -0,0 +1,15 @@ +// Transactions +syntax = "proto3"; +option java_package = "com.zbsnetwork.protobuf.account"; + +message Alias { + bytes chainId = 1; + string name = 2; +}; + +message Recipient { + oneof recipient { + bytes address = 1; + Alias alias = 2; + }; +}; \ No newline at end of file diff --git a/src/main/protobuf/block.proto b/src/main/protobuf/block.proto new file mode 100644 index 0000000..dabfa16 --- /dev/null +++ b/src/main/protobuf/block.proto @@ -0,0 +1,25 @@ +// Transactions +syntax = "proto3"; +option java_package = "com.zbsnetwork.protobuf.block"; +import "transactions.proto"; + +message Block { + message SignedHeader { + Header header = 1; + bytes signature = 8; + } + + message Header { + bytes reference = 1; + int64 baseTarget = 2; + bytes generationSignature = 3; + repeated uint32 featureVotes = 4; + int64 timestamp = 5; + int32 version = 6; + bytes generator = 7; + } + + bytes chainId = 1; + SignedHeader header = 2; + repeated SignedTransaction transactions = 3; +} \ No newline at end of file diff --git a/src/main/protobuf/scripts.proto b/src/main/protobuf/scripts.proto new file mode 100644 index 0000000..f9b9d30 --- /dev/null +++ b/src/main/protobuf/scripts.proto @@ -0,0 +1,7 @@ +// Transactions +syntax = "proto3"; +option java_package = "com.zbsnetwork.protobuf.transaction.smart.script"; + +message Script { + bytes bytes = 1; +}; \ No newline at end of file diff --git a/src/main/protobuf/transactions.proto b/src/main/protobuf/transactions.proto new file mode 100644 index 0000000..6bd2a31 --- /dev/null +++ b/src/main/protobuf/transactions.proto @@ -0,0 +1,180 @@ +// Transactions +syntax = "proto3"; +option java_package = "com.zbsnetwork.protobuf.transaction"; +import "scripts.proto"; +import "address.proto"; + +message AssetAmount { + bytes assetId = 1; + int64 amount = 2; +} + +message Amount { + oneof amount { + int64 zbsAmount = 1; + AssetAmount assetAmount = 2; + } +} + +message SignedTransaction { + Transaction transaction = 1; + repeated bytes proofs = 2; +} + +message Transaction { + bytes chainId = 1; + bytes senderPublicKey = 2; + Amount fee = 3; + int64 timestamp = 4; + int32 version = 5; + + oneof data { + GenesisTransactionData genesis = 101; + PaymentTransactionData payment = 102; + IssueTransactionData issue = 103; + TransferTransactionData transfer = 104; + ReissueTransactionData reissue = 105; + BurnTransactionData burn = 106; + ExchangeTransactionData exchange = 107; + LeaseTransactionData lease = 108; + LeaseCancelTransactionData leaseCancel = 109; + CreateAliasTransactionData createAlias = 110; + MassTransferTransactionData massTransfer = 111; + DataTransactionData dataTransaction = 112; + SetScriptTransactionData setScript = 113; + SponsorFeeTransactionData sponsorFee = 114; + SetAssetScriptTransactionData setAssetScript = 115; + // TODO: 116 = contract invocation + }; +}; + +message GenesisTransactionData { + bytes recipientAddress = 1; + int64 amount = 2; +}; + +message PaymentTransactionData { + bytes address = 1; + int64 amount = 2; +}; + +message TransferTransactionData { + Recipient recipient = 1; + Amount amount = 2; + bytes attachment = 3; +}; + +message CreateAliasTransactionData { + string alias = 1; +}; + +message DataTransactionData { + message DataEntry { + string key = 1; + oneof value { + int64 intValue = 10; + bool boolValue = 11; + bytes binaryValue = 12; + string stringValue = 13; + }; + }; + + repeated DataEntry data = 1; +}; + +message MassTransferTransactionData { + message Transfer { + Recipient address = 1; + int64 amount = 2; + }; + + bytes assetId = 1; + repeated Transfer transfers = 2; + bytes attachment = 3; +}; + +message LeaseTransactionData { + Recipient recipient = 1; + int64 amount = 2; +}; + +message LeaseCancelTransactionData { + bytes leaseId = 1; +}; + +message BurnTransactionData { + AssetAmount assetAmount = 1;; +}; + +message IssueTransactionData { + bytes name = 1; + bytes description = 2; + int64 amount = 3; + int32 decimals = 4; + bool reissuable = 5; + Script script = 6; +}; + + +message ReissueTransactionData { + AssetAmount assetAmount = 1; + bool reissuable = 2; +}; + +message SetAssetScriptTransactionData { + bytes assetId = 1; + Script script = 2; +}; + +message SetScriptTransactionData { + Script script = 2; +}; + +message ExchangeTransactionData { + message BuySellOrders { + Order buyOrder = 1; + Order sellOrder = 2; + } + + message MakerTakerOrders { + Order makerOrder = 1; + Order takerOrder = 2; + } + + message Order { + enum Side { + BUY = 0; + SELL = 1; + }; + + message AssetPair { + bytes amountAssetId = 1; + bytes priceAssetId = 2; + }; + + bytes senderPublicKey = 1; + bytes matcherPublicKey = 2; + AssetPair assetPair = 3; + Side orderSide = 4; + int64 amount = 5; + int64 price = 6; + int64 timestamp = 7; + int64 expiration = 8; + Amount matcherFee = 9; + int32 version = 10; + repeated bytes proofs = 11; + }; + + oneof orders { + BuySellOrders buySellOrders = 1; + MakerTakerOrders makerTakerOrders = 2; + } + int64 amount = 3; + int64 price = 4; + int64 buyMatcherFee = 5; + int64 sellMatcherFee = 6; +}; + +message SponsorFeeTransactionData { + AssetAmount minFee = 1; +}; \ No newline at end of file diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 7364fde..4463027 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -234,15 +234,9 @@ zbs { # Maximum allowed amount of orders retrieved via REST rest-order-limit = 100 - # A new order should have timestamp more than (previous_order_timestamp - order-timestamp-drift) - order-timestamp-drift = 1m - # Base assets used as price assets price-assets: [] - # Maximum difference with Matcher server time - max-timestamp-diff = 30d - # Blacklisted assets id blacklisted-assets: [] @@ -410,6 +404,15 @@ zbs { # Max time for buffer. When time is out, the node processes all transactions in batch max-buffer-time = 100ms + + # Max scheduler parallelism + parallelism = 4 + + # Max scheduler threads + max-threads = 8 + + # Max pending queue size + max-queue-size = 5000 } # MicroBlock synchronizer settings @@ -431,8 +434,6 @@ zbs { max-size = 100000 # Pool size in bytes max-bytes-size = 52428800 // 50 MB - # Utx cleanup task interval - cleanup-interval = 5m # Blacklist transactions from these addresses (Base58 strings) blacklist-sender-addresses = [] # Allow transfer transactions from the blacklisted addresses to these recipients (Base58 strings) @@ -604,7 +605,9 @@ akka { group.id = "0" auto.offset.reset = "earliest" enable.auto.commit = false - # max.poll.records = 10 # Should be <= ${zbs.matcher.events-queue.kafka.consumer.buffer-size} + session.timeout.ms = 10000 + max.poll.interval.ms = 11000 + max.poll.records = 100 # Should be <= ${zbs.matcher.events-queue.kafka.consumer.buffer-size} } # Time to wait for pending requests when a partition is closed @@ -644,6 +647,8 @@ akka { kafka-clients { bootstrap.servers = ${akka.kafka.consumer.kafka-clients.bootstrap.servers} + acks = all + # Buffer messages into a batch for this duration linger.ms = 5 diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 010af84..1f81dd9 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -41,6 +41,7 @@ + diff --git a/src/main/scala/com/zbsnetwork/Application.scala b/src/main/scala/com/zbsnetwork/Application.scala index f0cd1e8..c7e9b53 100644 --- a/src/main/scala/com/zbsnetwork/Application.scala +++ b/src/main/scala/com/zbsnetwork/Application.scala @@ -29,6 +29,7 @@ import com.zbsnetwork.network.RxExtensionLoader.RxExtensionLoaderShutdownHook import com.zbsnetwork.network._ import com.zbsnetwork.settings._ import com.zbsnetwork.state.appender.{BlockAppender, ExtensionAppender, MicroblockAppender} +import com.zbsnetwork.transaction.AssetId import com.zbsnetwork.utils.{NTP, ScorexLogging, SystemInformationReporter, forceStopApplication} import com.zbsnetwork.utx.{UtxPool, UtxPoolImpl} import com.zbsnetwork.wallet.Wallet @@ -58,9 +59,9 @@ class Application(val actorSystem: ActorSystem, val settings: ZbsSettings, confi private val LocalScoreBroadcastDebounce = 1.second - private val portfolioChanged = ConcurrentSubject.publish[Address] + private val spendableBalanceChanged = ConcurrentSubject.publish[(Address, Option[AssetId])] - private val blockchainUpdater = StorageFactory(settings, db, time, portfolioChanged) + private val blockchainUpdater = StorageFactory(settings, db, time, spendableBalanceChanged) private lazy val upnp = new UPnP(settings.networkSettings.uPnPSettings) // don't initialize unless enabled @@ -101,11 +102,11 @@ class Application(val actorSystem: ActorSystem, val settings: ZbsSettings, confi val establishedConnections = new ConcurrentHashMap[Channel, PeerInfo] val allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE) val utxStorage = - new UtxPoolImpl(time, blockchainUpdater, portfolioChanged, settings.blockchainSettings.functionalitySettings, settings.utxSettings) + new UtxPoolImpl(time, blockchainUpdater, spendableBalanceChanged, settings.blockchainSettings.functionalitySettings, settings.utxSettings) maybeUtx = Some(utxStorage) matcher = if (settings.matcherSettings.enable) { - Matcher(actorSystem, time, wallet, utxStorage, allChannels, blockchainUpdater, portfolioChanged, settings) + Matcher(actorSystem, time, wallet, utxStorage, allChannels, blockchainUpdater, spendableBalanceChanged, settings) } else None val knownInvalidBlocks = new InvalidBlockStorageImpl(settings.synchronizationSettings.invalidBlocksStorage) @@ -179,11 +180,16 @@ class Application(val actorSystem: ActorSystem, val settings: ZbsSettings, confi rxExtensionLoaderShutdown = Some(sh) UtxPoolSynchronizer.start(utxStorage, settings.synchronizationSettings.utxSynchronizerSettings, allChannels, transactions) - val microBlockSink = microblockDatas.mapTask(scala.Function.tupled(processMicroBlock)) - val blockSink = newBlocks.mapTask(scala.Function.tupled(processBlock)) + + val microBlockSink = microblockDatas + .mapTask(scala.Function.tupled(processMicroBlock)) + + val blockSink = newBlocks + .mapTask(scala.Function.tupled(processBlock)) Observable.merge(microBlockSink, blockSink).subscribe() miner.scheduleMining() + utxStorage.cleanup.runCleanupOn(blockSink) for (addr <- settings.networkSettings.declaredAddress if settings.networkSettings.uPnPSettings.enable) { upnp.addPort(addr.getPort) @@ -279,7 +285,7 @@ class Application(val actorSystem: ActorSystem, val settings: ZbsSettings, confi if (!shutdownInProgress) { shutdownInProgress = true - portfolioChanged.onComplete() + spendableBalanceChanged.onComplete() utx.close() shutdownAndWait(historyRepliesScheduler, "HistoryReplier", 5.minutes) diff --git a/src/main/scala/com/zbsnetwork/Importer.scala b/src/main/scala/com/zbsnetwork/Importer.scala index b942748..6cdeaf0 100644 --- a/src/main/scala/com/zbsnetwork/Importer.scala +++ b/src/main/scala/com/zbsnetwork/Importer.scala @@ -12,8 +12,9 @@ import com.zbsnetwork.db.openDB import com.zbsnetwork.history.StorageFactory import com.zbsnetwork.mining.MultiDimensionalMiningConstraint import com.zbsnetwork.settings.{ZbsSettings, loadConfig} +import com.zbsnetwork.state.Portfolio import com.zbsnetwork.state.appender.BlockAppender -import com.zbsnetwork.transaction.Transaction +import com.zbsnetwork.transaction.{AssetId, Transaction} import com.zbsnetwork.utils._ import com.zbsnetwork.utx.UtxPool import monix.execution.{Scheduler, UncaughtExceptionReporter} @@ -49,15 +50,15 @@ object Importer extends ScorexLogging { implicit val scheduler: Scheduler = Scheduler.singleThread("appender") val utxPoolStub = new UtxPool { - override def putIfNew(tx: Transaction) = ??? - override def removeAll(txs: Traversable[Transaction]): Unit = {} - override def accountPortfolio(addr: Address) = ??? - override def portfolio(addr: Address) = ??? - override def all = ??? - override def size = ??? - override def transactionById(transactionId: ByteStr) = ??? - override def packUnconfirmed(rest: MultiDimensionalMiningConstraint) = ??? - override def close(): Unit = {} + override def putIfNew(tx: Transaction) = ??? + override def removeAll(txs: Traversable[Transaction]): Unit = {} + override def spendableBalance(addr: Address, assetId: Option[AssetId]): Long = ??? + override def pessimisticPortfolio(addr: Address): Portfolio = ??? + override def all = ??? + override def size = ??? + override def transactionById(transactionId: ByteStr) = ??? + override def packUnconfirmed(rest: MultiDimensionalMiningConstraint) = ??? + override def close(): Unit = {} } val time = new NTP(settings.ntpServer) diff --git a/src/main/scala/com/zbsnetwork/account/Address.scala b/src/main/scala/com/zbsnetwork/account/Address.scala index 97bb9ae..f5125d9 100644 --- a/src/main/scala/com/zbsnetwork/account/Address.scala +++ b/src/main/scala/com/zbsnetwork/account/Address.scala @@ -30,11 +30,13 @@ object Address extends ScorexLogging { private class AddressImpl(val bytes: ByteStr) extends Address + private[this] def createUnsafe(address: ByteStr): Address = new AddressImpl(address) + def fromPublicKey(publicKey: Array[Byte], chainId: Byte = scheme.chainId): Address = { val publicKeyHash = crypto.secureHash(publicKey) val withoutChecksum = ByteBuffer.allocate(1 + 1 + HashLength).put(AddressVersion).put(chainId).put(publicKeyHash, 0, HashLength).array() val bytes = ByteBuffer.allocate(AddressLength).put(withoutChecksum).put(crypto.secureHash(withoutChecksum), 0, ChecksumLength).array() - new AddressImpl(ByteStr(bytes)) + createUnsafe(bytes) } def fromBytes(addressBytes: Array[Byte], chainId: Byte = scheme.chainId): Either[InvalidAddress, Address] = { @@ -49,7 +51,7 @@ object Address extends ScorexLogging { checkSum = addressBytes.takeRight(ChecksumLength) checkSumGenerated = calcCheckSum(addressBytes.dropRight(ChecksumLength)) _ <- Either.cond(checkSum.sameElements(checkSumGenerated), (), s"Bad address checksum") - } yield new AddressImpl(ByteStr(addressBytes))).left.map(InvalidAddress) + } yield createUnsafe(addressBytes)).left.map(InvalidAddress) } def fromString(addressStr: String): Either[ValidationError, Address] = { diff --git a/src/main/scala/com/zbsnetwork/account/Alias.scala b/src/main/scala/com/zbsnetwork/account/Alias.scala index 2ec2ba1..808f9fb 100644 --- a/src/main/scala/com/zbsnetwork/account/Alias.scala +++ b/src/main/scala/com/zbsnetwork/account/Alias.scala @@ -24,13 +24,12 @@ object Alias { private val AliasPatternInfo = "Alias string pattern is 'alias::" - private def currentChainId: Byte = AddressScheme.current.chainId + private[this] def currentChainId: Byte = AddressScheme.current.chainId - private def validAliasChar(c: Char): Boolean = + private[this] def validAliasChar(c: Char): Boolean = ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || c == '_' || c == '@' || c == '-' || c == '.' - private def buildAlias(chainId: Byte, name: String): Either[ValidationError, Alias] = { - + private[zbsnetwork] def buildAlias(chainId: Byte, name: String): Either[ValidationError, Alias] = { case class AliasImpl(chainId: Byte, name: String) extends Alias if (name.length < MinLength || MaxLength < name.length) diff --git a/src/main/scala/com/zbsnetwork/account/PublicKeyAccount.scala b/src/main/scala/com/zbsnetwork/account/PublicKeyAccount.scala index 5bad24c..ef105b7 100644 --- a/src/main/scala/com/zbsnetwork/account/PublicKeyAccount.scala +++ b/src/main/scala/com/zbsnetwork/account/PublicKeyAccount.scala @@ -1,9 +1,9 @@ package com.zbsnetwork.account import com.zbsnetwork.common.utils.Base58 -import com.zbsnetwork.utils.base58Length -import com.zbsnetwork.transaction.ValidationError.InvalidAddress import com.zbsnetwork.crypto._ +import com.zbsnetwork.transaction.ValidationError.InvalidAddress +import com.zbsnetwork.utils.base58Length trait PublicKeyAccount { def publicKey: Array[Byte] @@ -19,6 +19,7 @@ trait PublicKeyAccount { } object PublicKeyAccount { + val empty = apply(Array.emptyByteArray) val KeyStringLength: Int = base58Length(KeyLength) diff --git a/src/main/scala/com/zbsnetwork/api/http/AddressApiRoute.scala b/src/main/scala/com/zbsnetwork/api/http/AddressApiRoute.scala index dc0af68..417579d 100644 --- a/src/main/scala/com/zbsnetwork/api/http/AddressApiRoute.scala +++ b/src/main/scala/com/zbsnetwork/api/http/AddressApiRoute.scala @@ -20,6 +20,7 @@ import com.zbsnetwork.wallet.Wallet import io.netty.channel.group.ChannelGroup import io.swagger.annotations._ import javax.ws.rs.Path + import play.api.libs.json._ import scala.util.{Failure, Success, Try} @@ -349,13 +350,13 @@ case class AddressApiRoute(settings: RestAPISettings, Balance( acc.address, 0, - blockchain.portfolio(acc).balance + blockchain.balance(acc) ))) .getOrElse(InvalidAddress) } private def balancesDetailsJson(account: Address): BalanceDetails = { - val portfolio = blockchain.portfolio(account) + val portfolio = blockchain.zbsPortfolio(account) BalanceDetails( account.address, portfolio.balance, diff --git a/src/main/scala/com/zbsnetwork/api/http/CompositeHttpService.scala b/src/main/scala/com/zbsnetwork/api/http/CompositeHttpService.scala index fef18a7..54dd7db 100644 --- a/src/main/scala/com/zbsnetwork/api/http/CompositeHttpService.scala +++ b/src/main/scala/com/zbsnetwork/api/http/CompositeHttpService.scala @@ -7,7 +7,7 @@ import akka.http.scaladsl.model.{HttpRequest, StatusCodes} import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.RouteResult.Complete import akka.http.scaladsl.server.directives.{DebuggingDirectives, LoggingMagnet} -import akka.http.scaladsl.server.{Directive0, Route, RouteResult} +import akka.http.scaladsl.server.{Route, RouteResult} import akka.stream.ActorMaterializer import com.zbsnetwork.api.http.swagger.SwaggerDocService import com.zbsnetwork.settings.RestAPISettings @@ -16,45 +16,39 @@ import com.zbsnetwork.utils.ScorexLogging case class CompositeHttpService(apiTypes: Set[Class[_]], routes: Seq[ApiRoute], settings: RestAPISettings)(implicit system: ActorSystem) extends ScorexLogging { - val swaggerService = new SwaggerDocService(system, ActorMaterializer()(system), apiTypes, settings) - - def withCors: Directive0 = - if (settings.cors) - respondWithHeader(`Access-Control-Allow-Origin`.*) - else pass - - private val headers: scala.collection.immutable.Seq[String] = scala.collection.immutable.Seq("Authorization", - "Content-Type", - "X-Requested-With", - "Timestamp", - "x-api-key", - "Signature") ++ - (if (settings.apiKeyDifferentHost) Seq("api_key", "X-API-Key") else Seq.empty[String]) - - val compositeRoute: Route = - withCors(routes.map(_.route).reduce(_ ~ _)) ~ - swaggerService.routes ~ - (pathEndOrSingleSlash | path("swagger")) { - redirect("/api-docs/index.html", StatusCodes.PermanentRedirect) - } ~ - pathPrefix("api-docs") { - pathEndOrSingleSlash { - redirect("/api-docs/index.html", StatusCodes.PermanentRedirect) - } ~ - getFromResourceDirectory("swagger-ui") - } ~ options { - respondWithDefaultHeaders(`Access-Control-Allow-Credentials`(true), - `Access-Control-Allow-Headers`(headers), - `Access-Control-Allow-Methods`(OPTIONS, POST, PUT, GET, DELETE))(withCors(complete(StatusCodes.OK))) - } ~ complete(StatusCodes.NotFound) - - def logRequestResponse(req: HttpRequest)(res: RouteResult): Unit = res match { + private val swaggerService = new SwaggerDocService(system, ActorMaterializer()(system), apiTypes, settings) + private val redirectToSwagger = redirect("/api-docs/index.html", StatusCodes.PermanentRedirect) + private val swaggerRoute: Route = swaggerService.routes ~ + (pathEndOrSingleSlash | path("swagger"))(redirectToSwagger) ~ + pathPrefix("api-docs") { + pathEndOrSingleSlash(redirectToSwagger) ~ getFromResourceDirectory("swagger-ui") + } + + val compositeRoute: Route = extendRoute(routes.map(_.route).reduce(_ ~ _)) ~ swaggerRoute ~ complete(StatusCodes.NotFound) + val loggingCompositeRoute: Route = DebuggingDirectives.logRequestResult(LoggingMagnet(_ => logRequestResponse))(compositeRoute) + + private def logRequestResponse(req: HttpRequest)(res: RouteResult): Unit = res match { case Complete(resp) => val msg = s"HTTP ${resp.status.value} from ${req.method.value} ${req.uri}" - if (resp.status == StatusCodes.OK) log.debug(msg) else log.warn(msg) + if (resp.status == StatusCodes.OK) log.info(msg) else log.warn(msg) case _ => } - val loggingCompositeRoute: Route = - DebuggingDirectives.logRequestResult(LoggingMagnet(_ => logRequestResponse))(compositeRoute) + private val corsAllowedHeaders = (if (settings.apiKeyDifferentHost) List("api_key", "X-API-Key") else List.empty[String]) ++ + Seq("Authorization", "Content-Type", "X-Requested-With", "Timestamp", "Signature") + + private def corsAllowAll = if (settings.cors) respondWithHeader(`Access-Control-Allow-Origin`.*) else pass + + private def extendRoute(base: Route): Route = + if (settings.cors) { ctx => + val extendedRoute = corsAllowAll(base) ~ options { + respondWithDefaultHeaders( + `Access-Control-Allow-Credentials`(true), + `Access-Control-Allow-Headers`(corsAllowedHeaders), + `Access-Control-Allow-Methods`(OPTIONS, POST, PUT, GET, DELETE) + )(corsAllowAll(complete(StatusCodes.OK))) + } + + extendedRoute(ctx) + } else base } diff --git a/src/main/scala/com/zbsnetwork/api/http/ContractInvocationRequest.scala b/src/main/scala/com/zbsnetwork/api/http/ContractInvocationRequest.scala index 6e7dae4..7513ada 100644 --- a/src/main/scala/com/zbsnetwork/api/http/ContractInvocationRequest.scala +++ b/src/main/scala/com/zbsnetwork/api/http/ContractInvocationRequest.scala @@ -1,35 +1,56 @@ package com.zbsnetwork.api.http import cats.implicits._ -import com.zbsnetwork.account.{Address, PublicKeyAccount} import com.zbsnetwork.lang.v1.FunctionHeader import com.zbsnetwork.lang.v1.compiler.Terms._ -import com.zbsnetwork.state.{BinaryDataEntry, BooleanDataEntry, DataEntry, IntegerDataEntry, StringDataEntry} +import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.account.{Address, PublicKeyAccount} import com.zbsnetwork.transaction.smart.ContractInvocationTransaction import com.zbsnetwork.transaction.{Proofs, ValidationError} import io.swagger.annotations.{ApiModel, ApiModelProperty} -import play.api.libs.json.Json +import play.api.libs.json._ import scala.annotation.meta.field object ContractInvocationRequest { - case class FunctionCallPart(function: String, args: List[DataEntry[_]]) - implicit val functionCallReads = Json.reads[FunctionCallPart] + case class FunctionCallPart(function: String, args: List[EVALUATED]) + implicit val EvaluatedReads = new Reads[EVALUATED] { + def reads(jv: JsValue): JsResult[EVALUATED] = { + jv \ "type" match { + case JsDefined(JsString("integer")) => + jv \ "value" match { + case JsDefined(JsNumber(n)) => JsSuccess(CONST_LONG(n.toLong)) + case _ => JsError("value is missing or not an integer") + } + case JsDefined(JsString("boolean")) => + jv \ "value" match { + case JsDefined(JsBoolean(n)) => JsSuccess(CONST_BOOLEAN(n)) + case _ => JsError("value is missing or not an boolean") + } + case JsDefined(JsString("string")) => + jv \ "value" match { + case JsDefined(JsString(n)) => JsSuccess(CONST_STRING(n)) + case _ => JsError("value is missing or not an string") + } + case JsDefined(JsString("binary")) => + jv \ "value" match { + case JsDefined(JsString(n)) => + ByteStr.decodeBase64(n).fold(ex => JsError(ex.getMessage), bstr => JsSuccess(CONST_BYTESTR(bstr))) + case _ => JsError("value is missing or not an base64 encoded string") + } + case _ => JsError("type is missing") + } + } + } + + implicit val functionCallReads = Json.reads[FunctionCallPart] implicit val unsignedContractInvocationRequestReads = Json.reads[ContractInvocationRequest] implicit val signedContractInvocationRequestReads = Json.reads[SignedContractInvocationRequest] def buildFunctionCall(fc: FunctionCallPart): FUNCTION_CALL = - FUNCTION_CALL( - FunctionHeader.User(fc.function), - fc.args.map { - case BooleanDataEntry(_, b) => CONST_BOOLEAN(b) - case StringDataEntry(_, b) => CONST_STRING(b) - case IntegerDataEntry(_, b) => CONST_LONG(b) - case BinaryDataEntry(_, b) => CONST_BYTESTR(b) - } - ) + FUNCTION_CALL(FunctionHeader.User(fc.function), fc.args) } case class ContractInvocationRequest( diff --git a/src/main/scala/com/zbsnetwork/api/http/TransactionsApiRoute.scala b/src/main/scala/com/zbsnetwork/api/http/TransactionsApiRoute.scala index 7cc14ef..5d5a6cc 100644 --- a/src/main/scala/com/zbsnetwork/api/http/TransactionsApiRoute.scala +++ b/src/main/scala/com/zbsnetwork/api/http/TransactionsApiRoute.scala @@ -3,7 +3,7 @@ package com.zbsnetwork.api.http import akka.http.scaladsl.marshalling.ToResponseMarshallable import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server.Route -import com.zbsnetwork.account.{Address, AddressOrAlias} +import com.zbsnetwork.account.Address import com.zbsnetwork.api.http.DataRequest._ import com.zbsnetwork.api.http.alias.{CreateAliasV1Request, CreateAliasV2Request} import com.zbsnetwork.api.http.assets.SponsorFeeRequest._ @@ -28,8 +28,6 @@ import io.swagger.annotations._ import javax.ws.rs.Path import play.api.libs.json._ -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future import scala.util.Success @Path("/transactions") @@ -278,60 +276,43 @@ case class TransactionsApiRoute(settings: RestAPISettings, } } - /** - * Produces compact representation for large transactions by stripping unnecessary data. - * Currently implemented for MassTransfer transaction only. - */ - private def txToCompactJson(address: Address, addresses: Set[AddressOrAlias], tx: Transaction): JsObject = { - import com.zbsnetwork.transaction.transfer._ - tx match { - case mtt: MassTransferTransaction if mtt.sender.toAddress != address => - mtt.compactJson(addresses) - case _ => txToExtendedJson(tx) - } - } + def transactionsByAddress(addressParam: String, limitParam: Int, maybeAfterParam: Option[String]): Either[ApiError, JsArray] = { + def createTransactionsJsonArray(address: Address, limit: Int, fromId: Option[ByteStr]): Either[String, JsArray] = { + lazy val addressesCached = concurrent.blocking((blockchain.aliasesOfAddress(address) :+ address).toSet) - def getResponse(address: Address, limit: Int, fromId: Option[ByteStr]): Either[String, JsArray] = { - lazy val aoa = blockchain.aliasesOfAddress(address) :+ address - - val txs = - blockchain - .addressTransactions(address, Set.empty, limit, fromId) - - val json = - txs.map { txSeq => - txSeq.map { htx => - txToCompactJson(address, aoa.toSet, htx._2) + ("height" -> JsNumber(htx._1)) + /** + * Produces compact representation for large transactions by stripping unnecessary data. + * Currently implemented for MassTransfer transaction only. + */ + def txToCompactJson(address: Address, tx: Transaction): JsObject = { + import com.zbsnetwork.transaction.transfer._ + tx match { + case mtt: MassTransferTransaction if mtt.sender.toAddress != address => mtt.compactJson(addressesCached) + case _ => txToExtendedJson(tx) } } - json.map(txs => Json.arr(JsArray(txs))) - } - - def transactionsByAddress(addressParam: String, limitParam: Int, maybeAfterParam: Option[String]): Future[ToResponseMarshallable] = - Future { - val result = for { - address <- Address.fromString(addressParam).left.map(ApiError.fromValidationError) - limit <- Either.cond(limitParam < settings.transactionByAddressLimit, limitParam, TooBigArrayAllocation) - maybeAfter <- maybeAfterParam match { - case Some(v) => - ByteStr - .decodeBase58(v) - .fold( - _ => Left(CustomValidationError(s"Unable to decode transaction id $v")), - id => Right(Some(id)) - ) - case None => Right(None) - } - result <- getResponse(address, limit, maybeAfter).fold( - err => Left(CustomValidationError(err)), - arr => Right(arr) - ) - } yield result + val txs = concurrent.blocking(blockchain.addressTransactions(address, Set.empty, limit, fromId)) + txs.map(txs => Json.arr(JsArray(txs.map { case (height, tx) => txToCompactJson(address, tx) + ("height" -> JsNumber(height)) }))) + } - result match { - case Right(arr) => arr: ToResponseMarshallable - case Left(err) => err: ToResponseMarshallable + for { + address <- Address.fromString(addressParam).left.map(ApiError.fromValidationError) + limit <- Either.cond(limitParam <= settings.transactionByAddressLimit, limitParam, TooBigArrayAllocation) + maybeAfter <- maybeAfterParam match { + case Some(v) => + ByteStr + .decodeBase58(v) + .fold( + _ => Left(CustomValidationError(s"Unable to decode transaction id $v")), + id => Right(Some(id)) + ) + case None => Right(None) } - } + result <- createTransactionsJsonArray(address, limit, maybeAfter).fold( + err => Left(CustomValidationError(err)), + arr => Right(arr) + ) + } yield result + } } diff --git a/src/main/scala/com/zbsnetwork/api/http/UtilsApiRoute.scala b/src/main/scala/com/zbsnetwork/api/http/UtilsApiRoute.scala index ea49cf0..0cf096b 100644 --- a/src/main/scala/com/zbsnetwork/api/http/UtilsApiRoute.scala +++ b/src/main/scala/com/zbsnetwork/api/http/UtilsApiRoute.scala @@ -26,7 +26,7 @@ case class UtilsApiRoute(timeService: Time, settings: RestAPISettings) extends A } override val route: Route = pathPrefix("utils") { - decompile ~ compile ~ compileContract ~ estimate ~ time ~ seedRoute ~ length ~ hashFast ~ hashSecure ~ sign ~ transactionSerialize + decompile ~ compile ~ compileCode ~ estimate ~ time ~ seedRoute ~ length ~ hashFast ~ hashSecure ~ sign ~ transactionSerialize } @Path("/script/decompile") @@ -61,6 +61,7 @@ case class UtilsApiRoute(timeService: Time, settings: RestAPISettings) extends A } } + @Deprecated @Path("/script/compile") @ApiOperation(value = "Compile", notes = "Compiles string code to base64 script representation", httpMethod = "POST") @ApiImplicitParams( @@ -97,8 +98,8 @@ case class UtilsApiRoute(timeService: Time, settings: RestAPISettings) extends A } } - @Path("/script/compileContract") - @ApiOperation(value = "Compile Contract", notes = "Compiles string code to base64 contract representation", httpMethod = "POST") + @Path("/script/compileCode") + @ApiOperation(value = "Compile script", notes = "Compiles string code to base64 script representation", httpMethod = "POST") @ApiImplicitParams( Array( new ApiImplicitParam( @@ -106,7 +107,7 @@ case class UtilsApiRoute(timeService: Time, settings: RestAPISettings) extends A required = true, dataType = "string", paramType = "body", - value = "Contract code", + value = "Script code", example = "true" ) )) @@ -114,17 +115,17 @@ case class UtilsApiRoute(timeService: Time, settings: RestAPISettings) extends A Array( new ApiResponse(code = 200, message = "base64 or error") )) - def compileContract: Route = path("script" / "compileContract") { + def compileCode: Route = path("script" / "compileCode") { (post & entity(as[String])) { code => complete( ScriptCompiler - .contract(code) + .compile(code) .fold( e => ScriptCompilerError(e), { - case (contract) => + case (script, complexity) => Json.obj( - "script" -> contract.bytes().base64, - "complexity" -> 0, + "script" -> script.bytes().base64, + "complexity" -> complexity, "extraFee" -> CommonValidation.ScriptExtraFee ) } diff --git a/src/main/scala/com/zbsnetwork/api/http/assets/AssetsApiRoute.scala b/src/main/scala/com/zbsnetwork/api/http/assets/AssetsApiRoute.scala index 9941501..b8c3c84 100644 --- a/src/main/scala/com/zbsnetwork/api/http/assets/AssetsApiRoute.scala +++ b/src/main/scala/com/zbsnetwork/api/http/assets/AssetsApiRoute.scala @@ -206,9 +206,8 @@ case class AssetsApiRoute(settings: RestAPISettings, wallet: Wallet, utx: UtxPoo (for { acc <- Address.fromString(address) } yield - Json.obj("address" -> acc.address, - "assetId" -> assetIdStr, - "balance" -> JsNumber(BigDecimal(blockchain.portfolio(acc).assets.getOrElse(assetId, 0L))))).left.map(ApiError.fromValidationError) + Json.obj("address" -> acc.address, "assetId" -> assetIdStr, "balance" -> JsNumber(BigDecimal(blockchain.balance(acc, Some(assetId)))))).left + .map(ApiError.fromValidationError) case _ => Left(InvalidAddress) } } @@ -226,7 +225,7 @@ case class AssetsApiRoute(settings: RestAPISettings, wallet: Wallet, utx: UtxPoo assetInfo <- blockchain.assetDescription(assetId) (_, (issueTransaction: IssueTransaction)) <- blockchain.transactionInfo(assetId) sponsorBalance = if (assetInfo.sponsorship != 0) { - Some(blockchain.portfolio(issueTransaction.sender).spendableBalance) + Some(blockchain.zbsPortfolio(issueTransaction.sender).spendableBalance) } else { None } diff --git a/src/main/scala/com/zbsnetwork/block/Block.scala b/src/main/scala/com/zbsnetwork/block/Block.scala index 72650f8..0ba77d6 100644 --- a/src/main/scala/com/zbsnetwork/block/Block.scala +++ b/src/main/scala/com/zbsnetwork/block/Block.scala @@ -113,13 +113,13 @@ object BlockHeader extends ScorexLogging { } -case class Block private (override val timestamp: Long, - override val version: Byte, - override val reference: ByteStr, - override val signerData: SignerData, - override val consensusData: NxtLikeConsensusBlockData, - transactionData: Seq[Transaction], - override val featureVotes: Set[Short]) +case class Block private[block] (override val timestamp: Long, + override val version: Byte, + override val reference: ByteStr, + override val signerData: SignerData, + override val consensusData: NxtLikeConsensusBlockData, + transactionData: Seq[Transaction], + override val featureVotes: Set[Short]) extends BlockHeader(timestamp, version, reference, signerData, consensusData, transactionData.length, featureVotes) with Signed { @@ -173,7 +173,7 @@ case class Block private (override val timestamp: Long, val prevBlockFeePart: Coeval[Portfolio] = Coeval.evalOnce(Monoid[Portfolio].combineAll(transactionData.map(tx => tx.feeDiff().minus(tx.feeDiff().multiply(CurrentBlockFeePart))))) - protected val signatureValid: Coeval[Boolean] = Coeval.evalOnce { + override val signatureValid: Coeval[Boolean] = Coeval.evalOnce { import signerData.generator.publicKey !crypto.isWeakPublicKey(publicKey) && crypto.verify(signerData.signature.arr, bytesWithoutSignature(), publicKey) } diff --git a/src/main/scala/com/zbsnetwork/database/Caches.scala b/src/main/scala/com/zbsnetwork/database/Caches.scala index f18be64..9f85eb7 100644 --- a/src/main/scala/com/zbsnetwork/database/Caches.scala +++ b/src/main/scala/com/zbsnetwork/database/Caches.scala @@ -18,7 +18,7 @@ import scala.collection.JavaConverters._ import scala.concurrent.duration._ import scala.reflect.ClassTag -abstract class Caches(portfolioChanged: Observer[Address]) extends Blockchain with ScorexLogging { +abstract class Caches(spendableBalanceChanged: Observer[(Address, Option[AssetId])]) extends Blockchain with ScorexLogging { import Caches._ protected def maxCacheSize: Int @@ -130,16 +130,17 @@ abstract class Caches(portfolioChanged: Observer[Address]) extends Blockchain wi protected def discardLeaseBalance(address: Address): Unit = leaseBalanceCache.invalidate(address) override def leaseBalance(address: Address): LeaseBalance = leaseBalanceCache.get(address) - private val portfolioCache: LoadingCache[Address, Portfolio] = observedCache(maxCacheSize / 4, portfolioChanged, loadPortfolio) + private val portfolioCache: LoadingCache[Address, Portfolio] = cache(maxCacheSize / 4, loadPortfolio) protected def loadPortfolio(address: Address): Portfolio protected def discardPortfolio(address: Address): Unit = portfolioCache.invalidate(address) override def portfolio(a: Address): Portfolio = { portfolioCache.get(a) } - private val balancesCache: LoadingCache[(Address, Option[AssetId]), java.lang.Long] = cache(maxCacheSize * 16, loadBalance) - protected def discardBalance(key: (Address, Option[AssetId])) = balancesCache.invalidate(key) - override def balance(address: Address, mayBeAssetId: Option[AssetId]): Long = balancesCache.get(address -> mayBeAssetId) + private val balancesCache: LoadingCache[(Address, Option[AssetId]), java.lang.Long] = + observedCache(maxCacheSize * 16, spendableBalanceChanged, loadBalance) + protected def discardBalance(key: (Address, Option[AssetId])) = balancesCache.invalidate(key) + override def balance(address: Address, mayBeAssetId: Option[AssetId]): Long = balancesCache.get(address -> mayBeAssetId) protected def loadBalance(req: (Address, Option[AssetId])): Long private val assetDescriptionCache: LoadingCache[AssetId, Option[AssetDescription]] = cache(maxCacheSize, loadAssetDescription) @@ -266,21 +267,28 @@ abstract class Caches(portfolioChanged: Observer[Address]) extends Blockchain wi (orderId, fillInfo) <- diff.orderFills } yield orderId -> volumeAndFeeCache.get(orderId).combine(fillInfo) + val transactionList = diff.transactions.toList + + transactionList.foreach { + case (_, (_, tx, _)) => + transactionIds.put(tx.id(), newHeight) + } + val addressTransactions: Map[AddressId, List[TransactionId]] = - diff.transactions.toList + transactionList .flatMap { case (_, (h, tx, addrs)) => + transactionIds.put(tx.id(), newHeight) // be careful here! + addrs.map { addr => val addrId = AddressId(addressId(addr)) - val htx = (h, tx) - addrId -> htx + addrId -> TransactionId(tx.id()) } } .groupBy(_._1) - .mapValues { txs => - val sorted = txs.sortBy { case (_, (h, tx)) => (-h, -tx.timestamp) } - sorted.map { case (_, (_, tx)) => TransactionId(tx.id()) } - } + .mapValues(_.map { + case (_, txId) => txId + }) current = (newHeight, current._2 + block.blockScore(), Some(block)) diff --git a/src/main/scala/com/zbsnetwork/database/LevelDBWriter.scala b/src/main/scala/com/zbsnetwork/database/LevelDBWriter.scala index ed90217..c419804 100644 --- a/src/main/scala/com/zbsnetwork/database/LevelDBWriter.scala +++ b/src/main/scala/com/zbsnetwork/database/LevelDBWriter.scala @@ -70,12 +70,12 @@ object LevelDBWriter { } class LevelDBWriter(writableDB: DB, - portfolioChanged: Observer[Address], + spendableBalanceChanged: Observer[(Address, Option[AssetId])], fs: FunctionalitySettings, val maxCacheSize: Int, val maxRollbackDepth: Int, val rememberBlocksInterval: Long) - extends Caches(portfolioChanged) + extends Caches(spendableBalanceChanged) with ScorexLogging { private val balanceSnapshotMaxRollbackDepth: Int = maxRollbackDepth + 1000 @@ -364,7 +364,7 @@ class LevelDBWriter(writableDB: DB, (tx.builder.typeId, num) } - rw.put(Keys.addressTransactionHN(addressId, nextSeqNr), Some((Height(height), txTypeNumSeq))) + rw.put(Keys.addressTransactionHN(addressId, nextSeqNr), Some((Height(height), txTypeNumSeq.sortBy(-_._2)))) rw.put(kk, nextSeqNr) } @@ -598,61 +598,47 @@ class LevelDBWriter(writableDB: DB, override def addressTransactions(address: Address, types: Set[Type], count: Int, fromId: Option[ByteStr]): Either[String, Seq[(Int, Transaction)]] = readOnly { db => - def takeTypes(s: Stream[(Height, Type, TxNum)], maybeTypes: Set[Type]) = { - if (maybeTypes.nonEmpty) { - s.filter { case (_, tp, _) => maybeTypes.contains(tp) } - } else s + def takeTypes(txNums: Stream[(Height, Type, TxNum)], maybeTypes: Set[Type]) = + if (maybeTypes.nonEmpty) txNums.filter { case (_, tp, _) => maybeTypes.contains(tp) } else txNums + + def takeAfter(txNums: Stream[(Height, Type, TxNum)], maybeAfter: Option[(Height, TxNum)]): Stream[(Height, Type, TxNum)] = maybeAfter match { + case None => txNums + case Some((afterHeight, filterHeight)) => + txNums + .dropWhile { case (streamHeight, _, _) => streamHeight > afterHeight } + .dropWhile { case (streamHeight, _, streamNum) => streamNum >= filterHeight && streamHeight >= afterHeight } } - def takeAfter(s: Stream[(Height, Type, TxNum)], maybeAfter: Option[(Height, TxNum)]): Stream[(Height, Type, TxNum)] = { - maybeAfter match { - case None => s - case Some((h, num)) => - s.dropWhile { - case (s_h, _, s_n) => s_h != h ^ s_n != num - } - .drop(1) - - } - } - - val maybeAfter = fromId.flatMap(id => db.get(Keys.transactionHNById(TransactionId(id)))) + def readTransactions(): Seq[(Int, Transaction)] = { + val maybeAfter = fromId.flatMap(id => db.get(Keys.transactionHNById(TransactionId(id)))) - lazy val transactions: Seq[(Int, Transaction)] = db.get(Keys.addressId(address)).fold(Seq.empty[(Int, Transaction)]) { id => val addressId = AddressId(id) - val hnSeq = - (db.get(Keys.addressTransactionSeqNr(addressId)) to 1 by -1).toStream - .flatMap { seqNr => - val maybeHNSeq = db.get(Keys.addressTransactionHN(addressId, seqNr)) + val heightNumStream = (db.get(Keys.addressTransactionSeqNr(addressId)) to 1 by -1).toStream + .flatMap(seqNr => + db.get(Keys.addressTransactionHN(addressId, seqNr)) match { + case Some((height, txNums)) => txNums.map { case (txType, txNum) => (height, txType, txNum) } + case None => Nil + }) - maybeHNSeq match { - case Some((h, seq)) => - seq.map { case (tp, num) => (h, tp, num) }.toStream - case None => Stream.empty - } - } - - takeAfter(takeTypes(hnSeq, types), maybeAfter) - .flatMap { - case (h, _, num) => - db.get(Keys.transactionAt(h, num)) - .map((h, _)) - } + takeAfter(takeTypes(heightNumStream, types), maybeAfter) + .flatMap { case (height, _, txNum) => db.get(Keys.transactionAt(height, txNum)).map((height, _)) } .take(count) - .toList + .toVector } + } fromId match { - case None => Right(transactions) + case None => + Right(readTransactions()) + case Some(id) => db.get(Keys.transactionHNById(TransactionId(id))) match { case None => Left(s"Transaction $id does not exist") - case _ => Right(transactions) + case _ => Right(readTransactions()) } } - } override def resolveAlias(alias: Alias): Either[ValidationError, Address] = readOnly { db => @@ -968,15 +954,24 @@ class LevelDBWriter(writableDB: DB, ) } - override def zbsDistribution(height: Int): Map[Address, Long] = readOnly { db => - (for { - seqNr <- (1 to db.get(Keys.addressesForZbsSeqNr)).par - addressId <- db.get(Keys.addressesForZbs(seqNr)).par - history = db.get(Keys.zbsBalanceHistory(addressId)) - actualHeight <- history.partition(_ > height)._2.headOption - balance = db.get(Keys.zbsBalance(addressId)(actualHeight)) - if balance > 0 - } yield db.get(Keys.idToAddress(addressId)) -> balance).toMap.seq + override def zbsDistribution(height: Int): Either[ValidationError, Map[Address, Long]] = readOnly { db => + val canGetAfterHeight = db.get(Keys.safeRollbackHeight) + + def createMap() = + (for { + seqNr <- (1 to db.get(Keys.addressesForZbsSeqNr)).par + addressId <- db.get(Keys.addressesForZbs(seqNr)).par + history = db.get(Keys.zbsBalanceHistory(addressId)) + actualHeight <- history.partition(_ > height)._2.headOption + balance = db.get(Keys.zbsBalance(addressId)(actualHeight)) + if balance > 0 + } yield db.get(Keys.idToAddress(addressId)) -> balance).toMap.seq + + Either.cond( + height > canGetAfterHeight, + createMap(), + GenericError(s"Cannot get zbs distribution at height less than ${canGetAfterHeight + 1}") + ) } private[database] def loadBlock(height: Height): Option[Block] = readOnly { db => diff --git a/src/main/scala/com/zbsnetwork/features/BlockchainFeature.scala b/src/main/scala/com/zbsnetwork/features/BlockchainFeature.scala index 5e9e7cd..e14377c 100644 --- a/src/main/scala/com/zbsnetwork/features/BlockchainFeature.scala +++ b/src/main/scala/com/zbsnetwork/features/BlockchainFeature.scala @@ -15,6 +15,10 @@ object BlockchainFeatures { val SmartAssets = BlockchainFeature(9, "Smart Assets") val SmartAccountTrading = BlockchainFeature(10, "Smart Account Trading") val Ride4DApps = BlockchainFeature(11, "RIDE 4 DAPPS") + val OrderV3 = BlockchainFeature(12, "Order Version 3") + + // When next fork-parameter is created, you must replace all uses of the DummyFeature with the new one. + val DummyFeature = BlockchainFeature(-1, "Non Votable!") private val dict = Seq( SmallerMinimalGeneratingBalance, @@ -27,7 +31,8 @@ object BlockchainFeatures { FairPoS, SmartAccountTrading, SmartAssets, - Ride4DApps + Ride4DApps, + OrderV3 ).map(f => f.id -> f).toMap val implemented: Set[Short] = dict.keySet diff --git a/src/main/scala/com/zbsnetwork/history/StorageFactory.scala b/src/main/scala/com/zbsnetwork/history/StorageFactory.scala index a7191da..0149b33 100644 --- a/src/main/scala/com/zbsnetwork/history/StorageFactory.scala +++ b/src/main/scala/com/zbsnetwork/history/StorageFactory.scala @@ -4,25 +4,25 @@ import com.zbsnetwork.account.Address import com.zbsnetwork.database.{DBExt, Keys, LevelDBWriter} import com.zbsnetwork.settings.ZbsSettings import com.zbsnetwork.state.{BlockchainUpdaterImpl, NG} -import com.zbsnetwork.transaction.BlockchainUpdater +import com.zbsnetwork.transaction.{AssetId, BlockchainUpdater} import com.zbsnetwork.utils.{ScorexLogging, Time, UnsupportedFeature, forceStopApplication} import monix.reactive.Observer import org.iq80.leveldb.DB object StorageFactory extends ScorexLogging { - private val StorageVersion = 3 + private val StorageVersion = 4 - def apply(settings: ZbsSettings, db: DB, time: Time, portfolioChanged: Observer[Address]): BlockchainUpdater with NG = { + def apply(settings: ZbsSettings, db: DB, time: Time, spendableBalanceChanged: Observer[(Address, Option[AssetId])]): BlockchainUpdater with NG = { checkVersion(db) val levelDBWriter = new LevelDBWriter( db, - portfolioChanged, + spendableBalanceChanged, settings.blockchainSettings.functionalitySettings, settings.maxCacheSize, settings.maxRollbackDepth, settings.rememberBlocks.toMillis ) - new BlockchainUpdaterImpl(levelDBWriter, portfolioChanged, settings, time) + new BlockchainUpdaterImpl(levelDBWriter, spendableBalanceChanged, settings, time) } private def checkVersion(db: DB): Unit = db.readWrite { rw => diff --git a/src/main/scala/com/zbsnetwork/http/DebugApiRoute.scala b/src/main/scala/com/zbsnetwork/http/DebugApiRoute.scala index 8336403..136e82b 100644 --- a/src/main/scala/com/zbsnetwork/http/DebugApiRoute.scala +++ b/src/main/scala/com/zbsnetwork/http/DebugApiRoute.scala @@ -7,6 +7,7 @@ import akka.http.scaladsl.marshalling.ToResponseMarshallable import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server.Route import cats.implicits._ +import cats.kernel.Monoid import com.typesafe.config.{ConfigObject, ConfigRenderOptions} import com.zbsnetwork.account.Address import com.zbsnetwork.api.http._ @@ -26,13 +27,13 @@ import com.zbsnetwork.transaction.smart.Verifier import com.zbsnetwork.utils.{ScorexLogging, Time} import com.zbsnetwork.utx.UtxPool import com.zbsnetwork.wallet.Wallet +import com.zbsnetwork.utils.byteStrWrites import io.netty.channel.Channel import io.netty.channel.group.ChannelGroup import io.swagger.annotations._ import javax.ws.rs.Path import monix.eval.{Coeval, Task} import play.api.libs.json._ -import com.zbsnetwork.utils.byteStrWrites import scala.concurrent.Future import scala.concurrent.duration._ @@ -140,7 +141,8 @@ case class DebugApiRoute(ws: ZbsSettings, Address.fromString(rawAddress) match { case Left(_) => complete(InvalidAddress) case Right(address) => - val portfolio = if (considerUnspent.getOrElse(true)) utxStorage.portfolio(address) else ng.portfolio(address) + val base = ng.portfolio(address) + val portfolio = if (considerUnspent.getOrElse(true)) Monoid.combine(base, utxStorage.pessimisticPortfolio(address)) else base complete(Json.toJson(portfolio)) } } @@ -150,7 +152,7 @@ case class DebugApiRoute(ws: ZbsSettings, @ApiOperation(value = "State", notes = "Get current state", httpMethod = "GET") @ApiResponses(Array(new ApiResponse(code = 200, message = "Json state"))) def state: Route = (path("state") & get & withAuth) { - complete(ng.zbsDistribution(ng.height).map { case (a, b) => a.stringRepr -> b }) + complete(ng.zbsDistribution(ng.height).map(_.map { case (a, b) => a.stringRepr -> b })) } @Path("/stateZbs/{height}") @@ -160,7 +162,7 @@ case class DebugApiRoute(ws: ZbsSettings, new ApiImplicitParam(name = "height", value = "height", required = true, dataType = "integer", paramType = "path") )) def stateZbs: Route = (path("stateZbs" / IntNumber) & get & withAuth) { height => - complete(ng.zbsDistribution(height).map { case (a, b) => a.stringRepr -> b }) + complete(ng.zbsDistribution(height).map(_.map { case (a, b) => a.stringRepr -> b })) } private def rollbackToBlock(blockId: ByteStr, returnTransactionsToUtx: Boolean): Future[ToResponseMarshallable] = { diff --git a/src/main/scala/com/zbsnetwork/matcher/AddressActor.scala b/src/main/scala/com/zbsnetwork/matcher/AddressActor.scala index adaed13..cd22abf 100644 --- a/src/main/scala/com/zbsnetwork/matcher/AddressActor.scala +++ b/src/main/scala/com/zbsnetwork/matcher/AddressActor.scala @@ -11,7 +11,6 @@ import com.zbsnetwork.matcher.OrderDB.orderInfoOrdering import com.zbsnetwork.matcher.model.Events.{OrderAdded, OrderCanceled, OrderExecuted} import com.zbsnetwork.matcher.model.{LimitOrder, OrderInfo, OrderStatus, OrderValidator} import com.zbsnetwork.matcher.queue.QueueEvent -import com.zbsnetwork.state.Portfolio import com.zbsnetwork.transaction.AssetId import com.zbsnetwork.transaction.assets.exchange.AssetPair.assetIdStr import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order} @@ -26,11 +25,11 @@ import scala.util.{Failure, Success} class AddressActor( owner: Address, - portfolio: => Portfolio, - maxTimestampDrift: FiniteDuration, + spendableBalance: Option[AssetId] => Long, cancelTimeout: FiniteDuration, time: Time, orderDB: OrderDB, + hasOrder: Order.Id => Boolean, storeEvent: StoreEvent, ) extends Actor with ScorexLogging { @@ -70,22 +69,17 @@ class AddressActor( latestOrderTs = newTimestamp } - private def tradableBalance(assetId: Option[AssetId]): Long = { - val p = portfolio - assetId.fold(p.spendableBalance)(p.assets.getOrElse(_, 0L)) - openVolume(assetId) - } + private def tradableBalance(assetId: Option[AssetId]): Long = spendableBalance(assetId) - openVolume(assetId) private val validator = OrderValidator.accountStateAware(owner, tradableBalance, activeOrders.size, - latestOrderTs - maxTimestampDrift.toMillis, - id => activeOrders.contains(id) || orderDB.contains(id)) _ + id => activeOrders.contains(id) || orderDB.containsInfo(id) || hasOrder(id)) _ private def handleCommands: Receive = { - case BalanceUpdated => - val newPortfolio = portfolio - val toCancel = ordersToDelete(toSpendable(newPortfolio)) + case evt: BalanceUpdated => + val toCancel = ordersToDelete(toSpendable(evt)) if (toCancel.nonEmpty) { log.debug(s"Canceling: $toCancel") toCancel.foreach { x => @@ -122,17 +116,14 @@ class AddressActor( Future.successful(api.OrderCancelRejected(reason)) })(_.future) pipeTo sender() - case CancelAllOrders(maybePair, timestamp) => - if ((timestamp - latestOrderTs).abs <= maxTimestampDrift.toMillis) { - val batchCancelFutures = for { - lo <- activeOrders.values - if maybePair.forall(_ == lo.order.assetPair) - } yield storeCanceled(lo.order.assetPair, lo.order.id()).map(lo.order.id() -> _) + case CancelAllOrders(maybePair, _) => + val batchCancelFutures = for { + lo <- activeOrders.values + if maybePair.forall(_ == lo.order.assetPair) + } yield storeCanceled(lo.order.assetPair, lo.order.id()).map(lo.order.id() -> _) + + Future.sequence(batchCancelFutures).map(_.toMap).map(api.BatchCancelCompleted).pipeTo(sender()) - Future.sequence(batchCancelFutures).map(_.toMap).map(api.BatchCancelCompleted).pipeTo(sender()) - } else { - sender() ! api.OrderCancelRejected("Invalid timestamp") - } case CancelExpiredOrder(id) => expiration.remove(id) for (lo <- activeOrders.get(id)) { @@ -252,14 +243,20 @@ class AddressActor( private type SpendableBalance = Map[Option[AssetId], Long] + /** + * @param initBalance Contains only changed assets + */ private def ordersToDelete(initBalance: SpendableBalance): Queue[QueueEvent.Canceled] = { - // Probably, we need to check orders with changed assets only. + def keepChanged(requiredBalance: Map[Option[AssetId], Long]) = requiredBalance.filter { + case (requiredAssetId, _) => initBalance.contains(requiredAssetId) + } + // Now a user can have 100 active transaction maximum - easy to traverse. val (_, r) = activeOrders.values.toSeq .sortBy(_.order.timestamp)(Ordering[Long]) // Will cancel newest orders first .view .map { lo => - (lo.order.id(), lo.order.assetPair, lo.requiredBalance) + (lo.order.id(), lo.order.assetPair, keepChanged(lo.requiredBalance)) } .foldLeft((initBalance, Queue.empty[QueueEvent.Canceled])) { case ((restBalance, toDelete), (id, assetPair, requiredBalance)) => @@ -273,11 +270,10 @@ class AddressActor( r } - private def toSpendable(p: Portfolio): SpendableBalance = - p.assets - .map { case (k, v) => (Some(k): Option[AssetId]) -> v } - .updated(None, p.spendableBalance) - .withDefaultValue(0) + private def toSpendable(event: BalanceUpdated): SpendableBalance = { + val r: SpendableBalance = event.changedAssets.map(x => x -> spendableBalance(x))(collection.breakOut) + r.withDefaultValue(0) + } private def remove(from: SpendableBalance, xs: SpendableBalance): Option[SpendableBalance] = xs.foldLeft[Option[SpendableBalance]](Some(from)) { @@ -309,7 +305,7 @@ object AddressActor { } case class CancelOrder(orderId: ByteStr) extends Command case class CancelAllOrders(pair: Option[AssetPair], timestamp: Long) extends Command - case object BalanceUpdated extends Command + case class BalanceUpdated(changedAssets: Set[Option[AssetId]]) extends Command private case class CancelExpiredOrder(orderId: ByteStr) } diff --git a/src/main/scala/com/zbsnetwork/matcher/AddressDirectory.scala b/src/main/scala/com/zbsnetwork/matcher/AddressDirectory.scala index 0a58b75..a68bc5d 100644 --- a/src/main/scala/com/zbsnetwork/matcher/AddressDirectory.scala +++ b/src/main/scala/com/zbsnetwork/matcher/AddressDirectory.scala @@ -3,22 +3,17 @@ package com.zbsnetwork.matcher import akka.actor.{Actor, ActorRef, Props, SupervisorStrategy, Terminated} import com.zbsnetwork.account.Address import com.zbsnetwork.common.utils.EitherExt2 -import com.zbsnetwork.matcher.Matcher.StoreEvent import com.zbsnetwork.matcher.model.Events -import com.zbsnetwork.state.Portfolio -import com.zbsnetwork.utils.{ScorexLogging, Time} +import com.zbsnetwork.transaction.AssetId +import com.zbsnetwork.utils.ScorexLogging import monix.execution.Scheduler import monix.reactive.Observable import scala.collection.mutable -import scala.concurrent.duration._ -class AddressDirectory(portfolioChanged: Observable[Address], - portfolio: Address => Portfolio, - storeEvent: StoreEvent, +class AddressDirectory(spendableBalanceChanged: Observable[(Address, Option[AssetId])], settings: MatcherSettings, - time: Time, - orderDB: OrderDB) + addressActorProps: Address => Props) extends Actor with ScorexLogging { import AddressDirectory._ @@ -26,21 +21,22 @@ class AddressDirectory(portfolioChanged: Observable[Address], private[this] val children = mutable.AnyRefMap.empty[Address, ActorRef] - portfolioChanged - .filter(children.contains) + spendableBalanceChanged + .filter(x => children.contains(x._1)) .bufferTimed(settings.balanceWatchingBufferInterval) .filter(_.nonEmpty) - .foreach(_.toSet.foreach((address: Address) => children.get(address).foreach(_ ! AddressActor.BalanceUpdated)))(Scheduler(context.dispatcher)) + .foreach { changes => + val acc = mutable.Map.empty[Address, Set[Option[AssetId]]] + changes.foreach { case (addr, changed) => acc.update(addr, acc.getOrElse(addr, Set.empty) + changed) } + + acc.foreach { case (addr, changedAssets) => children.get(addr).foreach(_ ! AddressActor.BalanceUpdated(changedAssets)) } + }(Scheduler(context.dispatcher)) override def supervisorStrategy: SupervisorStrategy = SupervisorStrategy.stoppingStrategy private def createAddressActor(address: Address): ActorRef = { log.debug(s"Creating address actor for $address") - watch( - actorOf( - Props(new AddressActor(address, portfolio(address), settings.maxTimestampDiff, 5.seconds, time, orderDB, storeEvent)), - address.toString - )) + watch(actorOf(addressActorProps(address), address.toString)) } private def forward(address: Address, msg: Any): Unit = { diff --git a/src/main/scala/com/zbsnetwork/matcher/Matcher.scala b/src/main/scala/com/zbsnetwork/matcher/Matcher.scala index 766cc95..ba59ec6 100644 --- a/src/main/scala/com/zbsnetwork/matcher/Matcher.scala +++ b/src/main/scala/com/zbsnetwork/matcher/Matcher.scala @@ -21,7 +21,8 @@ import com.zbsnetwork.matcher.model.{ExchangeTransactionCreator, OrderBook, Orde import com.zbsnetwork.matcher.queue._ import com.zbsnetwork.network._ import com.zbsnetwork.settings.ZbsSettings -import com.zbsnetwork.state.Blockchain +import com.zbsnetwork.state.{Blockchain, VolumeAndFee} +import com.zbsnetwork.transaction.AssetId import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order} import com.zbsnetwork.utils.{ErrorStartingMatcher, ScorexLogging, Time, forceStopApplication} import com.zbsnetwork.utx.UtxPool @@ -39,7 +40,7 @@ class Matcher(actorSystem: ActorSystem, utx: UtxPool, allChannels: ChannelGroup, blockchain: Blockchain, - portfoliosChanged: Observable[Address], + spendableBalanceChanged: Observable[(Address, Option[AssetId])], settings: ZbsSettings, matcherPrivateKey: PrivateKeyAccount) extends ScorexLogging { @@ -163,10 +164,28 @@ class Matcher(actorSystem: ActorSystem, MatcherActor.name ) + private lazy val orderDb = OrderDB(matcherSettings, db) + private lazy val addressActors = actorSystem.actorOf( - Props(new AddressDirectory(portfoliosChanged, utx.portfolio, matcherQueue.storeEvent, matcherSettings, time, OrderDB(matcherSettings, db))), - "addresses") + Props( + new AddressDirectory( + spendableBalanceChanged, + matcherSettings, + address => + Props( + new AddressActor( + address, + utx.spendableBalance(address, _), + 5.seconds, + time, + orderDb, + id => blockchain.filledVolumeAndFee(id) != VolumeAndFee.empty, + matcherQueue.storeEvent + )) + )), + "addresses" + ) private lazy val blacklistedAddresses = settings.matcherSettings.blacklistedAddresses.map(Address.fromString(_).explicitGet()) private lazy val matcherPublicKey = PublicKeyAccount(matcherPrivateKey.publicKey) @@ -275,7 +294,7 @@ object Matcher extends ScorexLogging { utx: UtxPool, allChannels: ChannelGroup, blockchain: Blockchain, - portfoliosChanged: Observable[Address], + spendableBalanceChanged: Observable[(Address, Option[AssetId])], settings: ZbsSettings): Option[Matcher] = try { val privateKey = (for { @@ -283,7 +302,7 @@ object Matcher extends ScorexLogging { pk <- wallet.privateKeyAccount(address) } yield pk).explicitGet() - val matcher = new Matcher(actorSystem, time, utx, allChannels, blockchain, portfoliosChanged, settings, privateKey) + val matcher = new Matcher(actorSystem, time, utx, allChannels, blockchain, spendableBalanceChanged, settings, privateKey) matcher.runMatcher() Some(matcher) } catch { diff --git a/src/main/scala/com/zbsnetwork/matcher/MatcherSettings.scala b/src/main/scala/com/zbsnetwork/matcher/MatcherSettings.scala index 95582bc..bae2c83 100644 --- a/src/main/scala/com/zbsnetwork/matcher/MatcherSettings.scala +++ b/src/main/scala/com/zbsnetwork/matcher/MatcherSettings.scala @@ -30,11 +30,9 @@ case class MatcherSettings(enable: Boolean, startEventsProcessingTimeout: FiniteDuration, makeSnapshotsAtStart: Boolean, priceAssets: Seq[String], - maxTimestampDiff: FiniteDuration, blacklistedAssets: Set[String], blacklistedNames: Seq[Regex], maxOrdersPerRequest: Int, - orderTimestampDrift: Long, // this is not a Set[Address] because to parse an address, global AddressScheme must be initialized blacklistedAddresses: Set[String], orderBookSnapshotHttpCache: OrderBookSnapshotHttpCache.Settings, @@ -72,9 +70,7 @@ object MatcherSettings { val startEventsProcessingTimeout = config.as[FiniteDuration](s"$configPath.start-events-processing-timeout") val makeSnapshotsAtStart = config.as[Boolean](s"$configPath.make-snapshots-at-start") val maxOrdersPerRequest = config.as[Int](s"$configPath.rest-order-limit") - val orderTimestampDrift = config.as[FiniteDuration](s"$configPath.order-timestamp-drift") val baseAssets = config.as[List[String]](s"$configPath.price-assets") - val maxTimestampDiff = config.as[FiniteDuration](s"$configPath.max-timestamp-diff") val blacklistedAssets = config.as[List[String]](s"$configPath.blacklisted-assets") val blacklistedNames = config.as[List[String]](s"$configPath.blacklisted-names").map(_.r) @@ -103,11 +99,9 @@ object MatcherSettings { startEventsProcessingTimeout, makeSnapshotsAtStart, baseAssets, - maxTimestampDiff, blacklistedAssets.toSet, blacklistedNames, maxOrdersPerRequest, - orderTimestampDrift.toMillis, blacklistedAddresses, orderBookSnapshotHttpCache, balanceWatchingBufferInterval, diff --git a/src/main/scala/com/zbsnetwork/matcher/OrderDB.scala b/src/main/scala/com/zbsnetwork/matcher/OrderDB.scala index 7b18091..c41c8fc 100644 --- a/src/main/scala/com/zbsnetwork/matcher/OrderDB.scala +++ b/src/main/scala/com/zbsnetwork/matcher/OrderDB.scala @@ -10,7 +10,7 @@ import com.zbsnetwork.utils.ScorexLogging import org.iq80.leveldb.DB trait OrderDB { - def contains(id: ByteStr): Boolean + def containsInfo(id: ByteStr): Boolean def status(id: ByteStr): OrderStatus.Final def saveOrderInfo(id: ByteStr, sender: Address, oi: OrderInfo[OrderStatus.Final]): Unit def saveOrder(o: Order): Unit @@ -20,8 +20,10 @@ trait OrderDB { } object OrderDB { + private val OldestOrderIndexOffset = 100 + def apply(settings: MatcherSettings, db: DB): OrderDB = new OrderDB with ScorexLogging { - override def contains(id: ByteStr): Boolean = db.readOnly(_.has(MatcherKeys.order(id))) + override def containsInfo(id: ByteStr): Boolean = db.readOnly(_.has(MatcherKeys.orderInfo(id))) override def status(id: ByteStr): OrderStatus.Final = db.readOnly { ro => ro.get(MatcherKeys.orderInfo(id)).fold[OrderStatus.Final](OrderStatus.NotFound)(_.status) @@ -42,8 +44,14 @@ object OrderDB { db.readWrite { rw => val newCommonSeqNr = rw.inc(MatcherKeys.finalizedCommonSeqNr(sender)) rw.put(MatcherKeys.finalizedCommon(sender, newCommonSeqNr), Some(id)) + val newPairSeqNr = rw.inc(MatcherKeys.finalizedPairSeqNr(sender, oi.assetPair)) rw.put(MatcherKeys.finalizedPair(sender, oi.assetPair, newPairSeqNr), Some(id)) + if (newPairSeqNr > OldestOrderIndexOffset) // Indexes start with 1, so if newPairSeqNr=101, we delete 1 (the first) + rw.get(MatcherKeys.finalizedPair(sender, oi.assetPair, newPairSeqNr - OldestOrderIndexOffset)) + .map(MatcherKeys.order) + .foreach(x => rw.delete(x)) + rw.put(orderInfoKey, Some(oi)) } } diff --git a/src/main/scala/com/zbsnetwork/matcher/api/MatcherApiRoute.scala b/src/main/scala/com/zbsnetwork/matcher/api/MatcherApiRoute.scala index daa80d7..a046e5f 100644 --- a/src/main/scala/com/zbsnetwork/matcher/api/MatcherApiRoute.scala +++ b/src/main/scala/com/zbsnetwork/matcher/api/MatcherApiRoute.scala @@ -62,15 +62,9 @@ case class MatcherApiRoute(assetPairBuilder: AssetPairBuilder, override val settings: RestAPISettings = restAPISettings - private val timer = Kamon.timer("matcher.api-requests") - private val placeTimer = timer.refine("action" -> "place") - override lazy val route: Route = pathPrefix("matcher") { matcherStatusBarrier { - getMatcherPublicKey ~ getOrderBook ~ marketStatus ~ place ~ getAssetPairAndPublicKeyOrderHistory ~ getPublicKeyOrderHistory ~ - getAllOrderHistory ~ tradableBalance ~ reservedBalance ~ orderStatus ~ - historyDelete ~ cancel ~ cancelAll ~ orderbooks ~ orderBookDelete ~ getTransactionsByOrder ~ forceCancelOrder ~ - getSettings ~ getCurrentOffset ~ getOldestSnapshotOffset ~ getAllSnapshotOffsets + getMatcherPublicKey } } @@ -79,476 +73,12 @@ case class MatcherApiRoute(assetPairBuilder: AssetPairBuilder, case Matcher.Status.Starting => complete(DuringStart) case Matcher.Status.Stopping => complete(DuringShutdown) } - - private def unavailableOrderBookBarrier(p: AssetPair): Directive0 = orderBook(p) match { - case Some(Left(_)) => complete(OrderBookUnavailable) - case _ => pass - } - - private def withAssetPair(p: AssetPair, redirectToInverse: Boolean = false, suffix: String = ""): Directive1[AssetPair] = - assetPairBuilder.validateAssetPair(p) match { - case Right(_) => provide(p) - case Left(e) if redirectToInverse => - assetPairBuilder - .validateAssetPair(p.reverse) - .fold( - _ => complete(StatusCodes.NotFound -> Json.obj("message" -> e)), - _ => redirect(s"/matcher/orderbook/${p.priceAssetStr}/${p.amountAssetStr}$suffix", StatusCodes.MovedPermanently) - ) - case Left(e) => complete(StatusCodes.NotFound -> Json.obj("message" -> e)) - } - - private def withCancelRequest(f: CancelOrderRequest => Route): Route = - post { - entity(as[CancelOrderRequest]) { req => - if (req.isSignatureValid()) f(req) else complete(InvalidSignature) - } ~ complete(StatusCodes.BadRequest) - } ~ complete(StatusCodes.MethodNotAllowed) - - private def signedGet(publicKey: PublicKeyAccount): Directive0 = - (headerValueByName("Timestamp") & headerValueByName("Signature")).tflatMap { - case (ts, sig) => - val timestamp = ts.toLong - require(math.abs(timestamp - time.correctedTime()).millis < matcherSettings.maxTimestampDiff, "Incorrect timestamp") - require(crypto.verify(Base58.decode(sig).get, publicKey.publicKey ++ Longs.toByteArray(timestamp), publicKey.publicKey), - "Incorrect signature") - pass - } - - @inline - private def askAddressActor[A: ClassTag](sender: Address, msg: AddressActor.Command): Future[A] = { - (addressActor ? Env(sender, msg)) - .mapTo[A] - .andThen { - case Failure(e) => log.warn(s"Error processing $msg", e) - } - } - @Path("/") @ApiOperation(value = "Matcher Public Key", notes = "Get matcher public key", httpMethod = "GET") def getMatcherPublicKey: Route = (pathEndOrSingleSlash & get) { complete(JsString(Base58.encode(matcherPublicKey.publicKey))) } - @Path("/settings") - @ApiOperation(value = "Matcher Settings", notes = "Get matcher settings", httpMethod = "GET") - def getSettings: Route = (path("settings") & get) { - complete(StatusCodes.OK -> Json.obj("priceAssets" -> matcherSettings.priceAssets)) - } - - @Path("/orderbook/{amountAsset}/{priceAsset}") - @ApiOperation(value = "Get Order Book for a given Asset Pair", notes = "Get Order Book for a given Asset Pair", httpMethod = "GET") - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "depth", - value = "Limit the number of bid/ask records returned", - required = false, - dataType = "integer", - paramType = "query") - )) - def getOrderBook: Route = (path("orderbook" / AssetPairPM) & get) { p => - parameters('depth.as[Int].?) { depth => - withAssetPair(p, redirectToInverse = true) { pair => - complete(orderBookSnapshot.get(pair, depth)) - } - } - } - - @Path("/orderbook/{amountAsset}/{priceAsset}/status") - @ApiOperation(value = "Get Market Status", notes = "Get current market data such as last trade, best bid and ask", httpMethod = "GET") - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path") - )) - def marketStatus: Route = (path("orderbook" / AssetPairPM / "status") & get) { p => - withAssetPair(p, redirectToInverse = true) { pair => - getMarketStatus(pair).fold(complete(StatusCodes.NotFound -> Json.obj("message" -> "There is no information about this asset pair"))) { ms => - complete(StatusCodes.OK -> ms) - } - } - } - - @Path("/orderbook") - @ApiOperation(value = "Place order", - notes = "Place a new limit order (buy or sell)", - httpMethod = "POST", - produces = "application/json", - consumes = "application/json") - @ApiImplicitParams( - Array( - new ApiImplicitParam( - name = "body", - value = "Json with data", - required = true, - paramType = "body", - dataType = "com.zbsnetwork.transaction.assets.exchange.Order" - ) - )) - def place: Route = path("orderbook") { - (pathEndOrSingleSlash & post) { - _json[Order] { order => - unavailableOrderBookBarrier(order.assetPair) { - complete { - placeTimer.measureFuture { - orderValidator(order) match { - case Right(_) => - placeTimer.measureFuture(askAddressActor[MatcherResponse](order.sender, AddressActor.PlaceOrder(order))) - case Left(error) => Future.successful[MatcherResponse](OrderRejected(error)) - } - } - } - } - } - } - } - - @Path("/orderbook") - @ApiOperation(value = "Get the open trading markets", notes = "Get the open trading markets along with trading pairs meta data", httpMethod = "GET") - def orderbooks: Route = (path("orderbook") & pathEndOrSingleSlash & get) { - complete((matcher ? GetMarkets).mapTo[Seq[MarketData]].map { markets => - StatusCodes.OK -> Json.obj( - "matcherPublicKey" -> Base58.encode(matcherPublicKey.publicKey), - "markets" -> JsArray(markets.map(m => - Json.obj( - "amountAsset" -> m.pair.amountAssetStr, - "amountAssetName" -> m.amountAssetName, - "amountAssetInfo" -> m.amountAssetInfo, - "priceAsset" -> m.pair.priceAssetStr, - "priceAssetName" -> m.priceAssetName, - "priceAssetInfo" -> m.priceAssetinfo, - "created" -> m.created - ))) - ) - }) - } - - private def handleCancelRequest(assetPair: Option[AssetPair], sender: Address, orderId: Option[ByteStr], timestamp: Option[Long]): Route = - complete((timestamp, orderId) match { - case (Some(ts), None) => askAddressActor[MatcherResponse](sender, AddressActor.CancelAllOrders(assetPair, ts)) - case (None, Some(oid)) => askAddressActor[MatcherResponse](sender, AddressActor.CancelOrder(oid)) - case _ => OrderCancelRejected("Either timestamp or orderId must be specified") - }) - - private def handleCancelRequest(assetPair: Option[AssetPair]): Route = - withCancelRequest { req => - handleCancelRequest(assetPair, req.sender, req.orderId, req.timestamp) - } - - @Path("/orderbook/{amountAsset}/{priceAsset}/cancel") - @ApiOperation( - value = "Cancel order", - notes = "Cancel previously submitted order if it's not already filled completely", - httpMethod = "POST", - produces = "application/json", - consumes = "application/json" - ) - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam( - name = "body", - value = "Json with data", - required = true, - paramType = "body", - dataType = "com.zbsnetwork.matcher.api.CancelOrderRequest" - ) - )) - def cancel: Route = path("orderbook" / AssetPairPM / "cancel") { p => - withAssetPair(p) { pair => - handleCancelRequest(Some(pair)) - } - } - - @Path("/orderbook/cancel") - @ApiOperation( - value = "Cancel all active orders", - httpMethod = "POST", - produces = "application/json", - consumes = "application/json" - ) - @ApiImplicitParams( - Array( - new ApiImplicitParam( - name = "body", - value = "Json with data", - required = true, - paramType = "body", - dataType = "com.zbsnetwork.matcher.api.CancelOrderRequest" - ) - )) - def cancelAll: Route = path("orderbook" / "cancel") { - handleCancelRequest(None) - } - - @Path("/orderbook/{amountAsset}/{priceAsset}/delete") - @Deprecated - @ApiOperation( - value = "Delete Order from History by Id", - notes = "This method is deprecated and doesn't work anymore. Please don't use it.", - httpMethod = "POST", - produces = "application/json", - consumes = "application/json" - ) - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam( - name = "body", - value = "Json with data", - required = true, - paramType = "body", - dataType = "com.zbsnetwork.matcher.api.CancelOrderRequest" - ) - )) - def historyDelete: Route = (path("orderbook" / AssetPairPM / "delete") & post) { _ => - json[CancelOrderRequest] { req => - req.orderId.fold[MatcherResponse](NotImplemented("Batch order deletion is not supported yet"))(OrderDeleted) - } - } - - private def loadOrders(address: Address, pair: Option[AssetPair], activeOnly: Boolean): Route = complete { - askAddressActor[Seq[(ByteStr, OrderInfo[OrderStatus])]](address, AddressActor.GetOrders(pair, activeOnly)) - .map(orders => - StatusCodes.OK -> orders.map { - case (id, oi) => - Json.obj( - "id" -> id.base58, - "type" -> oi.side.toString, - "amount" -> oi.amount, - "price" -> oi.price, - "timestamp" -> oi.timestamp, - "filled" -> (oi.status match { - case OrderStatus.Filled(f) => f - case OrderStatus.PartiallyFilled(f) => f - case OrderStatus.Cancelled(f) => f - case _ => 0L - }), - "status" -> oi.status.name, - "assetPair" -> oi.assetPair.json - ) - }) - } - - @Path("/orderbook/{amountAsset}/{priceAsset}/publicKey/{publicKey}") - @ApiOperation(value = "Order History by Asset Pair and Public Key", - notes = "Get Order History for a given Asset Pair and Public Key", - httpMethod = "GET") - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "publicKey", value = "Public Key", required = true, dataType = "string", paramType = "path"), - new ApiImplicitParam( - name = "activeOnly", - value = "Return active only orders (Accepted and PartiallyFilled)", - required = false, - dataType = "boolean", - paramType = "query", - defaultValue = "false" - ), - new ApiImplicitParam(name = "Timestamp", value = "Timestamp", required = true, dataType = "integer", paramType = "header"), - new ApiImplicitParam(name = "Signature", - value = "Signature of [Public Key ++ Timestamp] bytes", - required = true, - dataType = "string", - paramType = "header") - )) - def getAssetPairAndPublicKeyOrderHistory: Route = (path("orderbook" / AssetPairPM / "publicKey" / PublicKeyPM) & get) { (p, publicKey) => - withAssetPair(p, redirectToInverse = true, s"/publicKey/$publicKey") { pair => - parameters('activeOnly.as[Boolean].?) { activeOnly => - signedGet(publicKey) { - loadOrders(publicKey, Some(pair), activeOnly.getOrElse(false)) - } - } - } - } - - @Path("/orderbook/{publicKey}") - @ApiOperation(value = "Order History by Public Key", notes = "Get Order History for a given Public Key", httpMethod = "GET") - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "publicKey", value = "Public Key", required = true, dataType = "string", paramType = "path"), - new ApiImplicitParam( - name = "activeOnly", - value = "Return active only orders (Accepted and PartiallyFilled)", - required = false, - dataType = "boolean", - paramType = "query", - defaultValue = "false" - ), - new ApiImplicitParam(name = "Timestamp", value = "Timestamp", required = true, dataType = "integer", paramType = "header"), - new ApiImplicitParam(name = "Signature", - value = "Signature of [Public Key ++ Timestamp] bytes", - required = true, - dataType = "string", - paramType = "header") - )) - def getPublicKeyOrderHistory: Route = (path("orderbook" / PublicKeyPM) & get) { publicKey => - parameters('activeOnly.as[Boolean].?) { activeOnly => - signedGet(publicKey) { - loadOrders(publicKey, None, activeOnly.getOrElse(false)) - } - } - } - - @Path("/orders/cancel/{orderId}") - @ApiOperation(value = "Cancel Order by ID without signature", notes = "Cancel Order by ID without signature", httpMethod = "POST") - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "orderId", value = "Order Id", required = true, dataType = "string", paramType = "path") - )) - def forceCancelOrder: Route = (path("orders" / "cancel" / ByteStrPM) & post & withAuth) { orderId => - DBUtils.order(db, orderId) match { - case Some(order) => handleCancelRequest(None, order.sender, Some(orderId), None) - case None => complete(OrderCancelRejected("Order not found")) - } - } - - @Path("/orders/{address}") - @ApiOperation(value = "All Order History by address", notes = "Get All Order History for a given address", httpMethod = "GET") - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "address", value = "Address", dataType = "string", paramType = "path"), - new ApiImplicitParam( - name = "activeOnly", - value = "Return active only orders (Accepted and PartiallyFilled)", - required = false, - dataType = "boolean", - paramType = "query", - defaultValue = "false" - ), - )) - def getAllOrderHistory: Route = (path("orders" / AddressPM) & get & withAuth) { address => - parameters('activeOnly.as[Boolean].?) { activeOnly => - loadOrders(address, None, activeOnly.getOrElse(true)) - } - } - - @Path("/orderbook/{amountAsset}/{priceAsset}/tradableBalance/{address}") - @ApiOperation(value = "Tradable balance for Asset Pair", notes = "Get Tradable balance for the given Asset Pair", httpMethod = "GET") - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "address", value = "Account Address", required = true, dataType = "string", paramType = "path") - )) - def tradableBalance: Route = (path("orderbook" / AssetPairPM / "tradableBalance" / AddressPM) & get) { (pair, address) => - withAssetPair(pair, redirectToInverse = true, s"/tradableBalance/$address") { pair => - complete { - askAddressActor[Map[Option[AssetId], Long]](address, AddressActor.GetTradableBalance(pair)) - .map(stringifyAssetIds) - } - } - } - - @Path("/balance/reserved/{publicKey}") - @ApiOperation(value = "Reserved Balance", notes = "Get non-zero balance of open orders", httpMethod = "GET") - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "publicKey", value = "Public Key", required = true, dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "Timestamp", value = "Timestamp", required = true, dataType = "integer", paramType = "header"), - new ApiImplicitParam(name = "Signature", - value = "Signature of [Public Key ++ Timestamp] bytes", - required = true, - dataType = "string", - paramType = "header") - )) - def reservedBalance: Route = (path("balance" / "reserved" / PublicKeyPM) & get) { publicKey => - signedGet(publicKey) { - complete { - askAddressActor[Map[Option[AssetId], Long]](publicKey, AddressActor.GetReservedBalance) - .map(stringifyAssetIds) - } - } - } - - @Path("/orderbook/{amountAsset}/{priceAsset}/{orderId}") - @ApiOperation(value = "Order Status", notes = "Get Order status for a given Asset Pair during the last 30 days", httpMethod = "GET") - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "orderId", value = "Order Id", required = true, dataType = "string", paramType = "path") - )) - def orderStatus: Route = (path("orderbook" / AssetPairPM / ByteStrPM) & get) { (p, orderId) => - withAssetPair(p, redirectToInverse = true, s"/$orderId") { _ => - complete( - DBUtils - .order(db, orderId) - .fold[Future[OrderStatus]](Future.successful(OrderStatus.NotFound)) { order => - askAddressActor[OrderStatus](order.sender, GetOrderStatus(orderId)) - } - .map(_.json)) - } - } - - @Path("/orderbook/{amountAsset}/{priceAsset}") - @ApiOperation(value = "Remove Order Book for a given Asset Pair", notes = "Remove Order Book for a given Asset Pair", httpMethod = "DELETE") - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "amountAsset", value = "Amount Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path"), - new ApiImplicitParam(name = "priceAsset", value = "Price Asset Id in Pair, or 'ZBS'", dataType = "string", paramType = "path") - )) - def orderBookDelete: Route = (path("orderbook" / AssetPairPM) & delete & withAuth) { p => - withAssetPair(p) { pair => - complete(storeEvent(QueueEvent.OrderBookDeleted(pair)).map(_ => SimpleResponse(StatusCodes.Accepted, "Deleting order book"))) - } - } - - @Path("/transactions/{orderId}") - @ApiOperation(value = "Get Exchange Transactions for order", - notes = "Get all exchange transactions created by DEX on execution of the given order", - httpMethod = "GET") - @ApiImplicitParams( - Array( - new ApiImplicitParam(name = "orderId", value = "Order Id", dataType = "string", paramType = "path") - )) - def getTransactionsByOrder: Route = (path("transactions" / ByteStrPM) & get) { orderId => - complete(StatusCodes.OK -> Json.toJson(DBUtils.transactionsForOrder(db, orderId))) - } - - @Path("/debug/currentOffset") - @ApiOperation(value = "Get a current offset in the queue", notes = "", httpMethod = "GET") - def getCurrentOffset: Route = (path("debug" / "currentOffset") & get & withAuth) { - complete(StatusCodes.OK -> currentOffset()) - } - - @Path("/debug/oldestSnapshotOffset") - @ApiOperation(value = "Get the oldest snapshot's offset in the queue", notes = "", httpMethod = "GET") - def getOldestSnapshotOffset: Route = (path("debug" / "oldestSnapshotOffset") & get & withAuth) { - complete { - (matcher ? GetSnapshotOffsets).mapTo[SnapshotOffsetsResponse].map { x => - StatusCodes.OK -> x.offsets.valuesIterator.min - } - } - } - - @Path("/debug/allSnapshotOffsets") - @ApiOperation(value = "Get all snapshots' offsets in the queue", notes = "", httpMethod = "GET") - def getAllSnapshotOffsets: Route = (path("debug" / "allSnapshotOffsets") & get & withAuth) { - complete { - (matcher ? GetSnapshotOffsets).mapTo[SnapshotOffsetsResponse].map { x => - val js = Json.obj( - x.offsets.map { - case (assetPair, offset) => - assetPair.key -> Json.toJsFieldJsValueWrapper(offset) - }.toSeq: _* - ) - - StatusCodes.OK -> js - } - } - } } -object MatcherApiRoute { - private implicit val timeout: Timeout = 5.seconds - - private def stringifyAssetIds(balances: Map[Option[AssetId], Long]): Map[String, Long] = - balances.map { case (aid, v) => AssetPair.assetIdStr(aid) -> v } -} +object MatcherApiRoute {} diff --git a/src/main/scala/com/zbsnetwork/matcher/model/OrderValidator.scala b/src/main/scala/com/zbsnetwork/matcher/model/OrderValidator.scala index abceb8e..d57265a 100644 --- a/src/main/scala/com/zbsnetwork/matcher/model/OrderValidator.scala +++ b/src/main/scala/com/zbsnetwork/matcher/model/OrderValidator.scala @@ -159,14 +159,12 @@ object OrderValidator { sender: Address, tradableBalance: Option[AssetId] => Long, activeOrderCount: => Int, - lowestOrderTimestamp: => Long, orderExists: ByteStr => Boolean, )(order: Order): ValidationResult = for { _ <- (Right(order): ValidationResult) .ensure(s"Order sender ${order.sender.toAddress} does not match expected $sender")(_.sender.toAddress == sender) .ensure(s"Limit of $MaxActiveOrders active orders has been reached")(_ => activeOrderCount < MaxActiveOrders) - .ensure(s"Order should have a timestamp after $lowestOrderTimestamp, but it is ${order.timestamp}")(_.timestamp > lowestOrderTimestamp) .ensure("Order has already been placed")(o => !orderExists(o.id())) _ <- validateBalance(order, tradableBalance) } yield order diff --git a/src/main/scala/com/zbsnetwork/matcher/queue/KafkaMatcherQueue.scala b/src/main/scala/com/zbsnetwork/matcher/queue/KafkaMatcherQueue.scala index 7098bb0..14d2fc2 100644 --- a/src/main/scala/com/zbsnetwork/matcher/queue/KafkaMatcherQueue.scala +++ b/src/main/scala/com/zbsnetwork/matcher/queue/KafkaMatcherQueue.scala @@ -76,10 +76,13 @@ class KafkaMatcherQueue(settings: Settings)(implicit mat: ActorMaterializer) ext private val consumerControl = new AtomicReference[Consumer.Control](Consumer.NoopControl) private val consumerSettings = { val config = mat.system.settings.config.getConfig("akka.kafka.consumer") - ConsumerSettings(config, new ByteArrayDeserializer, deserializer) + ConsumerSettings(config, new ByteArrayDeserializer, deserializer).withClientId("consumer") } - private val metadataConsumer = mat.system.actorOf(KafkaConsumerActor.props(consumerSettings)) + private val metadataConsumer = mat.system.actorOf( + KafkaConsumerActor.props(consumerSettings.withClientId("meta-consumer")), + "meta-consumer" + ) override def startConsume(fromOffset: QueueEventWithMeta.Offset, process: QueueEventWithMeta => Unit): Unit = { log.info(s"Start consuming from $fromOffset") diff --git a/src/main/scala/com/zbsnetwork/matcher/smart/MatcherContext.scala b/src/main/scala/com/zbsnetwork/matcher/smart/MatcherContext.scala index 031333d..4471bab 100644 --- a/src/main/scala/com/zbsnetwork/matcher/smart/MatcherContext.scala +++ b/src/main/scala/com/zbsnetwork/matcher/smart/MatcherContext.scala @@ -6,12 +6,12 @@ import cats.kernel.Monoid import com.zbsnetwork.lang.Global import com.zbsnetwork.lang.StdLibVersion._ import com.zbsnetwork.lang.v1.compiler.Terms.{CONST_LONG, CaseObj} -import com.zbsnetwork.lang.v1.compiler.Types.FINAL +import com.zbsnetwork.lang.v1.compiler.Types.{FINAL, UNIT} import com.zbsnetwork.lang.v1.evaluator.FunctionIds._ import com.zbsnetwork.lang.v1.evaluator.ctx._ import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.Bindings.{ordType, orderObject} import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.Types._ -import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext, _} +import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext} import com.zbsnetwork.lang.v1.traits.domain.OrdType import com.zbsnetwork.lang.v1.{CTX, FunctionHeader} import com.zbsnetwork.transaction.assets.exchange.Order diff --git a/src/main/scala/com/zbsnetwork/matcher/smart/MatcherScriptRunner.scala b/src/main/scala/com/zbsnetwork/matcher/smart/MatcherScriptRunner.scala index c648b07..0982296 100644 --- a/src/main/scala/com/zbsnetwork/matcher/smart/MatcherScriptRunner.scala +++ b/src/main/scala/com/zbsnetwork/matcher/smart/MatcherScriptRunner.scala @@ -5,17 +5,17 @@ import com.zbsnetwork.account.AddressScheme import com.zbsnetwork.lang.contract.Contract import com.zbsnetwork.lang.v1.compiler.Terms.{EVALUATED, FALSE, TRUE} import com.zbsnetwork.lang.v1.evaluator.{ContractEvaluator, EvaluatorV1, Log} -import com.zbsnetwork.transaction.{Authorized, Proven} import com.zbsnetwork.transaction.assets.exchange.Order -import com.zbsnetwork.transaction.smart.{RealTransactionWrapper, Verifier} +import com.zbsnetwork.transaction.smart.script.v1.ExprScript import com.zbsnetwork.transaction.smart.script.{ContractScript, Script} -import com.zbsnetwork.transaction.smart.script.v1.ExprScript.ExprScriprImpl +import com.zbsnetwork.transaction.smart.{RealTransactionWrapper, Verifier} +import com.zbsnetwork.transaction.{Authorized, Proven} import monix.eval.Coeval object MatcherScriptRunner { def apply(script: Script, order: Order, isTokenScript: Boolean): (Log, Either[String, EVALUATED]) = script match { - case s: ExprScriprImpl => + case s: ExprScript => val ctx = MatcherContext.build(script.stdLibVersion, AddressScheme.current.chainId, Coeval.evalOnce(order), !isTokenScript) EvaluatorV1.applywithLogging(ctx, s.expr) diff --git a/src/main/scala/com/zbsnetwork/network/UtxPoolSynchronizer.scala b/src/main/scala/com/zbsnetwork/network/UtxPoolSynchronizer.scala index 734c226..6066770 100644 --- a/src/main/scala/com/zbsnetwork/network/UtxPoolSynchronizer.scala +++ b/src/main/scala/com/zbsnetwork/network/UtxPoolSynchronizer.scala @@ -9,10 +9,11 @@ import com.zbsnetwork.transaction.Transaction import com.zbsnetwork.utils.ScorexLogging import com.zbsnetwork.utx.UtxPool import io.netty.channel.Channel -import io.netty.channel.group.{ChannelGroup, ChannelMatcher} +import io.netty.channel.group.ChannelGroup +import monix.eval.Task import monix.execution.{CancelableFuture, Scheduler} +import monix.reactive.OverflowStrategy -import scala.util.control.NonFatal import scala.util.{Failure, Success} object UtxPoolSynchronizer extends ScorexLogging { @@ -21,7 +22,7 @@ object UtxPoolSynchronizer extends ScorexLogging { settings: UtxSynchronizerSettings, allChannels: ChannelGroup, txSource: ChannelObservable[Transaction]): CancelableFuture[Unit] = { - implicit val scheduler: Scheduler = Scheduler.singleThread("utx-pool-sync") + implicit val scheduler: Scheduler = Scheduler.forkJoin(settings.parallelism, settings.maxThreads, "utx-pool-sync") val dummy = new Object() val knownTransactions = CacheBuilder @@ -30,39 +31,46 @@ object UtxPoolSynchronizer extends ScorexLogging { .expireAfterWrite(settings.networkTxCacheTime.toMillis, TimeUnit.MILLISECONDS) .build[ByteStr, Object] - val synchronizerFuture = txSource + val newTxSource = txSource .observeOn(scheduler) - .bufferTimedAndCounted(settings.maxBufferTime, settings.maxBufferSize) - .foreach { txBuffer => - val toAdd = txBuffer.filter { - case (_, tx) => - val isNew = Option(knownTransactions.getIfPresent(tx.id())).isEmpty - if (isNew) knownTransactions.put(tx.id(), dummy) - isNew - } + .filter { + case (_, tx) => + var isNew = false + knownTransactions.get(tx.id(), { () => + isNew = true; dummy + }) + isNew + } + + val synchronizerFuture = newTxSource + .whileBusyBuffer(OverflowStrategy.DropOldAndSignal(settings.maxQueueSize, { dropped => + log.warn(s"UTX queue overflow: $dropped transactions dropped") + None + })) + .mapParallelUnordered(settings.parallelism) { + case (sender, transaction) => + Task { + concurrent.blocking(utx.putIfNew(transaction)) match { + case Right((isNew, _)) => + if (isNew) Some(allChannels.write(RawBytes.from(transaction), (_: Channel) != sender)) + else None - if (toAdd.nonEmpty) { - toAdd - .groupBy { case (channel, _) => channel } - .foreach { - case (sender, xs) => - val channelMatcher: ChannelMatcher = { (_: Channel) != sender } - xs.foreach { - case (_, tx) => - utx.putIfNew(tx) match { - case Right((true, _)) => allChannels.write(RawBytes.from(tx), channelMatcher) - case _ => - } - } + case Left(error) => + log.debug(s"Error adding transaction to UTX pool: $error") + None } - allChannels.flush() - } + } } + .bufferTimedAndCounted(settings.maxBufferTime, settings.maxBufferSize) + .filter(_.flatten.nonEmpty) + .foreachL(_ => allChannels.flush()) + .runAsyncLogErr synchronizerFuture.onComplete { - case Success(_) => log.error("UtxPoolSynschronizer stops") - case Failure(NonFatal(th)) => log.error("Error in utx pool synchronizer", th) + case Success(_) => log.info("UtxPoolSynschronizer stops") + case Failure(error) => log.error("Error in utx pool synchronizer", error) } + synchronizerFuture } } diff --git a/src/main/scala/com/zbsnetwork/network/messages.scala b/src/main/scala/com/zbsnetwork/network/messages.scala index 99519d6..2a5f053 100644 --- a/src/main/scala/com/zbsnetwork/network/messages.scala +++ b/src/main/scala/com/zbsnetwork/network/messages.scala @@ -2,12 +2,12 @@ package com.zbsnetwork.network import java.net.InetSocketAddress -import com.zbsnetwork.crypto -import monix.eval.Coeval import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.block.{Block, MicroBlock} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.crypto import com.zbsnetwork.transaction.{Signed, Transaction} +import monix.eval.Coeval sealed trait Message @@ -41,7 +41,7 @@ case class MicroBlockRequest(totalBlockSig: ByteStr) extends Message case class MicroBlockResponse(microblock: MicroBlock) extends Message case class MicroBlockInv(sender: PublicKeyAccount, totalBlockSig: ByteStr, prevBlockSig: ByteStr, signature: ByteStr) extends Message with Signed { - override protected val signatureValid: Coeval[Boolean] = + override val signatureValid: Coeval[Boolean] = Coeval.evalOnce(crypto.verify(signature.arr, sender.toAddress.bytes.arr ++ totalBlockSig.arr ++ prevBlockSig.arr, sender.publicKey)) override def toString: String = s"MicroBlockInv(${totalBlockSig.trim} ~> ${prevBlockSig.trim})" diff --git a/src/main/scala/com/zbsnetwork/network/package.scala b/src/main/scala/com/zbsnetwork/network/package.scala index 3f38abf..339d51a 100644 --- a/src/main/scala/com/zbsnetwork/network/package.scala +++ b/src/main/scala/com/zbsnetwork/network/package.scala @@ -17,10 +17,13 @@ import monix.reactive.Observable import com.zbsnetwork.block.Block import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.transaction.Transaction +import kamon.Kamon import scala.concurrent.duration._ package object network extends ScorexLogging { + private val broadcastTimeStats = Kamon.timer("network-broadcast-time") + def inetSocketAddress(addr: String, defaultPort: Int): InetSocketAddress = { val uri = new URI(s"node://$addr") if (uri.getPort < 0) new InetSocketAddress(addr, defaultPort) @@ -69,9 +72,14 @@ package object network extends ScorexLogging { def broadcast(message: AnyRef, except: Set[Channel]): ChannelGroupFuture = { logBroadcast(message, except) - allChannels.writeAndFlush(message, { (channel: Channel) => - !except.contains(channel) - }) + val st = broadcastTimeStats.refine("object", message.getClass.getSimpleName).start() + allChannels + .writeAndFlush(message, { (channel: Channel) => + !except.contains(channel) + }) + .addListener { _: ChannelGroupFuture => + st.stop() + } } def broadcastMany(messages: Seq[AnyRef], except: Set[Channel] = Set.empty): Unit = { diff --git a/src/main/scala/com/zbsnetwork/protobuf/block/PBBlockSerialization.scala b/src/main/scala/com/zbsnetwork/protobuf/block/PBBlockSerialization.scala new file mode 100644 index 0000000..209127f --- /dev/null +++ b/src/main/scala/com/zbsnetwork/protobuf/block/PBBlockSerialization.scala @@ -0,0 +1,13 @@ +package com.zbsnetwork.protobuf.block +import com.google.protobuf.ByteString +import com.zbsnetwork.protobuf.utils.PBUtils + +private[block] object PBBlockSerialization { + def signedBytes(block: PBBlock): Array[Byte] = { + PBUtils.encodeDeterministic(block) + } + + def unsignedBytes(block: PBBlock): Array[Byte] = { + PBUtils.encodeDeterministic(block.withHeader(block.getHeader.withSignature(ByteString.EMPTY))) + } +} diff --git a/src/main/scala/com/zbsnetwork/protobuf/block/PBBlocks.scala b/src/main/scala/com/zbsnetwork/protobuf/block/PBBlocks.scala new file mode 100644 index 0000000..2e36ca5 --- /dev/null +++ b/src/main/scala/com/zbsnetwork/protobuf/block/PBBlocks.scala @@ -0,0 +1,79 @@ +package com.zbsnetwork.protobuf.block +import com.google.protobuf.ByteString +import com.zbsnetwork.account.PublicKeyAccount +import com.zbsnetwork.block.SignerData +import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.consensus.nxt.NxtLikeConsensusBlockData +import com.zbsnetwork.protobuf.transaction.{PBTransactions, VanillaTransaction} +import com.zbsnetwork.transaction.ValidationError +import com.zbsnetwork.transaction.ValidationError.GenericError + +object PBBlocks { + def vanilla(block: PBBlock): Either[ValidationError, VanillaBlock] = { + def create(version: Int, + timestamp: Long, + reference: ByteStr, + consensusData: NxtLikeConsensusBlockData, + transactionData: Seq[VanillaTransaction], + featureVotes: Set[Short], + generator: PublicKeyAccount, + signature: ByteStr): VanillaBlock = { + VanillaBlock(timestamp, version.toByte, reference, SignerData(generator, signature), consensusData, transactionData, featureVotes) + } + + for { + signedHeader <- block.header.toRight(GenericError("No block header")) + header <- signedHeader.header.toRight(GenericError("No block header")) + transactions <- { + val eithers = block.transactions.map(PBTransactions.vanilla(_)) + (eithers.find(_.isLeft): @unchecked) match { + case None => Right(eithers.map(_.right.get)) + case Some(Left(error)) => Left(error) + } + } + result = create( + header.version, + header.timestamp, + ByteStr(header.reference.toByteArray), + NxtLikeConsensusBlockData(header.baseTarget, ByteStr(header.generationSignature.toByteArray)), + transactions, + header.featureVotes.map(intToShort).toSet, + PublicKeyAccount(header.generator.toByteArray), + ByteStr(signedHeader.signature.toByteArray) + ) + } yield result + } + + def protobuf(block: VanillaBlock): PBBlock = { + import block._ + import consensusData._ + import signerData._ + + new PBBlock( + ByteString.EMPTY, + Some( + PBBlock.SignedHeader( + Some(PBBlock.Header( + ByteString.copyFrom(reference), + baseTarget, + ByteString.copyFrom(generationSignature), + featureVotes.map(shortToInt).toSeq, + timestamp, + version, + ByteString.copyFrom(generator.publicKey) + )), + ByteString.copyFrom(signature) + )), + transactionData.map(PBTransactions.protobuf) + ) + } + + private[this] def shortToInt(s: Short): Int = { + java.lang.Short.toUnsignedInt(s) + } + + private[this] def intToShort(int: Int): Short = { + require(int >= 0 && int <= 65535, s"Short overflow: $int") + int.toShort + } +} diff --git a/src/main/scala/com/zbsnetwork/protobuf/block/block.scala b/src/main/scala/com/zbsnetwork/protobuf/block/block.scala new file mode 100644 index 0000000..5acff06 --- /dev/null +++ b/src/main/scala/com/zbsnetwork/protobuf/block/block.scala @@ -0,0 +1,9 @@ +package com.zbsnetwork.protobuf + +package object block { + type PBBlock = com.zbsnetwork.protobuf.block.Block + val PBBlock = com.zbsnetwork.protobuf.block.Block + + type VanillaBlock = com.zbsnetwork.block.Block + val VanillaBlock = com.zbsnetwork.block.Block +} diff --git a/src/main/scala/com/zbsnetwork/protobuf/transaction/PBOrders.scala b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBOrders.scala new file mode 100644 index 0000000..b78ae93 --- /dev/null +++ b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBOrders.scala @@ -0,0 +1,59 @@ +package com.zbsnetwork.protobuf.transaction +import com.google.protobuf.ByteString +import com.zbsnetwork.account.PublicKeyAccount +import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.transaction.assets.exchange.{OrderV1, OrderV2} +import com.zbsnetwork.{transaction => vt} + +object PBOrders { + import com.zbsnetwork.protobuf.utils.PBInternalImplicits._ + + def vanilla(order: PBOrder, version: Int = 0): VanillaOrder = { + VanillaOrder( + PublicKeyAccount(order.senderPublicKey.toByteArray), + PublicKeyAccount(order.matcherPublicKey.toByteArray), + vt.assets.exchange.AssetPair(Some(order.getAssetPair.amountAssetId.toByteArray), Some(order.getAssetPair.priceAssetId.toByteArray)), + order.orderSide match { + case PBOrder.Side.BUY => vt.assets.exchange.OrderType.BUY + case PBOrder.Side.SELL => vt.assets.exchange.OrderType.SELL + case PBOrder.Side.Unrecognized(v) => throw new IllegalArgumentException(s"Unknown order type: $v") + }, + order.amount, + order.price, + order.timestamp, + order.expiration, + order.getMatcherFee.longAmount, + order.proofs.map(_.toByteArray: ByteStr), + if (version == 0) order.version.toByte else version.toByte + ) + } + + def vanillaV1(order: PBOrder): OrderV1 = vanilla(order, 1) match { + case v1: OrderV1 => v1 + case _ => ??? + } + + def vanillaV2(order: PBOrder): OrderV2 = vanilla(order, 2) match { + case v1: OrderV2 => v1 + case _ => ??? + } + + def protobuf(order: VanillaOrder): PBOrder = { + PBOrder( + ByteString.copyFrom(order.senderPublicKey.publicKey), + ByteString.copyFrom(order.matcherPublicKey.publicKey), + Some(PBOrder.AssetPair(order.assetPair.amountAsset.get, order.assetPair.priceAsset.get)), + order.orderType match { + case vt.assets.exchange.OrderType.BUY => PBOrder.Side.BUY + case vt.assets.exchange.OrderType.SELL => PBOrder.Side.SELL + }, + order.amount, + order.price, + order.timestamp, + order.expiration, + Some((order.matcherFeeAssetId, order.matcherFee)), + order.version, + order.proofs.map(bs => bs: ByteString) + ) + } +} diff --git a/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactionSerialization.scala b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactionSerialization.scala new file mode 100644 index 0000000..f7d8eb8 --- /dev/null +++ b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactionSerialization.scala @@ -0,0 +1,24 @@ +package com.zbsnetwork.protobuf.transaction +import com.google.protobuf.CodedOutputStream +import com.zbsnetwork.protobuf.utils.PBUtils + +private[transaction] object PBTransactionSerialization { + def signedBytes(tx: PBSignedTransaction): Array[Byte] = { + val outArray = new Array[Byte](tx.serializedSize + 2) + val outputStream = CodedOutputStream.newInstance(outArray) + outputStream.useDeterministicSerialization() + + outputStream.write(0xff.toByte) + outputStream.write(0x01.toByte) + tx.writeTo(outputStream) + + outputStream.flush() + outputStream.checkNoSpaceLeft() + + outArray + } + + def unsignedBytes(unsignedTx: PBTransaction): Array[Byte] = { + PBUtils.encodeDeterministic(unsignedTx) + } +} diff --git a/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactions.scala b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactions.scala new file mode 100644 index 0000000..d056a41 --- /dev/null +++ b/src/main/scala/com/zbsnetwork/protobuf/transaction/PBTransactions.scala @@ -0,0 +1,426 @@ +package com.zbsnetwork.protobuf.transaction +import com.google.protobuf.ByteString +import com.zbsnetwork.account.{Address, PublicKeyAccount} +import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.protobuf.transaction.ExchangeTransactionData.{BuySellOrders, Orders} +import com.zbsnetwork.protobuf.transaction.Transaction.Data +import com.zbsnetwork.protobuf.transaction.smart.script.{Script => PBScript} +import com.zbsnetwork.state.{BinaryDataEntry, BooleanDataEntry, IntegerDataEntry, StringDataEntry} +import com.zbsnetwork.transaction.ValidationError.GenericError +import com.zbsnetwork.transaction.smart.script.ScriptReader +import com.zbsnetwork.transaction.transfer.MassTransferTransaction +import com.zbsnetwork.transaction.transfer.MassTransferTransaction.ParsedTransfer +import com.zbsnetwork.transaction.{Proofs, ValidationError} +import com.zbsnetwork.{transaction => vt} + +object PBTransactions { + import com.zbsnetwork.protobuf.utils.PBInternalImplicits._ + + private[this] val NoChainId: Byte = 0: Byte + private[this] val NoAssetId = ByteStr.empty + + def create( + sender: com.zbsnetwork.account.PublicKeyAccount = PublicKeyAccount.empty, + chainId: Byte = 0, + fee: Long = 0L, + feeAssetId: VanillaAssetId = ByteStr.empty, + timestamp: Long = 0L, + version: Int = 0, + proofsArray: Seq[com.zbsnetwork.common.state.ByteStr] = Nil, + data: com.zbsnetwork.protobuf.transaction.Transaction.Data = com.zbsnetwork.protobuf.transaction.Transaction.Data.Empty): SignedTransaction = { + new SignedTransaction( + Some(Transaction(chainId, sender.publicKey: ByteStr, Some((feeAssetId, fee): Amount), timestamp, version, data)), + proofsArray.map(bs => ByteString.copyFrom(bs.arr)) + ) + } + + def vanilla(signedTx: PBSignedTransaction): Either[ValidationError, VanillaTransaction] = { + def toAmountAndAssetId(amount: Amount): Either[ValidationError, (Long, VanillaAssetId)] = amount.amount match { + case Amount.Amount.ZbsAmount(value) => Right((value, ByteStr.empty)) + case Amount.Amount.AssetAmount(AssetAmount(assetId, amount)) => Right((amount, ByteStr(assetId.toByteArray))) + case Amount.Amount.Empty => Left(GenericError("Empty amount")) + } + + for { + parsedTx <- signedTx.transaction.toRight(GenericError("Transaction must be specified")) + fee <- parsedTx.fee.toRight(GenericError("Fee must be specified")) + _ <- Either.cond(parsedTx.data.isDefined, (), GenericError("Transaction data must be specified")) + feeAmount <- toAmountAndAssetId(fee) + sender = PublicKeyAccount(parsedTx.senderPublicKey.toByteArray) + tx <- createVanilla( + parsedTx.version, + if (parsedTx.chainId.isEmpty) NoChainId else parsedTx.chainId.byteAt(0), + sender, + feeAmount._1, + Option(feeAmount._2).filterNot(_.isEmpty), + parsedTx.timestamp, + Proofs(signedTx.proofs.map(bs => ByteStr(bs.toByteArray))), + parsedTx.data + ) + } yield tx + } + + private[this] def createVanilla(version: Int, + chainId: Byte, + sender: PublicKeyAccount, + feeAmount: Long, + feeAssetId: Option[ByteStr], + timestamp: Long, + proofs: Proofs, + data: PBTransaction.Data): Either[ValidationError, VanillaTransaction] = { + + val signature = proofs.toSignature + val result: Either[ValidationError, VanillaTransaction] = data match { + case Data.Genesis(GenesisTransactionData(recipient, amount)) => + vt.GenesisTransaction.create(Address.fromBytes(recipient.toByteArray).right.get, amount, timestamp) + + case Data.Payment(PaymentTransactionData(recipient, amount)) => + vt.PaymentTransaction.create(sender, Address.fromBytes(recipient.toByteArray).right.get, amount, feeAmount, timestamp, signature) + + case Data.Transfer(TransferTransactionData(Some(recipient), Some(amount), attachment)) => + version match { + case 1 => + for { + address <- recipient.toAddressOrAlias + tx <- vt.transfer.TransferTransactionV1.create( + amount.assetId, + sender, + address, + amount.longAmount, + timestamp, + feeAssetId, + feeAmount, + attachment.toByteArray, + signature + ) + } yield tx + + case 2 => + for { + address <- recipient.toAddressOrAlias + tx <- vt.transfer.TransferTransactionV2.create( + amount.assetId, + sender, + address, + amount.longAmount, + timestamp, + feeAssetId, + feeAmount, + attachment.toByteArray, + proofs + ) + } yield tx + + case v => + throw new IllegalArgumentException(s"Unsupported transaction version: $v") + } + + case Data.CreateAlias(CreateAliasTransactionData(alias)) => + version match { + case 1 => + for { + alias <- com.zbsnetwork.account.Alias.buildAlias(chainId, alias) + tx <- vt.CreateAliasTransactionV1.create(sender, alias, feeAmount, timestamp, signature) + } yield tx + + case 2 => + for { + alias <- com.zbsnetwork.account.Alias.buildAlias(chainId, alias) + tx <- vt.CreateAliasTransactionV2.create(sender, alias, feeAmount, timestamp, proofs) + } yield tx + + case v => + throw new IllegalArgumentException(s"Unsupported transaction version: $v") + } + + case Data.Issue(IssueTransactionData(name, description, quantity, decimals, reissuable, script)) => + version match { + case 1 => + vt.assets.IssueTransactionV1.create( + sender, + name.toByteArray, + description.toByteArray, + quantity, + decimals.toByte, + reissuable, + feeAmount, + timestamp, + signature + ) + case 2 => + vt.assets.IssueTransactionV2.create( + chainId, + sender, + name.toByteArray, + description.toByteArray, + quantity, + decimals.toByte, + reissuable, + script.map(s => ScriptReader.fromBytes(s.bytes.toByteArray).right.get), + feeAmount, + timestamp, + proofs + ) + case v => throw new IllegalArgumentException(s"Unsupported transaction version: $v") + } + + case Data.Reissue(ReissueTransactionData(Some(AssetAmount(assetId, amount)), reissuable)) => + version match { + case 1 => + vt.assets.ReissueTransactionV1.create(sender, ByteStr(assetId.toByteArray), amount, reissuable, feeAmount, timestamp, signature) + case 2 => + vt.assets.ReissueTransactionV2.create(chainId, sender, assetId, amount, reissuable, feeAmount, timestamp, proofs) + case v => throw new IllegalArgumentException(s"Unsupported transaction version: $v") + } + + case Data.Burn(BurnTransactionData(Some(AssetAmount(assetId, amount)))) => + version match { + case 1 => vt.assets.BurnTransactionV1.create(sender, assetId, amount, feeAmount, timestamp, signature) + case 2 => vt.assets.BurnTransactionV2.create(chainId, sender, assetId, amount, feeAmount, timestamp, proofs) + case v => throw new IllegalArgumentException(s"Unsupported transaction version: $v") + } + + case Data.SetAssetScript(SetAssetScriptTransactionData(assetId, script)) => + vt.assets.SetAssetScriptTransaction.create( + chainId, + sender, + assetId, + script.map(s => ScriptReader.fromBytes(s.bytes.toByteArray).right.get), + feeAmount, + timestamp, + proofs + ) + + case Data.SetScript(SetScriptTransactionData(script)) => + vt.smart.SetScriptTransaction.create( + sender, + script.map(s => ScriptReader.fromBytes(s.bytes.toByteArray).right.get), + feeAmount, + timestamp, + proofs + ) + + case Data.Lease(LeaseTransactionData(Some(recipient), amount)) => + version match { + case 1 => + for { + address <- recipient.toAddressOrAlias + tx <- vt.lease.LeaseTransactionV1.create(sender, amount, feeAmount, timestamp, address, signature) + } yield tx + + case 2 => + for { + address <- recipient.toAddressOrAlias + tx <- vt.lease.LeaseTransactionV2.create(sender, amount, feeAmount, timestamp, address, proofs) + } yield tx + + case v => + throw new IllegalArgumentException(s"Unsupported transaction version: $v") + } + + case Data.LeaseCancel(LeaseCancelTransactionData(leaseId)) => + version match { + case 1 => vt.lease.LeaseCancelTransactionV1.create(sender, leaseId.byteStr, feeAmount, timestamp, signature) + case 2 => vt.lease.LeaseCancelTransactionV2.create(chainId, sender, leaseId.toByteArray, feeAmount, timestamp, proofs) + case v => throw new IllegalArgumentException(s"Unsupported transaction version: $v") + } + + case Data.Exchange( + ExchangeTransactionData(amount, + price, + buyMatcherFee, + sellMatcherFee, + Orders.BuySellOrders(BuySellOrders(Some(buyOrder), Some(sellOrder))))) => + version match { + case 1 => + vt.assets.exchange.ExchangeTransactionV1.create( + PBOrders.vanillaV1(buyOrder), + PBOrders.vanillaV1(sellOrder), + amount, + price, + buyMatcherFee, + sellMatcherFee, + feeAmount, + timestamp, + signature + ) + case 2 => + vt.assets.exchange.ExchangeTransactionV2.create(PBOrders.vanilla(buyOrder), + PBOrders.vanilla(sellOrder), + amount, + price, + buyMatcherFee, + sellMatcherFee, + feeAmount, + timestamp, + proofs) + case v => throw new IllegalArgumentException(s"Unsupported transaction version: $v") + } + + case Data.DataTransaction(DataTransactionData(data)) => + import DataTransactionData.DataEntry.Value._ + val entries = data.toList.map { de => + de.value match { + case IntValue(num) => IntegerDataEntry(de.key, num) + case BoolValue(bool) => BooleanDataEntry(de.key, bool) + case BinaryValue(bytes) => BinaryDataEntry(de.key, bytes.toByteArray) + case StringValue(str) => StringDataEntry(de.key, str) + case Empty => throw new IllegalArgumentException(s"Empty entries not supported: $data") + } + } + vt.DataTransaction.create( + sender, + entries, + feeAmount, + timestamp, + proofs + ) + + case Data.MassTransfer(MassTransferTransactionData(assetId, transfers, attachment)) => + vt.transfer.MassTransferTransaction.create( + Some(assetId.toByteArray: ByteStr).filterNot(_.isEmpty), + sender, + transfers.flatMap(t => t.getAddress.toAddressOrAlias.toOption.map(ParsedTransfer(_, t.amount))).toList, + timestamp, + feeAmount, + attachment.toByteArray, + proofs + ) + + case Data.SponsorFee(SponsorFeeTransactionData(Some(AssetAmount(assetId, minFee)))) => + vt.assets.SponsorFeeTransaction.create(sender, assetId.toByteArray, Option(minFee).filter(_ > 0), feeAmount, timestamp, proofs) + + case data => + throw new IllegalArgumentException(s"Unsupported transaction data: $data") + } + + result + } + + def protobuf(tx: VanillaTransaction): PBSignedTransaction = { + tx match { + // Uses version "2" for "modern" transactions with single version and proofs field + case vt.GenesisTransaction(recipient, amount, timestamp, signature) => + val data = GenesisTransactionData(ByteString.copyFrom(recipient.bytes), amount) + PBTransactions.create(sender = PublicKeyAccount(Array.emptyByteArray), timestamp = timestamp, version = 1, data = Data.Genesis(data)) + + case vt.PaymentTransaction(sender, recipient, amount, fee, timestamp, signature) => + val data = PaymentTransactionData(ByteString.copyFrom(recipient.bytes), amount) + PBTransactions.create(sender, NoChainId, fee, NoAssetId, timestamp, 1, Seq(signature), Data.Payment(data)) + + case vt.transfer.TransferTransactionV1(assetId, sender, recipient, amount, timestamp, feeAssetId, fee, attachment, signature) => + val data = TransferTransactionData(Some(recipient), Some((assetId, amount)), ByteString.copyFrom(attachment)) + PBTransactions.create(sender, NoChainId, fee, feeAssetId, timestamp, 1, Seq(signature), Data.Transfer(data)) + + case vt.transfer.TransferTransactionV2(sender, recipient, assetId, amount, timestamp, feeAssetId, fee, attachment, proofs) => + val data = TransferTransactionData(Some(recipient), Some((assetId, amount)), ByteString.copyFrom(attachment)) + PBTransactions.create(sender, NoChainId, fee, feeAssetId, timestamp, 2, proofs, Data.Transfer(data)) + + case tx @ vt.CreateAliasTransactionV1(sender, alias, fee, timestamp, signature) => + val data = CreateAliasTransactionData(alias.name) + PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.CreateAlias(data)) + + case tx @ vt.CreateAliasTransactionV2(sender, alias, fee, timestamp, proofs) => + val data = CreateAliasTransactionData(alias.name) + PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.CreateAlias(data)) + + case tx @ vt.assets.exchange + .ExchangeTransactionV1(buyOrder, sellOrder, amount, price, buyMatcherFee, sellMatcherFee, fee, timestamp, signature) => + val data = ExchangeTransactionData( + amount, + price, + buyMatcherFee, + sellMatcherFee, + Orders.BuySellOrders(BuySellOrders(Some(PBOrders.protobuf(buyOrder)), Some(PBOrders.protobuf(sellOrder)))) + ) + PBTransactions.create(tx.sender, NoChainId, fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.Exchange(data)) + + case tx @ vt.assets.exchange.ExchangeTransactionV2(buyOrder, sellOrder, amount, price, buyMatcherFee, sellMatcherFee, fee, timestamp, proofs) => + val data = ExchangeTransactionData( + amount, + price, + buyMatcherFee, + sellMatcherFee, + Orders.BuySellOrders(BuySellOrders(Some(PBOrders.protobuf(buyOrder)), Some(PBOrders.protobuf(sellOrder)))) + ) + PBTransactions.create(tx.sender, 0: Byte, fee, tx.assetFee._1, timestamp, 2, proofs, Data.Exchange(data)) + + case vt.assets.IssueTransactionV1(sender, name, description, quantity, decimals, reissuable, fee, timestamp, signature) => + val data = IssueTransactionData(ByteStr(name), ByteStr(description), quantity, decimals, reissuable, None) + PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.Issue(data)) + + case vt.assets.IssueTransactionV2(chainId, sender, name, description, quantity, decimals, reissuable, script, fee, timestamp, proofs) => + val data = IssueTransactionData(ByteStr(name), ByteStr(description), quantity, decimals, reissuable, script.map(s => PBScript(s.bytes()))) + PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.Issue(data)) + + case tx @ vt.assets.ReissueTransactionV1(sender, assetId, quantity, reissuable, fee, timestamp, signature) => + val data = ReissueTransactionData(Some(AssetAmount(assetId, quantity)), reissuable) + PBTransactions.create(sender, tx.chainByte.getOrElse(NoChainId), fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.Reissue(data)) + + case tx @ vt.assets.ReissueTransactionV2(chainId, sender, assetId, amount, reissuable, fee, timestamp, proofs) => + val data = ReissueTransactionData(Some(AssetAmount(assetId, amount)), reissuable) + PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.Reissue(data)) + + case tx @ vt.assets.BurnTransactionV1(sender, assetId, amount, fee, timestamp, signature) => + val data = BurnTransactionData(Some(AssetAmount(assetId, amount))) + PBTransactions.create(sender, tx.chainByte.getOrElse(NoChainId), fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.Burn(data)) + + case tx @ vt.assets.BurnTransactionV2(chainId, sender, assetId, amount, fee, timestamp, proofs) => + val data = BurnTransactionData(Some(AssetAmount(assetId, amount))) + PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.Burn(data)) + + case vt.assets.SetAssetScriptTransaction(chainId, sender, assetId, script, fee, timestamp, proofs) => + val data = SetAssetScriptTransactionData(assetId, script.map(s => PBScript(s.bytes()))) + PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.SetAssetScript(data)) + + case vt.smart.SetScriptTransaction(chainId, sender, script, fee, timestamp, proofs) => + val data = SetScriptTransactionData(script.map(s => PBScript(s.bytes()))) + PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.SetScript(data)) + + case tx @ vt.lease.LeaseTransactionV1(sender, amount, fee, timestamp, recipient, signature) => + val data = LeaseTransactionData(Some(recipient), amount) + PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.Lease(data)) + + case tx @ vt.lease.LeaseTransactionV2(sender, amount, fee, timestamp, recipient, proofs) => + val data = LeaseTransactionData(Some(recipient), amount) + PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.Lease(data)) + + case tx @ vt.lease.LeaseCancelTransactionV1(sender, leaseId, fee, timestamp, signature) => + val data = LeaseCancelTransactionData(leaseId) + PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 1, Seq(signature), Data.LeaseCancel(data)) + + case tx @ vt.lease.LeaseCancelTransactionV2(chainId, sender, leaseId, fee, timestamp, proofs) => + val data = LeaseCancelTransactionData(leaseId) + PBTransactions.create(sender, chainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.LeaseCancel(data)) + + case tx @ MassTransferTransaction(assetId, sender, transfers, timestamp, fee, attachment, proofs) => + val data = MassTransferTransactionData( + ByteString.copyFrom(assetId.getOrElse(ByteStr.empty)), + transfers.map(pt => MassTransferTransactionData.Transfer(Some(pt.address), pt.amount)), + attachment: ByteStr + ) + PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.MassTransfer(data)) + + case tx @ vt.DataTransaction(sender, data, fee, timestamp, proofs) => + val txData = DataTransactionData( + data.map(de => + DataTransactionData.DataEntry( + de.key, + de match { + case IntegerDataEntry(_, value) => DataTransactionData.DataEntry.Value.IntValue(value) + case BooleanDataEntry(_, value) => DataTransactionData.DataEntry.Value.BoolValue(value) + case BinaryDataEntry(_, value) => DataTransactionData.DataEntry.Value.BinaryValue(value) + case StringDataEntry(_, value) => DataTransactionData.DataEntry.Value.StringValue(value) + } + ))) + PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.DataTransaction(txData)) + + case tx @ vt.assets.SponsorFeeTransaction(sender, assetId, minSponsoredAssetFee, fee, timestamp, proofs) => + val data = SponsorFeeTransactionData(Some(AssetAmount(assetId, minSponsoredAssetFee.getOrElse(0L)))) + PBTransactions.create(sender, NoChainId, fee, tx.assetFee._1, timestamp, 2, proofs, Data.SponsorFee(data)) + + case _ => + throw new IllegalArgumentException(s"Unsupported transaction: $tx") + } + } +} diff --git a/src/main/scala/com/zbsnetwork/protobuf/transaction/transaction.scala b/src/main/scala/com/zbsnetwork/protobuf/transaction/transaction.scala new file mode 100644 index 0000000..5e32759 --- /dev/null +++ b/src/main/scala/com/zbsnetwork/protobuf/transaction/transaction.scala @@ -0,0 +1,22 @@ +package com.zbsnetwork.protobuf + +package object transaction { + type PBOrder = com.zbsnetwork.protobuf.transaction.ExchangeTransactionData.Order + val PBOrder = com.zbsnetwork.protobuf.transaction.ExchangeTransactionData.Order + + type VanillaOrder = com.zbsnetwork.transaction.assets.exchange.Order + val VanillaOrder = com.zbsnetwork.transaction.assets.exchange.Order + + type PBTransaction = com.zbsnetwork.protobuf.transaction.Transaction + val PBTransaction = com.zbsnetwork.protobuf.transaction.Transaction + + type PBSignedTransaction = com.zbsnetwork.protobuf.transaction.SignedTransaction + val PBSignedTransaction = com.zbsnetwork.protobuf.transaction.SignedTransaction + + type VanillaTransaction = com.zbsnetwork.transaction.Transaction + val VanillaTransaction = com.zbsnetwork.transaction.Transaction + + type VanillaSignedTransaction = com.zbsnetwork.transaction.SignedTransaction + + type VanillaAssetId = com.zbsnetwork.transaction.AssetId +} diff --git a/src/main/scala/com/zbsnetwork/protobuf/utils/PBInternalImplicits.scala b/src/main/scala/com/zbsnetwork/protobuf/utils/PBInternalImplicits.scala new file mode 100644 index 0000000..da11777 --- /dev/null +++ b/src/main/scala/com/zbsnetwork/protobuf/utils/PBInternalImplicits.scala @@ -0,0 +1,86 @@ +package com.zbsnetwork.protobuf.utils +import com.google.protobuf.ByteString +import com.zbsnetwork.account.PublicKeyAccount +import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.protobuf.account.{Alias, Recipient} +import com.zbsnetwork.protobuf.transaction.{Amount, AssetAmount, VanillaAssetId} +import com.zbsnetwork.transaction.ValidationError + +private[protobuf] object PBInternalImplicits { + import com.google.protobuf.{ByteString => PBByteString} + import com.zbsnetwork.account.{AddressOrAlias, Address => VAddress, Alias => VAlias} + + implicit def byteStringToByteStr(bs: PBByteString): ByteStr = bs.toByteArray + implicit def byteStrToByteString(bs: ByteStr): PBByteString = PBByteString.copyFrom(bs) + + implicit def fromAddressOrAlias(addressOrAlias: AddressOrAlias): Recipient = addressOrAlias match { + case a: VAddress => fromAddress(a) + case al: VAlias => fromAlias(al) + } + + implicit def fromAddress(address: VAddress): Recipient = { + Recipient.defaultInstance.withAddress(address.bytes) + } + + implicit def fromAlias(alias: VAlias): Recipient = { + Recipient.defaultInstance.withAlias(Alias(alias.chainId: Byte, alias.name)) + } + + implicit class PBRecipientImplicitConversionOps(recipient: Recipient) { + def toAddress: Either[ValidationError, VAddress] = { + VAddress.fromBytes(recipient.getAddress.toByteArray) + } + + def toAlias: Either[ValidationError, VAlias] = { + val alias = recipient.getAlias + VAlias.buildAlias(if (alias.chainId.isEmpty) 0: Byte else alias.chainId.byteAt(0), alias.name) + } + + def toAddressOrAlias: Either[ValidationError, AddressOrAlias] = recipient.recipient match { + case Recipient.Recipient.Alias(_) => this.toAlias + case Recipient.Recipient.Address(_) => this.toAddress + case Recipient.Recipient.Empty => throw new IllegalArgumentException("Empty address not supported") + } + } + + implicit def fromAssetIdOptionAndAmount(v: (Option[VanillaAssetId], Long)): Amount = v match { + case (Some(assetId), amount) => + Amount.defaultInstance.withAssetAmount(AssetAmount(assetId, amount)) + + case (None, amount) => + Amount.defaultInstance.withZbsAmount(amount) + } + + implicit def fromAssetIdAndAmount(v: (VanillaAssetId, Long)): Amount = { + fromAssetIdOptionAndAmount((Option(v._1).filterNot(_.isEmpty), v._2)) + } + + implicit class AmountImplicitConversions(a: Amount) { + def longAmount: Long = a.amount match { + case Amount.Amount.Empty => 0L + case Amount.Amount.ZbsAmount(value) => value + case Amount.Amount.AssetAmount(value) => value.amount + } + + def assetId: ByteStr = a.amount match { + case Amount.Amount.ZbsAmount(_) | Amount.Amount.Empty => ByteStr.empty + case Amount.Amount.AssetAmount(AssetAmount(assetId, _)) => ByteStr(assetId.toByteArray) + } + } + + implicit class PBByteStringOps(bs: PBByteString) { + def byteStr = ByteStr(bs.toByteArray) + def publicKeyAccount = PublicKeyAccount(bs.toByteArray) + } + + implicit def byteStringToByte(bytes: ByteString): Byte = + if (bytes.isEmpty) 0 + else bytes.byteAt(0) + + implicit def byteToByteString(chainId: Byte): ByteString = { + if (chainId == 0) ByteString.EMPTY else ByteString.copyFrom(Array(chainId)) + } + + implicit def assetIdToAssetIdOption(assetId: VanillaAssetId): Option[VanillaAssetId] = Option(assetId).filterNot(_.isEmpty) + implicit def assetIdOptionToAssetId(assetId: Option[VanillaAssetId]): VanillaAssetId = assetId.getOrElse(ByteStr.empty) +} diff --git a/src/main/scala/com/zbsnetwork/protobuf/utils/PBUtils.scala b/src/main/scala/com/zbsnetwork/protobuf/utils/PBUtils.scala new file mode 100644 index 0000000..c2dc99c --- /dev/null +++ b/src/main/scala/com/zbsnetwork/protobuf/utils/PBUtils.scala @@ -0,0 +1,14 @@ +package com.zbsnetwork.protobuf.utils +import com.google.protobuf.CodedOutputStream +import scalapb.GeneratedMessage + +object PBUtils { + def encodeDeterministic(msg: GeneratedMessage): Array[Byte] = { + val outArray = new Array[Byte](msg.serializedSize) + val outputStream = CodedOutputStream.newInstance(outArray) + outputStream.useDeterministicSerialization() // Adds this + msg.writeTo(outputStream) + outputStream.checkNoSpaceLeft() + outArray + } +} diff --git a/src/main/scala/com/zbsnetwork/serialization/Deser.scala b/src/main/scala/com/zbsnetwork/serialization/Deser.scala index 7ac2d09..ba44c17 100644 --- a/src/main/scala/com/zbsnetwork/serialization/Deser.scala +++ b/src/main/scala/com/zbsnetwork/serialization/Deser.scala @@ -6,7 +6,13 @@ object Deser { def serializeBoolean(b: Boolean): Array[Byte] = if (b) Array(1: Byte) else Array(0: Byte) - def serializeArray(b: Array[Byte]): Array[Byte] = Shorts.toByteArray(b.length.toShort) ++ b + def serializeArray(b: Array[Byte]): Array[Byte] = { + val length = b.length + if (length.isValidShort) + Shorts.toByteArray(length.toShort) ++ b + else + throw new IllegalArgumentException(s"Attempting to serialize array with size, but the size($length) exceeds MaxShort(${Short.MaxValue})") + } def parseArraySize(bytes: Array[Byte], position: Int): (Array[Byte], Int) = { val length = Shorts.fromByteArray(bytes.slice(position, position + 2)) diff --git a/src/main/scala/com/zbsnetwork/settings/BlockchainSettings.scala b/src/main/scala/com/zbsnetwork/settings/BlockchainSettings.scala index 5649d60..4e49875 100644 --- a/src/main/scala/com/zbsnetwork/settings/BlockchainSettings.scala +++ b/src/main/scala/com/zbsnetwork/settings/BlockchainSettings.scala @@ -52,17 +52,17 @@ object FunctionalitySettings { val MAINNET = apply( featureCheckBlocksPeriod = 5000, blocksForFeatureActivation = 4000, - allowTemporaryNegativeUntil = 1479168000000L, - generationBalanceDepthFrom50To1000AfterHeight = 232000, - minimalGeneratingBalanceAfter = 1479168000000L, - allowTransactionsFromFutureUntil = 1479168000000L, - allowUnissuedAssetsUntil = 1479416400000L, - allowInvalidReissueInSameBlockUntilTimestamp = 1492768800000L, - allowMultipleLeaseCancelTransactionUntilTimestamp = 1492768800000L, - resetEffectiveBalancesAtHeight = 462000, - blockVersion3AfterHeight = 795000, - preActivatedFeatures = Map.empty, - doubleFeaturesPeriodsAfterHeight = 810000, + allowTemporaryNegativeUntil = 0, + generationBalanceDepthFrom50To1000AfterHeight = 0, + minimalGeneratingBalanceAfter = 0, + allowTransactionsFromFutureUntil = 0, + allowUnissuedAssetsUntil = 0, + allowInvalidReissueInSameBlockUntilTimestamp = 0, + allowMultipleLeaseCancelTransactionUntilTimestamp = 0, + resetEffectiveBalancesAtHeight = 1, + blockVersion3AfterHeight = 0, + preActivatedFeatures = Map[Short, Int]((1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0), (7, 0), (8, 0), (9, 0), (10, 0), (11, 0)), + doubleFeaturesPeriodsAfterHeight = 0, maxTransactionTimeBackOffset = 120.minutes, maxTransactionTimeForwardOffset = 90.minutes ) @@ -100,13 +100,16 @@ case class GenesisSettings(blockTimestamp: Long, object GenesisSettings { val MAINNET = GenesisSettings( - 1538689931932L, - 1478000000000L, + 1550685686495L, + 1550685686495L, Constants.UnitsInZbs * Constants.TotalZbs, - ByteStr.decodeBase58("64dMbqe7XGomp7XU2SqfHGhUqGSwiQLD3TJCC4WMYPTAjkh2bprZJm9YT3mRxMqfMH5DwvnhdeyAm2FnUXYnUtm1").toOption, + ByteStr.decodeBase58("45c4qsxnnvdj1MNTK1DaBbs4mjuuQACbx46NxFR3URxp836oyhyvEChtVnRU2c5oF635tGVstZWrKaKg4Sp3EYNW").toOption, List( - GenesisTransactionSettings("3Qbnb1eSHqztPMvR7qHhEQoGn6HAcLRe4Yz", (Constants.UnitsInZbs * Constants.TotalZbs * 0.5).toLong), - GenesisTransactionSettings("3QE1Hju1y8CS3efwYwfikjUyABUd9BNfudc", (Constants.UnitsInZbs * Constants.TotalZbs * 0.5).toLong), + GenesisTransactionSettings("3QMdCYWmaSVHSNtvrWwYdshG9wekjekRxDa", 2549000000000000L), + GenesisTransactionSettings("3QNkA6HHfJ6vBenz41NpKsobe9WVgx7KfzQ", 1800000000000L), + GenesisTransactionSettings("3QcmkoyzMdTs6fhS8kiwttzwZjEtKtNdTiw", 2549000000000000L), + GenesisTransactionSettings("3QH6unL8QUpT9ptnonC9Eo6PuXBT65VW66D", 100000000000L), + GenesisTransactionSettings("3QY1vQeGyZ6sSeNRmw3wQQ33Yrkwuia9fJW", 100000000000L), ), 153722867L, 60.seconds diff --git a/src/main/scala/com/zbsnetwork/settings/Constants.scala b/src/main/scala/com/zbsnetwork/settings/Constants.scala index 55c75d5..8e5d534 100644 --- a/src/main/scala/com/zbsnetwork/settings/Constants.scala +++ b/src/main/scala/com/zbsnetwork/settings/Constants.scala @@ -11,5 +11,5 @@ object Constants extends ScorexLogging { val AgentName = s"Zbs v${Version.VersionString}" val UnitsInZbs = 100000000L - val TotalZbs = 100000000L + val TotalZbs = 51000000L } diff --git a/src/main/scala/com/zbsnetwork/settings/SynchronizationSettings.scala b/src/main/scala/com/zbsnetwork/settings/SynchronizationSettings.scala index c50deef..393e990 100644 --- a/src/main/scala/com/zbsnetwork/settings/SynchronizationSettings.scala +++ b/src/main/scala/com/zbsnetwork/settings/SynchronizationSettings.scala @@ -26,7 +26,13 @@ object SynchronizationSettings { case class HistoryReplierSettings(maxMicroBlockCacheSize: Int, maxBlockCacheSize: Int) - case class UtxSynchronizerSettings(networkTxCacheSize: Int, networkTxCacheTime: FiniteDuration, maxBufferSize: Int, maxBufferTime: FiniteDuration) + case class UtxSynchronizerSettings(networkTxCacheSize: Int, + networkTxCacheTime: FiniteDuration, + maxBufferSize: Int, + maxBufferTime: FiniteDuration, + parallelism: Int, + maxThreads: Int, + maxQueueSize: Int) val configPath: String = "zbs.synchronization" diff --git a/src/main/scala/com/zbsnetwork/settings/UtxSettings.scala b/src/main/scala/com/zbsnetwork/settings/UtxSettings.scala index 59a30f3..bb4bb25 100644 --- a/src/main/scala/com/zbsnetwork/settings/UtxSettings.scala +++ b/src/main/scala/com/zbsnetwork/settings/UtxSettings.scala @@ -1,10 +1,7 @@ package com.zbsnetwork.settings -import scala.concurrent.duration.FiniteDuration - case class UtxSettings(maxSize: Int, maxBytesSize: Long, blacklistSenderAddresses: Set[String], allowBlacklistedTransferTo: Set[String], - cleanupInterval: FiniteDuration, allowTransactionsFromSmartAccounts: Boolean) diff --git a/src/main/scala/com/zbsnetwork/state/Blockchain.scala b/src/main/scala/com/zbsnetwork/state/Blockchain.scala index a853bc7..bfb1ae4 100644 --- a/src/main/scala/com/zbsnetwork/state/Blockchain.scala +++ b/src/main/scala/com/zbsnetwork/state/Blockchain.scala @@ -71,14 +71,14 @@ trait Blockchain { def leaseBalance(address: Address): LeaseBalance - def balance(address: Address, mayBeAssetId: Option[AssetId]): Long + def balance(address: Address, mayBeAssetId: Option[AssetId] = None): Long def assetDistribution(assetId: ByteStr): AssetDistribution def assetDistributionAtHeight(assetId: AssetId, height: Int, count: Int, fromAddress: Option[Address]): Either[ValidationError, AssetDistributionPage] - def zbsDistribution(height: Int): Map[Address, Long] + def zbsDistribution(height: Int): Either[ValidationError, Map[Address, Long]] // the following methods are used exclusively by patches def allActiveLeases: Set[LeaseTransaction] diff --git a/src/main/scala/com/zbsnetwork/state/BlockchainUpdaterImpl.scala b/src/main/scala/com/zbsnetwork/state/BlockchainUpdaterImpl.scala index 2b42e0c..a66d916 100644 --- a/src/main/scala/com/zbsnetwork/state/BlockchainUpdaterImpl.scala +++ b/src/main/scala/com/zbsnetwork/state/BlockchainUpdaterImpl.scala @@ -1,6 +1,9 @@ package com.zbsnetwork.state +import java.util.concurrent.locks.{Lock, ReentrantReadWriteLock} + import cats.implicits._ +import cats.kernel.Monoid import com.zbsnetwork.account.{Address, Alias} import com.zbsnetwork.block.Block.BlockId import com.zbsnetwork.block.{Block, BlockHeader, MicroBlock} @@ -23,7 +26,7 @@ import kamon.metric.MeasurementUnit import monix.reactive.subjects.ConcurrentSubject import monix.reactive.{Observable, Observer} -class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[Address], settings: ZbsSettings, time: Time) +class BlockchainUpdaterImpl(blockchain: Blockchain, spendableBalanceChanged: Observer[(Address, Option[AssetId])], settings: ZbsSettings, time: Time) extends BlockchainUpdater with NG with ScorexLogging @@ -32,6 +35,19 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A import com.zbsnetwork.state.BlockchainUpdaterImpl._ import settings.blockchainSettings.functionalitySettings + private def inLock[R](l: Lock, f: => R) = { + try { + l.lock() + val res = f + res + } finally { + l.unlock() + } + } + private val lock = new ReentrantReadWriteLock + private def writeLock[B](f: => B): B = inLock(lock.writeLock(), f) + private def readLock[B](f: => B): B = inLock(lock.readLock(), f) + private lazy val maxBlockReadinessAge = settings.minerSettings.intervalAfterLastBlockThenGenerationIsAllowed.toMillis private var ngState: Option[NgState] = Option.empty @@ -40,12 +56,14 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A private val service = monix.execution.Scheduler.singleThread("last-block-info-publisher") private val internalLastBlockInfo = ConcurrentSubject.publish[LastBlockInfo](service) - override def isLastBlockId(id: ByteStr): Boolean = ngState.exists(_.contains(id)) || lastBlock.exists(_.uniqueId == id) + override def isLastBlockId(id: ByteStr): Boolean = readLock { + ngState.exists(_.contains(id)) || lastBlock.exists(_.uniqueId == id) + } override val lastBlockInfo: Observable[LastBlockInfo] = internalLastBlockInfo.cache(1) lastBlockInfo.subscribe()(monix.execution.Scheduler.global) // Start caching - def blockchainReady: Boolean = { + private def blockchainReady: Boolean = { val lastBlock = ngState.map(_.base.timestamp).orElse(blockchain.lastBlockTimestamp).get lastBlock + maxBlockReadinessAge > time.correctedTime() } @@ -99,7 +117,7 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A } } - override def processBlock(block: Block, verify: Boolean = true): Either[ValidationError, Option[DiscardedTransactions]] = { + override def processBlock(block: Block, verify: Boolean = true): Either[ValidationError, Option[DiscardedTransactions]] = writeLock { val height = blockchain.height val notImplementedFeatures: Set[Short] = blockchain.activatedFeaturesAt(height).diff(BlockchainFeatures.implemented) @@ -198,8 +216,7 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A restTotalConstraint = updatedTotalConstraint val prevNgState = ngState ngState = Some(new NgState(block, newBlockDiff, carry, featuresApprovedWithBlock(block))) - - prevNgState.toIterable.flatMap(_.bestLiquidDiff.portfolios.keys).foreach(portfolioChanged.onNext) + notifyChangedSpendable(prevNgState, ngState) lastBlockId.foreach(id => internalLastBlockInfo.onNext(LastBlockInfo(id, height, score, blockchainReady))) if ((block.timestamp > time @@ -211,7 +228,7 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A }) } - override def removeAfter(blockId: ByteStr): Either[ValidationError, Seq[Block]] = { + override def removeAfter(blockId: ByteStr): Either[ValidationError, Seq[Block]] = writeLock { log.info(s"Removing blocks after ${blockId.trim} from blockchain") val prevNgState = ngState @@ -227,11 +244,29 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A .leftMap(err => GenericError(err)) } - prevNgState.toIterable.flatMap(_.bestLiquidDiff.portfolios.keys).foreach(portfolioChanged.onNext) + notifyChangedSpendable(prevNgState, ngState) r } - override def processMicroBlock(microBlock: MicroBlock, verify: Boolean = true): Either[ValidationError, Unit] = { + private def notifyChangedSpendable(prevNgState: Option[NgState], newNgState: Option[NgState]): Unit = { + val changedPortfolios = (prevNgState, newNgState) match { + case (Some(p), Some(n)) => diff(p.bestLiquidDiff.portfolios, n.bestLiquidDiff.portfolios) + case (Some(x), _) => x.bestLiquidDiff.portfolios + case (_, Some(x)) => x.bestLiquidDiff.portfolios + case _ => Map.empty + } + + changedPortfolios.foreach { + case (addr, p) => + p.assetIds.view + .filter(x => p.spendableBalanceOf(x) != 0) + .foreach(assetId => spendableBalanceChanged.onNext(addr -> assetId)) + } + } + + private def diff(p1: Map[Address, Portfolio], p2: Map[Address, Portfolio]) = Monoid.combine(p1, p2.map { case (k, v) => k -> v.negate }) + + override def processMicroBlock(microBlock: MicroBlock, verify: Boolean = true): Either[ValidationError, Unit] = writeLock { ngState match { case None => Left(MicroBlockAppendError("No base block exists", microBlock)) @@ -265,7 +300,11 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A ng.append(microBlock, diff, carry, System.currentTimeMillis) log.info(s"$microBlock appended") internalLastBlockInfo.onNext(LastBlockInfo(microBlock.totalResBlockSig, height, score, ready = true)) - diff.portfolios.keys.foreach(portfolioChanged.onNext) + + for { + (addr, p) <- diff.portfolios + assetId <- p.assetIds + } spendableBalanceChanged.onNext(addr -> assetId) } } } @@ -278,12 +317,15 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A private def newlyApprovedFeatures = ngState.fold(Map.empty[Short, Int])(_.approvedFeatures.map(_ -> height).toMap) - override def approvedFeatures: Map[Short, Int] = newlyApprovedFeatures ++ blockchain.approvedFeatures + override def approvedFeatures: Map[Short, Int] = readLock { + newlyApprovedFeatures ++ blockchain.approvedFeatures + } - override def activatedFeatures: Map[Short, Int] = + override def activatedFeatures: Map[Short, Int] = readLock { newlyApprovedFeatures.mapValues(_ + functionalitySettings.activationWindowSize(height)) ++ blockchain.activatedFeatures + } - override def featureVotes(height: Int): Map[Short, Int] = { + override def featureVotes(height: Int): Map[Short, Int] = readLock { val innerVotes = blockchain.featureVotes(height) ngState match { case Some(ng) if this.height <= height => @@ -300,70 +342,92 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A (s.bestLiquidBlock, s.bestLiquidBlock.bytes().length) } - override def blockHeaderAndSize(blockId: BlockId): Option[(BlockHeader, Int)] = + override def blockHeaderAndSize(blockId: BlockId): Option[(BlockHeader, Int)] = readLock { liquidBlockHeaderAndSize().filter(_._1.uniqueId == blockId) orElse blockchain.blockHeaderAndSize(blockId) + } - override def height: Int = blockchain.height + ngState.fold(0)(_ => 1) + override def height: Int = readLock { + blockchain.height + ngState.fold(0)(_ => 1) + } - override def blockBytes(height: Int): Option[Array[Byte]] = + override def blockBytes(height: Int): Option[Array[Byte]] = readLock { blockchain .blockBytes(height) .orElse(ngState.collect { case ng if height == blockchain.height + 1 => ng.bestLiquidBlock.bytes() }) + } - override def scoreOf(blockId: BlockId): Option[BigInt] = + override def scoreOf(blockId: BlockId): Option[BigInt] = readLock { blockchain .scoreOf(blockId) .orElse(ngState.collect { case ng if ng.contains(blockId) => blockchain.score + ng.base.blockScore() }) + } - override def heightOf(blockId: BlockId): Option[Int] = + override def heightOf(blockId: BlockId): Option[Int] = readLock { blockchain .heightOf(blockId) .orElse(ngState.collect { case ng if ng.contains(blockId) => this.height }) + } - override def lastBlockIds(howMany: Int): Seq[BlockId] = + override def lastBlockIds(howMany: Int): Seq[BlockId] = readLock { ngState.fold(blockchain.lastBlockIds(howMany))(_.bestLiquidBlockId +: blockchain.lastBlockIds(howMany - 1)) + } - override def microBlock(id: BlockId): Option[MicroBlock] = + override def microBlock(id: BlockId): Option[MicroBlock] = readLock { for { ng <- ngState mb <- ng.microBlock(id) } yield mb + } - def lastBlockTimestamp: Option[Long] = ngState.map(_.base.timestamp).orElse(blockchain.lastBlockTimestamp) + def lastBlockTimestamp: Option[Long] = readLock { + ngState.map(_.base.timestamp).orElse(blockchain.lastBlockTimestamp) + } - def lastBlockId: Option[AssetId] = ngState.map(_.bestLiquidBlockId).orElse(blockchain.lastBlockId) + def lastBlockId: Option[AssetId] = readLock { + ngState.map(_.bestLiquidBlockId).orElse(blockchain.lastBlockId) + } - def blockAt(height: Int): Option[Block] = + def blockAt(height: Int): Option[Block] = readLock { if (height == this.height) ngState.map(_.bestLiquidBlock) else blockchain.blockAt(height) + } - override def lastPersistedBlockIds(count: Int): Seq[BlockId] = { + override def lastPersistedBlockIds(count: Int): Seq[BlockId] = readLock { blockchain.lastBlockIds(count) } - override def microblockIds: Seq[BlockId] = ngState.fold(Seq.empty[BlockId])(_.microBlockIds) + override def microblockIds: Seq[BlockId] = readLock { + ngState.fold(Seq.empty[BlockId])(_.microBlockIds) + } - override def bestLastBlockInfo(maxTimestamp: Long): Option[BlockMinerInfo] = { + override def bestLastBlockInfo(maxTimestamp: Long): Option[BlockMinerInfo] = readLock { ngState .map(_.bestLastBlockInfo(maxTimestamp)) .orElse(blockchain.lastBlock.map(b => BlockMinerInfo(b.consensusData, b.timestamp, b.uniqueId))) } - override def score: BigInt = blockchain.score + ngState.fold(BigInt(0))(_.bestLiquidBlock.blockScore()) + override def score: BigInt = readLock { + blockchain.score + ngState.fold(BigInt(0))(_.bestLiquidBlock.blockScore()) + } - override def lastBlock: Option[Block] = ngState.map(_.bestLiquidBlock).orElse(blockchain.lastBlock) + override def lastBlock: Option[Block] = readLock { + ngState.map(_.bestLiquidBlock).orElse(blockchain.lastBlock) + } - override def carryFee: Long = ngState.map(_.carryFee).getOrElse(blockchain.carryFee) + override def carryFee: Long = readLock { + ngState.map(_.carryFee).getOrElse(blockchain.carryFee) + } - override def blockBytes(blockId: ByteStr): Option[Array[Byte]] = + override def blockBytes(blockId: ByteStr): Option[Array[Byte]] = readLock { (for { ng <- ngState (block, _, _, _) <- ng.totalDiffOf(blockId) } yield block.bytes()).orElse(blockchain.blockBytes(blockId)) + } - override def blockIdsAfter(parentSignature: ByteStr, howMany: Int): Option[Seq[ByteStr]] = { + override def blockIdsAfter(parentSignature: ByteStr, howMany: Int): Option[Seq[ByteStr]] = readLock { ngState match { case Some(ng) if ng.contains(parentSignature) => Some(Seq.empty[ByteStr]) case maybeNg => @@ -373,7 +437,7 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A } } - override def parent(block: Block, back: Int): Option[Block] = { + override def parent(block: Block, back: Int): Option[Block] = readLock { ngState match { case Some(ng) if ng.contains(block.reference) => if (back == 1) Some(ng.base) else blockchain.parent(ng.base, back - 1) @@ -382,64 +446,76 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A } } - override def blockHeaderAndSize(height: Int): Option[(BlockHeader, Int)] = { + override def blockHeaderAndSize(height: Int): Option[(BlockHeader, Int)] = readLock { if (height == blockchain.height + 1) ngState.map(x => (x.bestLiquidBlock, x.bestLiquidBlock.bytes().length)) else blockchain.blockHeaderAndSize(height) } - override def portfolio(a: Address): Portfolio = { + override def portfolio(a: Address): Portfolio = readLock { val p = ngState.fold(Portfolio.empty)(_.bestLiquidDiff.portfolios.getOrElse(a, Portfolio.empty)) blockchain.portfolio(a).combine(p) } - private[this] def portfolioAt(a: Address, mb: ByteStr): Portfolio = { + private[this] def portfolioAt(a: Address, mb: ByteStr): Portfolio = readLock { val p = ngState.fold(Portfolio.empty)(_.diffFor(mb)._1.portfolios.getOrElse(a, Portfolio.empty)) blockchain.portfolio(a).combine(p) } - override def transactionInfo(id: AssetId): Option[(Int, Transaction)] = + override def transactionInfo(id: AssetId): Option[(Int, Transaction)] = readLock { ngState .fold(Diff.empty)(_.bestLiquidDiff) .transactions .get(id) .map(t => (t._1, t._2)) .orElse(blockchain.transactionInfo(id)) + } override def addressTransactions(address: Address, types: Set[Type], count: Int, fromId: Option[ByteStr]): Either[String, Seq[(Int, Transaction)]] = - addressTransactionsFromDiff(blockchain, ngState.map(_.bestLiquidDiff))(address, types, count, fromId) + readLock { + addressTransactionsFromDiff(blockchain, ngState.map(_.bestLiquidDiff))(address, types, count, fromId) + } - override def containsTransaction(tx: Transaction): Boolean = ngState.fold(blockchain.containsTransaction(tx)) { ng => - ng.bestLiquidDiff.transactions.contains(tx.id()) || blockchain.containsTransaction(tx) + override def containsTransaction(tx: Transaction): Boolean = readLock { + ngState.fold(blockchain.containsTransaction(tx)) { ng => + ng.bestLiquidDiff.transactions.contains(tx.id()) || blockchain.containsTransaction(tx) + } } - override def assetDescription(id: AssetId): Option[AssetDescription] = ngState.fold(blockchain.assetDescription(id)) { ng => - val diff = ng.bestLiquidDiff - CompositeBlockchain.composite(blockchain, diff).assetDescription(id) + override def assetDescription(id: AssetId): Option[AssetDescription] = readLock { + ngState.fold(blockchain.assetDescription(id)) { ng => + val diff = ng.bestLiquidDiff + CompositeBlockchain.composite(blockchain, diff).assetDescription(id) + } } - override def resolveAlias(alias: Alias): Either[ValidationError, Address] = ngState.fold(blockchain.resolveAlias(alias)) { ng => - CompositeBlockchain.composite(blockchain, ng.bestLiquidDiff).resolveAlias(alias) + override def resolveAlias(alias: Alias): Either[ValidationError, Address] = readLock { + ngState.fold(blockchain.resolveAlias(alias)) { ng => + CompositeBlockchain.composite(blockchain, ng.bestLiquidDiff).resolveAlias(alias) + } } - override def leaseDetails(leaseId: AssetId): Option[LeaseDetails] = ngState match { - case Some(ng) => - blockchain.leaseDetails(leaseId).map(ld => ld.copy(isActive = ng.bestLiquidDiff.leaseState.getOrElse(leaseId, ld.isActive))) orElse - ng.bestLiquidDiff.transactions.get(leaseId).collect { - case (h, lt: LeaseTransaction, _) => - LeaseDetails(lt.sender, lt.recipient, h, lt.amount, ng.bestLiquidDiff.leaseState(lt.id())) - } - case None => - blockchain.leaseDetails(leaseId) + override def leaseDetails(leaseId: AssetId): Option[LeaseDetails] = readLock { + ngState match { + case Some(ng) => + blockchain.leaseDetails(leaseId).map(ld => ld.copy(isActive = ng.bestLiquidDiff.leaseState.getOrElse(leaseId, ld.isActive))) orElse + ng.bestLiquidDiff.transactions.get(leaseId).collect { + case (h, lt: LeaseTransaction, _) => + LeaseDetails(lt.sender, lt.recipient, h, lt.amount, ng.bestLiquidDiff.leaseState(lt.id())) + } + case None => + blockchain.leaseDetails(leaseId) + } } - override def filledVolumeAndFee(orderId: AssetId): VolumeAndFee = + override def filledVolumeAndFee(orderId: AssetId): VolumeAndFee = readLock { ngState.fold(blockchain.filledVolumeAndFee(orderId))( _.bestLiquidDiff.orderFills.get(orderId).orEmpty.combine(blockchain.filledVolumeAndFee(orderId))) + } /** Retrieves Zbs balance snapshot in the [from, to] range (inclusive) */ - override def balanceSnapshots(address: Address, from: Int, to: BlockId): Seq[BalanceSnapshot] = { + override def balanceSnapshots(address: Address, from: Int, to: BlockId): Seq[BalanceSnapshot] = readLock { val blockchainBlock = blockchain.heightOf(to) if (blockchainBlock.nonEmpty || ngState.isEmpty) { blockchain.balanceSnapshots(address, from, to) @@ -449,14 +525,16 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A } } - override def accountScript(address: Address): Option[Script] = ngState.fold(blockchain.accountScript(address)) { ng => - ng.bestLiquidDiff.scripts.get(address) match { - case None => blockchain.accountScript(address) - case Some(scr) => scr + override def accountScript(address: Address): Option[Script] = readLock { + ngState.fold(blockchain.accountScript(address)) { ng => + ng.bestLiquidDiff.scripts.get(address) match { + case None => blockchain.accountScript(address) + case Some(scr) => scr + } } } - override def hasScript(address: Address): Boolean = + override def hasScript(address: Address): Boolean = readLock { ngState .flatMap( _.bestLiquidDiff.scripts @@ -464,33 +542,42 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A .map(_.nonEmpty) ) .getOrElse(blockchain.hasScript(address)) + } - override def assetScript(asset: AssetId): Option[Script] = ngState.fold(blockchain.assetScript(asset)) { ng => - ng.bestLiquidDiff.assetScripts.get(asset) match { - case None => blockchain.assetScript(asset) - case Some(scr) => scr + override def assetScript(asset: AssetId): Option[Script] = readLock { + ngState.fold(blockchain.assetScript(asset)) { ng => + ng.bestLiquidDiff.assetScripts.get(asset) match { + case None => blockchain.assetScript(asset) + case Some(scr) => scr + } } } - override def hasAssetScript(asset: AssetId): Boolean = ngState.fold(blockchain.hasAssetScript(asset)) { ng => - ng.bestLiquidDiff.assetScripts.get(asset) match { - case None => blockchain.hasAssetScript(asset) - case Some(x) => x.nonEmpty + override def hasAssetScript(asset: AssetId): Boolean = readLock { + ngState.fold(blockchain.hasAssetScript(asset)) { ng => + ng.bestLiquidDiff.assetScripts.get(asset) match { + case None => blockchain.hasAssetScript(asset) + case Some(x) => x.nonEmpty + } } } - override def accountData(acc: Address): AccountDataInfo = ngState.fold(blockchain.accountData(acc)) { ng => - val fromInner = blockchain.accountData(acc) - val fromDiff = ng.bestLiquidDiff.accountData.get(acc).orEmpty - fromInner.combine(fromDiff) + override def accountData(acc: Address): AccountDataInfo = readLock { + ngState.fold(blockchain.accountData(acc)) { ng => + val fromInner = blockchain.accountData(acc) + val fromDiff = ng.bestLiquidDiff.accountData.get(acc).orEmpty + fromInner.combine(fromDiff) + } } - override def accountData(acc: Address, key: String): Option[DataEntry[_]] = ngState.fold(blockchain.accountData(acc, key)) { ng => - val diffData = ng.bestLiquidDiff.accountData.get(acc).orEmpty - diffData.data.get(key).orElse(blockchain.accountData(acc, key)) + override def accountData(acc: Address, key: String): Option[DataEntry[_]] = readLock { + ngState.fold(blockchain.accountData(acc, key)) { ng => + val diffData = ng.bestLiquidDiff.accountData.get(acc).orEmpty + diffData.data.get(key).orElse(blockchain.accountData(acc, key)) + } } - private def changedBalances(pred: Portfolio => Boolean, f: Address => Long): Map[Address, Long] = + private def changedBalances(pred: Portfolio => Boolean, f: Address => Long): Map[Address, Long] = readLock { ngState .fold(Map.empty[Address, Long]) { ng => for { @@ -498,10 +585,11 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A if pred(p) } yield address -> f(address) } + } - override def assetDistribution(assetId: AssetId): AssetDistribution = { + override def assetDistribution(assetId: AssetId): AssetDistribution = readLock { val fromInner = blockchain.assetDistribution(assetId) - val fromNg = AssetDistribution(changedBalances(_.assets.getOrElse(assetId, 0L) != 0, portfolio(_).assets.getOrElse(assetId, 0L))) + val fromNg = AssetDistribution(changedBalances(_.assets.getOrElse(assetId, 0L) != 0, balance(_, Some(assetId)))) fromInner |+| fromNg } @@ -509,64 +597,78 @@ class BlockchainUpdaterImpl(blockchain: Blockchain, portfolioChanged: Observer[A override def assetDistributionAtHeight(assetId: AssetId, height: Int, count: Int, - fromAddress: Option[Address]): Either[ValidationError, AssetDistributionPage] = { + fromAddress: Option[Address]): Either[ValidationError, AssetDistributionPage] = readLock { blockchain.assetDistributionAtHeight(assetId, height, count, fromAddress) } - override def zbsDistribution(height: Int): Map[Address, Long] = ngState.fold(blockchain.zbsDistribution(height)) { ng => - val innerDistribution = blockchain.zbsDistribution(height) - if (height < this.height) innerDistribution - else { - innerDistribution ++ changedBalances(_.balance != 0, portfolio(_).balance) + override def zbsDistribution(height: Int): Either[ValidationError, Map[Address, Long]] = readLock { + ngState.fold(blockchain.zbsDistribution(height)) { ng => + val innerDistribution = blockchain.zbsDistribution(height) + if (height < this.height) innerDistribution + else { + innerDistribution.map(_ ++ changedBalances(_.balance != 0, balance(_))) + } } } - override def allActiveLeases: Set[LeaseTransaction] = ngState.fold(blockchain.allActiveLeases) { ng => - val (active, canceled) = ng.bestLiquidDiff.leaseState.partition(_._2) - val fromDiff = active.keys - .map { id => - ng.bestLiquidDiff.transactions(id)._2 - } - .collect { case lt: LeaseTransaction => lt } - .toSet - val fromInner = blockchain.allActiveLeases.filterNot(ltx => canceled.keySet.contains(ltx.id())) - fromDiff ++ fromInner + override def allActiveLeases: Set[LeaseTransaction] = readLock { + ngState.fold(blockchain.allActiveLeases) { ng => + val (active, canceled) = ng.bestLiquidDiff.leaseState.partition(_._2) + val fromDiff = active.keys + .map { id => + ng.bestLiquidDiff.transactions(id)._2 + } + .collect { case lt: LeaseTransaction => lt } + .toSet + val fromInner = blockchain.allActiveLeases.filterNot(ltx => canceled.keySet.contains(ltx.id())) + fromDiff ++ fromInner + } } /** Builds a new portfolio map by applying a partial function to all portfolios on which the function is defined. * * @note Portfolios passed to `pf` only contain Zbs and Leasing balances to improve performance */ - override def collectLposPortfolios[A](pf: PartialFunction[(Address, Portfolio), A]): Map[Address, A] = + override def collectLposPortfolios[A](pf: PartialFunction[(Address, Portfolio), A]): Map[Address, A] = readLock { ngState.fold(blockchain.collectLposPortfolios(pf)) { ng => val b = Map.newBuilder[Address, A] for ((a, p) <- ng.bestLiquidDiff.portfolios if p.lease != LeaseBalance.empty || p.balance != 0) { - pf.runWith(b += a -> _)(a -> portfolio(a).copy(assets = Map.empty)) + pf.runWith(b += a -> _)(a -> this.zbsPortfolio(a)) } blockchain.collectLposPortfolios(pf) ++ b.result() } + } - override def append(diff: Diff, carry: Long, block: Block): Unit = blockchain.append(diff, carry, block) + override def append(diff: Diff, carry: Long, block: Block): Unit = readLock { + blockchain.append(diff, carry, block) + } - override def rollbackTo(targetBlockId: AssetId): Either[String, Seq[Block]] = blockchain.rollbackTo(targetBlockId) + override def rollbackTo(targetBlockId: AssetId): Either[String, Seq[Block]] = readLock { + blockchain.rollbackTo(targetBlockId) + } - override def transactionHeight(id: AssetId): Option[Int] = + override def transactionHeight(id: AssetId): Option[Int] = readLock { ngState flatMap { ng => ng.bestLiquidDiff.transactions.get(id).map(_._1) } orElse blockchain.transactionHeight(id) + } - override def balance(address: Address, mayBeAssetId: Option[AssetId]): Long = ngState match { - case Some(ng) => - blockchain.balance(address, mayBeAssetId) + ng.bestLiquidDiff.portfolios.getOrElse(address, Portfolio.empty).balanceOf(mayBeAssetId) - case None => - blockchain.balance(address, mayBeAssetId) + override def balance(address: Address, mayBeAssetId: Option[AssetId]): Long = readLock { + ngState match { + case Some(ng) => + blockchain.balance(address, mayBeAssetId) + ng.bestLiquidDiff.portfolios.getOrElse(address, Portfolio.empty).balanceOf(mayBeAssetId) + case None => + blockchain.balance(address, mayBeAssetId) + } } - override def leaseBalance(address: Address): LeaseBalance = ngState match { - case Some(ng) => - cats.Monoid.combine(blockchain.leaseBalance(address), ng.bestLiquidDiff.portfolios.getOrElse(address, Portfolio.empty).lease) - case None => - blockchain.leaseBalance(address) + override def leaseBalance(address: Address): LeaseBalance = readLock { + ngState match { + case Some(ng) => + cats.Monoid.combine(blockchain.leaseBalance(address), ng.bestLiquidDiff.portfolios.getOrElse(address, Portfolio.empty).lease) + case None => + blockchain.leaseBalance(address) + } } } diff --git a/src/main/scala/com/zbsnetwork/state/NgState.scala b/src/main/scala/com/zbsnetwork/state/NgState.scala index 24529ed..54ea685 100644 --- a/src/main/scala/com/zbsnetwork/state/NgState.scala +++ b/src/main/scala/com/zbsnetwork/state/NgState.scala @@ -1,7 +1,6 @@ package com.zbsnetwork.state import java.util.concurrent.TimeUnit -import java.util.concurrent.locks.{Lock, ReentrantReadWriteLock} import cats.kernel.Monoid import com.google.common.cache.CacheBuilder @@ -11,14 +10,15 @@ import com.zbsnetwork.block.{Block, MicroBlock} import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.transaction.{DiscardedMicroBlocks, Transaction} +import scala.collection.mutable.{ListBuffer => MList, Map => MMap} + +/* This is not thread safe, used only from BlockchainUpdaterImpl */ class NgState(val base: Block, val baseBlockDiff: Diff, val baseBlockCarry: Long, val approvedFeatures: Set[Short]) extends ScorexLogging { private val MaxTotalDiffs = 3 - private val state = new SynchronizedAppendState[MicroBlock, BlockId, (Diff, Long, Long)](_.totalResBlockSig) - - private def microDiffs = state.mapping // microDiff, carryFee, timestamp - private def micros = state.stack // fresh head + private val microDiffs: MMap[BlockId, (Diff, Long, Long)] = MMap.empty // microDiff, carryFee, timestamp + private val micros: MList[MicroBlock] = MList.empty // fresh head private val totalBlockDiffCache = CacheBuilder .newBuilder() @@ -110,48 +110,9 @@ class NgState(val base: Block, val baseBlockDiff: Diff, val baseBlockCarry: Long } def append(m: MicroBlock, diff: Diff, microblockCarry: Long, timestamp: Long): Unit = { - state.append(m, (diff, microblockCarry, timestamp)) + microDiffs.put(m.totalResBlockSig, (diff, microblockCarry, timestamp)) + micros.prepend(m) } def carryFee: Long = baseBlockCarry + microDiffs.values.map(_._2).sum } - -/** - * Allow atomically appends to state - * Return internal stack and mapping state without dirty reads - */ -private class SynchronizedAppendState[T, K, V](toKey: T => K) { - private def inLock[R](l: Lock, f: => R) = { - try { - l.lock() - val res = f - res - } finally { - l.unlock() - } - } - private val lock = new ReentrantReadWriteLock - private def writeLock[B](f: => B): B = inLock(lock.writeLock(), f) - private def readLock[B](f: => B): B = inLock(lock.readLock(), f) - - @volatile private var internalStack = List.empty[T] - @volatile private var internalMap = Map.empty[K, V] - - /** - * Stack state - */ - def stack: List[T] = readLock(internalStack) - - /** - * Mapping state - */ - def mapping: Map[K, V] = readLock(internalMap) - - /** - * Atomically appends to state both stack and map - */ - def append(t: T, v: V): Unit = writeLock { - internalStack = t :: internalStack - internalMap = internalMap.updated(toKey(t), v) - } -} diff --git a/src/main/scala/com/zbsnetwork/state/Portfolio.scala b/src/main/scala/com/zbsnetwork/state/Portfolio.scala index bbf4a55..9218fd7 100644 --- a/src/main/scala/com/zbsnetwork/state/Portfolio.scala +++ b/src/main/scala/com/zbsnetwork/state/Portfolio.scala @@ -36,6 +36,7 @@ object Portfolio { } implicit class PortfolioExt(self: Portfolio) { + def spendableBalanceOf(assetId: Option[AssetId]): Long = assetId.fold(self.spendableBalance)(self.assets.getOrElse(_, 0L)) def pessimistic: Portfolio = Portfolio( balance = Math.min(self.balance, 0), @@ -52,7 +53,21 @@ object Portfolio { def minus(other: Portfolio): Portfolio = Portfolio(self.balance - other.balance, LeaseBalance.empty, Monoid.combine(self.assets, other.assets.mapValues(-_))) - def negate = Portfolio.empty minus self + def negate: Portfolio = Portfolio.empty minus self + + def assetIds: Set[Option[AssetId]] = { + val r: Set[Option[AssetId]] = self.assets.keySet.map(Some(_)) + r + None + } + + def changedAssetIds(that: Portfolio): Set[Option[AssetId]] = { + val a1 = assetIds + val a2 = that.assetIds + + val intersection = a1 & a2 + val sureChanged = (a1 | a2) -- intersection + intersection.filter(x => spendableBalanceOf(x) != that.spendableBalanceOf(x)) ++ sureChanged + } } } diff --git a/src/main/scala/com/zbsnetwork/state/diffs/CommonValidation.scala b/src/main/scala/com/zbsnetwork/state/diffs/CommonValidation.scala index ee28aff..bca6b92 100644 --- a/src/main/scala/com/zbsnetwork/state/diffs/CommonValidation.scala +++ b/src/main/scala/com/zbsnetwork/state/diffs/CommonValidation.scala @@ -1,6 +1,7 @@ package com.zbsnetwork.state.diffs import cats._ +import cats.implicits._ import com.zbsnetwork.account.Address import com.zbsnetwork.features.FeatureProvider._ import com.zbsnetwork.features.{BlockchainFeature, BlockchainFeatures} @@ -9,16 +10,15 @@ import com.zbsnetwork.settings.FunctionalitySettings import com.zbsnetwork.state._ import com.zbsnetwork.transaction.ValidationError._ import com.zbsnetwork.transaction.assets._ -import com.zbsnetwork.transaction.assets.exchange._ +import com.zbsnetwork.transaction.assets.exchange.{Order, _} import com.zbsnetwork.transaction.lease._ -import com.zbsnetwork.transaction.smart.script.ContractScript -import com.zbsnetwork.transaction.smart.script.Script -import com.zbsnetwork.transaction.smart.script.v1.ExprScript.ExprScriprImpl +import com.zbsnetwork.transaction.smart.script.v1.ExprScript +import com.zbsnetwork.transaction.smart.script.{ContractScript, Script} import com.zbsnetwork.transaction.smart.{ContractInvocationTransaction, SetScriptTransaction} import com.zbsnetwork.transaction.transfer._ import com.zbsnetwork.transaction.{smart, _} -import scala.util.{Left, Right} +import scala.util.{Left, Right, Try} object CommonValidation { @@ -28,20 +28,20 @@ object CommonValidation { val FeeConstants: Map[Byte, Long] = Map( GenesisTransaction.typeId -> 0, PaymentTransaction.typeId -> 1, - IssueTransaction.typeId -> 1000, - ReissueTransaction.typeId -> 1000, - BurnTransaction.typeId -> 1, - TransferTransaction.typeId -> 1, - MassTransferTransaction.typeId -> 1, - LeaseTransaction.typeId -> 1, - LeaseCancelTransaction.typeId -> 1, - ExchangeTransaction.typeId -> 3, - CreateAliasTransaction.typeId -> 1, - DataTransaction.typeId -> 1, - SetScriptTransaction.typeId -> 10, - SponsorFeeTransaction.typeId -> 1000, - SetAssetScriptTransaction.typeId -> (1000 - 4), - smart.ContractInvocationTransaction.typeId -> 5 + IssueTransaction.typeId -> 500000, + ReissueTransaction.typeId -> 200000, + BurnTransaction.typeId -> 5000, + TransferTransaction.typeId -> 50, + MassTransferTransaction.typeId -> 50, + LeaseTransaction.typeId -> 5000, + LeaseCancelTransaction.typeId -> 1000, + ExchangeTransaction.typeId -> 200, + CreateAliasTransaction.typeId -> 10000, + DataTransaction.typeId -> 30, + SetScriptTransaction.typeId -> 10000, + SponsorFeeTransaction.typeId -> 50000, + SetAssetScriptTransaction.typeId -> (10000 - 4), + smart.ContractInvocationTransaction.typeId -> 100 ) def disallowSendingGreaterThanBalance[T <: Transaction](blockchain: Blockchain, @@ -90,7 +90,9 @@ object CommonValidation { s"${blockchain.balance(ptx.sender, None)} is less than ${ptx.amount + ptx.fee}")) case ttx: TransferTransaction => checkTransfer(ttx.sender, ttx.assetId, ttx.amount, ttx.feeAssetId, ttx.fee) case mtx: MassTransferTransaction => checkTransfer(mtx.sender, mtx.assetId, mtx.transfers.map(_.amount).sum, None, mtx.fee) - case _ => Right(tx) + case citx: ContractInvocationTransaction => + checkTransfer(citx.sender, citx.payment.flatMap(_.assetId), citx.payment.map(_.amount).getOrElse(0), None, citx.fee) + case _ => Right(tx) } } else Right(tx) @@ -106,51 +108,67 @@ object CommonValidation { def disallowBeforeActivationTime[T <: Transaction](blockchain: Blockchain, height: Int, tx: T): Either[ValidationError, T] = { - def activationBarrier(b: BlockchainFeature, msg: Option[String] = None) = + def activationBarrier(b: BlockchainFeature, msg: Option[String] = None): Either[ActivationError, T] = Either.cond( blockchain.isFeatureActivated(b, height), tx, ValidationError.ActivationError(msg.getOrElse(tx.getClass.getSimpleName) + " has not been activated yet") ) - def scriptActivation(sc: Script) = { + def scriptActivation(sc: Script): Either[ActivationError, T] = { + val ab = activationBarrier(BlockchainFeatures.Ride4DApps, Some("Ride4DApps has not been activated yet")) - def scriptVersionActivation(sc: Script) = sc.stdLibVersion match { + + def scriptVersionActivation(sc: Script): Either[ActivationError, T] = sc.stdLibVersion match { case V1 | V2 if sc.containsBlockV2.value => ab case V1 | V2 => Right(tx) case V3 => ab } - def scriptTypeActivation(sc: Script) = sc match { - case e: ExprScriprImpl => Right(tx) + + def scriptTypeActivation(sc: Script): Either[ActivationError, T] = sc match { + case e: ExprScript => Right(tx) case c: ContractScript.ContractScriptImpl => ab } + for { _ <- scriptVersionActivation(sc) _ <- scriptTypeActivation(sc) } yield tx } + tx match { - case _: BurnTransactionV1 => Right(tx) - case _: PaymentTransaction => Right(tx) - case _: GenesisTransaction => Right(tx) - case _: TransferTransactionV1 => Right(tx) - case _: IssueTransactionV1 => Right(tx) - case _: ReissueTransactionV1 => Right(tx) - case _: ExchangeTransactionV1 => Right(tx) - case _: ExchangeTransactionV2 => activationBarrier(BlockchainFeatures.SmartAccountTrading) + case _: BurnTransactionV1 => Right(tx) + case _: PaymentTransaction => Right(tx) + case _: GenesisTransaction => Right(tx) + case _: TransferTransactionV1 => Right(tx) + case _: IssueTransactionV1 => Right(tx) + case _: ReissueTransactionV1 => Right(tx) + case _: ExchangeTransactionV1 => Right(tx) + + case exv2: ExchangeTransactionV2 => + activationBarrier(BlockchainFeatures.SmartAccountTrading).flatMap { tx => + (exv2.buyOrder, exv2.sellOrder) match { + case (_: OrderV3, _: Order) | (_: Order, _: OrderV3) => activationBarrier(BlockchainFeatures.OrderV3, Some("Order Version 3")) + case _ => Right(tx) + } + } + case _: LeaseTransactionV1 => Right(tx) case _: LeaseCancelTransactionV1 => Right(tx) case _: CreateAliasTransactionV1 => Right(tx) case _: MassTransferTransaction => activationBarrier(BlockchainFeatures.MassTransfer) case _: DataTransaction => activationBarrier(BlockchainFeatures.DataTransaction) + case sst: SetScriptTransaction => sst.script match { case None => Right(tx) case Some(sc) => scriptActivation(sc) } + case _: TransferTransactionV2 => activationBarrier(BlockchainFeatures.SmartAccounts) case it: IssueTransactionV2 => activationBarrier(if (it.script.isEmpty) BlockchainFeatures.SmartAccounts else BlockchainFeatures.SmartAssets) + case it: SetAssetScriptTransaction => it.script match { case None => Left(GenericError("Cannot set empty script")) @@ -286,4 +304,12 @@ object CommonValidation { } def cond[A](c: Boolean)(a: A, b: A): A = if (c) a else b + + def validateOverflow(dataList: Traversable[Long], errMsg: String): Either[ValidationError, Unit] = { + Try(dataList.foldLeft(0L)(Math.addExact)) + .fold( + _ => GenericError(errMsg).asLeft[Unit], + _ => ().asRight[ValidationError] + ) + } } diff --git a/src/main/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiff.scala b/src/main/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiff.scala index 37fabe8..c2ec5c2 100644 --- a/src/main/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiff.scala +++ b/src/main/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiff.scala @@ -7,7 +7,7 @@ import com.zbsnetwork.account.{Address, AddressScheme} import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.lang.contract.Contract -import com.zbsnetwork.lang.v1.FunctionHeader +import com.zbsnetwork.lang.v1.{ContractLimits, FunctionHeader} import com.zbsnetwork.lang.v1.compiler.Terms._ import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext} @@ -16,6 +16,7 @@ import com.zbsnetwork.lang.v1.traits.domain.Tx.ContractTransfer import com.zbsnetwork.lang.v1.traits.domain.{DataItem, Recipient} import com.zbsnetwork.lang.{Global, StdLibVersion} import com.zbsnetwork.state._ +import com.zbsnetwork.state.diffs.CommonValidation._ import com.zbsnetwork.state.reader.CompositeBlockchain import com.zbsnetwork.transaction.ValidationError import com.zbsnetwork.transaction.ValidationError._ @@ -62,13 +63,35 @@ object ContractInvocationTransactionDiff { case ContractResult(ds, ps) => import cats.implicits._ - val dataAndPaymentDiff = payableAndDataPart(height, tx, ds) val pmts: List[Map[Address, Map[Option[ByteStr], Long]]] = ps.map { case (Recipient.Address(addrBytes), amt, maybeAsset) => Map(Address.fromBytes(addrBytes.arr).explicitGet() -> Map(maybeAsset -> amt)) } for { - _ <- Either.cond(pmts.flatMap(_.values).flatMap(_.values).forall(_ >= 0), (), ValidationError.NegativeAmount(-42, "")) + feeInfo <- (tx.assetFee._1 match { + case None => Right((tx.fee, Map(tx.sender.toAddress -> Portfolio(-tx.fee, LeaseBalance.empty, Map.empty)))) + case Some(assetId) => + for { + assetInfo <- blockchain + .assetDescription(assetId) + .toRight(GenericError(s"Asset $assetId does not exist, cannot be used to pay fees")) + zbsFee <- Either.cond( + assetInfo.sponsorship > 0, + Sponsorship.toZbs(tx.fee, assetInfo.sponsorship), + GenericError(s"Asset $assetId is not sponsored, cannot be used to pay fees") + ) + } yield { + (zbsFee, + Map( + tx.sender.toAddress -> Portfolio(0, LeaseBalance.empty, Map(assetId -> -tx.fee)), + assetInfo.issuer.toAddress -> Portfolio(-zbsFee, LeaseBalance.empty, Map(assetId -> tx.fee)) + )) + } + }) + zbsFee = feeInfo._1 + dataAndPaymentDiff <- payableAndDataPart(height, tx, ds, feeInfo._2) + _ <- Either.cond(pmts.flatMap(_.values).flatMap(_.values).forall(_ >= 0), (), ValidationError.NegativeAmount(-42, "")) + _ <- validateOverflow(pmts.flatMap(_.values).flatMap(_.values), "Attempt to transfer unavailable funds in contract payment") _ <- Either.cond( pmts .flatMap(_.values) @@ -78,8 +101,17 @@ object ContractInvocationTransactionDiff { (), GenericError(s"Unissued assets are not allowed") ) - _ <- Either.cond(true, (), ValidationError.NegativeAmount(-42, "")) // - sum doesn't overflow - _ <- Either.cond(true, (), ValidationError.NegativeAmount(-42, "")) // - whatever else tranfser/massTransfer ensures + _ <- { + val totalScriptsInvoked = tx.checkedAssets().count(blockchain.hasAssetScript) + + ps.count(_._3.fold(false)(blockchain.hasAssetScript)) + val minZbs = totalScriptsInvoked * ScriptExtraFee + FeeConstants(ContractInvocationTransaction.typeId) * FeeUnit + Either.cond( + minZbs <= zbsFee, + (), + GenericError(s"Fee in ${tx.assetFee._1 + .fold("ZBS")(_.toString)} for ${tx.builder.classTag} with $totalScriptsInvoked total scripts invoked does not exceed minimal value of $minZbs ZBS: ${tx.assetFee._2}") + ) + } _ <- foldContractTransfers(blockchain, tx)(ps, dataAndPaymentDiff) } yield { val paymentReceiversMap: Map[Address, Portfolio] = Monoid @@ -97,13 +129,15 @@ object ContractInvocationTransactionDiff { } - private def payableAndDataPart(height: Int, tx: ContractInvocationTransaction, ds: List[DataItem[_]]) = { + private def payableAndDataPart(height: Int, tx: ContractInvocationTransaction, ds: List[DataItem[_]], feePart: Map[Address, Portfolio]) = { val r: Seq[DataEntry[_]] = ds.map { case DataItem.Bool(k, b) => BooleanDataEntry(k, b) case DataItem.Str(k, b) => StringDataEntry(k, b) case DataItem.Lng(k, b) => IntegerDataEntry(k, b) case DataItem.Bin(k, b) => BinaryDataEntry(k, b) } + val totalDataBytes = r.map(_.toBytes.size).sum + val payablePart: Map[Address, Portfolio] = tx.payment match { case None => Map.empty case Some(ContractInvocationTransaction.Payment(amt, assetOpt)) => @@ -117,45 +151,48 @@ object ContractInvocationTransactionDiff { .combine(Map(tx.contractAddress -> Portfolio(amt, LeaseBalance.empty, Map.empty))) } } - val feePart = Map(tx.sender.toAddress -> Portfolio(-tx.fee, LeaseBalance.empty, Map.empty)) - Diff( - height = height, - tx = tx, - portfolios = feePart combine payablePart, - accountData = Map(tx.contractAddress -> AccountDataInfo(r.map(d => d.key -> d).toMap)) - ) - + if (totalDataBytes <= ContractLimits.MaxWriteSetSizeInBytes) + Right( + Diff( + height = height, + tx = tx, + portfolios = feePart combine payablePart, + accountData = Map(tx.contractAddress -> AccountDataInfo(r.map(d => d.key -> d).toMap)) + )) + else Left(GenericError(s"WriteSet size can't exceed ${ContractLimits.MaxWriteSetSizeInBytes} bytes, actual: $totalDataBytes bytes")) } + private def foldContractTransfers(blockchain: Blockchain, tx: ContractInvocationTransaction)(ps: List[(Recipient.Address, Long, Option[ByteStr])], dataDiff: Diff): Either[ValidationError, Diff] = { - - ps.foldLeft(Either.right[ValidationError, Diff](dataDiff)) { (diffEi, payment) => - val (addressRepr, amount, asset) = payment - val address = Address.fromBytes(addressRepr.bytes.arr).explicitGet() - asset match { - case None => - diffEi combine Right( - Diff.stateOps( - portfolios = Map( - address -> Portfolio(amount, LeaseBalance.empty, Map.empty), - tx.contractAddress -> Portfolio(-amount, LeaseBalance.empty, Map.empty) - ))) - case Some(assetId) => - diffEi combine { - val nextDiff = Diff.stateOps( - portfolios = Map( - address -> Portfolio(0, LeaseBalance.empty, Map(assetId -> amount)), - tx.contractAddress -> Portfolio(0, LeaseBalance.empty, Map(assetId -> -amount)) - )) - blockchain.assetScript(assetId) match { - case None => - Right(nextDiff) - case Some(script) => - diffEi flatMap (d => validateContractTransferWithSmartAssetScript(blockchain, tx)(d, addressRepr, amount, asset, nextDiff, script)) + if (ps.length <= ContractLimits.MaxPaymentAmount) + ps.foldLeft(Either.right[ValidationError, Diff](dataDiff)) { (diffEi, payment) => + val (addressRepr, amount, asset) = payment + val address = Address.fromBytes(addressRepr.bytes.arr).explicitGet() + asset match { + case None => + diffEi combine Right( + Diff.stateOps( + portfolios = Map( + address -> Portfolio(amount, LeaseBalance.empty, Map.empty), + tx.contractAddress -> Portfolio(-amount, LeaseBalance.empty, Map.empty) + ))) + case Some(assetId) => + diffEi combine { + val nextDiff = Diff.stateOps( + portfolios = Map( + address -> Portfolio(0, LeaseBalance.empty, Map(assetId -> amount)), + tx.contractAddress -> Portfolio(0, LeaseBalance.empty, Map(assetId -> -amount)) + )) + blockchain.assetScript(assetId) match { + case None => + Right(nextDiff) + case Some(script) => + diffEi flatMap (d => validateContractTransferWithSmartAssetScript(blockchain, tx)(d, addressRepr, amount, asset, nextDiff, script)) + } } - } - } - } + } + } else + Left(GenericError(s"Too many ContractTransfers: max: ${ContractLimits.MaxPaymentAmount}, actual: ${ps.length}")) } private def validateContractTransferWithSmartAssetScript(blockchain: Blockchain, tx: ContractInvocationTransaction)( diff --git a/src/main/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiff.scala b/src/main/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiff.scala index 5584ed1..8174447 100644 --- a/src/main/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiff.scala +++ b/src/main/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiff.scala @@ -2,27 +2,32 @@ package com.zbsnetwork.state.diffs import cats._ import cats.implicits._ +import com.zbsnetwork.account.Address import com.zbsnetwork.features.BlockchainFeatures import com.zbsnetwork.state._ -import com.zbsnetwork.transaction.ValidationError +import com.zbsnetwork.transaction.{AssetId, ValidationError} import com.zbsnetwork.transaction.ValidationError.{GenericError, OrderValidationError} -import com.zbsnetwork.transaction.assets.exchange.ExchangeTransaction +import com.zbsnetwork.transaction.assets.exchange.{ExchangeTransaction, Order, OrderV3} import scala.util.Right object ExchangeTransactionDiff { def apply(blockchain: Blockchain, height: Int)(tx: ExchangeTransaction): Either[ValidationError, Diff] = { + val matcher = tx.buyOrder.matcherPublicKey.toAddress val buyer = tx.buyOrder.senderPublicKey.toAddress val seller = tx.sellOrder.senderPublicKey.toAddress + val assetIds = Set(tx.buyOrder.assetPair.amountAsset, tx.buyOrder.assetPair.priceAsset, tx.sellOrder.assetPair.amountAsset, tx.sellOrder.assetPair.priceAsset).flatten + val assets = assetIds.map(blockchain.assetDescription) val smartTradesEnabled = blockchain.activatedFeatures.contains(BlockchainFeatures.SmartAccountTrading.id) val smartAssetsEnabled = blockchain.activatedFeatures.contains(BlockchainFeatures.SmartAssets.id) + for { _ <- Either.cond(assets.forall(_.isDefined), (), GenericError("Assets should be issued before they can be traded")) _ <- Either.cond( @@ -47,37 +52,32 @@ object ExchangeTransactionDiff { sellAmountAssetChange <- t.sellOrder.getSpendAmount(t.amount, t.price).liftValidationError(tx).map(-_) } yield { - def zbsPortfolio(amt: Long) = Portfolio(amt, LeaseBalance.empty, Map.empty) - - val feeDiff = Monoid.combineAll( - Seq( - Map(matcher -> zbsPortfolio(t.buyMatcherFee + t.sellMatcherFee - t.fee)), - Map(buyer -> zbsPortfolio(-t.buyMatcherFee)), - Map(seller -> zbsPortfolio(-t.sellMatcherFee)) - )) - - val priceDiff = t.buyOrder.assetPair.priceAsset match { - case Some(assetId) => - Monoid.combine( - Map(buyer -> Portfolio(0, LeaseBalance.empty, Map(assetId -> buyPriceAssetChange))), - Map(seller -> Portfolio(0, LeaseBalance.empty, Map(assetId -> sellPriceAssetChange))) - ) - case None => - Monoid.combine(Map(buyer -> Portfolio(buyPriceAssetChange, LeaseBalance.empty, Map.empty)), - Map(seller -> Portfolio(sellPriceAssetChange, LeaseBalance.empty, Map.empty))) + def getAssetDiff(asset: Option[AssetId], buyAssetChange: Long, sellAssetChange: Long): Map[Address, Portfolio] = { + Monoid.combine( + Map(buyer → getAssetPortfolio(asset, buyAssetChange)), + Map(seller → getAssetPortfolio(asset, sellAssetChange)), + ) } - val amountDiff = t.buyOrder.assetPair.amountAsset match { - case Some(assetId) => - Monoid.combine( - Map(buyer -> Portfolio(0, LeaseBalance.empty, Map(assetId -> buyAmountAssetChange))), - Map(seller -> Portfolio(0, LeaseBalance.empty, Map(assetId -> sellAmountAssetChange))) + val matcherPortfolio = + Monoid.combineAll( + Seq( + getOrderFeePortfolio(t.buyOrder, t.buyMatcherFee), + getOrderFeePortfolio(t.sellOrder, t.sellMatcherFee), + zbsPortfolio(-t.fee), ) - case None => - Monoid.combine(Map(buyer -> Portfolio(buyAmountAssetChange, LeaseBalance.empty, Map.empty)), - Map(seller -> Portfolio(sellAmountAssetChange, LeaseBalance.empty, Map.empty))) - } + ) + val feeDiff = Monoid.combineAll( + Seq( + Map(matcher -> matcherPortfolio), + Map(buyer -> getOrderFeePortfolio(t.buyOrder, -t.buyMatcherFee)), + Map(seller -> getOrderFeePortfolio(t.sellOrder, -t.sellMatcherFee)) + ) + ) + + val priceDiff = getAssetDiff(t.buyOrder.assetPair.priceAsset, buyPriceAssetChange, sellPriceAssetChange) + val amountDiff = getAssetDiff(t.buyOrder.assetPair.amountAsset, buyAmountAssetChange, sellAmountAssetChange) val portfolios = Monoid.combineAll(Seq(feeDiff, priceDiff, amountDiff)) Diff( @@ -93,26 +93,40 @@ object ExchangeTransactionDiff { } private def enoughVolume(exTrans: ExchangeTransaction, blockchain: Blockchain): Either[ValidationError, ExchangeTransaction] = { + val filledBuy = blockchain.filledVolumeAndFee(exTrans.buyOrder.id()) val filledSell = blockchain.filledVolumeAndFee(exTrans.sellOrder.id()) - val buyTotal = filledBuy.volume + exTrans.amount - val sellTotal = filledSell.volume + exTrans.amount + val buyTotal = filledBuy.volume + exTrans.amount + val sellTotal = filledSell.volume + exTrans.amount + lazy val buyAmountValid = exTrans.buyOrder.amount >= buyTotal lazy val sellAmountValid = exTrans.sellOrder.amount >= sellTotal - def isFeeValid(feeTotal: Long, amountTotal: Long, maxfee: Long, maxAmount: Long): Boolean = - feeTotal <= BigInt(maxfee) * BigInt(amountTotal) / BigInt(maxAmount) + def isFeeValid(feeTotal: Long, amountTotal: Long, maxfee: Long, maxAmount: Long, order: Order): Boolean = { + feeTotal <= (order match { + case _: OrderV3 => BigInt(maxfee) + case _ => BigInt(maxfee) * BigInt(amountTotal) / BigInt(maxAmount) + }) + } - lazy val buyFeeValid = isFeeValid(feeTotal = filledBuy.fee + exTrans.buyMatcherFee, - amountTotal = buyTotal, - maxfee = exTrans.buyOrder.matcherFee, - maxAmount = exTrans.buyOrder.amount) + lazy val buyFeeValid = + isFeeValid( + feeTotal = filledBuy.fee + exTrans.buyMatcherFee, + amountTotal = buyTotal, + maxfee = exTrans.buyOrder.matcherFee, + maxAmount = exTrans.buyOrder.amount, + order = exTrans.buyOrder + ) - lazy val sellFeeValid = isFeeValid(feeTotal = filledSell.fee + exTrans.sellMatcherFee, - amountTotal = sellTotal, - maxfee = exTrans.sellOrder.matcherFee, - maxAmount = exTrans.sellOrder.amount) + lazy val sellFeeValid = + isFeeValid( + feeTotal = filledSell.fee + exTrans.sellMatcherFee, + amountTotal = sellTotal, + maxfee = exTrans.sellOrder.matcherFee, + maxAmount = exTrans.sellOrder.amount, + order = exTrans.sellOrder + ) if (!buyAmountValid) Left(OrderValidationError(exTrans.buyOrder, s"Too much buy. Already filled volume for the order: ${filledBuy.volume}")) else if (!sellAmountValid) @@ -121,4 +135,13 @@ object ExchangeTransactionDiff { else if (!sellFeeValid) Left(OrderValidationError(exTrans.sellOrder, s"Insufficient sell fee")) else Right(exTrans) } + + def zbsPortfolio(amt: Long) = Portfolio(amt, LeaseBalance.empty, Map.empty) + + def getAssetPortfolio(asset: Option[AssetId], amt: Long): Portfolio = { + asset.fold(zbsPortfolio(amt))(assetId => Portfolio(0, LeaseBalance.empty, Map(assetId -> amt))) + } + + /*** Calculates fee portfolio from the order (taking into account that in OrderV3 fee can be paid in asset != Zbs) */ + def getOrderFeePortfolio(order: Order, fee: Long): Portfolio = getAssetPortfolio(order.matcherFeeAssetId, fee) } diff --git a/src/main/scala/com/zbsnetwork/state/diffs/TransferTransactionDiff.scala b/src/main/scala/com/zbsnetwork/state/diffs/TransferTransactionDiff.scala index 0f01e78..725f576 100644 --- a/src/main/scala/com/zbsnetwork/state/diffs/TransferTransactionDiff.scala +++ b/src/main/scala/com/zbsnetwork/state/diffs/TransferTransactionDiff.scala @@ -4,11 +4,12 @@ import cats.implicits._ import com.zbsnetwork.settings.FunctionalitySettings import com.zbsnetwork.state._ import com.zbsnetwork.account.Address +import com.zbsnetwork.features.BlockchainFeatures import com.zbsnetwork.transaction.ValidationError import com.zbsnetwork.transaction.ValidationError.GenericError import com.zbsnetwork.transaction.transfer._ -import scala.util.Right +import scala.util.{Right, Try} object TransferTransactionDiff { def apply(blockchain: Blockchain, s: FunctionalitySettings, blockTime: Long, height: Int)( @@ -20,6 +21,9 @@ object TransferTransactionDiff { _ <- Either.cond((tx.feeAssetId >>= blockchain.assetDescription >>= (_.script)).isEmpty, (), GenericError("Smart assets can't participate in TransferTransactions as a fee")) + + _ <- validateOverflow(blockchain, tx) + portfolios = (tx.assetId match { case None => Map(sender -> Portfolio(-tx.amount, LeaseBalance.empty, Map.empty)).combine( @@ -60,4 +64,16 @@ object TransferTransactionDiff { Right(Diff(height, tx, portfolios)) } } + + private def validateOverflow(blockchain: Blockchain, tx: TransferTransaction) = { + if (blockchain.activatedFeatures.contains(BlockchainFeatures.Ride4DApps.id)) { + Right(()) // lets transaction validates itself + } else { + Try(Math.addExact(tx.fee, tx.amount)) + .fold( + _ => ValidationError.OverflowError.asLeft[Unit], + _ => ().asRight[ValidationError] + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/state/package.scala b/src/main/scala/com/zbsnetwork/state/package.scala index 991927d..eecda68 100644 --- a/src/main/scala/com/zbsnetwork/state/package.scala +++ b/src/main/scala/com/zbsnetwork/state/package.scala @@ -133,6 +133,12 @@ package object state { blockchain .heightOf(id) .getOrElse(throw new IllegalStateException(s"Can't find a block: $id")) + + def zbsPortfolio(address: Address): Portfolio = Portfolio( + blockchain.balance(address), + blockchain.leaseBalance(address), + Map.empty + ) } object AssetDistribution extends TaggedType[Map[Address, Long]] diff --git a/src/main/scala/com/zbsnetwork/state/reader/CompositeBlockchain.scala b/src/main/scala/com/zbsnetwork/state/reader/CompositeBlockchain.scala index d55735e..e949dbb 100644 --- a/src/main/scala/com/zbsnetwork/state/reader/CompositeBlockchain.scala +++ b/src/main/scala/com/zbsnetwork/state/reader/CompositeBlockchain.scala @@ -114,7 +114,7 @@ class CompositeBlockchain(inner: Blockchain, maybeDiff: => Option[Diff], carry: override def collectLposPortfolios[A](pf: PartialFunction[(Address, Portfolio), A]): Map[Address, A] = { val b = Map.newBuilder[Address, A] for ((a, p) <- diff.portfolios if p.lease != LeaseBalance.empty || p.balance != 0) { - pf.runWith(b += a -> _)(a -> portfolio(a).copy(assets = Map.empty)) + pf.runWith(b += a -> _)(a -> this.zbsPortfolio(a)) } inner.collectLposPortfolios(pf) ++ b.result() @@ -169,7 +169,7 @@ class CompositeBlockchain(inner: Blockchain, maybeDiff: => Option[Diff], carry: override def assetDistribution(assetId: ByteStr): AssetDistribution = { val fromInner = inner.assetDistribution(assetId) - val fromDiff = AssetDistribution(changedBalances(_.assets.getOrElse(assetId, 0L) != 0, portfolio(_).assets.getOrElse(assetId, 0L))) + val fromDiff = AssetDistribution(changedBalances(_.assets.getOrElse(assetId, 0L) != 0, balance(_, Some(assetId)))) fromInner |+| fromDiff } @@ -181,11 +181,11 @@ class CompositeBlockchain(inner: Blockchain, maybeDiff: => Option[Diff], carry: inner.assetDistributionAtHeight(assetId, height, count, fromAddress) } - override def zbsDistribution(height: Int): Map[Address, Long] = { + override def zbsDistribution(height: Int): Either[ValidationError, Map[Address, Long]] = { val innerDistribution = inner.zbsDistribution(height) if (height < this.height) innerDistribution else { - innerDistribution ++ changedBalances(_.balance != 0, portfolio(_).balance) + innerDistribution.map(_ ++ changedBalances(_.balance != 0, balance(_))) } } diff --git a/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV1.scala index f5072eb..529cbbc 100644 --- a/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV1.scala +++ b/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV1.scala @@ -1,13 +1,15 @@ package com.zbsnetwork.transaction +import cats.implicits._ import com.google.common.primitives.Bytes -import com.zbsnetwork.crypto -import monix.eval.Coeval import com.zbsnetwork.account._ import com.zbsnetwork.common.state.ByteStr -import com.zbsnetwork.crypto._ +import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.crypto +import com.zbsnetwork.transaction.description._ +import monix.eval.Coeval -import scala.util.{Failure, Success, Try} +import scala.util.Try case class CreateAliasTransactionV1 private (sender: PublicKeyAccount, alias: Alias, fee: Long, timestamp: Long, signature: ByteStr) extends CreateAliasTransaction @@ -26,15 +28,11 @@ object CreateAliasTransactionV1 extends TransactionParserFor[CreateAliasTransact override val typeId: Byte = CreateAliasTransaction.typeId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - for { - (sender, alias, fee, timestamp, end) <- CreateAliasTransaction.parseBase(0, bytes) - signature = ByteStr(bytes.slice(end, end + SignatureLength)) - tx <- CreateAliasTransactionV1 - .create(sender, alias, fee, timestamp, signature) - .fold(left => Failure(new Exception(left.toString)), right => Success(right)) - } yield tx - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + Either + .cond(tx.fee > 0, tx, ValidationError.InsufficientFee) + .foldToTry + } } def create(sender: PublicKeyAccount, alias: Alias, fee: Long, timestamp: Long, signature: ByteStr): Either[ValidationError, TransactionT] = { @@ -54,4 +52,14 @@ object CreateAliasTransactionV1 extends TransactionParserFor[CreateAliasTransact def selfSigned(sender: PrivateKeyAccount, alias: Alias, fee: Long, timestamp: Long): Either[ValidationError, TransactionT] = { signed(sender, alias, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[CreateAliasTransactionV1] = { + ( + PublicKeyAccountBytes(tailIndex(1), "Sender's public key"), + AliasBytes(tailIndex(2), "Alias object"), + LongBytes(tailIndex(3), "Fee"), + LongBytes(tailIndex(4), "Timestamp"), + SignatureBytes(tailIndex(5), "Signature") + ) mapN CreateAliasTransactionV1.apply + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV2.scala index d4264c4..14e7481 100644 --- a/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV2.scala +++ b/src/main/scala/com/zbsnetwork/transaction/CreateAliasTransactionV2.scala @@ -2,12 +2,14 @@ package com.zbsnetwork.transaction import cats.implicits._ import com.google.common.primitives.Bytes -import com.zbsnetwork.crypto -import monix.eval.Coeval import com.zbsnetwork.account.{Alias, PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.crypto +import com.zbsnetwork.transaction.description._ +import monix.eval.Coeval -import scala.util.{Failure, Success, Try} +import scala.util.Try final case class CreateAliasTransactionV2 private (sender: PublicKeyAccount, alias: Alias, fee: Long, timestamp: Long, proofs: Proofs) extends CreateAliasTransaction { @@ -27,15 +29,11 @@ object CreateAliasTransactionV2 extends TransactionParserFor[CreateAliasTransact override def supportedVersions: Set[Byte] = Set(2) override protected def parseTail(bytes: Array[Byte]): Try[CreateAliasTransactionV2] = { - Try { - for { - (sender, alias, fee, timestamp, end) <- CreateAliasTransaction.parseBase(0, bytes) - result <- (for { - proofs <- Proofs.fromBytes(bytes.drop(end)) - tx <- CreateAliasTransactionV2.create(sender, alias, fee, timestamp, proofs) - } yield tx).fold(left => Failure(new Exception(left.toString)), right => Success(right)) - } yield result - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + Either + .cond(tx.fee > 0, tx, ValidationError.InsufficientFee) + .foldToTry + } } def create(sender: PublicKeyAccount, @@ -64,4 +62,14 @@ object CreateAliasTransactionV2 extends TransactionParserFor[CreateAliasTransact def selfSigned(sender: PrivateKeyAccount, alias: Alias, fee: Long, timestamp: Long): Either[ValidationError, CreateAliasTransactionV2] = { signed(sender, alias, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[CreateAliasTransactionV2] = { + ( + PublicKeyAccountBytes(tailIndex(1), "Sender's public key"), + AliasBytes(tailIndex(2), "Alias object"), + LongBytes(tailIndex(3), "Fee"), + LongBytes(tailIndex(4), "Timestamp"), + ProofsBytes(tailIndex(5)) + ) mapN CreateAliasTransactionV2.apply + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/DataTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/DataTransaction.scala index 0e83073..ae19e71 100644 --- a/src/main/scala/com/zbsnetwork/transaction/DataTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/DataTransaction.scala @@ -1,16 +1,17 @@ package com.zbsnetwork.transaction +import cats.implicits._ import com.google.common.primitives.{Bytes, Longs, Shorts} import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto -import com.zbsnetwork.crypto._ import com.zbsnetwork.state._ +import com.zbsnetwork.transaction.description._ import monix.eval.Coeval import play.api.libs.json._ -import scala.util.{Failure, Success, Try} +import scala.util.Try case class DataTransaction private (sender: PublicKeyAccount, data: List[DataEntry[_]], fee: Long, timestamp: Long, proofs: Proofs) extends ProvenTransaction @@ -54,25 +55,21 @@ object DataTransaction extends TransactionParserFor[DataTransaction] with Transa val MaxEntryCount = 100 override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val p0 = KeyLength - val sender = PublicKeyAccount(bytes.slice(0, p0)) - - val entryCount = Shorts.fromByteArray(bytes.drop(p0)) - val (entries, p1) = - if (entryCount > 0) { - val parsed = List.iterate(DataEntry.parse(bytes, p0 + 2), entryCount) { case (e, p) => DataEntry.parse(bytes, p) } - (parsed.map(_._1), parsed.last._2) - } else (List.empty, p0 + 2) - - val timestamp = Longs.fromByteArray(bytes.drop(p1)) - val feeAmount = Longs.fromByteArray(bytes.drop(p1 + 8)) - val txEi = for { - proofs <- Proofs.fromBytes(bytes.drop(p1 + 16)) - tx <- create(sender, entries, feeAmount, timestamp, proofs) - } yield tx - txEi.fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + ( + if (tx.data.lengthCompare(MaxEntryCount) > 0 || tx.data.exists(!_.valid)) { + Left(ValidationError.TooBigArray) + } else if (tx.data.exists(_.key.isEmpty)) { + Left(ValidationError.GenericError("Empty key found")) + } else if (tx.data.map(_.key).distinct.lengthCompare(tx.data.size) < 0) { + Left(ValidationError.GenericError("Duplicate keys found")) + } else if (tx.fee <= 0) { + Left(ValidationError.InsufficientFee) + } else { + Either.cond(tx.bytes().length <= MaxBytes, tx, ValidationError.TooBigArray) + } + ).foldToTry + } } def create(sender: PublicKeyAccount, @@ -107,4 +104,23 @@ object DataTransaction extends TransactionParserFor[DataTransaction] with Transa def selfSigned(sender: PrivateKeyAccount, data: List[DataEntry[_]], feeAmount: Long, timestamp: Long): Either[ValidationError, TransactionT] = { signed(sender, data, feeAmount, timestamp, sender) } + + val byteTailDescription: ByteEntity[DataTransaction] = { + ( + PublicKeyAccountBytes(tailIndex(1), "Sender's public key"), + ListDataEntryBytes(tailIndex(2)), + LongBytes(tailIndex(3), "Timestamp"), + LongBytes(tailIndex(4), "Fee"), + ProofsBytes(tailIndex(5)) + ) mapN { + case (senderPublicKey, data, timestamp, fee, proofs) => + DataTransaction( + sender = senderPublicKey, + data = data, + fee = fee, + timestamp = timestamp, + proofs = proofs + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/FastHashId.scala b/src/main/scala/com/zbsnetwork/transaction/FastHashId.scala index ee9c91f..6d12ce1 100644 --- a/src/main/scala/com/zbsnetwork/transaction/FastHashId.scala +++ b/src/main/scala/com/zbsnetwork/transaction/FastHashId.scala @@ -5,6 +5,11 @@ import com.zbsnetwork.crypto import monix.eval.Coeval trait FastHashId extends ProvenTransaction { + val id: Coeval[AssetId] = Coeval.evalOnce(FastHashId.create(this.bodyBytes())) +} - val id: Coeval[AssetId] = Coeval.evalOnce(ByteStr(crypto.fastHash(bodyBytes()))) +object FastHashId { + def create(bodyBytes: Array[Byte]): AssetId = { + ByteStr(crypto.fastHash(bodyBytes)) + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/GenesisTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/GenesisTransaction.scala index e6f4e98..c30f0d0 100644 --- a/src/main/scala/com/zbsnetwork/transaction/GenesisTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/GenesisTransaction.scala @@ -1,15 +1,17 @@ package com.zbsnetwork.transaction +import cats.implicits._ import com.google.common.primitives.{Bytes, Ints, Longs} import com.zbsnetwork.account.Address import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto import com.zbsnetwork.transaction.TransactionParsers._ +import com.zbsnetwork.transaction.description.{AddressBytes, ByteEntity, LongBytes} import monix.eval.Coeval import play.api.libs.json.{JsObject, Json} -import scala.util.{Failure, Success, Try} +import scala.util.Try case class GenesisTransaction private (recipient: Address, amount: Long, timestamp: Long, signature: ByteStr) extends Transaction { @@ -66,22 +68,14 @@ object GenesisTransaction extends TransactionParserFor[GenesisTransaction] with override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { Try { - require(bytes.length >= BASE_LENGTH, "Data does not match base length") - - var position = 0 - - val timestampBytes = java.util.Arrays.copyOfRange(bytes, position, position + TimestampLength) - val timestamp = Longs.fromByteArray(timestampBytes) - position += TimestampLength - - val recipientBytes = java.util.Arrays.copyOfRange(bytes, position, position + RECIPIENT_LENGTH) - val recipient = Address.fromBytes(recipientBytes).explicitGet() - position += RECIPIENT_LENGTH - val amountBytes = java.util.Arrays.copyOfRange(bytes, position, position + AmountLength) - val amount = Longs.fromByteArray(amountBytes) + require(bytes.length >= BASE_LENGTH, "Data does not match base length") - GenesisTransaction.create(recipient, amount, timestamp).fold(left => Failure(new Exception(left.toString)), right => Success(right)) + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + Either + .cond(tx.amount >= 0, tx, ValidationError.NegativeAmount(tx.amount, "zbs")) + .foldToTry + } }.flatten } @@ -93,4 +87,20 @@ object GenesisTransaction extends TransactionParserFor[GenesisTransaction] with Right(GenesisTransaction(recipient, amount, timestamp, signature)) } } + + val byteTailDescription: ByteEntity[GenesisTransaction] = { + ( + LongBytes(tailIndex(1), "Timestamp"), + AddressBytes(tailIndex(2), "Recipient's address"), + LongBytes(tailIndex(3), "Amount") + ) mapN { + case (timestamp, recipient, amount) => + GenesisTransaction( + recipient = recipient, + amount = amount, + timestamp = timestamp, + signature = ByteStr(generateSignature(recipient, amount, timestamp)) + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/PaymentTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/PaymentTransaction.scala index 3a79362..a2a9198 100644 --- a/src/main/scala/com/zbsnetwork/transaction/PaymentTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/PaymentTransaction.scala @@ -1,7 +1,6 @@ package com.zbsnetwork.transaction -import java.util - +import cats.implicits._ import com.google.common.primitives.{Bytes, Ints, Longs} import com.zbsnetwork.account.{Address, PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr @@ -9,10 +8,11 @@ import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto import com.zbsnetwork.crypto._ import com.zbsnetwork.transaction.TransactionParsers._ +import com.zbsnetwork.transaction.description._ import monix.eval.Coeval import play.api.libs.json.{JsObject, Json} -import scala.util.{Failure, Success, Try} +import scala.util.Try case class PaymentTransaction private (sender: PublicKeyAccount, recipient: Address, amount: Long, fee: Long, timestamp: Long, signature: ByteStr) extends SignedTransaction { @@ -78,41 +78,43 @@ object PaymentTransaction extends TransactionParserFor[PaymentTransaction] with override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { Try { - require(bytes.length >= BaseLength, "Data does not match base length") - - var position = 0 - - //READ TIMESTAMP - val timestampBytes = bytes.take(TimestampLength) - val timestamp = Longs.fromByteArray(timestampBytes) - position += TimestampLength - - //READ SENDER - val senderBytes = util.Arrays.copyOfRange(bytes, position, position + SenderLength) - val sender = PublicKeyAccount(senderBytes) - position += SenderLength - - //READ RECIPIENT - val recipientBytes = util.Arrays.copyOfRange(bytes, position, position + RecipientLength) - val recipient = Address.fromBytes(recipientBytes).explicitGet() - position += RecipientLength - //READ AMOUNT - val amountBytes = util.Arrays.copyOfRange(bytes, position, position + AmountLength) - val amount = Longs.fromByteArray(amountBytes) - position += AmountLength - - //READ FEE - val feeBytes = util.Arrays.copyOfRange(bytes, position, position + FeeLength) - val fee = Longs.fromByteArray(feeBytes) - position += FeeLength - - //READ SIGNATURE - val signatureBytes = util.Arrays.copyOfRange(bytes, position, position + SignatureLength) + require(bytes.length >= BaseLength, "Data does not match base length") - PaymentTransaction - .create(sender, recipient, amount, fee, timestamp, ByteStr(signatureBytes)) - .fold(left => Failure(new Exception(left.toString)), right => Success(right)) + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + ( + if (tx.amount <= 0) { + Left(ValidationError.NegativeAmount(tx.amount, "zbs")) //CHECK IF AMOUNT IS POSITIVE + } else if (tx.fee <= 0) { + Left(ValidationError.InsufficientFee) //CHECK IF FEE IS POSITIVE + } else if (Try(Math.addExact(tx.amount, tx.fee)).isFailure) { + Left(ValidationError.OverflowError) // CHECK THAT fee+amount won't overflow Long + } else { + Right(tx) + } + ).foldToTry + } }.flatten } + + val byteTailDescription: ByteEntity[PaymentTransaction] = { + ( + LongBytes(tailIndex(1), "Timestamp"), + PublicKeyAccountBytes(tailIndex(2), "Sender's public key"), + AddressBytes(tailIndex(3), "Recipient's address"), + LongBytes(tailIndex(4), "Amount"), + LongBytes(tailIndex(5), "Fee"), + SignatureBytes(tailIndex(6), "Signature") + ) mapN { + case (timestamp, senderPublicKey, recipient, amount, fee, signature) => + PaymentTransaction( + sender = senderPublicKey, + recipient = recipient, + amount = amount, + fee = fee, + timestamp = timestamp, + signature = signature + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/Proofs.scala b/src/main/scala/com/zbsnetwork/transaction/Proofs.scala index 8905392..c818c8e 100644 --- a/src/main/scala/com/zbsnetwork/transaction/Proofs.scala +++ b/src/main/scala/com/zbsnetwork/transaction/Proofs.scala @@ -1,7 +1,7 @@ package com.zbsnetwork.transaction import com.zbsnetwork.common.state.ByteStr -import com.zbsnetwork.common.utils.{Base58, EitherExt2} +import com.zbsnetwork.common.utils.Base58 import com.zbsnetwork.serialization.Deser import com.zbsnetwork.transaction.ValidationError.GenericError import com.zbsnetwork.utils.base58Length @@ -12,19 +12,17 @@ import scala.util.Try case class Proofs(proofs: List[ByteStr]) { val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(Proofs.Version +: Deser.serializeArrays(proofs.map(_.arr))) val base58: Coeval[Seq[String]] = Coeval.evalOnce(proofs.map(p => Base58.encode(p.arr))) + def toSignature: ByteStr = proofs.headOption.getOrElse(ByteStr.empty) override def toString: String = s"Proofs(${proofs.mkString(", ")})" } object Proofs { - - def apply(proofs: Seq[AssetId]): Proofs = new Proofs(proofs.toList) - val Version = 1: Byte val MaxProofs = 8 val MaxProofSize = 64 val MaxProofStringSize = base58Length(MaxProofSize) - lazy val empty = create(List.empty).explicitGet() + lazy val empty = new Proofs(Nil) def create(proofs: Seq[ByteStr]): Either[ValidationError, Proofs] = for { @@ -38,4 +36,7 @@ object Proofs { arrs <- Try(Deser.parseArrays(ab.tail)).toEither.left.map(er => GenericError(er.toString)) r <- create(arrs.map(ByteStr(_)).toList) } yield r + + implicit def apply(proofs: Seq[ByteStr]): Proofs = new Proofs(proofs.toList) + implicit def toSeq(proofs: Proofs): Seq[ByteStr] = proofs.proofs } diff --git a/src/main/scala/com/zbsnetwork/transaction/Signed.scala b/src/main/scala/com/zbsnetwork/transaction/Signed.scala index fb3a0e1..e7d0df8 100644 --- a/src/main/scala/com/zbsnetwork/transaction/Signed.scala +++ b/src/main/scala/com/zbsnetwork/transaction/Signed.scala @@ -11,10 +11,13 @@ import scala.concurrent.duration.Duration trait Signed extends Authorized { protected val signatureValid: Coeval[Boolean] + @ApiModelProperty(hidden = true) protected val signedDescendants: Coeval[Seq[Signed]] = Coeval.evalOnce(Seq.empty) + @ApiModelProperty(hidden = true) protected val signaturesValidMemoized: Task[Either[InvalidSignature, this.type]] = Signed.validateTask[this.type](this).memoize + @ApiModelProperty(hidden = true) val signaturesValid: Coeval[Either[InvalidSignature, this.type]] = Coeval.evalOnce(Await.result(signaturesValidMemoized.runAsync(Signed.scheduler), Duration.Inf)) diff --git a/src/main/scala/com/zbsnetwork/transaction/TransactionParser.scala b/src/main/scala/com/zbsnetwork/transaction/TransactionParser.scala index 8a57773..fd7161a 100644 --- a/src/main/scala/com/zbsnetwork/transaction/TransactionParser.scala +++ b/src/main/scala/com/zbsnetwork/transaction/TransactionParser.scala @@ -1,5 +1,8 @@ package com.zbsnetwork.transaction +import cats.implicits._ +import com.zbsnetwork.transaction.description.{ByteEntity, ConstantByte, OneByte} + import scala.reflect.ClassTag import scala.util.Try @@ -12,11 +15,49 @@ trait TransactionParser { def supportedVersions: Set[Byte] def parseBytes(bytes: Array[Byte]): Try[TransactionT] = - parseHeader(bytes) flatMap (offset => parseTail(bytes.drop(offset))) + parseHeader(bytes) flatMap (offset => parseTail(bytes drop offset)) /** @return offset */ protected def parseHeader(bytes: Array[Byte]): Try[Int] protected def parseTail(bytes: Array[Byte]): Try[TransactionT] + + /** Byte description of the header of the transaction */ + val byteHeaderDescription: ByteEntity[Unit] + + /** + * Byte description of the transaction. Can be used for deserialization. + * + * Implementation example: + * {{{ + * val bytesTailDescription: ByteEntity[Transaction] = + * ( + * OneByte(1, "Transaction type"), + * OneByte(2, "Version"), + * LongBytes(3, "Fee") + * ) mapN { case (txType, version, fee) => Transaction(txType, version, fee) } + * + * // deserialization from buf: Array[Byte] + * val tx: Try[Transaction] = byteTailDescription.deserializeFromByteArray(buf) + * }}} + */ + val byteTailDescription: ByteEntity[TransactionT] + + /** + * Returns index of byte entity in `byteTailDescription` + * taking into account the last index in `byteHeaderDescription` + */ + protected def tailIndex(index: Int): Int = byteHeaderDescription.index + index + + /** + * Full byte description of the transaction (header + tail). Can be used for deserialization and generation of the documentation. + * + * Usage example: + * {{{ + * // generation of the documentation + * val txStringDocumentationForMD: String = byteDescription.getStringDocForMD + * }}} + */ + lazy val byteDescription: ByteEntity[TransactionT] = (byteHeaderDescription, byteTailDescription) mapN { case (_, tx) => tx } } object TransactionParser { @@ -35,6 +76,10 @@ object TransactionParser { 1 } + + lazy val byteHeaderDescription: ByteEntity[Unit] = { + ConstantByte(1, typeId, "Transaction type") map (_ => Unit) + } } trait OneVersion extends TransactionParser { @@ -55,6 +100,13 @@ object TransactionParser { 2 } + + lazy val byteHeaderDescription: ByteEntity[Unit] = { + ( + ConstantByte(1, value = typeId, name = "Transaction type"), + ConstantByte(2, value = version, name = "Version") + ) mapN ((_, _) => Unit) + } } trait MultipleVersions extends TransactionParser { @@ -72,8 +124,15 @@ object TransactionParser { 3 } - } + lazy val byteHeaderDescription: ByteEntity[Unit] = { + ( + ConstantByte(1, value = 0, name = "Transaction multiple version mark"), + ConstantByte(2, value = typeId, name = "Transaction type"), + OneByte(3, "Version") + ) mapN ((_, _, _) => Unit) + } + } } abstract class TransactionParserFor[T <: Transaction](implicit override val classTag: ClassTag[T]) extends TransactionParser { diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransaction.scala index 98f6be8..39cfde1 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransaction.scala @@ -1,9 +1,7 @@ package com.zbsnetwork.transaction.assets import com.google.common.primitives.{Bytes, Longs} -import com.zbsnetwork.account.PublicKeyAccount import com.zbsnetwork.common.state.ByteStr -import com.zbsnetwork.crypto._ import com.zbsnetwork.transaction._ import monix.eval.Coeval import play.api.libs.json.{JsObject, Json} @@ -50,16 +48,8 @@ object BurnTransaction { val typeId: Byte = 6 - def parseBase(start: Int, bytes: Array[Byte]): (PublicKeyAccount, AssetId, Long, Long, Long, Int) = { - val sender = PublicKeyAccount(bytes.slice(start, start + KeyLength)) - val assetId = ByteStr(bytes.slice(start + KeyLength, start + KeyLength + AssetIdLength)) - val quantityStart = start + KeyLength + AssetIdLength - - val quantity = Longs.fromByteArray(bytes.slice(quantityStart, quantityStart + 8)) - val fee = Longs.fromByteArray(bytes.slice(quantityStart + 8, quantityStart + 16)) - val timestamp = Longs.fromByteArray(bytes.slice(quantityStart + 16, quantityStart + 24)) - - (sender, assetId, quantity, fee, timestamp, quantityStart + 24) + def validateBurnParams(tx: BurnTransaction): Either[ValidationError, Unit] = { + validateBurnParams(tx.quantity, tx.fee) } def validateBurnParams(amount: Long, fee: Long): Either[ValidationError, Unit] = diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV1.scala index 4b81c87..7d464d2 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV1.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV1.scala @@ -1,14 +1,16 @@ package com.zbsnetwork.transaction.assets +import cats.implicits._ import com.google.common.primitives.Bytes -import com.zbsnetwork.crypto -import monix.eval.Coeval import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.crypto import com.zbsnetwork.transaction._ -import com.zbsnetwork.crypto._ +import com.zbsnetwork.transaction.description._ +import monix.eval.Coeval -import scala.util.{Failure, Success, Try} +import scala.util.Try case class BurnTransactionV1 private (sender: PublicKeyAccount, assetId: ByteStr, quantity: Long, fee: Long, timestamp: Long, signature: ByteStr) extends BurnTransaction @@ -28,13 +30,12 @@ object BurnTransactionV1 extends TransactionParserFor[BurnTransactionV1] with Tr override val typeId: Byte = BurnTransaction.typeId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val (sender, assetId, quantity, fee, timestamp, end) = BurnTransaction.parseBase(0, bytes) - val signature = ByteStr(bytes.slice(end, end + SignatureLength)) - BurnTransactionV1 - .create(sender, assetId, quantity, fee, timestamp, signature) - .fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + BurnTransaction + .validateBurnParams(tx) + .map(_ => tx) + .foldToTry + } } def create(sender: PublicKeyAccount, @@ -62,4 +63,15 @@ object BurnTransactionV1 extends TransactionParserFor[BurnTransactionV1] with Tr def selfSigned(sender: PrivateKeyAccount, assetId: ByteStr, quantity: Long, fee: Long, timestamp: Long): Either[ValidationError, TransactionT] = { signed(sender, assetId, quantity, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[BurnTransactionV1] = { + ( + PublicKeyAccountBytes(tailIndex(1), "Sender's public key"), + ByteStrDefinedLength(tailIndex(2), "Asset ID", AssetIdLength), + LongBytes(tailIndex(3), "Quantity"), + LongBytes(tailIndex(4), "Fee"), + LongBytes(tailIndex(5), "Timestamp"), + SignatureBytes(tailIndex(6), "Signature") + ) mapN BurnTransactionV1.apply + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV2.scala index 19121b7..5ff0cd9 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV2.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/BurnTransactionV2.scala @@ -1,14 +1,16 @@ package com.zbsnetwork.transaction.assets +import cats.implicits._ import com.google.common.primitives.Bytes -import com.zbsnetwork.crypto -import monix.eval.Coeval import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} -import com.zbsnetwork.transaction._ -import cats.implicits._ import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.crypto +import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.description._ +import monix.eval.Coeval -import scala.util.{Failure, Success, Try} +import scala.util.Try final case class BurnTransactionV2 private (chainId: Byte, sender: PublicKeyAccount, @@ -39,18 +41,12 @@ object BurnTransactionV2 extends TransactionParserFor[BurnTransactionV2] with Tr override val supportedVersions: Set[Byte] = Set(2) override protected def parseTail(bytes: Array[Byte]): Try[BurnTransactionV2] = { - Try { - val chainId = bytes(0) - val (sender, assetId, quantity, fee, timestamp, end) = BurnTransaction.parseBase(1, bytes) - - (for { - proofs <- Proofs.fromBytes(bytes.drop(end)) - tx <- create(chainId, sender, assetId, quantity, fee, timestamp, proofs) - } yield tx).fold( - err => Failure(new Exception(err.toString)), - t => Success(t) - ) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + BurnTransaction + .validateBurnParams(tx) + .map(_ => tx) + .foldToTry + } } def create(chainId: Byte, @@ -86,4 +82,16 @@ object BurnTransactionV2 extends TransactionParserFor[BurnTransactionV2] with Tr timestamp: Long): Either[ValidationError, TransactionT] = { signed(chainId, sender, assetId, quantity, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[BurnTransactionV2] = { + ( + OneByte(tailIndex(1), "Chain ID"), + PublicKeyAccountBytes(tailIndex(2), "Sender's public key"), + ByteStrDefinedLength(tailIndex(3), "Asset ID", AssetIdLength), + LongBytes(tailIndex(4), "Quantity"), + LongBytes(tailIndex(5), "Fee"), + LongBytes(tailIndex(6), "Timestamp"), + ProofsBytes(tailIndex(7)) + ) mapN BurnTransactionV2.apply + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransaction.scala index beb5650..f67b25c 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransaction.scala @@ -4,8 +4,6 @@ import java.nio.charset.StandardCharsets import cats.implicits._ import com.google.common.primitives.{Bytes, Longs} -import com.zbsnetwork.account.PublicKeyAccount -import com.zbsnetwork.crypto._ import com.zbsnetwork.serialization.Deser import com.zbsnetwork.transaction.smart.script.Script import com.zbsnetwork.transaction.validation._ @@ -57,6 +55,10 @@ object IssueTransaction { val MinAssetNameLength = 4 val MaxDecimals = 8 + def validateIssueParams(tx: IssueTransaction): Either[ValidationError, Unit] = { + validateIssueParams(tx.name, tx.description, tx.quantity, tx.decimals, tx.reissuable, tx.fee) + } + def validateIssueParams(name: Array[Byte], description: Array[Byte], quantity: Long, @@ -73,16 +75,4 @@ object IssueTransaction { .leftMap(_.head) .toEither } - - def parseBase(bytes: Array[Byte], start: Int) = { - val sender = PublicKeyAccount(bytes.slice(start, start + KeyLength)) - val (assetName, descriptionStart) = Deser.parseArraySize(bytes, start + KeyLength) - val (description, quantityStart) = Deser.parseArraySize(bytes, descriptionStart) - val quantity = Longs.fromByteArray(bytes.slice(quantityStart, quantityStart + 8)) - val decimals = bytes.slice(quantityStart + 8, quantityStart + 9).head - val reissuable = bytes.slice(quantityStart + 9, quantityStart + 10).head == (1: Byte) - val fee = Longs.fromByteArray(bytes.slice(quantityStart + 10, quantityStart + 18)) - val timestamp = Longs.fromByteArray(bytes.slice(quantityStart + 18, quantityStart + 26)) - (sender, assetName, description, quantity, decimals, reissuable, fee, timestamp, quantityStart + 26) - } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV1.scala index 8e891d0..0e1046b 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV1.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV1.scala @@ -1,16 +1,18 @@ package com.zbsnetwork.transaction.assets +import cats.implicits._ import com.google.common.primitives.Bytes import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto -import com.zbsnetwork.crypto.SignatureLength import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.description._ import com.zbsnetwork.transaction.smart.script.Script import monix.eval.Coeval import play.api.libs.json.JsObject -import scala.util.{Failure, Success, Try} +import scala.util.Try case class IssueTransactionV1 private (sender: PublicKeyAccount, name: Array[Byte], @@ -37,16 +39,14 @@ object IssueTransactionV1 extends TransactionParserFor[IssueTransactionV1] with override val typeId: Byte = IssueTransaction.typeId - override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = - Try { - val signature = ByteStr(bytes.slice(0, SignatureLength)) - val txId = bytes(SignatureLength) - require(txId == typeId, s"Signed tx id is not match") - val (sender, assetName, description, quantity, decimals, reissuable, fee, timestamp, _) = IssueTransaction.parseBase(bytes, SignatureLength + 1) - IssueTransactionV1 - .create(sender, assetName, description, quantity, decimals, reissuable, fee, timestamp, signature) - .fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + IssueTransaction + .validateIssueParams(tx) + .map(_ => tx) + .foldToTry + } + } def create(sender: PublicKeyAccount, name: Array[Byte], @@ -86,4 +86,33 @@ object IssueTransactionV1 extends TransactionParserFor[IssueTransactionV1] with timestamp: Long): Either[ValidationError, TransactionT] = { signed(sender, name, description, quantity, decimals, reissuable, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[IssueTransactionV1] = { + ( + SignatureBytes(tailIndex(1), "Signature"), + ConstantByte(tailIndex(2), value = typeId, name = "Transaction type"), + PublicKeyAccountBytes(tailIndex(3), "Sender's public key"), + BytesArrayUndefinedLength(tailIndex(4), "Asset name"), + BytesArrayUndefinedLength(tailIndex(5), "Description"), + LongBytes(tailIndex(6), "Quantity"), + OneByte(tailIndex(7), "Decimals"), + BooleanByte(tailIndex(8), "Reissuable flag (1 - True, 0 - False)"), + LongBytes(tailIndex(9), "Fee"), + LongBytes(tailIndex(10), "Timestamp") + ) mapN { + case (signature, txId, senderPublicKey, name, desc, quantity, decimals, reissuable, fee, timestamp) => + require(txId == typeId, s"Signed tx id is not match") + IssueTransactionV1( + sender = senderPublicKey, + name = name, + description = desc, + quantity = quantity, + decimals = decimals, + reissuable = reissuable, + fee = fee, + timestamp = timestamp, + signature = signature + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV2.scala index 6c7ee66..53872bf 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV2.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/IssueTransactionV2.scala @@ -1,13 +1,16 @@ package com.zbsnetwork.transaction.assets +import cats.implicits._ import com.google.common.primitives.Bytes import com.zbsnetwork.account.{AddressScheme, PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto import com.zbsnetwork.serialization.Deser import com.zbsnetwork.transaction.ValidationError.GenericError import com.zbsnetwork.transaction._ -import com.zbsnetwork.transaction.smart.script.{Script, ScriptReader} +import com.zbsnetwork.transaction.description._ +import com.zbsnetwork.transaction.smart.script.Script import monix.eval.Coeval import play.api.libs.json.{JsObject, Json} @@ -53,25 +56,13 @@ object IssueTransactionV2 extends TransactionParserFor[IssueTransactionV2] with private def currentChainId = AddressScheme.current.chainId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val chainId = bytes(0) - val (sender, assetName, description, quantity, decimals, reissuable, fee, timestamp, scriptStart) = IssueTransaction.parseBase(bytes, 1) - val (scriptOptEi: Option[Either[ValidationError.ScriptParseError, Script]], scriptEnd) = - Deser.parseOption(bytes, scriptStart)(ScriptReader.fromBytes) - val scriptEiOpt: Either[ValidationError.ScriptParseError, Option[Script]] = scriptOptEi match { - case None => Right(None) - case Some(Right(sc)) => Right(Some(sc)) - case Some(Left(err)) => Left(err) - } - - (for { - proofs <- Proofs.fromBytes(bytes.drop(scriptEnd)) - script <- scriptEiOpt - tx <- IssueTransactionV2 - .create(chainId, sender, assetName, description, quantity, decimals, reissuable, script, fee, timestamp, proofs) - } yield tx).left.map(e => new Throwable(e.toString)).toTry - - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + Either + .cond(tx.chainId == currentChainId, (), GenericError(s"Wrong chainId actual: ${tx.chainId.toInt}, expected: $currentChainId")) + .flatMap(_ => IssueTransaction.validateIssueParams(tx)) + .map(_ => tx) + .foldToTry + } } def create(chainId: Byte, @@ -120,4 +111,35 @@ object IssueTransactionV2 extends TransactionParserFor[IssueTransactionV2] with timestamp: Long): Either[ValidationError, TransactionT] = { signed(chainId, sender, name, description, quantity, decimals, reissuable, script, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[IssueTransactionV2] = { + ( + OneByte(tailIndex(1), "Chain ID"), + PublicKeyAccountBytes(tailIndex(2), "Sender's public key"), + BytesArrayUndefinedLength(tailIndex(3), "Name"), + BytesArrayUndefinedLength(tailIndex(4), "Description"), + LongBytes(tailIndex(5), "Quantity"), + OneByte(tailIndex(6), "Decimals"), + BooleanByte(tailIndex(7), "Reissuable flag (1 - True, 0 - False)"), + LongBytes(tailIndex(8), "Fee"), + LongBytes(tailIndex(9), "Timestamp"), + OptionBytes(index = tailIndex(10), name = "Script", nestedByteEntity = ScriptBytes(tailIndex(10), "Script")), + ProofsBytes(tailIndex(11)) + ) mapN { + case (chainId, senderPublicKey, name, desc, quantity, decimals, reissuable, fee, timestamp, script, proofs) => + IssueTransactionV2( + chainId = chainId, + sender = senderPublicKey, + name = name, + description = desc, + quantity = quantity, + decimals = decimals, + reissuable = reissuable, + script = script, + fee = fee, + timestamp = timestamp, + proofs = proofs + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransaction.scala index 4ff73b3..70a4e76 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransaction.scala @@ -2,13 +2,11 @@ package com.zbsnetwork.transaction.assets import cats.implicits._ import com.google.common.primitives.{Bytes, Longs} -import monix.eval.Coeval -import play.api.libs.json.{JsObject, Json} -import com.zbsnetwork.account.PublicKeyAccount import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.transaction.validation._ import com.zbsnetwork.transaction.{AssetId, ProvenTransaction, ValidationError, _} -import com.zbsnetwork.crypto._ +import monix.eval.Coeval +import play.api.libs.json.{JsObject, Json} trait ReissueTransaction extends ProvenTransaction with VersionedTransaction { def assetId: ByteStr @@ -45,23 +43,13 @@ object ReissueTransaction { val typeId: Byte = 5 + def validateReissueParams(tx: ReissueTransaction): Either[ValidationError, Unit] = { + validateReissueParams(tx.quantity, tx.fee) + } + def validateReissueParams(quantity: Long, fee: Long): Either[ValidationError, Unit] = (validateAmount(quantity, "assets"), validateFee(fee)) .mapN { case _ => () } .leftMap(_.head) .toEither - - def parseBase(bytes: Array[Byte], start: Int): (PublicKeyAccount, AssetId, Long, Boolean, Long, Long, Int) = { - val senderEnd = start + KeyLength - val assetIdEnd = senderEnd + AssetIdLength - val sender = PublicKeyAccount(bytes.slice(start, senderEnd)) - val assetId = ByteStr(bytes.slice(senderEnd, assetIdEnd)) - val quantity = Longs.fromByteArray(bytes.slice(assetIdEnd, assetIdEnd + 8)) - val reissuable = bytes.slice(assetIdEnd + 8, assetIdEnd + 9).head == (1: Byte) - val fee = Longs.fromByteArray(bytes.slice(assetIdEnd + 9, assetIdEnd + 17)) - val end = assetIdEnd + 25 - val timestamp = Longs.fromByteArray(bytes.slice(assetIdEnd + 17, end)) - - (sender, assetId, quantity, reissuable, fee, timestamp, end) - } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV1.scala index f5b4dea..2c5e43e 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV1.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV1.scala @@ -1,14 +1,16 @@ package com.zbsnetwork.transaction.assets +import cats.implicits._ import com.google.common.primitives.Bytes -import com.zbsnetwork.crypto -import monix.eval.Coeval import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.crypto import com.zbsnetwork.transaction._ -import com.zbsnetwork.crypto._ +import com.zbsnetwork.transaction.description._ +import monix.eval.Coeval -import scala.util.{Failure, Success, Try} +import scala.util.Try case class ReissueTransactionV1 private (sender: PublicKeyAccount, assetId: ByteStr, @@ -34,15 +36,12 @@ object ReissueTransactionV1 extends TransactionParserFor[ReissueTransactionV1] w override val typeId: Byte = ReissueTransaction.typeId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val signature = ByteStr(bytes.slice(0, SignatureLength)) - val txId = bytes(SignatureLength) - require(txId == typeId, s"Signed tx id is not match") - val (sender, assetId, quantity, reissuable, fee, timestamp, _) = ReissueTransaction.parseBase(bytes, SignatureLength + 1) - ReissueTransactionV1 - .create(sender, assetId, quantity, reissuable, fee, timestamp, signature) - .fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + ReissueTransaction + .validateReissueParams(tx) + .map(_ => tx) + .foldToTry + } } def create(sender: PublicKeyAccount, @@ -79,4 +78,29 @@ object ReissueTransactionV1 extends TransactionParserFor[ReissueTransactionV1] w unsigned.copy(signature = ByteStr(crypto.sign(sender, unsigned.bodyBytes()))) } } + + val byteTailDescription: ByteEntity[ReissueTransactionV1] = { + ( + SignatureBytes(tailIndex(1), "Signature"), + ConstantByte(tailIndex(2), value = typeId, name = "Transaction type"), + PublicKeyAccountBytes(tailIndex(3), "Sender's public key"), + ByteStrDefinedLength(tailIndex(4), "Asset ID", AssetIdLength), + LongBytes(tailIndex(5), "Quantity"), + BooleanByte(tailIndex(6), "Reissuable flag (1 - True, 0 - False)"), + LongBytes(tailIndex(7), "Fee"), + LongBytes(tailIndex(8), "Timestamp") + ) mapN { + case (signature, txId, sender, assetId, quantity, reissuable, fee, timestamp) => + require(txId == typeId, s"Signed tx id is not match") + ReissueTransactionV1( + sender = sender, + assetId = assetId, + quantity = quantity, + reissuable = reissuable, + fee = fee, + timestamp = timestamp, + signature = signature + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV2.scala index a3ac06d..74e5c9f 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV2.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/ReissueTransactionV2.scala @@ -1,11 +1,14 @@ package com.zbsnetwork.transaction.assets +import cats.implicits._ import com.google.common.primitives.Bytes import com.zbsnetwork.account.{AddressScheme, PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto import com.zbsnetwork.transaction.ValidationError.GenericError import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.description._ import monix.eval.Coeval import scala.util._ @@ -45,16 +48,13 @@ object ReissueTransactionV2 extends TransactionParserFor[ReissueTransactionV2] w private def currentChainId: Byte = AddressScheme.current.chainId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val chainId = bytes(0) - val (sender, assetId, quantity, reissuable, fee, timestamp, end) = ReissueTransaction.parseBase(bytes, 1) - (for { - proofs <- Proofs.fromBytes(bytes.drop(end)) - tx <- ReissueTransactionV2 - .create(chainId, sender, assetId, quantity, reissuable, fee, timestamp, proofs) - } yield tx) - .fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + Either + .cond(tx.chainId == currentChainId, (), GenericError(s"Wrong chainId actual: ${tx.chainId.toInt}, expected: $currentChainId")) + .flatMap(_ => ReissueTransaction.validateReissueParams(tx)) + .map(_ => tx) + .foldToTry + } } def create(chainId: Byte, @@ -94,4 +94,17 @@ object ReissueTransactionV2 extends TransactionParserFor[ReissueTransactionV2] w timestamp: Long): Either[ValidationError, TransactionT] = { signed(chainId, sender, assetId, quantity, reissuable, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[ReissueTransactionV2] = { + ( + OneByte(tailIndex(1), "Chain ID"), + PublicKeyAccountBytes(tailIndex(2), "Sender's public key"), + ByteStrDefinedLength(tailIndex(3), "Asset ID", AssetIdLength), + LongBytes(tailIndex(4), "Quantity"), + BooleanByte(tailIndex(5), "Reissuable flag (1 - True, 0 - False)"), + LongBytes(tailIndex(6), "Fee"), + LongBytes(tailIndex(7), "Timestamp"), + ProofsBytes(tailIndex(8)) + ) mapN ReissueTransactionV2.apply + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/SetAssetScriptTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/SetAssetScriptTransaction.scala index d51eea4..d323e42 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/SetAssetScriptTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/SetAssetScriptTransaction.scala @@ -1,18 +1,20 @@ package com.zbsnetwork.transaction.assets -import cats.data.State +import cats.implicits._ import com.google.common.primitives.{Bytes, Longs} import com.zbsnetwork.account._ import com.zbsnetwork.common.state.ByteStr -import monix.eval.Coeval -import play.api.libs.json.{JsObject, Json} import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto._ import com.zbsnetwork.serialization.Deser import com.zbsnetwork.transaction._ -import com.zbsnetwork.transaction.smart.script.{Script, ScriptReader} +import com.zbsnetwork.transaction.description._ +import com.zbsnetwork.transaction.smart.script.Script +import com.zbsnetwork.transaction.smart.script.v1.ExprScript +import monix.eval.Coeval +import play.api.libs.json.{JsObject, Json} -import scala.util.{Failure, Success, Try} +import scala.util.Try case class SetAssetScriptTransaction private (chainId: Byte, sender: PublicKeyAccount, @@ -70,7 +72,11 @@ object SetAssetScriptTransaction extends TransactionParserFor[SetAssetScriptTran fee: Long, timestamp: Long, proofs: Proofs): Either[ValidationError, TransactionT] = { + for { + _ <- Either.cond(script.fold(true)(_.isInstanceOf[ExprScript]), + (), + ValidationError.GenericError(s"Asset can oly be assigned with Expression script, not Contract")) _ <- Either.cond(chainId == currentChainId, (), ValidationError.GenericError(s"Wrong chainId actual: ${chainId.toInt}, expected: $currentChainId")) @@ -90,40 +96,34 @@ object SetAssetScriptTransaction extends TransactionParserFor[SetAssetScriptTran } } override def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - val readByte: State[Int, Byte] = State { from => - (from + 1, bytes(from)) + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + Either + .cond(tx.chainId == currentChainId, (), ValidationError.GenericError(s"Wrong chainId actual: ${tx.chainId.toInt}, expected: $currentChainId")) + .map(_ => tx) + .foldToTry } - def read[T](f: Array[Byte] => T, size: Int): State[Int, T] = State { from => - val end = from + size - (end, f(bytes.slice(from, end))) - } - def readUnsized[T](f: (Array[Byte], Int) => (T, Int)): State[Int, T] = State { from => - val (v, end) = f(bytes, from) - (end, v) - } - def readEnd[T](f: Array[Byte] => T): State[Int, T] = State { from => - (from, f(bytes.drop(from))) - } - - Try { - val makeTransaction = for { - chainId <- readByte - sender <- read(PublicKeyAccount.apply, KeyLength) - assetId <- read(ByteStr.apply, AssetIdLength) - fee <- read(Longs.fromByteArray _, 8) - timestamp <- read(Longs.fromByteArray _, 8) - scriptOrE <- readUnsized((b: Array[Byte], p: Int) => Deser.parseOption(b, p)(ScriptReader.fromBytes)) - proofs <- readEnd(Proofs.fromBytes) - } yield { - (scriptOrE match { - case Some(Left(err)) => Left(err) - case Some(Right(s)) => Right(Some(s)) - case None => Right(None) - }).flatMap(script => create(chainId, sender, assetId, script, fee, timestamp, proofs.right.get)) - .fold(left => Failure(new Exception(left.toString)), right => Success(right)) - } - makeTransaction.run(0).value._2 - }.flatten } + val byteTailDescription: ByteEntity[SetAssetScriptTransaction] = { + ( + OneByte(tailIndex(1), "Chain ID"), + PublicKeyAccountBytes(tailIndex(2), "Sender's public key"), + ByteStrDefinedLength(tailIndex(3), "Asset ID", AssetIdLength), + LongBytes(tailIndex(4), "Fee"), + LongBytes(tailIndex(5), "Timestamp"), + OptionBytes(index = tailIndex(6), name = "Script", nestedByteEntity = ScriptBytes(tailIndex(6), "Script")), + ProofsBytes(tailIndex(7)) + ) mapN { + case (chainId, sender, assetId, fee, timestamp, script, proofs) => + SetAssetScriptTransaction( + chainId = chainId, + sender = sender, + assetId = assetId, + script = script, + fee = fee, + timestamp = timestamp, + proofs = proofs + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/SponsorFeeTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/SponsorFeeTransaction.scala index 9f21bba..617ea88 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/SponsorFeeTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/SponsorFeeTransaction.scala @@ -1,16 +1,17 @@ package com.zbsnetwork.transaction.assets +import cats.implicits._ import com.google.common.primitives.{Bytes, Longs} import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto -import com.zbsnetwork.crypto._ import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.description._ import monix.eval.Coeval import play.api.libs.json.{JsObject, Json} -import scala.util.{Failure, Success, Try} +import scala.util.Try case class SponsorFeeTransaction private (sender: PublicKeyAccount, assetId: ByteStr, @@ -60,26 +61,17 @@ object SponsorFeeTransaction extends TransactionParserFor[SponsorFeeTransaction] override val supportedVersions: Set[Byte] = Set(version) override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val txId = bytes(0) - require(txId == typeId, s"Signed tx id is not match") - val bodyVersion = bytes(1) - require(bodyVersion == version, s"versions are not match ($version, $bodyVersion)") - val sender = PublicKeyAccount(bytes.slice(2, KeyLength + 2)) - val assetId = ByteStr(bytes.slice(KeyLength + 2, KeyLength + AssetIdLength + 2)) - val minFeeStart = KeyLength + AssetIdLength + 2 - - val minFee = Longs.fromByteArray(bytes.slice(minFeeStart, minFeeStart + 8)) - val fee = Longs.fromByteArray(bytes.slice(minFeeStart + 8, minFeeStart + 16)) - val timestamp = Longs.fromByteArray(bytes.slice(minFeeStart + 16, minFeeStart + 24)) - val tx = for { - proofs <- Proofs.fromBytes(bytes.drop(minFeeStart + 24)) - tx <- SponsorFeeTransaction.create(sender, assetId, Some(minFee).filter(_ != 0), fee, timestamp, proofs) - } yield { - tx - } - tx.fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + ( + if (tx.minSponsoredAssetFee.exists(_ < 0)) { + Left(ValidationError.NegativeMinFee(tx.minSponsoredAssetFee.get, "asset")) + } else if (tx.fee <= 0) { + Left(ValidationError.InsufficientFee()) + } else { + Right(tx) + } + ).foldToTry + } } def create(sender: PublicKeyAccount, @@ -115,4 +107,29 @@ object SponsorFeeTransaction extends TransactionParserFor[SponsorFeeTransaction] timestamp: Long): Either[ValidationError, TransactionT] = { signed(sender, assetId, minSponsoredAssetFee, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[SponsorFeeTransaction] = { + ( + OneByte(tailIndex(1), "Transaction type"), + OneByte(tailIndex(2), "Version"), + PublicKeyAccountBytes(tailIndex(3), "Sender's public key"), + ByteStrDefinedLength(tailIndex(4), "Asset ID", AssetIdLength), + SponsorFeeOptionLongBytes(tailIndex(5), "Minimal fee in assets*"), + LongBytes(tailIndex(6), "Fee"), + LongBytes(tailIndex(7), "Timestamp"), + ProofsBytes(tailIndex(8)) + ) mapN { + case (txId, bodyVersion, sender, assetId, minSponsoredAssetFee, fee, timestamp, proofs) => + require(txId == typeId, s"Signed tx id is not match") + require(bodyVersion == version, s"versions are not match ($version, $bodyVersion)") + SponsorFeeTransaction( + sender = sender, + assetId = assetId, + minSponsoredAssetFee = minSponsoredAssetFee, + fee = fee, + timestamp = timestamp, + proofs = proofs + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransaction.scala index a030211..e84bc3e 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransaction.scala @@ -58,6 +58,10 @@ object ExchangeTransaction { else ExchangeTransactionV1.parseBytes(bytes) } + def validateExchangeParams(tx: ExchangeTransaction): Either[ValidationError, Unit] = { + validateExchangeParams(tx.buyOrder, tx.sellOrder, tx.amount, tx.price, tx.buyMatcherFee, tx.sellMatcherFee, tx.fee, tx.timestamp) + } + def validateExchangeParams(buyOrder: Order, sellOrder: Order, amount: Long, diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV1.scala index 0f2c4e7..6bf03cf 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV1.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV1.scala @@ -1,17 +1,18 @@ package com.zbsnetwork.transaction.assets.exchange -import cats.data.State +import cats.implicits._ import com.google.common.primitives.{Ints, Longs} import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto -import com.zbsnetwork.crypto._ import com.zbsnetwork.transaction._ import com.zbsnetwork.transaction.assets.exchange.ExchangeTransaction._ +import com.zbsnetwork.transaction.description._ import io.swagger.annotations.ApiModelProperty import monix.eval.Coeval -import scala.util.{Failure, Success, Try} +import scala.util.Try case class ExchangeTransactionV1(buyOrder: OrderV1, sellOrder: OrderV1, @@ -25,7 +26,8 @@ case class ExchangeTransactionV1(buyOrder: OrderV1, extends ExchangeTransaction with SignedTransaction { - override def version: Byte = 1 + override def version: Byte = 1 + override val builder = ExchangeTransactionV1 override val assetFee: (Option[AssetId], Long) = (None, fee) @@ -88,29 +90,40 @@ object ExchangeTransactionV1 extends TransactionParserFor[ExchangeTransactionV1] } override def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - def read[T](f: Array[Byte] => T, size: Int): State[Int, T] = State { from => - val end = from + size - (end, f(bytes.slice(from, end))) + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + ExchangeTransaction + .validateExchangeParams(tx) + .map(_ => tx) + .foldToTry } + } - Try { - val makeTransaction = for { - o1Size <- read(Ints.fromByteArray _, 4) - o2Size <- read(Ints.fromByteArray _, 4) - o1 <- read(OrderV1.parseBytes _, o1Size).map(_.get) - o2 <- read(OrderV1.parseBytes _, o2Size).map(_.get) - price <- read(Longs.fromByteArray _, 8) - amount <- read(Longs.fromByteArray _, 8) - buyMatcherFee <- read(Longs.fromByteArray _, 8) - sellMatcherFee <- read(Longs.fromByteArray _, 8) - fee <- read(Longs.fromByteArray _, 8) - timestamp <- read(Longs.fromByteArray _, 8) - signature <- read(ByteStr.apply, SignatureLength) - } yield { - create(o1, o2, amount, price, buyMatcherFee, sellMatcherFee, fee, timestamp, signature) - .fold(left => Failure(new Exception(left.toString)), right => Success(right)) - } - makeTransaction.run(0).value._2 - }.flatten + val byteTailDescription: ByteEntity[ExchangeTransactionV1] = { + ( + IntBytes(tailIndex(1), "Buy order object length (BN)"), + IntBytes(tailIndex(2), "Sell order object length (SN)"), + OrderV1Bytes(tailIndex(3), "Buy order object", "BN"), + OrderV1Bytes(tailIndex(4), "Sell order object", "SN"), + LongBytes(tailIndex(5), "Price"), + LongBytes(tailIndex(6), "Amount"), + LongBytes(tailIndex(7), "Buy matcher fee"), + LongBytes(tailIndex(8), "Sell matcher fee"), + LongBytes(tailIndex(9), "Fee"), + LongBytes(tailIndex(10), "Timestamp"), + SignatureBytes(tailIndex(11), "Signature") + ) mapN { + case (_, _, buyOrder, sellOrder, price, amount, buyMatcherFee, sellMatcherFee, fee, timestamp, signature) => + ExchangeTransactionV1( + buyOrder = buyOrder, + sellOrder = sellOrder, + amount = amount, + price = price, + buyMatcherFee = buyMatcherFee, + sellMatcherFee = sellMatcherFee, + fee = fee, + timestamp = timestamp, + signature = signature + ) + } } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV2.scala index 9427e4e..37d3ea3 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV2.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/ExchangeTransactionV2.scala @@ -1,16 +1,18 @@ package com.zbsnetwork.transaction.assets.exchange -import cats.data.State +import cats.implicits._ import com.google.common.primitives.{Ints, Longs} import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto import com.zbsnetwork.transaction._ import com.zbsnetwork.transaction.assets.exchange.ExchangeTransaction._ +import com.zbsnetwork.transaction.description._ import io.swagger.annotations.ApiModelProperty import monix.eval.Coeval -import scala.util.{Failure, Success, Try} +import scala.util.Try case class ExchangeTransactionV2(buyOrder: Order, sellOrder: Order, @@ -96,42 +98,38 @@ object ExchangeTransactionV2 extends TransactionParserFor[ExchangeTransactionV2] } override def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - def back(off: Int): State[Int, Unit] = State { from => - (from - off, ()) - } - val readByte: State[Int, Byte] = State { from => - (from + 1, bytes(from)) - } - def read[T](f: Array[Byte] => T, size: Int): State[Int, T] = State { from => - val end = from + size - (end, f(bytes.slice(from, end))) - } - def readEnd[T](f: Array[Byte] => T): State[Int, T] = State { from => - (from, f(bytes.drop(from))) + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + ExchangeTransaction + .validateExchangeParams(tx) + .map(_ => tx) + .foldToTry } + } - Try { - val makeTransaction = for { - o1Size <- read(Ints.fromByteArray _, 4) - o1Ver <- readByte - _ <- back(if (o1Ver != 1) { 1 } else { 0 }) - o1 <- read(if (o1Ver == 1) { OrderV1.parseBytes _ } else { OrderV2.parseBytes _ }, o1Size).map(_.get) - o2Size <- read(Ints.fromByteArray _, 4) - o2Ver <- readByte - _ <- back(if (o2Ver != 1) { 1 } else { 0 }) - o2 <- read(if (o2Ver == 1) { OrderV1.parseBytes _ } else { OrderV2.parseBytes _ }, o2Size).map(_.get) - price <- read(Longs.fromByteArray _, 8) - amount <- read(Longs.fromByteArray _, 8) - buyMatcherFee <- read(Longs.fromByteArray _, 8) - sellMatcherFee <- read(Longs.fromByteArray _, 8) - fee <- read(Longs.fromByteArray _, 8) - timestamp <- read(Longs.fromByteArray _, 8) - proofs <- readEnd(Proofs.fromBytes) - } yield { - create(o1, o2, amount, price, buyMatcherFee, sellMatcherFee, fee, timestamp, proofs.right.get) - .fold(left => Failure(new Exception(left.toString)), right => Success(right)) - } - makeTransaction.run(0).value._2 - }.flatten + val byteTailDescription: ByteEntity[ExchangeTransactionV2] = { + ( + OrderBytes(tailIndex(1), "Buy order"), + OrderBytes(tailIndex(2), "Sell order"), + LongBytes(tailIndex(3), "Price"), + LongBytes(tailIndex(4), "Amount"), + LongBytes(tailIndex(5), "Buy matcher fee"), + LongBytes(tailIndex(6), "Sell matcher fee"), + LongBytes(tailIndex(7), "Fee"), + LongBytes(tailIndex(8), "Timestamp"), + ProofsBytes(tailIndex(9)) + ) mapN { + case (buyOrder, sellOrder, price, amount, buyMatcherFee, sellMatcherFee, fee, timestamp, proofs) => + ExchangeTransactionV2( + buyOrder = buyOrder, + sellOrder = sellOrder, + amount = amount, + price = price, + buyMatcherFee = buyMatcherFee, + sellMatcherFee = sellMatcherFee, + fee = fee, + timestamp = timestamp, + proofs = proofs + ) + } } } diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/Order.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/Order.scala index 82e6e80..7004603 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/Order.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/Order.scala @@ -7,6 +7,7 @@ import com.zbsnetwork.crypto import com.zbsnetwork.serialization.{BytesSerializable, JsonSerializable} import com.zbsnetwork.transaction.ValidationError.GenericError import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.assets.exchange.OrderOps._ import com.zbsnetwork.transaction.assets.exchange.Validation.booleanOperators import com.zbsnetwork.utils.byteStrWrites import io.swagger.annotations.ApiModelProperty @@ -44,6 +45,8 @@ trait Order extends BytesSerializable with JsonSerializable with Proven { def signature: Array[Byte] = proofs.proofs(0).arr + def matcherFeeAssetId: Option[AssetId] = None + import Order._ @ApiModelProperty(hidden = true) @@ -160,7 +163,8 @@ trait Order extends BytesSerializable with JsonSerializable with Proven { @ApiModelProperty(hidden = true) override def toString: String = { - s"OrderV$version(id=${idStr()}, sender=$senderPublicKey, matcher=$matcherPublicKey, pair=$assetPair, tpe=$orderType, amount=$amount, price=$price, ts=$timestamp, exp=$expiration, fee=$matcherFee, proofs=$proofs)" + val matcherFeeAssetIdStr = if (version == 3) s" matcherFeeAssetId=${matcherFeeAssetId.fold("Zbs")(_.toString)}," else "" + s"OrderV$version(id=${idStr()}, sender=$senderPublicKey, matcher=$matcherPublicKey, pair=$assetPair, tpe=$orderType, amount=$amount, price=$price, ts=$timestamp, exp=$expiration, fee=$matcherFee,$matcherFeeAssetIdStr proofs=$proofs)" } } @@ -181,12 +185,9 @@ object Order { expiration: Long, matcherFee: Long, proofs: Proofs, - version: Byte = 1): Order = { - if (version == 1) { - OrderV1(senderPublicKey, matcherPublicKey, assetPair, orderType, amount, price, timestamp, expiration, matcherFee, proofs) - } else { - OrderV2(senderPublicKey, matcherPublicKey, assetPair, orderType, amount, price, timestamp, expiration, matcherFee, proofs) - } + version: Byte = 1): Order = version match { + case 1 => OrderV1(senderPublicKey, matcherPublicKey, assetPair, orderType, amount, price, timestamp, expiration, matcherFee, proofs) + case 2 => OrderV2(senderPublicKey, matcherPublicKey, assetPair, orderType, amount, price, timestamp, expiration, matcherFee, proofs) } def apply(senderPublicKey: PublicKeyAccount, @@ -198,17 +199,11 @@ object Order { timestamp: Long, expiration: Long, matcherFee: Long, - signature: Array[Byte]): Order = { - OrderV1(senderPublicKey, - matcherPublicKey, - assetPair, - orderType, - amount, - price, - timestamp, - expiration, - matcherFee, - Proofs(Seq(ByteStr(signature)))) + proofs: Proofs, + version: Byte, + matcherFeeAssetId: Option[AssetId]): Order = version match { + case 3 => + OrderV3(senderPublicKey, matcherPublicKey, assetPair, orderType, amount, price, timestamp, expiration, matcherFee, matcherFeeAssetId, proofs) } def correctAmount(a: Long, price: Long): Long = { @@ -226,8 +221,13 @@ object Order { timestamp: Long, expiration: Long, matcherFee: Long, - version: Byte = 1): Order = { - val unsigned = Order(sender, matcher, pair, OrderType.BUY, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version) + version: Byte = 1, + matcherFeeAssetId: Option[AssetId] = None): Order = { + val unsigned = version match { + case 3 => + Order(sender, matcher, pair, OrderType.BUY, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version, matcherFeeAssetId) + case _ => Order(sender, matcher, pair, OrderType.BUY, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version) + } sign(unsigned, sender) } @@ -239,8 +239,13 @@ object Order { timestamp: Long, expiration: Long, matcherFee: Long, - version: Byte = 1): Order = { - val unsigned = Order(sender, matcher, pair, OrderType.SELL, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version) + version: Byte = 1, + matcherFeeAssetId: Option[AssetId] = None): Order = { + val unsigned = version match { + case 3 => + Order(sender, matcher, pair, OrderType.SELL, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version, matcherFeeAssetId) + case _ => Order(sender, matcher, pair, OrderType.SELL, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version) + } sign(unsigned, sender) } @@ -258,15 +263,25 @@ object Order { sign(unsigned, sender) } + def apply(sender: PrivateKeyAccount, + matcher: PublicKeyAccount, + pair: AssetPair, + orderType: OrderType, + amount: Long, + price: Long, + timestamp: Long, + expiration: Long, + matcherFee: Long, + version: Byte, + matcherFeeAssetId: Option[AssetId]): Order = { + val unsigned = Order(sender, matcher, pair, orderType, amount, price, timestamp, expiration, matcherFee, Proofs.empty, version, matcherFeeAssetId) + sign(unsigned, sender) + } + def sign(unsigned: Order, sender: PrivateKeyAccount): Order = { require(unsigned.senderPublicKey == sender) val sig = crypto.sign(sender, unsigned.bodyBytes()) - unsigned match { - case o @ OrderV2(_, _, _, _, _, _, _, _, _, _) => - o.copy(proofs = Proofs(Seq(ByteStr(sig)))) - case o @ OrderV1(_, _, _, _, _, _, _, _, _, _) => - o.copy(proofs = Proofs(Seq(ByteStr(sig)))) - } + unsigned.updateProofs(Proofs(Seq(ByteStr(sig)))) } def splitByType(o1: Order, o2: Order): (Order, Order) = { diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderJson.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderJson.scala index 1eece71..968ec60 100644 --- a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderJson.scala +++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderJson.scala @@ -4,7 +4,7 @@ import com.zbsnetwork.account.PublicKeyAccount import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.Base58 import com.zbsnetwork.crypto.SignatureLength -import com.zbsnetwork.transaction.Proofs +import com.zbsnetwork.transaction.{AssetId, Proofs} import play.api.libs.json._ import scala.util.{Failure, Success} @@ -42,19 +42,36 @@ object OrderJson { case _ => JsError(Seq(JsPath() -> Seq(JsonValidationError("error.expected.jsstring")))) } - def readOrder(sender: PublicKeyAccount, - matcher: PublicKeyAccount, - assetPair: AssetPair, - orderType: OrderType, - amount: Long, - price: Long, - timestamp: Long, - expiration: Long, - matcherFee: Long, - signature: Option[Array[Byte]], - proofs: Option[Array[Array[Byte]]], - version: Option[Byte]): Order = { - val eproofs = proofs.map(p => Proofs(p.map(ByteStr.apply))).orElse(signature.map(s => Proofs(Seq(ByteStr(s))))).getOrElse(Proofs.empty) + implicit val assetIdReads: Reads[AssetId] = { + case JsString(s) => + Base58.decode(s) match { + case Success(bytes) => JsSuccess(ByteStr(bytes)) + case _ => JsError(Seq(JsPath() -> Seq(JsonValidationError("error.incorrect.assetId")))) + } + case _ => JsError(Seq(JsPath() -> Seq(JsonValidationError("error.expected.jsstring")))) + } + + def readOrderV1V2(sender: PublicKeyAccount, + matcher: PublicKeyAccount, + assetPair: AssetPair, + orderType: OrderType, + amount: Long, + price: Long, + timestamp: Long, + expiration: Long, + matcherFee: Long, + signature: Option[Array[Byte]], + proofs: Option[Array[Array[Byte]]], + version: Option[Byte]): Order = { + + val eproofs = + proofs + .map(p => Proofs(p.map(ByteStr.apply))) + .orElse(signature.map(s => Proofs(Seq(ByteStr(s))))) + .getOrElse(Proofs.empty) + + val vrsn: Byte = version.getOrElse(if (eproofs.proofs.size == 1 && eproofs.proofs.head.arr.length == SignatureLength) 1 else 2) + Order( sender, matcher, @@ -66,7 +83,43 @@ object OrderJson { expiration, matcherFee, eproofs, - version.getOrElse(if (eproofs.proofs.size == 1 && eproofs.proofs.head.arr.length == SignatureLength) 1 else 2) + vrsn + ) + } + + def readOrderV3(sender: PublicKeyAccount, + matcher: PublicKeyAccount, + assetPair: AssetPair, + orderType: OrderType, + amount: Long, + price: Long, + timestamp: Long, + expiration: Long, + matcherFee: Long, + signature: Option[Array[Byte]], + proofs: Option[Array[Array[Byte]]], + version: Byte, + matcherFeeAssetId: Option[AssetId]): Order = { + + val eproofs = + proofs + .map(p => Proofs(p.map(ByteStr.apply))) + .orElse(signature.map(s => Proofs(Seq(ByteStr(s))))) + .getOrElse(Proofs.empty) + + Order( + sender, + matcher, + assetPair, + orderType, + amount, + price, + timestamp, + expiration, + matcherFee, + eproofs, + version, + matcherFeeAssetId ) } @@ -83,7 +136,7 @@ object OrderJson { implicit val orderTypeReads: Reads[OrderType] = JsPath.read[String].map(OrderType.apply) - implicit val orderReads: Reads[Order] = { + private val orderV1V2Reads: Reads[Order] = { val r = (JsPath \ "senderPublicKey").read[PublicKeyAccount] and (JsPath \ "matcherPublicKey").read[PublicKeyAccount] and (JsPath \ "assetPair").read[AssetPair] and @@ -96,7 +149,33 @@ object OrderJson { (JsPath \ "signature").readNullable[Array[Byte]] and (JsPath \ "proofs").readNullable[Array[Array[Byte]]] and (JsPath \ "version").readNullable[Byte] - r(readOrder _) + r(readOrderV1V2 _) + } + + private val orderV3Reads: Reads[Order] = { + val r = (JsPath \ "senderPublicKey").read[PublicKeyAccount] and + (JsPath \ "matcherPublicKey").read[PublicKeyAccount] and + (JsPath \ "assetPair").read[AssetPair] and + (JsPath \ "orderType").read[OrderType] and + (JsPath \ "amount").read[Long] and + (JsPath \ "price").read[Long] and + (JsPath \ "timestamp").read[Long] and + (JsPath \ "expiration").read[Long] and + (JsPath \ "matcherFee").read[Long] and + (JsPath \ "signature").readNullable[Array[Byte]] and + (JsPath \ "proofs").readNullable[Array[Array[Byte]]] and + (JsPath \ "version").read[Byte] and + (JsPath \ "matcherFeeAssetId").readNullable[AssetId] + r(readOrderV3 _) + } + + implicit val orderReads: Reads[Order] = { + case jsOrder @ JsObject(map) => + map.getOrElse("version", JsNumber(1)) match { + case JsNumber(x) if x.byteValue() == 3 => orderV3Reads.reads(jsOrder) + case _ => orderV1V2Reads.reads(jsOrder) + } + case invalidOrder => JsError(s"Can't parse invalid order $invalidOrder") } implicit val orderFormat: Format[Order] = Format(orderReads, Writes[Order](_.json())) diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderOps.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderOps.scala new file mode 100644 index 0000000..b64f809 --- /dev/null +++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderOps.scala @@ -0,0 +1,90 @@ +package com.zbsnetwork.transaction.assets.exchange + +import com.zbsnetwork.account.PrivateKeyAccount +import com.zbsnetwork.transaction.Proofs + +class OrderOps(val o: Order) extends AnyVal { + @inline def copy(withV1: OrderV1 => OrderV1, withV2: OrderV2 => OrderV2, withV3: OrderV3 => OrderV3): Order = { + o match { + case o1: OrderV1 => withV1(o1) + case o2: OrderV2 => withV2(o2) + case o3: OrderV3 => withV3(o3) + } + } + + @inline def updateProofs(p: Proofs): Order = { + copy( + _.copy(proofs = p), + _.copy(proofs = p), + _.copy(proofs = p) + ) + } + + @inline def updateExpiration(expiration: Long): Order = { + copy( + _.copy(expiration = expiration), + _.copy(expiration = expiration), + _.copy(expiration = expiration) + ) + } + @inline def updateTimestamp(timestamp: Long): Order = { + copy( + _.copy(timestamp = timestamp), + _.copy(timestamp = timestamp), + _.copy(timestamp = timestamp) + ) + } + @inline def updateFee(fee: Long): Order = { + copy( + _.copy(matcherFee = fee), + _.copy(matcherFee = fee), + _.copy(matcherFee = fee) + ) + } + @inline def updateAmount(amount: Long): Order = { + copy( + _.copy(amount = amount), + _.copy(amount = amount), + _.copy(amount = amount) + ) + } + @inline def updatePrice(price: Long): Order = { + copy( + _.copy(price = price), + _.copy(price = price), + _.copy(price = price) + ) + } + @inline def updateMatcher(pk: PrivateKeyAccount): Order = { + copy( + _.copy(matcherPublicKey = pk), + _.copy(matcherPublicKey = pk), + _.copy(matcherPublicKey = pk) + ) + } + @inline def updateSender(pk: PrivateKeyAccount): Order = { + copy( + _.copy(senderPublicKey = pk), + _.copy(senderPublicKey = pk), + _.copy(senderPublicKey = pk) + ) + } + @inline def updatePair(pair: AssetPair): Order = { + copy( + _.copy(assetPair = pair), + _.copy(assetPair = pair), + _.copy(assetPair = pair) + ) + } + @inline def updateType(t: OrderType): Order = { + copy( + _.copy(orderType = t), + _.copy(orderType = t), + _.copy(orderType = t) + ) + } +} + +object OrderOps { + implicit def toOps(o: Order): OrderOps = new OrderOps(o) +} diff --git a/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderV3.scala b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderV3.scala new file mode 100644 index 0000000..e68d989 --- /dev/null +++ b/src/main/scala/com/zbsnetwork/transaction/assets/exchange/OrderV3.scala @@ -0,0 +1,185 @@ +package com.zbsnetwork.transaction.assets.exchange + +import cats.data.State +import com.google.common.primitives.Longs +import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} +import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.Base58 +import com.zbsnetwork.crypto +import com.zbsnetwork.crypto.KeyLength +import com.zbsnetwork.serialization.Deser +import com.zbsnetwork.transaction.{AssetId, Proofs} +import com.zbsnetwork.utils.byteStrWrites +import monix.eval.Coeval +import play.api.libs.json.{JsObject, Json} + +import scala.util.Try + +case class OrderV3(senderPublicKey: PublicKeyAccount, + matcherPublicKey: PublicKeyAccount, + assetPair: AssetPair, + orderType: OrderType, + amount: Long, + price: Long, + timestamp: Long, + expiration: Long, + matcherFee: Long, + override val matcherFeeAssetId: Option[AssetId], + proofs: Proofs) + extends Order { + + def version: Byte = 3 + + override def signature: Array[Byte] = proofs.proofs.head.arr + + val bodyBytes: Coeval[Array[Byte]] = + Coeval.evalOnce( + Array(version) ++ + senderPublicKey.publicKey ++ + matcherPublicKey.publicKey ++ + assetPair.bytes ++ + orderType.bytes ++ + Longs.toByteArray(amount) ++ + Longs.toByteArray(price) ++ + Longs.toByteArray(timestamp) ++ + Longs.toByteArray(expiration) ++ + Longs.toByteArray(matcherFee) ++ + Order.assetIdBytes(matcherFeeAssetId) + ) + + val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(bodyBytes() ++ proofs.bytes()) + + override val json: Coeval[JsObject] = + Coeval.evalOnce( + { + val sig = Base58.encode(signature) + Json.obj( + "version" -> version, + "id" -> idStr(), + "sender" -> senderPublicKey.address, + "senderPublicKey" -> Base58.encode(senderPublicKey.publicKey), + "matcherPublicKey" -> Base58.encode(matcherPublicKey.publicKey), + "assetPair" -> assetPair.json, + "orderType" -> orderType.toString, + "amount" -> amount, + "price" -> price, + "timestamp" -> timestamp, + "expiration" -> expiration, + "matcherFee" -> matcherFee, + "matcherFeeAssetId" -> matcherFeeAssetId.map(_.base58), + "signature" -> sig, + "proofs" -> proofs.proofs + ) + } + ) +} + +object OrderV3 { + + private val AssetIdLength = 32 + + def buy(sender: PrivateKeyAccount, + matcher: PublicKeyAccount, + pair: AssetPair, + amount: Long, + price: Long, + timestamp: Long, + expiration: Long, + matcherFee: Long, + matcherFeeAssetId: Option[AssetId]): Order = { + + val unsigned = OrderV3(sender, matcher, pair, OrderType.BUY, amount, price, timestamp, expiration, matcherFee, matcherFeeAssetId, Proofs.empty) + val sig = crypto.sign(sender, unsigned.bodyBytes()) + + unsigned.copy(proofs = Proofs(Seq(ByteStr(sig)))) + } + + def sell(sender: PrivateKeyAccount, + matcher: PublicKeyAccount, + pair: AssetPair, + amount: Long, + price: Long, + timestamp: Long, + expiration: Long, + matcherFee: Long, + matcherFeeAssetId: Option[AssetId]): Order = { + + val unsigned = OrderV3(sender, matcher, pair, OrderType.SELL, amount, price, timestamp, expiration, matcherFee, matcherFeeAssetId, Proofs.empty) + val sig = crypto.sign(sender, unsigned.bodyBytes()) + + unsigned.copy(proofs = Proofs(Seq(ByteStr(sig)))) + } + + def apply(sender: PrivateKeyAccount, + matcher: PublicKeyAccount, + pair: AssetPair, + orderType: OrderType, + amount: Long, + price: Long, + timestamp: Long, + expiration: Long, + matcherFee: Long, + matcherFeeAssetId: Option[AssetId]): Order = { + + val unsigned = OrderV3(sender, matcher, pair, orderType, amount, price, timestamp, expiration, matcherFee, matcherFeeAssetId, Proofs.empty) + val sig = crypto.sign(sender, unsigned.bodyBytes()) + + unsigned.copy(proofs = Proofs(Seq(ByteStr(sig)))) + } + + def parseBytes(bytes: Array[Byte]): Try[Order] = Try { + + val longLength = 8 + + val readByte: State[Int, Byte] = State { from => + (from + 1, bytes(from)) + } + + def read[T](f: Array[Byte] => T, size: Int): State[Int, T] = State { from => + val end = from + size + (end, f(bytes.slice(from, end))) + } + + def readEnd[T](f: Array[Byte] => T): State[Int, T] = State { from => + (from, f(bytes.drop(from))) + } + + def parse[T](f: (Array[Byte], Int, Int) => (T, Int), size: Int): State[Int, T] = State { from => + val (res, off) = f(bytes, from, size) + (off, res) + } + + val makeOrder = for { + version <- readByte + _ = if (version != 3) { throw new Exception(s"Incorrect order version: expect 3 but found $version") } + sender <- read(PublicKeyAccount.apply, KeyLength) + matcher <- read(PublicKeyAccount.apply, KeyLength) + amountAssetId <- parse(Deser.parseByteArrayOption, AssetIdLength) + priceAssetId <- parse(Deser.parseByteArrayOption, AssetIdLength) + orderType <- readByte + amount <- read(Longs.fromByteArray, longLength) + price <- read(Longs.fromByteArray, longLength) + timestamp <- read(Longs.fromByteArray, longLength) + expiration <- read(Longs.fromByteArray, longLength) + matcherFee <- read(Longs.fromByteArray, longLength) + matcherFeeAssetId <- parse(Deser.parseByteArrayOption, AssetIdLength) + maybeProofs <- readEnd(Proofs.fromBytes) + } yield { + OrderV3( + senderPublicKey = sender, + matcherPublicKey = matcher, + assetPair = AssetPair(amountAssetId.map(ByteStr.apply), priceAssetId.map(ByteStr.apply)), + orderType = OrderType(orderType), + amount = amount, + price = price, + timestamp = timestamp, + expiration = expiration, + matcherFee = matcherFee, + matcherFeeAssetId = matcherFeeAssetId.map(ByteStr.apply), + proofs = maybeProofs.right.get + ) + } + + makeOrder.run(0).value._2 + } +} diff --git a/src/main/scala/com/zbsnetwork/transaction/description/ByteEntities.scala b/src/main/scala/com/zbsnetwork/transaction/description/ByteEntities.scala new file mode 100644 index 0000000..0159dc8 --- /dev/null +++ b/src/main/scala/com/zbsnetwork/transaction/description/ByteEntities.scala @@ -0,0 +1,481 @@ +package com.zbsnetwork.transaction.description + +import cats.{Functor, Semigroupal} +import com.google.common.primitives.{Ints, Longs, Shorts} +import com.zbsnetwork.account.{Address, AddressOrAlias, Alias, PublicKeyAccount} +import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.crypto.{KeyLength, SignatureLength} +import com.zbsnetwork.lang.v1.Serde +import com.zbsnetwork.lang.v1.compiler.Terms +import com.zbsnetwork.lang.v1.compiler.Terms.FUNCTION_CALL +import com.zbsnetwork.serialization.Deser +import com.zbsnetwork.state.DataEntry +import com.zbsnetwork.transaction.ValidationError.Validation +import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.assets.exchange._ +import com.zbsnetwork.transaction.smart.ContractInvocationTransaction.Payment +import com.zbsnetwork.transaction.smart.script.{Script, ScriptReader} +import com.zbsnetwork.transaction.transfer.MassTransferTransaction.ParsedTransfer + +import scala.util.{Failure, Success, Try} + +/** + * Represents description of the byte entity + * Field `additionalInfo` can be used for specifying of the repeating byte entities + */ +case class ByteEntityDescription(index: Int, name: String, tpe: String, length: String, subIndex: Int = 0, additionalInfo: String = "") + +/** + * Describes byte representation of the different types. Composition of Byte Entities can be used for deserialization + * and generation of the documentation of the complex data structures, such as transactions, messages, orders, etc + */ +sealed trait ByteEntity[T] { self => + + private[description] val ByteType = "Byte" + private[description] val BooleanType = "Boolean" + private[description] val IntType = "Int" + private[description] val LongType = "Long" + private[description] val ByteArrayType = "Array[Byte]" + private[description] val ByteStrType = s"ByteStr ($ByteArrayType)" + private[description] val AddressType = "Address" + private[description] val AliasType = "Alias" + private[description] val AddressOrAliasType = "Address or Alias" + private[description] val OrderV1Type = "OrderV1" + private[description] val OrderType = "Order" + private[description] val UnimportantType = "" + + /** Index of the byte entity. In case of composition of byte entities returns index of the last one */ + val index: Int + + private[description] def generateDoc: Seq[ByteEntityDescription] + + private[description] def deserialize(buf: Array[Byte], offset: Int): Try[(T, Int)] + + def deserializeFromByteArray(buf: Array[Byte]): Try[T] = deserialize(buf, 0) map { case (value, _) => value } + + def map[U](f: T => U): ByteEntity[U] = new ByteEntity[U] { + + val index: Int = self.index + + def generateDoc: Seq[ByteEntityDescription] = self.generateDoc + + def deserialize(buf: Array[Byte], offset: Int): Try[(U, Int)] = self.deserialize(buf, offset).map { case (t, o) => f(t) -> o } + } + + /** Generates documentation ready for pasting into .md files */ + def getStringDocForMD: String = { + + val docs = generateDoc + + docs + .map { + case ByteEntityDescription(idx, name, tpe, length, subIndex, additionalInfo) => + s"| $idx${Option(subIndex).filter(_ != 0).fold("")(si => s".$si")} | $name | $tpe | $length $additionalInfo\n" + .replace("...", "| ... | ... | ... | ... |") + .replace("(", "\\(") + .replace(")", "\\)") + .replace("*", "\\*") + } + .foldLeft("""| \# | Field name | Type | Length |""" + "\n| --- | --- | --- | --- |\n")(_ + _) + } +} + +object ByteEntity { + + implicit def byteEntityFunctor: Functor[ByteEntity] = new Functor[ByteEntity] { + def map[A, B](fa: ByteEntity[A])(f: A => B): ByteEntity[B] = fa map f + } + + implicit def byteEntitySemigroupal: Semigroupal[ByteEntity] = new Semigroupal[ByteEntity] { + def product[A, B](fa: ByteEntity[A], fb: ByteEntity[B]): ByteEntity[(A, B)] = Composition(fa, fb) + } +} + +case class ConstantByte(index: Int, value: Byte, name: String) extends ByteEntity[Byte] { + + def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, s"$ByteType (constant, value = $value)", "1")) + + def deserialize(buf: Array[Byte], offset: Int): Try[(Byte, Int)] = { + Try { value -> (offset + 1) } + } +} + +case class OneByte(index: Int, name: String) extends ByteEntity[Byte] { + + def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, ByteType, "1")) + + def deserialize(buf: Array[Byte], offset: Int): Try[(Byte, Int)] = { + Try { buf(offset) -> (offset + 1) } + } +} + +case class LongBytes(index: Int, name: String) extends ByteEntity[Long] { + + def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, LongType, "8")) + + def deserialize(buf: Array[Byte], offset: Int): Try[(Long, Int)] = { + Try { Longs.fromByteArray(buf.slice(offset, offset + 8)) -> (offset + 8) } + } +} + +case class IntBytes(index: Int, name: String) extends ByteEntity[Int] { + + def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, IntType, "4")) + + def deserialize(buf: Array[Byte], offset: Int): Try[(Int, Int)] = { + Try { Ints.fromByteArray(buf.slice(offset, offset + 4)) -> (offset + 4) } + } +} + +case class BooleanByte(index: Int, name: String) extends ByteEntity[Boolean] { + + def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, BooleanType, "1")) + + def deserialize(buf: Array[Byte], offset: Int): Try[(Boolean, Int)] = { + Try { (buf(offset) == 1) -> (offset + 1) } + } +} + +case class BytesArrayDefinedLength(index: Int, name: String, length: Int) extends ByteEntity[Array[Byte]] { + + def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, ByteArrayType, length.toString)) + + def deserialize(buf: Array[Byte], offset: Int): Try[(Array[Byte], Int)] = { + Try { buf.slice(offset, offset + length) -> (offset + length) } + } +} + +case class BytesArrayUndefinedLength(index: Int, name: String) extends ByteEntity[Array[Byte]] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq( + ByteEntityDescription(index, s"$name length (N)", UnimportantType, "2", subIndex = 1), + ByteEntityDescription(index, name, ByteArrayType, "N", subIndex = 2) + ) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(Array[Byte], Int)] = { + Try { + val length = Shorts.fromByteArray(buf.slice(offset, offset + 2)) + val (arrayStart, arrayEnd) = (offset + 2, offset + 2 + length) + buf.slice(arrayStart, arrayEnd) -> arrayEnd + } + } +} + +case class ByteStrDefinedLength(index: Int, name: String, length: Int) extends ByteEntity[ByteStr] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq(ByteEntityDescription(index, name, ByteStrType, length.toString)) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(ByteStr, Int)] = { + Try { ByteStr(buf.slice(offset, offset + length)) -> (offset + length) } + } +} + +case class PublicKeyAccountBytes(index: Int, name: String) extends ByteEntity[PublicKeyAccount] { + + def generateDoc: Seq[ByteEntityDescription] = + Seq(ByteEntityDescription(index, name, s"PublicKeyAccount ($ByteArrayType)", KeyLength.toString)) + + def deserialize(buf: Array[Byte], offset: Int): Try[(PublicKeyAccount, Int)] = { + Try { PublicKeyAccount(buf.slice(offset, offset + KeyLength)) -> (offset + KeyLength) } + } +} + +case class SignatureBytes(index: Int, name: String) extends ByteEntity[ByteStr] { + + def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, ByteStrType, SignatureLength.toString)) + + def deserialize(buf: Array[Byte], offset: Int): Try[(ByteStr, Int)] = { + Try { ByteStr(buf.slice(offset, offset + SignatureLength)) -> (offset + SignatureLength) } + } +} + +case class SponsorFeeOptionLongBytes(index: Int, name: String) extends ByteEntity[Option[Long]] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq(ByteEntityDescription(index, name, LongType, "8")) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(Option[Long], Int)] = { + Try { Option(Longs.fromByteArray(buf.slice(offset, offset + 8))).filter(_ != 0) -> (offset + 8) } + } +} + +case class AddressBytes(index: Int, name: String) extends ByteEntity[Address] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq(ByteEntityDescription(index, name, AddressType, s"${Address.AddressLength}")) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(Address, Int)] = { + Try { + + val recipientBytes = java.util.Arrays.copyOfRange(buf, offset, offset + Address.AddressLength) + val recipient = Address.fromBytes(recipientBytes).explicitGet() + + recipient -> (offset + Address.AddressLength) + } + } +} + +case class AliasBytes(index: Int, name: String) extends ByteEntity[Alias] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq( + ByteEntityDescription(index, s"$name length (A)", UnimportantType, "2", subIndex = 1), + ByteEntityDescription(index, s"$name", AliasType, "A", subIndex = 2) + ) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(Alias, Int)] = { + val aliasLength = Shorts.fromByteArray(buf.slice(offset, offset + 2)) + Alias + .fromBytes(buf.slice(offset + 2, offset + 2 + aliasLength)) + .map(alias => alias -> (offset + 2 + aliasLength)) + .fold(err => Failure(new Exception(err.toString)), Success.apply) + } +} + +case class AddressOrAliasBytes(index: Int, name: String) extends ByteEntity[AddressOrAlias] { + + def generateDoc: Seq[ByteEntityDescription] = + Seq(ByteEntityDescription(index, name, AddressOrAliasType, "depends on first byte (1 - Address, 2 - Alias)")) + + def deserialize(buf: Array[Byte], offset: Int): Try[(AddressOrAlias, Int)] = { + Try { AddressOrAlias.fromBytes(buf, offset).explicitGet() } + } +} + +case class ProofsBytes(index: Int) extends ByteEntity[Proofs] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq( + ByteEntityDescription(index, s"Proofs version (${Proofs.Version})", UnimportantType, "1", subIndex = 1), + ByteEntityDescription(index, "Proofs count", UnimportantType, "2", subIndex = 2), + ByteEntityDescription(index, "Proof 1 length (P1)", UnimportantType, "2", subIndex = 3), + ByteEntityDescription(index, "Proof 1", ByteStrType, "P1", subIndex = 4), + ByteEntityDescription(index, "Proof 2 length (P2)", UnimportantType, "2", subIndex = 5), + ByteEntityDescription(index, "Proof 2 ", ByteStrType, "P2", subIndex = 6, additionalInfo = "\n...") + ) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(Proofs, Int)] = { + Try { Proofs.fromBytes(buf.drop(offset)).map(p => p -> (offset + p.bytes.value.length)).explicitGet() } + } +} + +case class TransfersBytes(index: Int) extends ByteEntity[List[ParsedTransfer]] { + + import cats.implicits._ + + private def readTransfer(buf: Array[Byte], offset: Int): (Validation[ParsedTransfer], Int) = { + AddressOrAlias.fromBytes(buf, offset) match { + case Right((addressOrAlias, ofs)) => + val amount = Longs.fromByteArray(buf.slice(ofs, ofs + 8)) + Right[ValidationError, ParsedTransfer](ParsedTransfer(addressOrAlias, amount)) -> (ofs + 8) + case Left(validationError) => Left(validationError) -> offset + } + } + + def generateDoc: Seq[ByteEntityDescription] = { + Seq( + ByteEntityDescription(index, "Number of transfers", UnimportantType, "2", 1), + ByteEntityDescription(index, "Address or alias for transfer 1", AddressOrAliasType, "depends on first byte (1 - Address, 2 - Alias)", 2), + ByteEntityDescription(index, "Amount for transfer 1", LongType, "8", 3), + ByteEntityDescription(index, "Address or alias for transfer 2", AddressOrAliasType, "depends on first byte (1 - Address, 2 - Alias)", 4), + ByteEntityDescription(index, "Amount for transfer 2", LongType, "8", 5, additionalInfo = "\n...") + ) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(List[ParsedTransfer], Int)] = { + Try { + + val transferCount = Shorts.fromByteArray(buf.slice(offset, offset + 2)) + + val transfersList: List[(Validation[ParsedTransfer], Int)] = + List.iterate(readTransfer(buf, offset + 2), transferCount) { case (_, offst) => readTransfer(buf, offst) } + + val resultOffset = transfersList.lastOption.map(_._2).getOrElse(offset + 2) + val resultList = transfersList.map { case (ei, _) => ei }.sequence.explicitGet() + + resultList -> resultOffset + } + } + +} + +case class OrderBytes(index: Int, name: String) extends ByteEntity[Order] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq( + ByteEntityDescription(index, s"$name size (N)", UnimportantType, "4", subIndex = 1), + ByteEntityDescription(index, s"$name version mark", UnimportantType, "1 (version 1) / 0 (version 2)", subIndex = 2), + ByteEntityDescription(index, name, OrderType, "N", subIndex = 3) + ) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(Order, Int)] = { + Try { + + val orderSize = Ints.fromByteArray(buf.slice(offset, offset + 4)) + val orderMark = buf(offset + 4) + + orderMark match { + case 1 => OrderV1.parseBytes(buf.drop(offset + 5)).map(order => order -> (offset + 5 + orderSize)) + case 2 => OrderV2.parseBytes(buf.drop(offset + 4)).map(order => order -> (offset + 4 + orderSize)) + case 3 => OrderV3.parseBytes(buf.drop(offset + 4)).map(order => order -> (offset + 4 + orderSize)) + } + }.flatten + } +} + +case class OrderV1Bytes(index: Int, name: String, length: String) extends ByteEntity[OrderV1] { + + def generateDoc: Seq[ByteEntityDescription] = Seq(ByteEntityDescription(index, name, OrderV1Type, s"$length")) + + def deserialize(buf: Array[Byte], offset: Int): Try[(OrderV1, Int)] = { + OrderV1.parseBytes(buf.drop(offset)).map { order => + order -> (offset + order.bytes.value.length) + } + } +} + +case class ListDataEntryBytes(index: Int) extends ByteEntity[List[DataEntry[_]]] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq( + ByteEntityDescription(index, "Data entries count", UnimportantType, "2", subIndex = 1), + ByteEntityDescription(index, "Key 1 length (K1)", UnimportantType, "2", subIndex = 2), + ByteEntityDescription(index, "Key 1 bytes", "UTF-8 encoded", "K1", subIndex = 3), + ByteEntityDescription(index, "Value 1 type (0 = integer, 1 = boolean, 2 = binary array, 3 = string)", UnimportantType, "1", subIndex = 4), + ByteEntityDescription(index, "Value 1 bytes", "Value 1 type", "depends on value type", subIndex = 5, additionalInfo = "\n...") + ) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(List[DataEntry[_]], Int)] = { + Try { + + val entryCount = Shorts.fromByteArray(buf.slice(offset, offset + 2)) + + if (entryCount > 0) { + val parsed = List.iterate(DataEntry.parse(buf, offset + 2), entryCount) { case (_, p) => DataEntry.parse(buf, p) } + parsed.map(_._1) -> parsed.last._2 + } else + List.empty -> (offset + 2) + } + } +} + +case class FunctionCallBytes(index: Int, name: String) extends ByteEntity[Terms.FUNCTION_CALL] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq(ByteEntityDescription(index, name, "EXPR", "F")) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(Terms.FUNCTION_CALL, Int)] = { + Try { + val (expr, remaining) = Serde.deserialize(buf.drop(offset), all = false).explicitGet() + expr.asInstanceOf[FUNCTION_CALL] -> (buf.length - remaining) + } + } +} + +case class AssetIdBytes(index: Int, name: String) extends ByteEntity[AssetId] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq(ByteEntityDescription(index, name, "AssetId (ByteStr = Array[Byte])", AssetIdLength.toString)) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(AssetId, Int)] = { + Try { ByteStr(buf.slice(offset, offset + AssetIdLength)) -> (offset + AssetIdLength) } + } +} + +case class ScriptBytes(index: Int, name: String) extends ByteEntity[Script] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq( + ByteEntityDescription(index, s"$name length (S)", UnimportantType, "2", subIndex = 1), + ByteEntityDescription(index, name, "Script", "S", subIndex = 2) + ) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(Script, Int)] = { + Try { + val scriptLength = Shorts.fromByteArray(buf.slice(offset, offset + 2)) + ScriptReader.fromBytes(buf.slice(offset + 2, offset + 2 + scriptLength)).explicitGet() -> (offset + 2 + scriptLength) + } + } +} + +case class PaymentBytes(index: Int, name: String) extends ByteEntity[Payment] { + + def generateDoc: Seq[ByteEntityDescription] = { + Seq( + ByteEntityDescription(index, s"$name length (P)", UnimportantType, "2", subIndex = 1), + ByteEntityDescription(index, name, "Payment (Long, Option[AssetId])", "P", subIndex = 2) + ) + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(Payment, Int)] = { + Try { + + val paymentLength = Shorts.fromByteArray(buf.slice(offset, offset + 2)) + val arr = buf.slice(offset + 2, offset + 2 + paymentLength) + val amt: Long = Longs.fromByteArray(arr.take(8)) + val (maybeAsset: Option[AssetId], _) = Deser.parseOption(arr, 8)(ByteStr.apply) + + Payment(amt, maybeAsset) -> (offset + 2 + paymentLength) + } + } +} + +/** + * Represents byte description of Option[U] + * + * @param nestedByteEntity describes byte entity of type U + * @param firstByteInterpretation how to interpret first byte + */ +class OptionBytes[U](val index: Int, name: String, nestedByteEntity: ByteEntity[U], firstByteInterpretation: String = "existence flag (1/0)") + extends ByteEntity[Option[U]] { + + def generateDoc: Seq[ByteEntityDescription] = { + ByteEntityDescription(index, s"$name $firstByteInterpretation", UnimportantType, "1", subIndex = 1) +: + nestedByteEntity.generateDoc.map { desc => + desc.copy( + length = desc.length + s"/0 (depends on byte in $index.1)", + subIndex = if (desc.subIndex != 0) desc.subIndex + 1 else desc.subIndex + 2 + ) + } + } + + def deserialize(buf: Array[Byte], offset: Int): Try[(Option[U], Int)] = { + if (buf(offset) == 1) nestedByteEntity.deserialize(buf, offset + 1).map { case (value, offst) => Some(value) -> offst } else + Try { None -> (offset + 1) } + } +} + +object OptionBytes { + def apply[U](index: Int, + name: String, + nestedByteEntity: ByteEntity[U], + firstByteInterpretation: String = "existence flag (1/0)"): ByteEntity[Option[U]] = + new OptionBytes(index, name, nestedByteEntity, firstByteInterpretation) +} + +case class Composition[T1, T2](e1: ByteEntity[T1], e2: ByteEntity[T2]) extends ByteEntity[(T1, T2)] { + + val index: Int = e2.index // use last index in composition + + def generateDoc: Seq[ByteEntityDescription] = e1.generateDoc ++ e2.generateDoc + + def deserialize(buf: Array[Byte], offset: Int): Try[((T1, T2), Int)] = + for { + (value1, offset1) <- e1.deserialize(buf, offset) + (value2, offset2) <- e2.deserialize(buf, offset1) + } yield ((value1, value2), offset2) +} diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransaction.scala index 8b1fced..86eaccc 100644 --- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransaction.scala @@ -1,13 +1,11 @@ package com.zbsnetwork.transaction.lease import com.google.common.primitives.{Bytes, Longs} +import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.crypto +import com.zbsnetwork.transaction.{AssetId, ProvenTransaction, ValidationError, VersionedTransaction} import monix.eval.Coeval import play.api.libs.json.{JsObject, Json} -import com.zbsnetwork.account.PublicKeyAccount -import com.zbsnetwork.common.state.ByteStr -import com.zbsnetwork.transaction.{AssetId, ProvenTransaction, ValidationError, VersionedTransaction} -import com.zbsnetwork.crypto.KeyLength trait LeaseCancelTransaction extends ProvenTransaction with VersionedTransaction { def chainByte: Option[Byte] @@ -31,19 +29,14 @@ object LeaseCancelTransaction { val typeId: Byte = 9 - def validateLeaseCancelParams(leaseId: ByteStr, fee: Long) = + def validateLeaseCancelParams(tx: LeaseCancelTransaction): Either[ValidationError, Unit] = { + validateLeaseCancelParams(tx.leaseId, tx.fee) + } + + def validateLeaseCancelParams(leaseId: ByteStr, fee: Long): Either[ValidationError, Unit] = if (leaseId.arr.length != crypto.DigestSize) { Left(ValidationError.GenericError("Lease transaction id is invalid")) } else if (fee <= 0) { Left(ValidationError.InsufficientFee()) } else Right(()) - - def parseBase(bytes: Array[Byte], start: Int) = { - val sender = PublicKeyAccount(bytes.slice(start, start + KeyLength)) - val fee = Longs.fromByteArray(bytes.slice(start + KeyLength, start + KeyLength + 8)) - val timestamp = Longs.fromByteArray(bytes.slice(start + KeyLength + 8, start + KeyLength + 16)) - val end = start + KeyLength + 16 + crypto.DigestSize - val leaseId = ByteStr(bytes.slice(start + KeyLength + 16, end)) - (sender, fee, timestamp, leaseId, end) - } } diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV1.scala index 3f45a83..ab04e77 100644 --- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV1.scala +++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV1.scala @@ -1,14 +1,16 @@ package com.zbsnetwork.transaction.lease +import cats.implicits._ import com.google.common.primitives.Bytes -import com.zbsnetwork.crypto -import monix.eval.Coeval import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.crypto import com.zbsnetwork.transaction._ -import com.zbsnetwork.crypto._ +import com.zbsnetwork.transaction.description._ +import monix.eval.Coeval -import scala.util.{Failure, Success, Try} +import scala.util.Try case class LeaseCancelTransactionV1 private (sender: PublicKeyAccount, leaseId: ByteStr, fee: Long, timestamp: Long, signature: ByteStr) extends LeaseCancelTransaction @@ -32,13 +34,12 @@ object LeaseCancelTransactionV1 extends TransactionParserFor[LeaseCancelTransact override val typeId: Byte = LeaseCancelTransaction.typeId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val (sender, fee, timestamp, leaseId, end) = LeaseCancelTransaction.parseBase(bytes, 0) - val signature = ByteStr(bytes.slice(end, KeyLength + 16 + crypto.DigestSize + SignatureLength)) - LeaseCancelTransactionV1 - .create(sender, leaseId, fee, timestamp, signature) - .fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + LeaseCancelTransaction + .validateLeaseCancelParams(tx) + .map(_ => tx) + .foldToTry + } } def create(sender: PublicKeyAccount, leaseId: ByteStr, fee: Long, timestamp: Long, signature: ByteStr): Either[ValidationError, TransactionT] = { @@ -58,4 +59,23 @@ object LeaseCancelTransactionV1 extends TransactionParserFor[LeaseCancelTransact def selfSigned(sender: PrivateKeyAccount, leaseId: ByteStr, fee: Long, timestamp: Long): Either[ValidationError, TransactionT] = { signed(sender, leaseId, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[LeaseCancelTransactionV1] = { + ( + PublicKeyAccountBytes(tailIndex(1), "Sender's public key"), + LongBytes(tailIndex(2), "Fee"), + LongBytes(tailIndex(3), "Timestamp"), + ByteStrDefinedLength(tailIndex(4), "Lease ID", crypto.DigestSize), + SignatureBytes(tailIndex(5), "Signature") + ) mapN { + case (sender, fee, timestamp, leaseId, signature) => + LeaseCancelTransactionV1( + sender = sender, + leaseId = leaseId, + fee = fee, + timestamp = timestamp, + signature = signature + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV2.scala index 4fd2641..c9aa264 100644 --- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV2.scala +++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseCancelTransactionV2.scala @@ -1,5 +1,6 @@ package com.zbsnetwork.transaction.lease +import cats.implicits._ import com.google.common.primitives.Bytes import com.zbsnetwork.account.{AddressScheme, PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr @@ -7,9 +8,10 @@ import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto import com.zbsnetwork.transaction.ValidationError.GenericError import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.description._ import monix.eval.Coeval -import scala.util.{Failure, Success, Try} +import scala.util.Try case class LeaseCancelTransactionV2 private (chainId: Byte, sender: PublicKeyAccount, leaseId: ByteStr, fee: Long, timestamp: Long, proofs: Proofs) extends LeaseCancelTransaction @@ -35,14 +37,13 @@ object LeaseCancelTransactionV2 extends TransactionParserFor[LeaseCancelTransact private def currentChainId: Byte = AddressScheme.current.chainId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val chainId = bytes(0) - val (sender, fee, timestamp, leaseId, end) = LeaseCancelTransaction.parseBase(bytes, 1) - (for { - proofs <- Proofs.fromBytes(bytes.drop(end)) - tx <- LeaseCancelTransactionV2.create(chainId, sender, leaseId, fee, timestamp, proofs) - } yield tx).fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + Either + .cond(tx.chainId == currentChainId, (), GenericError(s"Wrong chainId actual: ${tx.chainId.toInt}, expected: $currentChainId")) + .flatMap(_ => LeaseCancelTransaction.validateLeaseCancelParams(tx)) + .map(_ => tx) + .foldToTry + } } def create(chainId: Byte, @@ -71,4 +72,25 @@ object LeaseCancelTransactionV2 extends TransactionParserFor[LeaseCancelTransact def selfSigned(chainId: Byte, sender: PrivateKeyAccount, leaseId: ByteStr, fee: Long, timestamp: Long): Either[ValidationError, TransactionT] = { signed(chainId, sender, leaseId, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[LeaseCancelTransactionV2] = { + ( + OneByte(tailIndex(1), "Chain ID"), + PublicKeyAccountBytes(tailIndex(2), "Sender's public key"), + LongBytes(tailIndex(3), "Fee"), + LongBytes(tailIndex(4), "Timestamp"), + ByteStrDefinedLength(tailIndex(5), "Lease ID", crypto.DigestSize), + ProofsBytes(tailIndex(6)) + ) mapN { + case (chainId, senderPublicKey, fee, timestamp, leaseId, proofs) => + LeaseCancelTransactionV2( + chainId = chainId, + sender = senderPublicKey, + leaseId = leaseId, + fee = fee, + timestamp = timestamp, + proofs = proofs + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransaction.scala index 3b595a5..433f0d3 100644 --- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransaction.scala @@ -1,11 +1,11 @@ package com.zbsnetwork.transaction.lease import com.google.common.primitives.{Bytes, Longs} -import monix.eval.Coeval -import play.api.libs.json.{JsObject, Json} import com.zbsnetwork.account.{Address, AddressOrAlias, PublicKeyAccount} import com.zbsnetwork.transaction.{AssetId, ProvenTransaction, ValidationError, VersionedTransaction} -import com.zbsnetwork.crypto._ +import monix.eval.Coeval +import play.api.libs.json.{JsObject, Json} + import scala.util.Try trait LeaseTransaction extends ProvenTransaction with VersionedTransaction { @@ -36,7 +36,11 @@ object LeaseTransaction { val Canceled = "canceled" } - def validateLeaseParams(amount: Long, fee: Long, recipient: AddressOrAlias, sender: PublicKeyAccount) = + def validateLeaseParams(tx: LeaseTransaction): Either[ValidationError, Unit] = { + validateLeaseParams(tx.amount, tx.fee, tx.recipient, tx.sender) + } + + def validateLeaseParams(amount: Long, fee: Long, recipient: AddressOrAlias, sender: PublicKeyAccount): Either[ValidationError, Unit] = if (amount <= 0) { Left(ValidationError.NegativeAmount(amount, "zbs")) } else if (Try(Math.addExact(amount, fee)).isFailure) { @@ -46,18 +50,4 @@ object LeaseTransaction { } else if (recipient.isInstanceOf[Address] && sender.stringRepr == recipient.stringRepr) { Left(ValidationError.ToSelf) } else Right(()) - - def parseBase(bytes: Array[Byte], start: Int) = { - val sender = PublicKeyAccount(bytes.slice(start, start + KeyLength)) - for { - recRes <- AddressOrAlias.fromBytes(bytes, start + KeyLength) - (recipient, recipientEnd) = recRes - quantityStart = recipientEnd - quantity = Longs.fromByteArray(bytes.slice(quantityStart, quantityStart + 8)) - fee = Longs.fromByteArray(bytes.slice(quantityStart + 8, quantityStart + 16)) - end = quantityStart + 24 - timestamp = Longs.fromByteArray(bytes.slice(quantityStart + 16, end)) - } yield (sender, recipient, quantity, fee, timestamp, end) - } - } diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV1.scala index 1fe35ff..44c2030 100644 --- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV1.scala +++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV1.scala @@ -1,14 +1,16 @@ package com.zbsnetwork.transaction.lease +import cats.implicits._ import com.google.common.primitives.Bytes -import com.zbsnetwork.crypto -import monix.eval.Coeval import com.zbsnetwork.account.{AddressOrAlias, PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.crypto import com.zbsnetwork.transaction._ -import com.zbsnetwork.crypto.SignatureLength +import com.zbsnetwork.transaction.description._ +import monix.eval.Coeval -import scala.util.{Failure, Success, Try} +import scala.util.Try case class LeaseTransactionV1 private (sender: PublicKeyAccount, amount: Long, @@ -32,14 +34,12 @@ object LeaseTransactionV1 extends TransactionParserFor[LeaseTransactionV1] with override val typeId: Byte = LeaseTransaction.typeId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - (for { - parsed <- LeaseTransaction.parseBase(bytes, 0) - (sender, recipient, quantity, fee, timestamp, end) = parsed - signature = ByteStr(bytes.slice(end, end + SignatureLength)) - lt <- LeaseTransactionV1.create(sender, quantity, fee, timestamp, recipient, signature) - } yield lt).fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + LeaseTransaction + .validateLeaseParams(tx) + .map(_ => tx) + .foldToTry + } } def create(sender: PublicKeyAccount, @@ -71,4 +71,25 @@ object LeaseTransactionV1 extends TransactionParserFor[LeaseTransactionV1] with recipient: AddressOrAlias): Either[ValidationError, TransactionT] = { signed(sender, amount, fee, timestamp, recipient, sender) } + + val byteTailDescription: ByteEntity[LeaseTransactionV1] = { + ( + PublicKeyAccountBytes(tailIndex(1), "Sender's public key"), + AddressOrAliasBytes(tailIndex(2), "Recipient"), + LongBytes(tailIndex(3), "Amount"), + LongBytes(tailIndex(4), "Fee"), + LongBytes(tailIndex(5), "Timestamp"), + SignatureBytes(tailIndex(6), "Signature") + ) mapN { + case (sender, recipient, amount, fee, timestamp, signature) => + LeaseTransactionV1( + sender = sender, + amount = amount, + fee = fee, + timestamp = timestamp, + recipient = recipient, + signature = signature + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV2.scala index b01e066..9e7aa4d 100644 --- a/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV2.scala +++ b/src/main/scala/com/zbsnetwork/transaction/lease/LeaseTransactionV2.scala @@ -1,14 +1,17 @@ package com.zbsnetwork.transaction.lease +import cats.implicits._ import com.google.common.primitives.Bytes import com.zbsnetwork.account.{AddressOrAlias, PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto import com.zbsnetwork.serialization.Deser import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.description._ import monix.eval.Coeval -import scala.util.{Either, Failure, Success, Try} +import scala.util.{Either, Try} case class LeaseTransactionV2 private (sender: PublicKeyAccount, amount: Long, fee: Long, timestamp: Long, recipient: AddressOrAlias, proofs: Proofs) extends LeaseTransaction @@ -33,16 +36,14 @@ object LeaseTransactionV2 extends TransactionParserFor[LeaseTransactionV2] with override val typeId: Byte = LeaseTransaction.typeId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val (assetIdOpt, s0) = Deser.parseByteArrayOption(bytes, 0, AssetIdLength) - (for { - _ <- Either.cond(assetIdOpt.isEmpty, (), ValidationError.GenericError("Leasing assets is not supported yet")) - parsed <- LeaseTransaction.parseBase(bytes, s0) - (sender, recipient, quantity, fee, timestamp, end) = parsed - proofs <- Proofs.fromBytes(bytes.drop(end)) - lt <- LeaseTransactionV2.create(sender, quantity, fee, timestamp, recipient, proofs) - } yield lt).fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + val (assetIdOpt, _) = Deser.parseByteArrayOption(bytes, 0, AssetIdLength) + Either + .cond(assetIdOpt.isEmpty, (), ValidationError.GenericError("Leasing assets is not supported yet")) + .flatMap(_ => LeaseTransaction.validateLeaseParams(tx)) + .map(_ => tx) + .foldToTry + } } def create(sender: PublicKeyAccount, @@ -75,4 +76,26 @@ object LeaseTransactionV2 extends TransactionParserFor[LeaseTransactionV2] with recipient: AddressOrAlias): Either[ValidationError, TransactionT] = { signed(sender, amount, fee, timestamp, recipient, sender) } + + val byteTailDescription: ByteEntity[LeaseTransactionV2] = { + ( + OptionBytes(tailIndex(1), "Leasing asset", AssetIdBytes(tailIndex(1), "Leasing asset"), "flag (1 - asset, 0 - Zbs)"), + PublicKeyAccountBytes(tailIndex(2), "Sender's public key"), + AddressOrAliasBytes(tailIndex(3), "Recipient"), + LongBytes(tailIndex(4), "Amount"), + LongBytes(tailIndex(5), "Fee"), + LongBytes(tailIndex(6), "Timestamp"), + ProofsBytes(tailIndex(7)) + ) mapN { + case (_, senderPublicKey, recipient, amount, fee, timestamp, proofs) => + LeaseTransactionV2( + sender = senderPublicKey, + amount = amount, + fee = fee, + timestamp = timestamp, + recipient = recipient, + proofs = proofs + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/ContractInvocationTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/smart/ContractInvocationTransaction.scala index a7147ef..2f32ebf 100644 --- a/src/main/scala/com/zbsnetwork/transaction/smart/ContractInvocationTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/smart/ContractInvocationTransaction.scala @@ -1,23 +1,24 @@ package com.zbsnetwork.transaction.smart +import cats.implicits._ import com.google.common.primitives.{Bytes, Longs} import com.zbsnetwork.account._ import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto -import com.zbsnetwork.crypto.KeyLength -import com.zbsnetwork.lang.v1.Serde import com.zbsnetwork.lang.v1.compiler.Terms -import com.zbsnetwork.lang.v1.compiler.Terms.{EVALUATED, FUNCTION_CALL, REF} +import com.zbsnetwork.lang.v1.compiler.Terms.{EVALUATED, REF} +import com.zbsnetwork.lang.v1.{ContractLimits, Serde} import com.zbsnetwork.serialization.Deser import com.zbsnetwork.transaction.ValidationError.GenericError import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.description._ import com.zbsnetwork.transaction.smart.ContractInvocationTransaction.Payment import com.zbsnetwork.utils.byteStrWrites import monix.eval.Coeval -import play.api.libs.json.{Format, JsObject} +import play.api.libs.json.JsObject -import scala.util.{Failure, Success, Try} +import scala.util.Try case class ContractInvocationTransaction private (chainId: Byte, sender: PublicKeyAccount, @@ -70,8 +71,7 @@ case class ContractInvocationTransaction private (chainId: Byte, object ContractInvocationTransaction extends TransactionParserFor[ContractInvocationTransaction] with TransactionParser.MultipleVersions { - import play.api.libs.json._ - import play.api.libs.json.Json + import play.api.libs.json.{Json, _} case class Payment(amount: Long, assetId: Option[AssetId]) @@ -82,10 +82,10 @@ object ContractInvocationTransaction extends TransactionParserFor[ContractInvoca "function" -> JsString(fc.function.asInstanceOf[com.zbsnetwork.lang.v1.FunctionHeader.User].name), "args" -> JsArray( fc.args.map { - case Terms.CONST_LONG(l) => Json.obj("key" -> "", "type" -> "integer", "value" -> l) - case Terms.CONST_BOOLEAN(l) => Json.obj("key" -> "", "type" -> "boolean", "value" -> l) - case Terms.CONST_BYTESTR(l) => Json.obj("key" -> "", "type" -> "binary", "value" -> l.base64) - case Terms.CONST_STRING(l) => Json.obj("key" -> "", "type" -> "string", "value" -> l) + case Terms.CONST_LONG(l) => Json.obj("type" -> "integer", "value" -> l) + case Terms.CONST_BOOLEAN(l) => Json.obj("type" -> "boolean", "value" -> l) + case Terms.CONST_BYTESTR(l) => Json.obj("type" -> "binary", "value" -> l.base64) + case Terms.CONST_STRING(l) => Json.obj("type" -> "string", "value" -> l) case _ => ??? } ) @@ -95,31 +95,25 @@ object ContractInvocationTransaction extends TransactionParserFor[ContractInvoca override val typeId: Byte = 16 override val supportedVersions: Set[Byte] = Set(1) - private def currentChainId = AddressScheme.current.chainId + private def currentChainId: Byte = AddressScheme.current.chainId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val chainId = bytes(0) - val sender = PublicKeyAccount(bytes.slice(1, KeyLength + 1)) - val contractAddress = Address.fromBytes(bytes.drop(KeyLength + 1).take(Address.AddressLength)).explicitGet() - val fcStart = KeyLength + 1 + Address.AddressLength - val rest = bytes.drop(fcStart) - val (fc, remaining) = Serde.deserialize(rest, all = false).explicitGet() - val paymentFeeTsProofs = rest.takeRight(remaining) - val (payment: Option[(Option[AssetId], Long)], offset) = Deser.parseOption(paymentFeeTsProofs, 0)(arr => { - val amt: Long = Longs.fromByteArray(arr.take(8)) - val (maybeAsset: Option[AssetId], offset) = Deser.parseOption(arr, 8)(ByteStr(_)) - (maybeAsset, amt) - }) - val feeTsProofs = paymentFeeTsProofs.drop(offset) - val fee = Longs.fromByteArray(feeTsProofs.slice(0, 8)) - val timestamp = Longs.fromByteArray(feeTsProofs.slice(8, 16)) - (for { - _ <- Either.cond(chainId == currentChainId, (), GenericError(s"Wrong chainId ${chainId.toInt}")) - proofs <- Proofs.fromBytes(feeTsProofs.drop(16)) - tx <- create(sender, contractAddress, fc.asInstanceOf[FUNCTION_CALL], payment.map(p => Payment(p._2, p._1)), fee, timestamp, proofs) - } yield tx).fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + Either + .cond(tx.chainId == currentChainId, (), GenericError(s"Wrong chainId ${tx.chainId.toInt}")) + .flatMap(_ => Either.cond(tx.fee > 0, (), ValidationError.InsufficientFee(s"insufficient fee: ${tx.fee}"))) + .flatMap(_ => + tx.payment match { + case Some(Payment(amt, token)) => Either.cond(amt > 0, (), ValidationError.NegativeAmount(0, token.toString)) + case _ => Right(()) + }) + .flatMap(_ => + Either.cond(tx.fc.args.forall(x => x.isInstanceOf[EVALUATED] || x == REF("unit")), + (), + GenericError("all arguments of contractInvocation must be EVALUATED"))) + .map(_ => tx) + .foldToTry + } } def create(sender: PublicKeyAccount, @@ -131,6 +125,11 @@ object ContractInvocationTransaction extends TransactionParserFor[ContractInvoca proofs: Proofs): Either[ValidationError, TransactionT] = { for { _ <- Either.cond(fee > 0, (), ValidationError.InsufficientFee(s"insufficient fee: $fee")) + _ <- Either.cond( + fc.args.size <= ContractLimits.MaxContractInvocationArgs, + (), + ValidationError.GenericError(s"ContractInvocation can't have more than ${ContractLimits.MaxContractInvocationArgs} arguments") + ) _ <- p match { case Some(Payment(amt, token)) => Either.cond(amt > 0, (), ValidationError.NegativeAmount(0, token.toString)) case _ => Right(()) @@ -139,7 +138,10 @@ object ContractInvocationTransaction extends TransactionParserFor[ContractInvoca _ <- Either.cond(fc.args.forall(x => x.isInstanceOf[EVALUATED] || x == REF("unit")), (), GenericError("all arguments of contractInvocation must be EVALUATED")) - } yield new ContractInvocationTransaction(currentChainId, sender, contractAddress, fc, p, fee, timestamp, proofs) + tx = new ContractInvocationTransaction(currentChainId, sender, contractAddress, fc, p, fee, timestamp, proofs) + size = tx.bytes().length + _ <- Either.cond(size <= ContractLimits.MaxContractInvocationSizeInBytes, (), ValidationError.TooBigArray) + } yield tx } def signed(sender: PublicKeyAccount, @@ -162,4 +164,17 @@ object ContractInvocationTransaction extends TransactionParserFor[ContractInvoca timestamp: Long): Either[ValidationError, TransactionT] = { signed(sender, contractAddress, fc, p, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[ContractInvocationTransaction] = { + ( + OneByte(tailIndex(1), "Chain ID"), + PublicKeyAccountBytes(tailIndex(2), "Sender's public key"), + AddressBytes(tailIndex(3), "Contract address"), + FunctionCallBytes(tailIndex(4), "Function call"), + OptionBytes(tailIndex(5), "Payment", PaymentBytes(tailIndex(5), "Payment")), + LongBytes(tailIndex(6), "Fee"), + LongBytes(tailIndex(7), "Timestamp"), + ProofsBytes(tailIndex(8)) + ) mapN ContractInvocationTransaction.apply + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/RealTransactionWrapper.scala b/src/main/scala/com/zbsnetwork/transaction/smart/RealTransactionWrapper.scala index a9ad09e..e127adc 100644 --- a/src/main/scala/com/zbsnetwork/transaction/smart/RealTransactionWrapper.scala +++ b/src/main/scala/com/zbsnetwork/transaction/smart/RealTransactionWrapper.scala @@ -48,7 +48,8 @@ object RealTransactionWrapper { expiration = o.expiration, matcherFee = o.matcherFee, bodyBytes = ByteStr(o.bodyBytes()), - proofs = o.proofs.proofs.map(a => ByteStr(a.arr)).toIndexedSeq + proofs = o.proofs.proofs.map(a => ByteStr(a.arr)).toIndexedSeq, + matcherFeeAssetId = o.matcherFeeAssetId ) implicit def aoaToRecipient(aoa: AddressOrAlias): Recipient = aoa match { diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/SetScriptTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/smart/SetScriptTransaction.scala index 04c79aa..b9adbab 100644 --- a/src/main/scala/com/zbsnetwork/transaction/smart/SetScriptTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/smart/SetScriptTransaction.scala @@ -1,19 +1,20 @@ package com.zbsnetwork.transaction.smart +import cats.implicits._ import com.google.common.primitives.{Bytes, Longs} import com.zbsnetwork.account._ import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto -import com.zbsnetwork.crypto.KeyLength import com.zbsnetwork.serialization.Deser import com.zbsnetwork.transaction.ValidationError.GenericError import com.zbsnetwork.transaction._ -import com.zbsnetwork.transaction.smart.script.{Script, ScriptReader} +import com.zbsnetwork.transaction.description._ +import com.zbsnetwork.transaction.smart.script.Script import monix.eval.Coeval import play.api.libs.json.Json -import scala.util.{Failure, Success, Try} +import scala.util.Try case class SetScriptTransaction private (chainId: Byte, sender: PublicKeyAccount, script: Option[Script], fee: Long, timestamp: Long, proofs: Proofs) extends ProvenTransaction @@ -45,29 +46,16 @@ object SetScriptTransaction extends TransactionParserFor[SetScriptTransaction] w override val typeId: Byte = 13 override val supportedVersions: Set[Byte] = Set(1) - private def chainId = AddressScheme.current.chainId + private def chainId: Byte = AddressScheme.current.chainId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val chainId = bytes(0) - val sender = PublicKeyAccount(bytes.slice(1, KeyLength + 1)) - val (scriptOptEi: Option[Either[ValidationError.ScriptParseError, Script]], scriptEnd) = - Deser.parseOption(bytes, KeyLength + 1)(ScriptReader.fromBytes) - val scriptEiOpt = scriptOptEi match { - case None => Right(None) - case Some(Right(sc)) => Right(Some(sc)) - case Some(Left(err)) => Left(err) - } - - lazy val fee = Longs.fromByteArray(bytes.slice(scriptEnd, scriptEnd + 8)) - lazy val timestamp = Longs.fromByteArray(bytes.slice(scriptEnd + 8, scriptEnd + 16)) - (for { - scriptOpt <- scriptEiOpt - _ <- Either.cond(chainId == chainId, (), GenericError(s"Wrong chainId ${chainId.toInt}")) - proofs <- Proofs.fromBytes(bytes.drop(scriptEnd + 16)) - tx <- create(sender, scriptOpt, fee, timestamp, proofs) - } yield tx).fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + Either + .cond(tx.chainId == chainId, (), GenericError(s"Wrong chainId ${tx.chainId.toInt}")) + .flatMap(_ => Either.cond(tx.fee > 0, (), ValidationError.InsufficientFee(s"insufficient fee: ${tx.fee}"))) + .map(_ => tx) + .foldToTry + } } def create(sender: PublicKeyAccount, script: Option[Script], fee: Long, timestamp: Long, proofs: Proofs): Either[ValidationError, TransactionT] = { @@ -89,4 +77,15 @@ object SetScriptTransaction extends TransactionParserFor[SetScriptTransaction] w def selfSigned(sender: PrivateKeyAccount, script: Option[Script], fee: Long, timestamp: Long): Either[ValidationError, TransactionT] = { signed(sender, script, fee, timestamp, sender) } + + val byteTailDescription: ByteEntity[SetScriptTransaction] = { + ( + OneByte(tailIndex(1), "Chain ID"), + PublicKeyAccountBytes(tailIndex(2), "Sender's public key"), + OptionBytes(index = tailIndex(3), name = "Script", nestedByteEntity = ScriptBytes(tailIndex(3), "Script")), + LongBytes(tailIndex(4), "Fee"), + LongBytes(tailIndex(5), "Timestamp"), + ProofsBytes(tailIndex(6)) + ) mapN SetScriptTransaction.apply + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/ContractScript.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/ContractScript.scala index ed7a2f5..5560d93 100644 --- a/src/main/scala/com/zbsnetwork/transaction/smart/script/ContractScript.scala +++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/ContractScript.scala @@ -1,29 +1,32 @@ package com.zbsnetwork.transaction.smart.script import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.crypto -import com.zbsnetwork.lang.ScriptType -import com.zbsnetwork.lang.StdLibVersion.{StdLibVersion, V1} +import com.zbsnetwork.lang.ContentType +import com.zbsnetwork.lang.StdLibVersion.StdLibVersion import com.zbsnetwork.lang.contract.{Contract, ContractSerDe} import com.zbsnetwork.lang.v1.compiler.Terms._ -import com.zbsnetwork.lang.v1.evaluator.FunctionIds.SIGVERIFY import com.zbsnetwork.lang.v1.{FunctionHeader, ScriptEstimator} +import com.zbsnetwork.lang.v1.ContractLimits._ import com.zbsnetwork.transaction.smart.script.v1.ExprScript.checksumLength import com.zbsnetwork.utils.{functionCosts, varNames} import monix.eval.Coeval object ContractScript { - private val maxComplexity = 20 * functionCosts(V1)(FunctionHeader.Native(SIGVERIFY))() + def validateBytes(bs: Array[Byte]): Either[String, Unit] = + Either.cond(bs.length <= MaxContractSizeInBytes, (), s"Script is too large: ${bs.length} bytes > $MaxContractSizeInBytes bytes") def apply(version: StdLibVersion, contract: Contract): Either[String, Script] = { for { funcMaxComplexity <- estimateComplexity(version, contract) _ <- Either.cond( - funcMaxComplexity._2 <= maxComplexity, + funcMaxComplexity._2 <= MaxContractComplexity, (), - s"Contract function (${funcMaxComplexity._1}) is too complex: ${funcMaxComplexity._2} > $maxComplexity" + s"Contract function (${funcMaxComplexity._1}) is too complex: ${funcMaxComplexity._2} > $MaxContractComplexity" ) - s = new ContractScriptImpl(version, contract, funcMaxComplexity._2) + s = ContractScriptImpl(version, contract, funcMaxComplexity._2) + _ <- validateBytes(s.bytes().arr) + } yield s } @@ -32,7 +35,7 @@ object ContractScript { override type Expr = Contract override val bytes: Coeval[ByteStr] = Coeval.evalOnce { - val s = Array(0: Byte, ScriptType.Contract.toByte, stdLibVersion.toByte) ++ ContractSerDe.serialize(expr) + val s = Array(0: Byte, ContentType.Contract.toByte, stdLibVersion.toByte) ++ ContractSerDe.serialize(expr) ByteStr(s ++ crypto.secureHash(s).take(checksumLength)) } override val containsBlockV2: Coeval[Boolean] = Coeval.evalOnce(true) @@ -45,15 +48,15 @@ object ContractScript { (contract.cfs.map(func => (func.annotation.invocationArgName, func.u)) ++ contract.vf.map(func => (func.annotation.invocationArgName, func.u))) .map { case (annotationArgName, funcExpr) => - ScriptEstimator(varNames(version), functionCosts(version), constructExprFromFuncAndContex(contract.dec, annotationArgName, funcExpr)) + ScriptEstimator(varNames(version), functionCosts(version), constructExprFromFuncAndContext(contract.dec, annotationArgName, funcExpr)) .map(complexity => (funcExpr.name, complexity)) } val funcsWithComplexityEi: E[Vector[(String, Long)]] = funcsWithComplexity.toVector.sequence - funcsWithComplexityEi.map(namesAndComp => namesAndComp.maxBy(_._2)) + funcsWithComplexityEi.map(namesAndComp => (("", 0L) +: namesAndComp).maxBy(_._2)) } - private def constructExprFromFuncAndContex(dec: List[DECLARATION], annotationArgName: String, funcExpr: FUNC): EXPR = { + private def constructExprFromFuncAndContext(dec: List[DECLARATION], annotationArgName: String, funcExpr: FUNC): EXPR = { val funcWithAnnotationContext = BLOCK( LET(annotationArgName, TRUE), diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/Script.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/Script.scala index 996b04f..0cb7891 100644 --- a/src/main/scala/com/zbsnetwork/transaction/smart/script/Script.scala +++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/Script.scala @@ -5,8 +5,8 @@ import com.zbsnetwork.common.utils.Base64 import com.zbsnetwork.lang.StdLibVersion._ import com.zbsnetwork.lang.v1.compiler.Decompiler import com.zbsnetwork.transaction.ValidationError.ScriptParseError -import com.zbsnetwork.transaction.smart.script.v1.ExprScript.ExprScriprImpl import monix.eval.Coeval +import com.zbsnetwork.transaction.smart.script.v1.ExprScript trait Script { type Expr @@ -39,7 +39,7 @@ object Script { } yield script def decompile(s: Script): String = s match { - case ExprScriprImpl(_, expr, _) => Decompiler(expr, com.zbsnetwork.utils.defaultDecompilerContext) + case e: ExprScript => Decompiler(e.expr, com.zbsnetwork.utils.defaultDecompilerContext) case com.zbsnetwork.transaction.smart.script.ContractScript.ContractScriptImpl(_, contract, _) => Decompiler(contract, com.zbsnetwork.utils.defaultDecompilerContext) } diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptCompiler.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptCompiler.scala index 2bcab3f..80f31c5 100644 --- a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptCompiler.scala +++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptCompiler.scala @@ -1,47 +1,41 @@ package com.zbsnetwork.transaction.smart.script -import com.zbsnetwork.lang.ScriptType.ScriptType +import com.zbsnetwork.lang.ContentType.ContentType import com.zbsnetwork.lang.StdLibVersion.StdLibVersion import com.zbsnetwork.lang.directives.DirectiveParser import com.zbsnetwork.lang.utils._ import com.zbsnetwork.lang.v1.ScriptEstimator import com.zbsnetwork.lang.v1.compiler.{ContractCompiler, ExpressionCompiler} -import com.zbsnetwork.lang.v1.parser.Parser -import com.zbsnetwork.lang.{ScriptType, StdLibVersion} +import com.zbsnetwork.lang.{ContentType, ScriptType} import com.zbsnetwork.transaction.smart.script.ContractScript._ import com.zbsnetwork.transaction.smart.script.v1.ExprScript -import com.zbsnetwork.transaction.smart.script.v1.ExprScript.ExprScriprImpl import com.zbsnetwork.utils._ object ScriptCompiler extends ScorexLogging { - def contract(scriptText: String): Either[String, Script] = { - val ctx = compilerContext(StdLibVersion.V3, isAssetScript = false) - ContractCompiler(ctx, Parser.parseContract(scriptText).get.value) - .flatMap(s => ContractScript(StdLibVersion.V3, s)) - } - + @Deprecated def apply(scriptText: String, isAssetScript: Boolean): Either[String, (Script, Long)] = { val directives = DirectiveParser(scriptText) - - val scriptWithoutDirectives = - scriptText.linesIterator - .filter(str => !str.contains("{-#")) - .mkString("\n") - for { ver <- extractStdLibVersion(directives) - tpe <- extractScriptType(directives) - script <- tryCompile(scriptWithoutDirectives, tpe, ver, isAssetScript) + tpe <- extractContentType(directives) + script <- tryCompile(scriptText, tpe, ver, isAssetScript) } yield (script, script.complexity) } - def tryCompile(src: String, tpe: ScriptType, version: StdLibVersion, isAssetScript: Boolean): Either[String, Script] = { + def compile(scriptText: String): Either[String, (Script, Long)] = { + for { + scriptType <- extractScriptType(DirectiveParser(scriptText)) + result <- apply(scriptText, scriptType == ScriptType.Asset) + } yield result + } + + private def tryCompile(src: String, tpe: ContentType, version: StdLibVersion, isAssetScript: Boolean): Either[String, Script] = { val ctx = compilerContext(version, isAssetScript) try { tpe match { - case ScriptType.Expression => ExpressionCompiler.compile(src, ctx).flatMap(expr => ExprScript.apply(version, expr)) - case ScriptType.Contract => ContractCompiler.compile(src, ctx).flatMap(expr => ContractScript.apply(version, expr)) + case ContentType.Expression => ExpressionCompiler.compile(src, ctx).flatMap(expr => ExprScript.apply(version, expr)) + case ContentType.Contract => ContractCompiler.compile(src, ctx).flatMap(expr => ContractScript.apply(version, expr)) } } catch { case ex: Throwable => @@ -53,7 +47,7 @@ object ScriptCompiler extends ScorexLogging { } def estimate(script: Script, version: StdLibVersion): Either[String, Long] = script match { - case s: ExprScriprImpl => ScriptEstimator(varNames(version), functionCosts(version), s.expr) + case s: ExprScript => ScriptEstimator(varNames(version), functionCosts(version), s.expr) case s: ContractScriptImpl => ContractScript.estimateComplexity(version, s.expr).map(_._2) case _ => ??? } diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptReader.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptReader.scala index 0744521..9378293 100644 --- a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptReader.scala +++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptReader.scala @@ -3,7 +3,7 @@ package com.zbsnetwork.transaction.smart.script import com.zbsnetwork.crypto import com.zbsnetwork.lang.contract.ContractSerDe import com.zbsnetwork.lang.v1.Serde -import com.zbsnetwork.lang.{ScriptType, StdLibVersion} +import com.zbsnetwork.lang.{ContentType, StdLibVersion} import com.zbsnetwork.transaction.ValidationError.ScriptParseError import com.zbsnetwork.transaction.smart.script.v1._ @@ -15,24 +15,26 @@ object ScriptReader { val checkSum = bytes.takeRight(checksumLength) val computedCheckSum = crypto.secureHash(bytes.dropRight(checksumLength)).take(checksumLength) val versionByte: Byte = bytes.head - val (scriptType, stdLibVersion, offset) = - if (versionByte == 0) - (ScriptType.parseVersion(bytes(1)), StdLibVersion.parseVersion(bytes(2)), 3) - else if (versionByte == StdLibVersion.V1.toByte || versionByte == StdLibVersion.V2.toByte) - (ScriptType.Expression, StdLibVersion(versionByte.toInt), 1) - else ??? - val scriptBytes = bytes.drop(offset).dropRight(checksumLength) - (for { + a <- { + if (versionByte == 0) + Right((ContentType.parseId(bytes(1)), StdLibVersion.parseVersion(bytes(2)), 3)) + else if (versionByte == StdLibVersion.V1.toByte || versionByte == StdLibVersion.V2.toByte) + Right((ContentType.Expression, StdLibVersion(versionByte.toInt), 1)) + else Left(ScriptParseError(s"Can't parse script bytes starting with [${bytes(0).toInt},${bytes(1).toInt},${bytes(2).toInt}]")) + } + (scriptType, stdLibVersion, offset) = a + scriptBytes = bytes.drop(offset).dropRight(checksumLength) + _ <- Either.cond(checkSum.sameElements(computedCheckSum), (), ScriptParseError("Invalid checksum")) s <- scriptType match { - case ScriptType.Expression => + case ContentType.Expression => for { _ <- ExprScript.validateBytes(scriptBytes) bytes <- Serde.deserialize(scriptBytes).map(_._1) s <- ExprScript(stdLibVersion, bytes, checkSize = false) } yield s - case ScriptType.Contract => + case ContentType.Contract => for { bytes <- ContractSerDe.deserialize(scriptBytes) s <- ContractScript(stdLibVersion, bytes) diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptRunner.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptRunner.scala index 4e7353e..cb0e9fe 100644 --- a/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptRunner.scala +++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/ScriptRunner.scala @@ -2,16 +2,14 @@ package com.zbsnetwork.transaction.smart.script import cats.implicits._ import com.zbsnetwork.account.AddressScheme -import com.zbsnetwork.lang.v1.compiler.Terms.EVALUATED -import com.zbsnetwork.lang.v1.evaluator.EvaluatorV1 import com.zbsnetwork.lang._ import com.zbsnetwork.lang.contract.Contract -import com.zbsnetwork.lang.v1.evaluator._ -import com.zbsnetwork.lang.v1.compiler.Terms.{FALSE, TRUE} +import com.zbsnetwork.lang.v1.compiler.Terms.{EVALUATED, FALSE, TRUE} +import com.zbsnetwork.lang.v1.evaluator.{EvaluatorV1, _} import com.zbsnetwork.state._ -import com.zbsnetwork.transaction.{Authorized, Proven} +import com.zbsnetwork.transaction.smart.script.v1.ExprScript import com.zbsnetwork.transaction.smart.{BlockchainContext, RealTransactionWrapper, Verifier} -import com.zbsnetwork.transaction.smart.script.v1.ExprScript.ExprScriprImpl +import com.zbsnetwork.transaction.{Authorized, Proven} import monix.eval.Coeval object ScriptRunner { @@ -19,7 +17,7 @@ object ScriptRunner { def apply(height: Int, in: TxOrd, blockchain: Blockchain, script: Script, isTokenScript: Boolean): (Log, Either[ExecutionError, EVALUATED]) = { script match { - case s: ExprScriprImpl => + case s: ExprScript => val ctx = BlockchainContext.build( script.stdLibVersion, AddressScheme.current.chainId, diff --git a/src/main/scala/com/zbsnetwork/transaction/smart/script/v1/ExprScript.scala b/src/main/scala/com/zbsnetwork/transaction/smart/script/v1/ExprScript.scala index fa9e911..992e4be 100644 --- a/src/main/scala/com/zbsnetwork/transaction/smart/script/v1/ExprScript.scala +++ b/src/main/scala/com/zbsnetwork/transaction/smart/script/v1/ExprScript.scala @@ -3,9 +3,9 @@ package com.zbsnetwork.transaction.smart.script.v1 import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.crypto import com.zbsnetwork.lang.StdLibVersion._ +import com.zbsnetwork.lang.v1.ContractLimits._ import com.zbsnetwork.lang.v1.compiler.Terms._ -import com.zbsnetwork.lang.v1.evaluator.FunctionIds._ -import com.zbsnetwork.lang.v1.{FunctionHeader, ScriptEstimator, Serde} +import com.zbsnetwork.lang.v1.{ScriptEstimator, Serde} import com.zbsnetwork.transaction.smart.script.Script import com.zbsnetwork.utils.{functionCosts, varNames} import monix.eval.Coeval @@ -14,24 +14,22 @@ import scala.annotation.tailrec import scala.collection.mutable._ object ExprScript { - val checksumLength = 4 - private val maxComplexity = 20 * functionCosts(V1)(FunctionHeader.Native(SIGVERIFY))() - private val maxSizeInBytes = 8 * 1024 + val checksumLength = 4 def validateBytes(bs: Array[Byte]): Either[String, Unit] = - Either.cond(bs.length <= maxSizeInBytes, (), s"Script is too large: ${bs.length} bytes > $maxSizeInBytes bytes") + Either.cond(bs.length <= MaxExprSizeInBytes, (), s"Script is too large: ${bs.length} bytes > $MaxExprSizeInBytes bytes") def apply(x: EXPR): Either[String, Script] = apply(V1, x) def apply(version: StdLibVersion, x: EXPR, checkSize: Boolean = true): Either[String, Script] = for { scriptComplexity <- ScriptEstimator(varNames(version), functionCosts(version), x) - _ <- Either.cond(scriptComplexity <= maxComplexity, (), s"Script is too complex: $scriptComplexity > $maxComplexity") - s = new ExprScriprImpl(version, x, scriptComplexity) + _ <- Either.cond(scriptComplexity <= MaxExprComplexity, (), s"Script is too complex: $scriptComplexity > $MaxExprComplexity") + s = new ExprScriptImpl(version, x, scriptComplexity) _ <- if (checkSize) validateBytes(s.bytes().arr) else Right(()) } yield s - case class ExprScriprImpl(stdLibVersion: StdLibVersion, expr: EXPR, complexity: Long) extends Script { + private case class ExprScriptImpl(stdLibVersion: StdLibVersion, expr: EXPR, complexity: Long) extends ExprScript { override type Expr = EXPR override val bytes: Coeval[ByteStr] = @@ -61,3 +59,10 @@ object ExprScript { horTraversal(Queue(e)) } } + +trait ExprScript extends Script { + override type Expr = EXPR + val stdLibVersion: StdLibVersion + val expr: EXPR + val complexity: Long +} diff --git a/src/main/scala/com/zbsnetwork/transaction/transfer/MassTransferTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/transfer/MassTransferTransaction.scala index dd852ca..1e131f7 100644 --- a/src/main/scala/com/zbsnetwork/transaction/transfer/MassTransferTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/transfer/MassTransferTransaction.scala @@ -6,17 +6,17 @@ import com.zbsnetwork.account.{AddressOrAlias, PrivateKeyAccount, PublicKeyAccou import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.{Base58, EitherExt2} import com.zbsnetwork.crypto -import com.zbsnetwork.crypto._ import com.zbsnetwork.serialization.Deser import com.zbsnetwork.transaction.ValidationError.Validation import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.description._ import com.zbsnetwork.transaction.transfer.MassTransferTransaction.{ParsedTransfer, toJson} import io.swagger.annotations.{ApiModel, ApiModelProperty} import monix.eval.Coeval import play.api.libs.json.{Format, JsObject, JsValue, Json} import scala.annotation.meta.field -import scala.util.{Either, Failure, Success, Try} +import scala.util.{Either, Try} case class MassTransferTransaction private (assetId: Option[AssetId], sender: PublicKeyAccount, @@ -49,6 +49,7 @@ case class MassTransferTransaction private (assetId: Option[AssetId], Deser.serializeArray(attachment) ) } + override val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(Bytes.concat(bodyBytes(), proofs.bytes())) override val assetFee: (Option[AssetId], Long) = (None, fee) @@ -90,34 +91,25 @@ object MassTransferTransaction extends TransactionParserFor[MassTransferTransact implicit val transferFormat: Format[Transfer] = Json.format override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val sender = PublicKeyAccount(bytes.slice(0, KeyLength)) - val (assetIdOpt, s0) = Deser.parseByteArrayOption(bytes, KeyLength, AssetIdLength) - val transferCount = Shorts.fromByteArray(bytes.slice(s0, s0 + 2)) - - def readTransfer(offset: Int): (Validation[ParsedTransfer], Int) = { - AddressOrAlias.fromBytes(bytes, offset) match { - case Right((addr, ofs)) => - val amount = Longs.fromByteArray(bytes.slice(ofs, ofs + 8)) - (Right[ValidationError, ParsedTransfer](ParsedTransfer(addr, amount)), ofs + 8) - case Left(e) => (Left(e), offset) - } - } - - val transfersList: List[(Validation[ParsedTransfer], Int)] = - List.iterate(readTransfer(s0 + 2), transferCount) { case (_, offset) => readTransfer(offset) } - - val s1 = transfersList.lastOption.map(_._2).getOrElse(s0 + 2) - val tx: Validation[MassTransferTransaction] = for { - transfers <- transfersList.map { case (ei, _) => ei }.sequence - timestamp = Longs.fromByteArray(bytes.slice(s1, s1 + 8)) - feeAmount = Longs.fromByteArray(bytes.slice(s1 + 8, s1 + 16)) - (attachment, attachEnd) = Deser.parseArraySize(bytes, s1 + 16) - proofs <- Proofs.fromBytes(bytes.drop(attachEnd)) - mtt <- MassTransferTransaction.create(assetIdOpt.map(ByteStr(_)), sender, transfers, timestamp, feeAmount, attachment, proofs) - } yield mtt - tx.fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + Try { tx.transfers.map(_.amount).fold(tx.fee)(Math.addExact) } + .fold( + ex => Left(ValidationError.OverflowError), + totalAmount => + if (tx.transfers.lengthCompare(MaxTransferCount) > 0) { + Left(ValidationError.GenericError(s"Number of transfers ${tx.transfers.length} is greater than $MaxTransferCount")) + } else if (tx.transfers.exists(_.amount < 0)) { + Left(ValidationError.GenericError("One of the transfers has negative amount")) + } else if (tx.attachment.length > TransferTransaction.MaxAttachmentSize) { + Left(ValidationError.TooBigArray) + } else if (tx.fee <= 0) { + Left(ValidationError.InsufficientFee()) + } else { + Right(tx) + } + ) + .foldToTry + } } def create(assetId: Option[AssetId], @@ -177,4 +169,27 @@ object MassTransferTransaction extends TransactionParserFor[MassTransferTransact private def toJson(transfers: List[ParsedTransfer]): JsValue = { Json.toJson(transfers.map { case ParsedTransfer(address, amount) => Transfer(address.stringRepr, amount) }) } + + val byteTailDescription: ByteEntity[MassTransferTransaction] = { + ( + PublicKeyAccountBytes(tailIndex(1), "Sender's public key"), + OptionBytes(index = tailIndex(2), name = "Asset ID", nestedByteEntity = AssetIdBytes(tailIndex(2), "Asset ID")), + TransfersBytes(tailIndex(3)), + LongBytes(tailIndex(4), "Timestamp"), + LongBytes(tailIndex(5), "Fee"), + BytesArrayUndefinedLength(tailIndex(6), "Attachments"), + ProofsBytes(tailIndex(7)) + ) mapN { + case (sender, assetId, transfer, timestamp, fee, attachment, proofs) => + MassTransferTransaction( + assetId = assetId, + sender = sender, + transfers = transfer, + timestamp = timestamp, + fee = fee, + attachment = attachment, + proofs = proofs + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransaction.scala b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransaction.scala index bd5b11a..bbb0032 100644 --- a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransaction.scala +++ b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransaction.scala @@ -2,7 +2,7 @@ package com.zbsnetwork.transaction.transfer import cats.implicits._ import com.google.common.primitives.{Bytes, Longs} -import com.zbsnetwork.account.{AddressOrAlias, PublicKeyAccount} +import com.zbsnetwork.account.AddressOrAlias import com.zbsnetwork.common.utils.Base58 import com.zbsnetwork.serialization.Deser import com.zbsnetwork.transaction._ @@ -10,7 +10,6 @@ import com.zbsnetwork.transaction.validation._ import com.zbsnetwork.utils.base58Length import monix.eval.Coeval import play.api.libs.json.{JsObject, Json} -import com.zbsnetwork.crypto._ trait TransferTransaction extends ProvenTransaction with VersionedTransaction { def assetId: Option[AssetId] @@ -62,30 +61,21 @@ object TransferTransaction { val MaxAttachmentSize = 140 val MaxAttachmentStringSize: Int = base58Length(MaxAttachmentSize) - def validate(amount: Long, feeAmount: Long, attachment: Array[Byte]): Either[ValidationError, Unit] = { + def validate(tx: TransferTransaction): Either[ValidationError, Unit] = { + validate(tx.amount, tx.assetId, tx.fee, tx.feeAssetId, tx.attachment) + } + + def validate(amt: Long, + maybeAmtAsset: Option[AssetId], + feeAmt: Long, + maybeFeeAsset: Option[AssetId], + attachment: Array[Byte]): Either[ValidationError, Unit] = { ( - validateAmount(amount, "zbs"), - validateFee(feeAmount), - validateAttachment(attachment), - validateSum(Seq(amount, feeAmount)) + validateAmount(amt, maybeAmtAsset.map(_.base58).getOrElse("zbs")), + validateFee(feeAmt), + validateAttachment(attachment) ).mapN { case _ => () } .toEither .leftMap(_.head) } - - def parseBase(bytes: Array[Byte], start: Int) = { - val sender = PublicKeyAccount(bytes.slice(start, start + KeyLength)) - val (assetIdOpt, s0) = Deser.parseByteArrayOption(bytes, start + KeyLength, AssetIdLength) - val (feeAssetIdOpt, s1) = Deser.parseByteArrayOption(bytes, s0, AssetIdLength) - val timestamp = Longs.fromByteArray(bytes.slice(s1, s1 + 8)) - val amount = Longs.fromByteArray(bytes.slice(s1 + 8, s1 + 16)) - val feeAmount = Longs.fromByteArray(bytes.slice(s1 + 16, s1 + 24)) - for { - recRes <- AddressOrAlias.fromBytes(bytes, s1 + 24) - (recipient, recipientEnd) = recRes - (attachment, end) = Deser.parseArraySize(bytes, recipientEnd) - } yield (sender, assetIdOpt, feeAssetIdOpt, timestamp, amount, feeAmount, recipient, attachment, end) - - } - } diff --git a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV1.scala b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV1.scala index 0dfe547..a64957d 100644 --- a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV1.scala +++ b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV1.scala @@ -1,14 +1,16 @@ package com.zbsnetwork.transaction.transfer +import cats.implicits._ import com.google.common.primitives.Bytes import com.zbsnetwork.account.{AddressOrAlias, PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.description._ import monix.eval.Coeval -import com.zbsnetwork.crypto._ -import scala.util.{Failure, Success, Try} +import scala.util.Try case class TransferTransactionV1 private (assetId: Option[AssetId], sender: PublicKeyAccount, @@ -34,25 +36,12 @@ object TransferTransactionV1 extends TransactionParserFor[TransferTransactionV1] override val typeId: Byte = TransferTransaction.typeId override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - val signature = ByteStr(bytes.slice(0, SignatureLength)) - val txId = bytes(SignatureLength) - require(txId == typeId, s"Signed tx id is not match") - - (for { - parsed <- TransferTransaction.parseBase(bytes, SignatureLength + 1) - (sender, assetIdOpt, feeAssetIdOpt, timestamp, amount, feeAmount, recipient, attachment, _) = parsed - tt <- TransferTransactionV1.create(assetIdOpt.map(ByteStr(_)), - sender, - recipient, - amount, - timestamp, - feeAssetIdOpt.map(ByteStr(_)), - feeAmount, - attachment, - signature) - } yield tt).fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + TransferTransaction + .validate(tx) + .map(_ => tx) + .foldToTry + } } def create(assetId: Option[AssetId], @@ -65,7 +54,7 @@ object TransferTransactionV1 extends TransactionParserFor[TransferTransactionV1] attachment: Array[Byte], signature: ByteStr): Either[ValidationError, TransactionT] = { TransferTransaction - .validate(amount, feeAmount, attachment) + .validate(amount, assetId, feeAmount, feeAssetId, attachment) .map(_ => TransferTransactionV1(assetId, sender, recipient, amount, timestamp, feeAssetId, feeAmount, attachment, signature)) } @@ -93,4 +82,33 @@ object TransferTransactionV1 extends TransactionParserFor[TransferTransactionV1] attachment: Array[Byte]): Either[ValidationError, TransactionT] = { signed(assetId, sender, recipient, amount, timestamp, feeAssetId, feeAmount, attachment, sender) } + + val byteTailDescription: ByteEntity[TransferTransactionV1] = { + ( + SignatureBytes(tailIndex(1), "Signature"), + ConstantByte(tailIndex(2), value = typeId, name = "Transaction type"), + PublicKeyAccountBytes(tailIndex(3), "Sender's public key"), + OptionBytes[AssetId](tailIndex(4), "Asset ID", AssetIdBytes(tailIndex(4), "Asset ID"), "flag (1 - asset, 0 - Zbs)"), + OptionBytes[AssetId](tailIndex(5), "Fee's asset ID", AssetIdBytes(tailIndex(5), "Fee's asset ID"), "flag (1 - asset, 0 - Zbs)"), + LongBytes(tailIndex(6), "Timestamp"), + LongBytes(tailIndex(7), "Amount"), + LongBytes(tailIndex(8), "Fee"), + AddressOrAliasBytes(tailIndex(9), "Recipient"), + BytesArrayUndefinedLength(tailIndex(10), "Attachment") + ) mapN { + case (signature, txId, senderPublicKey, assetId, feeAssetId, timestamp, amount, fee, recipient, attachments) => + require(txId == typeId, s"Signed tx id is not match") + TransferTransactionV1( + assetId = assetId, + sender = senderPublicKey, + recipient = recipient, + amount = amount, + timestamp = timestamp, + feeAssetId = feeAssetId, + fee = fee, + attachment = attachments, + signature = signature + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV2.scala b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV2.scala index fde0103..558a3ae 100644 --- a/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV2.scala +++ b/src/main/scala/com/zbsnetwork/transaction/transfer/TransferTransactionV2.scala @@ -1,14 +1,16 @@ package com.zbsnetwork.transaction.transfer +import cats.implicits._ import com.google.common.primitives.Bytes import com.zbsnetwork.account.{AddressOrAlias, PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.crypto import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.description._ import monix.eval.Coeval -import scala.util.{Failure, Success, Try} +import scala.util.Try case class TransferTransactionV2 private (sender: PublicKeyAccount, recipient: AddressOrAlias, @@ -26,7 +28,8 @@ case class TransferTransactionV2 private (sender: PublicKeyAccount, override val builder: TransactionParser = TransferTransactionV2 override val bodyBytes: Coeval[Array[Byte]] = Coeval.evalOnce(Array(builder.typeId, version) ++ bytesBase()) override val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(Bytes.concat(Array(0: Byte), bodyBytes(), proofs.bytes())) - override def version: Byte = 2 + + override def version: Byte = 2 } object TransferTransactionV2 extends TransactionParserFor[TransferTransactionV2] with TransactionParser.MultipleVersions { @@ -35,22 +38,12 @@ object TransferTransactionV2 extends TransactionParserFor[TransferTransactionV2] override val supportedVersions: Set[Byte] = Set(2) override protected def parseTail(bytes: Array[Byte]): Try[TransactionT] = { - Try { - (for { - parsed <- TransferTransaction.parseBase(bytes, 0) - (sender, assetIdOpt, feeAssetIdOpt, timestamp, amount, feeAmount, recipient, attachment, end) = parsed - proofs <- Proofs.fromBytes(bytes.drop(end)) - tt <- TransferTransactionV2.create(assetIdOpt.map(ByteStr(_)), - sender, - recipient, - amount, - timestamp, - feeAssetIdOpt.map(ByteStr(_)), - feeAmount, - attachment, - proofs) - } yield tt).fold(left => Failure(new Exception(left.toString)), right => Success(right)) - }.flatten + byteTailDescription.deserializeFromByteArray(bytes).flatMap { tx => + TransferTransaction + .validate(tx) + .map(_ => tx) + .foldToTry + } } def create(assetId: Option[AssetId], @@ -63,7 +56,7 @@ object TransferTransactionV2 extends TransactionParserFor[TransferTransactionV2] attachment: Array[Byte], proofs: Proofs): Either[ValidationError, TransactionT] = { for { - _ <- TransferTransaction.validate(amount, feeAmount, attachment) + _ <- TransferTransaction.validate(amount, assetId, feeAmount, feeAssetId, attachment) } yield TransferTransactionV2(sender, recipient, assetId, amount, timestamp, feeAssetId, feeAmount, attachment, proofs) } @@ -91,4 +84,31 @@ object TransferTransactionV2 extends TransactionParserFor[TransferTransactionV2] attachment: Array[Byte]): Either[ValidationError, TransactionT] = { signed(assetId, sender, recipient, amount, timestamp, feeAssetId, feeAmount, attachment, sender) } + + val byteTailDescription: ByteEntity[TransferTransactionV2] = { + ( + PublicKeyAccountBytes(tailIndex(1), "Sender's public key"), + OptionBytes(tailIndex(2), "Asset ID", AssetIdBytes(tailIndex(2), "Asset ID"), "flag (1 - asset, 0 - Zbs)"), + OptionBytes(tailIndex(3), "Fee's asset ID", AssetIdBytes(tailIndex(3), "Fee's asset ID"), "flag (1 - asset, 0 - Zbs)"), + LongBytes(tailIndex(4), "Timestamp"), + LongBytes(tailIndex(5), "Amount"), + LongBytes(tailIndex(6), "Fee"), + AddressOrAliasBytes(tailIndex(7), "Recipient"), + BytesArrayUndefinedLength(tailIndex(8), "Attachment"), + ProofsBytes(tailIndex(9)) + ) mapN { + case (senderPublicKey, assetId, feeAssetId, timestamp, amount, fee, recipient, attachments, proofs) => + TransferTransactionV2( + sender = senderPublicKey, + recipient = recipient, + assetId = assetId, + amount = amount, + timestamp = timestamp, + feeAssetId = feeAssetId, + fee = fee, + attachment = attachments, + proofs = proofs + ) + } + } } diff --git a/src/main/scala/com/zbsnetwork/transaction/validation/package.scala b/src/main/scala/com/zbsnetwork/transaction/validation/package.scala index 0c6e516..0869a8a 100644 --- a/src/main/scala/com/zbsnetwork/transaction/validation/package.scala +++ b/src/main/scala/com/zbsnetwork/transaction/validation/package.scala @@ -26,7 +26,7 @@ package object validation { ) } - def validateAmount(amount: Long, of: String): Validated[Long] = { + def validateAmount(amount: Long, of: => String): Validated[Long] = { Validated .condNel( amount > 0, diff --git a/src/main/scala/com/zbsnetwork/utils/EmptyBlockchain.scala b/src/main/scala/com/zbsnetwork/utils/EmptyBlockchain.scala index fe43888..41aba63 100644 --- a/src/main/scala/com/zbsnetwork/utils/EmptyBlockchain.scala +++ b/src/main/scala/com/zbsnetwork/utils/EmptyBlockchain.scala @@ -88,7 +88,7 @@ object EmptyBlockchain extends Blockchain { override def assetDistribution(assetId: ByteStr): AssetDistribution = Monoid.empty[AssetDistribution] - override def zbsDistribution(height: Int): Map[Address, Long] = Map.empty + override def zbsDistribution(height: Int): Either[ValidationError, Map[Address, Long]] = Right(Map.empty) override def allActiveLeases: Set[LeaseTransaction] = Set.empty diff --git a/src/main/scala/com/zbsnetwork/utils/package.scala b/src/main/scala/com/zbsnetwork/utils/package.scala index c0c808e..53c9874 100644 --- a/src/main/scala/com/zbsnetwork/utils/package.scala +++ b/src/main/scala/com/zbsnetwork/utils/package.scala @@ -7,7 +7,6 @@ import com.google.common.base.Throwables import com.zbsnetwork.account.AddressScheme import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.state.ByteStr._ -import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.db.{Storage, VersionedStorage} import com.zbsnetwork.lang.Global import com.zbsnetwork.lang.StdLibVersion._ @@ -15,7 +14,7 @@ import com.zbsnetwork.lang.v1.compiler.{CompilerContext, DecompilerContext} import com.zbsnetwork.lang.v1.evaluator.ctx._ import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext} -import com.zbsnetwork.lang.v1.{CTX, FunctionHeader, ScriptEstimator} +import com.zbsnetwork.lang.v1.{CTX, FunctionHeader} import com.zbsnetwork.transaction.smart.ZbsEnvironment import monix.eval.Coeval import monix.execution.UncaughtExceptionReporter @@ -123,11 +122,11 @@ package object utils extends ScorexLogging { def dummyEvalContext(version: StdLibVersion): EvaluationContext = lazyContexts(version)().evaluationContext private val lazyFunctionCosts: Map[StdLibVersion, Coeval[Map[FunctionHeader, Coeval[Long]]]] = - lazyContexts.mapValues(_.map(ctx => estimate(ctx.evaluationContext))) + lazyContexts.map(el => (el._1, el._2.map(ctx => estimate(el._1, ctx.evaluationContext)))) def functionCosts(version: StdLibVersion): Map[FunctionHeader, Coeval[Long]] = lazyFunctionCosts(version)() - def estimate(ctx: EvaluationContext): Map[FunctionHeader, Coeval[Long]] = { + def estimate(version: StdLibVersion, ctx: EvaluationContext): Map[FunctionHeader, Coeval[Long]] = { val costs: mutable.Map[FunctionHeader, Coeval[Long]] = ctx.typeDefs.collect { case (typeName, CaseType(_, fields)) => FunctionHeader.User(typeName) -> Coeval.now(fields.size.toLong) }(collection.breakOut) @@ -135,11 +134,10 @@ package object utils extends ScorexLogging { ctx.functions.values.foreach { func => val cost = func match { case f: UserFunction => - import f.signature.args - Coeval.evalOnce(ScriptEstimator(ctx.letDefs.keySet ++ args.map(_._1), costs, f.ev).explicitGet() + args.size * 5) - case f: NativeFunction => Coeval.now(f.cost) + f.costByLibVersion(version) + case f: NativeFunction => f.cost } - costs += func.header -> cost + costs += func.header -> Coeval.now(cost) } costs.toMap diff --git a/src/main/scala/com/zbsnetwork/utx/UtxPool.scala b/src/main/scala/com/zbsnetwork/utx/UtxPool.scala index 1f14338..2d1fe76 100644 --- a/src/main/scala/com/zbsnetwork/utx/UtxPool.scala +++ b/src/main/scala/com/zbsnetwork/utx/UtxPool.scala @@ -13,9 +13,9 @@ trait UtxPool extends AutoCloseable { def removeAll(txs: Traversable[Transaction]): Unit - def accountPortfolio(addr: Address): Portfolio + def spendableBalance(addr: Address, assetId: Option[AssetId]): Long - def portfolio(addr: Address): Portfolio + def pessimisticPortfolio(addr: Address): Portfolio def all: Seq[Transaction] diff --git a/src/main/scala/com/zbsnetwork/utx/UtxPoolImpl.scala b/src/main/scala/com/zbsnetwork/utx/UtxPoolImpl.scala index 2f9aba6..25fab04 100644 --- a/src/main/scala/com/zbsnetwork/utx/UtxPoolImpl.scala +++ b/src/main/scala/com/zbsnetwork/utx/UtxPoolImpl.scala @@ -5,7 +5,6 @@ import java.time.temporal.ChronoUnit import java.util.concurrent.ConcurrentHashMap import cats._ -import cats.implicits._ import com.zbsnetwork.account.Address import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.consensus.TransactionsOrdering @@ -24,14 +23,17 @@ import kamon.Kamon import kamon.metric.MeasurementUnit import monix.eval.Task import monix.execution.schedulers.SchedulerService -import monix.execution.{CancelableFuture, Scheduler} -import monix.reactive.Observer +import monix.execution.{Cancelable, Scheduler} +import monix.reactive.{Observable, Observer} import scala.collection.JavaConverters._ -import scala.concurrent.duration.DurationLong import scala.util.{Left, Right} -class UtxPoolImpl(time: Time, blockchain: Blockchain, portfolioChanges: Observer[Address], fs: FunctionalitySettings, utxSettings: UtxSettings) +class UtxPoolImpl(time: Time, + blockchain: Blockchain, + spendableBalanceChanged: Observer[(Address, Option[AssetId])], + fs: FunctionalitySettings, + utxSettings: UtxSettings) extends ScorexLogging with Instrumented with AutoCloseable @@ -40,18 +42,11 @@ class UtxPoolImpl(time: Time, blockchain: Blockchain, portfolioChanges: Observer import com.zbsnetwork.utx.UtxPoolImpl._ - private implicit val scheduler: SchedulerService = Scheduler.singleThread("utx-pool-cleanup") - - private val transactions = new ConcurrentHashMap[ByteStr, Transaction]() - private val pessimisticPortfolios = new PessimisticPortfolios(portfolioChanges) - - private val removeInvalidTask: Task[Unit] = - Task.eval(removeInvalid()) >> - Task.sleep(utxSettings.cleanupInterval) >> - removeInvalidTask - - private val cleanup: CancelableFuture[Unit] = removeInvalidTask.runAsyncLogErr + // State + private[this] val transactions = new ConcurrentHashMap[ByteStr, Transaction]() + private[this] val pessimisticPortfolios = new PessimisticPortfolios(spendableBalanceChanged) + // Metrics private[this] object PoolMetrics { private[this] val sizeStats = Kamon.rangeSampler("utx-pool-size", MeasurementUnit.none, Duration.of(500, ChronoUnit.MILLIS)) private[this] val bytesStats = Kamon.rangeSampler("utx-pool-bytes", MeasurementUnit.information.bytes, Duration.of(500, ChronoUnit.MILLIS)) @@ -60,79 +55,115 @@ class UtxPoolImpl(time: Time, blockchain: Blockchain, portfolioChanges: Observer def addTransaction(tx: Transaction): Unit = { sizeStats.increment() - bytesStats.increment(tx.bytes().size) + bytesStats.increment(tx.bytes().length) } def removeTransaction(tx: Transaction): Unit = { sizeStats.decrement() - bytesStats.decrement(tx.bytes().size) + bytesStats.decrement(tx.bytes().length) } } - override def close(): Unit = { - cleanup.cancel() - scheduler.shutdown() - } + override def putIfNew(tx: Transaction): Either[ValidationError, (Boolean, Diff)] = { + def canReissue(blockchain: Blockchain, tx: Transaction) = tx match { + case r: ReissueTransaction if blockchain.assetDescription(r.assetId).exists(!_.reissuable) => Left(GenericError(s"Asset is not reissuable")) + case _ => Right(()) + } - private def removeExpired(currentTs: Long): Unit = { - def isExpired(tx: Transaction) = (currentTs - tx.timestamp).millis > fs.maxTransactionTimeBackOffset + def checkAlias(blockchain: Blockchain, tx: Transaction) = tx match { + case cat: CreateAliasTransaction if !blockchain.canCreateAlias(cat.alias) => Left(GenericError("Alias already claimed")) + case _ => Right(()) + } - transactions.values.asScala - .collect { - case tx if isExpired(tx) => tx.id() - } - .foreach(remove) - } + def checkScripted(blockchain: Blockchain, tx: Transaction) = tx match { + case _ if utxSettings.allowTransactionsFromSmartAccounts => Right(()) + case a: AuthorizedTransaction if blockchain.hasScript(a.sender.toAddress) => + Left(GenericError("transactions from scripted accounts are denied from UTX pool")) + case _ => Right(()) + } - override def putIfNew(tx: Transaction): Either[ValidationError, (Boolean, Diff)] = putIfNew(blockchain, tx) + def checkNotBlacklisted(tx: Transaction) = { + if (utxSettings.blacklistSenderAddresses.isEmpty) { + Right(()) + } else { + val sender: Option[String] = tx match { + case x: Authorized => Some(x.sender.address) + case _ => None + } - private def checkNotBlacklisted(tx: Transaction): Either[ValidationError, Unit] = { - if (utxSettings.blacklistSenderAddresses.isEmpty) { - Right(()) - } else { - val sender: Option[String] = tx match { - case x: Authorized => Some(x.sender.address) - case _ => None + sender match { + case Some(addr) if utxSettings.blacklistSenderAddresses.contains(addr) => + val recipients = tx match { + case tt: TransferTransaction => Seq(tt.recipient) + case mtt: MassTransferTransaction => mtt.transfers.map(_.address) + case _ => Seq() + } + val allowed = + recipients.nonEmpty && + recipients.forall(r => utxSettings.allowBlacklistedTransferTo.contains(r.stringRepr)) + Either.cond(allowed, (), SenderIsBlacklisted(addr)) + case _ => Right(()) + } } + } - sender match { - case Some(addr) if utxSettings.blacklistSenderAddresses.contains(addr) => - val recipients = tx match { - case tt: TransferTransaction => Seq(tt.recipient) - case mtt: MassTransferTransaction => mtt.transfers.map(_.address) - case _ => Seq() - } - val allowed = - recipients.nonEmpty && - recipients.forall(r => utxSettings.allowBlacklistedTransferTo.contains(r.stringRepr)) - Either.cond(allowed, (), SenderIsBlacklisted(addr)) - case _ => Right(()) + PoolMetrics.putRequestStats.increment() + val result = measureSuccessful( + PoolMetrics.processingTimeStats, { + for { + _ <- Either.cond(transactions.size < utxSettings.maxSize, (), GenericError("Transaction pool size limit is reached")) + + transactionsBytes = transactions.values.asScala // Bytes size of all transactions in pool + .map(_.bytes().length) + .sum + _ <- Either.cond((transactionsBytes + tx.bytes().length) <= utxSettings.maxBytesSize, + (), + GenericError("Transaction pool bytes size limit is reached")) + + _ <- checkNotBlacklisted(tx) + _ <- checkScripted(blockchain, tx) + _ <- checkAlias(blockchain, tx) + _ <- canReissue(blockchain, tx) + diff <- TransactionDiffer(fs, blockchain.lastBlockTimestamp, time.correctedTime(), blockchain.height)(blockchain, tx) + } yield { + pessimisticPortfolios.add(tx.id(), diff) + val isNew = Option(transactions.put(tx.id(), tx)).isEmpty + if (isNew) PoolMetrics.addTransaction(tx) + (isNew, diff) + } } - } + ) + + result.fold( + err => log.trace(s"UTX putIfNew(${tx.id()}) failed with $err"), + r => log.trace(s"UTX putIfNew(${tx.id()}) succeeded, isNew = ${r._1}") + ) + + result } override def removeAll(txs: Traversable[Transaction]): Unit = { txs.view.map(_.id()).foreach(remove) - removeExpired(time.correctedTime()) + cleanup.doExpiredCleanup() } - private def remove(txId: ByteStr): Unit = { - Option(transactions.remove(txId)).foreach(PoolMetrics.removeTransaction) - pessimisticPortfolios.remove(txId) + private[this] def afterRemove(tx: Transaction): Unit = { + PoolMetrics.removeTransaction(tx) + pessimisticPortfolios.remove(tx.id()) } - private def removeInvalid(): Unit = { - val b = blockchain - val transactionsToRemove = transactions.values.asScala.filter { t => - TransactionDiffer(fs, b.lastBlockTimestamp, time.correctedTime(), b.height)(b, t).isLeft - } - removeAll(transactionsToRemove) - } + private[this] def remove(txId: ByteStr): Unit = + Option(transactions.remove(txId)) + .foreach(afterRemove) - override def accountPortfolio(addr: Address): Portfolio = blockchain.portfolio(addr) + override def spendableBalance(addr: Address, assetId: Option[AssetId]): Long = + blockchain.balance(addr, assetId) - + assetId.fold(blockchain.leaseBalance(addr).out)(_ => 0L) + + pessimisticPortfolios + .getAggregated(addr) + .spendableBalanceOf(assetId) - override def portfolio(addr: Address): Portfolio = - Monoid.combine(blockchain.portfolio(addr), pessimisticPortfolios.getAggregated(addr)) + override def pessimisticPortfolio(addr: Address): Portfolio = pessimisticPortfolios.getAggregated(addr) override def all: Seq[Transaction] = transactions.values.asScala.toSeq.sorted(TransactionsOrdering.InUTXPool) @@ -141,16 +172,15 @@ class UtxPoolImpl(time: Time, blockchain: Blockchain, portfolioChanges: Observer override def transactionById(transactionId: ByteStr): Option[Transaction] = Option(transactions.get(transactionId)) override def packUnconfirmed(rest: MultiDimensionalMiningConstraint): (Seq[Transaction], MultiDimensionalMiningConstraint) = { - val currentTs = time.correctedTime() - removeExpired(currentTs) - val b = blockchain - val differ = TransactionDiffer(fs, blockchain.lastBlockTimestamp, currentTs, b.height) _ + cleanup.doExpiredCleanup() + + val differ = TransactionDiffer(fs, blockchain.lastBlockTimestamp, time.correctedTime(), blockchain.height) _ val (invalidTxs, reversedValidTxs, _, finalConstraint, _) = transactions.values.asScala.toSeq .sorted(TransactionsOrdering.InUTXPool) .iterator .scanLeft((Seq.empty[ByteStr], Seq.empty[Transaction], Monoid[Diff].empty, rest, false)) { case ((invalid, valid, diff, currRest, isEmpty), tx) => - val updatedBlockchain = composite(b, diff) + val updatedBlockchain = composite(blockchain, diff) val updatedRest = currRest.put(updatedBlockchain, tx) if (updatedRest.isOverfilled) { (invalid, valid, diff, currRest, isEmpty) @@ -171,61 +201,65 @@ class UtxPoolImpl(time: Time, blockchain: Blockchain, portfolioChanges: Observer (txs, finalConstraint) } - private def canReissue(b: Blockchain, tx: Transaction) = tx match { - case r: ReissueTransaction if b.assetDescription(r.assetId).exists(!_.reissuable) => Left(GenericError(s"Asset is not reissuable")) - case _ => Right(()) - } + //noinspection ScalaStyle + private[this] object TxCheck { + private[this] val ExpirationTime = fs.maxTransactionTimeBackOffset.toMillis - private def checkAlias(b: Blockchain, tx: Transaction) = tx match { - case cat: CreateAliasTransaction if !blockchain.canCreateAlias(cat.alias) => Left(GenericError("Alias already claimed")) - case _ => Right(()) - } + def transactionIsExpired(transaction: Transaction, currentTime: Long = time.correctedTime()) = { + (currentTime - transaction.timestamp) > ExpirationTime + } - private def checkScripted(b: Blockchain, tx: Transaction) = - tx match { - case a: AuthorizedTransaction if blockchain.hasScript(a.sender.toAddress) && (!utxSettings.allowTransactionsFromSmartAccounts) => - Left(GenericError("transactions from scripted accounts are denied from UTX pool")) - case _ => Right(()) + def transactionIsValid(transaction: Transaction, + lastBlockTimestamp: Option[Long] = blockchain.lastBlockTimestamp, + currentTime: Long = time.correctedTime(), + height: Int = blockchain.height) = { + !transactionIsExpired(transaction) && TransactionDiffer(fs, lastBlockTimestamp, currentTime, height)(blockchain, transaction).isRight } + } - private def putIfNew(b: Blockchain, tx: Transaction): Either[ValidationError, (Boolean, Diff)] = { - PoolMetrics.putRequestStats.increment() - val result = measureSuccessful( - PoolMetrics.processingTimeStats, { - for { - _ <- Either.cond(transactions.size < utxSettings.maxSize, (), GenericError("Transaction pool size limit is reached")) + //noinspection ScalaStyle + object cleanup { + private[UtxPoolImpl] implicit val scheduler: SchedulerService = Scheduler.singleThread("utx-pool-cleanup") + + val runCleanupTask: Task[Unit] = Task + .eval(doCleanup()) + .executeOn(scheduler) + + def runCleanupOn(observable: Observable[_]): Cancelable = { + observable + .whileBusyDropEventsAndSignal(dropped => log.warn(s"UTX pool cleanup is too slow, $dropped cleanups skipped")) + .mapTask(_ => runCleanupTask) + .doOnComplete(() => log.debug("UTX pool cleanup stopped")) + .doOnError(err => log.error("UTX pool cleanup error", err)) + .subscribe() + } - transactionsBytes = transactions.values.asScala // Bytes size of all transactions in pool - .map(_.bytes().size) - .sum - _ <- Either.cond((transactionsBytes + tx.bytes().size) <= utxSettings.maxBytesSize, - (), - GenericError("Transaction pool bytes size limit is reached")) + private[UtxPoolImpl] def doExpiredCleanup(): Unit = { + transactions.entrySet().removeIf { entry => + val tx = entry.getValue + val remove = TxCheck.transactionIsExpired(tx) + if (remove) UtxPoolImpl.this.afterRemove(tx) + remove + } + } - _ <- checkNotBlacklisted(tx) - _ <- checkScripted(b, tx) - _ <- checkAlias(b, tx) - _ <- canReissue(b, tx) - diff <- TransactionDiffer(fs, blockchain.lastBlockTimestamp, time.correctedTime(), blockchain.height)(b, tx) - } yield { - pessimisticPortfolios.add(tx.id(), diff) - val isNew = Option(transactions.put(tx.id(), tx)).isEmpty - if (isNew) PoolMetrics.addTransaction(tx) - (isNew, diff) - } + private[UtxPoolImpl] def doCleanup(): Unit = { + transactions.entrySet().removeIf { entry => + val tx = entry.getValue + val remove = !TxCheck.transactionIsValid(tx) + if (remove) UtxPoolImpl.this.afterRemove(tx) + remove } - ) - result.fold( - err => log.trace(s"UTX putIfNew(${tx.id()}) failed with $err"), - r => log.trace(s"UTX putIfNew(${tx.id()}) succeeded, isNew = ${r._1}") - ) - result + } + } + + override def close(): Unit = { + cleanup.scheduler.shutdown() } } object UtxPoolImpl { - - private class PessimisticPortfolios(portfolioChanges: Observer[Address]) { + private class PessimisticPortfolios(spendableBalanceChanged: Observer[(Address, Option[AssetId])]) { private type Portfolios = Map[Address, Portfolio] private val transactionPortfolios = new ConcurrentHashMap[ByteStr, Portfolios]() private val transactions = new ConcurrentHashMap[Address, Set[ByteStr]]() @@ -238,12 +272,13 @@ object UtxPoolImpl { Option(transactionPortfolios.put(txId, nonEmptyPessimisticPortfolios)).isEmpty) { nonEmptyPessimisticPortfolios.keys.foreach { address => transactions.put(address, transactions.getOrDefault(address, Set.empty) + txId) - portfolioChanges.onNext(address) } } // Because we need to notify about balance changes when they are applied - pessimisticPortfolios.iterator.collect { case (addr, p) if p.isEmpty => addr }.foreach(portfolioChanges.onNext) + pessimisticPortfolios.foreach { + case (addr, p) => p.assetIds.foreach(assetId => spendableBalanceChanged.onNext(addr -> assetId)) + } } def getAggregated(accountAddr: Address): Portfolio = { @@ -257,13 +292,15 @@ object UtxPoolImpl { } def remove(txId: ByteStr): Unit = { - if (Option(transactionPortfolios.remove(txId)).isDefined) { - transactions.keySet().asScala.foreach { addr => - transactions.put(addr, transactions.getOrDefault(addr, Set.empty) - txId) - portfolioChanges.onNext(addr) - } + Option(transactionPortfolios.remove(txId)) match { + case Some(txPortfolios) => + txPortfolios.foreach { + case (addr, p) => + transactions.computeIfPresent(addr, (_, prevTxs) => prevTxs - txId) + p.assetIds.foreach(assetId => spendableBalanceChanged.onNext(addr -> assetId)) + } + case None => } } } - } diff --git a/src/test/scala/com/zbsnetwork/TransactionGen.scala b/src/test/scala/com/zbsnetwork/TransactionGen.scala index 4d30f4b..15daf9a 100644 --- a/src/test/scala/com/zbsnetwork/TransactionGen.scala +++ b/src/test/scala/com/zbsnetwork/TransactionGen.scala @@ -1,19 +1,13 @@ package com.zbsnetwork -import cats.syntax.semigroup._ import com.zbsnetwork.account.PublicKeyAccount._ import com.zbsnetwork.account._ import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 -import com.zbsnetwork.lang.Global import com.zbsnetwork.lang.StdLibVersion._ -import com.zbsnetwork.lang.contract.Contract -import com.zbsnetwork.lang.contract.Contract.{CallableAnnotation, CallableFunction} -import com.zbsnetwork.lang.v1.FunctionHeader +import com.zbsnetwork.lang.v1.{ContractLimits, FunctionHeader} import com.zbsnetwork.lang.v1.compiler.Terms._ -import com.zbsnetwork.lang.v1.compiler.{ExpressionCompiler, Terms} -import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext} -import com.zbsnetwork.lang.v1.testing.ScriptGen +import com.zbsnetwork.lang.v1.testing.{ScriptGen, TypedScriptGen} import com.zbsnetwork.settings.Constants import com.zbsnetwork.state._ import com.zbsnetwork.state.diffs.ENOUGH_AMT @@ -21,8 +15,8 @@ import com.zbsnetwork.transaction._ import com.zbsnetwork.transaction.assets._ import com.zbsnetwork.transaction.assets.exchange._ import com.zbsnetwork.transaction.lease._ -import com.zbsnetwork.transaction.smart.script.{ContractScript, Script} import com.zbsnetwork.transaction.smart.script.v1.ExprScript +import com.zbsnetwork.transaction.smart.script.{ContractScript, Script} import com.zbsnetwork.transaction.smart.{ContractInvocationTransaction, SetScriptTransaction} import com.zbsnetwork.transaction.transfer.MassTransferTransaction.{MaxTransferCount, ParsedTransfer} import com.zbsnetwork.transaction.transfer._ @@ -36,7 +30,7 @@ trait TransactionGen extends TransactionGenBase { _: Suite => } -trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => +trait TransactionGenBase extends ScriptGen with TypedScriptGen with NTPTime { _: Suite => val ScriptExtraFee = 400000L protected def zbs(n: Float): Long = (n * 100000000L).toLong @@ -90,7 +84,7 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => val positiveLongGen: Gen[Long] = Gen.choose(1, 100000000L * 100000000L / 100) val positiveIntGen: Gen[Int] = Gen.choose(1, Int.MaxValue / 100) - val smallFeeGen: Gen[Long] = Gen.choose(400000, 100000000) + val smallFeeGen: Gen[Long] = Gen.choose(50000000000L, 100000000000L) val maxOrderTimeGen: Gen[Long] = Gen.choose(10000L, Order.MaxLiveTime).map(_ + ntpTime.correctedTime()) val timestampGen: Gen[Long] = Gen.choose(1, Long.MaxValue - 100) @@ -110,25 +104,16 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => proofs <- Gen.listOfN(proofsAmount, genBoundedBytes(0, 50)) } yield Proofs.create(proofs.map(ByteStr(_))).explicitGet() - val scriptGen = BOOLgen(100).map { - case (expr, _) => - val typed = - ExpressionCompiler(PureContext.build(V1).compilerContext |+| CryptoContext.compilerContext(Global), expr).explicitGet() - ExprScript(typed._1).explicitGet() - } - - val contractGen = Gen.const( - ContractScript(V3, Contract(List.empty, List(CallableFunction(CallableAnnotation("sender"), Terms.FUNC("foo", List("a"), Terms.REF("a")))), None)) - .explicitGet() - ) - + val scriptGen: Gen[Script] = exprGen.map(e => ExprScript(e).explicitGet()) + val contractScriptGen: Gen[Script] = contractGen.map(e => ContractScript(V3, e).explicitGet()) + val contractOrExpr = Gen.oneOf(scriptGen, contractScriptGen) val setAssetScriptTransactionGen: Gen[(Seq[Transaction], SetAssetScriptTransaction)] = for { version <- Gen.oneOf(SetScriptTransaction.supportedVersions.toSeq) (sender, assetName, description, quantity, decimals, _, iFee, timestamp) <- issueParamGen fee <- smallFeeGen timestamp <- timestampGen proofs <- proofsGen - script <- Gen.option(Gen.oneOf(scriptGen, contractGen)) + script <- Gen.option(scriptGen) issue = IssueTransactionV2 .selfSigned(AddressScheme.current.chainId, sender, assetName, description, quantity, decimals, reissuable = true, script, iFee, timestamp) .explicitGet() @@ -143,7 +128,7 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => fee <- smallFeeGen timestamp <- timestampGen proofs <- proofsGen - script <- Gen.option(scriptGen) + script <- Gen.option(contractOrExpr) } yield SetScriptTransaction.create(sender, script, fee, timestamp, proofs).explicitGet() def selfSignedSetScriptTransactionGenP(sender: PrivateKeyAccount, s: Script): Gen[SetScriptTransaction] = @@ -336,9 +321,8 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => val massTransferGen: Gen[MassTransferTransaction] = massTransferGen(MaxTransferCount) - def massTransferGen(maxTransfersCount: Int) = + def massTransferGen(maxTransfersCount: Int): Gen[MassTransferTransaction] = { for { - version <- Gen.oneOf(MassTransferTransaction.supportedVersions.toSeq) (assetId, sender, _, _, timestamp, _, feeAmount, attachment) <- transferParamGen transferCount <- Gen.choose(0, maxTransfersCount) transferGen = for { @@ -347,8 +331,9 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => } yield ParsedTransfer(recipient, amount) recipients <- Gen.listOfN(transferCount, transferGen) } yield MassTransferTransaction.selfSigned(assetId, sender, recipients, timestamp, feeAmount, attachment).explicitGet() + } - val MinIssueFee = 100000000 + val MinIssueFee = 50000000000L val createAliasGen: Gen[CreateAliasTransaction] = for { timestamp: Long <- positiveLongGen @@ -416,7 +401,6 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => fee: Long, timestamp: Long): Gen[ReissueTransaction] = { for { - version <- versionGen(ReissueTransactionV2) tx <- Gen.oneOf( ReissueTransactionV1 .selfSigned(reissuer, assetId, quantity, reissuable, fee, timestamp) @@ -496,9 +480,9 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => assetId = issue.assetId() } yield (issue, - SponsorFeeTransaction.selfSigned(sender, assetId, Some(minFee), 1 * Constants.UnitsInZbs, timestamp).explicitGet(), - SponsorFeeTransaction.selfSigned(sender, assetId, Some(minFee1), 1 * Constants.UnitsInZbs, timestamp).explicitGet(), - SponsorFeeTransaction.selfSigned(sender, assetId, None, 1 * Constants.UnitsInZbs, timestamp).explicitGet(), + SponsorFeeTransaction.selfSigned(sender, assetId, Some(minFee), 50 * Constants.UnitsInZbs, timestamp).explicitGet(), + SponsorFeeTransaction.selfSigned(sender, assetId, Some(minFee1), 50 * Constants.UnitsInZbs, timestamp).explicitGet(), + SponsorFeeTransaction.selfSigned(sender, assetId, None, 50 * Constants.UnitsInZbs, timestamp).explicitGet(), ) val sponsorFeeGen = for { @@ -518,12 +502,12 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => val funcCallGen = for { functionName <- genBoundedString(1, 32).map(_.toString) - amt <- Gen.choose(0, 10) + amt <- Gen.choose(0, ContractLimits.MaxContractInvocationArgs) args <- Gen.listOfN(amt, argGen) } yield FUNCTION_CALL(FunctionHeader.User(functionName), args) - val contractInvokationGen = for { + val contractInvocationGen = for { sender <- accountGen contractAddress <- accountGen fc <- funcCallGen @@ -562,7 +546,12 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => (sender, matcher, pair, orderType, amount, price, timestamp, expiration, matcherFee) <- orderParamGen } yield Order(sender, matcher, pair, orderType, amount, price, timestamp, expiration, matcherFee, 2: Byte) - val orderGen: Gen[Order] = Gen.oneOf(orderV1Gen, orderV2Gen) + val orderV3Gen: Gen[Order] = for { + (sender, matcher, pair, orderType, price, amount, timestamp, expiration, matcherFee) <- orderParamGen + matcherFeeAssetId <- assetIdGen + } yield Order(sender, matcher, pair, orderType, amount, price, timestamp, expiration, matcherFee, 3: Byte, matcherFeeAssetId) + + val orderGen: Gen[Order] = Gen.oneOf(orderV1Gen, orderV2Gen, orderV3Gen) val arbitraryOrderGen: Gen[Order] = for { (sender, matcher, pair, orderType, _, _, _, _, _) <- orderParamGen @@ -577,17 +566,27 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => sender1: PrivateKeyAccount <- accountGen sender2: PrivateKeyAccount <- accountGen assetPair <- assetPairGen + buyerAnotherAsset <- assetIdGen + sellerAnotherAsset <- assetIdGen + buyerMatcherFeeAssetId <- Gen.oneOf(assetPair.amountAsset, assetPair.priceAsset, buyerAnotherAsset, None) + sellerMatcherFeeAssetId <- Gen.oneOf(assetPair.amountAsset, assetPair.priceAsset, sellerAnotherAsset, None) r <- Gen.oneOf( exchangeV1GeneratorP(sender1, sender2, assetPair.amountAsset, assetPair.priceAsset), - exchangeV2GeneratorP(sender1, sender2, assetPair.amountAsset, assetPair.priceAsset) + exchangeV2GeneratorP( + buyer = sender1, + seller = sender2, + amountAssetId = assetPair.amountAsset, + priceAssetId = assetPair.priceAsset, + buyMatcherFeeAssetId = buyerMatcherFeeAssetId, + sellMatcherFeeAssetId = sellerMatcherFeeAssetId + ) ) } yield r def exchangeGeneratorP(buyer: PrivateKeyAccount, seller: PrivateKeyAccount, amountAssetId: Option[ByteStr], - priceAssetId: Option[ByteStr], - fixedMatcherFee: Option[Long] = None): Gen[ExchangeTransaction] = { + priceAssetId: Option[ByteStr]): Gen[ExchangeTransaction] = { Gen.oneOf( exchangeV1GeneratorP(buyer, seller, amountAssetId, priceAssetId), exchangeV2GeneratorP(buyer, seller, amountAssetId, priceAssetId) @@ -633,23 +632,38 @@ trait TransactionGenBase extends ScriptGen with NTPTime { _: Suite => amountAssetId: Option[ByteStr], priceAssetId: Option[ByteStr], fixedMatcherFee: Option[Long] = None, - orderVersions: Set[Byte] = Set(1, 2)): Gen[ExchangeTransactionV2] = { - def mkBuyOrder(version: Byte): OrderConstructor = if (version == 1) OrderV1.buy else OrderV2.buy - def mkSellOrder(version: Byte): OrderConstructor = if (version == 1) OrderV1.sell else OrderV2.sell + orderVersions: Set[Byte] = Set(1, 2, 3), + buyMatcherFeeAssetId: Option[ByteStr] = None, + sellMatcherFeeAssetId: Option[ByteStr] = None, + fixedMatcher: Option[PrivateKeyAccount] = None): Gen[ExchangeTransactionV2] = { + + def mkBuyOrder(version: Byte): OrderConstructor = version match { + case 1 => OrderV1.buy + case 2 => OrderV2.buy + case 3 => OrderV3.buy(_, _, _, _, _, _, _, _, buyMatcherFeeAssetId) + } + + def mkSellOrder(version: Byte): OrderConstructor = version match { + case 1 => OrderV1.sell + case 2 => OrderV2.sell + case 3 => OrderV3.sell(_, _, _, _, _, _, _, _, sellMatcherFeeAssetId) + } for { - (_, matcher, _, _, price, amount1, timestamp, expiration, genMatcherFee) <- orderParamGen - amount2: Long <- matcherAmountGen - matcherFee = fixedMatcherFee.getOrElse(genMatcherFee) + (_, generatedMatcher, _, _, amount1, price, timestamp, expiration, generatedMatcherFee) <- orderParamGen + amount2: Long <- matcherAmountGen + matcher = fixedMatcher.getOrElse(generatedMatcher) + matcherFee = fixedMatcherFee.getOrElse(generatedMatcherFee) matchedAmount: Long <- Gen.choose(Math.min(amount1, amount2) / 2000, Math.min(amount1, amount2) / 1000) assetPair = AssetPair(amountAssetId, priceAssetId) mkO1 <- Gen.oneOf(orderVersions.map(mkBuyOrder).toSeq) mkO2 <- Gen.oneOf(orderVersions.map(mkSellOrder).toSeq) } yield { + val buyFee = (BigInt(matcherFee) * BigInt(matchedAmount) / BigInt(amount1)).longValue() val sellFee = (BigInt(matcherFee) * BigInt(matchedAmount) / BigInt(amount2)).longValue() - val o1 = mkO1(seller, matcher, assetPair, amount1, price, timestamp, expiration, matcherFee) + val o1 = mkO1(buyer, matcher, assetPair, amount1, price, timestamp, expiration, matcherFee) val o2 = mkO2(seller, matcher, assetPair, amount2, price, timestamp, expiration, matcherFee) ExchangeTransactionV2 diff --git a/src/test/scala/com/zbsnetwork/WithDB.scala b/src/test/scala/com/zbsnetwork/WithDB.scala index 79e900c..94d0030 100644 --- a/src/test/scala/com/zbsnetwork/WithDB.scala +++ b/src/test/scala/com/zbsnetwork/WithDB.scala @@ -4,6 +4,7 @@ import java.nio.file.Files import com.zbsnetwork.account.Address import com.zbsnetwork.db.LevelDBFactory +import com.zbsnetwork.transaction.AssetId import com.zbsnetwork.utils.Implicits.SubjectOps import monix.reactive.subjects.Subject import org.iq80.leveldb.{DB, Options} @@ -17,7 +18,7 @@ trait WithDB extends BeforeAndAfterEach { def db: DB = currentDBInstance - protected val ignorePortfolioChanged: Subject[Address, Address] = Subject.empty[Address] + protected val ignoreSpendableBalanceChanged: Subject[(Address, Option[AssetId]), (Address, Option[AssetId])] = Subject.empty override def beforeEach(): Unit = { currentDBInstance = LevelDBFactory.factory.open(path.toFile, new Options().createIfMissing(true)) diff --git a/src/test/scala/com/zbsnetwork/consensus/FPPoSSelectorTest.scala b/src/test/scala/com/zbsnetwork/consensus/FPPoSSelectorTest.scala index a7376c6..ee43cc3 100644 --- a/src/test/scala/com/zbsnetwork/consensus/FPPoSSelectorTest.scala +++ b/src/test/scala/com/zbsnetwork/consensus/FPPoSSelectorTest.scala @@ -203,10 +203,10 @@ class FPPoSSelectorTest extends FreeSpec with Matchers with WithDB with Transact } def withEnv(gen: Time => Gen[(Seq[PrivateKeyAccount], Seq[Block])])(f: Env => Unit): Unit = { - val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) + val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load())) val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false)) - val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime) + val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime) val pos = new PoSSelector(bcu, settings.blockchainSettings, settings.synchronizationSettings) try { val (accounts, blocks) = gen(ntpTime).sample.get diff --git a/src/test/scala/com/zbsnetwork/database/LevelDBWriterSpec.scala b/src/test/scala/com/zbsnetwork/database/LevelDBWriterSpec.scala index 15830ec..b119c82 100644 --- a/src/test/scala/com/zbsnetwork/database/LevelDBWriterSpec.scala +++ b/src/test/scala/com/zbsnetwork/database/LevelDBWriterSpec.scala @@ -39,7 +39,7 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with import TestFunctionalitySettings.Enabled "correctly joins height ranges" in { val fs = Enabled.copy(preActivatedFeatures = Map(BlockchainFeatures.SmartAccountTrading.id -> 0)) - val writer = new LevelDBWriter(db, ignorePortfolioChanged, fs, maxCacheSize, 2000, 120 * 60 * 1000) + val writer = new LevelDBWriter(db, ignoreSpendableBalanceChanged, fs, maxCacheSize, 2000, 120 * 60 * 1000) writer.merge(Seq(15, 12, 3), Seq(12, 5)) shouldEqual Seq((15, 12), (12, 12), (3, 5)) writer.merge(Seq(12, 5), Seq(15, 12, 3)) shouldEqual Seq((12, 15), (12, 12), (5, 3)) writer.merge(Seq(8, 4), Seq(8, 4)) shouldEqual Seq((8, 8), (4, 4)) @@ -47,14 +47,14 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with } "hasScript" - { "returns false if a script was not set" in { - val writer = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) + val writer = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) writer.hasScript(accountGen.sample.get.toAddress) shouldBe false } "returns false if a script was set and then unset" in { assume(BlockchainFeatures.implemented.contains(BlockchainFeatures.SmartAccounts.id)) resetTest { (_, account) => - val writer = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) + val writer = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) writer.hasScript(account) shouldBe false } } @@ -63,7 +63,7 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with "if there is a script in db" in { assume(BlockchainFeatures.implemented.contains(BlockchainFeatures.SmartAccounts.id)) test { (_, account) => - val writer = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) + val writer = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) writer.hasScript(account) shouldBe true } } @@ -110,10 +110,10 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with } def baseTest(gen: Time => Gen[(PrivateKeyAccount, Seq[Block])])(f: (LevelDBWriter, PrivateKeyAccount) => Unit): Unit = { - val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) + val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load())) val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false)) - val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime) + val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime) try { val (account, blocks) = gen(ntpTime).sample.get @@ -130,10 +130,10 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with } def testWithBlocks(gen: Time => Gen[(PrivateKeyAccount, Seq[Block])])(f: (LevelDBWriter, Seq[Block], PrivateKeyAccount) => Unit): Unit = { - val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000) + val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000) val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load())) val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false)) - val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime) + val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime) try { val (account, blocks) = gen(ntpTime).sample.get @@ -175,10 +175,10 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with } "correctly reassemble block from header and transactions" in { - val rw = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000) + val rw = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000) val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load())) val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false)) - val bcu = new BlockchainUpdaterImpl(rw, ignorePortfolioChanged, settings, ntpTime) + val bcu = new BlockchainUpdaterImpl(rw, ignoreSpendableBalanceChanged, settings, ntpTime) try { val master = PrivateKeyAccount("master".getBytes()) val recipient = PrivateKeyAccount("recipient".getBytes()) @@ -266,10 +266,10 @@ class LevelDBWriterSpec extends FreeSpec with Matchers with TransactionGen with } yield (leaser, leaseTxs, blocks.reverse) } - val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000) + val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, 100000, 2000, 120 * 60 * 1000) val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load())) val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false)) - val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime) + val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime) try { val (leaser, leases, blocks) = precs.sample.get diff --git a/src/test/scala/com/zbsnetwork/db/ScriptCacheTest.scala b/src/test/scala/com/zbsnetwork/db/ScriptCacheTest.scala index 7b4bd94..7acfa11 100644 --- a/src/test/scala/com/zbsnetwork/db/ScriptCacheTest.scala +++ b/src/test/scala/com/zbsnetwork/db/ScriptCacheTest.scala @@ -129,10 +129,10 @@ class ScriptCacheTest extends FreeSpec with Matchers with WithDB with Transactio } def withBlockchain(gen: Time => Gen[(Seq[PrivateKeyAccount], Seq[Block])])(f: (Seq[PrivateKeyAccount], BlockchainUpdater with NG) => Unit): Unit = { - val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, CACHE_SIZE, 2000, 120 * 60 * 1000) + val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, CACHE_SIZE, 2000, 120 * 60 * 1000) val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load())) val settings = settings0.copy(featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false)) - val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime) + val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime) try { val (accounts, blocks) = gen(ntpTime).sample.get diff --git a/src/test/scala/com/zbsnetwork/db/WithState.scala b/src/test/scala/com/zbsnetwork/db/WithState.scala index 15fc579..0db4cdd 100644 --- a/src/test/scala/com/zbsnetwork/db/WithState.scala +++ b/src/test/scala/com/zbsnetwork/db/WithState.scala @@ -8,17 +8,18 @@ import com.zbsnetwork.database.LevelDBWriter import com.zbsnetwork.history.Domain import com.zbsnetwork.settings.{FunctionalitySettings, ZbsSettings, loadConfig} import com.zbsnetwork.state.{Blockchain, BlockchainUpdaterImpl} +import com.zbsnetwork.transaction.AssetId import com.zbsnetwork.utils.Implicits.SubjectOps import com.zbsnetwork.{NTPTime, TestHelpers} import monix.reactive.subjects.Subject import org.scalatest.Suite trait WithState extends DBCacheSettings { - protected val ignorePortfolioChanged: Subject[Address, Address] = Subject.empty[Address] + protected val ignoreSpendableBalanceChanged: Subject[(Address, Option[AssetId]), (Address, Option[AssetId])] = Subject.empty protected def withState[A](fs: FunctionalitySettings)(f: Blockchain => A): A = { val path = Files.createTempDirectory("leveldb-test") val db = openDB(path.toAbsolutePath.toString) - try f(new LevelDBWriter(db, ignorePortfolioChanged, fs, maxCacheSize, 2000, 120 * 60 * 1000)) + try f(new LevelDBWriter(db, ignoreSpendableBalanceChanged, fs, maxCacheSize, 2000, 120 * 60 * 1000)) finally { db.close() TestHelpers.deleteRecursively(path) @@ -33,7 +34,7 @@ trait WithDomain extends WithState with NTPTime { def withDomain[A](settings: ZbsSettings = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load())))(test: Domain => A): A = { try withState(settings.blockchainSettings.functionalitySettings) { blockchain => - val bcu = new BlockchainUpdaterImpl(blockchain, ignorePortfolioChanged, settings, ntpTime) + val bcu = new BlockchainUpdaterImpl(blockchain, ignoreSpendableBalanceChanged, settings, ntpTime) try test(Domain(bcu)) finally bcu.shutdown() } finally {} diff --git a/src/test/scala/com/zbsnetwork/history/BlockchainUpdaterInMemoryDiffTest.scala b/src/test/scala/com/zbsnetwork/history/BlockchainUpdaterInMemoryDiffTest.scala index 412bb73..33e3443 100644 --- a/src/test/scala/com/zbsnetwork/history/BlockchainUpdaterInMemoryDiffTest.scala +++ b/src/test/scala/com/zbsnetwork/history/BlockchainUpdaterInMemoryDiffTest.scala @@ -45,7 +45,7 @@ class BlockchainUpdaterInMemoryDiffTest domain.blockchainUpdater.height shouldBe MaxTransactionsPerBlockDiff * 2 + 2 - val mastersBalanceAfterPayment1AndPayment2 = domain.blockchainUpdater.portfolio(genesis.recipient).balance + val mastersBalanceAfterPayment1AndPayment2 = domain.blockchainUpdater.balance(genesis.recipient) mastersBalanceAfterPayment1AndPayment2 shouldBe (ENOUGH_AMT - payment1.amount - payment1.fee - payment2.amount - payment2.fee) } } @@ -61,7 +61,7 @@ class BlockchainUpdaterInMemoryDiffTest firstBlocks.foreach(b => domain.blockchainUpdater.processBlock(b).explicitGet()) domain.blockchainUpdater.processBlock(payment1Block).explicitGet() domain.blockchainUpdater.processBlock(emptyBlock).explicitGet() - val mastersBalanceAfterPayment1 = domain.blockchainUpdater.portfolio(genesis.recipient).balance + val mastersBalanceAfterPayment1 = domain.blockchainUpdater.balance(genesis.recipient) mastersBalanceAfterPayment1 shouldBe (ENOUGH_AMT - payment1.amount - payment1.fee) // discard liquid block @@ -70,7 +70,7 @@ class BlockchainUpdaterInMemoryDiffTest domain.blockchainUpdater.height shouldBe MaxTransactionsPerBlockDiff * 2 + 1 - val mastersBalanceAfterPayment1AndPayment2 = domain.blockchainUpdater.portfolio(genesis.recipient).balance + val mastersBalanceAfterPayment1AndPayment2 = domain.blockchainUpdater.balance(genesis.recipient) mastersBalanceAfterPayment1AndPayment2 shouldBe (ENOUGH_AMT - payment1.amount - payment1.fee - payment2.amount - payment2.fee) } } diff --git a/src/test/scala/com/zbsnetwork/http/AssetsBroadcastRouteSpec.scala b/src/test/scala/com/zbsnetwork/http/AssetsBroadcastRouteSpec.scala index f430968..95920a7 100644 --- a/src/test/scala/com/zbsnetwork/http/AssetsBroadcastRouteSpec.scala +++ b/src/test/scala/com/zbsnetwork/http/AssetsBroadcastRouteSpec.scala @@ -14,8 +14,7 @@ import com.zbsnetwork.transaction.transfer._ import com.zbsnetwork.transaction.{Proofs, Transaction} import com.zbsnetwork.utx.UtxPool import com.zbsnetwork.wallet.Wallet -import io.netty.channel.group.ChannelGroup -import org.scalacheck.Gen._ +import io.netty.channel.group.{ChannelGroup, ChannelGroupFuture, ChannelMatcher} import org.scalacheck.{Gen => G} import org.scalamock.scalatest.PathMockFactory import org.scalatest.prop.PropertyChecks @@ -127,7 +126,7 @@ class AssetsBroadcastRouteSpec extends RouteSpec("/assets/broadcast/") with Requ def posting[A: Writes](v: A): RouteTestResult = Post(routePath("transfer"), v) ~> route forAll(nonPositiveLong) { q => - posting(tr.copy(amount = q)) should produce(NegativeAmount(s"$q of zbs")) + posting(tr.copy(amount = q)) should produce(NegativeAmount(s"$q of ${tr.assetId.getOrElse("zbs")}")) } forAll(invalidBase58) { pk => posting(tr.copy(senderPublicKey = pk)) should produce(InvalidAddress) @@ -144,9 +143,6 @@ class AssetsBroadcastRouteSpec extends RouteSpec("/assets/broadcast/") with Requ forAll(longAttachment) { a => posting(tr.copy(attachment = Some(a))) should produce(CustomValidationError("invalid.attachment")) } - forAll(posNum[Long]) { quantity => - posting(tr.copy(amount = quantity, fee = Long.MaxValue)) should produce(OverflowError) - } forAll(nonPositiveLong) { fee => posting(tr.copy(fee = fee)) should produce(InsufficientFee()) } @@ -159,7 +155,11 @@ class AssetsBroadcastRouteSpec extends RouteSpec("/assets/broadcast/") with Requ (alwaysApproveUtx.putIfNew _).when(*).onCall((_: Transaction) => Right((true, Diff.empty))).anyNumberOfTimes() val alwaysSendAllChannels = stub[ChannelGroup] - (alwaysSendAllChannels.writeAndFlush(_: Any)).when(*).onCall((_: Any) => null).anyNumberOfTimes() + (alwaysSendAllChannels + .writeAndFlush(_: Any, _: ChannelMatcher)) + .when(*, *) + .onCall((_: Any, _: ChannelMatcher) => stub[ChannelGroupFuture]) + .anyNumberOfTimes() val route = AssetsBroadcastApiRoute(settings, alwaysApproveUtx, alwaysSendAllChannels).route @@ -213,7 +213,7 @@ class AssetsBroadcastRouteSpec extends RouteSpec("/assets/broadcast/") with Requ } "returns a error if it is not a transfer request" in posting(issueReq.sample.get) ~> check { - status shouldNot be(StatusCodes.OK) + status shouldBe StatusCodes.BadRequest } } @@ -250,7 +250,7 @@ class AssetsBroadcastRouteSpec extends RouteSpec("/assets/broadcast/") with Requ } "returns a error if it is not a transfer request" in posting(List(issueReq.sample.get)) ~> check { - status shouldNot be(StatusCodes.OK) + status shouldBe StatusCodes.BadRequest } } diff --git a/src/test/scala/com/zbsnetwork/http/AssetsRouteSpec.scala b/src/test/scala/com/zbsnetwork/http/AssetsRouteSpec.scala index bcd5a89..0a22ffd 100644 --- a/src/test/scala/com/zbsnetwork/http/AssetsRouteSpec.scala +++ b/src/test/scala/com/zbsnetwork/http/AssetsRouteSpec.scala @@ -7,7 +7,7 @@ import com.zbsnetwork.settings.RestAPISettings import com.zbsnetwork.state.{Blockchain, Diff} import com.zbsnetwork.utx.UtxPool import com.zbsnetwork.{RequestGen, TestTime} -import io.netty.channel.group.ChannelGroup +import io.netty.channel.group.{ChannelGroup, ChannelGroupFuture, ChannelMatcher} import org.scalamock.scalatest.PathMockFactory import org.scalatest.concurrent.Eventually import play.api.libs.json.Writes @@ -31,7 +31,7 @@ class AssetsRouteSpec extends RouteSpec("/assets") with RequestGen with PathMock (wallet.privateKeyAccount _).when(senderPrivateKey.toAddress).onCall((_: Address) => Right(senderPrivateKey)).anyNumberOfTimes() (utx.putIfNew _).when(*).onCall((_: Transaction) => Right((true, Diff.empty))).anyNumberOfTimes() - (allChannels.writeAndFlush(_: Any)).when(*).onCall((_: Any) => null).anyNumberOfTimes() + (allChannels.writeAndFlush(_: Any, _: ChannelMatcher)).when(*, *).onCall((_: Any, _: ChannelMatcher) => stub[ChannelGroupFuture]).anyNumberOfTimes() "/transfer" - { val route = AssetsApiRoute(settings, wallet, utx, allChannels, state, new TestTime()).route diff --git a/src/test/scala/com/zbsnetwork/http/LeaseBroadcastRouteSpec.scala b/src/test/scala/com/zbsnetwork/http/LeaseBroadcastRouteSpec.scala index fedf69e..f4126bd 100644 --- a/src/test/scala/com/zbsnetwork/http/LeaseBroadcastRouteSpec.scala +++ b/src/test/scala/com/zbsnetwork/http/LeaseBroadcastRouteSpec.scala @@ -25,7 +25,7 @@ class LeaseBroadcastRouteSpec extends RouteSpec("/leasing/broadcast/") with Requ (utx.putIfNew _).when(*).onCall((t: Transaction) => Left(TransactionValidationError(GenericError("foo"), t))).anyNumberOfTimes() - "returns StateCheckFiled" - { + "returns StateCheckFailed" - { val route = LeaseBroadcastApiRoute(settings, utx, allChannels).route val vt = Table[String, G[_ <: Transaction], (JsValue) => JsValue]( diff --git a/src/test/scala/com/zbsnetwork/http/PaymentRouteSpec.scala b/src/test/scala/com/zbsnetwork/http/PaymentRouteSpec.scala index 7ae544e..0939680 100644 --- a/src/test/scala/com/zbsnetwork/http/PaymentRouteSpec.scala +++ b/src/test/scala/com/zbsnetwork/http/PaymentRouteSpec.scala @@ -9,7 +9,7 @@ import com.zbsnetwork.transaction.transfer._ import com.zbsnetwork.utils.Time import com.zbsnetwork.utx.UtxPool import com.zbsnetwork.{NoShrink, TestWallet, TransactionGen} -import io.netty.channel.group.ChannelGroup +import io.netty.channel.group.{ChannelGroup, ChannelGroupFuture, ChannelMatcher} import org.scalamock.scalatest.MockFactory import org.scalatest.prop.PropertyChecks import play.api.libs.json.{JsObject, Json} @@ -23,10 +23,12 @@ class PaymentRouteSpec with TransactionGen with NoShrink { - private val utx = stub[UtxPool] - (utx.putIfNew _).when(*).onCall((t: Transaction) => Right((true, Diff.empty))).anyNumberOfTimes() + private val utx = stub[UtxPool] private val allChannels = stub[ChannelGroup] + (utx.putIfNew _).when(*).onCall((t: Transaction) => Right((true, Diff.empty))).anyNumberOfTimes() + (allChannels.writeAndFlush(_: Any, _: ChannelMatcher)).when(*, *).onCall((_: Any, _: ChannelMatcher) => stub[ChannelGroupFuture]).anyNumberOfTimes() + "accepts payments" in { forAll(accountOrAliasGen.label("recipient"), positiveLongGen.label("amount"), smallFeeGen.label("fee")) { case (recipient, amount, fee) => diff --git a/src/test/scala/com/zbsnetwork/matcher/AddressActorSpecification.scala b/src/test/scala/com/zbsnetwork/matcher/AddressActorSpecification.scala index ab6f4d6..7e0ad72 100644 --- a/src/test/scala/com/zbsnetwork/matcher/AddressActorSpecification.scala +++ b/src/test/scala/com/zbsnetwork/matcher/AddressActorSpecification.scala @@ -184,20 +184,24 @@ class AddressActorSpecification Props( new AddressActor( address, - currentPortfolio.get(), - 1.day, + x => currentPortfolio.get().spendableBalanceOf(x), 1.day, ntpTime, EmptyOrderDB, + _ => false, event => { eventsProbe.ref ! event Future.successful(QueueEventWithMeta(0, 0, event)) } ))) - f(addressActor, eventsProbe, (updatedPortfolio, notify) => { - currentPortfolio.set(updatedPortfolio) - if (notify) addressActor ! BalanceUpdated - }) + f( + addressActor, + eventsProbe, + (updatedPortfolio, notify) => { + val prevPortfolio = currentPortfolio.getAndSet(updatedPortfolio) + if (notify) addressActor ! BalanceUpdated(prevPortfolio.changedAssetIds(updatedPortfolio)) + } + ) addressActor ! PoisonPill } diff --git a/src/test/scala/com/zbsnetwork/matcher/EmptyOrderDB.scala b/src/test/scala/com/zbsnetwork/matcher/EmptyOrderDB.scala index 98e1d4b..f3eb919 100644 --- a/src/test/scala/com/zbsnetwork/matcher/EmptyOrderDB.scala +++ b/src/test/scala/com/zbsnetwork/matcher/EmptyOrderDB.scala @@ -6,7 +6,7 @@ import com.zbsnetwork.matcher.model.{OrderInfo, OrderStatus} import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order} object EmptyOrderDB extends OrderDB { - override def contains(id: ByteStr): Boolean = false + override def containsInfo(id: ByteStr): Boolean = false override def status(id: ByteStr): OrderStatus.Final = OrderStatus.NotFound override def saveOrderInfo(id: ByteStr, sender: Address, oi: OrderInfo[OrderStatus.Final]): Unit = {} override def saveOrder(o: Order): Unit = {} diff --git a/src/test/scala/com/zbsnetwork/matcher/TestOrderDB.scala b/src/test/scala/com/zbsnetwork/matcher/TestOrderDB.scala index 4aed4bb..044da10 100644 --- a/src/test/scala/com/zbsnetwork/matcher/TestOrderDB.scala +++ b/src/test/scala/com/zbsnetwork/matcher/TestOrderDB.scala @@ -11,11 +11,11 @@ class TestOrderDB(maxOrdersPerRequest: Int) extends OrderDB { private var idsForPair = Map.empty[(Address, AssetPair), Seq[ByteStr]].withDefaultValue(Seq.empty) private var idsForAddress = Map.empty[Address, Seq[ByteStr]].withDefaultValue(Seq.empty) - override def contains(id: ByteStr): Boolean = knownOrders.contains(id) + override def containsInfo(id: ByteStr): Boolean = orderInfo.contains(id) override def status(id: ByteStr): OrderStatus.Final = orderInfo.get(id).fold[OrderStatus.Final](OrderStatus.NotFound)(_.status) - override def saveOrderInfo(id: ByteStr, sender: Address, oi: OrderInfo[OrderStatus.Final]): Unit = if (!orderInfo.contains(id)) { + override def saveOrderInfo(id: ByteStr, sender: Address, oi: OrderInfo[OrderStatus.Final]): Unit = if (!containsInfo(id)) { orderInfo += id -> oi idsForAddress += sender -> (id +: idsForAddress(sender)) idsForPair += (sender, oi.assetPair) -> (id +: idsForPair(sender -> oi.assetPair)) diff --git a/src/test/scala/com/zbsnetwork/matcher/market/MatcherActorSpecification.scala b/src/test/scala/com/zbsnetwork/matcher/market/MatcherActorSpecification.scala index 7a7338b..88049ad 100644 --- a/src/test/scala/com/zbsnetwork/matcher/market/MatcherActorSpecification.scala +++ b/src/test/scala/com/zbsnetwork/matcher/market/MatcherActorSpecification.scala @@ -99,8 +99,10 @@ class MatcherActorSpecification actor ! wrap(order1) actor ! wrap(order2) - ob.get()(pair1) shouldBe 'right - ob.get()(pair2) shouldBe 'right + eventually { + ob.get()(pair1) shouldBe 'right + ob.get()(pair2) shouldBe 'right + } val toKill = actor.getChild(List(OrderBookActor.name(pair1)).iterator) diff --git a/src/test/scala/com/zbsnetwork/matcher/market/OrderBookActorSpecification.scala b/src/test/scala/com/zbsnetwork/matcher/market/OrderBookActorSpecification.scala index cf291d1..e04456a 100644 --- a/src/test/scala/com/zbsnetwork/matcher/market/OrderBookActorSpecification.scala +++ b/src/test/scala/com/zbsnetwork/matcher/market/OrderBookActorSpecification.scala @@ -5,7 +5,6 @@ import java.util.concurrent.ConcurrentHashMap import akka.actor.{ActorRef, Props} import akka.testkit.{ImplicitSender, TestProbe} import com.zbsnetwork.NTPTime -import com.zbsnetwork.OrderOps._ import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.matcher.MatcherTestData import com.zbsnetwork.matcher.api.AlreadyProcessed @@ -15,6 +14,7 @@ import com.zbsnetwork.matcher.market.MatcherActor.SaveSnapshot import com.zbsnetwork.matcher.market.OrderBookActor._ import com.zbsnetwork.matcher.model.Events.OrderAdded import com.zbsnetwork.matcher.model._ +import com.zbsnetwork.transaction.assets.exchange.OrderOps._ import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order} import com.zbsnetwork.utils.EmptyBlockchain import org.scalamock.scalatest.PathMockFactory diff --git a/src/test/scala/com/zbsnetwork/matcher/market/OrderValidatorSpecification.scala b/src/test/scala/com/zbsnetwork/matcher/market/OrderValidatorSpecification.scala index a3a5104..41d31c0 100644 --- a/src/test/scala/com/zbsnetwork/matcher/market/OrderValidatorSpecification.scala +++ b/src/test/scala/com/zbsnetwork/matcher/market/OrderValidatorSpecification.scala @@ -1,7 +1,6 @@ package com.zbsnetwork.matcher.market import com.google.common.base.Charsets -import com.zbsnetwork.OrderOps._ import com.zbsnetwork.account.{Address, PrivateKeyAccount} import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 @@ -13,6 +12,7 @@ import com.zbsnetwork.matcher.model._ import com.zbsnetwork.settings.Constants import com.zbsnetwork.state.diffs.produce import com.zbsnetwork.state.{AssetDescription, Blockchain, LeaseBalance, Portfolio} +import com.zbsnetwork.transaction.assets.exchange.OrderOps._ import com.zbsnetwork.transaction.assets.exchange._ import com.zbsnetwork.transaction.smart.script.ScriptCompiler import com.zbsnetwork.transaction.smart.script.v1.ExprScript @@ -36,7 +36,6 @@ class OrderValidatorSpecification private val wbtc = mkAssetId("WBTC").get private val pairZbsBtc = AssetPair(None, Some(wbtc)) - private val defaultTs = 1000 private val defaultPortfolio = Portfolio(0, LeaseBalance.empty, Map(wbtc -> 10 * Constants.UnitsInZbs)) @@ -116,28 +115,10 @@ class OrderValidatorSpecification ov(order) should produce("Script doesn't exist and proof doesn't validate as signature") } - "default ts - drift > its for new users" in { + "order exists" in { val pk = PrivateKeyAccount(randomBytes()) - val ov = OrderValidator.accountStateAware(pk, defaultPortfolio.balanceOf, 0, 0L, _ => false)(_) - ov(newBuyOrder(pk, defaultTs - matcherSettings.orderTimestampDrift - 1)) should produce("Order should have a timestamp") - } - - "default ts - drift = its ts for new users" in { - val pk = PrivateKeyAccount(randomBytes()) - val ov = OrderValidator.accountStateAware(pk, defaultPortfolio.balanceOf, 0, 0L, _ => false)(_) - ov(newBuyOrder(pk, defaultTs - matcherSettings.orderTimestampDrift)) should produce("Order should have a timestamp") - } - - "ts1 - drift > ts2" in { - val pk = PrivateKeyAccount(randomBytes()) - val ov = OrderValidator.accountStateAware(pk, defaultPortfolio.balanceOf, 0, defaultTs + 1000, _ => false)(_) - ov(newBuyOrder(pk, defaultTs + 999 - matcherSettings.orderTimestampDrift)) should produce("Order should have a timestamp") - } - - "ts1 - drift = ts2" in { - val pk = PrivateKeyAccount(randomBytes()) - val ov = OrderValidator.accountStateAware(pk, defaultPortfolio.balanceOf, 0, defaultTs + 1000, _ => false)(_) - ov(newBuyOrder(pk, defaultTs + 1000 - matcherSettings.orderTimestampDrift)) should produce("Order should have a timestamp") + val ov = OrderValidator.accountStateAware(pk, defaultPortfolio.balanceOf, 1, _ => true)(_) + ov(newBuyOrder(pk, 1000)) should produce("Order has already been placed") } "order price has invalid non-zero trailing decimals" in forAll(assetIdGen(1), accountGen, Gen.choose(1, 7)) { @@ -332,7 +313,7 @@ class OrderValidatorSpecification orderStatus: ByteStr => Boolean = _ => false, o: Order = newBuyOrder )(f: Either[String, Order] => A): A = - f(OrderValidator.accountStateAware(o.sender, tradableBalance(p), 0, 0, orderStatus)(o)) + f(OrderValidator.accountStateAware(o.sender, tradableBalance(p), 0, orderStatus)(o)) private def msa(ba: Set[Address], o: Order) = OrderValidator.matcherSettingsAware(o.matcherPublicKey, ba, Set.empty) _ } diff --git a/src/test/scala/com/zbsnetwork/matcher/matching/ReservedBalanceSpecification.scala b/src/test/scala/com/zbsnetwork/matcher/matching/ReservedBalanceSpecification.scala index 2640bbb..cf3b274 100644 --- a/src/test/scala/com/zbsnetwork/matcher/matching/ReservedBalanceSpecification.scala +++ b/src/test/scala/com/zbsnetwork/matcher/matching/ReservedBalanceSpecification.scala @@ -8,7 +8,6 @@ import com.zbsnetwork.matcher.market.MatcherSpecLike import com.zbsnetwork.matcher.model.Events.{OrderAdded, OrderExecuted} import com.zbsnetwork.matcher.model.{LimitOrder, OrderHistoryStub} import com.zbsnetwork.matcher.{AssetPairDecimals, MatcherTestData, _} -import com.zbsnetwork.state.Portfolio import com.zbsnetwork.transaction.AssetId import com.zbsnetwork.transaction.assets.exchange.OrderType.{BUY, SELL} import com.zbsnetwork.transaction.assets.exchange.{AssetPair, Order, OrderType} @@ -83,12 +82,19 @@ class ReservedBalanceSpecification private val addressDir = system.actorOf( Props( new AddressDirectory( - ignorePortfolioChanged, - _ => Portfolio.empty, - _ => Future.failed(new IllegalStateException("Should not be used in the test")), + ignoreSpendableBalanceChanged, matcherSettings, - ntpTime, - new TestOrderDB(100) + address => + Props( + new AddressActor( + address, + _ => 0L, + 5.seconds, + ntpTime, + new TestOrderDB(100), + _ => false, + _ => Future.failed(new IllegalStateException("Should not be used in the test")) + )) ) )) diff --git a/src/test/scala/com/zbsnetwork/matcher/model/OrderDBSpec.scala b/src/test/scala/com/zbsnetwork/matcher/model/OrderDBSpec.scala index 8d13896..4893c50 100644 --- a/src/test/scala/com/zbsnetwork/matcher/model/OrderDBSpec.scala +++ b/src/test/scala/com/zbsnetwork/matcher/model/OrderDBSpec.scala @@ -35,9 +35,8 @@ class OrderDBSpec extends FreeSpec with Matchers with WithDB with MatcherTestDat "stores" - { "order" in test { odb => forAll(orderGenerator) { - case (order, _) => - odb.saveOrder(order) - odb.contains(order.id()) shouldBe true + case (o, _) => + odb.saveOrder(o) } } @@ -45,6 +44,7 @@ class OrderDBSpec extends FreeSpec with Matchers with WithDB with MatcherTestDat forAll(finalizedOrderInfoGen) { case (o, oi) => odb.saveOrderInfo(o.id(), o.sender, oi) + odb.containsInfo(o.id()) shouldBe true odb.status(o.id()) shouldBe oi.status } } diff --git a/src/test/scala/com/zbsnetwork/matcher/model/OrderHistoryStub.scala b/src/test/scala/com/zbsnetwork/matcher/model/OrderHistoryStub.scala index f445daa..eef04c1 100644 --- a/src/test/scala/com/zbsnetwork/matcher/model/OrderHistoryStub.scala +++ b/src/test/scala/com/zbsnetwork/matcher/model/OrderHistoryStub.scala @@ -5,7 +5,6 @@ import com.zbsnetwork.account.Address import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.matcher.queue.QueueEventWithMeta import com.zbsnetwork.matcher.{AddressActor, TestOrderDB} -import com.zbsnetwork.state.Portfolio import com.zbsnetwork.utils.Time import scala.collection.mutable @@ -23,11 +22,11 @@ class OrderHistoryStub(system: ActorSystem, time: Time) { Props( new AddressActor( lo.order.sender, - Portfolio.empty, - 5.seconds, + _ => 0L, 5.seconds, time, new TestOrderDB(100), + _ => false, e => Future.successful(QueueEventWithMeta(0, 0, e)), ))) ) diff --git a/src/test/scala/com/zbsnetwork/mining/BlockWithMaxBaseTargetTest.scala b/src/test/scala/com/zbsnetwork/mining/BlockWithMaxBaseTargetTest.scala index f4350ba..0fa0680 100644 --- a/src/test/scala/com/zbsnetwork/mining/BlockWithMaxBaseTargetTest.scala +++ b/src/test/scala/com/zbsnetwork/mining/BlockWithMaxBaseTargetTest.scala @@ -18,7 +18,7 @@ import com.zbsnetwork.settings.{ZbsSettings, _} import com.zbsnetwork.state._ import com.zbsnetwork.state.appender.BlockAppender import com.zbsnetwork.state.diffs.ENOUGH_AMT -import com.zbsnetwork.transaction.{BlockchainUpdater, GenesisTransaction, Transaction} +import com.zbsnetwork.transaction.{AssetId, BlockchainUpdater, GenesisTransaction, Transaction} import com.zbsnetwork.utils.BaseTargetReachedMaximum import com.zbsnetwork.utx.UtxPool import com.zbsnetwork.wallet.Wallet @@ -109,7 +109,7 @@ class BlockWithMaxBaseTargetTest extends FreeSpec with Matchers with WithDB with } def withEnv(f: Env => Unit): Unit = { - val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) + val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) val settings0 = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load())) val minerSettings = settings0.minerSettings.copy(quorum = 0) @@ -126,19 +126,19 @@ class BlockWithMaxBaseTargetTest extends FreeSpec with Matchers with WithDB with featuresSettings = settings0.featuresSettings.copy(autoShutdownOnUnsupportedFeature = false) ) - val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime) + val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime) val pos = new PoSSelector(bcu, settings.blockchainSettings, settings.synchronizationSettings) val utxPoolStub = new UtxPool { - override def putIfNew(tx: Transaction) = ??? - override def removeAll(txs: Traversable[Transaction]): Unit = {} - override def accountPortfolio(addr: Address) = ??? - override def portfolio(addr: Address) = ??? - override def all = ??? - override def size = ??? - override def transactionById(transactionId: ByteStr) = ??? - override def packUnconfirmed(rest: MultiDimensionalMiningConstraint) = ??? - override def close(): Unit = {} + override def putIfNew(tx: Transaction) = ??? + override def removeAll(txs: Traversable[Transaction]): Unit = {} + override def spendableBalance(addr: Address, assetId: Option[AssetId]): Long = ??? + override def pessimisticPortfolio(addr: Address): Portfolio = ??? + override def all = ??? + override def size = ??? + override def transactionById(transactionId: ByteStr) = ??? + override def packUnconfirmed(rest: MultiDimensionalMiningConstraint) = ??? + override def close(): Unit = {} } val schedulerService: SchedulerService = Scheduler.singleThread("appender") diff --git a/src/test/scala/com/zbsnetwork/settings/MatcherSettingsSpecification.scala b/src/test/scala/com/zbsnetwork/settings/MatcherSettingsSpecification.scala index 73924b1..2a01872 100644 --- a/src/test/scala/com/zbsnetwork/settings/MatcherSettingsSpecification.scala +++ b/src/test/scala/com/zbsnetwork/settings/MatcherSettingsSpecification.scala @@ -25,13 +25,11 @@ class MatcherSettingsSpecification extends FlatSpec with Matchers { | snapshots-loading-timeout = 423s | start-events-processing-timeout = 543s | rest-order-limit = 100 - | order-timestamp-drift = 10m | price-assets = [ | ZBS | 8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS | DHgwrRvVyqJsepd32YbBqUeDH4GJ1N984X8QoekjgH8J | ] - | max-timestamp-diff = 30d | blacklisted-assets = ["a"] | blacklisted-names = ["b"] | blacklisted-addresses = [ @@ -80,7 +78,6 @@ class MatcherSettingsSpecification extends FlatSpec with Matchers { settings.snapshotsLoadingTimeout should be(423.seconds) settings.startEventsProcessingTimeout should be(543.seconds) settings.maxOrdersPerRequest should be(100) - settings.orderTimestampDrift should be(10.minutes.toMillis) settings.priceAssets should be(Seq("ZBS", "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", "DHgwrRvVyqJsepd32YbBqUeDH4GJ1N984X8QoekjgH8J")) settings.blacklistedAssets shouldBe Set("a") settings.blacklistedNames.map(_.pattern.pattern()) shouldBe Seq("b") diff --git a/src/test/scala/com/zbsnetwork/settings/SynchronizationSettingsSpecification.scala b/src/test/scala/com/zbsnetwork/settings/SynchronizationSettingsSpecification.scala index a66ebb9..ef4a653 100644 --- a/src/test/scala/com/zbsnetwork/settings/SynchronizationSettingsSpecification.scala +++ b/src/test/scala/com/zbsnetwork/settings/SynchronizationSettingsSpecification.scala @@ -32,6 +32,9 @@ class SynchronizationSettingsSpecification extends FlatSpec with Matchers { | network-tx-cache-time = 70s | max-buffer-size = 777 | max-buffer-time = 999ms + | max-queue-size = 7777 + | parallelism = 4 + | max-threads = 2 | } | | micro-block-synchronizer { @@ -62,7 +65,6 @@ class SynchronizationSettingsSpecification extends FlatSpec with Matchers { maxBlockCacheSize = 2 ) - settings.utxSynchronizerSettings shouldBe UtxSynchronizerSettings(7000000, 70.seconds, 777, 999.millis) - + settings.utxSynchronizerSettings shouldBe UtxSynchronizerSettings(7000000, 70.seconds, 777, 999.millis, 4, 2, 7777) } } diff --git a/src/test/scala/com/zbsnetwork/settings/TestFunctionalitySettings.scala b/src/test/scala/com/zbsnetwork/settings/TestFunctionalitySettings.scala index 2e5aec6..1fd220a 100644 --- a/src/test/scala/com/zbsnetwork/settings/TestFunctionalitySettings.scala +++ b/src/test/scala/com/zbsnetwork/settings/TestFunctionalitySettings.scala @@ -16,10 +16,12 @@ object TestFunctionalitySettings { allowMultipleLeaseCancelTransactionUntilTimestamp = 0L, resetEffectiveBalancesAtHeight = 0, blockVersion3AfterHeight = 0, - preActivatedFeatures = Map(BlockchainFeatures.SmartAccounts.id -> 0, - BlockchainFeatures.SmartAssets.id -> 0, - BlockchainFeatures.FairPoS.id -> 0, - BlockchainFeatures.Ride4DApps.id -> 0), + preActivatedFeatures = Map( + BlockchainFeatures.SmartAccounts.id -> 0, + BlockchainFeatures.SmartAssets.id -> 0, + BlockchainFeatures.FairPoS.id -> 0, + BlockchainFeatures.Ride4DApps.id -> 0 + ), doubleFeaturesPeriodsAfterHeight = Int.MaxValue, maxTransactionTimeBackOffset = 120.minutes, maxTransactionTimeForwardOffset = 90.minutes diff --git a/src/test/scala/com/zbsnetwork/settings/UTXSettingsSpecification.scala b/src/test/scala/com/zbsnetwork/settings/UTXSettingsSpecification.scala index c04403e..9a360ea 100644 --- a/src/test/scala/com/zbsnetwork/settings/UTXSettingsSpecification.scala +++ b/src/test/scala/com/zbsnetwork/settings/UTXSettingsSpecification.scala @@ -5,15 +5,12 @@ import net.ceedubs.ficus.Ficus._ import net.ceedubs.ficus.readers.ArbitraryTypeReader._ import org.scalatest.{FlatSpec, Matchers} -import scala.concurrent.duration._ - class UTXSettingsSpecification extends FlatSpec with Matchers { "UTXSettings" should "read values" in { val config = ConfigFactory.parseString("""zbs { | utx { | max-size = 100 | max-bytes-size = 100 - | cleanup-interval = 10m | blacklist-sender-addresses = ["a"] | allow-blacklisted-transfer-to = ["b"] | allow-transactions-from-smart-accounts = false @@ -23,7 +20,6 @@ class UTXSettingsSpecification extends FlatSpec with Matchers { val settings = config.as[UtxSettings]("zbs.utx") settings.maxSize shouldBe 100 settings.maxBytesSize shouldBe 100L - settings.cleanupInterval shouldBe 10.minutes settings.blacklistSenderAddresses shouldBe Set("a") settings.allowBlacklistedTransferTo shouldBe Set("b") settings.allowTransactionsFromSmartAccounts shouldBe false diff --git a/src/test/scala/com/zbsnetwork/state/BlockchainUpdaterImplSpec.scala b/src/test/scala/com/zbsnetwork/state/BlockchainUpdaterImplSpec.scala index 1310ac8..34df4c4 100644 --- a/src/test/scala/com/zbsnetwork/state/BlockchainUpdaterImplSpec.scala +++ b/src/test/scala/com/zbsnetwork/state/BlockchainUpdaterImplSpec.scala @@ -19,9 +19,10 @@ import org.scalatest.{FreeSpec, Matchers} class BlockchainUpdaterImplSpec extends FreeSpec with Matchers with WithDB with RequestGen with NTPTime with DBCacheSettings { def baseTest(gen: Time => Gen[(PrivateKeyAccount, Seq[Block])])(f: (BlockchainUpdaterImpl, PrivateKeyAccount) => Unit): Unit = { - val defaultWriter = new LevelDBWriter(db, ignorePortfolioChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) + val defaultWriter = new LevelDBWriter(db, ignoreSpendableBalanceChanged, TestFunctionalitySettings.Stub, maxCacheSize, 2000, 120 * 60 * 1000) val settings = ZbsSettings.fromConfig(loadConfig(ConfigFactory.load())) - val bcu = new BlockchainUpdaterImpl(defaultWriter, ignorePortfolioChanged, settings, ntpTime) + val bcu = new BlockchainUpdaterImpl(defaultWriter, ignoreSpendableBalanceChanged, settings, ntpTime) + try { val (account, blocks) = gen(ntpTime).sample.get diff --git a/src/test/scala/com/zbsnetwork/state/NgStateTest.scala b/src/test/scala/com/zbsnetwork/state/NgStateTest.scala new file mode 100644 index 0000000..e444dbd --- /dev/null +++ b/src/test/scala/com/zbsnetwork/state/NgStateTest.scala @@ -0,0 +1,96 @@ +package com.zbsnetwork.state + +import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.history._ +import com.zbsnetwork.state.diffs._ +import com.zbsnetwork.transaction.GenesisTransaction +import com.zbsnetwork.transaction.transfer._ +import com.zbsnetwork.{NoShrink, TransactionGen} +import org.scalacheck.Gen +import org.scalatest.prop.PropertyChecks +import org.scalatest.{Matchers, PropSpec} + +class NgStateTest extends PropSpec with PropertyChecks with Matchers with TransactionGen with NoShrink { + + def preconditionsAndPayments(amt: Int): Gen[(GenesisTransaction, Seq[TransferTransactionV1])] = + for { + master <- accountGen + recipient <- accountGen + ts <- positiveIntGen + genesis: GenesisTransaction = GenesisTransaction.create(master, ENOUGH_AMT, ts).explicitGet() + payments: Seq[TransferTransactionV1] <- Gen.listOfN(amt, zbsTransferGeneratorP(master, recipient)) + } yield (genesis, payments) + + property("can forge correctly signed blocks") { + forAll(preconditionsAndPayments(10)) { + case (genesis, payments) => + val (block, microBlocks) = chainBaseAndMicro(randomSig, genesis, payments.map(t => Seq(t))) + + val ng = new NgState(block, Diff.empty, 0L, Set.empty) + microBlocks.foreach(m => ng.append(m, Diff.empty, 0L, 0L)) + + ng.totalDiffOf(microBlocks.last.totalResBlockSig) + microBlocks.foreach { m => + ng.totalDiffOf(m.totalResBlockSig).get match { + case (forged, _, _, _) => forged.signaturesValid() shouldBe 'right + case _ => ??? + } + } + Seq(microBlocks(4)).map(x => ng.totalDiffOf(x.totalResBlockSig)) + } + } + + property("can resolve best liquid block") { + forAll(preconditionsAndPayments(5)) { + case (genesis, payments) => + val (block, microBlocks) = chainBaseAndMicro(randomSig, genesis, payments.map(t => Seq(t))) + + val ng = new NgState(block, Diff.empty, 0L, Set.empty) + microBlocks.foreach(m => ng.append(m, Diff.empty, 0L, 0L)) + + ng.bestLiquidBlock.uniqueId shouldBe microBlocks.last.totalResBlockSig + + new NgState(block, Diff.empty, 0L, Set.empty).bestLiquidBlock.uniqueId shouldBe block.uniqueId + } + } + + property("can resolve best last block") { + forAll(preconditionsAndPayments(5)) { + case (genesis, payments) => + val (block, microBlocks) = chainBaseAndMicro(randomSig, genesis, payments.map(t => Seq(t))) + + val ng = new NgState(block, Diff.empty, 0L, Set.empty) + + microBlocks.foldLeft(1000) { + case (thisTime, m) => + ng.append(m, Diff.empty, 0L, thisTime) + thisTime + 50 + } + + ng.bestLastBlockInfo(0).blockId shouldBe block.uniqueId + ng.bestLastBlockInfo(1001).blockId shouldBe microBlocks.head.totalResBlockSig + ng.bestLastBlockInfo(1051).blockId shouldBe microBlocks.tail.head.totalResBlockSig + ng.bestLastBlockInfo(2000).blockId shouldBe microBlocks.last.totalResBlockSig + + new NgState(block, Diff.empty, 0L, Set.empty).bestLiquidBlock.uniqueId shouldBe block.uniqueId + } + } + + property("calculates carry fee correctly") { + forAll(preconditionsAndPayments(5)) { + case (genesis, payments) => + val (block, microBlocks) = chainBaseAndMicro(randomSig, genesis, payments.map(t => Seq(t))) + + val ng = new NgState(block, Diff.empty, 0L, Set.empty) + microBlocks.foreach(m => ng.append(m, Diff.empty, 1L, 0L)) + + ng.totalDiffOf(block.uniqueId).map(_._3) shouldBe Some(0L) + microBlocks.zipWithIndex.foreach { + case (m, i) => + val u = ng.totalDiffOf(m.totalResBlockSig).map(_._3) + u shouldBe Some(i + 1) + } + ng.carryFee shouldBe microBlocks.size + } + } +} diff --git a/src/test/scala/com/zbsnetwork/state/diffs/AssetTransactionsDiffTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/AssetTransactionsDiffTest.scala index 87e24c9..8d27458 100644 --- a/src/test/scala/com/zbsnetwork/state/diffs/AssetTransactionsDiffTest.scala +++ b/src/test/scala/com/zbsnetwork/state/diffs/AssetTransactionsDiffTest.scala @@ -286,7 +286,7 @@ class AssetTransactionsDiffTest extends PropSpec with PropertyChecks with Matche case (blockDiff, newState) => val totalPortfolioDiff = Monoid.combineAll(blockDiff.portfolios.values) totalPortfolioDiff.assets(issue.id()) shouldEqual issue.quantity - newState.portfolio(newState.resolveAlias(transfer.recipient).explicitGet()).assets(issue.id()) shouldEqual transfer.amount + newState.balance(newState.resolveAlias(transfer.recipient).explicitGet(), Some(issue.id())) shouldEqual transfer.amount } } } diff --git a/src/test/scala/com/zbsnetwork/state/diffs/BlockDifferTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/BlockDifferTest.scala index d5abb68..2f8870f 100644 --- a/src/test/scala/com/zbsnetwork/state/diffs/BlockDifferTest.scala +++ b/src/test/scala/com/zbsnetwork/state/diffs/BlockDifferTest.scala @@ -52,12 +52,12 @@ class BlockDifferTest extends FreeSpecLike with Matchers with BlockGen with With "height < enableMicroblocksAfterHeight - a miner should receive 100% of the current block's fee" in { assertDiff(testChain.init, 1000) { case (_, s) => - s.portfolio(signerA).balance shouldBe 40 + s.balance(signerA) shouldBe 40 } assertDiff(testChain, 1000) { case (_, s) => - s.portfolio(signerB).balance shouldBe 50 + s.balance(signerB) shouldBe 50 } } @@ -79,7 +79,7 @@ class BlockDifferTest extends FreeSpecLike with Matchers with BlockGen with With "height = enableMicroblocksAfterHeight - a miner should receive 40% of the current block's fee only" in { assertDiff(testChain, 9) { case (_, s) => - s.portfolio(signerB).balance shouldBe 44 + s.balance(signerB) shouldBe 44 } } @@ -101,12 +101,12 @@ class BlockDifferTest extends FreeSpecLike with Matchers with BlockGen with With "height > enableMicroblocksAfterHeight - a miner should receive 60% of previous block's fee and 40% of the current one" in { assertDiff(testChain.init, 4) { case (_, s) => - s.portfolio(signerA).balance shouldBe 34 + s.balance(signerA) shouldBe 34 } assertDiff(testChain, 4) { case (_, s) => - s.portfolio(signerB).balance shouldBe 50 + s.balance(signerB) shouldBe 50 } } } diff --git a/src/test/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiffTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiffTest.scala index fcf3bcb..aab56ba 100644 --- a/src/test/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiffTest.scala +++ b/src/test/scala/com/zbsnetwork/state/diffs/ContractInvocationTransactionDiffTest.scala @@ -15,13 +15,13 @@ import com.zbsnetwork.lang.v1.compiler.Terms._ import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.FieldNames import com.zbsnetwork.settings.TestFunctionalitySettings import com.zbsnetwork.state._ -import com.zbsnetwork.transaction.{AssetId, GenesisTransaction, ValidationError} -import com.zbsnetwork.transaction.smart.script.ContractScript -import com.zbsnetwork.transaction.smart.{ContractInvocationTransaction, SetScriptTransaction} -import com.zbsnetwork.transaction.smart.ContractInvocationTransaction.Payment import com.zbsnetwork.transaction.assets.IssueTransactionV2 +import com.zbsnetwork.transaction.smart.ContractInvocationTransaction.Payment +import com.zbsnetwork.transaction.smart.script.ContractScript import com.zbsnetwork.transaction.smart.script.v1.ExprScript +import com.zbsnetwork.transaction.smart.{ContractInvocationTransaction, SetScriptTransaction} import com.zbsnetwork.transaction.transfer.TransferTransactionV2 +import com.zbsnetwork.transaction.{AssetId, GenesisTransaction} import com.zbsnetwork.{NoShrink, TransactionGen, WithDB} import org.scalacheck.Gen import org.scalatest.prop.PropertyChecks @@ -29,6 +29,12 @@ import org.scalatest.{Matchers, PropSpec} class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks with Matchers with TransactionGen with NoShrink with WithDB { + def ciFee(sc: Int = 0): Gen[Long] = + Gen.choose( + CommonValidation.FeeUnit * CommonValidation.FeeConstants(ContractInvocationTransaction.typeId) + sc * CommonValidation.ScriptExtraFee, + CommonValidation.FeeUnit * CommonValidation.FeeConstants(ContractInvocationTransaction.typeId) + (sc + 1) * CommonValidation.ScriptExtraFee - 1 + ) + private val fs = TestFunctionalitySettings.Enabled.copy( preActivatedFeatures = Map(BlockchainFeatures.SmartAccounts.id -> 0, BlockchainFeatures.SmartAssets.id -> 0, BlockchainFeatures.Ride4DApps.id -> 0)) @@ -36,87 +42,109 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks val assetAllowed = ExprScript(TRUE).explicitGet() val assetBanned = ExprScript(FALSE).explicitGet() - def dataContract(senderBinding: String, argName: String, funcName: String) = Contract( - List.empty, - List( - CallableFunction( - CallableAnnotation(senderBinding), - Terms.FUNC( - funcName, - List(argName), + def dataContract(senderBinding: String, argName: String, funcName: String, bigData: Boolean) = { + val datas = + if (bigData) List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("argument"), CONST_STRING("abcde" * 1024))), REF("nil")) + else + List( + FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("argument"), REF(argName))), FUNCTION_CALL( - User(FieldNames.WriteSet), - List(FUNCTION_CALL( - Native(1102), + Native(1100), + List(FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), GETTER(GETTER(REF(senderBinding), "caller"), "bytes"))), REF("nil"))) + ) + + Contract( + List.empty, + List( + CallableFunction( + CallableAnnotation(senderBinding), + Terms.FUNC( + funcName, + List(argName), + FUNCTION_CALL( + User(FieldNames.WriteSet), List( - FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("argument"), REF(argName))), - FUNCTION_CALL(User("DataEntry"), List(CONST_STRING("sender"), GETTER(GETTER(REF(senderBinding), "caller"), "bytes"))) - ) - )) + FUNCTION_CALL( + Native(1100), + datas + )) + ) ) - ) - )), - None - ) + )), + None + ) + } def paymentContract(senderBinding: String, argName: String, funcName: String, recipientAddress: Address, recipientAmount: Long, - assetId: Option[AssetId] = None) = Contract( - List.empty, - List( - CallableFunction( - CallableAnnotation(senderBinding), - Terms.FUNC( - funcName, - List(argName), - FUNCTION_CALL( - User(FieldNames.TransferSet), - List(FUNCTION_CALL( - Native(1102), - List( - FUNCTION_CALL( - User(FieldNames.ContractTransfer), - List( - FUNCTION_CALL(User("Address"), List(CONST_BYTESTR(recipientAddress.bytes))), - CONST_LONG(recipientAmount), - assetId.fold(REF("unit"): EXPR)(id => CONST_BYTESTR(id)) - ) - ) - ) - )) + masspayment: Boolean, + paymentCount: Int = 11, + assetId: Option[AssetId] = None) = { + val oneTransfer = FUNCTION_CALL( + User(FieldNames.ContractTransfer), + List( + FUNCTION_CALL(User("Address"), List(CONST_BYTESTR(recipientAddress.bytes))), + CONST_LONG(recipientAmount), + assetId.fold(REF("unit"): EXPR)(id => CONST_BYTESTR(id)) + ) + ) + + val payments = + if (masspayment) + List(Range(0, paymentCount).foldRight(REF("nil"): EXPR) { + case (_, in) => + FUNCTION_CALL(Native(1100), List(oneTransfer, in)) + }) + else + List(FUNCTION_CALL(Native(1100), List(oneTransfer, REF("nil")))) + + Contract( + List.empty, + List( + CallableFunction( + CallableAnnotation(senderBinding), + Terms.FUNC( + funcName, + List(argName), + FUNCTION_CALL( + User(FieldNames.TransferSet), + payments + ) ) - ) - )), - None - ) + )), + None + ) + } - def dataContractGen(func: String) = + def dataContractGen(func: String, bigData: Boolean) = for { senderBinging <- validAliasStringGen argBinding <- validAliasStringGen - } yield dataContract(senderBinging, argBinding, func) + } yield dataContract(senderBinging, argBinding, func, bigData) - def paymentContractGen(address: Address, amount: Long, assetId: Option[AssetId] = None)(func: String) = + def paymentContractGen(address: Address, amount: Long, masspayment: Boolean, assetId: Option[AssetId] = None, paymentCount: Int = 11)( + func: String) = for { senderBinging <- validAliasStringGen argBinding <- validAliasStringGen - } yield paymentContract(senderBinging, argBinding, func, address, amount, assetId) + } yield paymentContract(senderBinging, argBinding, func, address, amount, masspayment, paymentCount, assetId) def preconditionsAndSetContract( senderBindingToContract: String => Gen[Contract], invokerGen: Gen[PrivateKeyAccount] = accountGen, masterGen: Gen[PrivateKeyAccount] = accountGen, - payment: Option[Payment] = None): Gen[(List[GenesisTransaction], SetScriptTransaction, ContractInvocationTransaction)] = + payment: Option[Payment] = None, + feeGen: Gen[Long] = ciFee(0)): Gen[(List[GenesisTransaction], SetScriptTransaction, ContractInvocationTransaction)] = for { master <- masterGen invoker <- invokerGen ts <- timestampGen genesis: GenesisTransaction = GenesisTransaction.create(master, ENOUGH_AMT, ts).explicitGet() genesis2: GenesisTransaction = GenesisTransaction.create(invoker, ENOUGH_AMT, ts).explicitGet() - fee <- smallFeeGen + fee <- feeGen arg <- genBoundedString(1, 32) funcBinding <- validAliasStringGen contract <- senderBindingToContract(funcBinding) @@ -128,7 +156,7 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks property("invoking contract results contract's state") { forAll(for { - r <- preconditionsAndSetContract(dataContractGen) + r <- preconditionsAndSetContract(s => dataContractGen(s, false)) } yield (r._1, r._2, r._3)) { case (genesis, setScript, ci) => assertDiffAndState(Seq(TestBlock.create(genesis ++ Seq(setScript))), TestBlock.create(Seq(ci)), fs) { @@ -142,11 +170,22 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks } } + property("can't more than 5kb of data") { + forAll(for { + r <- preconditionsAndSetContract(s => dataContractGen(s, true)) + } yield (r._1, r._2, r._3)) { + case (genesis, setScript, ci) => + assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(setScript))), TestBlock.create(Seq(ci)), fs) { + _ should produce("WriteSet size can't exceed") + } + } + } + property("invoking payment contract results in accounts state") { forAll(for { a <- accountGen am <- smallFeeGen - contractGen = (paymentContractGen(a, am) _) + contractGen = (paymentContractGen(a, am, false) _) r <- preconditionsAndSetContract(contractGen) } yield (a, am, r._1, r._2, r._3)) { case (acc, amount, genesis, setScript, ci) => @@ -157,20 +196,37 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks } } + property("can't make more than 10 payments") { + forAll(for { + a <- accountGen + am <- smallFeeGen + contractGen = (paymentContractGen(a, am, true) _) + r <- preconditionsAndSetContract(contractGen) + } yield (a, am, r._1, r._2, r._3)) { + case (acc, amount, genesis, setScript, ci) => + assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(setScript))), TestBlock.create(Seq(ci)), fs) { + _ should produce("many ContractTransfers") + } + } + } + val chainId = AddressScheme.current.chainId val enoughFee = CommonValidation.ScriptExtraFee + CommonValidation.FeeConstants(IssueTransactionV2.typeId) * CommonValidation.FeeUnit - property("invoking contract recive payment") { + property("invoking contract receive payment") { forAll(for { a <- accountGen am <- smallFeeGen - contractGen = (paymentContractGen(a, am) _) + contractGen = (paymentContractGen(a, am, false) _) invoker <- accountGen ts <- timestampGen asset = IssueTransactionV2 .selfSigned(chainId, invoker, "Asset#1".getBytes, "".getBytes, 1000000, 8, false, Some(assetAllowed), enoughFee, ts) .explicitGet() - r <- preconditionsAndSetContract(contractGen, invokerGen = Gen.oneOf(Seq(invoker)), payment = Some(Payment(1, Some(asset.id())))) + r <- preconditionsAndSetContract(contractGen, + invokerGen = Gen.oneOf(Seq(invoker)), + payment = Some(Payment(1, Some(asset.id()))), + feeGen = ciFee(1)) } yield (a, am, r._1, r._2, r._3, asset, invoker)) { case (acc, amount, genesis, setScript, ci, asset, invoker) => assertDiffAndState(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) { @@ -186,13 +242,16 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks forAll(for { a <- accountGen am <- smallFeeGen - contractGen = (paymentContractGen(a, am) _) + contractGen = (paymentContractGen(a, am, false) _) invoker <- accountGen ts <- timestampGen asset = IssueTransactionV2 .selfSigned(chainId, invoker, "Asset#1".getBytes, "".getBytes, 1000000, 8, false, Some(assetBanned), enoughFee, ts) .explicitGet() - r <- preconditionsAndSetContract(contractGen, invokerGen = Gen.oneOf(Seq(invoker)), payment = Some(Payment(1, Some(asset.id())))) + r <- preconditionsAndSetContract(contractGen, + invokerGen = Gen.oneOf(Seq(invoker)), + payment = Some(Payment(1, Some(asset.id()))), + feeGen = ciFee(1)) } yield (a, am, r._1, r._2, r._3, asset, invoker)) { case (acc, amount, genesis, setScript, ci, asset, invoker) => assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) { blockDiffEi => @@ -211,8 +270,8 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks asset = IssueTransactionV2 .selfSigned(chainId, master, "Asset#1".getBytes, "".getBytes, quantity, 8, false, Some(assetAllowed), enoughFee, ts) .explicitGet() - contractGen = (paymentContractGen(a, am, Some(asset.id())) _) - r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master))) + contractGen = (paymentContractGen(a, am, false, Some(asset.id())) _) + r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master)), feeGen = ciFee(1)) } yield (a, am, r._1, r._2, r._3, asset, master)) { case (acc, amount, genesis, setScript, ci, asset, master) => assertDiffAndState(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) { @@ -233,8 +292,8 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks asset = IssueTransactionV2 .selfSigned(chainId, master, "Asset#1".getBytes, "".getBytes, quantity, 8, false, Some(assetBanned), enoughFee, ts) .explicitGet() - contractGen = (paymentContractGen(a, am, Some(asset.id())) _) - r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master))) + contractGen = (paymentContractGen(a, am, false, Some(asset.id())) _) + r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master)), feeGen = ciFee(1)) } yield (a, am, r._1, r._2, r._3, asset, master)) { case (acc, amount, genesis, setScript, ci, asset, master) => assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) { blockDiffEi => @@ -253,8 +312,8 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks asset = IssueTransactionV2 .selfSigned(chainId, master, "Asset#1".getBytes, "".getBytes, quantity, 8, false, Some(assetAllowed), enoughFee, ts) .explicitGet() - contractGen = (paymentContractGen(a, -1, Some(asset.id())) _) - r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master))) + contractGen = (paymentContractGen(a, -1, false, Some(asset.id())) _) + r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master)), feeGen = ciFee(1)) } yield (a, am, r._1, r._2, r._3, asset, master, ts)) { case (acc, amount, genesis, setScript, ci, asset, master, ts) => val t = TransferTransactionV2.selfSigned(Some(asset.id()), master, acc, asset.quantity / 10, ts, None, enoughFee, Array[Byte]()).explicitGet() @@ -271,13 +330,86 @@ class ContractInvocationTransactionDiffTest extends PropSpec with PropertyChecks ts <- timestampGen arg <- genBoundedString(1, 32) funcBinding <- validAliasStringGen + fee <- ciFee(1) fc = Terms.FUNCTION_CALL(FunctionHeader.User(funcBinding), List(CONST_BYTESTR(ByteStr(arg)))) - ci = ContractInvocationTransaction.selfSigned(invoker, master, fc, Some(Payment(-1, None)), enoughFee, ts) - } yield (ci)) { ci => - ci shouldBe 'left - ci.left.get.isInstanceOf[ValidationError.NegativeAmount] should be(true) + ci = ContractInvocationTransaction.selfSigned(invoker, master, fc, Some(Payment(-1, None)), fee, ts) + } yield (ci)) { _ should produce("NegativeAmount") } + } + + property("smart asset payment require extra fee") { + forAll(for { + a <- accountGen + quantity = 1000000 + am <- Gen.choose[Long](1L, quantity) + master <- accountGen + ts <- timestampGen + asset = IssueTransactionV2 + .selfSigned(chainId, master, "Asset#1".getBytes, "".getBytes, quantity, 8, false, Some(assetBanned), enoughFee, ts) + .explicitGet() + contractGen = (paymentContractGen(a, am, false, Some(asset.id())) _) + r <- preconditionsAndSetContract(contractGen, masterGen = Gen.oneOf(Seq(master)), feeGen = ciFee(0)) + } yield (a, am, r._1, r._2, r._3, asset, master)) { + case (acc, amount, genesis, setScript, ci, asset, master) => + assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) { blockDiffEi => + blockDiffEi should produce("does not exceed minimal value") + } + } + } + property("contract with payment of smart asset require extra fee") { + forAll(for { + a <- accountGen + am <- smallFeeGen + contractGen = (paymentContractGen(a, am, false) _) + invoker <- accountGen + ts <- timestampGen + asset = IssueTransactionV2 + .selfSigned(chainId, invoker, "Asset#1".getBytes, "".getBytes, 1000000, 8, false, Some(assetAllowed), enoughFee, ts) + .explicitGet() + r <- preconditionsAndSetContract(contractGen, + invokerGen = Gen.oneOf(Seq(invoker)), + payment = Some(Payment(1, Some(asset.id()))), + feeGen = ciFee(0)) + } yield (a, am, r._1, r._2, r._3, asset, invoker)) { + case (acc, amount, genesis, setScript, ci, asset, invoker) => + assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(asset, setScript))), TestBlock.create(Seq(ci)), fs) { blockDiffEi => + blockDiffEi should produce("does not exceed minimal value") + } } } + property("can't overflow payment + fee") { + forAll(for { + a <- accountGen + am <- smallFeeGen + contractGen = (paymentContractGen(a, am, false) _) + invoker <- accountGen + ts <- timestampGen + r <- preconditionsAndSetContract(contractGen, + invokerGen = Gen.oneOf(Seq(invoker)), + payment = Some(Payment(Long.MaxValue, None)), + feeGen = ciFee(1)) + } yield (r._1, r._2, r._3)) { + case (genesis, setScript, ci) => + assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(setScript))), TestBlock.create(Seq(ci)), fs) { + _ should produce("Attempt to transfer unavailable funds") + } + } + } + + property("can't overflow sum of payment in contract") { + forAll(for { + a <- accountGen + am <- smallFeeGen + contractGen = (paymentContractGen(a, Long.MaxValue / 2 + 2, true, None, 4) _) + invoker <- accountGen + ts <- timestampGen + r <- preconditionsAndSetContract(contractGen, invokerGen = Gen.oneOf(Seq(invoker)), payment = Some(Payment(1, None)), feeGen = ciFee(1)) + } yield (r._1, r._2, r._3)) { + case (genesis, setScript, ci) => + assertDiffEi(Seq(TestBlock.create(genesis ++ Seq(setScript))), TestBlock.create(Seq(ci)), fs) { + _ should produce("Attempt to transfer unavailable funds") + } + } + } } diff --git a/src/test/scala/com/zbsnetwork/state/diffs/DataTransactionDiffTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/DataTransactionDiffTest.scala index 620390d..e3054e9 100644 --- a/src/test/scala/com/zbsnetwork/state/diffs/DataTransactionDiffTest.scala +++ b/src/test/scala/com/zbsnetwork/state/diffs/DataTransactionDiffTest.scala @@ -58,7 +58,7 @@ class DataTransactionDiffTest extends PropSpec with PropertyChecks with Matchers assertDiffAndState(Seq(genesis), blocks(0), fs) { case (totalDiff, state) => assertBalanceInvariant(totalDiff) - state.portfolio(sender).balance shouldBe (ENOUGH_AMT - txs(0).fee) + state.balance(sender) shouldBe (ENOUGH_AMT - txs(0).fee) state.accountData(sender, item1.key) shouldBe Some(item1) state.accountData(sender).data.get(item1.key) shouldBe Some(item1) } @@ -67,7 +67,7 @@ class DataTransactionDiffTest extends PropSpec with PropertyChecks with Matchers assertDiffAndState(Seq(genesis, blocks(0)), blocks(1), fs) { case (totalDiff, state) => assertBalanceInvariant(totalDiff) - state.portfolio(sender).balance shouldBe (ENOUGH_AMT - txs.take(2).map(_.fee).sum) + state.balance(sender) shouldBe (ENOUGH_AMT - txs.take(2).map(_.fee).sum) state.accountData(sender, item1.key) shouldBe Some(item1) state.accountData(sender).data.get(item1.key) shouldBe Some(item1) state.accountData(sender, item2.key) shouldBe Some(item2) @@ -78,7 +78,7 @@ class DataTransactionDiffTest extends PropSpec with PropertyChecks with Matchers assertDiffAndState(Seq(genesis, blocks(0), blocks(1)), blocks(2), fs) { case (totalDiff, state) => assertBalanceInvariant(totalDiff) - state.portfolio(sender).balance shouldBe (ENOUGH_AMT - txs.map(_.fee).sum) + state.balance(sender) shouldBe (ENOUGH_AMT - txs.map(_.fee).sum) state.accountData(sender, item1.key) shouldBe Some(item3) state.accountData(sender).data.get(item1.key) shouldBe Some(item3) state.accountData(sender, item2.key) shouldBe Some(item2) diff --git a/src/test/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiffTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiffTest.scala index 821758e..b7c5ead 100644 --- a/src/test/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiffTest.scala +++ b/src/test/scala/com/zbsnetwork/state/diffs/ExchangeTransactionDiffTest.scala @@ -1,27 +1,31 @@ package com.zbsnetwork.state.diffs -import cats.{Order => _, _} -import com.zbsnetwork.OrderOps._ -import com.zbsnetwork.account.{AddressScheme, PrivateKeyAccount} +import cats.{Order ⇒ _, _} +import com.zbsnetwork.account.{AddressScheme, PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 import com.zbsnetwork.features.{BlockchainFeature, BlockchainFeatures} import com.zbsnetwork.lagonaki.mocks.TestBlock import com.zbsnetwork.settings.{Constants, FunctionalitySettings, TestFunctionalitySettings} import com.zbsnetwork.state._ +import com.zbsnetwork.state.diffs.ExchangeTransactionDiff.getOrderFeePortfolio import com.zbsnetwork.state.diffs.TransactionDiffer.TransactionValidationError import com.zbsnetwork.transaction.ValidationError.AccountBalanceError import com.zbsnetwork.transaction._ +import com.zbsnetwork.transaction.assets.exchange.OrderOps._ import com.zbsnetwork.transaction.assets.exchange.{Order, _} import com.zbsnetwork.transaction.assets.{IssueTransaction, IssueTransactionV1, IssueTransactionV2} import com.zbsnetwork.transaction.smart.SetScriptTransaction import com.zbsnetwork.transaction.smart.script.ScriptCompiler -import com.zbsnetwork.transaction.transfer.TransferTransaction +import com.zbsnetwork.transaction.transfer.MassTransferTransaction.ParsedTransfer +import com.zbsnetwork.transaction.transfer.{MassTransferTransaction, TransferTransaction} import com.zbsnetwork.{NoShrink, TransactionGen, crypto} import org.scalacheck.Gen import org.scalatest.prop.PropertyChecks import org.scalatest.{Inside, Matchers, PropSpec} +import scala.util.Random + class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matchers with TransactionGen with Inside with NoShrink { val MATCHER: PrivateKeyAccount = PrivateKeyAccount.fromSeed("matcher").explicitGet() @@ -35,7 +39,44 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc ) ) - property("preserves zbs invariant, stores match info, rewards matcher") { + val fsWithOrderV3Feature: FunctionalitySettings = fs.copy(preActivatedFeatures = fs.preActivatedFeatures + (BlockchainFeatures.OrderV3.id -> 0)) + + val fsOV3MT = + fsWithOrderV3Feature.copy(preActivatedFeatures = fsWithOrderV3Feature.preActivatedFeatures + (BlockchainFeatures.MassTransfer.id -> 0)) + + property("Validation fails when OrderV3 feature is not activation yet") { + + val preconditionsAndExchange + : Gen[(GenesisTransaction, GenesisTransaction, GenesisTransaction, IssueTransaction, IssueTransaction, ExchangeTransaction)] = for { + buyer <- accountGen + seller <- accountGen + matcher <- accountGen + ts <- timestampGen + gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet() + gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet() + gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet() + issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty) + issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty) + maybeAsset1 <- Gen.option(issue1.id()) + maybeAsset2 <- Gen.option(issue2.id()) suchThat (x => x != maybeAsset1) + exchange <- exchangeV2GeneratorP( + buyer = buyer, + seller = seller, + amountAssetId = maybeAsset2, + priceAssetId = maybeAsset1, + orderVersions = Set(3) + ) + } yield (gen1, gen2, gen3, issue1, issue2, exchange) + + forAll(preconditionsAndExchange) { + case (gen1, gen2, gen3, issue1, issue2, exchange) => + assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2))), TestBlock.create(Seq(exchange)), fs) { blockDiffEi => + blockDiffEi should produce("Order Version 3 has not been activated yet") + } + } + } + + property("Preserves zbs invariant, stores match info, rewards matcher") { val preconditionsAndExchange: Gen[(GenesisTransaction, GenesisTransaction, IssueTransaction, IssueTransaction, ExchangeTransaction)] = for { buyer <- accountGen @@ -52,7 +93,7 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc forAll(preconditionsAndExchange) { case (gen1, gen2, issue1, issue2, exchange) => - assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, issue1, issue2))), TestBlock.create(Seq(exchange)), fs) { + assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, issue1, issue2))), TestBlock.create(Seq(exchange)), fsWithOrderV3Feature) { case (blockDiff, state) => val totalPortfolioDiff: Portfolio = Monoid.combineAll(blockDiff.portfolios.values) totalPortfolioDiff.balance shouldBe 0 @@ -64,6 +105,309 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc } } + property("Preserves assets invariant (matcher's fee in one of the assets of the pair or in Zbs), stores match info, rewards matcher") { + + val preconditionsAndExchange + : Gen[(GenesisTransaction, GenesisTransaction, GenesisTransaction, IssueTransaction, IssueTransaction, ExchangeTransaction)] = for { + buyer <- accountGen + seller <- accountGen + matcher <- accountGen + ts <- timestampGen + gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet() + gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet() + gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet() + issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty) + issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty) + maybeAsset1 <- Gen.option(issue1.id()) + maybeAsset2 <- Gen.option(issue2.id()) suchThat (x => x != maybeAsset1) + buyMatcherFeeAssetId <- Gen.oneOf(maybeAsset1, maybeAsset2) + sellMatcherFeeAssetId <- Gen.oneOf(maybeAsset1, maybeAsset2) + exchange <- exchangeV2GeneratorP( + buyer = buyer, + seller = seller, + amountAssetId = maybeAsset2, + priceAssetId = maybeAsset1, + buyMatcherFeeAssetId = buyMatcherFeeAssetId, + sellMatcherFeeAssetId = sellMatcherFeeAssetId, + fixedMatcher = Some(matcher) + ) retryUntil transactionWithOrdersV3IsValid + } yield (gen1, gen2, gen3, issue1, issue2, exchange) + + forAll(preconditionsAndExchange) { + case (gen1, gen2, gen3, issue1, issue2, exchange) => + assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2))), TestBlock.create(Seq(exchange)), fsWithOrderV3Feature) { + case (blockDiff, state) => + val totalPortfolioDiff: Portfolio = Monoid.combineAll(blockDiff.portfolios.values) + totalPortfolioDiff.balance shouldBe 0 + totalPortfolioDiff.effectiveBalance shouldBe 0 + totalPortfolioDiff.assets.values.toSet shouldBe Set(0L) + + val matcherPortfolio = Monoid.combineAll(blockDiff.portfolios.filterKeys(_.address == exchange.sender.address).values) + + val restoredMatcherPortfolio = + Monoid.combineAll( + Seq( + ExchangeTransactionDiff.getOrderFeePortfolio(exchange.buyOrder, exchange.buyMatcherFee), + ExchangeTransactionDiff.getOrderFeePortfolio(exchange.sellOrder, exchange.sellMatcherFee), + ExchangeTransactionDiff.zbsPortfolio(-exchange.fee) + ) + ) + + matcherPortfolio shouldBe restoredMatcherPortfolio + } + } + } + + property("Validation fails when received amount of asset is less than fee in that asset (Orders V3 are used)") { + val preconditionsAndExchange + : Gen[(GenesisTransaction, GenesisTransaction, GenesisTransaction, IssueTransaction, IssueTransaction, ExchangeTransaction)] = for { + buyer <- accountGen + seller <- accountGen + matcher <- accountGen + ts <- timestampGen + gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet() + gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet() + gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet() + issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty) + issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty) + buyerIssuedAsset = Some(issue1.id()) + sellerIssuedAsset = Some(issue2.id()) + exchange <- exchangeV2GeneratorP( + buyer = buyer, + seller = seller, + amountAssetId = sellerIssuedAsset, // buyer buys sellerIssuedAsset (received asset) + priceAssetId = buyerIssuedAsset, // buyer sells buyerIssuedAsset + buyMatcherFeeAssetId = sellerIssuedAsset, // buyer pays fee in sellerIssuedAsset (received asset) + sellMatcherFeeAssetId = buyerIssuedAsset, + fixedMatcher = Some(matcher), + orderVersions = Set(3) + ).retryUntil(ex => !transactionWithOrdersV3IsValid(ex)) // fee in sellerIssuedAsset (received asset) is greater than amount of received sellerIssuedAsset + } yield (gen1, gen2, gen3, issue1, issue2, exchange) + + forAll(preconditionsAndExchange) { + case (gen1, gen2, gen3, issue1, issue2, exchange) => + assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2))), TestBlock.create(Seq(exchange)), fsWithOrderV3Feature) { + blockDiffEi => + blockDiffEi should produce("negative asset balance") + } + } + } + + property("Preserves assets invariant (matcher's fee in separately issued asset), stores match info, rewards matcher (Orders V3 are used)") { + + val preconditionsAndExchange: Gen[(GenesisTransaction, + GenesisTransaction, + GenesisTransaction, + IssueTransaction, + IssueTransaction, + IssueTransaction, + IssueTransaction, + ExchangeTransaction)] = for { + buyer <- accountGen + seller <- accountGen + matcher <- accountGen + ts <- timestampGen + gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet() + gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet() + gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet() + issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty) + issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty) + issue3: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty) + issue4: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty) + maybeAsset1 <- Gen.option(issue1.id()) + maybeAsset2 <- Gen.option(issue2.id()) suchThat (x => x != maybeAsset1) + buyMatcherFeeAssetId = Some(issue3.id()) + sellMatcherFeeAssetId = Some(issue4.id()) + exchange <- exchangeV2GeneratorP( + buyer = buyer, + seller = seller, + amountAssetId = maybeAsset2, + priceAssetId = maybeAsset1, + buyMatcherFeeAssetId = buyMatcherFeeAssetId, + sellMatcherFeeAssetId = sellMatcherFeeAssetId, + fixedMatcher = Some(matcher), + orderVersions = Set(3) + ) + } yield (gen1, gen2, gen3, issue1, issue2, issue3, issue4, exchange) + + forAll(preconditionsAndExchange) { + case (gen1, gen2, gen3, issue1, issue2, issue3, issue4, exchange) => + assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2, issue3, issue4))), + TestBlock.create(Seq(exchange)), + fsWithOrderV3Feature) { + case (blockDiff, state) => + val totalPortfolioDiff: Portfolio = Monoid.combineAll(blockDiff.portfolios.values) + totalPortfolioDiff.balance shouldBe 0 + totalPortfolioDiff.effectiveBalance shouldBe 0 + totalPortfolioDiff.assets.values.toSet shouldBe Set(0L) + + val matcherPortfolio = Monoid.combineAll(blockDiff.portfolios.filterKeys(_.address == exchange.sender.address).values) + + val restoredMatcherPortfolio = + Monoid.combineAll( + Seq( + ExchangeTransactionDiff.getOrderFeePortfolio(exchange.buyOrder, exchange.buyMatcherFee), + ExchangeTransactionDiff.getOrderFeePortfolio(exchange.sellOrder, exchange.sellMatcherFee), + ExchangeTransactionDiff.zbsPortfolio(-exchange.fee) + ) + ) + + matcherPortfolio shouldBe restoredMatcherPortfolio + } + } + } + + property("Validation fails in case of attempt to pay fee in unissued asset (Orders V3 are used)") { + + val preconditionsAndExchange + : Gen[(GenesisTransaction, GenesisTransaction, GenesisTransaction, IssueTransaction, IssueTransaction, ExchangeTransaction)] = for { + buyer <- accountGen + seller <- accountGen + matcher <- accountGen + ts <- timestampGen + gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet() + gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet() + gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet() + issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty) + issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty) + maybeAsset1 <- Gen.option(issue1.id()) + maybeAsset2 <- Gen.option(issue2.id()) suchThat (x => x != maybeAsset1) + matcherFeeAssetId <- assetIdGen retryUntil (_.nonEmpty) + exchange <- exchangeV2GeneratorP( + buyer = buyer, + seller = seller, + amountAssetId = maybeAsset2, + priceAssetId = maybeAsset1, + buyMatcherFeeAssetId = matcherFeeAssetId, + sellMatcherFeeAssetId = matcherFeeAssetId, + fixedMatcher = Some(matcher), + orderVersions = Set(3) + ) + } yield (gen1, gen2, gen3, issue1, issue2, exchange) + + forAll(preconditionsAndExchange) { + case (gen1, gen2, gen3, issue1, issue2, exchange) => + assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2))), TestBlock.create(Seq(exchange)), fsWithOrderV3Feature) { + blockDiffEi => + blockDiffEi should produce("negative asset balance") + } + } + } + + property("Validation fails when balance of asset issued separately (asset is not in the pair) is less than fee in that asset (Orders V3 are used)") { + + val preconditionsAndExchange: Gen[(GenesisTransaction, + GenesisTransaction, + GenesisTransaction, + IssueTransaction, + IssueTransaction, + IssueTransaction, + IssueTransaction, + ExchangeTransaction)] = for { + buyer <- accountGen + seller <- accountGen + matcher <- accountGen + ts <- timestampGen + gen1: GenesisTransaction = GenesisTransaction.create(buyer, ENOUGH_AMT, ts).explicitGet() + gen2: GenesisTransaction = GenesisTransaction.create(seller, ENOUGH_AMT, ts).explicitGet() + gen3: GenesisTransaction = GenesisTransaction.create(matcher, ENOUGH_AMT, ts).explicitGet() + issue1: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, buyer).map(_._1).retryUntil(_.script.isEmpty) + issue2: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT, seller).map(_._1).retryUntil(_.script.isEmpty) + issue3: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT / 1000000, buyer).map(_._1).retryUntil(_.script.isEmpty) + issue4: IssueTransaction <- issueReissueBurnGeneratorP(ENOUGH_AMT / 1000000, seller).map(_._1).retryUntil(_.script.isEmpty) + buyerIssuedAsset = Some(issue1.id()) + sellerIssuedAsset = Some(issue2.id()) + buyMatcherFeeAssetId = Some(issue3.id()) + sellMatcherFeeAssetId = Some(issue4.id()) + exchange <- exchangeV2GeneratorP( + buyer = buyer, + seller = seller, + amountAssetId = sellerIssuedAsset, + priceAssetId = buyerIssuedAsset, + fixedMatcherFee = Some(ENOUGH_AMT / 10), + buyMatcherFeeAssetId = buyMatcherFeeAssetId, + sellMatcherFeeAssetId = sellMatcherFeeAssetId, + fixedMatcher = Some(matcher), + orderVersions = Set(3) + ) + } yield (gen1, gen2, gen3, issue1, issue2, issue3, issue4, exchange) + + forAll(preconditionsAndExchange) { + case (gen1, gen2, gen3, issue1, issue2, issue3, issue4, exchange) => + assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, gen3, issue1, issue2, issue3, issue4))), + TestBlock.create(Seq(exchange)), + fsWithOrderV3Feature) { blockDiffEi => + blockDiffEi should produce("negative asset balance") + } + } + } + + property("Total matcher's fee (sum of matcher's fees in exchange transactions) is less than or equal to order's matcher fee") { + + val preconditions = + oneBuyFewSellsPreconditions( + totalBuyMatcherFeeBoundaries = (bigBuyOrderMatcherFee: Long) => (bigBuyOrderMatcherFee - 1000L, bigBuyOrderMatcherFee), // sum of buyMatcherFee in ex trs <= specified in bigBuyOrder + sellersTotalAmount = identity + ) + + forAll(preconditions) { + case (genesises, issueTx1, issueTx2, massTransfer, exchanges, bigBuyOrder) => + assertDiffAndState(Seq(TestBlock.create(genesises), TestBlock.create(Seq(issueTx1, issueTx2, massTransfer))), + TestBlock.create(exchanges), + fsOV3MT) { + case (blockDiff, _) => + val totalPortfolioDiff: Portfolio = Monoid.combineAll(blockDiff.portfolios.values) + + totalPortfolioDiff.balance shouldBe 0 + totalPortfolioDiff.effectiveBalance shouldBe 0 + totalPortfolioDiff.assets.values.toSet shouldBe Set(0L) + + val feeSumPaidByBuyer = + Monoid + .combineAll( + exchanges.map(ex => getOrderFeePortfolio(bigBuyOrder, ex.buyMatcherFee)) + ) + .assets(bigBuyOrder.matcherFeeAssetId.get) + + (feeSumPaidByBuyer <= exchanges.head.buyOrder.matcherFee) shouldBe true + } + } + } + + property("Validation fails when total matcher's fee (sum of matcher's fees in exchange transactions) is greater than order's matcher fee") { + + val preconditions = + oneBuyFewSellsPreconditions( + totalBuyMatcherFeeBoundaries = (bigBuyOrderMatcherFee: Long) => (bigBuyOrderMatcherFee + 1, bigBuyOrderMatcherFee + 100000L), // sum of buyMatcherFee in ex trs > specified in bigBuyOrder + sellersTotalAmount = identity + ) + + forAll(preconditions) { + case (genesises, issueTx1, issueTx2, massTransfer, exchanges, _) => + assertDiffEi(Seq(TestBlock.create(genesises), TestBlock.create(Seq(issueTx1, issueTx2, massTransfer))), TestBlock.create(exchanges), fsOV3MT) { + blockDiffEi => + blockDiffEi should produce("Insufficient buy fee") + } + } + } + + property("Validation fails when total sell amount overfills buy order amount") { + + val preconditions = + oneBuyFewSellsPreconditions( + totalBuyMatcherFeeBoundaries = (bigBuyOrderMatcherFee: Long) => (bigBuyOrderMatcherFee - 10000L, bigBuyOrderMatcherFee), // correct total buyMatcherFee in ex trs + sellersTotalAmount = (bigBuyOrderAmount: Long) => bigBuyOrderAmount + 10000L // sell orders overfill buy order + ) + + forAll(preconditions) { + case (genesises, issueTx1, issueTx2, massTransfer, exchanges, _) => + assertDiffEi(Seq(TestBlock.create(genesises), TestBlock.create(Seq(issueTx1, issueTx2, massTransfer))), TestBlock.create(exchanges), fsOV3MT) { + blockDiffEi => + blockDiffEi should produce("Too much buy") + } + } + } + property("buy zbs without enough money for fee") { val preconditions: Gen[(GenesisTransaction, GenesisTransaction, IssueTransactionV1, ExchangeTransaction)] = for { buyer <- accountGen @@ -83,7 +427,7 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc forAll(preconditions) { case (gen1, gen2, issue1, exchange) => whenever(exchange.amount > 300000) { - assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, issue1))), TestBlock.create(Seq(exchange)), fs) { + assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, issue1))), TestBlock.create(Seq(exchange)), fsWithOrderV3Feature) { case (blockDiff, _) => val totalPortfolioDiff: Portfolio = Monoid.combineAll(blockDiff.portfolios.values) totalPortfolioDiff.balance shouldBe 0 @@ -136,7 +480,7 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc assertDiffAndState(Seq(TestBlock.create(Seq(gen1, gen2, issue1))), TestBlock.create(Seq(tx)), fs) { case (blockDiff, state) => blockDiff.portfolios(tx.sender).balance shouldBe tx.buyMatcherFee + tx.sellMatcherFee - tx.fee - state.portfolio(tx.sender).balance shouldBe 0L + state.balance(tx.sender) shouldBe 0L } } } @@ -162,7 +506,7 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc val buy = Order.buy(buyer, matcher, assetPair, issue1.quantity + 1, price, Ts, Ts + 1, MatcherFee) val sell = Order.sell(seller, matcher, assetPair, issue1.quantity + 1, price, Ts, Ts + 1, MatcherFee) val tx = createExTx(buy, sell, price, matcher, Ts).explicitGet() - assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, issue1))), TestBlock.create(Seq(tx)), fs) { totalDiffEi => + assertDiffEi(Seq(TestBlock.create(Seq(gen1, gen2, issue1))), TestBlock.create(Seq(tx)), fsWithOrderV3Feature) { totalDiffEi => inside(totalDiffEi) { case Left(TransactionValidationError(AccountBalanceError(errs), _)) => errs should contain key seller.toAddress @@ -461,7 +805,7 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc lazy val contract = s""" | |{-# STDLIB_VERSION 3 #-} - |{-# SCRIPT_TYPE CONTRACT #-} + |{-# CONTENT_TYPE CONTRACT #-} | | @Verifier(tx) | func verify() = { @@ -556,4 +900,168 @@ class ExchangeTransactionDiffTest extends PropSpec with PropertyChecks with Matc exchange <- exchangeGeneratorP(buyer, seller, maybeAsset1, maybeAsset2) } yield (gen1, gen2, issue1, issue2, exchange) + /** + * Checks whether generated ExchangeTransactionV2 is valid. + * In case of using orders of version 3 it is possible that matched amount of received asset is less than matcher's + * fee in that asset. It leads to negative asset balance error + */ + def transactionWithOrdersV3IsValid(ex: ExchangeTransaction): Boolean = { + (ex.buyOrder, ex.sellOrder) match { + case (_: OrderV3, _: Order) | (_: Order, _: OrderV3) => + val isBuyerReceiveAmountGreaterThanFee = + if (ex.buyOrder.assetPair.amountAsset == ex.buyOrder.matcherFeeAssetId) { + ex.buyOrder.getReceiveAmount(ex.amount, ex.price).right.get > ex.buyMatcherFee + } else true + + val isSellerReceiveAmountGreaterThanFee = + if (ex.sellOrder.assetPair.amountAsset == ex.sellOrder.matcherFeeAssetId) { + ex.sellOrder.getReceiveAmount(ex.amount, ex.price).right.get > ex.sellMatcherFee + } else true + + isBuyerReceiveAmountGreaterThanFee && isSellerReceiveAmountGreaterThanFee + case _ => true + } + } + + /** Generates sequence of Longs with predefined sum and size */ + def getSeqWithPredefinedSum(sum: Long, count: Int): Seq[Long] = { + + val (rem, res) = (1 until count) + .foldLeft((sum, List.empty[Long])) { + case ((remainder, result), _) => + val next = java.util.concurrent.ThreadLocalRandom.current.nextLong(1, remainder) + (remainder - next) -> (next :: result) + } + + Random.shuffle(rem :: res) + } + + /** Generates sequence of sell orders for one big buy order */ + def sellOrdersForBigBuyOrderGenerator(matcher: PublicKeyAccount, + sellers: Seq[PrivateKeyAccount], + assetPair: AssetPair, + price: Long, + matcherFeeAssetId: Option[AssetId], + totalAmount: Long, + totalMatcherFee: Long): Gen[Seq[Order]] = { + + val randomAmountsAndFees = + getSeqWithPredefinedSum(totalAmount, sellers.length) zip getSeqWithPredefinedSum(totalMatcherFee, sellers.length) + + val sellers2AmountsAndFees = sellers zip randomAmountsAndFees + + def timestampAndExpirationGenerator: Gen[(Long, Long)] = { + for { + timestamp <- timestampGen + expiration <- maxOrderTimeGen + } yield (timestamp, expiration) + } + + for { timestampsAndExpiration <- Gen.listOfN(sellers.length, timestampAndExpirationGenerator) } yield { + + (timestampsAndExpiration zip sellers2AmountsAndFees) + .map { + case ((timestamp, expiration), (seller, (amount, fee))) => + OrderV3( + sender = seller, + matcher = matcher, + pair = assetPair, + orderType = OrderType.SELL, + amount = amount, + price = price, + timestamp = timestamp, + expiration = expiration, + matcherFee = fee, + matcherFeeAssetId = matcherFeeAssetId + ) + } + } + } + + /** + * Returns preconditions for tests based on case when there is one big buy order and few small sell orders + * + * @param totalBuyMatcherFeeBoundaries function for manipulating of total matcher's fee paid by buyer in exchange transactions + * @param sellersTotalAmount function for manipulating of total sell orders amount + */ + def oneBuyFewSellsPreconditions(totalBuyMatcherFeeBoundaries: Long => (Long, Long), sellersTotalAmount: Long => Long) + : Gen[(List[GenesisTransaction], IssueTransaction, IssueTransaction, MassTransferTransaction, Seq[ExchangeTransactionV2], Order)] = { + for { + matcher <- accountGen + sellOrdersCount <- Gen.choose(1, 5) + sellers <- Gen.listOfN(sellOrdersCount, accountGen) + (buyer, _, _, _, bigBuyOrderAmount, price, bigBuyOrderTimestamp, bigBuyOrderExpiration, bigBuyOrderMatcherFee) <- orderParamGen + genesisTimestamp <- timestampGen + issueTx1: IssueTransaction <- issueReissueBurnGeneratorP(Long.MaxValue - 1000L, buyer).map(_._1).retryUntil(_.script.isEmpty) + issueTx2: IssueTransaction <- issueReissueBurnGeneratorP(Long.MaxValue - 1000L, buyer).map(_._1).retryUntil(_.script.isEmpty) + + pair = AssetPair(Some(issueTx2.id()), Some(issueTx1.id())) + (minTotalBuyMatcherFee, maxTotalBuyMatcherFee) = totalBuyMatcherFeeBoundaries(bigBuyOrderMatcherFee) + + totalBuyMatcherFeeForExchangeTransactions <- Gen.choose(minTotalBuyMatcherFee, maxTotalBuyMatcherFee) + + bigBuyOrder = Order( + sender = buyer, + matcher = matcher, + pair = pair, + orderType = OrderType.BUY, + amount = bigBuyOrderAmount, + price = price, + timestamp = bigBuyOrderTimestamp, + expiration = bigBuyOrderExpiration, + matcherFee = bigBuyOrderMatcherFee, + version = 3: Byte, + matcherFeeAssetId = Some(issueTx1.id()) + ) + + sellOrders <- sellOrdersForBigBuyOrderGenerator( + matcher = matcher, + assetPair = pair, + price = price, + matcherFeeAssetId = Some(issueTx2.id()), + sellers = sellers, + totalAmount = sellersTotalAmount(bigBuyOrderAmount), + totalMatcherFee = bigBuyOrderMatcherFee + ) + } yield { + + val genesises = (matcher :: buyer :: sellers).map { recipient => + GenesisTransaction.create(recipient, ENOUGH_AMT, genesisTimestamp).explicitGet() + } + + val massTransfer = + MassTransferTransaction + .selfSigned( + assetId = Some(issueTx2.id()), + sender = buyer, + transfers = sellers.map(seller => ParsedTransfer(seller, issueTx2.quantity / sellOrdersCount)), + genesisTimestamp + 1000L, + feeAmount = 1000L, + Array.empty[Byte] + ) + .explicitGet() + + val buyMatcherFees = getSeqWithPredefinedSum(totalBuyMatcherFeeForExchangeTransactions, sellOrdersCount) + + val exchanges = (sellOrders zip buyMatcherFees).map { + case (sellOrder, buyMatcherFee) => + ExchangeTransactionV2 + .create( + matcher = matcher, + buyOrder = bigBuyOrder, + sellOrder = sellOrder, + amount = sellOrder.amount, + price = bigBuyOrder.price, + buyMatcherFee = buyMatcherFee, + sellMatcherFee = sellOrder.matcherFee, + fee = (bigBuyOrder.matcherFee + sellOrder.matcherFee) / 2, + timestamp = Math.min(sellOrder.expiration, bigBuyOrder.expiration) - 10000 + ) + .explicitGet() + } + + (genesises, issueTx1, issueTx2, massTransfer, exchanges, bigBuyOrder) + } + } + } diff --git a/src/test/scala/com/zbsnetwork/state/diffs/package.scala b/src/test/scala/com/zbsnetwork/state/diffs/package.scala index 2f95620..e53c94b 100644 --- a/src/test/scala/com/zbsnetwork/state/diffs/package.scala +++ b/src/test/scala/com/zbsnetwork/state/diffs/package.scala @@ -13,7 +13,7 @@ import com.zbsnetwork.transaction.{Transaction, ValidationError} import org.scalatest.Matchers package object diffs extends WithState with Matchers { - val ENOUGH_AMT: Long = Long.MaxValue / 3 + val ENOUGH_AMT: Long = Long.MaxValue / 2 def assertDiffEi(preconditions: Seq[Block], block: Block, fs: FunctionalitySettings = TFS.Enabled)( assertion: Either[ValidationError, Diff] => Unit): Unit = withStateAndHistory(fs) { state => @@ -29,7 +29,7 @@ package object diffs extends WithState with Matchers { private def assertDiffAndState(preconditions: Seq[Block], block: Block, fs: FunctionalitySettings, withNg: Boolean)( assertion: (Diff, Blockchain) => Unit): Unit = withStateAndHistory(fs) { state => - def differ(blockchain: Blockchain, prevBlock: Option[Block], b: Block) = + def differ(blockchain: Blockchain, prevBlock: Option[Block], b: Block): Either[ValidationError, (Diff, Long, MiningConstraint)] = BlockDiffer.fromBlock(fs, blockchain, if (withNg) prevBlock else None, b, MiningConstraint.Unlimited) preconditions.foldLeft[Option[Block]](None) { (prevBlock, curBlock) => diff --git a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/CommonFunctionsTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/CommonFunctionsTest.scala index 722cb2b..79d678c 100644 --- a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/CommonFunctionsTest.scala +++ b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/CommonFunctionsTest.scala @@ -194,11 +194,9 @@ class CommonFunctionsTest extends PropSpec with PropertyChecks with Matchers wit } } - property("shadowing of external variable") { - //TODO: script can be simplified after NODE-837 fix - try { - runScript( - s""" + property("shadowing of variable considered external") { + runScript( + s""" |match { | let aaa = 1 | tx @@ -207,12 +205,7 @@ class CommonFunctionsTest extends PropSpec with PropertyChecks with Matchers wit | case other => throw() | } |""".stripMargin - ) - - } catch { - case ex: MatchError => Assertions.assert(ex.getMessage().contains("Compilation failed: Value 'tx' already defined in the scope")) - case _: Throwable => Assertions.fail("Some unexpected error") - } + ) should produce("already defined") } property("data constructors") { diff --git a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/ContextFunctionsTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/ContextFunctionsTest.scala index e537f9d..1027a8c 100644 --- a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/ContextFunctionsTest.scala +++ b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/ContextFunctionsTest.scala @@ -86,11 +86,21 @@ class ContextFunctionsTest extends PropSpec with PropertyChecks with Matchers wi | let bin = extract(getBinary(d, "${bin.key}")) | let str = extract(getString(d, "${str.key}")) | + | let intV = getIntegerValue(d, "${int.key}") + | let boolV = getBooleanValue(d, "${bool.key}") + | let binV = getBinaryValue(d, "${bin.key}") + | let strV = getStringValue(d, "${str.key}") + | | let okInt = int == ${int.value} | let okBool = bool == ${bool.value} | let okBin = bin == base58'${Base58.encode(bin.asInstanceOf[BinaryDataEntry].value.arr)}' | let okStr = str == "${str.value}" | + | let okIntV = int + 1 == ${int.value} + 1 + | let okBoolV = bool || true == ${bool.value} || true + | let okBinV = bin == base58'${Base58.encode(bin.asInstanceOf[BinaryDataEntry].value.arr)}' + | let okStrV = str + "" == "${str.value}" + | | let badInt = isDefined(getInteger(d, "${bool.key}")) | let badBool = isDefined(getBoolean(d, "${bin.key}")) | let badBin = isDefined(getBinary(d, "${str.key}")) @@ -98,7 +108,7 @@ class ContextFunctionsTest extends PropSpec with PropertyChecks with Matchers wi | | let noSuchKey = isDefined(getInteger(d, "\u00a0")) | - | let positives = okInt && okBool && okBin && okStr + | let positives = okInt && okBool && okBin && okStr && okIntV && okBoolV && okBinV && okStrV | let negatives = badInt || badBool || badBin || badStr || noSuchKey | positives && ! negatives | } diff --git a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/SerContextFunctionsTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/SerContextFunctionsTest.scala index ab31961..1cfa2bd 100644 --- a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/SerContextFunctionsTest.scala +++ b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/SerContextFunctionsTest.scala @@ -51,159 +51,157 @@ class SerContextFunctionsTest extends PropSpec with PropertyChecks with Matchers val untypedScript = Parser.parseExpr(scriptWithAllFunctions(dtx, ttx)).get.value val compiledScript = ExpressionCompiler(compilerContext(V1, isAssetScript = false), untypedScript).explicitGet()._1 - val bytes = Array[Byte](10, 0, 0, 0, 0, 3, 114, 110, 100, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 106, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, - 9, 116, 105, 109, 101, 115, 116, 97, 109, 112, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 7, 108, 111, 110, 103, 65, - 108, 108, 3, 3, 3, 3, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 104, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, - 0, 0, 7, -48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 105, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, - -12, 7, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 106, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, - 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 100, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -22, 7, 9, 0, - 0, 0, 0, 0, 0, 2, 9, 0, 0, 101, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -26, 7, 10, 0, 0, 0, - 0, 9, 115, 117, 109, 83, 116, 114, 105, 110, 103, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, 44, 0, 0, 0, 2, 9, 0, 1, 44, 0, 0, 0, 2, 2, 0, 0, 0, 2, 104, - 97, 2, 0, 0, 0, 1, 45, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0, 0, 5, 104, 97, 45, 104, 97, 10, 0, 0, 0, 0, 13, 115, 117, 109, 66, 121, 116, 101, 86, - 101, 99, 116, 111, 114, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, - 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 2, 100, 48, 5, 0, - 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 10, 0, 0, 0, 0, 4, 98, 111, 100, 121, 8, 5, 0, 0, 0, 2, 100, 48, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, - 116, 101, 115, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, -53, 0, 0, 0, 2, 5, 0, 0, 0, 4, 98, 111, 100, 121, 1, 0, 0, 0, 100, 12, 1, -43, 40, -86, -66, - -61, 92, -95, 0, -40, 124, 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33, -17, 34, 0, 4, 0, 3, - 105, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105, 99, 101, 0, 3, 115, - 116, 114, 3, 0, 4, 116, 101, 115, 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 9, 0, 0, -53, 0, 0, 0, 2, 1, 0, 0, 0, 100, 12, - 1, -43, 40, -86, -66, -61, 92, -95, 0, -40, 124, 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33, - -17, 34, 0, 4, 0, 3, 105, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105, - 99, 101, 0, 3, 115, 116, 114, 3, 0, 4, 116, 101, 115, 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 1, 0, 0, 0, 100, 12, 1, - -43, 40, -86, -66, -61, 92, -95, 0, -40, 124, 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33, -17, - 34, 0, 4, 0, 3, 105, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105, 99, - 101, 0, 3, 115, 116, 114, 3, 0, 4, 116, 101, 115, 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, - 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, - 110, 6, 7, 10, 0, 0, 0, 0, 7, 101, 113, 85, 110, 105, 111, 110, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, + val bytes = Array[Byte](4, 0, 0, 0, 3, 114, 110, 100, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 106, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 9, + 116, 105, 109, 101, 115, 116, 97, 109, 112, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 7, 108, 111, 110, 103, 65, 108, + 108, 3, 3, 3, 3, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 104, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 7, -48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 105, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, -12, + 7, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 106, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 9, 0, + 0, 0, 0, 0, 0, 2, 9, 0, 0, 100, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -22, 7, 9, 0, 0, 0, + 0, 0, 0, 2, 9, 0, 0, 101, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -26, 7, 4, 0, 0, 0, 9, + 115, 117, 109, 83, 116, 114, 105, 110, 103, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, 44, 0, 0, 0, 2, 9, 0, 1, 44, 0, 0, 0, 2, 2, 0, 0, 0, 2, 104, 97, 2, + 0, 0, 0, 1, 45, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0, 0, 5, 104, 97, 45, 104, 97, 4, 0, 0, 0, 13, 115, 117, 109, 66, 121, 116, 101, 86, 101, 99, 116, + 111, 114, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, + 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 2, 100, 48, 5, 0, 0, 0, 7, 36, 109, 97, + 116, 99, 104, 48, 4, 0, 0, 0, 4, 98, 111, 100, 121, 8, 5, 0, 0, 0, 2, 100, 48, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 9, 0, 0, + 0, 0, 0, 0, 2, 9, 0, 0, -53, 0, 0, 0, 2, 5, 0, 0, 0, 4, 98, 111, 100, 121, 1, 0, 0, 0, 100, 12, 1, -43, 40, -86, -66, -61, 92, -95, 0, -40, 124, + 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33, -17, 34, 0, 4, 0, 3, 105, 110, 116, 0, 0, 0, 0, 0, + 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105, 99, 101, 0, 3, 115, 116, 114, 3, 0, 4, 116, 101, 115, + 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 9, 0, 0, -53, 0, 0, 0, 2, 1, 0, 0, 0, 100, 12, 1, -43, 40, -86, -66, -61, 92, + -95, 0, -40, 124, 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33, -17, 34, 0, 4, 0, 3, 105, 110, + 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105, 99, 101, 0, 3, 115, 116, 114, 3, + 0, 4, 116, 101, 115, 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 1, 0, 0, 0, 100, 12, 1, -43, 40, -86, -66, -61, 92, -95, 0, + -40, 124, 123, 122, 18, -122, 50, -6, -15, -100, -44, 69, 49, -127, -108, 87, 68, 81, 19, -93, 42, 33, -17, 34, 0, 4, 0, 3, 105, 110, 116, 0, 0, + 0, 0, 0, 0, 0, 0, 24, 0, 4, 98, 111, 111, 108, 1, 1, 0, 4, 98, 108, 111, 98, 2, 0, 5, 97, 108, 105, 99, 101, 0, 3, 115, 116, 114, 3, 0, 4, 116, + 101, 115, 116, 0, 0, 1, 99, -125, 4, -6, 10, 0, 0, 0, 0, 0, 1, -122, -96, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, + 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 7, 4, 0, 0, 0, 7, 101, 113, 85, + 110, 105, 111, 110, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, + 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, + 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, + 0, 0, 0, 2, 116, 48, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 116, 48, 0, 0, 0, 9, 114, 101, 99, + 105, 112, 105, 101, 110, 116, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0, 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, 100, 23, -115, -33, -128, + -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23, -25, 122, -18, -70, -100, -99, 7, 4, 0, 0, 0, 5, 98, 97, 115, 105, 99, 3, 3, 3, + 5, 0, 0, 0, 7, 108, 111, 110, 103, 65, 108, 108, 5, 0, 0, 0, 9, 115, 117, 109, 83, 116, 114, 105, 110, 103, 7, 5, 0, 0, 0, 13, 115, 117, 109, + 66, 121, 116, 101, 86, 101, 99, 116, 111, 114, 7, 5, 0, 0, 0, 7, 101, 113, 85, 110, 105, 111, 110, 7, 4, 0, 0, 0, 6, 110, 101, 80, 114, 105, + 109, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 3, -25, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, + 9, 0, 1, 44, 0, 0, 0, 2, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0, 0, 5, 104, 97, 45, 104, 97, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, + 0, 2, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 1, 0, 0, 0, 4, -123, -88, 90, -123, 7, 4, 0, 0, 0, 24, + 110, 101, 68, 97, 116, 97, 69, 110, 116, 114, 121, 65, 110, 100, 71, 101, 116, 69, 108, 101, 109, 101, 110, 116, 4, 0, 0, 0, 7, 36, 109, 97, + 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, + 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 2, 100, 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 2, + 33, 61, 0, 0, 0, 2, 9, 0, 1, -111, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 49, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, + 0, 9, 68, 97, 116, 97, 69, 110, 116, 114, 121, 0, 0, 0, 2, 2, 0, 0, 0, 2, 104, 97, 6, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, + 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 7, 4, 0, 0, 0, 24, + 110, 101, 79, 112, 116, 105, 111, 110, 65, 110, 100, 69, 120, 116, 114, 97, 99, 116, 72, 101, 105, 103, 104, 116, 4, 0, 0, 0, 7, 36, 109, 97, + 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, + 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, + 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, + 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 3, -23, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 7, 4, 0, 0, 0, 2, 110, 101, 3, 3, 5, 0, 0, 0, 6, 110, 101, 80, 114, 105, 109, 5, 0, 0, 0, 24, 110, 101, 68, 97, 116, 97, 69, 110, 116, + 114, 121, 65, 110, 100, 71, 101, 116, 69, 108, 101, 109, 101, 110, 116, 7, 5, 0, 0, 0, 24, 110, 101, 79, 112, 116, 105, 111, 110, 65, 110, 100, + 69, 120, 116, 114, 97, 99, 116, 72, 101, 105, 103, 104, 116, 7, 4, 0, 0, 0, 7, 103, 116, 101, 76, 111, 110, 103, 3, 9, 0, 0, 102, 0, 0, 0, 2, 0, + 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 3, -25, 9, 0, 0, 103, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 3, -25, 7, 4, + 0, 0, 0, 11, 103, 101, 116, 76, 105, 115, 116, 83, 105, 122, 101, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, - 111, 110, 6, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, - 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 2, 116, 48, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2, - 8, 5, 0, 0, 0, 2, 116, 48, 0, 0, 0, 9, 114, 101, 99, 105, 112, 105, 101, 110, 116, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0, 0, 0, - 1, 1, 0, 0, 0, 26, 1, 84, 100, 23, -115, -33, -128, -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23, -25, 122, -18, -70, -100, - -99, 7, 10, 0, 0, 0, 0, 5, 98, 97, 115, 105, 99, 3, 3, 3, 5, 0, 0, 0, 7, 108, 111, 110, 103, 65, 108, 108, 5, 0, 0, 0, 9, 115, 117, 109, 83, - 116, 114, 105, 110, 103, 7, 5, 0, 0, 0, 13, 115, 117, 109, 66, 121, 116, 101, 86, 101, 99, 116, 111, 114, 7, 5, 0, 0, 0, 7, 101, 113, 85, 110, - 105, 111, 110, 7, 10, 0, 0, 0, 0, 6, 110, 101, 80, 114, 105, 109, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, - 0, 0, 0, 0, 0, 3, -25, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 44, 0, 0, 0, 2, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0, 0, 2, 104, 97, 2, 0, 0, - 0, 5, 104, 97, 45, 104, 97, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, - 101, 115, 1, 0, 0, 0, 4, -123, -88, 90, -123, 7, 10, 0, 0, 0, 0, 24, 110, 101, 68, 97, 116, 97, 69, 110, 116, 114, 121, 65, 110, 100, 71, 101, - 116, 69, 108, 101, 109, 101, 110, 116, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, - 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, - 2, 100, 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -111, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, - 100, 49, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 9, 68, 97, 116, 97, 69, 110, 116, 114, 121, 0, 0, 0, 2, 2, 0, - 0, 0, 2, 104, 97, 6, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, - 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 7, 10, 0, 0, 0, 0, 24, 110, 101, 79, 112, 116, 105, 111, 110, 65, 110, 100, 69, 120, - 116, 114, 97, 99, 116, 72, 101, 105, 103, 104, 116, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, - 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 3, - 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, - 115, 97, 99, 116, 105, 111, 110, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 3, - -23, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 10, 0, 0, 0, 0, 2, 110, 101, 3, 3, 5, 0, 0, 0, - 6, 110, 101, 80, 114, 105, 109, 5, 0, 0, 0, 24, 110, 101, 68, 97, 116, 97, 69, 110, 116, 114, 121, 65, 110, 100, 71, 101, 116, 69, 108, 101, - 109, 101, 110, 116, 7, 5, 0, 0, 0, 24, 110, 101, 79, 112, 116, 105, 111, 110, 65, 110, 100, 69, 120, 116, 114, 97, 99, 116, 72, 101, 105, 103, - 104, 116, 7, 10, 0, 0, 0, 0, 7, 103, 116, 101, 76, 111, 110, 103, 3, 9, 0, 0, 102, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, - 3, -25, 9, 0, 0, 103, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, -24, 0, 0, 0, 0, 0, 0, 0, 3, -25, 7, 10, 0, 0, 0, 0, 11, 103, 101, 116, 76, 105, 115, - 116, 83, 105, 122, 101, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, - 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 2, 100, 50, 5, 0, - 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -112, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 100, 50, 0, 0, 0, 4, - 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, - 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 7, 10, 0, 0, 0, 0, 5, 117, 110, 97, 114, 121, 3, 9, 0, 0, 0, - 0, 0, 0, 2, 0, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, 9, 0, 0, 0, 0, 0, 0, 2, 7, 9, 1, 0, 0, 0, 1, 33, 0, 0, 0, 1, - 6, 7, 10, 0, 0, 0, 0, 8, 102, 114, 65, 99, 116, 105, 111, 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 107, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, - 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 9, 10, 0, 0, 0, 0, 8, 98, 121, 116, 101, 115, 79, 112, 115, 10, 0, 0, - 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, - 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 2, 100, 51, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, - 104, 48, 3, 3, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 0, -56, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, - 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 0, -55, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0, - 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 9, 49, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, - 0, 0, -54, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, - 2, 9, 49, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 14, 116, 97, 107, 101, 82, 105, 103, 104, 116, 66, 121, 116, 101, 115, 0, 0, - 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 9, 49, 7, 9, - 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 14, 100, 114, 111, 112, 82, 105, 103, 104, 116, 66, 121, 116, 101, 115, 0, 0, 0, 2, 8, 5, 0, - 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 9, 49, 7, 3, 9, 0, 0, 1, 0, - 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, - 105, 111, 110, 10, 0, 0, 0, 0, 2, 116, 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 9, 105, 115, 68, - 101, 102, 105, 110, 101, 100, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 49, 0, 0, 0, 10, 102, 101, 101, 65, 115, 115, 101, 116, 73, 100, 7, 7, 10, 0, - 0, 0, 0, 6, 115, 116, 114, 79, 112, 115, 3, 3, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 49, 0, 0, 0, 1, 2, 0, 0, 0, 4, 104, 97, 104, - 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 47, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, - 0, 0, 1, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 48, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 9, 116, 97, 107, 101, 82, 105, 103, 104, 116, 0, 0, 0, 2, 2, 0, 0, - 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 9, 100, 114, 111, 112, - 82, 105, 103, 104, 116, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 7, 10, 0, 0, 0, 0, 4, 112, 117, - 114, 101, 3, 3, 3, 3, 3, 3, 3, 5, 0, 0, 0, 5, 98, 97, 115, 105, 99, 5, 0, 0, 0, 2, 110, 101, 7, 5, 0, 0, 0, 7, 103, 116, 101, 76, 111, 110, 103, - 7, 5, 0, 0, 0, 11, 103, 101, 116, 76, 105, 115, 116, 83, 105, 122, 101, 7, 5, 0, 0, 0, 5, 117, 110, 97, 114, 121, 7, 5, 0, 0, 0, 8, 102, 114, - 65, 99, 116, 105, 111, 110, 7, 5, 0, 0, 0, 8, 98, 121, 116, 101, 115, 79, 112, 115, 7, 5, 0, 0, 0, 6, 115, 116, 114, 79, 112, 115, 7, 10, 0, 0, - 0, 0, 6, 116, 120, 66, 121, 73, 100, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, - 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 3, 9, 0, 0, 1, 0, - 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, - 105, 111, 110, 10, 0, 0, 0, 0, 1, 103, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 3, -24, 0, 0, 0, 1, 1, 0, 0, 0, 32, - -127, -8, -79, -21, -18, 42, -48, -20, -34, -84, -89, 17, 125, -43, 82, 88, -78, -58, -94, -5, -31, 50, 36, 76, 53, 88, 86, 48, 93, -67, 20, 11, - 9, 0, 0, 0, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 103, 0, 0, 0, 2, 105, 100, 1, 0, 0, 0, 32, -127, -8, -79, -21, -18, 42, -48, -20, -34, -84, -89, 17, - 125, -43, 82, 88, -78, -58, -94, -5, -31, 50, 36, 76, 53, 88, 86, 48, 93, -67, 20, 11, 7, 10, 0, 0, 0, 0, 7, 101, 110, 116, 114, 105, 101, 115, - 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, - 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 1, 100, 5, 0, 0, 0, 7, 36, 109, 97, 116, - 99, 104, 48, 10, 0, 0, 0, 0, 3, 105, 110, 116, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 16, 0, 0, 0, 2, 8, 5, 0, - 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 3, 105, 110, 116, 10, 0, 0, 0, 0, 4, 98, 111, 111, 108, 9, 1, 0, 0, 0, 7, 101, 120, 116, - 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 17, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 4, 98, 111, 111, 108, - 10, 0, 0, 0, 0, 4, 98, 108, 111, 98, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 18, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, - 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 4, 98, 108, 111, 98, 10, 0, 0, 0, 0, 3, 115, 116, 114, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, - 99, 116, 0, 0, 0, 1, 9, 0, 4, 19, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 3, 115, 116, 114, 10, 0, 0, 0, 0, - 9, 100, 97, 116, 97, 66, 121, 75, 101, 121, 3, 3, 3, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -92, 0, 0, 0, 1, 5, 0, 0, 0, 3, 105, 110, 116, 2, 0, 0, 0, - 2, 50, 52, 6, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -91, 0, 0, 0, 1, 5, 0, 0, 0, 4, 98, 111, 111, 108, 2, 0, 0, 0, 4, 116, 114, 117, 101, 6, 9, 0, 0, - 102, 0, 0, 0, 2, 9, 0, 0, -56, 0, 0, 0, 1, 5, 0, 0, 0, 4, 98, 108, 111, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 9, 0, 0, 0, 0, 0, 0, 2, 5, 0, 0, 0, 3, - 115, 116, 114, 2, 0, 0, 0, 4, 116, 101, 115, 116, 10, 0, 0, 0, 0, 2, 100, 48, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, - 1, 0, 0, 0, 10, 103, 101, 116, 73, 110, 116, 101, 103, 101, 114, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 10, 0, 0, 0, 0, 2, 100, 49, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 1, 0, 0, 0, 10, 103, 101, 116, 66, - 111, 111, 108, 101, 97, 110, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 0, 0, 0, 0, 2, 100, - 50, 9, 1, 0, 0, 0, 9, 103, 101, 116, 66, 105, 110, 97, 114, 121, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, - 0, 0, 0, 2, 10, 0, 0, 0, 0, 2, 100, 51, 9, 1, 0, 0, 0, 9, 103, 101, 116, 83, 116, 114, 105, 110, 103, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, - 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 3, 10, 0, 0, 0, 0, 11, 100, 97, 116, 97, 66, 121, 73, 110, 100, 101, 120, 3, 3, 3, 9, 0, 0, 0, - 0, 0, 0, 2, 9, 0, 1, -102, 0, 0, 0, 1, 5, 0, 0, 0, 2, 100, 48, 1, 0, 0, 0, 4, 105, -73, 29, 121, 6, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -100, 0, 0, - 0, 1, 5, 0, 0, 0, 2, 100, 49, 1, 0, 0, 0, 4, -126, 24, -93, -110, 6, 9, 1, 0, 0, 0, 9, 105, 115, 68, 101, 102, 105, 110, 101, 100, 0, 0, 0, 1, - 5, 0, 0, 0, 2, 100, 50, 6, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -101, 0, 0, 0, 1, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 5, - 0, 0, 0, 2, 100, 51, 1, 0, 0, 0, 4, -102, 122, 41, -86, 3, 5, 0, 0, 0, 9, 100, 97, 116, 97, 66, 121, 75, 101, 121, 5, 0, 0, 0, 11, 100, 97, 116, - 97, 66, 121, 73, 110, 100, 101, 120, 7, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, - 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 3, 97, 100, 100, 9, 1, 0, 0, 0, 7, 65, 100, 100, - 114, 101, 115, 115, 0, 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, 100, 23, -115, -33, -128, -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23, - -25, 122, -18, -70, -100, -99, 10, 0, 0, 0, 0, 4, 108, 111, 110, 103, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, - 0, 0, 0, 1, 9, 0, 4, 26, 0, 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 3, 105, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 10, 0, 0, 0, 0, 5, - 98, 111, 111, 108, 49, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 27, 0, 0, 0, 2, 5, 0, 0, - 0, 3, 97, 100, 100, 2, 0, 0, 0, 4, 98, 111, 111, 108, 6, 10, 0, 0, 0, 0, 3, 98, 105, 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, - 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 28, 0, 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 4, 98, 108, 111, 98, 1, 0, 0, 0, 5, 97, - 108, 105, 99, 101, 10, 0, 0, 0, 0, 4, 115, 116, 114, 49, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, - 9, 0, 4, 29, 0, 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 3, 115, 116, 114, 2, 0, 0, 0, 4, 116, 101, 115, 116, 3, 3, 3, 5, 0, 0, 0, 4, - 108, 111, 110, 103, 5, 0, 0, 0, 5, 98, 111, 111, 108, 49, 7, 5, 0, 0, 0, 3, 98, 105, 110, 7, 5, 0, 0, 0, 4, 115, 116, 114, 49, 7, 3, 9, 0, 0, 1, - 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 22, 67, 114, 101, 97, 116, 101, 65, 108, 105, 97, 115, 84, 114, 97, 110, - 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 1, 97, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 2, 0, 0, 0, 1, 2, 0, 0, 0, 5, - 111, 104, 32, 110, 111, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 66, 117, 114, 110, 84, 114, 97, - 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 1, 98, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 5, 116, 104, 114, 111, - 119, 0, 0, 0, 0, 7, 10, 0, 0, 0, 0, 7, 97, 70, 114, 111, 109, 80, 75, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 20, 97, 100, 100, 114, 101, 115, - 115, 70, 114, 111, 109, 80, 117, 98, 108, 105, 99, 75, 101, 121, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 15, 115, 101, 110, 100, 101, - 114, 80, 117, 98, 108, 105, 99, 75, 101, 121, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 6, 115, 101, 110, 100, 101, 114, 10, 0, 0, 0, 0, 15, 97, 70, - 114, 111, 109, 83, 116, 114, 79, 114, 82, 101, 99, 105, 112, 10, 0, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, - 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, - 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 17, 97, 100, 100, 114, 101, 115, 115, 70, 114, 111, 109, 83, 116, 114, 105, 110, 103, 0, 0, 0, 1, 2, - 0, 0, 0, 35, 51, 78, 53, 71, 82, 113, 122, 68, 66, 104, 106, 86, 88, 110, 67, 110, 52, 52, 98, 97, 72, 99, 122, 50, 71, 111, 90, 121, 53, 113, - 76, 120, 116, 84, 104, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0, 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, -88, 98, -11, -83, -97, -100, 82, - 58, 6, 114, -79, -62, -117, -100, -95, 112, -63, 95, 104, -126, 95, -112, -18, 0, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, - 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 10, 0, 0, 0, 0, 2, 116, - 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 4, 36, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 49, 0, 0, 0, 9, 114, - 101, 99, 105, 112, 105, 101, 110, 116, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0, 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, 100, 23, -115, - -33, -128, -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23, -25, 122, -18, -70, -100, -99, 7, 10, 0, 0, 0, 0, 8, 98, 97, 108, 97, - 110, 99, 101, 115, 3, 9, 0, 0, 102, 0, 0, 0, 2, 9, 0, 3, -21, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 6, 115, 101, 110, 100, 101, 114, - 5, 0, 0, 0, 4, 117, 110, 105, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 12, 119, 97, 118, 101, 115, - 66, 97, 108, 97, 110, 99, 101, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 6, 115, 101, 110, 100, 101, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, - 10, 0, 0, 0, 0, 5, 119, 97, 118, 101, 115, 3, 3, 3, 3, 3, 5, 0, 0, 0, 6, 116, 120, 66, 121, 73, 100, 5, 0, 0, 0, 7, 101, 110, 116, 114, 105, - 101, 115, 7, 5, 0, 0, 0, 8, 98, 97, 108, 97, 110, 99, 101, 115, 7, 5, 0, 0, 0, 7, 97, 70, 114, 111, 109, 80, 75, 7, 5, 0, 0, 0, 15, 97, 70, 114, - 111, 109, 83, 116, 114, 79, 114, 82, 101, 99, 105, 112, 7, 9, 0, 0, 102, 0, 0, 0, 2, 5, 0, 0, 0, 6, 104, 101, 105, 103, 104, 116, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 7, 10, 0, 0, 0, 0, 3, 98, 107, 115, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -10, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, - 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -11, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, - 1, -9, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 7, 10, 0, 0, 0, 0, 3, 115, 105, 103, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -12, 0, 0, - 0, 3, 1, 0, 0, 0, 2, 26, -66, 1, 0, 0, 0, 2, 0, 60, 1, 0, 0, 0, 2, 53, -72, 6, 10, 0, 0, 0, 0, 5, 115, 116, 114, 53, 56, 9, 0, 0, 0, 0, 0, 0, 2, - 9, 0, 2, 89, 0, 0, 0, 1, 9, 0, 2, 88, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, - 100, 10, 0, 0, 0, 0, 5, 115, 116, 114, 54, 52, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 2, 91, 0, 0, 0, 1, 9, 0, 2, 90, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, - 120, 0, 0, 0, 2, 105, 100, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 10, 0, 0, 0, 0, 6, 99, 114, 121, 112, 116, 111, 3, 3, 3, 5, 0, 0, - 0, 3, 98, 107, 115, 5, 0, 0, 0, 3, 115, 105, 103, 7, 5, 0, 0, 0, 5, 115, 116, 114, 53, 56, 7, 5, 0, 0, 0, 5, 115, 116, 114, 54, 52, 7, 3, 5, 0, - 0, 0, 3, 114, 110, 100, 3, 5, 0, 0, 0, 4, 112, 117, 114, 101, 5, 0, 0, 0, 5, 119, 97, 118, 101, 115, 7, 5, 0, 0, 0, 6, 99, 114, 121, 112, 116, - 111) + 111, 110, 4, 0, 0, 0, 2, 100, 50, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -112, 0, 0, 0, 1, + 8, 5, 0, 0, 0, 2, 100, 50, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, + 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 7, 4, 0, 0, 0, 5, 117, + 110, 97, 114, 121, 3, 9, 0, 0, 0, 0, 0, 0, 2, 0, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, 9, 0, 0, 0, 0, 0, 0, 2, 7, + 9, 1, 0, 0, 0, 1, 33, 0, 0, 0, 1, 6, 7, 4, 0, 0, 0, 8, 102, 114, 65, 99, 116, 105, 111, 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 0, 107, 0, 0, 0, 3, + 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 9, 4, 0, 0, 0, 8, 98, 121, 116, 101, + 115, 79, 112, 115, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, + 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 2, 100, 51, 5, 0, 0, 0, 7, 36, + 109, 97, 116, 99, 104, 48, 3, 3, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 0, -56, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, + 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 0, -55, 0, 0, 0, 2, 8, 5, 0, + 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 9, 49, 7, 9, 1, 0, 0, 0, 2, + 33, 61, 0, 0, 0, 2, 9, 0, 0, -54, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 0, 0, 0, 2, 9, 49, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 14, 116, 97, 107, 101, 82, 105, 103, 104, 116, 66, + 121, 116, 101, 115, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 0, 0, 0, 2, 9, 49, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 14, 100, 114, 111, 112, 82, 105, 103, 104, 116, 66, 121, 116, 101, + 115, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, 100, 51, 0, 0, 0, 9, 98, 111, 100, 121, 66, 121, 116, 101, 115, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 9, + 49, 7, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, + 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 2, 116, 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, + 0, 0, 9, 105, 115, 68, 101, 102, 105, 110, 101, 100, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 49, 0, 0, 0, 10, 102, 101, 101, 65, 115, 115, 101, 116, + 73, 100, 7, 7, 4, 0, 0, 0, 6, 115, 116, 114, 79, 112, 115, 3, 3, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 49, 0, 0, 0, 1, 2, 0, 0, + 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 47, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, + 97, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, 48, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, 9, 116, 97, 107, 101, 82, 105, 103, 104, 116, + 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 7, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 1, 0, 0, 0, + 9, 100, 114, 111, 112, 82, 105, 103, 104, 116, 0, 0, 0, 2, 2, 0, 0, 0, 4, 104, 97, 104, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 7, 4, 0, + 0, 0, 4, 112, 117, 114, 101, 3, 3, 3, 3, 3, 3, 3, 5, 0, 0, 0, 5, 98, 97, 115, 105, 99, 5, 0, 0, 0, 2, 110, 101, 7, 5, 0, 0, 0, 7, 103, 116, 101, + 76, 111, 110, 103, 7, 5, 0, 0, 0, 11, 103, 101, 116, 76, 105, 115, 116, 83, 105, 122, 101, 7, 5, 0, 0, 0, 5, 117, 110, 97, 114, 121, 7, 5, 0, 0, + 0, 8, 102, 114, 65, 99, 116, 105, 111, 110, 7, 5, 0, 0, 0, 8, 98, 121, 116, 101, 115, 79, 112, 115, 7, 5, 0, 0, 0, 6, 115, 116, 114, 79, 112, + 115, 7, 4, 0, 0, 0, 6, 116, 120, 66, 121, 73, 100, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, + 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 6, 3, 9, + 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, 110, 115, + 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 1, 103, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 3, -24, 0, 0, 0, 1, 1, 0, + 0, 0, 32, -127, -8, -79, -21, -18, 42, -48, -20, -34, -84, -89, 17, 125, -43, 82, 88, -78, -58, -94, -5, -31, 50, 36, 76, 53, 88, 86, 48, 93, + -67, 20, 11, 9, 0, 0, 0, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 103, 0, 0, 0, 2, 105, 100, 1, 0, 0, 0, 32, -127, -8, -79, -21, -18, 42, -48, -20, -34, + -84, -89, 17, 125, -43, 82, 88, -78, -58, -94, -5, -31, 50, 36, 76, 53, 88, 86, 48, 93, -67, 20, 11, 7, 4, 0, 0, 0, 7, 101, 110, 116, 114, 105, + 101, 115, 4, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, + 104, 48, 2, 0, 0, 0, 15, 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 1, 100, 5, 0, 0, 0, 7, 36, 109, 97, + 116, 99, 104, 48, 4, 0, 0, 0, 3, 105, 110, 116, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 16, 0, 0, 0, 2, 8, 5, 0, + 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 3, 105, 110, 116, 4, 0, 0, 0, 4, 98, 111, 111, 108, 9, 1, 0, 0, 0, 7, 101, 120, 116, + 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 17, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 4, 98, 111, 111, 108, 4, + 0, 0, 0, 4, 98, 108, 111, 98, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 18, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, + 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 4, 98, 108, 111, 98, 4, 0, 0, 0, 3, 115, 116, 114, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, + 0, 0, 1, 9, 0, 4, 19, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 2, 0, 0, 0, 3, 115, 116, 114, 4, 0, 0, 0, 9, 100, 97, + 116, 97, 66, 121, 75, 101, 121, 3, 3, 3, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -92, 0, 0, 0, 1, 5, 0, 0, 0, 3, 105, 110, 116, 2, 0, 0, 0, 2, 50, 52, + 6, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -91, 0, 0, 0, 1, 5, 0, 0, 0, 4, 98, 111, 111, 108, 2, 0, 0, 0, 4, 116, 114, 117, 101, 6, 9, 0, 0, 102, 0, 0, + 0, 2, 9, 0, 0, -56, 0, 0, 0, 1, 5, 0, 0, 0, 4, 98, 108, 111, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 9, 0, 0, 0, 0, 0, 0, 2, 5, 0, 0, 0, 3, 115, 116, + 114, 2, 0, 0, 0, 4, 116, 101, 115, 116, 4, 0, 0, 0, 2, 100, 48, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 1, 0, 0, 0, + 10, 103, 101, 116, 73, 110, 116, 101, 103, 101, 114, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 2, 100, 49, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 1, 0, 0, 0, 10, 103, 101, 116, 66, 111, 111, 108, 101, + 97, 110, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 0, 0, 0, 2, 100, 50, 9, 1, 0, 0, 0, 9, + 103, 101, 116, 66, 105, 110, 97, 114, 121, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 0, 0, + 0, 2, 100, 51, 9, 1, 0, 0, 0, 9, 103, 101, 116, 83, 116, 114, 105, 110, 103, 0, 0, 0, 2, 8, 5, 0, 0, 0, 1, 100, 0, 0, 0, 4, 100, 97, 116, 97, 0, + 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 11, 100, 97, 116, 97, 66, 121, 73, 110, 100, 101, 120, 3, 3, 3, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -102, 0, 0, + 0, 1, 5, 0, 0, 0, 2, 100, 48, 1, 0, 0, 0, 4, 105, -73, 29, 121, 6, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 1, -100, 0, 0, 0, 1, 5, 0, 0, 0, 2, 100, 49, 1, + 0, 0, 0, 4, -126, 24, -93, -110, 6, 9, 1, 0, 0, 0, 9, 105, 115, 68, 101, 102, 105, 110, 101, 100, 0, 0, 0, 1, 5, 0, 0, 0, 2, 100, 50, 6, 9, 0, + 0, 0, 0, 0, 0, 2, 9, 0, 1, -101, 0, 0, 0, 1, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 5, 0, 0, 0, 2, 100, 51, 1, 0, 0, 0, + 4, -102, 122, 41, -86, 3, 5, 0, 0, 0, 9, 100, 97, 116, 97, 66, 121, 75, 101, 121, 5, 0, 0, 0, 11, 100, 97, 116, 97, 66, 121, 73, 110, 100, 101, + 120, 7, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, + 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 3, 97, 100, 100, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0, 0, 0, 1, 1, 0, 0, + 0, 26, 1, 84, 100, 23, -115, -33, -128, -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23, -25, 122, -18, -70, -100, -99, 4, 0, 0, + 0, 4, 108, 111, 110, 103, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 26, 0, 0, 0, 2, 5, 0, + 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 3, 105, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 24, 4, 0, 0, 0, 5, 98, 111, 111, 108, 49, 9, 0, 0, 0, 0, 0, 0, 2, + 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 27, 0, 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 4, 98, 111, + 111, 108, 6, 4, 0, 0, 0, 3, 98, 105, 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 28, 0, + 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2, 0, 0, 0, 4, 98, 108, 111, 98, 1, 0, 0, 0, 5, 97, 108, 105, 99, 101, 4, 0, 0, 0, 4, 115, 116, 114, 49, + 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 7, 101, 120, 116, 114, 97, 99, 116, 0, 0, 0, 1, 9, 0, 4, 29, 0, 0, 0, 2, 5, 0, 0, 0, 3, 97, 100, 100, 2, + 0, 0, 0, 3, 115, 116, 114, 2, 0, 0, 0, 4, 116, 101, 115, 116, 3, 3, 3, 5, 0, 0, 0, 4, 108, 111, 110, 103, 5, 0, 0, 0, 5, 98, 111, 111, 108, 49, + 7, 5, 0, 0, 0, 3, 98, 105, 110, 7, 5, 0, 0, 0, 4, 115, 116, 114, 49, 7, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, + 2, 0, 0, 0, 22, 67, 114, 101, 97, 116, 101, 65, 108, 105, 97, 115, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 1, 97, 5, 0, + 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 2, 0, 0, 0, 1, 2, 0, 0, 0, 5, 111, 104, 32, 110, 111, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, + 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, 66, 117, 114, 110, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 1, 98, 5, 0, 0, + 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 1, 0, 0, 0, 5, 116, 104, 114, 111, 119, 0, 0, 0, 0, 7, 4, 0, 0, 0, 7, 97, 70, 114, 111, 109, 80, 75, 9, + 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 20, 97, 100, 100, 114, 101, 115, 115, 70, 114, 111, 109, 80, 117, 98, 108, 105, 99, 75, 101, 121, 0, 0, 0, + 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 15, 115, 101, 110, 100, 101, 114, 80, 117, 98, 108, 105, 99, 75, 101, 121, 8, 5, 0, 0, 0, 2, 116, 120, + 0, 0, 0, 6, 115, 101, 110, 100, 101, 114, 4, 0, 0, 0, 15, 97, 70, 114, 111, 109, 83, 116, 114, 79, 114, 82, 101, 99, 105, 112, 4, 0, 0, 0, 7, + 36, 109, 97, 116, 99, 104, 48, 5, 0, 0, 0, 2, 116, 120, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 15, + 68, 97, 116, 97, 84, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 9, 0, 0, 0, 0, 0, 0, 2, 9, 1, 0, 0, 0, 17, 97, 100, 100, 114, 101, 115, 115, + 70, 114, 111, 109, 83, 116, 114, 105, 110, 103, 0, 0, 0, 1, 2, 0, 0, 0, 35, 51, 78, 53, 71, 82, 113, 122, 68, 66, 104, 106, 86, 88, 110, 67, + 110, 52, 52, 98, 97, 72, 99, 122, 50, 71, 111, 90, 121, 53, 113, 76, 120, 116, 84, 104, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, 115, 0, + 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, -88, 98, -11, -83, -97, -100, 82, 58, 6, 114, -79, -62, -117, -100, -95, 112, -63, 95, 104, -126, 95, -112, -18, + 0, 3, 9, 0, 0, 1, 0, 0, 0, 2, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 2, 0, 0, 0, 19, 84, 114, 97, 110, 115, 102, 101, 114, 84, 114, 97, + 110, 115, 97, 99, 116, 105, 111, 110, 4, 0, 0, 0, 2, 116, 49, 5, 0, 0, 0, 7, 36, 109, 97, 116, 99, 104, 48, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 4, 36, + 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 49, 0, 0, 0, 9, 114, 101, 99, 105, 112, 105, 101, 110, 116, 9, 1, 0, 0, 0, 7, 65, 100, 100, 114, 101, 115, + 115, 0, 0, 0, 1, 1, 0, 0, 0, 26, 1, 84, 100, 23, -115, -33, -128, -20, -97, 62, -33, -42, 86, -24, -22, 104, -110, -85, 40, -23, -25, 122, -18, + -70, -100, -99, 7, 4, 0, 0, 0, 8, 98, 97, 108, 97, 110, 99, 101, 115, 3, 9, 0, 0, 102, 0, 0, 0, 2, 9, 0, 3, -21, 0, 0, 0, 2, 8, 5, 0, 0, 0, 2, + 116, 120, 0, 0, 0, 6, 115, 101, 110, 100, 101, 114, 5, 0, 0, 0, 4, 117, 110, 105, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, + 0, 0, 2, 9, 1, 0, 0, 0, 12, 119, 97, 118, 101, 115, 66, 97, 108, 97, 110, 99, 101, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 6, 115, 101, + 110, 100, 101, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 4, 0, 0, 0, 5, 119, 97, 118, 101, 115, 3, 3, 3, 3, 3, 5, 0, 0, 0, 6, 116, 120, 66, 121, 73, + 100, 5, 0, 0, 0, 7, 101, 110, 116, 114, 105, 101, 115, 7, 5, 0, 0, 0, 8, 98, 97, 108, 97, 110, 99, 101, 115, 7, 5, 0, 0, 0, 7, 97, 70, 114, 111, + 109, 80, 75, 7, 5, 0, 0, 0, 15, 97, 70, 114, 111, 109, 83, 116, 114, 79, 114, 82, 101, 99, 105, 112, 7, 9, 0, 0, 102, 0, 0, 0, 2, 5, 0, 0, 0, 6, + 104, 101, 105, 103, 104, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 4, 0, 0, 0, 3, 98, 107, 115, 3, 3, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, + -10, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -11, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 7, + 9, 1, 0, 0, 0, 2, 33, 61, 0, 0, 0, 2, 9, 0, 1, -9, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 7, 4, 0, 0, 0, 3, 115, 105, 103, 9, 1, 0, 0, 0, 2, + 33, 61, 0, 0, 0, 2, 9, 0, 1, -12, 0, 0, 0, 3, 1, 0, 0, 0, 2, 26, -66, 1, 0, 0, 0, 2, 0, 60, 1, 0, 0, 0, 2, 53, -72, 6, 4, 0, 0, 0, 5, 115, 116, + 114, 53, 56, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 2, 89, 0, 0, 0, 1, 9, 0, 2, 88, 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 8, 5, + 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 4, 0, 0, 0, 5, 115, 116, 114, 54, 52, 9, 0, 0, 0, 0, 0, 0, 2, 9, 0, 2, 91, 0, 0, 0, 1, 9, 0, 2, 90, + 0, 0, 0, 1, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 8, 5, 0, 0, 0, 2, 116, 120, 0, 0, 0, 2, 105, 100, 4, 0, 0, 0, 6, 99, 114, 121, + 112, 116, 111, 3, 3, 3, 5, 0, 0, 0, 3, 98, 107, 115, 5, 0, 0, 0, 3, 115, 105, 103, 7, 5, 0, 0, 0, 5, 115, 116, 114, 53, 56, 7, 5, 0, 0, 0, 5, + 115, 116, 114, 54, 52, 7, 3, 5, 0, 0, 0, 3, 114, 110, 100, 3, 5, 0, 0, 0, 4, 112, 117, 114, 101, 5, 0, 0, 0, 5, 119, 97, 118, 101, 115, 7, 5, 0, + 0, 0, 6, 99, 114, 121, 112, 116, 111) Serde.serialize(compiledScript) shouldBe bytes } diff --git a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/TransactionBindingsTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/TransactionBindingsTest.scala index 245e18b..96c3245 100644 --- a/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/TransactionBindingsTest.scala +++ b/src/test/scala/com/zbsnetwork/state/diffs/smart/predef/TransactionBindingsTest.scala @@ -19,7 +19,7 @@ import com.zbsnetwork.transaction.smart.BlockchainContext.In import com.zbsnetwork.transaction.smart.ZbsEnvironment import com.zbsnetwork.transaction.{Proofs, ProvenTransaction, VersionedTransaction} import com.zbsnetwork.utils.EmptyBlockchain -import com.zbsnetwork.{NoShrink, TransactionGen} +import com.zbsnetwork.{NoShrink, TransactionGen, crypto} import fastparse.core.Parsed.Success import monix.eval.Coeval import org.scalacheck.Gen @@ -43,7 +43,7 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers | let id = t.id == base58'${t.id().base58}' | let fee = t.fee == ${t.assetFee._2} | let timestamp = t.timestamp == ${t.timestamp} - | let bodyBytes = t.bodyBytes == base64'${ByteStr(t.bodyBytes.apply()).base64}' + | let bodyBytes = blake2b256(t.bodyBytes) == base64'${ByteStr(crypto.fastHash(t.bodyBytes.apply().array)).base64}' | let sender = t.sender == addressFromPublicKey(base58'${ByteStr(t.sender.publicKey).base58}') | let senderPublicKey = t.senderPublicKey == base58'${ByteStr(t.sender.publicKey).base58}' | let version = t.version == $version @@ -247,8 +247,8 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers |match tx { | case t : SetScriptTransaction => | ${provenPart(t)} - | let script = if (${t.script.isDefined}) then extract(t.script) == base64'${t.script - .map(_.bytes().base64) + | let script = if (${t.script.isDefined}) then blake2b256(extract(t.script)) == base64'${t.script + .map(s => ByteStr(crypto.fastHash(s.bytes().arr)).base64) .getOrElse("")}' else isDefined(t.script) == false | ${assertProvenPart("t")} && script | case other => throw() @@ -262,7 +262,7 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers } property("SetAssetScriptTransaction binding") { - forAll(setAssetScriptTransactionGen.sample.get._2) { t => + forAll(setAssetScriptTransactionGen.map(_._2)) { t => val result = runScript( s""" |match tx { @@ -390,6 +390,10 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers .getOrElse(ByteStr.empty) .base58}' | else isDefined(t.${oType}Order.assetPair.priceAsset) == false + | let ${oType}MatcherFeeAssetId = if (${ord.matcherFeeAssetId.isDefined}) then extract(t.${oType}Order.matcherFeeAssetId) == base58'${ord.matcherFeeAssetId + .getOrElse(ByteStr.empty) + .base58}' + | else isDefined(t.${oType}Order.matcherFeeAssetId) == false """.stripMargin val lets = List( @@ -405,7 +409,8 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers "BodyBytes", "AssetPairAmount", "AssetPairPrice", - "Proofs" + "Proofs", + "MatcherFeeAssetId" ).map(i => s"$oType$i") .mkString(" && ") @@ -451,7 +456,7 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers | let matcherFee = t.matcherFee == ${t.matcherFee} | let bodyBytes = t.bodyBytes == base64'${ByteStr(t.bodyBytes.apply()).base64}' | ${Range(0, 8).map(letProof(t.proofs, "t")).mkString("\n")} - | let assetPairAmount = if (${t.assetPair.amountAsset.isDefined}) then extract(t.assetPair.amountAsset) == base58'${t.assetPair.amountAsset + | let assetPairAmount = if (${t.assetPair.amountAsset.isDefined}) then extract(t.assetPair.amountAsset) == base58'${t.assetPair.amountAsset .getOrElse(ByteStr.empty) .base58}' | else isDefined(t.assetPair.amountAsset) == false @@ -459,8 +464,12 @@ class TransactionBindingsTest extends PropSpec with PropertyChecks with Matchers .getOrElse(ByteStr.empty) .base58}' | else isDefined(t.assetPair.priceAsset) == false - | id && sender && senderPublicKey && matcherPublicKey && timestamp && price && amount && expiration && matcherFee && bodyBytes && ${assertProofs( - "t")} && assetPairAmount && assetPairPrice + | let matcherFeeAssetId = if (${t.matcherFeeAssetId.isDefined}) then extract(t.matcherFeeAssetId) == base58'${t.matcherFeeAssetId + .getOrElse(ByteStr.empty) + .base58}' + | else isDefined(t.matcherFeeAssetId) == false + | id && sender && senderPublicKey && matcherPublicKey && timestamp && price && amount && expiration && matcherFee && bodyBytes && ${assertProofs( + "t")} && assetPairAmount && assetPairPrice && matcherFeeAssetId | case other => throw() | } |""".stripMargin diff --git a/src/test/scala/com/zbsnetwork/state/diffs/smart/scenarios/ScriptedSponsorTest.scala b/src/test/scala/com/zbsnetwork/state/diffs/smart/scenarios/ScriptedSponsorTest.scala index e497953..964229d 100644 --- a/src/test/scala/com/zbsnetwork/state/diffs/smart/scenarios/ScriptedSponsorTest.scala +++ b/src/test/scala/com/zbsnetwork/state/diffs/smart/scenarios/ScriptedSponsorTest.scala @@ -53,14 +53,11 @@ class ScriptedSponsorTest extends PropSpec with PropertyChecks with Matchers wit val sponsor = setupTxs.flatten.collectFirst { case t: SponsorFeeTransaction => t.sender }.get assertDiffAndState(setupBlocks :+ TestBlock.create(Nil), transferBlock, fs) { (diff, blck) => - val contractPortfolio = blck.portfolio(contract) - val sponsorPortfolio = blck.portfolio(sponsor) + blck.balance(contract, Some(assetId)) shouldEqual ENOUGH_FEE * 2 + blck.balance(contract) shouldEqual ENOUGH_AMT - contractSpent - contractPortfolio.balanceOf(Some(assetId)) shouldEqual ENOUGH_FEE * 2 - contractPortfolio.balanceOf(None) shouldEqual ENOUGH_AMT - contractSpent - - sponsorPortfolio.balanceOf(Some(assetId)) shouldEqual Long.MaxValue - ENOUGH_FEE * 2 - sponsorPortfolio.balanceOf(None) shouldEqual ENOUGH_AMT - sponsorSpent + blck.balance(sponsor, Some(assetId)) shouldEqual Long.MaxValue - ENOUGH_FEE * 2 + blck.balance(sponsor) shouldEqual ENOUGH_AMT - sponsorSpent } } } @@ -79,14 +76,11 @@ class ScriptedSponsorTest extends PropSpec with PropertyChecks with Matchers wit val recipientSpent: Long = 1 assertDiffAndState(setupBlocks :+ TestBlock.create(Nil), transferBlock, fs) { (diff, blck) => - val contractPortfolio = blck.portfolio(contract) - val recipientPortfolio = blck.portfolio(recipient) - - contractPortfolio.balanceOf(Some(assetId)) shouldEqual Long.MaxValue - ENOUGH_FEE * 2 - contractPortfolio.balanceOf(None) shouldEqual ENOUGH_AMT - contractSpent + blck.balance(contract, Some(assetId)) shouldEqual Long.MaxValue - ENOUGH_FEE * 2 + blck.balance(contract) shouldEqual ENOUGH_AMT - contractSpent - recipientPortfolio.balanceOf(Some(assetId)) shouldEqual ENOUGH_FEE * 2 - recipientPortfolio.balanceOf(None) shouldEqual ENOUGH_AMT - recipientSpent + blck.balance(recipient, Some(assetId)) shouldEqual ENOUGH_FEE * 2 + blck.balance(recipient) shouldEqual ENOUGH_AMT - recipientSpent } } } diff --git a/src/test/scala/com/zbsnetwork/state/reader/StateReaderEffectiveBalancePropertyTest.scala b/src/test/scala/com/zbsnetwork/state/reader/StateReaderEffectiveBalancePropertyTest.scala index fb5a687..dd60ce4 100644 --- a/src/test/scala/com/zbsnetwork/state/reader/StateReaderEffectiveBalancePropertyTest.scala +++ b/src/test/scala/com/zbsnetwork/state/reader/StateReaderEffectiveBalancePropertyTest.scala @@ -52,7 +52,7 @@ class StateReaderEffectiveBalancePropertyTest extends PropSpec with PropertyChec forAll(setup) { case (leaser, genesis, xfer1, lease1, xfer2, lease2) => assertDiffAndState(Seq(block(Seq(genesis)), block(Seq(xfer1, lease1))), block(Seq(xfer2, lease2)), fs) { (_, state) => - val portfolio = state.portfolio(lease1.sender) + val portfolio = state.zbsPortfolio(lease1.sender) val expectedBalance = xfer1.amount + xfer2.amount - 2 * Fee portfolio.balance shouldBe expectedBalance GeneratingBalanceProvider.balance(state, fs, leaser, state.lastBlockId.get) shouldBe 0 diff --git a/src/test/scala/com/zbsnetwork/transaction/ContractInvocationTransactionSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/ContractInvocationTransactionSpecification.scala index 748c4fa..e88ccf3 100644 --- a/src/test/scala/com/zbsnetwork/transaction/ContractInvocationTransactionSpecification.scala +++ b/src/test/scala/com/zbsnetwork/transaction/ContractInvocationTransactionSpecification.scala @@ -1,13 +1,12 @@ package com.zbsnetwork.transaction import com.zbsnetwork.TransactionGen -import com.zbsnetwork.account.{AddressScheme, DefaultAddressScheme, PrivateKeyAccount} +import com.zbsnetwork.account.{AddressScheme, DefaultAddressScheme, PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.api.http.{ContractInvocationRequest, SignedContractInvocationRequest} import com.zbsnetwork.common.state.ByteStr -import com.zbsnetwork.common.utils.Base64 -import com.zbsnetwork.lang.v1.FunctionHeader +import com.zbsnetwork.common.utils.{Base64, _} +import com.zbsnetwork.lang.v1.{ContractLimits, FunctionHeader} import com.zbsnetwork.lang.v1.compiler.Terms -import com.zbsnetwork.state._ import com.zbsnetwork.transaction.smart.ContractInvocationTransaction.Payment import com.zbsnetwork.transaction.smart.{ContractInvocationTransaction, Verifier} import org.scalatest._ @@ -17,7 +16,7 @@ import play.api.libs.json.{JsObject, Json} class ContractInvocationTransactionSpecification extends PropSpec with PropertyChecks with Matchers with TransactionGen { property("ContractInvocationTransaction serialization roundtrip") { - forAll(contractInvokationGen) { transaction: ContractInvocationTransaction => + forAll(contractInvocationGen) { transaction: ContractInvocationTransaction => val bytes = transaction.bytes() val deser = ContractInvocationTransaction.parseBytes(bytes).get deser.sender shouldEqual transaction.sender @@ -48,8 +47,7 @@ class ContractInvocationTransactionSpecification extends PropSpec with PropertyC "call": { "function" : "foo", "args" : [ - { "key" : "", - "type" : "binary", + { "type" : "binary", "value" : "base64:YWxpY2U=" } ] @@ -84,7 +82,7 @@ class ContractInvocationTransactionSpecification extends PropSpec with PropertyC val req = SignedContractInvocationRequest( senderPublicKey = "73pu8pHFNpj9tmWuYjqnZ962tXzJvLGX86dxjZxGYhoK", fee = 1, - call = ContractInvocationRequest.FunctionCallPart("bar", List(BinaryDataEntry("", ByteStr.decodeBase64("YWxpY2U=").get))), + call = ContractInvocationRequest.FunctionCallPart("bar", List(Terms.CONST_BYTESTR(ByteStr.decodeBase64("YWxpY2U=").get))), payment = Some(Payment(1, None)), contractAddress = "3Fb641A9hWy63K18KsBJwns64McmdEATgJd", timestamp = 11, @@ -94,4 +92,32 @@ class ContractInvocationTransactionSpecification extends PropSpec with PropertyC AddressScheme.current = DefaultAddressScheme } + property(s"can't have more than ${ContractLimits.MaxContractInvocationArgs} args") { + import com.zbsnetwork.common.state.diffs.ProduceError._ + val pk = PublicKeyAccount.fromBase58String("73pu8pHFNpj9tmWuYjqnZ962tXzJvLGX86dxjZxGYhoK").explicitGet() + ContractInvocationTransaction.create( + pk, + pk.toAddress, + Terms.FUNCTION_CALL(FunctionHeader.User("foo"), Range(0, 23).map(_ => Terms.CONST_LONG(0)).toList), + None, + 1, + 1, + Proofs.empty + ) should produce("more than 22 arguments") + } + + property("can't be more 5kb") { + val largeString = "abcde" * 1024 + import com.zbsnetwork.common.state.diffs.ProduceError._ + val pk = PublicKeyAccount.fromBase58String("73pu8pHFNpj9tmWuYjqnZ962tXzJvLGX86dxjZxGYhoK").explicitGet() + ContractInvocationTransaction.create( + pk, + pk.toAddress, + Terms.FUNCTION_CALL(FunctionHeader.User("foo"), List(Terms.CONST_STRING(largeString))), + None, + 1, + 1, + Proofs.empty + ) should produce("TooBigArray") + } } diff --git a/src/test/scala/com/zbsnetwork/transaction/ExchangeTransactionSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/ExchangeTransactionSpecification.scala index 9ab6e22..efddb53 100644 --- a/src/test/scala/com/zbsnetwork/transaction/ExchangeTransactionSpecification.scala +++ b/src/test/scala/com/zbsnetwork/transaction/ExchangeTransactionSpecification.scala @@ -1,10 +1,11 @@ package com.zbsnetwork.transaction -import com.zbsnetwork.OrderOps._ import com.zbsnetwork.account.{PrivateKeyAccount, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.{Base58, EitherExt2} import com.zbsnetwork.transaction.ValidationError.OrderValidationError +import com.zbsnetwork.transaction.assets.exchange.AssetPair.extractAssetId +import com.zbsnetwork.transaction.assets.exchange.OrderOps._ import com.zbsnetwork.transaction.assets.exchange.{Order, _} import com.zbsnetwork.{NTPTime, TransactionGen} import org.scalacheck.Gen @@ -16,6 +17,28 @@ import scala.math.pow class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with Matchers with TransactionGen with NTPTime { + val transactionV1versions = (1: Byte, 1: Byte, 1: Byte) // in ExchangeTransactionV1 only orders V1 are supported + val transactionV2versions = for { + o1ver <- 1 to 3 + o2ver <- 1 to 3 + } yield (o1ver.toByte, o2ver.toByte, 2.toByte) + + val versions = transactionV1versions +: transactionV2versions + val versionsGen: Gen[(Byte, Byte, Byte)] = Gen.oneOf(versions) + + val preconditions: Gen[(PrivateKeyAccount, PrivateKeyAccount, PrivateKeyAccount, AssetPair, Option[AssetId], Option[AssetId], (Byte, Byte, Byte))] = + for { + sender1 <- accountGen + sender2 <- accountGen + matcher <- accountGen + pair <- assetPairGen + buyerAnotherAsset <- assetIdGen + sellerAnotherAsset <- assetIdGen + buyerMatcherFeeAssetId <- Gen.oneOf(pair.amountAsset, pair.priceAsset, buyerAnotherAsset) + sellerMatcherFeeAssetId <- Gen.oneOf(pair.amountAsset, pair.priceAsset, sellerAnotherAsset) + versions <- versionsGen + } yield (sender1, sender2, matcher, pair, buyerMatcherFeeAssetId, sellerMatcherFeeAssetId, versions) + property("ExchangeTransaction transaction serialization roundtrip") { forAll(exchangeTransactionGen) { om => val recovered = ExchangeTransaction.parse(om.bytes()).get @@ -26,13 +49,9 @@ class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with } property("ExchangeTransaction balance changes") { - val versionsGen: Gen[(Byte, Byte, Byte)] = Gen.oneOf((1: Byte, 1: Byte, 1: Byte), - (1: Byte, 1: Byte, 2: Byte), - (1: Byte, 2: Byte, 2: Byte), - (2: Byte, 1: Byte, 2: Byte), - (2: Byte, 2: Byte, 2: Byte)) - forAll(accountGen, accountGen, accountGen, assetPairGen, versionsGen) { - case (sender1, sender2, matcher, pair, versions) => + + forAll(preconditions) { + case (sender1, sender2, matcher, pair, buyerMatcherFeeAssetId, sellerMatcherFeeAssetId, versions) => val time = ntpTime.correctedTime() val expirationTimestamp = time + Order.MaxLiveTime val buyPrice = 60 * Order.PriceConstant @@ -43,8 +62,8 @@ class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with val mf2 = 2 val (o1ver, o2ver, tver) = versions - val buy = Order.buy(sender1, matcher, pair, buyAmount, buyPrice, time, expirationTimestamp, mf1, o1ver) - val sell = Order.sell(sender2, matcher, pair, sellAmount, sellPrice, time, expirationTimestamp, mf2, o2ver) + val buy = Order.buy(sender1, matcher, pair, buyAmount, buyPrice, time, expirationTimestamp, mf1, o1ver, buyerMatcherFeeAssetId) + val sell = Order.sell(sender2, matcher, pair, sellAmount, sellPrice, time, expirationTimestamp, mf2, o2ver, sellerMatcherFeeAssetId) def create(matcher: PrivateKeyAccount = sender1, buyOrder: Order = buy, @@ -54,7 +73,7 @@ class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with buyMatcherFee: Long = mf1, sellMatcherFee: Long = 1, fee: Long = 1, - timestamp: Long = expirationTimestamp - Order.MaxLiveTime) = { + timestamp: Long = expirationTimestamp - Order.MaxLiveTime): Either[ValidationError, ExchangeTransaction] = { if (tver == 1) { ExchangeTransactionV1.create( matcher = sender1, @@ -159,31 +178,27 @@ class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with } property("Test transaction with small amount and expired order") { - forAll( - accountGen, - accountGen, - accountGen, - assetPairGen, - Gen.oneOf((1: Byte, 1: Byte, 1: Byte), - (1: Byte, 1: Byte, 2: Byte), - (1: Byte, 2: Byte, 2: Byte), - (2: Byte, 1: Byte, 2: Byte), - (2: Byte, 2: Byte, 2: Byte)) - ) { (sender1: PrivateKeyAccount, sender2: PrivateKeyAccount, matcher: PrivateKeyAccount, pair: AssetPair, versions) => - val time = ntpTime.correctedTime() - val expirationTimestamp = time + Order.MaxLiveTime - val buyPrice = 1 * Order.PriceConstant - val sellPrice = (0.50 * Order.PriceConstant).toLong - val mf = 300000L - val (o1ver, o2ver, tver) = versions - - val sell = Order.sell(sender2, matcher, pair, 2, sellPrice, time, expirationTimestamp, mf, o1ver) - val buy = Order.buy(sender1, matcher, pair, 1, buyPrice, time, expirationTimestamp, mf, o2ver) - - createExTx(buy, sell, sellPrice, matcher, tver) shouldBe an[Right[_, _]] - - val sell1 = Order.sell(sender1, matcher, pair, 1, buyPrice, time, time - 1, mf, o1ver) - createExTx(buy, sell1, buyPrice, matcher, tver) shouldBe Left(OrderValidationError(sell1, "expiration should be > currentTime")) + + forAll(preconditions) { + case (sender1, sender2, matcher, pair, buyerMatcherFeeAssetId, sellerMatcherFeeAssetId, versions) => + val time = ntpTime.correctedTime() + val expirationTimestamp = time + Order.MaxLiveTime + val buyPrice = 1 * Order.PriceConstant + val sellPrice = (0.50 * Order.PriceConstant).toLong + val mf = 300000L + val (o1ver, o2ver, tver) = versions + + val sell = Order.sell(sender2, matcher, pair, 2, sellPrice, time, expirationTimestamp, mf, o1ver, sellerMatcherFeeAssetId) + val buy = Order.buy(sender1, matcher, pair, 1, buyPrice, time, expirationTimestamp, mf, o2ver, buyerMatcherFeeAssetId) + + createExTx(buy, sell, sellPrice, matcher, tver) shouldBe an[Right[_, _]] + + val sell1 = + if (o1ver == 3) { + Order.sell(sender2, matcher, pair, 1, buyPrice, time, time - 1, mf, o1ver, sellerMatcherFeeAssetId) + } else Order.sell(sender2, matcher, pair, 1, buyPrice, time, time - 1, mf, o1ver) + + createExTx(buy, sell1, buyPrice, matcher, tver) shouldBe Left(OrderValidationError(sell1, "expiration should be > currentTime")) } } @@ -372,4 +387,97 @@ class ExchangeTransactionSpecification extends PropSpec with PropertyChecks with js shouldEqual tx.json() } + property("JSON format validation V2 OrderV3") { + val js = Json.parse("""{ + "version": 2, + "type":7, + "id":"4f4ntXNge7QNLP2DhMPJJzgngygEboLQQdBmzLYGeRoT", + "sender":"3N22UCTvst8N1i1XDvGHzyqdgmZgwDKbp44", + "senderPublicKey":"Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP", + "fee":1, + "timestamp":1526992336241, + "proofs":["5NxNhjMrrH5EWjSFnVnPbanpThic6fnNL48APVAkwq19y2FpQp4tNSqoAZgboC2ykUfqQs9suwBQj6wERmsWWNqa"], + "order1":{ + "version": 3, + "id":"BRAgLHDZFEMP5zRXrAis4EcwuYj11ksD1vq3ZsmiUVSp", + "sender":"3MthkhReCHXeaPZcWXcT3fa6ey1XWptLtwj", + "senderPublicKey":"BqeJY8CP3PeUDaByz57iRekVUGtLxoow4XxPvXfHynaZ", + "matcherPublicKey":"Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP", + "assetPair":{"amountAsset":null,"priceAsset":"9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy"}, + "orderType":"buy", + "price":6000000000, + "amount":2, + "timestamp":1526992336241, + "expiration":1529584336241, + "matcherFee":1, + "matcherFeeAssetId":"9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy", + "signature":"2bkuGwECMFGyFqgoHV4q7GRRWBqYmBFWpYRkzgYANR4nN2twgrNaouRiZBqiK2RJzuo9NooB9iRiuZ4hypBbUQs", + "proofs":["2bkuGwECMFGyFqgoHV4q7GRRWBqYmBFWpYRkzgYANR4nN2twgrNaouRiZBqiK2RJzuo9NooB9iRiuZ4hypBbUQs"] + }, + "order2":{ + "version": 1, + "id":"DS9HPBGRMJcquTb3sAGAJzi73jjMnFFSWWHfzzKK32Q7", + "sender":"3MswjKzUBKCD6i1w4vCosQSbC8XzzdBx1mG", + "senderPublicKey":"7E9Za8v8aT6EyU1sX91CVK7tWUeAetnNYDxzKZsyjyKV", + "matcherPublicKey":"Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP", + "assetPair":{"amountAsset":null,"priceAsset":"9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy"}, + "orderType":"sell", + "price":5000000000, + "amount":3, + "timestamp":1526992336241, + "expiration":1529584336241, + "matcherFee":2, + "signature":"2R6JfmNjEnbXAA6nt8YuCzSf1effDS4Wkz8owpCD9BdCNn864SnambTuwgLRYzzeP5CAsKHEviYKAJ2157vdr5Zq", + "proofs":["2R6JfmNjEnbXAA6nt8YuCzSf1effDS4Wkz8owpCD9BdCNn864SnambTuwgLRYzzeP5CAsKHEviYKAJ2157vdr5Zq"] + }, + "price":5000000000, + "amount":2, + "buyMatcherFee":1, + "sellMatcherFee":1 + } + """) + + val buy = OrderV3( + PublicKeyAccount.fromBase58String("BqeJY8CP3PeUDaByz57iRekVUGtLxoow4XxPvXfHynaZ").explicitGet(), + PublicKeyAccount.fromBase58String("Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP").explicitGet(), + AssetPair.createAssetPair("ZBS", "9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy").get, + OrderType.BUY, + 2, + 6000000000L, + 1526992336241L, + 1529584336241L, + 1, + extractAssetId("9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy").get, + Proofs(Seq(ByteStr.decodeBase58("2bkuGwECMFGyFqgoHV4q7GRRWBqYmBFWpYRkzgYANR4nN2twgrNaouRiZBqiK2RJzuo9NooB9iRiuZ4hypBbUQs").get)) + ) + + val sell = OrderV1( + PublicKeyAccount.fromBase58String("7E9Za8v8aT6EyU1sX91CVK7tWUeAetnNYDxzKZsyjyKV").explicitGet(), + PublicKeyAccount.fromBase58String("Fvk5DXmfyWVZqQVBowUBMwYtRAHDtdyZNNeRrwSjt6KP").explicitGet(), + AssetPair.createAssetPair("ZBS", "9ZDWzK53XT5bixkmMwTJi2YzgxCqn5dUajXFcT2HcFDy").get, + OrderType.SELL, + 3, + 5000000000L, + 1526992336241L, + 1529584336241L, + 2, + Base58.decode("2R6JfmNjEnbXAA6nt8YuCzSf1effDS4Wkz8owpCD9BdCNn864SnambTuwgLRYzzeP5CAsKHEviYKAJ2157vdr5Zq").get + ) + + val tx = ExchangeTransactionV2 + .create( + buy, + sell, + 2, + 5000000000L, + 1, + 1, + 1, + 1526992336241L, + Proofs(Seq(ByteStr.decodeBase58("5NxNhjMrrH5EWjSFnVnPbanpThic6fnNL48APVAkwq19y2FpQp4tNSqoAZgboC2ykUfqQs9suwBQj6wERmsWWNqa").get)) + ) + .explicitGet() + + js shouldEqual tx.json() + } } diff --git a/src/test/scala/com/zbsnetwork/transaction/IssueTransactionV1Specification.scala b/src/test/scala/com/zbsnetwork/transaction/IssueTransactionV1Specification.scala index 39f9844..3cd83ca 100644 --- a/src/test/scala/com/zbsnetwork/transaction/IssueTransactionV1Specification.scala +++ b/src/test/scala/com/zbsnetwork/transaction/IssueTransactionV1Specification.scala @@ -31,7 +31,7 @@ class IssueTransactionV1Specification extends PropSpec with PropertyChecks with "id": "9ekQuYn92natMnMq8KqeGK3Nn7cpKd3BvPEGgD6fFyyz", "sender": "3N5GRqzDBhjVXnCn44baHcz2GoZy5qLxtTh", "senderPublicKey": "FM5ojNqW7e9cZ9zhPYGkpSP1Pcd8Z3e3MNKYVS5pGJ8Z", - "fee": 100000000, + "fee": 1000000, "timestamp": 1526287561757, "version": 1, "signature": "28kE1uN1pX2bwhzr9UHw5UuB9meTFEDFgeunNgy6nZWpHX4pzkGYotu8DhQ88AdqUG6Yy5wcXgHseKPBUygSgRMJ", @@ -53,7 +53,7 @@ class IssueTransactionV1Specification extends PropSpec with PropertyChecks with 10000000000L, 8, true, - 100000000, + 1000000, 1526287561757L, ByteStr.decodeBase58("28kE1uN1pX2bwhzr9UHw5UuB9meTFEDFgeunNgy6nZWpHX4pzkGYotu8DhQ88AdqUG6Yy5wcXgHseKPBUygSgRMJ").get ) diff --git a/src/test/scala/com/zbsnetwork/transaction/OrderSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/OrderSpecification.scala index 5757cde..32b4a67 100644 --- a/src/test/scala/com/zbsnetwork/transaction/OrderSpecification.scala +++ b/src/test/scala/com/zbsnetwork/transaction/OrderSpecification.scala @@ -12,36 +12,44 @@ import org.scalatest.prop.PropertyChecks class OrderSpecification extends PropSpec with PropertyChecks with Matchers with TransactionGen with ValidationMatcher with NTPTime { + private def checkFieldsEquality(left: Order, right: Order): Assertion = { + + def defaultChecks: Assertion = { + left.bytes() shouldEqual right.bytes() + left.idStr() shouldBe right.idStr() + left.senderPublicKey.publicKey shouldBe right.senderPublicKey.publicKey + left.matcherPublicKey shouldBe right.matcherPublicKey + left.assetPair shouldBe right.assetPair + left.orderType shouldBe right.orderType + left.price shouldBe right.price + left.amount shouldBe right.amount + left.timestamp shouldBe right.timestamp + left.expiration shouldBe right.expiration + left.matcherFee shouldBe right.matcherFee + left.signature shouldBe right.signature + } + + (left, right) match { + case (l: OrderV3, r: OrderV3) => defaultChecks; l.matcherFeeAssetId shouldBe r.matcherFeeAssetId + case _ => defaultChecks + } + } + property("Order transaction serialization roundtrip") { + forAll(orderV1Gen) { order => val recovered = OrderV1.parseBytes(order.bytes()).get - recovered.bytes() shouldEqual order.bytes() - recovered.idStr() shouldBe order.idStr() - recovered.senderPublicKey.publicKey shouldBe order.senderPublicKey.publicKey - recovered.matcherPublicKey shouldBe order.matcherPublicKey - recovered.assetPair shouldBe order.assetPair - recovered.orderType shouldBe order.orderType - recovered.price shouldBe order.price - recovered.amount shouldBe order.amount - recovered.timestamp shouldBe order.timestamp - recovered.expiration shouldBe order.expiration - recovered.matcherFee shouldBe order.matcherFee - recovered.signature shouldBe order.signature + checkFieldsEquality(recovered, order) } + forAll(orderV2Gen) { order => val recovered = OrderV2.parseBytes(order.bytes()).get - recovered.bytes() shouldEqual order.bytes() - recovered.idStr() shouldBe order.idStr() - recovered.senderPublicKey.publicKey shouldBe order.senderPublicKey.publicKey - recovered.matcherPublicKey shouldBe order.matcherPublicKey - recovered.assetPair shouldBe order.assetPair - recovered.orderType shouldBe order.orderType - recovered.price shouldBe order.price - recovered.amount shouldBe order.amount - recovered.timestamp shouldBe order.timestamp - recovered.expiration shouldBe order.expiration - recovered.matcherFee shouldBe order.matcherFee - recovered.signature shouldBe order.signature + checkFieldsEquality(recovered, order) + } + + forAll(orderV3Gen) { order => + val recovered = OrderV3.parseBytes(order.bytes()).get + checkFieldsEquality(recovered, order) } } diff --git a/src/test/scala/com/zbsnetwork/transaction/SetAssetScriptTransactionSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/SetAssetScriptTransactionSpecification.scala index acf40f6..0bfb726 100644 --- a/src/test/scala/com/zbsnetwork/transaction/SetAssetScriptTransactionSpecification.scala +++ b/src/test/scala/com/zbsnetwork/transaction/SetAssetScriptTransactionSpecification.scala @@ -2,9 +2,12 @@ package com.zbsnetwork.transaction import com.zbsnetwork.account.{AddressScheme, PublicKeyAccount} import com.zbsnetwork.common.state.ByteStr +import com.zbsnetwork.common.state.diffs.ProduceError._ import com.zbsnetwork.common.utils.EitherExt2 +import com.zbsnetwork.lang.StdLibVersion +import com.zbsnetwork.lang.contract.Contract import com.zbsnetwork.transaction.assets.SetAssetScriptTransaction -import com.zbsnetwork.transaction.smart.script.Script +import com.zbsnetwork.transaction.smart.script.{ContractScript, Script} import org.scalacheck.Gen import play.api.libs.json._ @@ -46,4 +49,19 @@ class SetAssetScriptTransactionSpecification extends GenericTransactionSpecifica ) .explicitGet())) def transactionName: String = "SetAssetScriptTransaction" + + property("issuer can`t make SetAssetScript tx when Script is Contract") { + val accountA = PublicKeyAccount.fromBase58String("5k3gXC486CCFCwzUAgavH9JfPwmq9CbBZvTARnFujvgr").explicitGet() + + SetAssetScriptTransaction + .create( + AddressScheme.current.chainId, + accountA, + ByteStr.decodeBase58("DUyJyszsWcmZG7q2Ctk1hisDeGBPB8dEzyU8Gs5V2j3n").get, + Some(ContractScript(StdLibVersion.V3, Contract(List.empty, List.empty, None)).explicitGet()), + 1222, + System.currentTimeMillis(), + Proofs.empty + ) should produce("not Contract") + } } diff --git a/src/test/scala/com/zbsnetwork/transaction/SetScriptTransactionSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/SetScriptTransactionSpecification.scala index 0beed8f..3206a2b 100644 --- a/src/test/scala/com/zbsnetwork/transaction/SetScriptTransactionSpecification.scala +++ b/src/test/scala/com/zbsnetwork/transaction/SetScriptTransactionSpecification.scala @@ -58,7 +58,7 @@ class SetScriptTransactionSpecification extends GenericTransactionSpecification[ def transactionName: String = "SetScriptTransaction" property("SetScriptTransaction id doesn't depend on proof (spec)") { - forAll(accountGen, proofsGen, proofsGen, scriptGen) { + forAll(accountGen, proofsGen, proofsGen, contractOrExpr) { case (acc: PrivateKeyAccount, proofs1, proofs2, script) => val tx1 = SetScriptTransaction.create(acc, Some(script), 1, 1, proofs1).explicitGet() val tx2 = SetScriptTransaction.create(acc, Some(script), 1, 1, proofs2).explicitGet() diff --git a/src/test/scala/com/zbsnetwork/transaction/assets/exchange/OrderJsonSpecification.scala b/src/test/scala/com/zbsnetwork/transaction/assets/exchange/OrderJsonSpecification.scala index 7622ea2..98481ab 100644 --- a/src/test/scala/com/zbsnetwork/transaction/assets/exchange/OrderJsonSpecification.scala +++ b/src/test/scala/com/zbsnetwork/transaction/assets/exchange/OrderJsonSpecification.scala @@ -47,7 +47,42 @@ class OrderJsonSpecification extends PropSpec with PropertyChecks with Matchers o.timestamp shouldBe 0 o.expiration shouldBe 0 o.signature shouldBe Base58.decode("signature").get + } + val jsonOV3 = Json.parse(s""" + { + "version": 3, + "senderPublicKey": "$pubKeyStr", + "matcherPublicKey": "DZUxn4pC7QdYrRqacmaAJghatvnn1Kh1mkE2scZoLuGJ", + "assetPair": { + "amountAsset": "29ot86P3HoUZXH1FCoyvff7aeZ3Kt7GqPwBWXncjRF2b", + "priceAsset": "GEtBMkg419zhDiYRXKwn2uPcabyXKqUqj4w3Gcs1dq44" + }, + "orderType": "buy", + "amount": 0, + "matcherFee": 0, + "price": 0, + "timestamp": 0, + "expiration": 0, + "signature": "signature", + "matcherFeeAssetId": "29ot86P3HoUZXH1FCoyvff7aeZ3Kt7GqPwBWXncjRF2b" + } """) + + jsonOV3.validate[Order] match { + case JsError(e) => + fail("Error: " + e.toString()) + case JsSuccess(o, _) => + o.senderPublicKey shouldBe PublicKeyAccount(pk.publicKey) + o.matcherPublicKey shouldBe PublicKeyAccount(Base58.decode("DZUxn4pC7QdYrRqacmaAJghatvnn1Kh1mkE2scZoLuGJ").get) + o.assetPair.amountAsset.get shouldBe ByteStr.decodeBase58("29ot86P3HoUZXH1FCoyvff7aeZ3Kt7GqPwBWXncjRF2b").get + o.assetPair.priceAsset.get shouldBe ByteStr.decodeBase58("GEtBMkg419zhDiYRXKwn2uPcabyXKqUqj4w3Gcs1dq44").get + o.price shouldBe 0 + o.amount shouldBe 0 + o.matcherFee shouldBe 0 + o.timestamp shouldBe 0 + o.expiration shouldBe 0 + o.signature shouldBe Base58.decode("signature").get + o.matcherFeeAssetId.get shouldBe ByteStr.decodeBase58("29ot86P3HoUZXH1FCoyvff7aeZ3Kt7GqPwBWXncjRF2b").get } } diff --git a/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptCompilerV1Test.scala b/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptCompilerV1Test.scala index 7db9333..b639926 100644 --- a/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptCompilerV1Test.scala +++ b/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptCompilerV1Test.scala @@ -33,7 +33,18 @@ class ScriptCompilerV1Test extends PropSpec with PropertyChecks with Matchers { ScriptCompiler(script, isAssetScript = false) shouldBe Left("Can't parse language version") } - private val expectedExpr = BLOCK( + property("fails with right error position") { + val script = + """ + | {-# STDLIB_VERSION 3 #-} + | {-# STDLIB_VERSION 3 #-} + | let a = 1000 + | a > b + """.stripMargin + ScriptCompiler.compile(script) shouldBe Left("Compilation failed: A definition of 'b' is not found in 72-73") + } + + private val expectedExpr = LET_BLOCK( LET("x", CONST_LONG(10)), FUNCTION_CALL( PureContext.eq.header, diff --git a/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptReaderTest.scala b/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptReaderTest.scala index f666f8f..f38b3bc 100644 --- a/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptReaderTest.scala +++ b/src/test/scala/com/zbsnetwork/transaction/smart/script/ScriptReaderTest.scala @@ -1,33 +1,29 @@ package com.zbsnetwork.transaction.smart.script -import com.zbsnetwork.common.utils.EitherExt2 -import com.zbsnetwork.crypto -import com.zbsnetwork.lang.StdLibVersion.{V3, V1} -import com.zbsnetwork.lang.contract.Contract -import com.zbsnetwork.lang.contract.Contract.{CallableAnnotation, CallableFunction} +import com.zbsnetwork.common.utils._ +import com.zbsnetwork.lang.StdLibVersion._ import com.zbsnetwork.lang.v1.Serde -import com.zbsnetwork.lang.v1.compiler.Terms -import com.zbsnetwork.lang.v1.compiler.Terms.TRUE +import com.zbsnetwork.lang.v1.testing.TypedScriptGen import com.zbsnetwork.state.diffs.produce -import org.scalatest.{FreeSpec, Matchers} +import com.zbsnetwork.{NoShrink, crypto} +import org.scalatest.prop.PropertyChecks +import org.scalatest.{Inside, Matchers, PropSpec} -class ScriptReaderTest extends FreeSpec with Matchers { +class ScriptReaderTest extends PropSpec with PropertyChecks with Matchers with TypedScriptGen with Inside with NoShrink { val checksumLength = 4 - "should parse all bytes for V1" in { - val body = Array(V1.toByte) ++ Serde.serialize(TRUE) ++ "foo".getBytes - val allBytes = body ++ crypto.secureHash(body).take(checksumLength) - ScriptReader.fromBytes(allBytes) should produce("bytes left") + property("should parse all bytes for V1") { + forAll(exprGen) { sc => + val body = Array(V1.toByte) ++ Serde.serialize(sc) ++ "foo".getBytes + val allBytes = body ++ crypto.secureHash(body).take(checksumLength) + ScriptReader.fromBytes(allBytes) should produce("bytes left") + } } - "should parse all bytes for V3" in { - val sc = ContractScript( - V3, - Contract(List.empty, List(CallableFunction(CallableAnnotation("sender"), Terms.FUNC("foo", List("a"), Terms.REF("a")))), None) - ).explicitGet() - - val allBytes = sc.bytes().arr - ScriptReader.fromBytes(allBytes) shouldBe Right(sc) + property("should parse all bytes for V3") { + forAll(contractGen) { sc => + val allBytes = ContractScript.apply(V3, sc).explicitGet().bytes().arr + ScriptReader.fromBytes(allBytes).explicitGet().expr shouldBe sc + } } - } diff --git a/src/test/scala/com/zbsnetwork/transaction/smart/script/UserFunctionComplexityTest.scala b/src/test/scala/com/zbsnetwork/transaction/smart/script/UserFunctionComplexityTest.scala index 639c6cd..7e44513 100644 --- a/src/test/scala/com/zbsnetwork/transaction/smart/script/UserFunctionComplexityTest.scala +++ b/src/test/scala/com/zbsnetwork/transaction/smart/script/UserFunctionComplexityTest.scala @@ -1,10 +1,12 @@ package com.zbsnetwork.transaction.smart.script import cats.kernel.Monoid +import com.zbsnetwork.common.state.ByteStr import com.zbsnetwork.common.utils.EitherExt2 -import com.zbsnetwork.lang.v1.ScriptEstimator -import com.zbsnetwork.lang.v1.evaluator.ctx.UserFunction -import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs.ZbsContext +import com.zbsnetwork.lang.v1.FunctionHeader.User +import com.zbsnetwork.lang.v1.{CTX, FunctionHeader, ScriptEstimator} +import com.zbsnetwork.lang.v1.compiler.Terms._ +import com.zbsnetwork.lang.v1.evaluator.ctx.impl.zbs._ import com.zbsnetwork.lang.v1.evaluator.ctx.impl.{CryptoContext, PureContext} import com.zbsnetwork.lang.v1.testing.TypedScriptGen import com.zbsnetwork.lang.{Global, StdLibVersion} @@ -17,37 +19,214 @@ import org.scalatest.{Matchers, PropSpec} class UserFunctionComplexityTest extends PropSpec with PropertyChecks with Matchers with TypedScriptGen { - private val version = StdLibVersion.V3 + private def estimate(expr: EXPR, ctx: CTX, funcCosts: Map[FunctionHeader, Coeval[Long]]): Either[String, Long] = { + ScriptEstimator(ctx.evaluationContext.letDefs.keySet, funcCosts, expr) + } + + private val ctxV1 = { + utils.functionCosts(StdLibVersion.V1) + Monoid + .combineAll( + Seq( + PureContext.build(StdLibVersion.V1), + CryptoContext.build(Global), + ZbsContext.build( + StdLibVersion.V1, + new ZbsEnvironment('T'.toByte, Coeval(???), Coeval(???), EmptyBlockchain), + isTokenContext = false + ) + )) + } + private val funcCostsV1 = utils.functionCosts(StdLibVersion.V1) + + property("estimate script for stdLib V1 with UserFunctions") { + + def est: EXPR => Either[String, Long] = estimate(_, ctxV1, funcCostsV1) + + val exprNe = FUNCTION_CALL(PureContext.ne, List(CONST_LONG(1), CONST_LONG(2))) + est(exprNe).explicitGet() shouldBe 28 + + val exprThrow = FUNCTION_CALL(PureContext.throwNoMessage, List()) + est(exprThrow).explicitGet() shouldBe 2 + + val exprExtract = LET_BLOCK( + LET("x", CONST_LONG(2)), + FUNCTION_CALL(PureContext.extract, List(REF("x"))) + ) + est(exprExtract).explicitGet() shouldBe 21 + + val exprIsDefined = LET_BLOCK( + LET("x", CONST_LONG(2)), + FUNCTION_CALL(PureContext.isDefined, List(REF("x"))) + ) + est(exprIsDefined).explicitGet() shouldBe 43 - private val ctx = { + val exprDropRightBytes = FUNCTION_CALL(PureContext.dropRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1))) + est(exprDropRightBytes).explicitGet() shouldBe 21 + + val exprTakeRightBytes = FUNCTION_CALL(PureContext.takeRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1))) + est(exprTakeRightBytes).explicitGet() shouldBe 21 + + val exprDropRightString = FUNCTION_CALL(PureContext.dropRightString, List(CONST_STRING("str"), CONST_LONG(1))) + est(exprDropRightString).explicitGet() shouldBe 21 + + val exprTakeRightString = FUNCTION_CALL(PureContext.takeRightString, List(CONST_STRING("str"), CONST_LONG(1))) + est(exprTakeRightString).explicitGet() shouldBe 21 + + val exprUMinus = FUNCTION_CALL(PureContext.uMinus, List(CONST_LONG(1))) + est(exprUMinus).explicitGet() shouldBe 10 + + val exprUNot = FUNCTION_CALL(PureContext.uNot, List(TRUE)) + est(exprUNot).explicitGet() shouldBe 12 + + val exprAddressFromPublicKey = FUNCTION_CALL(User("addressFromPublicKey"), List(CONST_BYTESTR(ByteStr.fromLong(2)))) + est(exprAddressFromPublicKey).explicitGet() shouldBe 83 + + val exprAddressFromString = FUNCTION_CALL(User("addressFromString"), List(CONST_STRING("address"))) + est(exprAddressFromString).explicitGet() shouldBe 125 + + val exprZbsBalance = FUNCTION_CALL(User("zbsBalance"), List(CONST_STRING("alias"))) + est(exprZbsBalance).explicitGet() shouldBe 110 + } + + private val ctxV2 = { + utils.functionCosts(StdLibVersion.V2) + Monoid + .combineAll( + Seq( + PureContext.build(StdLibVersion.V2), + CryptoContext.build(Global), + ZbsContext.build( + StdLibVersion.V2, + new ZbsEnvironment('T'.toByte, Coeval(???), Coeval(???), EmptyBlockchain), + isTokenContext = false + ) + )) + } + private val funcCostsV2 = utils.functionCosts(StdLibVersion.V2) + + property("estimate script for stdLib V2 with UserFunctions") { + + def est: EXPR => Either[String, Long] = estimate(_, ctxV2, funcCostsV2) + + val exprNe = FUNCTION_CALL(PureContext.ne, List(CONST_LONG(1), CONST_LONG(2))) + est(exprNe).explicitGet() shouldBe 28 + + val exprThrow = FUNCTION_CALL(PureContext.throwNoMessage, List()) + est(exprThrow).explicitGet() shouldBe 2 + + val exprExtract = LET_BLOCK( + LET("x", CONST_LONG(2)), + FUNCTION_CALL(PureContext.extract, List(REF("x"))) + ) + est(exprExtract).explicitGet() shouldBe 21 + + val exprIsDefined = LET_BLOCK( + LET("x", CONST_LONG(2)), + FUNCTION_CALL(PureContext.isDefined, List(REF("x"))) + ) + est(exprIsDefined).explicitGet() shouldBe 43 + + val exprDropRightBytes = FUNCTION_CALL(PureContext.dropRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1))) + est(exprDropRightBytes).explicitGet() shouldBe 21 + + val exprTakeRightBytes = FUNCTION_CALL(PureContext.takeRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1))) + est(exprTakeRightBytes).explicitGet() shouldBe 21 + + val exprDropRightString = FUNCTION_CALL(PureContext.dropRightString, List(CONST_STRING("str"), CONST_LONG(1))) + est(exprDropRightString).explicitGet() shouldBe 21 + + val exprTakeRightString = FUNCTION_CALL(PureContext.takeRightString, List(CONST_STRING("str"), CONST_LONG(1))) + est(exprTakeRightString).explicitGet() shouldBe 21 + + val exprUMinus = FUNCTION_CALL(PureContext.uMinus, List(CONST_LONG(1))) + est(exprUMinus).explicitGet() shouldBe 10 + + val exprUNot = FUNCTION_CALL(PureContext.uNot, List(TRUE)) + est(exprUNot).explicitGet() shouldBe 12 + + val exprAddressFromPublicKey = FUNCTION_CALL(User("addressFromPublicKey"), List(CONST_BYTESTR(ByteStr.fromLong(2)))) + est(exprAddressFromPublicKey).explicitGet() shouldBe 83 + + val exprAddressFromString = FUNCTION_CALL(User("addressFromString"), List(CONST_STRING("address"))) + est(exprAddressFromString).explicitGet() shouldBe 125 + + val exprZbsBalance = FUNCTION_CALL(User("zbsBalance"), List(CONST_STRING("alias"))) + est(exprZbsBalance).explicitGet() shouldBe 110 + } + + private val ctxV3 = { utils.functionCosts(StdLibVersion.V3) Monoid .combineAll( Seq( - PureContext.build(version), + PureContext.build(StdLibVersion.V3), CryptoContext.build(Global), ZbsContext.build( - version, + StdLibVersion.V3, new ZbsEnvironment('T'.toByte, Coeval(???), Coeval(???), EmptyBlockchain), isTokenContext = false ) )) } + private val funcCostsV3 = utils.functionCosts(StdLibVersion.V3) + + property("estimate script for stdLib V3 with UserFunctions") { + + def est: EXPR => Either[String, Long] = estimate(_, ctxV3, funcCostsV3) + + val exprNe = FUNCTION_CALL(PureContext.ne, List(CONST_LONG(1), CONST_LONG(2))) + est(exprNe).explicitGet() shouldBe 3 + + val exprThrow = FUNCTION_CALL(PureContext.throwNoMessage, List()) + est(exprThrow).explicitGet() shouldBe 1 + + val exprExtract = LET_BLOCK( + LET("x", CONST_LONG(2)), + FUNCTION_CALL(PureContext.extract, List(REF("x"))) + ) + est(exprExtract).explicitGet() shouldBe 21 + + val exprIsDefined = LET_BLOCK( + LET("x", CONST_LONG(2)), + FUNCTION_CALL(PureContext.isDefined, List(REF("x"))) + ) + est(exprIsDefined).explicitGet() shouldBe 9 + + val exprDropRightBytes = FUNCTION_CALL(PureContext.dropRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1))) + est(exprDropRightBytes).explicitGet() shouldBe 21 + + val exprTakeRightBytes = FUNCTION_CALL(PureContext.takeRightBytes, List(CONST_BYTESTR(ByteStr.fromLong(2)), CONST_LONG(1))) + est(exprTakeRightBytes).explicitGet() shouldBe 21 + + val exprDropRightString = FUNCTION_CALL(PureContext.dropRightString, List(CONST_STRING("str"), CONST_LONG(1))) + est(exprDropRightString).explicitGet() shouldBe 21 + + val exprTakeRightString = FUNCTION_CALL(PureContext.takeRightString, List(CONST_STRING("str"), CONST_LONG(1))) + est(exprTakeRightString).explicitGet() shouldBe 21 + + val exprUMinus = FUNCTION_CALL(PureContext.uMinus, List(CONST_LONG(1))) + est(exprUMinus).explicitGet() shouldBe 2 + + val exprUNot = FUNCTION_CALL(PureContext.uNot, List(TRUE)) + est(exprUNot).explicitGet() shouldBe 2 + + val exprEnsure = FUNCTION_CALL(PureContext.ensure, List(TRUE)) + est(exprEnsure).explicitGet() shouldBe 17 + + val exprDataByIndex = LET_BLOCK( + LET("arr", FUNCTION_CALL(PureContext.listConstructor, List(CONST_STRING("str_1"), REF("nil")))), + FUNCTION_CALL(User("getString"), List(REF("arr"), CONST_LONG(0))) + ) + est(exprDataByIndex).explicitGet() shouldBe 43 + + val exprAddressFromPublicKey = FUNCTION_CALL(User("addressFromPublicKey"), List(CONST_BYTESTR(ByteStr.fromLong(2)))) + est(exprAddressFromPublicKey).explicitGet() shouldBe 83 + + val exprAddressFromString = FUNCTION_CALL(User("addressFromString"), List(CONST_STRING("address"))) + est(exprAddressFromString).explicitGet() shouldBe 125 - // If test fails than complexity of user function was changed and it could lead to fork. - property("WARNING - NODE FORK - check if user functions complexity changed") { - val funcCosts = utils.functionCosts(version) - - val userFuncs = ctx.functions.filter(_.isInstanceOf[UserFunction]) - userFuncs.foreach { - case func: UserFunction => - import func.signature.args - val complexity = - Coeval.now(ScriptEstimator(ctx.evaluationContext.letDefs.keySet ++ args.map(_._1), funcCosts, func.ev).explicitGet() + args.size * 5).value - if (complexity != func.cost) { - fail(s"Complexity of ${func.name} should be ${func.cost}, actual: $complexity.") - } - case _ => - } + val exprZbsBalance = FUNCTION_CALL(User("zbsBalance"), List(CONST_STRING("alias"))) + est(exprZbsBalance).explicitGet() shouldBe 110 } } diff --git a/src/test/scala/com/zbsnetwork/utils/UtilsSpecification.scala b/src/test/scala/com/zbsnetwork/utils/UtilsSpecification.scala index 46f4325..d013560 100644 --- a/src/test/scala/com/zbsnetwork/utils/UtilsSpecification.scala +++ b/src/test/scala/com/zbsnetwork/utils/UtilsSpecification.scala @@ -1,5 +1,6 @@ package com.zbsnetwork.utils +import com.zbsnetwork.lang.StdLibVersion import com.zbsnetwork.lang.v1.compiler.Terms.{FUNCTION_CALL, TRUE} import com.zbsnetwork.lang.v1.compiler.Types.BOOLEAN import com.zbsnetwork.lang.v1.evaluator.ctx.{EvaluationContext, UserFunction} @@ -16,7 +17,7 @@ class UtilsSpecification extends FreeSpec with Matchers { letDefs = Map.empty, functions = Seq(caller, callee).map(f => f.header -> f)(collection.breakOut) ) - estimate(ctx).size shouldBe 2 + estimate(StdLibVersion.V3, ctx).size shouldBe 2 } } } diff --git a/src/test/scala/com/zbsnetwork/utx/UtxPoolSpecification.scala b/src/test/scala/com/zbsnetwork/utx/UtxPoolSpecification.scala index 2fa8a6b..1b0e25a 100644 --- a/src/test/scala/com/zbsnetwork/utx/UtxPoolSpecification.scala +++ b/src/test/scala/com/zbsnetwork/utx/UtxPoolSpecification.scala @@ -19,13 +19,13 @@ import com.zbsnetwork.mining._ import com.zbsnetwork.settings._ import com.zbsnetwork.state._ import com.zbsnetwork.state.diffs._ -import com.zbsnetwork.transaction.Transaction import com.zbsnetwork.transaction.ValidationError.SenderIsBlacklisted import com.zbsnetwork.transaction.smart.SetScriptTransaction import com.zbsnetwork.transaction.smart.script.Script import com.zbsnetwork.transaction.smart.script.v1.ExprScript import com.zbsnetwork.transaction.transfer.MassTransferTransaction.ParsedTransfer import com.zbsnetwork.transaction.transfer._ +import com.zbsnetwork.transaction.{AssetId, Transaction} import com.zbsnetwork.utils.Implicits.SubjectOps import com.zbsnetwork.utils.Time import monix.reactive.subjects.Subject @@ -38,12 +38,12 @@ import org.scalatest.{FreeSpec, Matchers} import scala.concurrent.duration._ private object UtxPoolSpecification { - private val ignorePortfolioChanged: Subject[Address, Address] = Subject.empty[Address] + private val ignoreSpendableBalanceChanged = Subject.empty[(Address, Option[AssetId])] final case class TempDB(fs: FunctionalitySettings) { val path = Files.createTempDirectory("leveldb-test") val db = openDB(path.toAbsolutePath.toString) - val writer = new LevelDBWriter(db, ignorePortfolioChanged, fs, 100000, 2000, 120 * 60 * 1000) + val writer = new LevelDBWriter(db, ignoreSpendableBalanceChanged, fs, 100000, 2000, 120 * 60 * 1000) Runtime.getRuntime.addShutdownHook(new Thread(() => { db.close() @@ -78,7 +78,7 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with ) val dbContext = TempDB(settings.blockchainSettings.functionalitySettings) - val bcu = StorageFactory(settings, dbContext.db, new TestTime(), ignorePortfolioChanged) + val bcu = StorageFactory(settings, dbContext.db, new TestTime(), ignoreSpendableBalanceChanged) bcu.processBlock(Block.genesis(genesisSettings).explicitGet()).explicitGet() bcu } @@ -128,9 +128,9 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with new UtxPoolImpl( time, bcu, - ignorePortfolioChanged, + ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, - UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true) + UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true) ) val amountPart = (senderBalance - fee) / 2 - fee val txs = for (_ <- 1 to n) yield createZbsTransfer(sender, recipient, amountPart, fee, time.getTimestamp()).explicitGet() @@ -145,9 +145,9 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with new UtxPoolImpl( time, bcu, - ignorePortfolioChanged, + ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, - UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true) + UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true) ) (sender, bcu, utxPool) } @@ -159,8 +159,8 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with time = new TestTime() txs <- Gen.nonEmptyListOf(transferWithRecipient(sender, recipient, senderBalance / 10, time)) } yield { - val settings = UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true) - val utxPool = new UtxPoolImpl(time, bcu, ignorePortfolioChanged, FunctionalitySettings.TESTNET, settings) + val settings = UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true) + val utxPool = new UtxPoolImpl(time, bcu, ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, settings) txs.foreach(utxPool.putIfNew) (sender, bcu, utxPool, time, settings) }).label("withValidPayments") @@ -172,8 +172,8 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with txs <- Gen.nonEmptyListOf(transferWithRecipient(sender, recipient, senderBalance / 10, time)) // @TODO: Random transactions } yield { val settings = - UtxSettings(10, PoolDefaultMaxBytes, Set(sender.address), Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true) - val utxPool = new UtxPoolImpl(time, bcu, ignorePortfolioChanged, FunctionalitySettings.TESTNET, settings) + UtxSettings(10, PoolDefaultMaxBytes, Set(sender.address), Set.empty, allowTransactionsFromSmartAccounts = true) + val utxPool = new UtxPoolImpl(time, bcu, ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, settings) (sender, utxPool, txs) }).label("withBlacklisted") @@ -184,8 +184,8 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with txs <- Gen.nonEmptyListOf(transferWithRecipient(sender, recipient, senderBalance / 10, time)) // @TODO: Random transactions } yield { val settings = - UtxSettings(txs.length, PoolDefaultMaxBytes, Set(sender.address), Set(recipient.address), 5.minutes, allowTransactionsFromSmartAccounts = true) - val utxPool = new UtxPoolImpl(time, bcu, ignorePortfolioChanged, FunctionalitySettings.TESTNET, settings) + UtxSettings(txs.length, PoolDefaultMaxBytes, Set(sender.address), Set(recipient.address), allowTransactionsFromSmartAccounts = true) + val utxPool = new UtxPoolImpl(time, bcu, ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, settings) (sender, utxPool, txs) }).label("withBlacklistedAndAllowedByRule") @@ -199,20 +199,20 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with } yield { val whitelist: Set[String] = if (allowRecipients) recipients.map(_.address).toSet else Set.empty val settings = - UtxSettings(txs.length, PoolDefaultMaxBytes, Set(sender.address), whitelist, 5.minutes, allowTransactionsFromSmartAccounts = true) - val utxPool = new UtxPoolImpl(time, bcu, ignorePortfolioChanged, FunctionalitySettings.TESTNET, settings) + UtxSettings(txs.length, PoolDefaultMaxBytes, Set(sender.address), whitelist, allowTransactionsFromSmartAccounts = true) + val utxPool = new UtxPoolImpl(time, bcu, ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, settings) (sender, utxPool, txs) }).label("massTransferWithBlacklisted") - private def utxTest(utxSettings: UtxSettings = - UtxSettings(20, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true), - txCount: Int = 10)(f: (Seq[TransferTransactionV1], UtxPool, TestTime) => Unit): Unit = + private def utxTest( + utxSettings: UtxSettings = UtxSettings(20, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true), + txCount: Int = 10)(f: (Seq[TransferTransactionV1], UtxPool, TestTime) => Unit): Unit = forAll(stateGen, chooseNum(2, txCount).label("txCount")) { case ((sender, senderBalance, bcu), count) => val time = new TestTime() forAll(listOfN(count, transfer(sender, senderBalance / 2, time))) { txs => - val utx = new UtxPoolImpl(time, bcu, ignorePortfolioChanged, FunctionalitySettings.TESTNET, utxSettings) + val utx = new UtxPoolImpl(time, bcu, ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, utxSettings) f(txs, utx, time) } } @@ -229,9 +229,9 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with val utx = new UtxPoolImpl( time, bcu, - ignorePortfolioChanged, + ignoreSpendableBalanceChanged, FunctionalitySettings.TESTNET, - UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true) + UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true) ) (utx, time, tx1, tx2) } @@ -265,9 +265,9 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with val utx = new UtxPoolImpl( new TestTime(), bcu, - ignorePortfolioChanged, + ignoreSpendableBalanceChanged, smartAccountsFs, - UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, 1.day, allowTransactionsFromSmartAccounts = scEnabled) + UtxSettings(10, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = scEnabled) ) (sender, senderBalance, utx, bcu.lastBlock.fold(0L)(_.timestamp)) @@ -283,13 +283,13 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with "UTX Pool" - { "does not add new transactions when full" in utxTest( - UtxSettings(1, PoolDefaultMaxBytes, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true)) { (txs, utx, _) => + UtxSettings(1, PoolDefaultMaxBytes, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true)) { (txs, utx, _) => utx.putIfNew(txs.head) shouldBe 'right all(txs.tail.map(t => utx.putIfNew(t))) should produce("pool size limit") } "does not add new transactions when full in bytes" in utxTest( - UtxSettings(999999, 152, Set.empty, Set.empty, 5.minutes, allowTransactionsFromSmartAccounts = true)) { (txs, utx, _) => + UtxSettings(999999, 152, Set.empty, Set.empty, allowTransactionsFromSmartAccounts = true)) { (txs, utx, _) => utx.putIfNew(txs.head) shouldBe 'right all(txs.tail.map(t => utx.putIfNew(t))) should produce("pool bytes size limit") } @@ -348,50 +348,73 @@ class UtxPoolSpecification extends FreeSpec with Matchers with MockFactory with utx.all.size shouldBe 2 } - "portfolio" - { - "returns a count of assets from the state if there is no transaction" in forAll(emptyUtxPool) { - case (sender, state, utxPool) => - val basePortfolio = state.portfolio(sender) + "pessimisticPortfolio" - { + "is not empty if there are transactions" in forAll(withValidPayments) { + case (sender, _, utxPool, _, _) => + utxPool.size should be > 0 + utxPool.pessimisticPortfolio(sender) should not be empty + } + "is empty if there is no transactions" in forAll(emptyUtxPool) { + case (sender, _, utxPool) => utxPool.size shouldBe 0 - val utxPortfolio = utxPool.portfolio(sender) - - basePortfolio shouldBe utxPortfolio + utxPool.pessimisticPortfolio(sender) shouldBe empty } - "taking into account unconfirmed transactions" in forAll(withValidPayments) { - case (sender, state, utxPool, _, _) => - val basePortfolio = state.portfolio(sender) - - utxPool.size should be > 0 - val utxPortfolio = utxPool.portfolio(sender) - - utxPortfolio.balance should be <= basePortfolio.balance - utxPortfolio.lease.out should be <= basePortfolio.lease.out - // should not be changed - utxPortfolio.lease.in shouldBe basePortfolio.lease.in - utxPortfolio.assets.foreach { - case (assetId, count) => - count should be <= basePortfolio.assets.getOrElse(assetId, count) - } + "is empty if utx pool was cleaned" in forAll(withValidPayments) { + case (sender, _, utxPool, _, _) => + utxPool.removeAll(utxPool.all) + utxPool.pessimisticPortfolio(sender) shouldBe empty } "is changed after transactions with these assets are removed" in forAll(withValidPayments) { - case (sender, _, utxPool, time, settings) => - val utxPortfolioBefore = utxPool.portfolio(sender) - val poolSizeBefore = utxPool.size + case (sender, _, utxPool, time, _) => + val portfolioBefore = utxPool.pessimisticPortfolio(sender) + val poolSizeBefore = utxPool.size time.advance(maxAge * 2) utxPool.packUnconfirmed(limitByNumber(100)) poolSizeBefore should be > utxPool.size - val utxPortfolioAfter = utxPool.portfolio(sender) + val portfolioAfter = utxPool.pessimisticPortfolio(sender) - utxPortfolioAfter.balance should be >= utxPortfolioBefore.balance - utxPortfolioAfter.lease.out should be >= utxPortfolioBefore.lease.out - utxPortfolioAfter.assets.foreach { - case (assetId, count) => - count should be >= utxPortfolioBefore.assets.getOrElse(assetId, count) + portfolioAfter should not be portfolioBefore + } + } + + "spendableBalance" - { + "equal to state's portfolio if utx is empty" in forAll(emptyUtxPool) { + case (sender, state, utxPool) => + val pessimisticAssetIds = { + val p = utxPool.pessimisticPortfolio(sender) + p.assetIds.filter(x => p.balanceOf(x) != 0) + } + + pessimisticAssetIds shouldBe empty + } + + "takes into account unconfirmed transactions" in forAll(withValidPayments) { + case (sender, state, utxPool, _, _) => + val basePortfolio = state.portfolio(sender) + val baseAssetIds = basePortfolio.assetIds + + val pessimisticAssetIds = { + val p = utxPool.pessimisticPortfolio(sender) + p.assetIds.filter(x => p.balanceOf(x) != 0) + } + + val unchangedAssetIds = baseAssetIds -- pessimisticAssetIds + withClue("unchanged") { + unchangedAssetIds.foreach { assetId => + basePortfolio.balanceOf(assetId) shouldBe basePortfolio.balanceOf(assetId) + } + } + + val changedAssetIds = pessimisticAssetIds -- baseAssetIds + withClue("changed") { + changedAssetIds.foreach { assetId => + basePortfolio.balanceOf(assetId) should not be basePortfolio.balanceOf(assetId) + } } } } diff --git a/zbs-mainnet.conf b/zbs-mainnet.conf index 51d73df..879aed8 100644 --- a/zbs-mainnet.conf +++ b/zbs-mainnet.conf @@ -9,13 +9,14 @@ zbs { # Port number port = 7440 - known-peers = ["206.189.241.175:7440", "142.93.227.63:7440"] + known-peers = ["node1.0bsnetwork.com:7440", "node2.0bsnetwork.com:7440"] # Node name to send during handshake. Give your Node a name, or comment this string out to set random node name. - node-name = "ZBS Mainnet Node" + node-name = "0bsNetwork Full Node" + # String with IP address and port to send as external address during handshake. Could be set automatically if uPnP is enabled. - #declared-address = "1.2.3.4:7440" + # declared-address = "1.2.3.4:7440" } # Wallet settings @@ -33,7 +34,7 @@ zbs { # Node's REST API settings. Enable if you want to use your node for testing your own software developed for the 0bsnetwork platform. rest-api { # Enable/disable node's REST API - enable = no + enable = yes # Network address to bind to. IF YOU WANT TO ENABLE ACCESS VIA THE INTERNET, DO IT USING A REVERSE PROXY (eg. Nginx) FOR SECURITY REASONS. bind-address = "127.0.0.1" @@ -42,14 +43,20 @@ zbs { port = 7441 # Hash of API key string. CHANGE THIS IF YOU ENABLE NODE API. OTHERWISE YOUR NODE WILL BE OPEN TO EVERYONE. - api-key-hash = "86GJVSoboK12zXHYJFzoucAKaFS1yyXA2NztWSt9tGiX" + api-key-hash = "86GJVSoboK12zXHYfdgfgS1yyXA2NztWSt9tGiX" } # Vote to activate features features { auto-shutdown-on-unsupported-feature = yes - supported = [9] + supported = [] } + + miner { + # Enable/disable block generation + enable = no + + } } -include "local.conf" \ No newline at end of file +include "local.conf" diff --git a/zbs-testnet.conf b/zbs-testnet.conf index c31d9e3..21489b4 100644 --- a/zbs-testnet.conf +++ b/zbs-testnet.conf @@ -48,7 +48,7 @@ zbs { # Vote to activate features features { auto-shutdown-on-unsupported-feature = yes - supported = [9] + supported = [9,10,11] } }