Skip to content

Commit

Permalink
Merge pull request #1179 from wavesplatform/NODE-799-base58+base64
Browse files Browse the repository at this point in the history
NODE-799 Improve compile-time decoders
  • Loading branch information
ismagin authored Jun 6, 2018
2 parents 8a95df2 + 3abe2ef commit 0640bf9
Show file tree
Hide file tree
Showing 14 changed files with 81 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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") {
Expand Down
5 changes: 4 additions & 1 deletion lang/js/src/main/scala/com/wavesplatform/lang/Global.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 9 additions & 6 deletions lang/jvm/src/main/scala/com/wavesplatform/lang/Global.scala
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
6 changes: 3 additions & 3 deletions lang/jvm/src/main/scala/com/wavesplatform/utils/Base64.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
26 changes: 20 additions & 6 deletions lang/jvm/src/test/scala/com/wavesplatform/lang/ParserTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)))
Expand Down
3 changes: 2 additions & 1 deletion src/main/scala/com/wavesplatform/state/ByteStr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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) + "..."

Expand All @@ -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] {
Expand Down
3 changes: 1 addition & 2 deletions src/main/scala/com/wavesplatform/state/DataEntry.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down

0 comments on commit 0640bf9

Please sign in to comment.