├── .env ├── .gitignore ├── README.md ├── bcb_server ├── block.py ├── blockchain.py ├── certificate_authority.py ├── orderer.py ├── peer.py └── utils.py ├── docker-compose-peer-only.yml ├── docker-compose.yml ├── docker ├── certificate_authority │ └── Dockerfile ├── orderer │ └── Dockerfile ├── peer │ └── Dockerfile └── vosy │ └── Dockerfile ├── docs ├── architecture.png ├── bcb_vosy.pdf ├── network_sample.png └── sample.png ├── requirements.txt └── vosy_app ├── chaincode.py ├── templates ├── base.html └── index.html ├── utils.py └── vosy.py /.env: -------------------------------------------------------------------------------- 1 | ORDERER_IP=192.168.43.162 2 | CA_IP=192.168.43.162 3 | PEER_IP=192.168.43.162 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | */__pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | #sublime text 107 | *.sublime-project 108 | *.sublime-workspace -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Simple Blockchain-based Voting System 2 | 3 | A simple blockchain-based voting system application built from scratch by Python. It's available for running with multipeer. 4 | 5 | Materials: 6 | * How to run and how to use in [video demo](https://www.youtube.com/watch?v=CqNoDjuf6EE), 7 | * Tutorial [part 1](https://medium.com/datadriveninvestor/build-a-blockchain-application-from-scratch-in-python-understanding-blockchain-1a6f1592e42a). 8 | * Tutorial [part 2(not ready now)](https://github.com/ngocjr7/voting-blockchain). 9 | 10 | ## How it looks 11 | 12 |  13 | 14 | 15 | #### How to use 16 | -> Note: At the first run, click **Update Chaincode** and **Mine** to init chaincode (I wrote a simple chaincode ```count_down_opening_time``` to auto close survey after a period of time) before using. 17 | 18 | * Mine : mine unconfirmed transaction 19 | * Resync : Reload front-end 20 | * Update Chaincode : Load smart contract from chaincode.py in vosy_app to blockchain transaction 21 | * Pending Transaction : List unconfirmed transaction 22 | * List Node : List node in the network 23 | 24 | ## Instructions to run 25 | 26 | This project can run separately by [python](https://github.com/ngocjr7/voting-blockchain#running-by-python-command) or use [docker-compose](https://github.com/ngocjr7/voting-blockchain#running-by-docker-compose) 27 | 28 | ### Running by Docker-compose 29 | -> NOTE: Only available for linux user. If you have any problem with request ip address, try to uncomment `network_mode: "host"` in `docker-compose.yml`. 30 | 31 | #### Prerequisites 32 | 33 | You need to install `docker` and `docker-compose` before 34 | 35 | #### Running 36 | 37 | ###### In first machine 38 | 39 | Follow this command: 40 | 41 | ``` 42 | docker-compose build 43 | docker-compose up 44 | ``` 45 | 46 | You can run in background: 47 | ``` 48 | docker-compose up -d 49 | ``` 50 | You can stop this application: 51 | ``` 52 | docker-compose stop 53 | ``` 54 | Or down ( delete container ): 55 | ``` 56 | docker-compose down 57 | ``` 58 | 59 | ###### In second machine 60 | You have to provide IP address of machine 1 in `.env` file. For example: 61 | 62 | ``` 63 | ORDERER_IP=192.168.43.162 64 | CA_IP=192.168.43.162 65 | PEER_IP=192.168.43.162 66 | ``` 67 | 68 | Then run command 69 | 70 | ``` 71 | docker-compose -f docker-compose-peer-only.yml build 72 | docker-compose -f docker-compose-peer-only.yml up 73 | ``` 74 | 75 | ### Running by Python command 76 | 77 | #### Prerequisites 78 | 79 | It needs `python`, `pip` to run. Install requirements 80 | 81 | ``` 82 | pip install -r requirements.txt 83 | ``` 84 | 85 | #### Running 86 | 87 | ###### In first machine 88 | You need to run 4 app `orderer.py` `certificate_authority.py` `peer.py` `vosy.py` ( if you don't need front-end in this machine, you don't need to run `vosy.py`) . You can run each app on different machines but need to provide ip address for it. 89 | 90 | ``` 91 | python bcb_server/orderer.py 92 | ``` 93 | 94 | Certificate authority need to know aleast 1 orderer. so if is not default value `0.0.0.0`, you need to pass orderer ip address to certificate_authority by argument `--orderer` 95 | ``` 96 | python bcb_server/certificate_authority.py 97 | ``` 98 | 99 | Peer need to know aleast 1 orderer and 1 certificate_authority so you need to pass orderer ip address and ca ip address to peer by argument `--orderer` and `--ca` 100 | ``` 101 | python bcb_server/peer.py 102 | ``` 103 | 104 | Vosy need to know aleast 1 peer so you need to pass peer ip address to vosy app by argument `--host` 105 | ``` 106 | python vosy_app/vosy.py 107 | ``` 108 | 109 | ##### for example, with window users, ip address `0.0.0.0` is not available, so you need to run in `127.0.0.1` instead, so you have to follow this command in 4 cmd: 110 | 111 | ``` 112 | python bcb_server/orderer.py 113 | ``` 114 | ``` 115 | python bcb_server/certificate_authority.py --orderer 127.0.0.1 116 | ``` 117 | ``` 118 | python bcb_server/peer.py --orderer 127.0.0.1 --ca 127.0.0.1 119 | ``` 120 | ``` 121 | python vosy_app/vosy.py --host 127.0.0.1 122 | ``` 123 | 124 | ###### In second machine 125 | You just need to run `peer.py` and `vosy.py` but you need to provide Lan IP address `orderer.py` and `certificate_authority.py` run in machine 1. In my case, it is `192.168.43.162` 126 | 127 | ``` 128 | python bcb_server/peer.py --orderer 192.168.43.162 --ca 192.168.43.162 129 | ``` 130 | 131 | ``` 132 | python vosy_app/vosy.py 133 | ``` 134 | 135 | this vosy will auto connect to local peer in address `0.0.0.0:5000` 136 | 137 | ## Tutorial 138 | 139 | You can see [video demo](https://www.youtube.com/watch?v=CqNoDjuf6EE), or tutorials [part 1](https://medium.com/datadriveninvestor/build-a-blockchain-application-from-scratch-in-python-understanding-blockchain-1a6f1592e42a), [part 2(not ready now)](). 140 | 141 | It is simple architecture of my network 142 | 143 |  144 | 145 |  146 | 147 | 148 | #### Certificate Authority 149 | 150 | It can validate connection when a node ask to join to network and Set permission for each node and validate transaction 151 | 152 | #### Orderer 153 | 154 | It can hold a list of peers and broadcast to all peer when receive a request broadcast new block or new transaction. 155 | It also have consensus method, which can return the longest blockchain in the network 156 | 157 | #### Peer 158 | 159 | It hold all data about blockchain, it have some method like mine, validate_transaction, return chain, open surveys, ... 160 | 161 | #### Vosy 162 | 163 | A blockchain-based application for voting system 164 | -------------------------------------------------------------------------------- /bcb_server/block.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha256 2 | 3 | import json 4 | 5 | class Block: 6 | def __init__(self, index, transactions, timestamp, previous_hash, nonce = 0): 7 | self.index = index 8 | self.transactions = transactions 9 | self.timestamp = timestamp 10 | self.previous_hash = previous_hash 11 | self.nonce = nonce 12 | 13 | @staticmethod 14 | def fromDict(blockDict): 15 | block = Block(blockDict['index'],blockDict['transactions'],blockDict['timestamp'],blockDict['previous_hash'],blockDict['nonce']) 16 | block.hash = blockDict['hash'] 17 | return block 18 | 19 | def compute_hash(self): 20 | """ 21 | A function that return the hash of the block contents. 22 | """ 23 | block_string = json.dumps(self.__dict__, sort_keys=True) 24 | return sha256(block_string.encode()).hexdigest() 25 | -------------------------------------------------------------------------------- /bcb_server/blockchain.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha256 2 | 3 | from block import Block 4 | import time 5 | 6 | class Blockchain: 7 | # difficulty of our PoW algorithm 8 | difficulty = 2 9 | 10 | def __init__(self): 11 | self.unconfirmed_transactions = [] 12 | self.chain = [] 13 | # for validate transaction and return to user. like account in ethereum 14 | self.open_surveys = {} 15 | # for smart contract 16 | self.chain_code = {'chain': self.chain, 'open_surveys': self.open_surveys, 'unconfirmed_transactions': self.unconfirmed_transactions} 17 | self.create_genesis_block() 18 | 19 | @staticmethod 20 | def fromList(chain): 21 | blockchain = Blockchain() 22 | blockchain.unconfirmed_transactions = [] 23 | blockchain.chain = [] 24 | 25 | for block in chain: 26 | blockchain.chain.append(Block.fromDict(block)) 27 | 28 | return blockchain 29 | 30 | def create_genesis_block(self): 31 | """ 32 | A function to generate genesis block and appends it to 33 | the chain. The block has index 0, previous_hash as 0, and 34 | a valid hash. 35 | """ 36 | genesis_block = Block(0, [], time.time(), "0") 37 | #proof of work to init 38 | self.proof_of_work(genesis_block) 39 | 40 | genesis_block.hash = genesis_block.compute_hash() 41 | 42 | self.chain.append(genesis_block) 43 | 44 | @property 45 | def last_block(self): 46 | return self.chain[-1] 47 | 48 | def add_block(self, block, proof): 49 | """ 50 | A function that adds the block to the chain after verification. 51 | Verification includes: 52 | * Checking if the proof is valid. 53 | * The previous_hash referred in the block and the hash of latest block 54 | in the chain match. 55 | """ 56 | previous_hash = self.last_block.hash 57 | 58 | if previous_hash != block.previous_hash: 59 | return False 60 | 61 | if not Blockchain.is_valid_proof(block, proof): 62 | return False 63 | 64 | block.hash = proof 65 | self.chain.append(block) 66 | return True 67 | 68 | def proof_of_work(self, block): 69 | """ 70 | Function that tries different values of nonce to get a hash 71 | that satisfies our difficulty criteria. 72 | """ 73 | block.nonce = 0 74 | 75 | computed_hash = block.compute_hash() 76 | while not computed_hash.startswith('0' * Blockchain.difficulty): 77 | block.nonce += 1 78 | computed_hash = block.compute_hash() 79 | 80 | return computed_hash 81 | 82 | def add_new_transaction(self, transaction): 83 | self.unconfirmed_transactions.append(transaction) 84 | 85 | @classmethod 86 | def is_valid_proof(cls, block, block_hash): 87 | """ 88 | Check if block_hash is valid hash of block and satisfies 89 | the difficulty criteria. 90 | """ 91 | 92 | return (block_hash.startswith('0' * Blockchain.difficulty) and 93 | block_hash == block.compute_hash()) 94 | 95 | @classmethod 96 | def check_chain_validity(cls, chain): 97 | result = True 98 | previous_hash = "0" 99 | 100 | for block in chain: 101 | block_hash = block.hash 102 | # remove the hash field to recompute the hash again 103 | # using `compute_hash` method. 104 | delattr(block, "hash") 105 | 106 | if not cls.is_valid_proof(block, block_hash) or \ 107 | previous_hash != block.previous_hash: 108 | result = False 109 | break 110 | 111 | block.hash, previous_hash = block_hash, block_hash 112 | 113 | return result 114 | -------------------------------------------------------------------------------- /bcb_server/certificate_authority.py: -------------------------------------------------------------------------------- 1 | from block import Block 2 | from blockchain import Blockchain 3 | 4 | from flask import Flask, request, jsonify 5 | from utils import get_ip 6 | 7 | import requests 8 | 9 | app = Flask(__name__) 10 | 11 | orderer = '0.0.0.0' 12 | 13 | # the address to other participating members of the network 14 | peers = set() 15 | 16 | # list grouped peers 17 | groups = {} 18 | # list permission for each group 19 | # O : Open | C : Close | V : Vote 20 | permission = { 'admin' : 'OCVSE', 'peer' : 'OCVSE', 'guest' : 'V' } 21 | 22 | groups[get_ip() + ':5000'] = 'admin' 23 | 24 | 25 | @app.route('/add_node', methods=['GET','POST']) 26 | def validate_connection(): 27 | 28 | data = request.get_json() 29 | request_addr = get_ip(request.remote_addr) 30 | 31 | if not data: 32 | return 'Invalid data' , 400 33 | 34 | # node = data['ipaddress'] 35 | node = request_addr + ':' + str(data['port']) 36 | 37 | if not node: 38 | return 'Invalid data' , 400 39 | 40 | peers.add(node) 41 | # add some role with node in here 42 | # set permission for node 43 | if node not in groups: 44 | groups[node] = 'peer' 45 | 46 | url = 'http://{}:5002/add_node'.format(orderer) 47 | response = requests.post(url,json={'ipaddress': request_addr, 'port': data['port']}) 48 | 49 | if response.status_code >= 400: 50 | return 'Error to connect to orderer', 400 51 | 52 | return "Success", 201 53 | 54 | 55 | @app.route('/validate_permission', methods=['POST']) 56 | def validate_permission(): 57 | 58 | data = request.get_json() 59 | if not data: 60 | return 'Invalid data', 400 61 | 62 | node = data["peer"] 63 | action = data["action"] 64 | 65 | if not node in groups: 66 | groups[node] = 'guest' 67 | 68 | if permission[groups[node]].find(action[0].upper()) != -1 : 69 | return jsonify({'decision' : 'accept'}) 70 | 71 | return jsonify({'decision' : 'reject'}) 72 | 73 | 74 | if __name__ == '__main__': 75 | from argparse import ArgumentParser 76 | 77 | parser = ArgumentParser() 78 | parser.add_argument('-p', '--port', default=5001, type=int, help='port to listen on') 79 | parser.add_argument('--orderer', default='0.0.0.0', type=str, help='port to listen on') 80 | args = parser.parse_args() 81 | port = args.port 82 | orderer = args.orderer 83 | 84 | print('My ip address : ' + get_ip()) 85 | 86 | app.run(host='0.0.0.0', port=port, debug = True, threaded = True) 87 | 88 | -------------------------------------------------------------------------------- /bcb_server/orderer.py: -------------------------------------------------------------------------------- 1 | from block import Block 2 | from blockchain import Blockchain 3 | 4 | from flask import Flask, request, jsonify 5 | from utils import get_ip 6 | 7 | import json 8 | import requests 9 | 10 | app = Flask(__name__) 11 | 12 | 13 | # the address to other participating members of the network 14 | peers = set() 15 | 16 | # endpoint to add new peers to the network. 17 | @app.route('/add_node', methods=['POST']) 18 | def register_new_peers(): 19 | data = request.get_json() 20 | 21 | if not data: 22 | return 'Invalid data' , 400 23 | 24 | request_addr = data['ipaddress'] 25 | port = data['port'] 26 | node = request_addr + ':' + str(port) 27 | 28 | if not node: 29 | return "Invalid data", 400 30 | 31 | peers.add(node) 32 | 33 | return "Success", 201 34 | 35 | @app.route('/broadcast_block', methods=['POST']) 36 | def announce_new_block(): 37 | """ 38 | A function to announce to the network once a block has been mined. 39 | Other blocks can simply verify the proof of work and add it to their 40 | respective chains. 41 | """ 42 | block = Block.fromDict(request.get_json()) 43 | if not block: 44 | return "Invalid data at announce_new_block", 400 45 | 46 | request_addr = get_ip(request.remote_addr) 47 | 48 | offline_node = [] 49 | 50 | for peer in peers: 51 | try: 52 | if peer.find(request_addr) != -1: 53 | continue 54 | url = "http://{}/add_block".format(peer) 55 | requests.post(url, json=block.__dict__) 56 | except requests.exceptions.ConnectionError: 57 | print('Cant connect to node {}. Remove it from peers list'.format(peer)) 58 | offline_node.append(peer) 59 | 60 | for peer in offline_node: 61 | peers.remove(peer) 62 | 63 | return "Success", 201 64 | 65 | @app.route('/broadcast_transaction', methods=['POST']) 66 | def announce_new_transaction(): 67 | """ 68 | A function to announce to the network once a transaction has been added. 69 | Other blocks can simply verify the proof of work and add it to their 70 | respective chains. 71 | """ 72 | data = request.get_json() 73 | if not data: 74 | return "Invalid data at announce_new_block", 400 75 | 76 | request_addr = get_ip(request.remote_addr) 77 | 78 | offline_node = [] 79 | 80 | for peer in peers: 81 | try: 82 | if peer.find(request_addr) != -1: 83 | continue 84 | url = "http://{}/get_transaction".format(peer) 85 | requests.post(url, json=data) 86 | except requests.exceptions.ConnectionError: 87 | print('Cant connect to node {}. Remove it from peers list'.format(peer)) 88 | offline_node.append(peer) 89 | 90 | for peer in offline_node: 91 | peers.remove(peer) 92 | 93 | return "Success", 201 94 | 95 | @app.route('/consensus', methods=['GET']) 96 | def consensus(): 97 | """ 98 | Our simple consensus algorithm. If a longer valid chain is 99 | found, our chain is replaced with it. 100 | """ 101 | longest_chain = Blockchain() 102 | current_len = len(longest_chain.chain) 103 | 104 | offline_node = [] 105 | 106 | for peer in peers: 107 | try: 108 | response = requests.get('http://{}/local_chain'.format(peer)) 109 | length = response.json()['length'] 110 | chain = response.json()['chain'] 111 | new_blockchain = Blockchain.fromList(chain) 112 | 113 | if length > current_len and longest_chain.check_chain_validity(new_blockchain.chain): 114 | current_len = length 115 | longest_chain = new_blockchain 116 | except requests.exceptions.ConnectionError: 117 | print('Cant connect to node {}. Remove it from peers list'.format(peer)) 118 | offline_node.append(peer) 119 | 120 | for peer in offline_node: 121 | peers.remove(peer) 122 | 123 | chain_data = [] 124 | 125 | for block in longest_chain.chain: 126 | chain_data.append(block.__dict__) 127 | 128 | return jsonify({"length": len(chain_data), 129 | "chain": chain_data}) 130 | 131 | #get current list of nodes in the network 132 | @app.route('/list_nodes', methods=['GET','POST']) 133 | def get_node(): 134 | result = { 135 | 'Nodes in System' : list(peers), 136 | 'Count of Nodes' : len(peers) 137 | } 138 | return jsonify(result) 139 | 140 | if __name__ == '__main__': 141 | from argparse import ArgumentParser 142 | 143 | parser = ArgumentParser() 144 | parser.add_argument('-p', '--port', default=5002, type=int, help='port to listen on') 145 | args = parser.parse_args() 146 | port = args.port 147 | 148 | print('My ip address : ' + get_ip()) 149 | 150 | app.run(host='0.0.0.0', port=port, debug = True, threaded = True) 151 | 152 | -------------------------------------------------------------------------------- /bcb_server/peer.py: -------------------------------------------------------------------------------- 1 | from block import Block 2 | from blockchain import Blockchain 3 | from utils import get_ip 4 | 5 | from flask import Flask, request, jsonify 6 | 7 | import json 8 | import requests 9 | import time 10 | import threading 11 | 12 | 13 | app = Flask(__name__) 14 | 15 | # define server IP 16 | ordererIP = '0.0.0.0' 17 | ordererPort = '5002' 18 | caIP = '0.0.0.0' 19 | caPort = '5001' 20 | 21 | # the node's copy of blockchain 22 | blockchain = Blockchain() 23 | 24 | # endpoint to submit a new transaction. This will be used by 25 | # our application to add new data (posts) to the blockchain 26 | @app.route('/new_transaction', methods=['POST']) 27 | def new_transaction(): 28 | tx_data = request.get_json() 29 | required_fields = ["type", "content"] 30 | 31 | for field in required_fields: 32 | if not tx_data.get(field): 33 | return "Invalid transaction data", 404 34 | 35 | tx_data["timestamp"] = time.time() 36 | 37 | blockchain.add_new_transaction(tx_data) 38 | 39 | url = 'http://{}/broadcast_transaction'.format(ordererIP + ':' + ordererPort) 40 | response = requests.post(url, json=tx_data) 41 | 42 | return "Success", 201 43 | 44 | # endpoint to get a new transaction from another node. 45 | @app.route('/get_transaction', methods=['POST']) 46 | def get_transaction(): 47 | tx_data = request.get_json() 48 | required_fields = ["type", "content", "timestamp"] 49 | 50 | for field in required_fields: 51 | if not tx_data.get(field): 52 | return "Invalid transaction data", 404 53 | 54 | blockchain.add_new_transaction(tx_data) 55 | 56 | return "Success", 201 57 | 58 | # endpoint to return the node's copy of the chain. 59 | # Our application will be using this endpoint to query 60 | # all the posts to display. 61 | @app.route('/open_surveys', methods=['GET']) 62 | def get_open_surveys(): 63 | # make sure we've the longest chain 64 | global blockchain 65 | 66 | url = 'http://{}/consensus'.format(ordererIP + ':' + ordererPort) 67 | response = requests.get(url) 68 | 69 | length = response.json()['length'] 70 | chain = response.json()['chain'] 71 | longest_chain = Blockchain.fromList(chain) 72 | 73 | if len(blockchain.chain) < length and blockchain.check_chain_validity(longest_chain.chain): 74 | 75 | 76 | # recompute open_surveys 77 | longest_chain.open_surveys = {} 78 | 79 | for block in longest_chain.chain: 80 | if not compute_open_surveys(block,longest_chain.open_surveys,longest_chain.chain_code): 81 | return "Invalid Blockchain", 400 82 | 83 | blockchain = longest_chain 84 | 85 | surveys = [] 86 | for _ , survey in blockchain.open_surveys.items(): 87 | surveys.append(survey) 88 | 89 | return jsonify({"length": len(blockchain.open_surveys), 90 | "surveys": list(surveys)}) 91 | 92 | 93 | # endpoint to return the node's copy of the chain. 94 | # Our application will be using this endpoint to query 95 | # all the posts to display. 96 | @app.route('/chain', methods=['GET']) 97 | def get_chain(): 98 | # make sure we've the longest chain 99 | global blockchain 100 | 101 | url = 'http://{}/consensus'.format(ordererIP + ':' + ordererPort) 102 | response = requests.get(url) 103 | 104 | length = response.json()['length'] 105 | chain = response.json()['chain'] 106 | longest_chain = Blockchain.fromList(chain) 107 | 108 | if len(blockchain.chain) < length and blockchain.check_chain_validity(longest_chain.chain): 109 | # recompute open_surveys 110 | longest_chain.open_surveys = {} 111 | 112 | for block in longest_chain.chain: 113 | if not compute_open_surveys(block,longest_chain.open_surveys, longest_chain.chain_code): 114 | return "Invalid Blockchain", 400 115 | 116 | blockchain = longest_chain 117 | 118 | chain_data = [] 119 | for block in blockchain.chain: 120 | chain_data.append(block.__dict__) 121 | return jsonify({"length": len(chain_data), 122 | "chain": chain_data}) 123 | 124 | # get local chain for running consensus 125 | @app.route('/local_chain', methods=['GET']) 126 | def get_local_chain(): 127 | chain_data = [] 128 | 129 | for block in blockchain.chain: 130 | chain_data.append(block.__dict__) 131 | 132 | return jsonify({"length": len(chain_data), 133 | "chain": chain_data}) 134 | 135 | 136 | # endpoint to request the node to mine the unconfirmed 137 | # transactions (if any). We'll be using it to initiate 138 | # a command to mine from our application itself. 139 | @app.route('/mine', methods=['GET']) 140 | def mine_unconfirmed_transactions(): 141 | """ 142 | This function serves as an interface to add the pending 143 | transactions to the blockchain by adding them to the block 144 | and figuring out Proof Of Work. 145 | """ 146 | 147 | if not blockchain.unconfirmed_transactions: 148 | return jsonify({"response": "None transactions 0x001"}) 149 | 150 | last_block = blockchain.last_block 151 | 152 | new_block = Block(index=last_block.index + 1, 153 | transactions=[], 154 | timestamp=time.time(), 155 | previous_hash=last_block.hash) 156 | 157 | for transaction in blockchain.unconfirmed_transactions: 158 | #validate_transaction 159 | if not validate_transaction(transaction): 160 | continue 161 | 162 | new_block.transactions.append(transaction) 163 | 164 | blockchain.unconfirmed_transactions = [] 165 | 166 | if ( len(new_block.transactions) == 0 ): 167 | return jsonify({"response": "None transactions 0x002"}) 168 | 169 | proof = blockchain.proof_of_work(new_block) 170 | blockchain.add_block(new_block, proof) 171 | 172 | # announce it to the network 173 | url = 'http://{}/broadcast_block'.format(ordererIP + ':' + ordererPort) 174 | response = requests.post(url, json=new_block.__dict__) 175 | 176 | result = new_block.index 177 | 178 | if not result: 179 | return jsonify({"response": "None transactions to mine 0x002"}) 180 | return jsonify({"response": "Block #{} is mined.".format(result)}) 181 | 182 | 183 | # endpoint to add a block mined by someone else to 184 | # the node's chain. The block is first verified by the node 185 | # and then added to the chain. 186 | @app.route('/add_block', methods=['POST']) 187 | def validate_and_add_block(): 188 | global blockchain 189 | 190 | block_data = request.get_json() 191 | 192 | block = Block(block_data["index"], 193 | block_data["transactions"], 194 | block_data["timestamp"], 195 | block_data["previous_hash"], 196 | block_data["nonce"]) 197 | 198 | tmp_open_surveys = blockchain.open_surveys 199 | tmp_chain_code = blockchain.chain_code 200 | 201 | if not compute_open_surveys(block, tmp_open_surveys, tmp_chain_code): 202 | return "The block was discarded by the node", 400 203 | 204 | blockchain.open_surveys = tmp_open_surveys 205 | blockchain.chain_code = tmp_chain_code 206 | 207 | proof = block_data['hash'] 208 | added = blockchain.add_block(block, proof) 209 | 210 | 211 | if not added: 212 | return "The block was discarded by the node", 400 213 | 214 | return "Block added to the chain", 201 215 | 216 | # endpoint to query unconfirmed transactions 217 | @app.route('/pending_tx') 218 | def get_pending_tx(): 219 | return jsonify(blockchain.unconfirmed_transactions) 220 | 221 | @app.route('/list_nodes', methods=['GET','POST']) 222 | def list_node(): 223 | url = 'http://{}/list_nodes'.format(ordererIP + ':' + ordererPort) 224 | response = requests.get(url) 225 | 226 | data = response.json() 227 | 228 | return jsonify(data) 229 | 230 | 231 | def validate_transaction(transaction): 232 | global blockchain 233 | #check permission of transaction 234 | author = transaction['content']['author'] 235 | url = 'http://{}/validate_permission'.format(caIP + ':' + caPort) 236 | response = requests.post(url,json={'peer' : author, 'action' : transaction['type']}) 237 | 238 | if response.json()['decision'] != 'accept': 239 | print("Reject from server") 240 | return False 241 | 242 | #check validate transaction and compute open_surveys 243 | if transaction['type'].lower() == 'open': 244 | questionid = transaction['content']['questionid'] 245 | if questionid in blockchain.open_surveys: 246 | return False 247 | blockchain.open_surveys[questionid] = transaction['content'] 248 | return True 249 | elif transaction['type'].lower() == 'close': 250 | questionid = transaction['content']['questionid'] 251 | if questionid in blockchain.open_surveys and blockchain.open_surveys[questionid]['author'] == transaction['content']['author'] and blockchain.open_surveys[questionid]['status'] == 'opening': 252 | blockchain.open_surveys[questionid]['status'] = 'closed' 253 | return True 254 | return False 255 | elif transaction['type'].lower() == 'vote': 256 | questionid = transaction['content']['questionid'] 257 | if questionid in blockchain.open_surveys and blockchain.open_surveys[questionid]['status'] == 'opening': 258 | vote = transaction['content']['vote'] 259 | author = transaction['content']['author'] 260 | if author not in blockchain.open_surveys[questionid]['answers'][vote]: 261 | blockchain.open_surveys[questionid]['answers'][vote].append(author) 262 | return True 263 | return False 264 | elif transaction['type'].lower() == 'smartcontract': 265 | try: 266 | exec(transaction['content']['code'],blockchain.chain_code,blockchain.chain_code) 267 | return True 268 | except: 269 | print('Error when create new contract') 270 | return False 271 | elif transaction['type'].lower() == 'execute': 272 | try: 273 | thread = threading.Thread(target=blockchain.chain_code[transaction['content']['contract']], args=transaction['content']['arguments']) 274 | thread.start() 275 | return True 276 | except: 277 | print('Error when execute chain_code {}'.format(transaction['content']['contract'])) 278 | return False 279 | 280 | 281 | def compute_open_surveys(block, open_surveys, chain_code): 282 | for transaction in block.transactions: 283 | author = transaction['content']['author'] 284 | url = 'http://{}/validate_permission'.format(caIP + ':' + caPort) 285 | response = requests.post(url,json={'peer' : author, 'action' : transaction['type']}) 286 | 287 | if response.json()['decision'] != 'accept': 288 | print("Reject from server") 289 | return False 290 | 291 | #check validate transaction and compute open_surveys 292 | 293 | if transaction['type'].lower() == 'open': 294 | questionid = transaction['content']['questionid'] 295 | if questionid not in open_surveys: 296 | open_surveys[questionid] = transaction['content'] 297 | return True 298 | elif transaction['type'].lower() == 'close': 299 | questionid = transaction['content']['questionid'] 300 | if questionid in open_surveys and open_surveys[questionid]['author'] == transaction['content']['author'] and open_surveys[questionid]['status'] == 'opening': 301 | open_surveys[questionid]['status'] = 'closed' 302 | return True 303 | elif transaction['type'].lower() == 'vote': 304 | questionid = transaction['content']['questionid'] 305 | if questionid in open_surveys and open_surveys[questionid]['status'] == 'opening': 306 | vote = transaction['content']['vote'] 307 | author = transaction['content']['author'] 308 | if author not in open_surveys[questionid]['answers'][vote]: 309 | open_surveys[questionid]['answers'][vote].append(author) 310 | return True 311 | elif transaction['type'].lower() == 'smartcontract': 312 | try: 313 | exec(transaction['content']['code'],chain_code) 314 | return True 315 | except: 316 | print('Error when create new contract') 317 | return False 318 | else: 319 | return True 320 | return False 321 | return True 322 | 323 | # ask ca server to join network 324 | def join_to_network(orderer, ca, myIP, myPort): 325 | try: 326 | url = 'http://{}/add_node'.format(ca) 327 | response = requests.post(url, json={'ipaddress' : myIP, 'port' : myPort}) 328 | print('Connection successfull') 329 | return True 330 | except: 331 | print("Connection refused by the server..") 332 | 333 | return False 334 | 335 | if __name__ == '__main__': 336 | from argparse import ArgumentParser 337 | 338 | myIP = get_ip() 339 | 340 | parser = ArgumentParser() 341 | parser.add_argument('-p', '--port', default=5000, type=int, help='port to listen on') 342 | parser.add_argument('-c', '--ca', default='0.0.0.0', type=str, help='port to listen on') 343 | parser.add_argument('-o', '--orderer', default='0.0.0.0', type=str, help='port to listen on') 344 | args = parser.parse_args() 345 | port = args.port 346 | caIP = args.ca 347 | ordererIP = args.orderer 348 | 349 | print('My ip address : ' + get_ip()) 350 | 351 | # time.sleep(5) 352 | # join_to_network(ordererIP + ':' + ordererPort, caIP + ':' + caPort, myIP, port) 353 | while not join_to_network(ordererIP + ':' + ordererPort, caIP + ':' + caPort, myIP, port): 354 | print("Let me sleep for 5 seconds") 355 | time.sleep(5) 356 | 357 | app.run(host='0.0.0.0', port=port, debug = True, threaded = True) 358 | 359 | -------------------------------------------------------------------------------- /bcb_server/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | def get_ip(remote_addr = '127.0.0.1'): 4 | 5 | if remote_addr != '127.0.0.1': 6 | return remote_addr 7 | 8 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 9 | try: 10 | # doesn't even have to be reachable 11 | s.connect(('10.255.255.255', 1)) 12 | IP = s.getsockname()[0] 13 | except: 14 | IP = '127.0.0.1' 15 | finally: 16 | s.close() 17 | return IP 18 | -------------------------------------------------------------------------------- /docker-compose-peer-only.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | 4 | services: 5 | peer.org1.bcbvosy.com: 6 | container_name: peer.org1.bcbvosy.com 7 | build: 8 | context: . 9 | dockerfile: ./docker/peer/Dockerfile 10 | environment: 11 | - ORDERER_IP=${ORDERER_IP} 12 | - CA_IP=${CA_IP} 13 | ports: 14 | - "5000:5000" 15 | # network_mode: "host" 16 | volumes: 17 | - .:/code 18 | 19 | vosy.org1.bcbvosy.com: 20 | container_name: vosy.org1.bcbvosy.com 21 | build: 22 | context: . 23 | dockerfile: ./docker/vosy/Dockerfile 24 | environment: 25 | - HOST_IP=peer.org1.bcbvosy.com 26 | ports: 27 | - "8080:8080" 28 | # network_mode: "host" 29 | volumes: 30 | - .:/code 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | 4 | services: 5 | orderer.org1.bcbvosy.com: 6 | container_name: orderer.org1.bcbvosy.com 7 | build: 8 | context: . 9 | dockerfile: ./docker/orderer/Dockerfile 10 | ports: 11 | - "5002:5002" 12 | # network_mode: "host" 13 | volumes: 14 | - .:/code 15 | 16 | ca.org1.bcbvosy.com: 17 | container_name: ca.org1.bcbvosy.com 18 | build: 19 | context: . 20 | dockerfile: ./docker/certificate_authority/Dockerfile 21 | environment: 22 | - ORDERER_IP=orderer.org1.bcbvosy.com 23 | ports: 24 | - "5001:5001" 25 | # network_mode: "host" 26 | volumes: 27 | - .:/code 28 | 29 | peer.org1.bcbvosy.com: 30 | container_name: peer.org1.bcbvosy.com 31 | build: 32 | context: . 33 | dockerfile: ./docker/peer/Dockerfile 34 | environment: 35 | - ORDERER_IP=orderer.org1.bcbvosy.com 36 | - CA_IP=ca.org1.bcbvosy.com 37 | ports: 38 | - "5000:5000" 39 | # network_mode: "host" 40 | volumes: 41 | - .:/code 42 | depends_on: 43 | - orderer.org1.bcbvosy.com 44 | - ca.org1.bcbvosy.com 45 | 46 | vosy.org1.bcbvosy.com: 47 | container_name: vosy.org1.bcbvosy.com 48 | build: 49 | context: . 50 | dockerfile: ./docker/vosy/Dockerfile 51 | environment: 52 | - HOST_IP=peer.org1.bcbvosy.com 53 | ports: 54 | - "8080:8080" 55 | # network_mode: "host" 56 | volumes: 57 | - .:/code 58 | -------------------------------------------------------------------------------- /docker/certificate_authority/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | COPY . /code 3 | EXPOSE 5001 4 | 5 | WORKDIR /code 6 | 7 | RUN pip install -r requirements.txt 8 | 9 | # ENV ORDERER_IP '0.0.0.0' 10 | 11 | ENTRYPOINT python bcb_server/certificate_authority.py --orderer $ORDERER_IP -------------------------------------------------------------------------------- /docker/orderer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | COPY . /code 3 | EXPOSE 5001 4 | 5 | WORKDIR /code 6 | 7 | RUN pip install -r requirements.txt 8 | 9 | ENTRYPOINT python bcb_server/orderer.py -------------------------------------------------------------------------------- /docker/peer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | COPY . /code 3 | EXPOSE 5001 4 | 5 | WORKDIR /code 6 | 7 | RUN pip install -r requirements.txt 8 | 9 | # ENV ORDERER_IP '0.0.0.0' 10 | # ENV CA_IP '0.0.0.0' 11 | 12 | ENTRYPOINT python bcb_server/peer.py --ca $CA_IP --orderer $ORDERER_IP -------------------------------------------------------------------------------- /docker/vosy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | COPY . /code 3 | 4 | EXPOSE 5001 5 | 6 | WORKDIR /code 7 | 8 | RUN pip install -r requirements.txt 9 | 10 | # ENV HOST_IP '0.0.0.0' 11 | 12 | ENTRYPOINT python vosy_app/vosy.py --host $HOST_IP -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocbh/voting-blockchain/0125fa62eb6584d7c791e1eeb5484dffb3e8a41f/docs/architecture.png -------------------------------------------------------------------------------- /docs/bcb_vosy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocbh/voting-blockchain/0125fa62eb6584d7c791e1eeb5484dffb3e8a41f/docs/bcb_vosy.pdf -------------------------------------------------------------------------------- /docs/network_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocbh/voting-blockchain/0125fa62eb6584d7c791e1eeb5484dffb3e8a41f/docs/network_sample.png -------------------------------------------------------------------------------- /docs/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocbh/voting-blockchain/0125fa62eb6584d7c791e1eeb5484dffb3e8a41f/docs/sample.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ## pip install -r requirements.txt 2 | ######## example-requirements.txt ####### 3 | ## 4 | ####### Requirements without Version Specifiers ###### 5 | #nose 6 | #nose-cov 7 | #beautifulsoup4 8 | ## 9 | ####### Requirements with Version Specifiers ###### 10 | ## See https://www.python.org/dev/peps/pep-0440/#version-specifiers 11 | #docopt == 0.6.1 # Version Matching. Must be version 0.6.1 12 | #keyring >= 4.1.1 # Minimum version 4.1.1 13 | #coverage != 3.5 # Version Exclusion. Anything except version 3.5 14 | #Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.* 15 | ## 16 | ####### Refer to other requirements files ###### 17 | #-r other-requirements.txt 18 | ## 19 | ## 20 | ####### A particular file ###### 21 | #./downloads/numpy-1.9.2-cp34-none-win32.whl 22 | #http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl 23 | ## 24 | ####### Additional Requirements without Version Specifiers ###### 25 | ## Same as 1st section, just here to show that you can put things in any order. 26 | #rejected 27 | #green 28 | ## 29 | 30 | flask 31 | requests -------------------------------------------------------------------------------- /vosy_app/chaincode.py: -------------------------------------------------------------------------------- 1 | from threading import Timer 2 | 3 | import time 4 | import requests 5 | 6 | def count_down_opening_time(opening_time, author, questionid, CONNECTED_NODE_ADDRESS): 7 | def close_survey(author,questionid,CONNECTED_NODE_ADDRESS): 8 | post_object = { 9 | 'type' : 'close', 10 | 'content' : { 11 | 'questionid': questionid, 12 | 'author': author + ':5000', 13 | 'timestamp': time.time() 14 | } 15 | } 16 | # Submit a transaction 17 | new_tx_address = "{}/new_transaction".format(CONNECTED_NODE_ADDRESS) 18 | 19 | print(new_tx_address) 20 | 21 | requests.post(new_tx_address, 22 | json=post_object, 23 | headers={'Content-type': 'application/json'}) 24 | 25 | print(opening_time, author, questionid) 26 | t = Timer(opening_time, close_survey, args=[author,questionid,CONNECTED_NODE_ADDRESS]) 27 | t.start() -------------------------------------------------------------------------------- /vosy_app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 |To create new survey, please click Update Chaincode and Mine before
{{post.question}}
99 | {% for answer, votes in post.answers.items() %} 100 | {% if post.status == 'opening' %} 101 |