Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merkle Patricia Trie #10

Merged
merged 63 commits into from
Jan 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
6437e20
[MPT] Merkle Patricia Trie first version
Dec 22, 2016
b5c262c
Merge branch 'phase/0/handshake' into feature/patriciaTrie
AlanVerbner Jan 2, 2017
9009233
[MPT] Fix - Each node isn't encoded twice anymore & fixed bugs on cre…
AlanVerbner Jan 4, 2017
3ee73ca
Merge branch 'phase/0/handshake' of github.com:input-output-hk/etc-cl…
Jan 4, 2017
d7c6c67
[MPT] Refactoring due to changes in rlp package structure
Jan 4, 2017
aa5fcd1
[MPT] Refactoring: changed package name
Jan 5, 2017
ea3c48f
[MPT] Refactoring: changed visibility of methods and started using ha…
Jan 5, 2017
226d9a1
[MPT] Private get now operates with nodes and not node ids
Jan 5, 2017
b659e54
[MPT] getNode returns node and throws MPTException if node was not found
Jan 5, 2017
a9216e2
[MPT] Removed functions to get id of nodes inside other nodes to dire…
Jan 5, 2017
5355ae0
[MPT] More EthereumJ compatibility functions
Jan 5, 2017
01c665b
[MPT] Transformed MerklePatriciaTree and IodbDataSource into normal c…
Jan 6, 2017
4f5562b
[MPT] Removed unused functions from DataSource trait and its implemen…
Jan 6, 2017
3f8eeac
[MPT] Test reorganization and added performance test
Jan 6, 2017
4fab49e
[MPT] Store changes in db in a single update
AlanVerbner Jan 6, 2017
d24e76b
Merge branch 'feature/patriciaTrie' of github.com:input-output-hk/etc…
AlanVerbner Jan 6, 2017
f1a20d6
[MPT] It was trying to insert the same values multiple times and IODB…
AlanVerbner Jan 6, 2017
99330e3
[MPT] Fixed how nodes are inserted into toUpdateInStorage and toDelet…
Jan 6, 2017
b8d2af9
[MPT] When next node is required on fix function, we first search on …
Jan 9, 2017
5f9f795
[MPT] Fixed scalastyle warnings
Jan 9, 2017
7fdde94
[MPT] Version is now stored in IodbDataSource instread of MerklePatri…
AlanVerbner Jan 9, 2017
0f2c8e0
Merge branch 'feature/patriciaTrie' of github.com:input-output-hk/etc…
AlanVerbner Jan 9, 2017
45520b7
[MPT] Refactoring: removed unused DataSource function and added comments
Jan 9, 2017
2ac4c75
[MPT] Renamed trie package, class and object
Jan 9, 2017
1d8d248
[MPT] Renamed MPT Suite
Jan 9, 2017
af743d7
[MPT] Fixed bug that caused previous trie root to not be removed from…
Jan 9, 2017
cb1ebe3
[MPT] Removed unused imports
Jan 9, 2017
63ee543
[MPT] Set long tests to ignore and added FIXME regarding the hash fun…
Jan 9, 2017
358967f
Small example changes around map
mcsherrylabs Jan 10, 2017
acdb09e
Test email address correction
mcsherrylabs Jan 10, 2017
a215bc8
Merge pull request #6 from input-output-hk/feature/patriciaTrieAmcs
AlanVerbner Jan 10, 2017
7095566
[MPT] Replace some match statements by map
AlanVerbner Jan 10, 2017
019e61d
[MPT] Continued replacing match statements with map
Jan 10, 2017
6045cac
[MPT] Code refactoring, added Node file and package object
Jan 10, 2017
418b231
[MPT] Pull from branch phase/1/download
Jan 10, 2017
b13dc05
[MPT] Minor fixes in MPTSuite and build.sbt
Jan 10, 2017
48fe701
Merge branch 'phase/2/txHashValidation' of github.com:input-output-hk…
AlanVerbner Jan 11, 2017
ee34e28
[MPT] Added comments on DataSource trait and Node file functions
Jan 11, 2017
5b262ea
Merge branch 'feature/patriciaTrie' of github.com:input-output-hk/etc…
Jan 11, 2017
682c107
[MPT] Some IODB tests will now be ignored by default when running all…
Jan 11, 2017
59b081a
[MPT] Changed comments and renamed parameters in HexPrefix
Jan 13, 2017
bab3ef2
[MPT] Removed tryGetNode function and other minor fixes
Jan 13, 2017
ce2e822
[MPT] Changed nibblesToBytes function in HexPrefix to a more function…
Jan 13, 2017
a37b1c5
[MPT] Add to the node the hashFn and the encoder in order to be able …
AlanVerbner Jan 13, 2017
afc83d9
Merge branch 'feature/patriciaTrieAdamChangeSuggestion' into feature/…
AlanVerbner Jan 13, 2017
b3a79a6
[MPT] IODB version fixed to 0.1.1
AlanVerbner Jan 16, 2017
c863cce
[MPT] Removed unused variables and moved NodeInsertResult and NodeRem…
Jan 16, 2017
417792f
[MPT] hashFn removed from fix parameters
AlanVerbner Jan 16, 2017
62f6f3b
[MPT] hashFn used from TrieInstance when fixing instead of node one
AlanVerbner Jan 16, 2017
2bc5800
[MPT] Removed unused library from ObjectGenerators
Jan 16, 2017
80cda19
Merge branch 'feature/patriciaTrie' of github.com:input-output-hk/etc…
Jan 16, 2017
c50bf5b
[MPT] package io.iohk.ethereum.merklepatriciatrie renamed to package …
AlanVerbner Jan 16, 2017
5ad56b5
Merge branch 'feature/patriciaTrie' of github.com:input-output-hk/etc…
AlanVerbner Jan 16, 2017
c848180
Merge branch 'phase/2/txHashValidation' of github.com:input-output-hk…
AlanVerbner Jan 16, 2017
88d1524
[MPT] Renamed NodeRemoveResult class variable to newNode
Jan 16, 2017
4a78a5b
Merge branch 'feature/patriciaTrie' of github.com:input-output-hk/etc…
Jan 16, 2017
a8bf783
Merge branch 'phase/2/txHashValidation' of github.com:input-output-hk…
Jan 18, 2017
d1099ec
[MPT] Fixed scalastyle errors
Jan 18, 2017
f0f4a5b
[MPT] Test partitioning in order not to ignore long running tests
AlanVerbner Jan 18, 2017
601b2de
Merge pull request #18 from input-output-hk/feature/patriciaTrieTestP…
AlanVerbner Jan 18, 2017
cb120e6
[MPT] Scalastyle fix in EphemDataSource
AlanVerbner Jan 18, 2017
3a1e46b
[MPT] EphemDataSource and IodbDataSource tests
Jan 18, 2017
568b7f8
[MPT] Remove unused test code
AlanVerbner Jan 18, 2017
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
24 changes: 17 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
name := "etc-client"

