From ce1188d58a39270695c267c69518ecfc452617ba Mon Sep 17 00:00:00 2001 From: fluency03 Date: Sat, 21 Apr 2018 22:58:00 +0100 Subject: [PATCH] rewrite Transaction --- README.md | 5 +- build.sbt | 4 +- src/main/resources/genesis-block.json | 19 +- src/main/resources/private-key | 1 + src/main/resources/public-key | 1 + src/main/resources/signature | 1 + .../com/fluency03/blockchain/Crypto.scala | 48 +++++ .../scala/com/fluency03/blockchain/Util.scala | 22 ++- .../blockchain/api/actors/BlockActor.scala | 1 + .../api/actors/BlockchainActor.scala | 38 +--- .../blockchain/api/actors/NetworkActor.scala | 41 +++++ .../blockchain/api/actors/PeerActor.scala | 30 ++++ .../api/actors/TransactionActor.scala | 37 +++- .../fluency03/blockchain/api/package.scala | 1 + .../blockchain/api/routes/BlockRoutes.scala | 12 +- .../blockchain/api/routes/GenericRoutes.scala | 33 ++-- .../api/routes/TransactionRoutes.scala | 22 +-- .../blockchain/api/utils/GenericMessage.scala | 2 + .../com/fluency03/blockchain/core/Block.scala | 103 ++++++----- .../blockchain/core/BlockHeader.scala | 29 ++- .../blockchain/core/Blockchain.scala | 73 ++++---- .../blockchain/core/Difficulty.scala | 4 + .../core/{MerkleNode.scala => Merkle.scala} | 8 +- .../blockchain/core/Transaction.scala | 88 ++++++++-- .../fluency03/blockchain/core/package.scala | 11 +- src/test/resources/genesis-block.json | 2 +- src/test/resources/private-key | 1 + src/test/resources/public-key | 1 + src/test/resources/signature | 1 + .../com/fluency03/blockchain/CryptoTest.scala | 18 ++ .../com/fluency03/blockchain/UtilTest.scala | 4 +- .../blockchain/core/BlockHeaderTest.scala | 64 ++++--- .../fluency03/blockchain/core/BlockTest.scala | 166 +++++++++--------- .../blockchain/core/BlockchainTest.scala | 50 ++---- ...{MerkleNodeTest.scala => MerkleTest.scala} | 19 +- .../blockchain/core/TransactionTest.scala | 42 +++-- 36 files changed, 611 insertions(+), 391 deletions(-) create mode 100644 src/main/resources/private-key create mode 100644 src/main/resources/public-key create mode 100644 src/main/resources/signature create mode 100644 src/main/scala/com/fluency03/blockchain/Crypto.scala create mode 100644 src/main/scala/com/fluency03/blockchain/api/actors/NetworkActor.scala create mode 100644 src/main/scala/com/fluency03/blockchain/api/actors/PeerActor.scala rename src/main/scala/com/fluency03/blockchain/core/{MerkleNode.scala => Merkle.scala} (62%) create mode 100644 src/test/resources/private-key create mode 100644 src/test/resources/public-key create mode 100644 src/test/resources/signature create mode 100644 src/test/scala/com/fluency03/blockchain/CryptoTest.scala rename src/test/scala/com/fluency03/blockchain/core/{MerkleNodeTest.scala => MerkleTest.scala} (67%) diff --git a/README.md b/README.md index 55134f3..13e8064 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ Simple Blockchain Implementation in Scala. -Inspired by [Daniel van Flymen](http://www.dvf.nyc/)'s blog [Learn Blockchains by Building One](https://hackernoon.com/learn-blockchains-by-building-one-117428612f46), I started to implement this Scala version of a simple Blockchain. +Inspired by: +- [Daniel van Flymen](http://www.dvf.nyc/)'s blog [Learn Blockchains by Building One](https://hackernoon.com/learn-blockchains-by-building-one-117428612f46) +- [Naivecoin](https://github.com/lhartikk/naivecoin) of [@lhartikk](https://github.com/lhartikk) REST API service is built on the open source projects [akka](https://github.com/akka/akka) and [akka-http](https://github.com/akka/akka-http). - diff --git a/build.sbt b/build.sbt index bb8de00..ebd2f83 100644 --- a/build.sbt +++ b/build.sbt @@ -38,6 +38,8 @@ val testDependencies = Seq( libraryDependencies ++= { Seq( "org.scalaz" %% "scalaz-core" % scalazVersion, - "org.json4s" %% "json4s-native" % json4sVersion + "org.json4s" %% "json4s-native" % json4sVersion, + "org.json4s" %% "json4s-jackson" % json4sVersion, + "org.bouncycastle" % "bcprov-jdk15on" % "1.59" ) } ++ httpDependencies ++ testDependencies diff --git a/src/main/resources/genesis-block.json b/src/main/resources/genesis-block.json index 3682150..a65e28b 100644 --- a/src/main/resources/genesis-block.json +++ b/src/main/resources/genesis-block.json @@ -1,18 +1 @@ -{ - "header": { - "index": 0, - "previousHash": "0000000000000000000000000000000000000000000000000000000000000000", - "data": "Welcome to Blockchain in Scala!", - "merkleHash": "7814a9c43e9015462e5ffec1a3a9a69be024c1aacfa3ec4c879b5cd544761e7e", - "timestamp": 1523472721, - "nonce": 13860, - "hash": "00003607219f7a455e216f19ac3a34e3b158cf7282f7fdc624c93d593c2fc61f" - }, - "transactions": [ - { - "sender": "0000000000000000000000000000000000000000000000000000000000000000", - "receiver": "0000000000000000000000000000000000000000000000000000000000000000", - "amount": 50.0 - } - ] -} \ No newline at end of file +{"header":{"index":0,"previousHash":"0000000000000000000000000000000000000000000000000000000000000000","data":"Welcome to Blockchain in Scala!","merkleHash":"e580ae290899b8acc333fdb4e5ce52b6161c0df8f433a96cfcacbc4c211f1dc6","timestamp":1523472721,"difficulty":4,"nonce":289612},"transactions":[{"txIns":[{"previousOut":{"id":"","index":0},"signature":""}],"txOuts":[{"address":"3056301006072a8648ce3d020106052b8104000a034200049671ad288b396bdadf9d2d85640c6c61e14fa4a837b7b335bba21f226ba1525974c1a3f70fa1bc5a55c48ceced51468fe29bbbf67b22afa40383f99b98b841f9","amount":50}],"timestamp":1523472721,"id":"e580ae290899b8acc333fdb4e5ce52b6161c0df8f433a96cfcacbc4c211f1dc6"}],"hash":"0000d691591d3f999dce60c54719bc38b5bc2fb3ac39f76d9343b5ad4c01548b"} \ No newline at end of file diff --git a/src/main/resources/private-key b/src/main/resources/private-key new file mode 100644 index 0000000..95175cb --- /dev/null +++ b/src/main/resources/private-key @@ -0,0 +1 @@ +30818d020100301006072a8648ce3d020106052b8104000a04763074020101042005ab8a244673c5f9d86c64c28a0440368896a35daac9ec08a909de920c3dbc3aa00706052b8104000aa144034200049671ad288b396bdadf9d2d85640c6c61e14fa4a837b7b335bba21f226ba1525974c1a3f70fa1bc5a55c48ceced51468fe29bbbf67b22afa40383f99b98b841f9 \ No newline at end of file diff --git a/src/main/resources/public-key b/src/main/resources/public-key new file mode 100644 index 0000000..55c71c8 --- /dev/null +++ b/src/main/resources/public-key @@ -0,0 +1 @@ +3056301006072a8648ce3d020106052b8104000a034200049671ad288b396bdadf9d2d85640c6c61e14fa4a837b7b335bba21f226ba1525974c1a3f70fa1bc5a55c48ceced51468fe29bbbf67b22afa40383f99b98b841f9 \ No newline at end of file diff --git a/src/main/resources/signature b/src/main/resources/signature new file mode 100644 index 0000000..7c3c436 --- /dev/null +++ b/src/main/resources/signature @@ -0,0 +1 @@ +3045022100a15dff564d1e0fd4e475956d6fe1c8fc00fb86cb3c38729b47386011a8e9b38d02200c575841124500d4a1b250dec4fa6dfa48388cec54649b4f168767aae3062d65 \ No newline at end of file diff --git a/src/main/scala/com/fluency03/blockchain/Crypto.scala b/src/main/scala/com/fluency03/blockchain/Crypto.scala new file mode 100644 index 0000000..1e41186 --- /dev/null +++ b/src/main/scala/com/fluency03/blockchain/Crypto.scala @@ -0,0 +1,48 @@ +package com.fluency03.blockchain + +import java.security._ +import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec} + +import org.bouncycastle.jce.ECNamedCurveTable +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.jce.spec.ECParameterSpec + +object Crypto { + + Security.addProvider(new BouncyCastleProvider) + + val SPECP256K1 = "secp256k1" + val KEY_ALGORITHM = "ECDSA" + val KEY_PROVIDER = "BC" + + val ecSpec: ECParameterSpec = ECNamedCurveTable.getParameterSpec(SPECP256K1) + + def sign(data: Array[Byte], privateKey: Array[Byte]): Array[Byte] = { + val keySpec: PKCS8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey) + val keyFactory: KeyFactory = KeyFactory.getInstance(KEY_ALGORITHM) + val key: PrivateKey = keyFactory.generatePrivate(keySpec) + + val sig: Signature = Signature.getInstance(KEY_ALGORITHM, KEY_PROVIDER) + sig.initSign(key, new SecureRandom) + sig.update(data) + sig.sign() + } + + def verify(data: Array[Byte], publicKey: Array[Byte], signature: Array[Byte]): Boolean = { + val keySpec: X509EncodedKeySpec = new X509EncodedKeySpec(publicKey) + val keyFactory: KeyFactory = KeyFactory.getInstance(KEY_ALGORITHM) + val key: PublicKey = keyFactory.generatePublic(keySpec) + + val sig: Signature = Signature.getInstance(KEY_ALGORITHM, KEY_PROVIDER) + sig.initVerify(key) + sig.update(data) + sig.verify(signature) + } + + def generateKeyPair(): KeyPair = { + val gen: KeyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM, KEY_PROVIDER) + gen.initialize(ecSpec, new SecureRandom) + gen.generateKeyPair() + } + +} diff --git a/src/main/scala/com/fluency03/blockchain/Util.scala b/src/main/scala/com/fluency03/blockchain/Util.scala index d230992..c41d558 100644 --- a/src/main/scala/com/fluency03/blockchain/Util.scala +++ b/src/main/scala/com/fluency03/blockchain/Util.scala @@ -6,8 +6,6 @@ import java.util.Base64 object Util { - - /** * Generate SHA256 Hash from a input String. * https://gist.github.com/navicore/6234040bbfce3aa58f866db314c07c15 @@ -28,7 +26,12 @@ object Util { def getCurrentTimestamp: Long = Instant.now.getEpochSecond /** - * Calculate the hash of concatenation a List of Strings. + * + */ + def epochTimeOf(t: String): Long = Instant.parse(t).getEpochSecond + + /** + * Calculate the hash of concatenation a Seq of Strings. */ def hashOf(strings: String*): String = hashOf(strings mkString "") @@ -37,6 +40,11 @@ object Util { */ def hashOf(str: String): String = sha256HashOf(str) + /** + * Get binary representation of a hash. + */ + def binaryOfHash(hash: String): String = BigInt(hash, 16).toString(2) + /** * Check whether the given hash is with valid difficulty. */ @@ -45,12 +53,16 @@ object Util { /** * Encode a String to Base64. */ - def toBase64(text: String): String = Base64.getEncoder.encodeToString(text.getBytes()) + def base64Of(text: String): String = Base64.getEncoder.encodeToString(text.getBytes("UTF-8")) /** * Decode a Base64 to String. */ - def fromBase64(base64: String): String = new String(Base64.getDecoder.decode(base64), "ASCII") + def fromBase64(base64: String): String = new String(Base64.getDecoder.decode(base64), "UTF-8") + + + + } diff --git a/src/main/scala/com/fluency03/blockchain/api/actors/BlockActor.scala b/src/main/scala/com/fluency03/blockchain/api/actors/BlockActor.scala index b6e0ac1..9444502 100644 --- a/src/main/scala/com/fluency03/blockchain/api/actors/BlockActor.scala +++ b/src/main/scala/com/fluency03/blockchain/api/actors/BlockActor.scala @@ -30,6 +30,7 @@ class BlockActor extends Actor with ActorLogging { case CreateBlock(block) => onCreateBlock(block) case GetBlock(hash) => onGetBlock(hash) case DeleteBlock(hash) => onDeleteBlock(hash) + case _ => unhandled _ } private[this] def onGetBlocks(): Unit = sender() ! blocks.values.toList diff --git a/src/main/scala/com/fluency03/blockchain/api/actors/BlockchainActor.scala b/src/main/scala/com/fluency03/blockchain/api/actors/BlockchainActor.scala index 0ba8cd7..51b83cb 100644 --- a/src/main/scala/com/fluency03/blockchain/api/actors/BlockchainActor.scala +++ b/src/main/scala/com/fluency03/blockchain/api/actors/BlockchainActor.scala @@ -22,57 +22,29 @@ class BlockchainActor extends Actor with ActorLogging { val txActor: ActorSelection = context.actorSelection(PARENT_UP + TX_ACTOR_NAME) val blockActor: ActorSelection = context.actorSelection(PARENT_UP + BLOCK_ACTOR_NAME) + // TODO (Chang): not persistent var blockchainOpt: Option[Blockchain] = None def receive: Receive = { - /* ---------- Blockchain actions ---------- */ case GetBlockchain => onGetBlockchain() case CreateBlockchain => onCreateBlockchain() case DeleteBlockchain => onDeleteBlockchain() - /* ---------- Transaction actions ---------- */ - case GetTransactions => onGetTransactions() - case CreateTransaction(tx) => onCreateTransaction(tx) - case GetTransaction(hash) => onGetTransaction(hash) - case DeleteTransaction(hash) => onDeleteTransaction(hash) + case _ => unhandled _ } - /* ---------- Blockchain actions ---------- */ - private[this] def onGetBlockchain(): Unit = sender() ! blockchainOpt + private def onGetBlockchain(): Unit = sender() ! blockchainOpt - private[this] def onCreateBlockchain(): Unit = + private def onCreateBlockchain(): Unit = if (blockchainOpt.isDefined) sender() ! Response(s"Blockchain already exists.") else { blockchainOpt = Some(Blockchain()) sender() ! Response(s"Blockchain created, with difficulty ${blockchainOpt.get.difficulty}.") } - private[this] def onDeleteBlockchain(): Unit = + private def onDeleteBlockchain(): Unit = if (blockchainOpt.isDefined) { blockchainOpt = None sender() ! Response(s"Blockchain deleted.") } else sender() ! Response(s"Blockchain does not exist.") - /* ---------- Transaction actions ---------- */ - private[this] def onGetTransactions(): Unit = sender() ! { - if (blockchainOpt.isDefined) blockchainOpt.get.currentTransactions.toList - else List() - } - - private[this] def onCreateTransaction(tx: Transaction): Unit = - if (blockchainOpt.isDefined) { - blockchainOpt.get.currentTransactions += (tx.hash -> tx) - sender() ! Response(s"Transaction ${tx.hash} created.") - } else sender() ! Response(s"Blockchain does not exist.") - - private[this] def onGetTransaction(hash: String): Unit = sender() ! { - if (blockchainOpt.isDefined) blockchainOpt.get.currentTransactions(hash) - else None - } - - private[this] def onDeleteTransaction(hash: String): Unit = - if (blockchainOpt.isDefined) { - blockchainOpt.get.currentTransactions -= hash - sender() ! Response(s"Transaction $hash deleted.") - } else sender() ! Response(s"Blockchain does not exist.") - } diff --git a/src/main/scala/com/fluency03/blockchain/api/actors/NetworkActor.scala b/src/main/scala/com/fluency03/blockchain/api/actors/NetworkActor.scala new file mode 100644 index 0000000..9c2c9ad --- /dev/null +++ b/src/main/scala/com/fluency03/blockchain/api/actors/NetworkActor.scala @@ -0,0 +1,41 @@ +package com.fluency03.blockchain.api.actors + +import akka.actor.{Actor, ActorLogging, ActorRef, ActorSelection, Props} +import com.fluency03.blockchain.api.actors.NetworkActor._ +import com.fluency03.blockchain.api.utils.GenericMessage.Response +import com.fluency03.blockchain.api.{BLOCKCHAIN_ACTOR_NAME, BLOCK_ACTOR_NAME, PARENT_UP} + +import scala.collection.mutable + +object NetworkActor { + final case object GetPeers + final case class CreatePeer(id: String) + final case class GetPeer(id: String) + final case class DeletePeer(id: String) + + def props: Props = Props[NetworkActor] +} + +class NetworkActor extends Actor with ActorLogging { + override def preStart(): Unit = log.info("{} started!", this.getClass.getSimpleName) + override def postStop(): Unit = log.info("{} stopped!", this.getClass.getSimpleName) + + def receive: Receive = { + case GetPeers => context.children.map(_.path.name).toList + case CreatePeer(id) => + if (context.child(id).isDefined) sender() ! Response(s"Peer $id has been created.") + else { + val _ = context.actorOf(Props[PeerActor], name = id) + sender() ! Response(s"Peer $id created.") + } + case GetPeer(id) => + sender() ! context.child(id).isDefined + case DeletePeer(id) => + if (context.child(id).isDefined) { + context stop context.child(id).get + sender() ! Response(s"Peer $id deleted.") + } else sender() ! Response(s"Peer $id does not exist.") + case _ => unhandled _ + } + +} diff --git a/src/main/scala/com/fluency03/blockchain/api/actors/PeerActor.scala b/src/main/scala/com/fluency03/blockchain/api/actors/PeerActor.scala new file mode 100644 index 0000000..6edad51 --- /dev/null +++ b/src/main/scala/com/fluency03/blockchain/api/actors/PeerActor.scala @@ -0,0 +1,30 @@ +package com.fluency03.blockchain.api.actors + +import java.security.{KeyPair, PrivateKey, PublicKey} + +import akka.actor.{Actor, ActorLogging, Props} + +import scala.collection.mutable + +object PeerActor { + + + def props: Props = Props[PeerActor] +} + +class PeerActor extends Actor with ActorLogging { + override def preStart(): Unit = log.info("{} started!", this.getClass.getSimpleName) + override def postStop(): Unit = log.info("{} stopped!", this.getClass.getSimpleName) + + // TODO (Chang): not persistent + val wallets = mutable.Map.empty[String, KeyPair] + val publicKeys = mutable.Map.empty[String, PublicKey] + + def receive: Receive = { + case _ => ??? +// case _ => unhandled _ + } + + + +} diff --git a/src/main/scala/com/fluency03/blockchain/api/actors/TransactionActor.scala b/src/main/scala/com/fluency03/blockchain/api/actors/TransactionActor.scala index 030baab..8c1b8a0 100644 --- a/src/main/scala/com/fluency03/blockchain/api/actors/TransactionActor.scala +++ b/src/main/scala/com/fluency03/blockchain/api/actors/TransactionActor.scala @@ -1,8 +1,12 @@ package com.fluency03.blockchain.api.actors import akka.actor.{Actor, ActorLogging, ActorSelection, Props} +import com.fluency03.blockchain.api.actors.TransactionActor._ +import com.fluency03.blockchain.api.utils.GenericMessage.Response import com.fluency03.blockchain.api.{BLOCKCHAIN_ACTOR_NAME, BLOCK_ACTOR_NAME, PARENT_UP} -import com.fluency03.blockchain.core.Transaction +import com.fluency03.blockchain.core.{Outpoint, Transaction, TxOut} + +import scala.collection.mutable object TransactionActor { final case object GetTransactions @@ -17,20 +21,35 @@ class TransactionActor extends Actor with ActorLogging { override def preStart(): Unit = log.info("{} started!", this.getClass.getSimpleName) override def postStop(): Unit = log.info("{} stopped!", this.getClass.getSimpleName) + val currentTransactions: mutable.Map[String, Transaction] = mutable.Map.empty[String, Transaction] + val unspentTxOuts: mutable.Map[Outpoint, TxOut] = mutable.Map.empty[Outpoint, TxOut] + + // TODO (Chang): not persistent val blockchainActor: ActorSelection = context.actorSelection(PARENT_UP + BLOCKCHAIN_ACTOR_NAME) val blockActor: ActorSelection = context.actorSelection(PARENT_UP + BLOCK_ACTOR_NAME) def receive: Receive = { - case _ => blockchainActor forward _ + case GetTransactions => onGetTransactions() + case CreateTransaction(tx) => onCreateTransaction(tx) + case GetTransaction(hash) => onGetTransaction(hash) + case DeleteTransaction(hash) => onDeleteTransaction(hash) + case _ => unhandled _ + } + + private def onGetTransactions(): Unit = sender() ! currentTransactions.values.toList + + private def onCreateTransaction(tx: Transaction): Unit ={ + currentTransactions += (tx.id -> tx) + sender() ! Response(s"Transaction ${tx.id} created.") } -// -// { -// case msg @ GetTransactions => blockchainActor forward msg -// case msg: CreateTransaction => blockchainActor forward msg -// case msg: GetTransaction => blockchainActor forward msg -// case msg: DeleteTransaction => blockchainActor forward msg -// } + private def onGetTransaction(hash: String): Unit = sender() ! currentTransactions.get(hash) + + private def onDeleteTransaction(hash: String): Unit = + if (currentTransactions contains hash) { + currentTransactions -= hash + sender() ! Response(s"Transaction $hash deleted.") + } else sender() ! Response(s"Blockchain does not exist.") } diff --git a/src/main/scala/com/fluency03/blockchain/api/package.scala b/src/main/scala/com/fluency03/blockchain/api/package.scala index 46eeb35..ee6ad63 100644 --- a/src/main/scala/com/fluency03/blockchain/api/package.scala +++ b/src/main/scala/com/fluency03/blockchain/api/package.scala @@ -10,6 +10,7 @@ package object api { lazy val BLOCK_ACTOR_NAME = "blockActor" lazy val BLOCKCHAIN_ACTOR_NAME = "blockchainActor" lazy val TX_ACTOR_NAME = "txActor" + lazy val PEER_ACTOR_NAME = "peerActor" lazy val PARENT_UP = "../" diff --git a/src/main/scala/com/fluency03/blockchain/api/routes/BlockRoutes.scala b/src/main/scala/com/fluency03/blockchain/api/routes/BlockRoutes.scala index 7f7685a..4ca8406 100644 --- a/src/main/scala/com/fluency03/blockchain/api/routes/BlockRoutes.scala +++ b/src/main/scala/com/fluency03/blockchain/api/routes/BlockRoutes.scala @@ -22,12 +22,14 @@ trait BlockRoutes extends Routes { def blockActor: ActorRef lazy val blockRoutes: Route = - pathPrefix("blocks") { + path("blocks") { + get { + val blocks: Future[Blocks] = (blockActor ? GetBlocks).mapTo[Blocks] + complete(blocks) + } + } ~ + pathPrefix("block") { pathEnd { - get { - val blocks: Future[Blocks] = (blockActor ? GetBlocks).mapTo[Blocks] - complete(blocks) - } ~ post { entity(as[Block]) { block => val blockCreated: Future[Response] = (blockActor ? CreateBlock(block)).mapTo[Response] diff --git a/src/main/scala/com/fluency03/blockchain/api/routes/GenericRoutes.scala b/src/main/scala/com/fluency03/blockchain/api/routes/GenericRoutes.scala index bc6ec5a..ad8b2df 100644 --- a/src/main/scala/com/fluency03/blockchain/api/routes/GenericRoutes.scala +++ b/src/main/scala/com/fluency03/blockchain/api/routes/GenericRoutes.scala @@ -1,5 +1,7 @@ package com.fluency03.blockchain.api.routes +import java.time.Instant + import akka.event.Logging import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server.Directives._ @@ -7,8 +9,7 @@ import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.directives.MethodDirectives.post import akka.http.scaladsl.server.directives.RouteDirectives.complete import com.fluency03.blockchain.Util._ - -case class Input(data: String) +import com.fluency03.blockchain.api.utils.GenericMessage.Input trait GenericRoutes extends Routes { lazy val log = Logging(system, classOf[GenericRoutes]) @@ -25,16 +26,26 @@ trait GenericRoutes extends Routes { entity(as[Input]) { in => complete((StatusCodes.Created, hashOf(in.data))) } } } ~ - path("base64-of-string") { - post { - entity(as[Input]) { in => complete((StatusCodes.Created, toBase64(in.data))) } - } - } ~ - path("string-of-base64") { - post { - entity(as[Input]) { in => complete((StatusCodes.Created, fromBase64(in.data))) } - } + path("base64-of-string") { + post { + entity(as[Input]) { in => complete((StatusCodes.Created, base64Of(in.data))) } + } + } ~ + path("string-of-base64") { + post { + entity(as[Input]) { in => complete((StatusCodes.Created, fromBase64(in.data))) } + } + } ~ + path("epoch-time") { + post { + entity(as[Input]) { in => complete((StatusCodes.Created, epochTimeOf(in.data))) } + } + } ~ + path("time-from-epoch") { + post { + entity(as[Input]) { in => complete((StatusCodes.Created, Instant.ofEpochSecond(in.data.toLong))) } } + } } } diff --git a/src/main/scala/com/fluency03/blockchain/api/routes/TransactionRoutes.scala b/src/main/scala/com/fluency03/blockchain/api/routes/TransactionRoutes.scala index 92c5c9d..c95baf5 100644 --- a/src/main/scala/com/fluency03/blockchain/api/routes/TransactionRoutes.scala +++ b/src/main/scala/com/fluency03/blockchain/api/routes/TransactionRoutes.scala @@ -22,31 +22,33 @@ trait TransactionRoutes extends Routes { def txActor: ActorRef lazy val txRoutes: Route = - pathPrefix("transactions") { + path("transactions") { + get { + val transactions: Future[Transactions] = (txActor ? GetTransactions).mapTo[Transactions] + complete(transactions) + } + } ~ + pathPrefix("transaction") { pathEnd { - get { - val transactions: Future[Transactions] = (txActor ? GetTransactions).mapTo[Transactions] - complete(transactions) - } ~ post { entity(as[Transaction]) { tx => val txCreated: Future[Response] = (txActor ? CreateTransaction(tx)).mapTo[Response] onSuccess(txCreated) { resp => - log.info("Created transaction [{}]: {}", tx.hash, resp.message) + log.info("Created transaction [{}]: {}", tx.id, resp.message) complete((StatusCodes.Created, resp)) } } } } ~ - path(Segment) { hash => + path(Segment) { id => get { - val maybeTx: Future[Option[Transaction]] = (txActor ? GetTransaction(hash)).mapTo[Option[Transaction]] + val maybeTx: Future[Option[Transaction]] = (txActor ? GetTransaction(id)).mapTo[Option[Transaction]] rejectEmptyResponse { complete(maybeTx) } } ~ delete { - val txDeleted: Future[Response] = (txActor ? DeleteTransaction(hash)).mapTo[Response] + val txDeleted: Future[Response] = (txActor ? DeleteTransaction(id)).mapTo[Response] onSuccess(txDeleted) { resp => - log.info("Deleted transaction [{}]: {}", hash, resp.message) + log.info("Deleted transaction [{}]: {}", id, resp.message) complete((StatusCodes.OK, resp)) } } diff --git a/src/main/scala/com/fluency03/blockchain/api/utils/GenericMessage.scala b/src/main/scala/com/fluency03/blockchain/api/utils/GenericMessage.scala index e0cfcf8..ee8083b 100644 --- a/src/main/scala/com/fluency03/blockchain/api/utils/GenericMessage.scala +++ b/src/main/scala/com/fluency03/blockchain/api/utils/GenericMessage.scala @@ -2,6 +2,8 @@ package com.fluency03.blockchain.api.utils object GenericMessage { + final case class Input(data: String) + final case class Response(message: String) } diff --git a/src/main/scala/com/fluency03/blockchain/core/Block.scala b/src/main/scala/com/fluency03/blockchain/core/Block.scala index 8f3fcaf..0490c38 100644 --- a/src/main/scala/com/fluency03/blockchain/core/Block.scala +++ b/src/main/scala/com/fluency03/blockchain/core/Block.scala @@ -1,23 +1,25 @@ package com.fluency03.blockchain.core -import java.time.Instant - import com.fluency03.blockchain.Util.isWithValidDifficulty import com.fluency03.blockchain.core.BlockHeader.hashOfHeaderFields -import org.json4s.{Extraction, JValue} +import com.fluency03.blockchain.core.Transaction.createCoinbaseTx +import org.json4s.JsonAST.JObject +import org.json4s.JsonDSL._ import org.json4s.native.JsonMethods.{compact, render} +import org.json4s.{Extraction, JValue} /** * Block on the chain. * @param header Header of current Block - * @param transactions List of Transactions included in current Block + * @param transactions Seq of Transactions included in current Block */ -case class Block(header: BlockHeader, transactions: List[Transaction] = List()) { +case class Block(header: BlockHeader, transactions: Seq[Transaction] = Seq()) { lazy val index: Int = header.index lazy val previousHash: String = header.previousHash lazy val data: String = header.data lazy val merkleHash: String = header.merkleHash lazy val timestamp: Long = header.timestamp + lazy val difficulty: Int = header.difficulty lazy val nonce: Int = header.nonce lazy val hash: String = header.hash @@ -25,79 +27,90 @@ case class Block(header: BlockHeader, transactions: List[Transaction] = List()) def nextTrial(): Block = Block(header.nextTrial(), transactions) def addTransaction(t: Transaction): Block = - Block(index, previousHash, data, t :: transactions, timestamp, nonce) - - def addTransaction(sender: String, receiver: String, amount: Double): Block = - addTransaction(Transaction(sender, receiver, amount)) + Block(index, previousHash, data, timestamp, difficulty, nonce, t +: transactions) - def addTransaction(sender: String, receiver: String, amount: Double, timestamp: Long): Block = - addTransaction(Transaction(sender, receiver, amount, timestamp)) + def addTransactions(trans: Seq[Transaction]): Block = + Block(index, previousHash, data, timestamp, difficulty, nonce, trans ++ transactions) - def addTransactions(trans: List[Transaction]): Block = - Block(index, previousHash, data, trans ++ transactions, timestamp, nonce) + def isValid: Boolean = isWithValidDifficulty(hash, difficulty) && hasValidMerkleHash - def isValid(difficulty: Int): Boolean = hasValidHash(difficulty) && hasValidMerkleHash + def hasValidMerkleHash: Boolean = merkleHash == Merkle.computeRoot(transactions) - def hasValidHash(difficulty: Int): Boolean = header.isValidWith(difficulty) - - def hasValidMerkleHash: Boolean = merkleHash == MerkleNode.computeRoot(transactions) - - def toJson: JValue = Extraction.decompose(this) + def toJson: JValue = ("header" -> header.toJson) ~ ("transactions" -> transactions.map(_.toJson)) ~ ("hash" -> hash) override def toString: String = compact(render(toJson)) - } object Block { - def apply(index: Int, previousHash: String, data: String, merkleHash: String, timestamp: Long, nonce: Int): Block = - Block(BlockHeader(index, previousHash, data, merkleHash, timestamp, nonce)) - - def apply(index: Int, previousHash: String, data: String, transactions: List[Transaction], timestamp: Long, nonce: Int): Block ={ - val merkleHash = MerkleNode.computeRoot(transactions) - Block(BlockHeader(index, previousHash, data, merkleHash, timestamp, nonce), transactions) - } - - def apply(index: Int, previousHash: String, data: String, merkleHash: String, transactions: List[Transaction], timestamp: Long, nonce: Int): Block = - Block(BlockHeader(index, previousHash, data, merkleHash, timestamp, nonce), transactions) + def apply( + index: Int, + previousHash: String, + data: String, + merkleHash: String, + timestamp: Long, + difficulty: Int, + nonce: Int): Block = + Block(BlockHeader(index, previousHash, data, merkleHash, timestamp, difficulty, nonce)) + + def apply( + index: Int, + previousHash: String, + data: String, + timestamp: Long, + difficulty: Int, + nonce: Int, + transactions: Seq[Transaction]): Block = + Block(BlockHeader(index, previousHash, data, Merkle.computeRoot(transactions), timestamp, difficulty, nonce), + transactions) + + def apply( + index: Int, + previousHash: String, + data: String, + merkleHash: String, + timestamp: Long, + difficulty: Int, + nonce: Int, + transactions: Seq[Transaction]): Block = + Block(BlockHeader(index, previousHash, data, merkleHash, timestamp, difficulty, nonce), transactions) lazy val genesisBlock: Block = genesis() def genesis(difficulty: Int = 4): Block = - mineNextBlock( - 0, - ZERO64, - "Welcome to Blockchain in Scala!", - List(Transaction(ZERO64, ZERO64, 50, genesisTimestamp)), - genesisTimestamp, - difficulty) + mineNextBlock(0, ZERO64, "Welcome to Blockchain in Scala!", genesisTimestamp, difficulty, + Seq(createCoinbaseTx(0, genesisMiner, genesisTimestamp))) def mineNextBlock( nextIndex: Int, prevHash: String, newBlockData: String, - transactions: List[Transaction], timestamp: Long, - difficulty: Int): Block = { + difficulty: Int, + transactions: Seq[Transaction]): Block = { var nonce = 0 var nextHash = "" - val merkleHash = MerkleNode.computeRoot(transactions) + val merkleHash = Merkle.computeRoot(transactions) while (!isWithValidDifficulty(nextHash, difficulty)) { nonce += 1 - nextHash = hashOfHeaderFields(nextIndex, prevHash, newBlockData, merkleHash, timestamp, nonce) + nextHash = hashOfHeaderFields(nextIndex, prevHash, newBlockData, merkleHash, timestamp, difficulty, nonce) } - Block(nextIndex, prevHash, newBlockData, merkleHash, transactions, timestamp, nonce) + Block(nextIndex, prevHash, newBlockData, merkleHash, timestamp, difficulty, nonce, transactions) } def mineNextBlock( currentBlock: Block, newBlockData: String, - transactions: List[Transaction], timestamp: Long, - difficulty: Int): Block = - mineNextBlock(currentBlock.index + 1, currentBlock.hash, newBlockData, transactions, timestamp, difficulty) + difficulty: Int, + transactions: Seq[Transaction]): Block = + mineNextBlock(currentBlock.index + 1, currentBlock.hash, newBlockData, timestamp, difficulty, transactions) + + def canBeChained(newBlock: Block, previousBlock: Block): Boolean = + previousBlock.index + 1 == newBlock.index && previousBlock.hash == newBlock.previousHash + } diff --git a/src/main/scala/com/fluency03/blockchain/core/BlockHeader.scala b/src/main/scala/com/fluency03/blockchain/core/BlockHeader.scala index c0999fa..a32049d 100644 --- a/src/main/scala/com/fluency03/blockchain/core/BlockHeader.scala +++ b/src/main/scala/com/fluency03/blockchain/core/BlockHeader.scala @@ -1,6 +1,7 @@ package com.fluency03.blockchain.core -import com.fluency03.blockchain.Util.{hashOf, isWithValidDifficulty} +import com.fluency03.blockchain.core.BlockHeader.hashOfBlockHeader +import com.fluency03.blockchain.Util.hashOf import org.json4s.native.JsonMethods.{compact, render} import org.json4s.{Extraction, JValue} @@ -12,8 +13,8 @@ import org.json4s.{Extraction, JValue} * @param data Data attached to current Block * @param merkleHash Merkle root hash of current Block * @param timestamp Timestamp of current Block + * @param difficulty Difficulty for current Block * @param nonce Nonce of current Block - * @param hash Hash of current Block */ case class BlockHeader( index: Int, @@ -21,12 +22,12 @@ case class BlockHeader( data: String, merkleHash: String, timestamp: Long, - nonce: Int, - hash: String) { + difficulty: Int, + nonce: Int) { - def isValidWith(difficulty: Int): Boolean = isWithValidDifficulty(hash, difficulty) + lazy val hash: String = hashOfBlockHeader(this) - def nextTrial(): BlockHeader = BlockHeader(index, previousHash, data, merkleHash, timestamp, nonce + 1) + def nextTrial(): BlockHeader = BlockHeader(index, previousHash, data, merkleHash, timestamp, difficulty, nonce + 1) def toJson: JValue = Extraction.decompose(this) @@ -36,16 +37,6 @@ case class BlockHeader( object BlockHeader { - def apply( - index: Int, - previousHash: String, - data: String, - merkleHash: String, - timestamp: Long, - nonce: Int): BlockHeader = - BlockHeader(index, previousHash, data, merkleHash, timestamp, nonce, - hashOfHeaderFields(index, previousHash, data, merkleHash, timestamp, nonce)) - def hashOfBlockHeader(header: BlockHeader): String = hashOfHeaderFields( header.index, @@ -53,15 +44,17 @@ object BlockHeader { header.data, header.merkleHash, header.timestamp, + header.difficulty, header.nonce) def hashOfHeaderFields( - index: Long, + index: Int, previousHash: String, data: String, merkleHash: String, timestamp: Long, + difficulty: Int, nonce: Int): String = - hashOf(index.toString, previousHash, data, merkleHash, timestamp.toString, nonce.toString) + hashOf(index.toString, previousHash, data, merkleHash, timestamp.toString, difficulty.toString, nonce.toString) } diff --git a/src/main/scala/com/fluency03/blockchain/core/Blockchain.scala b/src/main/scala/com/fluency03/blockchain/core/Blockchain.scala index 43deb2b..74dd25e 100644 --- a/src/main/scala/com/fluency03/blockchain/core/Blockchain.scala +++ b/src/main/scala/com/fluency03/blockchain/core/Blockchain.scala @@ -2,6 +2,7 @@ package com.fluency03.blockchain.core import com.fluency03.blockchain.Util.getCurrentTimestamp import com.fluency03.blockchain.core.Blockchain._ +import com.fluency03.blockchain.core.Block.canBeChained import scala.collection.mutable @@ -10,64 +11,68 @@ import scala.collection.mutable * @param difficulty Difficulty of a Blockchain * @param chain Chain of Blocks */ -case class Blockchain(difficulty: Int = 4, chain: List[Block] = List(Block.genesisBlock)) { - val currentTransactions: mutable.Map[String, Transaction] = mutable.Map.empty[String, Transaction] +case class Blockchain(difficulty: Int = 4, chain: Seq[Block] = Seq(Block.genesisBlock)) { - def addBlock(newBlockData: String): Blockchain = { - Blockchain(difficulty, mineNextBlock(newBlockData).addTransactions(currentTransactions.values.toList) :: chain) + def addBlock(newBlockData: String, transactions: Seq[Transaction]): Blockchain = { + Blockchain(difficulty, mineNextBlock(newBlockData, transactions) +: chain) } def addBlock(newBlock: Block): Blockchain = { - Blockchain(difficulty, newBlock :: chain) - } - - def addTransaction(tx: Transaction): Blockchain = { - currentTransactions += (tx.hash -> tx) - this - } - - def addTransaction(sender: String, receiver: String, amount: Double): Blockchain = - addTransaction(Transaction(sender, receiver, amount)) - - def addTransaction(sender: String, receiver: String, amount: Double, timestamp: Long): Blockchain = - addTransaction(Transaction(sender, receiver, amount, timestamp)) - - def addTransactions(trans: List[Transaction]): Blockchain = { - currentTransactions ++= trans.map(tx => (tx.hash, tx)) - this + Blockchain(difficulty, newBlock +: chain) } def lastBlock(): Option[Block] = chain.headOption - def mineNextBlock(newBlockData: String): Block = { + def mineNextBlock(newBlockData: String, transactions: Seq[Transaction]): Block = { val lastBlockOpt: Option[Block] = this.lastBlock() if (lastBlockOpt.isEmpty) throw new NoSuchElementException("Last Block does not exist!") val lastHeader = lastBlockOpt.get.header - Block.mineNextBlock( - lastHeader.index + 1, - lastHeader.hash, - newBlockData, - currentTransactions.values.toList, - getCurrentTimestamp, - difficulty) + Block.mineNextBlock(lastHeader.index + 1, lastHeader.hash, newBlockData, getCurrentTimestamp, difficulty, + transactions) } def isValid: Boolean = chain match { case Nil => throw new NoSuchElementException("Blockchain is Empty!") - case _ => isValidChain(chain, difficulty) + case _ => isValidChain(chain) } + } object Blockchain { - def apply(difficulty: Int): Blockchain = new Blockchain(difficulty, List(Block.genesis(difficulty))) + def apply(difficulty: Int): Blockchain = new Blockchain(difficulty, Seq(Block.genesis(difficulty))) - def isValidChain(chain: List[Block], difficulty: Int): Boolean = chain match { + def isValidChain(chain: Seq[Block]): Boolean = chain match { case Nil => true - case g :: Nil => g.previousHash == ZERO64 && g.isValid(difficulty) - case a :: b :: tail => a.previousHash == b.hash && a.isValid(difficulty) && isValidChain(b :: tail, difficulty) + case g +: Nil => g.previousHash == ZERO64 && g.index == 0 && g.isValid + case a +: b +: tail => canBeChained(a, b) && a.isValid && isValidChain(b +: tail) } + def updateUTxOs( + transactions: Seq[Transaction], + unspentTxOuts: Map[Outpoint, TxOut] + ): Map[Outpoint, TxOut] = { + val newUnspentTxOuts = getNewUTxOs(transactions) + val consumedTxOuts = getConsumedUTxOs(transactions) + unspentTxOuts.filterKeys(consumedTxOuts contains) ++ newUnspentTxOuts + } + + def getNewUTxOs(transactions: Seq[Transaction]): Map[Outpoint, TxOut] = + transactions + .map(t => t.txOuts.zipWithIndex.map { case (txOut, index) => Outpoint(t.id, index) -> txOut}.toMap) + .reduce { _ ++ _ } + + def getConsumedUTxOs(transactions: Seq[Transaction]): Map[Outpoint, TxOut] = + transactions.map(_.txIns) + .reduce { _ ++ _ } + .map(txIn => Outpoint(txIn.previousOut.id, txIn.previousOut.index) -> TxOut("", 0)) + .toMap + + + + + + } diff --git a/src/main/scala/com/fluency03/blockchain/core/Difficulty.scala b/src/main/scala/com/fluency03/blockchain/core/Difficulty.scala index 1abe0ed..0fd5775 100644 --- a/src/main/scala/com/fluency03/blockchain/core/Difficulty.scala +++ b/src/main/scala/com/fluency03/blockchain/core/Difficulty.scala @@ -2,9 +2,13 @@ package com.fluency03.blockchain.core object Difficulty { + // TODO (Chang): this object is not used for now + // TODO (Chang): implement difficulty adjustment + lazy val difficultyOneTarget: BigInt = targetOfBits("1d00ffff".hex) /** + * See the cpp source code here: * https://github.com/bitcoin/bitcoin/blob/master/src/arith_uint256.cpp#L206 */ def decodeCompact(nCompact: Long): (BigInt, Boolean, Boolean) = { diff --git a/src/main/scala/com/fluency03/blockchain/core/MerkleNode.scala b/src/main/scala/com/fluency03/blockchain/core/Merkle.scala similarity index 62% rename from src/main/scala/com/fluency03/blockchain/core/MerkleNode.scala rename to src/main/scala/com/fluency03/blockchain/core/Merkle.scala index f71649d..6cd9fdd 100644 --- a/src/main/scala/com/fluency03/blockchain/core/MerkleNode.scala +++ b/src/main/scala/com/fluency03/blockchain/core/Merkle.scala @@ -2,11 +2,11 @@ package com.fluency03.blockchain.core import com.fluency03.blockchain.Util.hashOf -object MerkleNode { - def computeRoot(trans: List[Transaction]): String = - computeRootOfHashes(trans.map(_.hash)) +object Merkle { + def computeRoot(trans: Seq[Transaction]): String = + computeRootOfHashes(trans.map(_.id)) - def computeRootOfHashes(hashes: List[String]): String = hashes.length match { + def computeRootOfHashes(hashes: Seq[String]): String = hashes.length match { case 0 => ZERO64 case 1 => hashes.head case n if n % 2 != 0 => computeRootOfHashes(hashes :+ hashes.last) // append last element again diff --git a/src/main/scala/com/fluency03/blockchain/core/Transaction.scala b/src/main/scala/com/fluency03/blockchain/core/Transaction.scala index 204d066..2242813 100644 --- a/src/main/scala/com/fluency03/blockchain/core/Transaction.scala +++ b/src/main/scala/com/fluency03/blockchain/core/Transaction.scala @@ -1,34 +1,90 @@ package com.fluency03.blockchain.core -import com.fluency03.blockchain.Util.{hashOf, getCurrentTimestamp} +import java.security.KeyPair + +import com.fluency03.blockchain.Crypto +import com.fluency03.blockchain.Util.hashOf import com.fluency03.blockchain.core.Transaction.hashOfTransaction +import org.json4s.JsonAST.JObject +import org.json4s.JsonDSL._ import org.json4s.native.JsonMethods.{compact, render} import org.json4s.{Extraction, JValue} -/** - * Transaction - * @param sender Sender of the current Transaction - * @param receiver Receiver of the current Transaction - * @param amount Amount of the current Transaction - * @param timestamp Unix epoch time of the current Transaction - */ -case class Transaction(sender: String, receiver: String, amount: Double, timestamp: Long) { - lazy val hash: String = hashOfTransaction(this) +import scala.collection.mutable + +case class Outpoint(id: String, index: Int) +case class TxIn(previousOut: Outpoint, signature: String) +case class TxOut(address: String, amount: Long) + +case class Transaction(txIns: Seq[TxIn], txOuts: Seq[TxOut], timestamp: Long) { + lazy val id: String = hashOfTransaction(this) + + def addTxIn(in: TxIn): Transaction = Transaction(in +: txIns, txOuts, timestamp) + + def addTxIns(ins: Seq[TxIn]): Transaction = Transaction(ins ++ txIns, txOuts, timestamp) + + def addTxOut(out: TxOut): Transaction = Transaction(txIns, out +: txOuts, timestamp) + + def addTxOuts(outs: Seq[TxOut]): Transaction = Transaction(txIns, outs ++ txOuts, timestamp) + + def removeTxIn(txIn: TxIn): Transaction = Transaction(txIns.filter(_ != txIn), txOuts, timestamp) + + def removeTxOut(txOut: TxOut): Transaction = Transaction(txIns, txOuts.filter(_ != txOut), timestamp) - def toJson: JValue = Extraction.decompose(this) + def toJson: JValue = Extraction.decompose(this).asInstanceOf[JObject] ~ ("id" -> id) override def toString: String = compact(render(toJson)) } object Transaction { - def apply(sender: String, receiver: String, amount: Double): Transaction = - Transaction(sender, receiver, amount, getCurrentTimestamp) + lazy val COINBASE_AMOUNT: Int = 50 + + def createCoinbase(blockIndex: Int): TxIn = TxIn(Outpoint("", blockIndex), "") + + def createCoinbaseTx(blockIndex: Int, miner: String, timestamp: Long): Transaction = { + val txIn = createCoinbase(blockIndex) + val txOut = TxOut(miner, COINBASE_AMOUNT) + Transaction(Seq(txIn), Seq(txOut), timestamp) + } def hashOfTransaction(tx: Transaction): String = - hashOfTransactionFields(tx.sender, tx.receiver, tx.amount, tx.timestamp) + hashOf(tx.txIns.map(tx => tx.previousOut.id + tx.previousOut.index).mkString, + tx.txOuts.map(tx => tx.address + tx.amount).mkString, tx.timestamp.toString) + + def signTxIn( + transactionHash: Array[Byte], + txIn: TxIn, + keyPair: KeyPair, + unspentTxOuts: mutable.Map[Outpoint, TxOut] + ): Option[TxIn] = unspentTxOuts.get(txIn.previousOut) match { + case Some(uTxO) => + if (keyPair.getPublic.getEncoded.mkString != uTxO.address) None + else Some(TxIn(txIn.previousOut, Crypto.sign(transactionHash, keyPair.getPrivate.getEncoded).mkString)) + case None => None + } + + def validateTxIn(txIn: TxIn, transaction: Transaction, unspentTxOuts: mutable.Map[Outpoint, TxOut]): Boolean = + unspentTxOuts.get(txIn.previousOut) match { + case Some(txOut) => + Crypto.verify(transaction.id.getBytes(defaultCharset), + txOut.address.getBytes(defaultCharset), + txIn.signature.getBytes(defaultCharset)) + case None => false + } + + def validateTxOutValues(transaction: Transaction, unspentTxOuts: mutable.Map[Outpoint, TxOut]): Boolean = { + val totalTxInValues: Long = transaction.txIns + .map(txIn => unspentTxOuts.get(txIn.previousOut) match { + case Some(txOut) => txOut.amount + case None => 0 + }).sum + + val totalTxOutValues: Long = transaction.txOuts.map( _.amount).sum + + totalTxInValues == totalTxOutValues + } + - def hashOfTransactionFields(sender: String, receiver: String, amount: Double, timestamp: Long): String = - hashOf(sender, receiver, amount.toString, timestamp.toString) } diff --git a/src/main/scala/com/fluency03/blockchain/core/package.scala b/src/main/scala/com/fluency03/blockchain/core/package.scala index e8ace5e..bb8051b 100644 --- a/src/main/scala/com/fluency03/blockchain/core/package.scala +++ b/src/main/scala/com/fluency03/blockchain/core/package.scala @@ -1,17 +1,26 @@ package com.fluency03.blockchain +import java.nio.charset.Charset import java.time.Instant import org.json4s.NoTypeHints import org.json4s.native.Serialization +import scala.io.Source + package object core { implicit val formats = Serialization.formats(NoTypeHints) - lazy val ZERO64: String = "0000000000000000000000000000000000000000000000000000000000000000" + implicit val defaultCharset: Charset = Charset.forName("UTF-8") + + val ZERO64: String = "0000000000000000000000000000000000000000000000000000000000000000" lazy val genesisTimestamp: Long = Instant.parse("2018-04-11T18:52:01Z").getEpochSecond + val genesisMiner: String = Source.fromResource("public-key").getLines.mkString + + val SLOGAN: String = "Welcome to Blockchain in Scala!" + class HexString(val s: String) { def hex: Long = java.lang.Long.parseLong(s, 16) } diff --git a/src/test/resources/genesis-block.json b/src/test/resources/genesis-block.json index d7c8c83..a65e28b 100644 --- a/src/test/resources/genesis-block.json +++ b/src/test/resources/genesis-block.json @@ -1 +1 @@ -{"header":{"index":0,"previousHash":"0000000000000000000000000000000000000000000000000000000000000000","data":"Welcome to Blockchain in Scala!","merkleHash":"21d1db6630265eb9e991b410d82870a8d3f62cb11d1d0917e926a49bdb3993b5","timestamp":1523472721,"nonce":33660,"hash":"0000a26af9a70022a6c6d270a0ced7478eb40bcfc4301b5e73c0ed3207a3de0e"},"transactions":[{"sender":"0000000000000000000000000000000000000000000000000000000000000000","receiver":"0000000000000000000000000000000000000000000000000000000000000000","amount":50.0,"timestamp":1523472721}]} +{"header":{"index":0,"previousHash":"0000000000000000000000000000000000000000000000000000000000000000","data":"Welcome to Blockchain in Scala!","merkleHash":"e580ae290899b8acc333fdb4e5ce52b6161c0df8f433a96cfcacbc4c211f1dc6","timestamp":1523472721,"difficulty":4,"nonce":289612},"transactions":[{"txIns":[{"previousOut":{"id":"","index":0},"signature":""}],"txOuts":[{"address":"3056301006072a8648ce3d020106052b8104000a034200049671ad288b396bdadf9d2d85640c6c61e14fa4a837b7b335bba21f226ba1525974c1a3f70fa1bc5a55c48ceced51468fe29bbbf67b22afa40383f99b98b841f9","amount":50}],"timestamp":1523472721,"id":"e580ae290899b8acc333fdb4e5ce52b6161c0df8f433a96cfcacbc4c211f1dc6"}],"hash":"0000d691591d3f999dce60c54719bc38b5bc2fb3ac39f76d9343b5ad4c01548b"} \ No newline at end of file diff --git a/src/test/resources/private-key b/src/test/resources/private-key new file mode 100644 index 0000000..95175cb --- /dev/null +++ b/src/test/resources/private-key @@ -0,0 +1 @@ +30818d020100301006072a8648ce3d020106052b8104000a04763074020101042005ab8a244673c5f9d86c64c28a0440368896a35daac9ec08a909de920c3dbc3aa00706052b8104000aa144034200049671ad288b396bdadf9d2d85640c6c61e14fa4a837b7b335bba21f226ba1525974c1a3f70fa1bc5a55c48ceced51468fe29bbbf67b22afa40383f99b98b841f9 \ No newline at end of file diff --git a/src/test/resources/public-key b/src/test/resources/public-key new file mode 100644 index 0000000..55c71c8 --- /dev/null +++ b/src/test/resources/public-key @@ -0,0 +1 @@ +3056301006072a8648ce3d020106052b8104000a034200049671ad288b396bdadf9d2d85640c6c61e14fa4a837b7b335bba21f226ba1525974c1a3f70fa1bc5a55c48ceced51468fe29bbbf67b22afa40383f99b98b841f9 \ No newline at end of file diff --git a/src/test/resources/signature b/src/test/resources/signature new file mode 100644 index 0000000..7c3c436 --- /dev/null +++ b/src/test/resources/signature @@ -0,0 +1 @@ +3045022100a15dff564d1e0fd4e475956d6fe1c8fc00fb86cb3c38729b47386011a8e9b38d02200c575841124500d4a1b250dec4fa6dfa48388cec54649b4f168767aae3062d65 \ No newline at end of file diff --git a/src/test/scala/com/fluency03/blockchain/CryptoTest.scala b/src/test/scala/com/fluency03/blockchain/CryptoTest.scala new file mode 100644 index 0000000..9b95713 --- /dev/null +++ b/src/test/scala/com/fluency03/blockchain/CryptoTest.scala @@ -0,0 +1,18 @@ +package com.fluency03.blockchain + +import java.security.KeyPair + +import org.bouncycastle.util.encoders.Hex +import org.scalatest.{FlatSpec, Matchers} + +class CryptoTest extends FlatSpec with Matchers { + + "Crypto" should "be able to sign a data and verify the signature. " in { + val pair: KeyPair = Crypto.generateKeyPair() + val data = "Welcome to Blockchain in Scala!".toCharArray.map(_.toByte) + val signature = Crypto.sign(data, pair.getPrivate.getEncoded) + Crypto.verify(data, pair.getPublic.getEncoded, signature) shouldEqual true + } + + +} diff --git a/src/test/scala/com/fluency03/blockchain/UtilTest.scala b/src/test/scala/com/fluency03/blockchain/UtilTest.scala index bb00545..8a01714 100644 --- a/src/test/scala/com/fluency03/blockchain/UtilTest.scala +++ b/src/test/scala/com/fluency03/blockchain/UtilTest.scala @@ -34,9 +34,9 @@ class UtilTest extends FlatSpec with Matchers with MockFactory { } "A String" should "be converted to Base64 and converted back." in { - toBase64("open sesame") shouldEqual "b3BlbiBzZXNhbWU=" + base64Of("open sesame") shouldEqual "b3BlbiBzZXNhbWU=" fromBase64("b3BlbiBzZXNhbWU=") shouldEqual "open sesame" - fromBase64(toBase64("aeqfedq.'.[pl12l3[p,5`>}{::>{:")) shouldEqual "aeqfedq.'.[pl12l3[p,5`>}{::>{:" + fromBase64(base64Of("aeqfedq.'.[pl12l3[p,5`>}{::>{:")) shouldEqual "aeqfedq.'.[pl12l3[p,5`>}{::>{:" } } diff --git a/src/test/scala/com/fluency03/blockchain/core/BlockHeaderTest.scala b/src/test/scala/com/fluency03/blockchain/core/BlockHeaderTest.scala index e9f4d81..86271a5 100644 --- a/src/test/scala/com/fluency03/blockchain/core/BlockHeaderTest.scala +++ b/src/test/scala/com/fluency03/blockchain/core/BlockHeaderTest.scala @@ -1,53 +1,51 @@ package com.fluency03.blockchain.core import com.fluency03.blockchain.core.BlockHeader.hashOfBlockHeader -import org.json4s.JsonDSL._ +import com.fluency03.blockchain.core.Transaction.createCoinbaseTx +import org.json4s.JValue +import org.json4s.JsonAST.JInt import org.json4s.native.JsonMethods.parse import org.scalatest.{FlatSpec, Matchers} +import scala.io.Source + class BlockHeaderTest extends FlatSpec with Matchers { val genesisHeader: BlockHeader = Block.genesisBlock.header - val genesisTx: Transaction = Transaction(ZERO64, ZERO64, 50, genesisTimestamp) + val genesisTx: Transaction = createCoinbaseTx(0, genesisMiner, genesisTimestamp) + + val expectedBlockJson: JValue = parse(Source.fromResource("genesis-block.json").mkString) + val expectedGenesisBlock: Block = expectedBlockJson.extract[Block] + val expectedHeader: BlockHeader = expectedGenesisBlock.header "Genesis block header" should "be valid." in { - genesisHeader.index shouldEqual 0 - genesisHeader.previousHash shouldEqual ZERO64 - genesisHeader.data shouldEqual "Welcome to Blockchain in Scala!" - genesisHeader.merkleHash shouldEqual genesisTx.hash - genesisHeader.timestamp shouldEqual genesisTimestamp - genesisHeader.nonce shouldEqual 33660 - genesisHeader.hash shouldEqual "0000a26af9a70022a6c6d270a0ced7478eb40bcfc4301b5e73c0ed3207a3de0e" - genesisHeader.isValidWith(4) shouldEqual true - val json = ("index" -> 0) ~ - ("previousHash" -> ZERO64) ~ - ("data" -> "Welcome to Blockchain in Scala!") ~ - ("merkleHash" -> genesisTx.hash) ~ - ("timestamp" -> genesisTimestamp) ~ - ("nonce" -> 33660) ~ - ("hash" -> "0000a26af9a70022a6c6d270a0ced7478eb40bcfc4301b5e73c0ed3207a3de0e") - genesisHeader.toJson shouldEqual json - parse(genesisHeader.toString) shouldEqual json + genesisHeader shouldEqual expectedHeader + genesisHeader.index shouldEqual expectedHeader.index + genesisHeader.previousHash shouldEqual expectedHeader.previousHash + genesisHeader.data shouldEqual expectedHeader.data + genesisHeader.merkleHash shouldEqual expectedHeader.merkleHash + genesisHeader.merkleHash shouldEqual genesisTx.id + genesisHeader.timestamp shouldEqual expectedHeader.timestamp + genesisHeader.nonce shouldEqual expectedHeader.nonce + genesisHeader.hash shouldEqual expectedHeader.hash + genesisHeader.toJson shouldEqual expectedBlockJson \ "header" + parse(genesisHeader.toString) shouldEqual expectedBlockJson \ "header" } "Next trial of Genesis block header" should "equal to Genesis header except nonce+1 ." in { val genesisHeaderNextTrial: BlockHeader = genesisHeader.nextTrial() - genesisHeaderNextTrial.index shouldEqual genesisHeader.index - genesisHeaderNextTrial.previousHash shouldEqual genesisHeader.previousHash - genesisHeaderNextTrial.data shouldEqual genesisHeader.data - genesisHeaderNextTrial.merkleHash shouldEqual genesisTx.hash - genesisHeaderNextTrial.timestamp shouldEqual genesisHeader.timestamp - genesisHeaderNextTrial.nonce shouldEqual genesisHeader.nonce + 1 + genesisHeaderNextTrial.index shouldEqual expectedHeader.index + genesisHeaderNextTrial.previousHash shouldEqual expectedHeader.previousHash + genesisHeaderNextTrial.data shouldEqual expectedHeader.data + genesisHeaderNextTrial.merkleHash shouldEqual expectedHeader.merkleHash + genesisHeaderNextTrial.merkleHash shouldEqual genesisTx.id + genesisHeaderNextTrial.timestamp shouldEqual expectedHeader.timestamp + genesisHeaderNextTrial.nonce shouldEqual expectedHeader.nonce + 1 val newHash = hashOfBlockHeader(genesisHeaderNextTrial) genesisHeaderNextTrial.hash shouldEqual newHash - genesisHeaderNextTrial.isValidWith(4) shouldEqual false - val json = ("index" -> 0) ~ - ("previousHash" -> ZERO64) ~ - ("data" -> "Welcome to Blockchain in Scala!") ~ - ("merkleHash" -> genesisTx.hash) ~ - ("timestamp" -> 1523472721) ~ - ("nonce" -> (genesisHeader.nonce + 1)) ~ - ("hash" -> newHash) + val json = expectedHeader.toJson.transformField { + case ("nonce", JInt(x)) => ("nonce", JInt(x+1)) + } genesisHeaderNextTrial.toJson shouldEqual json parse(genesisHeaderNextTrial.toString) shouldEqual json } diff --git a/src/test/scala/com/fluency03/blockchain/core/BlockTest.scala b/src/test/scala/com/fluency03/blockchain/core/BlockTest.scala index cdccb3e..c82d107 100644 --- a/src/test/scala/com/fluency03/blockchain/core/BlockTest.scala +++ b/src/test/scala/com/fluency03/blockchain/core/BlockTest.scala @@ -1,132 +1,128 @@ package com.fluency03.blockchain.core import com.fluency03.blockchain.core.BlockHeader.hashOfBlockHeader +import com.fluency03.blockchain.core.Transaction.createCoinbaseTx +import org.json4s.JValue +import org.json4s.JsonAST.{JArray, JInt, JObject, JString} import org.json4s.JsonDSL._ import org.json4s.native.JsonMethods.parse import org.scalatest.{FlatSpec, Matchers} +import scala.io.Source + class BlockTest extends FlatSpec with Matchers { val genesis: Block = Block.genesisBlock - val genesisTx: Transaction = Transaction(ZERO64, ZERO64, 50, genesisTimestamp) + val genesisTx: Transaction = createCoinbaseTx(0, genesisMiner, genesisTimestamp) + + val expectedBlockJson: JValue = parse(Source.fromResource("genesis-block.json").mkString) + val expectedGenesisBlock: Block = expectedBlockJson.extract[Block] + val expectedHeader: BlockHeader = expectedGenesisBlock.header "Genesis block" should "be a valid Genesis block." in { - genesis.index shouldEqual 0 - genesis.previousHash shouldEqual ZERO64 - genesis.data shouldEqual "Welcome to Blockchain in Scala!" - genesis.merkleHash shouldEqual genesisTx.hash - genesis.timestamp shouldEqual genesisTimestamp - genesis.nonce shouldEqual 33660 - genesis.hash shouldEqual "0000a26af9a70022a6c6d270a0ced7478eb40bcfc4301b5e73c0ed3207a3de0e" - genesis.hasValidMerkleHash shouldEqual true - genesis.hasValidHash(4) shouldEqual true - genesis.isValid(4) shouldEqual true - val json = - ("header" -> - ("index" -> 0) ~ - ("previousHash" -> ZERO64) ~ - ("data" -> "Welcome to Blockchain in Scala!") ~ - ("merkleHash" -> genesisTx.hash) ~ - ("timestamp" -> genesisTimestamp) ~ - ("nonce" -> 33660) ~ - ("hash" -> "0000a26af9a70022a6c6d270a0ced7478eb40bcfc4301b5e73c0ed3207a3de0e")) ~ - ("transactions" -> List(genesisTx).map(_.toJson)) - genesis.toJson shouldEqual json - parse(genesis.toString) shouldEqual json + genesis.header shouldEqual expectedHeader + genesis.index shouldEqual expectedHeader.index + genesis.previousHash shouldEqual expectedHeader.previousHash + genesis.data shouldEqual expectedHeader.data + genesis.merkleHash shouldEqual expectedHeader.merkleHash + genesis.merkleHash shouldEqual expectedGenesisBlock.transactions.head.id + genesis.timestamp shouldEqual expectedHeader.timestamp + genesis.nonce shouldEqual expectedHeader.nonce + genesis.hash shouldEqual expectedHeader.hash + genesis.isValid shouldEqual true + genesis.toJson shouldEqual expectedBlockJson + parse(genesis.toString) shouldEqual expectedBlockJson } "Next trial of Genesis block" should "equal to Genesis block except nonce+1 ." in { val genesisNextTrial: Block = genesis.nextTrial() - genesisNextTrial.index shouldEqual genesis.index - genesisNextTrial.previousHash shouldEqual genesis.previousHash - genesisNextTrial.data shouldEqual genesis.data - genesisNextTrial.merkleHash shouldEqual genesisTx.hash - genesisNextTrial.timestamp shouldEqual genesis.timestamp - genesisNextTrial.nonce shouldEqual genesis.nonce + 1 + val newExpectedHeader = expectedHeader.nextTrial() + genesisNextTrial.header shouldEqual newExpectedHeader + genesisNextTrial.index shouldEqual newExpectedHeader.index + genesisNextTrial.previousHash shouldEqual newExpectedHeader.previousHash + genesisNextTrial.data shouldEqual newExpectedHeader.data + genesisNextTrial.merkleHash shouldEqual newExpectedHeader.merkleHash + genesisNextTrial.merkleHash shouldEqual expectedGenesisBlock.transactions.head.id + genesisNextTrial.timestamp shouldEqual newExpectedHeader.timestamp + genesisNextTrial.nonce shouldEqual newExpectedHeader.nonce val newHash = hashOfBlockHeader(genesisNextTrial.header) genesisNextTrial.hash shouldEqual newHash + genesisNextTrial.hash shouldEqual newExpectedHeader.hash genesisNextTrial.hasValidMerkleHash shouldEqual true - genesisNextTrial.hasValidHash(4) shouldEqual false - genesisNextTrial.isValid(4) shouldEqual false - val json = - ("header" -> - ("index" -> 0) ~ - ("previousHash" -> ZERO64) ~ - ("data" -> "Welcome to Blockchain in Scala!") ~ - ("merkleHash" -> genesisTx.hash) ~ - ("timestamp" -> 1523472721) ~ - ("nonce" -> (genesis.nonce + 1)) ~ - ("hash" -> newHash)) ~ - ("transactions" -> List(genesisTx).map(_.toJson)) + genesisNextTrial.isValid shouldEqual false + val headerJson = expectedHeader.toJson.transformField { + case ("nonce", JInt(x)) => ("nonce", JInt(x+1)) + } + val json = expectedBlockJson.transformField { + case ("header", JObject(_)) => ("header", headerJson) + }.transformField { + case ("hash", JString(_)) => ("hash", newHash) + } genesisNextTrial.toJson shouldEqual json parse(genesisNextTrial.toString) shouldEqual json } "Add Transactions to a Block" should "result to a new Block with new List of Transactions." in { - val t1: Transaction = Transaction(ZERO64, ZERO64, 10) + val t1: Transaction = createCoinbaseTx(1, genesisMiner, genesisTimestamp) - val newBlock: Block = genesis.addTransaction(ZERO64, ZERO64, 10) - newBlock.index shouldEqual genesis.index - newBlock.previousHash shouldEqual genesis.previousHash - newBlock.data shouldEqual genesis.data - val newMerkleHash = MerkleNode.computeRoot(t1 :: genesis.transactions) + val newBlock: Block = genesis.addTransaction(t1) + newBlock.index shouldEqual expectedGenesisBlock.index + newBlock.previousHash shouldEqual expectedGenesisBlock.previousHash + newBlock.data shouldEqual expectedGenesisBlock.data + val newMerkleHash = Merkle.computeRoot(t1 +: expectedGenesisBlock.transactions) newBlock.merkleHash shouldEqual newMerkleHash - newBlock.timestamp shouldEqual genesis.timestamp - newBlock.nonce shouldEqual genesis.nonce + newBlock.timestamp shouldEqual expectedGenesisBlock.timestamp + newBlock.nonce shouldEqual expectedGenesisBlock.nonce val newHash = hashOfBlockHeader(newBlock.header) newBlock.hash shouldEqual newHash newBlock.hasValidMerkleHash shouldEqual true - newBlock.hasValidHash(4) shouldEqual false - newBlock.isValid(4) shouldEqual false - val json = - ("header" -> - ("index" -> 0) ~ - ("previousHash" -> ZERO64) ~ - ("data" -> "Welcome to Blockchain in Scala!") ~ - ("merkleHash" -> newMerkleHash) ~ - ("timestamp" -> 1523472721) ~ - ("nonce" -> genesis.nonce) ~ - ("hash" -> newHash)) ~ - ("transactions" -> (t1 :: genesis.transactions).map(_.toJson)) + newBlock.isValid shouldEqual false + val headerJson = expectedHeader.toJson.transformField { + case ("merkleHash", JString(_)) => ("merkleHash", newMerkleHash) + } + val json = expectedBlockJson.transformField { + case ("header", JObject(_)) => ("header", headerJson) + }.transformField { + case ("hash", JString(_)) => ("hash", newHash) + }.transformField { + case ("transactions", JArray(arr)) => ("transactions", JArray(t1.toJson +: arr)) + } newBlock.toJson shouldEqual json parse(newBlock.toString) shouldEqual json } "Add a List of Transactions to a Block" should "result to a new Block with new List of Transactions." in { - val t1: Transaction = Transaction(ZERO64, ZERO64, 10) - val t2: Transaction = Transaction(ZERO64, ZERO64, 20) - val t3: Transaction = Transaction(ZERO64, ZERO64, 30) - val t4: Transaction = Transaction(ZERO64, ZERO64, 40) + val t1: Transaction = createCoinbaseTx(1, genesisMiner, genesisTimestamp) + val t2: Transaction = createCoinbaseTx(2, genesisMiner, genesisTimestamp) + val t3: Transaction = createCoinbaseTx(3, genesisMiner, genesisTimestamp) + val t4: Transaction = createCoinbaseTx(4, genesisMiner, genesisTimestamp) val newBlock: Block = genesis.addTransactions(t1 :: t2 :: t3 :: t4 :: Nil) - newBlock.index shouldEqual genesis.index - newBlock.previousHash shouldEqual genesis.previousHash - newBlock.data shouldEqual genesis.data - val newMerkleHash = MerkleNode.computeRoot(t1 :: t2 :: t3 :: t4 :: genesis.transactions) + newBlock.index shouldEqual expectedGenesisBlock.index + newBlock.previousHash shouldEqual expectedGenesisBlock.previousHash + newBlock.data shouldEqual expectedGenesisBlock.data + val newMerkleHash = Merkle.computeRoot(t1 +: t2 +: t3 +: t4 +: genesis.transactions) newBlock.merkleHash shouldEqual newMerkleHash - newBlock.timestamp shouldEqual genesis.timestamp - newBlock.nonce shouldEqual genesis.nonce + newBlock.timestamp shouldEqual expectedGenesisBlock.timestamp + newBlock.nonce shouldEqual expectedGenesisBlock.nonce val newHash = hashOfBlockHeader(newBlock.header) newBlock.hash shouldEqual newHash newBlock.hasValidMerkleHash shouldEqual true - newBlock.hasValidHash(4) shouldEqual false - newBlock.isValid(4) shouldEqual false - val json = - ("header" -> - ("index" -> 0) ~ - ("previousHash" -> ZERO64) ~ - ("data" -> "Welcome to Blockchain in Scala!") ~ - ("merkleHash" -> newMerkleHash) ~ - ("timestamp" -> 1523472721) ~ - ("nonce" -> genesis.nonce) ~ - ("hash" -> newHash)) ~ - ("transactions" -> (t1 :: t2 :: t3 :: t4 :: genesis.transactions).map(_.toJson)) + newBlock.isValid shouldEqual false + val headerJson = expectedHeader.toJson.transformField { + case ("merkleHash", JString(_)) => ("merkleHash", newMerkleHash) + } + val json = expectedBlockJson.transformField { + case ("header", JObject(_)) => ("header", headerJson) + }.transformField { + case ("hash", JString(_)) => ("hash", newHash) + }.transformField { + case ("transactions", JArray(arr)) => ("transactions", JArray(t1.toJson :: t2.toJson :: t3.toJson :: t4.toJson +: arr)) + } newBlock.toJson shouldEqual json parse(newBlock.toString) shouldEqual json } - - } diff --git a/src/test/scala/com/fluency03/blockchain/core/BlockchainTest.scala b/src/test/scala/com/fluency03/blockchain/core/BlockchainTest.scala index 6e23221..57dfe8a 100644 --- a/src/test/scala/com/fluency03/blockchain/core/BlockchainTest.scala +++ b/src/test/scala/com/fluency03/blockchain/core/BlockchainTest.scala @@ -1,29 +1,36 @@ package com.fluency03.blockchain.core +import com.fluency03.blockchain.core.Transaction.createCoinbaseTx +import org.json4s.JValue +import org.json4s.native.JsonMethods.parse import org.scalatest.{FlatSpec, Matchers} import scala.collection.mutable import scala.collection.mutable.ListBuffer +import scala.io.Source class BlockchainTest extends FlatSpec with Matchers { val blockchain: Blockchain = Blockchain() val genesis: Block = Block.genesisBlock + val expectedBlockJson: JValue = parse(Source.fromResource("genesis-block.json").mkString) + val expectedGenesisBlock: Block = expectedBlockJson.extract[Block] + val expectedHeader: BlockHeader = expectedGenesisBlock.header + val blockchainOf5: Blockchain = Blockchain(5) val genesisOf5: Block = Block.genesis(5) - val t1: Transaction = Transaction(ZERO64, ZERO64, 10) - val t2: Transaction = Transaction(ZERO64, ZERO64, 20) - val t3: Transaction = Transaction(ZERO64, ZERO64, 30) - val t4: Transaction = Transaction(ZERO64, ZERO64, 40) + val t1: Transaction = createCoinbaseTx(1, genesisMiner, genesisTimestamp) + val t2: Transaction = createCoinbaseTx(2, genesisMiner, genesisTimestamp) + val t3: Transaction = createCoinbaseTx(3, genesisMiner, genesisTimestamp) + val t4: Transaction = createCoinbaseTx(4, genesisMiner, genesisTimestamp) "A new Blockchain" should "have all default values." in { blockchain.difficulty shouldEqual 4 - blockchain.chain shouldEqual List(genesis) + blockchain.chain shouldEqual List(expectedGenesisBlock) blockchain.lastBlock().isEmpty shouldEqual false - blockchain.lastBlock().get shouldEqual genesis - blockchain.currentTransactions shouldEqual mutable.Map.empty[String, Transaction] + blockchain.lastBlock().get shouldEqual expectedGenesisBlock blockchain.isValid shouldEqual true } @@ -32,31 +39,6 @@ class BlockchainTest extends FlatSpec with Matchers { blockchainOf5.chain shouldEqual List(genesisOf5) blockchainOf5.lastBlock().isEmpty shouldEqual false blockchainOf5.lastBlock().get shouldEqual genesisOf5 - blockchainOf5.currentTransactions shouldEqual mutable.Map.empty[String, Transaction] - blockchainOf5.isValid shouldEqual true - } - - "Add a Transaction to a Blockchain" should "add these Transactions to its currentTransactions collection." in { - val trans = mutable.Map.empty[String, Transaction] - blockchain.addTransaction(t1) - trans += (t1.hash -> t1) - blockchain.currentTransactions shouldEqual trans - - blockchain.addTransaction(t2) - blockchain.addTransaction(t3) - trans += (t2.hash -> t2) - trans += (t3.hash -> t3) - blockchain.currentTransactions shouldEqual trans - blockchain.isValid shouldEqual true - } - - "Add a List of Transaction to a Blockchain" should "add these Transactions to its currentTransactions collection." in { - val trans = mutable.Map.empty[String, Transaction] - blockchainOf5.addTransactions(t2 :: t3 :: t4 :: Nil) - trans += (t2.hash -> t2) - trans += (t3.hash -> t3) - trans += (t4.hash -> t4) - blockchainOf5.currentTransactions shouldEqual trans blockchainOf5.isValid shouldEqual true } @@ -64,8 +46,8 @@ class BlockchainTest extends FlatSpec with Matchers { val blockchainToAdd: Blockchain = Blockchain() val genesis: Block = Block.genesisBlock - val actual = blockchainToAdd.mineNextBlock("This is next Block!") - val expected = Block.mineNextBlock(genesis, "This is next Block!", List(), actual.timestamp, blockchain.difficulty) + val actual = blockchainToAdd.mineNextBlock("This is next Block!", Seq(t1, t2)) + val expected = Block.mineNextBlock(genesis, "This is next Block!", actual.timestamp, blockchain.difficulty, Seq(t1, t2)) actual shouldEqual expected blockchainToAdd.lastBlock().get shouldEqual genesis diff --git a/src/test/scala/com/fluency03/blockchain/core/MerkleNodeTest.scala b/src/test/scala/com/fluency03/blockchain/core/MerkleTest.scala similarity index 67% rename from src/test/scala/com/fluency03/blockchain/core/MerkleNodeTest.scala rename to src/test/scala/com/fluency03/blockchain/core/MerkleTest.scala index eed0c11..5e86ef1 100644 --- a/src/test/scala/com/fluency03/blockchain/core/MerkleNodeTest.scala +++ b/src/test/scala/com/fluency03/blockchain/core/MerkleTest.scala @@ -1,10 +1,11 @@ package com.fluency03.blockchain.core import com.fluency03.blockchain.Util.hashOf -import com.fluency03.blockchain.core.MerkleNode._ +import com.fluency03.blockchain.core.Merkle._ +import com.fluency03.blockchain.core.Transaction.createCoinbaseTx import org.scalatest.{FlatSpec, Matchers} -class MerkleNodeTest extends FlatSpec with Matchers { +class MerkleTest extends FlatSpec with Matchers { "A empty Merkle Tree" should "have Zero 64 as root hash." in { computeRootOfHashes(List()) shouldEqual ZERO64 @@ -15,8 +16,8 @@ class MerkleNodeTest extends FlatSpec with Matchers { val h = "41ef4bb0b23661e66301aac36066912dac037827b4ae63a7b1165a5aa93ed4eb" computeRootOfHashes(List(h)) shouldEqual h - val t = Transaction(ZERO64, ZERO64, 50) - computeRoot(List(t)) shouldEqual t.hash + val t: Transaction = createCoinbaseTx(0, genesisMiner, genesisTimestamp) + computeRoot(List(t)) shouldEqual t.id } "A Merkle Tree" should "have a valid root hash." in { @@ -29,13 +30,13 @@ class MerkleNodeTest extends FlatSpec with Matchers { val h33 = hashOf(h3, h3) computeRootOfHashes(List(h1, h2, h3)) shouldEqual hashOf(h12, h33) - val t1 = Transaction(ZERO64, ZERO64, 50) - val t2 = Transaction(ZERO64, ZERO64, 20) - val th12 = hashOf(t1.hash, t2.hash) + val t1 = createCoinbaseTx(1, genesisMiner, genesisTimestamp) + val t2 = createCoinbaseTx(2, genesisMiner, genesisTimestamp) + val th12 = hashOf(t1.id, t2.id) computeRoot(List(t1, t2)) shouldEqual th12 - val t3 = Transaction(ZERO64, ZERO64, 10) - val th33 = hashOf(t3.hash, t3.hash) + val t3 = createCoinbaseTx(3, genesisMiner, genesisTimestamp) + val th33 = hashOf(t3.id, t3.id) computeRoot(List(t1, t2, t3)) shouldEqual hashOf(th12, th33) } diff --git a/src/test/scala/com/fluency03/blockchain/core/TransactionTest.scala b/src/test/scala/com/fluency03/blockchain/core/TransactionTest.scala index c1c219a..c123d4e 100644 --- a/src/test/scala/com/fluency03/blockchain/core/TransactionTest.scala +++ b/src/test/scala/com/fluency03/blockchain/core/TransactionTest.scala @@ -3,27 +3,39 @@ package com.fluency03.blockchain.core import java.time.Instant import com.fluency03.blockchain.Util.hashOf +import com.fluency03.blockchain.core.Transaction.createCoinbaseTx +import org.json4s.JValue import org.json4s.JsonDSL._ import org.json4s.native.JsonMethods.parse import org.scalatest.{FlatSpec, Matchers} +import scala.io.Source + class TransactionTest extends FlatSpec with Matchers { - "A Transaction" should "be valid." in { - val time = Instant.parse("2018-04-11T18:52:01Z").getEpochSecond - val t = Transaction(ZERO64, ZERO64, 50, time) - - t.sender shouldEqual ZERO64 - t.receiver shouldEqual ZERO64 - t.amount shouldEqual 50 - t.hash shouldEqual hashOf(t.sender, t.receiver, t.amount.toString, time.toString) - - val json = ("sender" -> ZERO64) ~ - ("receiver" -> ZERO64) ~ - ("amount" -> 50.toDouble) ~ - ("timestamp" -> time) - t.toJson shouldEqual json - parse(t.toString) shouldEqual json + val genesisTx: Transaction = createCoinbaseTx(0, genesisMiner, genesisTimestamp) + + val expectedBlockJson: JValue = parse(Source.fromResource("genesis-block.json").mkString) + val expectedGenesisBlock: Block = expectedBlockJson.extract[Block] + val expectedHeader: BlockHeader = expectedGenesisBlock.header + + "A genesis Transaction" should "be valid." in { + genesisTx shouldEqual expectedGenesisBlock.transactions.head + + genesisTx.txIns.length shouldEqual 1 + genesisTx.txIns.head shouldEqual TxIn(Outpoint("", 0), "") + + genesisTx.txOuts.length shouldEqual 1 + genesisTx.txOuts.head shouldEqual TxOut(genesisMiner, 50) + + val json = expectedGenesisBlock.transactions.head.toJson + genesisTx.toJson shouldEqual json + parse(genesisTx.toString) shouldEqual json } + + + + + }