decouple frank from the main repo

This commit is contained in:
vms 2019-10-10 20:50:26 +03:00
commit 3ef2f7f658
58 changed files with 5513 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# Scala compiled
target/
# Python compiled
*.pyc
# IntelliJ
.idea
.vscode
*.iml
# MacOS folder metadata
.DS_Store
# Text writing
*.scriv
# Temp
docs/checklist.md
# Node modules
node_modules
**/node_modules
# Fluence CLI binary
tools/deploy/fluence
# Contract typings and ABI
Network.d.ts
Network.json
Dashboard.d.ts

56
.scalafmt.conf Normal file
View File

@ -0,0 +1,56 @@
version = 2.0.1
docstrings = JavaDoc
maxColumn = 120
align = some
align.tokens = [{code = "=>", owner = "Case"}, ":=", "%", "%%", "%%%"]
assumeStandardLibraryStripMargin = true
includeCurlyBraceInSelectChains = false
continuationIndent {
callSite = 2
defnSite = 2
extendSite = 4
}
danglingParentheses = true
newlines {
alwaysBeforeTopLevelStatements = true
sometimesBeforeColonInMethodReturnType = true
penalizeSingleSelectMultiArgList = false
alwaysBeforeElseAfterCurlyIf = false
neverInResultType = false
}
spaces {
afterKeywordBeforeParen = true
}
binPack {
parentConstructors = true
literalArgumentLists = true
}
optIn {
breaksInsideChains = false
breakChainOnFirstMethodDot = true
configStyleArguments = true
}
runner {
optimizer {
forceConfigStyleOnOffset = 150
forceConfigStyleMinArgCount = 2
}
}
rewrite {
rules = [
SortImports
]
}

45
build.sbt Normal file
View File

@ -0,0 +1,45 @@
import SbtCommons._
name := "frank"
commons
/* Projects */
lazy val root = (project in file("."))
.aggregate(`vm-scala`, `vm-rust`)
lazy val `vm-rust` = (project in file("vm/src/main/rust/"))
.settings(
compileFrankVMSettings()
)
lazy val `vm-llamadb` = (project in file("vm/src/it/resources/llamadb"))
.settings(
downloadLlamadb()
)
lazy val `vm-scala` = (project in file("vm"))
.configs(IntegrationTest)
.settings(inConfig(IntegrationTest)(Defaults.itSettings): _*)
.settings(
commons,
libraryDependencies ++= Seq(
cats,
catsEffect,
ficus,
cryptoHashsign,
scalaTest,
scalaIntegrationTest,
mockito
),
assemblyJarName in assembly := "frank.jar",
assemblyMergeStrategy in assembly := SbtCommons.mergeStrategy.value,
test in assembly := {},
compile in Compile := (compile in Compile)
.dependsOn(compile in `vm-rust`).value,
test in IntegrationTest := (test in IntegrationTest)
.dependsOn(compile in `vm-llamadb`)
.value
)
.enablePlugins(AutomateHeaderPlugin)

207
project/SbtCommons.scala Normal file
View File

@ -0,0 +1,207 @@
import de.heikoseeberger.sbtheader.HeaderPlugin.autoImport.headerLicense
import de.heikoseeberger.sbtheader.License
import org.scalafmt.sbt.ScalafmtPlugin.autoImport.scalafmtOnCompile
import sbt.Keys.{javaOptions, _}
import sbt.{Def, addCompilerPlugin, taskKey, _}
import sbtassembly.AssemblyPlugin.autoImport.assemblyMergeStrategy
import sbtassembly.{MergeStrategy, PathList}
import scala.sys.process._
object SbtCommons {
val scalaV = scalaVersion := "2.12.9"
val kindProjector = Seq(
resolvers += Resolver.sonatypeRepo("releases"),
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.10.0")
)
val commons = Seq(
scalaV,
version := "0.1.1",
fork in Test := true,
parallelExecution in Test := false,
fork in IntegrationTest := true,
parallelExecution in IntegrationTest := false,
organizationName := "Fluence Labs Limited",
organizationHomepage := Some(new URL("https://fluence.network")),
startYear := Some(2019),
licenses += ("Apache-2.0", new URL("https://www.apache.org/licenses/LICENSE-2.0.txt")),
headerLicense := Some(License.ALv2("2019", organizationName.value)),
resolvers += Resolver.bintrayRepo("fluencelabs", "releases"),
scalafmtOnCompile := true,
// see good explanation https://gist.github.com/djspiewak/7a81a395c461fd3a09a6941d4cd040f2
scalacOptions ++= Seq("-Ypartial-unification", "-deprecation"),
javaOptions in Test ++= Seq(
"-XX:MaxMetaspaceSize=4G",
"-Xms4G",
"-Xmx4G",
"-Xss6M",
s"-Djava.library.path=${file("").getAbsolutePath}/vm/src/main/rust/target/release"
),
javaOptions in IntegrationTest ++= Seq(
"-XX:MaxMetaspaceSize=4G",
"-Xms4G",
"-Xmx4G",
"-Xss6M",
s"-Djava.library.path=${file("").getAbsolutePath}/vm/src/main/rust/target/release"
),
addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.0")
) ++ kindProjector
val mergeStrategy = Def.setting[String => MergeStrategy]({
// a module definition fails compilation for java 8, just skip it
case PathList("module-info.class", xs @ _*) => MergeStrategy.first
case "META-INF/io.netty.versions.properties" => MergeStrategy.first
case x =>
import sbtassembly.AssemblyPlugin.autoImport.assembly
val oldStrategy = (assemblyMergeStrategy in assembly).value
oldStrategy(x)
}: String => MergeStrategy)
val rustToolchain = "nightly-2019-09-23"
def installPrerequisites() = {
val installRust = s"curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $rustToolchain"
val installToolchain = s"~/.cargo/bin/rustup toolchain install $rustToolchain"
val installCross = "cargo install cross"
//assert((installRust !) == 0, "Rust installation failed")
//assert((installToolchain !) == 0 , s"toolchain $rustToolchain installation failed")
(installCross !)
}
def compileFrank() = {
val projectRoot = file("").getAbsolutePath
val frankFolder = s"$projectRoot/vm/src/main/rust"
val localCompileCmd = s"cargo +$rustToolchain build --manifest-path $frankFolder/Cargo.toml --release --lib"
val crossCompileCmd = s"cd $frankFolder ; cross build --target x86_64-unknown-linux-gnu --release --lib"
println(crossCompileCmd)
assert((localCompileCmd !) == 0, "Frank VM native compilation failed")
assert((crossCompileCmd !) == 0, "Cross compilation to linux failed")
}
def compileFrankVMSettings(): Seq[Def.Setting[_]] =
Seq(
publishArtifact := false,
test := (test in Test).dependsOn(compile).value,
compile := (compile in Compile)
.dependsOn(Def.task {
val log = streams.value.log
log.info("Installing prerequisites for native part compilation")
installPrerequisites()
log.info("Compiling Frank VM")
compileFrank()
})
.value
)
def downloadLlamadb(): Seq[Def.Setting[_]] =
Seq(
publishArtifact := false,
test := (test in Test).dependsOn(compile).value,
compile := (compile in Compile)
.dependsOn(Def.task {
// by defaults, user.dir in sbt points to a submodule directory while in Idea to the project root
val resourcesPath =
if (System.getProperty("user.dir").endsWith("/vm"))
System.getProperty("user.dir") + "/src/it/resources/"
else
System.getProperty("user.dir") + "/vm/src/it/resources/"
val log = streams.value.log
val llamadbUrl = "https://github.com/fluencelabs/llamadb-wasm/releases/download/0.1.2/llama_db.wasm"
val llamadbPreparedUrl =
"https://github.com/fluencelabs/llamadb-wasm/releases/download/0.1.2/llama_db_prepared.wasm"
log.info(s"Dowloading llamadb from $llamadbUrl to $resourcesPath")
// -nc prevents downloading if file already exists
val llamadbDownloadRet = s"wget -nc $llamadbUrl -O $resourcesPath/llama_db.wasm" !
val llamadbPreparedDownloadRet = s"wget -nc $llamadbPreparedUrl -O $resourcesPath/llama_db_prepared.wasm" !
// wget returns 0 of file was downloaded and 1 if file already exists
assert(llamadbDownloadRet == 0 || llamadbDownloadRet == 1, s"Download failed: $llamadbUrl")
assert(
llamadbPreparedDownloadRet == 0 || llamadbPreparedDownloadRet == 1,
s"Download failed: $llamadbPreparedUrl"
)
})
.value
)
/* Common deps */
val catsVersion = "2.0.0"
val cats = "org.typelevel" %% "cats-core" % catsVersion
val catsEffectVersion = "2.0.0"
val catsEffect = "org.typelevel" %% "cats-effect" % catsEffectVersion
val shapeless = "com.chuusai" %% "shapeless" % "2.3.3"
val fs2Version = "1.0.4"
val fs2 = "co.fs2" %% "fs2-core" % fs2Version
val fs2rx = "co.fs2" %% "fs2-reactive-streams" % fs2Version
val fs2io = "co.fs2" %% "fs2-io" % fs2Version
// functional wrapper around 'lightbend/config'
val ficus = "com.iheart" %% "ficus" % "1.4.5"
val cryptoVersion = "0.0.9"
val cryptoHashsign = "one.fluence" %% "crypto-hashsign" % cryptoVersion
val cryptoJwt = "one.fluence" %% "crypto-jwt" % cryptoVersion
val cryptoCipher = "one.fluence" %% "crypto-cipher" % cryptoVersion
val codecVersion = "0.0.5"
val codecCore = "one.fluence" %% "codec-core" % codecVersion
val sttpVersion = "1.6.3"
val sttp = "com.softwaremill.sttp" %% "core" % sttpVersion
val sttpCirce = "com.softwaremill.sttp" %% "circe" % sttpVersion
val sttpFs2Backend = "com.softwaremill.sttp" %% "async-http-client-backend-fs2" % sttpVersion
val sttpCatsBackend = "com.softwaremill.sttp" %% "async-http-client-backend-cats" % sttpVersion
val http4sVersion = "0.20.10"
val http4sDsl = "org.http4s" %% "http4s-dsl" % http4sVersion
val http4sServer = "org.http4s" %% "http4s-blaze-server" % http4sVersion
val http4sCirce = "org.http4s" %% "http4s-circe" % http4sVersion
val circeVersion = "0.12.1"
val circeCore = "io.circe" %% "circe-core" % circeVersion
val circeGeneric = "io.circe" %% "circe-generic" % circeVersion
val circeGenericExtras = "io.circe" %% "circe-generic-extras" % circeVersion
val circeParser = "io.circe" %% "circe-parser" % circeVersion
val circeFs2 = "io.circe" %% "circe-fs2" % "0.11.0"
val scodecBits = "org.scodec" %% "scodec-bits" % "1.1.9"
val scodecCore = "org.scodec" %% "scodec-core" % "1.11.3"
val web3jVersion = "4.5.0"
val web3jCrypto = "org.web3j" % "crypto" % web3jVersion
val web3jCore = "org.web3j" % "core" % web3jVersion
val toml = "com.electronwill.night-config" % "toml" % "3.4.2"
val rocksDb = "org.rocksdb" % "rocksdbjni" % "5.17.2"
val levelDb = "org.iq80.leveldb" % "leveldb" % "0.12"
val protobuf = "io.github.scalapb-json" %% "scalapb-circe" % "0.4.3"
val protobufUtil = "com.google.protobuf" % "protobuf-java-util" % "3.7.1"
val bouncyCastle = "org.bouncycastle" % "bcprov-jdk15on" % "1.61"
val asyncHttpClient = "org.asynchttpclient" % "async-http-client" % "2.8.1"
/* Test deps*/
val scalacheckShapeless = "com.github.alexarchambault" %% "scalacheck-shapeless_1.13" % "1.1.8" % Test
val catsTestkit = "org.typelevel" %% "cats-testkit" % catsVersion % Test
val disciplineScalaTest = "org.typelevel" %% "discipline-scalatest" % "1.0.0-M1" % Test
val scalaTest = "org.scalatest" %% "scalatest" % "3.0.8" % Test
val scalaIntegrationTest = "org.scalatest" %% "scalatest" % "3.0.8" % IntegrationTest
val mockito = "org.mockito" % "mockito-core" % "2.21.0" % Test
}

1
project/build.properties Normal file
View File

@ -0,0 +1 @@
sbt.version=1.2.8

9
project/plugins.sbt Normal file
View File

@ -0,0 +1,9 @@
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.1")
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.2.0")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
addSbtPlugin("ch.jodersky" % "sbt-jni" % "1.3.4")

2
vm/src/it/resources/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Downloaded test artifacts
*.wasm

View File

