├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_zh.md ├── blockchain ├── __init__.py ├── block.py ├── chain.py └── peer.py ├── cli ├── __init__.py ├── command.py └── interface.py ├── main.py └── tests ├── test_block.py └── test_chain.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | *.pyc 3 | 4 | venv/ 5 | .idea/ 6 | .DS_Store 7 | *.egg-info 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | script: 5 | - "python -m unittest discover -s tests" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zhiya Zang 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyBlockchainCLI [![Build Status](https://travis-ci.org/simpleapples/py-blockchain-cli.svg?branch=master)](https://travis-ci.org/simpleapples/py-blockchain-cli) 2 | 3 | [中文文档](README_zh.md) 4 | 5 | A simple blockchain command-line interface without using any 3rd party library. 6 | 7 | # Screenshot 8 | 9 | ![](http://ww1.sinaimg.cn/large/6ae0adaely1fpb0l9rznog20fo0d8gqg.gif) 10 | 11 | # Features 12 | 13 | - Mining block with data 14 | - Distributed peer-to-peer Network 15 | - Proof-of-work system 16 | - Blockchain validation 17 | 18 | # Installation 19 | 20 | Only support python 3.6+ 21 | 22 | # Run 23 | 24 | ```bash 25 | # clone this repository 26 | git clone git@github.com:simpleapples/py-blockchain-cli.git 27 | 28 | # Go into the project folder 29 | cd py-blockchain-cli 30 | 31 | # Run main.py 32 | python3 main.py 33 | ``` 34 | 35 | # Contributing 36 | 37 | Please submit a pull request to contribute. 38 | 39 | # License 40 | 41 | This project is licensed under the MIT License. -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # PyBlockchainCLI [![Build Status](https://travis-ci.org/simpleapples/py-blockchain-cli.svg?branch=master)](https://travis-ci.org/simpleapples/py-blockchain-cli) 2 | 3 | Python实现的区块链命令行界面 4 | 5 | # 截图 6 | 7 | ![](http://ww1.sinaimg.cn/large/6ae0adaely1fpb0l9rznog20fo0d8gqg.gif) 8 | 9 | # 功能 10 | 11 | - 为节点加入带有数据的新区块 12 | - 分布式点对点网络 13 | - 工作量证明系统 14 | - 区块链有效性验证 15 | 16 | # 运行 17 | 18 | ```bash 19 | # 克隆项目 20 | git clone git@github.com:simpleapples/py-blockchain-cli.git 21 | 22 | # 进入项目文件夹 23 | cd py-blockchain-cli 24 | 25 | # 使用Python3.6以上运行main.py 26 | python3 main.py 27 | ``` 28 | 29 | # 贡献代码 30 | 31 | 请Fork项目,通过提交Pull Request提交代码 32 | 33 | # License 34 | 35 | 本项目采用MIT协议授权 -------------------------------------------------------------------------------- /blockchain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simpleapples/py-blockchain-cli/71bc41bb22eb4479d1e44edbf591c905a83bbcdc/blockchain/__init__.py -------------------------------------------------------------------------------- /blockchain/block.py: -------------------------------------------------------------------------------- 1 | import time 2 | from hashlib import sha256 3 | 4 | 5 | class Block(object): 6 | 7 | def __init__( 8 | self, index=0, nonce=0, previous_hash=None, data=None, hash=None, 9 | timestamp=None): 10 | self.index = index 11 | self.previous_hash = previous_hash or '0' 12 | self.timestamp = timestamp or time.time() 13 | self.data = data or '' 14 | self.nonce = nonce 15 | self.hash = hash or self._calculate_hash() 16 | 17 | def to_dict(self): 18 | return { 19 | 'index': self.index, 20 | 'previous_hash': self.previous_hash, 21 | 'timestamp': self.timestamp, 22 | 'data': self.data, 23 | 'nonce': self.nonce, 24 | 'hash': self.hash 25 | } 26 | 27 | @staticmethod 28 | def genesis(): 29 | return Block( 30 | 0, 0, '0', 'Welcome to blockchain cli!', 31 | '8724f78170aee146b794ca6ad451d23c254717727e18e2b9643b81d5666aa908', 32 | 1520572079.336289) 33 | 34 | def _calculate_hash(self): 35 | original_str = ''.join([ 36 | str(self.index), self.previous_hash, str(self.timestamp), self.data, 37 | str(self.nonce)]) 38 | return sha256(original_str.encode('utf-8')).hexdigest() 39 | 40 | def __eq__(self, other): 41 | if (self.index == other.index 42 | and self.previous_hash == other.previous_hash 43 | and self.timestamp == other.timestamp 44 | and self.data == other.data 45 | and self.nonce == other.nonce 46 | and self.hash == other.hash): 47 | return True 48 | return False 49 | -------------------------------------------------------------------------------- /blockchain/chain.py: -------------------------------------------------------------------------------- 1 | from blockchain.block import Block 2 | 3 | 4 | class Chain(object): 5 | 6 | def __init__(self): 7 | genesis = Block.genesis() 8 | self._chain_list = [genesis] 9 | self._difficulty = 4 10 | 11 | def mine(self, data): 12 | previous = self._latest_block 13 | nonce = 0 14 | while True: 15 | nonce += 1 16 | new = Block(previous.index + 1, nonce, previous.hash, data) 17 | if self._is_hash_valid(new.hash): 18 | self._chain_list.append(new) 19 | break 20 | 21 | def replace_chain(self, chain): 22 | if len(chain) > len(self._chain_list): 23 | chain_list = [] 24 | for block in chain: 25 | chain_list.append(Block( 26 | int(block['index']), int(block['nonce']), 27 | block['previous_hash'], block['data'], block['hash'], 28 | float(block['timestamp']))) 29 | self._chain_list = chain_list 30 | 31 | def to_dict(self): 32 | return [item.to_dict() for item in self._chain_list] 33 | 34 | def _is_hash_valid(self, hash): 35 | return hash[:self._difficulty] == '0' * self._difficulty 36 | 37 | @property 38 | def _latest_block(self): 39 | return self._chain_list[-1] 40 | -------------------------------------------------------------------------------- /blockchain/peer.py: -------------------------------------------------------------------------------- 1 | import json 2 | import socket 3 | import socketserver 4 | import multiprocessing 5 | 6 | from blockchain.chain import Chain 7 | 8 | 9 | class _PeerRequestHandler(socketserver.BaseRequestHandler): 10 | 11 | def handle(self): 12 | message_str = self.request.recv(655350).strip().decode('utf-8') 13 | message_obj = json.loads(message_str) 14 | message_type = message_obj['type'] 15 | response = 'OK' 16 | peer = self.server.peer 17 | if message_type == 'MINE': 18 | peer.mine(message_obj['data']) 19 | elif message_type == 'CONNECT': 20 | host = message_obj['host'] 21 | port = message_obj['port'] 22 | peer.connect_to_peer(host, port) 23 | elif message_type == 'PEERS': 24 | response = json.dumps(peer.peers) 25 | elif message_type == 'SHOW': 26 | response = json.dumps(peer.chain.to_dict()) 27 | elif message_type == 'CHAIN': 28 | chain = message_obj['chain'] 29 | peer.replace_chain(chain) 30 | self.request.sendall(response.encode('utf-8')) 31 | 32 | 33 | class Peer(object): 34 | 35 | def __init__(self, host='127.0.0.1', port=5000): 36 | self.host = host 37 | self.port = port 38 | self._peers = set() 39 | self._chain = Chain() 40 | 41 | def start(self): 42 | server = socketserver.ThreadingTCPServer( 43 | (self.host, self.port), _PeerRequestHandler) 44 | server.peer = self 45 | try: 46 | server.serve_forever() 47 | except KeyboardInterrupt as _: 48 | server.server_close() 49 | 50 | def connect_to_peer(self, host, port): 51 | if (host, port) in self._peers: 52 | return 53 | self._peers.add((host, port)) 54 | peers = self._request_peers(host, port) 55 | self._add_peers(json.loads(peers)) 56 | self._request_connection() 57 | self._broadcast_chain() 58 | 59 | def mine(self, data): 60 | self._chain.mine(data) 61 | self._broadcast_chain() 62 | 63 | def replace_chain(self, chain): 64 | self._chain.replace_chain(chain) 65 | 66 | @property 67 | def chain(self): 68 | return self._chain 69 | 70 | @property 71 | def peers(self): 72 | return [{'host': host, 'port': port} for (host, port) in self._peers] 73 | 74 | def _add_peers(self, peers): 75 | for peer in peers: 76 | host = peer['host'] 77 | port = peer['port'] 78 | if host == self.host and port == self.port: 79 | continue 80 | if (host, port) in self._peers: 81 | continue 82 | self._peers.add((host, port)) 83 | 84 | # Communication 85 | 86 | def _request_connection(self): 87 | message = {'type': 'CONNECT', 'host': self.host, 'port': self.port} 88 | return self._broadcast(message) 89 | 90 | def _request_peers(self, host, port): 91 | message = {'type': 'PEERS', 'host': self.host, 'port': self.port} 92 | return self._unicast(host, port, message) 93 | 94 | def _broadcast_chain(self): 95 | message = {'type': 'CHAIN', 'chain': self._chain.to_dict()} 96 | return self._broadcast(message) 97 | 98 | # Base communication 99 | 100 | def _unicast(self, host, port, message): 101 | pool = multiprocessing.Pool(1) 102 | result = pool.apply_async( 103 | self._send_message, args=(host, port, message)) 104 | pool.close() 105 | pool.join() 106 | return result.get() 107 | 108 | def _broadcast(self, message): 109 | results = [] 110 | pool = multiprocessing.Pool(5) 111 | for (host, port) in self._peers: 112 | results.append(pool.apply_async( 113 | self._send_message, args=(host, port, message))) 114 | pool.close() 115 | pool.join() 116 | return [result.get() for result in results] 117 | 118 | def _send_message(self, host, port, message): 119 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 120 | s.connect((host, port)) 121 | s.sendall(json.dumps(message).encode('utf-8')) 122 | response = s.recv(655350, 0) 123 | return response.decode('utf-8') 124 | -------------------------------------------------------------------------------- /cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simpleapples/py-blockchain-cli/71bc41bb22eb4479d1e44edbf591c905a83bbcdc/cli/__init__.py -------------------------------------------------------------------------------- /cli/command.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import socket 3 | import json 4 | 5 | from blockchain.peer import Peer 6 | 7 | 8 | class Command(object): 9 | 10 | def start_peer(self, host, port): 11 | p = multiprocessing.Process(target=self._start_peer, args=(host, port)) 12 | p.start() 13 | print(f'Peer running at {host}:{port}') 14 | 15 | def _start_peer(self, host, port): 16 | peer = Peer(host, port) 17 | peer.start() 18 | 19 | def connect_peer(self, host, port, target_host, target_port): 20 | print('Connecting...') 21 | message = {'type': 'CONNECT', 'host': target_host, 'port': target_port} 22 | result = self._unicast(host, port, message) 23 | if result == 'OK': 24 | print(f'Peer {host}:{port} connected to {target_host}:{target_port}') 25 | else: 26 | print('Connect failed') 27 | return result 28 | 29 | def mine(self, host, port, data): 30 | print('Mining...') 31 | message = {'type': 'MINE', 'data': data} 32 | result = self._unicast(host, port, message) 33 | if result == 'OK': 34 | print('A new block was mined') 35 | else: 36 | print('Mine failed') 37 | return result 38 | 39 | def get_chain(self, host, port): 40 | message = {'type': 'SHOW'} 41 | result = self._unicast(host, port, message) 42 | if result: 43 | chain = json.loads(result) 44 | for block in chain: 45 | index = block['index'] 46 | prev_hash = block['previous_hash'] 47 | timestamp = block['timestamp'] 48 | data = block['data'] 49 | nonce = block['nonce'] 50 | hash = block['hash'] 51 | print('\n') 52 | print(f'# Block {index}') 53 | print('+-----------+-------------------------------------------' 54 | '---------------------+') 55 | print(f'| prev_hash |{prev_hash: >{64}}|') 56 | print('|-----------|-------------------------------------------' 57 | '---------------------|') 58 | print(f'| timestamp |{timestamp: >{64}}|') 59 | print('|-----------|-------------------------------------------' 60 | '---------------------|') 61 | print(f'| data |{data[:64]: >{64}}|') 62 | print('|-----------|-------------------------------------------' 63 | '---------------------|') 64 | print(f'| nonce |{nonce: >{64}}|') 65 | print('|-----------|-------------------------------------------' 66 | '---------------------|') 67 | print(f'| hash |{hash: >{64}}|') 68 | print('+-----------+-------------------------------------------' 69 | '---------------------+') 70 | else: 71 | print('Empty blockchain') 72 | return result 73 | 74 | def _unicast(self, host, port, message): 75 | pool = multiprocessing.Pool(1) 76 | result = pool.apply_async( 77 | self._send_message, args=(host, port, message)) 78 | pool.close() 79 | pool.join() 80 | return result.get() 81 | 82 | def _send_message(self, host, port, message): 83 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 84 | s.connect((host, port)) 85 | s.sendall(json.dumps(message).encode('utf-8')) 86 | response = s.recv(655350) 87 | return response.decode('utf-8') 88 | -------------------------------------------------------------------------------- /cli/interface.py: -------------------------------------------------------------------------------- 1 | import cmd 2 | 3 | from cli.command import Command 4 | 5 | 6 | class Interface(cmd.Cmd): 7 | 8 | def __init__(self): 9 | super().__init__() 10 | self.prompt = '(blockchain) ' 11 | self._default_host = '127.0.0.1' 12 | self._command = Command() 13 | 14 | def do_open(self, port): 15 | ''' 16 | Open peer listening port Eg: open 5000 17 | ''' 18 | self._command.start_peer(self._default_host, int(port)) 19 | 20 | def do_mine(self, arg): 21 | ''' 22 | Mine a new block Eg: mine hello 23 | ''' 24 | port = arg.split(' ')[0] 25 | data = arg.split(' ')[1] 26 | self._command.mine(self._default_host, int(port), data) 27 | 28 | def do_connect(self, arg): 29 | ''' 30 | Connect a peer to another Eg: connect 5000 5001 31 | ''' 32 | port = arg.split(' ')[0] 33 | target_port = arg.split(' ')[1] 34 | self._command.connect_peer( 35 | self._default_host, int(port), self._default_host, int(target_port)) 36 | 37 | def do_show(self, port): 38 | ''' 39 | Show blockchain of peer Eg: show 5000 40 | ''' 41 | self._command.get_chain(self._default_host, int(port)) 42 | 43 | def do_exit(self, _): 44 | exit(0) 45 | 46 | def do_help(self, _): 47 | print('\n') 48 | print('Commands:') 49 | print('\n') 50 | print('help \t\t\t\t Help for given commands') 51 | print('exit \t\t\t\t Exit application') 52 | print('\n') 53 | print('open \t\t\t Open peer listening port Eg: open 5000') 54 | print( 55 | 'connect \t ' 56 | 'Connect a peer to another Eg: connect 5000 5001') 57 | print('\n') 58 | print('mine \t\t Mine a new block Eg: mine hello') 59 | print('show \t\t\t Show blockchain of peer Eg: show 5000') 60 | print('\n') 61 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from cli.interface import Interface 2 | 3 | 4 | def main(): 5 | interface = Interface() 6 | interface.cmdloop() 7 | 8 | 9 | if __name__ == '__main__': 10 | main() 11 | -------------------------------------------------------------------------------- /tests/test_block.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from blockchain.block import Block 4 | 5 | 6 | class TestBlock(unittest.TestCase): 7 | 8 | def test_genesis_block(self): 9 | genesis = Block( 10 | 0, 0, '0', 'Welcome to blockchain cli!', 11 | '8724f78170aee146b794ca6ad451d23c254717727e18e2b9643b81d5666aa908', 12 | 1520572079.336289) 13 | self.assertEqual(genesis, Block.genesis()) 14 | 15 | def test_hash_generate(self): 16 | block = Block( 17 | index=1, nonce=80578, 18 | previous_hash=( 19 | '8724f78170aee146b794ca6ad451d23c254717727e18e2b9643b81d5666aa' 20 | '908'), 21 | data='hello', 22 | timestamp=1520923121.335219) 23 | self.assertEqual( 24 | block.hash, 25 | '0000f307ce360b39986cc164b7770f3029acd8f568be13765b22b482981675ca') 26 | 27 | def test_to_dict(self): 28 | block = Block( 29 | 0, 0, '0', 'Welcome to blockchain cli!', 30 | '8724f78170aee146b794ca6ad451d23c254717727e18e2b9643b81d5666aa908', 31 | 1520572079.336289) 32 | block_dict = { 33 | 'index': 0, 34 | 'previous_hash': '0', 35 | 'timestamp': 1520572079.336289, 36 | 'data': 'Welcome to blockchain cli!', 37 | 'nonce': 0, 38 | 'hash': ( 39 | '8724f78170aee146b794ca6ad451d23c254717727e18e2b9643b81d5666aa' 40 | '908') 41 | } 42 | self.assertEqual(block.to_dict(), block_dict) 43 | -------------------------------------------------------------------------------- /tests/test_chain.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestChain(unittest.TestCase): 5 | pass 6 | --------------------------------------------------------------------------------