diff --git a/src/main/scala/io/iohk/ethereum/vm/StackL.scala b/src/main/scala/io/iohk/ethereum/vm/StackL.scala new file mode 100644 index 0000000000..cee8b5f030 --- /dev/null +++ b/src/main/scala/io/iohk/ethereum/vm/StackL.scala @@ -0,0 +1,108 @@ +package io.iohk.ethereum.vm + +import io.iohk.ethereum.domain.UInt256 + +object StackL { + /** + * Stack max size as defined in the YP (9.1) + */ + val DefaultMaxSize = 1024 + + def empty(maxSize: Int = DefaultMaxSize): StackL = + new StackL(List(), maxSize) +} + +//TODO: consider a List with head being top of the stack (DUP,SWAP go at most the depth of 16) [EC-251] +/** + * Stack for the EVM. Instruction pop their arguments from it and push their results to it. + * The Stack doesn't handle overflow and underflow errors. Any operations that trascend given stack bounds will + * return the stack unchanged. Pop will always return zeroes in such case. + */ +class StackL private(private val underlying: List[UInt256], val maxSize: Int) { + + def pop: (UInt256, StackL) = underlying.headOption match { + case Some(word) => + val updated = underlying.tail + (word, copy(updated)) + + case None => + (UInt256.Zero, this) + } + + /** + * Pop n elements from the stack. The first element in the resulting sequence will be the top-most element + * in the current stack + */ + def pop(n: Int): (Seq[UInt256], StackL) = { + val (popped, updated) = underlying.splitAt(n) + if (popped.length == n) + (popped, copy(updated)) + else + (Seq.fill(n)(UInt256.Zero), this) + } + + def push(word: UInt256): StackL = { + val updated = word :: underlying + if (updated.length <= maxSize) + copy(updated) + else + this + } + + /** + * Push a sequence of elements to the stack. That last element of the sequence will be the top-most element + * in the resulting stack + */ + def push(words: Seq[UInt256]): StackL = { + val updated = underlying.reverse_:::(words.toList) + if (updated.length > maxSize) + this + else + copy(updated) + } + + /** + * Duplicate i-th element of the stack, pushing it to the top. i=0 is the top-most element. + */ + def dup(i: Int): StackL = { + if (i < 0 || i >= underlying.length || underlying.length >= maxSize) + this + else + copy(underlying(i) :: underlying) + } + + /** + * Swap i-th and the top-most elements of the stack. i=0 is the top-most element (and that would be a no-op) + */ + def swap(i: Int): StackL = { + if (i <= 0 || i >= underlying.length) + this + else { + val a = underlying.head + val b = underlying(i) + val updated = b :: underlying.updated(i, a).tail + copy(updated) + } + } + + def size: Int = underlying.size + + /** + * @return the elements of the stack as a sequence, with the top-most element of the stack + * as the first element in the sequence + */ + def toSeq: Seq[UInt256] = underlying + + override def equals(that: Any): Boolean = that match { + case that: StackL => this.underlying == that.underlying + case _ => false + } + + override def hashCode(): Int = underlying.hashCode + + override def toString: String = + underlying.reverse.mkString("Stack(", ",", ")") + + private def copy(updated: List[UInt256]): StackL = + new StackL(updated, maxSize) +} diff --git a/src/test/scala/io/iohk/ethereum/vm/StackLSpec.scala b/src/test/scala/io/iohk/ethereum/vm/StackLSpec.scala new file mode 100644 index 0000000000..568a1fac78 --- /dev/null +++ b/src/test/scala/io/iohk/ethereum/vm/StackLSpec.scala @@ -0,0 +1,93 @@ +package io.iohk.ethereum.vm + +import io.iohk.ethereum.domain.UInt256 +import org.scalacheck.Gen +import org.scalatest.prop.PropertyChecks +import org.scalatest.{FunSuite, Matchers} + +class StackLSpec extends FunSuite with Matchers with PropertyChecks { + + val maxStackSize = 32 + val stackGen = Generators.getStackLGen(maxSize = maxStackSize) + val intGen = Gen.choose(0, maxStackSize).filter(_ >= 0) + val uint256Gen = Generators.getUInt256Gen() + val uint256ListGen = Generators.getListGen(0, 16, uint256Gen) + + test("pop single element") { + forAll(stackGen) { stack => + val (v, stack1) = stack.pop + if (stack.size > 0) { + v shouldEqual stack.toSeq.head + stack1.toSeq shouldEqual stack.toSeq.tail + } else { + v shouldEqual 0 + stack1 shouldEqual stack + } + } + } + + test("pop multiple elements") { + forAll(stackGen, intGen) { (stack, i) => + val (vs, stack1) = stack.pop(i) + if (stack.size >= i) { + vs shouldEqual stack.toSeq.take(i) + stack1.toSeq shouldEqual stack.toSeq.drop(i) + } else { + vs shouldEqual Seq.fill(i)(UInt256.Zero) + stack1 shouldEqual stack + } + } + } + + test("push single element") { + forAll(stackGen, uint256Gen) { (stack, v) => + val stack1 = stack.push(v) + + if (stack.size < stack.maxSize) { + stack1.toSeq shouldEqual (v +: stack.toSeq) + } else { + stack1 shouldEqual stack + } + } + } + + test("push multiple elements") { + forAll(stackGen, uint256ListGen) { (stack, vs) => + val stack1 = stack.push(vs) + + if (stack.size + vs.size <= stack.maxSize) { + stack1.toSeq shouldEqual (vs.reverse ++ stack.toSeq) + } else { + stack1 shouldEqual stack + } + } + } + + test("duplicate element") { + forAll(stackGen, intGen) { (stack, i) => + val stack1 = stack.dup(i) + + if (i < stack.size && stack.size < stack.maxSize) { + val x = stack.toSeq(i) + stack1.toSeq shouldEqual (x +: stack.toSeq) + } else { + stack1 shouldEqual stack + } + } + } + + test("swap elements") { + forAll(stackGen, intGen) { (stack, i) => + val stack1 = stack.swap(i) + + if (i < stack.size) { + val x = stack.toSeq.head + val y = stack.toSeq(i) + stack1.toSeq shouldEqual stack.toSeq.updated(0, y).updated(i, x) + } else { + stack1 shouldEqual stack + } + } + } + +} diff --git a/src/test/scala/io/iohk/ethereum/vm/StackSpecComp.scala b/src/test/scala/io/iohk/ethereum/vm/StackSpecComp.scala new file mode 100644 index 0000000000..c8cc0cbc7e --- /dev/null +++ b/src/test/scala/io/iohk/ethereum/vm/StackSpecComp.scala @@ -0,0 +1,270 @@ +package io.iohk.ethereum.vm + +import io.iohk.ethereum.domain.UInt256 +import io.iohk.ethereum.rlp.RLP +import io.iohk.ethereum.utils.Logger +import org.scalacheck.Gen +import org.scalatest.prop.PropertyChecks +import org.scalatest.{FunSuite, Matchers} +import org.spongycastle.util.encoders.Hex + +import scala.collection.mutable.ListBuffer + +class StackSpecComp extends FunSuite with Matchers with PropertyChecks with Logger { + + val emptyStack = Stack.empty() + val emptyStackL = StackL.empty() + + val stackSwap = (1 to 1023).toList.map(UInt256(_)) + + val fullStack = Stack.empty().push(stackSwap) + val fullStackL = StackL.empty().push(stackSwap) + + val start = 0 + val end = 16 + val iterations = 100000 + val results = 50000 + + test("Stack Swap Comparision Test") { + import scala.collection.mutable.ListBuffer + var measures = new ListBuffer[Long]() + var measuresList = new ListBuffer[Long]() + (1 to iterations).foreach{n => + val r = scala.util.Random + val r1 = start + r.nextInt(( end - start) + 1) + + val start1: Long = System.nanoTime() + val newStack = fullStack.swap(r1) + val end1: Long = System.nanoTime() + + val start2: Long = System.nanoTime() + val newStackList = fullStackL.swap(r1) + val end2: Long = System.nanoTime() + + + newStack.size shouldEqual 1023 + newStackList.size shouldEqual 1023 + val stackTime = end1 - start1 + val stackLTime = end2 - start2 + measures += stackTime + measuresList += stackLTime + } + + + + val statsStack = getStats(measures) + val statsStackList = getStats(measuresList) + log.info(s"Time of $results Swap\t: " + sumOfAllRuns(measures) + "ms, for Vector based Stack" ) + log.info(s"Avarage time of vector based Swap is : ${statsStack._1} ns, with dev ${statsStack._2}") + log.info(s"Median time of vector based Swap is : ${statsStack._3} ns \n") + + + log.info(s"Time of $results Swap\t: " + sumOfAllRuns(measuresList) + "ms, for List based Stack") + log.info(s"Avarage time of list based Swap is : ${statsStackList._1} ns, with dev ${statsStackList._2}") + log.info(s"Median time of list based Swap is : ${statsStackList._3} ns \n") + } + + test("Stack Dup Test") { + import scala.collection.mutable.ListBuffer + var measures = new ListBuffer[Long]() + var measuresList = new ListBuffer[Long]() + (1 to iterations).foreach{n => + val r = scala.util.Random + val r1 = start + r.nextInt(( end - start) + 1) + + val start1: Long = System.nanoTime() + val newStack = fullStack.dup(r1) + val end1: Long = System.nanoTime() + + val start2: Long = System.nanoTime() + val newStackList = fullStackL.dup(r1) + val end2: Long = System.nanoTime() + + newStack.size shouldEqual 1024 + newStackList.size shouldEqual 1024 + val stackTime = end1 - start1 + val stackLTime = end2 - start2 + measures += stackTime + measuresList += stackLTime + } + + val statsStack = getStats(measures) + val statsStackList = getStats(measuresList) + log.info(s"Time of $results Dup\t: " + sumOfAllRuns(measures) + "ms, for Vector based Stack" ) + log.info(s"Avarage time of vector based Dup is : ${statsStack._1} ns, with dev ${statsStack._2}") + log.info(s"Median time of vector based Dup is : ${statsStack._3} ns \n") + + + log.info(s"Time of $results Dup\t: " + sumOfAllRuns(measuresList) + "ms, for List based Stack") + log.info(s"Avarage time of list based Dup is : ${statsStackList._1} ns, with dev ${statsStackList._2}") + log.info(s"Median time of list based Dup is : ${statsStackList._3} ns \n") + } + + test("Stack Push Almost Full Stack Test") { + import scala.collection.mutable.ListBuffer + var measures = new ListBuffer[Long]() + var measuresList = new ListBuffer[Long]() + (1 to iterations).foreach{n => + val start1: Long = System.nanoTime() + val newStack = fullStack.push(n) + val end1: Long = System.nanoTime() + + val start2: Long = System.nanoTime() + val newStackList = fullStackL.push(n) + val end2: Long = System.nanoTime() + + + newStack.pop._1 shouldEqual n + newStackList.pop._1 shouldEqual n + val stackTime = end1 - start1 + val stackLTime = end2 - start2 + measures += stackTime + measuresList += stackLTime + } + + val statsStack = getStats(measures) + val statsStackList = getStats(measuresList) + log.info(s"Time of $results Push\t: " + sumOfAllRuns(measures) + "ms, for Vector based Stack" ) + log.info(s"Avarage time of vector based Push is : ${statsStack._1} ns, with dev ${statsStack._2}") + log.info(s"Median time of vector based Push is : ${statsStack._3} ns \n") + + + log.info(s"Time of $results Push\t: " + sumOfAllRuns(measuresList) + "ms, for List based Stack") + log.info(s"Avarage time of list based Push is : ${statsStackList._1} ns, with dev ${statsStackList._2}") + log.info(s"Median time of list based Push is : ${statsStackList._3} ns \n") + } + + test("Stack Push empty Stack Test") { + import scala.collection.mutable.ListBuffer + var measures = new ListBuffer[Long]() + var measuresList = new ListBuffer[Long]() + (1 to iterations).foreach{n => + val start1: Long = System.nanoTime() + val newStack = emptyStack.push(n) + val end1: Long = System.nanoTime() + + val start2: Long = System.nanoTime() + val newStackList = emptyStackL.push(n) + val end2: Long = System.nanoTime() + + + newStack.pop._1 shouldEqual n + newStackList.pop._1 shouldEqual n + val stackTime = end1 - start1 + val stackLTime = end2 - start2 + measures += stackTime + measuresList += stackLTime + } + + val statsStack = getStats(measures) + val statsStackList = getStats(measuresList) + log.info(s"Time of $results Push\t: " + sumOfAllRuns(measures) + "ms, for Vector based Stack" ) + log.info(s"Avarage time of vector based Push is : ${statsStack._1} ns, with dev ${statsStack._2}") + log.info(s"Median time of vector based Push is : ${statsStack._3} ns \n") + + + log.info(s"Time of $results Push\t: " + sumOfAllRuns(measuresList) + "ms, for List based Stack") + log.info(s"Avarage time of list based Push is : ${statsStackList._1} ns, with dev ${statsStackList._2}") + log.info(s"Median time of list based Push is : ${statsStackList._3} ns \n") + + } + + test("Stack Pop Test") { + import scala.collection.mutable.ListBuffer + var measures = new ListBuffer[Long]() + var measuresList = new ListBuffer[Long]() + + + (1 to iterations).foreach{n => + val start1: Long = System.nanoTime() + val newStack = fullStack.pop + val end1: Long = System.nanoTime() + + + val start2: Long = System.nanoTime() + val newStackList = fullStackL.pop + val end2: Long = System.nanoTime() + + + newStack._1 shouldEqual 1023 + newStackList._1 shouldEqual 1023 + val stackTime = end1 - start1 + val stackLTime = end2 - start2 + measures += stackTime + measuresList += stackLTime + } + + val statsStack = getStats(measures) + val statsStackList = getStats(measuresList) + log.info(s"Time of $results Pop\t: " + sumOfAllRuns(measures) + "ms, for Vector based Stack" ) + log.info(s"Avarage time of vector based Pop is : ${statsStack._1} ns, with dev ${statsStack._2}") + log.info(s"Median time of vector based Pop is : ${statsStack._3} ns \n") + + + log.info(s"Time of $results Pop\t: " + sumOfAllRuns(measuresList) + "ms, for List based Stack") + log.info(s"Avarage time of list based Pop is : ${statsStackList._1} ns, with dev ${statsStackList._2}") + log.info(s"Median time of list based Pop is : ${statsStackList._3} ns \n") + } + + test("Stack Pop List Test") { + import scala.collection.mutable.ListBuffer + var measures = new ListBuffer[Long]() + var measuresList = new ListBuffer[Long]() + + val minPopValue = 1 + val maxPopValue = 4 + + (1 to iterations).foreach{n => + val r = scala.util.Random + val r1 = minPopValue + r.nextInt(( maxPopValue - minPopValue) + 1) + + + val start1: Long = System.nanoTime() + val newStack = fullStack.pop(r1) + val end1: Long = System.nanoTime() + + + val start2: Long = System.nanoTime() + val newStackList = fullStackL.pop(r1) + val end2: Long = System.nanoTime() + + + newStack._1.length shouldEqual r1 + newStackList._1.length shouldEqual r1 + val stackTime = end1 - start1 + val stackLTime = end2 - start2 + measures += stackTime + measuresList += stackLTime + } + + val statsStack = getStats(measures) + val statsStackList = getStats(measuresList) + log.info(s"Time of $results Pop\t: " + sumOfAllRuns(measures) + "ms, for Vector based Stack" ) + log.info(s"Avarage time of vector based Pop is : ${statsStack._1} ns, with dev ${statsStack._2}") + log.info(s"Median time of vector based Pop is : ${statsStack._3} ns \n") + + + log.info(s"Time of $results Pop\t: " + sumOfAllRuns(measuresList) + "ms, for List based Stack") + log.info(s"Avarage time of list based Pop is : ${statsStackList._1} ns, with dev ${statsStackList._2}") + log.info(s"Median time of list based Pop is : ${statsStackList._3} ns \n") + } + + + private def getStats(list: ListBuffer[Long]) = { + val interval = 500 + val mid = results / 2 + + val impres = list.takeRight(results).sorted.slice(mid - interval, mid + interval) + val mean = impres.sum.toDouble / impres.length + val median = impres(impres.length / 2) + + val variance = impres.map(_.toDouble).map(num => math.pow(num - mean, 2)).sum / impres.length.toDouble + val dev = math.sqrt(variance) + (mean, dev, median) + } + + private def sumOfAllRuns(list: ListBuffer[Long]): Double = { + list.takeRight(results).sum.toDouble / 1000000.0 + } + +}