@ -0,0 +1,60 @@
#
# These settings describe VM configs for integrations test launches with different memory sizes.
# More info about each field meaning can be found in vm/src/main/resources/reference.conf.
#
fluence.vm.client.4Mb {
// 65536 * 64 = 4 Mb
memPagesCount: 64
loggerEnabled: true
chunkSize: 4096
mainModuleConfig: {
allocateFunctionName: "allocate"
deallocateFunctionName: "deallocate"
invokeFunctionName: "invoke"
}
}
fluence.vm.client.100Mb {
// 65536 * 1600 = 100 Mb
memPagesCount: 1600
loggerEnabled: true
chunkSize: 4096
mainModuleConfig: {
allocateFunctionName: "allocate"
deallocateFunctionName: "deallocate"
invokeFunctionName: "invoke"
}
}
fluence.vm.client.2Gb {
// 65536 * 32767 ~ 2 Gb
memPagesCount: 12767
loggerEnabled: true
chunkSize: 4096
mainModuleConfig: {
allocateFunctionName: "allocate"
deallocateFunctionName: "deallocate"
invokeFunctionName: "invoke"
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2018 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm
import cats.data.EitherT
import cats.effect.IO
import org.scalatest.{Assertion, EitherValues, Matchers, OptionValues, WordSpec}
trait AppIntegrationTest extends WordSpec with Matchers with OptionValues with EitherValues {
protected def checkTestResult(result: InvocationResult, expectedString: String): Assertion = {
val resultAsString = new String(result.output)
resultAsString should startWith(expectedString)
}
protected def compareArrays(first: Array[Byte], second: Array[Byte]): Assertion =
first.deep shouldBe second.deep
implicit class EitherTValueReader[E <: Throwable, V](origin: EitherT[IO, E, V]) {
def success(): V =
origin.value.unsafeRunSync() match {
case Left(e) => println(s"got error $e"); throw e
case Right(v) => v
}
def failed(): E =
origin.value.unsafeRunSync().left.value
}
}

View File

@ -0,0 +1,115 @@
/*
* Copyright 2018 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm
import cats.data.NonEmptyList
import cats.effect.{IO, Timer}
import scala.concurrent.ExecutionContext
import scala.language.{higherKinds, implicitConversions}
import fluence.vm.Utils.getModuleDirPrefix
// TODO: to run this test from IDE It needs to download vm-llamadb project explicitly at first
// this test is separated from the main LlamadbIntegrationTest because gas price for each instruction could be changed
// and it would be difficult to update them in each test
class LlamadbInstrumentedIntegrationTest extends LlamadbIntegrationTestInterface {
override val llamadbFilePath: String = getModuleDirPrefix() +
"/src/it/resources/llama_db_prepared.wasm"
private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
"instrumented llamadb app" should {
"be able to instantiate" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
state vm.computeVmState[IO].toVmError
} yield {
state should not be None
}).success()
}
"be able to create table and insert to it" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
createResult createTestTable(vm)
} yield {
checkTestResult(createResult, "rows inserted")
createResult.spentGas should equal(527599L)
}).success()
}
"be able to select records" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
createResult createTestTable(vm)
emptySelectResult executeSql(vm, "SELECT * FROM Users WHERE name = 'unknown'")
selectAllResult executeSql(vm, "SELECT min(id), max(id), count(age), sum(age), avg(age) FROM Users")
explainResult executeSql(vm, "EXPLAIN SELECT id, name FROM Users")
} yield {
checkTestResult(createResult, "rows inserted")
checkTestResult(emptySelectResult, "id, name, age")
checkTestResult(
selectAllResult,
"_0, _1, _2, _3, _4\n" +
"1, 4, 4, 98, 24.5"
)
checkTestResult(
explainResult,
"query plan\n" +
"column names: (`id`, `name`)\n" +
"(scan `users` :source-id 0\n" +
" (yield\n" +
" (column-field :source-id 0 :column-offset 0)\n" +
" (column-field :source-id 0 :column-offset 1)))"
)
createResult.spentGas should equal(527599L)
emptySelectResult.spentGas should equal(370143L)
selectAllResult.spentGas should equal(754557L)
explainResult.spentGas should equal(387359L)
}).success()
}
"be able to launch VM with 2 GiB memory and a lot of data inserts" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.2Gb")
_ createTestTable(vm)
// trying to insert 30 times by ~200 KiB
_ = for (_ 1 to 30) yield { executeInsert(vm, 200) }.value.unsafeRunSync
insertResult executeInsert(vm, 1)
} yield {
checkTestResult(insertResult, "rows inserted")
insertResult.spentGas should equal(1469167L)
}).success()
}
}
}

View File

@ -0,0 +1,285 @@
/*
* Copyright 2018 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm
import cats.data.NonEmptyList
import cats.effect.{IO, Timer}
import scala.concurrent.ExecutionContext
import scala.language.{higherKinds, implicitConversions}
// TODO: to run this test from IDE It needs to download vm-llamadb project explicitly at first
class LlamadbIntegrationTest extends LlamadbIntegrationTestInterface {
private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
"llamadb app" should {
"be able to instantiate" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
state vm.computeVmState[IO].toVmError
} yield {
state should not be None
}).success()
}
"be able to create table and insert to it" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
createResult createTestTable(vm)
} yield {
checkTestResult(createResult, "rows inserted")
}).success()
}
"be able to select records" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
createResult createTestTable(vm)
emptySelectResult executeSql(vm, "SELECT * FROM Users WHERE name = 'unknown'")
selectAllResult executeSql(vm, "SELECT min(id), max(id), count(age), sum(age), avg(age) FROM Users")
explainResult executeSql(vm, "EXPLAIN SELECT id, name FROM Users")
} yield {
checkTestResult(createResult, "rows inserted")
checkTestResult(emptySelectResult, "id, name, age")
checkTestResult(
selectAllResult,
"_0, _1, _2, _3, _4\n" +
"1, 4, 4, 98, 24.5"
)
checkTestResult(
explainResult,
"query plan\n" +
"column names: (`id`, `name`)\n" +
"(scan `users` :source-id 0\n" +
" (yield\n" +
" (column-field :source-id 0 :column-offset 0)\n" +
" (column-field :source-id 0 :column-offset 1)))"
)
}).success()
}
"be able to delete records and drop table" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
createResult1 createTestTable(vm)
deleteResult executeSql(vm, "DELETE FROM Users WHERE id = 1")
selectAfterDeleteTable executeSql(vm, "SELECT * FROM Users WHERE id = 1")
truncateResult executeSql(vm, "TRUNCATE TABLE Users")
selectFromTruncatedTableResult executeSql(vm, "SELECT * FROM Users")
createResult2 createTestTable(vm)
dropTableResult executeSql(vm, "DROP TABLE Users")
selectFromDroppedTableResult executeSql(vm, "SELECT * FROM Users")
} yield {
checkTestResult(createResult1, "rows inserted")
checkTestResult(deleteResult, "rows deleted: 1")
checkTestResult(selectAfterDeleteTable, "id, name, age")
checkTestResult(truncateResult, "rows deleted: 3")
checkTestResult(selectFromTruncatedTableResult, "id, name, age")
checkTestResult(createResult2, "rows inserted")
checkTestResult(dropTableResult, "table was dropped")
checkTestResult(selectFromDroppedTableResult, "[Error] table does not exist: users")
}).success()
}
"be able to manipulate with 2 tables and selects records with join" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
createResult createTestTable(vm)
createRoleResult executeSql(vm, "CREATE TABLE Roles(user_id INT, role VARCHAR(128))")
roleInsertResult executeSql(
vm,
"INSERT INTO Roles VALUES(1, 'Teacher'), (2, 'Student'), (3, 'Scientist'), (4, 'Writer')"
)
selectWithJoinResult executeSql(
vm,
"SELECT u.name AS Name, r.role AS Role FROM Users u JOIN Roles r ON u.id = r.user_id WHERE r.role = 'Writer'"
)
deleteResult executeSql(
vm,
"DELETE FROM Users WHERE id = (SELECT user_id FROM Roles WHERE role = 'Student')"
)
updateResult executeSql(
vm,
"UPDATE Roles r SET r.role = 'Professor' WHERE r.user_id = " +
"(SELECT id FROM Users WHERE name = 'Sara')"
)
} yield {
checkTestResult(createResult, "rows inserted")
checkTestResult(createRoleResult, "table created")
checkTestResult(roleInsertResult, "rows inserted: 4")
checkTestResult(
selectWithJoinResult,
"name, role\n" +
"Tagless Final, Writer"
)
checkTestResult(deleteResult, "rows deleted: 1")
checkTestResult(updateResult, "[Error] subquery must yield exactly one row")
}).success()
}
"be able to operate with empty strings" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
_ executeSql(vm, "")
_ createTestTable(vm)
emptyQueryResult executeSql(vm, "")
} yield {
checkTestResult(
emptyQueryResult,
"[Error] Expected SELECT, INSERT, CREATE, DELETE, TRUNCATE or EXPLAIN statement; got no more tokens"
)
}).success()
}
"doesn't fail with incorrect queries" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
_ createTestTable(vm)
invalidQueryResult executeSql(vm, "SELECT salary FROM Users")
parserErrorResult executeSql(vm, "123")
incompatibleTypeResult executeSql(vm, "SELECT * FROM Users WHERE age = 'Bob'")
} yield {
checkTestResult(invalidQueryResult, "[Error] column does not exist: salary")
checkTestResult(
parserErrorResult,
"[Error] Expected SELECT, INSERT, CREATE, DELETE, TRUNCATE or EXPLAIN statement; got Number(\"123\")"
)
checkTestResult(incompatibleTypeResult, "[Error] 'Bob' cannot be cast to Integer { signed: true, bytes: 8 }")
}).success()
}
"be able to launch VM with 4 MiB memory and to insert a lot of data" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
_ createTestTable(vm)
// allocate ~1 MiB memory
insertResult1 executeInsert(vm, 512)
insertResult2 executeInsert(vm, 512)
} yield {
checkTestResult(insertResult1, "rows inserted")
checkTestResult(insertResult2, "rows inserted")
}).success()
}
"be able to launch VM with 4 MiB memory and a lot of data inserts" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.4Mb")
_ createTestTable(vm)
// trying to insert 1024 time by 1 KiB
_ = for (_ 1 to 1024) yield { executeInsert(vm, 1) }.value.unsafeRunSync
insertResult executeInsert(vm, 1)
} yield {
checkTestResult(insertResult, "rows inserted")
}).success()
}
"be able to launch VM with 100 MiB memory and to insert a lot of data" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.100Mb")
_ createTestTable(vm)
// allocate ~30 MiB memory
insertResult1 executeInsert(vm, 15 * 1024)
insertResult2 executeInsert(vm, 15 * 1024)
} yield {
checkTestResult(insertResult1, "rows inserted")
checkTestResult(insertResult2, "rows inserted")
}).success()
}
"be able to launch VM with 100 MiB memory and a lot of data inserts" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.100Mb")
_ createTestTable(vm)
// trying to insert 30 time by ~100 KiB
_ = for (_ 1 to 30) yield { executeInsert(vm, 100) }.value.unsafeRunSync
insertResult executeInsert(vm, 1)
} yield {
checkTestResult(insertResult, "rows inserted")
}).success()
}
"be able to launch VM with 2 GiB memory and to allocate 256 MiB of continuously memory" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.2Gb")
_ executeSql(vm, "create table USERS(name varchar(" + 256 * 1024 * 1024 + "))")
// trying to insert two records to ~256 MiB field
insertResult1 executeSql(vm, "insert into USERS values(\'" + "A" * 1024 + "\')")
insertResult2 executeSql(vm, "insert into USERS values(\'" + "A" * 1024 + "\')")
} yield {
checkTestResult(insertResult1, "rows inserted")
checkTestResult(insertResult2, "rows inserted")
}).success()
}
"be able to launch VM with 2 GiB memory and a lot of data inserts" in {
(for {
vm WasmVm[IO](NonEmptyList.one(llamadbFilePath), "fluence.vm.client.2Gb")
_ createTestTable(vm)
// trying to insert 30 time by ~200 KiB
_ = for (_ 1 to 30) yield { executeInsert(vm, 200) }.value.unsafeRunSync
insertResult executeInsert(vm, 1)
} yield {
checkTestResult(insertResult, "rows inserted")
}).success()
}
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2018 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm
import cats.data.EitherT
import cats.effect.IO
import fluence.vm.error.VmError
import org.scalatest.EitherValues
import fluence.vm.Utils.getModuleDirPrefix
import scala.language.{higherKinds, implicitConversions}
trait LlamadbIntegrationTestInterface extends AppIntegrationTest with EitherValues {
protected val llamadbFilePath: String = getModuleDirPrefix() +
"/src/it/resources/llama_db.wasm"
protected def executeSql(implicit vm: WasmVm, sql: String): EitherT[IO, VmError, InvocationResult] =
for {
result vm.invoke[IO](sql.getBytes())
_ vm.computeVmState[IO].toVmError
} yield result
protected def createTestTable(vm: WasmVm): EitherT[IO, VmError, InvocationResult] =
for {
_ executeSql(vm, "CREATE TABLE Users(id INT, name TEXT, age INT)")
insertResult executeSql(
vm,
"INSERT INTO Users VALUES(1, 'Monad', 23)," +
"(2, 'Applicative Functor', 19)," +
"(3, 'Free Monad', 31)," +
"(4, 'Tagless Final', 25)"
)
} yield insertResult
// inserts about (recordsCount KiB + const bytes)
protected def executeInsert(vm: WasmVm, recordsCount: Int): EitherT[IO, VmError, InvocationResult] =
for {
result executeSql(
vm,
"INSERT into USERS VALUES(1, 'A', 1)" + (",(1, \'" + "A" * 1024 + "\', 1)") * recordsCount
)
} yield result
}

View File

@ -0,0 +1,34 @@
#
# These settings describe the reasonable defaults for WasmVm.
#
fluence.vm.client {
# To obtain deterministic execution, all Wasm memory is preallocated on the VM startup.
# This parameter defines count of Wasm pages that should be preallocated. Each page contains 65536 bytes of data,
# `65536 * 1600 ~ 100MB`
memPagesCount: 1600
# if true, allows Wasm code to use logging
loggerEnabled: true
# Memory will be split by chunks to be able to build Merkle Tree on top of it.
# Size of memory in bytes must be dividable by chunkSize.
chunkSize: 4096
mainModuleConfig: {
# The main module name according to the conventions should be non set
# name: "main"
# The name of function that should be called for allocation memory. This function
# is used for passing array of bytes to the main module.
allocateFunctionName: "allocate"
# The name of function that should be called for deallocation of
# previously allocated memory by allocateFunction.
deallocateFunctionName: "deallocate"
# The name of the main module handler function.
invokeFunctionName: "invoke"
}
}

1272
vm/src/main/rust/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
[package]
name = "Frank"
description = "Virtual machine based on Wasmer for the Fluence network"
version = "0.1.0"
authors = ["Fluence Labs"]
edition = "2018"
license = "Apache-2.0"
keywords = ["fluence", "webassembly", "wasmer", "execution environment"]
categories = ["webassembly"]
repository = "https://github.com/fluencelabs/fluence/tree/master/vm/frank"
maintenance = { status = "actively-developed" }
[lib]
name = "frank"
path = "src/lib.rs"
crate-type = ["cdylib"]
[[bin]]
name = "frank"
path = "src/main.rs"
[dependencies]
wasmer-runtime = {git = "http://github.com/fluencelabs/wasmer", branch = "clif_jni_hardering"}
wasmer-runtime-core = {git = "http://github.com/fluencelabs/wasmer", branch = "clif_jni_hardering"}
jni = "0.13.1"
failure = "0.1.5"
lazy_static = "1.4.0"
sha2 = "0.8.0"
clap = "2.33.0"
exitfailure = "0.5.1"
boolinator = "2.4.0"
parity-wasm = "0.40.3"
pwasm-utils = "0.11.0"
[profile.release]
opt-level = 3
debug = false
lto = true
debug-assertions = false
overflow-checks = false
panic = "abort"

View File

@ -0,0 +1,143 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// Defines export functions that will be accessible from the Scala part.
use crate::jni::jni_results::*;
use crate::vm::config::Config;
use crate::vm::errors::FrankError;
use crate::vm::frank::{Frank, FRANK};
use crate::vm::frank_result::FrankResult;
use crate::vm::prepare::prepare_module;
use jni::objects::{JClass, JObject, JString};
use jni::sys::jbyteArray;
use jni::JNIEnv;
use sha2::digest::generic_array::GenericArray;
use std::fs;
/// Initializes Frank virtual machine.
/// This method is exported to Scala.
///
/// Arguments
///
/// * env - corresponding java native interface
/// * _class - represents caller class
/// * module_path - a path to module that should be loaded to Frank virtual machine
/// * config - contains some configs that manage module loading and instantiation process
///
/// Returns a object of RawInitializationResult case class
#[no_mangle]
pub extern "system" fn Java_fluence_vm_frank_FrankAdapter_initialize<'a>(
env: JNIEnv<'a>,
_class: JClass,
module_path: JString,
config: JObject,
) -> JObject<'a> {
fn initialize<'a>(
env: &JNIEnv<'a>,
module_path: JString,
config: JObject,
) -> Result<(bool), FrankError> {
let module_path: String = env.get_string(module_path)?.into();
let config = Config::new(&env, config)?;
let wasm_code = fs::read(module_path)?;
let prepared_module = prepare_module(&wasm_code, &config)?;
let frank = Frank::new(&prepared_module, config)?;
unsafe { FRANK = Some(Box::new(frank.0)) };
Ok(frank.1)
}
match initialize(&env, module_path, config) {
Ok(expects_eths) => create_initialization_result(&env, None, expects_eths),
Err(err) => create_initialization_result(&env, Some(format!("{}", err)), false),
}
}
/// Invokes the main module entry point function.
/// This method is exported to Scala.
///
/// Arguments
///
/// * env - corresponding java native interface
/// * _class - represents caller class
/// * fn_argument - an argument for thr main module entry point function
///
/// Returns a object of RawInvocationResult case class
#[no_mangle]
pub extern "system" fn Java_fluence_vm_frank_FrankAdapter_invoke<'a>(
env: JNIEnv<'a>,
_class: JClass,
fn_argument: jbyteArray,
) -> JObject<'a> {
fn invoke(env: &JNIEnv, fn_argument: jbyteArray) -> Result<FrankResult, FrankError> {
let input_len = env.get_array_length(fn_argument)?;
let mut input = vec![0; input_len as _];
env.get_byte_array_region(fn_argument, 0, input.as_mut_slice())?;
// converts Vec<i8> to Vec<u8> without additional allocation
let u8_input = unsafe {
Vec::<u8>::from_raw_parts(input.as_mut_ptr() as *mut u8, input.len(), input.capacity())
};
std::mem::forget(input);
unsafe {
match FRANK {
Some(ref mut vm) => Ok(vm.invoke(&u8_input)?),
None => Err(FrankError::FrankNotInitialized),
}
}
}
match invoke(&env, fn_argument) {
Ok(result) => create_invocation_result(&env, None, result),
Err(err) => {
create_invocation_result(&env, Some(format!("{}", err)), FrankResult::default())
}
}
}
/// Computes hash of the internal VM state.
/// This method is exported to Scala.
///
/// Arguments
///
/// * env - corresponding java native interface
/// * _class - represents caller class
///
/// Returns a object of RawStateComputationResult case class
#[no_mangle]
pub extern "system" fn Java_fluence_vm_frank_FrankAdapter_computeVmState<'a>(
env: JNIEnv<'a>,
_class: JClass,
) -> JObject<'a> {
unsafe {
match FRANK {
Some(ref mut vm) => {
let state = vm.compute_vm_state_hash();
create_state_computation_result(&env, None, state)
}
None => create_state_computation_result(
&env,
Some(format!("{}", FrankError::FrankNotInitialized)),
GenericArray::default(),
),
}
}
}

View File

@ -0,0 +1,109 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// Defines functions used to construct result of the VM invocation for the Scala part.
/// Corresponding case classes could be found in vm/src/main/scala/fluence/vm/frank/result.
use crate::jni::option::*;
use crate::vm::frank_result::FrankResult;
use jni::objects::{JObject, JValue};
use jni::JNIEnv;
use sha2::{digest::generic_array::GenericArray, digest::FixedOutput, Sha256};
/// Creates RawInitializationResult object for the Scala part.
pub fn create_initialization_result<'a>(
env: &JNIEnv<'a>,
error: Option<String>,
expects_eths: bool,
) -> JObject<'a> {
let error_value = match error {
Some(err) => create_some_value(&env, err),
None => create_none_value(&env),
};
// public static RawInitializationResult apply(final Option error, final Boolean expects_eth) {
// return RawInitializationResult$.MODULE$.apply(var0);
// }
env.call_static_method(
"fluence/vm/frank/result/RawInitializationResult",
"apply",
"(Lscala/Option;Z)Lfluence/vm/frank/result/RawInitializationResult;",
&[error_value, JValue::from(expects_eths)],
)
.expect("jni: couldn't allocate a new RawInitializationResult object")
.l()
.expect("jni: couldn't convert RawInitializationResult to a Java Object")
}
/// Creates RawInvocationResult object for the Scala part.
pub fn create_invocation_result<'a>(
env: &JNIEnv<'a>,
error: Option<String>,
result: FrankResult,
) -> JObject<'a> {
let error_value = match error {
Some(err) => create_some_value(&env, err),
None => create_none_value(&env),
};
// TODO: here we have 2 copying of result, first is from Wasm memory to a Vec<u8>, second is
// from the Vec<u8> to Java byte array. Optimization might be possible after memory refactoring.
let outcome = env.byte_array_from_slice(&result.outcome).unwrap();
let outcome = JObject::from(outcome);
let spent_gas = JValue::from(result.spent_gas);
// public static RawInvocationResult apply(final Option error, final byte[] output, final long spentGas) {
// return RawInvocationResult$.MODULE$.apply(var0, var1, var2);
// }
env.call_static_method(
"fluence/vm/frank/result/RawInvocationResult",
"apply",
"(Lscala/Option;[BJ)Lfluence/vm/frank/result/RawInvocationResult;",
&[error_value, JValue::from(outcome), spent_gas],
)
.expect("jni: couldn't allocate a new RawInvocationResult object")
.l()
.expect("jni: couldn't convert RawInvocationResult to a Java Object")
}
/// Creates RawStateComputationResult object for the Scala part.
pub fn create_state_computation_result<'a>(
env: &JNIEnv<'a>,
error: Option<String>,
state: GenericArray<u8, <Sha256 as FixedOutput>::OutputSize>,
) -> JObject<'a> {
let error_value = match error {
Some(err) => create_some_value(&env, err),
None => create_none_value(&env),
};
let state = env
.byte_array_from_slice(state.as_slice())
.expect("jni: couldn't allocate enough space for byte array");
let state = JObject::from(state);
// public static RawStateComputationResult apply(final Option error, final byte[] state) {
// return RawStateComputationResult$.MODULE$.apply(var0, var1);
// }
env.call_static_method(
"fluence/vm/frank/result/RawStateComputationResult",
"apply",
"(Lscala/Option;[B)Lfluence/vm/frank/result/RawStateComputationResult;",
&[error_value, JValue::from(state)],
)
.expect("jni: couldn't allocate a new RawInvocationResult object")
.l()
.expect("jni: couldn't convert RawInvocationResult to a Java Object")
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
pub mod exports;
mod jni_results;
mod option;

View File

@ -0,0 +1,52 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use jni::objects::{JObject, JValue};
/// Defines functions to construct the Scala Option[String] objects by calling the apply method:
///
/// ```
/// public static Option apply(final Object x) {
/// return Option$.MODULE$.apply(var0);
/// }
/// ```
use jni::JNIEnv;
/// creates Scala None value
pub fn create_none_value<'a>(env: &JNIEnv<'a>) -> JValue<'a> {
env.call_static_method(
"scala/Option",
"apply",
"(Ljava/lang/Object;)Lscala/Option;",
&[JValue::from(JObject::null())],
)
.expect("jni: error while creating None object")
}
/// creates Scala Some[String] value
pub fn create_some_value<'a>(env: &JNIEnv<'a>, value: String) -> JValue<'a> {
let value = env
.new_string(value)
.expect("jni: couldn't allocate new string");
let value = JObject::from(value);
env.call_static_method(
"scala/Option",
"apply",
"(Ljava/lang/Object;)Lscala/Option;",
&[JValue::from(value)],
)
.expect("jni: couldn't allocate a Some object")
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#![deny(
dead_code,
nonstandard_style,
unused_imports,
unused_mut,
unused_variables,
unused_unsafe,
unreachable_patterns
)]
mod jni;
mod vm;

View File

@ -0,0 +1,107 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#![deny(
dead_code,
nonstandard_style,
unused_imports,
unused_mut,
unused_variables,
unused_unsafe,
unreachable_patterns
)]
/// Command-line tool intended to test Frank VM.
mod jni;
mod vm;
use crate::vm::config::Config;
use crate::vm::prepare::prepare_module;
use crate::vm::frank::Frank;
use clap::{App, AppSettings, Arg, SubCommand};
use exitfailure::ExitFailure;
use failure::err_msg;
use std::fs;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
const IN_MODULE_PATH: &str = "in-wasm-path";
const INVOKE_ARG: &str = "arg";
fn prepare_args<'a, 'b>() -> [Arg<'a, 'b>; 2] {
[
Arg::with_name(IN_MODULE_PATH)
.required(true)
.takes_value(true)
.short("i")
.help("path to the wasm file"),
Arg::with_name(INVOKE_ARG)
.required(true)
.takes_value(true)
.short("a")
.help("argument for the invoke function in the Wasm module"),
]
}
fn execute_wasm<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("execute")
.about("Execute provided module on the Fluence Frank VM")
.args(&prepare_args())
}
fn main() -> Result<(), ExitFailure> {
let app = App::new("Fluence Frank Wasm execution environment for test purposes")
.version(VERSION)
.author(AUTHORS)
.about(DESCRIPTION)
.setting(AppSettings::ArgRequiredElseHelp)
.subcommand(execute_wasm());
match app.get_matches().subcommand() {
("execute", Some(arg)) => {
let config = Box::new(Config::default());
let in_module_path = arg.value_of(IN_MODULE_PATH).unwrap();
let wasm_code =
fs::read(in_module_path).unwrap_or_else(|err| panic!(format!("{}", err)));
let wasm_code = prepare_module(&wasm_code, &config)
.map_err(|e| panic!(format!("{}", e)))
.unwrap();
let invoke_arg = arg.value_of(INVOKE_ARG).unwrap();
let _ = Frank::new(&wasm_code, config)
.map_err(|err| panic!(format!("{}", err)))
.and_then(|mut executor| executor.0.invoke(invoke_arg.as_bytes()))
.map_err(|err| panic!(format!("{}", err)))
.map(|result| {
let outcome_copy = result.outcome.clone();
match String::from_utf8(result.outcome) {
Ok(s) => println!("result: {}\nspent gas: {} ", s, result.spent_gas),
Err(_) => println!(
"result: {:?}\nspent gas: {} ",
outcome_copy, result.spent_gas
),
}
});
Ok(())
}
c => Err(err_msg(format!("Unexpected command: {}", c.0)).into()),
}
}

View File

@ -0,0 +1,137 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// Defines the Config struct that is similar to vm/src/main/scala/fluence/vm/config/VmConfig.scala.
use crate::vm::errors::FrankError;
use jni::objects::{JObject, JString};
use jni::JNIEnv;
#[derive(Clone, Debug, PartialEq)]
pub struct Config {
/// Count of Wasm memory pages that will be preallocated on the VM startup.
/// Each Wasm pages is 65536 bytes long.
pub mem_pages_count: i32,
/// If true, registers the logger Wasm module with name 'logger'.
/// This functionality is just for debugging, and this module will be disabled in future.
pub logger_enabled: bool,
/// Memory will be split by chunks to be able to build Merkle Tree on top of it.
/// Size of memory in bytes must be dividable by chunkSize.
pub chunk_size: i32,
/// The name of the main module handler function.
pub invoke_function_name: String,
/// The name of function that should be called for allocation memory. This function
/// is used for passing array of bytes to the main module.
pub allocate_function_name: String,
/// The name of function that should be called for deallocation of
/// previously allocated memory by allocateFunction.
pub deallocate_function_name: String,
}
impl Default for Config {
fn default() -> Self {
// some reasonable defaults
Self {
// 65536*1600 ~ 100 Mb
mem_pages_count: 1600,
invoke_function_name: "invoke".to_string(),
allocate_function_name: "allocate".to_string(),
deallocate_function_name: "deallocate".to_string(),
logger_enabled: true,
chunk_size: 4096,
}
}
}
impl Config {
/// Creates a new config based on the supplied Scala object Config.
/// This config should have the following structure:
///
/// ```
/// case class MainModuleConfig(
/// name: Option[String],
/// allocateFunctionName: String,
/// deallocateFunctionName: String,
/// invokeFunctionName: String
/// )
///
/// case class VmConfig(
/// memPagesCount: Int,
/// loggerEnabled: Boolean,
/// chunkSize: Int,
/// mainModuleConfig: MainModuleConfig
/// )
/// ```
///
pub fn new(env: &JNIEnv, config: JObject) -> Result<Box<Self>, FrankError> {
let mem_pages_count = env.call_method(config, "memPagesCount", "()I", &[])?.i()?;
let logger_enabled = env.call_method(config, "loggerEnabled", "()Z", &[])?.z()?;
let chunk_size = env.call_method(config, "chunkSize", "()I", &[])?.i()?;
let main_module_config = env
.call_method(
config,
"mainModuleConfig",
"()Lfluence/vm/config/MainModuleConfig;",
&[],
)?
.l()?;
let allocate_function_name = env
.call_method(
main_module_config,
"allocateFunctionName",
"()Ljava/lang/String;",
&[],
)?
.l()?;
let deallocate_function_name = env
.call_method(
main_module_config,
"deallocateFunctionName",
"()Ljava/lang/String;",
&[],
)?
.l()?;
let invoke_function_name = env
.call_method(
main_module_config,
"invokeFunctionName",
"()Ljava/lang/String;",
&[],
)?
.l()?;
// converts JObject to JString (without copying, just enum type changes)
let allocate_function_name = env.get_string(JString::from(allocate_function_name))?;
let deallocate_function_name = env.get_string(JString::from(deallocate_function_name))?;
let invoke_function_name = env.get_string(JString::from(invoke_function_name))?;
Ok(Box::new(Self {
mem_pages_count,
logger_enabled,
chunk_size,
// and then finally to Rust String (requires one copy)
invoke_function_name: String::from(invoke_function_name),
allocate_function_name: String::from(allocate_function_name),
deallocate_function_name: String::from(deallocate_function_name),
}))
}
}

View File

@ -0,0 +1,143 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use jni::errors::Error as JNIWrapperError;
use wasmer_runtime::error::{
CallError, CompileError, CreationError, Error, ResolveError, RuntimeError,
};
// TODO: more errors to come (when preparation step will be completely landed)
/// Errors related to the preparation (instrumentation and so on) and compilation by Wasmer steps.
pub enum InitializationError {
/// Error that raises during compilation Wasm code by Wasmer.
WasmerCreationError(String),
/// Error that raises during creation of some Wasm objects (like table and memory) by Wasmer.
WasmerCompileError(String),
/// Error that raises on the preparation step.
PrepareError(String),
}
pub enum FrankError {
/// Errors related to the preparation (instrumentation and so on) and compilation by Wasmer steps.
InstantiationError(String),
/// Errors related to parameter passing from Java to Rust and back.
JNIError(String),
/// Errors for I/O errors raising while opening a file.
IOError(String),
/// This error type is produced by Wasmer during resolving a Wasm function.
WasmerResolveError(String),
/// Error related to calling a main Wasm module.
WasmerInvokeError(String),
/// Error indicates that smth really bad happened (like removing the global Frank state).
FrankNotInitialized,
}
impl std::fmt::Display for InitializationError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
match self {
InitializationError::WasmerCompileError(msg) => write!(f, "{}", msg),
InitializationError::WasmerCreationError(msg) => write!(f, "{}", msg),
InitializationError::PrepareError(msg) => write!(f, "{}", msg),
}
}
}
impl std::fmt::Display for FrankError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
match self {
FrankError::InstantiationError(msg) => write!(f, "InstantiationError: {}", msg),
FrankError::JNIError(msg) => write!(f, "JNIError: {}", msg),
FrankError::IOError(msg) => write!(f, "IOError: {}", msg),
FrankError::WasmerResolveError(msg) => write!(f, "WasmerResolveError: {}", msg),
FrankError::WasmerInvokeError(msg) => write!(f, "WasmerInvokeError: {}", msg),
FrankError::FrankNotInitialized => write!(
f,
"Attempt to use invoke virtual machine while it hasn't been initialized.\
Please call the initialization method first."
),
}
}
}
impl From<CreationError> for InitializationError {
fn from(err: CreationError) -> Self {
InitializationError::WasmerCreationError(format!("{}", err))
}
}
impl From<CompileError> for InitializationError {
fn from(err: CompileError) -> Self {
InitializationError::WasmerCompileError(format!("{}", err))
}
}
impl From<parity_wasm::elements::Error> for InitializationError {
fn from(err: parity_wasm::elements::Error) -> Self {
InitializationError::PrepareError(format!("{}", err))
}
}
impl From<InitializationError> for FrankError {
fn from(err: InitializationError) -> Self {
FrankError::InstantiationError(format!("{}", err))
}
}
impl From<JNIWrapperError> for FrankError {
fn from(err: JNIWrapperError) -> Self {
FrankError::JNIError(format!("{}", err))
}
}
impl From<CallError> for FrankError {
fn from(err: CallError) -> Self {
match err {
CallError::Resolve(err) => FrankError::WasmerResolveError(format!("{}", err)),
CallError::Runtime(err) => FrankError::WasmerInvokeError(format!("{}", err)),
}
}
}
impl From<ResolveError> for FrankError {
fn from(err: ResolveError) -> Self {
FrankError::WasmerResolveError(format!("{}", err))
}
}
impl From<RuntimeError> for FrankError {
fn from(err: RuntimeError) -> Self {
FrankError::WasmerInvokeError(format!("{}", err))
}
}
impl From<Error> for FrankError {
fn from(err: Error) -> Self {
FrankError::WasmerInvokeError(format!("{}", err))
}
}
impl From<std::io::Error> for FrankError {
fn from(err: std::io::Error) -> Self {
FrankError::IOError(format!("{}", err))
}
}

View File

@ -0,0 +1,206 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use crate::{
vm::modules::env_module::EnvModule,
vm::config::Config,
vm::errors::FrankError,
vm::frank_result::FrankResult,
};
use sha2::{digest::generic_array::GenericArray, digest::FixedOutput, Digest, Sha256};
use std::ffi::c_void;
use wasmer_runtime::{func, imports, instantiate, Ctx, Func, Instance};
use failure::_core::marker::PhantomData;
use wasmer_runtime_core::memory::ptr::{Array, WasmPtr};
pub struct Frank {
instance: &'static Instance,
// It is safe to use unwrap() while calling these functions because Option is used here
// to allow partially initialization of the struct. And all Option fields will contain
// Some if invoking Frank::new is succeed.
allocate: Option<Func<'static, i32, i32>>,
deallocate: Option<Func<'static, (i32, i32), ()>>,
invoke: Option<Func<'static, (i32, i32), i32>>,
_tag: PhantomData<&'static Instance>,
}
impl Drop for Frank {
// In normal situation this method should be called only while VM shutting down.
fn drop(&mut self) {
drop(self.allocate.as_ref());
drop(self.deallocate.as_ref());
drop(self.invoke.as_ref());
drop(Box::from(self.instance));
}
}
// Waiting for new release of Wasmer with https://github.com/wasmerio/wasmer/issues/748.
// It will allow to use lazy_static here. thread_local isn't suitable in our case because
// it is difficult to guarantee that jni code will be called on the same thead context
// every time from the Scala part.
pub static mut FRANK: Option<Box<Frank>> = None;
// A little hack: exporting functions with this name means that this module expects Ethereum blocks.
// Will be changed in the future.
const ETH_FUNC_NAME: &str = "expects_eth";
impl Frank {
/// Writes given value on the given address.
fn write_to_mem(&mut self, address: usize, value: &[u8]) -> Result<(), FrankError> {
let memory = self.instance.context().memory(0);
for (byte_id, cell) in memory.view::<u8>()[address as usize..(address + value.len())]
.iter()
.enumerate()
{
cell.set(value[byte_id]);
}
Ok(())
}
/// Reads given count of bytes from given address.
fn read_result_from_mem(&self, address: usize) -> Result<Vec<u8>, FrankError> {
let memory = self.instance.context().memory(0);
let mut result_size: usize = 0;
for (byte_id, cell) in memory.view::<u8>()[address..address + 4].iter().enumerate() {
result_size |= (cell.get() as usize) << (8 * byte_id);
}
let mut result = Vec::<u8>::with_capacity(result_size);
for cell in memory.view()[(address + 4) as usize..(address + result_size + 4)].iter() {
result.push(cell.get());
}
Ok(result)
}
/// Invokes a main module supplying byte array and expecting byte array with some outcome back.
pub fn invoke(&mut self, fn_argument: &[u8]) -> Result<FrankResult, FrankError> {
// renew the state of the registered environment module to track spent gas and eic
let env: &mut EnvModule = unsafe { &mut *(self.instance.context().data as *mut EnvModule) };
env.renew_state();
// allocate memory for the given argument and write it to memory
let argument_len = fn_argument.len() as i32;
let argument_address = if argument_len != 0 {
let address = self.allocate.as_ref().unwrap().call(argument_len)?;
self.write_to_mem(address as usize, fn_argument)?;
address
} else {
0
};
// invoke a main module, read a result and deallocate it
let result_address = self
.invoke
.as_ref()
.unwrap()
.call(argument_address, argument_len)?;
let result = self.read_result_from_mem(result_address as _)?;
self.deallocate
.as_ref()
.unwrap()
.call(result_address, result.len() as i32)?;
let state = env.get_state();
Ok(FrankResult::new(result, state.0, state.1))
}
/// Computes the virtual machine state.
pub fn compute_vm_state_hash(
&mut self,
) -> GenericArray<u8, <Sha256 as FixedOutput>::OutputSize> {
let mut hasher = Sha256::new();
let memory = self.instance.context().memory(0);
let wasm_ptr = WasmPtr::<u8, Array>::new(0 as _);
let raw_mem = wasm_ptr
.deref(memory, 0, (memory.size().bytes().0 - 1) as _)
.expect("frank: internal error in compute_vm_state_hash");
let raw_mem: &[u8] = unsafe { &*(raw_mem as *const [std::cell::Cell<u8>] as *const [u8]) };
hasher.input(raw_mem);
hasher.result()
}
/// Creates a new virtual machine executor.
pub fn new(module: &[u8], config: Box<Config>) -> Result<(Self, bool), FrankError> {
let env_state = move || {
// allocate EnvModule on the heap
let env_module = EnvModule::new();
let dtor = (|data: *mut c_void| unsafe {
drop(Box::from_raw(data as *mut EnvModule));
}) as fn(*mut c_void);
// and then release corresponding Box object obtaining the raw pointer
(Box::leak(env_module) as *mut EnvModule as *mut c_void, dtor)
};
let import_objects = imports! {
// this will enforce Wasmer to register EnvModule in the ctx.data field
env_state,
"logger" => {
"log_utf8_string" => func!(logger_log_utf8_string),
},
"env" => {
"gas" => func!(update_gas_counter),
"eic" => func!(update_eic),
},
};
let instance: &'static mut Instance =
Box::leak(Box::new(instantiate(module, &import_objects)?));
let expects_eth = instance.func::<(), ()>(ETH_FUNC_NAME).is_ok();
Ok((
Self {
instance,
allocate: Some(instance.func::<(i32), i32>(&config.allocate_function_name)?),
deallocate: Some(
instance.func::<(i32, i32), ()>(&config.deallocate_function_name)?,
),
invoke: Some(instance.func::<(i32, i32), i32>(&config.invoke_function_name)?),
_tag: PhantomData,
},
expects_eth,
))
}
}
// Prints utf8 string of the given size from the given offset.
fn logger_log_utf8_string(ctx: &mut Ctx, offset: i32, size: i32) {
let wasm_ptr = WasmPtr::<u8, Array>::new(offset as _);
match wasm_ptr.get_utf8_string(ctx.memory(0), size as _) {
Some(msg) => print!("{}", msg),
None => print!("frank logger: incorrect UTF8 string's been supplied to logger"),
}
}
fn update_gas_counter(ctx: &mut Ctx, spent_gas: i32) {
let env: &mut EnvModule = unsafe { &mut *(ctx.data as *mut EnvModule) };
env.gas(spent_gas);
}
fn update_eic(ctx: &mut Ctx, eic: i32) {
let env: &mut EnvModule = unsafe { &mut *(ctx.data as *mut EnvModule) };
env.eic(eic);
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#[derive(Clone, Debug, PartialEq)]
pub struct FrankResult {
pub outcome: Vec<u8>,
pub spent_gas: i64,
pub eic: i64,
}
impl FrankResult {
pub fn new(outcome: Vec<u8>, spent_gas: i64, eic: i64) -> Self {
Self {
outcome,
spent_gas,
eic,
}
}
}
impl Default for FrankResult {
fn default() -> Self {
Self {
outcome: Vec::new(),
spent_gas: 0,
eic: 0,
}
}
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
pub mod config;
pub mod errors;
pub mod frank;
pub mod frank_result;
pub mod prepare;
mod modules;

View File

@ -0,0 +1,61 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// Defines the environment module used for tracking execution state.
use std::ops::AddAssign;
#[derive(Clone, Debug, PartialEq)]
pub struct EnvModule {
spent_gas: i64,
eic: i64,
}
impl EnvModule {
pub fn new() -> Box<Self> {
Box::new(Self {
spent_gas: 0i64,
eic: 0i64,
})
}
pub fn gas(&mut self, gas: i32) {
// TODO: check for overflow
self.spent_gas.add_assign(i64::from(gas));
}
pub fn eic(&mut self, eic: i32) {
// TODO: check for overflow
self.eic.add_assign(i64::from(eic));
}
pub fn get_state(&self) -> (i64, i64) {
(self.spent_gas, self.eic)
}
pub fn renew_state(&mut self) {
self.spent_gas = 0;
self.eic = 0;
}
}
impl Default for EnvModule {
fn default() -> Self {
Self {
spent_gas: 0i64,
eic: 0i64,
}
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// TODO: more modules to come
pub mod env_module;

View File

@ -0,0 +1,83 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Similar to
// https://github.com/paritytech/substrate/blob/master/srml/contracts/src/wasm/prepare.rs
// https://github.com/nearprotocol/nearcore/blob/master/runtime/near-vm-runner/src/prepare.rs
use parity_wasm::builder;
use parity_wasm::elements;
use crate::vm::config::Config;
use crate::vm::errors::InitializationError;
use parity_wasm::elements::{MemorySection, MemoryType, ResizableLimits};
struct ModulePreparator {
module: elements::Module,
}
impl<'a> ModulePreparator {
fn init(module_code: &[u8]) -> Result<Self, InitializationError> {
let module = elements::deserialize_buffer(module_code)?;
Ok(Self { module })
}
fn set_mem_pages_count(self, mem_pages_count: u32) -> Self {
let Self { mut module } = self;
// At now, there is could be only one memory section, so
// it needs just to extract previous initial page count, delete existing memory section
let limits = match module.memory_section_mut() {
Some(section) => match section.entries_mut().pop() {
Some(entry) => *entry.limits(),
None => ResizableLimits::new(0 as _, Some(mem_pages_count)),
},
None => ResizableLimits::new(0 as _, Some(mem_pages_count)),
};
let memory_entry = MemoryType::new(limits.initial(), Some(mem_pages_count));
let mut default_mem_section = MemorySection::default();
// and create a new one
module
.memory_section_mut()
.unwrap_or_else(|| &mut default_mem_section)
.entries_mut()
.push(memory_entry);
let builder = builder::from_module(module);
Self {
module: builder.build(),
}
}
fn to_wasm(self) -> Result<Vec<u8>, InitializationError> {
elements::serialize(self.module).map_err(Into::into)
}
}
/// Prepares a Wasm module:
/// - set memory page count
/// - TODO: instrument module with gas counter
/// - TODO: instrument module with eic
pub fn prepare_module(module: &[u8], config: &Config) -> Result<Vec<u8>, InitializationError> {
ModulePreparator::init(module)?
.set_mem_pages_count(config.mem_pages_count as _)
.to_wasm()
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm
/**
* Represents result of the VM invocation.
*
* @param output the computed result by Frank VM
* @param spentGas spent gas by producing the output
*/
case class InvocationResult(output: Array[Byte], spentGas: Long)

View File

@ -0,0 +1,27 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm
object Utils {
def getModuleDirPrefix(): String =
// getProperty could return different path depends on the run method (Idea or sbt)
if (System.getProperty("user.dir").endsWith("/vm"))
System.getProperty("user.dir")
else
System.getProperty("user.dir") + "/vm/"
}

View File

@ -0,0 +1,100 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm
import cats.data.{EitherT, NonEmptyList}
import cats.effect.{LiftIO, Sync}
import cats.Monad
import com.typesafe.config.{Config, ConfigFactory}
import fluence.vm.config.VmConfig
import fluence.vm.error.{InitializationError, InvocationError, StateComputationError}
import fluence.vm.frank.{FrankAdapter, FrankWasmVm}
import scodec.bits.ByteVector
import scala.language.higherKinds
/**
* Virtual Machine api.
*/
trait WasmVm {
/**
* Invokes Wasm ''function'' from specified Wasm ''module''. Each function receives and returns array of bytes.
*
* Note that, modules should be registered when VM started!
*
* @param fnArgument a Function arguments
* @tparam F a monad with an ability to absorb 'IO'
*/
def invoke[F[_]: LiftIO: Monad](
fnArgument: Array[Byte] = Array.emptyByteArray
): EitherT[F, InvocationError, InvocationResult]
/**
* Returns hash of all significant inner state of this VM. This function calculates
* hashes for the state of each module and then concatenates them together.
* It's behaviour will change in future, till it looks like this:
* {{{
* vmState = hash(hash(module1 state), hash(module2 state), ...))
* }}}
* '''Note!''' It's very expensive operation, try to avoid frequent use.
*/
def computeVmState[F[_]: LiftIO: Monad]: EitherT[F, StateComputationError, ByteVector]
/**
* Temporary way to pass a flag from userland (the WASM file) to the Node, denotes whether an app
* expects outer world to pass Ethereum blocks data into it.
* TODO move this flag to the Smart Contract
*/
val expectsEth: Boolean
}
object WasmVm {
val javaLibPath: String = System.getProperty("java.library.path")
println(s"java.library.path = $javaLibPath")
/**
* Main method factory for building VM.
* Compiles all files immediately by Asmble and returns VM implementation with eager module instantiation.
*
* @param inFiles input files in wasm or wast format
* @param configNamespace a path of config in 'lightbend/config terms, please see reference.conf
*/
def apply[F[_]: Sync](
inFiles: NonEmptyList[String],
configNamespace: String = "fluence.vm.client",
conf: Config = ConfigFactory.load()
): EitherT[F, InitializationError, WasmVm] =
for {
// reading config
config VmConfig.readT[F](configNamespace, conf)
vmRunnerInvoker <- EitherT.right(Sync[F].delay(new FrankAdapter()))
initializationResult <- EitherT.right(Sync[F].delay(vmRunnerInvoker.initialize(inFiles.head, config)))
_ EitherT.cond(
initializationResult.error.isEmpty,
(),
InitializationError(initializationResult.error.get)
)
} yield new FrankWasmVm(
vmRunnerInvoker,
initializationResult.expectsEth
)
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm.config
import cats.data.EitherT
import cats.Monad
import cats.syntax.either._
import com.typesafe.config.Config
import fluence.vm.error.InitializationError
import scala.language.higherKinds
/**
* Main module settings.
*
* @param name a name of the main module (None means absence of name section in a Wasm module)
* @param allocateFunctionName name of a function that should be called for allocation memory
* (used for passing complex data structures)
* @param deallocateFunctionName name of a function that should be called for deallocation
* of previously allocated memory
* @param invokeFunctionName name of main module handler function
*/
case class MainModuleConfig(
name: Option[String],
allocateFunctionName: String,
deallocateFunctionName: String,
invokeFunctionName: String
)
/**
* WasmVm settings.
*
* @param memPagesCount the maximum count of memory pages when a module doesn't say
* @param loggerEnabled if set, registers the logger Wasm module with name 'logger'
* @param chunkSize a size of the memory chunks, that memory will be split into
* @param mainModuleConfig settings for the main module
*/
case class VmConfig(
memPagesCount: Int,
loggerEnabled: Boolean,
chunkSize: Int,
mainModuleConfig: MainModuleConfig
)
object VmConfig {
import net.ceedubs.ficus.Ficus._
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
def readT[F[_]: Monad](namespace: String, conf: Config): EitherT[F, InitializationError, VmConfig] = {
EitherT
.fromEither[F](Either.catchNonFatal(conf.getConfig(namespace).as[VmConfig]))
.leftMap(e InitializationError("Unable to parse the virtual machine config " + e))
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm.error
import scala.util.control.NoStackTrace
/**
* Base trait for errors occurred in Virtual machine.
*
* @param message detailed error message
* @param causedBy caught [[Throwable]], if any
*/
sealed abstract class VmError(val message: String, val causedBy: Option[Throwable])
extends Throwable(message, causedBy.orNull, true, false) with NoStackTrace
/**
* Corresponds to errors occurred during VM initialization.
*
* @param message detailed error message
* @param causedBy caught [[Throwable]], if any
*/
case class InitializationError(override val message: String, override val causedBy: Option[Throwable] = None)
extends VmError(message, causedBy)
/**
* Corresponds to errors occurred during VM function invocation.
*
* @param message detailed error message
* @param causedBy caught [[Throwable]], if any
*/
case class InvocationError(override val message: String, override val causedBy: Option[Throwable] = None)
extends VmError(message, causedBy)
/**
* Corresponds to errors occurred during computing VM state hash.
*
* @param message detailed error message
* @param causedBy caught [[Throwable]], if any
*/
case class StateComputationError(override val message: String, override val causedBy: Option[Throwable] = None)
extends VmError(message, causedBy)

View File

@ -0,0 +1,46 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm.frank
import fluence.vm.config.VmConfig
import fluence.vm.frank.result.{RawInitializationResult, RawInvocationResult, RawStateComputationResult}
import ch.jodersky.jni.nativeLoader
/**
* Realizes connection to the virtual machine runner based on Wasmer through JNI.
*/
@nativeLoader("frank")
class FrankAdapter {
/**
* Initializes execution environment with given file path.
*
* @param filePath path to a wasm file
*/
@native def initialize(filePath: String, config: VmConfig): RawInitializationResult
/**
* Invokes main module handler.
*
* @param arg argument for invoked module
*/
@native def invoke(arg: Array[Byte]): RawInvocationResult
/**
* Returns hash of all significant inner state of the VM.
*/
@native def computeVmState(): RawStateComputationResult
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm.frank
import cats.Monad
import cats.data.EitherT
import cats.syntax.either._
import cats.effect.{IO, LiftIO}
import fluence.vm.{InvocationResult, WasmVm}
import scodec.bits.ByteVector
import fluence.vm.error.{InvocationError, StateComputationError}
import fluence.vm.frank.result.{RawInvocationResult, RawStateComputationResult}
import scala.language.higherKinds
/**
* Base implementation of [[WasmVm]] based on the Wasmer execution environment.
*
* '''Note!!! This implementation isn't thread-safe. The provision of calls
* linearization is the task of the caller side.'''
*/
class FrankWasmVm(
private val vmRunnerInvoker: FrankAdapter,
val expectsEth: Boolean
) extends WasmVm {
override def invoke[F[_]: LiftIO: Monad](
fnArgument: Array[Byte]
): EitherT[F, InvocationError, InvocationResult] =
EitherT(
IO(vmRunnerInvoker.invoke(fnArgument)).attempt
.to[F]
).leftMap(e InvocationError(s"Frank invocation failed by exception. Cause: ${e.getMessage}", Some(e)))
.subflatMap {
case RawInvocationResult(Some(err), _, _)
InvocationError(s"Frank invocation failed. Cause: $err").asLeft[InvocationResult]
case RawInvocationResult(None, output, spentGas)
InvocationResult(output, spentGas).asRight[InvocationError]
}
override def computeVmState[F[_]: LiftIO: Monad]: EitherT[F, StateComputationError, ByteVector] =
EitherT(
IO(vmRunnerInvoker.computeVmState()).attempt
.to[F]
).leftMap(e StateComputationError(s"Frank getting VM state failed. Cause: ${e.getMessage}", Some(e))).subflatMap {
case RawStateComputationResult(Some(err), _)
StateComputationError(s"Frank invocation failed. Cause: $err").asLeft[ByteVector]
case RawStateComputationResult(None, state)
ByteVector(state).asRight[StateComputationError]
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm.frank.result
/**
* Represents raw JNI result of FrankAdapter::initialize invoking.
*
* @param error represent various initialization errors, None - no error occurred
*/
final case class RawInitializationResult(error: Option[String], expectsEth: Boolean)

View File

@ -0,0 +1,26 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm.frank.result
/**
* Represents raw JNI result of FrankAdapter::invoke invoking.
*
* @param error represents various invocation errors, None - no error occurred
* @param output the computed result by Frank VM, valid only if no error occurred (error == None)
* @param spentGas spent gas by producing the output, valid only if no error occurred (error == None)
*/
final case class RawInvocationResult(error: Option[String], output: Array[Byte], spentGas: Long)

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm.frank.result
/**
* Represents raw JNI result of FrankAdapter::computeVmState invoking.
*
* @param error represent various initialization errors, None - no error occurred
* @param state computed state of Frank VM, valid only if no error occurred (error == None)
*/
final case class RawStateComputationResult(error: Option[String], state: Array[Byte])

View File

@ -0,0 +1,41 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence
import cats.Functor
import cats.data.EitherT
import fluence.vm.error.VmError
import scala.language.higherKinds
package object vm {
implicit class VmErrorMapper[F[_]: Functor, E <: VmError, T](eitherT: EitherT[F, E, T]) {
def toVmError: EitherT[F, VmError, T] = {
eitherT.leftMap { e: VmError
e
}
}
}
object eitherT {
implicit class EitherTOps[F[_]: Functor, A, B](ef: F[Either[A, B]]) {
def eitherT: EitherT[F, A, B] = EitherT(ef)
}
}
}

View File

@ -0,0 +1,2 @@
mock-maker-inline
# allows to mock final classes from 'asmble' project for testing

View File

@ -0,0 +1,24 @@
;; this example has "bad" allocation function that returns offset out of ByteBuffer limits as f64
(module
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(func (export "allocate") (param $0 i32) (result f64)
;; returns floating-point f64 number instead od integer
(f64.const 200000000.12345)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
(func (export "invoke") (param $0 i32 ) (param $1 i32) (result i32)
;; simply returns 10000
(i32.const 10000)
)
)

View File

@ -0,0 +1,25 @@
;; this example has "bad" allocation function that returns offset out of ByteBuffer limits as i64
(module
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(func (export "allocate") (param $0 i32) (result i64)
;; returns maximum value of signed 64-bit integer that wittingly exceeds maximum ByteBuffer size
;; (and the address space limit on amd64 architecture)
(i64.const 9223372036854775807)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
(func (export "invoke") (param $0 i32 ) (param $1 i32) (result i32)
;; simply returns 10000
(i32.const 10000)
)
)

View File

@ -0,0 +1,77 @@
;; copy of simple counter module with different name
(module $CounterCopyModule
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(func (export "allocate") (param $0 i32) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 10000)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
;;
;; Initializes counter with zero value
;;
(data (i32.const 12) "\00\00\00\00")
;;
;; Increments variable in memory by one and returns it.
;;
(func (export "invoke") (param $buffer i32) (param $size i32) (result i32)
(i32.store offset=12
(i32.const 0)
(i32.add
(i32.load offset=12 (i32.const 0))
(i32.const 1)
)
)
(call $putIntResult
(i32.load offset=12 (i32.const 0))
)
)
;; int putIntResult(int result) {
;; const int address = 1024*1024;
;;
;; globalBuffer[address] = 0;
;; globalBuffer[address + 1] = 0;
;; globalBuffer[address + 2] = 0;
;; globalBuffer[address + 3] = 4;
;;
;; for(int i = 0; i < 4; ++i) {
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
;; }
;;
;; return address;
;; }
(func $putIntResult (param $result i32) (result i32)
(local $1 i32)
(local $2 i32)
(set_local $2 (i32.const 0))
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
(set_local $1 (i32.const 1048596))
(loop $label$0
(i32.store8
(get_local $1)
(i32.shr_u (get_local $result) (get_local $2))
)
(set_local $1
(i32.add (get_local $1) (i32.const 1))
)
(br_if $label$0
(i32.ne
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
(i32.const 32)
)
)
)
(i32.const 1048592)
)
)

View File

@ -0,0 +1,77 @@
;; simple in-memory counter
(module
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(func (export "allocate") (param $0 i32) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 10000)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
;;
;; Initializes counter with zero value
;;
(data (i32.const 12) "\00\00\00\00")
;;
;; Increments variable in memory by one and returns it.
;;
(func (export "invoke") (param $buffer i32) (param $size i32) (result i32)
(i32.store offset=12
(i32.const 0)
(i32.add
(i32.load offset=12 (i32.const 0))
(i32.const 1)
)
)
(call $putIntResult
(i32.load offset=12 (i32.const 0))
)
)
;; int putIntResult(int result) {
;; const int address = 1024*1024;
;;
;; globalBuffer[address] = 0;
;; globalBuffer[address + 1] = 0;
;; globalBuffer[address + 2] = 0;
;; globalBuffer[address + 3] = 4;
;;
;; for(int i = 0; i < 4; ++i) {
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
;; }
;;
;; return address;
;; }
(func $putIntResult (param $result i32) (result i32)
(local $1 i32)
(local $2 i32)
(set_local $2 (i32.const 0))
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
(set_local $1 (i32.const 1048596))
(loop $label$0
(i32.store8
(get_local $1)
(i32.shr_u (get_local $result) (get_local $2))
)
(set_local $1
(i32.add (get_local $1) (i32.const 1))
)
(br_if $label$0
(i32.ne
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
(i32.const 32)
)
)
)
(i32.const 1048592)
)
)

View File

@ -0,0 +1,108 @@
;; this example has some functions that recieve and put strings
(module
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(data 0 (offset (i32.const 128)) "Hello from Fluence Labs!\00")
(func (export "allocate") (param $0 i32) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 10000)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
;; puts 0x00FFFFFF as result size in memory at offset 1048592 and returns pointer to it
(func (export "invoke") (param $buffer i32) (param $size i32) (result i32)
(local $0 i32)
(set_local $0 (i32.const 1048592))
(i32.store8
(get_local $0)
(i32.const 255)
)
(i32.store8
(i32.add (get_local $0) (i32.const 1))
(i32.const 255)
)
(i32.store8
(i32.add (get_local $0) (i32.const 2))
(i32.const 255)
)
(i32.store8
(i32.add (get_local $0) (i32.const 3))
(i32.const 0)
)
(i32.const 1048592)
)
;; int putStringResult(const char *string, int stringSize, int address) {
;;
;; globalBuffer[address] = (stringSize >> 24) & 0xFF;
;; globalBuffer[address + 1] = (stringSize >> 16) & 0xFF;
;; globalBuffer[address + 2] = (stringSize >> 8) & 0xFF;
;; globalBuffer[address + 3] = stringSize & 0xFF;
;;
;; for(int i = 0; i < stringSize; ++i) {
;; globalBuffer[address + 4 + i] = string[i];
;; }
;; }
(func $putStringResult (param $string i32) (param $stringSize i32) (param $address i32) (result i32)
(local $3 i32)
(local $4 i32)
(i32.store8
(get_local $address)
(get_local $stringSize)
)
(i32.store8
(i32.add (get_local $address) (i32.const 1))
(i32.shr_u (get_local $stringSize) (i32.const 8))
)
(i32.store8
(i32.add (get_local $address) (i32.const 2))
(i32.shr_u (get_local $stringSize) (i32.const 16))
)
(i32.store8
(i32.add (get_local $address) (i32.const 3))
(i32.shr_u (get_local $stringSize) (i32.const 24))
)
(set_local $3 (get_local $address))
(set_local $address (i32.add (get_local $address) (i32.const 4)))
(loop $label$0
;; globalBuffer[address + 4 + i] = string[i];
(i32.store8
(get_local $address)
(i32.load8_u (get_local $string))
)
;; ++string
(set_local $string
(i32.add (get_local $string) (i32.const 1))
)
;; ++globalBuffer
(set_local $address
(i32.add (get_local $address) (i32.const 1))
)
(br_if $label$0
(i32.ne
(tee_local $4 (i32.add (get_local $4) (i32.const 1)))
(get_local $stringSize)
)
)
)
(get_local $3)
)
)

View File

@ -0,0 +1,149 @@
;; this example simply returns product of two integers
(module $MulModule
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(func (export "allocate") (param $0 i32) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 10000)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
;; int extractInt(const char *buffer, int begin, int end) {
;; int value = 0;
;; const int size = end - begin;
;;
;; // it is assumed that bytes already in little endian
;; for(int byteIdx = 0; byteIdx < size; ++byteIdx) {
;; value |= buffer[begin + byteIdx] << byteIdx * 8;
;; }
;;
;; return value;
;; }
(func $extractInt (param $buffer i32) (param $begin i32) (param $end i32) (result i32)
(local $3 i32)
(block $label$0
(br_if $label$0
(i32.lt_s
(tee_local $3
(i32.sub (get_local $end) (get_local $begin))
)
(i32.const 1)
)
)
(set_local $begin
(i32.add (get_local $buffer) (get_local $begin))
)
(set_local $end (i32.const 0))
(set_local $buffer (i32.const 0))
(loop $label$1
(set_local $buffer
(i32.or
(i32.shl
(i32.load8_s (get_local $begin))
(get_local $end)
)
(get_local $buffer)
)
)
(set_local $begin
(i32.add (get_local $begin) (i32.const 1))
)
(set_local $end
(i32.add (get_local $end) (i32.const 8))
)
(br_if $label$1
(tee_local $3
(i32.add (get_local $3) (i32.const -1))
)
)
)
(return (get_local $buffer))
)
(i32.const 0)
)
;; int invoke(const char *buffer, int size) {
;; if(size != 8) {
;; return 0;
;; }
;;
;; const int a = extractInt(buffer, 0, 4);
;; const int b = extractInt(buffer, 4, 8);
;;
;; return a * b;
;; }
(func (export "invoke") (param $buffer i32) (param $size i32) (result i32)
(local $2 i32)
(set_local $2 (i32.const 0))
(block $label$0
(br_if $label$0
(i32.ne (get_local $size) (i32.const 8))
)
(set_local $2
(i32.mul
(call $extractInt
(get_local $buffer)
(i32.const 0)
(i32.const 4)
)
(call $extractInt
(get_local $buffer)
(i32.const 4)
(i32.const 8)
)
)
)
)
(call $putIntResult (get_local $2))
)
;; int putIntResult(int result) {
;; const int address = 1024*1024;
;;
;; globalBuffer[address] = 0;
;; globalBuffer[address + 1] = 0;
;; globalBuffer[address + 2] = 0;
;; globalBuffer[address + 3] = 4;
;;
;; for(int i = 0; i < 4; ++i) {
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
;; }
;;
;; return address;
;; }
(func $putIntResult (param $result i32) (result i32)
(local $1 i32)
(local $2 i32)
(set_local $2 (i32.const 0))
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
(set_local $1 (i32.const 1048596))
(loop $label$0
(i32.store8
(get_local $1)
(i32.shr_u (get_local $result) (get_local $2))
)
(set_local $1
(i32.add (get_local $1) (i32.const 1))
)
(br_if $label$0
(i32.ne
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
(i32.const 32)
)
)
)
(i32.const 1048592)
)
)

View File

@ -0,0 +1,20 @@
;; this example has allocate/deallocate functions but doesn't have memory sections.
;; Asmble version 0.4.0 doesn't generate getMemory function in this case.
(module
(func (export "allocate") (param $0 i32 ) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 10000)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
(func (export "invoke") (param $0 i32) (param $1 i32) (result i32)
;; simply returns 10000
(i32.const 10000)
)
)

View File

@ -0,0 +1,19 @@
;; this example has allocate/deallocate functions but doesn't have invoke function.
(module
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(func (export "allocate") (param $0 i32) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 10000)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
)

View File

@ -0,0 +1,117 @@
;; this example adds 1 to each byte in supplied string
(module
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(func (export "allocate") (param $0 i32) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 10000)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
;; char* invoke(const char *array, int arraySize) {
;; for(int i = 0; i < stringSize; ++i) {
;; ++array[i];
;; }
;;
;; return array;
;; }
(func (export "invoke") (param $array i32) (param $arraySize i32) (result i32)
(local $arrayIdx i32)
(loop $label$0
(i32.store8
(i32.add (get_local $array) (get_local $arrayIdx))
(i32.add
(i32.load8_s
(i32.add (get_local $array) (get_local $arrayIdx))
)
(i32.const 1)
)
)
(br_if $label$0
(i32.ne
(tee_local $arrayIdx
(i32.add (get_local $arrayIdx) (i32.const 1))
)
(get_local $arraySize)
)
)
)
(call $putArrayResult
(get_local $array)
(get_local $arraySize)
(i32.const 1048592)
)
)
;; int putArrayResult(const char *string, int stringSize, int address) {
;;
;; globalBuffer[address] = (stringSize >> 24) & 0xFF;
;; globalBuffer[address + 1] = (stringSize >> 16) & 0xFF;
;; globalBuffer[address + 2] = (stringSize >> 8) & 0xFF;
;; globalBuffer[address + 3] = stringSize & 0xFF;
;;
;; for(int i = 0; i < stringSize; ++i) {
;; globalBuffer[address + 4 + i] = string[i];
;; }
;; }
(func $putArrayResult (param $string i32) (param $stringSize i32) (param $address i32) (result i32)
(local $3 i32)
(local $4 i32)
(i32.store8
(get_local $address)
(get_local $stringSize)
)
(i32.store8
(i32.add (get_local $address) (i32.const 1))
(i32.shr_u (get_local $stringSize) (i32.const 8))
)
(i32.store8
(i32.add (get_local $address) (i32.const 2))
(i32.shr_u (get_local $stringSize) (i32.const 16))
)
(i32.store8
(i32.add (get_local $address) (i32.const 3))
(i32.shr_u (get_local $stringSize) (i32.const 24))
)
(set_local $3 (get_local $address))
(set_local $address (i32.add (get_local $address) (i32.const 4)))
(loop $label$0
;; globalBuffer[address + 4 + i] = string[i];
(i32.store8
(get_local $address)
(i32.load8_u (get_local $string))
)
;; ++string
(set_local $string
(i32.add (get_local $string) (i32.const 1))
)
;; ++globalBuffer
(set_local $address
(i32.add (get_local $address) (i32.const 1))
)
(br_if $label$0
(i32.ne
(tee_local $4 (i32.add (get_local $4) (i32.const 1)))
(get_local $stringSize)
)
)
)
(get_local $3)
)
)

View File

@ -0,0 +1,92 @@
;; this example simply returns pointer to "Hello from Fluence Labs!\00" string
(module
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(data 0 (offset (i32.const 128)) "Hello from Fluence Labs!\00")
(func (export "allocate") (param $0 i32) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 10000)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
;; returns pointer to const string from memory
(func (export "invoke") (param $buffer i32) (param $bufferSize i32) (result i32)
(call $putArrayResult
(i32.const 128)
(i32.const 24)
(i32.const 1048592)
)
)
;; int putArrayResult(const char *string, int stringSize, int address) {
;;
;; globalBuffer[address] = (stringSize >> 24) & 0xFF;
;; globalBuffer[address + 1] = (stringSize >> 16) & 0xFF;
;; globalBuffer[address + 2] = (stringSize >> 8) & 0xFF;
;; globalBuffer[address + 3] = stringSize & 0xFF;
;;
;; for(int i = 0; i < stringSize; ++i) {
;; globalBuffer[address + 4 + i] = string[i];
;; }
;; }
(func $putArrayResult (param $string i32) (param $stringSize i32) (param $address i32) (result i32)
(local $3 i32)
(local $4 i32)
(i32.store8
(get_local $address)
(get_local $stringSize)
)
(i32.store8
(i32.add (get_local $address) (i32.const 1))
(i32.shr_u (get_local $stringSize) (i32.const 8))
)
(i32.store8
(i32.add (get_local $address) (i32.const 2))
(i32.shr_u (get_local $stringSize) (i32.const 16))
)
(i32.store8
(i32.add (get_local $address) (i32.const 3))
(i32.shr_u (get_local $stringSize) (i32.const 24))
)
(set_local $3 (get_local $address))
(set_local $address (i32.add (get_local $address) (i32.const 4)))
(loop $label$0
;; globalBuffer[address + 4 + i] = string[i];
(i32.store8
(get_local $address)
(i32.load8_u (get_local $string))
)
;; ++string
(set_local $string
(i32.add (get_local $string) (i32.const 1))
)
;; ++globalBuffer
(set_local $address
(i32.add (get_local $address) (i32.const 1))
)
(br_if $label$0
(i32.ne
(tee_local $4 (i32.add (get_local $4) (i32.const 1)))
(get_local $stringSize)
)
)
)
(get_local $3)
)
)

View File

@ -0,0 +1,90 @@
;; this example calculates circular xor of supplied buffer
(module
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(func (export "allocate") (param $0 i32) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 10000)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
;; int invoke(const char *buffer, int size) {
;; int value = 0;
;;
;; for(int byteId = 0; byteId < size; ++byteId) {
;; value ^= buffer[byteId];
;; }
;;
;; return value;
;; }
(func (export "invoke") (param $buffer i32 ) (param $size i32) (result i32)
(local $value i32)
(set_local $value (i32.const 0) )
(block $label$0
(br_if $label$0
(i32.lt_s (get_local $size) (i32.const 1) )
)
(loop $label$1
(set_local $value
(i32.xor (get_local $value) (i32.load8_s (get_local $buffer) ) )
)
(set_local $buffer
(i32.add (get_local $buffer) (i32.const 1) )
)
(br_if $label$1
(tee_local $size
(i32.add (get_local $size) (i32.const -1) )
)
)
)
)
(call $putIntResult (get_local $value))
)
;; int putIntResult(int result) {
;; const int address = 1024*1024;
;;
;; globalBuffer[address] = 0;
;; globalBuffer[address + 1] = 0;
;; globalBuffer[address + 2] = 0;
;; globalBuffer[address + 3] = 4;
;;
;; for(int i = 0; i < 4; ++i) {
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
;; }
;;
;; return address;
;; }
(func $putIntResult (param $result i32) (result i32)
(local $1 i32)
(local $2 i32)
(set_local $2 (i32.const 0))
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
(set_local $1 (i32.const 1048596))
(loop $label$0
(i32.store8
(get_local $1)
(i32.shr_u (get_local $result) (get_local $2))
)
(set_local $1
(i32.add (get_local $1) (i32.const 1))
)
(br_if $label$0
(i32.ne
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
(i32.const 32)
)
)
)
(i32.const 1048592)
)
)

View File

@ -0,0 +1,169 @@
;; copy of sum module with the same module name
(module $SumModule
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(func (export "allocate") (param $0 i32) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 10000)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
;; int extractInt(const char *buffer, int begin, int end) {
;; int value = 0;
;; const int size = end - begin;
;;
;; // it is assumed that bytes already in little endian
;; for(int byteIdx = 0; byteIdx < size; ++byteIdx) {
;; value |= buffer[begin + byteIdx] << byteIdx * 8;
;; }
;;
;; return value;
;; }
(func $extractInt (param $buffer i32) (param $begin i32) (param $end i32) (result i32)
(local $3 i32)
(block $label$0
(br_if $label$0
(i32.lt_s
(tee_local $3
(i32.sub (get_local $end) (get_local $begin))
)
(i32.const 1)
)
)
(set_local $begin
(i32.add (get_local $buffer) (get_local $begin))
)
(set_local $end (i32.const 0))
(set_local $buffer (i32.const 0))
(loop $label$1
(set_local $buffer
(i32.or
(i32.shl
(i32.load8_s (get_local $begin))
(get_local $end)
)
(get_local $buffer)
)
)
(set_local $begin
(i32.add (get_local $begin) (i32.const 1))
)
(set_local $end
(i32.add (get_local $end) (i32.const 8))
)
(br_if $label$1
(tee_local $3
(i32.add (get_local $3) (i32.const -1))
)
)
)
(return (get_local $buffer))
)
(i32.const 0)
)
;; int extractInt(const char *buffer, int begin, int end) {
;; int value = 0;
;; const int size = end - begin;
;;
;; // it is assumed that bytes already in little endian
;; for(int byteIdx = 0; byteIdx < size; ++byteIdx) {
;; value |= buffer[begin + byteIdx] << byteIdx * 8;
;; }
;;
;; return value;
;; }
(func $extractInt (param $0 i32) (param $1 i32) (param $2 i32) (result i32)
(local $3 i32)
(block $label$0
(br_if $label$0
(i32.lt_s
(tee_local $3
(i32.sub (get_local $2) (get_local $1))
)
(i32.const 1)
)
)
(set_local $1
(i32.add (get_local $0) (get_local $1))
)
(set_local $2 (i32.const 0))
(set_local $0 (i32.const 0))
(loop $label$1
(set_local $0
(i32.or
(i32.shl
(i32.load8_s (get_local $1))
(get_local $2)
)
(get_local $0)
)
)
(set_local $1
(i32.add (get_local $1) (i32.const 1))
)
(set_local $2
(i32.add (get_local $2) (i32.const 8))
)
(br_if $label$1
(tee_local $3
(i32.add (get_local $3) (i32.const -1))
)
)
)
(return (get_local $0))
)
(i32.const 0)
)
;; int putIntResult(int result) {
;; const int address = 1024*1024;
;;
;; globalBuffer[address] = 0;
;; globalBuffer[address + 1] = 0;
;; globalBuffer[address + 2] = 0;
;; globalBuffer[address + 3] = 4;
;;
;; for(int i = 0; i < 4; ++i) {
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
;; }
;;
;; return address;
;; }
(func $putIntResult (param $result i32) (result i32)
(local $1 i32)
(local $2 i32)
(set_local $2 (i32.const 0))
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
(set_local $1 (i32.const 1048596))
(loop $label$0
(i32.store8
(get_local $1)
(i32.shr_u (get_local $result) (get_local $2))
)
(set_local $1
(i32.add (get_local $1) (i32.const 1))
)
(br_if $label$0
(i32.ne
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
(i32.const 32)
)
)
)
(i32.const 1048592)
)
)

View File

@ -0,0 +1,25 @@
;; this example simply returns
(module
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(func (export "allocate") (param $0 i32) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 10000)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
;; int sum(int a, int b) {
;; return a + b;
;; }
(func (export "invoke") (param $0 i32) (param $1 i32) (result i32)
(unreachable) ;; unreachable: An instruction which always traps.
)
)

View File

@ -0,0 +1,149 @@
;; this example compute sum of two given integers
(module
;; force Asmble to use memory
(memory $0 20)
(export "memory" (memory $0))
(func (export "allocate") (param $0 i32) (result i32)
;; just return constant offset in ByteBuffer
(i32.const 0)
)
(func (export "deallocate") (param $address i32) (param $size i32) (return)
;; in this simple example deallocation function does nothing
(drop)
(drop)
)
;; int extractInt(const char *buffer, int begin, int end) {
;; int value = 0;
;; const int size = end - begin;
;;
;; // it is assumed that bytes already in little endian
;; for(int byteIdx = 0; byteIdx < size; ++byteIdx) {
;; value |= buffer[begin + byteIdx] << byteIdx * 8;
;; }
;;
;; return value;
;; }
(func $extractInt (param $buffer i32) (param $begin i32) (param $end i32) (result i32)
(local $3 i32)
(block $label$0
(br_if $label$0
(i32.lt_s
(tee_local $3
(i32.sub (get_local $end) (get_local $begin))
)
(i32.const 1)
)
)
(set_local $begin
(i32.add (get_local $buffer) (get_local $begin))
)
(set_local $end (i32.const 0))
(set_local $buffer (i32.const 0))
(loop $label$1
(set_local $buffer
(i32.or
(i32.shl
(i32.load8_s (get_local $begin))
(get_local $end)
)
(get_local $buffer)
)
)
(set_local $begin
(i32.add (get_local $begin) (i32.const 1))
)
(set_local $end
(i32.add (get_local $end) (i32.const 8))
)
(br_if $label$1
(tee_local $3
(i32.add (get_local $3) (i32.const -1))
)
)
)
(return (get_local $buffer))
)
(i32.const 0)
)
;; int invoke(const char *buffer, int size) {
;; if(size != 8) {
;; return 0;
;; }
;;
;; const int a = extractInt(buffer, 0, 4);
;; const int b = extractInt(buffer, 4, 8);
;;
;; return a + b;
;; }
(func (export "invoke") (param $buffer i32) (param $size i32) (result i32)
(local $2 i32)
(set_local $2 (i32.const 0))
(block $label$0
(br_if $label$0
(i32.ne (get_local $size) (i32.const 8))
)
(set_local $2
(i32.add
(call $extractInt
(get_local $buffer)
(i32.const 0)
(i32.const 4)
)
(call $extractInt
(get_local $buffer)
(i32.const 4)
(i32.const 8)
)
)
)
)
(call $putIntResult (get_local $2))
)
;; int putIntResult(int result) {
;; const int address = 1024*1024;
;;
;; globalBuffer[address] = 0;
;; globalBuffer[address + 1] = 0;
;; globalBuffer[address + 2] = 0;
;; globalBuffer[address + 3] = 4;
;;
;; for(int i = 0; i < 4; ++i) {
;; globalBuffer[address + 4 + i ] = ((result >> 8*i) & 0xFF);
;; }
;;
;; return address;
;; }
(func $putIntResult (param $result i32) (result i32)
(local $1 i32)
(local $2 i32)
(set_local $2 (i32.const 0))
(i32.store offset=1048592 (i32.const 0) (i32.const 4))
(set_local $1 (i32.const 1048596))
(loop $label$0
(i32.store8
(get_local $1)
(i32.shr_u (get_local $result) (get_local $2))
)
(set_local $1
(i32.add (get_local $1) (i32.const 1))
)
(br_if $label$0
(i32.ne
(tee_local $2 (i32.add (get_local $2) (i32.const 8)))
(i32.const 32)
)
)
)
(i32.const 1048592)
)
)

View File

@ -0,0 +1,36 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm
import cats.data.EitherT
import cats.effect.IO
import scala.concurrent.duration.Duration
import scala.concurrent.duration._
object TestUtils {
implicit class EitherTValueReader[E, V](origin: EitherT[IO, E, V]) {
def success(timeout: Duration = 3.seconds): V =
origin.value.unsafeRunTimed(timeout).get.right.get
def failed(timeout: Duration = 3.seconds): E =
origin.value.unsafeRunTimed(timeout).get.left.get
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fluence.vm
import cats.data.{EitherT, NonEmptyList}
import cats.effect.{IO, Timer}
import fluence.vm.TestUtils._
import fluence.vm.error.InitializationError
import org.scalatest.{Matchers, WordSpec}
import scala.concurrent.ExecutionContext
import scala.language.implicitConversions
class WasmVmSpec extends WordSpec with Matchers {
implicit def error[E](either: EitherT[IO, E, _]): E = either.value.unsafeRunSync().left.get
private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
"apply" should {
"raise error" when {
"config error" in {
val res = for {
vm <- WasmVm[IO](NonEmptyList.one("unknown file"), "wrong config namespace")
} yield vm
val error = res.failed()
error shouldBe a[InitializationError]
error.getMessage should startWith("Unable to parse the virtual machine config")
}
"file not found" in {
val res = for {
vm <- WasmVm[IO](NonEmptyList.one("unknown file"))
} yield vm
val error = res.failed()
error shouldBe a[InitializationError]
error.getMessage should startWith("IOError: No such file or directory (os error 2)")
}
}
}
"initialize Vm success" when {
"one module without name is provided" ignore {
val sumFile = getClass.getResource("/wast/sum.wast").getPath
WasmVm[IO](NonEmptyList.one(sumFile)).success()
}
"one module with name is provided" ignore {
// Mul modules have name
val mulFile = getClass.getResource("/wast/mul.wast").getPath
WasmVm[IO](NonEmptyList.one(mulFile)).success()
}
"two modules with different module names are provided" ignore {
val sumFile = getClass.getResource("/wast/sum.wast").getPath
val mulFile = getClass.getResource("/wast/mul.wast").getPath
WasmVm[IO](NonEmptyList.of(mulFile, sumFile)).success()
}
"two modules with functions with the same names are provided" ignore {
// module without name and with some functions with the same name ("allocate", "deallocate", "invoke", ...)
val sum1File = getClass.getResource("/wast/counter.wast").getPath
// module with name "Sum" and with some functions with the same name ("allocate", "deallocate", "invoke", ...)
val sum2File = getClass.getResource("/wast/mul.wast").getPath
val res = for {
vm <- WasmVm[IO](NonEmptyList.of(sum1File, sum2File))
} yield vm
res.success()
}
}
"initialize Vm failed" when {
"two main modules provided" ignore {
// these modules both don't contain a name section
val sumFile = getClass.getResource("/wast/sum.wast").getPath
val mulFile = getClass.getResource("/wast/bad-allocation-function-i64.wast").getPath
WasmVm[IO](NonEmptyList.of(mulFile, sumFile)).failed()
}
}
}

View File

@ -0,0 +1,293 @@
/*
* Copyright 2019 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// TODO: Adapt tests for Wasmer
package fluence.vm
import java.nio.{ByteBuffer, ByteOrder}
import cats.data.NonEmptyList
import cats.effect.{IO, Timer}
import fluence.vm.TestUtils._
import fluence.vm.error.{InitializationError, InvocationError}
import org.scalatest.{Assertion, Matchers, WordSpec}
import scala.concurrent.ExecutionContext
import scala.language.{higherKinds, implicitConversions}
class WasmerWasmVmSpec extends WordSpec with Matchers {
private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
/**
* By element comparision of arrays.
*/
private def compareArrays(first: Array[Byte], second: Array[Byte]): Assertion =
first.deep shouldBe second.deep
/**
* Converts ints to byte array by supplied byte order.
*
* @param ints array of int
* @param byteOrder byte order that used for int converting
*/
private def intsToBytes(
ints: List[Int],
byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN
): ByteBuffer = {
val intBytesSize = 4
val converter = ByteBuffer.allocate(intBytesSize * ints.length)
converter.order(byteOrder)
ints.foreach(converter.putInt)
converter.flip()
converter
}
"invoke" should {
"raise error" when {
"trying to invoke when a module doesn't have one" ignore {
val noInvokeTestFile = getClass.getResource("/wast/no-invoke.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(noInvokeTestFile))
result vm.invoke[IO]().toVmError
} yield result
val error = res.failed()
error shouldBe a[InitializationError]
error.getMessage should startWith("The main module must have functions with names")
}
"trying to use Wasm memory when getMemory function isn't defined" ignore {
val noGetMemoryTestFile = getClass.getResource("/wast/no-getMemory.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(noGetMemoryTestFile))
_ vm.invoke[IO]("test".getBytes())
state vm.computeVmState[IO].toVmError
} yield state
val error = res.failed()
error.getMessage should
startWith("Unable to initialize module=null")
error shouldBe a[InitializationError]
}
"wasm code falls into the trap" ignore {
val sumTestFile = getClass.getResource("/wast/sum-with-trap.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(sumTestFile))
result vm.invoke[IO](fnArgument = intsToBytes(100 :: 13 :: Nil).array()).toVmError // Integer overflow
} yield result
val error = res.failed()
error shouldBe a[InvocationError]
error.getMessage should startWith("Function invoke with args:")
error.getMessage should include("was failed")
}
"Wasm allocate function returns an incorrect i64 value" ignore {
val badAllocationFunctionFile = getClass.getResource("/wast/bad-allocation-function-i64.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(badAllocationFunctionFile))
_ vm.invoke[IO]("test".getBytes())
state vm.computeVmState[IO].toVmError
} yield state
val error = res.failed()
error.getMessage shouldBe "Writing to -1 failed"
error shouldBe a[InvocationError]
}
"Wasm allocate function returns an incorrect f64 value" ignore {
val badAllocationFunctionFile = getClass.getResource("/wast/bad-allocation-function-f64.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(badAllocationFunctionFile))
result vm.invoke[IO]("test".getBytes())
state vm.computeVmState[IO].toVmError
} yield state
val error = res.failed()
error.getMessage shouldBe "Writing to 200000000 failed"
error shouldBe a[InvocationError]
}
"trying to extract array with incorrect size from Wasm memory" ignore {
val incorrectArrayReturningTestFile = getClass.getResource("/wast/incorrect-array-returning.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(incorrectArrayReturningTestFile))
result vm.invoke[IO]().toVmError
} yield result
val error = res.failed()
error shouldBe a[InvocationError]
error.getMessage shouldBe "Reading from offset=1048596 16777215 bytes failed"
}
}
}
"invokes function success" when {
"run sum.wast" ignore {
val sumTestFile = getClass.getResource("/wast/sum.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(sumTestFile))
result vm.invoke[IO](intsToBytes(100 :: 17 :: Nil).array()).toVmError
} yield {
compareArrays(result.output, Array[Byte](117, 0, 0, 0))
}
res.success()
}
"run counter.wast" ignore {
val counterTestFile = getClass.getResource("/wast/counter.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(counterTestFile))
get1 vm.invoke[IO]() // 0 -> 1; read 1
get2 vm.invoke[IO]() // 1 -> 2; read 2
get3 vm.invoke[IO]().toVmError // 2 -> 3; read 3
} yield {
compareArrays(get1.output, Array[Byte](1, 0, 0, 0))
compareArrays(get2.output, Array[Byte](2, 0, 0, 0))
compareArrays(get3.output, Array[Byte](3, 0, 0, 0))
}
res.success()
}
"run simple test with array passsing" ignore {
val simpleStringPassingTestFile = getClass.getResource("/wast/simple-string-passing.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(simpleStringPassingTestFile))
value1 vm.invoke[IO]("test_argument".getBytes())
value2 vm.invoke[IO]("XX".getBytes())
value3 vm.invoke[IO]("XXX".getBytes())
value4 vm.invoke[IO]("".getBytes()) // empty string
value5 vm.invoke[IO]("\"".getBytes()).toVmError // " string
} yield {
compareArrays(value1.output, Array[Byte](90, 0, 0, 0))
compareArrays(value2.output, Array[Byte](0, 0, 0, 0))
compareArrays(value3.output, Array[Byte]('X'.toByte, 0, 0, 0))
compareArrays(value4.output, Array[Byte](0, 0, 0, 0)) // this Wasm example returns 0 on empty strings
compareArrays(value5.output, Array[Byte]('"'.toByte, 0, 0, 0))
}
res.success()
}
"run simple test with array returning" ignore {
val simpleArrayPassingTestFile = getClass.getResource("/wast/simple-array-returning.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(simpleArrayPassingTestFile))
value1 vm.invoke[IO]()
_ vm.computeVmState[IO].toVmError
} yield {
val stringValue = new String(value1.output)
stringValue shouldBe "Hello from Fluence Labs!"
}
res.success()
}
"run simple test with array mutation" ignore {
val simpleArrayMutationTestFile = getClass.getResource("/wast/simple-array-mutation.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(simpleArrayMutationTestFile))
value1 vm.invoke[IO]("AAAAAAA".getBytes())
state vm.computeVmState[IO].toVmError
} yield {
val stringValue = new String(value1.output)
stringValue shouldBe "BBBBBBB"
}
res.success()
}
}
"getVmState" should {
"returns state" when {
"there is one module with memory present" ignore {
// the code in 'counter.wast' uses 'memory', instance for this module created with 'memory' field
val counterTestFile = getClass.getResource("/wast/counter.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.one(counterTestFile))
get1 vm.invoke[IO]() // 0 -> 1; return 1
state1 vm.computeVmState[IO]
get2 vm.invoke[IO]() // 1 -> 2; return 2
get3 vm.invoke[IO]() // 2 -> 3; return 3
state2 vm.computeVmState[IO]
get4 vm.invoke[IO]().toVmError // 3 -> 4; return 4
} yield {
compareArrays(get1.output, Array[Byte](1, 0, 0, 0))
compareArrays(get2.output, Array[Byte](2, 0, 0, 0))
compareArrays(get3.output, Array[Byte](3, 0, 0, 0))
compareArrays(get4.output, Array[Byte](4, 0, 0, 0))
state1.size shouldBe 32
state2.size shouldBe 32
state1 should not be state2
}
res.success()
}
"there are several modules present" ignore {
val counterTestFile = getClass.getResource("/wast/counter.wast").getPath
val counterCopyTestFile = getClass.getResource("/wast/counter-copy.wast").getPath
val mulTestFile = getClass.getResource("/wast/mul.wast").getPath
val res = for {
vm WasmVm[IO](NonEmptyList.of(counterTestFile, counterCopyTestFile, mulTestFile))
get1 vm.invoke[IO]() // 0 -> 1; read 1
_ vm.invoke[IO]() // 1 -> 2; read 2
state1 vm.computeVmState[IO]
_ vm.invoke[IO]() // 2 -> 3
get2 vm.invoke[IO]() // 3 -> 4
state2 vm.computeVmState[IO].toVmError
} yield {
compareArrays(get1.output, Array[Byte](1, 0, 0, 0))
compareArrays(get2.output, Array[Byte](4, 0, 0, 0))
state1.size shouldBe 32
state2.size shouldBe 32
state1 should not be state2
}
res.success()
}
}
}
}