version := "0.1"

scalaVersion := "2.11.8"
val commonSettings = Seq(
name := "etc-client",
version := "0.1",
scalaVersion := "2.11.8"
)

libraryDependencies ++= Seq(
val dep = Seq(
"com.typesafe.akka" %% "akka-actor" % "2.4.16",
"org.consensusresearch" %% "scrypto" % "1.2.0-RC3",
"com.madgag.spongycastle" % "core" % "1.54.0.0",
"org.scalatest" %% "scalatest" % "3.0.1" % "test",
"org.scalacheck" %% "scalacheck" % "1.13.4" % "test"
"org.scalatest" %% "scalatest" % "3.0.1" % "it,test",
"org.scalacheck" %% "scalacheck" % "1.13.4" % "it,test",
"org.scorexfoundation" %% "iodb" % "0.1.1"
)

val Integration = config("it") extend Test

val root = project.in(file("."))
.configs(Integration)
.settings(commonSettings: _*)
.settings(libraryDependencies ++= dep)
.settings(inConfig(Integration)(Defaults.testSettings) : _*)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.iohk.ethereum.mpt

import org.scalatest.FunSuite
import org.scalatest.prop.PropertyChecks
import io.iohk.ethereum.ObjectGenerators
import io.iohk.iodb.LSMStore
import java.io.File

class IodbDataSourceIntegrationSuite extends FunSuite
with PropertyChecks
with ObjectGenerators {

val KeySize: Int = 32
val KeyNumberLimit: Int = 40
val DefaultRootHash: Array[Byte] = Array.emptyByteArray

def putMultiple(dataSource: DataSource, toInsert: Seq[(Array[Byte], Array[Byte])]): DataSource = {
toInsert.foldLeft(dataSource){ case (recDB, keyValuePair) =>
recDB.update(DefaultRootHash, Seq(), Seq(keyValuePair))
}
}

test("IodbDataSource insert"){
forAll(seqByteArrayOfNItemsGen(KeySize)) { unFilteredKeyList: Seq[Array[Byte]] =>
//create temporary dir
val dir = File.createTempFile("iodbInsert", "iodbInsert")
dir.delete()
dir.mkdir()

val keyList = unFilteredKeyList.filter(_.length == KeySize).take(KeyNumberLimit)
val db = putMultiple(
dataSource = new IodbDataSource(new LSMStore(dir = dir, keySize = KeySize)),
toInsert = keyList.zip(keyList)
)
keyList.foreach { key =>
val obtained = db.get(key)
assert(obtained.isDefined)
assert(obtained.get sameElements key)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package io.iohk.ethereum.mpt

import java.io.File
import java.nio.ByteBuffer
import java.security.MessageDigest

import akka.util.ByteString
import io.iohk.ethereum.ObjectGenerators
import io.iohk.ethereum.crypto.sha3
import io.iohk.ethereum.mpt.MerklePatriciaTrie.defaultByteArraySerializable
import io.iohk.ethereum.rlp.{decode => decodeRLP, encode => encodeRLP}
import io.iohk.iodb.LSMStore
import org.scalacheck.{Arbitrary, Gen}
import org.scalatest.FunSuite
import org.scalatest.prop.PropertyChecks
import org.spongycastle.util.encoders.Hex

import scala.util.Random

class MerklePatriciaTreeIntegrationSuite extends FunSuite
with PropertyChecks
with ObjectGenerators {
val hashFn = (input: Array[Byte]) => sha3(input)

val EmptyTrie = MerklePatriciaTrie[Array[Byte], Array[Byte]](EphemDataSource(), hashFn)

implicit val intByteArraySerializable = new ByteArraySerializable[Int] {
override def toBytes(input: Int): Array[Byte] = {
val b: ByteBuffer = ByteBuffer.allocate(4)
b.putInt(input)
b.array
}

override def fromBytes(bytes: Array[Byte]): Int = ByteBuffer.wrap(bytes).getInt()
}

def md5(bytes: Array[Byte]): Array[Byte] = {
MessageDigest.getInstance("MD5").digest(bytes)
}


test("IODB test - Insert of the first 5000 numbers hashed and then remove half of them"){
//create temporary dir
val dir = File.createTempFile("iodb", "iodb")
dir.delete()
dir.mkdir()

val dataSource = new IodbDataSource(new LSMStore(dir = dir, keySize = 32))
val emptyTrie = MerklePatriciaTrie[Array[Byte], Array[Byte]](dataSource, hashFn)

val keys = (0 to 100).map(intByteArraySerializable.toBytes)
val trie = Random.shuffle(keys).foldLeft(emptyTrie) { case (recTrie, key) => recTrie.put(md5(key), key) }

// We delete have of the (key-value) pairs we had inserted
val trieAfterDelete = Random.shuffle(keys.take(100/2)).foldLeft(trie) { case (recTrie, key) => recTrie.remove(md5(key)) }

// We delete keys with no effect so as to test that is the case (and for more code coverage)
val trieAfterDeleteNoEffect = keys.take(100/2).foldLeft(trieAfterDelete) { case (recTrie, key) => recTrie.remove(md5(key)) }
assert(Hex.toHexString(trieAfterDeleteNoEffect.getRootHash) == "b0bfbf4d2d6f3c9863c27f41a087208131f775edd9de2cb66242d1e0981aa94c")
}

test("IODB Test - PatriciaTrie insert and get") {
forAll(keyValueListGen()) { keyValueList: Seq[(Int, Int)] =>
//create temporary dir
val dir = File.createTempFile("iodb", "iodb")
dir.delete()
dir.mkdir()

val dataSource = new IodbDataSource(new LSMStore(dir = dir, keySize = 32))
val trie = keyValueList.foldLeft(MerklePatriciaTrie[Int, Int](dataSource, hashFn)) {
case (recTrie, (key, value)) => recTrie.put(key, value)
}
keyValueList.foreach { case (key, value) =>
val obtained = trie.get(key)
assert(obtained.isDefined)
assert(obtained.get == value)
}
}
}

test("IODB Test - PatriciaTrie delete") {
forAll(Gen.nonEmptyListOf(Arbitrary.arbitrary[Int])) { keyList: List[Int] =>
//create temporary dir
val dirWithDelete = File.createTempFile("iodb", "iodb1")
dirWithDelete.delete()
dirWithDelete.mkdir()
val dataSourceWithDelete = new IodbDataSource(new LSMStore(dir = dirWithDelete, keySize = 32))

val keyValueList = keyList.distinct.zipWithIndex
val trieAfterInsert = keyValueList.foldLeft(MerklePatriciaTrie[Int, Int](dataSourceWithDelete, hashFn)) {
case (recTrie, (key, value)) => recTrie.put(key, value)
}
val (keyValueToDelete, keyValueLeft) = Random.shuffle(keyValueList).splitAt(Gen.choose(0, keyValueList.size).sample.get)
val trieAfterDelete = keyValueToDelete.foldLeft(trieAfterInsert) {
case (recTrie, (key, value)) => recTrie.remove(key)
}

keyValueLeft.foreach { case (key, value) =>
val obtained = trieAfterDelete.get(key)
assert(obtained.isDefined)
assert(obtained.get == value)
}
keyValueToDelete.foreach { case (key, value) =>
val obtained = trieAfterDelete.get(key)
assert(obtained.isEmpty)
}

val dirOnlyInsert = File.createTempFile("iodb", "iodb2")
dirOnlyInsert.delete()
dirOnlyInsert.mkdir()
val dataSourceOnlyInsert = new IodbDataSource(new LSMStore(dir = dirOnlyInsert, keySize = 32))

val trieWithKeyValueLeft = keyValueLeft.foldLeft(MerklePatriciaTrie[Int, Int](dataSourceOnlyInsert, hashFn)) {
case (recTrie, (key, value)) => recTrie.put(key, value)
}
assert(trieAfterDelete.getRootHash sameElements trieWithKeyValueLeft.getRootHash)
}
}

test("EthereumJ compatibility - Insert of the first 40000 numbers"){
val shuffledKeys = Random.shuffle(0 to 40000).map(intByteArraySerializable.toBytes)
val trie = shuffledKeys.foldLeft(EmptyTrie) { case (recTrie, key) => recTrie.put(key, key) }
assert(Hex.toHexString(trie.getRootHash) == "3f8b75707975e5c16588fa1ba3e69f8da39f4e7bf3ca28b029c7dcb589923463")
}

test("EthereumJ compatibility - Insert of the first 20000 numbers hashed"){
val shuffledKeys = Random.shuffle(0 to 20000).map(intByteArraySerializable.toBytes)
val trie = shuffledKeys.foldLeft(EmptyTrie) { case (recTrie, key) => recTrie.put(md5(key), key) }

// We insert keys that should have no effect so as to test that is the case (and for more code coverage)
val trieAfterInsertNoEffect = shuffledKeys.take(20000/2).foldLeft(trie) { case (recTrie, key) => recTrie.put(md5(key), key) }
assert(Hex.toHexString(trieAfterInsertNoEffect.getRootHash) == "a522b23a640c5fdb726e3f9644863e8913fe86339909fe881957efa0c23cebaa")
}

test("EthereumJ compatibility - Insert of the first 20000 numbers hashed and then remove half of them"){
val keys = (0 to 20000).map(intByteArraySerializable.toBytes)
val trie = Random.shuffle(keys).foldLeft(EmptyTrie) { case (recTrie, key) => recTrie.put(md5(key), key) }

// We delete have of the (key-value) pairs we had inserted
val trieAfterDelete = Random.shuffle(keys.take(20000/2)).foldLeft(trie) { case (recTrie, key) => recTrie.remove(md5(key)) }

// We delete keys with no effect so as to test that is the case (and for more code coverage)
val trieAfterDeleteNoEffect = keys.take(20000/2).foldLeft(trieAfterDelete) { case (recTrie, key) => recTrie.remove(md5(key)) }
assert(Hex.toHexString(trieAfterDeleteNoEffect.getRootHash) == "a693b82dcc5a9e581e9bf9aa7af3aed31fe3eb61f97fd733ce44c9f9df2d7f45")
}

test("EthereumJ compatibility - Insert of the first 20000 numbers hashed (with some sliced)"){
val keys = (0 to 20000).map(intByteArraySerializable.toBytes)

// We slice some of the keys so that me test more code coverage (if not we only test keys with the same length)
val slicedKeys = keys.zipWithIndex.map{case (key, index) =>
val hashedKey = md5(key)
if(index%2==0) hashedKey.take(hashedKey.length/2) else hashedKey
}
val keyValuePairs = slicedKeys.zip(keys)

val trie = Random.shuffle(keyValuePairs).foldLeft(EmptyTrie) { case (recTrie, (key, value)) => recTrie.put(key, value) }
assert(Hex.toHexString(trie.getRootHash) == "46cde8656f3be6ce93ba9dcb1017548f44c65d1ea659ac827fac8c9ac77cf6b3")
}

test("EthereumJ compatibility - Insert of the first 20000 numbers hashed (with some sliced) and then remove half of them") {
val keys = (0 to 20000).map(intByteArraySerializable.toBytes)

// We slice some of the keys so that me test more code coverage (if not we only test keys with the same length)
val slicedKeys = keys.zipWithIndex.map { case (key, index) =>
val hashedKey = md5(key)
if (index % 2 == 0) hashedKey.take(hashedKey.length / 2) else hashedKey }
val keyValuePairs = slicedKeys.zip(keys)

val trie = Random.shuffle(keyValuePairs).foldLeft(EmptyTrie) { case (recTrie, (key, value)) => recTrie.put(key, value) }

assert(Hex.toHexString(trie.getRootHash) == "46cde8656f3be6ce93ba9dcb1017548f44c65d1ea659ac827fac8c9ac77cf6b3")

// We delete have of the (key-value) pairs we had inserted
val trieAfterDelete = Random.shuffle(keyValuePairs.take(20000 / 2)).foldLeft(trie) { case (recTrie, (key, _)) => recTrie.remove(key) }

assert(Hex.toHexString(trieAfterDelete.getRootHash) == "ae7b65dddd3ac0428082160cf3ceff0276cf6e6deaa23b42c4c156b50a459822")
}

/* Performance test */
test("Performance test (From: https://github.com/ethereum/wiki/wiki/Benchmarks)"){
val debug = false
val Rounds = 1000
val Symmetric = true

val start: Long = System.currentTimeMillis
val emptyTrie = MerklePatriciaTrie[Array[Byte], Array[Byte]](EphemDataSource(), hashFn)
var seed: Array[Byte] = Array.fill(32)(0.toByte)

val trieResult = (0 until Rounds).foldLeft(emptyTrie){ case (recTrie, i) =>
seed = hashFn(seed)
if(!Symmetric) recTrie.put(seed, seed)
else{
val mykey = seed
seed = hashFn(seed)
val myval = if((seed(0) & 0xFF) % 2 == 1) Array[Byte](seed.last) else seed
recTrie.put(mykey, myval)
}
}
val rootHash = Hex.toHexString(trieResult.getRootHash)
if(debug){
println("Time taken(ms): " + (System.currentTimeMillis - start))
println("Root hash obtained: " + rootHash)
}
if(Symmetric) assert(rootHash.take(4) == "36f6" && rootHash.drop(rootHash.length-4) == "93a3")
else assert(rootHash.take(4) == "da8a" && rootHash.drop(rootHash.length-4) == "0ca4")
}
}
18 changes: 18 additions & 0 deletions src/main/scala/io/iohk/ethereum/mpt/EphemDataSource.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.iohk.ethereum.mpt

import akka.util.ByteString

case class EphemDataSource(storage: Map[ByteString, Array[Byte]]) extends DataSource {

override def get(key: Array[Byte]): Option[Array[Byte]] = storage.get(ByteString(key))

override def update(rootHash: Array[Byte], toRemove: Seq[Key], toUpdate: Seq[(Key, Value)]): DataSource = {
val afterRemoval = toRemove.foldLeft(storage)((storage, key) => storage - ByteString(key))
val afterUpdate = toUpdate.foldLeft(afterRemoval)((storage, toUpdate) => storage + (ByteString(toUpdate._1) -> toUpdate._2))
EphemDataSource(afterUpdate)
}
}

object EphemDataSource {
def apply(): EphemDataSource = EphemDataSource(Map())
}
66 changes: 66 additions & 0 deletions src/main/scala/io/iohk/ethereum/mpt/HexPrefix.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.iohk.ethereum.mpt

object HexPrefix {
/**
* Pack nibbles to binary
*
* @param nibbles sequence
* @param isLeaf boolean used to encode whether or not the data being encoded corresponds to a LeafNode or an ExtensionNode
* @return hex-encoded byte array
*
*/
def encode(nibbles: Array[Byte], isLeaf: Boolean): Array[Byte] = {
val hasOddLength = nibbles.length % 2 == 1
val firstByteFlag: Byte = (2*(if(isLeaf) 1 else 0) + (if(hasOddLength) 1 else 0)).toByte
val lengthFlag = if(hasOddLength) 1 else 2

val nibblesWithFlag = new Array[Byte](nibbles.length + lengthFlag)
Array.copy(nibbles, 0, nibblesWithFlag, lengthFlag, nibbles.length)
nibblesWithFlag(0) = firstByteFlag
if(!hasOddLength) nibblesWithFlag(1) = 0
nibblesToBytes(nibblesWithFlag)
}

/**
* Unpack a binary string to its nibbles equivalent
*
* @param src of binary data
* @return array of nibbles in byte-format and
* boolean used to encode whether or not the data being decoded corresponds to a LeafNode or an ExtensionNode
*
*/
def decode(src: Array[Byte]): (Array[Byte], Boolean) = {
val srcNibbles: Array[Byte] = bytesToNibbles(bytes = src)
val t = (srcNibbles(0) & 2) != 0
val hasOddLength = (srcNibbles(0) & 1) != 0
val flagLength = if(hasOddLength) 1 else 2

val res = new Array[Byte](srcNibbles.length - flagLength)
Array.copy(srcNibbles, flagLength, res, 0, srcNibbles.length-flagLength)
(res, t)
}

/**
* Transforms an array of 8bit values to the corresponding array of 4bit values (hexadecimal format)
*
* @param bytes byte[]
* @return array with each individual nibble
*
*/
def bytesToNibbles(bytes: Array[Byte]): Array[Byte] = {
bytes.foldRight[List[Byte]](List()){(elem, rec) =>
((elem >> 4) & 0xF).toByte :: (elem & 0xF).toByte :: rec}.toArray
}

/**
* Transforms an array of 4bit values (hexadecimal format) to the corresponding array of 8bit values
*
* @param nibbles byte[]
* @return array with bytes combining pairs of nibbles
*
*/
def nibblesToBytes(nibbles: Array[Byte]): Array[Byte] = {
require(nibbles.length % 2 == 0)
nibbles.grouped(2).map{case Array(n1,n2) => (16*n1 + n2).toByte}.toArray
}
}
Loading