diff --git a/.gitignore b/.gitignore index 2210e21..5b9c75b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ .idea target/ persistence/journal/* -persistence/snapshots/* \ No newline at end of file +persistence/snapshots/* +.DS_Store \ No newline at end of file diff --git a/docker/Dockerfile b/Dockerfile similarity index 87% rename from docker/Dockerfile rename to Dockerfile index fc09d8e..39feb4e 100644 --- a/docker/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ RUN apk add --no-cache git openssh && \ mkdir /development # GIT -ADD repo-key / +COPY docker/repo-key / RUN chmod 600 /repo-key && \ echo "IdentityFile /repo-key" >> /etc/ssh/ssh_config && \ echo -e "StrictHostKeyChecking no" >> /etc/ssh/ssh_config && \ @@ -19,7 +19,10 @@ RUN apk add --no-cache bash && \ chmod 0755 /usr/local/bin/sbt && \ apk del build-dependencies -ADD init.sh /etc/init.sh +RUN cd /development && \ + sbt sbtVersion + +COPY docker/init.sh /etc/init.sh RUN chmod +x /etc/init.sh diff --git a/README.md b/README.md index a773ea5..5c50014 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,22 @@ # Scalachain Scalachain is a blockchain implemented using the Scala programming language and the actor model (Akka Framework). The main purpose of the project is: - -* Understand how a simple blockchain works -* Learn the Scala programming language and Akka -* Share what I learned - +* Understand how a simple blockchain works +* Learn the Scala programming language and Akka +* Share what I learned For this reason, any contribution is welcome! :-) -I published a story on freeCodeCamp where I explain in depth the development of Scalachain. Read it [here](https://medium.freecodecamp.org/how-to-build-a-simple-actor-based-blockchain-aac1e996c177)! :-) +I published a story on freeCodeCamp where I explain in depth the development of Scalachain [v1.0](https://github.com/elleFlorio/scalachain/releases/tag/v.1.0). Read it [here](https://medium.freecodecamp.org/how-to-build-a-simple-actor-based-blockchain-aac1e996c177)! :-) -### TL;DR -Run the Scalachain node using ```sbt``` (```sbt run```) or the Docker container (see ```/docker``` folder). -The API to interact with Scalachain are documented [here](https://documenter.getpostman.com/view/4636741/RWaHw8yx). +## TL;DR +Run Scalachain following these steps: +1. build the Docker image as described in the ```/docker``` folder +2. Run the cluster of 3 nodes using the ```docker-compose up``` command in the root folder of the project. +3. Use The API documented [here](https://documenter.getpostman.com/view/4636741/RWaHw8yx) to interact with Scalachain nodes. -### Quick introduction to the blockchain +Enjoy! :grin: + +## Quick introduction to the blockchain There a lot of good articles that explain how a blockchain works, so I will do a high level introduction just to provide some context to this project. The blockchain is a distributed ledger: it registers some transaction of values (e.g., coins) between a sender and a receiver. What makes a blockchain different from a traditional database is the decentralized nature of the blockchain: it is distributed among several communicating nodes that guarantee the validity of the transactions registered. @@ -27,18 +29,18 @@ The solution of the cryptographic puzzle depends on the values stored in the las It may happen that different nodes mine a block at the same time, creating different "branches" from the same blockchain - this is called a fork in the blockchain. when this happens, sets of nodes will continue mining starting from the different blocks of the new branches, that will grow in different ways. This situation is solved automatically when a branch becomes longer than the others: the longest chain always wins, so the winning branch becomes the new blockchain. -### The Blockchain Model +## The Blockchain Model Scalachain is based on a blockchain model that is a simplification of the Bitcoin one. The main components of our blockchain model are the **transaction**, the **chain**, the **proof of work** (PoW) algorithm, and the **Node**. The transactions are stored inside the blocks of the chain, that are mined using the PoW. The node is the server that runs the blockchain. -##### Transaction +### Transaction Transactions register the movement of coins between two entities. Every transaction is composed by a sender, a recipient and an amount of coin. Transactions will be registered inside the blocks of our blockchain. -##### Chain +### Chain The chain is a linked list of blocks containing a list of transactions. Every block of the chain has an index, the proof that validates it (more on this later), the list of transactions, the hash of the previous block, the list of previous blocks, and a timestamp. Every block is chained to the previous one by its hash, that is computed converting the block to a JSON string and then hashing it through a ```SHA-256``` hashing function. -##### PoW +### PoW The PoW algorithm is required to mine the blocks composing the blockchain. The idea is to solve a cryptographic puzzle that is hard to solve, but easy to verify having a proof. The PoW algorithm that is implemented in Scalachain is similar to the Bitcoin one (based on [Hashcash](https://en.wikipedia.org/wiki/Hashcash)), and consists in finding a hash with N leading zeros that is computed starting from the hash of the previous block and a number, that represents the proof of our algorithm. We can formalize it as: @@ -46,26 +48,34 @@ We can formalize it as: The higher is N, the harder is to find the proof. In Scalachain N=4 (It will be configurable eventually). -##### Node +### Node The Node is the server running our blockchain. It provides some REST API to interact with it and perform basic operations such as send a new transaction, get the list of pending transactions, mine a block, and get the current status of the blockchain. -### The Actor Model +## The Actor Model Scalachain has been implemented using the actor model through the use of the [Akka Framework](https://akka.io/). The hierarchy of actors is depicted in the following diagram of the Scalachain node. ![Scalachain node](img/scalachain_node.png) -* **Broker Actor** is responsible of managing the transactions. It can add a new transaction, read the pending transactions (the ones that are not yet included in a block), and clear the list of pending transactions. - -* **Miner Actor** is responsible of the mining process. It validates the proof of added nodes and executes the PoW algorithm to mine a new block. It is implemented as a state machine composed of two states (ready and busy) in order to handle mining concurrency - If the node is mining a block, it cannot start to mine a new one. - -* **Blockchain Actor** is responsible of managing the chain. It manages the addition of new blocks, and provides information about the chain: the last index, the last hash, and the status of the chain itself. This is a *Persistent Actor*, so it keeps a journal of events and saves a snapshot of the state of the chain when a new block is added. If the node goes down, when it is up again the Blockchain Actor can restore the status of the chain loading the last snapshot. The journal of events and the snapshots are stored inside the ```persistence``` folder. - -* **Node Actor** is on the top of the hierarchy of actors: it manages all the operations executed by the other actors, and it is the junction point between the API and the business logic implemented inside the actors. - -### Scalachain API -Scalachain node runs a server at default address ```http://localhost:8080/``` using Akka HTTP. +* **Broker Actor** is responsible of managing the transactions. It can add a new transaction, read the pending transactions (the ones that are not yet included in a block), and clear the list of pending transactions. +* **Miner Actor** is responsible of the mining process. It validates the proof of added nodes and executes the PoW algorithm to mine a new block. It is implemented as a state machine composed of two states (ready and busy) in order to handle mining concurrency - If the node is mining a block, it cannot start to mine a new one. +* **Blockchain Actor** is responsible of managing the chain. It manages the addition of new blocks, and provides information about the chain: the last index, the last hash, and the status of the chain itself. This is a *Persistent Actor*, so it keeps a journal of events and saves a snapshot of the state of the chain when a new block is added. If the node goes down, when it is up again the Blockchain Actor can restore the status of the chain loading the last snapshot. The journal of events and the snapshots are stored inside the ```persistence``` folder. +* **Node Actor** is on the top of the hierarchy of actors: it manages all the operations executed by the other actors, and it is the junction point between the API and the business logic implemented inside the actors. +* **ClusterManager Actor** is the manager of the cluster. Its in charge of everything cluster related, such as return the current active members of the cluster. +* **ClusterListener Actor** listens to the events that happen in the cluster - a member is up/down, etc. - and logs it for debugging purpose. + +## Scalachain cluster +Since scalachain v2.0 is possible to run a cluster of Scalachain nodes. The easiest way to create a simple cluster is to run the docker-compose.yml file with the command ```docker-compose up```. This will spawn a 3 nodes cluster with 1 seed and 2 nodes. You can send a transaction to one node that will be propagated to the other ones in the cluster. Once a node mines a block, it will send it to the other nodes of the cluster that will add it to their local blockchain. + +## Scalachain API +The cluster is composed of 3 nodes, mapped to the following addresses: +* ```http://localhost:8000``` - ```seed``` +* ```http://localhost:8001``` - ```node1``` +* ```http://localhost:8002``` - ```node2``` + The API to interact with the node is documented [here](https://documenter.getpostman.com/view/4636741/RWaHw8yx) -### How to run Scalachain -Scalachain node can be run using ```sbt``` (```sbt run```) or a Docker container. The instructions on how to run it using Docker can be found inside the ```/docker``` folder. +## How to run Scalachain +Run Scalachain following these steps: +1. build the Docker image as described in the ```/docker``` folder +2. Run the cluster of 3 nodes using the ```docker-compose up``` command in the root folder of the project. diff --git a/build.sbt b/build.sbt index cdfa847..664ed4e 100644 --- a/build.sbt +++ b/build.sbt @@ -6,15 +6,18 @@ scalaVersion := "2.12.4" resolvers ++= Seq( "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/", + "dnvriend" at "http://dl.bintray.com/dnvriend/maven", Resolver.jcenterRepo ) -lazy val akkaVersion = "2.5.3" -lazy val akkaHttpVersion = "10.1.4" +lazy val akkaVersion = "2.5.21" +lazy val akkaHttpVersion = "10.1.7" +lazy val akkaPersistenceInmemoryVersion = "2.5.15.1" +lazy val scalaTestVersion = "3.0.5" libraryDependencies ++= Seq( - "com.typesafe.akka" %% "akka-testkit" % "2.5.14" % Test, - "org.scalatest" %% "scalatest" % "3.0.4" % "test", + "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test, + "org.scalatest" %% "scalatest" % "3.0.5" % "test", "com.typesafe.akka" %% "akka-persistence" % akkaVersion, "org.iq80.leveldb" % "leveldb" % "0.10", "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8", @@ -22,7 +25,9 @@ libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-actor" % akkaVersion, "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, "com.typesafe.akka" %% "akka-stream" % akkaVersion, - "com.github.dnvriend" %% "akka-persistence-inmemory" % "2.5.1.1" + "com.typesafe.akka" %% "akka-cluster" % akkaVersion, + "com.typesafe.akka" %% "akka-cluster-tools" % akkaVersion, + "com.github.dnvriend" %% "akka-persistence-inmemory" % akkaPersistenceInmemoryVersion ) - +fork in Test := true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5ea755f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +version: '3.7' + +networks: + scalachain-network: + +services: + seed: + networks: + - scalachain-network + image: elleflorio/scalachain + volumes: + - ~/Development/scala/scalachain/:/tmp/scalachain + ports: + - '2552:2552' + - '8000:8000' + environment: + SERVER_IP: 0.0.0.0 + CLUSTER_IP: seed + CLUSTER_SEED_IP: seed + SCALACHAIN_PERSISTENCE_DIR: '/persistence/seed/journal' + SCALACHAIN_SNAPSHOTS_DIR: '/persistence/seed/snapshots' + command: ["local", "/tmp/scalachain"] + + node1: + networks: + - scalachain-network + image: elleflorio/scalachain + volumes: + - ~/Development/scala/scalachain/:/tmp/scalachain + ports: + - '8001:8000' + environment: + SERVER_IP: 0.0.0.0 + CLUSTER_IP: node1 + CLUSTER_PORT: 1600 + CLUSTER_SEED_IP: seed + CLUSTER_SEED_PORT: 2552 + SCALACHAIN_NODE_ID: node1 + SCALACHAIN_PERSISTENCE_DIR: '/persistence/node1/journal' + SCALACHAIN_SNAPSHOTS_DIR: '/persistence/node1/snapshots' + command: ["local", "/tmp/scalachain"] + + node2: + networks: + - scalachain-network + image: elleflorio/scalachain + volumes: + - ~/Development/scala/scalachain/:/tmp/scalachain + ports: + - '8002:8000' + environment: + SERVER_IP: 0.0.0.0 + CLUSTER_IP: node2 + CLUSTER_PORT: 1600 + CLUSTER_SEED_IP: seed + CLUSTER_SEED_PORT: 2552 + SCALACHAIN_NODE_ID: node2 + SCALACHAIN_PERSISTENCE_DIR: '/persistence/node2/journal' + SCALACHAIN_SNAPSHOTS_DIR: '/persistence/node2/snapshots' + command: ["local", "/tmp/scalachain"] \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 2b1babd..341825b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,15 +1,15 @@ # Scalachain Docker image The Dockerfile lets you build the Docker Image of a scalachain node. In details, the Dockerfile: -* copy the ssh private key to pull from the repo - **N.B. the key is only for read operations** -* download the [sbt](https://www.scala-sbt.org/) version specified through the image ```build-arg``` parameter -* install git and openssh -* checkout the project -* copy the script file init.sh -* ```EXPOSE``` port 8080 +* copy the ssh private key to pull from the repo - **N.B. the key is only for read operations** +* download the [sbt](https://www.scala-sbt.org/) version specified through the image ```build-arg``` parameter +* install git and openssh +* checkout the project +* copy the script file init.sh +* ```EXPOSE``` port 8080 The image can be built using the command: -```docker build --build-arg SBT_VERSION="1.1.6" -t elleflorio/scalachain .``` +```docker build --build-arg SBT_VERSION="1.2.7" -t elleflorio/scalachain .``` The docker image is already available in the [Dockerhub](https://hub.docker.com/r/elleflorio/scalachain/) @@ -18,13 +18,13 @@ The docker container compiles the source code and run the scalachain node. For t To run a branch of the remote repo simply pass the name of the branch as the argument. e.g.: -```docker run --name scalachain-dev -p 8080:8080 elleflorio/scalachain development``` +```docker run --name scalachain-dev -p 8000:8000 elleflorio/scalachain development``` This will run the code of the branch ```development```. -To run the local source code mounting the local folder to the container /development one, use the ```-v /your/source/folder:/development``` and pass ```local``` as the argument to the script. e.g.: +To run the local source code mounting the local folder to the container ```/development``` one, use the ```-v /your/source/folder:/development``` and pass ```local``` as the argument to the script. You can optionally specify a folder inside the container where the source in the volume should be mounted, e.g. ```/tmp/scalachain```. The source will be automatically copied inside the ```/development``` folder. This is useful when you are running multiple containers in the same machine. This is a complete example: -```docker run --name scalachain -p 8080:8080 -v ~/Development/scala/scalachain/:/development elleflorio/scalachain local``` +```docker run --name scalachain -p 8080:8080 -v ~/Development/scala/scalachain/:/tmp/scalachain elleflorio/scalachain local /tmp/scalachain``` This will run the code contained in the folder ```~/Development/scala/scalachain/```. diff --git a/docker/init.sh b/docker/init.sh index 8a73aeb..e6337bb 100644 --- a/docker/init.sh +++ b/docker/init.sh @@ -1,9 +1,15 @@ #! /bin/sh SOURCE=$1 -if [ $SOURCE == "local" ] +SOURCE_FOLDER=$2 +if [[ ${SOURCE} == "local" ]] then echo "Running local source code" - cd /development + if ! [[ -z "$SOURCE_FOLDER" ]] + then + echo "copying local sources from $SOURCE_FOLDER to /development" + cp -r ${SOURCE_FOLDER}/. /development + fi + cd /development else echo "Runnig repo souce code" cd scalachain diff --git a/img/scalachain_node.drawio b/img/scalachain_node.drawio new file mode 100644 index 0000000..dbe7398 --- /dev/null +++ b/img/scalachain_node.drawio @@ -0,0 +1 @@ +3Vpdc+I2FP01flzGlixjHgNJt53ZtJlmO90+KpbAygqLkUWA/vrKWMYWMqxJAbPhgdG9lmTpnHuPrz88OJmvP0u8SB8FodwDPll78N4DYIhi/V84NqUDxaB0zCQjpSuoHc/sX2qcvvEuGaG51VEJwRVb2M5EZBlNlOXDUoqV3W0quH3WBZ5Rx/GcYO56/2ZEpaU3BsPa/ytls7Q6cxCNyiNzXHU2O8lTTMSq4YIPHpxIIVTZmq8nlBfYVbiU4345cHS3MEkz1WXAK3z9dP9HToIxeUpJ8tfXMAw/mW28Yb40GzaLVZsKASmWGaHFJL4Hx6uUKfq8wElxdKUp175Uzbm2At2cMs4nggup7UxkutPYnIFKRdcHlx7sANGBRMWcKrnRXcwAMDIYmiACQ2OvakqQb3xpgw4QGyc2YTDbzV0jpRsGrBOAq+KzARQlOnKMKaRKxUxkmD/U3rENZd3nixALA+ArVWpj0gAvlbDhpWumvjXa/xRTDQAy5v3aTL01NpWR6Q1/axrNYYVdj9ta1UCC83S72mBHY7HH4yRqSMRSJvQYdiaPsZxRdaQfag8KSTlW7M1ex/kJdjPhigT7A9SgOOjI7wBZDP+A3h4Yhb0y6mjdhC9zReUjzrSMSw9EXC9+/FK0ZkXrLlFayQ4LYnCaIHoAEkzjaaL9uZLiO20ciZKYvkzPo5fQR5ZewtjVywC06GV0KbkEDvaPLCsg9y+AMaIxCdswjsELjKLzYBwhG+Owd4zhofj+wvR/diTA990/IR37Id8/HaFDx7jY/0eKedA7yEHgwHjNq7RVhg07lmGBXYYN+yrDUMeL9oGYuNJFG95Onf2+MvtH9PbAaNQro+HtMPrOO6cbpDTslVJ0O5R2VeHbpxT0SSlyqpffBaF17XLZkvEqd0m7EvFm7pIit2TkIvmepJhlH6hs7B/oUa9V4xWe7VyqZow7alevj+5iJ43+fHj+WiTQ028nKdeJD7x12kynU5C0KheJXiJ0poTaV6625+GtCQUvdh/mSlfxEgVX0uXCXl5O9qDXkCgbXwdHf/vzWl81YM5mmTYTjaO+0YbjAmWm13FnDswZIdtkbmPV5v0MNHV5bTFEAzAKg2AYhUMfxTFsIQ1dijQ3UVxdzMhd8RqtQJXjPGeJzc+7hMZFzHqR40JQ+TrrhznDk2D6xDtCgj1CINjLh1L/zKgaXWcigNAAHZ+qlEhnqi1Ru43/D+5GDncTMZ8vMx3uiuZbSDWipyaYSaTbz61gXwLj0Mmt0TUVsMqdpgIuF1S+sbytXhbTDleiD8sWRK4SXpct92H5ydJ3+2oH9kWqSpLT9S4+PtG71U6b9ScQZff6OxL48B8= \ No newline at end of file diff --git a/img/scalachain_node.png b/img/scalachain_node.png index fd23013..e9fb011 100644 Binary files a/img/scalachain_node.png and b/img/scalachain_node.png differ diff --git a/project/build.properties b/project/build.properties index 16eecf5..c03cdb8 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.1.6 \ No newline at end of file +sbt.version = 1.2.7 \ No newline at end of file diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 2595b52..a1df634 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -1,9 +1,63 @@ -akka.persistence.journal.plugin = "akka.persistence.journal.leveldb" -akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local" +akka { + actor { + provider = "cluster" + } + persistence { + journal { + plugin = "akka.persistence.journal.leveldb" + leveldb { + dir = ${scalachain.persistence.dir} + # DO NOT USE THIS IN PRODUCTION !!! + # See also https://github.com/typesafehub/activator/issues/287 + native = false + } + } + snapshot-store { + plugin = "akka.persistence.snapshot-store.local" + local.dir = ${scalachain.snapshots.dir} + } + } + remote { + log-remote-lifecycle-events = on + netty.tcp { + hostname = ${clustering.ip} + port = ${clustering.port} + } + } + cluster { + seed-nodes = [ + "akka.tcp://"${clustering.cluster.name}"@"${clustering.seed-ip}":"${clustering.seed-port} + ] + auto-down-unreachable-after = 10s + } +} +http { + ip = "127.0.0.1" + ip = ${?SERVER_IP} -akka.persistence.journal.leveldb.dir = "persistence/journal" -akka.persistence.snapshot-store.local.dir = "persistence/snapshots" + port = 8000 + port = ${?SERVER_PORT} +} +clustering { + ip = "127.0.0.1" + ip = ${?CLUSTER_IP} -# DO NOT USE THIS IN PRODUCTION !!! -# See also https://github.com/typesafehub/activator/issues/287 -akka.persistence.journal.leveldb.native = false \ No newline at end of file + port = 2552 + port = ${?CLUSTER_PORT} + + seed-ip = "127.0.0.1" + seed-ip = ${?CLUSTER_SEED_IP} + + seed-port = 2552 + seed-port = ${?CLUSTER_SEED_PORT} + + cluster.name = "scalachain" +} +scalachain { + node.id = "node0" + node.id = ${?SCALACHAIN_NODE_ID} + persistence.dir = "persistence/journal" + persistence.dir = ${?SCALACHAIN_PERSISTENCE_DIR} + snapshots.dir = "persistence/snapshots" + snapshots.dir = ${?SCALACHAIN_SNAPSHOTS_DIR} +} \ No newline at end of file diff --git a/src/main/scala/com/elleflorio/scalachain/Server.scala b/src/main/scala/com/elleflorio/scalachain/Server.scala index 4185292..ffac2c3 100644 --- a/src/main/scala/com/elleflorio/scalachain/Server.scala +++ b/src/main/scala/com/elleflorio/scalachain/Server.scala @@ -1,31 +1,36 @@ package com.elleflorio.scalachain import akka.actor.{ActorRef, ActorSystem} +import akka.cluster.pubsub.DistributedPubSub import akka.http.scaladsl.Http import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Directives._ import akka.stream.ActorMaterializer import com.elleflorio.scalachain.actor.Node import com.elleflorio.scalachain.api.NodeRoutes +import com.elleflorio.scalachain.cluster.ClusterManager +import com.typesafe.config.{Config, ConfigFactory} import scala.concurrent.Await import scala.concurrent.duration.Duration object Server extends App with NodeRoutes { - val address = if (args.length > 0) args(0) else "localhost" - val port = if (args.length > 1) args(1).toInt else 8080 - implicit val system: ActorSystem = ActorSystem("scalachain") - implicit val materializer: ActorMaterializer = ActorMaterializer() - val node: ActorRef = system.actorOf(Node.props("scalaChainNode0")) + val config: Config = ConfigFactory.load() + val address = config.getString("http.ip") + val port = config.getInt("http.port") + val nodeId = config.getString("scalachain.node.id") lazy val routes: Route = statusRoutes ~ transactionRoutes ~ mineRoutes - Http().bindAndHandle(routes, address, port) + val clusterManager: ActorRef = system.actorOf(ClusterManager.props(nodeId), "clusterManager") + val mediator: ActorRef = DistributedPubSub(system).mediator + val node: ActorRef = system.actorOf(Node.props(nodeId, mediator), "node") + Http().bindAndHandle(routes, address, port) println(s"Server online at http://$address:$port/") Await.result(system.whenTerminated, Duration.Inf) diff --git a/src/main/scala/com/elleflorio/scalachain/actor/Blockchain.scala b/src/main/scala/com/elleflorio/scalachain/actor/Blockchain.scala index a1f61c3..16bacb2 100644 --- a/src/main/scala/com/elleflorio/scalachain/actor/Blockchain.scala +++ b/src/main/scala/com/elleflorio/scalachain/actor/Blockchain.scala @@ -3,15 +3,22 @@ package com.elleflorio.scalachain.actor import akka.actor.{ActorLogging, Props} import akka.persistence._ import com.elleflorio.scalachain.blockchain.{Chain, ChainLink, Transaction} +import Blockchain._ object Blockchain { + sealed trait BlockchainEvent - case class AddBlockEvent(transactions: List[Transaction], proof: Long) extends BlockchainEvent + + case class AddBlockEvent(transactions: List[Transaction], proof: Long, timestamp: Long) extends BlockchainEvent sealed trait BlockchainCommand - case class AddBlockCommand(transactions: List[Transaction], proof: Long) extends BlockchainCommand + + case class AddBlockCommand(transactions: List[Transaction], proof: Long, timestamp: Long) extends BlockchainCommand + case object GetChain extends BlockchainCommand + case object GetLastHash extends BlockchainCommand + case object GetLastIndex extends BlockchainCommand case class State(chain: Chain) @@ -20,7 +27,6 @@ object Blockchain { } class Blockchain(chain: Chain, nodeId: String) extends PersistentActor with ActorLogging { - import Blockchain._ var state = State(chain) @@ -38,8 +44,8 @@ class Blockchain(chain: Chain, nodeId: String) extends PersistentActor with Acto override def receiveCommand: Receive = { case SaveSnapshotSuccess(metadata) => log.info(s"Snapshot ${metadata.sequenceNr} saved successfully") case SaveSnapshotFailure(metadata, reason) => log.error(s"Error saving snapshot ${metadata.sequenceNr}: ${reason.getMessage}") - case AddBlockCommand(transactions : List[Transaction], proof: Long) => { - persist(AddBlockEvent(transactions, proof)) {event => + case AddBlockCommand(transactions: List[Transaction], proof: Long, timestamp: Long) => { + persist(AddBlockEvent(transactions, proof, timestamp)) { event => updateState(event) } @@ -49,17 +55,16 @@ class Blockchain(chain: Chain, nodeId: String) extends PersistentActor with Acto sender() ! state.chain.index } } - case AddBlockCommand(_, _) => log.error("invalid add block command") + case AddBlockCommand(_, _, _) => log.error("invalid add block command") case GetChain => sender() ! state.chain case GetLastHash => sender() ! state.chain.hash case GetLastIndex => sender() ! state.chain.index } def updateState(event: BlockchainEvent) = event match { - case AddBlockEvent(transactions, proof) => - { - state = State(ChainLink(state.chain.index + 1, proof, transactions) :: state.chain) - log.info(s"Added block ${state.chain.index} containing ${transactions.size} transactions") - } + case AddBlockEvent(transactions, proof, timestamp) => { + state = State(ChainLink(state.chain.index + 1, proof, transactions, timestamp = timestamp) :: state.chain) + log.info(s"Added block ${state.chain.index} containing ${transactions.size} transactions") + } } } diff --git a/src/main/scala/com/elleflorio/scalachain/actor/Broker.scala b/src/main/scala/com/elleflorio/scalachain/actor/Broker.scala index f8232cd..f735fd1 100644 --- a/src/main/scala/com/elleflorio/scalachain/actor/Broker.scala +++ b/src/main/scala/com/elleflorio/scalachain/actor/Broker.scala @@ -1,19 +1,28 @@ package com.elleflorio.scalachain.actor import akka.actor.{Actor, ActorLogging, Props} +import akka.cluster.pubsub.DistributedPubSubMediator.{Subscribe, SubscribeAck} import com.elleflorio.scalachain.blockchain.Transaction +import Broker._ object Broker { + sealed trait BrokerMessage + + case class TransactionMessage(transaction: Transaction) extends BrokerMessage + case class AddTransaction(transaction: Transaction) extends BrokerMessage + + case class DiffTransaction(transactions: List[Transaction]) extends BrokerMessage + case object GetTransactions extends BrokerMessage + case object Clear extends BrokerMessage val props: Props = Props(new Broker) } class Broker extends Actor with ActorLogging { - import Broker._ var pending: List[Transaction] = List() @@ -26,9 +35,14 @@ class Broker extends Actor with ActorLogging { log.info(s"Getting pending transactions") sender() ! pending } + case DiffTransaction(externalTransactions) => { + pending = pending diff externalTransactions + } case Clear => { pending = List() log.info("Clear pending transaction List") } + case SubscribeAck(Subscribe("transaction", None, `self`)) => + log.info("subscribing") } } diff --git a/src/main/scala/com/elleflorio/scalachain/actor/Miner.scala b/src/main/scala/com/elleflorio/scalachain/actor/Miner.scala index 2680a21..909c08f 100644 --- a/src/main/scala/com/elleflorio/scalachain/actor/Miner.scala +++ b/src/main/scala/com/elleflorio/scalachain/actor/Miner.scala @@ -2,32 +2,37 @@ package com.elleflorio.scalachain.actor import akka.actor.Status.{Failure, Success} import akka.actor.{Actor, ActorLogging, Props} +import com.elleflorio.scalachain.actor.Miner._ import com.elleflorio.scalachain.exception.{InvalidProofException, MinerBusyException} import com.elleflorio.scalachain.proof.ProofOfWork import scala.concurrent.Future object Miner { + sealed trait MinerMessage + case class Validate(hash: String, proof: Long) extends MinerMessage + case class Mine(hash: String) extends MinerMessage + case object Ready extends MinerMessage val props: Props = Props(new Miner) } -class Miner extends Actor with ActorLogging{ - import Miner._ +class Miner extends Actor with ActorLogging { + import context._ def validate: Receive = { case Validate(hash, proof) => { log.info(s"Validating proof $proof") - if (ProofOfWork.validProof(hash, proof)){ + if (ProofOfWork.validProof(hash, proof)) { log.info("proof is valid!") sender() ! Success } - else{ + else { log.info("proof is not valid") sender() ! Failure(new InvalidProofException(hash, proof)) } @@ -37,7 +42,9 @@ class Miner extends Actor with ActorLogging{ def ready: Receive = validate orElse { case Mine(hash) => { log.info(s"Mining hash $hash...") - val proof = Future {(ProofOfWork.proofOfWork(hash))} + val proof = Future { + ProofOfWork.proofOfWork(hash) + } sender() ! proof become(busy) } diff --git a/src/main/scala/com/elleflorio/scalachain/actor/Node.scala b/src/main/scala/com/elleflorio/scalachain/actor/Node.scala index 891da4a..4bddd46 100644 --- a/src/main/scala/com/elleflorio/scalachain/actor/Node.scala +++ b/src/main/scala/com/elleflorio/scalachain/actor/Node.scala @@ -1,11 +1,12 @@ package com.elleflorio.scalachain.actor -import com.elleflorio.scalachain.actor.Blockchain.{AddBlockCommand, GetChain, GetLastHash, GetLastIndex} -import com.elleflorio.scalachain.actor.Broker.Clear -import com.elleflorio.scalachain.actor.Miner.{Ready, Validate} -import akka.actor.{Actor, ActorLogging, Props} +import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import akka.cluster.pubsub.DistributedPubSubMediator.{Publish, Subscribe} import akka.pattern.ask import akka.util.Timeout +import com.elleflorio.scalachain.actor.Blockchain.{AddBlockCommand, GetChain, GetLastHash, GetLastIndex} +import com.elleflorio.scalachain.actor.Miner.{Ready, Validate} +import com.elleflorio.scalachain.actor.Node._ import com.elleflorio.scalachain.blockchain._ import scala.concurrent.ExecutionContext.Implicits.global @@ -19,10 +20,14 @@ object Node { case class AddTransaction(transaction: Transaction) extends NodeMessage + case class TransactionMessage(transaction: Transaction, nodeId: String) extends NodeMessage + case class CheckPowSolution(solution: Long) extends NodeMessage case class AddBlock(proof: Long) extends NodeMessage + case class AddBlockMessage(proof: Long, tansactions: List[Transaction], timestamp: Long) extends NodeMessage + case object GetTransactions extends NodeMessage case object Mine extends NodeMessage @@ -35,53 +40,61 @@ object Node { case object GetLastBlockHash extends NodeMessage - def props(nodeId: String): Props = Props(new Node(nodeId)) + def props(nodeId: String, mediator: ActorRef): Props = Props(new Node(nodeId, mediator)) def createCoinbaseTransaction(nodeId: String) = Transaction("coinbase", nodeId, 100) } -class Node(nodeId: String) extends Actor with ActorLogging { - - import Node._ +class Node(nodeId: String, mediator: ActorRef) extends Actor with ActorLogging { implicit lazy val timeout = Timeout(5.seconds) - val broker = context.actorOf(Broker.props) - val miner = context.actorOf(Miner.props) - val blockchain = context.actorOf(Blockchain.props(EmptyChain, nodeId)) + mediator ! Subscribe("newBlock", self) + mediator ! Subscribe("transaction", self) + + val broker: ActorRef = context.actorOf(Broker.props) + val miner: ActorRef = context.actorOf(Miner.props) + val blockchain: ActorRef = context.actorOf(Blockchain.props(EmptyChain, nodeId)) miner ! Ready override def receive: Receive = { + case TransactionMessage(transaction, messageNodeId) => { + log.info(s"Received transaction message from $messageNodeId") + if (messageNodeId != nodeId) { + broker ! Broker.AddTransaction(transaction) + } + } + case AddTransaction(transaction) => { val node = sender() - broker ! Broker.AddTransaction(transaction) (blockchain ? GetLastIndex).mapTo[Int] onComplete { - case Success(index) => node ! (index + 1) + case Success(index) => + broker ! Broker.AddTransaction(transaction) + mediator ! Publish("transaction", TransactionMessage(transaction, nodeId)) + node ! (index + 1) case Failure(e) => node ! akka.actor.Status.Failure(e) } } - case CheckPowSolution(solution) => { + + case CheckPowSolution(solution) => val node = sender() (blockchain ? GetLastHash).mapTo[String] onComplete { case Success(hash: String) => miner.tell(Validate(hash, solution), node) case Failure(e) => node ! akka.actor.Status.Failure(e) } - } - case AddBlock(proof) => { + + case AddBlockMessage(proof, transactions, timestamp) => val node = sender() (self ? CheckPowSolution(proof)) onComplete { - case Success(_) => { - (broker ? Broker.GetTransactions).mapTo[List[Transaction]] onComplete { - case Success(transactions) => blockchain.tell(AddBlockCommand(transactions, proof), node) - case Failure(e) => node ! akka.actor.Status.Failure(e) - } - broker ! Clear - } case Failure(e) => node ! akka.actor.Status.Failure(e) + case Success(_) => + broker ! Broker.DiffTransaction(transactions) + blockchain.tell(AddBlockCommand(transactions, proof, timestamp), node) + miner ! Ready } - } - case Mine => { + + case Mine => val node = sender() (blockchain ? GetLastHash).mapTo[String] onComplete { case Success(hash) => (miner ? Miner.Mine(hash)).mapTo[Future[Long]] onComplete { @@ -90,7 +103,7 @@ class Node(nodeId: String) extends Actor with ActorLogging { } case Failure(e) => node ! akka.actor.Status.Failure(e) } - } + case GetTransactions => broker forward Broker.GetTransactions case GetStatus => blockchain forward GetChain case GetLastBlockIndex => blockchain forward GetLastIndex @@ -99,11 +112,14 @@ class Node(nodeId: String) extends Actor with ActorLogging { def waitForSolution(solution: Future[Long]) = Future { solution onComplete { - case Success(proof) => { + case Success(proof) => + val node = sender() + val ts = System.currentTimeMillis() broker ! Broker.AddTransaction(createCoinbaseTransaction(nodeId)) - self ! AddBlock(proof) - miner ! Ready - } + (broker ? Broker.GetTransactions).mapTo[List[Transaction]] onComplete { + case Success(transactions) => mediator ! Publish("newBlock", AddBlockMessage(proof, transactions, ts)) + case Failure(e) => node ! akka.actor.Status.Failure(e) + } case Failure(e) => log.error(s"Error finding PoW solution: ${e.getMessage}") } } diff --git a/src/main/scala/com/elleflorio/scalachain/api/NodeRoutes.scala b/src/main/scala/com/elleflorio/scalachain/api/NodeRoutes.scala index a67ed74..140b073 100644 --- a/src/main/scala/com/elleflorio/scalachain/api/NodeRoutes.scala +++ b/src/main/scala/com/elleflorio/scalachain/api/NodeRoutes.scala @@ -9,6 +9,7 @@ import akka.http.scaladsl.server.Route import akka.pattern.ask import akka.util.Timeout import com.elleflorio.scalachain.blockchain.{Chain, Transaction} +import com.elleflorio.scalachain.cluster.ClusterManager.GetMembers import com.elleflorio.scalachain.utils.JsonSupport._ import scala.concurrent.Future @@ -19,6 +20,7 @@ trait NodeRoutes extends SprayJsonSupport { implicit def system: ActorSystem def node: ActorRef + def clusterManager: ActorRef implicit lazy val timeout = Timeout(5.seconds) @@ -33,6 +35,20 @@ trait NodeRoutes extends SprayJsonSupport { } } ) + }, + pathPrefix("members") { + concat( + pathEnd { + concat( + get { + val membersFuture: Future[List[String]] = (clusterManager ? GetMembers).mapTo[List[String]] + onSuccess(membersFuture) { members => + complete(StatusCodes.OK, members) + } + } + ) + } + ) } ) } diff --git a/src/main/scala/com/elleflorio/scalachain/blockchain/Chain.scala b/src/main/scala/com/elleflorio/scalachain/blockchain/Chain.scala index 7e07681..3f579a7 100644 --- a/src/main/scala/com/elleflorio/scalachain/blockchain/Chain.scala +++ b/src/main/scala/com/elleflorio/scalachain/blockchain/Chain.scala @@ -15,7 +15,7 @@ sealed trait Chain { val timestamp: Long def ::(link: Chain): Chain = link match { - case l:ChainLink => ChainLink(l.index, l.proof, l.values, this.hash, this) + case l:ChainLink => ChainLink(l.index, l.proof, l.values, this.hash, l.timestamp, this) case _ => throw new InvalidParameterException("Cannot add invalid link to chain") } } @@ -25,12 +25,12 @@ object Chain { if (b.isEmpty) EmptyChain else { val link = b.head.asInstanceOf[ChainLink] - ChainLink(link.index, link.proof, link.values, link.previousHash, apply(b.tail: _*)) + ChainLink(link.index, link.proof, link.values, link.previousHash, link.timestamp, apply(b.tail: _*)) } } } -case class ChainLink(index: Int, proof: Long, values: List[Transaction], previousHash: String = "", tail: Chain = EmptyChain, timestamp: Long = System.currentTimeMillis()) extends Chain { +case class ChainLink(index: Int, proof: Long, values: List[Transaction], previousHash: String = "", timestamp: Long = System.currentTimeMillis(), tail: Chain = EmptyChain) extends Chain { val hash = Crypto.sha256Hash(this.toJson.toString) } @@ -39,5 +39,5 @@ case object EmptyChain extends Chain { val hash = "1" val values = Nil val proof = 100L - val timestamp = System.currentTimeMillis() + val timestamp = 0L } diff --git a/src/main/scala/com/elleflorio/scalachain/cluster/ClusterListener.scala b/src/main/scala/com/elleflorio/scalachain/cluster/ClusterListener.scala new file mode 100644 index 0000000..c78623a --- /dev/null +++ b/src/main/scala/com/elleflorio/scalachain/cluster/ClusterListener.scala @@ -0,0 +1,30 @@ +package com.elleflorio.scalachain.cluster + +import akka.actor.{Actor, ActorLogging, Props} +import akka.cluster.Cluster +import akka.cluster.ClusterEvent._ + +object ClusterListener { + def props(nodeId: String, cluster: Cluster) = Props(new ClusterListener(nodeId, cluster)) +} + +class ClusterListener(nodeId: String, cluster: Cluster) extends Actor with ActorLogging { + + override def preStart(): Unit = { + cluster.subscribe(self, initialStateMode = InitialStateAsEvents, + classOf[MemberEvent], classOf[UnreachableMember]) + } + + override def postStop(): Unit = cluster.unsubscribe(self) + + def receive = { + case MemberUp(member) => + log.info("Node {} - Member is Up: {}", nodeId, member.address) + case UnreachableMember(member) => + log.info(s"Node {} - Member detected as unreachable: {}", nodeId, member) + case MemberRemoved(member, previousStatus) => + log.info(s"Node {} - Member is Removed: {} after {}", + nodeId, member.address, previousStatus) + case _: MemberEvent => // ignore + } +} diff --git a/src/main/scala/com/elleflorio/scalachain/cluster/ClusterManager.scala b/src/main/scala/com/elleflorio/scalachain/cluster/ClusterManager.scala new file mode 100644 index 0000000..c60669d --- /dev/null +++ b/src/main/scala/com/elleflorio/scalachain/cluster/ClusterManager.scala @@ -0,0 +1,28 @@ +package com.elleflorio.scalachain.cluster + +import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import akka.cluster.pubsub.DistributedPubSub +import akka.cluster.{Cluster, MemberStatus} +import com.elleflorio.scalachain.cluster.ClusterManager.GetMembers + +object ClusterManager { + + sealed trait ClusterMessage + case object GetMembers extends ClusterMessage + + def props(nodeId: String) = Props(new ClusterManager(nodeId)) +} + +class ClusterManager(nodeId: String) extends Actor with ActorLogging { + + val cluster: Cluster = Cluster(context.system) + val listener: ActorRef = context.actorOf(ClusterListener.props(nodeId, cluster), "clusterListener") + + override def receive: Receive = { + case GetMembers => { + sender() ! cluster.state.members.filter(_.status == MemberStatus.up) + .map(_.address.toString) + .toList + } + } +} diff --git a/src/main/scala/com/elleflorio/scalachain/proof/ProofOfWork.scala b/src/main/scala/com/elleflorio/scalachain/proof/ProofOfWork.scala index fd75130..7eb2d29 100644 --- a/src/main/scala/com/elleflorio/scalachain/proof/ProofOfWork.scala +++ b/src/main/scala/com/elleflorio/scalachain/proof/ProofOfWork.scala @@ -1,9 +1,8 @@ package com.elleflorio.scalachain.proof -import spray.json._ -import DefaultJsonProtocol._ import com.elleflorio.scalachain.crypto.Crypto -import spray.json.pimpAny +import spray.json.DefaultJsonProtocol._ +import spray.json._ import scala.annotation.tailrec diff --git a/src/main/scala/com/elleflorio/scalachain/utils/JsonSupport.scala b/src/main/scala/com/elleflorio/scalachain/utils/JsonSupport.scala index 6943f19..c825b4a 100644 --- a/src/main/scala/com/elleflorio/scalachain/utils/JsonSupport.scala +++ b/src/main/scala/com/elleflorio/scalachain/utils/JsonSupport.scala @@ -22,9 +22,9 @@ object JsonSupport extends DefaultJsonProtocol { } implicit object ChainLinkJsonFormat extends RootJsonFormat[ChainLink] { - override def read(json: JsValue): ChainLink = json.asJsObject.getFields("index", "proof", "values", "previousHash", "tail", "timestamp") match { - case Seq(JsNumber(index), JsNumber(proof), values, JsString(previousHash), tail, JsNumber(timestamp)) => - ChainLink(index.toInt, proof.toLong, values.convertTo[List[Transaction]], previousHash, tail.convertTo(ChainJsonFormat), timestamp.toLong) + override def read(json: JsValue): ChainLink = json.asJsObject.getFields("index", "proof", "values", "previousHash", "timestamp", "tail") match { + case Seq(JsNumber(index), JsNumber(proof), values, JsString(previousHash), JsNumber(timestamp), tail) => + ChainLink(index.toInt, proof.toLong, values.convertTo[List[Transaction]], previousHash, timestamp.toLong, tail.convertTo(ChainJsonFormat)) case _ => throw new DeserializationException("Cannot deserialize: Chainlink expected") } @@ -33,8 +33,8 @@ object JsonSupport extends DefaultJsonProtocol { "proof" -> JsNumber(obj.proof), "values" -> JsArray(obj.values.map(_.toJson).toVector), "previousHash" -> JsString(obj.previousHash), - "tail" -> ChainJsonFormat.write(obj.tail), - "timestamp" -> JsNumber(obj.timestamp) + "timestamp" -> JsNumber(obj.timestamp), + "tail" -> ChainJsonFormat.write(obj.tail) ) } diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf index fbd8740..0439b48 100644 --- a/src/test/resources/application.conf +++ b/src/test/resources/application.conf @@ -3,4 +3,11 @@ akka { journal.plugin = "inmemory-journal" snapshot-store.plugin = "inmemory-snapshot-store" } + remote { + log-remote-lifecycle-events = on + netty.tcp { + hostname = 127.0.0.1 + port = 0 + } + } } \ No newline at end of file diff --git a/src/test/scala/com/elleflorio/scalachain/actor/BlockchainTest.scala b/src/test/scala/com/elleflorio/scalachain/actor/BlockchainTest.scala index a076068..92dee95 100644 --- a/src/test/scala/com/elleflorio/scalachain/actor/BlockchainTest.scala +++ b/src/test/scala/com/elleflorio/scalachain/actor/BlockchainTest.scala @@ -8,7 +8,7 @@ import org.scalatest.{BeforeAndAfterAll, FlatSpecLike, Matchers} import scala.concurrent.duration._ -class BlockchainTest(_system: ActorSystem) extends TestKit(_system) +class BlockchainTest(sys: ActorSystem) extends TestKit(sys) with ImplicitSender with Matchers with FlatSpecLike @@ -35,7 +35,7 @@ class BlockchainTest(_system: ActorSystem) extends TestKit(_system) val transactions = List(Transaction("a", "b", 1L)) val proof = 1L - blockchain ! AddBlockCommand(transactions, proof) + blockchain ! AddBlockCommand(transactions, proof, System.currentTimeMillis()) expectMsg(1000 millis, 1) blockchain ! GetLastIndex diff --git a/src/test/scala/com/elleflorio/scalachain/actor/BrokerTest.scala b/src/test/scala/com/elleflorio/scalachain/actor/BrokerTest.scala index 23c4a27..5e332b3 100644 --- a/src/test/scala/com/elleflorio/scalachain/actor/BrokerTest.scala +++ b/src/test/scala/com/elleflorio/scalachain/actor/BrokerTest.scala @@ -1,33 +1,35 @@ package com.elleflorio.scalachain.actor +import akka.actor.{ActorRef, ActorSystem} +import akka.cluster.pubsub.DistributedPubSub +import akka.testkit.{ImplicitSender, TestKit} import com.elleflorio.scalachain.actor.Broker.{AddTransaction, Clear, GetTransactions} -import akka.actor.ActorSystem -import akka.testkit.{ImplicitSender, TestKit, TestProbe} import com.elleflorio.scalachain.blockchain.Transaction import org.scalatest.{BeforeAndAfterAll, FlatSpecLike, Matchers} import scala.concurrent.duration._ -class BrokerTest(_system: ActorSystem) extends TestKit(_system) +class BrokerTest(sys: ActorSystem) extends TestKit(sys) with ImplicitSender with Matchers with FlatSpecLike with BeforeAndAfterAll { def this() = this(ActorSystem("broker-test")) + val mediator: ActorRef = DistributedPubSub(this.system).mediator override def afterAll: Unit = { shutdown(system) } - "A Broker com.elleflorio.scalachain.actor" should "start with an empty list of transactions" in { + "A Broker Actor" should "start with an empty list of transactions" in { val broker = system.actorOf(Broker.props) broker ! GetTransactions expectMsg(500 millis, List()) } - "A Broker com.elleflorio.scalachain.actor" should "return the correct list of added transactions" in { + "A Broker Actor" should "return the correct list of added transactions" in { val broker = system.actorOf(Broker.props) val transaction1 = Transaction("A", "B", 100) val transaction2 = Transaction("C", "D", 1000) @@ -39,7 +41,7 @@ class BrokerTest(_system: ActorSystem) extends TestKit(_system) expectMsg(500 millis, List(transaction2, transaction1)) } - "A Broker com.elleflorio.scalachain.actor" should "clear the transaction lists when requested" in { + "A Broker Actor" should "clear the transaction lists when requested" in { val broker = system.actorOf(Broker.props) val transaction1 = Transaction("A", "B", 100) val transaction2 = Transaction("C", "D", 1000) diff --git a/src/test/scala/com/elleflorio/scalachain/actor/MinerTest.scala b/src/test/scala/com/elleflorio/scalachain/actor/MinerTest.scala index 6c91ce0..7909d76 100644 --- a/src/test/scala/com/elleflorio/scalachain/actor/MinerTest.scala +++ b/src/test/scala/com/elleflorio/scalachain/actor/MinerTest.scala @@ -9,19 +9,19 @@ import org.scalatest.{BeforeAndAfterAll, FlatSpecLike, Matchers} import scala.concurrent.Future import scala.concurrent.duration._ -class MinerTest(_system: ActorSystem) extends TestKit(_system) +class MinerTest(sys: ActorSystem) extends TestKit(sys) with ImplicitSender with Matchers with FlatSpecLike with BeforeAndAfterAll { - def this() = this(ActorSystem("broker-test")) + def this() = this(ActorSystem("miner-test")) override def afterAll: Unit = { shutdown(system) } - "A Miner com.elleflorio.scalachain.actor" should "be ready when requested" in { + "A Miner Actor" should "be ready when requested" in { val miner = system.actorOf(Miner.props) miner ! Ready @@ -30,7 +30,7 @@ class MinerTest(_system: ActorSystem) extends TestKit(_system) expectMsg(500 millis, Success("OK")) } - "A Miner com.elleflorio.scalachain.actor" should "be busy while mining a new block" in { + "A Miner Actor" should "be busy while mining a new block" in { val miner = system.actorOf(Miner.props) miner ! Ready diff --git a/src/test/scala/com/elleflorio/scalachain/blockchain/ChainTest.scala b/src/test/scala/com/elleflorio/scalachain/blockchain/ChainTest.scala index e5c7951..4744ecc 100644 --- a/src/test/scala/com/elleflorio/scalachain/blockchain/ChainTest.scala +++ b/src/test/scala/com/elleflorio/scalachain/blockchain/ChainTest.scala @@ -11,11 +11,11 @@ class ChainTest extends Matchers val empty = Chain() empty.index should be (0) - val link = new ChainLink(1, 1, List(transaction), "abc", empty) + val link = new ChainLink(1, 1, List(transaction), "abc", tail = empty) val justOne = Chain(link) justOne.index should be (1) - val link2 = new ChainLink(2, 1, List(transaction), "abc", empty) + val link2 = new ChainLink(2, 1, List(transaction), "abc", tail = empty) val justTwo = link2 :: link justTwo.index should be (2) diff --git a/src/test/scala/com/elleflorio/scalachain/proof/ProofOfWorkTest.scala b/src/test/scala/com/elleflorio/scalachain/proof/ProofOfWorkTest.scala index bcfcecd..dc23a1b 100644 --- a/src/test/scala/com/elleflorio/scalachain/proof/ProofOfWorkTest.scala +++ b/src/test/scala/com/elleflorio/scalachain/proof/ProofOfWorkTest.scala @@ -5,7 +5,7 @@ import org.scalatest.{FlatSpecLike, Matchers} class ProofOfWorkTest extends Matchers with FlatSpecLike { - "Validation of com.elleflorio.scalachain.proof" should "correctly validate proofs" in { + "Validation of proof" should "correctly validate proofs" in { val lastHash = "1" val correctProof = 7178 val wrongProof = 0