├── blockchain ├── chain │ ├── __init__.py │ ├── api │ │ └── v0 │ │ │ ├── permissions.py │ │ │ ├── urls.py │ │ │ ├── serializers.py │ │ │ └── views.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── x.py │ │ │ ├── mine_block.py │ │ │ ├── sync.py │ │ │ └── create_transaction.py │ ├── views.py │ ├── apps.py │ ├── tests.py │ ├── urls.py │ ├── admin.py │ ├── utils.py │ └── models.py ├── blockchain │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── requirements.txt └── manage.py ├── README.md ├── LICENSE └── .gitignore /blockchain/chain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blockchain/blockchain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blockchain/chain/api/v0/permissions.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blockchain/chain/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blockchain/chain/management/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sayone' 2 | -------------------------------------------------------------------------------- /blockchain/chain/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sayone' 2 | -------------------------------------------------------------------------------- /blockchain/chain/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # check api.v0.views for JSON api 4 | -------------------------------------------------------------------------------- /blockchain/chain/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlockchainConfig(AppConfig): 5 | name = 'blockchain' 6 | -------------------------------------------------------------------------------- /blockchain/chain/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | #TODO: write unittests for block hashing, lookup etc -------------------------------------------------------------------------------- /blockchain/chain/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | 3 | urlpatterns = [ 4 | url(r'^api/v0/', include('chain.api.v0.urls'), name='api'), 5 | ] 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-blockchain 2 | native Blockchain implementation using django and python 3 | 4 | ## Contributing 5 | 6 | Contributions are welcome! Please feel free to submit a Pull Request. 7 | 8 | -------------------------------------------------------------------------------- /blockchain/chain/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from chain.models import * 3 | 4 | # Register your models here. 5 | admin.site.register(Block) 6 | admin.site.register(Chain) 7 | admin.site.register(Peer) 8 | admin.site.register(Transactions) -------------------------------------------------------------------------------- /blockchain/chain/management/commands/x.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | from chain.models import Peer, Transactions 3 | 4 | __author__ = 'sayone' 5 | 6 | 7 | class Command(BaseCommand): 8 | 9 | def handle(self, *args, **options): 10 | x = Peer.objects.latest('id') 11 | x.mine_block('first', 'agdfelksuygdflsergflserfglhsebrlgfvsZDgfvsDfzsfcSZfvdszegdfv') 12 | print(x) -------------------------------------------------------------------------------- /blockchain/blockchain/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for blockchain project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blockchain.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /blockchain/requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto==0.24.0 2 | blessings==1.6.1 3 | bpython==0.17 4 | certifi==2017.11.5 5 | cffi==1.11.4 6 | chardet==3.0.4 7 | cryptography==2.3 8 | curtsies==0.2.11 9 | Django==2.0.12 10 | djangorestframework==3.9.1 11 | Faker==0.8.9 12 | greenlet==0.4.12 13 | idna==2.6 14 | pycparser==2.18 15 | Pygments==2.2.0 16 | python-dateutil==2.6.1 17 | pytz==2017.3 18 | requests==2.20.0 19 | six==1.11.0 20 | text-unidecode==1.1 21 | ua-parser==0.3.6 22 | urllib3==1.24.2 23 | wcwidth==0.1.7 24 | -------------------------------------------------------------------------------- /blockchain/chain/management/commands/mine_block.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | from chain.models import Peer, Transactions 3 | 4 | __author__ = 'sayone' 5 | 6 | 7 | class Command(BaseCommand): 8 | 9 | def handle(self, *args, **options): 10 | 11 | transactions = Transactions.objects.filter(added_to_block=False).order_by('id')[0] 12 | transactions.added_to_block = True 13 | transactions.save() 14 | transactions.__dict__.pop("added_to_block", 0) 15 | transactions.__dict__.pop("_state", 0) 16 | peer = Peer.objects.order_by('-id')[1] 17 | peer.mine_block('second', str(transactions.__dict__)) 18 | peer.synchronize('second') -------------------------------------------------------------------------------- /blockchain/chain/management/commands/sync.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | from chain.models import Peer, Transactions, Block 3 | 4 | __author__ = 'sayone' 5 | 6 | 7 | class Command(BaseCommand): 8 | 9 | def handle(self, *args, **options): 10 | 11 | # transactions = Transactions.objects.filter(added_to_block=False).order_by('id')[0] 12 | # transactions.added_to_block = True 13 | # transactions.save() 14 | # transactions.__dict__.pop("added_to_block", 0) 15 | # transactions.__dict__.pop("_state", 0) 16 | # peer = Peer.objects.order_by('-id')[0] 17 | # peer.mine_block('first', str(transactions.__dict__)) 18 | for block in Block.objects.all(): 19 | block.sync 20 | -------------------------------------------------------------------------------- /blockchain/chain/management/commands/create_transaction.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | from chain.models import Transactions, Peer 3 | 4 | class Command(BaseCommand): 5 | 6 | def handle(self, *args, **options): 7 | from faker import Faker 8 | fake = Faker() 9 | transaction = Transactions.objects.create(sender=fake.name(), receiver=fake.name(), amount=fake.numerify()) 10 | 11 | transactions = Transactions.objects.latest('id') 12 | transactions.added_to_block = True 13 | transactions.save() 14 | transactions.__dict__.pop("added_to_block", 0) 15 | transactions.__dict__.pop("_state", 0) 16 | peer = Peer.objects.order_by('-id')[0] 17 | peer.mine_block('second', str(transactions.__dict__)) 18 | peer.synchronize('second') -------------------------------------------------------------------------------- /blockchain/chain/api/v0/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from chain.api.v0.views import BlockApiView, ChainApiView, LatestBlockApiView, BlockCreateView, PeerApiView, TransactionsList 3 | 4 | urlpatterns = [ 5 | url(r'^blocks/(?P[A-Za-z0-9-]+)/latest$', LatestBlockApiView.as_view(), name='latest-block'), 6 | url(r'^blocks/(?P[A-Za-z0-9-]+)/mine-block$', BlockCreateView.as_view(), name='mine-block'), 7 | url(r'^blocks/(?P[A-Za-z0-9-]+)$', BlockApiView.as_view(), name='get'), 8 | url(r'^chains/(?P[A-Za-z0-9-]+)$', ChainApiView.as_view(), name='chain'), 9 | url(r'^peers/$', PeerApiView.as_view(), name='peers'), 10 | url(r'^peers/add$', PeerApiView.as_view(), name='add-peer'), 11 | url(r'^transaction/$', TransactionsList.as_view(), name='transaction'), 12 | ] 13 | -------------------------------------------------------------------------------- /blockchain/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blockchain.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /blockchain/blockchain/urls.py: -------------------------------------------------------------------------------- 1 | """blockchain URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | from django.conf.urls.static import static 19 | from blockchain import settings 20 | 21 | urlpatterns = [ 22 | url(r'^admin/', admin.site.urls), 23 | url(r'^chain/', include('chain.urls')), 24 | ] + static(settings.STATIC_URL,document_root=settings.STATIC_ROOT) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 SayOne Technologies 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 | -------------------------------------------------------------------------------- /blockchain/chain/api/v0/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from chain.models import Block, Chain, Peer, Transactions 3 | 4 | import logging 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | class TransactionSerializer(serializers.ModelSerializer): 9 | 10 | class Meta: 11 | model = Transactions 12 | fields = ('sender', 'receiver', 'amount',) 13 | 14 | 15 | class BlockSerializer(serializers.ModelSerializer): 16 | 17 | class Meta: 18 | model = Block 19 | fields = ('hash', 'previous_hash', 'data', 'time_stamp', 'index', 'nonce',) 20 | 21 | def as_json(self): 22 | self.is_valid() 23 | data = dict(self.validated_data) 24 | data['time_stamp'] = str(data['time_stamp']) 25 | return data 26 | 27 | 28 | class ChainSerializer(serializers.ModelSerializer): 29 | block_set = BlockSerializer(many=True, 30 | read_only=True, 31 | allow_null=True) 32 | 33 | class Meta: 34 | model = Chain 35 | fields = ('name', 'block_set') 36 | 37 | 38 | class PeerSerializer(serializers.ModelSerializer): 39 | 40 | class Meta: 41 | model = Peer 42 | fields = ('name', 'address',) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 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 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | media/* 104 | *.pyc 105 | *.db 106 | .idea/* 107 | .DS_Store 108 | *.rdb 109 | *.log 110 | *.sqlite3 111 | 112 | timelog.* 113 | 114 | 115 | *.pyo 116 | ~* 117 | *~ 118 | static/ 119 | node_modules/ 120 | geos-3.3.9/ 121 | postgis-2.1.8/ 122 | whoosh_index/ 123 | -------------------------------------------------------------------------------- /blockchain/chain/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-01-18 16:45 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Block', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('index', models.IntegerField(auto_created=True, blank=True)), 20 | ('time_stamp', models.DateTimeField()), 21 | ('data', models.TextField(blank=True, max_length=255)), 22 | ('hash', models.CharField(blank=True, max_length=255)), 23 | ('previous_hash', models.CharField(max_length=255)), 24 | ('nonce', models.CharField(blank=True, default=0, max_length=255)), 25 | ], 26 | ), 27 | migrations.CreateModel( 28 | name='Chain', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('time_stamp', models.DateTimeField(auto_now_add=True)), 32 | ('name', models.CharField(max_length=255)), 33 | ], 34 | ), 35 | migrations.CreateModel( 36 | name='Peer', 37 | fields=[ 38 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('name', models.CharField(max_length=255)), 40 | ('address', models.CharField(max_length=255, unique=True)), 41 | ], 42 | ), 43 | migrations.CreateModel( 44 | name='Transactions', 45 | fields=[ 46 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 47 | ('sender', models.CharField(max_length=200)), 48 | ('receiver', models.CharField(max_length=200)), 49 | ('amount', models.IntegerField(default=0)), 50 | ('time_stamp', models.DateTimeField(auto_now_add=True)), 51 | ('added_to_block', models.BooleanField(default=False)), 52 | ], 53 | ), 54 | migrations.AddField( 55 | model_name='block', 56 | name='chain', 57 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='chain.Chain'), 58 | ), 59 | ] 60 | -------------------------------------------------------------------------------- /blockchain/chain/api/v0/views.py: -------------------------------------------------------------------------------- 1 | # from snippets.models import Snippet 2 | from rest_framework.permissions import AllowAny 3 | from rest_framework import generics 4 | from rest_framework import status 5 | from rest_framework.response import Response 6 | from rest_framework.views import APIView 7 | from rest_framework.renderers import TemplateHTMLRenderer 8 | from chain.api.v0.serializers import BlockSerializer, ChainSerializer, PeerSerializer, TransactionSerializer 9 | from chain.models import * 10 | 11 | 12 | class BlockApiView(generics.RetrieveAPIView): 13 | permission_classes = (AllowAny,) 14 | serializer_class = BlockSerializer 15 | queryset = Block.objects.all() 16 | lookup_field = 'hash' 17 | 18 | 19 | class BlockCreateView(generics.CreateAPIView): 20 | permission_classes = (AllowAny,) 21 | serializer_class = BlockSerializer 22 | 23 | def create(self, request, *args, **kwargs): 24 | serializer = BlockSerializer(data=request.data) 25 | serializer.is_valid(raise_exception=True) 26 | block = Block(**serializer.validated_data) 27 | block.chain, _ = Chain.objects.get_or_create(name=kwargs.get('chain_name')) 28 | print(block.is_valid_block(block.chain.last_block)) 29 | print("# ", request.data) 30 | 31 | 32 | if not block.chain.is_valid_next_block(block): 33 | return Response({}, status=status.HTTP_304_NOT_MODIFIED) 34 | try: 35 | block.save() 36 | except Exception as e: 37 | print(" ON SAVE ::", e) 38 | headers = self.get_success_headers(serializer.data) 39 | return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) 40 | 41 | 42 | class LatestBlockApiView(generics.GenericAPIView): 43 | permission_classes = (AllowAny,) 44 | serializer_class = BlockSerializer 45 | 46 | def get(self, request, *args, **kwargs): 47 | instance = Block.objects\ 48 | .filter(chain__name=kwargs.get('chain_name'))\ 49 | .order_by('index')\ 50 | .last() 51 | return Response(BlockSerializer(instance).data, 52 | status=status.HTTP_200_OK) 53 | 54 | 55 | class ChainApiView(generics.RetrieveAPIView): 56 | permission_classes = (AllowAny,) 57 | serializer_class = ChainSerializer 58 | queryset = Chain.objects.all() 59 | lookup_field = 'name' 60 | 61 | 62 | class PeerApiView(generics.ListAPIView, 63 | generics.CreateAPIView): 64 | permission_classes = (AllowAny,) 65 | serializer_class = PeerSerializer 66 | queryset = Peer.objects.all() 67 | 68 | 69 | class TransactionsList(APIView): 70 | """ 71 | Create a new transaction. 72 | """ 73 | renderer_classes = [TemplateHTMLRenderer] 74 | template_name = 'transaction.html' 75 | 76 | def get(self, request, *args, **kwargs): 77 | serializer = TransactionSerializer(data=request.data) 78 | if serializer.is_valid(): 79 | serializer.save() 80 | transaction = Transactions(**serializer.validated_data) 81 | transaction.save() 82 | if Transactions.objects.filter(added_to_block = False).count() >= 2: 83 | trans = Transactions.objects.filter(added_to_block = False) 84 | t = [] 85 | for item in trans: 86 | data1 = item.__dict__ 87 | data1.pop("_state",0) 88 | data1.pop("added_to_block",0) 89 | t.append(data1) 90 | trans.update(added_to_block=True) 91 | block_obj = Block.mine_block("first",t,) 92 | block_obj.save() 93 | headers = self.get_success_headers(serializer.data) 94 | return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) -------------------------------------------------------------------------------- /blockchain/blockchain/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for blockchain project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '4v%minb=9yh(+z%8a(g5jg_(2a4(gk!8tpjn=k1(s!dv^8!0*z' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'chain', 41 | 'cryptography', 42 | 'rest_framework', 43 | 'bpython', 44 | 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ] 56 | 57 | ROOT_URLCONF = 'blockchain.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'blockchain.wsgi.application' 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.sqlite3', 84 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 85 | } 86 | } 87 | 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 104 | }, 105 | ] 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 110 | 111 | LANGUAGE_CODE = 'en-us' 112 | 113 | TIME_ZONE = 'UTC' 114 | 115 | USE_I18N = True 116 | 117 | USE_L10N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 124 | 125 | STATIC_URL = '/static/' 126 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 127 | -------------------------------------------------------------------------------- /blockchain/chain/utils.py: -------------------------------------------------------------------------------- 1 | from cryptography.fernet import Fernet, base64, InvalidSignature, InvalidToken 2 | import hashlib 3 | from django.contrib.auth.hashers import make_password, PBKDF2PasswordHasher, BasePasswordHasher, get_random_string 4 | import logging 5 | 6 | log = logging.getLogger(__name__) 7 | import requests 8 | 9 | 10 | class JsonApi(object): 11 | 12 | @classmethod 13 | def get(cls, base_url, api_url): 14 | url = '{}{}'.format(base_url, api_url) 15 | data = {} 16 | response = None 17 | try: 18 | response = requests.get(url) 19 | response.raise_for_status() 20 | data = response.json() 21 | except Exception as exc: 22 | log.warning('GET failed: {} - {}'.format(url, exc)) 23 | if response is not None and hasattr(response, 'content'): 24 | log.warning('RESPONSE {}'.format(response.content)) 25 | finally: 26 | return data 27 | 28 | 29 | @classmethod 30 | def post(cls, base_url, api_url, data): 31 | url = '{}{}'.format(base_url, api_url) 32 | response_data = {} 33 | response = None 34 | try: 35 | response = requests.post(url, json=data) 36 | response.raise_for_status() 37 | if response.status_code == 201: 38 | log.info('Peer {} accepted block.'.format(base_url)) 39 | if not len(response.content): 40 | if response.status_code == 304: 41 | log.warning('Peer {}: unable to accept block.'.format(base_url)) 42 | else: 43 | response_data = response.json() 44 | except Exception as exc: 45 | log.warning('POST failed: {} - {}'.format(url, exc)) 46 | if response is not None and hasattr(response, 'content'): 47 | log.warning('RESPONSE {}'.format(response.content)) 48 | finally: 49 | return response_data 50 | 51 | 52 | class SymmetricEncryption(object): 53 | """ 54 | AES256 encryption driven through Fernet library 55 | """ 56 | @staticmethod 57 | def generate_key(): 58 | return Fernet.generate_key() 59 | 60 | @staticmethod 61 | def safe_encode(value): 62 | if type(value) is str: 63 | value = value.encode('utf-8') 64 | return base64.urlsafe_b64encode(value) 65 | 66 | @staticmethod 67 | def generate_salt(length=12): 68 | return get_random_string(length=length) 69 | 70 | @classmethod 71 | def build_encryption_key(cls, password_hash): 72 | reduced = password_hash[:32].encode('utf-8') 73 | return base64.urlsafe_b64encode(reduced) 74 | 75 | @staticmethod 76 | def encrypt(key, secret): 77 | if type(key) is bytes: 78 | pass 79 | if type(secret) is str: 80 | secret = secret.encode('utf-8') 81 | if type(secret) is not bytes: 82 | raise TypeError('%s: Encryption requires string or bytes' % type(secret)) 83 | 84 | return Fernet(key).encrypt(secret) 85 | 86 | @staticmethod 87 | def decrypt(key, token): 88 | return Fernet(key).decrypt(token) 89 | 90 | @staticmethod 91 | def hash(key): 92 | return hashlib.sha512(key).hexdigest() 93 | 94 | 95 | class EncryptionApi(object): 96 | 97 | @staticmethod 98 | def make_password(raw_password, salt): 99 | """10000 iterations of pbkdf2 and return only hash""" 100 | hasher = PBKDF2PasswordHasher() 101 | hashed_password = hasher.encode(raw_password, salt) 102 | return hashed_password.split('$').pop() 103 | 104 | @classmethod 105 | def encrypt(cls, raw_password, data): 106 | salt = SymmetricEncryption.generate_salt() 107 | password = cls.make_password(raw_password, salt) 108 | encryption_key = SymmetricEncryption.build_encryption_key(password) 109 | e_data = SymmetricEncryption.encrypt(encryption_key, data) 110 | return '{}${}'.format(salt, e_data.decode('utf-8')) 111 | 112 | @classmethod 113 | def decrypt(cls, raw_password, stored_data): 114 | salt, e_data = stored_data.split('$') 115 | password = cls.make_password(raw_password, salt) 116 | encryption_key = SymmetricEncryption.build_encryption_key(password) 117 | data = SymmetricEncryption.decrypt(encryption_key, e_data.encode('utf-8')) 118 | return data.decode('utf-8') 119 | 120 | 121 | -------------------------------------------------------------------------------- /blockchain/chain/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from hashlib import sha256 3 | import datetime 4 | import logging 5 | import pytz 6 | from django.urls import reverse 7 | from .utils import SymmetricEncryption, JsonApi, EncryptionApi 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class Transactions(models.Model): 13 | sender = models.CharField(max_length=200) 14 | receiver = models.CharField(max_length=200) 15 | amount = models.IntegerField(default =0) 16 | time_stamp = models.DateTimeField(auto_now_add=True) 17 | added_to_block = models.BooleanField(default=False) 18 | 19 | def __str__(self): 20 | return "'%s' transfered '%d' coins to '%s'" % (self.sender, self.amount, self.receiver, ) 21 | 22 | 23 | from faker import Faker 24 | rand = Faker() 25 | 26 | 27 | class Block(models.Model): 28 | time_stamp = models.DateTimeField(auto_now_add=False) 29 | index = models.IntegerField(auto_created=True, blank=True) 30 | data = models.TextField(blank=True, max_length=255) 31 | hash = models.CharField(max_length=255, blank=True) 32 | previous_hash = models.CharField(max_length=255) 33 | chain = models.ForeignKey(to='Chain', on_delete=models.CASCADE) 34 | nonce = models.CharField(max_length=255, default=0, blank=True) 35 | 36 | def __str__(self): 37 | return "Block " + str(self.index) + " on " + self.chain.name 38 | 39 | def __repr__(self): 40 | return '{}: {}'.format(self.index, str(self.hash)[:6]) 41 | 42 | def __hash__(self): 43 | return sha256( 44 | u'{}{}{}{}'.format( 45 | self.index, 46 | self.data, 47 | self.previous_hash, 48 | self.nonce).encode('utf-8')).hexdigest() 49 | 50 | @staticmethod 51 | def generate_next(latest_block, data): 52 | block = Block( 53 | data=data, 54 | index=latest_block.index + 1, 55 | time_stamp=datetime.datetime.now(tz=pytz.utc), 56 | previous_hash=latest_block.hash, 57 | nonce=SymmetricEncryption.generate_salt(26), 58 | ) 59 | while not block.valid_hash(): 60 | block.nonce = SymmetricEncryption.generate_salt(26) 61 | block.hash = block.__hash__() 62 | 63 | # block.save() # todo: remove 64 | 65 | return block 66 | 67 | def is_valid_block(self, previous_block): 68 | if self.index != previous_block.index + 1: 69 | log.warning('%s: Invalid index: %s and %s' % (self.index, self.index, previous_block.index)) 70 | return False 71 | if self.previous_hash != previous_block.hash: 72 | log.warning('%s: Invalid previous hash: %s and %s' % (self.index, self.hash, previous_block.hash)) 73 | return False 74 | 75 | if self.__hash__() != self.hash and self.index > 1: 76 | log.warning('%s: Invalid hash of content: %s and %s' % (self.index, self.hash, self.__hash__())) 77 | return False 78 | if not self.valid_hash() and self.index > 1: 79 | log.warning('%s: Invalid hash value: %s' % (self.index, self.hash)) 80 | return False 81 | return True 82 | 83 | def valid_hash(self): 84 | """simulate Proof of work""" 85 | return self.__hash__()[:4] == '0000' 86 | 87 | 88 | class Chain(models.Model): 89 | """ 90 | allows for multiple blockchain entities to exist simultaneously 91 | """ 92 | time_stamp = models.DateTimeField(auto_now_add=True) 93 | name = models.CharField(max_length=255) 94 | 95 | def __str__(self): 96 | return self.name 97 | 98 | def __len__(self): 99 | return self.block_set.count() 100 | 101 | def __repr__(self): 102 | return '{}: {}'.format(self.name, self.last_block) 103 | 104 | @property 105 | def last_block(self): 106 | return self.block_set.order_by('index').last() 107 | 108 | def create_seed(self): 109 | assert self.pk is not None 110 | seed = Block.generate_next( 111 | Block(hash=sha256('seed'.encode('utf-8')).hexdigest(), 112 | index=-1), 113 | data='Seed data', 114 | ) 115 | seed.chain = self 116 | seed.save() 117 | 118 | def is_valid_next_block(self, block): 119 | return block.is_valid_block(self.last_block) 120 | 121 | def add(self, data): 122 | if not self.block_set.count(): 123 | self.create_seed() 124 | 125 | block = Block.generate_next( 126 | self.last_block, 127 | data 128 | ) 129 | block.chain = self 130 | return block 131 | 132 | def is_valid_chain(self, blocks=None): 133 | blocks = blocks or list(self.block_set.order_by('index')) 134 | if not len(blocks): 135 | log.warning('Empty chain') 136 | return False 137 | if len(blocks) == 1 and blocks[0].index != 0: 138 | log.warning('Missing seed block in chain.') 139 | return False 140 | if not all(pblock.index + 1 == block.index == required_index 141 | for pblock, block, required_index in zip(blocks[:-1], blocks[1:], range(1, len(blocks)))): 142 | log.warning('Chain is not sequential') 143 | return False 144 | return all(block.is_valid_block(pblock) 145 | for pblock, block in zip(blocks[:-1], blocks[1:])) 146 | 147 | def replace_chain(self, new_chain): 148 | if self.is_valid_chain(new_chain) and len(new_chain) > len(self): 149 | self.block_set.all().delete() 150 | for block in new_chain: 151 | block.chain = self 152 | block.save() 153 | 154 | 155 | 156 | class Peer(models.Model): 157 | name = models.CharField(max_length=255) 158 | address = models.CharField(max_length=255, unique=True) 159 | 160 | def __str__(self): 161 | return self.name + "@" + self.address 162 | 163 | def __repr__(self): 164 | return '{}: {}'.format(self.name, self.address) 165 | 166 | def broadcast(self, chain_name, block): 167 | from .api.v0.serializers import BlockSerializer 168 | block_data = BlockSerializer(data=block.__dict__).as_json() 169 | for peer in Peer.objects.order_by('-id'): 170 | # import pdb 171 | # pdb.set_trace() 172 | print("sending to ", peer) 173 | JsonApi.post( 174 | peer.address, 175 | reverse('mine-block', 176 | kwargs={'chain_name': chain_name}), 177 | data=block_data) 178 | 179 | def query_latest_block(self, chain_name): 180 | from .api.v0.serializers import BlockSerializer 181 | data = JsonApi.get(self.address, 182 | reverse('latest-block', 183 | kwargs={'chain_name': chain_name})) 184 | serializer = BlockSerializer(data=data) 185 | serializer.is_valid() 186 | instance = Block(**serializer.validated_data) 187 | instance.chain = Chain.objects.get(name=chain_name) 188 | return instance 189 | 190 | def query_chain(self, chain_name): 191 | from .api.v0.serializers import BlockSerializer 192 | chain = Chain.objects.get(name=chain_name) 193 | data = JsonApi.get(self.address, 194 | reverse('chain', 195 | kwargs={'name': chain_name})) 196 | 197 | blocks = [] 198 | for block_data in data.get('block_set', []): 199 | serializer = BlockSerializer(data=block_data) 200 | if serializer.is_valid(): 201 | block = Block(**serializer.validated_data) 202 | block.chain = chain 203 | blocks.append(block) 204 | 205 | return blocks 206 | 207 | def fetch_longest_chain(self, chain_name): 208 | chain = max( 209 | (peer.query_chain(chain_name) 210 | for peer in self.discover_all_peers()), 211 | key=len 212 | ) 213 | return sorted(chain, key=lambda x: x.index) 214 | 215 | def mine_block(self, chain_name, data, password=None): 216 | chain = Chain.objects.get(name=chain_name) 217 | if password is not None: 218 | data = EncryptionApi.encrypt(password, data) 219 | new_block = chain.add(data) 220 | self.broadcast(chain_name, new_block) 221 | 222 | def synchronize(self, chain_name): 223 | self.discover_all_peers(commit=True) 224 | chain = Chain.objects.get(name=chain_name) 225 | longest_chain = self.fetch_longest_chain(chain_name) 226 | chain.replace_chain(longest_chain) 227 | 228 | def query_peers(self): 229 | from .api.v0.serializers import PeerSerializer 230 | data = JsonApi.get(self.address, 231 | reverse('peers')) 232 | 233 | peers = [] 234 | for peer in data: 235 | serializer = PeerSerializer(data=peer) 236 | if serializer.is_valid(): 237 | peers.append(Peer(**serializer.validated_data)) 238 | 239 | return peers 240 | 241 | @classmethod 242 | def scan_peers(cls, peers, known_peers): 243 | known_peers = set(p.address for p in known_peers) 244 | new_peers = [] 245 | for peer in peers: 246 | foreign_peers = peer.query_peers() 247 | for fp in foreign_peers: 248 | if fp.address not in known_peers: 249 | new_peers.append(fp) 250 | 251 | return new_peers 252 | 253 | @classmethod 254 | def discover_all_peers(cls, commit=False): 255 | discoveries = Peer.objects.all() 256 | known_peers = [] 257 | while len(discoveries): 258 | known_peers.extend(discoveries) 259 | discoveries = cls.scan_peers(discoveries, known_peers) 260 | 261 | if commit: 262 | for peer in known_peers: 263 | peer.save() 264 | 265 | return known_peers 266 | 267 | --------------------------------------------------------------------------------