├── .travis.yml ├── .gitignore ├── Dockerfile ├── src └── blockchain │ ├── proof.clj │ ├── utils.clj │ ├── api.clj │ ├── core.clj │ ├── nodes.clj │ └── impl.clj ├── project.clj ├── LICENSE ├── test └── blockchain │ └── api_test.clj └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM clojure:alpine 2 | 3 | RUN mkdir -p /usr/src/app 4 | WORKDIR /usr/src/app 5 | COPY target/blockchain-0.1.1-BURNINGASS-standalone.jar /usr/src/app/ 6 | 7 | EXPOSE 8090 8 | 9 | CMD ["java", "-jar", "blockchain-0.1.1-BURNINGASS-standalone.jar"] 10 | -------------------------------------------------------------------------------- /src/blockchain/proof.clj: -------------------------------------------------------------------------------- 1 | (ns blockchain.proof 2 | (:require [blockchain.utils :as utils])) 3 | 4 | (defn valid-proof 5 | [last_proof proof] 6 | (= 7 | (subs (.toString (.reverse (StringBuilder. (utils/sha256hash (str (str last_proof) (str proof)))))) 0 4) 8 | "0000" )) 9 | 10 | (defn proof-of-work 11 | [last_proof] 12 | (loop [x 0] (if (valid-proof last_proof x) x (recur (inc x))))) 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject blockchain "0.1.1-BURNINGASS" 2 | :description "A simple clojure implementation of a blockchain" 3 | :url "https://github.com/paoloo/blockchain" 4 | :min-lein-version "2.0.0" 5 | :dependencies [[org.clojure/clojure "1.8.0"] 6 | [compojure "1.5.1"] 7 | [clj-http "3.7.0"] 8 | [cheshire "5.8.0"] 9 | [ring/ring-defaults "0.2.1"] 10 | [ring/ring-json "0.4.0"]] 11 | 12 | :plugins [[lein-ring "0.12.5"]] 13 | :ring {:handler blockchain.api/app 14 | :port 8090} 15 | :profiles 16 | {:dev {:dependencies [[javax.servlet/servlet-api "2.5"] 17 | [ring/ring-mock "0.3.0"]]}}) 18 | -------------------------------------------------------------------------------- /src/blockchain/utils.clj: -------------------------------------------------------------------------------- 1 | (ns blockchain.utils 2 | (:import [java.security.MessageDigest] 3 | [java.net.URL] 4 | [java.util.UUID])) 5 | 6 | (defn sha256 7 | [data] 8 | (.digest (java.security.MessageDigest/getInstance "SHA-256") (bytes (byte-array (map byte data))))) 9 | 10 | (defn hash-str 11 | [data-bytes] 12 | (->> data-bytes 13 | (map #(.substring 14 | (Integer/toString 15 | (+ (bit-and % 0xff) 0x100) 16) 1)) 16 | (apply str))) 17 | 18 | (defn sha256hash 19 | [data] 20 | (hash-str (sha256 data))) 21 | 22 | (defn uuidv4 23 | [] 24 | (str (java.util.UUID/randomUUID))) 25 | 26 | (defn URLnetLoc 27 | [-url] 28 | (let [u (java.net.URL. -url)] (str (.getHost u) ":" (.getPort u)))) 29 | -------------------------------------------------------------------------------- /src/blockchain/api.clj: -------------------------------------------------------------------------------- 1 | (ns blockchain.api 2 | (:require [compojure.core :refer :all] 3 | [compojure.route :as route] 4 | [ring.middleware.defaults :refer [wrap-defaults site-defaults]] 5 | [ring.middleware.json :as middleware] 6 | [blockchain.impl :as impl])) 7 | 8 | (defroutes app-routes 9 | (GET "/mine" [] (impl/mine)) 10 | (GET "/chain" [] (impl/chain)) 11 | (POST "/transactions/new" req (impl/transaction-new req)) 12 | (POST "/nodes/register" req (impl/nodes-register req)) 13 | (GET "/nodes/resolve" [] (impl/nodes-resolve)) 14 | (route/not-found "Not Found")) 15 | 16 | (def app 17 | (do 18 | (println (str "Initialzing blockchain with identifier " (impl/get-node-id) ".")) 19 | (println "Inserting genesis block...") 20 | (impl/genesis-block) 21 | (println "Genesis block inserted. Starting API...") 22 | (-> 23 | (wrap-defaults app-routes (assoc-in site-defaults [:security :anti-forgery] false)) 24 | (middleware/wrap-json-body {:keywords? true}) 25 | middleware/wrap-json-response))) 26 | -------------------------------------------------------------------------------- /src/blockchain/core.clj: -------------------------------------------------------------------------------- 1 | (ns blockchain.core 2 | (:require [blockchain.utils :as utils])) 3 | 4 | (def chain (atom [])) 5 | 6 | (def current-transactions (atom [])) 7 | 8 | (defn chain-add 9 | [-data] 10 | (swap! chain conj -data)) 11 | 12 | (defn chain-reset 13 | [new-chain] 14 | (reset! chain new-chain)) 15 | 16 | (defn last-block 17 | [] 18 | (last @chain)) 19 | 20 | (defn transactions-add 21 | [-data] 22 | (swap! current-transactions conj -data)) 23 | 24 | (defn transactions-reset 25 | [] 26 | (reset! current-transactions [])) 27 | 28 | (defn new-transaction 29 | [sender recipient amount] 30 | (transactions-add (into (sorted-map) {:sender sender :recipient recipient :amount amount})) 31 | (inc (get-in (last-block) [:index]))) 32 | 33 | (defn new-block 34 | ([proof previous-hash] 35 | (do 36 | (chain-add (into (sorted-map) {:index (count @chain) :timestamp (System/currentTimeMillis) :transactions @current-transactions :proof proof :previous-hash previous-hash})) 37 | (transactions-reset) 38 | (count @chain))) 39 | ([proof] (new-block proof (utils/sha256hash (str (last-block)))))) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Paolo Oliveira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/blockchain/api_test.clj: -------------------------------------------------------------------------------- 1 | (ns blockchain.api-test 2 | (:require [clojure.test :refer :all] 3 | [cheshire.core :as json] 4 | [ring.mock.request :as mock] 5 | [blockchain.api :refer :all])) 6 | 7 | (deftest test-app 8 | (testing "chain setup and genesis block" 9 | (let [response (app (mock/request :get "/chain"))] 10 | (is (= (:status response) 200)) 11 | (is (= ((json/parse-string (:body response) true) :length) 1)))) 12 | 13 | (testing "mining" 14 | (let [response (app (mock/request :get "/mine"))] 15 | (is (= (:status response) 200)) 16 | (is (= ((json/parse-string (:body response) true) :message) "New block forged")))) 17 | 18 | (testing "not-found route" 19 | (let [response (app (mock/request :get "/invalid"))] 20 | (is (= (:status response) 404))))) 21 | 22 | (deftest test-consensus 23 | (testing "add new node" 24 | (let [fields {:node "http://localhost:8090"} 25 | response (app (-> (mock/request :post "/nodes/register") 26 | (mock/content-type "application/json") 27 | (mock/body (json/generate-string fields))))] 28 | (is (= (:status response) 200)) 29 | (is (= ((json/parse-string (:body response) true) :message) "Node inserted"))))) 30 | 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blockchain 2 | 3 | [![Travis CI](https://travis-ci.org/paoloo/blockchain.svg?branch=master)](https://travis-ci.org/paoloo/blockchain) 4 | 5 | A clojure implementation of a blockchain based on [Learn Blockchains by Building One](https://hackernoon.com/learn-blockchains-by-building-one-117428612f46) 6 | 7 | ## Prerequisites 8 | 9 | You will need [Leiningen][] 2.0.0 or above installed. 10 | 11 | [leiningen]: https://github.com/technomancy/leiningen 12 | 13 | ## Running 14 | 15 | To get the dependencies, run: 16 | 17 | lein deps 18 | 19 | To start a server for the application, run: 20 | 21 | lein ring server 22 | 23 | ## Usage 24 | 25 | - Requesting the whole Blockchain: 26 | ```json 27 | $ curl -X GET 127.0.0.1:8090/chain 28 | ``` 29 | - Mining coins: 30 | ```json 31 | $ curl -X GET 127.0.0.1:8090/mine 32 | ``` 33 | - Make a new transaction: 34 | ```json 35 | $ curl -X POST -H "Content-Type: application/json" -d '{ 36 | "sender": "d4ee26eee15148ee92c6cd394edd974e", 37 | "recipient": "someone-other-address", 38 | "amount": 5 39 | }' "http://127.0.0.1:8090/transactions/new" 40 | ``` 41 | - Register a new node: 42 | ```json 43 | $ curl -X POST -H "Content-Type: application/json" -d '{ 44 | "node": "http://127.0.0.1:8091" 45 | }' "http://127.0.0.1:8090/nodes/register" 46 | ``` 47 | - Resolving Blockchain differences in each node: 48 | ```json 49 | $ curl -X GET 127.0.0.1:8090/nodes/resolve 50 | ``` 51 | ## Tests 52 | 53 | lain test 54 | 55 | ## Docker 56 | 57 | - Create a self contained version of application with: `lein ring uberjar`; 58 | - Run `docker build -t paoloo/blockchain .` to create image; 59 | - And finally, run `docker run -p 8090:8090 paoloo/blockchain` to instantiate it. 60 | 61 | ## License 62 | 63 | MIT 64 | 65 | Copyright (c) 2017 Paolo Oliveira 66 | -------------------------------------------------------------------------------- /src/blockchain/nodes.clj: -------------------------------------------------------------------------------- 1 | (ns blockchain.nodes 2 | (:require [clj-http.client :as client] 3 | [cheshire.core :as json] 4 | [blockchain.core :as core] 5 | [blockchain.utils :as utils])) 6 | 7 | (def nodes (atom [])) 8 | 9 | (defn node-add 10 | "Add a new node if it is not already there." 11 | [-data] 12 | (if-not (boolean (some #(= -data %) @nodes)) 13 | (do (swap! nodes conj -data) {:message "Node inserted" :total-nodes @nodes}) 14 | {:message "Node already inserted" :total-nodes @nodes})) 15 | 16 | (defn register-node 17 | "Register a new node." 18 | [-url] 19 | (node-add (utils/URLnetLoc -url))) 20 | 21 | 22 | 23 | (defn valid-chain 24 | "This method is responsible for checking if a chain is valid by looping through each block and verifying both the hash and the proof." 25 | [chain] 26 | (apply (fn [& x] (and (apply = x) (= (last x) true))) 27 | (rest 28 | (for [index (range (count chain)) :let [[before [current]] (split-at index chain)]] 29 | (= (utils/sha256hash(str(last before))) (get-in current [:previous-hash]))))) ) 30 | 31 | (defn resolve-conflicts 32 | "This method loops through all our neighbouring nodes, downloads their chains and verifies them using the above method. If a valid chain is found, whose length is greater than ours, we replace ours." 33 | [] 34 | (if-not (= 0 (count @nodes)) 35 | (doseq [node @nodes] 36 | (let [url (str "http://" node "/chain") 37 | body (:body (client/get url)) 38 | data (json/parse-string body true) 39 | length (data :length) 40 | chain (apply list (data :chain))] 41 | (if (and (> length (count @core/chain)) (valid-chain chain)) 42 | (core/chain-reset (into '[] chain))))) 43 | "There are no registered nodes. There is no conflict to resolve")) 44 | -------------------------------------------------------------------------------- /src/blockchain/impl.clj: -------------------------------------------------------------------------------- 1 | (ns blockchain.impl 2 | (:require [blockchain.core :as core] 3 | [blockchain.proof :as proof] 4 | [blockchain.nodes :as nodes] 5 | [blockchain.utils :as utils])) 6 | 7 | (def node-identifier (utils/uuidv4)) 8 | 9 | (defn get-node-id 10 | [] 11 | node-identifier) 12 | 13 | (defn output-json 14 | [status-code information] 15 | {:status status-code 16 | :headers {"Content-Type" "application/json"} 17 | :body information }) 18 | 19 | (defn json-ok 20 | [information] 21 | (output-json 200 information)) 22 | 23 | (defn genesis-block 24 | [] 25 | (core/new-block 100 1)) 26 | 27 | (defn chain 28 | [] 29 | (json-ok {:length (count @core/chain) :chain @core/chain} )) 30 | 31 | (defn mine 32 | [] 33 | (let 34 | [proof (proof/proof-of-work (get-in (core/last-block) [:proof]))] 35 | (do 36 | (core/new-transaction "0" (get-node-id) 1) 37 | (core/new-block proof) 38 | (json-ok {:message "New block forged" :index (get-in (core/last-block) [:index]) :transactions (get-in (core/last-block) [:transactions]) :proof proof :previous_hash (get-in (core/last-block) [:previous-hash]) })))) 39 | 40 | (defn transaction-new 41 | [x] 42 | (if (and (get-in x [:body :sender]) (get-in x [:body :recipient]) (get-in x [:body :amount])) 43 | (json-ok {:message (str "Transaction will be added to Block " (str (core/new-transaction (get-in x [:body :sender]) (get-in x [:body :recipient]) (get-in x [:body :amount]))))}) 44 | (output-json 400 {:error "A parameter is missing on the request." }))) 45 | 46 | (defn nodes-register 47 | [x] 48 | (json-ok (nodes/register-node (get-in x [:body :node] )))) 49 | 50 | (defn nodes-resolve 51 | [] 52 | (let [k (nodes/resolve-conflicts)] 53 | (if (= (type k) (type "")) 54 | k 55 | "All conflicts were resolved. Chain was updated"))) 56 | --------------------------------------------------------------------------------