Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions src/main/scala/io/iohk/ethereum/vm/Blake2bCompression.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package io.iohk.ethereum.vm

import java.util.Arrays.copyOfRange

// scalastyle:off magic.number
object Blake2bCompression {
val MessageBytesLength = 213

import org.bouncycastle.util.Pack

private val IV: Array[Long] = Array(
0x6a09e667f3bcc908L,
0xbb67ae8584caa73bL,
0x3c6ef372fe94f82bL,
0xa54ff53a5f1d36f1L,
0x510e527fade682d1L,
0x9b05688c2b3e6c1fL,
0x1f83d9abfb41bd6bL,
0x5be0cd19137e2179L)

private val PRECOMPUTED: Array[Array[Byte]] = Array(
Array(0, 2, 4, 6, 1, 3, 5, 7, 8, 10, 12, 14, 9, 11, 13, 15),
Array(14, 4, 9, 13, 10, 8, 15, 6, 1, 0, 11, 5, 12, 2, 7, 3),
Array(11, 12, 5, 15, 8, 0, 2, 13, 10, 3, 7, 9, 14, 6, 1, 4),
Array(7, 3, 13, 11, 9, 1, 12, 14, 2, 5, 4, 15, 6, 10, 0, 8),
Array(9, 5, 2, 10, 0, 7, 4, 15, 14, 11, 6, 3, 1, 12, 8, 13),
Array(2, 6, 0, 8, 12, 10, 11, 3, 4, 7, 15, 1, 13, 5, 14, 9),
Array(12, 1, 14, 4, 5, 15, 13, 10, 0, 6, 9, 8, 7, 3, 2, 11),
Array(13, 7, 12, 3, 11, 14, 1, 9, 5, 15, 8, 2, 0, 4, 6, 10),
Array(6, 14, 11, 0, 15, 9, 3, 8, 12, 13, 1, 10, 2, 7, 4, 5),
Array(10, 8, 7, 1, 2, 4, 6, 5, 15, 9, 3, 13, 11, 14, 12, 0))

private def bytesToInt(bytes: Array[Byte]) = Pack.bigEndianToInt(bytes, 0)

private def bytesToLong(bytes: Array[Byte]) = Pack.littleEndianToLong(bytes, 0)

def isValidInput(input: Array[Byte]): Boolean =
!(input.length != MessageBytesLength || (input(212) & 0xFE) != 0)

def parseNumberOfRounds(input: Array[Byte]): Long =
Integer.toUnsignedLong(bytesToInt(copyOfRange(input, 0, 4)))
/**
* Parses input according to the rules defined in: https://eips.ethereum.org/EIPS/eip-152
* The encoded inputs are corresponding to the ones specified in the BLAKE2 RFC Section 3.2:

* rounds - the number of rounds - 32-bit unsigned big-endian word
* h - the state vector - 8 unsigned 64-bit little-endian words
* m - the message block vector - 16 unsigned 64-bit little-endian words
* t_0, t_1 - offset counters - 2 unsigned 64-bit little-endian words
* f - the final block indicator flag - 8-bit word
*
* @param input [4 bytes for rounds][64 bytes for h][128 bytes for m][8 bytes for t_0][8 bytes for t_1][1 byte for f]
* @return all parsed inputs from input array: (rounds, h, m, t, f)
*/
private def parseInput(input: Array[Byte]): (Long, Array[Long], Array[Long], Array[Long], Boolean) = {
val rounds = parseNumberOfRounds(input)
val h = new Array[Long](8)
val m = new Array[Long](16)
val t = new Array[Long](2)

var i = 0
while (i < h.length) {
val offset = 4 + i * 8
h(i) = bytesToLong(copyOfRange(input, offset, offset + 8))
i += 1
}

var j = 0
while (j < 16) {
val offset = 68 + j * 8
m(j) = bytesToLong(copyOfRange(input, offset, offset + 8))
j += 1
}

t(0) = bytesToLong(copyOfRange(input, 196, 204))
t(1) = bytesToLong(copyOfRange(input, 204, 212))
val f = input(212) != 0
(rounds, h, m, t, f)
}

def blake2bCompress(input: Array[Byte]): Option[Array[Byte]] = {
if (isValidInput(input)) {
val (rounds, h, m, t, f) = parseInput(input)
compress(rounds, h, m, t, f)
Some(convertToBytes(h))
} else {
None
}
}

private def convertToBytes(h: Array[Long]): Array[Byte] = {
var i = 0
val out = new Array[Byte](h.length * 8)
while (i < h.length) {
System.arraycopy(Pack.longToLittleEndian(h(i)), 0, out, i * 8, 8)
i += 1
}
out
}

private def compress(rounds: Long, h: Array[Long], m: Array[Long], t: Array[Long], f: Boolean): Unit = {
val v = new Array[Long](16)
val t0 = t(0)
val t1 = t(1)
System.arraycopy(h, 0, v, 0, 8)
System.arraycopy(IV, 0, v, 8, 8)
v(12) ^= t0
v(13) ^= t1

if (f) {
v(14) ^= 0xffffffffffffffffL
}

var j = 0L
while (j < rounds) {
val s: Array[Byte] = PRECOMPUTED((j % 10).toInt)
mix(v, m(s(0)), m(s(4)), 0, 4, 8, 12)
mix(v, m(s(1)), m(s(5)), 1, 5, 9, 13)
mix(v, m(s(2)), m(s(6)), 2, 6, 10, 14)
mix(v, m(s(3)), m(s(7)), 3, 7, 11, 15)
mix(v, m(s(8)), m(s(12)), 0, 5, 10, 15)
mix(v, m(s(9)), m(s(13)), 1, 6, 11, 12)
mix(v, m(s(10)), m(s(14)), 2, 7, 8, 13)
mix(v, m(s(11)), m(s(15)), 3, 4, 9, 14)
j += 1
}

// update h:
var offset = 0
while (offset < h.length) {
h(offset) ^= v(offset) ^ v(offset + 8)
offset += 1
}
}

private def mix(v: Array[Long], a: Long, b: Long, i: Int, j: Int, k: Int, l: Int): Unit = {
v(i) += a + v(j)
v(l) = java.lang.Long.rotateLeft(v(l) ^ v(i), -32)
v(k) += v(l)
v(j) = java.lang.Long.rotateLeft(v(j) ^ v(k), -24)
v(i) += b + v(j)
v(l) = java.lang.Long.rotateLeft(v(l) ^ v(i), -16)
v(k) += v(l)
v(j) = java.lang.Long.rotateLeft(v(j) ^ v(k), -63)
}
}
33 changes: 31 additions & 2 deletions src/main/scala/io/iohk/ethereum/vm/PrecompiledContracts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.iohk.ethereum.utils.ByteUtils
import io.iohk.ethereum.vm.BlockchainConfigForEvm.EtcForks.EtcFork
import io.iohk.ethereum.vm.BlockchainConfigForEvm.EthForks.EthFork
import io.iohk.ethereum.vm.BlockchainConfigForEvm.{EtcForks, EthForks}

