├── .gitignore ├── Dockerfile ├── Readme.md ├── config ├── node1.yaml ├── node2.yaml └── node3.yaml ├── docker-compose.yml ├── requirements.txt ├── setup.py ├── test ├── __init__.py ├── test_blocks.py └── test_mine.py └── tinyblockchain ├── app.py ├── model.py └── resources ├── blocks.py ├── mine.py └── transaction.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.4 2 | 3 | # Copy all project files and chdir 4 | COPY . /opt/server 5 | WORKDIR /opt/server 6 | 7 | # Install requirements 8 | RUN pip install -r requirements.txt 9 | 10 | RUN pip install -e . 11 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Tiny Blockchain 2 | 3 | This is a port of [snakecoin](https://gist.github.com/aunyks/47d157f8bc7d1829a729c2a6a919c173) to use Docker and Python 3. All credit to [@aunyks](https://github.com/aunyks) and [https://github.com/zacanger](https://github.com/zacanger) who wrote the originals. 4 | 5 | The idea is a toy Python blockchain to be used as a playground for experimentation. 6 | 7 | This version adds the following to the original: 8 | 9 | * Python 3 10 | * [Falcon Web Framework](http://falcon.readthedocs.io/en/stable/index.html) 11 | * docker-compose to more easily run multiple nodes 12 | 13 | ## Usage 14 | 15 | Start everything: 16 | 17 | docker-compose up 18 | 19 | Add some transactions: 20 | 21 | curl "localhost:5001/transaction" \ 22 | -H "Content-Type: application/json" \ 23 | -d '{"from": "alice", "to":"bob", "amount": 3}'; \ 24 | curl "localhost:5002/transaction" \ 25 | -H "Content-Type: application/json" \ 26 | -d '{"from": "alice", "to":"pete", "amount": 5}'; \ 27 | curl "localhost:5003/transaction" \ 28 | -H "Content-Type: application/json" \ 29 | -d '{"from": "jeff", "to":"joe", "amount": 5}'; \ 30 | curl "localhost:5001/mine"; \ 31 | curl "localhost:5002/mine"; \ 32 | curl "localhost:5003/mine" 33 | 34 | `node1` has the longest chain. 35 | 36 | Check the output, it should be the same on each node: 37 | 38 | curl localhost:5001/blocks | jq 39 | curl localhost:5002/blocks | jq 40 | curl localhost:5003/blocks | jq 41 | -------------------------------------------------------------------------------- /config/node1.yaml: -------------------------------------------------------------------------------- 1 | node: 2 | peer_nodes: 3 | - http://localhost:5002 4 | - http://localhost:5003 5 | gunicorn: 6 | bind: "0.0.0.0:5001" 7 | workers: 1 8 | -------------------------------------------------------------------------------- /config/node2.yaml: -------------------------------------------------------------------------------- 1 | node: 2 | peer_nodes: 3 | - http://localhost:5001 4 | - http://localhost:5003 5 | 6 | 7 | gunicorn: 8 | bind: "0.0.0.0:5002" 9 | workers: 1 10 | timeout: 999 11 | -------------------------------------------------------------------------------- /config/node3.yaml: -------------------------------------------------------------------------------- 1 | node: 2 | peer_nodes: 3 | - http://localhost:5001 4 | - http://localhost:5002 5 | 6 | 7 | gunicorn: 8 | bind: "0.0.0.0:5003" 9 | workers: 1 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | node1: 4 | build: . 5 | volumes: 6 | - .:/opt/server/ 7 | command: [ "python", "-u", "/opt/server/tinyblockchain/app.py", "--config-file", "/opt/server/config/node1.yaml"] 8 | ports: 9 | - "5001:5001" 10 | networks: 11 | - backend 12 | node2: 13 | build: . 14 | volumes: 15 | - .:/opt/server/ 16 | command: [ "python", "-u", "/opt/server/tinyblockchain/app.py", "--config-file", "/opt/server/config/node2.yaml"] 17 | ports: 18 | - "5002:5002" 19 | networks: 20 | - backend 21 | depends_on: 22 | - node1 23 | node3: 24 | build: . 25 | volumes: 26 | - .:/opt/server/ 27 | command: [ "python", "-u", "/opt/server/tinyblockchain/app.py", "--config-file", "/opt/server/config/node3.yaml"] 28 | ports: 29 | - "5003:5003" 30 | networks: 31 | - backend 32 | depends_on: 33 | - node2 34 | 35 | networks: 36 | backend: 37 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | docopt 2 | falcon>=1.0 3 | gunicorn>=19.4.5 4 | requests 5 | pyyaml -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | setup( 5 | name='tinyblockchain', 6 | version='1.0.0', 7 | description='Blockchain playground', 8 | author='Luc Russell', 9 | license='', 10 | classifiers=[ 11 | 'Programming Language :: Python :: 3.4' 12 | ], 13 | keywords='', 14 | packages=find_packages(exclude=['contrib', 'docs', 'spec*']), 15 | install_requires=[ 16 | 'docopt', 17 | 'falcon>=1.0', 18 | 'gunicorn>=19.4.5', 19 | 'requests', 20 | 'pyyaml' 21 | ], 22 | package_data={}, 23 | data_files=[], 24 | entry_points={ 25 | 'console_scripts': [ 26 | 'tinyblockchain = tinyblockchain.app:main' 27 | ], 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixelwrench/tiny-blockchain/e799d42f9acfee1927263084494b547e115fbad1/test/__init__.py -------------------------------------------------------------------------------- /test/test_blocks.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import unittest 4 | 5 | from tinyblockchain.model import State, Block 6 | from tinyblockchain.resources.blocks import BlocksResource 7 | 8 | 9 | class TestApp(unittest.TestCase): 10 | 11 | def test_get_blocks(self): 12 | blockchain = [Block(0, datetime.datetime.now(), { 13 | "proof-of-work": 9, 14 | "transactions": None 15 | }, "0")] 16 | 17 | state = State('test', blockchain, [], [], True) 18 | 19 | blocks_resource = BlocksResource(state) 20 | response = MockResponse() 21 | blocks_resource.on_get(None, response) 22 | result = json.loads(response.body) 23 | self.assertEquals(0, result[0].get('index')) 24 | 25 | 26 | class MockResponse(object): 27 | def __init__(self): 28 | self.body = None 29 | -------------------------------------------------------------------------------- /test/test_mine.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import unittest 4 | 5 | from tinyblockchain.model import State, Block 6 | from tinyblockchain.resources.blocks import BlocksResource 7 | 8 | 9 | class TestApp(unittest.TestCase): 10 | 11 | def test_get_blocks(self): 12 | blockchain = [Block(0, datetime.datetime.now(), { 13 | "proof-of-work": 9, 14 | "transactions": None 15 | }, "0")] 16 | 17 | state = State('test', blockchain, [], [], True) 18 | 19 | blocks_resource = BlocksResource(state) 20 | response = MockResponse() 21 | blocks_resource.on_get(None, response) 22 | result = json.loads(response.body) 23 | self.assertEquals(0, result[0].get('index')) 24 | 25 | 26 | class MockResponse(object): 27 | def __init__(self): 28 | self.body = None 29 | -------------------------------------------------------------------------------- /tinyblockchain/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Usage: 4 | tinyblockchain [options] 5 | 6 | Options: 7 | --config-file= Config file path. 8 | """ 9 | import datetime 10 | import os 11 | import uuid 12 | 13 | import falcon 14 | import yaml 15 | from docopt import docopt 16 | from gunicorn.app.base import BaseApplication 17 | 18 | from tinyblockchain.model import Block, State 19 | from tinyblockchain.resources import blocks 20 | from tinyblockchain.resources import mine 21 | from tinyblockchain.resources import transaction 22 | 23 | 24 | class ApiServer(falcon.API): 25 | 26 | def __init__(self, config): 27 | super().__init__() 28 | 29 | # Random address of the owner of this node 30 | miner_address = str(uuid.uuid4()) 31 | 32 | # Manually construct a block with index zero and arbitrary 33 | # previous hash 34 | genesis_block = Block(0, datetime.datetime.now(), { 35 | "proof-of-work": 9, 36 | "transactions": None 37 | }, "0") 38 | 39 | # This node's blockchain copy 40 | blockchain = [genesis_block] 41 | peer_nodes = config['node']['peer_nodes'] 42 | 43 | state = State(miner_address, blockchain, peer_nodes, [], True) 44 | self.add_route('/transaction', transaction.TransactionResource(state)) 45 | self.add_route('/mine', mine.MineResource(state)) 46 | self.add_route('/blocks', blocks.BlocksResource(state)) 47 | 48 | 49 | class GunicornApp(BaseApplication): 50 | 51 | def __init__(self, app, options=None): 52 | self.options = options or {} 53 | self.application = app 54 | 55 | super().__init__() 56 | 57 | def load_config(self): 58 | for key, value in self.options.items(): 59 | self.cfg.set(key.lower(), value) 60 | 61 | def load(self): 62 | return self.application 63 | 64 | 65 | def configure(filename): 66 | if os.path.exists(filename) is False: 67 | raise IOError("{0} does not exist".format(filename)) 68 | 69 | with open(filename) as config_file: 70 | config_data = yaml.load(config_file) 71 | 72 | return config_data 73 | 74 | 75 | def main(arguments=None): 76 | if not arguments: 77 | arguments = docopt(__doc__) 78 | config = configure(arguments['--config-file']) 79 | app = ApiServer(config) 80 | GunicornApp(app, config['gunicorn']).run() 81 | 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /tinyblockchain/model.py: -------------------------------------------------------------------------------- 1 | import hashlib as hasher 2 | 3 | 4 | class State(object): 5 | def __init__(self, miner_address, 6 | blockchain, 7 | peer_nodes, 8 | node_transactions, 9 | mining): 10 | self.miner_address = miner_address 11 | self.blockchain = blockchain 12 | self.peer_nodes = peer_nodes 13 | self.node_transactions = node_transactions 14 | self.mining = mining 15 | 16 | 17 | class Block(object): 18 | def __init__(self, index, timestamp, data, previous_hash): 19 | self.index = index 20 | self.timestamp = timestamp 21 | self.data = data 22 | self.previous_hash = previous_hash 23 | self.hash = self.hash_block() 24 | 25 | def hash_block(self): 26 | sha = hasher.sha256() 27 | block = str(self.index) + str(self.timestamp) + str(self.data) + str( 28 | self.previous_hash) 29 | sha.update(block.encode('utf-8')) 30 | return sha.hexdigest() 31 | 32 | def __str__(self): 33 | return "Block <%s>: %s" % (self.hash, self.data) 34 | -------------------------------------------------------------------------------- /tinyblockchain/resources/blocks.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class BlocksResource(object): 5 | def __init__(self, state): 6 | self.state = state 7 | 8 | def on_get(self, req, resp, **params): 9 | print("BlocksResource called") 10 | chain_to_send = [] 11 | # Return chain as dictionary 12 | for i in range(len(self.state.blockchain)): 13 | block = self.state.blockchain[i] 14 | chain_to_send.append({ 15 | "index": block.index, 16 | "timestamp": str(block.timestamp), 17 | "data": block.data, 18 | "hash": block.hash 19 | }) 20 | resp.body = json.dumps(chain_to_send) 21 | -------------------------------------------------------------------------------- /tinyblockchain/resources/mine.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | 4 | import requests 5 | 6 | from tinyblockchain.model import Block 7 | 8 | 9 | class MineResource(object): 10 | def __init__(self, state): 11 | self.state = state 12 | 13 | def on_get(self, req, resp, **params): 14 | self.consensus() 15 | 16 | # Get the last proof of work 17 | last_block = self.state.blockchain[len(self.state.blockchain) - 1] 18 | last_proof = last_block.data['proof-of-work'] 19 | # Find the proof of work for the current block being mined 20 | # Note: The program will hang here until a new 21 | # proof of work is found 22 | proof = self.proof_of_work(last_proof) 23 | # Once we find a valid proof of work, we know we can mine a block so 24 | # we reward the miner by adding a transaction 25 | self.state.node_transactions.append( 26 | {"from": "network", "to": self.state.miner_address, "amount": 1} 27 | ) 28 | # Now we can gather the data needed to create the new block 29 | new_block_data = { 30 | "proof-of-work": proof, 31 | "transactions": list(self.state.node_transactions) 32 | } 33 | new_block_index = last_block.index + 1 34 | new_block_timestamp = datetime.now() 35 | last_block_hash = last_block.hash 36 | # Empty transaction list 37 | self.state.node_transactions[:] = [] 38 | # Now create the new block! 39 | mined_block = Block( 40 | new_block_index, 41 | new_block_timestamp, 42 | new_block_data, 43 | last_block_hash 44 | ) 45 | self.state.blockchain.append(mined_block) 46 | # Let the client know we mined a block 47 | resp.body = json.dumps({ 48 | "index": new_block_index, 49 | "timestamp": str(new_block_timestamp), 50 | "data": new_block_data, 51 | "hash": last_block_hash 52 | }) 53 | 54 | def proof_of_work(self, last_proof): 55 | # Create a variable that we will use to find our next proof of work 56 | incrementor = last_proof + 1 57 | # Keep incrementing until it's equal to a number divisible 58 | # by 9 and the proof of work of the previous block in the chain 59 | while not (incrementor % 9 == 0 and incrementor % last_proof == 0): 60 | incrementor += 1 61 | # Once that number is found, we can return it as a proof of our work 62 | return incrementor 63 | 64 | def consensus(self): 65 | # Get the blocks from other nodes 66 | other_chains = self.find_new_chains() 67 | print("Found other {} chains".format(len(other_chains))) 68 | # If our chain isn't longest, then we store the longest chain 69 | longest_chain = self.state.blockchain 70 | for node, chain in other_chains.items(): 71 | if len(longest_chain) < len(chain): 72 | print( 73 | "Replacing blockchain with chain from node: {} ".format( 74 | node)) 75 | longest_chain = chain 76 | # If the longest chain isn't ours, then we stop mining and set 77 | # our chain to the longest one 78 | self.state.blockchain = longest_chain 79 | 80 | def find_new_chains(self): 81 | # Get the blockchains of every other node 82 | other_chains = {} 83 | print("Searching peer nodes: {} ".format(self.state.peer_nodes)) 84 | for node_url in self.state.peer_nodes: 85 | chain = None 86 | try: 87 | chain = requests.get(node_url + "/blocks") 88 | except: 89 | print('Unable to retrieve chain from peer {}'.format( 90 | node_url)) 91 | print('Chain: '.format(chain)) 92 | if chain: 93 | node_chain = [] 94 | # Chains will be returned as json, convert them to objects 95 | c = json.loads(chain.content.decode('utf-8')) 96 | for block in c: 97 | node_chain.append(Block( 98 | block['index'], 99 | block['timestamp'], 100 | block['data'], 101 | block['hash'])) 102 | other_chains[node_url] = node_chain 103 | return other_chains 104 | -------------------------------------------------------------------------------- /tinyblockchain/resources/transaction.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import falcon 4 | 5 | 6 | class TransactionResource(object): 7 | def __init__(self, state): 8 | self.state = state 9 | 10 | def on_post(self, req, resp, **params): 11 | resp.status = falcon.HTTP_200 12 | # On each new POST request, extract the transaction data 13 | new_txion = req.stream.read().decode('utf-8') 14 | new_txion = json.loads(new_txion) 15 | self.state.node_transactions.append(new_txion) 16 | print("New transaction") 17 | print("FROM: {}".format(new_txion['from'].encode('ascii', 'replace'))) 18 | print("TO: {}".format(new_txion['to'].encode('ascii', 'replace'))) 19 | print("AMOUNT: {}\n".format(new_txion['amount'])) 20 | resp.body = 'Transaction submission successful' 21 | --------------------------------------------------------------------------------