diff --git a/it/src/test/scala/com/wavesplatform/it/sync/transactions/DataTransactionSuite.scala b/it/src/test/scala/com/wavesplatform/it/sync/transactions/DataTransactionSuite.scala index 00d433a4a53..eed2331c28f 100644 --- a/it/src/test/scala/com/wavesplatform/it/sync/transactions/DataTransactionSuite.scala +++ b/it/src/test/scala/com/wavesplatform/it/sync/transactions/DataTransactionSuite.scala @@ -138,9 +138,9 @@ class DataTransactionSuite extends BaseTransactionSuite { sender.getData(secondAddress, "int") shouldBe intEntry2 sender.getData(secondAddress, "bool") shouldBe boolEntry2 - sender.getData(secondAddress, "blob").equals(blobEntry2) - sender.getData(secondAddress, "str").equals(stringEntry2) - sender.getData(secondAddress).equals(dataAllTypes) + sender.getData(secondAddress, "blob") shouldBe blobEntry2 + sender.getData(secondAddress, "str") shouldBe stringEntry2 + sender.getData(secondAddress) shouldBe dataAllTypes.sortBy(_.key) notMiner.assertBalances(secondAddress, balance2 - fee, eff2 - fee) @@ -218,9 +218,6 @@ class DataTransactionSuite extends BaseTransactionSuite { assertBadRequestAndResponse(sender.postJson("/addresses/data", request(notValidBlobValue + ("value" -> JsString("base64:not a base64")))), "Illegal base64 character") - - assertBadRequestAndResponse(sender.postJson("/addresses/data", request(notValidBlobValue + ("value" -> JsString("yomp")))), - "base64:chars expected") } test("transaction requires a valid proof") { diff --git a/lang/js/src/main/scala/com/wavesplatform/lang/Global.scala b/lang/js/src/main/scala/com/wavesplatform/lang/Global.scala index 0d7f341ac60..fbebaf1790c 100644 --- a/lang/js/src/main/scala/com/wavesplatform/lang/Global.scala +++ b/lang/js/src/main/scala/com/wavesplatform/lang/Global.scala @@ -4,7 +4,10 @@ import com.wavesplatform.lang.v1.BaseGlobal object Global extends BaseGlobal { def base58Encode(input: Array[Byte]): Either[String, String] = impl.Global.base58Encode(input) - def base58Decode(input: String): Either[String, Array[Byte]] = impl.Global.base58Decode(input) + def base58Decode(input: String, limit: Int): Either[String, Array[Byte]] = impl.Global.base58Decode(input, limit) + + def base64Encode(input: Array[Byte]): Either[String, String] = impl.Global.base64Encode(input) + def base64Decode(input: String, limit: Int): Either[String, Array[Byte]] = impl.Global.base64Decode(input, limit) def curve25519verify(message: Array[Byte], sig: Array[Byte], pub: Array[Byte]): Boolean = impl.Global.curve25519verify(message, sig, pub) def keccak256(message: Array[Byte]): Array[Byte] = impl.Global.keccak256(message) diff --git a/lang/js/src/main/scala/com/wavesplatform/lang/impl/Global.scala b/lang/js/src/main/scala/com/wavesplatform/lang/impl/Global.scala index d14e596226f..9082e054aae 100644 --- a/lang/js/src/main/scala/com/wavesplatform/lang/impl/Global.scala +++ b/lang/js/src/main/scala/com/wavesplatform/lang/impl/Global.scala @@ -7,7 +7,10 @@ import scala.scalajs.{js => platform} @JSGlobalScope object Global extends platform.Object { def base58Encode(input: Array[Byte]): Either[String, String] = platform.native - def base58Decode(input: String): Either[String, Array[Byte]] = platform.native + def base58Decode(input: String, limit: Int): Either[String, Array[Byte]] = platform.native + + def base64Encode(input: Array[Byte]): Either[String, String] = platform.native + def base64Decode(input: String, limit: Int): Either[String, Array[Byte]] = platform.native def curve25519verify(message: Array[Byte], sig: Array[Byte], pub: Array[Byte]): Boolean = platform.native def keccak256(message: Array[Byte]): Array[Byte] = platform.native diff --git a/lang/jvm/src/main/scala/com/wavesplatform/lang/Global.scala b/lang/jvm/src/main/scala/com/wavesplatform/lang/Global.scala index a56e00fe523..f33e0528ef5 100644 --- a/lang/jvm/src/main/scala/com/wavesplatform/lang/Global.scala +++ b/lang/jvm/src/main/scala/com/wavesplatform/lang/Global.scala @@ -1,22 +1,25 @@ package com.wavesplatform.lang import com.wavesplatform.lang.v1.BaseGlobal -import com.wavesplatform.utils.Base58 +import com.wavesplatform.utils.{Base58, Base64} import scorex.crypto.hash.{Blake2b256, Keccak256, Sha256} import scorex.crypto.signatures.{Curve25519, PublicKey, Signature} object Global extends BaseGlobal { - val MaxBase58Bytes = 64 - val MaxBase58Chars = 100 - def base58Encode(input: Array[Byte]): Either[String, String] = if (input.length > MaxBase58Bytes) Left(s"base58Encode input exceeds $MaxBase58Bytes") else Right(Base58.encode(input)) - def base58Decode(input: String): Either[String, Array[Byte]] = - if (input.length > MaxBase58Chars) Left(s"base58Decode input exceeds $MaxBase58Chars") + def base58Decode(input: String, limit: Int): Either[String, Array[Byte]] = + if (input.length > limit) Left(s"base58Decode input exceeds $limit") else Base58.decode(input).toEither.left.map(_ => "can't parse Base58 string") + def base64Encode(input: Array[Byte]): Either[String, String] = Right(Base64.encode(input)) + + def base64Decode(input: String, limit: Int): Either[String, Array[Byte]] = + if (input.length > limit) Left(s"base58Decode input exceeds $limit") + else Base64.decode(input).toEither.left.map(_ => "can't parse Base64 string") + def curve25519verify(message: Array[Byte], sig: Array[Byte], pub: Array[Byte]): Boolean = Curve25519.verify(Signature(sig), message, PublicKey(pub)) def keccak256(message: Array[Byte]): Array[Byte] = Keccak256.hash(message) diff --git a/lang/jvm/src/main/scala/com/wavesplatform/utils/Base64.scala b/lang/jvm/src/main/scala/com/wavesplatform/utils/Base64.scala index 78385a6f96e..aae94c82228 100644 --- a/lang/jvm/src/main/scala/com/wavesplatform/utils/Base64.scala +++ b/lang/jvm/src/main/scala/com/wavesplatform/utils/Base64.scala @@ -3,10 +3,10 @@ package com.wavesplatform.utils import scala.util.Try object Base64 { - def encode(input: Array[Byte]): String = "base64:" + new String(java.util.Base64.getEncoder.encode(input)) + def encode(input: Array[Byte]): String = new String(java.util.Base64.getEncoder.encode(input)) def decode(input: String): Try[Array[Byte]] = Try { - if (!input.startsWith("base64:")) throw new IllegalArgumentException("String of the form base64:chars expected") - else java.util.Base64.getDecoder.decode(input.substring(7)) + val str = if (input.startsWith("base64:")) input.substring(7) else input + java.util.Base64.getDecoder.decode(str) } } diff --git a/lang/jvm/src/test/scala/com/wavesplatform/lang/ParserTest.scala b/lang/jvm/src/test/scala/com/wavesplatform/lang/ParserTest.scala index 78d6130dabc..611b3a86053 100644 --- a/lang/jvm/src/test/scala/com/wavesplatform/lang/ParserTest.scala +++ b/lang/jvm/src/test/scala/com/wavesplatform/lang/ParserTest.scala @@ -161,12 +161,26 @@ class ParserTest extends PropSpec with PropertyChecks with Matchers with ScriptG parseOne("base58' bQbp'") shouldBe CONST_BYTEVECTOR(0, 13, PART.INVALID(8, 12, "can't parse Base58 string")) } - property("long base58 definition") { - import Global.MaxBase58Chars - val longBase58 = "A" * (MaxBase58Chars + 1) - val to = 8 + MaxBase58Chars - parseOne(s"base58'$longBase58'") shouldBe - CONST_BYTEVECTOR(0, to + 1, PART.INVALID(8, to, s"base58Decode input exceeds $MaxBase58Chars")) + property("valid non-empty base64 definition") { + parseOne("base64'TElLRQ=='") shouldBe CONST_BYTEVECTOR(0, 16, PART.VALID(8, 15, ByteVector("LIKE".getBytes))) + } + + property("valid empty base64 definition") { + parseOne("base64''") shouldBe CONST_BYTEVECTOR(0, 8, PART.VALID(8, 7, ByteVector.empty)) + } + + property("invalid base64 definition") { + parseOne("base64'mid-size'") shouldBe CONST_BYTEVECTOR(0, 16, PART.INVALID(8, 15, "can't parse Base64 string")) + } + + property("literal too long") { + import Global.MaxLiteralLength + val longLiteral = "A" * (MaxLiteralLength + 1) + val to = 8 + MaxLiteralLength + parseOne(s"base58'$longLiteral'") shouldBe + CONST_BYTEVECTOR(0, to + 1, PART.INVALID(8, to, s"base58Decode input exceeds $MaxLiteralLength")) + parseOne(s"base64'base64:$longLiteral'") shouldBe + CONST_BYTEVECTOR(0, to + 8, PART.INVALID(8, to + 7, s"base58Decode input exceeds $MaxLiteralLength")) } property("string is consumed fully") { diff --git a/lang/jvm/src/test/scala/com/wavesplatform/utils/Base64Test.scala b/lang/jvm/src/test/scala/com/wavesplatform/utils/Base64Test.scala index 5a5d43c76dc..d5282787d30 100644 --- a/lang/jvm/src/test/scala/com/wavesplatform/utils/Base64Test.scala +++ b/lang/jvm/src/test/scala/com/wavesplatform/utils/Base64Test.scala @@ -18,10 +18,9 @@ class Base64Test extends PropSpec with PropertyChecks with Matchers { } yield chars.mkString property("handles empty sequences") { - Base64.encode(Array.emptyByteArray) shouldBe "base64:" - val d = Base64.decode("base64:") - d.isSuccess shouldBe true - d.get.length shouldBe 0 + Base64.encode(Array.emptyByteArray) shouldBe "" + Base64.decode("").get.length shouldBe 0 + Base64.decode("base64:").get.length shouldBe 0 } property("decoding fails on illegal characters") { diff --git a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/BaseGlobal.scala b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/BaseGlobal.scala index 2a1f6e02f5e..4cf97eb4e78 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/BaseGlobal.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/BaseGlobal.scala @@ -5,8 +5,15 @@ package com.wavesplatform.lang.v1 * And IDEA can't find the Global class in the "shared" module, but it must! */ trait BaseGlobal { + val MaxBase58Bytes = 64 + val MaxLiteralLength = 12 * 1024 + val MaxAddressLength = 36 + def base58Encode(input: Array[Byte]): Either[String, String] - def base58Decode(input: String): Either[String, Array[Byte]] + def base58Decode(input: String, limit: Int = MaxLiteralLength): Either[String, Array[Byte]] + + def base64Encode(input: Array[Byte]): Either[String, String] + def base64Decode(input: String, limit: Int = MaxLiteralLength): Either[String, Array[Byte]] def curve25519verify(message: Array[Byte], sig: Array[Byte], pub: Array[Byte]): Boolean diff --git a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/CryptoContext.scala b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/CryptoContext.scala index 15242c96067..12d1e8157c6 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/CryptoContext.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/CryptoContext.scala @@ -29,6 +29,11 @@ object CryptoContext { case (bytes: ByteVector) :: Nil => global.base58Encode(bytes.toArray) case _ => ??? } - EvaluationContext.build(Map.empty, Seq(keccak256F, blake2b256F, sha256F, sigVerifyF, toBase58StringF)) + + def toBase64StringF: PredefFunction = PredefFunction("toBase64String", 10, STRING, List(("bytes", BYTEVECTOR)), "toBase64String") { + case (bytes: ByteVector) :: Nil => global.base64Encode(bytes.toArray) + case _ => ??? + } + EvaluationContext.build(Map.empty, Seq(keccak256F, blake2b256F, sha256F, sigVerifyF, toBase58StringF, toBase64StringF)) } } diff --git a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/EnvironmentFunctions.scala b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/EnvironmentFunctions.scala index 87025e20875..894eaf989c3 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/EnvironmentFunctions.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/evaluator/ctx/impl/EnvironmentFunctions.scala @@ -20,7 +20,7 @@ class EnvironmentFunctions(environment: Environment) { def addressFromString(str: String): Either[String, Option[ByteVector]] = { val base58String = if (str.startsWith(Prefix)) str.drop(Prefix.length) else str - Global.base58Decode(base58String) match { + Global.base58Decode(base58String, Global.MaxAddressLength) match { case Left(e) => Left(e) case Right(addressBytes) => val version = addressBytes.head diff --git a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/parser/Parser.scala b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/parser/Parser.scala index 2be84d87ecb..a851b77eba9 100644 --- a/lang/shared/src/main/scala/com/wavesplatform/lang/v1/parser/Parser.scala +++ b/lang/shared/src/main/scala/com/wavesplatform/lang/v1/parser/Parser.scala @@ -19,7 +19,7 @@ object Parser { import White._ import fastparse.noApi._ - val keywords = Set("let", "base58", "true", "false", "if", "then", "else", "match", "case") + val keywords = Set("let", "base58", "base64", "true", "false", "if", "then", "else", "match", "case") private val lowerChar = CharIn('a' to 'z') private val upperChar = CharIn('A' to 'Z') private val char = lowerChar | upperChar @@ -191,12 +191,15 @@ object Parser { } private val byteVectorP: P[EXPR] = - P(Index ~~ "base58'" ~/ Pass ~~ CharPred(_ != '\'').repX.! ~~ "'" ~~ Index) + P(Index ~~ "base" ~~ ("58" | "64").! ~~ "'" ~/ Pass ~~ CharPred(_ != '\'').repX.! ~~ "'" ~~ Index) .map { - case (start, xs, end) => - val decoded = if (xs.isEmpty) Right(Array.emptyByteArray) else Global.base58Decode(xs) + case (start, base, xs, end) => val innerStart = start + 8 val innerEnd = end - 1 + val decoded = base match { + case "58" => Global.base58Decode(xs) + case "64" => Global.base64Decode(xs) + } decoded match { case Left(err) => CONST_BYTEVECTOR(start, end, PART.INVALID(innerStart, innerEnd, err)) case Right(r) => CONST_BYTEVECTOR(start, end, PART.VALID(innerStart, innerEnd, ByteVector(r))) diff --git a/src/main/scala/com/wavesplatform/state/ByteStr.scala b/src/main/scala/com/wavesplatform/state/ByteStr.scala index 5c58828a668..5865590608a 100644 --- a/src/main/scala/com/wavesplatform/state/ByteStr.scala +++ b/src/main/scala/com/wavesplatform/state/ByteStr.scala @@ -15,7 +15,7 @@ case class ByteStr(arr: Array[Byte]) { lazy val base58: String = Base58.encode(arr) - lazy val base64: String = Base64.encode(arr) + lazy val base64: String = "base64:" + Base64.encode(arr) lazy val trim: String = base58.toString.take(7) + "..." @@ -24,6 +24,7 @@ case class ByteStr(arr: Array[Byte]) { object ByteStr { def decodeBase58(s: String): Try[ByteStr] = Base58.decode(s).map(ByteStr(_)) + def decodeBase64(s: String): Try[ByteStr] = Base64.decode(s).map(ByteStr(_)) val empty: ByteStr = ByteStr(Array.emptyByteArray) implicit val byteStrWrites: Format[ByteStr] = new Format[ByteStr] { diff --git a/src/main/scala/com/wavesplatform/state/DataEntry.scala b/src/main/scala/com/wavesplatform/state/DataEntry.scala index b66ee4a77cd..3d689dbb158 100644 --- a/src/main/scala/com/wavesplatform/state/DataEntry.scala +++ b/src/main/scala/com/wavesplatform/state/DataEntry.scala @@ -4,7 +4,6 @@ import java.nio.charset.StandardCharsets.UTF_8 import com.google.common.primitives.{Longs, Shorts} import com.wavesplatform.state.DataEntry._ -import com.wavesplatform.utils.Base64 import play.api.libs.json._ import scorex.serialization.Deser @@ -70,7 +69,7 @@ object DataEntry { case JsDefined(JsString("binary")) => jsv \ "value" match { case JsDefined(JsString(enc)) => - Base64.decode(enc).fold(ex => JsError(ex.getMessage), arr => JsSuccess(BinaryDataEntry(key, ByteStr(arr)))) + ByteStr.decodeBase64(enc).fold(ex => JsError(ex.getMessage), bstr => JsSuccess(BinaryDataEntry(key, bstr))) case _ => JsError("value is missing or not a string") } case JsDefined(JsString("string")) => diff --git a/src/test/scala/com/wavesplatform/state/diffs/smart/scenarios/NotaryControlledTransferScenartioTest.scala b/src/test/scala/com/wavesplatform/state/diffs/smart/scenarios/NotaryControlledTransferScenartioTest.scala index 789c185f249..11553e73179 100644 --- a/src/test/scala/com/wavesplatform/state/diffs/smart/scenarios/NotaryControlledTransferScenartioTest.scala +++ b/src/test/scala/com/wavesplatform/state/diffs/smart/scenarios/NotaryControlledTransferScenartioTest.scala @@ -118,15 +118,21 @@ class NotaryControlledTransferScenartioTest extends PropSpec with PropertyChecks } property("Script toBase58String") { - eval[Boolean]("toBase58String(base58'AXiXp5CmwVaq4Tp6h6') == \"AXiXp5CmwVaq4Tp6h6\"").explicitGet() shouldBe true + val s = "AXiXp5CmwVaq4Tp6h6" + eval[Boolean](s"""toBase58String(base58'$s') == \"$s\"""").explicitGet() shouldBe true + } + + property("Script toBase64String") { + val s = "Kl0pIkOM3tRikA==" + eval[Boolean](s"""toBase64String(base64'$s') == \"$s\"""").explicitGet() shouldBe true } property("addressFromString() fails when address is too long") { - import Global.MaxBase58Chars - val longAddress = "A" * (MaxBase58Chars + 1) + import Global.MaxAddressLength + val longAddress = "A" * (MaxAddressLength + 1) val r = eval[ByteVector](s"""addressFromString("$longAddress")""") r.isLeft shouldBe true - r.left.get.toString.contains(s"base58Decode input exceeds $MaxBase58Chars") shouldBe true + r.left.get shouldBe s"base58Decode input exceeds $MaxAddressLength" } property("Scenario") {