import scala.util.Try

// scalastyle:off magic.number
Expand All @@ -23,6 +24,7 @@ object PrecompiledContracts {
val Bn128AddAddr = Address(6)
val Bn128MulAddr = Address(7)
val Bn128PairingAddr = Address(8)
val Blake2bCompressionAddr = Address(9)

val contracts = Map(
EcDsaRecAddr -> EllipticCurveRecovery,
Expand All @@ -37,6 +39,10 @@ object PrecompiledContracts {
Bn128MulAddr -> Bn128Mul,
Bn128PairingAddr -> Bn128Pairing
)

val istanbulPhoenixContracts = byzantiumAtlantisContracts ++ Map(
Blake2bCompressionAddr -> Blake2bCompress
)
/**
* Checks whether `ProgramContext#recipientAddr` points to a precompiled contract
*/
Expand All @@ -53,8 +59,12 @@ object PrecompiledContracts {

private def getContract(context: ProgramContext[_, _]): Option[PrecompiledContract] = {
context.recipientAddr.flatMap{ addr =>
if (context.evmConfig.blockchainConfig.ethForkForBlockNumber(context.blockHeader.number) >= EthForks.Byzantium ||
context.evmConfig.blockchainConfig.etcForkForBlockNumber(context.blockHeader.number) >= EtcForks.Atlantis) {
val ethFork = context.evmConfig.blockchainConfig.ethForkForBlockNumber(context.blockHeader.number)
val etcFork = context.evmConfig.blockchainConfig.etcForkForBlockNumber(context.blockHeader.number)

if (ethFork >= EthForks.Istanbul || etcFork >= EtcForks.Phoenix) {
istanbulPhoenixContracts.get(addr)
} else if (ethFork >= EthForks.Byzantium || etcFork >= EtcForks.Atlantis) {
// byzantium and atlantis hard fork introduce the same set of precompiled contracts
byzantiumAtlantisContracts.get(addr)
} else
Expand Down Expand Up @@ -375,4 +385,23 @@ object PrecompiledContracts {
input.slice(from, from + wordLength)
}
}

//Spec: https://eips.ethereum.org/EIPS/eip-152
// scalastyle: off
object Blake2bCompress extends PrecompiledContract {
def exec(inputData: ByteString): Option[ByteString] = {
Blake2bCompression.blake2bCompress(inputData.toArray).map(ByteString.fromArrayUnsafe)
}

def gas(inputData: ByteString, etcFork: EtcFork, ethFork: EthFork): BigInt = {
val inputArray = inputData.toArray
if (Blake2bCompression.isValidInput(inputArray)) {
// Each round costs 1gas
Blake2bCompression.parseNumberOfRounds(inputArray)
} else {
// bad input to contract, contract will not execute, set price to zero
0
}
}
}
}
33 changes: 33 additions & 0 deletions src/test/scala/io/iohk/ethereum/vm/BlakeCompressionSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.iohk.ethereum.vm

import org.bouncycastle.util.encoders.Hex
import org.scalatest.prop.PropertyChecks
import org.scalatest.{FlatSpec, Matchers}

class BlakeCompressionSpec extends FlatSpec with Matchers with PropertyChecks {
// test vectors from: https://eips.ethereum.org/EIPS/eip-152
val testVectors = Table[String, Option[String]](
("value", "result"),
("00000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", None),
("000000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", None),
("0000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000002", None),
("0000000048c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", Some("08c9bcf367e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d282e6ad7f520e511f6c3e2b8c68059b9442be0454267ce079217e1319cde05b")),
("0000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", Some("ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d17d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923")),
("0000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000", Some("75ab69d3190a562c51aef8d88f1c2775876944407270c42c9844252c26d2875298743e7f6d5ea2f2d3e8d226039cd31b4e426ac4f2d3d666a610c2116fde4735")),
("0000000148c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", Some("b63a380cb2897d521994a85234ee2c181b5f844d2c624c002677e9703449d2fba551b3a8333bcdf5f2f7e08993d53923de3d64fcc68c034e717b9293fed7a421"))
)

"Blake2b compression function" should "handle all test vectors" in {
forAll(testVectors) { (value, expectedResult) =>
val asBytes = Hex.decode(value)
val result = Blake2bCompression.blake2bCompress(asBytes)
val resultAsString = result.map(str => Hex.toHexString(str))
assert(resultAsString == expectedResult)
}
}

it should "handle empty input" in {
val result = Blake2bCompression.blake2bCompress(Array())
assert(result.isEmpty)
}
}
17 changes: 17 additions & 0 deletions src/test/scala/io/iohk/ethereum/vm/PrecompiledContractsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,21 @@ class PrecompiledContractsSpec extends FunSuite with Matchers with PropertyCheck
result.returnData shouldEqual ByteString(Hex.decode(expectedResult))
}
}

test("BLAKE2bCompress") {
val testData = Table(("input", "Expected"),
("0000000148c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001", "b63a380cb2897d521994a85234ee2c181b5f844d2c624c002677e9703449d2fba551b3a8333bcdf5f2f7e08993d53923de3d64fcc68c034e717b9293fed7a421")
)

forAll(testData) { (input, expectedResult) =>
val inputArray = Hex.decode(input)
val expectedNumOfRounds = BigInt(1, inputArray.take(4))
val context = buildContext(PrecompiledContracts.Blake2bCompressionAddr, ByteString(inputArray))
val result = vm.run(context)
val gasUsed = context.startGas - result.gasRemaining
gasUsed shouldEqual expectedNumOfRounds
result.returnData shouldEqual ByteString(Hex.decode(expectedResult))
}
}

}